From 4749ce3284e5d043df39030bdc7966c6d4958442 Mon Sep 17 00:00:00 2001 From: Max Isbey <224885523+maxisbey@users.noreply.github.com> Date: Fri, 19 Jun 2026 14:59:31 +0000 Subject: [PATCH] Relax monolith ElicitRequestURLParams.elicitation_id to optional for 2026-07-28 Follow-up to #2912. The re-vendored 2026-07-28 schema dropped elicitationId from ElicitRequestURLParams, but the version-superset monolith model still required it, so parse_server_result rejected a spec-valid 2026 InputRequiredResult embedding a URL elicitation at the monolith step. The v2025_11_25 surface still enforces requiredness for 2025 callers. Also updates the CancelledNotificationParams.request_id docstring to note that 2026-07-28 makes the field required again. --- src/mcp/types/_types.py | 9 ++++--- .../interaction/lowlevel/test_elicitation.py | 2 +- tests/types/test_methods.py | 27 +++++++++++++++++++ 3 files changed, 34 insertions(+), 4 deletions(-) diff --git a/src/mcp/types/_types.py b/src/mcp/types/_types.py index 390c24e81..d64f5ea4f 100644 --- a/src/mcp/types/_types.py +++ b/src/mcp/types/_types.py @@ -1868,7 +1868,9 @@ class CancelledNotificationParams(NotificationParams): The ID of the request to cancel. This MUST correspond to the ID of a request previously issued in the same direction. - Required on the wire through 2025-06-18; optional from 2025-11-25. + Required on the wire through 2025-06-18; optional at 2025-11-25; required again from + 2026-07-28, where it must name a request the client previously issued (servers send + this notification only to terminate a `subscriptions/listen` stream). """ reason: str | None = None """An optional string describing the reason for the cancellation.""" @@ -1956,10 +1958,11 @@ class ElicitRequestURLParams(RequestParams): url: str """The URL that the user should navigate to.""" - elicitation_id: str + elicitation_id: str | None = None """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. + The client MUST treat this ID as an opaque value. Required on the wire at + 2025-11-25; removed at 2026-07-28. """ task: TaskMetadata | None = None diff --git a/tests/interaction/lowlevel/test_elicitation.py b/tests/interaction/lowlevel/test_elicitation.py index 8e57e1429..52adc3b1e 100644 --- a/tests/interaction/lowlevel/test_elicitation.py +++ b/tests/interaction/lowlevel/test_elicitation.py @@ -309,7 +309,7 @@ async def test_elicitation_complete_notification_carries_the_elicited_id_back_to returns; the same ordering already holds on in-memory and SSE transports. """ elicitation_id = "auth-001" - elicited_ids: list[str] = [] + elicited_ids: list[str | None] = [] received: list[IncomingMessage] = [] async def collect(message: IncomingMessage) -> None: diff --git a/tests/types/test_methods.py b/tests/types/test_methods.py index 1a467f507..c6d6823d7 100644 --- a/tests/types/test_methods.py +++ b/tests/types/test_methods.py @@ -674,6 +674,33 @@ def test_embedded_input_request_entries_without_method_reject_at_the_surface_ste methods.parse_server_result("tools/call", "2026-07-28", body) +def test_input_required_url_elicit_without_elicitation_id_parses_at_2026(): + """A 2026-07-28 `InputRequiredResult` embedding a URL-mode elicitation parses + through both the surface and monolith steps without `elicitationId`. + + Spec-mandated: the field is required at 2025-11-25 only and removed at + 2026-07-28; the monolith model carries it as optional so the superset can + accept both versions. + """ + body = { + "resultType": "input_required", + "inputRequests": { + "r1": { + "method": "elicitation/create", + "params": {"mode": "url", "message": "Please sign in", "url": "https://example.com/auth"}, + } + }, + } + parsed = methods.parse_server_result("tools/call", "2026-07-28", body) + assert isinstance(parsed, types.InputRequiredResult) + assert parsed.input_requests is not None + request = parsed.input_requests["r1"] + assert isinstance(request, types.ElicitRequest) + assert isinstance(request.params, types.ElicitRequestURLParams) + assert request.params.url == "https://example.com/auth" + assert request.params.elicitation_id is None + + def test_none_params_omit_the_key_so_required_params_reject(): with pytest.raises(pydantic.ValidationError) as excinfo: methods.parse_client_request("tools/call", "2025-11-25", None)