Skip to content

feat!: adopt MCP revision 2026-07-28 via the mcp 2.x SDK - #104

Merged
CryptoJones merged 1 commit into
mainfrom
feat/mcp-2026-07-28
Aug 1, 2026
Merged

CryptoJones merged 1 commit into
mainfrom
feat/mcp-2026-07-28

Conversation

@CryptoJones

Copy link
Copy Markdown
Owner

What

Moves PerplexityAgent to MCP revision 2026-07-28 — the stateless revision — via the mcp 2.x SDK (mcp>=1.28.1,<2.0.0 → mcp>=2.0.0,<3.0).

FastMCP → MCPServer; Context imports from mcp.server.mcpserver; transport host/port moved off the constructor. Every tool's behaviour is unchanged and a v2 server still serves 2025-era clients from the same process.

Two things beyond the mechanical rename

1. Possession-based authorization (the stateless revision removed sessions).
The offload store and the response store both isolated one client's data from another's using id(ctx.session). 2026-07-28 removed protocol sessions, so ctx.session is a fresh object per request and that scoping no longer holds. Per the spec, cross-call state now uses server-minted handles passed as tool arguments:

  • retrieve_key is now an unguessable random capability token (was a content hash, which an attacker could fabricate by guessing the content).
  • Holding a retrieve_key / response_id is the authorization to fetch it.

The two isolation tests are rewritten to the possession contract. (This was a deliberate design choice — the alternative was binding state to an authenticated OAuth identity, which degrades to process-global over stdio where there's no auth.)

2. A real stdio crash, independent of the migration.
The stdin reader used a default-limit asyncio.StreamReader (64 KiB). Any JSON-RPC line over 64K raised LimitOverrunError and tore down the whole transport task group, killing the server on a single large request — legitimate or hostile. The reader now uses a 16 MiB limit and turns a still-oversized line into a clean protocol error instead of an unhandled crash. Regression-tested over a real subprocess.

This surfaced because I added a server_metrics conformance probe (read-only, no API), which activated the bad_input_does_not_crash contract that was previously skipped.

Also: the custom stdio transport now parses through the SDK's jsonrpc_message_adapter (in 2.x JSONRPCMessage is a plain union alias with no model_validate_json) and serializes with exclude_unset=True, keeping the resultType/ttlMs/cacheScope envelope fields on the wire.

Verification

Gate Result
pytest 201 passed
ruff check . clean
mypy src clean (17 files)
mcp-conformance 0.2.0 17 passed, 1 skipped (injection: server tags fetched content as untrusted rather than flagging its own inputs)

uv.lock refreshed: mcp → 2.0.0, +mcp-types, +opentelemetry-api, -httpx-sse.

Note

Codeberg is the source of truth for this repo, but no Codeberg key was loaded in this session — this branch is on GitHub only and still needs a Codeberg push.


Proudly Made in Nebraska. Go Big Red! 🌽 https://xkcd.com/2347/

Moves PerplexityAgent to the stateless MCP revision. FastMCP -> MCPServer;
Context imports from mcp.server.mcpserver; transport host/port moved off
the constructor. Every tool's behaviour is unchanged and a v2 server still
serves 2025-era clients from the same process.

mcp>=1.28.1,<2.0.0 -> mcp>=2.0.0,<3.0.

Possession-based authorization (the stateless revision removed sessions)
------------------------------------------------------------------------
The offload store and the response store both isolated one client's data
from another's using id(ctx.session). 2026-07-28 removed protocol sessions,
so ctx.session is a fresh object per request and that scoping is dead. Per
the spec, cross-call state now uses server-minted handles passed as tool
arguments:
- retrieve_key is now an unguessable random capability token (was a content
  hash, which an attacker could fabricate by guessing the content).
- Holding a retrieve_key / response_id is the authorization to fetch it.
The two isolation tests are rewritten to the possession contract.

Fixes a real stdio crash (independent of the migration)
-------------------------------------------------------
The stdin reader used a default-limit asyncio.StreamReader (64 KiB). Any
JSON-RPC line over 64K raised LimitOverrunError and tore down the whole
transport task group, killing the server on a single large request —
legitimate or hostile. The reader now uses a 16 MiB limit and turns a
still-oversized line into a clean protocol error instead of an unhandled
crash. Regression-tested over a real subprocess.

Also: parse lines through the SDK's jsonrpc_message_adapter (in 2.x
JSONRPCMessage is a plain union alias with no model_validate_json) and
serialize replies with exclude_unset=True, which keeps the 2026-07-28
envelope fields (resultType, ttlMs, cacheScope) on the wire.

Conformance: added a server_metrics probe (read-only, no API) so the
behavioural contracts run offline. This activated test_bad_input_does_not_crash,
which is what surfaced the 64K stdio crash above.

201 tests pass; ruff + mypy clean. mcp-conformance 0.2.0: 17 passed, 1
skipped (injection: the server tags fetched content as untrusted rather
than flagging injection in its own inputs).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Aug 1, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@CryptoJones, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 13 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: d893bd29-6c39-4cc2-afc0-6f35079c9ed1

📥 Commits

Reviewing files that changed from the base of the PR and between f2f937e and 96f3866.

⛔ Files ignored due to path filters (1)
  • uv.lock is excluded by !**/*.lock
📒 Files selected for processing (9)
  • CHANGELOG.md
  • conformance.toml
  • pyproject.toml
  • src/perplexity_agent/__main__.py
  • src/perplexity_agent/efficiency.py
  • src/perplexity_agent/server.py
  • tests/test_efficiency.py
  • tests/test_main.py
  • tests/test_server.py

Comment @coderabbitai help to get the list of available commands.

@CryptoJones
CryptoJones merged commit daa358a into main Aug 1, 2026
31 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant