Registration
You register a tool withserver.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
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. Theinput 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.
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 writereturn 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
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 aProtocolError — 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
Beyondname, 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 afterserver.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