Let Client take StdioServerParameters directly - #3321
Conversation
Client picks its transport from the type of its argument: a server object connects in-process, a str is a Streamable HTTP URL, and anything else is entered as a Transport. stdio was the odd one out, needing Client(stdio_client(StdioServerParameters(...))). Add the missing arm so Client(StdioServerParameters(...)) launches the command via stdio_client; wrapping it yourself remains the way to redirect the child's stderr. Docs: the transports page's stdio section and recap, the "what you can pass" list, and the two other places that enumerate the connection forms. The stories harness drops the TODO that anticipated this.
📚 Documentation preview
|
No-Verification-Needed: doc and docstring wording only
There was a problem hiding this comment.
Additional findings (outside current diff — PR may have been updated during review):
-
🟡
src/mcp/client/client.py— [quality] nit: stale enumerations of non-URL Client targets after adding the StdioServerParameters arm — the cache ValueError message still says only "in-process servers and Transport instances get a random per-client identity", and docs/client/caching.md line 80 has the same two-item list [also at: docs/client/index.md:205 - [quality] Stale three-form Recap bullet on the very page the diff updates to "four" connection forms: docs/client/inde]Extended reasoning...
A user who writes Client(StdioServerParameters(...), cache=CacheConfig(store=my_store)) gets a ValueError whose message enumerates only in-process servers and Transport instances, and the caching docs (docs/client/caching.md:80) likewise omit stdio params from the "no URL means random identity" explanation — so the new fourth connection form's cache-identity behavior (random per-client identity, target_id required for shared stores) is undocumented and the error text doesn't cover the case that triggered it. Concrete cost: misleading error/docs for the newly added public API surface; fix is updating both enumerations in the same PR that adds the arm.
Verification: nit — src/mcp/client/client.py:413-417: the cache ValueError raised when
target_id is None and config.store is not Nonestill reads "in-process servers and Transport instances get a random per-client identity", yet the new arm at lines 400-401 (elif isinstance(srv, StdioServerParameters): self._connect = _connect_transport(stdio_client(srv))) routes stdio-params clients into this same non-UR
Clientresolves its transport from the type of its one positional argument: a server object connects in-process, astris a Streamable HTTP URL, and anything else is entered as aTransport. stdio was the odd one out — you had to writeClient(stdio_client(StdioServerParameters(...))), and the transports page had a paragraph explaining why the parameters object alone doesn't work. This adds the missing arm:Client(stdio_client(server, errlog=...))still works and is now documented as the way to redirect the child's stderr (the one knobstdio_clienthas beyond the params).Motivation and Context
Connecting to a local server over stdio is one of the two normal ways to use a client, and it was the only one that needed a wrapper and a second import. The examples harness already carried a
# becomes Client(params) once that overload landsnote for exactly this.Deliberately not in scope: accepting
SseServerParameters/StreamableHttpParameters(the URL string already covers HTTP; SSE is legacy), and any string/path inference.How Has This Been Tested?
tests/client/test_client.pythat handsClientaStdioServerParametersfor a one-lineMCPServerchild interpreter and asserts the negotiatedserver_infoand atools/listround trip. It uses a real subprocess because the process boundary is the behaviour; thefail_afteris wider than the usual 5s for interpreter cold start + shutdown escalation, with the reason in a comment.tests/docs_src/test_client_transports.pyupdated for the newtutorial004.py; the gated stories smoke test (MCP_EXAMPLES_SMOKE=1, real stdio) passes with the harness returning bare params.server.py/client.pypair withcommand="uv", args=["run", "server.py"], env={...}— tool call round-trips at2026-07-28,env=reaches the child, child stderr lands on the terminal; a missing binary raisesFileNotFoundErrorfromasync with; the same params object serves two sequential clients;Client(stdio_client(server, errlog=f))sends stderr to the file.python -m stories.tools.client(stdio) exitsOK../scripts/test(full suite, coverage 100%, strict-no-cover) passes locally.Breaking Changes
None. Additive:
Client(stdio_client(...))keeps working unchanged. Passing a bareStdioServerParameterspreviously failed atasync with(it isn't an async context manager), so no working code changes meaning.Types of changes
Checklist
Additional context
Docs touched:
client/transports.md(stdio section, resolution rule, recap),client/index.md("What you can pass"), and the two other sentences that enumerate the connection forms (whats-new.md,get-started/real-host.md). TheClientclass andserverfield docstrings list the new arm. Translations underi18n/regenerate from these and are not edited here.AI Disclaimer