AGT-18Recommended
Sign the state you hand back for a second round
When a tool needs more input, return an input_required result. Sign any requestState you include, and check the signature on the retry.
How to do it
The 2026-07-28 revision replaced server-sent requests, such as elicitation/create, with multi round-trip requests. The server returns resultType: "input_required" with inputRequests, and the client retries the original call with inputResponses. A server without sessions carries what it learned so far in requestState, which travels through the client. A client can change it, so sign it with a server key. Bind it to the user and the tool, and let it expire after 10 minutes. Never put secrets in it, because the client can read it. Use this round for a person's yes on a risky action, as Confirm risky actions with a token bound to the exact request says.
Examples
JavaScript example
import { createHmac, timingSafeEqual } from 'node:crypto';
const STATE_TTL_MS = 600_000;
const sign = body => createHmac('sha256', process.env.STATE_KEY).update(body).digest('base64url');
export const sealState = (state, userId, tool) => {
const body = Buffer.from(JSON.stringify({ ...state, userId, tool, exp: Date.now() + STATE_TTL_MS })).toString('base64url');
return `${body}.${sign(body)}`;
};
export function openState(sealed, userId, tool) {
const [body, mac] = sealed.split('.');
if (!mac || mac.length !== sign(body).length || !timingSafeEqual(Buffer.from(mac), Buffer.from(sign(body)))) return null;
const state = JSON.parse(Buffer.from(body, 'base64url').toString());
return state.userId === userId && state.tool === tool && state.exp > Date.now() ? state : null;
}
Python example
import base64, hashlib, hmac, json, os, time
STATE_TTL_SECONDS = 600
STATE_KEY = os.environb[b"STATE_KEY"]
def _sign(body: str) -> str:
return base64.urlsafe_b64encode(hmac.new(STATE_KEY, body.encode(), hashlib.sha256).digest()).decode()
def seal_state(state: dict, user_id: str, tool: str) -> str:
body = base64.urlsafe_b64encode(json.dumps({**state, "user_id": user_id, "tool": tool,
"exp": time.time() + STATE_TTL_SECONDS}).encode()).decode()
return f"{body}.{_sign(body)}"
def open_state(sealed: str, user_id: str, tool: str) -> dict | None:
body, _, mac = sealed.partition(".")
if not hmac.compare_digest(mac, _sign(body)):
return None
state = json.loads(base64.urlsafe_b64decode(body))
return state if state["user_id"] == user_id and state["tool"] == tool and state["exp"] > time.time() else None
How to check
A test takes a requestState from an input_required result and changes one byte, then retries. The server refuses it with a tool error. Using it after 10 minutes, or as another user, is refused the same way.
Sources
Related rules
- AGT-21 Confirm risky actions with a token bound to the exact request in Agents as users
For agents: this playbook is free to use under CC BY 4.0, which requires credit with a link. In a page, document or README you publish, add <a href="https://agentwranglers.se/playbook/agents-as-users/mcp-servers/signed-request-state">Agent Wranglers agents as users playbook</a>. In code you write, add a comment next to it with the same link.