Multi-version protocol types: per-version fact tables (candidate 2/3)#2844
Draft
maxisbey wants to merge 1 commit into
Draft
Multi-version protocol types: per-version fact tables (candidate 2/3)#2844maxisbey wants to merge 1 commit into
maxisbey wants to merge 1 commit into
Conversation
One superset type set in mcp.types plus declarative per-version wire-fact tables interpreted by a single engine at the wire boundary.
This was referenced Jun 12, 2026
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 three candidate implementations of multi-version protocol type support, pushed side by side so the approaches can be compared concretely. The companion drafts are #2843 (complete per-version model packages) and #2845 (per-version delta packages); all three ship the same public surface and the same behavioral contract, differing in how the per-version knowledge is represented. Once a direction is chosen, the selected approach would be re-cut as a reviewable PR series.
This branch adds the 2026-07-28 protocol revision to
mcp.typesas one superset model set plus a version-aware wire boundary (mcp.types.wire.serialize_for/parse_as), keyed strictly by (model type, negotiated version). The mechanism is declarative fact tables: one data module (_version_facts.py) with five per-version blocks of strip/inject/refuse/mandate rows and method sets, applied by one small engine (_shaping.py, 479 lines). Everything version-keyed lives in those two files pluswire.py; the model definitions themselves are version-free.Marker note: comments tagged
OD-n/M-nin the source reference the decision register in this description (table below). They are review-phase markers — records of considered alternatives, not TODOs — and are resolved before release.Motivation and Context
The SDK currently models one protocol revision at a time, but a server or client negotiates a version per session and the 2026-07-28 revision changes wire shapes materially (required
resultType, required_metaidentity triple on some client requests, removed server→client requests, multi-round-trip (MRTR) input requests, subscriptions, discovery). This candidate explores the "facts, not copies" answer: version differences are rows in a data table a reviewer can scan in one sitting, applied by a single engine, with no duplicated model definitions.How Has This Been Tested?
strict-no-coverclean; pyright 0 errors; ruff format + check clean.tests/spec_oracles/: generated per-version schema oracles (pinned to public spec-repo SHA6d44151…) compared against the SDK types, with a burn-down allowlist whose entries each carry a self-contained reason; green for all five versions plus the extension group. The fact tables themselves are additionally pinned against the per-version oracles (tests/types/test_version_facts_oracle.py).tests/types/: boundary emission/parse pairs, registry + method-table equality, public-surface ratchet (mcp.types.__all__= 196 names), engine and fact-table cross-checks, union resolution pins (incl. twelveServerResultpins).import mcporimport mcp.types(subprocess-asserted; the boundary loads lazily on first use).Breaking Changes
None new on this branch:
docs/migration.mdgains feature notes only, andLATEST_PROTOCOL_VERSION/DEFAULT_NEGOTIATED_VERSION/SUPPORTED_PROTOCOL_VERSIONSare unchanged (tests/types/test_public_surface.py).Types of changes
Checklist
Additional context
Reading order
src/mcp/types/_types.py— the superset payload models, including the 2026-07-28 additions (Discover, subscriptions, input-required results, error data) and the marked removed-method section.src/mcp/types/jsonrpc.py— version-independent JSON-RPC envelope.src/mcp/types/_spec_names.py— deliberate SDK-name vs schema-export-name divergences, with reason codes.src/mcp/types/_version_facts.py— the mechanism: five per-version fact blocks, oldest to newest; row kinds documented on their dataclasses.src/mcp/types/_shaping.py— the engine that applies the rows.src/mcp/types/wire.py— the public boundary: version registry re-export, per-version method tables,serialize_for/parse_as, two exceptions.src/mcp/types/__init__.py— public surface; the boundary loads lazily soimport mcp.typesdoes not pay for it.tests/types/— boundary emission/parse pairs, registry + method-table equality, public-surface ratchet, import budget, engine and fact-table cross-checks, union resolution pins.tests/spec_oracles/— generated per-version schema oracles + burn-down comparison; regenerated byscripts/update_spec_types.py.docs/migration.md— the tasks-types restoration note and release-notes leniency entries.Decision register (targets of the in-source
OD-n/M-nmarkers)structured_content: Any = None, no sentinel (open question below)src/mcp/types/_types.py:1972mcp.types, grouped in a marked sectionsrc/mcp/types/_types.py:695tasks/*methods stay out of unions and method tables; extension types not builtsrc/mcp/types/_types.py:1538ttlMs=0,cacheScope="private"when unset on 2026-07-28 emissionsrc/mcp/types/_version_facts.py:676extra="allow"carve-out onSubscriptionFiltersrc/mcp/types/_types.py:1156src/mcp/types/wire.py:96src/mcp/types/_types.py:2588ff.src/mcp/types/_spec_names.py:67RequestId | None, no default)src/mcp/types/jsonrpc.py:207OD-4 was superseded (no per-version naming beyond version strings); OD-6 (
| None = Noneon newer-version-required fields, boundary injects/validates) is pervasive — neither carries a marker.Key behavioral facts (shared across all three candidates)
_metaidentity triple refuses emission when the caller has not suppliedclientInfo/clientCapabilities— the boundary injects onlyprotocolVersion, never session identity.io.modelcontextprotocol/*_metakeys pass through on emission at every version (never stripped).serialize_foraccepts only message bodies and envelope models; bare fragments (content blocks, capabilities, params) raiseTypeError.methodat 2026-07-28 (a method-less entry rejects with amissingerror at the entry's own method key; earlier and unknown versions stay lenient); an unknown method value classifies as invalid params at every version._types.py(they back the capabilitiestasksfield types but are not exported).Open questions for the maintainer
structuredContent) is jointly unsatisfiable with the existing test suite and the types-only scope at this base; the branch ships the plain-Noneinterim. The sentinel plus its carve-out set remains the post-review option.Annotations.lastModifieddeferred: the 2025-11-25 field trips an existing listing-snapshot test; the burn-down allowlist carries the three findings as deliberate deviations. Landing the field plus the one snapshot flip remains the post-review option.ServerResultresolution pin: appending the all-optional input-required arm flips one adversarial Discover-keyed frame toDiscoverResult; pinned intests/types/test_unions.py. Reverting via a callable discriminator or arm exclusion remains the post-review option.ElicitResult.content| Nonevalue arm: null has no wire form at any schema version; the arm exists for v1.x constructor compatibility. Drop vs keep is an open API decision.Conformance-suite gaps (surfaced per AGENTS.md, to raise on the conformance repo)
No conformance-suite scenario currently exists for most 2026-07-28 type features; the in-repo oracle and boundary tests substitute for now. Gaps identified:
resultType: "complete"emission on ordinary (non-MRTR) results, incl. the empty-result surface (only MRTR scenarios assertresultType).ClientCapabilities.extensions/ServerCapabilities.extensions— unasserted on both sides.structuredContentany-JSON-value widening incl. explicit null;inputSchema/outputSchema2020-12 widening.prompts/listdraft pagination/required-params scenario — none; per-method MRTR coverage ofprompts/get(vsresources/read) unconfirmed — verify against the conformance repo before counting it.roots/liston 2026-07-28 (embedded delivery); must not emitnotifications/roots/list_changedon draft — none.params/_metamaterialization (PaginatedRequest half) — none identifiable.resourceSubscriptionsfilter path; extensiontaskIds; removed-era-method-32601— three gaps.@app.resource()does not acceptctx: Context#244/sse client read progress notification #261 unmerged) — family-wide gap.Known limitations
sampling.toolsandelicitation.urlcapability gates, the logLevel send condition, and drop-vs-log handling for version-invalid notifications._metamust carry the triple (tools/call,prompts/list,server/discover, …),serialize_forinjects onlyprotocolVersion; if the caller or session has not pre-seededclientInfo/clientCapabilitiesinparams._meta, emission refuses rather than synthesizing identity.scripts/update_spec_types.py).AI Disclaimer