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)