> ## Documentation Index
> Fetch the complete documentation index at: https://docs.agentfront.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Tools

> AI-callable functions with typed input/output schemas, validation, and execution context

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 theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
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 theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
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 theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
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 theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
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.

`tools/list` describes the input the caller sends, not the value after parsing: a field with `.default()` is not listed as required, and a field with `.transform()` is listed by its input type. The same JSON Schema is used for agents and skills.

### Basic Types

```ts theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
@Tool({
  name: 'user-action',
  inputSchema: {
    userId: z.string(),
    count: z.number(),
    enabled: z.boolean(),
  },
})
```

### With Descriptions

```ts theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
@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 theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
@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 theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
@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 theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
@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 };
  }
}
```

A result that does not match `outputSchema` fails the call: the client gets an error result with `_meta.code: "INVALID_OUTPUT"`, and nothing from the rejected result is sent. When the result matches, fields the schema does not declare are dropped, with one exception: a `_meta` object on the returned value is copied into the response's `_meta` without validation, so keep secrets out of it. A number field holding `Infinity`, `-Infinity`, or `NaN` also fails with `INVALID_OUTPUT`, unless the server sets `output: { allowNonFinite: true }`. JSON cannot encode those values, so with that setting the client receives `null` in their place.

### 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 theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
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 theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
@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 theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
@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

Without an `outputSchema`, the result is sent as JSON: in `structuredContent`, and as JSON text in `content`. A plain value (string, number, boolean or array) is wrapped as `{ value: … }`:

```ts theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
// Object -> structuredContent { id, status }, text '{"id":"123","status":"created"}'
execute() {
  return { id: '123', status: 'created' };
}

// String -> structuredContent { value: 'Operation completed' }, text '{"value":"Operation completed"}'
execute() {
  return 'Operation completed';
}

// Number -> structuredContent { value: 42 }, text '{"value":42}'
execute() {
  return 42;
}
```

To send plain text or a bare number, declare the primitive output type. The text content is then the value itself, and `structuredContent` is `{ content: value }`:

```ts theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
@Tool({ name: 'greet', inputSchema: { name: z.string() }, outputSchema: 'string' })
class GreetTool extends ToolContext {
  async execute({ name }: { name: string }) {
    return `Hello, ${name}!`; // text content: Hello, Alice!
  }
}
```

<Warning>
  `execute()` must return a value. A tool that returns `undefined` (for example, an `async execute()` with no `return`) fails with `_meta.code: "FLOW_EXITED_WITHOUT_OUTPUT"` ("Flow exited without producing output"). Return an explicit result, such as `{ ok: true }`, from tools that only have side effects.
</Warning>

### Full MCP Format

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

```ts theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
execute() {
  return {
    content: [
      {
        type: 'text',
        text: 'Operation completed',
      },
    ],
    isError: false,
  };
}
```

### Multiple Content Items

Return an array to include multiple content blocks:

```ts theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
execute() {
  return {
    content: [
      { type: 'text', text: 'Summary of results' },
      { type: 'text', text: JSON.stringify(details) },
    ],
  };
}
```

***

## Tool Metadata

```ts theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
@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 listed by `tools/list`; omitted when not set, so `annotations.title` applies |
| `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 theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
  @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 theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
  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, `codecall:describe` generates one from the tool's intent (create, list, get, update, delete, search). Its arguments come only from the properties your input schema declares: a query-like property (or else the first filter property such as `status`) for a search, the pagination properties the tool really has for a list, the required properties, and the first value of an enum. Values stay within the property's `enum`, `const`, bounds and length limits; a `pattern` or `format` is not generated, so an optional property that has one is left out. With nothing to go on the example passes `{}`; it never invents an argument like `query`. When you provide examples, `codecall:describe` returns only yours (up to 5), with no generated example next to them.
</Tip>

***

## Tool Annotations

Annotations provide hints to clients about tool behavior:

