Two-surface protocol types: per-surface fact tables (candidate 2/2)#2850
Draft
maxisbey wants to merge 1 commit into
Draft
Two-surface protocol types: per-surface fact tables (candidate 2/2)#2850maxisbey wants to merge 1 commit into
maxisbey wants to merge 1 commit into
Conversation
One superset type set in mcp.types plus two declarative wire-fact blocks interpreted by a single engine at the wire boundary: an empty block serving every protocol version up to and including 2025-11-25, and a strict block for 2026-07-28 carrying the required-field injections (resultType, the ttlMs/cacheScope don't-cache defaults, the reserved protocolVersion _meta entry) and the refusals for constructs that revision removed. Serialization is additive-only: nothing is ever removed from a model's dump at any version. The empty block makes emission for 2025-11-25 and earlier the plain dump by construction; the schemas through 2025-11-25 evolve strictly additively, and a structural coverage test proves the newest one covers them all.
9 tasks
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.
Draft — not intended to merge as-is. This is one of two candidate implementations of the same design, pushed side by side so the approaches can be compared concretely. The companion draft is #2849: identical public surface, identical wire behavior, differing only in how the per-surface knowledge is represented — declarative fact rows here, committed model packages in the companion. The pair supersedes the earlier per-version-factored drafts #2843, #2844, and #2845, which stay open for comparison. Once a direction is chosen, the selected approach would be re-cut as a reviewable PR series.
The design
Protocol types are organized as two wire surfaces plus one user-facing type set:
mcp.types— the single public type set (the "monolith" throughout the source): one class per protocol construct, a superset covering every released protocol revision plus the upcoming 2026-07-28 revision. Existing names are unchanged; everything new is additive.v2025_11_25surface serving every released revision (2024-11-05 through 2025-11-25). The released schemas evolved strictly additively — each defines a subset of the newest one — andtests/types/test_version_facts_oracle.pyproves that coverage field-by-field against generated oracles of all four schemas.v2026_07_28surface carrying strict typing for the upcoming revision.The wire boundary —
mcp.types.wire.serialize_for(model, version)/parse_as(type, data, version)— is additive-only. Serializing dumps the model; on a 2026-07-28 session it additionally injects the fields that revision requires when the caller left them unset (resultType, thettlMs/cacheScopedon't-cache defaults, the reservedprotocolVersion_metaentry) and validates strictly. Nothing is ever removed from a payload at any version: deployed peers ignore unknown fields, so a newer field flowing to an older peer is harmless, while silently deleting a caller's field never is. Parsing is one lenient superset parse at every version, plus the checks the 2026-07-28 revision mandates.This candidate: declarative fact rows
The per-surface knowledge is data:
src/mcp/types/_version_facts.pyholds the per-version method tables plus two surface fact blocks of inject/refuse/mandate rows, applied by one small engine (src/mcp/types/_shaping.py) that carries no per-version knowledge of its own. Thev2025_11_25block is empty by design — emitting to a released-revision peer is the plain dump, by construction rather than by check — and thev2026_07_28block carries the new revision's required-field injections, its emission refusals, and its inbound mandates. An oracle test pins the fact blocks against the generated schemas, including the additive-evolution claim that lets one empty block serve all four released revisions.Motivation and Context
The SDK models one protocol revision at a time, but every session negotiates its own version, and the upcoming 2026-07-28 revision changes wire shapes materially: a required
resultTypeon results, required reserved_metaentries on client requests, theinitializehandshake and the server→client request channel removed in favor ofserver/discoverand embedded input requests, resource subscriptions, and tasks continuing as an extension. One type set can carry all of that only if something version-aware sits at the wire boundary. Keying that boundary on two surfaces instead of five versions follows from a property of the released schemas: from 2024-11-05 to 2025-11-25 they only ever added, so one lenient surface serves all of them and only the upcoming revision needs strict treatment.How Has This Been Tested?
./scripts/test); pyright in strict mode and ruff are clean.tests/spec_oracles/holds per-version schema oracles regenerated verbatim from the pinned public schemas;tests/types/test_version_facts_oracle.pypins the fact blocks against them, oracle by oracle.Breaking Changes
None — additive only. Existing
mcp.typesnames and shapes are unchanged (pinned bytests/types/test_public_surface.py), andmcp.types.wireis new API.Types of changes
Checklist
Additional context
Reading order
src/mcp/types/_types.py— the monolith: one class per protocol construct, every revision's fields, removed-construct sections marked in place. (src/mcp/types/jsonrpc.pyis the version-independent envelope;src/mcp/types/_spec_names.pyrecords the deliberate SDK↔schema naming divergences.)src/mcp/types/_version_facts.py— the mechanism: per-version method tables as literal frozensets, then the two surface fact blocks; the released-revisions block is empty on purpose, with the reasoning stated in the module docstring.src/mcp/types/_shaping.py— the one interpreter for the fact rows; fixed order refuse → dump → inject → required-_meta, with the parse mandates on the inbound side.src/mcp/types/wire.py— the public boundary: version registry, method-table re-exports,serialize_for/parse_as, and the two error types.tests/types/test_version_facts_oracle.py— pins the fact blocks against the generated schema oracles, including the proof that one surface covers all four released schemas.tests/types/test_wire_boundary.pyandtests/types/test_wire_parse.py— emission and parse behavior across versions (alongsidetest_shaping_engine.py,test_unions.py,test_version_registry.py,test_public_surface.py, andtest_import_budget.py).tests/spec_oracles/— the generated per-version oracles and the burn-down comparison; regenerated byscripts/update_spec_types.py.Decision markers
Source comments tagged
OD-n/M-nflag open decisions at the line where they bite. They are records of considered alternatives, not TODOs, and are resolved before release. Index:src/mcp/types/_types.py:1981—structured_contentships as plainAny = None, vs a sentinel distinguishing wire-absent from explicit null.src/mcp/types/_types.py:704— types removed in 2026-07-28 stay inmcp.typesin marked sections, vs moving to a lazily-aliased legacy module.src/mcp/types/_types.py:1547— the 2025-11-25 task types are kept for those sessions (theirtasks/*methods stay out of the unions and method tables); the extension's own task types are not built.src/mcp/types/_version_facts.py:376— when a handler leaves the required caching fields unset on 2026-07-28 emission, the boundary injects the don't-cache pair (ttlMs=0,cacheScope="private"), vs requiring handlers to set both fields.src/mcp/types/_types.py:1165— oneextra="allow"carve-out onSubscriptionFilter(extensions merge keys into it on the wire), vs a parse-side allow layer on every extension-carrying model.src/mcp/types/wire.py:81— no outbound narrowing when emitting to released revisions; pass-through chosen over stripping content a peer's revision predates.src/mcp/types/_spec_names.py:67— elicitationrequestedSchemastays an untyped dict; the restricted-schema vocabulary is not modeled.src/mcp/types/jsonrpc.py:207—JSONRPCError.idkeeps the required-but-nullable shape; the 2025-11-25 schema reading that allows an absent id would give the field aNonedefault.AI Disclaimer