You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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.
Copy file name to clipboardExpand all lines: docs/advanced/pagination.md
+8-4Lines changed: 8 additions & 4 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -26,9 +26,13 @@ Pagination is for the server whose resource list is really a database: thousands
26
26
27
27
### Try it
28
28
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:
30
30
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"`.
32
36
33
37
Hand it back with `list_resources(cursor="10")` and the first resource is `book-11`, the new `next_cursor` is `"20"`.
34
38
@@ -38,15 +42,15 @@ The tenth page comes back with `next_cursor` set to `None`. Done.
38
42
39
43
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`:
40
44
41
-
```python hl_lines="26-32"
45
+
```pythontitle="client.py" hl_lines="9-15"
42
46
--8<--"docs_src/pagination/tutorial002.py"
43
47
```
44
48
45
49
*`cursor` starts as `None`, so the first request carries no cursor.
46
50
* Extend **before** you look at `next_cursor`: the last page has resources too.
47
51
*`next_cursor is None` is the exit. Anything else goes straight back into `cursor=`, untouched.
48
52
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.
50
54
51
55
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.
Copy file name to clipboardExpand all lines: docs/client/caching.md
+17-3Lines changed: 17 additions & 3 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -25,7 +25,7 @@ Out of the box every result says `ttlMs: 0, cacheScope: "private"`: immediately
25
25
26
26
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:
27
27
28
-
```python title="server.py" hl_lines="10 16"
28
+
```python title="server.py" hl_lines="11 17"
29
29
--8<--"docs_src/caching/tutorial002.py"
30
30
```
31
31
@@ -37,12 +37,26 @@ One caveat on paginated lists: the protocol requires the **same `cacheScope` on
37
37
38
38
## What the client sees
39
39
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.
41
41
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"
43
49
--8<--"docs_src/caching/tutorial003.py"
44
50
```
45
51
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
+
46
60
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`):
47
61
48
62
*`"use"` (the default) serves a fresh entry if there is one, and stores the fetch if not.
Copy file name to clipboardExpand all lines: docs/client/index.md
+13-13Lines changed: 13 additions & 13 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -6,7 +6,7 @@ It is one object with one lifecycle: construct it, enter `async with`, call meth
6
6
7
7
## Your first client
8
8
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:
10
10
11
11
```python title="server.py"
12
12
--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
16
16
uv run mcp run server.py --transport streamable-http
17
17
```
18
18
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:
20
20
21
21
```python title="client.py" hl_lines="7-11"
22
22
--8<--"docs_src/client/tutorial001_client.py"
@@ -37,8 +37,6 @@ The client is its own program:
37
37
38
38
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)**.
39
39
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
-
42
40
### What's on a connected client
43
41
44
42
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
56
54
57
55
## Listing tools
58
56
59
-
```python title="client.py" hl_lines="15-20"
57
+
```python title="client.py" hl_lines="8-13"
60
58
--8<--"docs_src/client/tutorial002.py"
61
59
```
62
60
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:
64
62
65
63
```python
66
64
tool.name # 'search_books'
@@ -84,6 +82,8 @@ and `tool.input_schema` is the JSON Schema the server derived from the function'
84
82
85
83
That schema is everything a UI needs to render an argument form, and everything a model needs to produce valid arguments.
86
84
85
+
The second tool, `lookup_book`, was registered without a `title=`, so its `tool.title` is `None`.
86
+
87
87
!!! tip
88
88
`title` is optional, so a UI showing tools to a human has to pick: the `title` if there is one,
89
89
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
93
93
94
94
`call_tool(name, arguments)` runs the tool and gives you back a `CallToolResult`.
95
95
96
-
```python title="client.py" hl_lines="27-34"
96
+
```python title="client.py" hl_lines="9-16"
97
97
--8<--"docs_src/client/tutorial003.py"
98
98
```
99
99
@@ -149,7 +149,7 @@ A tool that raises does **not** raise in your client. It comes back as an ordina
149
149
150
150
The resource verbs come in pairs: two ways to list, one way to read.
151
151
152
-
```python title="client.py" hl_lines="22-31"
152
+
```python title="client.py" hl_lines="9-18"
153
153
--8<--"docs_src/client/tutorial004.py"
154
154
```
155
155
@@ -163,7 +163,7 @@ A client can also be told when a resource changes. On 2025-era connections that
163
163
164
164
## Prompts
165
165
166
-
```python title="client.py" hl_lines="15-20"
166
+
```python title="client.py" hl_lines="8-13"
167
167
--8<--"docs_src/client/tutorial005.py"
168
168
```
169
169
@@ -188,7 +188,7 @@ A host hands those messages straight to the model. That is the whole feature.
188
188
189
189
A server with a completion handler can autocomplete prompt and resource-template arguments as the user types.
190
190
191
-
```python title="client.py" hl_lines="27-31"
191
+
```python title="client.py" hl_lines="9-13"
192
192
--8<--"docs_src/client/tutorial006.py"
193
193
```
194
194
@@ -201,15 +201,15 @@ The answer is in `result.completion.values`. Type `"p"` and the server comes bac
201
201
202
202
Every `list_*` method takes a `cursor=` keyword and every result carries a `next_cursor`. When `next_cursor` is `None`, you have everything.
203
203
204
-
```python title="client.py" hl_lines="22-30"
204
+
```python title="client.py" hl_lines="7-15"
205
205
--8<--"docs_src/client/tutorial007.py"
206
206
```
207
207
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)**.
209
209
210
210
## In tests
211
211
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.
213
213
214
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.
Copy file name to clipboardExpand all lines: docs/client/transports.md
+1-1Lines changed: 1 addition & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -93,7 +93,7 @@ In a test there is nothing to deploy and nothing to launch. Pass the server obje
93
93
--8<--"docs_src/client_transports/tutorial001.py"
94
94
```
95
95
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.
97
97
98
98
The same form doubles as an embedding API: an application that constructs the server itself can call its tools without a network hop.
0 commit comments