Noodle Seed
Guides

Apps & widgets

Render React interfaces inside an MCP host and connect them to your tools. Learn widget authoring, resource delivery and the current host limitations.

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.

To see how Noodle Seed itself uses a local build-time widget and four remote operational widgets, read the developer plugin guide.

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 { Feedback, generateHelpers } from '@noodleseed/one/react';

const { useToolInfo } = generateHelpers();

type OrderingResult = { readonly menuText: string };

function isOrderingResult(value: unknown): value is OrderingResult {
  return (
    value !== null &&
    typeof value === 'object' &&
    typeof (value as Partial<OrderingResult>).menuText === 'string' &&
    (value as Partial<OrderingResult>).menuText!.length > 0
  );
}

export default function OrderingFlow() {
  const toolInfo = useToolInfo('open_ordering');
  const pending = Object.keys(toolInfo).length === 0;
  const result = isOrderingResult(toolInfo.structuredContent)
    ? toolInfo.structuredContent
    : undefined;

  if (pending) return <Feedback status="loading">Loading the menu…</Feedback>;
  if (toolInfo.isError) return <Feedback status="error">Could not load the menu.</Feedback>;
  if (!result) return <Feedback status="error">The menu result was incomplete.</Feedback>;

  return <div>{result.menuText}</div>;
}

Keep the complete useToolInfo() result. Treat an empty envelope as pending, handle isError explicitly, and validate every required field or identifier before rendering it. A non-pending result with invalid required data is a malformed-result error. Render result-dependent actions only after validation succeeds; local form defaults must not masquerade as loaded business data.

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