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: 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).
Copy file name to clipboardExpand all lines: docs/advanced/low-level-server.md
+12-6Lines changed: 12 additions & 6 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -31,18 +31,22 @@ Three things changed, and they are the whole low-level API:
31
31
32
32
### Try it
33
33
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:
35
35
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"
37
43
import asyncio
38
44
39
45
from mcp import Client
40
46
41
-
from server import server
42
-
43
47
44
48
asyncdefmain() -> None:
45
-
asyncwith Client(server) as client:
49
+
asyncwith Client("http://localhost:8000/mcp") as client:
46
50
result =await client.call_tool("search_books", {"query": "dune", "limit": 5})
47
51
print(result.content)
48
52
@@ -59,6 +63,8 @@ The same text the `@mcp.tool()` version produced. Two honest differences:
59
63
*`result.structured_content` is `None`. The high-level server wraps a `-> str` into `{"result": ...}` for you; here nobody builds what you didn't build.
60
64
*`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.
61
65
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
+
62
68
## Nothing is checked for you
63
69
64
70
`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
210
216
*`add_request_handler(method, params_type, handler)` serves any method. `initialize` is reserved.
211
217
* The capabilities a `Server` advertises are derived from which handlers you registered.
212
218
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)**.
Copy file name to clipboardExpand all lines: docs/advanced/pagination.md
+2-2Lines changed: 2 additions & 2 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -26,7 +26,7 @@ Pagination is for the server whose resource list is really a database: thousands
26
26
27
27
### Try it
28
28
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.
30
30
31
31
Call `list_resources()` with no arguments. You get ten resources, `book-1` through `book-10`, and `next_cursor` is the string `"10"`.
32
32
@@ -38,7 +38,7 @@ The tenth page comes back with `next_cursor` set to `None`. Done.
38
38
39
39
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`:
Copy file name to clipboardExpand all lines: docs/client/caching.md
+3-3Lines changed: 3 additions & 3 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -37,9 +37,9 @@ 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.
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.
41
41
42
-
```pythontitle="client.py" hl_lines="33 35 38"
42
+
```python hl_lines="33 35 38"
43
43
--8<--"docs_src/caching/tutorial003.py"
44
44
```
45
45
@@ -51,7 +51,7 @@ Four calls, three fetches. The second call found a fresh entry and never reached
51
51
52
52
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.
53
53
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.
55
55
56
56
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.
Copy file name to clipboardExpand all lines: docs/client/index.md
+21-9Lines changed: 21 additions & 9 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -6,27 +6,39 @@ It is one object with one lifecycle: construct it, enter `async with`, call meth
6
6
7
7
## Your first client
8
8
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"
10
12
--8<--"docs_src/client/tutorial001.py"
11
13
```
12
14
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:
14
20
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.
16
26
*`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.
17
27
* Inside the block the connection facts are already there as plain properties.
18
28
19
29
### What you can pass to `Client`
20
30
21
31
`Client` takes one positional argument and resolves the transport from its type:
22
32
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.
26
35
* 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.
27
37
28
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)**.
29
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
+
30
42
### What's on a connected client
31
43
32
44
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
197
209
198
210
## In tests
199
211
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.
201
213
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.
203
215
204
216
## Recap
205
217
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.
207
219
*`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.
208
220
*`list_tools()` gives you each tool's `name`, `title`, `description` and `input_schema`.
209
221
*`call_tool()` returns `content` for the model, `structured_content` for your code, and `is_error`. A raising tool is a result, not an exception.
Copy file name to clipboardExpand all lines: docs/client/oauth-clients.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
@@ -87,7 +87,7 @@ You wrote none of it. Two keyword arguments remain (`client_metadata_url` and `v
87
87
88
88
### Try it
89
89
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.
91
91
92
92
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.
0 commit comments