Agent Protocols Explained
← /learn · 05

Server to client: sampling and elicitation

When a server needs the user or a model, the direction reverses: a request from the server (2025-11-25) or an input_required result and a retry (2026-07-28), with the human gates in the host.

Loading the animation…

Concept

Most traffic goes from client to server: list this, call that. Sometimes the server, half-way through a tool call, needs something only the client side has. Elicitation asks the user for a decision or a value ("Delete build/?"). Sampling asks the client's model to write something, so a server can use a language model without holding its own API key. Both run through the host, which owns the user interface and the model, and both are gates the host controls.

2025-11-25: the direction reverses. The server sends its own JSON-RPC request to the client, elicitation/create or sampling/createMessage, with an id of its own (s1 in the recordings), while the client's tools/call stays open. The host shows the form or asks the model, the client answers with a result for that id, and only then does the server finish the call. Step through 2025-11-25: elicitation, accept: the arrows run right to left, and the client's request waits for 4 s of (simulated) human thinking with the server holding its state.

2026-07-28: input_required and a retry. Servers may no longer send requests to clients. Instead the call ends at once with resultType: "input_required" and the requests it needs in inputRequests; the host gathers the answers and sends the same call again, with a new id, carrying inputResponses (changelog, accessed 2026-10-08; it calls the pattern multi round-trip requests). Nothing waits on the server between the rounds, which is what makes the revision stateless. Compare the two accept sessions: 9 messages against 6, because the 2026-07-28 client does not need a handshake.

The gates. The protocol does not say how the host asks, but it says what the host must allow. For elicitation, clients MUST "Provide UI that makes it clear which server is requesting information" and "provide clear decline and cancel options", and servers "MUST NOT use form mode elicitation to request sensitive information such as passwords, API keys, access tokens"; those go through URL mode, which sends the user to a page outside the client (elicitation, accessed 2026-10-08). For sampling, "there SHOULD always be a human in the loop with the ability to deny sampling requests" (sampling, accessed 2026-10-08). The dashed purple arrows in the chart are those gates: inside the host, not on the wire. Try the decline sessions: the server gets "action": "decline" and keeps the file.

Maths

A call that needs rr answers costs, in both eras,

Nmsgs=2+2rN_{\text{msgs}} = 2 + 2r

messages: in 2025-11-25 a request and a result for the call plus a server request and a client result per answer; in 2026-07-28 one call and one input_required result per round plus the final call and result. The difference is in state and connections, not in count. In 2025-11-25 the server holds the call open for the whole human wait tht_h, and over HTTP that is one open SSE stream per waiting call: S=λ thS = \lambda\,t_h open streams for calls arriving at rate λ\lambda (Little's law). In 2026-07-28 that number is zero, and the retry can land on any server instance.

Code

The server's request to the client in 2025-11-25, as the engine and the SDK send it:

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "elicitation/create",
  "params": {
    "mode": "form",
    "message": "Delete build/?",
    "requestedSchema": {
      "type": "object",
      "properties": { "confirm": { "type": "boolean" } },
      "required": ["confirm"]
    }
  }
}

The same need in 2026-07-28: a result, not a request:

{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "inputRequests": {
      "confirm": {
        "method": "elicitation/create",
        "params": {
          "mode": "form",
          "message": "Delete build/?",
          "requestedSchema": {
            "type": "object",
            "properties": { "confirm": { "type": "boolean" } },
            "required": ["confirm"]
          }
        }
      }
    },
    "resultType": "input_required",
    "_meta": { "io.modelcontextprotocol/serverInfo": { "name": "fixture-server", "version": "1.0.0" } }
  }
}

The client's side of the retry, cut from the vendored engine:

while (r !== null && "result" in r && r.result.resultType === "input_required" && rounds < 8) {
  rounds += 1;
  client.state = "gathering input";
  const answers: Obj = {};
  for (const [key, ir] of Object.entries(r.result.inputRequests as Obj)) answers[key] = client.answer(ir as Obj);
  const retry: Obj = { inputResponses: answers, ...base };
  r = call("tools/call", retry, "the client retries with the answers");
}