Skip to content

Let Client take StdioServerParameters directly - #3321

Open
maxisbey wants to merge 2 commits into
mainfrom
feat/client-accepts-stdio-params
Open

Let Client take StdioServerParameters directly#3321
maxisbey wants to merge 2 commits into
mainfrom
feat/client-accepts-stdio-params

Conversation

@maxisbey

Copy link
Copy Markdown
Contributor

Client resolves its transport from the type of its one positional 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 — you had to write Client(stdio_client(StdioServerParameters(...))), and the transports page had a paragraph explaining why the parameters object alone doesn't work. This adds the missing arm:

from mcp import Client, StdioServerParameters

server = StdioServerParameters(command="uv", args=["run", "server.py"])
async with Client(server) as client:
    ...

Client(stdio_client(server, errlog=...)) still works and is now documented as the way to redirect the child's stderr (the one knob stdio_client has 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 lands note 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?

  • New end-to-end test in tests/client/test_client.py that hands Client a StdioServerParameters for a one-line MCPServer child interpreter and asserts the negotiated server_info and a tools/list round trip. It uses a real subprocess because the process boundary is the behaviour; the fail_after is wider than the usual 5s for interpreter cold start + shutdown escalation, with the reason in a comment.
  • tests/docs_src/test_client_transports.py updated for the new tutorial004.py; the gated stories smoke test (MCP_EXAMPLES_SMOKE=1, real stdio) passes with the harness returning bare params.
  • Ran it by hand outside the test suite: a docs-shaped server.py/client.py pair with command="uv", args=["run", "server.py"], env={...} — tool call round-trips at 2026-07-28, env= reaches the child, child stderr lands on the terminal; a missing binary raises FileNotFoundError from async 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) exits OK.
  • ./scripts/test (full suite, coverage 100%, strict-no-cover) passes locally.

Breaking Changes

None. Additive: Client(stdio_client(...)) keeps working unchanged. Passing a bare StdioServerParameters previously failed at async with (it isn't an async context manager), so no working code changes meaning.

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update

Checklist

  • I have read the MCP Documentation
  • My code follows the repository's style guidelines
  • New and existing tests pass locally
  • I have added appropriate error handling
  • I have added or updated documentation as needed

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). The Client class and server field docstrings list the new arm. Translations under i18n/ regenerate from these and are not edited here.

AI Disclaimer

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.
@github-actions

github-actions Bot commented Aug 16, 2026

Copy link
Copy Markdown
Contributor

📚 Documentation preview

Preview https://pr-3321.mcp-python-docs.pages.dev
Deployment https://d2df28bb.mcp-python-docs.pages.dev
Commit f13b194
Triggered by @maxisbey
Updated 2026-08-16 17:10:04 UTC

No-Verification-Needed: doc and docstring wording only
@maxisbey
maxisbey marked this pull request as ready for review August 16, 2026 17:09

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues found across 9 files

Re-trigger cubic

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 None still 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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant