effect/ai: support the MCP Apps extension (`io.modelcontextprotocol/ui`)

オープン
#10,399 コメント 2 件 リアクション 1 件 担当者 1 名 GitHub で見る

メンテナーはふだん 1 日以内に返信

@effect-bot がすでに取り組んでいます。

2026年11月5日 から。

評価

この issue はまだ評価されていません。

説明

Need

MCP Apps is the
stable MCP extension that lets a server give the human a visual for a tool call. A tool links to a ui:// HTML
resource. The host renders it in a sandboxed iframe, and the page (the spec calls it the View) talks to the host over
JSON-RPC on postMessage. A tool result serves two readers: structuredContent holds the data for the page and the
agent, and content holds the text for the model and for hosts without Apps. Claude (web and desktop), VS Code,
ChatGPT, Goose and others render MCP Apps.

An Effect MCP server can link a tool to a UI today only by hand: an untyped Tool.Meta record, a hand-written
extensions key, and a content effect that returns a raw ReadResourceResult. The page itself has no Effect
support. Concretely:

  • No typed extension announcement, ui:// resource, resource _meta.ui (CSP, permissions, domain, border) or tool
    _meta.ui (resourceUri, visibility). registerResource has no _meta option.
  • No way to check whether the host supports Apps, other than reading clientCapabilities.extensions by hand.
  • content is always the JSON of structuredContent. McpServer ignores a tool's toModelOutput, so a tool
    can't give the model a text summary while the page gets the data.
  • A handler can't see the JSON-RPC id of its own tools/call. hostContext.toolInfo.id is the only standard key
    a page has for the call that created it while that call is still running, so a server can't tie a page's polling
    calls to the run they belong to. Handlers also can't see Mcp-Session-Id, the scope in which that id is unique
    over sessionful HTTP. clientId is assigned per HTTP POST, so it can't serve as that scope.
  • McpClient.toolkit binds tools marked visibility: ["app"] for the model, though the spec says hosts MUST NOT
    give them to the agent. McpClient can't declare the extension.
  • Nothing for the page: the handshake, tool input and result, host context, calling server tools, display modes,
    sizing and teardown.

The motivating consumer is the effect/test MCP server. Its tool results stay Schema'd data the agent acts on, and the
human gets a live view of a run, run reports and snapshot diffs in the host.

Prior art

  • #6353 asks for first-class MCP Apps support: a ui:// resource kind and typed Tool.Meta keys. This issue covers
    that request.
  • Effect-TS/effect-smol#1440 added the _meta and extensions passthrough on MCP schemas, and
    Effect-TS/effect-smol#1653 exposed the client capabilities to handlers. Both are what makes MCP Apps possible today.
  • @lloydrichards's examples (edu_mcp-apps,
    interactivethings-data-viz-mcp-lab, and
    the helpers posted on #6353) build MCP Apps on the current McpServer. makeUiResource, makeUiRenderTool and
    makeUiAppTool are the shape the server half below makes first-class. The polling dashboard there is the pattern
    the effect/test live view follows.
  • #8631 (Tasks) and the Skills extension set the pattern for extensions: a public module per extension, a server that
    declares the extension once it is in use, and McpClient options grouped under extensions. This proposal follows
    it.

Alternatives considered

  • Keep hand-writing _meta and HTML resources. This works for the linkage, but nothing is typed or validated,
    the model gets JSON where the spec suggests a summary, and the page side gets no help.
  • Use @modelcontextprotocol/ext-apps on the page. It is Promise- and zod-based, built on the TypeScript MCP
    SDK, and uses on* setters that must be registered before connect(). It can't reuse the server's Tool
    definitions to type calls and results.
  • Extend McpClient to be the page's bridge. McpClient is request/response. The bridge is bidirectional: the
    host sends notifications and the ui/resource-teardown request. It also has its own handshake (ui/initialize).
  • Vary a tool's _meta by host. In 2026-07-28, tools/list must not vary per connection, and a
    cacheScope: "public" list may be served to any client. Hosts without Apps already treat a linked tool as a plain
    tool, and EnabledWhen covers text-only variants.
  • Correlate a page with its call through a server-minted handle in the result. The handle arrives only when the
    call completes, which is too late for a long-running call. The request id is available from the start.

Solution

Two PRs, opened after #8631 merges because it changes the same files. The server and host parts come first, then the
page module. Both cover the stable 2026-01-26 spec plus the draft's ui/download-file and
ui/notifications/request-teardown. Names below are provisional.

1. Server and host

A new module, effect/ai/McpApps, for the extension itself:

  • Schemas and constants: ResourceMeta (with its CSP and permissions parts), ToolMeta, ClientCapabilities,
    EXTENSION_ID and MIME_TYPE.
  • McpApps.registerResource and McpApps.resource. The URI must be ui://…, or registration fails with
    IllegalArgumentError. The MIME type text/html;profile=mcp-app goes on the listing and on every content item,
    and the resource metadata goes as _meta.ui on both.
  • Registering an app resource declares the extension ({}) on every protocol revision. An explicit extensions
    record still merges in.
  • A tool annotation that registerToolkit merges into the tool's _meta.ui. Registration dies if Tool.Meta also
    sets ui, or if resourceUri (optional, as app-only tools have none) isn't ui://. Only the nested form is
    written, not the deprecated _meta["ui/resourceUri"].
  • McpApps.isSupported(capabilities), usable in EnabledWhen and in handlers.

General fixes, in the existing modules:

  • A _meta option for McpServer.registerResource and McpServer.resource (the listing entry).
  • toModelOutput becomes the MCP content (text, image, audio, resource link or embedded resource), and
    structuredContent stays the encoded success. Tools without toModelOutput are unchanged.
  • McpRequestContext gains requestId, the JSON-RPC id as received, and sessionId. The docs say where the id is
    unique on each transport: within the process on stdio, within the session over sessionful HTTP, and on 2026-07-28
    only among the sender's requests still in flight. Fix the clientId docs.
  • McpClient.make({ extensions: { ui: true } }) declares { mimeTypes: ["text/html;profile=mcp-app"] }.
    McpClient.toolkit skips tools whose visibility lacks "model".

Out of scope: result _meta, ui:// templates, and the host-side bridge (rendering, CSP, proxying).

2. The page

effect/ai/McpAppView, platform-agnostic:

  • A McpAppView service and layer over any postMessage-capable target. It performs the ui/initialize
    handshake and buffers host messages from the moment it connects.
  • Host data as Effect types:
    • toolInput (an Effect)
    • toolInputPartial (a Stream)
    • toolResult (an Effect that fails with ToolCancelled)
    • hostContext (a Subscribable that merges partial updates)
    • hostInfo and hostCapabilities
  • toolCall(tool) decodes the input and the result with the tool's schemas. callTool(tool, params) calls a
    server tool through the host, typed from the same Tool definitions. A failure is a ToolError that keeps the
    result, as in McpClient.callTool.
  • readResource, openLink, sendMessage, updateModelContext, requestDisplayMode, sendSizeChanged, log,
    downloadFile and requestTeardown. They fail fast when the host lacks a capability the stable spec defines.
  • run(program): on ui/resource-teardown, the program and every fiber the bridge owns are interrupted, their
    finalizers run, and only then does the host get its answer. Any other host request the page doesn't implement gets
    JSON-RPC -32601 (method not found).
  • It must work in opaque-origin documents, which is how hosts load pages (cf. #9196).
  • A public in-memory test host for pages, in the style of McpTest.

@effect/platform-browser adds only what needs the DOM: ResizeObserver auto-resize (on by default) and opt-in
helpers for the host's theme, CSS variables and fonts. Is that the right home, or should these stay in the core
module's docs as examples?

Out of scope: app-provided tools and WebMCP (see modelcontextprotocol/ext-apps#797; not planned), sampling from the
page, framework bindings, and HTML bundling.

Development uses MCPJam Inspector to render and check pages against
emulated hosts. The repo's tests use the in-memory test host.

主要言語
TypeScript
スター
16.7k
フォーク
808
平均マージ
11時間 31分
マージ済み PR(30日)
834

環境構築

このプロジェクトには開発コンテナ、Dockerfile、コントリビューションガイドがありません。まず README を読み、一般的な手順ははじめてのコントリビューションガイドを参照してください。

はじめの一歩

  1. issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
  2. 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
  3. リポジトリをフォークし、ブランチを切って変更します。
  4. issue 番号を参照したプルリクエストを送ります。

Effect-TS/effect のほかの issue

Effect-TS/effect の issue をすべて見る

似ている issue

TypeScript の issue をもっと見る

新しい issue をメールで受け取る

初心者向けの GitHub issue を短くまとめたダイジェスト。