Skip to content

Commit 682b788

Browse files
committed
lowlevel Server: widen on_* return types for InputRequiredResult; add subscriptions/listen slot
- on_call_tool / on_get_prompt / on_read_resource return types widened to include InputRequiredResult, matching MONOLITH_RESULTS in types/methods.py. Covariant widening; existing handlers returning the concrete type still type-check. - on_subscriptions_listen kwarg + _spec_requests row added so the 2026 subscriptions/listen method has a dispatch slot. - InputRequiredResult: model_validator enforces at-least-one of input_requests / request_state (spec MUST).
1 parent a527142 commit 682b788

3 files changed

Lines changed: 31 additions & 5 deletions

File tree

src/mcp/server/lowlevel/server.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ def __init__(
148148
| None = None,
149149
on_call_tool: Callable[
150150
[ServerRequestContext[LifespanResultT], types.CallToolRequestParams],
151-
Awaitable[types.CallToolResult],
151+
Awaitable[types.CallToolResult | types.InputRequiredResult],
152152
]
153153
| None = None,
154154
on_list_resources: Callable[
@@ -163,7 +163,7 @@ def __init__(
163163
| None = None,
164164
on_read_resource: Callable[
165165
[ServerRequestContext[LifespanResultT], types.ReadResourceRequestParams],
166-
Awaitable[types.ReadResourceResult],
166+
Awaitable[types.ReadResourceResult | types.InputRequiredResult],
167167
]
168168
| None = None,
169169
on_subscribe_resource: Callable[
@@ -176,14 +176,19 @@ def __init__(
176176
Awaitable[types.EmptyResult],
177177
]
178178
| None = None,
179+
on_subscriptions_listen: Callable[
180+
[ServerRequestContext[LifespanResultT], types.SubscriptionsListenRequestParams],
181+
Awaitable[types.EmptyResult],
182+
]
183+
| None = None,
179184
on_list_prompts: Callable[
180185
[ServerRequestContext[LifespanResultT], types.PaginatedRequestParams | None],
181186
Awaitable[types.ListPromptsResult],
182187
]
183188
| None = None,
184189
on_get_prompt: Callable[
185190
[ServerRequestContext[LifespanResultT], types.GetPromptRequestParams],
186-
Awaitable[types.GetPromptResult],
191+
Awaitable[types.GetPromptResult | types.InputRequiredResult],
187192
]
188193
| None = None,
189194
on_completion: Callable[
@@ -242,6 +247,7 @@ def __init__(
242247
("resources/read", types.ReadResourceRequestParams, on_read_resource),
243248
("resources/subscribe", types.SubscribeRequestParams, on_subscribe_resource),
244249
("resources/unsubscribe", types.UnsubscribeRequestParams, on_unsubscribe_resource),
250+
("subscriptions/listen", types.SubscriptionsListenRequestParams, on_subscriptions_listen),
245251
("tools/list", types.PaginatedRequestParams, on_list_tools),
246252
("tools/call", types.CallToolRequestParams, on_call_tool),
247253
("logging/setLevel", types.SetLevelRequestParams, on_set_logging_level),

src/mcp/types/_types.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,10 @@
1616
Field,
1717
FileUrl,
1818
TypeAdapter,
19+
model_validator,
1920
)
2021
from pydantic.alias_generators import to_camel
21-
from typing_extensions import NotRequired, TypedDict
22+
from typing_extensions import NotRequired, Self, TypedDict
2223

2324
from mcp.types.jsonrpc import RequestId
2425

@@ -2052,7 +2053,7 @@ class InputRequiredResult(Result):
20522053
(`tools/call`, `prompts/get`, `resources/read`). The client fulfills
20532054
`input_requests` and retries the original request, carrying the responses
20542055
and the echoed `request_state`. At least one of those two fields is
2055-
present on the wire (spec MUST; not enforced by the model).
2056+
present on the wire (spec MUST).
20562057
"""
20572058

20582059
result_type: Literal["input_required"] = "input_required"
@@ -2064,6 +2065,12 @@ class InputRequiredResult(Result):
20642065
request_state: str | None = None
20652066
"""Opaque state to pass back verbatim when the client retries the original request."""
20662067

2068+
@model_validator(mode="after")
2069+
def _require_one_field(self) -> Self:
2070+
if self.input_requests is None and self.request_state is None:
2071+
raise ValueError("InputRequiredResult requires at least one of input_requests or request_state")
2072+
return self
2073+
20672074

20682075
# Forward refs to InputResponses; rebuild at import time rather than first use.
20692076
InputResponseRequestParams.model_rebuild()

tests/types/test_input_required.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import pytest
2+
from pydantic import ValidationError
3+
4+
from mcp import types
5+
6+
7+
def test_input_required_result_requires_one_field():
8+
with pytest.raises(ValidationError):
9+
types.InputRequiredResult()
10+
assert types.InputRequiredResult(input_requests={}).request_state is None
11+
assert types.InputRequiredResult(request_state="x").input_requests is None
12+
both = types.InputRequiredResult(input_requests={}, request_state="x")
13+
assert both.input_requests == {} and both.request_state == "x"

0 commit comments

Comments
 (0)