Agent Wranglers

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

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.