Skip to content

[FEATURE]: MCP Client advertises elicitation.form capability but never handles elicitation/create requests, causing tool calls to hang and timeout #51856

Description

@Alireza-ALZ

Feature hasn't been suggested before.

  • 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

  1. Configure an MCP server in opencode.json that has tools using server.elicitInput() for user approval:
{
  "mcp": {
    "servers": {
      "my-server": {
        "type": "remote",
        "url": "http://127.0.0.1:4001/mcp"
      }
    }
  }
}
  1. Call any MCP tool that invokes server.elicitInput() (e.g., a tool that asks "Are you sure you want to update this config?")
  2. 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:
  3. 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.
  4. 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

Activity

  1. github-actions commented on Sep 28, 2026

    @github-actions
    Contributor

    This issue doesn't fully meet our contributing guidelines.

    What needs to be fixed:

    • 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.

  2. 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
  3. github-actions commented on Sep 28, 2026

    @github-actions
    Contributor

    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.

  4. github-actions commented on Sep 28, 2026

    @github-actions
    Contributor

    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.

  5. github-actions commented on Sep 28, 2026

    @github-actions
    Contributor

    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.

  6. github-actions commented on Sep 28, 2026

    @github-actions
    Contributor

    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.

  7. github-actions commented on Sep 28, 2026

    @github-actions
    Contributor

    Thanks for updating the issue with the required details!

  8. chrischall commented on Oct 6, 2026

    @chrischall

    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)

    1. Run any local MCP server whose tool asks for a form before acting, e.g. a tool that returns a confirmed: boolean form elicitation.
    2. Model calls the tool (through Code Mode execute → tools.<server>.<tool>(…)).
    3. 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.

  9. z-a-f commented on Oct 6, 2026

    @z-a-f

    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?

  10. dimitriskourg commented on Oct 8, 2026

    @dimitriskourg

    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:

    -    const runFork = Effect.runForkWith(yield* Effect.context())
    +    const context = yield* Effect.context()
    +    const runFork = Effect.runForkWith(context)
    +    const runPromise = Effect.runPromiseWith(context)

    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:

    const location = context.location ?? context.data.location.default()
    context.data.session.form.invalidate("global", location)
    await context.data.session.form.sync("global", location)

    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.

  11. ericmasiello commented on Oct 9, 2026

    @ericmasiello

    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.

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

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions