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

Aperta
#10,399 2 commenti 1 reazione 1 assegnatario Vedi su GitHub

I maintainer di solito rispondono entro 1 giorno

@effect-bot ci sta già lavorando.

Dal 5/11/2026.

Valutazione

Questa issue non è ancora stata valutata.

Descrizione

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.

Lingua principale
TypeScript
Stelle
16.7k
Fork
808
Merge medio
11h 31m
PR unite (30g)
834

Preparare l'ambiente

Questo progetto non fornisce container di sviluppo, Dockerfile né guida per i contributori, quindi l'ambiente è a tuo carico: parti dal suo README e consulta la nostra guida al primo contributo per i passaggi generali.

Come iniziare

  1. Leggi tutta la issue e poi la guida ai contributi del progetto.
  2. Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
  3. Fai un fork del repository e lavora su un branch.
  4. Apri una pull request che faccia riferimento al numero della issue.

Altre issue di Effect-TS/effect

Tutte le issue di Effect-TS/effect

Issue simili

Altre issue su TypeScript

Ricevi le nuove issue nella tua casella

Un breve riepilogo di issue GitHub adatte ai principianti.