---
title: Tools
description: AI-callable functions with typed input/output schemas, validation, and execution context
slug: servers/tools
icon: wrench
keywords: ['frontmcp', 'mcp', 'model context protocol', 'mcp server', 'server', 'components', 'tools', '@Tool', 'function calling', 'typescript']
---

Tools are **typed actions** that execute operations with side effects. They're the primary way to enable an AI model to interact with external systems—calling APIs, modifying data, performing calculations, or triggering workflows.

<Info>
This feature implements the [MCP Tools specification](https://modelcontextprotocol.io/specification/2025-11-25/server/tools). FrontMCP handles all protocol details automatically.
</Info>

<Tip>
**Nx users:** Scaffold with `nx g @frontmcp/nx:tool my-tool --project my-app`. See [Tool Generator](/frontmcp/nx-plugin/generators/tool).
</Tip>

## Why Tools?

In the Model Context Protocol, tools serve a distinct purpose from resources and prompts:

| Aspect           | Tool                                | Resource         | Prompt                         |
| ---------------- | ----------------------------------- | ---------------- | ------------------------------ |
| **Purpose**      | Execute actions                     | Provide data     | Provide templated instructions |
| **Direction**    | Model triggers execution            | Model pulls data | Model uses messages            |
| **Side effects** | Yes (mutations, API calls)          | No (read-only)   | No (message generation)        |
| **Use case**     | Actions, calculations, integrations | Context loading  | Conversation templates         |

Tools are ideal for:

- **API integrations** — call external services, webhooks, third-party APIs
- **Data mutations** — create, update, delete records
- **Calculations** — perform computations, transformations
- **System operations** — file operations, process management
- **Workflows** — trigger multi-step processes, orchestration

---

## Creating Tools

### Class Style

Use class decorators for tools that need dependency injection, lifecycle hooks, or complex logic:

```ts
import { Tool, ToolContext } from '@frontmcp/sdk';
import { z } from '@frontmcp/sdk';

@Tool({
  name: 'greet',
  description: 'Greets a user by name',
  inputSchema: { name: z.string() },
})
class GreetTool extends ToolContext {
  async execute({ name }: { name: string }) {
    return `Hello, ${name}!`;
  }
}
```

### Function Style

For simpler tools, use the functional builder:

```ts
import { tool } from '@frontmcp/sdk';
import { z } from '@frontmcp/sdk';

const GreetTool = tool({
  name: 'greet',
  description: 'Greets a user by name',
  inputSchema: { name: z.string() },
})(({ name }) => `Hello, ${name}!`);
```

---

## Registering Tools

Add tools to your app via the `tools` array:

```ts
import { App } from '@frontmcp/sdk';

@App({
  id: 'my-app',
  name: 'My Application',
  tools: [GreetTool, CalculateTool, SendEmailTool],
})
class MyApp {}
```

Tools can also be generated dynamically by **adapters** (e.g., OpenAPI adapter) or **plugins**.

### Loading from npm or Remote Servers

You can mix local tool classes with tools loaded from npm packages or proxied from remote MCP servers:

```ts
import { App, Tool } from '@frontmcp/sdk';

@App({
  id: 'my-app',
  name: 'My Application',
  tools: [
    GreetTool,                                                // Local class
    Tool.esm('@acme/tools@^1.0.0', 'echo'),                  // Single tool from npm
    Tool.remote('https://api.example.com/mcp', 'search'),     // Single tool from remote server
  ],
})
class MyApp {}
```

<Info>
`Tool.esm()` loads a single named tool from an npm package at runtime. `Tool.remote()` proxies a single tool from a remote MCP server.
For loading **entire apps** (all tools, resources, prompts), use [`App.esm()`](/frontmcp/servers/esm-packages) or [`App.remote()`](/frontmcp/servers/apps#remote-apps).
</Info>

---

## Input Schemas

Tools use Zod schemas for type-safe input validation. The schema is automatically converted to JSON Schema for MCP protocol compatibility.

### Basic Types

```ts
@Tool({
  name: 'user-action',
  inputSchema: {
    userId: z.string(),
    count: z.number(),
    enabled: z.boolean(),
  },
})
```

### With Descriptions

```ts
@Tool({
  name: 'send-email',
  inputSchema: {
    to: z.string().email().describe('Recipient email address'),
    subject: z.string().describe('Email subject line'),
    body: z.string().describe('Email body content'),
  },
})
```

### Optional and Default Values

```ts
@Tool({
  name: 'search',
  inputSchema: {
    query: z.string(),
    limit: z.number().default(10).describe('Max results to return'),
    offset: z.number().optional().describe('Pagination offset'),
  },
})
```

### Complex Types

```ts
@Tool({
  name: 'create-order',
  inputSchema: {
    customerId: z.string(),
    items: z.array(z.object({
      productId: z.string(),
      quantity: z.number().min(1),
    })),
    shipping: z.enum(['standard', 'express', 'overnight']),
  },
})
```

---

## Output Schemas

Optionally define an output schema for response validation:

```ts
@Tool({
  name: 'calculate-total',
  inputSchema: {
    items: z.array(z.object({
      price: z.number(),
      quantity: z.number(),
    })),
  },
  outputSchema: z.object({
    subtotal: z.number(),
    tax: z.number(),
    total: z.number(),
  }),
})
class CalculateTotalTool extends ToolContext {
  execute({ items }) {
    const subtotal = items.reduce((sum, item) => sum + item.price * item.quantity, 0);
    const tax = subtotal * 0.1;
    return { subtotal, tax, total: subtotal + tax };
  }
}
```

### Output schema exposure

By default a structured `outputSchema` is advertised as the tool's `outputSchema` (JSON Schema) in `tools/list`. The `output` policy lets you control **where** that schema surfaces. It is declarable on `@Tool`, `@App`, and `@FrontMcp`, and the effective value for a tool is resolved with a **Tool > App > server > default** cascade — mirroring the [OpenAPI adapter's `outputSchema.mode`](/frontmcp/adapters/openapi-adapter#output-schema-display).

```ts
output?: {
  allowNonFinite?: boolean;                                    // default false — see Output validation
  schemaMode?: 'definition' | 'description' | 'both' | 'none'; // default 'definition'
  schemaDescriptionFormat?: 'summary' | 'jsonSchema';          // default 'summary'
}
```

| `schemaMode`    | Effect                                                                                           |
| --------------- | ------------------------------------------------------------------------------------------------ |
| `'definition'`  | **(Default)** Advertise the schema as the tool's `outputSchema` (JSON Schema).                   |
| `'description'` | Fold a readable rendering of the schema into the tool `description`, and omit `outputSchema`.    |
| `'both'`        | Advertise as `outputSchema` **and** fold it into the description.                                |
| `'none'`        | Do not expose the output schema anywhere.                                                        |

When the schema is folded into the description (`'description'` / `'both'`), `schemaDescriptionFormat` picks the rendering: `'summary'` (a compact human-readable property list) or `'jsonSchema'` (a fenced JSON Schema code block).

```ts
@Tool({
  name: 'calculate-total',
  inputSchema: { /* … */ },
  outputSchema: z.object({ subtotal: z.number(), tax: z.number(), total: z.number() }),
  // Some clients don't render `outputSchema` — fold it into the description instead.
  output: { schemaMode: 'description' },
})
class CalculateTotalTool extends ToolContext {
  /* … */
}
```

Set it once at the server level to apply a house style everywhere, then override per tool where needed:

```ts
@FrontMcp({
  info: { name: 'My Server', version: '1.0.0' },
  apps: [MyApp],
  output: { schemaMode: 'both', schemaDescriptionFormat: 'summary' },
})
export default class MyServer {}
```

<Note>
Output-schema exposure only applies to structured object outputs (a Zod raw shape or a top-level `z.object`) — the same forms that get advertised as `outputSchema`. Primitive, media, multi-content, and union outputs flow through `content` and have nothing object-shaped to expose. Runtime output validation still applies to every form regardless of `schemaMode`.
</Note>

---

## Return Values

Tools support multiple return formats. The SDK automatically converts your return value to the MCP `CallToolResult` format.

### Simple Returns

```ts
// String -> text content
execute() {
  return 'Operation completed successfully';
}

// Object -> auto-serialized to JSON
execute() {
  return { id: '123', status: 'created' };
}

// Number/Boolean -> converted to text
execute() {
  return 42;
}
```

### Full MCP Format

For complete control over the response, return the full `CallToolResult` structure:

```ts
execute() {
  return {
    content: [
      {
        type: 'text',
        text: 'Operation completed',
      },
    ],
    isError: false,
  };
}
```

### Multiple Content Items

Return an array to include multiple content blocks:

```ts
execute() {
  return {
    content: [
      { type: 'text', text: 'Summary of results' },
      { type: 'text', text: JSON.stringify(details) },
    ],
  };
}
```

---

## Tool Metadata

```ts
@Tool({
  name: string,              // Required: unique identifier
  description?: string,      // Optional: hint for the LLM
  inputSchema: ZodSchema,    // Required: Zod schema for input validation
  outputSchema?: ZodSchema,  // Optional: Zod schema for output validation
  examples?: Array<{         // Optional: usage examples for discovery
    description: string;     //   - what this example demonstrates
    input: Record<string, unknown>; //   - example input parameters
    output?: unknown;        //   - optional expected output
  }>,
  title?: string,            // Optional: human-readable display name
  icons?: Icon[],            // Optional: UI icons
  tags?: string[],           // Optional: categorization tags
  annotations?: {            // Optional: MCP tool annotations
    title?: string;          //   - display title
    readOnlyHint?: boolean;  //   - hint that tool is read-only
    destructiveHint?: boolean; // - hint that tool is destructive
    idempotentHint?: boolean;  // - hint that tool is idempotent
    openWorldHint?: boolean;   // - hint for open-world assumption
  },
  ui?: ToolUIConfig,         // Optional: visual widget configuration
  hideFromDiscovery?: boolean, // Optional: hide from tools/list (default: false)
  availableWhen?: {          // Optional: environment availability constraint
    platform?: string[];     //   - OS: 'darwin', 'linux', 'win32'
    runtime?: string[];      //   - runtime: 'node', 'browser', 'edge', 'bun', 'deno'
    deployment?: string[];   //   - deployment: 'serverless', 'standalone'
    env?: string[];          //   - NODE_ENV: 'production', 'development', 'test'
  },
})
```

**Field descriptions:**

| Field               | Description                                                  |
| ------------------- | ------------------------------------------------------------ |
| `name`              | Programmatic identifier used internally and in MCP responses |
| `description`       | Helps the model understand when and how to use this tool     |
| `inputSchema`       | Zod schema defining expected input parameters                |
| `outputSchema`      | Zod schema for validating and documenting output             |
| `title`             | Human-friendly name for UI display                           |
| `icons`             | Array of icons for visual representation in clients          |
| `tags`              | Categorization for organization and filtering                |
| `annotations`       | MCP-defined hints about tool behavior                        |
| `examples`          | Usage examples for discovery and LLM understanding           |
| `ui`                | Visual widget configuration (template, display mode, etc.)   |
| `output`            | Output-validation + output-schema exposure policy — see [Output schema exposure](#output-schema-exposure) |
| `hideFromDiscovery` | When `true`, tool is callable but not listed in `tools/list` |
| `availableWhen`     | Restrict discovery and execution to specific environments. See [Environment Awareness](/frontmcp/features/environment-awareness) |

---

## Tool Examples

Provide examples to improve discoverability and help LLMs understand how to use your tools effectively.

<CodeGroup>
```ts Class Style
@Tool({
  name: 'users:create',
  description: 'Create a new user account',
  inputSchema: {
    email: z.string().email(),
    role: z.enum(['admin', 'user']),
  },
  examples: [
    {
      description: 'Create an admin user',
      input: { email: 'admin@example.com', role: 'admin' },
    },
    {
      description: 'Create a regular user',
      input: { email: 'user@example.com', role: 'user' },
    },
  ],
})
class CreateUserTool extends ToolContext {
  async execute({ email, role }) {
    // Implementation
  }
}
```

```ts Function Style
const CreateUserTool = tool({
  name: 'users:create',
  description: 'Create a new user account',
  inputSchema: {
    email: z.string().email(),
    role: z.enum(['admin', 'user']),
  },
  examples: [
    {
      description: 'Create an admin user',
      input: { email: 'admin@example.com', role: 'admin' },
    },
    {
      description: 'Create a regular user',
      input: { email: 'user@example.com', role: 'user' },
    },
  ],
})(({ email, role }) => {
  // Implementation
});
```

</CodeGroup>

<CardGroup cols={2}>
  <Card title="CodeCall Discovery" icon="magnifying-glass">
    Examples are indexed for semantic search with 2x weight, helping users find the right tools faster.
  </Card>
  <Card title="LLM Understanding" icon="brain">
    The `codecall:describe` tool returns up to 5 examples per tool to help LLMs understand usage patterns.
  </Card>
</CardGroup>

<Tip>
If you don't provide examples, FrontMCP auto-generates smart examples based on tool intent (create, list, get, update, delete, search). User-provided examples always take priority.
</Tip>

---

## Tool Annotations

Annotations provide hints to clients about tool behavior:

```ts
@Tool({
  name: 'delete-record',
  description: 'Permanently delete a record',
  inputSchema: { id: z.string() },
  annotations: {
    destructiveHint: true,  // Warns clients this operation is destructive
  },
})
```

| Annotation        | Description                                                            |
| ----------------- | ---------------------------------------------------------------------- |
| `title`           | Display title for the tool                                             |
| `readOnlyHint`    | Tool doesn't modify state (like a resource, but returns computed data) |
| `destructiveHint` | Tool performs irreversible operations (delete, overwrite)              |
| `idempotentHint`  | Multiple identical calls produce the same result                       |
| `openWorldHint`   | Tool interacts with external systems (APIs, services)                  |

---

## Tool Context

Class-based tools have access to a rich execution context via `this`:

```ts
@Tool({
  name: 'context-example',
  inputSchema: { query: z.string() },
})
class ContextExampleTool extends ToolContext {
  async execute({ query }: { query: string }) {
    // Input received by the tool
    this.input;              // { query: 'value' }
    this.metadata;           // Tool metadata (name, description, etc.)

    // Authentication
    this.context.authInfo;   // Partial<AuthInfo> — preferred (token, user, sessionId)
    this.auth;               // FrontMcpAuthContext (typed identity, roles, claims)
    // this.authInfo is a deprecated alias kept for backward-compat — prefer `this.context.authInfo`

    // Dependency injection
    this.get(ConfigService); // Resolve a provider
    this.tryGet(Cache);      // Resolve or return undefined

    // Scope access
    this.scope;              // Access the current scope

    // Utilities
    this.fetch(url);         // Built-in fetch for HTTP requests

    // Notifications (MCP 2025-11-25)
    this.notify(message);           // Send notifications/message to client
    this.progress(current, total);  // Send notifications/progress to client

    // Flow control
    this.respond(value);     // End execution with a response

    return `Processed: ${query}`;
  }
}
```

### Using Providers

Inject services via the `get()` method:

```ts
@Tool({
  name: 'create-user',
  inputSchema: {
    email: z.string().email(),
    name: z.string(),
  },
})
class CreateUserTool extends ToolContext {
  async execute({ email, name }) {
    const db = this.get(DatabaseProvider);
    const user = await db.users.create({ email, name });
    return { id: user.id, email: user.email };
  }
}
```

### Request Context

Class-based tools have access to the full request context (`FrontMcpContext`) including tracing, timing, and authentication:

```ts
@Tool({
  name: 'traced-tool',
  inputSchema: { query: z.string() },
})
class TracedTool extends ToolContext {
  async execute({ query }: { query: string }) {
    // Full request context
    const ctx = this.context;

    // Tracing (W3C Trace Context)
    console.log(ctx.requestId);                    // Unique request ID
    console.log(ctx.traceContext.traceId);         // W3C Trace ID
    console.log(ctx.traceContext.parentId);        // Parent span ID (if present)

    // Authentication
    console.log(ctx.authInfo.token);               // JWT token
    // Typed identity helpers also available via this.auth (FrontMcpAuthContext)

    // Session
    console.log(ctx.sessionId);                    // MCP session ID

    // Timing marks for performance tracking
    ctx.mark('db-query-start');
    await this.queryDatabase();
    ctx.mark('db-query-end');
    console.log(ctx.elapsed('db-query-start', 'db-query-end')); // ms

    // Request metadata
    console.log(ctx.metadata.userAgent);
    console.log(ctx.metadata.clientIp);

    return `Processed: ${query}`;
  }
}
```

<Tip>
  `this.context` returns the active `FrontMcpContext`. For typed identity (roles, scopes, claims) prefer `this.auth`.
  See [Request Context](/frontmcp/extensibility/request-context) for complete API reference.
</Tip>

---

## Progress Notifications

Send real-time updates to clients during long-running operations using MCP notifications.

### Sending Messages

Use `this.notify()` to send log messages to the client:

```ts
@Tool({
  name: 'long-task',
  inputSchema: { items: z.array(z.string()) },
})
class LongTaskTool extends ToolContext {
  async execute({ items }) {
    await this.notify('Starting processing...', 'info');

    for (const item of items) {
      await this.notify(`Processing: ${item}`, 'debug');
      await this.processItem(item);
    }

    await this.notify('All items processed!', 'info');
    return { processed: items.length };
  }
}
```

Log levels: `'debug'`, `'info'`, `'warning'`, `'error'`

### Sending Progress

Use `this.progress()` for progress bar updates. This only works when the client includes a `progressToken` in the request:

```ts
@Tool({
  name: 'file-processor',
  inputSchema: { files: z.array(z.string()) },
})
class FileProcessorTool extends ToolContext {
  async execute({ files }) {
    for (let i = 0; i < files.length; i++) {
      // progress(current, total, message?)
      await this.progress(i + 1, files.length, `Processing ${files[i]}`);
      await this.processFile(files[i]);
    }
    return { processed: files.length };
  }
}
```

<Info>
Progress notifications are only sent if the client includes a `progressToken` in the request's `_meta` field.
If no token is provided, `this.progress()` returns `false` and silently succeeds.
</Info>

### Progress Token Access

The `progressToken` is automatically extracted from the request's `_meta` field and made available via the `ToolCallExtra` type for advanced use cases:

```ts
import type { ToolCallExtra } from '@frontmcp/sdk';

// In custom tool handlers or plugins, you can access the progressToken directly
const extra: ToolCallExtra = {
  authInfo: { ... },
  progressToken: 'token-123',  // From request._meta.progressToken
};
```

For most use cases, simply use `this.progress()` which handles the token automatically.

---

## Real-World Examples

### Calculator Tool

```ts
@Tool({
  name: 'calculate',
  description: 'Perform mathematical calculations',
  inputSchema: {
    expression: z.string().describe('Mathematical expression to evaluate'),
  },
  annotations: {
    readOnlyHint: true,
    idempotentHint: true,
  },
})
class CalculateTool extends ToolContext {
  execute({ expression }) {
    // Using a safe math parser (not eval!)
    const result = safeEvaluate(expression);
    return { expression, result };
  }
}
```

### API Integration Tool

```ts
@Tool({
  name: 'create-github-issue',
  description: 'Create a new issue in a GitHub repository',
  inputSchema: {
    owner: z.string().describe('Repository owner'),
    repo: z.string().describe('Repository name'),
    title: z.string().describe('Issue title'),
    body: z.string().optional().describe('Issue body'),
    labels: z.array(z.string()).optional().describe('Labels to apply'),
  },
  annotations: {
    openWorldHint: true,
  },
})
class CreateGitHubIssueTool extends ToolContext {
  async execute({ owner, repo, title, body, labels }) {
    const config = this.get(ConfigProvider);

    const response = await this.fetch(
      `https://api.github.com/repos/${owner}/${repo}/issues`,
      {
        method: 'POST',
        headers: {
          'Authorization': `token ${config.githubToken}`,
          'Accept': 'application/vnd.github.v3+json',
        },
        body: JSON.stringify({ title, body, labels }),
      }
    );

    if (!response.ok) {
      throw new Error(`GitHub API error: ${response.status}`);
    }

    const issue = await response.json();
    return {
      id: issue.id,
      number: issue.number,
      url: issue.html_url,
    };
  }
}
```

### Database Mutation Tool

```ts
@Tool({
  name: 'update-user-status',
  description: 'Update a user\'s account status',
  inputSchema: {
    userId: z.string().describe('User ID'),
    status: z.enum(['active', 'suspended', 'deleted']).describe('New status'),
    reason: z.string().optional().describe('Reason for status change'),
  },
  annotations: {
    destructiveHint: true,
  },
})
class UpdateUserStatusTool extends ToolContext {
  async execute({ userId, status, reason }) {
    const db = this.get(DatabaseProvider);
    const audit = this.get(AuditLogProvider);

    const user = await db.users.findById(userId);
    if (!user) {
      throw new Error(`User ${userId} not found`);
    }

    await db.users.update(userId, { status });
    await audit.log({
      action: 'user_status_change',
      userId,
      previousStatus: user.status,
      newStatus: status,
      reason,
      performedBy: this.context.authInfo?.user?.sub,
    });

    return {
      userId,
      previousStatus: user.status,
      newStatus: status,
      updatedAt: new Date().toISOString(),
    };
  }
}
```

### File Operation Tool

```ts
@Tool({
  name: 'write-file',
  description: 'Write content to a file',
  inputSchema: {
    path: z.string().describe('File path'),
    content: z.string().describe('File content'),
    encoding: z.enum(['utf-8', 'base64']).default('utf-8'),
  },
})
class WriteFileTool extends ToolContext {
  async execute({ path, content, encoding }) {
    const fs = await import('fs/promises');
    const pathModule = await import('path');

    // Security: validate path is within allowed directory
    const safePath = pathModule.resolve(process.cwd(), 'workspace', path);
    if (!safePath.startsWith(pathModule.resolve(process.cwd(), 'workspace'))) {
      throw new Error('Path traversal not allowed');
    }

    await fs.writeFile(safePath, content, encoding);

    return {
      path: safePath,
      size: Buffer.byteLength(content, encoding),
      written: true,
    };
  }
}
```

---

## MCP Protocol Integration

Tools integrate with the MCP protocol via two flows:

| Flow         | Description                                                       |
| ------------ | ----------------------------------------------------------------- |
| `tools/list` | Returns all available tools with their metadata and input schemas |
| `tools/call` | Executes a specific tool with provided arguments                  |

When a client requests `tools/call` with a name and arguments:

1. The SDK locates the tool by name
2. Arguments are validated against the tool's `inputSchema`
3. The `execute()` method is called with the validated arguments
4. The return value is validated against `outputSchema` (if provided) and converted to MCP `CallToolResult` format

### Capabilities

FrontMCP automatically advertises tool capabilities during MCP initialization:

```json
{
  "capabilities": {
    "tools": {
      "listChanged": true
    }
  }
}
```

| Capability    | Description                                                                                          |
| ------------- | ---------------------------------------------------------------------------------------------------- |
| `listChanged` | When `true`, the server will send `notifications/tools/list_changed` when tools are added or removed |

The SDK sets `listChanged: true` when you have any tools registered, enabling clients to receive real-time notifications when tools are dynamically added or removed.

### Change Notifications

When tools change dynamically (e.g., via adapters or plugins), FrontMCP automatically sends `notifications/tools/list_changed` to connected clients. Clients that support this notification will refresh their tool list.

<Tip>
For the full protocol specification, see [MCP Tools](https://modelcontextprotocol.io/specification/2025-11-25/server/tools).
</Tip>

---

## Tool UI

Tools can render visual widgets alongside their responses. This enables rich, interactive presentations of tool outputs—weather cards, order summaries, data tables, and more.

### Basic UI Configuration

Add a `ui` property to attach a visual template:

```ts
@Tool({
  name: 'get_weather',
  description: 'Get current weather for a location',
  inputSchema: {
    location: z.string(),
  },
  ui: {
    template: (ctx) => `
      <div class="weather-card">
        <h2>${ctx.helpers.escapeHtml(ctx.output.location)}</h2>
        <p>${ctx.output.temperature}°C - ${ctx.output.conditions}</p>
      </div>
    `,
    widgetDescription: 'Displays current weather conditions',
  },
})
class GetWeatherTool extends ToolContext {
  async execute({ location }) {
    return { location, temperature: 22, conditions: 'Sunny' };
  }
}
```

### Template Types

FrontMCP auto-detects your template type:

<CodeGroup>
```ts HTML Template
import { type TemplateContext } from '@frontmcp/sdk';