```ts theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
@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 theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
@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 theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
@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 theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
@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 theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
@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 are the eight MCP levels: `'debug'`, `'info'`, `'notice'`, `'warning'`, `'error'`, `'critical'`, `'alert'`, `'emergency'`. A message is sent only at or above the level the client asked for: with `logging/setLevel` on a session, or with `_meta["io.modelcontextprotocol/logLevel"]` on a 2026-07-28 request. Until the client asks, `notify()` returns `false`.

### Sending Progress

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

```ts theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
@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 theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
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 theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
@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 theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
@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 theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
@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 theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
@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 theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
{
  "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 theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
@Tool({
  name: 'get_weather',
  description: 'Get current weather for a location',
  inputSchema: {
    location: z.string(),
  },
  ui: {
    // `html` escapes every interpolated value; see "Trusted markup" in the Tool UI guide.
    template: (ctx) => ctx.helpers.html`
      <div class="weather-card">
        <h2>${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 FileSource (recommended) theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
  import { fileURLToPath } from 'node:url';

  const widgetPath = fileURLToPath(new URL('./weather.widget.tsx', import.meta.url));

  ui: {
    template: { file: widgetPath }, // .tsx / .jsx / .html source file
  }
  ```

  ```ts HTML Template theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
  import { type TemplateContext } from '@frontmcp/sdk';

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

  ```ts Markdown String theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
  ui: {
    // No `<` and `>` together => treated as Markdown and converted to HTML.
    template: '# Weather Report\n\n- Data updated every 15 minutes',
  }
  ```
</CodeGroup>

A string that contains both `<` and `>` is used as HTML. **MDX is not compiled**: JSX tags in a string reach the client as HTML, `{output.field}` expressions are not evaluated, and `mdxComponents` is ignored. Use a `.tsx` FileSource widget for anything interactive.

Markdown strings are converted on the server (headings, paragraphs, fenced code, lists, bold, italic, inline code, links). Raw HTML in the Markdown is escaped, and a link survives only when its target starts with `http:`, `https:`, `mailto:`, `/` or `#`.

<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` | `{ file }` FileSource, HTML function, React component, or HTML / Markdown string |
| `csp` | Content Security Policy (allowed domains) |
| `servingMode` | How HTML is delivered: `'auto'` (default), `'inline'`, `'static'`, `'hybrid'` |
| `resourceMode` | `'cdn'` or `'inline'` bundling for `.tsx` widgets (host-detected when unset) |
| `escapeStringResults` | Escape plain string results; `html` / `trustedHtml` stay markup (default in 1.9) |

**Options with no effect yet.** These pass validation but nothing reads them, so setting one changes nothing; startup logs a warning per tool that lists them: `widgetDescription`, `widgetAccessible`, `displayMode`, `prefersBorder`, `sandboxDomain`, `contentSecurity`, `hydrate`, `runtimeOptions`, `mdxComponents`, `bundlingMode`, `uiType` and `htmlResponsePrefix`. There is no dual HTML payload for Claude, so `htmlResponsePrefix` does nothing.

**Serving modes.** `'direct-url'` and `'custom-url'` are not implemented: the widget is served inline, as with `'inline'`, and startup logs a warning. `'hybrid'` pre-compiles the shell at startup and sends only a reference in each call's `_meta['ui/component']` (`{ type, hash, toolName }`) — no component code and no data.

**Sanitization.** Markup you author (template literals, HTML strings, `.tsx` files) is trusted and not sanitized. Values interpolated into `` html`…` `` are escaped; plain string results are escaped only with `escapeStringResults: true`; Markdown strings are always escaped.

<Warning>
  **Build template markup with `ctx.helpers.html`.** A plain string a template function returns is rendered as markup when it looks like HTML, so returning unescaped tool output injects tags. The `html` tagged template escapes interpolated values; `ctx.helpers.trustedHtml(markup)` marks markup you already made safe. Set `ui.escapeStringResults: true` (or `@FrontMcp({ ui: { escapeStringResults: true } })`) to escape plain string results now — FrontMCP 1.9 does so by default. See [Trusted markup](/frontmcp/guides/building-tool-ui#trusted-markup).
</Warning>

<Info>
  **Widget resources carry no caller data.** `resources/read ui://widget/{toolName}.html` returns the widget compiled at startup (`servingMode: 'static'`, or the `hybrid` shell) or a data-free placeholder that receives the result through the bridge. An `inline` render, which embeds the call's input and output, is returned only in that call's `_meta['ui/html']`. The advertised URI percent-encodes the tool name (`app:tool` becomes `ui://widget/app%3Atool.html`); both forms read back. See [building-tool-ui](/frontmcp/guides/building-tool-ui#ui-configuration-options).
</Info>

<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.

  Bundling also needs **`esbuild`**. `@frontmcp/uipack` loads it on demand to bundle the widget when the tool is called, so install it as a runtime dependency where the server runs (`npm install esbuild` — in `dependencies`, not `devDependencies`). Projects created with `frontmcp create` already get it through the `frontmcp` package. Without it, the call fails with an error naming the widget.

  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 theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
  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 independent of `process.cwd()`, but not of the build: once the tool is compiled they resolve next to the **compiled** file, so the widget has to ship there.

  `frontmcp build` copies every `*.widget.tsx` / `*.widget.jsx` under the entry's directory into the output. Targets that run tsc's output as emitted get them at the same relative path, next to each compiled tool. Bundled targets (`node`, `cli`, `lambda`, `vercel`) get them directly next to the bundle, because every bundled module's `__dirname` is the bundle's directory — keep each widget beside the tool that uses it and give it a unique file name. Only widget files are copied, not other local files a widget imports. A plain `tsc` build copies nothing, so add a copy step; otherwise the call fails with an `ENOENT` error that names the path it looked for.
</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 theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
  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>

<Info>
  **The widget page follows the host theme.** When an MCP Apps host sends `theme: 'light'` or `'dark'` — in the `ui/initialize` result or a later `ui/notifications/host-context-changed` — the `window.FrontMcpBridge` runtime sets `<meta name="color-scheme">` and `<html data-theme>` to it, so the frame's background and form controls match the host and your CSS can use `[data-theme="dark"]`. `getTheme()` returns the same value and `onContextChange` listeners also fire for the handshake context. Nothing is written when the host sends no theme. A widget styled for light only can keep it with `:root { color-scheme: light }`, which takes precedence over the meta tag.
</Info>

### Using @frontmcp/ui Components

`@frontmcp/ui` is a React library: components come from `@frontmcp/ui/components` and bridge hooks from `@frontmcp/ui/react`. It has no `card()`, `badge()`, `descriptionList()`, `button()`, `form()` or `input()` functions.

```tsx get-order.widget.tsx theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
import { Badge, Card } from '@frontmcp/ui/components';
import { useCallTool } from '@frontmcp/ui/react';

export default function OrderWidget({ output }: { output: { id: string; status: string; customer: string } | null }) {
  // useCallTool returns a tuple: [call, { data, loading, error, called }, reset]
  const [refresh, { loading }] = useCallTool('get_order');

  if (!output) return <Card title="Order Details" subtitle="Loading..." />;

  return (
    <Card title="Order Details" elevation={2}>
      <span>{output.id}</span> <Badge variant="success">{output.status}</Badge>
      <p>{output.customer}</p>
      <button disabled={loading} onClick={() => refresh({ orderId: output.id })}>
        Refresh
      </button>
    </Card>
  );
}
```

```ts get-order.tool.ts theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
import { fileURLToPath } from 'node:url';

@Tool({
  name: 'get_order',
  inputSchema: { orderId: z.string() },
  ui: { template: { file: fileURLToPath(new URL('./get-order.widget.tsx', import.meta.url)) } },
})
class GetOrderTool extends ToolContext { /* ... */ }
```

### Testing Tool UI

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

```ts theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
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


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.