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
Scope the construction-time requirement to resolver tools
The startup requirement now applies exactly where the SDK authors the
requestState content itself: tools with Resolve(...) parameters, whose
state carries elicited answers the server later trusts. Hand-built
requestState — a tool, prompt, or resource template returning
InputRequiredResult directly — is user-authored, so nothing is required
there: an unconfigured server passes it through verbatim, and the docs
make protection a clear recommendation instead. Configuring
request_state_security= still seals hand-built state on the three
carrier methods with no code changes.
Consequences: the boundary middleware is installed only when a policy
is supplied (no more unconfigured rejection paths), the unprotected()
opt-out is deleted (not configuring is the unprotected posture), and
the boundary scopes strictly to the carrier methods — requestState-
shaped members on custom methods belong to their own protocols and are
neither sealed nor policed. All cryptographic hardening, the claims
envelope, and resolver question-pinning are unchanged.
Copy file name to clipboardExpand all lines: docs/advanced/multi-round-trip.md
+12-13Lines changed: 12 additions & 13 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -35,12 +35,12 @@ Everything else in that file (the explicit `input_schema`, the hand-built `CallT
35
35
36
36
`tools/call` is not special: at 2026-07-28 a server may answer `prompts/get` and `resources/read` the same way. On `MCPServer`, an `@mcp.prompt()` function — or an `@mcp.resource()`**template** function — returns the `InputRequiredResult` itself and reads the retry's answers off the context:
37
37
38
-
```python title="server.py" hl_lines="6 21 23 25"
38
+
```python title="server.py" hl_lines="21 23 25"
39
39
--8<--"docs_src/mrtr/tutorial004.py"
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
-
*The `request_state_security=` argument is not optional: declaring an `InputRequiredResult` return means this server can mint a `requestState`, and `MCPServer` refuses to construct until you choose how to protect it. `ephemeral()` is the right answer for a single-process server like this one; **[Protecting `requestState`](#protecting-requeststate)** below covers what it does and the other choices.
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.
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,9 +89,9 @@ 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
-
This SDK is deliberately stricter than that conditional requirement: `MCPServer`refuses to construct at all while any registration can mint a `requestState` — a `Resolve(...)` parameter, or a tool, prompt, or resource-template function declaring an `InputRequiredResult` return — until you pass `request_state_security=`. The alternative is a server that runs fine in development and ships unprotected state the first time it matters.
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 — returning `InputRequiredResult` from a tool, prompt, or resourcetemplate — 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 — write plaintext, 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.
93
93
94
-
There are three choices:
94
+
There are two configurations:
95
95
96
96
```python
97
97
from mcp.server.mcpserver import MCPServer, RequestStateSecurity
*`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.
110
-
*`.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 tutorial servers in these docs all use it for that reason.
111
-
*`.unprotected()` sends state exactly as handlers wrote it and accepts whatever comes back. The spec permits this only when tampering can cause nothing worse than a failed request. `Resolve(...)`tools refuse this mode at registration: their state carries elicited answers, which are business inputs.
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.
108
+
*For your own crypto — a KMS, an existing token service — pass `RequestStateSecurity(codec=...)`instead of `keys`; **[Bring your own crypto](#bring-your-own-crypto)** below covers the contract.
112
109
113
110
### What the seal carries
114
111
115
-
With either of the first two choices, `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:
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:
116
113
117
114
***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.
118
115
***The authenticated client.** When the request carries an OAuth access token the SDK validated, the state is bound to that `client_id`: a token minted for one principal fails under another. 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.
@@ -153,11 +150,13 @@ Every inbound failure — tampered, expired, replayed against a different reques
153
150
{"code": -32602, "message": "Invalid or expired requestState"}
154
151
```
155
152
156
-
One frozen message for every cause, so the wire never reveals which check failed; the real reason goes to the server log. A server that never mints state at all — no MRTR registrations, no `request_state_security=` — rejects any inbound `requestState`the same way.
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.
157
154
158
155
### Hand-built state
159
156
160
-
A `request_state` you set yourself — returning `InputRequiredResult` from a tool, prompt, or resource-template function — is sealed and verified by the same machinery: write plaintext, read plaintext. The one thing the SDK cannot pin for you is question identity, because 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.
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.
158
+
159
+
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.
161
160
162
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 — one line, shown in **[The low-level Server](low-level-server.md#the-other-handlers)**.
163
162
@@ -185,6 +184,6 @@ The low-level `Server` is the no-batteries tier: nothing is required at construc
185
184
* To inspect or persist rounds, use `client.session.call_tool(..., allow_input_required=True)` and own the `while isinstance(result, InputRequiredResult)` loop yourself.
186
185
* 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.
187
186
* 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.
188
-
*`requestState` comes back as client-supplied input. `MCPServer` requires a `request_state_security=` choice before it will mint one, and the seal binds every token to a time window, the originating request, and — when the request carries auth the SDK validated, or `bind_principal=` supplies your own identity signal — the authenticated client (**[Protecting `requestState`](#protecting-requeststate)**).
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 — when the request carries auth the SDK validated, or `bind_principal=` supplies your own identity signal — the authenticated client (**[Protecting `requestState`](#protecting-requeststate)**).
189
188
190
189
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
+4-8Lines changed: 4 additions & 8 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -423,23 +423,19 @@ 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
-
### Servers that mint `requestState` must configure`request_state_security=`
426
+
### Tools with `Resolve(...)` parameters require`request_state_security=`
427
427
428
-
`requestState` round-trips through the client, so what comes back is client-supplied input. `MCPServer` now requires a protection choice at construction from any server that can mint one: registering a tool that uses `Resolve(...)` parameters, or a tool, prompt, or resource-template function that declares an `InputRequiredResult` return, raises `ValueError` until you pass `request_state_security=`. The one-line fix for a single-process server:
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(...)` parametersraises `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
429
430
430
```python
431
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, and `RequestStateSecurity.unprotected()` is the explicit opt-out for manual flows where tampering can cause nothing worse than a failed request (refused at registration for `Resolve(...)` tools). The choices, what gets sealed, key rotation, and custom codecs are covered in [Protecting `requestState`](advanced/multi-round-trip.md#protecting-requeststate).
436
+
Multi-instance deployments share secret keys instead (`RequestStateSecurity(keys=[...])`) so every instance can verify what a sibling minted. The choices, what gets sealed, key rotation, and custom codecs are covered in [Protecting `requestState`](advanced/multi-round-trip.md#protecting-requeststate).
437
437
438
-
Three behavior changes ride along:
439
-
440
-
* On a protected server, `ctx.request_state` returns the verified plaintext your handler originally wrote, not the wire token — sealing and verification happen at the wire boundary, so handler code reads exactly what it minted.
441
-
* A handler that returns an `InputRequiredResult` carrying `requestState` without having declared that return type — no annotation, or annotations the registration gate cannot resolve — on a server with no `request_state_security=` now answers `-32603`*"Internal error"* instead of shipping the state unprotected. The remediation goes to the server log: declare the return type, or configure `request_state_security=`.
442
-
* A server that never minted any state (no MRTR-capable registrations, no `request_state_security=`) now rejects any inbound `requestState` with `-32602`*"Invalid or expired requestState"* — the same frozen error every protected server answers when a token fails verification.
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.
443
439
444
440
### `call_tool` mirrors `x-mcp-header` arguments into `Mcp-Param-*` headers ([SEP-2243](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2243))
0 commit comments