ui: {
  // Annotate `ctx` explicitly under strict / noImplicitAny.
  template: (ctx: TemplateContext<MyInput, MyOutput>) =>
    `<p>${ctx.helpers.escapeHtml(ctx.output.message)}</p>`,
}
```

```tsx React Component
import WeatherCard from './components/WeatherCard';

ui: {
  template: WeatherCard, // React component
}
```

```ts MDX Template
ui: {
  template: `
# Weather Report

**Location:** {output.location}
**Temperature:** {output.temperature}°C

<Alert type="info">Data updated every 15 minutes</Alert>
  `,
  mdxComponents: { Alert },
}
```

</CodeGroup>

<Warning>
**TypeScript: annotate the `ctx` parameter (TS7006).** When you inline an HTML template as an arrow function, you need to annotate `ctx`. Because `ui.template` is a union of multiple callable shapes (`TemplateBuilderFn | string | ((props: any) => any) | FileSource`), TypeScript can't pick a single contextual type for the arrow's parameter, so `template: (ctx) => …` errors with `Parameter 'ctx' implicitly has an 'any' type`. 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: … }`) — both avoid the inference gap.
</Warning>

### UI Configuration Options

| Option               | Description                                                                             |
| -------------------- | --------------------------------------------------------------------------------------- |
| `template`           | HTML function, React component, or MDX string                                           |
| `displayMode`        | `'inline'` (default), `'fullscreen'`, or `'pip'`                                        |
| `widgetDescription`  | Human-readable description shown to users                                               |
| `widgetAccessible`   | Allow widget to call tools via MCP Bridge                                               |
| `csp`                | Content Security Policy (allowed domains)                                               |
| `servingMode`        | How HTML is delivered: `'auto'` (default), `'inline'`, `'static'`, `'hybrid'`, etc.     |
| `htmlResponsePrefix` | Text prefix for Claude dual-payload HTML block (default: `'Here is the visual result'`) |
| `mdxComponents`      | Custom components for MDX templates                                                     |
| `hydrate`            | Enable client-side React hydration for interactivity                                    |

<Info>
**`.tsx`/`.jsx` widgets require `@frontmcp/ui` installed.** When `ui.template` points 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`. Install `@frontmcp/ui` in the consuming project at the same version as `@frontmcp/sdk` — without it, server-side bundling fails.

In the default `resourceMode: 'cdn'` mode, `react` / `react-dom` stay external and load from the CDN at runtime, so only `@frontmcp/ui` needs to be present on disk. When the framework selects `resourceMode: 'inline'` — either explicitly or via host detection (Claude, #456) — `react` and `react-dom` must also be resolvable from the consuming project so esbuild can bundle them into the widget; install them as devDependencies if your project doesn't already pull them in. See [building-tool-ui](/frontmcp/guides/building-tool-ui#step-1-install-the-ui-package) for the install snippet and the per-host trade-offs.
</Info>

<Warning>
**FileSource paths resolve against `process.cwd()`, not the tool file.** A bare `template: { file: './widget.tsx' }` from `src/tools/foo.tool.ts` looks for `<cwd>/widget.tsx`, not `src/tools/widget.tsx` — and the mismatch only surfaces at tool-call time as `ENOENT`. Always anchor relative paths to the tool source with `fileURLToPath(new URL('./widget.tsx', import.meta.url))` (from `node:url`), or pass an absolute path:

```ts
import { fileURLToPath } from 'node:url';
const widgetPath = fileURLToPath(new URL('./weather.widget.tsx', import.meta.url));

