Noodle Seed
Guides

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.

src/server.ts
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.

src/views/ordering-flow.tsx
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 React view: { 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

On this page