Skip to content

Commit 52230cc

Browse files
committed
docs: stop presenting the in-memory client as the way to connect
Client(server_object) connects in-process, which is a testing technique, but much of the documentation led with it: the client page's first example, the transports page's first section, several "what Client accepts" lists, and a number of feature pages whose snippets connect that way so they can run as-is. This reframes all of that around URL and stdio as the normal ways to connect. The client page now starts a small server over HTTP and connects to it by URL, then says once that the remaining snippets build their server inline the way a test would. The transports page leads with Streamable HTTP and stdio and moves the in-memory section down, scoped to tests and embedding. Enumerations put the server object last, "in tests". Feature pages whose snippets connect in-process get one sentence saying so instead of a rewrite, and fences that define a server are no longer titled client.py. The progress page, the prior_discover example and the low-level Try-it now use real connections, since their point depends on one. A few stale statements found along the way are corrected (the callbacks page's "first argument is a transport object", the testing page's "In-process by default" heading, the Client docstring example).
1 parent 979208c commit 52230cc

36 files changed

Lines changed: 171 additions & 137 deletions

docs/advanced/apps.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,11 @@ canonical pattern is one tool, two answers. Look at `get_time` again:
5959
`io.modelcontextprotocol/ui` extension **and** listed `text/html;profile=mcp-app`
6060
in its `mimeTypes` settings. The field is required, so a client that omits it
6161
does not count. That is exactly what `main()` in the same file declares: the
62-
client half of the negotiation, and the rich answer comes back.
62+
client half of the negotiation, and the rich answer comes back. `main()` hands
63+
`Client` the `mcp` object so the file runs as-is, the way a test does
64+
([Testing](../get-started/testing.md)). In a real client that argument is a URL or
65+
`StdioServerParameters`, and the `extensions=[...]` declaration stays exactly the
66+
same.
6367

6468
!!! warning
6569
Never return a placeholder like `"[Rendered UI]"` as the only content. If the

docs/advanced/extensions.md

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -69,12 +69,17 @@ The smallest useful extension is one tool and a settings map:
6969
* The extension never receives the server. It declares contributions as data;
7070
`MCPServer` consumes them. There is no `self.server` to mutate.
7171

72-
And `main()` is the proof, an in-memory client straight against `mcp`:
72+
And `main()` is the proof, an in-memory client straight against `mcp`, the way a test
73+
connects ([Testing](../get-started/testing.md)):
7374

7475
```python title="server.py" hl_lines="29-34"
7576
--8<-- "docs_src/extensions/tutorial003.py"
7677
```
7778

79+
Every `main()` on this page connects that way, so each file runs as-is. In your own
80+
program the first argument to `Client` is a URL or `StdioServerParameters` and nothing
81+
else changes.
82+
7883
### Serving your own methods
7984

8085
An extension can register **new request methods**: its own verbs, served next to the
@@ -158,7 +163,7 @@ A **client extension** is the same contract from the consuming side: a bundle of
158163
client-side behaviour behind one identifier. Pass instances to
159164
`Client(extensions=[...])` and call tools normally:
160165

161-
```python title="client.py" hl_lines="66-68"
166+
```python hl_lines="66-68"
162167
--8<-- "docs_src/extensions/tutorial006.py"
163168
```
164169

@@ -180,15 +185,15 @@ the capability, the client does nothing, as in the search client above), use
180185
```python
181186
from mcp.client import advertise
182187

183-
client = Client(mcp, extensions=[advertise("com.example/search")])
188+
client = Client("https://example.com/mcp", extensions=[advertise("com.example/search")])
184189
```
185190

186191
## Writing a client extension
187192

188193
Subclass `ClientExtension` and override only what you need. Three contribution
189194
kinds, each with a default: `settings()`, `claims()`, and `notifications()`.
190195

191-
```python title="client.py" hl_lines="17-18 43-44 46-47"
196+
```python hl_lines="17-18 43-44 46-47"
192197
--8<-- "docs_src/extensions/tutorial006.py"
193198
```
194199

@@ -231,7 +236,7 @@ as in [Serving your own methods](#serving-your-own-methods). One addition: when
231236
params key must ride the `Mcp-Name` header (extension specs such as tasks require
232237
this for their verbs), the request type declares `name_param`:
233238

234-
```python title="client.py" hl_lines="22-25 46-47"
239+
```python hl_lines="22-25 46-47"
235240
--8<-- "docs_src/extensions/tutorial007.py"
236241
```
237242

docs/advanced/low-level-server.md

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,18 +31,22 @@ Three things changed, and they are the whole low-level API:
3131

3232
### Try it
3333

34-
There is no Inspector for this one: `mcp dev` and `mcp run` only accept an `MCPServer`. The in-memory `Client` doesn't care; it takes a low-level `Server` exactly like it takes an `MCPServer`:
34+
`mcp dev` and `mcp run` only accept an `MCPServer`, so you serve this one yourself. The last line of `server.py` builds an ordinary ASGI app from it, and uvicorn runs that:
3535

36-
```python title="main.py"
36+
```console
37+
uvicorn server:app --port 8000
38+
```
39+
40+
Point the Inspector, or any client, at `http://localhost:8000/mcp`:
41+
42+
```python title="client.py"
3743
import asyncio
3844

3945
from mcp import Client
4046

41-
from server import server
42-
4347

4448
async def main() -> None:
45-
async with Client(server) as client:
49+
async with Client("http://localhost:8000/mcp") as client:
4650
result = await client.call_tool("search_books", {"query": "dune", "limit": 5})
4751
print(result.content)
4852

@@ -59,6 +63,8 @@ The same text the `@mcp.tool()` version produced. Two honest differences:
5963
* `result.structured_content` is `None`. The high-level server wraps a `-> str` into `{"result": ...}` for you; here nobody builds what you didn't build.
6064
* `list_tools` returns the schema **you** typed, character for character. The high-level version had `"title": "Query"` on every property and a `"title": "search_booksArguments"` at the root: Pydantic artifacts. Down here, if it's on the wire, you put it there.
6165

66+
In a test you skip uvicorn and the port: `Client(server)` takes a low-level `Server` in-process exactly like it takes an `MCPServer`, and **[Testing](../get-started/testing.md)** is that pattern.
67+
6268
## Nothing is checked for you
6369

6470
`MCPServer` rejects a bad argument before your function ever runs, validating the call against the schema it generated (**[Tools](../servers/tools.md)**).
@@ -210,4 +216,4 @@ Each of these is one idea you now have the vocabulary for; each has its own page
210216
* `add_request_handler(method, params_type, handler)` serves any method. `initialize` is reserved.
211217
* The capabilities a `Server` advertises are derived from which handlers you registered.
212218

213-
`Client(server)` treated both servers identically because they *are* the same protocol, which is the whole point. The next layer down isn't a class at all: it's **[Middleware](middleware.md)**.
219+
The client treated both servers identically because they *are* the same protocol, which is the whole point. The next layer down isn't a class at all: it's **[Middleware](middleware.md)**.

docs/advanced/pagination.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ Pagination is for the server whose resource list is really a database: thousands
2626

2727
### Try it
2828

29-
`Client(server)` connects to a low-level `Server` in memory exactly as it connects to an `MCPServer`.
29+
In a test, `Client(server)` connects to a low-level `Server` in memory exactly as it connects to an `MCPServer` ([Testing](../get-started/testing.md)), and that is how the client loop below runs. In your own program you hand `Client` a URL or `StdioServerParameters` instead, and every call reads the same.
3030

3131
Call `list_resources()` with no arguments. You get ten resources, `book-1` through `book-10`, and `next_cursor` is the string `"10"`.
3232

@@ -38,7 +38,7 @@ The tenth page comes back with `next_cursor` set to `None`. Done.
3838

3939
Every `list_*` method on `Client` (`list_tools`, `list_resources`, `list_resource_templates`, `list_prompts`) takes a `cursor=` keyword. Draining a paged list is one `while True`:
4040

41-
```python title="client.py" hl_lines="26-32"
41+
```python hl_lines="26-32"
4242
--8<-- "docs_src/pagination/tutorial002.py"
4343
```
4444

docs/client/caching.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,9 @@ One caveat on paginated lists: the protocol requires the **same `cacheScope` on
3737

3838
## What the client sees
3939

40-
On a 2026-07-28 session, `Client` honors the hints for you: it has a built-in response cache, on by default. A result that arrives carrying a `ttlMs` is stored, and an identical call within that TTL is served from the cache with no round trip. A result that carries *no* hint is not cached: hint-less results get `CacheConfig.default_ttl_ms`, which defaults to `0` (immediately stale), so a server that declares nothing sees exactly the call-for-call traffic it always did.
40+
On a 2026-07-28 session, `Client` honors the hints for you: it has a built-in response cache, on by default. A result that arrives carrying a `ttlMs` is stored, and an identical call within that TTL is served from the cache with no round trip. A result that carries *no* hint is not cached: hint-less results get `CacheConfig.default_ttl_ms`, which defaults to `0` (immediately stale), so a server that declares nothing sees exactly the call-for-call traffic it always did. The demo below hands `Client` the server object and an injected clock so it runs as-is, the way a test does ([Testing](../get-started/testing.md)). In your own program that first argument is a URL or `StdioServerParameters`, and the calls read the same.
4141

42-
```python title="client.py" hl_lines="33 35 38"
42+
```python hl_lines="33 35 38"
4343
--8<-- "docs_src/caching/tutorial003.py"
4444
```
4545

@@ -51,7 +51,7 @@ Four calls, three fetches. The second call found a fresh entry and never reached
5151

5252
One rule sits above `"use"`: **calls carrying `meta` always reach the server.** A request with `meta` set (a progress token, tracing fields) expects a wire request, so under `cache_mode="use"` it is treated as `"refresh"`: the cache read is skipped, and the fetched result still replaces the cached entry. `"bypass"` and an explicit `"refresh"` behave as they always do.
5353

54-
To turn caching off entirely, construct with `Client(server, cache=None)`: every call is a round trip again, and `cache_mode`, while still accepted, does nothing.
54+
To turn caching off entirely, pass `cache=None` when constructing the `Client`: every call is a round trip again, and `cache_mode`, while still accepted, does nothing.
5555

5656
Scope is honored automatically too: `"private"` entries are keyed to the cache's *partition* (below), while `"public"` ones may opt into wider sharing. And **notifications beat TTL** for the exact entries they name: a `list_changed` notification evicts the matching cached listing, and `resources/updated` evicts the cached read stored under exactly its URI, however fresh they were. On a 2026-07-28 connection those notifications arrive on a `subscriptions/listen` stream you open with `client.listen(...)`, and eviction completes before your watcher sees the event; **[Subscriptions](subscriptions.md)** is that page.
5757

docs/client/callbacks.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ One `tools/call` from you, one `elicitation/create` back from the server, answer
5858
`mode="legacy"` on the `Client(...)` call is doing real work. By default `Client(...)` negotiates the modern
5959
protocol path, and that path has no back-channel for server-to-client requests: `ctx.elicit`
6060
fails before your callback ever runs. The transport doesn't decide that; the negotiated
61-
protocol does, in-memory and over a URL alike. Pin `mode="legacy"` whenever your client has
61+
protocol does. Pin `mode="legacy"` whenever your client has
6262
to answer one; every test behind this page does. **[Protocol versions](../protocol-versions.md)** has the whole story.
6363

6464
On a 2026-07-28 session the callback isn't dead, it's fed differently: when a tool returns an
@@ -146,4 +146,4 @@ Two more. Neither declares anything.
146146
* `sampling_callback` and `list_roots_callback` work the same way but serve deprecated features; modern servers use multi-round-trip requests instead.
147147
* `logging_callback` and `message_handler` receive notifications. They declare nothing.
148148

149-
The first argument to `Client(...)` is a transport object. **[Client transports](transports.md)** covers every kind.
149+
The first argument to `Client(...)` picks the transport. **[Client transports](transports.md)** covers every kind.

docs/client/index.md

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,27 +6,39 @@ It is one object with one lifecycle: construct it, enter `async with`, call meth
66

77
## Your first client
88

9-
```python title="client.py" hl_lines="14-18"
9+
A client needs a server to talk to. This small one will do. Save it as `server.py` and leave it running over HTTP:
10+
11+
```python title="server.py"
1012
--8<-- "docs_src/client/tutorial001.py"
1113
```
1214

13-
The server at the top is only there so you have something to connect to. The client is the five highlighted lines.
15+
```console
16+
uv run mcp run server.py --transport streamable-http
17+
```
18+
19+
The client is its own program:
1420

15-
* `Client(mcp)` is given the **server object itself**. That is the in-memory transport: no subprocess, no port, no HTTP. It is how every example on this page, and every test you write, connects.
21+
```python title="client.py" hl_lines="7-11"
22+
--8<-- "docs_src/client/tutorial001_client.py"
23+
```
24+
25+
* `Client("http://localhost:8000/mcp")` is given a **URL**, so it connects over Streamable HTTP to the server you just started.
1626
* `async with` is the **lifecycle**. Entering it connects and negotiates; leaving it disconnects. There is no `connect()` / `close()` pair, and a `Client` cannot be reused after the block ends.
1727
* Inside the block the connection facts are already there as plain properties.
1828

1929
### What you can pass to `Client`
2030

2131
`Client` takes one positional argument and resolves the transport from its type:
2232

23-
* An `MCPServer` (or low-level `Server`) instance: connected **in-process**.
24-
* A URL string (`Client("http://localhost:8000/mcp")`): Streamable HTTP, the production path.
25-
* A `StdioServerParameters`: the command to launch as a **subprocess**, spoken to over its stdin and stdout.
33+
* A URL string (`Client("http://localhost:8000/mcp")`): Streamable HTTP, the transport you deploy behind.
34+
* A `StdioServerParameters`: the command to launch as a local **subprocess**, spoken to over its stdin and stdout.
2635
* A **transport**: anything you can `async with ... as (read, write)`, such as `streamable_http_client(url, http_client=...)` around your own HTTP client.
36+
* An `MCPServer` (or low-level `Server`) instance: connected **in-process**, with no subprocess and no port. That one is for tests, and **[Testing](../get-started/testing.md)** builds on it.
2737

2838
Everything else on this page is identical across all four. Headers, subprocesses, timeouts, and the `Transport` protocol get their own page: **[Client transports](transports.md)**.
2939

40+
The snippets below use the last form so that each one runs as-is: it builds its Bookshop server inline and hands it to `Client`, the way a test would. In your own program that argument is the URL or `StdioServerParameters` above.
41+
3042
### What's on a connected client
3143

3244
Four read-only properties, populated the moment you enter the block:
@@ -197,13 +209,13 @@ This loop is correct against every server. `MCPServer` returns everything in one
197209

198210
## In tests
199211

200-
`Client(mcp)` with no process and no port is already a test harness for your server.
212+
`Client(mcp)`, the form the snippets above use, is already a test harness for your server: no process, no port.
201213

202-
There is one constructor flag built for that: `Client(mcp, raise_exceptions=True)`. It only has an effect on in-memory connections, and **[Testing](../get-started/testing.md)** is the page that explains it and builds the whole pattern around it.
214+
There is one constructor flag built for that: `Client(mcp, raise_exceptions=True)`. It only has an effect on in-process connections, and **[Testing](../get-started/testing.md)** is the page that explains it and builds the whole pattern around it.
203215

204216
## Recap
205217

206-
* `Client(x)` connects in-memory to a server object, over Streamable HTTP to a URL string, and over anything else via a transport.
218+
* `Client(x)` connects over Streamable HTTP to a URL string, launches a subprocess for a `StdioServerParameters`, enters a transport directly, and in tests takes the server object itself.
207219
* `async with` is the whole lifecycle. Inside it, `server_capabilities` and `protocol_version` are already populated; `server_info` and `instructions` are too when the server provides them.
208220
* `list_tools()` gives you each tool's `name`, `title`, `description` and `input_schema`.
209221
* `call_tool()` returns `content` for the model, `structured_content` for your code, and `is_error`. A raising tool is a result, not an exception.

docs/client/oauth-clients.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ You wrote none of it. Two keyword arguments remain (`client_metadata_url` and `v
8787

8888
### Try it
8989

90-
Most examples in these docs you can check with an in-memory `Client(server)`. Not this: the whole point of the flow is an HTTP `401`, and there is no HTTP between an in-memory client and its server.
90+
The in-memory `Client(server)` your tests use is no help here: the whole point of the flow is an HTTP `401`, and there is no HTTP between an in-memory client and its server.
9191

9292
The repository ships the live version. `examples/servers/simple-auth/` runs a standalone authorization server and a protected MCP server; `examples/clients/simple-auth-client/` is this page's client grown into a small CLI. Its README has the two commands: start the servers, run the client against them, and you watch the four steps go by.
9393

docs/client/session-groups.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ If you already hold a connected `ClientSession` (`Client.session` is one), hand
7373
## Recap
7474

7575
* `ClientSessionGroup` holds many server connections and merges their tools, resources, and prompts into one `dict` each.
76-
* `connect_to_server(params)` per server. It takes transport parameters, never the server object or URL a `Client` takes.
76+
* `connect_to_server(params)` per server. It takes transport parameters, never the URL or `Transport` a `Client` takes.
7777
* `group.call_tool(name, arguments)` routes to the owning server for you.
7878
* Names must be unique across the whole group; two servers with a `search` tool cannot coexist on their own.
7979
* `component_name_hook=` rewrites every registered name. The dict key changes, the wire name does not.

0 commit comments

Comments
 (0)