English | 简体中文
A TypeScript framework for building Model Context Protocol (MCP) servers with decorator-based syntax. This package simplifies the creation of MCP servers by providing intuitive decorators for defining tools, prompts, and resources.
- 🎯 Decorator-based API - Use simple decorators to define MCP tools, prompts, and resources
- 🚀 Multiple Transport Types - Support for stdio, SSE, and streamable HTTP transports
- 📝 TypeScript First - Built with TypeScript for excellent type safety and IntelliSense
- 🔧 Zod Integration - Built-in parameter validation using Zod schemas
- 🌐 HTTP Integrations - Works with Express, Koa v3, hapi, AdonisJS, NestJS
- 📝 Doc-Driven Metadata (TSDoc-like) - Infer tool description and parameter schema from doc comments when decorator options are omitted
- ⚖️ Decorator Precedence - Decorator options always override doc comments for the same fields
- 🧪 DX-Friendly Dev/Test - Run tests with tsx against TypeScript sources via an alias; build src and tests into a flat
dist/ - 📒 Structured Logging - Pino-based logging with pretty output in development
npm install fastmcp-tsimport { FastMCP, tool } from 'fastmcp-ts';
import { z } from 'zod';
class MathTools {
/**
* Add two numbers together.
* @param a First number
* @param b Second number
* @returns Sum of a and b
*/
@tool() // name defaults to method, description/params inferred from docs
async add({ a, b }: { a: number; b: number }) {
return a + b;
}
/**
* Multiplies two numbers.
* @param a First number
* @param b Second number
* @returns Product of a and b
*/
@tool({
name: 'product', // decorator overrides doc name
parameters: z.object({ // decorator overrides doc-inferred params
a: z.number(),
b: z.number(),
}),
})
async multiply({ a, b }: { a: number; b: number }) {
return a * b;
}
}
const server = new FastMCP({ name: 'math-server', version: '1.0.0' });
server.register(new MathTools());
await server.serve(); // stdio by defaultimport express from 'express'; // optional, for HTTP example only
import { FastMCP, tool } from 'fastmcp-ts';
import { z } from 'zod';
class MyTools {
@tool({
name: "greet",
description: "Greet someone",
parameters: z.object({
name: z.string(),
}),
})
async greet({ name }: { name: string }) {
return `Hello, ${name}!`;
}
}
const server = new FastMCP({
name: "greeting-server",
version: "1.0.0"
});
server.register(new MyTools());
// Configure for HTTP transport
const transportConfig = FastMCP.createStreamableHTTPConfig({
sessionIdGenerator: undefined
});
await server.serve(transportConfig);
// Set up Express app
const app = express();
app.use(express.json());
app.post('/mcp', async (req, res) => {
const transport = server.getTransport();
await transport.handleRequest(req, res, req.body);
});
app.listen(3000, () => {
console.log('MCP server running on port 3000');
});import Koa from 'koa';
import bodyParser from 'koa-bodyparser';
import Router from '@koa/router';
import { FastMCP, tool } from 'fastmcp-ts';
import { z } from 'zod';
class MyTools {
@tool({
name: 'greet',
parameters: z.object({ name: z.string() })
})
async greet({ name }: { name: string }) { return `Hello, ${name}!`; }
}
const server = new FastMCP({ name: 'greeting-server', version: '1.0.0' });
server.register(new MyTools());
await server.serve(FastMCP.createStreamableHTTPConfig({ sessionIdGenerator: undefined }));
const transport = server.getTransport();
const app = new Koa();
const router = new Router();
app.use(bodyParser());
router.post('/mcp', async (ctx) => {
await transport.handleRequest(
// Express-like shim: req, res, body
ctx.req as any,
ctx.res as any,
ctx.request.body
);
// Transport writes to res directly; ensure Koa doesn’t re-send
ctx.respond = false;
});
app.use(router.routes());
app.use(router.allowedMethods());
app.listen(3000);import Hapi from '@hapi/hapi';
import { FastMCP, tool } from 'fastmcp-ts';
import { z } from 'zod';
class MyTools {
@tool({ name: 'greet', parameters: z.object({ name: z.string() }) })
async greet({ name }: { name: string }) { return `Hello, ${name}!`; }
}
const server = new FastMCP({ name: 'greeting-server', version: '1.0.0' });
server.register(new MyTools());
await server.serve(FastMCP.createStreamableHTTPConfig({ sessionIdGenerator: undefined }));
const transport = server.getTransport();
const app = Hapi.server({ port: 3000, host: 'localhost' });
app.route({
method: 'POST',
path: '/mcp',
options: { payload: { parse: true, output: 'data' } },
handler: async (request, h) => {
const res = request.raw.res;
await transport.handleRequest(request.raw.req as any, res as any, request.payload);
return h.abandon; // response already written
},
});
await app.start();// start/routes.ts
import Route from '@ioc:Adonis/Core/Route';
import { FastMCP, tool } from 'fastmcp-ts';
import { z } from 'zod';
class MyTools {
@tool({ name: 'greet', parameters: z.object({ name: z.string() }) })
async greet({ name }: { name: string }) { return `Hello, ${name}!`; }
}
const mcp = new FastMCP({ name: 'greeting-server', version: '1.0.0' });
mcp.register(new MyTools());
await mcp.serve(FastMCP.createStreamableHTTPConfig({ sessionIdGenerator: undefined }));
const transport = mcp.getTransport();
Route.post('/mcp', async ({ request, response }) => {
await transport.handleRequest(request.request as any, response.response as any, request.body());
});import { Controller, Post, Req, Res } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import { Module } from '@nestjs/common';
import { FastMCP, tool } from 'fastmcp-ts';
import { z } from 'zod';
class MyTools {
@tool({ name: 'greet', parameters: z.object({ name: z.string() }) })
async greet({ name }: { name: string }) { return `Hello, ${name}!`; }
}
const mcp = new FastMCP({ name: 'greeting-server', version: '1.0.0' });
mcp.register(new MyTools());
await mcp.serve(FastMCP.createStreamableHTTPConfig({ sessionIdGenerator: undefined }));
const transport = mcp.getTransport();
@Controller()
class AppController {
@Post('mcp')
async mcp(@Req() req: any, @Res() res: any) {
await transport.handleRequest(req, res, req.body);
}
}
@Module({ controllers: [AppController] })
class AppModule {}
async function bootstrap() {
const app = await NestFactory.create(AppModule);
await app.listen(3000);
}
bootstrap();Define a method as an MCP tool.
interface ToolOptions {
name?: string; // Tool name; defaults to method name
description?: string; // Description; inferred from doc comments if omitted
parameters?: z.ZodSchema<any>; // Zod schema; inferred from @param docs if omitted
}Example:
@tool({
name: "file_read",
description: "Read contents of a file",
parameters: z.object({
path: z.string().describe("File path to read"),
encoding: z.string().optional().describe("File encoding (default: utf-8)"),
}),
})
async readFile({ path, encoding = 'utf-8' }: { path: string; encoding?: string }) {
// Implementation here
return fs.readFileSync(path, encoding);
}Notes:
- Arguments are a single object (MCP protocol). Even for “two numbers,” define your method as
async fn({ a, b }: { a:number; b:number }). - Doc inference supports summary lines,
@param name desc, and@returns desc. - Decorator precedence: if a field is provided in the decorator, doc comments won’t override it.
Define a method as an MCP prompt.
interface PromptOptions {
name: string; // Prompt name
description?: string; // Prompt description
arguments?: z.ZodSchema<any>; // Zod schema for arguments
}Example:
@prompt({
name: "code_review",
description: "Generate a code review prompt",
arguments: z.object({
language: z.string(),
code: z.string(),
}),
})
async codeReviewPrompt({ language, code }: { language: string; code: string }) {
return `Please review this ${language} code:\n\n${code}`;
}Define a method as an MCP resource.
interface ResourceOptions {
uri: string | RegExp; // Resource URI or pattern
name?: string; // Resource name
description?: string; // Resource description
mimeType?: string; // MIME type (default: text/plain)
}Example:
@resource({
uri: /^file:\/\/(.+)$/,
name: "file_system",
description: "File system resource",
mimeType: "text/plain",
})
async readResource({ uri }: { uri: string }) {
const match = uri.match(/^file:\/\/(.+)$/);
if (match) {
return fs.readFileSync(match[1], 'utf-8');
}
throw new Error('Invalid file URI');
}new FastMCP(options: { name: string; version: string })Register a class instance containing decorated methods.
Start the server with the specified transport configuration.
Close the server and transport.
FastMCP.createStdioConfig(): StdioTransportConfig
FastMCP.createSSEConfig(endpoint: string, response: ServerResponse, options?: SSEServerTransportOptions): SSETransportConfig
FastMCP.createStreamableHTTPConfig(options: StreamableHTTPServerTransportOptions): StreamableHTTPTransportConfigawait server.serve(); // Uses stdio by default
// or explicitly:
await server.serve(FastMCP.createStdioConfig());import type { ServerResponse } from 'node:http';
const config = FastMCP.createSSEConfig('/sse', response, {
// SSE options
});
await server.serve(config);const config = FastMCP.createStreamableHTTPConfig({
sessionIdGenerator: undefined
});
await server.serve(config);When you omit description or parameters in @tool, FastMCP-TS will try to read the doc block above the decorator and infer:
- Description: from the summary text.
- Parameters: from
@paramannotations. Simple heuristics map words like “number”, “boolean”, “array” to corresponding Zod types; otherwise string.
Decorator options always win. For example:
/**
* Multiplies two numbers
* @param a First number
* @param b Second number
* @returns Product of a and b
*/
@tool({
name: 'multiply1',
parameters: z.object({ a: z.number(), b: z.number() })
})
async multiply({ a, b }: { a: number; b: number }) { /* ... */ }
// Result: name=multiply1, description from docs, parameters from decoratorThis repo compiles both src/ and tests/ to a flat dist/:
tsconfig.src.jsonbuildssrc → dist(emits d.ts)tsconfig.tests.jsonbuildstests → dist(no d.ts)npm run buildruns both projects sequentially
Dev workflow with tsx (no build):
- We use a package
importsalias inpackage.json:#fastmcp→./src/index.tsin development#fastmcp→./dist/index.jsby default
- Tests import the alias:
import { FastMCP, tool } from '#fastmcp' - Run with tsx:
tsx tests/simple-demo.tsBuild and run compiled JS:
npm run build
node dist/simple-demo.jsCollision note: If src/foo.ts and tests/foo.ts both exist, they would emit the same dist/foo.js. Rename to avoid overwrites, or adjust tests outDir to dist/tests.
FastMCP-TS uses pino for structured logs.
- Level: set
FASTMCP_LOG_LEVEL=debug(orLOG_LEVEL). - Pretty output: set
PINO_PRETTY=1for human-readable logs.
Example (PowerShell):
$env:FASTMCP_LOG_LEVEL = 'debug'
$env:PINO_PRETTY = '1'
tsx tests/simple-demo.tsimport { FastMCP, tool, resource } from 'fastmcp-ts';
import { z } from 'zod';
import * as fs from 'fs';
import * as path from 'path';
class FileOperations {
@tool({
name: "list_directory",
description: "List contents of a directory",
parameters: z.object({
path: z.string().describe("Directory path"),
}),
})
async listDirectory({ path: dirPath }: { path: string }) {
const files = fs.readdirSync(dirPath);
return files.map(file => ({
name: file,
isDirectory: fs.statSync(path.join(dirPath, file)).isDirectory()
}));
}
@tool({
name: "write_file",
description: "Write content to a file",
parameters: z.object({
path: z.string(),
content: z.string(),
}),
})
async writeFile({ path: filePath, content }: { path: string; content: string }) {
fs.writeFileSync(filePath, content, 'utf-8');
return `File written to ${filePath}`;
}
@resource({
uri: /^file:\/\/(.+)$/,
description: "Read file contents as resource",
mimeType: "text/plain",
})
async readFileResource({ uri }: { uri: string }) {
const match = uri.match(/^file:\/\/(.+)$/);
if (!match) throw new Error('Invalid file URI');
const filePath = match[1];
return fs.readFileSync(filePath, 'utf-8');
}
}
const server = new FastMCP({
name: "file-operations-server",
version: "1.0.0"
});
server.register(new FileOperations());
await server.serve();# Install dependencies
npm install
# Build the project
npm run build
# Run in development mode with watch
npm run dev
# Run tests
npm test
# Start the demo server
npm start- Node.js 18+
- TypeScript 5.0+
@modelcontextprotocol/sdk- Core MCP SDKzod- Schema validationreflect-metadata- Decorator metadata support
HTTP frameworks are optional and only needed for examples/tests:
express,koa,@koa/router,koa-bodyparser@hapi/hapi@adonisjs/*(depending on your app)@nestjs/common,@nestjs/core(if using NestJS)
ISC
Contributions are welcome! Please feel free to submit a Pull Request.