Summary
On the self-hosted Cloudflare Worker host, an approval-gated tool call with ?elicitation_mode=native hangs until the MCP client times out. The server's elicitation/create request never reaches the client request handler.
Executor then converts the transport failure into action: "cancel", so the resulting error incorrectly says the request was cancelled by the user.
Environment
- Executor self-hosted on Cloudflare Workers / Durable Objects
- Console reports v1.5.35
- Pinned source commit:
b6bb4969a663e6a21ce251b76ba6bb79df482c55
- MCP transport: Streamable HTTP through the Cloudflare host
- Endpoint:
/mcp?elicitation_mode=native
- MCP client SDK:
@modelcontextprotocol/sdk 1.29.0
- Client:
dmmulroy/pi-mcp
Reproduction
- Connect an MCP client that advertises both form and URL elicitation capabilities and registers an
ElicitRequestSchema handler before client.connect(...).
- Connect to the self-hosted Cloudflare endpoint with
?elicitation_mode=native.
- Call Executor's
execute tool with code that invokes any tool annotated requiresApproval: true (for example, an OpenAPI POST operation).
- Wait for the native elicitation.
A minimal standalone client using the same pi-mcp manager as the interactive client produced:
CONNECT_STATUS {"status":"connected"}
MCP error -32001: Request timed out
The deterministic onElicitation callback was never invoked.
The same client repository's local and remote Streamable HTTP elicitation smoke tests pass, including form elicitations. This also reproduces outside the Pi TUI, ruling out its UI/context integration.
Expected behavior
Executor sends elicitation/create to the MCP client. The client responds with accept, decline, or cancel, and the tool call continues accordingly.
Actual behavior
No elicitation arrives at the client. The outer tools/call waits until the client timeout.
Cloudflare Worker logs then show:
[executor:quickjs] tool dispatch defect
Tool "...createTransaction" requires approval but the request was cancelled by the user.
cause._tag = "ElicitationDeclinedError"
cause.action = "cancel"
No upstream mutation was committed.
Relevant source path
packages/hosts/mcp/src/tool-server.ts:
- Native mode calls
engine.execute(..., { onElicitation: makeMcpElicitationHandler(...) }).
makeMcpElicitationHandler awaits server.server.elicitInput(...).
- Its catch block maps every transport/protocol failure to
{ action: "cancel" }.
That fallback explains the misleading user-cancelled error but obscures the underlying native-elicitation transport failure.
The Cloudflare host builds native mode through:
apps/host-cloudflare/src/mcp/agent-handler.ts
apps/host-cloudflare/src/mcp/session-durable-object.ts
I could not find a Cloudflare-host end-to-end test covering native elicitation. Existing native tests appear to exercise the generic/in-process MCP server path.
Additional observations
At session construction, Worker logs include:
{
"clientCapabilities": null,
"elicitationSupport": { "form": false, "url": false },
"elicitationMode": "native",
"resumeEnabled": false
}
That snapshot may be expected before MCP initialization, but it could be relevant if the Cloudflare session/server instance handling the later call does not retain the initialized client capabilities or request channel.
elicitation_mode=browser and elicitation_mode=model avoid this specific native request path and are usable workarounds.
Suggested regression coverage
Add a host-cloudflare integration/e2e test that:
- starts a Streamable HTTP MCP session in native mode;
- initializes a client with elicitation capabilities and a request handler;
- invokes an approval-gated tool;
- asserts the handler receives
elicitation/create and the accepted/declined response reaches the execution engine;
- asserts protocol/transport failures are surfaced distinctly rather than reported as user cancellation.
Summary
On the self-hosted Cloudflare Worker host, an approval-gated tool call with
?elicitation_mode=nativehangs until the MCP client times out. The server'selicitation/createrequest never reaches the client request handler.Executor then converts the transport failure into
action: "cancel", so the resulting error incorrectly says the request was cancelled by the user.Environment
b6bb4969a663e6a21ce251b76ba6bb79df482c55/mcp?elicitation_mode=native@modelcontextprotocol/sdk1.29.0dmmulroy/pi-mcpReproduction
ElicitRequestSchemahandler beforeclient.connect(...).?elicitation_mode=native.executetool with code that invokes any tool annotatedrequiresApproval: true(for example, an OpenAPI POST operation).A minimal standalone client using the same
pi-mcpmanager as the interactive client produced:The deterministic
onElicitationcallback was never invoked.The same client repository's local and remote Streamable HTTP elicitation smoke tests pass, including form elicitations. This also reproduces outside the Pi TUI, ruling out its UI/context integration.
Expected behavior
Executor sends
elicitation/createto the MCP client. The client responds withaccept,decline, orcancel, and the tool call continues accordingly.Actual behavior
No elicitation arrives at the client. The outer
tools/callwaits until the client timeout.Cloudflare Worker logs then show:
No upstream mutation was committed.
Relevant source path
packages/hosts/mcp/src/tool-server.ts:engine.execute(..., { onElicitation: makeMcpElicitationHandler(...) }).makeMcpElicitationHandlerawaitsserver.server.elicitInput(...).{ action: "cancel" }.That fallback explains the misleading user-cancelled error but obscures the underlying native-elicitation transport failure.
The Cloudflare host builds native mode through:
apps/host-cloudflare/src/mcp/agent-handler.tsapps/host-cloudflare/src/mcp/session-durable-object.tsI could not find a Cloudflare-host end-to-end test covering native elicitation. Existing native tests appear to exercise the generic/in-process MCP server path.
Additional observations
At session construction, Worker logs include:
{ "clientCapabilities": null, "elicitationSupport": { "form": false, "url": false }, "elicitationMode": "native", "resumeEnabled": false }That snapshot may be expected before MCP initialization, but it could be relevant if the Cloudflare session/server instance handling the later call does not retain the initialized client capabilities or request channel.
elicitation_mode=browserandelicitation_mode=modelavoid this specific native request path and are usable workarounds.Suggested regression coverage
Add a host-cloudflare integration/e2e test that:
elicitation/createand the accepted/declined response reaches the execution engine;