A TypeScript framework for building MCP servers capable of handling client sessions.
[!NOTE]
For a Python implementation, see FastMCP.
Features
- Simple Tool, Resource, Prompt definition
- Authentication
- Passing headers through context
- Session ID and Request ID tracking
- Sessions
- Image content
- Audio content
- Embedded
- Logging
- Error handling
- HTTP Streaming (with SSE compatibility)
- HTTPS Support for secure connections
- Custom HTTP routes for REST APIs, webhooks, and admin interfaces
- Edge Runtime Support for Cloudflare Workers, Deno Deploy, and more
- Stateless mode for serverless deployments
- CORS (enabled by default)
- Progress notifications
- Streaming output
- Typed server events
- Prompt argument auto-completion
- Sampling
- Configurable ping behavior
- Health-check endpoint
- Roots
- CLI for testing and debugging
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 all the server components
- Handling of connections
- Handling of tools
- Handling of responses
- Handling of resources
- Adding prompts, resources, resource templates
- Embedding resources, image and audio 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
npm install fastmcp
Quickstart
[!NOTE]
There are many real-world examples of using FastMCP in the wild. See the Showcase for examples.
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:
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.
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 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:
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.endpointoption (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/issuer1per 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:
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:
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:
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 filesslKey- Path to SSL private key filesslCa- (Optional) Path to CA certificate for mutual TLS authentication
For testing, you can generate self-signed certificates:
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 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:
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 settingsfalse- disable CORS entirely- An object with these fields:
origin- a string, array of strings, or a function(origin: string) => booleanallowedHeaders- a string or array of stringsmethods- array of allowed HTTP methodsexposedHeaders- array of headers to exposecredentials- boolean to allow credentialsmaxAge- 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.
// Add REST API endpoints
server.addRoute("GET", "/api/users", async (req, res) => {
res.json({ users: [] });
});
// Handle path parameters
server.addRoute("GET", "/api/users/:id", async (req, res) => {
res.json({
userId: req.params.id,
query: req.query, // Access query parameters
});
});
// Handle POST requests with body parsing
server.addRoute("POST", "/api/users", async (req, res) => {
const body = await req.json();
res.status(201).json({ created: body });
});
// Serve HTML content
server.addRoute("GET", "/admin", async (req, res) => {
res.send("<html><body><h1>Admin Panel</h1></body></html>");
});
// Handle webhooks
server.addRoute("POST", "/webhook/github", async (req, res) => {
const payload = await req.json();
const event = req.headers["x-github-event"];
// Process webhook...
res.json({ received: true });
});
Custom routes support:
- All HTTP methods: GET, POST, PUT, DELETE, PATCH, OPTIONS
- Path parameters (
:param) and wildcards (*) - Query string parsing
- JSON and text body parsing
- Custom status codes and headers
- Authentication via the same
authenticatefunction as MCP - Public routes that bypass authentication
Routes are matched in the order they are registered, allowing you to define specific routes before catch-all patterns.
Public Routes
By default, custom routes require authentication (if configured). You can make routes public by adding the { public: true } option:
// Public route - no authentication required
server.addRoute(
"GET",
"/.well-known/openid-configuration",
async (req, res) => {
res.json({
issuer: "https://example.com",
authorization_endpoint: "https://example.com/auth",
token_endpoint: "https://example.com/token",
});
},
{ public: true },
);
// Private route - requires authentication
server.addRoute("GET", "/api/users", async (req, res) => {
// req.auth contains authenticated user data
res.json({ users: [] });
});
// Public static files
server.addRoute(
"GET",
"/public/*",
async (req, res) => {
// Serve static files without authentication
res.send(`File: ${req.url}`);
},
{ public: true },
);
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 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
authenticateoption that accepts webRequestinstead of Node.jshttp.IncomingMessage.In the meantime, use Hono middleware:
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:
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:
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:
name = "my-mcp-server"
main = "src/index.ts"
compatibility_date = "2024-01-01"
Deploy with:
wrangler deploy
See the edge-cloudflare-worker example 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:
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:
# 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:
{
"mode": "stateless",
"ready": 1,
"status": "ready",
"total": 1
}
Core Concepts
Tools
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 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:
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:
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.
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);
},
});
Tools Without Parameters
When creating tools that don't require parameters, you have two options:
-
Omit the parameters property entirely:
server.addTool({ name: "sayHello", description: "Say hello", // No parameters property execute: async () => { return "Hello, world!"; }, }); -
Explicitly define empty parameters:
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.
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:
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.
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:
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:
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:
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:
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:
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:
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
stdioconnections (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:
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 - 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:
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:
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:
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:
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
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.
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:
server.addTool({
name: "download",
description: "Download a file",
parameters: z.object({
url: z.string(),
}),
execute: async (args, { log }) => {
log.info("Downloading file...", {