Skip to content

[BUG] Remote (Streamable HTTP) MCP form elicitation never reaches the client: no dialog, no Elicitation hook, server times out at -32001 #85442

Description

@yungjurick

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

  1. Connect a remote MCP server over Streamable HTTP whose write tool calls elicitInput with mode: "form" (a single required boolean field, in our case).
  2. 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.)
  3. Invoke the tool. Approve the normal permission prompt when it appears.
  4. 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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions