UNPKG

fastmcp

Version:

A TypeScript framework for building MCP servers.

1,699 lines (1,362 loc) 79.5 kB
# FastMCP A TypeScript framework for building [MCP](https://glama.ai/mcp) servers capable of handling client sessions. > [!NOTE] > > For a Python implementation, see [FastMCP](https://github.com/jlowin/fastmcp). ## Features - Simple Tool, Resource, Prompt definition - [Authentication](#authentication) - [Passing headers through context](#passing-headers-through-context) - [Session ID and Request ID tracking](#session-id-and-request-id-tracking) - [Sessions](#sessions) - [Image content](#returning-an-image) - [Audio content](#returning-an-audio) - [Embedded](#embedded-resources) - [Logging](#logging) - [Error handling](#errors) - [HTTP Streaming](#http-streaming) (with SSE compatibility) - [HTTPS Support](#https-support) for secure connections - [Custom HTTP routes](#custom-http-routes) for REST APIs, webhooks, and admin interfaces - [Edge Runtime Support](#edge-runtime-support) for Cloudflare Workers, Deno Deploy, and more - [Stateless mode](#stateless-mode) for serverless deployments - CORS (enabled by default) - [Progress notifications](#progress) - [Streaming output](#streaming-output) - [Typed server events](#typed-server-events) - [Prompt argument auto-completion](#prompt-argument-auto-completion) - [Sampling](#requestsampling) - [Elicitation](#elicitation) - [Configurable ping behavior](#configurable-ping-behavior) - [Health-check endpoint](#health-check-endpoint) - [Roots](#roots-management) - [In-memory transport](#unit-testing-with-an-in-memory-transport) for unit testing without binding a port - CLI for [testing](#test-with-mcp-cli) and [debugging](#inspect-with-mcp-inspector) ## When to use FastMCP over the official SDK? FastMCP is built on top of the official SDK. The official SDK provides foundational blocks for building MCPs, but leaves many implementation details to you: - [Initiating and configuring](https://github.com/punkpeye/fastmcp/blob/06c2af7a3d7e3d8c638deac1964ce269ce8e518b/src/FastMCP.ts#L664-L744) all the server components - [Handling of connections](https://github.com/punkpeye/fastmcp/blob/06c2af7a3d7e3d8c638deac1964ce269ce8e518b/src/FastMCP.ts#L760-L850) - [Handling of tools](https://github.com/punkpeye/fastmcp/blob/06c2af7a3d7e3d8c638deac1964ce269ce8e518b/src/FastMCP.ts#L1303-L1498) - [Handling of responses](https://github.com/punkpeye/fastmcp/blob/06c2af7a3d7e3d8c638deac1964ce269ce8e518b/src/FastMCP.ts#L989-L1060) - [Handling of resources](https://github.com/punkpeye/fastmcp/blob/06c2af7a3d7e3d8c638deac1964ce269ce8e518b/src/FastMCP.ts#L1151-L1242) - Adding [prompts](https://github.com/punkpeye/fastmcp/blob/06c2af7a3d7e3d8c638deac1964ce269ce8e518b/src/FastMCP.ts#L760-L850), [resources](https://github.com/punkpeye/fastmcp/blob/06c2af7a3d7e3d8c638deac1964ce269ce8e518b/src/FastMCP.ts#L960-L962), [resource templates](https://github.com/punkpeye/fastmcp/blob/06c2af7a3d7e3d8c638deac1964ce269ce8e518b/src/FastMCP.ts#L964-L987) - Embedding [resources](https://github.com/punkpeye/fastmcp/blob/06c2af7a3d7e3d8c638deac1964ce269ce8e518b/src/FastMCP.ts#L1569-L1643), [image](https://github.com/punkpeye/fastmcp/blob/06c2af7a3d7e3d8c638deac1964ce269ce8e518b/src/FastMCP.ts#L51-L111) and [audio](https://github.com/punkpeye/fastmcp/blob/06c2af7a3d7e3d8c638deac1964ce269ce8e518b/src/FastMCP.ts#L113-L173) content blocks FastMCP eliminates this complexity by providing an opinionated framework that: - Handles all the boilerplate automatically - Provides simple, intuitive APIs for common tasks - Includes built-in best practices and error handling - Lets you focus on your MCP's core functionality **When to choose FastMCP:** You want to build MCP servers quickly without dealing with low-level implementation details. **When to use the official SDK:** You need maximum control or have specific architectural requirements. In this case, we encourage referencing FastMCP's implementation to avoid common pitfalls. ## Installation ```bash npm install fastmcp ``` ## Quickstart > [!NOTE] > > There are many real-world examples of using FastMCP in the wild. See the [Showcase](#showcase) for examples. ```ts import { FastMCP } from "fastmcp"; import { z } from "zod"; // Or any validation library that supports Standard Schema const server = new FastMCP({ name: "My Server", version: "1.0.0", }); server.addTool({ name: "add", description: "Add two numbers", parameters: z.object({ a: z.number(), b: z.number(), }), execute: async (args) => { return String(args.a + args.b); }, }); server.start({ transportType: "stdio", }); ``` _That's it!_ You have a working MCP server. You can test the server in terminal with: ```bash git clone https://github.com/punkpeye/fastmcp.git cd fastmcp pnpm install pnpm build # Test the addition server example using CLI: npx fastmcp dev src/examples/addition.ts # Test the addition server example using MCP Inspector: npx fastmcp inspect src/examples/addition.ts ``` If you are looking for a boilerplate repository to build your own MCP server, check out [fastmcp-boilerplate](https://github.com/punkpeye/fastmcp-boilerplate). ### Remote Server Options FastMCP supports multiple transport options for remote communication, allowing an MCP hosted on a remote machine to be accessed over the network. #### HTTP Streaming [HTTP streaming](https://www.cloudflare.com/learning/video/what-is-http-live-streaming/) provides a more efficient alternative to SSE in environments that support it, with potentially better performance for larger payloads. You can run the server with HTTP streaming support: ```ts server.start({ transportType: "httpStream", httpStream: { port: 8080, }, }); ``` This will start the server and listen for HTTP streaming connections on `http://localhost:8080/mcp`. > **Note:** You can also customize the endpoint path using the `httpStream.endpoint` option (default is `/mcp`). > **Note:** To serve HTTP streaming and built-in OAuth routes under an issuer path, set `httpStream.basePath` (for example, `/issuer1`). This exposes authorization server metadata at `/.well-known/oauth-authorization-server/issuer1` per RFC 8414. > **Note:** This also starts an SSE server on `http://localhost:8080/sse`. You can connect to these servers using the appropriate client transport. For HTTP streaming connections: ```ts import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; const client = new Client( { name: "example-client", version: "1.0.0", }, { capabilities: {}, }, ); const transport = new StreamableHTTPClientTransport( new URL(`http://localhost:8080/mcp`), ); await client.connect(transport); ``` For SSE connections: ```ts import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js"; const client = new Client( { name: "example-client", version: "1.0.0", }, { capabilities: {}, }, ); const transport = new SSEClientTransport(new URL(`http://localhost:8080/sse`)); await client.connect(transport); ``` ##### HTTPS Support FastMCP supports HTTPS for secure connections by providing SSL certificate options: ```ts server.start({ transportType: "httpStream", httpStream: { port: 8443, sslCert: "./path/to/cert.pem", sslKey: "./path/to/key.pem", sslCa: "./path/to/ca.pem", // Optional: for client certificate authentication }, }); ``` This will start the server with HTTPS on `https://localhost:8443/mcp`. **SSL Options:** - `sslCert` - Path to SSL certificate file - `sslKey` - Path to SSL private key file - `sslCa` - (Optional) Path to CA certificate for mutual TLS authentication **For testing**, you can generate self-signed certificates: ```bash openssl req -x509 -newkey rsa:2048 -keyout key.pem -out cert.pem -days 365 -nodes -subj "/CN=localhost" ``` **For production**, obtain certificates from a trusted CA like Let's Encrypt. See the [https-server example](src/examples/https-server.ts) for a complete demonstration. ##### CORS Configuration By default, FastMCP enables CORS with a standard set of allowed headers. You can customize the CORS behavior by passing a `cors` option: ```ts server.start({ transportType: "httpStream", httpStream: { port: 8080, cors: { origin: "http://localhost:3000", allowedHeaders: [ "Content-Type", "Authorization", "Accept", "Mcp-Session-Id", "Mcp-Protocol-Version", "Last-Event-Id", "X-Custom-Header", ], credentials: true, }, }, }); ``` The `cors` option accepts: - `true` (default) - enable CORS with default settings - `false` - disable CORS entirely - An object with these fields: - `origin` - a string, array of strings, or a function `(origin: string) => boolean` - `allowedHeaders` - a string or array of strings - `methods` - array of allowed HTTP methods - `exposedHeaders` - array of headers to expose - `credentials` - boolean to allow credentials - `maxAge` - preflight cache duration in seconds The `CorsOptions` type is exported from `fastmcp` for convenience. #### Custom HTTP Routes FastMCP allows you to add custom HTTP routes alongside MCP endpoints, enabling you to build comprehensive HTTP services that include REST APIs, webhooks, admin interfaces, and more - all within the same server process. ```ts const app = server.getApp(); // Add REST API endpoints with Hono's native API app.get("/api/users", async (c) => { return c.json({ users: [] }); }); // Handle path parameters app.get("/api/users/:id", async (c) => { return c.json({ userId: c.req.param("id"), query: c.req.query(), // Access query parameters }); }); // Handle POST requests with body parsing app.post("/api/users", async (c) => { const body = await c.req.json(); return c.json({ created: body }, 201); }); // Serve HTML content app.get("/admin", async (c) => { return c.html("<html><body><h1>Admin Panel</h1></body></html>"); }); // Handle webhooks app.post("/webhook/github", async (c) => { const payload = await c.req.json(); const event = c.req.header("x-github-event"); // Process webhook... return c.json({ received: true }); }); ``` Custom routes use the underlying [Hono](https://hono.dev/) app returned by `server.getApp()` and support: - Hono's HTTP methods: `get`, `post`, `put`, `delete`, `patch`, `options`, and more - Path parameters (`:param`) and wildcards (`*`) - Query string parsing - JSON, text, form, and other body helpers from `c.req` - Custom status codes and headers - Middleware and route groups through Hono Routes are matched in the order they are registered, allowing you to define specific routes before catch-all patterns. ##### Public and Protected Routes Custom Hono routes are public unless you add your own route middleware or authentication checks. For protected custom routes, put your auth logic in a reusable helper and call it from both FastMCP's `authenticate` option and your Hono route handlers: ```ts import type { IncomingMessage } from "node:http"; import type { Context } from "hono"; import { FastMCP } from "fastmcp"; async function authenticateRequest(request: IncomingMessage) { const apiKey = request.headers["x-api-key"]; return apiKey === "123" ? { userId: "123" } : undefined; } const server = new FastMCP({ name: "My Server", version: "1.0.0", authenticate: authenticateRequest, }); const app = server.getApp(); async function requireAuth(c: Context) { const auth = await authenticateRequest(c.env.incoming); if (!auth) { return c.json({ error: "Authentication required" }, 401); } return auth; } // Public route - no authentication required app.get("/.well-known/openid-configuration", async (c) => { return c.json({ issuer: "https://example.com", authorization_endpoint: "https://example.com/auth", token_endpoint: "https://example.com/token", }); }); // Private route - requires authentication app.get("/api/users", async (c) => { const auth = await requireAuth(c); if (auth instanceof Response) { return auth; } return c.json({ users: [] }); }); // Public static files app.get("/public/*", async (c) => { return c.text(`File: ${c.req.path}`); }); ``` Public routes are perfect for: - OAuth discovery endpoints (`.well-known/*`) - Health checks and status pages - Static assets and documentation - Webhook endpoints from external services - Public APIs that don't require user authentication See the [custom-routes example](src/examples/custom-routes.ts) for a complete demonstration. #### Edge Runtime Support FastMCP supports edge runtimes like Cloudflare Workers, enabling deployment of MCP servers to the edge with minimal latency worldwide. ##### Choosing Between FastMCP and EdgeFastMCP | Use Case | Class | Import | | ------------------------------- | ------------- | -------------------------------------------- | | Node.js, Express, Bun | `FastMCP` | `import { FastMCP } from "fastmcp"` | | Cloudflare Workers, Deno Deploy | `EdgeFastMCP` | `import { EdgeFastMCP } from "fastmcp/edge"` | | Feature | FastMCP | EdgeFastMCP | | -------------------- | ------------------------------ | -------------------------------------- | | Runtime | Node.js | Edge (V8 isolates) | | Start method | `server.start({ port })` | `export default server` | | Transport | stdio, httpStream, SSE | HTTP Streamable only | | Sessions | Stateful or stateless | Stateless only | | File system | Yes | No | | OAuth/Authentication | Built-in `authenticate` option | Use Hono middleware (built-in planned) | | Custom routes | `server.getApp()` | `server.getApp()` | > **Note:** Built-in authentication for EdgeFastMCP is planned for a future release. Both FastMCP and EdgeFastMCP use Hono internally, so there's no technical barrier—EdgeFastMCP was simply written before OAuth was added to FastMCP. PRs are welcome to add an `authenticate` option that accepts web `Request` instead of Node.js `http.IncomingMessage`. > > In the meantime, use Hono middleware: > > ```ts > const app = server.getApp(); > app.use("/api/*", async (c, next) => { > if (c.req.header("authorization") !== "Bearer secret") { > return c.json({ error: "Unauthorized" }, 401); > } > await next(); > }); > ``` ##### Cloudflare Workers To deploy FastMCP to Cloudflare Workers, use the `EdgeFastMCP` class from the `/edge` subpath: ```ts import { EdgeFastMCP } from "fastmcp/edge"; import { z } from "zod"; const server = new EdgeFastMCP({ name: "My Edge Server", version: "1.0.0", description: "MCP server running on Cloudflare Workers", }); // Add tools, resources, prompts as usual server.addTool({ name: "greet", description: "Greet someone", parameters: z.object({ name: z.string(), }), execute: async ({ name }) => { return `Hello, ${name}! Served from the edge.`; }, }); // Export the server as the default (required for Cloudflare Workers) export default server; ``` ##### Edge Runtime Differences When running on edge runtimes: - **Stateless by default**: Each request is handled independently - **No filesystem access**: Use fetch APIs for external data - **V8 Isolates**: Fast cold starts and efficient resource usage - **Global deployment**: Automatic distribution to edge locations ##### Custom Routes on Edge You can access the underlying Hono app to add custom HTTP routes: ```ts const app = server.getApp(); // Add a landing page app.get("/", (c) => c.html("<h1>Welcome to my MCP server</h1>")); // Add REST API endpoints app.get("/api/status", (c) => c.json({ status: "ok" })); ``` ##### Deployment Configure your `wrangler.toml`: ```toml name = "my-mcp-server" main = "src/index.ts" compatibility_date = "2024-01-01" ``` Deploy with: ```bash wrangler deploy ``` See the [edge-cloudflare-worker example](src/examples/edge-cloudflare-worker.ts) for a complete demonstration. #### Stateless Mode FastMCP supports stateless operation for HTTP streaming, where each request is handled independently without maintaining persistent sessions. This is ideal for serverless environments, load-balanced deployments, or when session state isn't required. In stateless mode: - No sessions are tracked on the server - Each request creates a temporary session that's discarded after the response - Reduced memory usage and better scalability - Perfect for stateless deployment environments You can enable stateless mode by adding the `stateless: true` option: ```ts server.start({ transportType: "httpStream", httpStream: { port: 8080, stateless: true, }, }); ``` > **Note:** Stateless mode is only available with HTTP streaming transport. Features that depend on persistent sessions (like session-specific state) will not be available in stateless mode. You can also enable stateless mode using CLI arguments or environment variables: ```bash # Via CLI argument npx fastmcp dev src/server.ts --transport http-stream --port 8080 --stateless true # Via environment variable FASTMCP_STATELESS=true npx fastmcp dev src/server.ts ``` The `/ready` health check endpoint will indicate when the server is running in stateless mode: ```json { "mode": "stateless", "ready": 1, "status": "ready", "total": 1 } ``` ## Core Concepts ### Tools [Tools](https://modelcontextprotocol.io/docs/concepts/tools) in MCP allow servers to expose executable functions that can be invoked by clients and used by LLMs to perform actions. FastMCP uses the [Standard Schema](https://standardschema.dev) specification for defining tool parameters. This allows you to use your preferred schema validation library (like Zod, ArkType, or Valibot) as long as it implements the spec. **Zod Example:** ```typescript import { z } from "zod"; server.addTool({ name: "fetch-zod", description: "Fetch the content of a url (using Zod)", parameters: z.object({ url: z.string(), }), execute: async (args) => { return await fetchWebpageContent(args.url); }, }); ``` **ArkType Example:** ```typescript import { type } from "arktype"; server.addTool({ name: "fetch-arktype", description: "Fetch the content of a url (using ArkType)", parameters: type({ url: "string", }), execute: async (args) => { return await fetchWebpageContent(args.url); }, }); ``` **Valibot Example:** Valibot requires the peer dependency @valibot/to-json-schema. ```typescript import * as v from "valibot"; server.addTool({ name: "fetch-valibot", description: "Fetch the content of a url (using Valibot)", parameters: v.object({ url: v.string(), }), execute: async (args) => { return await fetchWebpageContent(args.url); }, }); ``` **Plain JSON Schema Example:** If you already have a JSON Schema — from an OpenAPI document, a config file, or another server — `jsonSchemaAdapter` wraps it so it can be used directly, with no schema library in between. It requires the peer dependency `ajv`, which does the validation, plus `ajv-formats` if your schema uses `format` keywords such as `email` or `uri`. Both are imported the first time a tool is called, so servers that don't use this pay nothing for it. ```bash npm install ajv ajv-formats ``` ```typescript import { jsonSchemaAdapter } from "fastmcp"; server.addTool({ name: "fetch-json-schema", description: "Fetch the content of a url (using plain JSON Schema)", parameters: jsonSchemaAdapter({ type: "object", properties: { url: { type: "string", format: "uri" }, }, required: ["url"], }), execute: async (args) => { const { url } = args as { url: string }; return await fetchWebpageContent(url); }, }); ``` Works for `outputSchema` too. Note that FastMCP advertises every tool schema with `additionalProperties: false`, whatever your schema said — the same treatment Zod and Valibot schemas get. Unlike the schema libraries above, a plain JSON Schema carries no TypeScript types, so `execute` receives `unknown` arguments. Cast or narrow them yourself. #### Tools Without Parameters When creating tools that don't require parameters, you have two options: 1. Omit the parameters property entirely: ```typescript server.addTool({ name: "sayHello", description: "Say hello", // No parameters property execute: async () => { return "Hello, world!"; }, }); ``` 2. Explicitly define empty parameters: ```typescript import { z } from "zod"; server.addTool({ name: "sayHello", description: "Say hello", parameters: z.object({}), // Empty object execute: async () => { return "Hello, world!"; }, }); ``` > [!NOTE] > > Both approaches are fully compatible with all MCP clients, including Cursor. FastMCP automatically generates the proper schema in both cases. #### Structured Tool Output Tools can declare an `outputSchema` and return structured data. FastMCP exposes that value as MCP `structuredContent`, while also returning a JSON text fallback for clients that only render text content. ```typescript server.addTool({ name: "get-weather", description: "Get weather for a city", parameters: z.object({ city: z.string(), }), outputSchema: z.object({ temperature: z.number(), humidity: z.number(), }), execute: async ({ city }) => { const weather = await getWeather(city); return { temperature: weather.temperature, humidity: weather.humidity, }; }, }); ``` You can also return explicit text content and structured content together: ```typescript server.addTool({ name: "get-weather", description: "Get weather for a city", parameters: z.object({ city: z.string(), }), outputSchema: z.object({ temperature: z.number(), humidity: z.number(), }), execute: async ({ city }) => { const weather = await getWeather(city); return { content: [ { type: "text", text: `${city}: ${weather.temperature}F`, }, ], structuredContent: { temperature: weather.temperature, humidity: weather.humidity, }, }; }, }); ``` When `outputSchema` is provided, FastMCP validates `structuredContent` before sending the tool result. Invalid structured output is returned to the client as a tool error instead of silently violating the advertised schema. #### Tool Authorization You can control which tools are available to authenticated users by adding an optional `canAccess` function to a tool's definition. This function receives the authentication context and should return `true` if the user is allowed to access the tool. ```typescript server.addTool({ name: "admin-tool", description: "An admin-only tool", canAccess: (auth) => auth?.role === "admin", execute: async () => "Welcome, admin!", }); ``` #### Returning a string `execute` can return a string: ```js server.addTool({ name: "download", description: "Download a file", parameters: z.object({ url: z.string(), }), execute: async (args) => { return "Hello, world!"; }, }); ``` The latter is equivalent to: ```js server.addTool({ name: "download", description: "Download a file", parameters: z.object({ url: z.string(), }), execute: async (args) => { return { content: [ { type: "text", text: "Hello, world!", }, ], }; }, }); ``` #### Returning a list If you want to return a list of messages, you can return an object with a `content` property: ```js server.addTool({ name: "download", description: "Download a file", parameters: z.object({ url: z.string(), }), execute: async (args) => { return { content: [ { type: "text", text: "First message" }, { type: "text", text: "Second message" }, ], }; }, }); ``` #### Returning an image Use the `imageContent` to create a content object for an image: ```js import { imageContent } from "fastmcp"; server.addTool({ name: "download", description: "Download a file", parameters: z.object({ url: z.string(), }), execute: async (args) => { return imageContent({ url: "https://example.com/image.png", }); // or... // return imageContent({ // path: "/path/to/image.png", // }); // or... // return imageContent({ // buffer: Buffer.from("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=", "base64"), // }); // or... // return { // content: [ // await imageContent(...) // ], // }; }, }); ``` The `imageContent` function takes the following options: - `url`: The URL of the image. - `path`: The path to the image file. - `buffer`: The image data as a buffer. Only one of `url`, `path`, or `buffer` must be specified. The above example is equivalent to: ```js server.addTool({ name: "download", description: "Download a file", parameters: z.object({ url: z.string(), }), execute: async (args) => { return { content: [ { type: "image", data: "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=", mimeType: "image/png", }, ], }; }, }); ``` #### Configurable Ping Behavior FastMCP includes a configurable ping mechanism to maintain connection health. The ping behavior can be customized through server options: ```ts const server = new FastMCP({ name: "My Server", version: "1.0.0", ping: { // Explicitly enable or disable pings (defaults vary by transport) enabled: true, // Configure ping interval in milliseconds (default: 5000ms) intervalMs: 10000, // Set log level for ping-related messages (default: 'debug') logLevel: "debug", }, }); ``` By default, ping behavior is optimized for each transport type: - Enabled for SSE and HTTP streaming connections (which benefit from keep-alive) - Disabled for `stdio` connections (where pings are typically unnecessary) This configurable approach helps reduce log verbosity and optimize performance for different usage scenarios. ### Health-check Endpoint When you run FastMCP with the `httpStream` transport you can optionally expose a simple HTTP endpoint that returns a plain-text response useful for load-balancer or container orchestration liveness checks. Enable (or customise) the endpoint via the `health` key in the server options: ```ts const server = new FastMCP({ name: "My Server", version: "1.0.0", health: { // Enable / disable (default: true) enabled: true, // Body returned by the endpoint (default: 'ok') message: "healthy", // Path that should respond (default: '/health') path: "/healthz", // HTTP status code to return (default: 200) status: 200, }, }); await server.start({ transportType: "httpStream", httpStream: { port: 8080 }, }); ``` Now a request to `http://localhost:8080/healthz` will return: ``` HTTP/1.1 200 OK content-type: text/plain healthy ``` The endpoint is ignored when the server is started with the `stdio` transport. #### Roots Management FastMCP supports [Roots](https://modelcontextprotocol.io/docs/concepts/roots) - Feature that allows clients to provide a set of filesystem-like root locations that can be listed and dynamically updated. The Roots feature can be configured or disabled in server options: ```ts const server = new FastMCP({ name: "My Server", version: "1.0.0", roots: { // Set to false to explicitly disable roots support enabled: false, // By default, roots support is enabled (true) }, }); ``` This provides the following benefits: - Better compatibility with different clients that may not support Roots - Reduced error logs when connecting to clients that don't implement roots capability - More explicit control over MCP server capabilities - Graceful degradation when roots functionality isn't available You can listen for root changes in your server: ```ts server.on("connect", (event) => { const session = event.session; // Access the current roots console.log("Initial roots:", session.roots); // Listen for changes to the roots session.on("rootsChanged", (event) => { console.log("Roots changed:", event.roots); }); }); ``` When a client doesn't support roots or when roots functionality is explicitly disabled, these operations will gracefully handle the situation without throwing errors. ### Returning an audio Use the `audioContent` to create a content object for an audio: ```js import { audioContent } from "fastmcp"; server.addTool({ name: "download", description: "Download a file", parameters: z.object({ url: z.string(), }), execute: async (args) => { return audioContent({ url: "https://example.com/audio.mp3", }); // or... // return audioContent({ // path: "/path/to/audio.mp3", // }); // or... // return audioContent({ // buffer: Buffer.from("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=", "base64"), // }); // or... // return { // content: [ // await audioContent(...) // ], // }; }, }); ``` The `audioContent` function takes the following options: - `url`: The URL of the audio. - `path`: The path to the audio file. - `buffer`: The audio data as a buffer. Only one of `url`, `path`, or `buffer` must be specified. The above example is equivalent to: ```js server.addTool({ name: "download", description: "Download a file", parameters: z.object({ url: z.string(), }), execute: async (args) => { return { content: [ { type: "audio", data: "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=", mimeType: "audio/mpeg", }, ], }; }, }); ``` #### Return combination type You can combine various types in this way and send them back to AI ```js server.addTool({ name: "download", description: "Download a file", parameters: z.object({ url: z.string(), }), execute: async (args) => { return { content: [ { type: "text", text: "Hello, world!", }, { type: "image", data: "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=", mimeType: "image/png", }, { type: "audio", data: "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=", mimeType: "audio/mpeg", }, ], }; }, // or... // execute: async (args) => { // const imgContent = await imageContent({ // url: "https://example.com/image.png", // }); // const audContent = await audioContent({ // url: "https://example.com/audio.mp3", // }); // return { // content: [ // { // type: "text", // text: "Hello, world!", // }, // imgContent, // audContent, // ], // }; // }, }); ``` #### Custom Logger FastMCP allows you to provide a custom logger implementation to control how the server logs messages. This is useful for integrating with existing logging infrastructure or customizing log formatting. ```ts import { FastMCP, Logger } from "fastmcp"; class CustomLogger implements Logger { debug(...args: unknown[]): void { console.log("[DEBUG]", new Date().toISOString(), ...args); } error(...args: unknown[]): void { console.error("[ERROR]", new Date().toISOString(), ...args); } info(...args: unknown[]): void { console.info("[INFO]", new Date().toISOString(), ...args); } log(...args: unknown[]): void { console.log("[LOG]", new Date().toISOString(), ...args); } warn(...args: unknown[]): void { console.warn("[WARN]", new Date().toISOString(), ...args); } } const server = new FastMCP({ name: "My Server", version: "1.0.0", logger: new CustomLogger(), }); ``` See `src/examples/custom-logger.ts` for examples with Winston, Pino, and file-based logging. #### Logging Tools can log messages to the client using the `log` object in the context object: ```js server.addTool({ name: "download", description: "Download a file", parameters: z.object({ url: z.string(), }), execute: async (args, { log }) => { log.info("Downloading file...", { url, }); // ... log.info("Downloaded file"); return "done"; }, }); ``` The `log` object has the following methods: - `debug(message: string, data?: SerializableValue)` - `error(message: string, data?: SerializableValue)` - `info(message: string, data?: SerializableValue)` - `warn(message: string, data?: SerializableValue)` #### Errors The errors that are meant to be shown to the user should be thrown as `UserError` instances: ```js import { UserError } from "fastmcp"; server.addTool({ name: "download", description: "Download a file", parameters: z.object({ url: z.string(), }), execute: async (args) => { if (args.url.startsWith("https://example.com")) { throw new UserError("This URL is not allowed"); } return "done"; }, }); ``` #### Progress Tools can report progress by calling `reportProgress` in the context object: ```js server.addTool({ name: "download", description: "Download a file", parameters: z.object({ url: z.string(), }), execute: async (args, { reportProgress }) => { await reportProgress({ progress: 0, total: 100, }); // ... await reportProgress({ progress: 100, total: 100, }); return "done"; }, }); ``` `reportProgress` accepts an optional human-readable `message` alongside the numeric fields, which clients can display next to the progress indicator: ```js await reportProgress({ progress: 40, total: 100, message: "Downloading chunk 4 of 10…", }); ``` Progress notifications are only emitted when the client opts in by supplying a `progressToken` on the tool call; otherwise `reportProgress` is a no-op. Because `notifications/progress` is part of the MCP specification (the `message` field since revision 2025-03-26), this is the portable way to send incremental updates during a long-running tool call — see [Streaming Output](#streaming-output) below for the difference. #### Streaming Output FastMCP can stream partial results from tools while they're still executing, enabling responsive UIs and real-time feedback. This is particularly useful for: - Long-running operations that generate content incrementally - Progressive generation of text, images, or other media - Operations where users benefit from seeing immediate partial results > [!IMPORTANT] > `streamContent` is a **FastMCP extension, not part of the MCP specification**. It emits a `notifications/tool/streamContent` notification, which the MCP specification does not define — as of revision `2025-11-25` there is no standard mechanism for streaming tool output ([SEP-2998](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2998) is the in-progress proposal to add one). > > Clients discard notifications they have no handler registered for, silently and without error. A client only sees streamed content if it registers a handler for the method (or sets a `fallbackNotificationHandler`), and **no client is known to render it as tool output** — MCP Inspector, for example, logs it in its notifications pane via a fallback handler, but the tool result itself still shows only what `execute` returned. Streaming is therefore mainly useful when you also control the client — see [Consuming streamed content](#consuming-streamed-content) below. If you need incremental updates that work on any client, use [`reportProgress`](#progress) with a `message` instead. To stream from a tool, use the `streamContent` method: ```js server.addTool({ name: "generateText", description: "Generate text incrementally", parameters: z.object({ prompt: z.string(), }), annotations: { streamingHint: true, // Advisory only; see below readOnlyHint: true, }, execute: async (args, { streamContent }) => { // Send initial content immediately await streamContent({ type: "text", text: "Starting generation...\n" }); // Simulate incremental content generation const words = "The quick brown fox jumps over the lazy dog.".split(" "); for (const word of words) { await streamContent({ type: "text", text: word + " " }); await new Promise((resolve) => setTimeout(resolve, 300)); // Simulate delay } // Always return a final result. Returning nothing sends an empty tool // result, so clients that ignore the streamed notifications see no output // at all. return "The quick brown fox jumps over the lazy dog."; }, }); ``` > [!WARNING] > Returning `undefined` from `execute` produces a tool result with empty `content`. If you stream everything and return nothing, the tool call resolves to an empty result with no indication that anything was lost — including on clients that do log the notification. Return the complete result as well, and treat streamed content purely as a progressive-rendering enhancement. The `streamingHint` annotation is advisory metadata. It is forwarded verbatim to clients in `tools/list`, but it does not enable or gate `streamContent`, and FastMCP itself never reads it. No client is known to act on it today, though [SEP-2998](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2998) proposes standardizing the same annotation name. ##### Consuming streamed content A client sees these notifications only if it registers a handler for the method (or sets a `fallbackNotificationHandler`): ```ts import { z } from "zod"; const StreamContentNotificationSchema = z.object({ method: z.literal("notifications/tool/streamContent"), params: z.object({ content: z.array(z.any()), toolName: z.string(), }), }); client.setNotificationHandler( StreamContentNotificationSchema, (notification) => { const { content, toolName } = notification.params; // Render the partial content however you like. }, ); ``` Note that notifications carry only `toolName`, not a request or progress token, so concurrent calls to the same tool on one session cannot be told apart. Streaming works with all content types (text, image, audio) and can be combined with progress reporting: ```js server.addTool({ name: "processData", description: "Process data with streaming updates", parameters: z.object({ datasetSize: z.number(), }), annotations: { streamingHint: true, }, execute: async (args, { streamContent, reportProgress }) => { const total = args.datasetSize; for (let i = 0; i < total; i++) { // Standard progress notification: reaches every spec-compliant client await reportProgress({ progress: i, total, message: `Processed ${i} of ${total} items`, }); // Richer partial content: only reaches clients that opt in if (i % 10 === 0) { await streamContent({ type: "text", text: `Processed ${i} of ${total} items...\n`, }); } await new Promise((resolve) => setTimeout(resolve, 50)); } return "Processing complete!"; }, }); ``` #### Elicitation Tools can request additional information from the user mid-execution via [elicitation](https://modelcontextprotocol.io/specification/2025-06-18/client/elicitation), using the `elicit` method in the context object. The client must advertise the matching `elicitation` capability mode — `elicitation: { form: {} }` for form requests (the default) and/or `elicitation: { url: {} }` for url requests. ```js server.addTool({ name: "delete-file", description: "Delete a file", parameters: z.object({ path: z.string(), }), execute: async (args, { elicit }) => { const response = await elicit({ message: `Are you sure you want to delete ${args.path}?`, requestedSchema: { type: "object", properties: { confirmed: { type: "boolean", }, }, required: ["confirmed"], }, }); if (response.action !== "accept" || !response.content?.confirmed) { return "Deletion cancelled."; } // ... return `Deleted ${args.path}`; }, }); ``` The response `action` is `"accept"`, `"decline"`, or `"cancel"`; on accept, `content` holds the user's answers matching `requestedSchema`. Elicitation is also available outside of tools via [`session.requestElicitation`](#requestelicitation). #### Tool Annotations As of the MCP Specification (2025-03-26), tools can include annotations that provide richer context and control by adding metadata about a tool's behavior: ```typescript server.addTool({ name: "fetch-content", description: "Fetch content from a URL", parameters: z.object({ url: z.string(), }), annotations: { title: "Web Content Fetcher", // Human-readable title for UI display readOnlyHint: true, // Tool doesn't modify its environment openWorldHint: true, // Tool interacts with external entities }, execute: async (args) => { return await fetchWebpageContent(args.url); }, }); ``` The available annotations are: | Annotation | Type | Default | Description | | :---------------- | :------ | :------ | :----------------------------------------------------------------------------------------------------------------------------------- | | `title` | string | - | A human-readable title for the tool, useful for UI display | | `readOnlyHint` | boolean | `false` | If true, indicates the tool does not modify its environment | | `destructiveHint` | boolean | `true` | If true, the tool may perform destructive updates (only meaningful when `readOnlyHint` is false) | | `idempotentHint` | boolean | `false` | If true, calling the tool repeatedly with the same arguments has no additional effect (only meaningful when `readOnlyHint` is false) | | `openWorldHint` | boolean | `true` | If true, the tool may interact with an "open world" of external entities | These annotations help clients and LLMs better understand how to use the tools and what to expect when calling them. ### Resources [Resources](https://modelcontextprotocol.io/docs/concepts/resources) represent any kind of data that an MCP server wants to make available to clients. This can include: - File contents - Screenshots and images - Log files - And more Each resource is identified by a unique URI and can contain either text or binary data. ```ts server.addResource({ uri: "file:///logs/app.log", name: "Application Logs", mimeType: "text/plain", async load() { return { text: await readLogFile(), }; }, }); ``` > [!NOTE] > > `load` can return multiple resources. This could be used, for example, to return a list of files inside a directory when the directory is read. > > ```ts > async load() { > return [ > { > text: "First file content", > }, > { > text: "Second file content", > }, > ]; > } > ``` You can also return binary contents in `load`: ```ts async load() { return { blob: 'base64-encoded-data' }; } ``` `load` also receives `auth` (the value returned by your `authenticate` function, if any) and a `context` object as its second and third arguments. `context` mirrors the `client`, `log`, `session`, and `sessionId` fields available to `tool.execute` (see [Session ID and Request ID Tracking](#session-id-and-request-id-tracking)); `reportProgress` and `streamContent` are not included since they are tied to a tool call's progress token: ```ts server.addResource({ uri: "file:///logs/app.log", name: "Application Logs", mimeType: "text/plain", async load(auth, context) { context.log.info("loading application logs", { requestedBy: auth?.userId }); return { text: await readLogFile(), }; }, }); ``` #### Subscribing to resource updates Clients can subscribe to a resource with the MCP [`resources/subscribe`](https://modelcontextprotocol.io/specification/2025-06-18/server/resources#subscriptions) method to be notified whenever its contents change. FastMCP advertises the `subscribe` capability automatically for any server that exposes resources, tracks each client's subscriptions, and lets you emit an update with `sendResourceUpdated`: ```ts server.addResource({ uri: "file:///logs/app.log", name: "Application Logs", mimeType: "text/plain", async load() { return { text: await readLogFile() }; }, }); // Whenever the underlying data changes, notify subscribed clients: await server.sendResourceUpdated("file:///logs/app.log"); ``` `sendResourceUpdated` only notifies clients that have subscribed to the given URI, so it is safe to call whenever your data changes. FastMCP also advertises the `listChanged` capability for resources and prompts and emits `notifications/resources/list_changed` / `notifications/prompts/list_changed` automatically when you add or remove resources, resource templates, or prompts at runtime. ### Resource templates You can also define resource templates: ```ts server.addResourceTemplate({ uriTemplate: "file:///logs/{name}.log", name: "Application Logs", mimeType: "text/plain", arguments: [ { name: "name", description: "Name of the log", required: true, }, ], async load({ name }) { return { text: `Example log content for ${name}`, }; }, }); ``` Like plain resources, `load` also receives `auth` and `context` as its second and third arguments (see [Resources](#resources)). #### Resource template argument auto-completion Provide `complete` functions for resource template arguments to enable automatic completion: ```ts server.addResourceTemplate({ uriTemplate: "file:///logs/{name}.log", name: "Application Logs", mimeType: "text/plain", arguments: [ { name: "name", description: "Name of the log", required: true, complete: async (value) => { if (value === "Example") { return { values: ["Example Log"], }; } return { values: [], }; }, }, ], async load({ name }) { return { text: `Example log content for ${name}`, }; }, }); ``` ### Embedded Resources FastMCP provides a convenient `embedded()` method that simplifies including resources in tool responses. This feature reduces code duplication and makes it easier to reference resources from within tools. #### Basic Usage ```js server.addTool({ name: "get_user_data", description: "Retrieve user information", parameters: z.object({ userId: z.string(), }), execute: async (args) => { return { content: [ { type: "resource", resource: await server.embedded(`user://profile/${args.userId}`), }, ], }; }, }); ``` #### Working with Resource Templates The `embedded()` method works seamlessly with resource templates: ```js // Define a resource template server.addResourceTemplate({ uriTemplate: "docs://project/{section}", name: "Project Documentation", mimeType: "text/markdown", arguments: [ { name: "section", required: true, }, ], async load(args) { const docs = { "getting-started": "# Getting Started\n\nWelcome to our project!", "api-reference": "# API Reference\n\nAuthentication is required.", }; return { text: docs[args.section] || "Documentation not found", }; }, }); // Use embedded resources in a tool server.addTool({ name: "get_documentation", description: "Retrieve project documentation", parameters: z.object({ section: z.enum(["getting-started", "api-reference"]), }), execute: async (args) => { return { content: [ { type: "resource", resource: await server.embedded(`docs://project/${args.section}`), }, ], }; }, }); ``` #### Working with Direct Resources It also works with directly defined resources: ```js // Define a direct resource server.addResource({ uri: "system://status", name: "System Status", mimeType: "text/plain", async load() { return { text: "System operational", }; }, }); // Use in a tool server.addTool({ name: "get_system_status", description: "Get current system status", parameters: z.object({}), execute: async () => { return { content: [ { type: "resource", resource: await server.embedded("system://status"), }, ], }; }, }); ``` ### Prompts [Prompts](https://modelcontextprotocol.io/docs/concepts/prompts) enable servers to define reusable prompt templates and workflows that clients can easily surface to users and LLMs. They provide a powerful way to standardize and share common LLM interactions. ```ts server.addPrompt({ name: "git-commit", description: "Generate a Git commit message", arguments: [ { name: "changes", d