ui: { template: { file: widgetPath } }
```

In a **CommonJS** project (`"type": "commonjs"`), `import.meta.url` is unavailable — anchor with `join(__dirname, 'weather.widget.tsx')` (from `node:path`) instead. Both forms are invariant to `process.cwd()`.
</Warning>

<Info>
**`resourceMode` is host-detected for `.tsx` widgets.** When you leave `resourceMode` unset, FrontMCP picks the right default per connecting client: Claude auto-switches to `'inline'` (React bundled in — #456), so the widget actually renders in Claude's sandboxed iframe. Other hosts keep `'cdn'` (smaller payload, fetched from esm.sh). Set the value explicitly to opt out of detection:

```ts
ui: {
  template: { file: widgetPath },
  // Leave resourceMode unset to host-detect (recommended).
  // Or pin it explicitly:
  // resourceMode: 'inline', // always bundle React inline
}
```

Detection only applies to per-call rendering (inline / hybrid / lean serving modes). `servingMode: 'static'` widgets compile at server startup with no client context — set `resourceMode: 'inline'` explicitly when a static widget needs to render in Claude.
</Info>

<Info>
**`ui.csp` is honored by Claude (#455).** FrontMCP now attaches `ui.csp` to the `resources/read` content item's `_meta.ui.csp` (and `_meta['ui/csp']`), not just to the tool listing. Claude only reads CSP from the resource content — declarations on the tool were previously silently ignored. Use `ui.csp.connectDomains` for fetch/XHR/WebSocket allow-lists and `ui.csp.resourceDomains` for images / scripts / fonts / styles.
</Info>

<Info>
**Widget files use the `*.widget.tsx` naming convention.** `.tsx`/`.jsx` widgets are bundled separately by uipack/esbuild at render time. The `tsconfig.json` scaffolded by `frontmcp init` excludes `**/*.widget.tsx` / `**/*.widget.jsx` from the server typecheck so widgets don't force the project to set `jsx: 'react-jsx'` or pull in `@types/react`. Running `frontmcp init` on an existing project also adds these excludes. For IDE typecheck of widget sources, add a sibling `tsconfig.widget.json` with `jsx: 'react-jsx'` and `include: ['src/**/*.widget.tsx']`.
</Info>

### Using @frontmcp/ui Components

Combine Tool UI with the `@frontmcp/ui` component library:

```ts
import { card, badge, button, descriptionList } from '@frontmcp/ui';

