docs: stop presenting the in-memory client as the way to connect (#3443)
This commit is contained in:
parent
0c9136841f
commit
7bb486a10f
67 changed files with 803 additions and 598 deletions
|
|
@ -20,7 +20,7 @@ then come back.
|
||||||
|
|
||||||
## A clock with a face
|
## A clock with a face
|
||||||
|
|
||||||
```python title="server.py" hl_lines="19 22 30 32"
|
```python title="server.py" hl_lines="17 20 28 30"
|
||||||
--8<-- "docs_src/apps/tutorial001.py"
|
--8<-- "docs_src/apps/tutorial001.py"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -51,15 +51,36 @@ The model reads `content`; the iframe is for humans. A UI-capable host still fee
|
||||||
the text result to the model, and a text-only client gets *only* that. So the
|
the text result to the model, and a text-only client gets *only* that. So the
|
||||||
canonical pattern is one tool, two answers. Look at `get_time` again:
|
canonical pattern is one tool, two answers. Look at `get_time` again:
|
||||||
|
|
||||||
```python title="server.py" hl_lines="23-27"
|
```python title="server.py" hl_lines="21-25"
|
||||||
--8<-- "docs_src/apps/tutorial001.py"
|
--8<-- "docs_src/apps/tutorial001.py"
|
||||||
```
|
```
|
||||||
|
|
||||||
`client_supports_apps(ctx)` is `True` only when the client declared the
|
`client_supports_apps(ctx)` is `True` only when the client declared the
|
||||||
`io.modelcontextprotocol/ui` extension **and** listed `text/html;profile=mcp-app`
|
`io.modelcontextprotocol/ui` extension **and** listed `text/html;profile=mcp-app`
|
||||||
in its `mimeTypes` settings. The field is required, so a client that omits it
|
in its `mimeTypes` settings. The field is required, so a client that omits it
|
||||||
does not count. That is exactly what `main()` in the same file declares: the
|
does not count. Here is the client half of the negotiation:
|
||||||
client half of the negotiation, and the rich answer comes back.
|
|
||||||
|
```python title="client.py" hl_lines="8 12"
|
||||||
|
--8<-- "docs_src/apps/tutorial001_client.py"
|
||||||
|
```
|
||||||
|
|
||||||
|
Serve `server.py` over HTTP, then run the client from a second terminal:
|
||||||
|
|
||||||
|
```console
|
||||||
|
uv run mcp run server.py --transport streamable-http
|
||||||
|
```
|
||||||
|
|
||||||
|
```console
|
||||||
|
python client.py
|
||||||
|
```
|
||||||
|
|
||||||
|
```text
|
||||||
|
2026-06-26T12:00:00Z
|
||||||
|
```
|
||||||
|
|
||||||
|
The rich answer came back. Drop `extensions=[APPS_SUPPORT]` from the `Client` call
|
||||||
|
and the same program prints `The time is 2026-06-26T12:00:00Z.` instead, which is
|
||||||
|
all a text-only client ever sees.
|
||||||
|
|
||||||
!!! warning
|
!!! warning
|
||||||
Never return a placeholder like `"[Rendered UI]"` as the only content. If the
|
Never return a placeholder like `"[Rendered UI]"` as the only content. If the
|
||||||
|
|
|
||||||
|
|
@ -57,7 +57,7 @@ specified by the MCP project itself.
|
||||||
|
|
||||||
The smallest useful extension is one tool and a settings map:
|
The smallest useful extension is one tool and a settings map:
|
||||||
|
|
||||||
```python title="server.py" hl_lines="17 19-20 22-23 26"
|
```python title="server.py" hl_lines="16 18-19 21-22 25"
|
||||||
--8<-- "docs_src/extensions/tutorial003.py"
|
--8<-- "docs_src/extensions/tutorial003.py"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -69,18 +69,25 @@ The smallest useful extension is one tool and a settings map:
|
||||||
* The extension never receives the server. It declares contributions as data;
|
* The extension never receives the server. It declares contributions as data;
|
||||||
`MCPServer` consumes them. There is no `self.server` to mutate.
|
`MCPServer` consumes them. There is no `self.server` to mutate.
|
||||||
|
|
||||||
And `main()` is the proof, an in-memory client straight against `mcp`:
|
Serve it over HTTP, and a client is the proof:
|
||||||
|
|
||||||
```python title="server.py" hl_lines="29-34"
|
```console
|
||||||
--8<-- "docs_src/extensions/tutorial003.py"
|
uv run mcp run server.py --transport streamable-http
|
||||||
```
|
```
|
||||||
|
|
||||||
|
```python title="client.py" hl_lines="7-11"
|
||||||
|
--8<-- "docs_src/extensions/tutorial003_client.py"
|
||||||
|
```
|
||||||
|
|
||||||
|
Every `server.py` on this page is served with that command, and every `client.py`
|
||||||
|
runs beside it with `python client.py` from a second terminal.
|
||||||
|
|
||||||
### Serving your own methods
|
### Serving your own methods
|
||||||
|
|
||||||
An extension can register **new request methods**: its own verbs, served next to the
|
An extension can register **new request methods**: its own verbs, served next to the
|
||||||
spec's:
|
spec's:
|
||||||
|
|
||||||
```python title="server.py" hl_lines="16-22 31 40-48"
|
```python title="server.py" hl_lines="14-20 24 33-41"
|
||||||
--8<-- "docs_src/extensions/tutorial004.py"
|
--8<-- "docs_src/extensions/tutorial004.py"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -107,10 +114,10 @@ runtime:
|
||||||
|
|
||||||
### The client side
|
### The client side
|
||||||
|
|
||||||
The same file's `main()` is the whole client story, both halves of it:
|
The client is its own program, and it carries both halves of the client story:
|
||||||
|
|
||||||
```python title="server.py" hl_lines="54-58"
|
```python title="client.py" hl_lines="21-23 27-30"
|
||||||
--8<-- "docs_src/extensions/tutorial004.py"
|
--8<-- "docs_src/extensions/tutorial004_client.py"
|
||||||
```
|
```
|
||||||
|
|
||||||
* `Client(..., extensions=[advertise(EXTENSION_ID)])` declares the extension. The
|
* `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:
|
||||||
* Vendor methods drop one layer to `client.session.send_request(...)`; `Client`
|
* Vendor methods drop one layer to `client.session.send_request(...)`; `Client`
|
||||||
only grows first-class methods for spec verbs. `send_request` accepts any
|
only grows first-class methods for spec verbs. `send_request` accepts any
|
||||||
`Request` subclass, so the vendor request passes as-is.
|
`Request` subclass, so the vendor request passes as-is.
|
||||||
|
* `SearchRequest` and the two models it carries are the extension's wire contract,
|
||||||
|
so the client declares them for itself. A published extension would ship them in
|
||||||
|
a package that both sides import.
|
||||||
|
|
||||||
### Intercepting `tools/call`
|
### Intercepting `tools/call`
|
||||||
|
|
||||||
|
|
@ -155,13 +165,20 @@ The hook wraps `tools/call` and nothing else. For every-message concerns, use
|
||||||
## Using a client extension
|
## Using a client extension
|
||||||
|
|
||||||
A **client extension** is the same contract from the consuming side: a bundle of
|
A **client extension** is the same contract from the consuming side: a bundle of
|
||||||
client-side behaviour behind one identifier. Pass instances to
|
client-side behaviour behind one identifier. The server here answers `buy` with a
|
||||||
`Client(extensions=[...])` and call tools normally:
|
receipt to redeem instead of the goods, and only for a client that declared the
|
||||||
|
extension:
|
||||||
|
|
||||||
```python title="client.py" hl_lines="66-68"
|
```python title="server.py" hl_lines="22-25"
|
||||||
--8<-- "docs_src/extensions/tutorial006.py"
|
--8<-- "docs_src/extensions/tutorial006.py"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
On the client, pass instances to `Client(extensions=[...])` and call tools normally:
|
||||||
|
|
||||||
|
```python title="client.py" hl_lines="33-35"
|
||||||
|
--8<-- "docs_src/extensions/tutorial006_client.py"
|
||||||
|
```
|
||||||
|
|
||||||
`call_tool("buy", ...)` returns a plain `CallToolResult`, like every other call. What
|
`call_tool("buy", ...)` returns a plain `CallToolResult`, like every other call. What
|
||||||
the extension changed: the server may now answer `buy` with a `receipt` **result
|
the extension changed: the server may now answer `buy` with a `receipt` **result
|
||||||
shape** instead of a final result, and `Receipts` finishes it (here by redeeming the
|
shape** instead of a final result, and `Receipts` finishes it (here by redeeming the
|
||||||
|
|
@ -180,7 +197,7 @@ the capability, the client does nothing, as in the search client above), use
|
||||||
```python
|
```python
|
||||||
from mcp.client import advertise
|
from mcp.client import advertise
|
||||||
|
|
||||||
client = Client(mcp, extensions=[advertise("com.example/search")])
|
client = Client("http://localhost:8000/mcp", extensions=[advertise("com.example/search")])
|
||||||
```
|
```
|
||||||
|
|
||||||
## Writing a client extension
|
## Writing a client extension
|
||||||
|
|
@ -188,8 +205,8 @@ client = Client(mcp, extensions=[advertise("com.example/search")])
|
||||||
Subclass `ClientExtension` and override only what you need. Three contribution
|
Subclass `ClientExtension` and override only what you need. Three contribution
|
||||||
kinds, each with a default: `settings()`, `claims()`, and `notifications()`.
|
kinds, each with a default: `settings()`, `claims()`, and `notifications()`.
|
||||||
|
|
||||||
```python title="client.py" hl_lines="17-18 43-44 46-47"
|
```python title="client.py" hl_lines="16-17 25-26 28-29"
|
||||||
--8<-- "docs_src/extensions/tutorial006.py"
|
--8<-- "docs_src/extensions/tutorial006_client.py"
|
||||||
```
|
```
|
||||||
|
|
||||||
* The identifier follows the same grammar as the server's, validated when the class
|
* 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`.
|
||||||
|
|
||||||
An extension's own request methods need no client-side registration. A vendor request
|
An extension's own request methods need no client-side registration. A vendor request
|
||||||
type subclasses `mcp.types.Request` and goes through `client.session.send_request`,
|
type subclasses `mcp.types.Request` and goes through `client.session.send_request`,
|
||||||
as in [Serving your own methods](#serving-your-own-methods). One addition: when a
|
as in [Serving your own methods](#serving-your-own-methods). Take a server whose
|
||||||
params key must ride the `Mcp-Name` header (extension specs such as tasks require
|
extension serves one verb about a named job:
|
||||||
this for their verbs), the request type declares `name_param`:
|
|
||||||
|
|
||||||
```python title="client.py" hl_lines="22-25 46-47"
|
```python title="server.py" hl_lines="12-13 30"
|
||||||
--8<-- "docs_src/extensions/tutorial007.py"
|
--8<-- "docs_src/extensions/tutorial007.py"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
One addition on the client: when a params key must ride the `Mcp-Name` header
|
||||||
|
(extension specs such as tasks require this for their verbs), the request type
|
||||||
|
declares `name_param`:
|
||||||
|
|
||||||
|
```python title="client.py" hl_lines="20-23 28-29"
|
||||||
|
--8<-- "docs_src/extensions/tutorial007_client.py"
|
||||||
|
```
|
||||||
|
|
||||||
The session mirrors `params["jobId"]` into `Mcp-Name` on every send path, and a
|
The session mirrors `params["jobId"]` into `Mcp-Name` on every send path, and a
|
||||||
missing value fails loudly rather than silently omitting a required header.
|
missing value fails loudly rather than silently omitting a required header.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -31,18 +31,22 @@ Three things changed, and they are the whole low-level API:
|
||||||
|
|
||||||
### Try it
|
### Try it
|
||||||
|
|
||||||
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`:
|
`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:
|
||||||
|
|
||||||
```python title="main.py"
|
```console
|
||||||
|
uvicorn server:app --port 8000
|
||||||
|
```
|
||||||
|
|
||||||
|
Point the Inspector, or any client, at `http://localhost:8000/mcp`:
|
||||||
|
|
||||||
|
```python title="client.py"
|
||||||
import asyncio
|
import asyncio
|
||||||
|
|
||||||
from mcp import Client
|
from mcp import Client
|
||||||
|
|
||||||
from server import server
|
|
||||||
|
|
||||||
|
|
||||||
async def main() -> None:
|
async def main() -> None:
|
||||||
async with Client(server) as client:
|
async with Client("http://localhost:8000/mcp") as client:
|
||||||
result = await client.call_tool("search_books", {"query": "dune", "limit": 5})
|
result = await client.call_tool("search_books", {"query": "dune", "limit": 5})
|
||||||
print(result.content)
|
print(result.content)
|
||||||
|
|
||||||
|
|
@ -59,6 +63,8 @@ The same text the `@mcp.tool()` version produced. Two honest differences:
|
||||||
* `result.structured_content` is `None`. The high-level server wraps a `-> str` into `{"result": ...}` for you; here nobody builds what you didn't build.
|
* `result.structured_content` is `None`. The high-level server wraps a `-> str` into `{"result": ...}` for you; here nobody builds what you didn't build.
|
||||||
* `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.
|
* `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.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
## Nothing is checked for you
|
## Nothing is checked for you
|
||||||
|
|
||||||
`MCPServer` rejects a bad argument before your function ever runs, validating the call against the schema it generated (**[Tools](../servers/tools.md)**).
|
`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
|
||||||
* `add_request_handler(method, params_type, handler)` serves any method. `initialize` is reserved.
|
* `add_request_handler(method, params_type, handler)` serves any method. `initialize` is reserved.
|
||||||
* The capabilities a `Server` advertises are derived from which handlers you registered.
|
* The capabilities a `Server` advertises are derived from which handlers you registered.
|
||||||
|
|
||||||
`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)**.
|
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)**.
|
||||||
|
|
|
||||||
|
|
@ -26,9 +26,13 @@ Pagination is for the server whose resource list is really a database: thousands
|
||||||
|
|
||||||
### Try it
|
### Try it
|
||||||
|
|
||||||
`Client(server)` connects to a low-level `Server` in memory exactly as it connects to an `MCPServer`.
|
`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:
|
||||||
|
|
||||||
Call `list_resources()` with no arguments. You get ten resources, `book-1` through `book-10`, and `next_cursor` is the string `"10"`.
|
```console
|
||||||
|
uvicorn server:app --port 8000
|
||||||
|
```
|
||||||
|
|
||||||
|
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"`.
|
||||||
|
|
||||||
Hand it back with `list_resources(cursor="10")` and the first resource is `book-11`, the new `next_cursor` is `"20"`.
|
Hand it back with `list_resources(cursor="10")` and the first resource is `book-11`, the new `next_cursor` is `"20"`.
|
||||||
|
|
||||||
|
|
@ -38,7 +42,7 @@ The tenth page comes back with `next_cursor` set to `None`. Done.
|
||||||
|
|
||||||
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`:
|
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`:
|
||||||
|
|
||||||
```python title="client.py" hl_lines="26-32"
|
```python title="client.py" hl_lines="9-15"
|
||||||
--8<-- "docs_src/pagination/tutorial002.py"
|
--8<-- "docs_src/pagination/tutorial002.py"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -46,7 +50,7 @@ Every `list_*` method on `Client` (`list_tools`, `list_resources`, `list_resourc
|
||||||
* Extend **before** you look at `next_cursor`: the last page has resources too.
|
* Extend **before** you look at `next_cursor`: the last page has resources too.
|
||||||
* `next_cursor is None` is the exit. Anything else goes straight back into `cursor=`, untouched.
|
* `next_cursor is None` is the exit. Anything else goes straight back into `cursor=`, untouched.
|
||||||
|
|
||||||
Run its `main()` and it prints `100 resources`: ten pages of ten, stitched together by a loop that never knew there were ten pages.
|
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.
|
||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ Out of the box every result says `ttlMs: 0, cacheScope: "private"`: immediately
|
||||||
|
|
||||||
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:
|
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:
|
||||||
|
|
||||||
```python title="server.py" hl_lines="10 16"
|
```python title="server.py" hl_lines="11 17"
|
||||||
--8<-- "docs_src/caching/tutorial002.py"
|
--8<-- "docs_src/caching/tutorial002.py"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -39,10 +39,24 @@ One caveat on paginated lists: the protocol requires the **same `cacheScope` on
|
||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||
```python title="client.py" hl_lines="33 35 38"
|
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:
|
||||||
|
|
||||||
|
```console
|
||||||
|
uvicorn server:app --port 8000
|
||||||
|
```
|
||||||
|
|
||||||
|
```python title="client.py" hl_lines="20 23 28"
|
||||||
--8<-- "docs_src/caching/tutorial003.py"
|
--8<-- "docs_src/caching/tutorial003.py"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
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`:
|
||||||
|
|
||||||
|
```text
|
||||||
|
1000 public
|
||||||
|
```
|
||||||
|
|
||||||
|
The server's terminal tells the rest of the story: between uvicorn's request logs, `tools/list served` appears three times.
|
||||||
|
|
||||||
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`):
|
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`):
|
||||||
|
|
||||||
* `"use"` (the default) serves a fresh entry if there is one, and stores the fetch if not.
|
* `"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
|
||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -58,7 +58,7 @@ One `tools/call` from you, one `elicitation/create` back from the server, answer
|
||||||
`mode="legacy"` on the `Client(...)` call is doing real work. By default `Client(...)` negotiates the modern
|
`mode="legacy"` on the `Client(...)` call is doing real work. By default `Client(...)` negotiates the modern
|
||||||
protocol path, and that path has no back-channel for server-to-client requests: `ctx.elicit`
|
protocol path, and that path has no back-channel for server-to-client requests: `ctx.elicit`
|
||||||
fails before your callback ever runs. The transport doesn't decide that; the negotiated
|
fails before your callback ever runs. The transport doesn't decide that; the negotiated
|
||||||
protocol does, in-memory and over a URL alike. Pin `mode="legacy"` whenever your client has
|
protocol does. Pin `mode="legacy"` whenever your client has
|
||||||
to answer one; every test behind this page does. **[Protocol versions](../protocol-versions.md)** has the whole story.
|
to answer one; every test behind this page does. **[Protocol versions](../protocol-versions.md)** has the whole story.
|
||||||
|
|
||||||
On a 2026-07-28 session the callback isn't dead, it's fed differently: when a tool returns an
|
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.
|
||||||
* `sampling_callback` and `list_roots_callback` work the same way but serve deprecated features; modern servers use multi-round-trip requests instead.
|
* `sampling_callback` and `list_roots_callback` work the same way but serve deprecated features; modern servers use multi-round-trip requests instead.
|
||||||
* `logging_callback` and `message_handler` receive notifications. They declare nothing.
|
* `logging_callback` and `message_handler` receive notifications. They declare nothing.
|
||||||
|
|
||||||
The first argument to `Client(...)` is a transport object. **[Client transports](transports.md)** covers every kind.
|
The first argument to `Client(...)` picks the transport. **[Client transports](transports.md)** covers every kind.
|
||||||
|
|
|
||||||
|
|
@ -6,13 +6,23 @@ It is one object with one lifecycle: construct it, enter `async with`, call meth
|
||||||
|
|
||||||
## Your first client
|
## Your first client
|
||||||
|
|
||||||
```python title="client.py" hl_lines="14-18"
|
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:
|
||||||
|
|
||||||
|
```python title="server.py"
|
||||||
--8<-- "docs_src/client/tutorial001.py"
|
--8<-- "docs_src/client/tutorial001.py"
|
||||||
```
|
```
|
||||||
|
|
||||||
The server at the top is only there so you have something to connect to. The client is the five highlighted lines.
|
```console
|
||||||
|
uv run mcp run server.py --transport streamable-http
|
||||||
|
```
|
||||||
|
|
||||||
* `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.
|
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:
|
||||||
|
|
||||||
|
```python title="client.py" hl_lines="7-11"
|
||||||
|
--8<-- "docs_src/client/tutorial001_client.py"
|
||||||
|
```
|
||||||
|
|
||||||
|
* `Client("http://localhost:8000/mcp")` is given a **URL**, so it connects over Streamable HTTP to the server you just started.
|
||||||
* `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.
|
* `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.
|
||||||
* Inside the block the connection facts are already there as plain properties.
|
* Inside the block the connection facts are already there as plain properties.
|
||||||
|
|
||||||
|
|
@ -20,10 +30,10 @@ The server at the top is only there so you have something to connect to. The cli
|
||||||
|
|
||||||
`Client` takes one positional argument and resolves the transport from its type:
|
`Client` takes one positional argument and resolves the transport from its type:
|
||||||
|
|
||||||
* An `MCPServer` (or low-level `Server`) instance: connected **in-process**.
|
* A URL string (`Client("http://localhost:8000/mcp")`): Streamable HTTP, the transport you deploy behind.
|
||||||
* A URL string (`Client("http://localhost:8000/mcp")`): Streamable HTTP, the production path.
|
* A `StdioServerParameters`: the command to launch as a local **subprocess**, spoken to over its stdin and stdout.
|
||||||
* A `StdioServerParameters`: the command to launch as a **subprocess**, spoken to over its stdin and stdout.
|
|
||||||
* A **transport**: anything you can `async with ... as (read, write)`, such as `streamable_http_client(url, http_client=...)` around your own HTTP client.
|
* A **transport**: anything you can `async with ... as (read, write)`, such as `streamable_http_client(url, http_client=...)` around your own HTTP client.
|
||||||
|
* 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.
|
||||||
|
|
||||||
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)**.
|
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)**.
|
||||||
|
|
||||||
|
|
@ -44,11 +54,11 @@ You never picked a protocol version. By default the `Client` probes the server a
|
||||||
|
|
||||||
## Listing tools
|
## Listing tools
|
||||||
|
|
||||||
```python title="client.py" hl_lines="15-20"
|
```python title="client.py" hl_lines="8-13"
|
||||||
--8<-- "docs_src/client/tutorial002.py"
|
--8<-- "docs_src/client/tutorial002.py"
|
||||||
```
|
```
|
||||||
|
|
||||||
`list_tools()` returns a `ListToolsResult`; the tools are in `.tools`. Each one is the complete definition a host would hand to a model:
|
`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:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
tool.name # 'search_books'
|
tool.name # 'search_books'
|
||||||
|
|
@ -72,6 +82,8 @@ and `tool.input_schema` is the JSON Schema the server derived from the function'
|
||||||
|
|
||||||
That schema is everything a UI needs to render an argument form, and everything a model needs to produce valid arguments.
|
That schema is everything a UI needs to render an argument form, and everything a model needs to produce valid arguments.
|
||||||
|
|
||||||
|
The second tool, `lookup_book`, was registered without a `title=`, so its `tool.title` is `None`.
|
||||||
|
|
||||||
!!! tip
|
!!! tip
|
||||||
`title` is optional, so a UI showing tools to a human has to pick: the `title` if there is one,
|
`title` is optional, so a UI showing tools to a human has to pick: the `title` if there is one,
|
||||||
the `name` if not. `from mcp.shared.metadata_utils import get_display_name` does exactly that,
|
the `name` if not. `from mcp.shared.metadata_utils import get_display_name` does exactly that,
|
||||||
|
|
@ -81,7 +93,7 @@ That schema is everything a UI needs to render an argument form, and everything
|
||||||
|
|
||||||
`call_tool(name, arguments)` runs the tool and gives you back a `CallToolResult`.
|
`call_tool(name, arguments)` runs the tool and gives you back a `CallToolResult`.
|
||||||
|
|
||||||
```python title="client.py" hl_lines="27-34"
|
```python title="client.py" hl_lines="9-16"
|
||||||
--8<-- "docs_src/client/tutorial003.py"
|
--8<-- "docs_src/client/tutorial003.py"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -137,7 +149,7 @@ A tool that raises does **not** raise in your client. It comes back as an ordina
|
||||||
|
|
||||||
The resource verbs come in pairs: two ways to list, one way to read.
|
The resource verbs come in pairs: two ways to list, one way to read.
|
||||||
|
|
||||||
```python title="client.py" hl_lines="22-31"
|
```python title="client.py" hl_lines="9-18"
|
||||||
--8<-- "docs_src/client/tutorial004.py"
|
--8<-- "docs_src/client/tutorial004.py"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -151,7 +163,7 @@ A client can also be told when a resource changes. On 2025-era connections that
|
||||||
|
|
||||||
## Prompts
|
## Prompts
|
||||||
|
|
||||||
```python title="client.py" hl_lines="15-20"
|
```python title="client.py" hl_lines="8-13"
|
||||||
--8<-- "docs_src/client/tutorial005.py"
|
--8<-- "docs_src/client/tutorial005.py"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -176,7 +188,7 @@ A host hands those messages straight to the model. That is the whole feature.
|
||||||
|
|
||||||
A server with a completion handler can autocomplete prompt and resource-template arguments as the user types.
|
A server with a completion handler can autocomplete prompt and resource-template arguments as the user types.
|
||||||
|
|
||||||
```python title="client.py" hl_lines="27-31"
|
```python title="client.py" hl_lines="9-13"
|
||||||
--8<-- "docs_src/client/tutorial006.py"
|
--8<-- "docs_src/client/tutorial006.py"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -189,21 +201,21 @@ The answer is in `result.completion.values`. Type `"p"` and the server comes bac
|
||||||
|
|
||||||
Every `list_*` method takes a `cursor=` keyword and every result carries a `next_cursor`. When `next_cursor` is `None`, you have everything.
|
Every `list_*` method takes a `cursor=` keyword and every result carries a `next_cursor`. When `next_cursor` is `None`, you have everything.
|
||||||
|
|
||||||
```python title="client.py" hl_lines="22-30"
|
```python title="client.py" hl_lines="7-15"
|
||||||
--8<-- "docs_src/client/tutorial007.py"
|
--8<-- "docs_src/client/tutorial007.py"
|
||||||
```
|
```
|
||||||
|
|
||||||
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)**.
|
`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)**.
|
||||||
|
|
||||||
## In tests
|
## In tests
|
||||||
|
|
||||||
`Client(mcp)` with no process and no port is already a test harness for your server.
|
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.
|
||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||
## Recap
|
## Recap
|
||||||
|
|
||||||
* `Client(x)` connects in-memory to a server object, over Streamable HTTP to a URL string, and over anything else via a transport.
|
* `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.
|
||||||
* `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.
|
* `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.
|
||||||
* `list_tools()` gives you each tool's `name`, `title`, `description` and `input_schema`.
|
* `list_tools()` gives you each tool's `name`, `title`, `description` and `input_schema`.
|
||||||
* `call_tool()` returns `content` for the model, `structured_content` for your code, and `is_error`. A raising tool is a result, not an exception.
|
* `call_tool()` returns `content` for the model, `structured_content` for your code, and `is_error`. A raising tool is a result, not an exception.
|
||||||
|
|
|
||||||
|
|
@ -89,7 +89,7 @@ You wrote none of it. Two keyword arguments remain (`client_metadata_url` and `v
|
||||||
|
|
||||||
### Try it
|
### Try it
|
||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -73,7 +73,7 @@ If you already hold a connected `ClientSession` (`Client.session` is one), hand
|
||||||
## Recap
|
## Recap
|
||||||
|
|
||||||
* `ClientSessionGroup` holds many server connections and merges their tools, resources, and prompts into one `dict` each.
|
* `ClientSessionGroup` holds many server connections and merges their tools, resources, and prompts into one `dict` each.
|
||||||
* `connect_to_server(params)` per server. It takes transport parameters, never the server object or URL a `Client` takes.
|
* `connect_to_server(params)` per server. It takes transport parameters, never the URL or `Transport` a `Client` takes.
|
||||||
* `group.call_tool(name, arguments)` routes to the owning server for you.
|
* `group.call_tool(name, arguments)` routes to the owning server for you.
|
||||||
* Names must be unique across the whole group; two servers with a `search` tool cannot coexist on their own.
|
* Names must be unique across the whole group; two servers with a `search` tool cannot coexist on their own.
|
||||||
* `component_name_hook=` rewrites every registered name. The dict key changes, the wire name does not.
|
* `component_name_hook=` rewrites every registered name. The dict key changes, the wire name does not.
|
||||||
|
|
|
||||||
|
|
@ -6,24 +6,9 @@ You never configure one separately. `Client` takes a single positional argument
|
||||||
|
|
||||||
The *server* side of each (what `mcp.run()` does and what you deploy) is **[Running your server](../run/index.md)**.
|
The *server* side of each (what `mcp.run()` does and what you deploy) is **[Running your server](../run/index.md)**.
|
||||||
|
|
||||||
## In memory
|
|
||||||
|
|
||||||
Pass the server object itself:
|
|
||||||
|
|
||||||
```python title="client.py" hl_lines="14"
|
|
||||||
--8<-- "docs_src/client_transports/tutorial001.py"
|
|
||||||
```
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
That makes it two things at once:
|
|
||||||
|
|
||||||
* **A test harness.** Every example in this documentation is exercised this way, and the **[Testing](../get-started/testing.md)** page builds the whole pattern around it.
|
|
||||||
* **An embedding API.** An application that constructs the server doesn't need a network hop to call its tools.
|
|
||||||
|
|
||||||
## Streamable HTTP
|
## Streamable HTTP
|
||||||
|
|
||||||
Pass a URL string and you get **Streamable HTTP**, the transport you deploy behind:
|
Pass a URL string and you get **Streamable HTTP**, the transport you deploy behind and the one to reach for first:
|
||||||
|
|
||||||
```python title="client.py" hl_lines="5"
|
```python title="client.py" hl_lines="5"
|
||||||
--8<-- "docs_src/client_transports/tutorial002.py"
|
--8<-- "docs_src/client_transports/tutorial002.py"
|
||||||
|
|
@ -121,6 +106,18 @@ The child's stderr goes to yours. To send it somewhere else, build the transport
|
||||||
A server that needs an API key won't find it there. Pass it explicitly with `env=`; those
|
A server that needs an API key won't find it there. Pass it explicitly with `env=`; those
|
||||||
variables are merged on top of the allow-list. That is what `BOOKSHOP_API_KEY` is doing above.
|
variables are merged on top of the allow-list. That is what `BOOKSHOP_API_KEY` is doing above.
|
||||||
|
|
||||||
|
## In memory
|
||||||
|
|
||||||
|
In a test there is nothing to deploy and nothing to launch. Pass the server object itself:
|
||||||
|
|
||||||
|
```python hl_lines="14"
|
||||||
|
--8<-- "docs_src/client_transports/tutorial001.py"
|
||||||
|
```
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
The same form doubles as an embedding API: an application that constructs the server itself can call its tools without a network hop.
|
||||||
|
|
||||||
## SSE
|
## SSE
|
||||||
|
|
||||||
`sse_client(url)`, from `mcp.client.sse`, is the HTTP transport that Streamable HTTP superseded. Wrap it the same way, `Client(sse_client("http://localhost:8000/sse"))`, to talk to a server that still speaks it, and don't build anything new on it.
|
`sse_client(url)`, from `mcp.client.sse`, is the HTTP transport that Streamable HTTP superseded. Wrap it the same way, `Client(sse_client("http://localhost:8000/sse"))`, to talk to a server that still speaks it, and don't build anything new on it.
|
||||||
|
|
@ -129,16 +126,16 @@ The child's stderr goes to yours. To send it somewhere else, build the transport
|
||||||
|
|
||||||
To `Client`, all of the above are the same thing.
|
To `Client`, all of the above are the same thing.
|
||||||
|
|
||||||
A **transport** is any async context manager that yields a `(read, write)` pair of message streams: formally, the `Transport` protocol in `mcp.client`. `Client` resolves its argument by type: a server object connects in-process, a `str` becomes `streamable_http_client(url)`, a `StdioServerParameters` becomes `stdio_client(params)`, and anything else is entered as a transport directly. That last rule is why `stdio_client(...)`, `streamable_http_client(...)` and `sse_client(...)` all drop into the same slot, and why you can write your own.
|
A **transport** is any async context manager that yields a `(read, write)` pair of message streams: formally, the `Transport` protocol in `mcp.client`. `Client` resolves its argument by type: a `str` becomes `streamable_http_client(url)`, a `StdioServerParameters` becomes `stdio_client(params)`, a server object connects in-process, and anything else is entered as a transport directly. That last rule is why `stdio_client(...)`, `streamable_http_client(...)` and `sse_client(...)` all drop into the same slot, and why you can write your own.
|
||||||
|
|
||||||
## Recap
|
## Recap
|
||||||
|
|
||||||
* `Client(mcp)` (the server object) connects in memory. Use it for tests and for embedding.
|
|
||||||
* `Client("http://.../mcp")` (a URL) connects over Streamable HTTP, the production transport.
|
* `Client("http://.../mcp")` (a URL) connects over Streamable HTTP, the production transport.
|
||||||
* Headers, auth, proxies and timeouts belong on an `httpx2.AsyncClient` you pass to `streamable_http_client(url, http_client=...)`. There is no `headers=` keyword.
|
* Headers, auth, proxies and timeouts belong on an `httpx2.AsyncClient` you pass to `streamable_http_client(url, http_client=...)`. There is no `headers=` keyword.
|
||||||
* Redirects are followed only within the URL's own origin (a trailing-slash `307`/`308`), plus `http`→`https` on the same host. Anything else fails with `Redirect to … not followed`; configure the final URL.
|
* Redirects are followed only within the URL's own origin (a trailing-slash `307`/`308`), plus `http`→`https` on the same host. Anything else fails with `Redirect to … not followed`; configure the final URL.
|
||||||
* stdio is `Client(StdioServerParameters(...))`. Wrap it in `stdio_client(...)` yourself only to redirect the child's stderr.
|
* stdio is `Client(StdioServerParameters(...))`. Wrap it in `stdio_client(...)` yourself only to redirect the child's stderr.
|
||||||
* The subprocess gets an allow-listed environment, not yours; `env=` adds to it.
|
* The subprocess gets an allow-listed environment, not yours; `env=` adds to it.
|
||||||
|
* `Client(mcp)` (the server object) connects in memory. Use it in tests, or to embed a server in the application that built it.
|
||||||
* A transport is anything you can `async with x as (read, write)`. `Client` hands anything that isn't a server object, a URL or `StdioServerParameters` straight to that protocol.
|
* A transport is anything you can `async with x as (read, write)`. `Client` hands anything that isn't a server object, a URL or `StdioServerParameters` straight to that protocol.
|
||||||
* Constructing a `Client` picks the transport. `async with` opens it.
|
* Constructing a `Client` picks the transport. `async with` opens it.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ Three words you'll see on every page from here on:
|
||||||
* A **client** lives inside the host and speaks MCP. The host runs one client per server it's connected to.
|
* A **client** lives inside the host and speaks MCP. The host runs one client per server it's connected to.
|
||||||
* A **server** is what you build with this SDK. It exposes things to clients. It never talks to the model directly.
|
* A **server** is what you build with this SDK. It exposes things to clients. It never talks to the model directly.
|
||||||
|
|
||||||
You write the server. Hosts are someone else's product. The SDK also gives you a `Client`. You'll use it to test your servers, and it shows up later on this page.
|
You write the server. Hosts are someone else's product. The SDK also gives you a `Client`, the same class a host would use to reach a server by URL or launch it as a subprocess. It shows up later on this page, and it is also how you'll test your servers.
|
||||||
|
|
||||||
## The three primitives
|
## The three primitives
|
||||||
|
|
||||||
|
|
@ -78,22 +78,20 @@ You saw three tabs in the Inspector. How did it know there were three?
|
||||||
|
|
||||||
When a client connects, the server declares its **capabilities**: which families of requests it will answer. The client uses that declaration to decide what to even ask for. You never wrote it; `MCPServer` declares it for you.
|
When a client connects, the server declares its **capabilities**: which families of requests it will answer. The client uses that declaration to decide what to even ask for. You never wrote it; `MCPServer` declares it for you.
|
||||||
|
|
||||||
Look at it yourself. The SDK's `Client` accepts the server object directly and connects to it **in memory** (no subprocess, no port):
|
Look at it yourself. Leave `server.py` running over HTTP in one terminal:
|
||||||
|
|
||||||
```python
|
```console
|
||||||
import asyncio
|
uv run mcp run server.py --transport streamable-http
|
||||||
|
```
|
||||||
|
|
||||||
from mcp import Client
|
and point a client at it from another:
|
||||||
|
|
||||||
from server import mcp
|
```python title="client.py" hl_lines="7-8"
|
||||||
|
--8<-- "docs_src/first_steps/tutorial001_client.py"
|
||||||
|
```
|
||||||
|
|
||||||
|
```console
|
||||||
async def main() -> None:
|
python client.py
|
||||||
async with Client(mcp) as client:
|
|
||||||
print(client.server_capabilities.model_dump(exclude_none=True))
|
|
||||||
|
|
||||||
|
|
||||||
asyncio.run(main())
|
|
||||||
```
|
```
|
||||||
|
|
||||||
```text
|
```text
|
||||||
|
|
@ -113,8 +111,9 @@ That dictionary is your server's declared **capabilities**. It's the first thing
|
||||||
Notice what isn't there. `completions` (argument autocomplete for resource templates and prompts) needs a handler you write, this server doesn't have one, so the capability is absent and a well-behaved client won't ask. That's the rule for everything optional: register the thing and the capability appears; **[Completions](../servers/completions.md)** proves it.
|
Notice what isn't there. `completions` (argument autocomplete for resource templates and prompts) needs a handler you write, this server doesn't have one, so the capability is absent and a well-behaved client won't ask. That's the rule for everything optional: register the thing and the capability appears; **[Completions](../servers/completions.md)** proves it.
|
||||||
|
|
||||||
!!! info
|
!!! info
|
||||||
`Client(mcp)` is the same in-memory client every example in these docs is tested with, and
|
That `client.py` is a complete MCP client, and **[The Client](../client/index.md)** is its page.
|
||||||
it's how you'll test yours. It gets a whole page: **[Testing](testing.md)**.
|
In a test you skip the terminal and the port and hand `Client` the server object itself,
|
||||||
|
`Client(mcp)`. That gets a whole page too: **[Testing](testing.md)**.
|
||||||
|
|
||||||
## What you did not write
|
## What you did not write
|
||||||
|
|
||||||
|
|
@ -123,7 +122,7 @@ Look back over this page. You wrote three small Python functions. You did **not*
|
||||||
* A JSON Schema. `a: int, b: int` *is* the schema for `add`.
|
* A JSON Schema. `a: int, b: int` *is* the schema for `add`.
|
||||||
* A request handler. `tools/list`, `resources/read`, `prompts/get`: all served for you.
|
* A request handler. `tools/list`, `resources/read`, `prompts/get`: all served for you.
|
||||||
* A capability declaration. `MCPServer` made it for you.
|
* A capability declaration. `MCPServer` made it for you.
|
||||||
* A line of protocol. The version negotiation, the JSON-RPC framing, the capability exchange: all of it happened inside `mcp dev` and `Client(mcp)`, and you never saw it.
|
* A line of protocol. The version negotiation, the JSON-RPC framing, the capability exchange: all of it happened inside `mcp dev` and `client.py`, and you never saw it.
|
||||||
|
|
||||||
That ratio is the whole point of the SDK.
|
That ratio is the whole point of the SDK.
|
||||||
|
|
||||||
|
|
@ -134,6 +133,6 @@ That ratio is the whole point of the SDK.
|
||||||
* One decorator per primitive: `@mcp.tool()`, `@mcp.resource(uri)`, `@mcp.prompt()`. Name, description, and schema come from the function.
|
* One decorator per primitive: `@mcp.tool()`, `@mcp.resource(uri)`, `@mcp.prompt()`. Name, description, and schema come from the function.
|
||||||
* A URI with a `{param}` makes a resource **template**, listed separately from concrete resources.
|
* A URI with a `{param}` makes a resource **template**, listed separately from concrete resources.
|
||||||
* The server's **capabilities** are declared for you, and a client only asks for what a server declares.
|
* The server's **capabilities** are declared for you, and a client only asks for what a server declares.
|
||||||
* `Client(mcp)` connects to the server object in memory: your test harness from day one.
|
* `Client("http://localhost:8000/mcp")` talks to your running server. Hand it the server object instead, `Client(mcp)`, and it is your test harness from day one.
|
||||||
|
|
||||||
Next up is **[Connect to a real host](real-host.md)**: this server inside Claude Desktop or an IDE, for real. Then **[Testing](testing.md)**: one page, one in-memory client, and you're never guessing whether it works. After that, each primitive gets its own page, starting with the one the model drives: **[Tools](../servers/tools.md)**.
|
Next up is **[Connect to a real host](real-host.md)**: this server inside Claude Desktop or an IDE, for real. Then **[Testing](testing.md)**: one page, one in-memory client, and you're never guessing whether it works. After that, each primitive gets its own page, starting with the one the model drives: **[Tools](../servers/tools.md)**.
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
# Testing
|
# Testing
|
||||||
|
|
||||||
The Python SDK ships a `Client` class with an **in-memory transport**: pass it your server object and it connects to it directly.
|
The SDK's `Client` class, the same one that connects to a URL or launches a subprocess, also connects **in memory**: pass it your server object and it talks to it directly.
|
||||||
|
|
||||||
No subprocess. No port. No transport at all. It's the same idea as FastAPI's `TestClient`.
|
No subprocess. No port. Nothing on a wire. It's the same idea as FastAPI's `TestClient`.
|
||||||
|
|
||||||
## Basic usage
|
## Basic usage
|
||||||
|
|
||||||
|
|
@ -91,7 +91,7 @@ instead of the sanitised one.
|
||||||
|
|
||||||
Leave it on in tests. It has no meaning in production code.
|
Leave it on in tests. It has no meaning in production code.
|
||||||
|
|
||||||
## In-process by default
|
## Era-neutral by default
|
||||||
|
|
||||||
!!! note
|
!!! note
|
||||||
`Client(mcp)` connects in-process and is **era-neutral** by default: it probes the server and
|
`Client(mcp)` connects in-process and is **era-neutral** by default: it probes the server and
|
||||||
|
|
|
||||||
|
|
@ -159,7 +159,7 @@ The low-level `Server` is the no-batteries tier: unlike `MCPServer`, nothing is
|
||||||
|
|
||||||
## A 2026-07-28 result
|
## A 2026-07-28 result
|
||||||
|
|
||||||
`InputRequiredResult` only exists at protocol version **2026-07-28**. The in-memory `Client(server)` negotiates it for you; over the wire, `mode="auto"` discovers it. After connecting, `client.protocol_version` tells you what you got.
|
`InputRequiredResult` only exists at protocol version **2026-07-28**. `Client`'s default `mode="auto"` discovers it on any connection. After connecting, `client.protocol_version` tells you what you got.
|
||||||
|
|
||||||
!!! warning
|
!!! warning
|
||||||
A pre-2026 session has nowhere to put an `InputRequiredResult`. Return one from your handler on a
|
A pre-2026 session has nowhere to put an `InputRequiredResult`. Return one from your handler on a
|
||||||
|
|
|
||||||
|
|
@ -24,19 +24,17 @@ Three arguments, and you decide what they mean:
|
||||||
|
|
||||||
The client opts in **per call**, by passing `progress_callback=` to `call_tool`:
|
The client opts in **per call**, by passing `progress_callback=` to `call_tool`:
|
||||||
|
|
||||||
```python title="client.py" hl_lines="7 16"
|
```python title="client.py" hl_lines="5 14"
|
||||||
import anyio
|
import anyio
|
||||||
from mcp import Client
|
from mcp import Client
|
||||||
|
|
||||||
from server import mcp
|
|
||||||
|
|
||||||
|
|
||||||
async def show(progress: float, total: float | None, message: str | None) -> None:
|
async def show(progress: float, total: float | None, message: str | None) -> None:
|
||||||
print(f"{message} ({progress}/{total})")
|
print(f"{message} ({progress}/{total})")
|
||||||
|
|
||||||
|
|
||||||
async def main() -> None:
|
async def main() -> None:
|
||||||
async with Client(mcp) as client:
|
async with Client("http://localhost:8000/mcp") as client:
|
||||||
result = await client.call_tool(
|
result = await client.call_tool(
|
||||||
"import_catalog",
|
"import_catalog",
|
||||||
{"urls": ["https://example.com/a.json", "https://example.com/b.json"]},
|
{"urls": ["https://example.com/a.json", "https://example.com/b.json"]},
|
||||||
|
|
@ -51,28 +49,31 @@ anyio.run(main)
|
||||||
The callback is an `async` function taking exactly what the server reported: `progress`, `total`, `message`.
|
The callback is an `async` function taking exactly what the server reported: `progress`, `total`, `message`.
|
||||||
|
|
||||||
!!! info
|
!!! info
|
||||||
`Client(mcp)` connects straight to the server object, in memory, the same client the **[Testing](../get-started/testing.md)**
|
`progress_callback` is the same parameter whatever you handed `Client`: a URL as here, a
|
||||||
page is built on. `progress_callback` is the same parameter whatever transport the `Client`
|
`StdioServerParameters`, or the server object in a test. Mind the timing over a real
|
||||||
uses; the *timing* you are about to see is the in-memory connection's. It runs your callback
|
transport, though. Each notification is delivered on its own, beside the response, so a slow
|
||||||
inline, so every report lands before `call_tool` returns. Over a real transport the
|
callback can still be running after `call_tool` has returned. Only the in-process test
|
||||||
notifications race the result, and a slow callback can still be running after `call_tool` has
|
connection runs the callback inline and guarantees every report lands first.
|
||||||
returned.
|
|
||||||
|
|
||||||
### Try it
|
### Try it
|
||||||
|
|
||||||
Put `client.py` next to `server.py` and run it:
|
Serve `server.py` over HTTP, then run the client from a second terminal:
|
||||||
|
|
||||||
|
```console
|
||||||
|
uv run mcp run server.py --transport streamable-http
|
||||||
|
```
|
||||||
|
|
||||||
```console
|
```console
|
||||||
python client.py
|
python client.py
|
||||||
```
|
```
|
||||||
|
|
||||||
```text
|
```text
|
||||||
Imported https://example.com/a.json (1/2)
|
Imported https://example.com/a.json (1.0/2.0)
|
||||||
Imported https://example.com/b.json (2/2)
|
Imported https://example.com/b.json (2.0/2.0)
|
||||||
{'result': 'Imported 2 records.'}
|
{'result': 'Imported 2 records.'}
|
||||||
```
|
```
|
||||||
|
|
||||||
Every `await ctx.report_progress(...)` on the server became one call to `show` on the client, in order, and both lines printed **before** `call_tool` returned. Progress is not bundled into the result; it streams while the tool is still working.
|
Every `await ctx.report_progress(...)` on the server became one call to `show` on the client, in order. Progress is not bundled into the result. It streams while the tool is still working.
|
||||||
|
|
||||||
!!! warning
|
!!! warning
|
||||||
`progress_callback` belongs to the **call**, not the `Client`. There is no constructor argument
|
`progress_callback` belongs to the **call**, not the `Client`. There is no constructor argument
|
||||||
|
|
|
||||||
|
|
@ -6,9 +6,17 @@ Servers released before 2026-07-28 open every connection with the **`initialize`
|
||||||
|
|
||||||
You almost never have to care, because `Client` negotiates for you. This page is about the one constructor argument that controls it, `mode=`, and the three times you change it.
|
You almost never have to care, because `Client` negotiates for you. This page is about the one constructor argument that controls it, `mode=`, and the three times you change it.
|
||||||
|
|
||||||
|
Every snippet on this page is a `client.py` that talks to the Bookshop `server.py` from **[The Client](client/index.md)**. Start that server in one terminal:
|
||||||
|
|
||||||
|
```console
|
||||||
|
uv run mcp run server.py --transport streamable-http
|
||||||
|
```
|
||||||
|
|
||||||
|
Then run each snippet in a second terminal with `python client.py`.
|
||||||
|
|
||||||
## `mode="auto"`
|
## `mode="auto"`
|
||||||
|
|
||||||
```python title="client.py" hl_lines="14-15"
|
```python title="client.py" hl_lines="7-8"
|
||||||
--8<-- "docs_src/protocol_versions/tutorial001.py"
|
--8<-- "docs_src/protocol_versions/tutorial001.py"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -26,13 +34,14 @@ Either way you come out connected, and `client.protocol_version` tells you which
|
||||||
That is the whole feature. One `Client`, any era of server, no branching in your code.
|
That is the whole feature. One `Client`, any era of server, no branching in your code.
|
||||||
|
|
||||||
!!! info
|
!!! info
|
||||||
`MCPServer` answers `server/discover` on every transport — in-memory, stdio, streamable
|
`MCPServer` answers `server/discover` on every transport — Streamable HTTP, stdio, and the
|
||||||
HTTP — so against your own server `auto` always lands on `2026-07-28`. The fallback only
|
in-process connection your tests use — so against your own server `auto` always lands on
|
||||||
ever fires against a real pre-2026 server, which is exactly when you want it to.
|
`2026-07-28`. The fallback only ever fires against a real pre-2026 server, which is exactly
|
||||||
|
when you want it to.
|
||||||
|
|
||||||
## `mode="legacy"`
|
## `mode="legacy"`
|
||||||
|
|
||||||
```python title="client.py" hl_lines="14"
|
```python title="client.py" hl_lines="7"
|
||||||
--8<-- "docs_src/protocol_versions/tutorial002.py"
|
--8<-- "docs_src/protocol_versions/tutorial002.py"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -56,7 +65,7 @@ At 2026-07-28 it is gone. The server *returns* its questions and you retry the c
|
||||||
|
|
||||||
`mode` also accepts a modern protocol version string. Today that set is exactly `["2026-07-28"]`.
|
`mode` also accepts a modern protocol version string. Today that set is exactly `["2026-07-28"]`.
|
||||||
|
|
||||||
```python title="client.py" hl_lines="14"
|
```python title="client.py" hl_lines="7"
|
||||||
--8<-- "docs_src/protocol_versions/tutorial003.py"
|
--8<-- "docs_src/protocol_versions/tutorial003.py"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -89,7 +98,7 @@ The probe is cheap, but it is still a round trip you pay on every reconnect, and
|
||||||
|
|
||||||
So keep it. After an `auto` connection, `client.session.discover_result` holds the exact `DiscoverResult` the server sent: its `supported_versions`, its `capabilities`, its `instructions`, and the identity the server stamped into the result's `_meta`. Hand it back as `prior_discover=` the next time:
|
So keep it. After an `auto` connection, `client.session.discover_result` holds the exact `DiscoverResult` the server sent: its `supported_versions`, its `capabilities`, its `instructions`, and the identity the server stamped into the result's `_meta`. Hand it back as `prior_discover=` the next time:
|
||||||
|
|
||||||
```python title="client.py" hl_lines="15 17"
|
```python title="client.py" hl_lines="8 10"
|
||||||
--8<-- "docs_src/protocol_versions/tutorial004.py"
|
--8<-- "docs_src/protocol_versions/tutorial004.py"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -137,4 +137,4 @@ A browser-based client needs two permissions from you: to **send** its MCP reque
|
||||||
* Browser clients need CORS: `allow_headers` for the `Mcp-*` request headers, `expose_headers=["Mcp-Session-Id"]` for the response.
|
* Browser clients need CORS: `allow_headers` for the `Mcp-*` request headers, `expose_headers=["Mcp-Session-Id"]` for the response.
|
||||||
* `@mcp.custom_route()` adds plain, unauthenticated HTTP endpoints next to `/mcp`.
|
* `@mcp.custom_route()` adds plain, unauthenticated HTTP endpoints next to `/mcp`.
|
||||||
|
|
||||||
Once the server is reachable at a real URL, **[The Client](../client/index.md)** connects to it with that URL instead of a server object.
|
Once the server is reachable at a real URL, **[The Client](../client/index.md)** connects to it with that URL.
|
||||||
|
|
|
||||||
|
|
@ -124,6 +124,6 @@ An authorization server can also accept an enterprise identity provider's signed
|
||||||
* `token_verifier=` and `auth=AuthSettings(issuer_url=..., resource_server_url=..., required_scopes=[...])` always travel together.
|
* `token_verifier=` and `auth=AuthSettings(issuer_url=..., resource_server_url=..., required_scopes=[...])` always travel together.
|
||||||
* The SDK publishes [RFC 9728](https://datatracker.ietf.org/doc/html/rfc9728) Protected Resource Metadata at `/.well-known/oauth-protected-resource/...` and answers unauthenticated requests with a 401 whose `WWW-Authenticate` header points at it. That is the entire discovery story.
|
* The SDK publishes [RFC 9728](https://datatracker.ietf.org/doc/html/rfc9728) Protected Resource Metadata at `/.well-known/oauth-protected-resource/...` and answers unauthenticated requests with a 401 whose `WWW-Authenticate` header points at it. That is the entire discovery story.
|
||||||
* `get_access_token()` in any handler is who's calling.
|
* `get_access_token()` in any handler is who's calling.
|
||||||
* Authorization is an HTTP concern. `stdio` and the in-memory client never see it.
|
* Authorization is an HTTP concern. `stdio` and the in-memory test client never see it.
|
||||||
|
|
||||||
The client half (discovering your authorization server and fetching the token for you) is **[OAuth clients](../client/oauth-clients.md)**. And a client that *asserts* an identity instead of asking a user for one is **[Identity assertion](../client/identity-assertion.md)**.
|
The client half (discovering your authorization server and fetching the token for you) is **[OAuth clients](../client/oauth-clients.md)**. And a client that *asserts* an identity instead of asking a user for one is **[Identity assertion](../client/identity-assertion.md)**.
|
||||||
|
|
|
||||||
|
|
@ -16,15 +16,25 @@ So a legacy client is not something you build *for*. It is something that connec
|
||||||
|
|
||||||
## One handler, both eras
|
## One handler, both eras
|
||||||
|
|
||||||
Here is a tool that has to ask the user something, and both eras of client calling it:
|
Here is a tool that has to ask the user something:
|
||||||
|
|
||||||
```python title="server.py" hl_lines="24 37-38"
|
```python title="server.py" hl_lines="21"
|
||||||
--8<-- "docs_src/legacy_clients/tutorial001.py"
|
--8<-- "docs_src/legacy_clients/tutorial001.py"
|
||||||
```
|
```
|
||||||
|
|
||||||
`reserve` needs one thing the model didn't supply: how many copies. `Annotated[..., Resolve(ask_quantity)]` is how a tool declares that (**[Dependencies](../handlers/dependencies.md)** is that whole story). Nothing in `reserve` names a version, checks a capability, or branches.
|
`reserve` needs one thing the model didn't supply: how many copies. `Annotated[..., Resolve(ask_quantity)]` is how a tool declares that (**[Dependencies](../handlers/dependencies.md)** is that whole story). Nothing in `reserve` names a version, checks a capability, or branches.
|
||||||
|
|
||||||
The two clients are open **at the same time**, on the same `mcp` object. `mode="legacy"` runs the `initialize` handshake: the exact connection a pre-2026 client opens. The other one takes the default and lands on `2026-07-28`.
|
Serve it over HTTP, and here are both eras of client calling it:
|
||||||
|
|
||||||
|
```console
|
||||||
|
uv run mcp run server.py --transport streamable-http
|
||||||
|
```
|
||||||
|
|
||||||
|
```python title="client.py" hl_lines="14-15"
|
||||||
|
--8<-- "docs_src/legacy_clients/tutorial001_client.py"
|
||||||
|
```
|
||||||
|
|
||||||
|
The two clients are open **at the same time**, against the same running server. `mode="legacy"` runs the `initialize` handshake: the exact connection a pre-2026 client opens. The other one takes the default and lands on `2026-07-28`. Run `python client.py` from a second terminal:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
2025-11-25 {'result': "Reserved 2 of 'Dune'."}
|
2025-11-25 {'result': "Reserved 2 of 'Dune'."}
|
||||||
|
|
@ -115,7 +125,7 @@ Two things about it matter more than what it does.
|
||||||
|
|
||||||
!!! check
|
!!! check
|
||||||
Do the wrong thing. `reserve` is the exact tool that just served both clients. Deploy it with
|
Do the wrong thing. `reserve` is the exact tool that just served both clients. Deploy it with
|
||||||
`stateless_http=True`, connect the same two clients over HTTP, and call it from each.
|
`stateless_http=True`, connect the same two clients, and call it from each.
|
||||||
|
|
||||||
The modern client still gets `Reserved 2 of 'Dune'.` The modern leg didn't change.
|
The modern client still gets `Reserved 2 of 'Dune'.` The modern leg didn't change.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,12 @@ Several entries run against this one server. One tool and one templated resource
|
||||||
--8<-- "docs_src/troubleshooting/tutorial001.py"
|
--8<-- "docs_src/troubleshooting/tutorial001.py"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Those entries reach it at `http://localhost:8000/mcp`, so leave it running over HTTP:
|
||||||
|
|
||||||
|
```console
|
||||||
|
uv run mcp run server.py --transport streamable-http
|
||||||
|
```
|
||||||
|
|
||||||
The errors this page quotes are real: the SDK's own test suite reproduces every one of them.
|
The errors this page quotes are real: the SDK's own test suite reproduces every one of them.
|
||||||
|
|
||||||
## `ExceptionGroup: unhandled errors in a TaskGroup (1 sub-exception)`
|
## `ExceptionGroup: unhandled errors in a TaskGroup (1 sub-exception)`
|
||||||
|
|
@ -18,7 +24,7 @@ This is not an MCP error. It is anyio noise, and your real error is the **last l
|
||||||
|
|
||||||
```python
|
```python
|
||||||
async def main() -> None:
|
async def main() -> None:
|
||||||
async with Client(mcp) as client:
|
async with Client("http://localhost:8000/mcp") as client:
|
||||||
await client.read_resource("weather://Atlantis")
|
await client.read_resource("weather://Atlantis")
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -44,7 +50,7 @@ Two things to do with that:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
async def main() -> None:
|
async def main() -> None:
|
||||||
async with Client(mcp) as client:
|
async with Client("http://localhost:8000/mcp") as client:
|
||||||
try:
|
try:
|
||||||
await client.read_resource("weather://Atlantis")
|
await client.read_resource("weather://Atlantis")
|
||||||
except MCPError as e:
|
except MCPError as e:
|
||||||
|
|
@ -62,7 +68,7 @@ async def main() -> None:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
async def main() -> None:
|
async def main() -> None:
|
||||||
client = Client(mcp)
|
client = Client("http://localhost:8000/mcp")
|
||||||
tools = await client.list_tools() # RuntimeError
|
tools = await client.list_tools() # RuntimeError
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -70,7 +76,7 @@ Enter it. `__aenter__` is the connection:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
async def main() -> None:
|
async def main() -> None:
|
||||||
async with Client(mcp) as client:
|
async with Client("http://localhost:8000/mcp") as client:
|
||||||
tools = await client.list_tools()
|
tools = await client.list_tools()
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -270,7 +276,13 @@ One thing does **not** produce this error, despite being a request the modern pr
|
||||||
|
|
||||||
Your server wants to ask the user something, and this client never said it can be asked.
|
Your server wants to ask the user something, and this client never said it can be asked.
|
||||||
|
|
||||||
An elicitation resolver refuses up front when the connected client did not declare form elicitation, and `e.error.data` names exactly what is missing:
|
This Bistro asks before it books, through a resolver:
|
||||||
|
|
||||||
|
```python title="server.py" hl_lines="15-17 21"
|
||||||
|
--8<-- "docs_src/troubleshooting/tutorial007.py"
|
||||||
|
```
|
||||||
|
|
||||||
|
Serve it in place of the Weather server and call `book_table` from a client that passed no `elicitation_callback`. The resolver refuses up front, because the connected client never declared form elicitation, and `e.error.data` names exactly what is missing:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
|
|
@ -284,7 +296,7 @@ Pass `elicitation_callback=` to `Client(...)`. Registering the callback *is* the
|
||||||
|
|
||||||
```python
|
```python
|
||||||
async def main() -> None:
|
async def main() -> None:
|
||||||
async with Client(mcp, elicitation_callback=handle_elicitation) as client:
|
async with Client("http://localhost:8000/mcp", elicitation_callback=handle_elicitation) as client:
|
||||||
result = await client.call_tool("book_table", {"date": "Friday"})
|
result = await client.call_tool("book_table", {"date": "Friday"})
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -309,14 +321,14 @@ You see this one from `ctx.elicit()` on a legacy connection, and on any connecti
|
||||||
|
|
||||||
Your handler tried to reach the client mid-request, on a connection whose call has no channel that can carry a request from the server. There are three server configurations that put a call there.
|
Your handler tried to reach the client mid-request, on a connection whose call has no channel that can carry a request from the server. There are three server configurations that put a call there.
|
||||||
|
|
||||||
**A `2026-07-28` connection: any transport, always.** The modern protocol has no server-initiated requests at all, so the server refuses before anything is sent. `ctx.elicit()` inside a tool is the classic way to meet this (on the very first in-memory test, since `Client(server)` negotiates `2026-07-28` without being asked), and passing `elicitation_callback=` changes nothing, because no request ever reaches the client for it to answer:
|
**A `2026-07-28` connection: any transport, always.** The modern protocol has no server-initiated requests at all, so the server refuses before anything is sent. `ctx.elicit()` inside a tool is the classic way to meet this, usually in that tool's very first in-memory **[test](get-started/testing.md)**, since `Client(mcp)` negotiates `2026-07-28` without being asked. Passing `elicitation_callback=` changes nothing, because no request ever reaches the client for it to answer:
|
||||||
|
|
||||||
```python title="server.py" hl_lines="16"
|
```python title="server.py" hl_lines="16"
|
||||||
--8<-- "docs_src/troubleshooting/tutorial006.py"
|
--8<-- "docs_src/troubleshooting/tutorial006.py"
|
||||||
```
|
```
|
||||||
|
|
||||||
```python
|
```python
|
||||||
async def main() -> None:
|
async def test_book_table() -> None:
|
||||||
async with Client(mcp) as client:
|
async with Client(mcp) as client:
|
||||||
await client.call_tool("book_table", {"date": "Friday"})
|
await client.call_tool("book_table", {"date": "Friday"})
|
||||||
```
|
```
|
||||||
|
|
@ -358,7 +370,7 @@ The server could not verify the `requestState` token your client echoed back, so
|
||||||
|
|
||||||
```python
|
```python
|
||||||
async def main() -> None:
|
async def main() -> None:
|
||||||
async with Client(mcp) as client:
|
async with Client("http://localhost:8000/mcp") as client:
|
||||||
await client.call_tool("forecast", {"city": "London"}, request_state="round-1-from-worker-a")
|
await client.call_tool("forecast", {"city": "London"}, request_state="round-1-from-worker-a")
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -37,11 +37,11 @@ Not everything a tool needs should come from the model. New in v2, a tool parame
|
||||||
|
|
||||||
v1 handed you three nested layers: a transport context manager yielding raw streams, a `ClientSession` wrapped around them, and a hand-called `await session.initialize()`. v2 has one object:
|
v1 handed you three nested layers: a transport context manager yielding raw streams, a `ClientSession` wrapped around them, and a hand-called `await session.initialize()`. v2 has one object:
|
||||||
|
|
||||||
```python title="client.py" hl_lines="14-18"
|
```python title="client.py" hl_lines="7-11"
|
||||||
--8<-- "docs_src/client/tutorial001.py"
|
--8<-- "docs_src/client/tutorial001_client.py"
|
||||||
```
|
```
|
||||||
|
|
||||||
`Client` takes a server object (in memory, no transport: the testing story), a URL (Streamable HTTP), a `StdioServerParameters` (a stdio subprocess), or any other transport context manager such as `sse_client(...)`. Entering `async with` connects and negotiates the protocol version, whichever era the server speaks; `client.server_capabilities` and `client.protocol_version` are simply there afterwards, and `client.server_info` is too when the server identifies itself (it is `Implementation | None` now, since 2026-era identity is optional). The sampling and elicitation callbacks you registered in v1 still work (their bodies see the same snake_case attribute rename as everything else on this page), they now also answer the 2026-style requests-inside-results (below), and they run concurrently instead of one at a time. `ClientSession` is still underneath for anyone who wants the low-level surface, and `client.session` hands it to you; it moved too (it runs on the new dispatcher engine, and some of its own signatures changed), so read the **[Migration Guide](migration.md#clientsession-now-runs-on-jsonrpcdispatcher-basesession-removed)** before you drop down.
|
`Client` takes a URL (Streamable HTTP), a `StdioServerParameters` (a stdio subprocess), any other transport context manager such as `sse_client(...)`, or, in tests, the server object itself (in memory, no transport). Entering `async with` connects and negotiates the protocol version, whichever era the server speaks; `client.server_capabilities` and `client.protocol_version` are simply there afterwards, and `client.server_info` is too when the server identifies itself (it is `Implementation | None` now, since 2026-era identity is optional). The sampling and elicitation callbacks you registered in v1 still work (their bodies see the same snake_case attribute rename as everything else on this page), they now also answer the 2026-style requests-inside-results (below), and they run concurrently instead of one at a time. `ClientSession` is still underneath for anyone who wants the low-level surface, and `client.session` hands it to you; it moved too (it runs on the new dispatcher engine, and some of its own signatures changed), so read the **[Migration Guide](migration.md#clientsession-now-runs-on-jsonrpcdispatcher-basesession-removed)** before you drop down.
|
||||||
|
|
||||||
**[The Client](client/index.md)** introduces it, **[Client transports](client/transports.md)** covers the four connection forms, **[Client callbacks](client/callbacks.md)** covers the callbacks themselves, and **[Testing](get-started/testing.md)** shows the in-memory pattern that replaces v1's `create_connected_server_and_client_session()` helper.
|
**[The Client](client/index.md)** introduces it, **[Client transports](client/transports.md)** covers the four connection forms, **[Client callbacks](client/callbacks.md)** covers the callbacks themselves, and **[Testing](get-started/testing.md)** shows the in-memory pattern that replaces v1's `create_connected_server_and_client_session()` helper.
|
||||||
|
|
||||||
|
|
@ -166,11 +166,15 @@ Every server-initiated request is gone at 2026-07-28: push elicitation, sampling
|
||||||
|
|
||||||
The replacement turns the call around. A tool that needs something from the user *returns* the question (`InputRequiredResult`), the client answers it with the same callbacks it always had, and the call is retried with the answers attached. `Client` drives that loop for you. On the server you rarely build the result yourself, because a **[dependency](handlers/dependencies.md)** does it: annotate a parameter with `Resolve(ask_quantity)`, where `ask_quantity` is an ordinary function you write, and the SDK asks over whichever mechanism the connection supports, a live elicitation request on a legacy session or a multi-round-trip on 2026. One tool body, both eras:
|
The replacement turns the call around. A tool that needs something from the user *returns* the question (`InputRequiredResult`), the client answers it with the same callbacks it always had, and the call is retried with the answers attached. `Client` drives that loop for you. On the server you rarely build the result yourself, because a **[dependency](handlers/dependencies.md)** does it: annotate a parameter with `Resolve(ask_quantity)`, where `ask_quantity` is an ordinary function you write, and the SDK asks over whichever mechanism the connection supports, a live elicitation request on a legacy session or a multi-round-trip on 2026. One tool body, both eras:
|
||||||
|
|
||||||
```python title="dual_era.py" hl_lines="24 37-38"
|
```python title="server.py" hl_lines="21"
|
||||||
--8<-- "docs_src/legacy_clients/tutorial001.py"
|
--8<-- "docs_src/legacy_clients/tutorial001.py"
|
||||||
```
|
```
|
||||||
|
|
||||||
That file is the pitch in one place: one server, one `Resolve`-backed tool, and a legacy client plus a modern client both getting their answer, in memory. **[Multi-round-trip requests](handlers/multi-round-trip.md)** explains the mechanism (including `request_state`, which the SDK seals and verifies for you); **[Elicitation](handlers/elicitation.md)** covers the asking.
|
```python title="client.py" hl_lines="14-15"
|
||||||
|
--8<-- "docs_src/legacy_clients/tutorial001_client.py"
|
||||||
|
```
|
||||||
|
|
||||||
|
Those two files are the whole pitch: one server, one `Resolve`-backed tool, and a legacy client plus a modern client both getting their answer from the same running server (**[Serving legacy clients](run/legacy-clients.md)** walks through them). **[Multi-round-trip requests](handlers/multi-round-trip.md)** explains the mechanism (including `request_state`, which the SDK seals and verifies for you); **[Elicitation](handlers/elicitation.md)** covers the asking.
|
||||||
|
|
||||||
!!! warning "This is the one place a ported v1 server changes behavior"
|
!!! warning "This is the one place a ported v1 server changes behavior"
|
||||||
Your own tests hit it first: `Client(mcp)` negotiates 2026-07-28 against your v2 server by
|
Your own tests hit it first: `Client(mcp)` negotiates 2026-07-28 against your v2 server by
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue