Skip to main content
FrontMCP tools can render rich HTML widgets for display in OpenAI Apps, Claude Artifacts, and other UI-capable hosts. This guide shows you how to use the @frontmcp/ui library to build professional-looking tool outputs.
Prerequisites:

What You’ll Build

A weather tool that displays temperature, conditions, and other data in a styled card with badges and description lists.

Step 1: Install the UI Package

Required for .tsx/.jsx FileSource widgets. When you point ui.template at a .tsx/.jsx file (the recommended pattern for non-trivial widgets), FrontMCP injects an auto-generated React mount that imports McpBridgeProvider from @frontmcp/ui/react. Server-side bundling fails without this package installed. Match the version to @frontmcp/sdk. react and react-dom stay external and load from the CDN at runtime, so only @frontmcp/ui needs to be present on disk.
Anchor FileSource paths to the tool file. Relative template: { file: './widget.tsx' } paths are resolved against process.cwd(), not the tool source’s directory. A widget at src/tools/foo.widget.tsx referenced as ./foo.widget.tsx from src/tools/foo.tool.ts will fail with ENOENT at tool-call time. Use fileURLToPath(new URL('./widget.tsx', import.meta.url)) (from node:url) or pass an absolute path. In a CommonJS project, import.meta.url is unavailable — use join(__dirname, 'widget.tsx') (from node:path) instead.
resourceMode is host-detected. Leave it unset and FrontMCP auto-switches to 'inline' when the connecting client is Claude (#456), so the widget actually renders in Claude’s sandboxed iframe — React is bundled into the widget itself (#454). Other hosts keep 'cdn' for a smaller payload. Set resourceMode explicitly to override the detection. Auto-detection only applies to per-call rendering modes (inline / hybrid / lean); servingMode: 'static' widgets are pre-compiled at startup with no client context, so set resourceMode: 'inline' explicitly when targeting Claude in static mode.
ui.csp is now emitted on the widget resource (#455). FrontMCP attaches ui.csp to the resources/read content item’s _meta.ui.csp (and _meta['ui/csp']) so MCP Apps hosts — particularly Claude — actually honor it. Previously the CSP only appeared on the tool listing, where Claude ignored it.
Use the *.widget.tsx naming convention. The scaffolded tsconfig.json excludes **/*.widget.tsx / **/*.widget.jsx from the server typecheck (frontmcp init also adds the excludes to existing tsconfigs). Widget sources are bundled separately by uipack/esbuild at render time, so the server tsconfig doesn’t need jsx: 'react-jsx' or @types/react for them. Add a sibling tsconfig.widget.json if you want IDE typecheck for widget files.

Step 2: Create a Tool with UI Template

The ui property in the @Tool decorator lets you define a template function that renders HTML:

UI Configuration Options

string
Human-readable description of what the widget displays. Shown to users in UI-capable hosts.
'inline' | 'fullscreen' | 'pip'
Preferred display mode (hint to the host — may be ignored):
  • inline - Rendered inline in the conversation (default)
  • fullscreen - Request fullscreen display
  • pip - Picture-in-picture
'auto' | 'inline' | 'static' | 'hybrid' | 'direct-url' | 'custom-url'
How the HTML is delivered to the client (default: auto):
  • auto - Auto-select per host (OpenAI / Claude / unknown)
  • inline - Embedded in tool response _meta['ui/html'] (works everywhere)
  • static - Pre-compiled at startup; client fetches ui://widget/{toolName}.html via resources/read
  • hybrid - Shell pre-compiled; component code + data delivered per call in _meta['ui/component']
  • direct-url - Served from an HTTP path on the MCP server (directPath)
  • custom-url - Served from an external URL (customWidgetUrl, supports a {token} placeholder)
function
A function that receives the execution context and returns an HTML string.

Available UI Components

Card

Wrap content in a styled container:

Badge

Display status or category labels:

Description List

Show key-value pairs:

Button

Create styled buttons:

Form and Input

Build forms with validation:

Template Context

The template function receives a context object with:
TypeScript: ctx must be annotated explicitly (TS7006). Under strict / noImplicitAny, template: (ctx) => … fails with Parameter 'ctx' implicitly has an 'any' type. The ui.template field is a union of multiple callable shapes (TemplateBuilderFn | string | ((props: any) => any) | FileSource), so TypeScript can’t pick a single contextual type for the arrow’s parameter. Either annotate ctx: TemplateContext<MyInput, MyOutput> (imported from @frontmcp/sdk), or move the widget into its own .tsx file and use the FileSource form (template: { file: … }).

Available Helpers

Always use helpers.escapeHtml() when rendering user-provided data to prevent XSS vulnerabilities.

Practical Example: Expense Summary


Platform Considerations

Different MCP hosts have different capabilities and network policies:
  • OpenAI Apps SDK — any CDN reachable; widget URI surfaces under _meta['openai/outputTemplate']
  • Claude (MCP-UI)only cdnjs.cloudflare.com is reachable; prefer resourceMode: 'inline' so the widget shell is self-contained, and pin any externals to cdnjs URLs via dependencies
  • MCP Inspector — useful for local development; honors servingMode: 'static'
  • Gemini / unknown hostsui is ignored; the tool returns JSON only
The default servingMode: 'auto' selects the right mode per host. See the Tool UI reference for the full surface: serving modes, CSP, the window.FrontMcpBridge runtime, file-based .tsx widgets, and platform-specific troubleshooting.

Next Steps

React SDK Components

Pre-built React components

Tool Reference

Full @Tool decorator options

CodeCall CRM Demo

See UI in a full application

Create Prompts

Build prompts alongside tools