Title: Multi-version protocol types: complete per-version model packages (candidate 1/3) by maxisbey · Pull Request #2843 · modelcontextprotocol/python-sdk · GitHub
Open Graph Title: Multi-version protocol types: complete per-version model packages (candidate 1/3) by maxisbey · Pull Request #2843 · modelcontextprotocol/python-sdk
X Title: Multi-version protocol types: complete per-version model packages (candidate 1/3) by maxisbey · Pull Request #2843 · modelcontextprotocol/python-sdk
Description: 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.types surface 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 under src/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 _meta identity 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? Full suite: 2,171 passed / 4 skipped / 1 xfailed; coverage 100.00% (branch) with strict-no-cover clean; pyright 0 errors; ruff format + check clean. tests/spec_oracles/: generated per-version schema oracles (pinned to public spec-repo SHA 6d44151…) 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 cost: import mcp / import mcp.types load none of the per-version packages or the boundary (asserted in a subprocess); total import time grows ~+30 ms vs main, 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.md gains feature notes only, and LATEST_PROTOCOL_VERSION / DEFAULT_NEGOTIATED_VERSION / SUPPORTED_PROTOCOL_VERSIONS are unchanged (pinned with v1-idiom spot checks in tests/types/test_public_surface.py). Types of changes Bug fix (non-breaking change which fixes an issue) New feature (non-breaking change which adds functionality) Breaking change (fix or feature that would cause existing functionality to change) Documentation update Checklist I have read the MCP Documentation My code follows the repository's style guidelines New and existing tests pass locally I have added appropriate error handling I have added or updated documentation as needed 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.id requiredness) 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 (required resultType, required _meta triple, 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 --check reports as drift and the parity test pins: the JSONValue alias widened to all six JSON types, and float arms on the fields where the pinned schema rendering says "integer" for a schema.ts number (ElicitResult.content, NumberSchema bounds/default). Decision register (targets of the in-source # Alternative considered: markers) Decision As built Marker structuredContent CallToolResult.structured_content: Any = None (interim; no sentinel) _types.py:1938 Removed-in-2026-07-28 types stay in flat mcp.types, marked section _types.py:729 Tasks 2025-11-25 task types restored types-only; no extension package _types.py:867 Caching defaults injected on 2026-07-28 emission (ttlMs=0, cacheScope="private") wire.py:311 Extra fields models stay extra="ignore"; single allow carve-out SubscriptionFilter _types.py:1412 Outbound narrowing none beyond the documented strip ledger wire.py:299 Union adapters the four public adapters stay plain smart unions _types.py:2736,2751,2769,2790 Elicitation requestedSchema stays an untyped dict _spec_names.py:88 JSONRPCError.id keeps the v2-base shape (RequestId | None, no default) jsonrpc.py:207 (plain comment at the field, not the # Alternative considered: form) Key behavioral facts (shared across all three candidates) Emission strips ONLY the documented ledger (fields the target version lacks); no schema-purity narrowing of caller values. At 2026-07-28 the boundary injects only protocolVersion and refuses a client request missing caller-supplied clientInfo/clientCapabilities in params._meta — identity is session-owned by design. Tool-content blocks in sampling results emit from 2025-11-25 (that schema admits them); capability gating is session-layer. Reserved io.modelcontextprotocol/* _meta keys pass through verbatim at every released version. serialize_for accepts message bodies and envelope models only; bare fragments raise TypeError. Result-union parses use ranked structural member selection: every arm matching more of the payload's top-level keys than the base result is a candidate, candidates are validated best match first and the first success wins (fallback EmptyResult; rejects surface the best-matching arm's errors). The emission alignment walk is keys-only — re-validation through a version package decides which keys survive; every emitted leaf value comes from the superset dump (a package's numeric kind never rewrites a caller's value). Embedded input-request entries must carry method at 2026-07-28 (rejecting with missing at the entry's own method key; earlier and unknown versions stay lenient); unknown method values classify as invalid params at every version. Null-valued elicitation content entries (constructible for v1.x compatibility, typed by no schema version) pass through verbatim at every version that models elicitation; the boundary withholds them from package re-validation and the alignment walk restores them. Open questions for the maintainer structuredContent interim: plain Any = None ships because the sentinel design (wire-absent vs explicit-null) fails 7 existing tests at this base; the Unset sentinel + its compat carve-outs remains the post-review option. Annotations.last_modified deferred: 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. ServerResult resolution pin: a frame carrying the complete server/discover result key set resolves to DiscoverResult on the raw public adapter (all typed/empty v2-base frames unchanged — twelve pins in tests/types/test_wire_parse.py); reverting via a callable discriminator or arm exclusion remains an option. ElicitResult.content | None value 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: Generic resultType: "complete" emission on ordinary (non-MRTR) results, incl. the empty-result surface (only MRTR scenarios assert resultType). Superset-union membership / version-gated dispatch+emission as such. ClientCapabilities.extensions / ServerCapabilities.extensions — unasserted on both sides. structuredContent any-JSON-value widening incl. explicit null; inputSchema/outputSchema 2020-12 widening. prompts/list draft pagination/required-params scenario — none; per-method MRTR coverage of prompts/get (vs resources/read) unconfirmed — verify against the conformance repo before counting it. Roots draft behavior: no top-level roots/list on 2026-07-28 (embedded delivery); must not emit notifications/roots/list_changed on draft — none. Required-params/_meta materialization (PaginatedRequest half) — none identifiable. Subscriptions: resourceSubscriptions filter path; extension taskIds; removed-era-method -32601 — three gaps. MRTR-embedded sampling member payloads specifically — none. Tasks: zero scenarios (conformance issues #244/#261 unmerged) — family-wide gap. Known limitations The tasks extension package is not built; the restored 2025-11-25 task types are types-only (never dispatched), and the attach point is marked. Session-layer halves are out of scope and listed in plain words in the comment block after the table re-exports in wire.py: capability-gated emission checks, log-level injection, capability-violation hygiene. Caller-supplied identity at 2026-07-28 (see behavioral facts above) is designed, not a gap in the boundary. Closed-model divergence: the generated packages validate with extra="ignore" where the schemas declare open objects; deliberate, stated in each package docstring; inbound leniency is unaffected (superset parse). scripts/update_spec_types.py regenerates 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
Open Graph Description: 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 com...
X Description: 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 com...
Opengraph URL: https://github.com/modelcontextprotocol/python-sdk/pull/2843
X: @github
Domain: github.com
| route-pattern | /:user_id/:repository/pull/:id/files(.:format) |
| route-controller | pull_requests |
| route-action | files |
| fetch-nonce | v2:9b824b94-30a3-37fc-aee1-0e352eebdff3 |
| current-catalog-service-hash | ae870bc5e265a340912cde392f23dad3671a0a881730ffdadd82f2f57d81641b |
| request-id | E830:2965D1:2F752F:415314:6A591FE1 |
| html-safe-nonce | 63d3f1bcd751da21c43da705d03fcf3c30d0d36e73b190fbd1d7bfcd69a34d69 |
| visitor-payload | eyJyZWZlcnJlciI6IiIsInJlcXVlc3RfaWQiOiJFODMwOjI5NjVEMToyRjc1MkY6NDE1MzE0OjZBNTkxRkUxIiwidmlzaXRvcl9pZCI6IjMzMjI0ODkxNTQ2NzI1OTkwMDkiLCJyZWdpb25fZWRnZSI6ImlhZCIsInJlZ2lvbl9yZW5kZXIiOiJpYWQifQ== |
| visitor-hmac | f004a67a1f2224362a76246bb3f020ee533ff7714ac6141a074b96a1a803a4fa |
| hovercard-subject-tag | pull_request:3853697132 |
| github-keyboard-shortcuts | repository,pull-request-list,pull-request-conversation,pull-request-files-changed,copilot |
| google-site-verification | Apib7-x98H0j5cPqHWwSMm6dNU4GmODRoqxLiDzdx9I |
| octolytics-url | https://collector.github.com/github/collect |
| analytics-location | / |
| fb:app_id | 1401488693436528 |
| apple-itunes-app | app-id=1477376905, app-argument=https://github.com/modelcontextprotocol/python-sdk/pull/2843/files |
| twitter:image | https://avatars.githubusercontent.com/u/224885523?s=400&v=4 |
| twitter:card | summary_large_image |
| og:image | https://avatars.githubusercontent.com/u/224885523?s=400&v=4 |
| og:image:alt | 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 com... |
| og:site_name | GitHub |
| og:type | object |
| hostname | github.com |
| expected-hostname | github.com |
| None | 30abb2b48d3b1b476bbaf01f49ead8ef91062bfd762d3fa28a956f4c99653343 |
| turbo-cache-control | no-preview |
| diff-view | unified |
| go-import | github.com/modelcontextprotocol/python-sdk git https://github.com/modelcontextprotocol/python-sdk.git |
| octolytics-dimension-user_id | 182288589 |
| octolytics-dimension-user_login | modelcontextprotocol |
| octolytics-dimension-repository_id | 862584018 |
| octolytics-dimension-repository_nwo | modelcontextprotocol/python-sdk |
| octolytics-dimension-repository_public | true |
| octolytics-dimension-repository_is_fork | false |
| octolytics-dimension-repository_network_root_id | 862584018 |
| octolytics-dimension-repository_network_root_nwo | modelcontextprotocol/python-sdk |
| turbo-body-classes | logged-out env-production page-responsive full-width |
| disable-turbo | true |
| browser-stats-url | https://api.github.com/_private/browser/stats |
| browser-errors-url | https://api.github.com/_private/browser/errors |
| release | f2e5b4fd472ea10ac216e358bbd0821a90dae8bb |
| ui-target | full |
| theme-color | #1e2327 |
| color-scheme | light dark |
Links:
Viewport: width=device-width