Files
modelcontextprotocol--pytho…/docs/client/transports.md
T
wehub-resource-sync 49b9bb6724
Deploy Docs / deploy-docs (push) Failing after 1s
Conformance Tests / client-conformance (push) Failing after 3s
Conformance Tests / server-conformance (push) Failing after 1s
GitHub Actions Security Analysis / zizmor (push) Failing after 1s
CI / checks (push) Failing after 59m20s
CI / all-green (push) Waiting to run
chore: import upstream snapshot with attribution
2026-07-13 12:10:27 +08:00

116 lines
6.3 KiB
Markdown

# Client transports
Every `Client` talks to its server over a **transport**: the thing that actually carries the messages.
You never configure one separately. `Client` takes a single positional argument and works the transport out from its type.
The *server* side of each (what `mcp.run()` does and what you deploy) is **[Running your server](../run/index.md)**.
## In memory
Pass the server object itself:
```python title="client.py" hl_lines="14"
--8<-- "docs_src/client_transports/tutorial001.py"
```
No subprocess, no port, no bytes on a wire. The client and the server are two objects in the same process, and the call still goes through the real protocol layer: `search_books` is listed, validated and invoked exactly as it would be over HTTP.
That makes it two things at once:
* **A test harness.** Every example in this documentation is exercised this way, and the **[Testing](../get-started/testing.md)** page builds the whole pattern around it.
* **An embedding API.** An application that constructs the server doesn't need a network hop to call its tools.
## Streamable HTTP
Pass a URL string and you get **Streamable HTTP**, the transport you deploy behind:
```python title="client.py" hl_lines="5"
--8<-- "docs_src/client_transports/tutorial002.py"
```
That is the whole production client. `Client` wraps the URL in `streamable_http_client(...)` for you, on top of an `httpx.AsyncClient` configured the way MCP needs: `follow_redirects=True`, a 30-second timeout for connect/write/pool, and a 300-second read timeout because the server may hold a response stream open.
!!! check
A `Client` you have constructed is **not** connected. Construction only picks the transport;
`async with` is what opens it. Reach for the connection before entering and the SDK tells you so:
```text
RuntimeError: Client must be used within an async context manager
```
Nothing was resolved, fetched or spawned when you wrote `Client("http://...")`. That line is free.
### Bring your own `httpx.AsyncClient`
The moment you need an `Authorization` header, a cookie, a proxy, mTLS, or a different timeout, build the `httpx.AsyncClient` yourself and hand it to `streamable_http_client`:
```python title="client.py" hl_lines="8-14"
--8<-- "docs_src/client_transports/tutorial003.py"
```
Two things to notice:
* You own the `httpx.AsyncClient`, so **you** enter and exit it. The SDK never closes a client it didn't create.
* `streamable_http_client(url, http_client=...)` returns a transport, and `Client(transport)` accepts it like anything else.
!!! warning
`streamable_http_client` used to take `headers=` and `timeout=` directly. It does not any more:
its only parameters are `url`, `http_client` and `terminate_on_close`. Reach for `headers=` out
of habit and you get:
```text
TypeError: streamable_http_client() got an unexpected keyword argument 'headers'
```
Everything HTTP-shaped now lives on the one `httpx.AsyncClient` you pass in.
!!! info
If you know `httpx`, you already know how to do auth, proxies, event hooks, retries and connection
limits here. The SDK adds nothing on top and takes nothing away. It is also where OAuth plugs in:
`httpx.AsyncClient(auth=OAuthClientProvider(...))`. That whole flow is **[OAuth clients](oauth-clients.md)**.
## stdio
A **stdio** server is a subprocess. The client launches it, writes JSON-RPC to its stdin and reads JSON-RPC from its stdout. It is how a desktop host runs a server on your machine: a host *is* this code plus a UI, and **[Connect to a real host](../get-started/real-host.md)** is the same relationship seen from the host's side, as a config file.
Describe the process with `StdioServerParameters`, turn it into a transport with `stdio_client`, and hand *that* to `Client`:
```python title="client.py" hl_lines="4-8 12"
--8<-- "docs_src/client_transports/tutorial004.py"
```
`Client` does not accept the parameters object on its own. `StdioServerParameters` is configuration; `stdio_client(server)` is the transport that knows how to spawn a process from it. Always wrap.
Leaving the `async with` block also shuts the subprocess down: close stdin, wait, kill if it lingers. You never clean it up yourself.
!!! warning
The child does **not** inherit your environment. It gets a minimal allow-list (`HOME`, `LOGNAME`,
`PATH`, `SHELL`, `TERM` and `USER` on POSIX) so nothing sensitive leaks into a process you may
not have written.
A server that needs an API key won't find it there. Pass it explicitly with `env=`; those
variables are merged on top of the allow-list. That is what `BOOKSHOP_API_KEY` is doing above.
## SSE
`sse_client(url)`, from `mcp.client.sse`, is the HTTP transport that Streamable HTTP superseded. Wrap it the same way, `Client(sse_client("http://localhost:8000/sse"))`, to talk to a server that still speaks it, and don't build anything new on it.
## The `Transport` protocol
To `Client`, all of the above are the same thing.
A **transport** is any async context manager that yields a `(read, write)` pair of message streams: formally, the `Transport` protocol in `mcp.client`. `Client` resolves its argument by type: a server object connects in-process, a `str` becomes `streamable_http_client(url)`, and anything else is entered as a transport directly. That last rule is why `stdio_client(...)`, `streamable_http_client(...)` and `sse_client(...)` all drop into the same slot, and why you can write your own.
## Recap
* `Client(mcp)` (the server object) connects in memory. Use it for tests and for embedding.
* `Client("http://.../mcp")` (a URL) connects over Streamable HTTP, the production transport.
* Headers, auth, proxies and timeouts belong on an `httpx.AsyncClient` you pass to `streamable_http_client(url, http_client=...)`. There is no `headers=` keyword.
* stdio is `Client(stdio_client(StdioServerParameters(...)))`, never the parameters object alone.
* The subprocess gets an allow-listed environment, not yours; `env=` adds to it.
* A transport is anything you can `async with x as (read, write)`. `Client` hands anything that isn't a server object or a URL straight to that protocol.
* Constructing a `Client` picks the transport. `async with` opens it.
Once the transport is open the two sides have to agree on a protocol version. You normally never think about it; when you do, **[Protocol versions](../protocol-versions.md)** is the page.