Client call_tool: input_responses/request_state retry params; InputRequiredResult via allow_input_required (#2968)
This commit is contained in:
parent
96bf22e57a
commit
03681ed55e
3 changed files with 239 additions and 13 deletions
|
|
@ -5,7 +5,7 @@ from __future__ import annotations
|
|||
from collections.abc import Awaitable, Callable, Mapping
|
||||
from contextlib import AsyncExitStack
|
||||
from dataclasses import KW_ONLY, dataclass, field
|
||||
from typing import Any, Literal, TypeVar
|
||||
from typing import Any, Literal, TypeVar, overload
|
||||
|
||||
import anyio
|
||||
from typing_extensions import deprecated
|
||||
|
|
@ -30,6 +30,8 @@ from mcp.types import (
|
|||
EmptyResult,
|
||||
GetPromptResult,
|
||||
Implementation,
|
||||
InputRequiredResult,
|
||||
InputResponses,
|
||||
ListPromptsResult,
|
||||
ListResourcesResult,
|
||||
ListResourceTemplatesResult,
|
||||
|
|
@ -374,6 +376,7 @@ class Client:
|
|||
"""Unsubscribe from resource updates."""
|
||||
return await self.session.unsubscribe_resource(uri, meta=meta)
|
||||
|
||||
@overload
|
||||
async def call_tool(
|
||||
self,
|
||||
name: str,
|
||||
|
|
@ -381,8 +384,38 @@ class Client:
|
|||
read_timeout_seconds: float | None = None,
|
||||
progress_callback: ProgressFnT | None = None,
|
||||
*,
|
||||
input_responses: InputResponses | None = None,
|
||||
request_state: str | None = None,
|
||||
meta: RequestParamsMeta | None = None,
|
||||
) -> CallToolResult:
|
||||
allow_input_required: Literal[False] = False,
|
||||
) -> CallToolResult: ...
|
||||
|
||||
@overload
|
||||
async def call_tool(
|
||||
self,
|
||||
name: str,
|
||||
arguments: dict[str, Any] | None = None,
|
||||
read_timeout_seconds: float | None = None,
|
||||
progress_callback: ProgressFnT | None = None,
|
||||
*,
|
||||
input_responses: InputResponses | None = None,
|
||||
request_state: str | None = None,
|
||||
meta: RequestParamsMeta | None = None,
|
||||
allow_input_required: bool,
|
||||
) -> CallToolResult | InputRequiredResult: ...
|
||||
|
||||
async def call_tool(
|
||||
self,
|
||||
name: str,
|
||||
arguments: dict[str, Any] | None = None,
|
||||
read_timeout_seconds: float | None = None,
|
||||
progress_callback: ProgressFnT | None = None,
|
||||
*,
|
||||
input_responses: InputResponses | None = None,
|
||||
request_state: str | None = None,
|
||||
meta: RequestParamsMeta | None = None,
|
||||
allow_input_required: bool = False,
|
||||
) -> CallToolResult | InputRequiredResult:
|
||||
"""Call a tool on the server.
|
||||
|
||||
Args:
|
||||
|
|
@ -390,17 +423,32 @@ class Client:
|
|||
arguments: Arguments to pass to the tool
|
||||
read_timeout_seconds: Timeout for the tool call
|
||||
progress_callback: Callback for progress updates
|
||||
input_responses: Responses to a prior `InputRequiredResult.input_requests`
|
||||
request_state: Opaque state echoed from a prior `InputRequiredResult`
|
||||
meta: Additional metadata for the request
|
||||
allow_input_required: When ``False`` (default), an `InputRequiredResult`
|
||||
from the server raises `RuntimeError`; when ``True``, it is returned
|
||||
so the caller can resolve the requests and retry.
|
||||
|
||||
Returns:
|
||||
The tool result.
|
||||
The tool result. When ``allow_input_required=True``, may instead be an
|
||||
`InputRequiredResult` carrying the server's input requests and opaque
|
||||
``request_state`` for the retry.
|
||||
|
||||
Raises:
|
||||
RuntimeError: If the server returns an `InputRequiredResult` and
|
||||
``allow_input_required`` is ``False``.
|
||||
"""
|
||||
# TODO(L84): stop forwarding allow_input_required; run the MRTR auto-loop driver here (S6).
|
||||
return await self.session.call_tool(
|
||||
name=name,
|
||||
arguments=arguments,
|
||||
read_timeout_seconds=read_timeout_seconds,
|
||||
progress_callback=progress_callback,
|
||||
input_responses=input_responses,
|
||||
request_state=request_state,
|
||||
meta=meta,
|
||||
allow_input_required=allow_input_required,
|
||||
)
|
||||
|
||||
async def list_prompts(
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import logging
|
|||
from collections.abc import Callable, Mapping
|
||||
from dataclasses import dataclass
|
||||
from types import TracebackType
|
||||
from typing import Any, Protocol, cast
|
||||
from typing import Any, Literal, Protocol, cast, overload
|
||||
|
||||
import anyio
|
||||
import anyio.abc
|
||||
|
|
@ -173,6 +173,10 @@ async def _default_logging_callback(
|
|||
|
||||
ClientResponse: TypeAdapter[types.ClientResult | types.ErrorData] = TypeAdapter(types.ClientResult | types.ErrorData)
|
||||
|
||||
_CallToolResultAdapter: TypeAdapter[types.CallToolResult | types.InputRequiredResult] = TypeAdapter(
|
||||
types.CallToolResult | types.InputRequiredResult
|
||||
)
|
||||
|
||||
|
||||
class ClientSession:
|
||||
"""Client half of an MCP connection, running on a `Dispatcher`.
|
||||
|
|
@ -269,7 +273,7 @@ class ClientSession:
|
|||
async def send_request(
|
||||
self,
|
||||
request: types.ClientRequest,
|
||||
result_type: type[ReceiveResultT],
|
||||
result_type: type[ReceiveResultT] | TypeAdapter[ReceiveResultT],
|
||||
request_read_timeout_seconds: float | None = None,
|
||||
metadata: ClientMessageMetadata | None = None,
|
||||
progress_callback: ProgressFnT | None = None,
|
||||
|
|
@ -308,6 +312,8 @@ class ClientSession:
|
|||
_methods.validate_server_result(method, version, raw)
|
||||
except KeyError:
|
||||
pass
|
||||
if isinstance(result_type, TypeAdapter):
|
||||
return result_type.validate_python(raw, by_name=False)
|
||||
return result_type.model_validate(raw, by_name=False)
|
||||
|
||||
async def send_notification(self, notification: types.ClientNotification) -> None:
|
||||
|
|
@ -596,6 +602,7 @@ class ClientSession:
|
|||
types.EmptyResult,
|
||||
)
|
||||
|
||||
@overload
|
||||
async def call_tool(
|
||||
self,
|
||||
name: str,
|
||||
|
|
@ -603,22 +610,75 @@ class ClientSession:
|
|||
read_timeout_seconds: float | None = None,
|
||||
progress_callback: ProgressFnT | None = None,
|
||||
*,
|
||||
input_responses: types.InputResponses | None = None,
|
||||
request_state: str | None = None,
|
||||
meta: RequestParamsMeta | None = None,
|
||||
) -> types.CallToolResult:
|
||||
"""Send a tools/call request with optional progress callback support."""
|
||||
allow_input_required: Literal[False] = False,
|
||||
) -> types.CallToolResult: ...
|
||||
|
||||
@overload
|
||||
async def call_tool(
|
||||
self,
|
||||
name: str,
|
||||
arguments: dict[str, Any] | None = None,
|
||||
read_timeout_seconds: float | None = None,
|
||||
progress_callback: ProgressFnT | None = None,
|
||||
*,
|
||||
input_responses: types.InputResponses | None = None,
|
||||
request_state: str | None = None,
|
||||
meta: RequestParamsMeta | None = None,
|
||||
allow_input_required: bool,
|
||||
) -> types.CallToolResult | types.InputRequiredResult: ...
|
||||
|
||||
async def call_tool(
|
||||
self,
|
||||
name: str,
|
||||
arguments: dict[str, Any] | None = None,
|
||||
read_timeout_seconds: float | None = None,
|
||||
progress_callback: ProgressFnT | None = None,
|
||||
*,
|
||||
input_responses: types.InputResponses | None = None,
|
||||
request_state: str | None = None,
|
||||
meta: RequestParamsMeta | None = None,
|
||||
allow_input_required: bool = False,
|
||||
) -> types.CallToolResult | types.InputRequiredResult:
|
||||
"""Send a tools/call request with optional progress callback support.
|
||||
|
||||
Args:
|
||||
input_responses: Responses to a prior `InputRequiredResult.input_requests`.
|
||||
request_state: Opaque state echoed from a prior `InputRequiredResult`.
|
||||
allow_input_required: When ``False`` (default), an `InputRequiredResult`
|
||||
from the server raises `RuntimeError`; when ``True``, it is returned
|
||||
so the caller can resolve the requests and retry.
|
||||
|
||||
Raises:
|
||||
RuntimeError: If the server returns an `InputRequiredResult` and
|
||||
``allow_input_required`` is ``False``.
|
||||
"""
|
||||
|
||||
result = await self.send_request(
|
||||
types.CallToolRequest(
|
||||
params=types.CallToolRequestParams(name=name, arguments=arguments, _meta=meta),
|
||||
params=types.CallToolRequestParams(
|
||||
name=name,
|
||||
arguments=arguments,
|
||||
input_responses=input_responses,
|
||||
request_state=request_state,
|
||||
_meta=meta,
|
||||
),
|
||||
),
|
||||
types.CallToolResult,
|
||||
_CallToolResultAdapter,
|
||||
request_read_timeout_seconds=read_timeout_seconds,
|
||||
progress_callback=progress_callback,
|
||||
)
|
||||
|
||||
if not result.is_error:
|
||||
if isinstance(result, types.CallToolResult) and not result.is_error:
|
||||
await self._validate_tool_result(name, result)
|
||||
|
||||
if isinstance(result, types.InputRequiredResult) and not allow_input_required:
|
||||
raise RuntimeError(
|
||||
"Server returned InputRequiredResult; pass allow_input_required=True to receive it "
|
||||
"and retry call_tool(..., input_responses=..., request_state=result.request_state)."
|
||||
)
|
||||
return result
|
||||
|
||||
async def _validate_tool_result(self, name: str, result: types.CallToolResult) -> None:
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import logging
|
|||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from types import TracebackType
|
||||
from typing import Any, TypeAlias
|
||||
from typing import Any, Literal, TypeAlias, overload
|
||||
|
||||
import anyio
|
||||
import httpx
|
||||
|
|
@ -190,6 +190,7 @@ class ClientSessionGroup:
|
|||
"""Returns the tools as a dictionary of names to tools."""
|
||||
return self._tools
|
||||
|
||||
@overload
|
||||
async def call_tool(
|
||||
self,
|
||||
name: str,
|
||||
|
|
@ -197,9 +198,44 @@ class ClientSessionGroup:
|
|||
read_timeout_seconds: float | None = None,
|
||||
progress_callback: ProgressFnT | None = None,
|
||||
*,
|
||||
input_responses: types.InputResponses | None = None,
|
||||
request_state: str | None = None,
|
||||
meta: types.RequestParamsMeta | None = None,
|
||||
) -> types.CallToolResult:
|
||||
"""Executes a tool given its name and arguments."""
|
||||
allow_input_required: Literal[False] = False,
|
||||
) -> types.CallToolResult: ...
|
||||
|
||||
@overload
|
||||
async def call_tool(
|
||||
self,
|
||||
name: str,
|
||||
arguments: dict[str, Any] | None = None,
|
||||
read_timeout_seconds: float | None = None,
|
||||
progress_callback: ProgressFnT | None = None,
|
||||
*,
|
||||
input_responses: types.InputResponses | None = None,
|
||||
request_state: str | None = None,
|
||||
meta: types.RequestParamsMeta | None = None,
|
||||
allow_input_required: bool,
|
||||
) -> types.CallToolResult | types.InputRequiredResult: ...
|
||||
|
||||
async def call_tool(
|
||||
self,
|
||||
name: str,
|
||||
arguments: dict[str, Any] | None = None,
|
||||
read_timeout_seconds: float | None = None,
|
||||
progress_callback: ProgressFnT | None = None,
|
||||
*,
|
||||
input_responses: types.InputResponses | None = None,
|
||||
request_state: str | None = None,
|
||||
meta: types.RequestParamsMeta | None = None,
|
||||
allow_input_required: bool = False,
|
||||
) -> types.CallToolResult | types.InputRequiredResult:
|
||||
"""Executes a tool given its name and arguments.
|
||||
|
||||
Raises:
|
||||
RuntimeError: If the server returns an `InputRequiredResult` and
|
||||
``allow_input_required`` is ``False``.
|
||||
"""
|
||||
session = self._tool_to_session[name]
|
||||
session_tool_name = self.tools[name].name
|
||||
return await session.call_tool(
|
||||
|
|
@ -207,7 +243,10 @@ class ClientSessionGroup:
|
|||
arguments=arguments,
|
||||
read_timeout_seconds=read_timeout_seconds,
|
||||
progress_callback=progress_callback,
|
||||
input_responses=input_responses,
|
||||
request_state=request_state,
|
||||
meta=meta,
|
||||
allow_input_required=allow_input_required,
|
||||
)
|
||||
|
||||
async def disconnect_from_server(self, session: mcp.ClientSession) -> None:
|
||||
|
|
|
|||
Loading…
Reference in a new issue