You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
[FEATURE]: MCP Client advertises elicitation.form capability but never handles elicitation/create requests, causing tool calls to hang and timeout #51856
I have verified this feature I'm about to request hasn't been suggested before.
Describe the enhancement you want to request
Problem
OpenCode declares elicitation capability (including elicitation.form) during the MCP initialize handshake with remote MCP servers. However, when an MCP server sends an elicitation/create request back to OpenCode, it is never handled — the request hangs indefinitely until the server-side dispatch timeout kills the tool call (typically 30 seconds).
This affects any MCP tool that uses server.elicitInput() to get user confirmation before performing a mutating action. These tools work correctly in Claude CLI and Hermes (which properly handle elicitation/create and display a confirmation prompt to the user), but always fail in OpenCode with a timeout error. Steps to reproduce
Configure an MCP server in opencode.json that has tools using server.elicitInput() for user approval:
Call any MCP tool that invokes server.elicitInput() (e.g., a tool that asks "Are you sure you want to update this config?")
The tool call hangs for ~30 seconds and then returns:
MCP error -32001: MCP target timed out after 30000ms: /mcp:tool/ Expected behavior
OpenCode should either:
Handle elicitation/create requests — display the elicitation form/prompt to the user (similar to how permission prompts work), collect their response, and send it back to the MCP server. This is what Claude CLI and Hermes do.
Or, do not advertise elicitation capability — if OpenCode doesn't support it, it should not declare elicitation in clientCapabilities during initialize. MCP servers that check server.getClientCapabilities()?.elicitation?.form before calling elicitInput() would then gracefully skip the prompt. The MCP SDK itself throws "Client does not support form elicitation." when the capability is absent, which servers can catch and handle.
Option 1 is strongly preferred since elicitation is increasingly used by MCP servers for confirmation flows on destructive/mutating operations. Environment
OpenCode version: 2.0.18
MCP SDK version on server: @modelcontextprotocol/sdk 1.30.0
Update the title to start with the required [FEATURE]: prefix (currently it starts with [FEATURE-MCP]:).
Replace “latest” with the exact OpenCode version, and add the operating system and client/interface (TUI, Desktop, or Web) to help reproduce the issue.
Please edit this issue to address the above within 2 hours, or it will be automatically closed.
This issue might be a duplicate of existing issues. Please check:
#23066: Tracks MCP elicitation support, including server-initiated elicitation during tool calls.
#28567: Tracks gaps in OpenCode’s MCP client capabilities, including elicitation.
If you believe this was flagged incorrectly, please let a maintainer know.
changed the title [-][FEATURE-MCP]: Client advertises elicitation.form capability but never handles elicitation/create requests, causing tool calls to hang and timeout[/-][+][FEATURE]: MCP Client advertises elicitation.form capability but never handles elicitation/create requests, causing tool calls to hang and timeout[/+]on Sep 28, 2026
Thanks for updating the title. The issue still needs reproducibility details in the Environment section: please provide the exact OpenCode version (rather than "latest"), operating system, and client/interface (TUI, Desktop, or Web). The needs:compliance label will remain until those details are added.
Thanks for updating the issue. The title, feature verification checkbox, description, and exact OpenCode version are now provided. To complete the Environment details and make the report reproducible, please add your operating system and whether you’re using the TUI, Desktop, or Web interface. The needs:compliance label will remain until those details are added.
Thanks for updating the issue. To complete the system details and make the report reproducible, please add your operating system and whether you’re using the TUI, Desktop, or Web interface in the Environment section. The needs:compliance label will remain until those details are added.
Thanks for updating the issue. The feature title, verification checkbox, description, reproduction steps, and exact OpenCode version are now present. The Environment section lists “OpenCode Terminal (CLI)” as the operating system, but that describes the client/interface rather than the OS. Please add the actual operating system (for example, macOS, Linux, or Windows) so the system details are complete. The needs:compliance label will remain until then.
Same bug over stdio, and still present in 2.0.24 (also seen on 2.0.11). Here is what's going on, from the JSON-RPC traffic and the shipped binary.
The request is received and turned into a form, but the form is owned by "global" rather than the calling session. The MCP elicitation handler calls ask({ sessionID: <const "global">, title: "<server> is requesting input", metadata: { kind: "mcp-elicitation", … } }), and the session view never renders forms owned by "global". So the tool call waits until something times it out. In the TUI I watched it sit for several minutes. opencode run cancels it straight away ({"action":"cancel"}).
The session that should own it is already known. opencode sends it on every call: tools/call carries _meta: {"ai.opencode/sessionID": "ses_…"}. Using the session of the in-flight tools/call that the elicitation/create belongs to would route the prompt to the view that's waiting on it.
Repro (stdio, 2.0.24, macOS arm64, TUI)
Run any local MCP server whose tool asks for a form before acting, e.g. a tool that returns a confirmed: boolean form elicitation.
Model calls the tool (through Code Mode execute → tools.<server>.<tool>(…)).
Captured traffic:
→ {"method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{"elicitation":{"form":{"applyDefaults":true},"url":{}},"roots":{}},"clientInfo":{"name":"cli","version":"2.0.24"}}, …}
→ {"method":"tools/call","params":{"name":"…","arguments":{…},"_meta":{"ai.opencode/sessionID":"ses_…","progressToken":2}},"id":2}
← {"method":"elicitation/create","params":{"message":"Review and confirm: …","requestedSchema":{"type":"object","properties":{"confirmed":{"type":"boolean"}},"required":["confirmed"]},"mode":"form"},"id":0}
(no prompt appears in the session; no response to id 0; the tools/call never completes)
The same server, sent the same elicitation/create, completes normally with a client that answers {"action":"accept","content":{"confirmed":true}}.
Workaround for server authors: until this is fixed, servers need an opt-out of elicitation for opencode. We added MCP_CONFIRM_ELICITATION=off to @chrischall/mcp-utils (users set it in the server's environment block), which switches to a two-step preview + token confirmation in chat.
The request is received and turned into a form, but the form is owned by "global" rather than the calling session. The MCP elicitation handler calls ask({ sessionID: <const "global">, title: "<server> is req
Looks like this is the case in the code. I put together #53608 to route attributable tool-call prompts to their sessions + some isolation tests. It shouls be complementary to #53199.
Could you help validate your stdio repr against the PR?
Reproduced on OpenCode 2.0.25, macOS arm64, TUI, local stdio, with protocol: "legacy" and codemode: false. A harmless MCP SDK v2 diagnostic tool requests one required boolean (approve, default false) through inputRequired. It has no ERP access or write operations.
There is a missing-location problem in addition to the session-attribution work in #53608:
The server creates the form with sessionID: "global", but the live form.created event has no location. The tool then waits until its elicitation timeout.
In mcp/client.ts, initialization captures the Effect context for runFork, but the elicitation callbacks use bare Effect.runPromise.
Bus.publish derives the event location from the ambient Location.Service. Running outside the captured context loses it.
The client data layer returns early for unlocated events before handling form.created. The TUI can display global forms once they are cached with a location; global ownership alone is not sufficient to explain this reproduction.
Proposed upstream fix: preserve the initialization context for the promise callbacks, as the MCP OAuth code already does:
Use runPromise(...) instead of Effect.runPromise(...) in both elicitation/create and notifications/elicitation/complete. This is a proposed source change; I have not run the upstream test suite against it. It should complement #53608's session ownership/cancellation changes.
Verified workaround: a small TUI plugin listens for unlocated form.created events with metadata.kind === "mcp-elicitation" from our configured server, then calls:
The plugin only refreshes the native form cache; it never replies to or cancels a form. With this workaround, the actual native TUI displayed the approval form. Selecting No completed the diagnostic tool call in about seven seconds with {"approved":false,"writes":0}, rather than timing out. No business data was accessed or changed.
For regression coverage in #53199, please assert that the published, serialized form.created event retains the connection's directory (and workspace identity where applicable), in addition to asserting the elicitation round trip. A form can exist on the server and still be invisible to the TUI when this field is missing.
Corroborating repro via Code Mode (the `execute` tool / `tools..(...)` sandboxed-script call path), OpenCode CLI, macOS arm64, local stdio, server `skylight-mcp` (which depends on `@chrischall/mcp-utils`, same author as this thread).
Calling a confirm-gated tool (`skylight_clear_list`) through Code Mode hung for exactly 600092ms, then failed with:
```
Fulfilling input required by 'tools/call' failed: Request timed out
```
No form ever appeared in the session/TUI. This matches the `elicitation/create` round-trip described above (confirmed by diffing against `skylight_delete_list_items`, a non-confirm-gated bulk-delete on the same server, which completed in 52ms) — but the outer timeout and error text here differ from both the ~30s MCP dispatch timeout in the original report and `opencode run`'s immediate `{"action":"cancel"}`. Worth checking that whatever layer produces "Fulfilling input required by 'tools/call' failed" also gets torn down correctly once #53608 / the `location`-on-`form.created` fix lands, since Code Mode wraps the same tool call through an additional execution layer.
Workaround applied in the meantime: `MCP_CONFIRM_ELICITATION=off` set in that one server's own `mcp.servers.skylight.environment` block (per-server, not global) — confirms @chrischall's workaround note above.
Activity
github-actions commentedon Sep 28, 2026
This issue doesn't fully meet our contributing guidelines.
What needs to be fixed:
[FEATURE]:prefix (currently it starts with[FEATURE-MCP]:).Please edit this issue to address the above within 2 hours, or it will be automatically closed.
This issue might be a duplicate of existing issues. Please check:
If you believe this was flagged incorrectly, please let a maintainer know.
[-][FEATURE-MCP]: Client advertises elicitation.form capability but never handles elicitation/create requests, causing tool calls to hang and timeout[/-][+][FEATURE]: MCP Client advertises elicitation.form capability but never handles elicitation/create requests, causing tool calls to hang and timeout[/+]github-actions commentedon Sep 28, 2026
Thanks for updating the title. The issue still needs reproducibility details in the Environment section: please provide the exact OpenCode version (rather than "latest"), operating system, and client/interface (TUI, Desktop, or Web). The needs:compliance label will remain until those details are added.
github-actions commentedon Sep 28, 2026
Thanks for updating the issue. The title, feature verification checkbox, description, and exact OpenCode version are now provided. To complete the Environment details and make the report reproducible, please add your operating system and whether you’re using the TUI, Desktop, or Web interface. The needs:compliance label will remain until those details are added.
github-actions commentedon Sep 28, 2026
Thanks for updating the issue. To complete the system details and make the report reproducible, please add your operating system and whether you’re using the TUI, Desktop, or Web interface in the Environment section. The needs:compliance label will remain until those details are added.
github-actions commentedon Sep 28, 2026
Thanks for updating the issue. The feature title, verification checkbox, description, reproduction steps, and exact OpenCode version are now present. The Environment section lists “OpenCode Terminal (CLI)” as the operating system, but that describes the client/interface rather than the OS. Please add the actual operating system (for example, macOS, Linux, or Windows) so the system details are complete. The needs:compliance label will remain until then.
github-actions commentedon Sep 28, 2026
Thanks for updating the issue with the required details!
chrischall commentedon Oct 6, 2026
Same bug over stdio, and still present in 2.0.24 (also seen on 2.0.11). Here is what's going on, from the JSON-RPC traffic and the shipped binary.
The request is received and turned into a form, but the form is owned by
"global"rather than the calling session. The MCP elicitation handler callsask({ sessionID: <const "global">, title: "<server> is requesting input", metadata: { kind: "mcp-elicitation", … } }), and the session view never renders forms owned by"global". So the tool call waits until something times it out. In the TUI I watched it sit for several minutes.opencode runcancels it straight away ({"action":"cancel"}).The session that should own it is already known. opencode sends it on every call:
tools/callcarries_meta: {"ai.opencode/sessionID": "ses_…"}. Using the session of the in-flighttools/callthat theelicitation/createbelongs to would route the prompt to the view that's waiting on it.Repro (stdio, 2.0.24, macOS arm64, TUI)
confirmed: booleanform elicitation.execute→tools.<server>.<tool>(…)).The same server, sent the same
elicitation/create, completes normally with a client that answers{"action":"accept","content":{"confirmed":true}}.Workaround for server authors: until this is fixed, servers need an opt-out of elicitation for opencode. We added
MCP_CONFIRM_ELICITATION=offto@chrischall/mcp-utils(users set it in the server'senvironmentblock), which switches to a two-step preview + token confirmation in chat.fix(deps): require @chrischall/mcp-utils 2.15.0 so confirmed writes w…
z-a-f commentedon Oct 6, 2026
Looks like this is the case in the code. I put together #53608 to route attributable tool-call prompts to their sessions + some isolation tests. It shouls be complementary to #53199.
Could you help validate your
stdiorepr against the PR?dimitriskourg commentedon Oct 8, 2026
Reproduced on OpenCode 2.0.25, macOS arm64, TUI, local stdio, with
protocol: "legacy"andcodemode: false. A harmless MCP SDK v2 diagnostic tool requests one required boolean (approve, defaultfalse) throughinputRequired. It has no ERP access or write operations.There is a missing-location problem in addition to the session-attribution work in #53608:
sessionID: "global", but the liveform.createdevent has nolocation. The tool then waits until its elicitation timeout.runFork, but the elicitation callbacks use bareEffect.runPromise.Location.Service. Running outside the captured context loses it.form.created. The TUI can display global forms once they are cached with a location; global ownership alone is not sufficient to explain this reproduction.Proposed upstream fix: preserve the initialization context for the promise callbacks, as the MCP OAuth code already does:
Use
runPromise(...)instead ofEffect.runPromise(...)in bothelicitation/createandnotifications/elicitation/complete. This is a proposed source change; I have not run the upstream test suite against it. It should complement #53608's session ownership/cancellation changes.Verified workaround: a small TUI plugin listens for unlocated
form.createdevents withmetadata.kind === "mcp-elicitation"from our configured server, then calls:The plugin only refreshes the native form cache; it never replies to or cancels a form. With this workaround, the actual native TUI displayed the approval form. Selecting No completed the diagnostic tool call in about seven seconds with
{"approved":false,"writes":0}, rather than timing out. No business data was accessed or changed.For regression coverage in #53199, please assert that the published, serialized
form.createdevent retains the connection's directory (and workspace identity where applicable), in addition to asserting the elicitation round trip. A form can exist on the server and still be invisible to the TUI when this field is missing.ericmasiello commentedon Oct 9, 2026
Corroborating repro via Code Mode (the `execute` tool / `tools..(...)` sandboxed-script call path), OpenCode CLI, macOS arm64, local stdio, server `skylight-mcp` (which depends on `@chrischall/mcp-utils`, same author as this thread).
Calling a confirm-gated tool (`skylight_clear_list`) through Code Mode hung for exactly 600092ms, then failed with:
```
Fulfilling input required by 'tools/call' failed: Request timed out
```
No form ever appeared in the session/TUI. This matches the `elicitation/create` round-trip described above (confirmed by diffing against `skylight_delete_list_items`, a non-confirm-gated bulk-delete on the same server, which completed in 52ms) — but the outer timeout and error text here differ from both the ~30s MCP dispatch timeout in the original report and `opencode run`'s immediate `{"action":"cancel"}`. Worth checking that whatever layer produces "Fulfilling input required by 'tools/call' failed" also gets torn down correctly once #53608 / the `location`-on-`form.created` fix lands, since Code Mode wraps the same tool call through an additional execution layer.
Workaround applied in the meantime: `MCP_CONFIRM_ELICITATION=off` set in that one server's own `mcp.servers.skylight.environment` block (per-server, not global) — confirms @chrischall's workaround note above.