Loading
0x90Lesson 10 of 15

Ask the user mid-call: multi round-trip requests

Let a server request input or a model completion during a call, without keeping server-side state.

22 min 6-question quiz 2 code exercises
By the end of this lesson you can
  • Explain how a server asks for more input with input_required
  • Build the retry request with inputResponses and requestState
  • Protect requestState against tampering

Sometimes a tool can’t finish without help: it needs the user’s confirmation, a missing detail, or a quick model completion. In the stateless 2026-07-28 revision this uses multi round-trip requests (MRTR):

  1. The client calls the tool as usual.
  2. The server replies with resultType: "input_required", listing what it needs in inputRequests (each keyed by a name it picks), plus an opaque requestState.
  3. The client gathers the answers - by asking the user, say - and retries the original request (with a new id), adding inputResponses under the same keys and echoing requestState exactly.
  4. The server completes the call.

The request kinds a server can ask for are elicitation/create (ask the user, via a form), sampling/createMessage and roots/list - and only ones the client declared support for in its capabilities.

Try it

Booking that needs a confirmation

Follow a booking tool that asks the user to confirm the seat before buying. Predict each step.

Message 1 of 6Predicted 0/0
User
Host + client
MCP server

Never trust what comes back

Because requestState travels through the client, a malicious client could edit it - say, change the price. The spec says servers must treat it as attacker-controlled: if it affects authorization or business logic, protect it with an HMAC or authenticated encryption and reject anything that fails verification. Servers should also bind it to the user, give it a short expiry, and tie it to the original request.

sign_state.py
1import hashlib, hmac
2secret = b"server-only-secret"
3state = '{"flight": "TP123", "price": 120}'
4signature = hmac.new(secret, state.encode(), hashlib.sha256).hexdigest()[:16]
5tampered = '{"flight": "TP123", "price": 1}'
6print(hmac.compare_digest(signature, hmac.new(secret, tampered.encode(), hashlib.sha256).hexdigest()[:16]))
Output
False

Key takeaways

  • Servers ask for input with resultType: "input_required" and inputRequests; clients retry with inputResponses.

  • The retry uses a new id, the same keys, and the exact same requestState.

  • Servers must integrity-protect requestState and only request what the client supports.

Lesson quiz

6 questions · pass with 5 correct · up to 50 XP

Passing this quiz completes the lesson and keeps your streak going. Questions you miss come back in review sessions later.

Practice: write Python

Write Python in the editor and run it against sample inputs. Python runs locally in your browser using a WebAssembly runtime.

Exercise 1

Build the retry request

+25 XP

The first line is the original tools/call request (JSON). The second is the server’s input_required result (just the result object). The third is a JSON object of the user’s answers, keyed like inputRequests, each already shaped like {"action": "accept", "content": {...}}.

Print the retry request as compact JSON (json.dumps): a copy of the original with id set to the original id + 1, params.inputResponses set to the answers, and params.requestState set to the result’s requestState if it has one. If any input request has no answer, print missing KEY for the first such key (in inputRequests order) instead.

  • One answer with state
  • An unanswered request
main.py
Loading editor…

Python runs in a sandboxed browser worker with a 60 second time limit. Its runtime loads from the Pyodide CDN; your code stays in this browser.

Exercise 2

Verify request state

+25 XP

The first line is the server’s secret. Each following line is a requestState of the form PAYLOAD.SIGNATURE, where SIGNATURE should be the first 16 hex characters of HMAC-SHA256(secret, PAYLOAD). Print valid PAYLOAD or rejected for each line, comparing with hmac.compare_digest.

  • Genuine and tampered
  • Unsigned
main.py
Loading editor…

Python runs in a sandboxed browser worker with a 60 second time limit. Its runtime loads from the Pyodide CDN; your code stays in this browser.

Questions about this lesson

Stuck? Ask. Figured something out? Share it. Explaining is one of the best ways to learn.

Loading posts…

Did you like the lesson? 😆👍
Consider a donation to support our work: