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

npm install @frontmcp/ui

Step 2: Create a Tool with UI Template

The ui property in the @Tool decorator lets you define a template function that renders HTML:
import { Tool, ToolContext } from '@frontmcp/sdk';
import { card, descriptionList, badge } from '@frontmcp/ui';
import { z } from 'zod';

const inputSchema = {
  location: z.string().describe('City name or location'),
  units: z.enum(['celsius', 'fahrenheit']).optional().describe('Temperature units'),
};

const outputSchema = z.object({
  location: z.string(),
  temperature: z.number(),
  units: z.enum(['celsius', 'fahrenheit']),
  conditions: z.string(),
  humidity: z.number(),
  windSpeed: z.number(),
});

@Tool({
  name: 'get_weather',
  description: 'Get current weather for a location',
  inputSchema,
  outputSchema,
  ui: {
    widgetDescription: 'Displays current weather conditions',
    displayMode: 'inline',
    servingMode: 'static',
    template: (ctx) => {
      const { output, helpers } = ctx;
      const tempSymbol = output.units === 'celsius' ? '°C' : '°F';

      // Build weather details using descriptionList component
      const weatherDetails = descriptionList(
        [
          { term: 'Humidity', description: `${output.humidity}%` },
          { term: 'Wind Speed', description: `${output.windSpeed} km/h` },
        ],
        { layout: 'grid', className: 'mt-4' }
      );

      // Build condition badge
      const conditionBadge = badge(helpers.escapeHtml(output.conditions), {
        variant: output.conditions === 'sunny' ? 'success' : 'secondary',
        size: 'md',
      });

      // Main content
      const content = `
        <div class="text-center py-6">
          <div class="text-5xl font-light mb-2">
            ${output.temperature}${tempSymbol}
          </div>
          <div class="flex justify-center">
            ${conditionBadge}
          </div>
        </div>
        ${weatherDetails}
      `;

      // Wrap in card component
      return card(content, {
        title: helpers.escapeHtml(output.location),
        subtitle: 'Current Weather',
        variant: 'elevated',
        size: 'md',
      });
    },
  },
})
export default class GetWeatherTool extends ToolContext<typeof inputSchema, typeof outputSchema> {
  async execute(input: { location: string; units?: 'celsius' | 'fahrenheit' }) {
    // In production, call a real weather API
    return {
      location: input.location,
      temperature: 22,
      units: input.units || 'celsius',
      conditions: 'sunny',
      humidity: 55,
      windSpeed: 10,
    };
  }
}

UI Configuration Options

ui.widgetDescription
string
Human-readable description of what the widget displays. Shown to users in UI-capable hosts.
ui.displayMode
'inline' | 'modal' | 'panel'
How the widget should be displayed:
  • inline - Rendered directly in the conversation
  • modal - Opens in a modal dialog
  • panel - Shows in a side panel
ui.servingMode
'static' | 'iframe'
How the HTML is served:
  • static - HTML string is returned directly
  • iframe - Content is served via iframe URL
ui.template
function
A function that receives the execution context and returns an HTML string.

Available UI Components

Card

Wrap content in a styled container:
import { card } from '@frontmcp/ui';

card('<p>Card content</p>', {
  title: 'Card Title',
  subtitle: 'Optional subtitle',
  variant: 'elevated',  // 'default' | 'elevated' | 'outlined'
  size: 'md',           // 'sm' | 'md' | 'lg'
});

Badge

Display status or category labels:
import { badge } from '@frontmcp/ui';

badge('Active', {
  variant: 'success',  // 'primary' | 'secondary' | 'success' | 'warning' | 'error' | 'info'
  size: 'md',          // 'sm' | 'md' | 'lg'
});

Description List

Show key-value pairs:
import { descriptionList } from '@frontmcp/ui';

descriptionList([
  { term: 'Status', description: 'Active' },
  { term: 'Created', description: '2024-01-15' },
  { term: 'Owner', description: 'john@example.com' },
], {
  layout: 'grid',      // 'stacked' | 'grid' | 'inline'
  className: 'mt-4',
});

Button

Create styled buttons:
import { button } from '@frontmcp/ui';

button('Submit', {
  variant: 'primary',  // 'primary' | 'secondary' | 'outline' | 'ghost' | 'danger'
  type: 'submit',      // 'button' | 'submit' | 'reset'
});

Form and Input

Build forms with validation:
import { form, input, button } from '@frontmcp/ui';

form(`
  ${input({ name: 'email', label: 'Email', type: 'email', required: true })}
  ${input({ name: 'message', label: 'Message', type: 'textarea' })}
  ${button('Send', { type: 'submit', variant: 'primary' })}
`, {
  action: '/api/contact',
  method: 'post',
});

Template Context

The template function receives a context object with:
template: (ctx) => {
  // Input that was passed to the tool
  const { location, units } = ctx.input;

  // Output from execute()
  const { temperature, conditions } = ctx.output;

  // Helper functions
  const { escapeHtml, formatDate, formatNumber } = ctx.helpers;

  // Always escape user-provided strings!
  return `<p>${helpers.escapeHtml(location)}</p>`;
}

Available Helpers

HelperDescription
escapeHtml(str)Escape HTML entities to prevent XSS
formatDate(date)Format a date string
formatNumber(num, options)Format numbers with locale support
Always use helpers.escapeHtml() when rendering user-provided data to prevent XSS vulnerabilities.

Practical Example: Expense Summary

import { Tool, ToolContext } from '@frontmcp/sdk';
import { card, descriptionList, badge } from '@frontmcp/ui';
import { z } from 'zod';

@Tool({
  name: 'expense_summary',
  description: 'Get expense summary for a user',
  inputSchema: { userId: z.string() },
  outputSchema: z.object({
    userName: z.string(),
    totalExpenses: z.number(),
    pendingCount: z.number(),
    approvedCount: z.number(),
    status: z.enum(['under_budget', 'at_budget', 'over_budget']),
  }),
  ui: {
    widgetDescription: 'Expense summary dashboard',
    displayMode: 'inline',
    servingMode: 'static',
    template: (ctx) => {
      const { output, helpers } = ctx;

      const statusColors = {
        under_budget: 'success',
        at_budget: 'warning',
        over_budget: 'error',
      };

      const statusBadge = badge(
        output.status.replace('_', ' ').toUpperCase(),
        { variant: statusColors[output.status], size: 'lg' }
      );

      const details = descriptionList([
        { term: 'Total', description: helpers.formatNumber(output.totalExpenses, { style: 'currency', currency: 'USD' }) },
        { term: 'Pending', description: String(output.pendingCount) },
        { term: 'Approved', description: String(output.approvedCount) },
      ], { layout: 'grid' });

      return card(`
        <div class="flex justify-between items-center mb-4">
          <h3 class="text-lg font-semibold">${helpers.escapeHtml(output.userName)}</h3>
          ${statusBadge}
        </div>
        ${details}
      `, {
        variant: 'elevated',
        size: 'md',
      });
    },
  },
})
export default class ExpenseSummaryTool extends ToolContext {
  async execute(input: { userId: string }) {
    // Fetch from database in production
    return {
      userName: 'John Doe',
      totalExpenses: 1234.56,
      pendingCount: 3,
      approvedCount: 12,
      status: 'under_budget' as const,
    };
  }
}

Platform Detection

Different platforms (OpenAI, Claude, browsers) have different capabilities. Use theme utilities to adapt:
import { createTheme, canUseCdn, needsInlineScripts } from '@frontmcp/uipack/theme';

// Claude Artifacts blocks external requests
// Use inline scripts when needed
if (needsInlineScripts(platform)) {
  // Inline Tailwind CSS
}

Next Steps

UI Library Reference

Complete UI component documentation

Tool Reference

Full @Tool decorator options

CodeCall CRM Demo

See UI in a full application

Create Prompts

Build prompts alongside tools