Preflight Checklist
What's Wrong?
A remote (Streamable HTTP) MCP server sends a valid elicitation/create with mode: "form" during a tool call. Claude Code advertised elicitation.form at initialize, so the server enters the elicit path — then:
- No dialog is rendered.
- No response of any kind is sent back. The server waits its full 20s budget and fails with
MCP error -32001: Request timed out.
- The
Elicitation hook never fires, even though hooks are provably live at that instant.
Unlike #62319 / #79174 / #84207, this is not an auto-decline or a cancel. Those all deliver some result to the server promptly. Here the client is simply silent.
What Should Happen?
Per the docs: "elicitation dialogs appear automatically when a server requests them." A form dialog should render, or — failing that — the request should be declined promptly with a distinguishable result so the server can fall back, and the Elicitation hook should fire so the situation is observable.
Error Messages/Logs
Server side, matching to the millisecond on every attempt:
MCP add_note elicitation 실패 (team <redacted>, deal #<redacted>): MCP error -32001: Request timed out
POST /mcp duration 20019ms status 200
20019ms is our ELICITATION_TIMEOUT_MS = 20_000 expiring — not an early return.
Client side, hook probe log (see repro step 4):
=== LOADPROBE PostToolBatch 2026-08-10T05:56:13Z <- control fired, same batch as the tool call
(no "=== Elicitation" line)
(no "=== Notification" line)
Steps to Reproduce
- Connect a remote MCP server over Streamable HTTP whose write tool calls
elicitInput with mode: "form" (a single required boolean field, in our case).
- Confirm Claude Code advertised the capability:
getClientCapabilities()?.elicitation?.form is truthy server-side. (Our server has a separate branch for the unsupported case and it is not taken — so the capability was declared.)
- Invoke the tool. Approve the normal permission prompt when it appears.
- Instrument the client to distinguish "never received" from "received but not rendered":
"Elicitation": [
{ "matcher": "", "hooks": [ { "type": "command",
"command": "{ printf '=== Elicitation %s\\n' \"$(date -u +%Y-%m-%dT%H:%M:%SZ)\"; cat; } >> /tmp/probe.log" } ] }
],
"Notification": [
{ "matcher": "elicitation_dialog|elicitation_complete|elicitation_response", "hooks": [ { "type": "command",
"command": "{ printf '=== Notification %s\\n' \"$(date -u +%Y-%m-%dT%H:%M:%SZ)\"; cat; } >> /tmp/probe.log" } ] }
],
"PostToolBatch": [
{ "hooks": [ { "type": "command",
"command": "{ printf '=== LOADPROBE %s\\n' \"$(date -u +%Y-%m-%dT%H:%M:%SZ)\"; } >> /tmp/probe.log" } ] }
]
The Elicitation hook emits no stdout, so it registers no decision and does not suppress the dialog. PostToolBatch is a positive control: it fires per batch, so a LOADPROBE line carrying the same timestamp as the tool call proves the hook machinery was loaded and running at that moment. Without that control, "no log line" is ambiguous.
Result: LOADPROBE fires (including in the tool call's own batch). Elicitation and Notification never fire. 20s later the server reports -32001.
Environment
|
|
| Claude Code |
2.1.226 |
| Platform |
macOS (Darwin 25.5.0) |
| MCP transport |
Remote, Streamable HTTP (https://…/mcp) |
| Elicitation mode |
form, one required boolean field |
Ruled Out
Each varied independently; the failure is unchanged:
- Permission mode — reproduces in
auto and acceptEdits. The English permission prompt for the same tool call renders correctly in acceptEdits, so interactive prompting works in that very session; only the elicitation path is silent.
- Version — first seen on
2.1.224, reproduces identically on 2.1.226.
- Dead SSE stream — the standalone
GET /mcp stream was open (05:19:02 → 05:29:02) when the call fired at 05:28:01, and the POST itself was open for its whole 20s. Both candidate delivery channels were live.
- A hook swallowing it — no
Elicitation hook was configured for the original failures; the one added later is log-only and emits no stdout.
- Server not actually sending — the server's capability-unsupported branch returns immediately with a different message and is not taken; the 20019ms duration shows it sent and waited.
Additional Information
Possibly related and visible in the same logs, though not the cause here: every SSE GET /mcp stream terminates at ~599,9xx ms with Truncated response body, which is Cloud Run's 600s request cap. Since that stream is a delivery channel for server→client requests, an elicitation landing in the gap between stream death and client reconnect would fail with an identical signature. Our reproduction had a live stream, so this is a second, independent failure window rather than an explanation of this one.
One consequence worth flagging for triage: because the client returns nothing rather than a decline, a server cannot distinguish "user ignored the dialog" from "client never showed it." Both collapse into the same timeout, so fail-closed write gates become unusable rather than merely degraded.
Preflight Checklist
Elicitationhook never fires, which localizes the loss to before the client's elicitation handling. Also related: Elicitation hooks not triggered with --input-format stream-json #38755 (elicitation hooks not triggered under--input-format stream-json).2.1.226)What's Wrong?
A remote (Streamable HTTP) MCP server sends a valid
elicitation/createwithmode: "form"during a tool call. Claude Code advertisedelicitation.formatinitialize, so the server enters the elicit path — then:MCP error -32001: Request timed out.Elicitationhook never fires, even though hooks are provably live at that instant.Unlike #62319 / #79174 / #84207, this is not an auto-decline or a cancel. Those all deliver some result to the server promptly. Here the client is simply silent.
What Should Happen?
Per the docs: "elicitation dialogs appear automatically when a server requests them." A form dialog should render, or — failing that — the request should be declined promptly with a distinguishable result so the server can fall back, and the
Elicitationhook should fire so the situation is observable.Error Messages/Logs
Server side, matching to the millisecond on every attempt:
20019msis ourELICITATION_TIMEOUT_MS = 20_000expiring — not an early return.Client side, hook probe log (see repro step 4):
Steps to Reproduce
elicitInputwithmode: "form"(a single required boolean field, in our case).getClientCapabilities()?.elicitation?.formis truthy server-side. (Our server has a separate branch for the unsupported case and it is not taken — so the capability was declared.)The
Elicitationhook emits no stdout, so it registers no decision and does not suppress the dialog.PostToolBatchis a positive control: it fires per batch, so aLOADPROBEline carrying the same timestamp as the tool call proves the hook machinery was loaded and running at that moment. Without that control, "no log line" is ambiguous.Result:
LOADPROBEfires (including in the tool call's own batch).ElicitationandNotificationnever fire. 20s later the server reports-32001.Environment
2.1.226https://…/mcp)form, one required boolean fieldRuled Out
Each varied independently; the failure is unchanged:
autoandacceptEdits. The English permission prompt for the same tool call renders correctly inacceptEdits, so interactive prompting works in that very session; only the elicitation path is silent.2.1.224, reproduces identically on2.1.226.GET /mcpstream was open (05:19:02 → 05:29:02) when the call fired at05:28:01, and the POST itself was open for its whole 20s. Both candidate delivery channels were live.Elicitationhook was configured for the original failures; the one added later is log-only and emits no stdout.Additional Information
Possibly related and visible in the same logs, though not the cause here: every SSE
GET /mcpstream terminates at ~599,9xx mswithTruncated response body, which is Cloud Run's 600s request cap. Since that stream is a delivery channel for server→client requests, an elicitation landing in the gap between stream death and client reconnect would fail with an identical signature. Our reproduction had a live stream, so this is a second, independent failure window rather than an explanation of this one.One consequence worth flagging for triage: because the client returns nothing rather than a decline, a server cannot distinguish "user ignored the dialog" from "client never showed it." Both collapse into the same timeout, so fail-closed write gates become unusable rather than merely degraded.