@Tool({
  name: 'get_order',
  inputSchema: { orderId: z.string() },
  ui: {
    template: (ctx) => {
      const { output, helpers } = ctx;
      return card(`
        <div class="flex justify-between">
          <span>${helpers.escapeHtml(output.id)}</span>
          ${badge(output.status, { variant: 'success' })}
        </div>
        ${descriptionList([
          { term: 'Customer', description: output.customer },
          { term: 'Total', description: helpers.formatCurrency(output.total) },
        ])}
      `, { title: 'Order Details' });
    },
  },
})
class GetOrderTool extends ToolContext { /* ... */ }
```

### Testing Tool UI

Use `@frontmcp/testing` for E2E validation of rendered widgets:

```ts
import { test, expect, UIAssertions } from '@frontmcp/testing';

test('renders weather UI correctly', async ({ mcp }) => {
  const result = await mcp.tools.call('get_weather', { location: 'London' });

  expect(result).toHaveRenderedHtml();
  expect(result).toBeXssSafe();
  expect(result).toContainBoundValue('London');

  const html = UIAssertions.assertValidUI(result, ['location', 'temperature']);
});
```

<CardGroup cols={2}>
  <Card title="Building Tool UI" icon="palette" href="/frontmcp/guides/building-tool-ui">
    Complete configuration options and examples
  </Card>
  <Card title="React SDK" icon="react" href="/frontmcp/react/components">
    Pre-built React components from @frontmcp/react
  </Card>
</CardGroup>

---

## Best Practices

**Do:**

- Use descriptive `name` and `description` fields to help models understand tool purpose
- Define clear input schemas with `.describe()` on each field
- Use appropriate annotations (`destructiveHint`, `idempotentHint`, etc.) to guide client behavior
- Validate inputs thoroughly and return meaningful error messages
- Keep tools focused on a single action or operation

**Don't:**

- Create tools for read-only data retrieval (use resources instead)
- Skip input validation—always define a proper `inputSchema`
- Ignore error handling—wrap external calls in try/catch
- Create overly complex tools—split into multiple tools if needed
- Expose sensitive operations without proper authentication checks
