Skip to content

Commit 1d358db

Browse files
committed
docs: give every client example a served server and a real connection
The previous commit left most feature pages' snippets connecting to the server object in-process and explained that away with a sentence per page. That was a disclaimer, not a fix. This replaces it: each affected page now shows its server once as server.py with the command that serves it, and every client snippet is its own client.py that connects to http://localhost:8000/mcp. Where a client has logic worth testing (the pagination loop, the caching demo) it is a function the tests drive in-process against the server module, the pattern the subscriptions page already used. Pages: the client page (one Bookshop server, six clients), protocol versions (four clients against that same server), extensions and MCP Apps (server/client pairs), pagination and caching (uvicorn-served low-level servers, the caching handler prints each real fetch), serving legacy clients (both eras from one client program over HTTP), and the inline fragments on the troubleshooting page. The framing sentences are gone.
1 parent af72034 commit 1d358db

44 files changed

Lines changed: 655 additions & 500 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 & 8 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,19 +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. `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.
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.
6784

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

docs/advanced/extensions.md

Lines changed: 41 additions & 22 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,23 +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`, the way a test
73-
connects ([Testing](../get-started/testing.md)):
72+
Serve it over HTTP, and a client is the proof:
7473

75-
```python title="server.py" hl_lines="29-34"
76-
--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"
7780
```
7881

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+
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.
8284

8385
### Serving your own methods
8486

8587
An extension can register **new request methods**: its own verbs, served next to the
8688
spec's:
8789

88-
```python title="server.py" hl_lines="16-22 31 40-48"
90+
```python title="server.py" hl_lines="14-20 24 33-41"
8991
--8<-- "docs_src/extensions/tutorial004.py"
9092
```
9193

@@ -112,10 +114,10 @@ runtime:
112114

113115
### The client side
114116

115-
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:
116118

117-
```python title="server.py" hl_lines="54-58"
118-
--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"
119121
```
120122

121123
* `Client(..., extensions=[advertise(EXTENSION_ID)])` declares the extension. The
@@ -127,6 +129,9 @@ The same file's `main()` is the whole client story, both halves of it:
127129
* Vendor methods drop one layer to `client.session.send_request(...)`; `Client`
128130
only grows first-class methods for spec verbs. `send_request` accepts any
129131
`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.
130135

131136
### Intercepting `tools/call`
132137

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

162167
A **client extension** is the same contract from the consuming side: a bundle of
163-
client-side behaviour behind one identifier. Pass instances to
164-
`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:
165171

166-
```python hl_lines="66-68"
172+
```python title="server.py" hl_lines="22-25"
167173
--8<-- "docs_src/extensions/tutorial006.py"
168174
```
169175

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+
170182
`call_tool("buy", ...)` returns a plain `CallToolResult`, like every other call. What
171183
the extension changed: the server may now answer `buy` with a `receipt` **result
172184
shape** instead of a final result, and `Receipts` finishes it (here by redeeming the
@@ -185,16 +197,16 @@ the capability, the client does nothing, as in the search client above), use
185197
```python
186198
from mcp.client import advertise
187199

188-
client = Client("https://example.com/mcp", extensions=[advertise("com.example/search")])
200+
client = Client("http://localhost:8000/mcp", extensions=[advertise("com.example/search")])
189201
```
190202

191203
## Writing a client extension
192204

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

196-
```python hl_lines="17-18 43-44 46-47"
197-
--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"
198210
```
199211

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

