Skip to content

Commit f18da95

Browse files
committed
docs: server identity moved to result _meta; clientInfo optional
Migration guide gets a grouped section for the spec #3002 reshape (field removal, stamping default and opt-out, Optional server_info, pair-only requests and the client_capabilities accessor). Whats-new, protocol-versions, client, session-groups, media, testing, and low-level pages updated to the new shapes.
1 parent a51c9f5 commit f18da95

8 files changed

Lines changed: 50 additions & 11 deletions

File tree

docs/advanced/low-level-server.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,10 +102,13 @@ Call it and the result carries both representations:
102102
"content": [{"type": "text", "text": "Found 3 books matching 'dune'."}],
103103
"structuredContent": {"matches": 3, "query": "dune"},
104104
"isError": false,
105-
"resultType": "complete"
105+
"resultType": "complete",
106+
"_meta": {"io.modelcontextprotocol/serverInfo": {"name": "Bookshop", "version": "2.0.0"}}
106107
}
107108
```
108109

110+
The `_meta` block is the server's identity stamp: the SDK adds it to every 2026-era result, with the `version` taken from the constructor or the installed package. Pass `include_server_info=False` to the server to turn it off.
111+
109112
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)**.
110113

111114
## `_meta`: for the application, not the model

docs/client/index.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ Everything else on this page is identical across all three. Headers, subprocesse
3030

3131
Four read-only properties, populated the moment you enter the block:
3232

33-
* `client.server_info`: the server's identity. `server_info.name` here is `"Bookshop"`, `server_info.version` is whatever the server reports.
33+
* `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.
3434
* `client.server_capabilities`: what the server can do (`tools`, `resources`, `prompts`, `completions`, ...). A capability the server doesn't have is `None`.
3535
* `client.protocol_version`: the protocol version the two sides agreed on. Here it is `"2026-07-28"`.
3636
* `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
202202
## Recap
203203

204204
* `Client(x)` connects in-memory to a server object, over Streamable HTTP to a URL string, and over anything else via a transport.
205-
* `async with` is the whole lifecycle. Inside it, `server_info`, `server_capabilities`, `protocol_version` and `instructions` are already populated.
205+
* `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.
206206
* `list_tools()` gives you each tool's `name`, `title`, `description` and `input_schema`.
207207
* `call_tool()` returns `content` for the model, `structured_content` for your code, and `is_error`. A raising tool is a result, not an exception.
208208
* `content` is a union of block types; narrow with `isinstance` before reading.

docs/client/session-groups.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ Run it again. `print(sorted(group.tools))` now shows both:
6464

6565
`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.
6666

67-
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.
67+
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.
6868

6969
## The classic handshake
7070

docs/get-started/testing.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,8 @@ async def client(): # (2)!
5959
@pytest.mark.anyio
6060
async def test_call_add_tool(client: Client):
6161
result = await client.call_tool("add", {"a": 1, "b": 2})
62+
# Ignore the server identity stamp in `_meta`; its `version` tracks the installed package.
63+
result.meta = None
6264
assert result == snapshot(
6365
CallToolResult(
6466
content=[TextContent(type="text", text="3")],

docs/migration.md

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -468,6 +468,38 @@ now a plain `dict[str, Any]`: pass a dict when constructing params
468468
and read extras with dictionary access (`params.meta["traceparent"]`) instead of
469469
attribute access. The JSON wire format is unchanged.
470470

471+
### Server identity moved from the `server/discover` result body to result `_meta`
472+
473+
The 2026-07-28 draft changed how identity travels
474+
([spec #3002](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/3002)):
475+
`serverInfo` is no longer a field of the `server/discover` result. Servers now
476+
report who they are by stamping `io.modelcontextprotocol/serverInfo` into the
477+
`_meta` of every 2026-era result, and the request-side `clientInfo` `_meta` key
478+
became optional (`protocolVersion` and `clientCapabilities` remain required).
479+
480+
What changed in the SDK:
481+
482+
- `DiscoverResult` no longer has a `server_info` field. Read or write identity
483+
through result `_meta`; the key is exported as `mcp_types.SERVER_INFO_META_KEY`.
484+
- Servers stamp `serverInfo` into every 2026-era result's `_meta` by default,
485+
built from the constructor identity fields (`name`, `version`, `title`, and
486+
so on). Pass `include_server_info=False` to `Server(...)` or `MCPServer(...)`
487+
to turn it off. A `serverInfo` value your handler already set in `_meta` is
488+
never overwritten. Handshake-era responses are unchanged, and notifications
489+
and error responses are never stamped.
490+
- `client.server_info` and `session.server_info` are `Implementation | None`
491+
on 2026-era connections: identity is optional on the wire, so a server that
492+
does not stamp it reads as `None`. Handshake-era connections still always
493+
have it. A pinned `Client(target, mode="2026-07-28")` without
494+
`prior_discover=` now reports `None` instead of an `Implementation` with
495+
empty strings. The stamp is display-only in the spec, so a malformed value
496+
from a misbehaving server also reads as `None` rather than failing the call.
497+
- Requests that omit `clientInfo` are now accepted. On the server,
498+
`ctx.session.client_params` is `None` for such requests; capability checks
499+
should use `ctx.session.client_capabilities` (or `ctx.client_capabilities`
500+
in `MCPServer` handlers), which is recorded whether or not the client
501+
identified itself.
502+
471503
### `SUPPORTED_PROTOCOL_VERSIONS` deprecated; `LATEST_PROTOCOL_VERSION` changed meaning
472504

473505
`SUPPORTED_PROTOCOL_VERSIONS` is deprecated — it's now the union of `HANDSHAKE_PROTOCOL_VERSIONS` (initialize-handshake versions) and `MODERN_PROTOCOL_VERSIONS` (per-request-envelope versions). If you were using it to mean "versions the initialize handshake accepts", switch to `HANDSHAKE_PROTOCOL_VERSIONS`. Named scalars derived from these tuples are now exported alongside them — `LATEST_HANDSHAKE_VERSION`, `LATEST_MODERN_VERSION`, `OLDEST_SUPPORTED_VERSION` — so prefer those over indexing the tuples directly. All of these live in `mcp_types.version` (previously `mcp.shared.version`): `from mcp_types.version import HANDSHAKE_PROTOCOL_VERSIONS`.
@@ -1498,7 +1530,7 @@ version = session.protocol_version
14981530

14991531
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`.
15001532

1501-
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.)
1533+
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` (see [Server identity moved to result `_meta`](#server-identity-moved-from-the-serverdiscover-result-body-to-result-_meta)). (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.)
15021534

15031535
### `cursor` parameter removed from `ClientSession` list methods
15041536

docs/protocol-versions.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -68,10 +68,10 @@ A pin is a promise *you* make: you already know the server speaks that version.
6868
A pin is not a discovery. Print `client.server_info` and the price is right there:
6969

7070
```text
71-
name='' title=None version='' description=None website_url=None icons=None
71+
None
7272
```
7373

74-
The client never asked the server who it is, so `server_info` is a blank. `client.server_capabilities`
74+
The client never asked the server who it is, so `server_info` is `None`. `client.server_capabilities`
7575
is the same story: every capability is `None`. Tool calls still work (the protocol needs none of it);
7676
code that reads `server_capabilities` to decide what to offer does not.
7777

@@ -87,7 +87,7 @@ ValueError: mode must be 'legacy', 'auto', or one of ['2026-07-28']; got '2025-0
8787

8888
The probe is cheap, but it is still a round trip you pay on every reconnect, and the answer almost never changes.
8989

90-
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:
90+
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:
9191

9292
```python title="client.py" hl_lines="15 17"
9393
--8<-- "docs_src/protocol_versions/tutorial004.py"
@@ -112,7 +112,7 @@ The second connection made **zero** negotiation round trips and still knows exac
112112
| --- | --- | --- |
113113
| `Client(target)` | one `server/discover` probe; the `initialize` handshake if it fails | the newest version both sides speak, whichever era |
114114
| `Client(target, mode="legacy")` | the `initialize` handshake | a handshake-era version; server-initiated requests work |
115-
| `Client(target, mode="2026-07-28")` | none | that version, pinned, with a blank `server_info` |
115+
| `Client(target, mode="2026-07-28")` | none | that version, pinned, with `server_info` as `None` |
116116
| `Client(target, mode="2026-07-28", prior_discover=saved)` | none | that version, pinned, *and* the identity you saved last time |
117117

118118
## Recap
@@ -121,7 +121,7 @@ The second connection made **zero** negotiation round trips and still knows exac
121121
* `mode="auto"` is the default: probe, fall back. Leave it alone unless one of the other three rows describes you.
122122
* `client.protocol_version` is always the answer to "what did I get?".
123123
* `mode="legacy"` forces the handshake. It is what you need for server-initiated requests: sampling, push elicitation, `message_handler`.
124-
* A version pin (`mode="2026-07-28"`) sends no negotiation traffic at all, at the cost of a blank `server_info`.
124+
* A version pin (`mode="2026-07-28"`) sends no negotiation traffic at all, at the cost of `client.server_info` being `None`.
125125
* `prior_discover=` pays that cost back: save `client.session.discover_result`, reconnect with it, get both.
126126

127127
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)**.

docs/servers/media.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,9 +97,10 @@ The same `icons=[...]` keyword is accepted by `MCPServer(...)`, `@mcp.tool()`, `
9797

9898
### Where a client sees them
9999

100-
Icons travel with whatever they decorate. The server's arrive when the client connects, on `client.server_info`:
100+
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):
101101

102102
```python
103+
assert client.server_info is not None # python-sdk servers identify themselves by default
103104
client.server_info.icons # [Icon(src="https://example.com/brand-kit.png", mime_type="image/png", sizes=["48x48"])]
104105
```
105106

docs/whats-new.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,7 @@ At 2026-07-28 the standalone HTTP GET stream and `resources/subscribe` are repla
197197

198198
### The rest, quickly
199199

200+
* **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 stamps by default and `include_server_info=False` turns it off; `client.server_info` is `None` when a server chooses not to identify itself. The **[Migration Guide](migration.md#server-identity-moved-from-the-serverdiscover-result-body-to-result-_meta)** has the details.
200201
* **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.
201202
* **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)**.
202203
* **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)**.

0 commit comments

Comments
 (0)