Commit c1a8732
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
File tree
- .changeset
- docs
- mcp
- examples/ts-react-chat
- public
- scripts
- src
- components
- lib
- routes
- packages
- ai-client
- src
- tests
- ai-mcp
- skills/ai-mcp
- src
- apps
- tests
- apps
- helpers
- ai-preact
- src
- tests
- ai-react
- src
- tests
- ai
- skills/ai-core/tool-calling
- src
- activities/chat
- mcp
- stream
- tools
- tests
- helpers
- testing/e2e
- fixtures/mcp-apps
- src
- routes
- tests
Some content is hidden
Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
134 | 134 | | |
135 | 135 | | |
136 | 136 | | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
137 | 143 | | |
138 | 144 | | |
139 | 145 | | |
| |||
0 commit comments