Adopt MCP 2026-07-28 in production and pin untested invariants - #66
Merged
Conversation
Production now runs the v2 SDK, so recon serves the 2026-07-28 revision. That is what makes it reachable from a modern-only client at all: the spec's compatibility matrix says modern client plus legacy server fails. A v2 server still answers the legacy initialize handshake, so clients on 2025-11-25 keep working, and recon's own gate proves that rather than trusting it. 1.28.1 stays the rollback pin and stays blocking, so both generations remain exercised: 24 of 24 checks pass on 2.0.0, 17 pass with 7 not-applicable on 1.28.1. Clearing the blocker meant porting the optional remote adapter, which refused any family but v1. Two differences moved into sdk_compat. The read-only allow-list read `annotations.readOnlyHint`, which is not an attribute on v2, so every tool would have looked non-read-only and the remote surface would have come up empty rather than restricted. And the transport options moved from a mutable settings object to keyword arguments on streamable_http_app. That disjoint-spelling problem recurs across the SDK boundary: outputSchema, inputSchema, mimeType, and the CallToolResult shape all differ. Each is resolved once in sdk_compat and read through the protocol's own spelling, which is why the generated surface inventory is byte-identical across generations. Separately, a sweep for rules enforced in code that no test named. The shareable_scope defect happened because grep found nothing; eight more of that shape are now pinned, each verified to fail when the guard is removed. Two are worth calling out. The MCP chain depth cap was a literal 3 rather than the shared constant, so the two copies could drift apart silently. And the result-cache poison table asserted nothing at all: every one of its twelve rows wrote a payload for one domain into another domain's cache file, so the read was rejected on the domain binding before it ever reached the field under test. Re-keying the rows made all twelve real and immediately caught a range check that had never been exercised.
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Production now serves the MCP 2026-07-28 revision, and a sweep pins eight rules that were enforced in code but named by no test.
MCP v2 adoption
Moving the pin is what makes recon reachable from a modern-only client at all: the spec's compatibility matrix says modern client plus legacy server fails, and
server/discover, per-request_meta,resultType, and cache hints are all SDK-provided.A v2 server still answers the legacy
initializehandshake, so clients on 2025-11-25 keep working. recon's own gate proves that rather than trusting release notes. 1.28.1 stays the rollback pin and stays blocking, so both generations keep being exercised:Clearing the blocker: the remote adapter
build_remote_applicationrefused any family but v1, so the previous review stopped here rather than disable a shipped feature. Porting it surfaced a genuine latent bug:annotations.readOnlyHint. That attribute does not exist on v2 (it isread_only_hint, with the camelCase form surviving only as a serialization alias). Every tool would have looked non-read-only, so the adapter would have removed all of them and served an empty surface rather than a restricted one.settingsobject to keyword arguments onstreamable_http_app.Verified against the real v2 SDK: 22 tools filter to the same 17 read-only tools as before.
The disjoint-spelling problem
This recurs across the whole SDK boundary:
outputSchema,inputSchema,mimeType, and theCallToolResultshape (v1 returns a tuple, v2 a model, so unpacking raises). Each is resolved once insdk_compatand read through the protocol's own spelling.Evidence that this is right: the generated surface inventory is byte-identical across generations.
Note for anyone touching tool annotations: passing the v2 spelling to v1 does not raise. Pydantic stores it as an unrelated extra attribute and leaves the real hint unset, so a mechanical rename would silently drop every annotation on the rollback pin.
Eight invariants pinned
The
shareable_scopedefect happened becausegrepfound nothing. Each of these was confirmed by mutation (delete the guard, watch nothing fail) and each new test was verified to fail when the guard is removed:chain_resolveclamps depth (the existing test held vacuously: its mock returned no related domains).3, not the sharedMAX_CHAIN_DEPTHso the two copies could drift silently. Depth also scales the aggregate timeout.One candidate was rejected after mutation: the per-channel cached-replay guard is pinned, just from a different test file than you would expect.
scripts/check.pypasses green locally, as do both compatibility matrices andrelease_readiness.py.