Skip to content

Bind requestState to the principal and the request it was minted for #623

Open

Description

@BrocksiNet

RequestStateCodec seals the requestState of a multi round-trip request with an HMAC and a TTL.
That makes it tamper-proof, but not tied to anything: a valid state verifies on any request within its TTL.
A client can echo a state minted for one call on another tool, on the same tool with other arguments, or under another user's token, and the server hands its payload to the handler as if it belonged there.

The 2026-07-28 specification asks for more than integrity (Multi Round-Trip Requests, Server Requirements):

To prevent replay, servers SHOULD include the following inside the integrity-protected requestState payload and verify each on receipt:

  • the authenticated principal, rejecting state presented by a different principal.
  • a short expiry (TTL), rejecting state presented after it lapses;
  • an identifier for the originating request, e.g. the method name and a digest of its salient parameters, rejecting state presented on a request that does not match.

The SDK covers the TTL.
A handler can put the principal and request identity into the payload itself and compare them on the retry, but every handler has to remember to, and ElicitationReplay (the state the SDK mints for ClientGateway::elicit() on the modern era) has no way to do it at all.

Proposal

A binder the server configures once, whose value the SDK covers with the MAC of every state it mints for a request and recomputes when it verifies the echoed state:

  • RequestStateBinderInterface::bind(Request $request, ?AccessToken $accessToken): string, called once per modern-era request with the request as the client sent it and the token it was authorized with.
  • an optional binder argument of Builder::setRequestState() to configure it.
  • A shipped OriginatingRequestBinder implementing what the specification recommends: the token's issuer, client and subject, the method, the tool or prompt name and a digest of its arguments, or the resource URI.
  • A mismatch fails exactly like a bad signature: -32602 "The supplied requestState failed verification." with HTTP 400.
  • Opt-in: without a binder, the wire format and verification stay as they are.

Precedents

  • TypeScript SDK: createRequestStateCodec({ key, bind }) takes an optional bind(ctx) callback whose value is HMAC'd into a tag in the envelope (requestStateCodec.ts).
  • Python SDK: RequestStateSecurity binds the principal (client, issuer, subject), the target (tool or prompt name, or resource URI) and a digest of the arguments, from a per-method allowlist (request_state.py, python-sdk#3032).

Context

Shopware (https://github.com/shopware/shopware) plans URL-mode elicitation on the 2026-07-28 era, for example to confirm an action out of band before a tool runs.
The requestState then carries what was confirmed, so it has to be bound to the user and the call that asked for it.

Activity

  1. added
    ServerIssues & PRs related to the Server component
    enhancementRequest for a new feature that's not currently supported
    2026-07-28All issues and PRs related to the spec release 2026-07-28
    on Oct 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    2026-07-28All issues and PRs related to the spec release 2026-07-28ServerIssues & PRs related to the Server componentenhancementRequest for a new feature that's not currently supported

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions