Skip to main content

Setup

1. Configure TypeScript

Ensure your tsconfig.json supports JSX:
tsconfig.json
{
  "compilerOptions": {
    "jsx": "react-jsx",
    "jsxImportSource": "react"
  }
}

2. Install React

React is a peer dependency:
npm install react react-dom
npm install -D @types/react @types/react-dom

Basic Usage

Create a component file:
src/widgets/weather-widget.tsx
import { TemplateContext } from '@frontmcp/ui';

interface WeatherOutput {
  location: string;
  temperature: number;
  conditions: string;
}

export function WeatherWidget({ output }: TemplateContext<unknown, WeatherOutput>) {
  return (
    <div className="p-6 bg-white rounded-xl shadow-lg">
      <h2 className="text-2xl font-bold">{output.location}</h2>
      <div className="text-4xl my-4">{output.temperature}°C</div>
      <p className="text-gray-600">{output.conditions}</p>
    </div>
  );
}
Use it in your tool:
src/tools/weather.tool.ts
import { Tool, ToolContext } from '@frontmcp/sdk';
import { WeatherWidget } from '../widgets/weather-widget';

@Tool({
  name: 'get_weather',
  ui: {
    template: WeatherWidget,
  },
})
export class GetWeatherTool extends ToolContext {
  // ...
}

Props Interface

React templates receive the same context as other templates:
import { TemplateContext } from '@frontmcp/ui';

// Define your input/output types
interface MyInput {
  userId: string;
}

interface MyOutput {
  name: string;
  email: string;
}

// Use them in the component
export function MyWidget({ input, output, helpers }: TemplateContext<MyInput, MyOutput>) {
  return (
    <div>
      <p>User ID: {input.userId}</p>
      <p>Name: {output.name}</p>
      <p>Email: {output.email}</p>
    </div>
  );
}

Using Helpers

The helpers prop provides utility functions:
export function OrderWidget({ output, helpers }: TemplateContext<unknown, Order>) {
  return (
    <div>
      <p>Order Date: {helpers.formatDate(output.date)}</p>
      <p>Total: {helpers.formatCurrency(output.total, 'USD')}</p>
    </div>
  );
}
React automatically escapes content, so you don’t need helpers.escapeHtml() for JSX text content. Only use it when dangerously setting innerHTML.

Using FrontMCP Components

Import and use string components:
import { card, badge, button } from '@frontmcp/ui';

export function ProfileWidget({ output }: TemplateContext<unknown, User>) {
  // Components return HTML strings - use dangerouslySetInnerHTML
  const statusBadge = badge(output.status, {
    variant: output.status === 'Active' ? 'success' : 'default',
  });

  return (
    <div className="p-4">
      <div className="flex items-center gap-3">
        <h2 className="text-xl font-bold">{output.name}</h2>
        <span dangerouslySetInnerHTML={{ __html: statusBadge }} />
      </div>
      {/* Or wrap the whole thing */}
      <div
        dangerouslySetInnerHTML={{
          __html: card(output.bio, { title: 'About' }),
        }}
      />
    </div>
  );
}

Creating React Wrappers

For cleaner code, wrap string components:
src/widgets/components.tsx
import { button as htmlButton, ButtonOptions } from '@frontmcp/ui';

// React wrapper for button
export function Button({ children, ...props }: ButtonOptions & { children: string }) {
  return (
    <span dangerouslySetInnerHTML={{ __html: htmlButton(children, props) }} />
  );
}

// Usage
<Button variant="primary" onClick="submit()">Submit</Button>

Conditional Rendering

Use standard React patterns:
export function StatusWidget({ output }: TemplateContext<unknown, Status>) {
  if (output.loading) {
    return (
      <div className="flex justify-center p-8">
        <div className="animate-spin h-8 w-8 border-4 border-blue-500 rounded-full border-t-transparent" />
      </div>
    );
  }

  if (output.error) {
    return (
      <div className="bg-red-50 border border-red-200 rounded-lg p-4">
        <p className="text-red-800">{output.error}</p>
      </div>
    );
  }

  return (
    <div className="bg-green-50 border border-green-200 rounded-lg p-4">
      <p className="text-green-800">Success!</p>
    </div>
  );
}

Lists and Mapping

interface ListOutput {
  items: Array<{ id: string; name: string; price: number }>;
}

export function ItemList({ output, helpers }: TemplateContext<unknown, ListOutput>) {
  if (output.items.length === 0) {
    return <p className="text-gray-500 text-center py-4">No items found</p>;
  }

  return (
    <ul className="divide-y">
      {output.items.map((item) => (
        <li key={item.id} className="py-3 flex justify-between">
          <span>{item.name}</span>
          <span>{helpers.formatCurrency(item.price)}</span>
        </li>
      ))}
    </ul>
  );
}

Component Composition

Split complex widgets into smaller components:
src/widgets/order/index.tsx
import { TemplateContext } from '@frontmcp/ui';
import { OrderHeader } from './order-header';
import { OrderItems } from './order-items';
import { OrderTotal } from './order-total';

export function OrderWidget({ output, helpers }: TemplateContext<unknown, Order>) {
  return (
    <div className="bg-white rounded-xl shadow-lg overflow-hidden">
      <OrderHeader order={output} />
      <OrderItems items={output.items} formatCurrency={helpers.formatCurrency} />
      <OrderTotal total={output.total} formatCurrency={helpers.formatCurrency} />
    </div>
  );
}

Styling

Tailwind Classes

export function Card({ output }: TemplateContext<unknown, CardData>) {
  return (
    <div className="bg-white rounded-xl shadow-lg p-6 hover:shadow-xl transition-shadow">
      <h3 className="text-lg font-semibold text-gray-800">{output.title}</h3>
      <p className="mt-2 text-gray-600">{output.description}</p>
    </div>
  );
}

CSS Modules

import styles from './widget.module.css';

export function Widget({ output }: TemplateContext<unknown, Data>) {
  return (
    <div className={styles.container}>
      <h2 className={styles.title}>{output.title}</h2>
    </div>
  );
}

Inline Styles

export function Badge({ text, color }: { text: string; color: string }) {
  return (
    <span
      style={{
        backgroundColor: `${color}20`,
        color: color,
        padding: '2px 8px',
        borderRadius: '9999px',
        fontSize: '12px',
      }}
    >
      {text}
    </span>
  );
}

TypeScript Integration

Full type safety with generics:
import { TemplateContext } from '@frontmcp/ui';
import { z } from 'zod';

// Define schemas using z.object()
const inputSchema = z.object({
  userId: z.string(),
});

const outputSchema = z.object({
  user: z.object({
    id: z.string(),
    name: z.string(),
    email: z.string(),
    role: z.enum(['admin', 'user', 'guest']),
  }),
});

// Infer types from schemas
type Input = z.infer<typeof inputSchema>;
type Output = z.infer<typeof outputSchema>;

// Fully typed component
export function UserWidget({ input, output }: TemplateContext<Input, Output>) {
  const { user } = output;

  return (
    <div>
      <p>Requested: {input.userId}</p>
      <p>Name: {user.name}</p>
      <p>Role: {user.role}</p> {/* TypeScript knows this is 'admin' | 'user' | 'guest' */}
    </div>
  );
}

Server-Side Rendering

React templates are rendered server-side by default using react-dom/server:
// FrontMCP automatically does this:
import { renderToString } from 'react-dom/server';

const html = renderToString(<YourWidget {...context} />);
For client-side interactivity, see Hydration.

Best Practices

  1. Keep widgets pure - Avoid side effects, use props only
  2. Type everything - Use TypeScript for better DX
  3. Split large widgets - Compose from smaller components
  4. Don’t use hooks for SSR - They won’t work without hydration
  5. Escape dangerouslySetInnerHTML - When using string components
// ✅ Good - pure, typed, composed
export function Dashboard({ output }: TemplateContext<unknown, DashboardData>) {
  return (
    <div className="space-y-4">
      <StatsCards stats={output.stats} />
      <RecentActivity items={output.activity} />
      <QuickActions />
    </div>
  );
}

// ❌ Avoid - hooks without hydration
export function BadWidget({ output }) {
  const [count, setCount] = useState(0); // Won't work in SSR!
  // ...
}