You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Every MCPServer now installs the request-state boundary under an
ephemeral process-local key when no policy is supplied, so resolver
state and hand-built state are sealed with zero configuration. The
resolver registration gate and its teaching error are gone: nothing
is required at construction anymore. One check remains, scoped to
explicit configuration: a supplied policy on a server without a real
name must set audience=, since shared keys are what make the audience
claim load-bearing.
Deployment guidance moves to the docs: multi-worker or load-balanced
servers (and state that must survive restarts) share keys=[...] or
bring a codec; the default key dies with the process and a
cross-instance retry gets the frozen rejection and restarts the flow.
Docs, tutorials, and stories drop the now-redundant ceremony; the
migration entry is removed since the surface is new on this line.
Copy file name to clipboardExpand all lines: docs/advanced/low-level-server.md
+1-1Lines changed: 1 addition & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -181,7 +181,7 @@ The handshake belongs to the runner. `server/discover`, `ping`, and every other
181
181
182
182
Each of these is one idea you now have the vocabulary for; each has its own chapter.
183
183
184
-
*`on_call_tool`, `on_get_prompt`, and `on_read_resource` may return an `InputRequiredResult` instead of their normal result to pause the call and ask the client for input; see **[Multi-round-trip requests](multi-round-trip.md)**. True to this tier, nothing is required at construction: the `request_state` you set crosses the wire exactly as written until you opt in with `server.middleware.append(RequestStateBoundary(RequestStateSecurity(keys=[...]), default_audience=server.name))`: one line (both names import from `mcp.server.request_state`) for the identical sealing and verification `MCPServer`enforces (**[Protecting `requestState`](multi-round-trip.md#protecting-requeststate)**).
184
+
*`on_call_tool`, `on_get_prompt`, and `on_read_resource` may return an `InputRequiredResult` instead of their normal result to pause the call and ask the client for input; see **[Multi-round-trip requests](multi-round-trip.md)**. True to this tier, nothing is installed for you: where `MCPServer` seals `requestState` by default, here the `request_state` you set crosses the wire exactly as written until you opt in with `server.middleware.append(RequestStateBoundary(RequestStateSecurity(keys=[...]), default_audience=server.name))`: one line (both names import from `mcp.server.request_state`) for the identical sealing and verification `MCPServer`performs (**[Protecting `requestState`](multi-round-trip.md#protecting-requeststate)**).
185
185
*`on_list_resources`, `on_read_resource`, `on_list_prompts`, `on_get_prompt`, `on_completion` are the same `(ctx, params) -> result` shape for the other primitives.
186
186
*`server.streamable_http_app()` returns the same Starlette app `MCPServer`'s does; deploy it the way **[Running your server](../run/index.md)** deploys any other ASGI app. There is no `server.run(transport=...)` down here: `server.run(read_stream, write_stream, server.create_initialization_options())` drives one connection over a pair of streams, and that one line is the whole story.
Copy file name to clipboardExpand all lines: docs/advanced/multi-round-trip.md
+12-15Lines changed: 12 additions & 15 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -40,7 +40,7 @@ Everything else in that file (the explicit `input_schema`, the hand-built `CallT
40
40
```
41
41
42
42
* The first round returns the `InputRequiredResult`. On the retry, `ctx.input_responses` holds the answers under the same keys and the function returns its ordinary result — prompt messages here, resource content for a template resource.
43
-
*Nothing extra is required to register this form: only `Resolve(...)` tools force a `request_state_security=` choice at construction. But if your function sets a `request_state`, what the client echoes back is client-supplied input; **[Protecting `requestState`](#protecting-requeststate)** below covers why you should configure protection anyway, and what you get when you do.
43
+
*A `request_state` you set is sealed before it crosses the wire and verified on the echo, like everything else on the server; **[Protecting `requestState`](#protecting-requeststate)** below covers what the seal gives you and when you need to configure keys.
44
44
* An `@mcp.tool()` function can return the result directly the same way, when the dependency form doesn't fit.
45
45
* Static `@mcp.resource()` functions don't participate: they take no `Context`, so they could never read the retry. Only template resources can ask.
46
46
* The era rules below apply unchanged: returning an `InputRequiredResult` on a pre-2026 session is the same `-32603` the warning describes.
@@ -89,27 +89,24 @@ Drop to the underlying session, where `allow_input_required=True` hands you the
89
89
90
90
Everything above treats `request_state` as an echo, and on the wire that is all it is. But the client holds it between legs (writing it down across processes is exactly what the previous section blessed), so what comes back is **client-supplied input**: it can be modified, expired, or lifted from a different call entirely. The spec requires servers to integrity-protect this state and reject the round when verification fails, whenever the state can influence authorization, resource access, or business logic.
91
91
92
-
The SDK requires a protection choice exactly where it authors the state itself: registering a `Resolve(...)` tool refuses to construct until you pass `request_state_security=`, because resolver state carries elicited answers the server will later trust. For state **you** build by returning `InputRequiredResult` from a tool, prompt, or resource template, nothing is required. But the echoed value is attacker-controlled input all the same, so you should configure protection there too: with `request_state_security=` set, your hand-built state is sealed and verified by the same machinery with zero code changes. You write plaintext and read plaintext. Without it, your state crosses the wire exactly as written, and the spec's integrity requirement is yours to satisfy: running unconfigured is a risk you accept, not a default the SDK chose for you.
92
+
`MCPServer` protects it by default. Every server seals outgoing `requestState` and verifies every echo — resolver state and hand-built state alike — under a key generated at process start. You configure nothing, write plaintext, and read plaintext; the wire only ever carries an opaque encrypted token.
93
93
94
-
There are two configurations:
94
+
The default key lives and dies with the process, which is the one thing you must know before deploying beyond a single process:
95
95
96
96
```python
97
97
from mcp.server.mcpserver import MCPServer, RequestStateSecurity
98
98
99
-
# Multi-instance: one or more shared secret keys (>= 32 bytes each).
99
+
# Multi-instance or restart-surviving: one or more shared secret keys (>= 32 bytes each).
*`keys=[...]` is the built-in encrypting codec under your secret(s). Required whenever a retry can reach a **different instance** (multi-worker or load-balanced HTTP), because every instance must be able to verify what any sibling minted.
107
-
*`.ephemeral()` generates the key at process start. State minted before a restart, or by another instance, is rejected and the client must start the flow over: right for a single process, wrong for a fleet. The resolver tutorials in these docs use it for that reason.
103
+
***The default (no configuration)** suits a single process: stdio, or exactly one HTTP worker. A retry that lands on a different worker, a different instance behind a load balancer, or the same server after a restart is sealed under a key that process doesn't have — the client gets the frozen rejection below and must start the flow over.
104
+
***`keys=[...]`** is required whenever a retry can reach a **different instance** (multi-worker `uvicorn`, load-balanced HTTP) or must survive restarts: every instance verifies what any sibling minted. Same machinery, your secret instead of a generated one.
108
105
* For your own crypto, such as a KMS or an existing token service, pass `RequestStateSecurity(codec=...)` instead of `keys`; **[Bring your own crypto](#bring-your-own-crypto)** below covers the contract.
109
106
110
107
### What the seal carries
111
108
112
-
With either built-in configuration, `requestState` on the wire is an encrypted, authenticated token. Your code never sees it: handlers and resolvers write plaintext and read plaintext (`ctx.request_state`); the SDK seals on the way out and verifies on the way in. Beyond integrity, each token is bound to:
109
+
Default or configured, `requestState` on the wire is an encrypted, authenticated token. Your code never sees it: handlers and resolvers write plaintext and read plaintext (`ctx.request_state`); the SDK seals on the way out and verifies on the way in. Beyond integrity, each token is bound to:
113
110
114
111
***A time window.** Every round re-seals with a fresh expiry, so `RequestStateSecurity(ttl=...)` (default 600 seconds) bounds per-round think time, not the whole flow.
115
112
***The authenticated principal.** When the request carries an OAuth access token the SDK validated, the state is bound to the token's client, issuer, and subject: state minted for one user fails under another, even when both users share one OAuth client. A verifier that supplies no subject degrades the binding to the client identity alone, which under URL-based client IDs is shared by every user of that client software. When auth is terminated outside the SDK (a fronting proxy), or the transport is unauthenticated, there is no principal to bind and this check is inert, unless `RequestStateSecurity(bind_principal=...)` supplies one from your own identity signal. Whichever components your token verifier supplies, it must supply them consistently: a verifier that includes the subject on some requests and omits it on others changes the principal mid-flow, and in-flight rounds are rejected.
@@ -130,7 +127,7 @@ RequestStateSecurity(keys=[NEW]) # 3: one ttl after phase 2 is fully out,
130
127
131
128
Never promote the minter first: minting under a key some instance can't yet verify drops in-flight rounds mid-rollout.
132
129
133
-
Keys are scoped to one service. The sealed envelope also carries the server's name as an audience claim, so a token minted by a different service that happens to share a secret is rejected anyway. The claim is only as distinctive as the name, which is why `MCPServer` refuses `request_state_security=` on an unnamed server. `RequestStateSecurity(audience=...)`overrides the claim for deliberate multi-service topologies where one service must accept state another minted.
130
+
Keys are scoped to one service. The sealed envelope also carries the server's name as an audience claim, so a token minted by a different service that happens to share a secret is rejected anyway. The claim is only as distinctive as the name, so a server given an explicit policy must have a real name or set `RequestStateSecurity(audience=...)`— an unnamed one raises at construction. `audience=` also serves deliberate multi-service topologies where one service must accept state another minted. (The no-configuration default is exempt: its key never leaves the process, so the audience claim has nothing to add.)
134
131
135
132
### Bring your own crypto
136
133
@@ -150,15 +147,15 @@ Every inbound failure, whether tampered, expired, replayed against a different r
150
147
{"code": -32602, "message": "Invalid or expired requestState"}
151
148
```
152
149
153
-
One frozen message for every cause, so the wire never reveals which check failed; the real reason goes to the server log. Verification is a configured server's behavior: with `request_state_security=` set, every inbound `requestState` on `tools/call`, `prompts/get`, and `resources/read` is checked, including one arriving for a handler that never mints state. Without it, nothing is checked: inbound state reaches your handler exactly as the client sent it.
150
+
One frozen message for every cause, so the wire never reveals which check failed; the real reason goes to the server log. Every inbound `requestState` on `tools/call`, `prompts/get`, and `resources/read` is checked, including one arriving for a handler that never mints state. The most common rejection in practice isn't an attacker — it's the default process-local key meeting a retry from before a restart or from another instance; the client restarts the flow, and `keys=[...]` is the fix when that matters.
154
151
155
152
### Hand-built state
156
153
157
-
A `request_state` you set yourself (returning `InputRequiredResult` from a tool, prompt, or resource-template function) never requires `request_state_security=`. Configure it anyway and your hand-built state is sealed and verified by the same machinery, with zero code changes: write plaintext, read plaintext, and every binding above applies. Don't, and the state crosses the wire exactly as written: whatever comes back is the client's word, and the spec's integrity requirement is yours to satisfy before you act on it.
154
+
A `request_state` you set yourself (returning `InputRequiredResult` from a tool, prompt, or resource-template function) is sealed and verified by the same machinery as resolver state, with zero code changes: write plaintext, read plaintext, and every binding above applies.
158
155
159
156
The one thing the SDK cannot pin for you, even when configured, is question identity: it doesn't know which of *your* questions an answer in your state belongs to. If you store answers keyed by question, include your own question identifier in the state and check it on the retry.
160
157
161
-
The low-level `Server` is the no-batteries tier: nothing is required at construction and nothing is sealed until you append the boundary yourself. The one-line opt-in is shown in **[The low-level Server](low-level-server.md#the-other-handlers)**.
158
+
The low-level `Server` is the no-batteries tier: unlike `MCPServer`, nothing is sealed until you append the boundary yourself, and your `request_state` crosses the wire exactly as written until you do. The one-line opt-in is shown in **[The low-level Server](low-level-server.md#the-other-handlers)**.
162
159
163
160
## A 2026-07-28 result
164
161
@@ -184,6 +181,6 @@ The low-level `Server` is the no-batteries tier: nothing is required at construc
184
181
* To inspect or persist rounds, use `client.session.call_tool(..., allow_input_required=True)` and own the `while isinstance(result, InputRequiredResult)` loop yourself.
185
182
* On `@mcp.tool()`, a dependency that asks the user produces this result for you (**[Dependencies](../tutorial/dependencies.md)**); the **low-level**`Server` is the manual form.
186
183
* Prompts and resources participate too: an `@mcp.prompt()` or template `@mcp.resource()` function returns the `InputRequiredResult` itself and reads `ctx.input_responses` on the retry.
187
-
*`requestState` comes back as client-supplied input. `MCPServer`requires a `request_state_security=` choice before it will register a `Resolve(...)`tool, and seals hand-built state with the same machinery once you configure it. The seal binds every token to a time window, the originating request, and the authenticated principal when the request carries auth the SDK validated or `bind_principal=` supplies your own identity signal (**[Protecting `requestState`](#protecting-requeststate)**).
184
+
*`requestState` comes back as client-supplied input, so `MCPServer`seals it by default — resolver state and hand-built state alike — under a process-local key; multi-instance deployments pass `RequestStateSecurity(keys=[...])`(or a custom codec) so every instance can verify what a sibling minted. The seal binds every token to a time window, the originating request, and the authenticated principal when the request carries auth the SDK validated or `bind_principal=` supplies your own identity signal (**[Protecting `requestState`](#protecting-requeststate)**).
188
185
189
186
This is the mechanism that replaces server-initiated sampling and the rest of the push-style back-channel; see **[Deprecated features](deprecated.md)**.
Copy file name to clipboardExpand all lines: docs/migration.md
-14Lines changed: 0 additions & 14 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -423,20 +423,6 @@ On the high-level `Client`, `call_tool`, `get_prompt`, and `read_resource` resol
423
423
424
424
On `ClientSession`, `call_tool` / `get_prompt` / `read_resource` still return the bare result and raise `RuntimeError` if the server requests input. Pass `allow_input_required=True` to receive the `InputRequiredResult` instead, then drive the loop yourself with `input_responses=` / `request_state=`. `ClientSessionGroup.call_tool` accepts the same flag.
425
425
426
-
### Tools with `Resolve(...)` parameters require `request_state_security=`
427
-
428
-
`requestState` round-trips through the client, so what comes back is client-supplied input. `MCPServer` now requires a protection choice where the SDK authors that state itself: registering a tool that uses `Resolve(...)` parameters raises `ValueError` until you pass `request_state_security=`, because resolver state carries elicited answers the server later trusts. The one-line fix for a single-process server:
429
-
430
-
```python
431
-
from mcp.server.mcpserver import MCPServer, RequestStateSecurity
Multi-instance deployments share secret keys instead (`RequestStateSecurity(keys=[...])`) so every instance can verify what a sibling minted. A configured server must also be named (or pass `RequestStateSecurity(audience=...)`): the name becomes the sealed token's audience claim, so an unnamed server raises `ValueError` at construction. The choices, what gets sealed, key rotation, and custom codecs are covered in [Protecting `requestState`](advanced/multi-round-trip.md#protecting-requeststate).
437
-
438
-
On a protected server the wire `requestState` is an opaque sealed token, and `ctx.request_state` returns the verified plaintext your handler originally wrote. Sealing and verification happen at the wire boundary, so handler code reads exactly what it minted. Hand-built `requestState` (a tool, prompt, or resource-template function returning `InputRequiredResult` itself) is unaffected unless you opt in, in which case it is sealed and verified automatically too.
439
-
440
426
### `call_tool` mirrors `x-mcp-header` arguments into `Mcp-Param-*` headers ([SEP-2243](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2243))
441
427
442
428
For protocol 2026-07-28 over Streamable HTTP, a tool's input-schema property may carry an `x-mcp-header` annotation. When a tool the client has listed is called, each annotated argument is mirrored into an `Mcp-Param-<name>` request header (string verbatim, integer as decimal, boolean as `true`/`false`, base64-sentinel-wrapped when not header-safe; `null`/absent arguments are omitted). The argument is also left in the request body. `list_tools` caches a tool's annotations, so list a tool before calling it to enable mirroring; a tool the client never listed emits no `Mcp-Param-*` headers. Other transports ignore the annotation.
Copy file name to clipboardExpand all lines: docs/tutorial/dependencies.md
+1-2Lines changed: 1 addition & 2 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -8,14 +8,13 @@ A tool's arguments come from the model. Some values never should: a price looked
8
8
9
9
Wrap the parameter's type in `Annotated[...]` and add `Resolve(fn)`:
10
10
11
-
```python title="server.py" hl_lines="8 18-19 23"
11
+
```python title="server.py" hl_lines="18-19 23"
12
12
--8<--"docs_src/dependencies/tutorial001.py"
13
13
```
14
14
15
15
*`check_stock` is a **resolver**: a plain function the SDK runs before `reserve_book`, whose return value becomes the `stock` argument.
16
16
* Its `title` parameter is the tool's own `title` argument, matched **by name**. The resolver sees exactly the validated value the tool body will see.
17
17
* The tool body starts from a `Stock` that already exists. No lookup code in the tool, no "what if it's missing" preamble.
18
-
*`request_state_security=` is the one piece of ceremony. A tool with resolvers can pause mid-call to ask the user (that's later in this chapter), and resuming sends a token through the client, so the SDK makes you choose how that token is protected before it will build the server. `ephemeral()`, a key generated at process start, is the right choice for a single-process server like this one; **[Protecting `requestState`](../advanced/multi-round-trip.md#protecting-requeststate)** has the full story.
19
18
20
19
!!! info
21
20
If you've used FastAPI, this is `Depends`. Same move, same reason: the function declares what
0 commit comments