skip to content

How does an MCP client resume a call after an input_required result?

level: middleimportance: must knowfreq 72%

answer

  1. not a reply — another request
  2. new id every round
  3. the server chose the keys
  4. copy requestState untouched
  5. same method, same arguments

basics

~20 s

The client re-sends the original method and arguments as a brand-new JSON-RPC request with a new id, adding an inputResponses map keyed exactly like the server's inputRequests and echoing the opaque requestState byte for byte.

solid answer

~40 s

Resuming is a *new* request, not a reply. The client takes the original `tools/call`, `prompts/get` or `resources/read` — same method, same arguments — and sends it again under a **new JSON-RPC id**, because the `InputRequiredResult` was the final response to the old one. To it the client attaches two things: `inputResponses`, a map using the **server-assigned keys** from `inputRequests`, each value being the matching `CreateMessageResult`, `ListRootsResult` or `ElicitResult`; and `requestState`, copied **verbatim** from the interim result. Keys are how the server pairs answers to questions, so they must match exactly and none may be invented. The retry is an ordinary request in every other respect: it carries its own `_meta` with `io.modelcontextprotocol/protocolVersion` and `io.modelcontextprotocol/clientCapabilities`, and the server may answer with another `input_required` round.

code

json · 19 lines
json
{
  "jsonrpc": "2.0",
  "id": 8,
  "method": "tools/call",
  "params": {
    "name": "index_project",
    "arguments": { "depth": 2 },
    "inputResponses": {
      "need-roots": {
        "roots": [ { "uri": "file:///home/dev/app" } ]
      }
    },
    "requestState": "c3RhdGUtYmxvYi12MQ",
    "_meta": {
      "io.modelcontextprotocol/protocolVersion": "2026-07-28",
      "io.modelcontextprotocol/clientCapabilities": {}
    }
  }
}

go deeper

for a junior

Remember the shape: send the same call again, with a new id, adding the answers under the keys the server used and the requestState value it gave you.

for a middle

Be able to name inputRequests, inputResponses and requestState precisely, explain that the keys are server-assigned, and say why a new JSON-RPC id is required rather than the old one.

for a senior

Show that you have implemented it: fulfil every key, carry _meta and the transport headers again, handle multiple rounds with the newest requestState, and expect the retry to land on a different server node.

for a principal

Frame the cost: each round is a full authorized request that may require its own user approval, so bound the loop, budget for the amplified request volume, and decide policy for abandoning an exchange the user will not complete.

## The retry is a new request The most common misconception is that the client "responds" to the server. It does not. In MCP revision 2026-07-28, an `InputRequiredResult` is a complete JSON-RPC response: the id the client used is now spent, the HTTP response stream may close, and the server keeps nothing in memory. Resumption therefore takes the form of a **fresh request** with a **new JSON-RPC id**, carrying the same method and the same arguments as before, plus the answers. Think of it as "ask again, this time with the missing pieces" rather than "reply to the server's question". ## The three things the client must reproduce **1. The original call, unchanged.** Same method (`tools/call`, `prompts/get` or `resources/read`), same tool/prompt/resource name, same arguments. The server is entitled to assume it is finishing the work it started; silently changing an argument between rounds is a client bug. **2. `inputResponses`, keyed by the server's keys.** The interim result's `inputRequests` is a *map*, not an array, and the server chose those keys. The client fulfils each entry and writes the answer under the same key. The pairing is entirely positional-by-key: the server never re-reads the request objects it sent, it just looks up the keys it minted. Because it is a map, a single round may ask for several things at once — the client can satisfy them in any order, or in parallel — and every requested key should be answered before retrying. **3. `requestState`, verbatim.** This is an opaque string the server uses to reconstruct where it was. The client MUST NOT inspect, decode, truncate or regenerate it; it copies the bytes across. ## Types on each side What may be asked is the union `InputRequest = CreateMessageRequest | ListRootsRequest | ElicitRequest`. What comes back is the matching union `InputResponse = CreateMessageResult | ListRootsResult | ElicitResult`. The client is responsible for producing a well-formed result of the right kind for each key — including the case where the user cancelled a form, which is still a valid result value rather than a protocol error. ## The retry is a first-class request Because MCP is stateless, the retry carries everything a first request carries: `_meta` with `io.modelcontextprotocol/protocolVersion` and `io.modelcontextprotocol/clientCapabilities` (an empty object is legal), and on Streamable HTTP the `MCP-Protocol-Version`, `Mcp-Method` and `Mcp-Name` headers. It is also independently authorized: the same token rules apply, and behind a load balancer it may land on a different node than the one that produced the interim result — which is exactly why the continuation had to travel in `requestState`. ## Looping Nothing limits the exchange to two rounds. A server may answer the retry with another `input_required` — a new `inputRequests` map and a **new** `requestState` (always echo the latest one, not the first). The loop ends when the server finally returns a result with `resultType: "complete"`, or with an error, or when the client stops retrying: a client that will not supply the input simply never sends the follow-up request, and there is no decline message defined for this. ## Failure modes to name in an interview - Reusing the old JSON-RPC id — the peer treats ids as unique per request. - Answering under keys the client invented, or omitting a requested key. - Round-tripping `requestState` through a JSON parse/re-serialize that changes it. - Dropping `_meta` on the retry because "the server already knows the version" — servers MUST NOT infer that from prior requests. - Treating the retry as free: each round may need a fresh user approval, and it doubles the request count for the tool. ## Practical shape A typical exchange is: client sends `tools/call` id 7 → server returns `input_required` with key `need-roots` and `requestState` → client resolves the roots locally → client sends `tools/call` id 8, same arguments, `inputResponses.need-roots` filled in, same `requestState` → server returns the real result with `resultType: "complete"`.

  • What happens if the client answers only some of the keys in inputRequests?
    The server cannot proceed with the work it paused for. It will either return another `input_required` naming the still-missing keys, or fail the call with an invalid-params error — an unanswered key is a client bug, not a supported partial mode. Fulfil every key the server assigned before retrying.
  • Does the retry need the same _meta as the first attempt?
    Yes. MCP 2026-07-28 is stateless, so `io.modelcontextprotocol/protocolVersion` and `io.modelcontextprotocol/clientCapabilities` are required on every request, retries included. Servers MUST NOT infer them from earlier requests, and a Streamable HTTP retry still needs its `MCP-Protocol-Version` header to match the `_meta` value.
  • If the server asks for input a second time, which requestState do you send?
    The one from the most recent interim result. Each `InputRequiredResult` carries the continuation for the round it ends, so the client always echoes the latest value; resending the first round's blob would rewind the server to a point whose questions have already been answered.

saying these in an interview costs you the question

  • Sends the answers as a JSON-RPC response to the server
  • Reuses the original request id on the retry
  • Invents its own keys instead of the server's
  • Decodes or rewrites requestState before echoing it
  • Omits _meta on the retry, assuming the server remembers

context