Title: Two-surface protocol types: per-surface fact tables (candidate 2/2) by maxisbey · Pull Request #2850 · modelcontextprotocol/python-sdk · GitHub
Open Graph Title: Two-surface protocol types: per-surface fact tables (candidate 2/2) by maxisbey · Pull Request #2850 · modelcontextprotocol/python-sdk
X Title: Two-surface protocol types: per-surface fact tables (candidate 2/2) by maxisbey · Pull Request #2850 · modelcontextprotocol/python-sdk
Description: 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. A v2025_11_25 surface 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 — and tests/types/test_version_facts_oracle.py proves that coverage field-by-field against generated oracles of all four schemas. A v2026_07_28 surface 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, the ttlMs/cacheScope don't-cache defaults, the reserved protocolVersion _meta entry) 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.py holds 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. The v2025_11_25 block is empty by design — emitting to a released-revision peer is the plain dump, by construction rather than by check — and the v2026_07_28 block 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 resultType on results, required reserved _meta entries on client requests, the initialize handshake and the server→client request channel removed in favor of server/discover and 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? Full suite passes at 100% line + branch coverage (./scripts/test); pyright in strict mode and ruff are clean. A 185-case wire-fixture corpus exercises serialization and parsing at every protocol revision, in both directions (client→server and server→client). tests/spec_oracles/ holds per-version schema oracles regenerated verbatim from the pinned public schemas; tests/types/test_version_facts_oracle.py pins the fact blocks against them, oracle by oracle. Breaking Changes None — additive only. Existing mcp.types names and shapes are unchanged (pinned by tests/types/test_public_surface.py), and mcp.types.wire is new API. 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 monolith: one class per protocol construct, every revision's fields, removed-construct sections marked in place. (src/mcp/types/jsonrpc.py is the version-independent envelope; src/mcp/types/_spec_names.py records 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.py and tests/types/test_wire_parse.py — emission and parse behavior across versions (alongside test_shaping_engine.py, test_unions.py, test_version_registry.py, test_public_surface.py, and test_import_budget.py). tests/spec_oracles/ — the generated per-version oracles and the burn-down comparison; regenerated by scripts/update_spec_types.py. Decision markers Source comments tagged OD-n / M-n flag open decisions at the line where they bite. They are records of considered alternatives, not TODOs, and are resolved before release. Index: OD-1 src/mcp/types/_types.py:1981 — structured_content ships as plain Any = None, vs a sentinel distinguishing wire-absent from explicit null. OD-2 src/mcp/types/_types.py:704 — types removed in 2026-07-28 stay in mcp.types in marked sections, vs moving to a lazily-aliased legacy module. OD-3 src/mcp/types/_types.py:1547 — the 2025-11-25 task types are kept for those sessions (their tasks/* methods stay out of the unions and method tables); the extension's own task types are not built. OD-5 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. OD-9 src/mcp/types/_types.py:1165 — one extra="allow" carve-out on SubscriptionFilter (extensions merge keys into it on the wire), vs a parse-side allow layer on every extension-carrying model. OD-11 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. OD-13 src/mcp/types/_spec_names.py:67 — elicitation requestedSchema stays an untyped dict; the restricted-schema vocabulary is not modeled. M-2 src/mcp/types/jsonrpc.py:207 — JSONRPCError.id keeps the required-but-nullable shape; the 2025-11-25 schema reading that allows an absent id would give the field a None default. AI Disclaimer
Open Graph Description: 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:...
X Description: 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:...
Opengraph URL: https://github.com/modelcontextprotocol/python-sdk/pull/2850
X: @github
Domain: github.com
| route-pattern | /:user_id/:repository/pull/:id/files(.:format) |
| route-controller | pull_requests |
| route-action | files |
| fetch-nonce | v2:55cf632e-3eb9-9b08-8317-5a7ca97ebc1c |
| current-catalog-service-hash | ae870bc5e265a340912cde392f23dad3671a0a881730ffdadd82f2f57d81641b |
| request-id | EC60:1ABBFF:993B95:CE7B98:6A5938E0 |
| html-safe-nonce | 5b331765a295a1b4affd0b349c9c79b9c600ec7691416cbf0c4bcf3c25bc52fb |
| visitor-payload | eyJyZWZlcnJlciI6IiIsInJlcXVlc3RfaWQiOiJFQzYwOjFBQkJGRjo5OTNCOTU6Q0U3Qjk4OjZBNTkzOEUwIiwidmlzaXRvcl9pZCI6IjY1MzUxMzU0MzgwODg3ODAwMDAiLCJyZWdpb25fZWRnZSI6ImlhZCIsInJlZ2lvbl9yZW5kZXIiOiJpYWQifQ== |
| visitor-hmac | 92aabe293bbff20e40c48dc30d8295e26fc18d76e183903d5732b800fedde62e |
| hovercard-subject-tag | pull_request:3854651805 |
| 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/2850/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 two candidate implementations of the same design, pushed side by side so the approaches can be compared concretely. The companion draft is #2849:... |
| og:site_name | GitHub |
| og:type | object |
| hostname | github.com |
| expected-hostname | github.com |
| None | 1b31c9453e1607b2781513084b4770f67217e549834c85b76960d7b13626f7c8 |
| 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 | 220f27779da5b92b5e17fcaef93e474cc8affba3 |
| ui-target | canary-2 |
| theme-color | #1e2327 |
| color-scheme | light dark |
Links:
Viewport: width=device-width