# @Tool

> Declare a function an AI model can call, with validated input and optional structured output.

Source: https://frontmcp.dev/reference/sdk/tool

`@Tool` declares an MCP [tool](https://frontmcp.dev/learn/your-first-tool): a function an AI model can decide to call. FrontMCP checks every call against the tool's input schema before your code runs.

```ts
@Tool(options)
class MyTool extends ToolContext {
  async execute(input) { /* ... */ }
}
```

---

## Reference

### `@Tool(options)`

Apply `@Tool` to a class that extends `ToolContext`, and list the class in an app's `tools` array.

```ts get-weather.tool.ts
import { Tool, ToolContext, z } from "@frontmcp/sdk";

@Tool({
  name: "get_weather",
  description: "Get current weather for a city",
  inputSchema: {
    city: z.string().describe("City name"),
    units: z.enum(["celsius", "fahrenheit"]).default("celsius"),
  },
})
class GetWeatherTool extends ToolContext {
  async execute(input: { city: string; units: "celsius" | "fahrenheit" }) {
    const weather = await fetchWeather(input.city, input.units);
    return { temperature: weather.temp, conditions: weather.conditions };
  }
}
```

[See more examples below.](#usage)

#### Options

Required:

| Option | Type | Description |
| --- | --- | --- |
| `name` | `string` | What the model calls. Unique within the server. Prefer `verb_noun` in snake_case. |
| `inputSchema` | object of Zod types | One Zod type per argument, like `{ city: z.string() }`. Becomes the JSON Schema clients see in `tools/list`. |

Optional:

| Option | Type | Description |
| --- | --- | --- |
| `description` | `string` | What the tool does, what it returns, and when to use it. Always set it; it's the model's only documentation. |
| `title` | `string` | A display name for people, like `"Search tickets"`. Sent in `tools/list` only when set. |
| `outputSchema` | Zod schema, or one of `"string"`, `"number"`, `"boolean"`, `"date"`, `"image"`, `"audio"`, `"resource"`, `"resource_link"` | The shape of the result. A Zod schema is advertised in `tools/list`, and every result is checked against it: fields it doesn't declare are dropped, and a result that doesn't match fails with `INVALID_OUTPUT` (see [Shaping Tool Results](https://frontmcp.dev/learn/shaping-tool-results#declaring-the-shape-with-outputschema)). |
| `annotations` | `ToolAnnotations` | Hints for clients: `title`, `readOnlyHint`, `destructiveHint`, `idempotentHint`, `openWorldHint`. See [describing side effects](#describing-side-effects-with-annotations). |
| `examples` | `ToolExample[]` | Sample calls: `description`, `input` and optionally `output`. |
| `tags` | `string[]` | Labels for grouping and filtering. |
| `visibility` | `"public" \| "hidden" \| "internal"` | `hidden` leaves the tool out of `tools/list` but it can still be called by name. `internal` keeps it for in-process callers only. |
| `rateLimit` | `RateLimitConfig` | Per-tool rate limit: `maxRequests`, `windowMs`, `partitionBy`. See [Guard options](https://frontmcp.dev/reference/sdk/guard). |
| `concurrency` | `ConcurrencyConfig` | Cap on simultaneous executions: `maxConcurrent`, optional queue. See [Guard options](https://frontmcp.dev/reference/sdk/guard). |
| `timeout` | `TimeoutConfig` | Execution timeout: `executeMs`. See [Guard options](https://frontmcp.dev/reference/sdk/guard). |
| `authProviders` | `ToolAuthProviderRef[]` | Credentials the tool needs, such as `{ name: "github", scopes: ["repo"] }`. Required by default. See [Progressive auth](https://frontmcp.dev/reference/auth/progressive#credentials-authproviders-and-thiscredentials). |
| `availableWhen` | `EntryAvailability` | Only offer the tool on certain platforms, runtimes or deployments, or to some callers: `surface` lists who may call it, like `["mcp"]` for clients only, or `["agent"]` for [agents](https://frontmcp.dev/reference/sdk/agent#giving-an-agent-tools) only. Since 1.8.4, `surface` holds for agents and [jobs](https://frontmcp.dev/reference/sdk/call-tool#where-it-works) too. See [Environment awareness](https://frontmcp.dev/reference/server/environment). |
| `execution` | `{ taskSupport }` | Whether the tool can run as a background task: `"required"`, `"optional"` or `"forbidden"`. See [Background tasks](https://frontmcp.dev/reference/server/tasks). |
| `ui` | `ToolUIConfig` | A widget to render the result in clients that support it. With `ui`, `outputSchema` still drops the fields it doesn't declare ([Fields the schema doesn't declare](https://frontmcp.dev/reference/ui#fields-the-schema-doesnt-declare)). |

Plugins add options of their own, which only take effect where the plugin is installed: `cache` ([Cache](https://frontmcp.dev/reference/plugins/cache#cache-in-tool)), `approval` ([Approval](https://frontmcp.dev/reference/plugins/approval#the-approval-tool-option)), `featureFlag` ([Feature flags](https://frontmcp.dev/reference/plugins/feature-flags#the-featureflag-option)) and `codecall` ([CodeCall](https://frontmcp.dev/reference/plugins/codecall#the-codecall-field-on-tool)). A server whose tool sets `approval` or `featureFlag`, with no plugin that enforces it, doesn't start: see [`Unenforced metadata`](https://frontmcp.dev/reference/sdk/plugin#unenforced-metadata-tool--declares-).

#### `execute(input)`

FrontMCP calls `execute()` once the arguments pass validation. Inside it, `this` is a `ToolContext`: [Context classes](https://frontmcp.dev/reference/sdk/contexts) lists everything on it.

- `input`: the validated arguments. Its TypeScript type must match `inputSchema`.
- **Returns** anything. Objects arrive as the result's `structuredContent`. Plain values, like strings and numbers, arrive as `{ "value": ... }`, unless `outputSchema` is `"string"`, `"number"` or `"boolean"`: then the text block is the value itself. The same data is also sent as text in `content`, for clients that don't read structured output.

Inside `execute()`, `this` is the `ToolContext`: `this.get()` for providers, `this.fetch()` for outbound HTTP, `this.notify()` and `this.progress()` for status, `this.elicit()` to ask the user for input, and `this.fail()` to return an error.

#### Caveats

- The class **must extend `ToolContext`**.
- The type of `execute()`'s parameter **must match `inputSchema`** exactly, and with an `outputSchema`, its return type must match that. `@Tool` checks all three when TypeScript compiles, and reports a mismatch on the decorator line (see [Troubleshooting](#typescript-says-unable-to-resolve-signature-of-class-decorator)).
- Arguments that aren't in `inputSchema` are **dropped** before `execute()` runs.
- If `execute()` throws a plain `Error`, clients see the message and stack in development, and a generic "Internal FrontMCP error" with an error ID in production. Use [`PublicMcpError`](#returning-an-error-the-model-can-read) for messages the model should read.
- The class can declare [hooks](https://frontmcp.dev/reference/sdk/hooks#where-hooks-can-be-declared) that run for this tool's calls only: instance methods from `Did("createToolCallContext")` on, and, since 1.9.4, `static` methods on any stage, like `parseInput`. An instance method on an earlier stage stops the server from starting.

---

## Usage

### Validating input

Put every constraint in the schema. The model sees it in `tools/list`, and FrontMCP rejects calls that break it before `execute()` runs, with an `isError` result that lists each problem.

```ts create-ticket.tool.ts
import { Tool, ToolContext, z } from "@frontmcp/sdk";

@Tool({
  name: "create_ticket",
  description: "Open a new support ticket",
  inputSchema: {
    title: z.string().min(5).max(120).describe("One-line summary of the problem"),
    priority: z.enum(["low", "normal", "high"]).default("normal"),
    email: z.string().email().optional().describe("Where to send updates"),
  },
})
export class CreateTicket extends ToolContext {
  async execute(input: { title: string; priority: "low" | "normal" | "high"; email?: string }) {
    return { id: "T-4", ...input };
  }
}
```

### Returning results

Return a plain value, an object, or an object that matches an `outputSchema`. Clients get objects as `structuredContent`, and plain values wrapped as `{ "value": ... }`. Both also arrive as text. To send a plain value as it is, declare it with a primitive `outputSchema`.

<Examples title="Return values">

#### Example: Plain value
A number, string or boolean is wrapped as `{ "value": ... }`.

```ts count-open.tool.ts
import { Tool, ToolContext } from "@frontmcp/sdk";

@Tool({ name: "count_open", description: "How many tickets are open", inputSchema: {} })
export class CountOpen extends ToolContext {
  async execute() {
    return 12;
  }
}
```

#### Example: Object
An object arrives as `structuredContent` as it is.

```ts get-ticket.tool.ts
import { Tool, ToolContext, z } from "@frontmcp/sdk";

@Tool({ name: "get_ticket", description: "Get one ticket by id", inputSchema: { id: z.string() } })
export class GetTicket extends ToolContext {
  async execute({ id }: { id: string }) {
    return { id, title: "Cannot log in", status: "open" };
  }
}
```

#### Example: Plain text
With `outputSchema: "string"`, the text block is the string itself, and `structuredContent` is `{ "content": ... }`.

```ts ticket-status.tool.ts
import { Tool, ToolContext, z } from "@frontmcp/sdk";

@Tool({
  name: "ticket_status",
  description: "Get the status of a ticket, as a sentence",
  inputSchema: { id: z.string() },
  outputSchema: "string",
})
export class TicketStatus extends ToolContext {
  async execute({ id }: { id: string }) {
    return `Ticket ${id} is open.`;
  }
}
```

```ts ticket-status.test.ts
import { test, expect } from "@frontmcp/testing";

test("the text is the sentence itself", async ({ mcp }) => {
  const result = await mcp.tools.call("ticket_status", { id: "T-1" });
  expect(result.text()).toBe("Ticket T-1 is open.");
  expect(result.raw.structuredContent).toEqual({ content: "Ticket T-1 is open." });
});
```

#### Example: Declared output
With `outputSchema`, clients see the result's shape in `tools/list` before they call, and a result that doesn't match fails with `INVALID_OUTPUT`.

```ts ticket-stats.tool.ts
import { Tool, ToolContext, z } from "@frontmcp/sdk";

@Tool({
  name: "ticket_stats",
  description: "Count open and closed tickets",
  inputSchema: {},
  outputSchema: z.object({ open: z.number(), closed: z.number() }),
  annotations: { readOnlyHint: true },
})
export class TicketStats extends ToolContext {
  async execute() {
    return { open: 12, closed: 40 };
  }
}
```

### Returning an error the model can read

Call `this.fail()` with a `PublicMcpError`. The call ends, and the client gets an `isError` result with your message, in development and in production.

```ts get-ticket.tool.ts
import { PublicMcpError, Tool, ToolContext, z } from "@frontmcp/sdk";

@Tool({
  name: "get_ticket",
  description: "Get one support ticket by id",
  inputSchema: { id: z.string() },
})
export class GetTicket extends ToolContext {
  async execute({ id }: { id: string }) {
    if (id !== "T-1") this.fail(new PublicMcpError(`There's no ticket ${id}.`));
    return { id, title: "Cannot log in" };
  }
}
```

### Describing side effects with annotations

Annotations tell clients how a tool behaves, so they can decide how carefully to confirm a call with the user. They're hints: clients may rely on them, but FrontMCP doesn't enforce them.

| Annotation | Meaning when `true` |
| --- | --- |
| `readOnlyHint` | The tool doesn't change anything. |
| `destructiveHint` | The tool may delete or overwrite things. Only meaningful when it isn't read-only. |
| `idempotentHint` | Calling it again with the same arguments has no further effect. |
| `openWorldHint` | The tool reaches outside systems, like the web, rather than a closed set of data. |

```ts delete-ticket.tool.ts
import { Tool, ToolContext, z } from "@frontmcp/sdk";

@Tool({
  name: "delete_ticket",
  description: "Permanently delete a ticket",
  inputSchema: { id: z.string() },
  annotations: { title: "Delete ticket", destructiveHint: true, idempotentHint: true },
})
export class DeleteTicket extends ToolContext {
  async execute({ id }: { id: string }) {
    return { deleted: id };
  }
}
```

### Using a provider

Get shared services with `this.get()`. Register the provider in the same app's `providers` array.

```ts whoami.tool.ts active
import { Tool, ToolContext } from "@frontmcp/sdk";
import { Directory } from "./directory";

@Tool({ name: "whoami", description: "Which support team is on duty", inputSchema: {} })
export class WhoAmI extends ToolContext {
  async execute() {
    return this.get(Directory).onDuty();
  }
}
```

```ts directory.ts
import { Provider, ProviderScope } from "@frontmcp/sdk";

@Provider({ name: "Directory", scope: ProviderScope.GLOBAL })
export class Directory {
  onDuty() {
    return { team: "EMEA support", lead: "Nour" };
  }
}
```

### Writing a tool as a function

For a small tool that doesn't need context, the function form is shorter. It takes the same options.

```ts shout.tool.ts
import { tool, z } from "@frontmcp/sdk";

export const shout = tool({
  name: "shout",
  description: "Uppercase some text",
  inputSchema: { text: z.string() },
})(async ({ text }) => text.toUpperCase());
```

---

## Troubleshooting

### My tool doesn't show up in `tools/list`

Check, in order:

1. The class is in its app's `tools` array, and the app is in `@FrontMcp({ apps })`.
2. `tsconfig.json` sets `experimentalDecorators` and `emitDecoratorMetadata` to `true`.
3. `import "reflect-metadata"` is the first line of your entry file.
4. The tool doesn't set `visibility: "hidden"` or `"internal"`, and its `availableWhen` matches the environment.

### TypeScript says `Unable to resolve signature of class decorator`

`@Tool` checks the class against its options, and error `TS1238` on the decorator line means a check failed. The rest of the message says which one:

- `'execute() parameter error': "Parameter type does not match the input schema."`: `execute()`'s parameter type disagrees with `inputSchema`. `"Parameter type is too wide"` means it allows more than the schema, like `limit?: number` for a field with `.default()`.
- `'execute() return type error'`: with an `outputSchema`, `execute()` returns something that doesn't match it, or `any`.
- `'Tool class error': "Class must extend ToolContext"`: the class doesn't extend `ToolContext`.

The message also shows `expected_input_type` or `expected_output_type`, the type to use:

```ts
// 🚩 inputSchema says string, execute says number
@Tool({ name: "get_ticket", inputSchema: { id: z.string() } })
class GetTicket extends ToolContext {
  async execute(input: { id: number }) { /* ... */ }
}

// ✅ The types match
@Tool({ name: "get_ticket", inputSchema: { id: z.string() } })
class GetTicket extends ToolContext {
  async execute(input: { id: string }) { /* ... */ }
}
```

### The client sees "Internal FrontMCP error" instead of my message

In production, FrontMCP hides the message of any error that isn't a `PublicMcpError`, like a plain `Error` thrown from `execute()`, so internals don't leak. The client gets an error ID, and FrontMCP logs the same ID with the original message and stack. If the model should see the message, fail with `this.fail(new PublicMcpError("..."))` instead.

### Calls fail with `INVALID_OUTPUT`

The result didn't match `outputSchema`, so nothing from it was sent. In development the text names the first field that didn't match; in production it's only "Output validation failed. Please contact support." A `NaN` or `Infinity` in a number field fails the same way, with or without an `outputSchema`, because JSON can't carry it.

```ts ticket-stats.tool.ts
import { Tool, ToolContext, z } from "@frontmcp/sdk";

@Tool({
  name: "ticket_stats",
  description: "Count open and closed tickets",
  inputSchema: {},
  outputSchema: z.object({ open: z.number(), closed: z.number() }),
})
export class TicketStats extends ToolContext {
  async execute() {
    const counts: Record<string, number> = { open: 12, pending: 3 }; // no "closed" today
    return { open: counts.open, closed: counts.closed };
  }
}
```

```ts ticket-stats.test.ts
import { test, expect } from "@frontmcp/testing";

test("a missing field fails the call", async ({ mcp }) => {
  const result = await mcp.tools.call("ticket_stats", {});
  expect(result.raw._meta?.code).toBe("INVALID_OUTPUT");
  expect(result.text()).toBe("Tool output validation failed (output does not match outputSchema at closed)");
});
```

Fix the result, or change `outputSchema` if the result is right. Here, `counts.closed ?? 0` would do.

### Calls fail with "Invalid tool input"

The arguments didn't match `inputSchema`, so `execute()` never ran. The result text lists each problem with the field's path. If a model keeps sending bad arguments, the schema probably isn't telling it enough: add `.describe()` notes, and prefer `z.enum()` over free text where you can.

### Calls fail with error `-32001` and an `authUrl`

The tool lists a required entry in `authProviders`, and the user hasn't connected that credential yet. `execute()` doesn't run. Send the user to `authUrl` to connect it, or mark the provider `required: false` if the tool can work without it.
