Apps & widgets
Render React UI inside the host as an MCP App, linked to your tools.
Phase 2, partial
MCP Apps and widgets are Partial (Phase 2). Widgets render in-host and can call back into your server, and React-first authoring is in place. CSP minimization, dependency policy, and cross-host visual QA are still being hardened. Build with this in mind.
An MCP App is a React UI that the host (Claude, ChatGPT) renders alongside a tool result. In
Noodle Seed you author widgets as React components exposed as ui:// resources, linked to a tool.
There is no separate public widget primitive: React view metadata belongs on the tool whose result
renders it.
A tool with a widget
tool declares the tool and its optional view together. The view points at a React component file.
import { server, tool, z } from '@noodleseed/one';
export default server('ordering', { title: 'Ordering', version: '1.0.0' }, [
tool('open_ordering', {
description: 'Show the menu and render an ordering widget.',
input: z.object({ customer: z.string() }),
output: z.object({ menuText: z.string(), total: z.number() }),
fulfil: ({ input }) => ({ menuText: 'Falafel wrap', total: 12 }),
viewTitle: 'Food order',
view: { component: 'ordering-flow', entry: './views/ordering-flow.tsx' },
csp: { connectDomains: ['https://example.com'] },
}),
]);The React view
A view component imports typed helpers from @noodleseed/one/react to read the tool result and call
back into the server.
import { generateHelpers } from '@noodleseed/one/react';
const { useToolInfo } = generateHelpers();
export default function OrderingFlow() {
const result = useToolInfo('open_ordering').structuredContent as
| { readonly menuText?: string }
| undefined;
return <div>{result?.menuText}</div>;
}Widget-only tools
A widget often needs actions the model should not call directly. Declare the same tool with
visibility: ['app']; the widget invokes it with callServerTool. These calls re-enter the
same authentication and policy path as any other tool call.
import { tool, z } from '@noodleseed/one';
tool('sync_cart', {
visibility: ['app'],
description: 'Sync a cart update from the ordering widget.',
input: z.object({ itemName: z.string(), quantity: z.number().int().min(1) }),
output: z.object({ total: z.number() }),
fulfil: ({ input }) => ({ total: input.quantity * 12 }),
});Raw HTML and handoff
view: { html }is the explicit lower-level escape hatch for self-contained markup. Prefer a Reactview: { component, entry }.handoffSession({ url, purpose, expiresAt })builds a typed envelope to hand the user off to an external URL (checkout, booking, payment) from a widget or tool result.
Preview locally
noodle check # MCP Apps / widget readiness checks (no service)
noodle devtools # preview widget metadata and rendering locally