Skip to content

fix(app): show MCP elicitation forms in desktop and web - #58305

Merged
Hona merged 2 commits into
devfrom
app-mcp-elicitation
Oct 23, 2026
Merged

Hona merged 2 commits into
devfrom
app-mcp-elicitation

Conversation

@rekram1-node

@rekram1-node rekram1-node commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

Problem

An MCP server can ask the user for input partway through a tool call (elicitation/create). Since #58184 the TUI shows these forms, but the desktop and web app only render forms of kind question and websearch.provider. In the app the form never appears, and the tool call waits until the server gives up.

App support was originally added in #35064, lost in the #35080 revert, and not reapplied when #35115 restored the core side.

Change

 createSessionRequestModel
   formRequest          // session tree: question | websearch.provider
+                       //               | mcp-elicitation
+  elicitationRequest   // session-tree form, else a "global" form for the session's Location
+  sync("global", session.location)
   blocked = permission || question || elicitation || websearch

 <SessionComposerRegion>
   <SessionWebSearchDock />
   <SessionQuestionDock />
+  <SessionElicitationDock />   // new
   <SessionPermissionDock />
  • New SessionElicitationDock. Shows the form title, the server's message, and one control per field: a switch for booleans, number inputs for number and integer fields, a select for string choices, text inputs for free strings, checkboxes for multiselect, and the URL with an Open button for URL-mode elicitations. Submit replies and Dismiss (or Escape) cancels.
  • Global forms. Elicitations are owned by the "global" sentinel session, so the model also lists and syncs global forms for the session's Location. Replies and cancellations go through data.session.form.reply/cancel, which add the Location header for global forms.
  • Strings. Adds one English string, session.elicitation.selectPlaceholder. Everything else reuses existing keys.
  • Stories. Adds two Storybook stories, MCP elicitation request and MCP URL elicitation request. The Storybook data mock gains no-op form.reply/cancel.

Demo

mcp-elicitation-web-final.mp4

This is the production web build against a local backend. A stdio MCP server's deploy tool asks for approve (boolean), environment (staging | production) and notes (string). The server received:

{ "action": "accept", "content": { "approve": true, "environment": "production", "notes": "Ships the MCP elicitation dock." } }

Out of scope

  • Forms are still owned by the "global" session, so an elicitation shows in every session view for that Location.

Verification

  • bun test … ./src/session/requests in packages/app: 26 pass, including a new case for mcp-elicitation forms in sessionFormRequest.
  • bun run typecheck in packages/app and bun run check at the root: pass, 34/34 tasks.
  • Live in the web app as in the demo above. Dismiss and server-side cancellation were also exercised against the same backend.

MCP servers can ask for input during a tool call. The TUI renders those
forms, but desktop and web only showed question and web search forms,
so the tool call waited until the server gave up.

Add an elicitation dock that renders the form's boolean, number,
string, choice, multiselect, and URL fields, and pick up forms both
from the session tree and from the Location (elicitations are owned by
the "global" sentinel session). Replies and cancellations go through
the data layer so global forms carry their Location.

@opencode-agent opencode-agent Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The change looks sound. I read the diff against the client data layer and the core MCP elicitation handler. The new tests in packages/app/src/session/requests pass (26), and tsc -b in packages/app passes. I couldn't run it live: that needs the web app in a browser plus a backend and an MCP server that sends elicitations, so I'm relying on your demo for the end-to-end path.

Public API: unchanged. The change only touches the app UI, adds one i18n key and updates Storybook mocks.

No blocking concerns. Replies and cancels go through data.session.form.reply/cancel, which already add the Location for global forms. The server checks required fields and value types, and any error it returns shows as a toast. One known limit, already listed as out of scope and the same as in the TUI: while an elicitation is pending, blocked hides the prompt box in every session of that Location, not only the session that triggered it.

@opencode-agent opencode-agent Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new commit is a lint cleanup and changes no behaviour, apart from trimming free-text answers before sending, which is harmless because the server still rejects a blank required field. Values now live in one store split by field type, the message is decoded with an Effect schema, and the text-input type comes from a format lookup checked against FormStringField["format"]. On the new head, the tests in src/session/requests pass (26), tsc -b in packages/app passes, and oxlint on the changed directory reports no errors. I still couldn't run the dock live; your demo covers that. The first review raised no blocking points. Public API: unchanged.

@Hona
Hona merged commit 8482dbf into dev Oct 23, 2026
20 checks passed
@Hona
Hona deleted the app-mcp-elicitation branch October 23, 2026 04:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants