Multi-version protocol types: complete per-version model packages (candidate 1/3)#2843
Draft
maxisbey wants to merge 1 commit into
Draft
Multi-version protocol types: complete per-version model packages (candidate 1/3)#2843maxisbey wants to merge 1 commit into
maxisbey wants to merge 1 commit into
Conversation
One superset type set in mcp.types plus a complete, hand-validated model package per spec version; serialization validates dumps through the negotiated version's models at a single 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 #2844 (per-version fact tables) 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 makes the type layer protocol-version-aware: the flat
mcp.typessurface grows the 2025-11-25 and 2026-07-28 spec additions as one version-superset model set (the "monolith"), and a new boundary —serialize_for(model, version)/parse_as(type, data, version)— emits and parses frames for a specific negotiated version. The mechanism is five committed per-version model packages (src/mcp/types/v2024_11_05…v2026_07_28), generated from the pinned schemas and hand-validated, that the boundary re-validates against on emission. Everything else undersrc/mcp/types/is the shared superset.Marker note: comments in the source reading
# Alternative considered: …mark reviewed design decisions at the spot where they bite; the decision register below maps each one to its location. They are review-phase 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 "full per-version packages" answer: every revision is a complete, separately readable model set, and version-correct emission falls out of re-validating through the negotiated revision's package.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.tests/types/: package↔schema parity pins, boundary emission/parse behavior, version registry and method tables, public-surface ratchet (mcp.types.__all__= 196 names), and round-trip byte-identity checks.import mcp/import mcp.typesload none of the per-version packages or the boundary (asserted in a subprocess); total import time grows ~+30 ms vsmain, of which the version mechanism itself contributes ~+2 ms (the rest is the larger shared model set).Breaking Changes
None new on this branch:
docs/migration.mdgains feature notes only, andLATEST_PROTOCOL_VERSION/DEFAULT_NEGOTIATED_VERSION/SUPPORTED_PROTOCOL_VERSIONSare unchanged (pinned with v1-idiom spot checks intests/types/test_public_surface.py).Types of changes
Checklist
Additional context
Reading order
src/mcp/types/_types.py(~2,800 lines) — the superset model set: every protocol construct once, all versions' fields, version bounds in docstrings.src/mcp/types/jsonrpc.py— version-independent envelope; the one open question (JSONRPCError.idrequiredness) is marked at the field.src/mcp/types/_spec_names.py— the divergence map: SDK↔schema renames, deliberately-not-modeled names, SDK-only names.src/mcp/types/_versions.py— per-version method tables, pure data.src/mcp/types/_wire_base.py— the two wire-model base classes.src/mcp/types/v2025_11_25/__init__.py— read in full next to the 2025-11-25 schema; one file per version, every alias/requiredness explicit.src/mcp/types/v2026_07_28/__init__.py— skim for the newest-revision delta (requiredresultType, required_metatriple, no server→client requests).src/mcp/types/wire.py— the boundary: shape rules, strip ledger, merge-and-align, parse mandates, member selection.tests/types/— parity pin (package == schema), boundary/parse/registry facts, public-surface ratchet, import budget.scripts/update_spec_types.py— the generator; regeneration from the pinned schemas is byte-identical for the 2024-11-05 and 2025-03-26 packages (--check); the 2025-06-18, 2025-11-25, and 2026-07-28 packages carry annotated hand deviations that--checkreports as drift and the parity test pins: theJSONValuealias widened to all six JSON types, and float arms on the fields where the pinned schema rendering says "integer" for a schema.tsnumber(ElicitResult.content,NumberSchemabounds/default).Decision register (targets of the in-source
# Alternative considered:markers)CallToolResult.structured_content: Any = None(interim; no sentinel)_types.py:1938mcp.types, marked section_types.py:729_types.py:867ttlMs=0,cacheScope="private")wire.py:311extra="ignore"; singleallowcarve-outSubscriptionFilter_types.py:1412wire.py:299_types.py:2736,2751,2769,2790requestedSchema_spec_names.py:88JSONRPCError.idRequestId | None, no default)jsonrpc.py:207(plain comment at the field, not the# Alternative considered:form)Key behavioral facts (shared across all three candidates)
protocolVersionand refuses a client request missing caller-suppliedclientInfo/clientCapabilitiesinparams._meta— identity is session-owned by design.io.modelcontextprotocol/*_metakeys pass through verbatim at every released version.serialize_foraccepts message bodies and envelope models only; bare fragments raiseTypeError.EmptyResult; rejects surface the best-matching arm's errors).methodat 2026-07-28 (rejecting withmissingat the entry's own method key; earlier and unknown versions stay lenient); unknown method values classify as invalid params at every version.Open questions for the maintainer
Any = Noneships because the sentinel design (wire-absent vs explicit-null) fails 7 existing tests at this base; theUnsetsentinel + its compat carve-outs remains the post-review option.Annotations.last_modifieddeferred: an existing listing-snapshot test pins the field's absence from the shared model set (the per-version packages model it); landing it plus that one snapshot flip is the post-review option.ServerResultresolution pin: a frame carrying the completeserver/discoverresult key set resolves toDiscoverResulton the raw public adapter (all typed/empty v2-base frames unchanged — twelve pins intests/types/test_wire_parse.py); reverting via a callable discriminator or arm exclusion remains an 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
wire.py: capability-gated emission checks, log-level injection, capability-violation hygiene.extra="ignore"where the schemas declare open objects; deliberate, stated in each package docstring; inbound leniency is unaffected (superset parse).scripts/update_spec_types.pyregenerates the five packages from the pinned public schema revisions; the three newest packages carry the annotated hand deviations listed in reading-order item 10.AI Disclaimer