mirror of
https://github.com/PrefectHQ/fastmcp.git
synced 2026-08-21 04:54:17 +02:00
Name the client-side flag for who drives the exchange
allow_input_required was the SDK session's own vocabulary. drive=True (the default) answers each ask from the client's handlers and returns a terminal result; drive=False hands the ask back so the caller can answer it on a later call, which is what an app whose user replies minutes later actually needs.
This commit is contained in:
parent
2018242664
commit
5fe1887fa0
2 changed files with 20 additions and 21 deletions
|
|
@ -147,7 +147,7 @@ class ClientToolsMixin:
|
|||
*,
|
||||
input_responses: mcp_types.InputResponses | None = None,
|
||||
request_state: str | None = None,
|
||||
allow_input_required: Literal[False] = False,
|
||||
drive: Literal[True] = True,
|
||||
) -> mcp_types.CallToolResult: ...
|
||||
|
||||
@overload
|
||||
|
|
@ -161,7 +161,7 @@ class ClientToolsMixin:
|
|||
*,
|
||||
input_responses: mcp_types.InputResponses | None = None,
|
||||
request_state: str | None = None,
|
||||
allow_input_required: Literal[True],
|
||||
drive: Literal[False],
|
||||
) -> mcp_types.CallToolResult | mcp_types.InputRequiredResult: ...
|
||||
|
||||
async def call_tool_mcp(
|
||||
|
|
@ -174,7 +174,7 @@ class ClientToolsMixin:
|
|||
*,
|
||||
input_responses: mcp_types.InputResponses | None = None,
|
||||
request_state: str | None = None,
|
||||
allow_input_required: bool = False,
|
||||
drive: bool = True,
|
||||
) -> mcp_types.CallToolResult | mcp_types.InputRequiredResult:
|
||||
"""Send a tools/call request and return the complete MCP protocol result.
|
||||
|
||||
|
|
@ -185,13 +185,13 @@ class ClientToolsMixin:
|
|||
(SEP-2322) rather than a final result. By default that is resolved for
|
||||
you, the same way `call_tool` does it — each embedded request is
|
||||
dispatched to this client's handlers and the call is retried until it
|
||||
completes. Pass `allow_input_required=True` to receive the ask instead
|
||||
and drive the exchange one leg at a time, feeding the answers back
|
||||
through `input_responses` and `request_state`:
|
||||
completes. Pass `drive=False` to take the wheel: the ask is handed back
|
||||
to you, and you answer it by calling again with `input_responses` and
|
||||
the `request_state` from the previous leg.
|
||||
|
||||
```python
|
||||
async with Client(mcp) as client:
|
||||
leg = await client.call_tool_mcp("book", {}, allow_input_required=True)
|
||||
leg = await client.call_tool_mcp("book", {}, drive=False)
|
||||
answers = {
|
||||
key: mcp_types.ElicitResult(action="accept", content={"value": "Paris"})
|
||||
for key in leg.input_requests
|
||||
|
|
@ -201,7 +201,7 @@ class ClientToolsMixin:
|
|||
{},
|
||||
input_responses=answers,
|
||||
request_state=leg.request_state,
|
||||
allow_input_required=True,
|
||||
drive=False,
|
||||
)
|
||||
```
|
||||
|
||||
|
|
@ -218,7 +218,8 @@ class ClientToolsMixin:
|
|||
Returns:
|
||||
The complete response object from the protocol. An
|
||||
`InputRequiredResult` when the tool asked for input and
|
||||
`allow_input_required` is set; otherwise a `CallToolResult`.
|
||||
`drive=False` and the tool asked for input; otherwise a
|
||||
`CallToolResult`.
|
||||
|
||||
Raises:
|
||||
RuntimeError: If called while the client is not connected.
|
||||
|
|
@ -274,9 +275,7 @@ class ClientToolsMixin:
|
|||
first = await self._await_with_session_monitoring(
|
||||
_retry(input_responses, request_state)
|
||||
)
|
||||
if allow_input_required and isinstance(
|
||||
first, mcp_types.InputRequiredResult
|
||||
):
|
||||
if not drive and isinstance(first, mcp_types.InputRequiredResult):
|
||||
# The caller is driving the exchange, so hand back the ask
|
||||
# untouched rather than resolving it against our own handlers.
|
||||
return first
|
||||
|
|
@ -348,7 +347,7 @@ class ClientToolsMixin:
|
|||
meta: dict[str, Any] | None = None,
|
||||
input_responses: mcp_types.InputResponses | None = None,
|
||||
request_state: str | None = None,
|
||||
allow_input_required: bool = False,
|
||||
drive: bool = True,
|
||||
) -> CallToolResult:
|
||||
"""Call a tool on the server.
|
||||
|
||||
|
|
@ -395,7 +394,7 @@ class ClientToolsMixin:
|
|||
meta=request_meta or None,
|
||||
input_responses=input_responses,
|
||||
request_state=request_state,
|
||||
allow_input_required=cast("Literal[True]", allow_input_required),
|
||||
drive=cast("Literal[False]", drive),
|
||||
)
|
||||
if isinstance(result, mcp_types.InputRequiredResult):
|
||||
# The caller is driving; hand the ask back rather than parsing it as
|
||||
|
|
|
|||
|
|
@ -576,7 +576,7 @@ def carried(result) -> str | None:
|
|||
|
||||
|
||||
class TestDrivingLegsByHand:
|
||||
"""`allow_input_required=True` hands back each leg instead of resolving it,
|
||||
"""`drive=False` hands back each leg instead of resolving it,
|
||||
so a test can assert on the wire shape a client would actually receive."""
|
||||
|
||||
async def test_each_leg_is_visible(self):
|
||||
|
|
@ -594,14 +594,14 @@ class TestDrivingLegsByHand:
|
|||
|
||||
# No elicitation_handler — nothing drives the exchange but this test.
|
||||
async with Client(mcp) as client:
|
||||
first = await client.call_tool("book", allow_input_required=True)
|
||||
first = await client.call_tool("book", drive=False)
|
||||
assert questions(first) == {"destination": "Where would you like to fly?"}
|
||||
|
||||
second = await client.call_tool(
|
||||
"book",
|
||||
input_responses=accepted(destination="Paris"),
|
||||
request_state=carried(first),
|
||||
allow_input_required=True,
|
||||
drive=False,
|
||||
)
|
||||
# Only the airport — the destination is not asked again.
|
||||
assert questions(second) == {"airport": "Which airport in Paris?"}
|
||||
|
|
@ -610,7 +610,7 @@ class TestDrivingLegsByHand:
|
|||
"book",
|
||||
input_responses=accepted(airport="CDG"),
|
||||
request_state=carried(second),
|
||||
allow_input_required=True,
|
||||
drive=False,
|
||||
)
|
||||
|
||||
assert final.input_required is None
|
||||
|
|
@ -627,7 +627,7 @@ class TestDrivingLegsByHand:
|
|||
return f"{destination} on {date}"
|
||||
|
||||
async with Client(mcp) as client:
|
||||
first = await client.call_tool("book", allow_input_required=True)
|
||||
first = await client.call_tool("book", drive=False)
|
||||
assert questions(first) == {
|
||||
"destination": "Where to?",
|
||||
"date": "When?",
|
||||
|
|
@ -637,7 +637,7 @@ class TestDrivingLegsByHand:
|
|||
"book",
|
||||
input_responses=accepted(destination="Paris", date="2026-08-01"),
|
||||
request_state=carried(first),
|
||||
allow_input_required=True,
|
||||
drive=False,
|
||||
)
|
||||
|
||||
assert final.data == "Paris on 2026-08-01"
|
||||
|
|
@ -661,7 +661,7 @@ class TestDrivingLegsByHand:
|
|||
|
||||
async with Client(mcp) as client:
|
||||
result = await client.call_tool(
|
||||
"book", {"destination": "London"}, allow_input_required=True
|
||||
"book", {"destination": "London"}, drive=False
|
||||
)
|
||||
|
||||
# Terminal on the first leg — there was never anything to ask.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue