Sign MCP requestState for paid render approvals: user, TTL, digest
MCP says requestState is attacker-controlled. For a paid render approval, bind it to the user, an expiry and an argument digest with an HMAC. Python included.

Sign the state. The MCP 2026-07-28 spec says servers MUST treat requestState as attacker-controlled and protect its integrity with an HMAC or AEAD, and it recommends putting the principal, a TTL and a digest of the request inside the payload. For a paid render approval that means a user cannot replay an old approval, move it to another account, or change the prompt after agreeing to a cost. Source: the MRTR pattern page, read 2026-10-03.
What each field prevents
Each recommended field closes one specific replay. Leave one out and the matching attack works.
| Field | Attack it stops |
|---|---|
| Principal | Using one user's approval on another account |
| TTL | Replaying an approval days later at a changed price |
| Request digest | Editing the prompt or duration after the user agreed to the cost |
| HMAC or AEAD over all of it | Forging or editing any field |
Why idempotency keys are not approval
Sume's hosted MCP requires an idempotency_key on write and paid tools, and its docs describe it as a stable key for transport and dedup, not human approval. A repeated key protects against double submission. It does not prove a person agreed to a cost. If you want proof, you need a signed approval of your own, and requestState is a natural carrier.
The same docs offer dry_run for a cost preview and an optional max_spend_usd cap. Put the previewed estimate inside the signed payload so the approved number is the one you enforce.
A signing helper
The Python below builds and checks a state token. It refuses an empty secret, compares in constant time and rejects a mismatched principal, an expired token or changed arguments. Store the secret outside the code, and rotate it on a schedule.
import base64, hashlib, hmac, json, time
def _digest(args: dict) -> str:
return hashlib.sha256(json.dumps(args, sort_keys=True).encode()).hexdigest()
def make_state(secret: str, principal: str, args: dict, ttl: int = 600) -> str:
if not secret:
raise ValueError("empty secret")
body = json.dumps({"p": principal, "exp": int(time.time()) + ttl, "d": _digest(args)})
raw = base64.urlsafe_b64encode(body.encode()).decode()
mac = hmac.new(secret.encode(), raw.encode(), hashlib.sha256).hexdigest()
return f"{raw}.{mac}"
def check_state(secret: str, state: str, principal: str, args: dict) -> bool:
if not secret or "." not in state:
return False
raw, mac = state.rsplit(".", 1)
good = hmac.new(secret.encode(), raw.encode(), hashlib.sha256).hexdigest()
if not hmac.compare_digest(mac, good):
return False
body = json.loads(base64.urlsafe_b64decode(raw.encode()))
return body["p"] == principal and body["exp"] >= time.time() and body["d"] == _digest(args)Operational notes
Keep the TTL short; minutes, not hours. Derive the principal from the authenticated session, never from a field the client sends. Hash the arguments after you normalize them, so key order does not change the digest.
Log a failed check without logging the state itself. If you accept a state once, store its id to refuse a second use inside the TTL. And keep the approval separate from the credential: a Sume OAuth token is not an API key, and the docs warn against pasting either into prompts.
Sources
Related posts
More in Developers
- Snap a requested video length to supported durations in Python
Veo 3.1 accepts only 4, 6 or 8 seconds. A short Python helper snaps any requested length to a model's supported_durations list from GET /v1/videos/models.
- Sora ended in two steps: app in April, API on September 24
OpenAI's docs say the Sora API shut down Sep 24, 2026 with no direct replacement. A help-center snippet dates the app and web end at Apr 26. What to inventory.
- Sora replacement smoke test: one prompt, three Sume models, in Python
A short Python script that sends the same 5-second prompt to seedance-2.5, wan-3.0 and gemini-omni-flash-1.1 on Sume and prints status, cost and URLs.
- Sort your MCP tool allowlist: stable order keeps the prompt cache warm
MCP 2026-07-28 asks servers for ttlMs, cacheScope and a deterministic tool order. On the client, sort your media tool allowlist the same way. Python snippet.
Written by Sume