Let Client take StdioServerParameters directly

Merged
MA
2mo ago
+51-27

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