Skip to content

Re-vendor 2026-07-28 schema and absorb spec #2907 error-code renumber#2912

Merged
maxisbey merged 2 commits into
mainfrom
schema-revendor-2907
Jun 19, 2026
Merged

Re-vendor 2026-07-28 schema and absorb spec #2907 error-code renumber#2912
maxisbey merged 2 commits into
mainfrom
schema-revendor-2907

Conversation

@maxisbey

@maxisbey maxisbey commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

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.json was at spec commit 6d441518 (2026-06-05); spec PRs #2889, #2890, #2891 and #2907 have all merged since. The hand-written mcp.types.jsonrpc constants still carried the pre-#2907 numerals.

Pinned spec commit: 2852f30e (current main HEAD at the time of this PR). The draft schema is byte-identical from f817239f (the #2907 merge) through 2852f30e — 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 to 2852f30e, regenerated src/mcp/types/v2026_07_28/__init__.py.
  • scripts/gen_surface_types.py: added NotificationMetaObject (new in spec [v1.x] Pass a list to parametrize in test_docs_examples (pytest 9.1.0 compat) #2889) to the 2026 OPEN_CLASSES set so it keeps extra="allow" like the other _meta carriers.
  • src/mcp/types/jsonrpc.py: MISSING_REQUIRED_CLIENT_CAPABILITY -32003 → -32021, UNSUPPORTED_PROTOCOL_VERSION -32004 → -32022, new HEADER_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..-32019 implementation-defined, -32020..-32099 spec-reserved, -32002/-32042 reserved-never-reused). CONNECTION_CLOSED/REQUEST_TIMEOUT stay in the implementation-defined band.
  • src/mcp/types/__init__.py: re-export HEADER_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 draft ClientNotification/ServerNotification unions 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 HeaderMismatch comment → -32020.
  • Tests: tests/types/{test_methods.py,test_parity.py} updated for the regen delta (ElicitationCompleteNotification removed; HeaderMismatchError/NotificationMetaObject/Error3 added; ClientNotification collapsed to single-arm). tests/server/test_runner.py switches its post-drop barrier from notifications/progress (no longer a 2026 client notification) to a custom non-spec method.

SUPPORTED_PROTOCOL_VERSIONS and LATEST_PROTOCOL_VERSION are 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_complete helpers in peer.py, server/session.py and server/mcpserver/context.py still take a mandatory elicitation_id and emit notifications/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 MRTR input_required results, 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-cover clean).

Breaking Changes

The numeric values of mcp.types.MISSING_REQUIRED_CLIENT_CAPABILITY and mcp.types.UNSUPPORTED_PROTOCOL_VERSION change. 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

  • 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

The published @modelcontextprotocol/conformance@0.2.0-alpha.4 (this repo's current pin, set by #2911) was cut from 7a620cb1, which predates conformance#353 — the harness's http-standard-headers scenario 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..-32022 without a waiver.

AI Disclaimer

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.
@maxisbey maxisbey marked this pull request as ready for review June 19, 2026 14:39
…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.
@maxisbey maxisbey enabled auto-merge (squash) June 19, 2026 14:45
@maxisbey maxisbey merged commit 510832a into main Jun 19, 2026
30 checks passed
@maxisbey maxisbey deleted the schema-revendor-2907 branch June 19, 2026 14:46
Comment on lines 148 to 153
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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 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)
  1. Surface step: v2026.AnyCallToolResult accepts the body — the regenerated v2026.ElicitRequestURLParams (this hunk) only requires message, mode, url. ✅
  2. Monolith step: the CallToolResult arm fails (no content). The InputRequiredResult arm reaches ElicitRequest.params: the URL arm ElicitRequestURLParams rejects the params for missing elicitationId (wire parsing is alias-only, by_name=False, so there is no way to supply it), and the form arm rejects them for the wrong mode literal and missing requestedSchema. ❌
  3. Both union arms fail, so parse_server_result raises pydantic.ValidationError for 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.

Comment on lines +2164 to +2182
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.
"""

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants