This commit is contained in:
parent
3a6f2996cd
commit
837ef904f8
10 changed files with 1757 additions and 567 deletions
|
|
@ -133,14 +133,21 @@ or veto a tool call:
|
|||
```
|
||||
|
||||
* `params` is the validated `CallToolRequestParams`: you get `params.name` and
|
||||
`params.arguments` without touching raw JSON.
|
||||
* `call_next(ctx)` runs the rest of the chain. Return its result unchanged (observe),
|
||||
return something else (replace), or raise an `MCPError` (refuse).
|
||||
`params.arguments` without touching raw JSON. It is also what decides which
|
||||
tool call runs: passing a rewritten context through `call_next` changes what
|
||||
the handler observes on `ctx`, not the tool invocation. Wire-level request
|
||||
rewriting belongs to [Middleware](middleware.md).
|
||||
* `call_next(ctx)` runs the rest of the chain and returns the handler's result.
|
||||
Return it unchanged (observe), return something else (replace), or raise an
|
||||
`MCPError` (refuse). Whatever you return is serialized like any handler
|
||||
result, including the 2026-era `serverInfo` identity stamp, so a
|
||||
short-circuiting interceptor never produces an anonymous or off-schema
|
||||
response.
|
||||
* With several extensions, interceptors nest in registration order: the first
|
||||
extension in `extensions=[...]` is outermost.
|
||||
* The default implementation is a pass-through, and a server whose extensions never
|
||||
override this hook installs **no** middleware at all. You don't pay for what
|
||||
you don't use.
|
||||
override this hook keeps the bare `tools/call` handler untouched. You don't
|
||||
pay for what you don't use.
|
||||
|
||||
The hook wraps `tools/call` and nothing else. For every-message concerns, use
|
||||
[Middleware](middleware.md). That is what it is for.
|
||||
|
|
|
|||
|
|
@ -102,10 +102,13 @@ Call it and the result carries both representations:
|
|||
"content": [{"type": "text", "text": "Found 3 books matching 'dune'."}],
|
||||
"structuredContent": {"matches": 3, "query": "dune"},
|
||||
"isError": false,
|
||||
"resultType": "complete"
|
||||
"resultType": "complete",
|
||||
"_meta": {"io.modelcontextprotocol/serverInfo": {"name": "Bookshop", "version": "2.0.0"}}
|
||||
}
|
||||
```
|
||||
|
||||
The `_meta` block is the server's identity stamp: the SDK adds it to every 2026-era result, with the `version` from the constructor (a server that sets none reports an empty string). A server that must not identify itself can strip the key with a middleware, which owns the results it returns.
|
||||
|
||||
The server never compares the two fields. This SDK's `Client` does: return `structured_content` that doesn't satisfy the `output_schema` you declared and `call_tool` raises a `RuntimeError` that starts with `Invalid structured content returned by tool search_books` and goes on to quote the `jsonschema` failure. Promising a schema is cheap; keeping it is on you. The whole ladder of return types and schemas is in **[Structured Output](../servers/structured-output.md)**.
|
||||
|
||||
## `_meta`: for the application, not the model
|
||||
|
|
|
|||
|
|
@ -63,6 +63,10 @@ In increasing order of how much you should hesitate:
|
|||
`initialize`: the result the client gets back is built from your rewritten params, but the
|
||||
server commits its connection state from the original wire params. The two sides can finish
|
||||
the handshake disagreeing about what they negotiated.
|
||||
* **Answer.** Return a result without calling `call_next(ctx)` and it goes to the client as
|
||||
your response. `call_next` hands you the finished wire form, and the pipeline never patches
|
||||
what you return, so the whole envelope is yours: on a 2026-era connection that includes the
|
||||
`serverInfo` `_meta` stamp, which the SDK adds to handler results but not to yours.
|
||||
|
||||
!!! check
|
||||
`initialize` is one of the things middleware wraps, and it is the *only* hook you get
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ Everything else on this page is identical across all three. Headers, subprocesse
|
|||
|
||||
Four read-only properties, populated the moment you enter the block:
|
||||
|
||||
* `client.server_info`: the server's identity. `server_info.name` here is `"Bookshop"`, `server_info.version` is whatever the server reports.
|
||||
* `client.server_info`: the server's identity, or `None` for a 2026-era server that does not report one (python-sdk servers do by default). `server_info.name` here is `"Bookshop"`, `server_info.version` is whatever the server reports.
|
||||
* `client.server_capabilities`: what the server can do (`tools`, `resources`, `prompts`, `completions`, ...). A capability the server doesn't have is `None`.
|
||||
* `client.protocol_version`: the protocol version the two sides agreed on. Here it is `"2026-07-28"`.
|
||||
* `client.instructions`: the server's `instructions=` string, or `None` if it didn't set one.
|
||||
|
|
@ -202,7 +202,7 @@ There is one constructor flag built for that: `Client(mcp, raise_exceptions=True
|
|||
## Recap
|
||||
|
||||
* `Client(x)` connects in-memory to a server object, over Streamable HTTP to a URL string, and over anything else via a transport.
|
||||
* `async with` is the whole lifecycle. Inside it, `server_info`, `server_capabilities`, `protocol_version` and `instructions` are already populated.
|
||||
* `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`.
|
||||
* `call_tool()` returns `content` for the model, `structured_content` for your code, and `is_error`. A raising tool is a result, not an exception.
|
||||
* `content` is a union of block types; narrow with `isinstance` before reading.
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ Run it again. `print(sorted(group.tools))` now shows both:
|
|||
|
||||
`connect_to_server` returns the `ClientSession` it opened. Keep it if you ever want that server gone: `await group.disconnect_from_server(session)` removes its tools, resources, and prompts from the group.
|
||||
|
||||
If you already hold a connected `ClientSession` (`Client.session` is one), hand it to `await group.connect_with_session(server_info, session)` instead of opening a new transport. It aggregates the same way. The group never closes a session it didn't open.
|
||||
If you already hold a connected `ClientSession` (`Client.session` is one), hand it to `await group.connect_with_session(server_info, session)` instead of opening a new transport. It aggregates the same way. The group never closes a session it didn't open. `server_info` names the server for component prefixes; on a 2026-era connection `client.server_info` can be `None` (identity is optional), so pass your own `Implementation(name=..., version=...)` in that case.
|
||||
|
||||
## The classic handshake
|
||||
|
||||
|
|
|
|||
|
|
@ -59,6 +59,8 @@ async def client(): # (2)!
|
|||
@pytest.mark.anyio
|
||||
async def test_call_add_tool(client: Client):
|
||||
result = await client.call_tool("add", {"a": 1, "b": 2})
|
||||
# Drop the server identity stamp in `_meta`; it is not what this test is about.
|
||||
result.meta = None
|
||||
assert result == snapshot(
|
||||
CallToolResult(
|
||||
content=[TextContent(type="text", text="3")],
|
||||
|
|
|
|||
|
|
@ -606,6 +606,14 @@ mcp = MCPServer("Demo", instructions="You answer questions about the weather.")
|
|||
|
||||
Keep `name` positional and pass everything else by keyword.
|
||||
|
||||
### Unversioned servers report an empty version
|
||||
|
||||
In v1, a server constructed without a `version` reported the installed `mcp`
|
||||
package's version as its own in the `initialize` result's `serverInfo`. In v2
|
||||
it reports an empty string instead: the SDK's version is not your server's
|
||||
version. Pass `version="..."` to `Server(...)` or `MCPServer(...)` to identify
|
||||
your server properly. The field is display-only; nothing breaks either way.
|
||||
|
||||
### `mount_path` parameter removed from MCPServer
|
||||
|
||||
The `mount_path` parameter has been removed from `MCPServer.__init__()`, `MCPServer.run()`, `MCPServer.run_sse_async()`, and `MCPServer.sse_app()`. It was also removed from the `Settings` class.
|
||||
|
|
@ -1498,7 +1506,7 @@ version = session.protocol_version
|
|||
|
||||
The raw handshake result is also retained: `session.initialize_result` is set after `initialize()` (≤2025-11-25 servers — including `stateless_http=True` servers, which still answer `initialize`); `session.discover_result` is set after `discover()` (2026-07-28+ servers). At most one is non-`None`.
|
||||
|
||||
On the high-level `Client`, `client.server_capabilities`, `client.server_info`, and `client.protocol_version` are non-nullable inside the context manager. `client.instructions` remains `str | None` since the server may omit it. (The lowlevel `ClientSession` still lets you call methods before any handshake, as in v1; `Client` always connects on enter — by default it probes `server/discover` and falls back to the initialize handshake.)
|
||||
On the high-level `Client`, `client.server_capabilities` and `client.protocol_version` are non-nullable inside the context manager. `client.instructions` remains `str | None` since the server may omit it, and `client.server_info` is `Implementation | None`: on 2026-era connections identity is optional wire metadata, so a server that does not report it reads as `None`. (The lowlevel `ClientSession` still lets you call methods before any handshake, as in v1; `Client` always connects on enter — by default it probes `server/discover` and falls back to the initialize handshake.)
|
||||
|
||||
### `cursor` parameter removed from `ClientSession` list methods
|
||||
|
||||
|
|
|
|||
|
|
@ -68,10 +68,10 @@ A pin is a promise *you* make: you already know the server speaks that version.
|
|||
A pin is not a discovery. Print `client.server_info` and the price is right there:
|
||||
|
||||
```text
|
||||
name='' title=None version='' description=None website_url=None icons=None
|
||||
None
|
||||
```
|
||||
|
||||
The client never asked the server who it is, so `server_info` is a blank. `client.server_capabilities`
|
||||
The client never asked the server who it is, so `server_info` is `None`. `client.server_capabilities`
|
||||
is the same story: every capability is `None`. Tool calls still work (the protocol needs none of it);
|
||||
code that reads `server_capabilities` to decide what to offer does not.
|
||||
|
||||
|
|
@ -87,7 +87,7 @@ ValueError: mode must be 'legacy', 'auto', or one of ['2026-07-28']; got '2025-0
|
|||
|
||||
The probe is cheap, but it is still a round trip you pay on every reconnect, and the answer almost never changes.
|
||||
|
||||
So keep it. After an `auto` connection, `client.session.discover_result` holds the exact `DiscoverResult` the server sent: its `supported_versions`, its `capabilities`, its `server_info`, its `instructions`. 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"
|
||||
--8<-- "docs_src/protocol_versions/tutorial004.py"
|
||||
|
|
@ -112,7 +112,7 @@ The second connection made **zero** negotiation round trips and still knows exac
|
|||
| --- | --- | --- |
|
||||
| `Client(target)` | one `server/discover` probe; the `initialize` handshake if it fails | the newest version both sides speak, whichever era |
|
||||
| `Client(target, mode="legacy")` | the `initialize` handshake | a handshake-era version; server-initiated requests work |
|
||||
| `Client(target, mode="2026-07-28")` | none | that version, pinned, with a blank `server_info` |
|
||||
| `Client(target, mode="2026-07-28")` | none | that version, pinned, with `server_info` as `None` |
|
||||
| `Client(target, mode="2026-07-28", prior_discover=saved)` | none | that version, pinned, *and* the identity you saved last time |
|
||||
|
||||
## Recap
|
||||
|
|
@ -121,7 +121,7 @@ The second connection made **zero** negotiation round trips and still knows exac
|
|||
* `mode="auto"` is the default: probe, fall back. Leave it alone unless one of the other three rows describes you.
|
||||
* `client.protocol_version` is always the answer to "what did I get?".
|
||||
* `mode="legacy"` forces the handshake. It is what you need for server-initiated requests: sampling, push elicitation, `message_handler`.
|
||||
* A version pin (`mode="2026-07-28"`) sends no negotiation traffic at all, at the cost of a blank `server_info`.
|
||||
* A version pin (`mode="2026-07-28"`) sends no negotiation traffic at all, at the cost of `client.server_info` being `None`.
|
||||
* `prior_discover=` pays that cost back: save `client.session.discover_result`, reconnect with it, get both.
|
||||
|
||||
A modern connection has no push channel, so how does a 2026 server ask you a question mid-call? It returns it: **[Multi-round-trip requests](handlers/multi-round-trip.md)**.
|
||||
|
|
|
|||
|
|
@ -97,9 +97,10 @@ The same `icons=[...]` keyword is accepted by `MCPServer(...)`, `@mcp.tool()`, `
|
|||
|
||||
### Where a client sees them
|
||||
|
||||
Icons travel with whatever they decorate. The server's arrive when the client connects, on `client.server_info`:
|
||||
Icons travel with whatever they decorate. The server's arrive when the client connects, on `client.server_info` (optional on 2026-era connections, so narrow it first):
|
||||
|
||||
```python
|
||||
assert client.server_info is not None # python-sdk servers identify themselves by default
|
||||
client.server_info.icons # [Icon(src="https://example.com/brand-kit.png", mime_type="image/png", sizes=["48x48"])]
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ v1 handed you three nested layers: a transport context manager yielding raw stre
|
|||
--8<-- "docs_src/client/tutorial001.py"
|
||||
```
|
||||
|
||||
`Client` takes a server object (in memory, no transport: the testing story), a URL (Streamable HTTP), or any transport context manager such as `stdio_client(...)`. Entering `async with` connects and negotiates the protocol version, whichever era the server speaks; `client.server_info`, `client.server_capabilities`, and `client.protocol_version` are simply there afterwards. 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 server object (in memory, no transport: the testing story), a URL (Streamable HTTP), or any transport context manager such as `stdio_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.
|
||||
|
||||
**[The Client](client/index.md)** introduces it, **[Client transports](client/transports.md)** covers the three 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.
|
||||
|
||||
|
|
@ -197,6 +197,7 @@ At 2026-07-28 the standalone HTTP GET stream and `resources/subscribe` are repla
|
|||
|
||||
### The rest, quickly
|
||||
|
||||
* **Identity is optional, per-message metadata.** The request-side `clientInfo` `_meta` key is optional (the required pair is `protocolVersion` + `clientCapabilities`), and `serverInfo` moved out of the `server/discover` result body: servers stamp it into every 2026-era result's `_meta` instead (since `2.0.0b3`; [spec #3002](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/3002)). The SDK always stamps; `client.server_info` is `None` when a server does not identify itself (for example, a middleware stripped the key). **[The low-level Server](advanced/low-level-server.md)** shows the stamp on the wire.
|
||||
* **Requests are routable without parsing bodies.** Modern HTTP requests carry `Mcp-Method` (and, for the three tool-ish calls, `Mcp-Name`); a tool input-schema property annotated with `x-mcp-header` is mirrored into an `Mcp-Param-*` header and cross-checked by the server ([SEP-2243](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2243)). Gateways and rate limiters can route on headers alone; the **[Migration Guide](migration.md#servers-validate-mcp-param-headers-against-the-request-body-sep-2243)** has the rules.
|
||||
* **Results carry cache hints.** List and read results declare `ttlMs` and `cacheScope` ([SEP-2549](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2549)); you set them per method with `cache_hints=`, and `Client` honors them with a built-in response cache. A server that sends no hints (every pre-2026 server) sees identical, uncached traffic. **[Caching hints](client/caching.md)**.
|
||||
* **Extensions are first class.** Servers and clients declare optional capability bundles under reverse-DNS identifiers ([SEP-2133](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2133)); the built-in `Apps` extension (MCP Apps) is the reference. **[Extensions](advanced/extensions.md)** and **[MCP Apps](advanced/apps.md)**.
|
||||
|
|
|
|||
Loading…
Reference in a new issue