Skip to content

Commit 7bb486a

Browse files
authored
docs: stop presenting the in-memory client as the way to connect (#3443)
1 parent 0c91368 commit 7bb486a

67 files changed

Lines changed: 803 additions & 598 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

docs/advanced/apps.md

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ then come back.
2020

2121
## A clock with a face
2222

23-
```python title="server.py" hl_lines="19 22 30 32"
23+
```python title="server.py" hl_lines="17 20 28 30"
2424
--8<-- "docs_src/apps/tutorial001.py"
2525
```
2626

@@ -51,15 +51,36 @@ The model reads `content`; the iframe is for humans. A UI-capable host still fee
5151
the text result to the model, and a text-only client gets *only* that. So the
5252
canonical pattern is one tool, two answers. Look at `get_time` again:
5353

54-
```python title="server.py" hl_lines="23-27"
54+
```python title="server.py" hl_lines="21-25"
5555
--8<-- "docs_src/apps/tutorial001.py"
5656
```
5757

5858
`client_supports_apps(ctx)` is `True` only when the client declared the
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
61-
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.
61+
does not count. Here is the client half of the negotiation:
62+
63+
```python title="client.py" hl_lines="8 12"
64+
--8<-- "docs_src/apps/tutorial001_client.py"
65+
```
66+
67+
Serve `server.py` over HTTP, then run the client from a second terminal:
68+
69+
```console
70+
uv run mcp run server.py --transport streamable-http
71+
```
72+
73+
```console
74+
python client.py
75+
```
76+
77+
```text
78+
2026-06-26T12:00:00Z
79+
```
80+
81+
The rich answer came back. Drop `extensions=[APPS_SUPPORT]` from the `Client` call
82+
and the same program prints `The time is 2026-06-26T12:00:00Z.` instead, which is
83+
all a text-only client ever sees.
6384

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

docs/advanced/extensions.md

Lines changed: 42 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ specified by the MCP project itself.
5757

5858
The smallest useful extension is one tool and a settings map:
5959

60-
```python title="server.py" hl_lines="17 19-20 22-23 26"
60+
```python title="server.py" hl_lines="16 18-19 21-22 25"
6161
--8<-- "docs_src/extensions/tutorial003.py"
6262
```
6363

@@ -69,18 +69,25 @@ 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+
Serve it over HTTP, and a client is the proof:
7373

74-
```python title="server.py" hl_lines="29-34"
75-
--8<-- "docs_src/extensions/tutorial003.py"
74+
```console
75+
uv run mcp run server.py --transport streamable-http
76+
```
77+
78+
```python title="client.py" hl_lines="7-11"
79+
--8<-- "docs_src/extensions/tutorial003_client.py"
7680
```
7781

82+
Every `server.py` on this page is served with that command, and every `client.py`
83+
runs beside it with `python client.py` from a second terminal.
84+
7885
### Serving your own methods
7986

8087
An extension can register **new request methods**: its own verbs, served next to the
8188
spec's:
8289

83-
```python title="server.py" hl_lines="16-22 31 40-48"
90+
```python title="server.py" hl_lines="14-20 24 33-41"
8491
--8<-- "docs_src/extensions/tutorial004.py"
8592
```
8693

@@ -107,10 +114,10 @@ runtime:
107114

108115
### The client side
109116

110-
The same file's `main()` is the whole client story, both halves of it:
117+
The client is its own program, and it carries both halves of the client story:
111118

112-
```python title="server.py" hl_lines="54-58"
113-
--8<-- "docs_src/extensions/tutorial004.py"
119+
```python title="client.py" hl_lines="21-23 27-30"
120+
--8<-- "docs_src/extensions/tutorial004_client.py"
114121
```
115122

116123
* `Client(..., extensions=[advertise(EXTENSION_ID)])` declares the extension. The
@@ -122,6 +129,9 @@ The same file's `main()` is the whole client story, both halves of it:
122129
* Vendor methods drop one layer to `client.session.send_request(...)`; `Client`
123130
only grows first-class methods for spec verbs. `send_request` accepts any
124131
`Request` subclass, so the vendor request passes as-is.
132+
* `SearchRequest` and the two models it carries are the extension's wire contract,
133+
so the client declares them for itself. A published extension would ship them in
134+
a package that both sides import.
125135

126136
### Intercepting `tools/call`
127137

@@ -155,13 +165,20 @@ The hook wraps `tools/call` and nothing else. For every-message concerns, use
155165
## Using a client extension
156166

157167
A **client extension** is the same contract from the consuming side: a bundle of
158-
client-side behaviour behind one identifier. Pass instances to
159-
`Client(extensions=[...])` and call tools normally:
168+
client-side behaviour behind one identifier. The server here answers `buy` with a
169+
receipt to redeem instead of the goods, and only for a client that declared the
170+
extension:
160171

161-
```python title="client.py" hl_lines="66-68"
172+
```python title="server.py" hl_lines="22-25"
162173
--8<-- "docs_src/extensions/tutorial006.py"
163174
```
164175

176+
On the client, pass instances to `Client(extensions=[...])` and call tools normally:
177+
178+
```python title="client.py" hl_lines="33-35"
179+
--8<-- "docs_src/extensions/tutorial006_client.py"
180+
```
181+
165182
`call_tool("buy", ...)` returns a plain `CallToolResult`, like every other call. What
166183
the extension changed: the server may now answer `buy` with a `receipt` **result
167184
shape** instead of a final result, and `Receipts` finishes it (here by redeeming the
@@ -180,16 +197,16 @@ the capability, the client does nothing, as in the search client above), use
180197
```python
181198
from mcp.client import advertise
182199

183-
client = Client(mcp, extensions=[advertise("com.example/search")])
200+
client = Client("http://localhost:8000/mcp", extensions=[advertise("com.example/search")])
184201
```
185202

186203
## Writing a client extension
187204

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

191-
```python title="client.py" hl_lines="17-18 43-44 46-47"
192-
--8<-- "docs_src/extensions/tutorial006.py"
208+
```python title="client.py" hl_lines="16-17 25-26 28-29"
209+
--8<-- "docs_src/extensions/tutorial006_client.py"
193210
```
194211

195212
* The identifier follows the same grammar as the server's, validated when the class
@@ -227,14 +244,21 @@ claimed shape reaching a session-tier caller raises `UnexpectedClaimedResult`.
227244

228245
An extension's own request methods need no client-side registration. A vendor request
229246
type subclasses `mcp.types.Request` and goes through `client.session.send_request`,
230-
as in [Serving your own methods](#serving-your-own-methods). One addition: when a
231-
params key must ride the `Mcp-Name` header (extension specs such as tasks require
232-
this for their verbs), the request type declares `name_param`:
247+
as in [Serving your own methods](#serving-your-own-methods). Take a server whose
248+
extension serves one verb about a named job:
233249

234-
```python title="client.py" hl_lines="22-25 46-47"
250+
```python title="server.py" hl_lines="12-13 30"
235251
--8<-- "docs_src/extensions/tutorial007.py"
236252
```
237253

254+
One addition on the client: when a params key must ride the `Mcp-Name` header
255+
(extension specs such as tasks require this for their verbs), the request type
256+
declares `name_param`:
257+
258+
```python title="client.py" hl_lines="20-23 28-29"
259+
--8<-- "docs_src/extensions/tutorial007_client.py"
260+
```
261+
238262
The session mirrors `params["jobId"]` into `Mcp-Name` on every send path, and a
239263
missing value fails loudly rather than silently omitting a required header.
240264

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: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,13 @@ 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+
`mcp run` only accepts an `MCPServer`, so you serve this one yourself. The last line of `server.py` builds an ordinary ASGI app from the `Server`, and uvicorn runs that:
3030

31-
Call `list_resources()` with no arguments. You get ten resources, `book-1` through `book-10`, and `next_cursor` is the string `"10"`.
31+
```console
32+
uvicorn server:app --port 8000
33+
```
34+
35+
Point any client (**[The Client](../client/index.md)**, or the Inspector) at `http://localhost:8000/mcp` and call `list_resources()` with no arguments. You get ten resources, `book-1` through `book-10`, and `next_cursor` is the string `"10"`.
3236

3337
Hand it back with `list_resources(cursor="10")` and the first resource is `book-11`, the new `next_cursor` is `"20"`.
3438

@@ -38,15 +42,15 @@ The tenth page comes back with `next_cursor` set to `None`. Done.
3842

3943
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`:
4044

41-
```python title="client.py" hl_lines="26-32"
45+
```python title="client.py" hl_lines="9-15"
4246
--8<-- "docs_src/pagination/tutorial002.py"
4347
```
4448

4549
* `cursor` starts as `None`, so the first request carries no cursor.
4650
* Extend **before** you look at `next_cursor`: the last page has resources too.
4751
* `next_cursor is None` is the exit. Anything else goes straight back into `cursor=`, untouched.
4852

49-
Run its `main()` and it prints `100 resources`: ten pages of ten, stitched together by a loop that never knew there were ten pages.
53+
With uvicorn still serving `server.py`, run `python client.py` in a second terminal. It prints `100 resources`: ten pages of ten, stitched together by a loop that never knew there were ten pages.
5054

5155
This is the same loop **[The Client](../client/index.md)** shows for every `list_*` verb, and it costs nothing against a server that doesn't page: `next_cursor` is `None` on the first response and the loop runs once.
5256

docs/client/caching.md

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ Out of the box every result says `ttlMs: 0, cacheScope: "private"`: immediately
2525

2626
On the low-level `Server`, handlers build their results by hand, and `ttl_ms` / `cache_scope` are just fields on the result models. A handler that sets them explicitly always wins over the constructor map, field by field:
2727

28-
```python title="server.py" hl_lines="10 16"
28+
```python title="server.py" hl_lines="11 17"
2929
--8<-- "docs_src/caching/tutorial002.py"
3030
```
3131

@@ -39,10 +39,24 @@ One caveat on paginated lists: the protocol requires the **same `cacheScope` on
3939

4040
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.
4141

42-
```python title="client.py" hl_lines="33 35 38"
42+
To watch that happen, serve the `server.py` from the previous section with uvicorn (its last line builds the ASGI app). The handler prints a line every time it actually runs:
43+
44+
```console
45+
uvicorn server:app --port 8000
46+
```
47+
48+
```python title="client.py" hl_lines="20 23 28"
4349
--8<-- "docs_src/caching/tutorial003.py"
4450
```
4551

52+
Run `python client.py` from a second terminal. It prints the hints the first result carried, the handler's `ttlMs` next to the map's `cacheScope`:
53+
54+
```text
55+
1000 public
56+
```
57+
58+
The server's terminal tells the rest of the story: between uvicorn's request logs, `tools/list served` appears three times.
59+
4660
Four calls, three fetches. The second call found a fresh entry and never reached the server; advancing the (injected) clock past the TTL made the third fetch again; the fourth said `cache_mode="refresh"`. That kwarg exists on the five caching verbs (`list_tools`, `list_prompts`, `list_resources`, `list_resource_templates`, `read_resource`):
4761

4862
* `"use"` (the default) serves a fresh entry if there is one, and stores the fetch if not.
@@ -51,7 +65,7 @@ Four calls, three fetches. The second call found a fresh entry and never reached
5165

5266
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.
5367

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.
68+
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.
5569

5670
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.
5771

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.

0 commit comments

Comments
 (0)