Skip to content

Commit c1a8732

Browse files
AlemTuzlakautofix-ci[bot]tombeckenhamclaude
authored
feat: MCP Apps support (interactive ui:// widgets) (#843)
* chore(deps): add @mcp-ui/client + ext-apps for MCP Apps support * feat(ai): add UIResourcePart + UIResourceEvent types * feat(ai-mcp): capture serverId + _meta.ui.resourceUri at tool discovery * feat(ai): extend MCPToolSource with optional readResource * feat(ai-mcp): in-memory McpSessionStore (#785-shaped seam) * feat(ai-mcp): surface embedded ui:// resources separately from model text * feat(ai): reconcile ui-resource CUSTOM events into UIResourcePart * feat(ai-mcp): add public callTool method on MCPClient * feat(ai): emit ui-resource events from MCP tool results (eager read, fail-soft) * feat(ai-mcp): createMcpAppCallHandler + ./apps subpath (reconnect default, allowlist) * fix(ai-mcp): type callTool return as SDK union to unblock declaration build * feat(ai-client): createMcpAppBridge (framework-agnostic tool/prompt/link routing) * feat(ai-react): MCPAppResource wrapper for @mcp-ui/client AppRenderer * feat(ai-preact): MCPAppResource wrapper for @mcp-ui/client AppRenderer * docs(skills): document MCP Apps support in ai-mcp + tool-calling skills * docs: MCP Apps guide (server + client, React/Preact) * fix(ai-client): handle UIResourcePart variant in MessagePart union * fix(ai-react,ai-preact): narrow onCallTool structuredContent to satisfy CallToolResult * test(e2e): MCP Apps static render + interactive call + allowlist rejection * chore: add changeset and format MCP Apps files * fix: satisfy eslint in MCP Apps source (require-await, arrow signatures) * chore: align MCP Apps deps + knip ignore for optional @mcp-ui/client peer * docs: make MCP Apps snippets type-check under kiira (no as-casts) * fix(ai-client): bridge HTTP-status guard, drop dead onNotify/onIntent, warn on dropped link * feat(ai): add toolName to UIResourcePart (AppRenderer requires the MCP tool name) * fix(ai-mcp): pool readResource + call-handler prefix/serverId routing; drop dead extractUiResources * fix(ai-react,ai-preact): use part.toolName, run .tsx tests, coalesce result, empty-prompt guard * docs: correct MCP Apps skill/docs to shipped API; gpt-4o -> gpt-5.5 in tool-calling skill * fix(ai): emit ui-resource only on uri match; reconcile to tool-call owner message * fix(ai-mcp): drop harmful prefix-strip, pool readResource uri-ownership, sliding session TTL * fix: openLink throw-safety, meta doc honesty, prop JSDoc parity, stronger test assertions * docs: correct allowlist/prefix descriptions to match strip-free handler; pin ui:// exclusion test * fix(ai-mcp): type error detail as string|undefined to satisfy no-unnecessary-condition * ci: apply automated fixes * feat(ai-mcp): createMcpAppCallHandler takes clients; MCPClient.getInfo + MCPClients.getServers * test: adopt clients-based call handler + test-hygiene cleanup (remove casts, extract helpers) * docs: clients-based createMcpAppCallHandler API; clarify sandbox proxy URL * fix(ai-mcp): key app registry by serverId + collision throw, store->clients fallback, close e2e client, error causes * test(ai-mcp): cover registry keying/collision, store fallback, readResource error causes * fix(ai-mcp): getInfo/getServers retain only serializable TransportConfig (drop single-use Transport instances) * test(ai-mcp): instance-built clients expose no reconnectable transport descriptor * docs: multi-server interactive routing requires a per-server prefix * ci: apply automated fixes * fix(mcp-apps): reject malformed call args + validate openLink URL scheme Address PR review feedback: - createMcpAppCallHandler: reject a non-object args payload (array/primitive/ null) instead of silently coercing it to {}; absent args still defaults to {}. - createMcpAppBridge.openLink: only forward http(s)/mailto URLs to the host onLink handler; reject javascript:/data:/file:/etc. from untrusted widgets. - docs(SKILL): point Preact readers at the @tanstack/ai-preact/mcp-apps subpath. * fix(mcp-apps): address review — observability, fail-soft scope, type dedup Applies the PR-review findings on the MCP Apps surface: - processor: warn (not silently drop) a ui-resource event that resolves to no target message — a vanished widget is otherwise undebuggable client-side. - call-handler: add optional onError(err, { phase, req }) so the otherwise opaque server handler can report 'call' and 'close' failures; library stays console-free. - tool-calls: move emitCustomEvent out of the read try so an emit-path error can't be mislabeled as a read failure. - pool.readResource: attach ALL per-client errors via AggregateError instead of last-error-wins, so the owning server's failure isn't buried. - session-store: opportunistic expiry sweep on set() to bound growth for set-but-never-read threads. - types: extract shared McpResourceReadResult (kills the hand-copied shape); type the processor event as UIResourceEvent['value'] and drop the as-cast; narrow isToolCallResponse without a cast; fix orphaned/inaccurate JSDoc and add a per-run mutation note on bindReadResource. - docs: drop redundant updatedAt on the new page; document that unsafe link schemes are rejected even with an onLink handler. Tests: pin the "widget never enters model input" invariant; onLink-throws fail-soft; tool-result-still-flows on read failure; session-store sweep. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ci: apply automated fixes * fix(mcp-apps): onError sync-throw safety + test-quality fixes from re-review Round-2 review of the prior fixes commit: - call-handler: extract reportError() so onError is invoked inside the promise chain — a SYNCHRONOUSLY-throwing hook no longer escapes during argument evaluation and can't break the handler's fail-soft result (the previous `Promise.resolve(onError(...)).catch()` only absorbed async rejections). - tests: cover the onError hook (phase 'call', phase 'close', and both sync- throw and async-reject safety) — previously untested. - tests: drop a tautological `not.toContain('ui-resource')` assertion and reword the messages.ts invariant comment to claim only the load-bearing uri/HTML checks; reword the session-store sweep test to state honestly that it guards set() correctness across the sweep, not the (unobservable) memory reclamation. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ci: apply automated fixes * docs(example): add MCP Apps demo (static + interactive) to ts-react-chat Adds a `/mcp-apps` route demonstrating both kinds of MCP Apps end to end: - STATIC: an in-process MCP server (`api.mcp-apps-weather-server`) exposing a display-only `show_weather_card` tool whose `ui://weather/card` resource renders as a self-contained forecast card. - INTERACTIVE: the official Three.js MCP server (@modelcontextprotocol/ server-threejs) run on :3001, whose widget calls tools back through the bridge. Wiring: - `api.mcp-apps-chat` connects both servers via createMCPClient and streams `ui-resource` parts (tolerates :3001 being down). - `api.mcp-apps-call` mounts createMcpAppCallHandler over both for the interactive plane. - The page renders `ui-resource` parts with `MCPAppResource` + a `createMcpAppBridge`, seeds `toolInput` from the sibling tool-call part, and withholds the bridge for the static widget (display-only). Suggestion pills trigger each app. - Vendors the official sandbox-proxy page and serves it cross-origin on :8765 (a hard requirement of @mcp-ui/client AppRenderer); `dev` now runs the proxy, the Three.js server, and Vite via concurrently. Verified: page renders with no console errors, the static MCP server route and the Three.js server both respond, the proxy serves, and the example type-checks. The live model->tool->widget render requires a provider API key. * ci: apply automated fixes * feat(example): add interactive storefront MCP App + solar-system scene Address feedback on the /mcp-apps demo: - Add an INTERACTIVE storefront widget (api.mcp-apps-shop-server) that demonstrates the full bridge round-trip: clicking "Buy now" in the sandbox sends a tools/call over a hand-rolled MCP Apps app-bridge -> AppRenderer -> createMcpAppBridge -> POST /api/mcp-apps-call -> createMcpAppCallHandler -> buy_product() on the server -> order confirmation rendered back in the widget. The widget speaks the app-bridge protocol in plain JS (no build step). - Wire the shop server into the chat + call routes; gate the bridge on non-static widgets (the weather card stays display-only). - Change the Three.js suggestion to render a solar system instead of a cube. - Fix the tool-call note: show a check when done instead of a perpetual spinner. - Make the weather pill name a city so the tool fires deterministically. Verified live in the browser: static card, interactive buy round-trip (correct server order ids, no auto-fire), and the 3D solar system all render. * ci: apply automated fixes * feat(ai-react,ai-preact): add useMcpAppBridge hook A React/Preact wrapper over createMcpAppBridge that returns a stable bridge for a given threadId/callEndpoint while always invoking the latest chat.sendMessage and onLink (kept in refs) — removing the hand-written useMemo + exhaustive-deps disable the example previously needed. - Exported from the main entry of @tanstack/ai-react and @tanstack/ai-preact (no @mcp-ui/client needed — it only wraps the ai-client bridge); also re-export createMcpAppBridge / McpAppBridge / CreateMcpAppBridgeOptions. - Unit tests cover stable identity, recreation on threadId change, latest- callback invocation (no stale closure), and display-only openLink. - Use the hook in the ts-react-chat /mcp-apps example. - Update docs/mcp/apps.md (interactive example + API reference) and the ai-mcp SKILL to use the hook; bump the docs updatedAt and the changeset. * style: format useMcpAppBridge signatures --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Tom Beckenham <34339192+tombeckenham@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 234d54e commit c1a8732

69 files changed

Lines changed: 6414 additions & 60 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.changeset/mcp-apps.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
---
2+
'@tanstack/ai': minor
3+
'@tanstack/ai-mcp': minor
4+
'@tanstack/ai-client': minor
5+
'@tanstack/ai-react': minor
6+
'@tanstack/ai-preact': minor
7+
---
8+
9+
feat: MCP Apps support — render interactive `ui://` widgets served by MCP servers
10+
11+
Adds support for the ratified [MCP Apps](https://modelcontextprotocol.io/extensions/apps/overview) standard, letting MCP server tools return interactive UI widgets that render in the chat.
12+
13+
- **`@tanstack/ai`** — MCP tool results that link a `ui://` resource (via `_meta.ui.resourceUri`) now surface as a new `UIResourcePart` on the assistant `UIMessage` (carried as an AG-UI `CUSTOM` event). The widget never enters model input. The `ui://` resource is read eagerly during the run, fail-soft.
14+
- **`@tanstack/ai-mcp`** — tool discovery now captures `serverId` + the UI resource link; `MCPClient` gains a public `callTool` and `getInfo()` (returns the client's transport descriptor); `MCPClients` gains `getServers()` (returns all pool entries' descriptors). New `@tanstack/ai-mcp/apps` subpath exports `createMcpAppCallHandler` — a server-side tool-call proxy for interactive widgets that takes the MCP client(s)/pool you already created (`clients: MCPClient | MCPClients | Array<MCPClient | MCPClients>`), reads each client's transport descriptor via `MCPClient.getInfo()` / `MCPClients.getServers()` (pure config, no live socket required), and **reconnects per call** (stateless, serverless-safe by default, same-server allowlist). Also exports an in-memory `McpSessionStore` seam for stateful transports.
15+
- **`@tanstack/ai-client`**`createMcpAppBridge`, a framework-agnostic bridge routing widget tool-calls to the call handler, follow-up prompts into the chat, and blocking links unless a handler is supplied.
16+
- **`@tanstack/ai-react` / `@tanstack/ai-preact`** — a `MCPAppResource` component (new `./mcp-apps` subpath) that renders a `UIResourcePart` via `@mcp-ui/client`'s `AppRenderer` (optional peer dependency), wired to the bridge. Plus a `useMcpAppBridge` hook (main entry) that returns a stable `createMcpAppBridge` for a given `threadId`/`callEndpoint` while always calling the latest `sendMessage`/`onLink`.
17+
18+
Persistence is intentionally out of scope (in-memory seams only); Solid/Vue/Svelte/Angular renderers are deferred (the renderer SDK is currently React-only).

docs/config.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,12 @@
134134
"label": "MCP Type Generation",
135135
"to": "tools/mcp-codegen",
136136
"addedAt": "2026-06-05"
137+
},
138+
{
139+
"label": "MCP Apps",
140+
"to": "mcp/apps",
141+
"addedAt": "2026-06-24",
142+
"updatedAt": "2026-06-26"
137143
}
138144
]
139145
},

0 commit comments

Comments
 (0)