lowlevel Server: widen on_* return types for InputRequiredResult; add subscriptions/listen slot (#2967)
This commit is contained in:
parent
a527142312
commit
ae13ede143
3 changed files with 29 additions and 6 deletions
|
|
@ -148,7 +148,7 @@ class Server(Generic[LifespanResultT]):
|
||||||
| None = None,
|
| None = None,
|
||||||
on_call_tool: Callable[
|
on_call_tool: Callable[
|
||||||
[ServerRequestContext[LifespanResultT], types.CallToolRequestParams],
|
[ServerRequestContext[LifespanResultT], types.CallToolRequestParams],
|
||||||
Awaitable[types.CallToolResult],
|
Awaitable[types.CallToolResult | types.InputRequiredResult],
|
||||||
]
|
]
|
||||||
| None = None,
|
| None = None,
|
||||||
on_list_resources: Callable[
|
on_list_resources: Callable[
|
||||||
|
|
@ -163,7 +163,7 @@ class Server(Generic[LifespanResultT]):
|
||||||
| None = None,
|
| None = None,
|
||||||
on_read_resource: Callable[
|
on_read_resource: Callable[
|
||||||
[ServerRequestContext[LifespanResultT], types.ReadResourceRequestParams],
|
[ServerRequestContext[LifespanResultT], types.ReadResourceRequestParams],
|
||||||
Awaitable[types.ReadResourceResult],
|
Awaitable[types.ReadResourceResult | types.InputRequiredResult],
|
||||||
]
|
]
|
||||||
| None = None,
|
| None = None,
|
||||||
on_subscribe_resource: Callable[
|
on_subscribe_resource: Callable[
|
||||||
|
|
@ -176,6 +176,11 @@ class Server(Generic[LifespanResultT]):
|
||||||
Awaitable[types.EmptyResult],
|
Awaitable[types.EmptyResult],
|
||||||
]
|
]
|
||||||
| None = None,
|
| None = None,
|
||||||
|
on_subscriptions_listen: Callable[
|
||||||
|
[ServerRequestContext[LifespanResultT], types.SubscriptionsListenRequestParams],
|
||||||
|
Awaitable[types.EmptyResult],
|
||||||
|
]
|
||||||
|
| None = None,
|
||||||
on_list_prompts: Callable[
|
on_list_prompts: Callable[
|
||||||
[ServerRequestContext[LifespanResultT], types.PaginatedRequestParams | None],
|
[ServerRequestContext[LifespanResultT], types.PaginatedRequestParams | None],
|
||||||
Awaitable[types.ListPromptsResult],
|
Awaitable[types.ListPromptsResult],
|
||||||
|
|
@ -183,7 +188,7 @@ class Server(Generic[LifespanResultT]):
|
||||||
| None = None,
|
| None = None,
|
||||||
on_get_prompt: Callable[
|
on_get_prompt: Callable[
|
||||||
[ServerRequestContext[LifespanResultT], types.GetPromptRequestParams],
|
[ServerRequestContext[LifespanResultT], types.GetPromptRequestParams],
|
||||||
Awaitable[types.GetPromptResult],
|
Awaitable[types.GetPromptResult | types.InputRequiredResult],
|
||||||
]
|
]
|
||||||
| None = None,
|
| None = None,
|
||||||
on_completion: Callable[
|
on_completion: Callable[
|
||||||
|
|
@ -242,6 +247,7 @@ class Server(Generic[LifespanResultT]):
|
||||||
("resources/read", types.ReadResourceRequestParams, on_read_resource),
|
("resources/read", types.ReadResourceRequestParams, on_read_resource),
|
||||||
("resources/subscribe", types.SubscribeRequestParams, on_subscribe_resource),
|
("resources/subscribe", types.SubscribeRequestParams, on_subscribe_resource),
|
||||||
("resources/unsubscribe", types.UnsubscribeRequestParams, on_unsubscribe_resource),
|
("resources/unsubscribe", types.UnsubscribeRequestParams, on_unsubscribe_resource),
|
||||||
|
("subscriptions/listen", types.SubscriptionsListenRequestParams, on_subscriptions_listen),
|
||||||
("tools/list", types.PaginatedRequestParams, on_list_tools),
|
("tools/list", types.PaginatedRequestParams, on_list_tools),
|
||||||
("tools/call", types.CallToolRequestParams, on_call_tool),
|
("tools/call", types.CallToolRequestParams, on_call_tool),
|
||||||
("logging/setLevel", types.SetLevelRequestParams, on_set_logging_level),
|
("logging/setLevel", types.SetLevelRequestParams, on_set_logging_level),
|
||||||
|
|
|
||||||
|
|
@ -16,9 +16,10 @@ from pydantic import (
|
||||||
Field,
|
Field,
|
||||||
FileUrl,
|
FileUrl,
|
||||||
TypeAdapter,
|
TypeAdapter,
|
||||||
|
model_validator,
|
||||||
)
|
)
|
||||||
from pydantic.alias_generators import to_camel
|
from pydantic.alias_generators import to_camel
|
||||||
from typing_extensions import NotRequired, TypedDict
|
from typing_extensions import NotRequired, Self, TypedDict
|
||||||
|
|
||||||
from mcp.types.jsonrpc import RequestId
|
from mcp.types.jsonrpc import RequestId
|
||||||
|
|
||||||
|
|
@ -2052,7 +2053,7 @@ class InputRequiredResult(Result):
|
||||||
(`tools/call`, `prompts/get`, `resources/read`). The client fulfills
|
(`tools/call`, `prompts/get`, `resources/read`). The client fulfills
|
||||||
`input_requests` and retries the original request, carrying the responses
|
`input_requests` and retries the original request, carrying the responses
|
||||||
and the echoed `request_state`. At least one of those two fields is
|
and the echoed `request_state`. At least one of those two fields is
|
||||||
present on the wire (spec MUST; not enforced by the model).
|
present on the wire (spec MUST).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
result_type: Literal["input_required"] = "input_required"
|
result_type: Literal["input_required"] = "input_required"
|
||||||
|
|
@ -2064,6 +2065,12 @@ class InputRequiredResult(Result):
|
||||||
request_state: str | None = None
|
request_state: str | None = None
|
||||||
"""Opaque state to pass back verbatim when the client retries the original request."""
|
"""Opaque state to pass back verbatim when the client retries the original request."""
|
||||||
|
|
||||||
|
@model_validator(mode="after")
|
||||||
|
def _require_one_field(self) -> Self:
|
||||||
|
if self.input_requests is None and self.request_state is None:
|
||||||
|
raise ValueError("InputRequiredResult requires at least one of input_requests or request_state")
|
||||||
|
return self
|
||||||
|
|
||||||
|
|
||||||
# Forward refs to InputResponses; rebuild at import time rather than first use.
|
# Forward refs to InputResponses; rebuild at import time rather than first use.
|
||||||
InputResponseRequestParams.model_rebuild()
|
InputResponseRequestParams.model_rebuild()
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ from typing import Any
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from inline_snapshot import snapshot
|
from inline_snapshot import snapshot
|
||||||
|
from pydantic import ValidationError
|
||||||
|
|
||||||
from mcp.types import (
|
from mcp.types import (
|
||||||
LATEST_PROTOCOL_VERSION,
|
LATEST_PROTOCOL_VERSION,
|
||||||
|
|
@ -436,4 +437,13 @@ def test_empty_result_dumps_result_type_only_when_explicitly_tagged():
|
||||||
|
|
||||||
|
|
||||||
def test_input_required_result_dumps_its_discriminating_tag():
|
def test_input_required_result_dumps_its_discriminating_tag():
|
||||||
assert _wire_dump(InputRequiredResult()) == snapshot({"resultType": "input_required"})
|
assert _wire_dump(InputRequiredResult(request_state="s")) == snapshot(
|
||||||
|
{"resultType": "input_required", "requestState": "s"}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_input_required_result_requires_at_least_one_of_input_requests_or_request_state():
|
||||||
|
with pytest.raises(ValidationError):
|
||||||
|
InputRequiredResult()
|
||||||
|
assert InputRequiredResult(input_requests={}).request_state is None
|
||||||
|
assert InputRequiredResult(request_state="s").input_requests is None
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue