Skip to main content
Tools are the action primitive: functions the model calls to do things. Where a resource is data the client reads and a prompt is a template the client fetches, a tool runs — it takes validated arguments, executes your code, and returns a value the model can act on. FastMCP validates the inputs before your handler sees them and converts the return value into MCP content afterward, so a tool handler is just an ordinary function in the middle.

Registration

You register a tool with server.tool(config, handler). The config object comes first and the handler second, and that order is deliberate: TypeScript resolves generics left to right, so the schema declared in the config determines the argument types in the handler. Declare input as a Standard Schema and the handler’s parameter is typed for you — there is nothing extra to annotate.
server.ts
Both name and description are optional. When you omit name, FastMCP uses the handler function’s own .name. When you omit description, it derives one from the resolved name by splitting camelCase into words (getWeather becomes "get weather"). A named function expression therefore registers a fully described tool with no redundant strings.
server.ts

Schemas

A tool carries two independent schema layers, and keeping them straight is the key to understanding tool validation. One layer validates arguments at runtime; the other advertises the tool’s shape to clients. They usually agree, and FastMCP keeps them in sync for you, but they are separate concerns. The input and output fields are Standard Schema validators. Standard Schema is the shared interface implemented by Zod, Valibot, ArkType, and others, so the same input field accepts any of them without locking you to one library.
The inputSchema and outputSchema fields are explicit JSON Schema objects, the form MCP uses to describe a tool to clients in tools/list. You rarely set them. When omitted, FastMCP generates them from your input/output validators, using Zod v4’s z.toJSONSchema() for Zod schemas. If a validator can’t be translated to JSON Schema, FastMCP falls back to { type: 'object' } and warns on the console — the tool still works, but clients see an untyped shape. Provide an explicit inputSchema only when you deliberately want to advertise something different from what you validate. Validation runs at two moments with two different meanings. Input validation happens before your handler runs: client-supplied arguments are checked against input, and a failure throws ProtocolError(InvalidParams) — a protocol-level error that says the client sent bad arguments. The handler is never invoked. Output validation happens after your handler returns: the raw return value is checked against output before conversion. The output schema describes your handler’s contract — primitives, objects, and arrays are all valid return types — not the MCP content shape. A failure here returns isError: true, a tool execution error rather than a protocol error, because the client’s input was valid and the fault is server-side.

Return values

Your handler returns a plain value and FastMCP converts it to MCP content. This is the magic conversion: you write return a + b, not return { content: [{ type: 'text', text: String(a + b) }] }. The conversion is driven by the runtime type of what you return. Raw bytes are the one case the conversion can’t guess: a Buffer or Uint8Array carries no MIME type, so you wrap it in Image or File to declare what it is. Returning an image is then a single line.
server.ts
When you need full control over the result — multiple content blocks, suppressing structuredContent, or hand-building raw MCP output — return a ToolResult. It is the escape hatch the table’s last row points to, and it bypasses conversion entirely so what you construct is what the client receives.
server.ts

Errors

A tool can fail in two ways, and FastMCP routes them to two different channels so the model and the protocol each learn what they need to. When your handler throws an ordinary error — anything that is not a ProtocolError — FastMCP catches it and returns { isError: true } with the message as content. The protocol call itself succeeds; the model receives the error as a tool result and can react to it, retry, or report it. This is the right channel for “the operation failed”: a missing record, a downstream timeout, a refused request. When the failure is protocol-level, throw a ProtocolError. Input validation throws ProtocolError(InvalidParams); middleware such as rate limiting throws its own ProtocolError; you can throw one directly when the client has violated the contract. A ProtocolError propagates as a JSON-RPC error rather than a tool result, signalling that the request was malformed rather than that the work failed.

Configuration

Beyond name, description, and the schema fields, the config object accepts these fields. Each is optional and each changes how the tool is advertised or executed.

Dynamic registration

You can register a tool before or after server.run(). Adding one to a running server is a live operation: FastMCP sends notifications/tools/list_changed to every connected client so they re-fetch the list and discover the new tool. The same holds for resources and prompts — the registration system is uniform across all three primitives.
server.ts