Re-vendor 2026-07-28 schema and absorb spec #2907 error-code renumber#2912
Conversation
Bumps the draft schema pin from spec 6d441518 to 2852f30e, picking up spec PRs #2889/#2890/#2891/#2907, and updates the hand-written code that referenced the old shapes: - mcp.types.jsonrpc: HEADER_MISMATCH=-32020 added, MISSING_REQUIRED_CLIENT_CAPABILITY -32003->-32021, UNSUPPORTED_PROTOCOL_VERSION -32004->-32022, allocation-policy comment rewritten per #2907. - mcp.types.methods: drop the 2026-07-28 rows for notifications/elicitation/complete (spec #2891) and notifications/progress (spec #2889). - gen_surface_types.py: NotificationMetaObject is an open _meta carrier. - expected-failures comments: -32001 HeaderMismatch -> -32020. SUPPORTED_PROTOCOL_VERSIONS / LATEST_PROTOCOL_VERSION are unchanged; the renumbered constants are not used on any 2025-era code path.
…mment CONNECTION_CLOSED and REQUEST_TIMEOUT can reach the wire via jsonrpc_dispatcher's _write_error path; the band is implementation-defined per spec, not SDK-internal-only.
| model_config = ConfigDict( | ||
| extra="ignore", | ||
| ) | ||
| elicitation_id: Annotated[str, Field(alias="elicitationId")] | ||
| """ | ||
| The ID of the elicitation, which must be unique within the context of the server. | ||
| The client MUST treat this ID as an opaque value. | ||
| """ | ||
| message: str | ||
| """ | ||
| The message to present to the user explaining why the interaction is needed. |
There was a problem hiding this comment.
🔴 The re-vendored 2026-07-28 schema drops the required elicitationId from ElicitRequestURLParams and the regenerated surface here follows, but the version-superset monolith mcp.types._types.ElicitRequestURLParams still declares elicitation_id: str as required. As a result, a spec-valid 2026-07-28 InputRequiredResult embedding a URL-mode elicitation/create (now just message/mode/url) passes the surface step of methods.parse_server_result('tools/call'|'prompts/get'|'resources/read', '2026-07-28', ...) but raises pydantic.ValidationError at the monolith step (same for the 2026 retry path in parse_client_request). The monolith field should be relaxed to optional with a version note, mirroring how CancelledNotificationParams.request_id absorbs cross-version requiredness changes.
Extended reasoning...
What the bug is. This PR re-vendors the 2026-07-28 draft schema, which removed elicitationId from ElicitRequestURLParams (it is gone from both the properties and required lists in schema/2026-07-28.json), and the regenerated surface model in src/mcp/types/v2026_07_28/__init__.py correctly drops the field. However, the version-superset monolith model ElicitRequestURLParams in src/mcp/types/_types.py still declares elicitation_id: str as a required field with no default — the PR touches _types.py but only to update error-code numerals in two docstrings. The monolith's stated contract is to carry every field from every supported protocol version so application code sees one set of types; after this PR it instead requires a field that no longer exists on the 2026-07-28 wire.
The code path that triggers it. mcp.types.methods.parse_server_result(method, version, data) runs a two-step validation: first against the per-version surface row, then against the version-free monolith row. For the 2026-07-28 dual-result rows, SERVER_RESULTS[('tools/call', '2026-07-28')] = v2026.AnyCallToolResult (and likewise AnyGetPromptResult/AnyReadResourceResult), while MONOLITH_RESULTS['tools/call'] = CallToolResult | InputRequiredResult. InputRequiredResult.input_requests carries InputRequest = CreateMessageRequest | ListRootsRequest | ElicitRequest, and the monolith ElicitRequest.params is ElicitRequestURLParams | ElicitRequestFormParams.
Step-by-step proof. Take a spec-valid 2026-07-28 server response to tools/call:
body = {
"resultType": "input_required",
"inputRequests": {
"r1": {
"method": "elicitation/create",
"params": {"mode": "url", "message": "Please sign in", "url": "https://example.com/auth"},
}
},
}
methods.parse_server_result("tools/call", "2026-07-28", body)- Surface step:
v2026.AnyCallToolResultaccepts the body — the regeneratedv2026.ElicitRequestURLParams(this hunk) only requiresmessage,mode,url. ✅ - Monolith step: the
CallToolResultarm fails (nocontent). TheInputRequiredResultarm reachesElicitRequest.params: the URL armElicitRequestURLParamsrejects the params for missingelicitationId(wire parsing is alias-only,by_name=False, so there is no way to supply it), and the form arm rejects them for the wrongmodeliteral and missingrequestedSchema. ❌ - Both union arms fail, so
parse_server_resultraisespydantic.ValidationErrorfor a payload that conforms exactly to the schema this PR vendors.
The same divergence hits the prompts/get and resources/read dual-result rows, and the 2026 retry path in parse_client_request for requests embedding inputRequests/inputResponses. Conversely, server code constructing a 2026 URL elicitation through the monolith type is forced to invent an elicitation_id that the outbound sieve then strips.
Why nothing else prevents it, and impact. Before this PR the vendored 2026 schema still required elicitationId, so every spec-valid payload carried the field and the surface and monolith agreed; the re-vendor relaxes the surface without relaxing the superset, introducing the inconsistency. mcp.types.methods is documented as supported public API, so any consumer parsing 2026-07-28 traffic hits this — the only mitigation is that the SDK's own runtime does not yet drive the SEP-2322 input_required flow (those conformance scenarios are still in expected-failures), so no shipping code path triggers it today.
How to fix. Relax the monolith field in src/mcp/types/_types.py to elicitation_id: str | None = None with a version-availability note ("required on the wire on 2025-11-25; removed in 2026-07-28"), the same pattern CancelledNotificationParams.request_id already uses to absorb a cross-version requiredness change. tests/types/test_parity.py only checks that the monolith is a superset of surface fields, so keeping the field as optional stays consistent with that invariant.
| class CancelledNotificationParams(WireModel): | ||
| """ | ||
| Parameters for a `notifications/cancelled` notification. | ||
| """ | ||
|
|
||
| model_config = ConfigDict( | ||
| extra="ignore", | ||
| ) | ||
| meta: Annotated[NotificationMetaObject | None, Field(alias="_meta")] = None | ||
| reason: str | None = None | ||
| """ | ||
| An optional string describing the reason for the cancellation. This MAY be logged or presented to the user. | ||
| """ | ||
| request_id: Annotated[RequestId, Field(alias="requestId")] | ||
| """ | ||
| The ID of the request to cancel. | ||
|
|
||
| This MUST correspond to the ID of a request the client previously issued. | ||
| """ |
There was a problem hiding this comment.
🟡 The 2026-07-28 schema re-vendored by this PR adds required: [requestId] to CancelledNotificationParams (reflected here, where request_id now has no default), but the monolith mcp.types._types.CancelledNotificationParams.request_id docstring still says "Required on the wire through 2025-06-18; optional from 2025-11-25", which is now stale. Consider updating that docstring (in src/mcp/types/_types.py) to note that 2026-07-28 requires requestId on the wire again and narrows it to a request the client previously issued, consistent with the other version-note docstring updates this PR makes in _types.py.
Extended reasoning...
What the bug is. The re-vendored schema/2026-07-28.json in this PR adds "required": ["requestId"] to CancelledNotificationParams and narrows its description to "a request the client previously issued" (servers may only use the notification to terminate a subscriptions/listen stream). The regenerated surface model here (v2026_07_28.CancelledNotificationParams.request_id, declared as Annotated[RequestId, Field(alias="requestId")] with no default) correctly reflects that. However, the hand-written version-superset model mcp.types._types.CancelledNotificationParams.request_id keeps its existing docstring: "Required on the wire through 2025-06-18; optional from 2025-11-25." After this PR that note is inaccurate — it implies the field stays optional for every version after 2025-11-25, but at 2026-07-28 it is wire-required again.\n\nThe code path that makes it matter. A user constructing a cancellation through the public monolith type — types.CancelledNotificationParams(reason="..."), omitting request_id because the docstring says it's optional from 2025-11-25 onward — produces a payload that is fine on a 2025-era session but is rejected on a 2026-07-28 session: notifications/cancelled is the only client notification at 2026-07-28, and the receiving side gates it through CLIENT_NOTIFICATIONS[("notifications/cancelled", "2026-07-28")] -> v2026.CancelledNotification, whose params model now requires requestId. validate_client_notification/parse_client_notification raise a pydantic ValidationError ("missing" at ("params", "requestId")), and the runner drops the notification as malformed.\n\nWhy nothing else prevents it. The monolith model itself must keep request_id optional — it is the version superset, and pre-2026 versions allow omitting it — so the field declaration is correct. The only place the per-version requirement is communicated to SDK users is the per-field docstring, which is exactly the convention _types.py uses ("Per-field docstrings note version availability", per the module docstring). This PR already updates two other hand-written docstrings in the same file for the same schema bump (UnsupportedProtocolVersionErrorData -32004→-32022, MissingRequiredClientCapabilityErrorData -32003→-32021), so this one was in scope and was simply missed.\n\nStep-by-step proof.\n1. Before this PR, the vendored 2026 draft had requestId optional, and the generated field was request_id: RequestId | None = None — the monolith docstring was accurate.\n2. This PR re-vendors the schema; schema/2026-07-28.json now contains "required": ["requestId"] under CancelledNotificationParams, and the regenerated v2026_07_28.CancelledNotificationParams declares request_id: Annotated[RequestId, Field(alias="requestId")] with no default.\n3. A user reads types.CancelledNotificationParams.request_id's docstring ("optional from 2025-11-25") and sends {"method": "notifications/cancelled", "params": {"reason": "user aborted"}} on a 2026-07-28 session.\n4. The receiver calls parse_client_notification("notifications/cancelled", "2026-07-28", {"reason": "user aborted"}); the surface validation step fails with a missing-field error for requestId and the cancellation is dropped — the in-flight request is never cancelled.\n\nImpact and fix. Documentation-only drift; no functional change to generated code is needed (the regen is correct). The fix is a one-line docstring update on CancelledNotificationParams.request_id in src/mcp/types/_types.py — e.g. "Required on the wire through 2025-06-18; optional from 2025-11-25; required again from 2026-07-28, where it must be the ID of a request the client previously issued (servers use it only to terminate a subscriptions/listen stream)." Optionally the class docstring of the monolith CancelledNotification ("can be sent by either side") could carry the same 2026 caveat. Non-blocking nit.
Re-vendors the draft (
2026-07-28) schema and absorbs the spec's error-code allocation policy from modelcontextprotocol#2907. Part of #2891.Motivation and Context
schema/PINNED.jsonwas at spec commit6d441518(2026-06-05); spec PRs #2889, #2890, #2891 and #2907 have all merged since. The hand-writtenmcp.types.jsonrpcconstants still carried the pre-#2907 numerals.Pinned spec commit:
2852f30e(currentmainHEAD at the time of this PR). The draft schema is byte-identical fromf817239f(the #2907 merge) through2852f30e— the three commits in between are docs/blog only — so any commit in that range would do; pinning at HEAD avoids an immediate re-bump.What changed
schema/PINNED.json+schema/2026-07-28.json: bumped to2852f30e, regeneratedsrc/mcp/types/v2026_07_28/__init__.py.scripts/gen_surface_types.py: addedNotificationMetaObject(new in spec [v1.x] Pass a list to parametrize in test_docs_examples (pytest 9.1.0 compat) #2889) to the 2026OPEN_CLASSESset so it keepsextra="allow"like the other_metacarriers.src/mcp/types/jsonrpc.py:MISSING_REQUIRED_CLIENT_CAPABILITY-32003 → -32021,UNSUPPORTED_PROTOCOL_VERSION-32004 → -32022, newHEADER_MISMATCH = -32020, and the allocation-policy comment rewritten to match fix(client/stdio): default encoding_error_handler to 'replace' for resilient UTF-8 decoding #2907 (-32000..-32019implementation-defined,-32020..-32099spec-reserved,-32002/-32042reserved-never-reused).CONNECTION_CLOSED/REQUEST_TIMEOUTstay in the implementation-defined band.src/mcp/types/__init__.py: re-exportHEADER_MISMATCH.src/mcp/types/_types.py: docstring numerals updated on the two error-data models.src/mcp/types/methods.py: dropped the("notifications/elicitation/complete", "2026-07-28")and("notifications/progress", "2026-07-28")rows — both removed from the draftClientNotification/ServerNotificationunions by spec [v1.x] Pass a list to parametrize in test_docs_examples (pytest 9.1.0 compat) #2889/2026-07-28 spec support — tracking #2891..github/actions/conformance/expected-failures*.yml: stale-32001 HeaderMismatchcomment →-32020.tests/types/{test_methods.py,test_parity.py}updated for the regen delta (ElicitationCompleteNotificationremoved;HeaderMismatchError/NotificationMetaObject/Error3added;ClientNotificationcollapsed to single-arm).tests/server/test_runner.pyswitches its post-drop barrier fromnotifications/progress(no longer a 2026 client notification) to a custom non-spec method.SUPPORTED_PROTOCOL_VERSIONSandLATEST_PROTOCOL_VERSIONare untouched; the renumbered constants are not referenced on any 2025-era code path, so this is a no-op for unpinned clients and legacy servers.Not in this PR: the
elicit_url/send_elicit_completehelpers inpeer.py,server/session.pyandserver/mcpserver/context.pystill take a mandatoryelicitation_idand emitnotifications/elicitation/complete. Those are 2025-11-25 API built on the monolith types (which are a superset and keep the field); at 2026-07-28 the URL-elicitation flow is replaced by MRTRinput_requiredresults, so version-gating or deprecating those helpers belongs with #2898 / #2904, not the schema bump.How Has This Been Tested?
gen_surface_types.py --check,pyright,ruff, and./scripts/test(full suite, 100% coverage,strict-no-coverclean).Breaking Changes
The numeric values of
mcp.types.MISSING_REQUIRED_CLIENT_CAPABILITYandmcp.types.UNSUPPORTED_PROTOCOL_VERSIONchange. These constants were added for the unreleased 2026-07-28 surface and are not emitted on any current code path, so there's no migration entry.Types of changes
Checklist
Additional context
The published
@modelcontextprotocol/conformance@0.2.0-alpha.4(this repo's current pin, set by #2911) was cut from7a620cb1, which predates conformance#353 — the harness'shttp-standard-headersscenario still asserts the old codes. That's fine for this PR (nothing here emits these codes yet), but the conformance pin will need a bump to a post-#353 alpha before the server-stateless and header-validation implementations can assert-32020..-32022without a waiver.AI Disclaimer