Skip to content

Elicitation

Some servers need a person, not a model. smtp-mcp asks before it sends a message; imap-mcp asks before it expunges a mailbox. MCP calls that elicitation: the server puts a question in front of whoever is sitting at the client, and the model cannot answer on their behalf.

Behind a gateway that question used to have nowhere to go, and a server that cannot ask does not stop working — it falls back to something weaker. Both of those two fall back to a confirmation token returned inside a tool result, which the model reads and can satisfy by calling twice in the same turn. That still catches a mistake. It does not catch a model that has been talked into something.

On the 2026-07-28 revision the hub carries the question through, in both directions.

How it works

The reason this fits a stateless hub is that the question is not a push. On 2026-07-28 there is no server→client request channel at all: a server answers input_required instead of a result, and the call ends.

client ──tools/call──► hub ──────► child
                                     │ "may I really delete these?"
client ◄──input_required── hub ◄─────┘

                        ( the person reads it and decides )

client ──tools/call + the answer──► hub ──────► child
client ◄──────── the result ─────── hub ◄───────┘

Nothing is held open while somebody thinks. No request occupies one of the hub's concurrency slots, no timer runs, and the five-minute call deadline is never anywhere near being reached — the hub→child leg is two short calls, not one long one. Statelessness is the reason this works rather than an obstacle to it.

What the hub adds

The question was written by a child server and is shown to a human as though the hub were asking. That crosses a trust boundary, so four things happen to it on the way through, and each is a refusal rather than a repair.

It is attributed. The message is prefixed with Server "<name>" asks:, and the text is stripped of bidirectional overrides, zero-width and control characters first — otherwise a child could visually reverse the very line that names it.

Only questions travel. A child may embed other requests in an input_required; the hub carries elicitation/create and drops the rest. An embedded sampling/createMessage would spend the caller's model budget on a child's prompt, and roots/list would hand a child the client's workspace layout. Neither is something to relay on a child's say-so. What was dropped is named in the hub log.

The child's _meta is removed. A progress token or a related-task id belongs to the child's own id space; forwarded downstream it would collide with the client's.

The state is sealed. requestState goes out to the client and comes back, so the hub treats it as attacker-controlled: it is signed with the same secret that signs the login cookie and bound to the server, the tool, the OAuth client and the endpoint it was minted at. A state that does not open — expired, out of rounds, forged, or minted for a different call — is refused as a whole, without saying which part was wrong.

When the hub asks nothing

The hub only announces to a child that a question is answerable when it actually is. All four have to hold at once:

  1. the operator has not switched pass-through off, globally or for this server;
  2. the client declared elicitation in this request — which is a 2026-07-28 thing, so a 2025 client is ruled out here;
  3. the child negotiated 2026-07-28, so its answer can be a result;
  4. the child actually asked something.

The capability is mirrored from what the client declared for that one call and never widened. That is what keeps the announcement honest: it says only "the caller of this one call can answer you".

A 2025 client over HTTP therefore never sees an elicitation, and the child is told so rather than left to discover it — it takes its own fallback, which is the correct behaviour for a client that genuinely cannot be asked. Over stdio there is no such limit: mcp-hub-stdio is spawned per client session, so both eras reach a person.

Finding out why nothing was asked

A child's fallback is deliberately quiet, and that is the problem: a server that took its two-call token instead of asking looks exactly like a server that had nothing to ask. Two ways to tell them apart.

From the operator's side, the hub says it once per client, server and reason:

mcp-hub: [freshrss] elicitation not forwarded for client "…" — the caller
declared no elicitation capability for this request, so a question would have
nowhere to go

Once, not per call: a connector that cannot be asked calls all day and the answer never changes.

From inside the client, switch MCP_DIAGNOSTICS on and ask describe_connection — it reports the era, the capability this request carried, and for a named server whether a question would reach you.

Switching it off

Per server, in mcp.json:

json
{
  "mcpServers": {
    "imap": { "command": "npx", "args": ["-y", "@ni-c/imap-mcp"], "passthrough": "off" }
  }
}

"off" withdraws that upstream's right to put words in front of the user. It is a phishing judgement, not an availability one: the server keeps working and falls back on its own. "auto" is the default.

Globally, MCP_ELICITATION=false is the emergency brake. The rest of the limits exist so that one child cannot turn a prompt into a denial of service:

VariableDefaultWhat it bounds
MCP_ELICITATIONtruethe whole feature
MCP_ELICITATION_MAX_ROUNDS8how often one call may come back for more input
MCP_ELICITATION_STATE_TTL_MS900000how long a half-finished call stays resumable
MCP_ELICITATION_MAX_MESSAGE_BYTES4096one prompt — anything longer is not a prompt
MCP_ELICITATION_MAX_PAYLOAD_BYTES131072the whole question, schemas included

The round cap is enforced through the sealed state rather than a counter, because the hub keeps nothing between requests.

What is not logged

Never the question and never the answer. Both are text a person read or typed; what the log gets is the server name, the tool and the fact that something was dropped.

What this is worth, next to an annotation

A child's tool annotations travel through the hub unchanged, and they are a hint a client may ignore — the specification says so in as many words. Elicitation is not: the child raises the question, and until an answer comes back it does nothing.

That is why passing it through matters more than it looks. A server whose delete_* tool asks a person keeps asking behind the hub; one that is only annotated destructiveHint: true is relying on a client to notice. The servers in this family do the former — see, for instance, imap-mcp's approval guide — and each of them can be switched onto its two-call token fallback with ELICITATION=false if a dialog is the wrong shape for a deployment.

Note the two names are deliberately different. MCP_ELICITATION here switches off the hub's carrying of the question; ELICITATION in a child switches off that child's asking of it. Two layers, two switches — and neither of them turns a guarded call into an unguarded one.

Next

  • Hub meta-tools — what a child's annotations look like coming out of list_tools
  • Standards — the full capability matrix, per revision
  • Architecture — why the stateless transport helps here and hurts elsewhere
  • Configuration — where passthrough goes

Released under the MIT License. Not affiliated with Anthropic; “Claude” is a trademark of Anthropic PBC.