request_state
Integrity protection for the multi-round-trip requestState (MCP 2026-07-28).
The spec requires servers to treat the client-echoed requestState as
attacker-controlled: RequestStateBoundary seals every outgoing value and
verifies every inbound echo, so handlers only ever see plaintext they minted.
InvalidRequestState
Bases: Exception
A sealed requestState token failed verification.
The message is a log-only reason code; the boundary never puts it on the wire.
Source code in src/mcp/server/request_state.py
46 47 48 49 50 | |
RequestStateCodec
Bases: Protocol
Authenticated crypto over the framework's request-state envelope.
The framework stamps and re-verifies every envelope claim (expiry, request binding, principal); a codec only provides integrity and, ideally, confidentiality (a sign-only codec leaves the payload client-readable).
Requirements: unseal(seal(payload)) round-trips, and unseal raises
InvalidRequestState for any token it did not mint unmodified; tokens
never name their algorithm (version with a format prefix bound under the
authentication tag, RFC 8725); comparisons are constant-time. Both methods
are synchronous, so cache key material rather than calling a KMS per token.
Source code in src/mcp/server/request_state.py
53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 | |
seal
Return an opaque URL-safe token protecting payload.
Source code in src/mcp/server/request_state.py
67 68 69 | |
unseal
Reverse seal.
Raises:
| Type | Description |
|---|---|
InvalidRequestState
|
Malformed, unauthentic, or unknown-key token. |
Source code in src/mcp/server/request_state.py
71 72 73 74 75 76 77 | |
authenticated_principal
authenticated_principal(
ctx: ServerRequestContext[Any, Any],
) -> str | None
Default principal binding: the authenticated (client, issuer, subject) identity.
Uses the same components session ownership uses, so two users of one OAuth
client are distinct principals whenever the token verifier supplies a
subject, and the binding degrades to the client identity when it does not.
Returns None (state not principal-bound) on unauthenticated transports.
Source code in src/mcp/server/request_state.py
80 81 82 83 84 85 86 87 88 89 90 91 | |
RequestStateSecurity
Policy for protecting requestState: codec, TTL, principal, audience.
Exactly one of keys or codec:
RequestStateSecurity(keys=[secret]) # built-in AES-256-GCM
RequestStateSecurity(codec=MyKmsCodec()) # bring your own crypto
RequestStateSecurity.ephemeral() # process-local key
keys is the rotation ring: keys[0] seals, every key unseals.
Zero-downtime rotation, each phase fully rolled out before the next:
keys=[old, new], then keys=[new, old], then keys=[new] after one TTL.
The boundary enforces expiry, request binding, audience, and principal for
every codec, fail-closed in both directions. audience=None defers to the
boundary's default_audience (MCPServer passes its server name).
Source code in src/mcp/server/request_state.py
94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 | |
ephemeral
classmethod
ephemeral(
*, ttl: float = 600.0, audience: str | None = None
) -> RequestStateSecurity
Protection under a key generated now and held only by this process.
This is the policy MCPServer installs when request_state_security=
is omitted; call it yourself on the lowlevel tier or to set ttl/
audience. Suits single-process deployments (stdio, one HTTP worker):
state minted before a restart or by another worker is rejected.
Multi-instance deployments must share a key via keys=[...].
Source code in src/mcp/server/request_state.py
139 140 141 142 143 144 145 146 147 148 149 | |
compact_json
Canonical JSON for everything the state path digests or seals.
ASCII output keeps the encode total: a lone surrogate in client-supplied
text escapes instead of raising. Anything consuming this must parse with
stdlib json.loads, which accepts those escapes (pydantic's JSON parser
does not).
Source code in src/mcp/server/request_state.py
159 160 161 162 163 164 165 166 167 | |
AESGCMRequestStateCodec
Built-in codec: AES-256-GCM under key(s) derived with HKDF-SHA256.
Tokens are encrypted, not merely signed, so clients cannot read the state.
keys[0] seals; all keys unseal (rotation, see RequestStateSecurity).
Each token carries a 4-byte non-secret key fingerprint for an O(1) ring
lookup, and the "v1." prefix and fingerprint are bound into the GCM
associated data, so a token cannot be replayed into another format version
or ring slot. Key bytes are copied at construction.
Source code in src/mcp/server/request_state.py
187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 | |
RequestStateBoundary
Server middleware sealing/unsealing requestState at the wire boundary.
Acts only on the multi-round-trip carriers (tools/call, prompts/get, resources/read); every other method passes through untouched.
Inbound state is verified (codec unseal plus claims check) and replaced
with the plaintext the server minted before any interceptor or handler
runs; failure answers -32602 with the frozen message "Invalid or expired
requestState", the real reason going to the server log only. Outbound, an
input_required result carrying requestState is sealed in a fresh
claims envelope; handlers and resolvers never call the codec.
default_audience seeds the audience claim when the policy sets none, and
must be stated explicitly: it is the service identity that stops state
minted by another service sharing the same keys. MCPServer installs this
middleware with its server name by default (under an ephemeral policy
unless request_state_security= supplies one); lowlevel Server users
append one to server.middleware, passing their server's name (or None
to deliberately leave tokens audience-free).
Source code in src/mcp/server/request_state.py
327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 | |