Uh oh!
There was an error while loading. Please reload this page.
Client call_tool: input_responses/request_state retry params; InputRequiredResult via allow_input_required - #2968
Conversation
…equiredResult via allow_input_required opt-in - ClientSession.send_request: accept TypeAdapter[T] alongside type[T] for result_type so callers can parse union results. - ClientSession.call_tool (mechanics): add input_responses= / request_state= retry kwargs; return CallToolResult | InputRequiredResult; gate output-schema validation on isinstance(result, CallToolResult). - Client.call_tool / ClientSessionGroup.call_tool (policy): @overload on allow_input_required — Literal[False] (default) returns CallToolResult; Literal[True] returns the union. Default raises RuntimeError on InputRequiredResult with a retry steer (TODO(L80) marks where the auto-loop driver replaces this). - Examples and tests that call ClientSession.call_tool directly narrow with isinstance(result, CallToolResult); README.v2.md regenerated from snippets.
adbfd73 to
0b4d8a9CompareThe gate now lives once on ClientSession (mechanics layer); Client and ClientSessionGroup are pure passthroughs that forward allow_input_required. Third 'bool' overload on ClientSession.call_tool lets the passthrough impls type-check. Reverts the isinstance narrowing in examples/ and tests/shared/; default return is CallToolResult everywhere, so the change is additive.
There was a problem hiding this comment.
Additional findings (outside current diff — PR may have been updated during review):
🟡
src/mcp/client/session.py:402-418— The Raises section ofsend_discoverdocuments aProbeNotRecognizedexception, but no class with that name exists anywhere in the repository — callers cannot catch it. The bare-4xx case it describes is actually surfaced asMCPError(the streamable-HTTP transport maps it into a JSON-RPC error frame), so this entry should be removed or folded into the existingMCPErrorline.Extended reasoning...
The bug: The docstring of
ClientSession.send_discover(src/mcp/client/session.py:402-418) lists two exception types under Raises:MCPErrorfor a JSON-RPC error from the server, andProbeNotRecognizedfor "the transport bounced the request at its own layer (HTTP 4xx without a JSON-RPC error body)". The second entry references an exception class that does not exist: a repo-wide grep findsProbeNotRecognizedexactly once — in this docstring. It is not defined or exported inmcp.shared.exceptions(which hasMCPError,MCPDeprecationWarning,NoBackChannelError, …),mcp.client.streamable_http, or anywhere else, so a caller writingexcept ProbeNotRecognized:would hit anImportError/NameErrorbefore even reaching the call.Why the documented path can't happen as described: The streamable-HTTP transport never lets a bare HTTP 4xx escape as a transport-layer exception. In
_handle_post_request(src/mcp/client/streamable_http.py), a non-2xx response with no parseable JSON-RPC error body is converted into a syntheticJSONRPCErrorframe and written onto the read stream — this PR even adds the new pre-session mapping where a bare 404 becomesMETHOD_NOT_FOUND("Not Found"), and other statuses becomeINVALID_REQUEST/INTERNAL_ERROR. The dispatcher then surfaces that frame tosend_discover'ssend_raw_requestcall as anMCPError— exactly the exception type the first Raises line already documents.Step-by-step proof:
negotiate_auto(ordiscover()) callssession.send_discover("2026-07-28"), which POSTsserver/discover.- A legacy server / gateway answers with a bare
404and no JSON-RPC body. _handle_post_requestseesresponse.is_successis false, fails to parse a JSON-RPC error from the body, and (sinceself.session_id is None) buildsErrorData(code=METHOD_NOT_FOUND, message="Not Found"), wrapping it in aJSONRPCErrorsent to the read stream (this is pinned bytests/client/test_streamable_http.py::test_pre_session_bare_404_maps_to_method_not_found).JSONRPCDispatchercorrelates the error frame to the pending request and raisesMCPError(code=-32601)fromsend_raw_request.- The caller of
send_discovertherefore catchesMCPError— never anyProbeNotRecognized. Consistently, bothnegotiate_autoin_probe.pyandClientSession.discover()only catchMCPError, and_probe.py's own module docstring says HTTP-layer 4xx rejections "are mapped into MCPError codes".
Why nothing else prevents it: This is documentation only — no code references the name, so type checkers and tests are silent. It looks like a leftover from an earlier design where the transport surfaced bare 4xx as a distinct exception before the JSONRPCError-frame mapping was adopted.
Impact: A reader of the docstring may try to import and catch
ProbeNotRecognizedand get a confusingImportError, or wrongly conclude theirexcept MCPErrorblock won't cover the bare-4xx case. No functional impact on the SDK itself.Fix: Delete the
ProbeNotRecognizedentry, or fold it into theMCPErrorline, e.g. "MCPError: The server returned a JSON-RPC error, or the transport rejected the request at the HTTP layer (a bare 4xx is mapped into a JSON-RPC error frame)."
Uh oh!
There was an error while loading. Please reload this page.
Aligns with the class branch and the existing validate_python(..., by_name=False) call sites in types/methods.py — wire data must use camelCase aliases only.
There was a problem hiding this comment.
2 issues found and verified against the latest diff
Reply with feedback, questions, or to request a fix.
Fix all with cubic | Re-trigger cubic
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
maxisbey
commented
Jun 25, 2026
Re the "additional findings" note about |
…ayers A runtime bool variable now matches the second overload (union return); literal False and the default still match the first (CallToolResult). Drops the redundant Literal[True] arm.
There was a problem hiding this comment.
Both follow-up fixes look right — the TypeAdapter branch now passes by_name=False (78b1f9d) and the overloads are collapsed to Literal[False]/bool consistently across all three layers (68fbef9) — but this widens the public call_tool API surface for a new protocol feature, so it deserves a human maintainer's look rather than auto-approval.
Extended reasoning...
Overview
This PR widens the client-side call_tool path across three layers (ClientSession, Client, ClientSessionGroup) so a 2026-07-28 tools/call can return an InputRequiredResult, adding input_responses/request_state retry kwargs and an allow_input_required opt-in gate (default False raises RuntimeError). ClientSession.send_request is generalized to accept a TypeAdapter result type, and migration docs plus tests for all three layers are added.
Security risks
None identified. The change is client-side result parsing and parameter plumbing; no auth, crypto, or permission logic is touched. The earlier wire-strictness inconsistency (lenient snake_case acceptance on the new TypeAdapter branch) was fixed in 78b1f9d by passing by_name=False, restoring uniform alias-only validation of inbound server data.
Level of scrutiny
Moderate-to-high. Although no bugs remain after the follow-up commits, this is a deliberate public-API design change (union return type, new opt-in flag, overload shape, a TODO placeholder for a future auto-loop driver) on a core client code path used by every SDK consumer. API-surface and naming decisions like allow_input_required and the interim RuntimeError behavior are judgment calls a maintainer should sign off on, especially since the description notes follow-up work (get_prompt/read_resource widening, conformance un-waiving) builds on this shape.
Other factors
Both prior reviewer findings (mine on by_name=False, cubic's on the missing bool overload) were addressed in commits 78b1f9d and 68fbef9, and I verified the fixes are present in the current branch. New tests cover the InputRequiredResult passthrough, the params threading, the no-opt-in RuntimeError, and the session-group forwarding. Existing default-path callers keep the narrow CallToolResult return via the Literal[False] overload, so backward compatibility looks preserved.
This pull request is included in pre-release v2.0.0a3 |
Stacks on
s3-client-modern-path(#2950). Widens the client-sidecall_toolpath so anInputRequiredResultcan pass through and the caller can retry withinput_responses/request_state.Changes
ClientSession.send_request—result_typenow acceptsTypeAdapter[T]alongsidetype[T], so callers can parse a union result. All existing single-class callers are unchanged.ClientSession.call_tool(mechanics layer) — addsinput_responses=/request_state=retry kwargs and returnsCallToolResult | InputRequiredResult(matchingMONOLITH_RESULTS["tools/call"]). Output-schema validation is gated onisinstance(result, CallToolResult).Client.call_tool/ClientSessionGroup.call_tool(policy layer) —@overloadonallow_input_required: the default (False) still returnsCallToolResultso existing callers are unaffected;allow_input_required=Truereturns the union. With the default, anInputRequiredResultraisesRuntimeErrorwith a retry steer —# TODO(L80)marks where the auto-loop driver replaces this.Direct
ClientSession.call_toolcallers — examples and tests that bypassClientnow narrow withisinstance(result, CallToolResult).README.v2.mdis regenerated from the updated snippets.Not in this PR
get_prompt/read_resourcewidening — mechanically identical, follow separately.allow_input_required=Falsedefault will resolveInputRequiredResultautomatically once it lands).InputRequiredResultat-least-one-of validator — already onmainvia lowlevel Server: widen on_* return types for InputRequiredResult; add subscriptions/listen slot #2967; lands here on rebase.Conformance
sep-2322-client-request-statealready exists upstream at the pinned conformance build; it's currently waived inexpected-failures*.yml. Un-waiving + adding the@registerhandler in.github/actions/conformance/client.pyis the burndown step once this is onmain.Part of #2891.
AI Disclaimer