René's URL Explorer Experiment
Title: Multi-version protocol types: per-version delta packages (candidate 3/3) by maxisbey · Pull Request #2845 · modelcontextprotocol/python-sdk · GitHub
Open Graph Title: Multi-version protocol types: per-version delta packages (candidate 3/3) by maxisbey · Pull Request #2845 · modelcontextprotocol/python-sdk
X Title: Multi-version protocol types: per-version delta packages (candidate 3/3) by maxisbey · Pull Request #2845 · 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 #2843 (complete per-version model packages) and #2844 (per-version fact tables); 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 versioned wire-shape support for protocol revisions 2024-11-05 through 2026-07-28: mcp.types stays one superset model set (the "monolith"), and each revision gets a wire-shape module that defines only what changed since the previous revision, importing everything else from the module that last defined it. The mechanism is "delta modules + one boundary": serialize_for/parse_as in src/mcp/types/wire.py re-validate payloads through the negotiated revision's models, loaded lazily on first boundary use. The mechanism lives in src/mcp/types/v2024_11_05/ … v2026_07_28/ and src/mcp/types/wire.py; everything else is shared surface.
Marker note: comments of the form # OD- alternative: … (and # M- in mcp.types.jsonrpc) mark reviewed design decisions at the spot where they bite; the convention is explained in the mcp.types._types module docstring, and the register below maps each id to its location. They are records, not TODOs.
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 "delta" answer: each revision's module IS the reviewable diff against the previous revision, so future small revisions cost a small module rather than a full copy.
How Has This Been Tested?
Full suite: 2,197 passed / 4 skipped / 1 xfailed; coverage 100.00% (branch) with strict-no-cover clean; pyright strict 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 revisions plus the extension and SDK directions. Each oracle module doubles as the flattened one-page view of its revision.
tests/types/: per-revision surface pins against the oracles (test_version_surfaces.py), boundary emission/parse behavior, version registry and method tables, public-surface ratchet (mcp.types.__all__ = 196 names), result-union resolution pins.
Import cost: no per-revision module is in sys.modules after import mcp (subprocess-asserted); total import time grows ~+30 ms vs main, of which the version mechanism itself contributes ~+1.4 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 byte-identical.
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 — the superset payload types; the module docstring explains the superset principle and the # OD- alternative: marker convention.
src/mcp/types/jsonrpc.py — version-independent JSON-RPC envelope.
src/mcp/types/_versions.py — per-version method tables (plain data).
src/mcp/types/_spec_names.py — the SDK-name ↔ schema-name divergence record.
src/mcp/types/_wire_base.py — the wire-shape model base (closed-by-default rationale).
src/mcp/types/v2024_11_05/__init__.py — the full base revision; its header documents the delta layout.
src/mcp/types/v2025_03_26/__init__.py — the smallest delta; the template in action. Then v2025_06_18, v2025_11_25, v2026_07_28 (the last leads with its recursive JSON alias pair).
src/mcp/types/wire.py — serialize_for (shape rules, validate, re-dump, restore walk) and parse_as (superset parse, mandates, result-union selection).
tests/types/ — boundary, parse, registry, surface, and import-budget pins.
tests/spec_oracles/ — generated per-version oracles + burn-down gate; each oracle module is also the flattened one-page view of that revision.
Decision register (targets of the in-source OD-n / M-n markers)
OD-1 structured content: plain Any = None (key absent when unset; interim, see open questions) — src/mcp/types/_types.py:1741
OD-2 removed-revision types stay in flat mcp.types, grouped section — src/mcp/types/_types.py:645
OD-3 tasks: 2025-11-25 task types restored types-only; extension package not built — src/mcp/types/_types.py:759
OD-5 caching defaults: don't-cache pair injected on 2026-07-28 emission — src/mcp/types/wire.py:311
OD-9 extra fields: single extra="allow" carve-out on SubscriptionFilter — src/mcp/types/_types.py:1266
OD-11 no outbound narrowing of opened schemas/content on older revisions — src/mcp/types/wire.py:299
OD-12 the four public adapters stay plain smart unions — src/mcp/types/_types.py:2381, 2396, 2414, 2435
OD-13 elicitation requestedSchema stays an untyped dict — src/mcp/types/_spec_names.py:111
M-2 JSONRPCError.id keeps the required-nullable v2 shape — src/mcp/types/jsonrpc.py:189
Key behavioral facts (shared across all three candidates)
Emission strips ONLY the documented six-key ledger; no narrowing of caller values beyond it.
At 2026-07-28 the boundary injects only protocolVersion and refuses a request missing caller-supplied clientInfo/clientCapabilities in params._meta — identity is session-owned by design.
Tool-content emission bound is ≤ 2025-06-18 (the 2025-11-25 schema admits tool content, with the array-content sampling carrier); capability gating is session-layer.
Reserved io.modelcontextprotocol/* _meta keys pass through verbatim at every revision.
serialize_for refuses bare fragments (content blocks, capabilities, params) with TypeError.
Result-bearing unions parse by ranked structural member selection (first success wins; all-fail surfaces the best-ranked arm's errors).
The emission alignment walk is keys-only: re-validation through a revision module decides only which keys survive; every emitted leaf value comes from the superset dump, so a revision module's numeric kind never rewrites a caller's value (1.0 stays 1.0).
Number-render widening: the elicitation form-answer values (2025-06-18/2025-11-25/2026-07-28) and the NumberSchema bounds/default (the two older revisions that model them) carry a float arm — the pinned schema rendering says integer where schema.ts says number; each widening is pinned per position, and a closure sweep fails any remaining int-only field that is not pinned intended-integer.
Embedded input-request entries must carry method at 2026-07-28 (missing at the entry's own method key; earlier and unknown revisions stay lenient); unknown method values classify as invalid params at every revision.
Null-valued elicitation content entries (constructible for v1.x compatibility, typed by no schema version) pass through verbatim at every revision that models elicitation; the boundary withholds them from the revision module's re-validation and the alignment walk restores them.
Open questions for the maintainer
structuredContent interim: structured_content: Any = None, no Unset sentinel, no wrap serializer — the sentinel form breaks 7 existing tests at this base. The sentinel + carve-out path remains the post-review option.
Annotations.last_modified deferred in the superset model set (it trips an existing listing-snapshot test); the per-revision modules from 2025-06-18 on model lastModified exactly as their schemas do.
ServerResult resolution pin: the Discover-keyed adversarial frame resolves to DiscoverResult (pinned in tests/types/test_wire_parse.py); reverting 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.
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
Honest cost: only the 2025-03-26 delta is small. Three of the four transitions rewrite 50–90% of the surface, so the committed-source saving vs full per-revision packages is roughly 27–32% — and "read one revision as a single page" requires the oracle module (the flattened per-version view, shipped in tests/spec_oracles/) or an import-following editor. The genuine advantage is review-shape: the next small revision's delta module IS the reviewable change.
The tasks extension (mcp/extensions/tasks/) is not built; its types remain provisional upstream.
Session-layer halves are out of scope: method-table enforcement, the sampling-tools and elicitation-url capability gates, the per-request logLevel send condition (listed at src/mcp/types/wire.py:77-86).
Caller-supplied identity at 2026-07-28 (see behavioral facts above) is designed; the session layer pre-seeds identity.
Tooling asymmetry: the delta-split generator is not committed (the per-revision modules carry pathless provenance and are pinned to their schemas by tests/types/test_version_surfaces.py); the oracle generator is committed (scripts/update_spec_types.py).
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/2845
X: @github
direct link
Domain: github.com
| route-pattern | /:user_id/:repository/pull/:id/files(.:format) |
| route-controller | pull_requests |
| route-action | files |
| fetch-nonce | v2:cb5fd475-beb2-0a54-6d20-32f66d2ed3ae |
| current-catalog-service-hash | ae870bc5e265a340912cde392f23dad3671a0a881730ffdadd82f2f57d81641b |
| request-id | DAB4:159FE3:CA5D3B:1233ED5:6A5A0199 |
| html-safe-nonce | 5e729e5f1ebd8990a644eb0a816c805952c901fb5bb54c5121cfde08c966f54d |
| visitor-payload | eyJyZWZlcnJlciI6IiIsInJlcXVlc3RfaWQiOiJEQUI0OjE1OUZFMzpDQTVEM0I6MTIzM0VENTo2QTVBMDE5OSIsInZpc2l0b3JfaWQiOiI4NjQ0NDkzMjcxMjg3NTk1NDE3IiwicmVnaW9uX2VkZ2UiOiJpYWQiLCJyZWdpb25fcmVuZGVyIjoiaWFkIn0= |
| visitor-hmac | 7f690a8aab357b4224a4300714bb6734f27dc658828b4d03049eb5eaad0f5b3d |
| hovercard-subject-tag | pull_request:3853699805 |
| 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 | ///pull_requests/show/files |
| fb:app_id | 1401488693436528 |
| apple-itunes-app | app-id=1477376905, app-argument=https://github.com/modelcontextprotocol/python-sdk/pull/2845/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 | 4d3f961cfc83a4348ee5be7691cf4ae3515aab3ef5200c13a7e41b048125fb61 |
| 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 | ac4d28603af300ac851b62521abb878ab5d3319a |
| ui-target | full |
| theme-color | #1e2327 |
| color-scheme | light dark |
Links:
Viewport: width=device-width
URLs of crawlers that visited me.