Uh oh!
There was an error while loading. Please reload this page.
feat: MCP Apps support (interactive ui:// widgets) - #843
Conversation
…, warn on dropped link
…; drop dead extractUiResources
…result, empty-prompt guard
…n tool-calling skill
…ip, sliding session TTL
# Conflicts: # packages/ai-mcp/tests/tools.test.ts
There was a problem hiding this comment.
Actionable comments posted: 3
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
packages/ai-mcp/src/apps/call-handler.ts (1)
201-231: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick winHandle reconnect failures inside the normalized error path.
Line 201 awaits
createMCPClient()before thetry, so a transport/connect failure rejects the handler instead of returning the{ ok: false, error }shape this API advertises.Suggested fix
- const client = await createMCPClient({- transport: descriptor.transport,- prefix: descriptor.prefix,- })-- try {+ let client: MCPClient | undefined+ try {+ client = await createMCPClient({+ transport: descriptor.transport,+ prefix: descriptor.prefix,+ }) // The widget sends the server-native (UNPREFIXED) tool name // (`UIResourcePart.toolName` is the native name), so we match it directly // against the native names the server exposes — carried on // `metadata.mcp.serverToolName` (falling back to `name` for unprefixed // clients) — and forward `req.toolName` unchanged to `client.callTool`. const exposedNative = new Set( (await client.tools()).map((t) => serverToolNameOf(t)), ) @@ } catch (err) { return { ok: false, error: err instanceof Error ? err.message : 'MCP call failed', } } finally { - await client.close().catch(() => undefined)+ await client?.close().catch(() => undefined) }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/ai-mcp/src/apps/call-handler.ts` around lines 201 - 231, The normalized error handling in call-handler should also cover failures from createMCPClient, since awaiting it before the try lets transport/connect errors escape instead of returning the expected { ok: false, error } response. Move the createMCPClient call into the existing try in call-handler and keep the catch/finally flow intact so reconnect or transport failures are converted into the same error shape as tool-call failures.
🧹 Nitpick comments (3)
packages/ai-mcp/tests/client.test.ts (2)
184-202: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAdd a config-backed
getInfo()case.Both new assertions only cover the
transport: undefinedbranch. The reconnectable path depends oncreateMCPClient({ transport: <TransportConfig> })preserving the original config, so the positive branch can still regress unnoticed without one coverage point.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/ai-mcp/tests/client.test.ts` around lines 184 - 202, Add a test that covers the config-backed getInfo() path for createMCPClient. The current cases in client.test.ts only verify that getInfo() returns transport: undefined for a Transport instance and a raw Transport, so the reconnectable branch can still regress. Add a case using createMCPClient with a serializable TransportConfig and assert that client.getInfo() preserves that original transport config alongside the prefix.
94-114: 📐 Maintainability & Code Quality | 🔵 Trivial | 🏗️ Heavy liftMove these client tests next to
src/client.ts.This file keeps growing under
packages/ai-mcp/tests/, but the repo rule is to colocate unit tests with the source module they cover. As per coding guidelines, "Place unit tests alongside source code in*.test.tsfiles`."Also applies to: 184-202
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/ai-mcp/tests/client.test.ts` around lines 94 - 114, The client tests are misplaced under the shared tests directory instead of being colocated with the module they cover. Move the relevant cases from client.test.ts next to src/client.ts as a local *.test.ts file, keeping the existing coverage for createMCPClientFromTransport and the metadata assertions on client.tools intact while updating any imports/paths needed after relocation.Source: Coding guidelines
packages/ai-mcp/tests/pool.test.ts (1)
101-146: 📐 Maintainability & Code Quality | 🔵 Trivial | 🏗️ Heavy liftMove these pool tests next to
src/pool.ts.These cases are being added under
packages/ai-mcp/tests/, which keeps the package out of sync with the repo’s colocated-unit-test rule. As per coding guidelines, "Place unit tests alongside source code in*.test.tsfiles`."Also applies to: 157-167
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/ai-mcp/tests/pool.test.ts` around lines 101 - 146, The new pool unit cases should live alongside the implementation to match the colocated-test convention. Move the `readResource` and `getServers()` tests from the current `pool.test.ts` location to a `*.test.ts` file next to `src/pool.ts`, keeping the same assertions and helper usage (`createMCPClients`, `makeServerWithWeatherTool`, `makeServerWithMismatchedResource`). Ensure the test file stays aligned with `pool.ts` so future changes to `Pool` are covered in the same module area.Source: Coding guidelines
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/ai-client/tests/mcp-app-bridge.test.ts`:
- Around line 3-17: Move the createMcpAppBridge unit test to the colocated test
location next to the source module so it follows the repository’s *.test.ts
convention. Update the test placement for the mcp-app-bridge spec that currently
imports CreateMcpAppBridgeOptions and defines makeChatMock inside
packages/ai-client/tests, and ensure any relative imports still resolve
correctly after relocating it alongside src/mcp-app-bridge.ts.
In `@packages/ai-mcp/src/apps/call-handler.ts`:
- Around line 222-223: The `callTool` flow in `call-handler.ts` is silently
accepting malformed `req.args` by converting any non-record value to `{}`, which
can mask bad requests. Update the `handleCall` logic around
`isArgsRecord(req.args)` so invalid payloads like arrays or strings are rejected
with an error instead of being rewritten, and only pass through a validated
record to `client.callTool`.
In `@packages/ai-mcp/tests/apps/call-handler.test.ts`:
- Around line 1-4: The unit test placement for call-handler is incorrect:
`call-handler.test.ts` should live alongside `src/apps/call-handler.ts` to match
the repo’s `*.test.ts` convention. Move the existing test file next to the
`call-handler` source module and keep its imports/types (`MCPClient`,
`MCPClients`, `TransportConfig`) unchanged so the test continues to target the
same `call-handler` behavior from the new location.
---
Outside diff comments:
In `@packages/ai-mcp/src/apps/call-handler.ts`:
- Around line 201-231: The normalized error handling in call-handler should also
cover failures from createMCPClient, since awaiting it before the try lets
transport/connect errors escape instead of returning the expected { ok: false,
error } response. Move the createMCPClient call into the existing try in
call-handler and keep the catch/finally flow intact so reconnect or transport
failures are converted into the same error shape as tool-call failures.
---
Nitpick comments:
In `@packages/ai-mcp/tests/client.test.ts`:
- Around line 184-202: Add a test that covers the config-backed getInfo() path
for createMCPClient. The current cases in client.test.ts only verify that
getInfo() returns transport: undefined for a Transport instance and a raw
Transport, so the reconnectable branch can still regress. Add a case using
createMCPClient with a serializable TransportConfig and assert that
client.getInfo() preserves that original transport config alongside the prefix.
- Around line 94-114: The client tests are misplaced under the shared tests
directory instead of being colocated with the module they cover. Move the
relevant cases from client.test.ts next to src/client.ts as a local *.test.ts
file, keeping the existing coverage for createMCPClientFromTransport and the
metadata assertions on client.tools intact while updating any imports/paths
needed after relocation.
In `@packages/ai-mcp/tests/pool.test.ts`:
- Around line 101-146: The new pool unit cases should live alongside the
implementation to match the colocated-test convention. Move the `readResource`
and `getServers()` tests from the current `pool.test.ts` location to a
`*.test.ts` file next to `src/pool.ts`, keeping the same assertions and helper
usage (`createMCPClients`, `makeServerWithWeatherTool`,
`makeServerWithMismatchedResource`). Ensure the test file stays aligned with
`pool.ts` so future changes to `Pool` are covered in the same module area.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: f58e9894-31f3-43ba-99ab-fc293aaabf5d
📒 Files selected for processing (22)
.changeset/mcp-apps.mddocs/mcp/apps.mdpackages/ai-client/tests/mcp-app-bridge.test.tspackages/ai-mcp/skills/ai-mcp/SKILL.mdpackages/ai-mcp/src/apps/call-handler.tspackages/ai-mcp/src/apps/index.tspackages/ai-mcp/src/apps/session-store.tspackages/ai-mcp/src/client.tspackages/ai-mcp/src/pool.tspackages/ai-mcp/src/transport.tspackages/ai-mcp/tests/apps/call-handler.test.tspackages/ai-mcp/tests/client.test.tspackages/ai-mcp/tests/pool.test.tspackages/ai-mcp/tests/tools.test.tspackages/ai-preact/tests/mcp-apps.test.tsxpackages/ai-react/tests/mcp-apps.test.tsxpackages/ai/tests/chat-mcp-manager.test.tspackages/ai/tests/helpers/processor-harness.tspackages/ai/tests/stream-ui-resource.test.tspackages/ai/tests/ui-resource-part.test.tstesting/e2e/src/routes/api.mcp-apps-call.tstesting/e2e/tests/mcp-apps.spec.ts
✅ Files skipped from review due to trivial changes (4)
- packages/ai/tests/ui-resource-part.test.ts
- .changeset/mcp-apps.md
- packages/ai-mcp/skills/ai-mcp/SKILL.md
- docs/mcp/apps.md
🚧 Files skipped from review as they are similar to previous changes (9)
- packages/ai-mcp/src/apps/index.ts
- packages/ai-mcp/tests/tools.test.ts
- packages/ai/tests/stream-ui-resource.test.ts
- testing/e2e/src/routes/api.mcp-apps-call.ts
- testing/e2e/tests/mcp-apps.spec.ts
- packages/ai-mcp/src/apps/session-store.ts
- packages/ai-react/tests/mcp-apps.test.tsx
- packages/ai-preact/tests/mcp-apps.test.tsx
- packages/ai/tests/chat-mcp-manager.test.ts
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
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.AlemTuzlak
commented
Jun 25, 2026
@coderabbitai review |
✅ Action performedReview finished.
|
…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>…-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>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.
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.
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.
liady
commented
Jun 26, 2026
Great addition @AlemTuzlak! Thank you |
MCP Apps support
Adds support for the ratified MCP Apps standard (2026-01-26) — MCP server tools can return interactive
ui://resource widgets that render in the chat.What's in it
Data plane (
@tanstack/ai,@tanstack/ai-mcp)_meta.ui.resourceUrilink +serverId;MCPClientgains a publiccallTool, and theMCPClientspool gains a URI-ownership-routedreadResource.ui://resource is read eagerly during the run (fail-soft) and surfaced as a newUIResourceParton the assistantUIMessage, carried as an AG-UICUSTOMevent. The widget never enters model input — the model still receives the normal tool result.Interactive plane
@tanstack/ai-mcp/appsexportscreateMcpAppCallHandler({ servers, store?, allowTool? })— a server-side tool-call proxy for widgets: reconnect-per-call (stateless/serverless-safe), same-server allowlist by native tool name, and an in-memoryinMemoryMcpSessionStoreseam.@tanstack/ai-clientexportscreateMcpAppBridge({ threadId, callEndpoint, chat, onLink? })→{ callTool, sendPrompt, openLink }, a framework-agnostic bridge that routes widget tool-calls to the handler, follow-up prompts into the chat, and blocks links unless a handler is supplied.Rendering (
@tanstack/ai-react,@tanstack/ai-preact)MCPAppResourcecomponent (new./mcp-appssubpath) renders aUIResourcePartvia@mcp-ui/client'sAppRenderer(optional peer dependency), wired to the bridge.Scope decisions
@mcp-ui/clientv7'sAppRendereris React-only (no web component), so Solid/Vue/Svelte/Angular renderers are deferred to a follow-up.McpSessionStore) shaped so persistent backends can drop in later. Conversation-state writeback defaults to client-side.Testing
allowToolpaths), bridge, pool URI-ownership, and both wrappers.ui-resourcerender over the stream, interactive call returning the tool result, and allowlist rejection.Review
Implemented via spec → plan → wave-based execution with per-wave reviews, then three full code-review rounds (converged: no load-bearing findings remain). Docs (
docs/mcp/apps.md) and theai-mcp/ tool-calling skills are updated in this PR.Known / not introduced by this PR
test:kiirais red on two untouched docs (docs/adapters/grok.md,docs/media/video-generation.md) — a pre-existing baseline failure (grokVideo/generateVideoundefined), red onmain; this PR'sdocs/mcp/apps.mdpasses kiira.getTextContent, empty-string tool-result drop,RUN_ERROR-without-runIdclearing all runs, in-memory session-store growth for write-only threads,ToolCallManager.executeToolssecondary path, staleknipreact-aiblock).🤖 Generated with Claude Code
Summary by CodeRabbit
ui://widget resources from MCP tool calls, including static (presentational) and interactive flows (tool/prompt/link).MCPAppResourcecomponents.ui://widget data is excluded from model input and improved fail-soft behavior when resource reads fail.