Client call_tool: input_responses/request_state retry params; InputRequiredResult via allow_input_required (#2968)
This commit is contained in:
parent
96bf22e57a
commit
03681ed55e
2 changed files with 239 additions and 13 deletions
|
|
@ -12,7 +12,9 @@ from pydantic import FileUrl, ValidationError
|
|||
|
||||
from mcp import MCPError, types
|
||||
from mcp.client import ClientRequestContext
|
||||
from mcp.client.client import Client
|
||||
from mcp.client.session import DEFAULT_CLIENT_INFO, ClientSession
|
||||
from mcp.server import Server, ServerRequestContext
|
||||
from mcp.shared.direct_dispatcher import create_direct_dispatcher_pair
|
||||
from mcp.shared.dispatcher import CallOptions, DispatchContext, OnNotify, OnRequest
|
||||
from mcp.shared.message import SessionMessage
|
||||
|
|
@ -1656,3 +1658,59 @@ async def test_discover_reraises_unsupported_version_with_malformed_error_data()
|
|||
await session.discover()
|
||||
assert exc.value.error.code == UNSUPPORTED_PROTOCOL_VERSION
|
||||
assert [m for m, _ in dispatcher.calls] == ["server/discover"]
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_call_tool_returns_input_required_result_when_server_requests_input() -> None:
|
||||
# `on_call_tool` is still typed `-> CallToolResult` on this branch (#2967 widens it later);
|
||||
# `add_request_handler` is `HandlerResult`-typed and accepts `InputRequiredResult` cleanly.
|
||||
async def handler(ctx: ServerRequestContext, params: types.CallToolRequestParams) -> types.InputRequiredResult:
|
||||
return types.InputRequiredResult(request_state="s")
|
||||
|
||||
server = Server("test")
|
||||
server.add_request_handler("tools/call", types.CallToolRequestParams, handler)
|
||||
with anyio.fail_after(5):
|
||||
async with Client(server, mode="2026-07-28") as client:
|
||||
result = await client.call_tool("ask", allow_input_required=True)
|
||||
assert isinstance(result, types.InputRequiredResult)
|
||||
assert result.request_state == "s"
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_call_tool_threads_input_responses_and_request_state_into_params() -> None:
|
||||
captured: list[types.CallToolRequestParams] = []
|
||||
|
||||
async def on_call_tool(ctx: ServerRequestContext, params: types.CallToolRequestParams) -> CallToolResult:
|
||||
captured.append(params)
|
||||
return CallToolResult(content=[])
|
||||
|
||||
async def on_list_tools(
|
||||
ctx: ServerRequestContext, params: types.PaginatedRequestParams | None
|
||||
) -> types.ListToolsResult:
|
||||
return types.ListToolsResult(tools=[])
|
||||
|
||||
server = Server("test", on_call_tool=on_call_tool, on_list_tools=on_list_tools)
|
||||
with anyio.fail_after(5):
|
||||
async with Client(server, mode="2026-07-28") as client:
|
||||
await client.call_tool(
|
||||
"ask",
|
||||
input_responses={"k": types.ElicitResult(action="decline")},
|
||||
request_state="s",
|
||||
)
|
||||
assert captured[0].input_responses == {"k": types.ElicitResult(action="decline")}
|
||||
assert captured[0].request_state == "s"
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_client_call_tool_raises_on_input_required_without_opt_in() -> None:
|
||||
async def handler(ctx: ServerRequestContext, params: types.CallToolRequestParams) -> types.InputRequiredResult:
|
||||
return types.InputRequiredResult(request_state="s")
|
||||
|
||||
server = Server("test")
|
||||
server.add_request_handler("tools/call", types.CallToolRequestParams, handler)
|
||||
with anyio.fail_after(5):
|
||||
async with Client(server, mode="2026-07-28") as client:
|
||||
with pytest.raises(RuntimeError):
|
||||
await client.call_tool("t")
|
||||
result = await client.call_tool("t", allow_input_required=True)
|
||||
assert isinstance(result, types.InputRequiredResult)
|
||||
|
|
|
|||
|
|
@ -82,10 +82,27 @@ async def test_client_session_group_call_tool():
|
|||
arguments={"name": "value1", "args": {}},
|
||||
read_timeout_seconds=None,
|
||||
progress_callback=None,
|
||||
input_responses=None,
|
||||
request_state=None,
|
||||
meta=None,
|
||||
allow_input_required=False,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_client_session_group_call_tool_forwards_allow_input_required():
|
||||
mock_session = mock.AsyncMock()
|
||||
mcp_session_group = ClientSessionGroup()
|
||||
mcp_session_group._tools = {"my_tool": types.Tool(name="my_tool", input_schema={})}
|
||||
mcp_session_group._tool_to_session = {"my_tool": mock_session}
|
||||
mock_session.call_tool.return_value = types.InputRequiredResult(request_state="s")
|
||||
|
||||
result = await mcp_session_group.call_tool(name="my_tool", arguments={}, allow_input_required=True)
|
||||
assert isinstance(result, types.InputRequiredResult)
|
||||
assert result.request_state == "s"
|
||||
assert mock_session.call_tool.call_args.kwargs["allow_input_required"] is True
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_client_session_group_connect_to_server(mock_exit_stack: contextlib.AsyncExitStack):
|
||||
"""Test connecting to a server and aggregating components."""
|
||||
|
|
|
|||
Loading…
Reference in a new issue