233245
An extension's own request methods need no client-side registration. A vendor request
234246
type subclasses `mcp.types.Request` and goes through `client.session.send_request`,
235-
as in [Serving your own methods](#serving-your-own-methods). One addition: when a
236-
params key must ride the `Mcp-Name` header (extension specs such as tasks require
237-
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:
238249

239-
```python hl_lines="22-25 46-47"
250+
```python title="server.py" hl_lines="12-13 30"
240251
--8<-- "docs_src/extensions/tutorial007.py"
241252
```
242253

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+
243262
The session mirrors `params["jobId"]` into `Mcp-Name` on every send path, and a
244263
missing value fails loudly rather than silently omitting a required header.
245264

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

@@ -37,12 +37,26 @@ 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. 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.
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.
4141

42-
```python 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.

docs/client/index.md

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

77
## Your first client
88

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:
9+
A client needs a server to talk to. This Bookshop is the one every snippet on this page connects to. Save it as `server.py` and leave it running over HTTP:
1010

1111
```python title="server.py"
1212
--8<-- "docs_src/client/tutorial001.py"
@@ -16,7 +16,7 @@ A client needs a server to talk to. This small one will do. Save it as `server.p
1616
uv run mcp run server.py --transport streamable-http
1717
```
1818

19-
The client is its own program:
19+
That serves it at `http://localhost:8000/mcp`. The client is its own program. Save it as `client.py` and run `python client.py` in a second terminal:
2020

2121
```python title="client.py" hl_lines="7-11"
2222
--8<-- "docs_src/client/tutorial001_client.py"
@@ -37,8 +37,6 @@ The client is its own program:
3737

3838
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)**.
3939

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-
4240
### What's on a connected client
4341

4442
Four read-only properties, populated the moment you enter the block:
@@ -56,11 +54,11 @@ You never picked a protocol version. By default the `Client` probes the server a
5654

5755
## Listing tools
5856

59-
```python title="client.py" hl_lines="15-20"
57+
```python title="client.py" hl_lines="8-13"
6058
--8<-- "docs_src/client/tutorial002.py"
6159
```
6260

63-
`list_tools()` returns a `ListToolsResult`; the tools are in `.tools`. Each one is the complete definition a host would hand to a model:
61+
`list_tools()` returns a `ListToolsResult`; the tools are in `.tools`. Each one is the complete definition a host would hand to a model. Here is the first:
6462

6563
```python
6664
tool.name # 'search_books'
@@ -84,6 +82,8 @@ and `tool.input_schema` is the JSON Schema the server derived from the function'
8482

8583
That schema is everything a UI needs to render an argument form, and everything a model needs to produce valid arguments.
8684

85+
The second tool, `lookup_book`, was registered without a `title=`, so its `tool.title` is `None`.
86+
8787
!!! tip
8888
`title` is optional, so a UI showing tools to a human has to pick: the `title` if there is one,
8989
the `name` if not. `from mcp.shared.metadata_utils import get_display_name` does exactly that,
@@ -93,7 +93,7 @@ That schema is everything a UI needs to render an argument form, and everything
9393

9494
`call_tool(name, arguments)` runs the tool and gives you back a `CallToolResult`.
9595

96-
```python title="client.py" hl_lines="27-34"
96+
```python title="client.py" hl_lines="9-16"
9797
--8<-- "docs_src/client/tutorial003.py"
9898
```
9999

@@ -149,7 +149,7 @@ A tool that raises does **not** raise in your client. It comes back as an ordina
149149

150150
The resource verbs come in pairs: two ways to list, one way to read.
151151

152-
```python title="client.py" hl_lines="22-31"
152+
```python title="client.py" hl_lines="9-18"
153153
--8<-- "docs_src/client/tutorial004.py"
154154
```
155155

@@ -163,7 +163,7 @@ A client can also be told when a resource changes. On 2025-era connections that
163163

164164
## Prompts
165165

166-
```python title="client.py" hl_lines="15-20"
166+
```python title="client.py" hl_lines="8-13"
167167
--8<-- "docs_src/client/tutorial005.py"
168168
```
169169

@@ -188,7 +188,7 @@ A host hands those messages straight to the model. That is the whole feature.
188188

189189
A server with a completion handler can autocomplete prompt and resource-template arguments as the user types.
190190

191-
```python title="client.py" hl_lines="27-31"
191+
```python title="client.py" hl_lines="9-13"
192192
--8<-- "docs_src/client/tutorial006.py"
193193
```
194194

@@ -201,15 +201,15 @@ The answer is in `result.completion.values`. Type `"p"` and the server comes bac
201201

202202
Every `list_*` method takes a `cursor=` keyword and every result carries a `next_cursor`. When `next_cursor` is `None`, you have everything.
203203

204-
```python title="client.py" hl_lines="22-30"
204+
```python title="client.py" hl_lines="7-15"
205205
--8<-- "docs_src/client/tutorial007.py"
206206
```
207207

208-
This loop is correct against every server. `MCPServer` returns everything in one page, so `next_cursor` is `None` and the loop runs once, which is why most code never writes it. Servers that genuinely page, and the rules cursors obey, are in **[Pagination](../advanced/pagination.md)**.
208+
`list_all_tools` is correct against every server. `MCPServer` returns everything in one page, so `next_cursor` is `None` and the loop runs once, which is why most code never writes it. Servers that genuinely page, and the rules cursors obey, are in **[Pagination](../advanced/pagination.md)**.
209209

210210
## In tests
211211

212-
`Client(mcp)`, the form the snippets above use, is already a test harness for your server: no process, no port.
212+
Every `client.py` on this page reached `server.py` over HTTP. In a test you skip the network and hand `Client` the server object itself: `from server import mcp`, then `Client(mcp)`. No process, no port, and every method above works the same.
213213

214214
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.
215215

docs/client/transports.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ In a test there is nothing to deploy and nothing to launch. Pass the server obje
9393
--8<-- "docs_src/client_transports/tutorial001.py"
9494
```
9595

96-
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. **[Testing](../get-started/testing.md)** builds the whole pattern around it, and most snippets in these docs connect this way so they run as-is.
96+
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. **[Testing](../get-started/testing.md)** builds the whole pattern around it.
9797

9898
The same form doubles as an embedding API: an application that constructs the server itself can call its tools without a network hop.
9999

0 commit comments

Comments
 (0)