Files
claude-task-master/docs/fastmcp-docs.txt
Eyal Toledano 1abcf69ecd Adds 3 docs for MCP related context provision. Also updates the system prompt for the task update command. Updated the system prompt with clear guidelines about:
Preserving completed subtasks exactly as they are
Building upon what has already been done
Creating new subtasks instead of modifying completed ones
Making new subtasks specific and targeted
Added specific instructions to the Perplexity AI system message to emphasize preserving completed subtasks
Added an informative boxed message to the user explaining how completed subtasks will be handled during the update process
Added emphatic instructions in the user prompts to both Claude and Perplexity to highlight completed subtasks that must be preserved
These changes ensure that:
Completed subtasks will be preserved
The AI will build on top of what's already been done
If something needs to be changed/undone, it will be handled through new subtasks
The user is clearly informed about how subtasks are handled.
2025-03-27 00:58:14 -04:00

3850 lines
84 KiB
Plaintext

Directory Structure:
└── ./
├── src
│ ├── bin
│ │ └── fastmcp.ts
│ ├── examples
│ │ └── addition.ts
│ ├── FastMCP.test.ts
│ └── FastMCP.ts
├── eslint.config.js
├── package.json
├── README.md
└── vitest.config.js
---
File: /src/bin/fastmcp.ts
---
#!/usr/bin/env node
import yargs from "yargs";
import { hideBin } from "yargs/helpers";
import { execa } from "execa";
await yargs(hideBin(process.argv))
.scriptName("fastmcp")
.command(
"dev <file>",
"Start a development server",
(yargs) => {
return yargs.positional("file", {
type: "string",
describe: "The path to the server file",
demandOption: true,
});
},
async (argv) => {
try {
await execa({
stdin: "inherit",
stdout: "inherit",
stderr: "inherit",
})`npx @wong2/mcp-cli npx tsx ${argv.file}`;
} catch {
process.exit(1);
}
},
)
.command(
"inspect <file>",
"Inspect a server file",
(yargs) => {
return yargs.positional("file", {
type: "string",
describe: "The path to the server file",
demandOption: true,
});
},
async (argv) => {
try {
await execa({
stdout: "inherit",
stderr: "inherit",
})`npx @modelcontextprotocol/inspector npx tsx ${argv.file}`;
} catch {
process.exit(1);
}
},
)
.help()
.parseAsync();
---
File: /src/examples/addition.ts
---
/**
* This is a complete example of an MCP server.
*/
import { FastMCP } from "../FastMCP.js";
import { z } from "zod";
const server = new FastMCP({
name: "Addition",
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.addResource({
uri: "file:///logs/app.log",
name: "Application Logs",
mimeType: "text/plain",
async load() {
return {
text: "Example log content",
};
},
});
server.addPrompt({
name: "git-commit",
description: "Generate a Git commit message",
arguments: [
{
name: "changes",
description: "Git diff or description of changes",
required: true,
},
],
load: async (args) => {
return `Generate a concise but descriptive commit message for these changes:\n\n${args.changes}`;
},
});
server.start({
transportType: "stdio",
});
---
File: /src/FastMCP.test.ts
---
import { FastMCP, FastMCPSession, UserError, imageContent } from "./FastMCP.js";
import { z } from "zod";
import { test, expect, vi } from "vitest";
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
import { getRandomPort } from "get-port-please";
import { setTimeout as delay } from "timers/promises";
import {
CreateMessageRequestSchema,
ErrorCode,
ListRootsRequestSchema,
LoggingMessageNotificationSchema,
McpError,
PingRequestSchema,
Root,
} from "@modelcontextprotocol/sdk/types.js";
import { createEventSource, EventSourceClient } from 'eventsource-client';
const runWithTestServer = async ({
run,
client: createClient,
server: createServer,
}: {
server?: () => Promise<FastMCP>;
client?: () => Promise<Client>;
run: ({
client,
server,
}: {
client: Client;
server: FastMCP;
session: FastMCPSession;
}) => Promise<void>;
}) => {
const port = await getRandomPort();
const server = createServer
? await createServer()
: new FastMCP({
name: "Test",
version: "1.0.0",
});
await server.start({
transportType: "sse",
sse: {
endpoint: "/sse",
port,
},
});
try {
const client = createClient
? await createClient()
: new Client(
{
name: "example-client",
version: "1.0.0",
},
{
capabilities: {},
},
);
const transport = new SSEClientTransport(
new URL(`http://localhost:${port}/sse`),
);
const session = await new Promise<FastMCPSession>((resolve) => {
server.on("connect", (event) => {
resolve(event.session);
});
client.connect(transport);
});
await run({ client, server, session });
} finally {
await server.stop();
}
return port;
};
test("adds tools", async () => {
await runWithTestServer({
server: async () => {
const server = new FastMCP({
name: "Test",
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);
},
});
return server;
},
run: async ({ client }) => {
expect(await client.listTools()).toEqual({
tools: [
{
name: "add",
description: "Add two numbers",
inputSchema: {
additionalProperties: false,
$schema: "http://json-schema.org/draft-07/schema#",
type: "object",
properties: {
a: { type: "number" },
b: { type: "number" },
},
required: ["a", "b"],
},
},
],
});
},
});
});
test("calls a tool", async () => {
await runWithTestServer({
server: async () => {
const server = new FastMCP({
name: "Test",
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);
},
});
return server;
},
run: async ({ client }) => {
expect(
await client.callTool({
name: "add",
arguments: {
a: 1,
b: 2,
},
}),
).toEqual({
content: [{ type: "text", text: "3" }],
});
},
});
});
test("returns a list", async () => {
await runWithTestServer({
server: async () => {
const server = new FastMCP({
name: "Test",
version: "1.0.0",
});
server.addTool({
name: "add",
description: "Add two numbers",
parameters: z.object({
a: z.number(),
b: z.number(),
}),
execute: async () => {
return {
content: [
{ type: "text", text: "a" },
{ type: "text", text: "b" },
],
};
},
});
return server;
},
run: async ({ client }) => {
expect(
await client.callTool({
name: "add",
arguments: {
a: 1,
b: 2,
},
}),
).toEqual({
content: [
{ type: "text", text: "a" },
{ type: "text", text: "b" },
],
});
},
});
});
test("returns an image", async () => {
await runWithTestServer({
server: async () => {
const server = new FastMCP({
name: "Test",
version: "1.0.0",
});
server.addTool({
name: "add",
description: "Add two numbers",
parameters: z.object({
a: z.number(),
b: z.number(),
}),
execute: async () => {
return imageContent({
buffer: Buffer.from(
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=",
"base64",
),
});
},
});
return server;
},
run: async ({ client }) => {
expect(
await client.callTool({
name: "add",
arguments: {
a: 1,
b: 2,
},
}),
).toEqual({
content: [
{
type: "image",
data: "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=",
mimeType: "image/png",
},
],
});
},
});
});
test("handles UserError errors", async () => {
await runWithTestServer({
server: async () => {
const server = new FastMCP({
name: "Test",
version: "1.0.0",
});
server.addTool({
name: "add",
description: "Add two numbers",
parameters: z.object({
a: z.number(),
b: z.number(),
}),
execute: async () => {
throw new UserError("Something went wrong");
},
});
return server;
},
run: async ({ client }) => {
expect(
await client.callTool({
name: "add",
arguments: {
a: 1,
b: 2,
},
}),
).toEqual({
content: [{ type: "text", text: "Something went wrong" }],
isError: true,
});
},
});
});
test("calling an unknown tool throws McpError with MethodNotFound code", async () => {
await runWithTestServer({
server: async () => {
const server = new FastMCP({
name: "Test",
version: "1.0.0",
});
return server;
},
run: async ({ client }) => {
try {
await client.callTool({
name: "add",
arguments: {
a: 1,
b: 2,
},
});
} catch (error) {
expect(error).toBeInstanceOf(McpError);
// @ts-expect-error - we know that error is an McpError
expect(error.code).toBe(ErrorCode.MethodNotFound);
}
},
});
});
test("tracks tool progress", async () => {
await runWithTestServer({
server: async () => {
const server = new FastMCP({
name: "Test",
version: "1.0.0",
});
server.addTool({
name: "add",
description: "Add two numbers",
parameters: z.object({
a: z.number(),
b: z.number(),
}),
execute: async (args, { reportProgress }) => {
reportProgress({
progress: 0,
total: 10,
});
await delay(100);
return String(args.a + args.b);
},
});
return server;
},
run: async ({ client }) => {
const onProgress = vi.fn();
await client.callTool(
{
name: "add",
arguments: {
a: 1,
b: 2,
},
},
undefined,
{
onprogress: onProgress,
},
);
expect(onProgress).toHaveBeenCalledTimes(1);
expect(onProgress).toHaveBeenCalledWith({
progress: 0,
total: 10,
});
},
});
});
test("sets logging levels", async () => {
await runWithTestServer({
run: async ({ client, session }) => {
await client.setLoggingLevel("debug");
expect(session.loggingLevel).toBe("debug");
await client.setLoggingLevel("info");
expect(session.loggingLevel).toBe("info");
},
});
});
test("sends logging messages to the client", async () => {
await runWithTestServer({
server: async () => {
const server = new FastMCP({
name: "Test",
version: "1.0.0",
});
server.addTool({
name: "add",
description: "Add two numbers",
parameters: z.object({
a: z.number(),
b: z.number(),
}),
execute: async (args, { log }) => {
log.debug("debug message", {
foo: "bar",
});
log.error("error message");
log.info("info message");
log.warn("warn message");
return String(args.a + args.b);
},
});
return server;
},
run: async ({ client }) => {
const onLog = vi.fn();
client.setNotificationHandler(
LoggingMessageNotificationSchema,
(message) => {
if (message.method === "notifications/message") {
onLog({
level: message.params.level,
...(message.params.data ?? {}),
});
}
},
);
await client.callTool({
name: "add",
arguments: {
a: 1,
b: 2,
},
});
expect(onLog).toHaveBeenCalledTimes(4);
expect(onLog).toHaveBeenNthCalledWith(1, {
level: "debug",
message: "debug message",
context: {
foo: "bar",
},
});
expect(onLog).toHaveBeenNthCalledWith(2, {
level: "error",
message: "error message",
});
expect(onLog).toHaveBeenNthCalledWith(3, {
level: "info",
message: "info message",
});
expect(onLog).toHaveBeenNthCalledWith(4, {
level: "warning",
message: "warn message",
});
},
});
});
test("adds resources", async () => {
await runWithTestServer({
server: async () => {
const server = new FastMCP({
name: "Test",
version: "1.0.0",
});
server.addResource({
uri: "file:///logs/app.log",
name: "Application Logs",
mimeType: "text/plain",
async load() {
return {
text: "Example log content",
};
},
});
return server;
},
run: async ({ client }) => {
expect(await client.listResources()).toEqual({
resources: [
{
uri: "file:///logs/app.log",
name: "Application Logs",
mimeType: "text/plain",
},
],
});
},
});
});
test("clients reads a resource", async () => {
await runWithTestServer({
server: async () => {
const server = new FastMCP({
name: "Test",
version: "1.0.0",
});
server.addResource({
uri: "file:///logs/app.log",
name: "Application Logs",
mimeType: "text/plain",
async load() {
return {
text: "Example log content",
};
},
});
return server;
},
run: async ({ client }) => {
expect(
await client.readResource({
uri: "file:///logs/app.log",
}),
).toEqual({
contents: [
{
uri: "file:///logs/app.log",
name: "Application Logs",
text: "Example log content",
mimeType: "text/plain",
},
],
});
},
});
});
test("clients reads a resource that returns multiple resources", async () => {
await runWithTestServer({
server: async () => {
const server = new FastMCP({
name: "Test",
version: "1.0.0",
});
server.addResource({
uri: "file:///logs/app.log",
name: "Application Logs",
mimeType: "text/plain",
async load() {
return [
{
text: "a",
},
{
text: "b",
},
];
},
});
return server;
},
run: async ({ client }) => {
expect(
await client.readResource({
uri: "file:///logs/app.log",
}),
).toEqual({
contents: [
{
uri: "file:///logs/app.log",
name: "Application Logs",
text: "a",
mimeType: "text/plain",
},
{
uri: "file:///logs/app.log",
name: "Application Logs",
text: "b",
mimeType: "text/plain",
},
],
});
},
});
});
test("adds prompts", async () => {
await runWithTestServer({
server: async () => {
const server = new FastMCP({
name: "Test",
version: "1.0.0",
});
server.addPrompt({
name: "git-commit",
description: "Generate a Git commit message",
arguments: [
{
name: "changes",
description: "Git diff or description of changes",
required: true,
},
],
load: async (args) => {
return `Generate a concise but descriptive commit message for these changes:\n\n${args.changes}`;
},
});
return server;
},
run: async ({ client }) => {
expect(
await client.getPrompt({
name: "git-commit",
arguments: {
changes: "foo",
},
}),
).toEqual({
description: "Generate a Git commit message",
messages: [
{
role: "user",
content: {
type: "text",
text: "Generate a concise but descriptive commit message for these changes:\n\nfoo",
},
},
],
});
expect(await client.listPrompts()).toEqual({
prompts: [
{
name: "git-commit",
description: "Generate a Git commit message",
arguments: [
{
name: "changes",
description: "Git diff or description of changes",
required: true,
},
],
},
],
});
},
});
});
test("uses events to notify server of client connect/disconnect", async () => {
const port = await getRandomPort();
const server = new FastMCP({
name: "Test",
version: "1.0.0",
});
const onConnect = vi.fn();
const onDisconnect = vi.fn();
server.on("connect", onConnect);
server.on("disconnect", onDisconnect);
await server.start({
transportType: "sse",
sse: {
endpoint: "/sse",
port,
},
});
const client = new Client(
{
name: "example-client",
version: "1.0.0",
},
{
capabilities: {},
},
);
const transport = new SSEClientTransport(
new URL(`http://localhost:${port}/sse`),
);
await client.connect(transport);
await delay(100);
expect(onConnect).toHaveBeenCalledTimes(1);
expect(onDisconnect).toHaveBeenCalledTimes(0);
expect(server.sessions).toEqual([expect.any(FastMCPSession)]);
await client.close();
await delay(100);
expect(onConnect).toHaveBeenCalledTimes(1);
expect(onDisconnect).toHaveBeenCalledTimes(1);
await server.stop();
});
test("handles multiple clients", async () => {
const port = await getRandomPort();
const server = new FastMCP({
name: "Test",
version: "1.0.0",
});
await server.start({
transportType: "sse",
sse: {
endpoint: "/sse",
port,
},
});
const client1 = new Client(
{
name: "example-client",
version: "1.0.0",
},
{
capabilities: {},
},
);
const transport1 = new SSEClientTransport(
new URL(`http://localhost:${port}/sse`),
);
await client1.connect(transport1);
const client2 = new Client(
{
name: "example-client",
version: "1.0.0",
},
{
capabilities: {},
},
);
const transport2 = new SSEClientTransport(
new URL(`http://localhost:${port}/sse`),
);
await client2.connect(transport2);
await delay(100);
expect(server.sessions).toEqual([
expect.any(FastMCPSession),
expect.any(FastMCPSession),
]);
await server.stop();
});
test("session knows about client capabilities", async () => {
await runWithTestServer({
client: async () => {
const client = new Client(
{
name: "example-client",
version: "1.0.0",
},
{
capabilities: {
roots: {
listChanged: true,
},
},
},
);
client.setRequestHandler(ListRootsRequestSchema, () => {
return {
roots: [
{
uri: "file:///home/user/projects/frontend",
name: "Frontend Repository",
},
],
};
});
return client;
},
run: async ({ session }) => {
expect(session.clientCapabilities).toEqual({
roots: {
listChanged: true,
},
});
},
});
});
test("session knows about roots", async () => {
await runWithTestServer({
client: async () => {
const client = new Client(
{
name: "example-client",
version: "1.0.0",
},
{
capabilities: {
roots: {
listChanged: true,
},
},
},
);
client.setRequestHandler(ListRootsRequestSchema, () => {
return {
roots: [
{
uri: "file:///home/user/projects/frontend",
name: "Frontend Repository",
},
],
};
});
return client;
},
run: async ({ session }) => {
expect(session.roots).toEqual([
{
uri: "file:///home/user/projects/frontend",
name: "Frontend Repository",
},
]);
},
});
});
test("session listens to roots changes", async () => {
let clientRoots: Root[] = [
{
uri: "file:///home/user/projects/frontend",
name: "Frontend Repository",
},
];
await runWithTestServer({
client: async () => {
const client = new Client(
{
name: "example-client",
version: "1.0.0",
},
{
capabilities: {
roots: {
listChanged: true,
},
},
},
);
client.setRequestHandler(ListRootsRequestSchema, () => {
return {
roots: clientRoots,
};
});
return client;
},
run: async ({ session, client }) => {
expect(session.roots).toEqual([
{
uri: "file:///home/user/projects/frontend",
name: "Frontend Repository",
},
]);
clientRoots.push({
uri: "file:///home/user/projects/backend",
name: "Backend Repository",
});
await client.sendRootsListChanged();
const onRootsChanged = vi.fn();
session.on("rootsChanged", onRootsChanged);
await delay(100);
expect(session.roots).toEqual([
{
uri: "file:///home/user/projects/frontend",
name: "Frontend Repository",
},
{
uri: "file:///home/user/projects/backend",
name: "Backend Repository",
},
]);
expect(onRootsChanged).toHaveBeenCalledTimes(1);
expect(onRootsChanged).toHaveBeenCalledWith({
roots: [
{
uri: "file:///home/user/projects/frontend",
name: "Frontend Repository",
},
{
uri: "file:///home/user/projects/backend",
name: "Backend Repository",
},
],
});
},
});
});
test("session sends pings to the client", async () => {
await runWithTestServer({
run: async ({ client }) => {
const onPing = vi.fn().mockReturnValue({});
client.setRequestHandler(PingRequestSchema, onPing);
await delay(2000);
expect(onPing).toHaveBeenCalledTimes(1);
},
});
});
test("completes prompt arguments", async () => {
await runWithTestServer({
server: async () => {
const server = new FastMCP({
name: "Test",
version: "1.0.0",
});
server.addPrompt({
name: "countryPoem",
description: "Writes a poem about a country",
load: async ({ name }) => {
return `Hello, ${name}!`;
},
arguments: [
{
name: "name",
description: "Name of the country",
required: true,
complete: async (value) => {
if (value === "Germ") {
return {
values: ["Germany"],
};
}
return {
values: [],
};
},
},
],
});
return server;
},
run: async ({ client }) => {
const response = await client.complete({
ref: {
type: "ref/prompt",
name: "countryPoem",
},
argument: {
name: "name",
value: "Germ",
},
});
expect(response).toEqual({
completion: {
values: ["Germany"],
},
});
},
});
});
test("adds automatic prompt argument completion when enum is provided", async () => {
await runWithTestServer({
server: async () => {
const server = new FastMCP({
name: "Test",
version: "1.0.0",
});
server.addPrompt({
name: "countryPoem",
description: "Writes a poem about a country",
load: async ({ name }) => {
return `Hello, ${name}!`;
},
arguments: [
{
name: "name",
description: "Name of the country",
required: true,
enum: ["Germany", "France", "Italy"],
},
],
});
return server;
},
run: async ({ client }) => {
const response = await client.complete({
ref: {
type: "ref/prompt",
name: "countryPoem",
},
argument: {
name: "name",
value: "Germ",
},
});
expect(response).toEqual({
completion: {
values: ["Germany"],
total: 1,
},
});
},
});
});
test("completes template resource arguments", async () => {
await runWithTestServer({
server: async () => {
const server = new FastMCP({
name: "Test",
version: "1.0.0",
});
server.addResourceTemplate({
uriTemplate: "issue:///{issueId}",
name: "Issue",
mimeType: "text/plain",
arguments: [
{
name: "issueId",
description: "ID of the issue",
complete: async (value) => {
if (value === "123") {
return {
values: ["123456"],
};
}
return {
values: [],
};
},
},
],
load: async ({ issueId }) => {
return {
text: `Issue ${issueId}`,
};
},
});
return server;
},
run: async ({ client }) => {
const response = await client.complete({
ref: {
type: "ref/resource",
uri: "issue:///{issueId}",
},
argument: {
name: "issueId",
value: "123",
},
});
expect(response).toEqual({
completion: {
values: ["123456"],
},
});
},
});
});
test("lists resource templates", async () => {
await runWithTestServer({
server: async () => {
const server = new FastMCP({
name: "Test",
version: "1.0.0",
});
server.addResourceTemplate({
uriTemplate: "file:///logs/{name}.log",
name: "Application Logs",
mimeType: "text/plain",
arguments: [
{
name: "name",
description: "Name of the log",
required: true,
},
],
load: async ({ name }) => {
return {
text: `Example log content for ${name}`,
};
},
});
return server;
},
run: async ({ client }) => {
expect(await client.listResourceTemplates()).toEqual({
resourceTemplates: [
{
name: "Application Logs",
uriTemplate: "file:///logs/{name}.log",
},
],
});
},
});
});
test("clients reads a resource accessed via a resource template", async () => {
const loadSpy = vi.fn((_args) => {
return {
text: "Example log content",
};
});
await runWithTestServer({
server: async () => {
const server = new FastMCP({
name: "Test",
version: "1.0.0",
});
server.addResourceTemplate({
uriTemplate: "file:///logs/{name}.log",
name: "Application Logs",
mimeType: "text/plain",
arguments: [
{
name: "name",
description: "Name of the log",
},
],
async load(args) {
return loadSpy(args);
},
});
return server;
},
run: async ({ client }) => {
expect(
await client.readResource({
uri: "file:///logs/app.log",
}),
).toEqual({
contents: [
{
uri: "file:///logs/app.log",
name: "Application Logs",
text: "Example log content",
mimeType: "text/plain",
},
],
});
expect(loadSpy).toHaveBeenCalledWith({
name: "app",
});
},
});
});
test("makes a sampling request", async () => {
const onMessageRequest = vi.fn(() => {
return {
model: "gpt-3.5-turbo",
role: "assistant",
content: {
type: "text",
text: "The files are in the current directory.",
},
};
});
await runWithTestServer({
client: async () => {
const client = new Client(
{
name: "example-client",
version: "1.0.0",
},
{
capabilities: {
sampling: {},
},
},
);
return client;
},
run: async ({ client, session }) => {
client.setRequestHandler(CreateMessageRequestSchema, onMessageRequest);
const response = await session.requestSampling({
messages: [
{
role: "user",
content: {
type: "text",
text: "What files are in the current directory?",
},
},
],
systemPrompt: "You are a helpful file system assistant.",
includeContext: "thisServer",
maxTokens: 100,
});
expect(response).toEqual({
model: "gpt-3.5-turbo",
role: "assistant",
content: {
type: "text",
text: "The files are in the current directory.",
},
});
expect(onMessageRequest).toHaveBeenCalledTimes(1);
},
});
});
test("throws ErrorCode.InvalidParams if tool parameters do not match zod schema", async () => {
await runWithTestServer({
server: async () => {
const server = new FastMCP({
name: "Test",
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);
},
});
return server;
},
run: async ({ client }) => {
try {
await client.callTool({
name: "add",
arguments: {
a: 1,
b: "invalid",
},
});
} catch (error) {
expect(error).toBeInstanceOf(McpError);
// @ts-expect-error - we know that error is an McpError
expect(error.code).toBe(ErrorCode.InvalidParams);
// @ts-expect-error - we know that error is an McpError
expect(error.message).toBe("MCP error -32602: MCP error -32602: Invalid add parameters");
}
},
});
});
test("server remains usable after InvalidParams error", async () => {
await runWithTestServer({
server: async () => {
const server = new FastMCP({
name: "Test",
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);
},
});
return server;
},
run: async ({ client }) => {
try {
await client.callTool({
name: "add",
arguments: {
a: 1,
b: "invalid",
},
});
} catch (error) {
expect(error).toBeInstanceOf(McpError);
// @ts-expect-error - we know that error is an McpError
expect(error.code).toBe(ErrorCode.InvalidParams);
// @ts-expect-error - we know that error is an McpError
expect(error.message).toBe("MCP error -32602: MCP error -32602: Invalid add parameters");
}
expect(
await client.callTool({
name: "add",
arguments: {
a: 1,
b: 2,
},
}),
).toEqual({
content: [{ type: "text", text: "3" }],
});
},
});
});
test("allows new clients to connect after a client disconnects", async () => {
const port = await getRandomPort();
const server = new FastMCP({
name: "Test",
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);
},
});
await server.start({
transportType: "sse",
sse: {
endpoint: "/sse",
port,
},
});
const client1 = new Client(
{
name: "example-client",
version: "1.0.0",
},
{
capabilities: {},
},
);
const transport1 = new SSEClientTransport(
new URL(`http://localhost:${port}/sse`),
);
await client1.connect(transport1);
expect(
await client1.callTool({
name: "add",
arguments: {
a: 1,
b: 2,
},
}),
).toEqual({
content: [{ type: "text", text: "3" }],
});
await client1.close();
const client2 = new Client(
{
name: "example-client",
version: "1.0.0",
},
{
capabilities: {},
},
);
const transport2 = new SSEClientTransport(
new URL(`http://localhost:${port}/sse`),
);
await client2.connect(transport2);
expect(
await client2.callTool({
name: "add",
arguments: {
a: 1,
b: 2,
},
}),
).toEqual({
content: [{ type: "text", text: "3" }],
});
await client2.close();
await server.stop();
});
test("able to close server immediately after starting it", async () => {
const port = await getRandomPort();
const server = new FastMCP({
name: "Test",
version: "1.0.0",
});
await server.start({
transportType: "sse",
sse: {
endpoint: "/sse",
port,
},
});
// We were previously not waiting for the server to start.
// Therefore, this would have caused error 'Server is not running.'.
await server.stop();
});
test("closing event source does not produce error", async () => {
const port = await getRandomPort();
const server = new FastMCP({
name: "Test",
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);
},
});
await server.start({
transportType: "sse",
sse: {
endpoint: "/sse",
port,
},
});
const eventSource = await new Promise<EventSourceClient>((onMessage) => {
const eventSource = createEventSource({
onConnect: () => {
console.info('connected');
},
onDisconnect: () => {
console.info('disconnected');
},
onMessage: () => {
onMessage(eventSource);
},
url: `http://127.0.0.1:${port}/sse`,
});
});
expect(eventSource.readyState).toBe('open');
eventSource.close();
// We were getting unhandled error 'Not connected'
// https://github.com/punkpeye/mcp-proxy/commit/62cf27d5e3dfcbc353e8d03c7714a62c37177b52
await delay(1000);
await server.stop();
});
test("provides auth to tools", async () => {
const port = await getRandomPort();
const authenticate = vi.fn(async () => {
return {
id: 1,
};
});
const server = new FastMCP<{id: number}>({
name: "Test",
version: "1.0.0",
authenticate,
});
const execute = vi.fn(async (args) => {
return String(args.a + args.b);
});
server.addTool({
name: "add",
description: "Add two numbers",
parameters: z.object({
a: z.number(),
b: z.number(),
}),
execute,
});
await server.start({
transportType: "sse",
sse: {
endpoint: "/sse",
port,
},
});
const client = new Client(
{
name: "example-client",
version: "1.0.0",
},
{
capabilities: {},
},
);
const transport = new SSEClientTransport(
new URL(`http://localhost:${port}/sse`),
{
eventSourceInit: {
fetch: async (url, init) => {
return fetch(url, {
...init,
headers: {
...init?.headers,
"x-api-key": "123",
},
});
},
},
},
);
await client.connect(transport);
expect(authenticate, "authenticate should have been called").toHaveBeenCalledTimes(1);
expect(
await client.callTool({
name: "add",
arguments: {
a: 1,
b: 2,
},
}),
).toEqual({
content: [{ type: "text", text: "3" }],
});
expect(execute, "execute should have been called").toHaveBeenCalledTimes(1);
expect(execute).toHaveBeenCalledWith({
a: 1,
b: 2,
}, {
log: {
debug: expect.any(Function),
error: expect.any(Function),
info: expect.any(Function),
warn: expect.any(Function),
},
reportProgress: expect.any(Function),
session: { id: 1 },
});
});
test("blocks unauthorized requests", async () => {
const port = await getRandomPort();
const server = new FastMCP<{id: number}>({
name: "Test",
version: "1.0.0",
authenticate: async () => {
throw new Response(null, {
status: 401,
statusText: "Unauthorized",
});
},
});
await server.start({
transportType: "sse",
sse: {
endpoint: "/sse",
port,
},
});
const client = new Client(
{
name: "example-client",
version: "1.0.0",
},
{
capabilities: {},
},
);
const transport = new SSEClientTransport(
new URL(`http://localhost:${port}/sse`),
);
expect(async () => {
await client.connect(transport);
}).rejects.toThrow("SSE error: Non-200 status code (401)");
});
---
File: /src/FastMCP.ts
---
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequestSchema,
ClientCapabilities,
CompleteRequestSchema,
CreateMessageRequestSchema,
ErrorCode,
GetPromptRequestSchema,
ListPromptsRequestSchema,
ListResourcesRequestSchema,
ListResourceTemplatesRequestSchema,
ListToolsRequestSchema,
McpError,
ReadResourceRequestSchema,
Root,
RootsListChangedNotificationSchema,
ServerCapabilities,
SetLevelRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
import { zodToJsonSchema } from "zod-to-json-schema";
import { z } from "zod";
import { setTimeout as delay } from "timers/promises";
import { readFile } from "fs/promises";
import { fileTypeFromBuffer } from "file-type";
import { StrictEventEmitter } from "strict-event-emitter-types";
import { EventEmitter } from "events";
import Fuse from "fuse.js";
import { startSSEServer } from "mcp-proxy";
import { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
import parseURITemplate from "uri-templates";
import http from "http";
import {
fetch
} from "undici";
export type SSEServer = {
close: () => Promise<void>;
};
type FastMCPEvents<T extends FastMCPSessionAuth> = {
connect: (event: { session: FastMCPSession<T> }) => void;
disconnect: (event: { session: FastMCPSession<T> }) => void;
};
type FastMCPSessionEvents = {
rootsChanged: (event: { roots: Root[] }) => void;
error: (event: { error: Error }) => void;
};
/**
* Generates an image content object from a URL, file path, or buffer.
*/
export const imageContent = async (
input: { url: string } | { path: string } | { buffer: Buffer },
): Promise<ImageContent> => {
let rawData: Buffer;
if ("url" in input) {
const response = await fetch(input.url);
if (!response.ok) {
throw new Error(`Failed to fetch image from URL: ${response.statusText}`);
}
rawData = Buffer.from(await response.arrayBuffer());
} else if ("path" in input) {
rawData = await readFile(input.path);
} else if ("buffer" in input) {
rawData = input.buffer;
} else {
throw new Error(
"Invalid input: Provide a valid 'url', 'path', or 'buffer'",
);
}
const mimeType = await fileTypeFromBuffer(rawData);
const base64Data = rawData.toString("base64");
return {
type: "image",
data: base64Data,
mimeType: mimeType?.mime ?? "image/png",
} as const;
};
abstract class FastMCPError extends Error {
public constructor(message?: string) {
super(message);
this.name = new.target.name;
}
}
type Extra = unknown;
type Extras = Record<string, Extra>;
export class UnexpectedStateError extends FastMCPError {
public extras?: Extras;
public constructor(message: string, extras?: Extras) {
super(message);
this.name = new.target.name;
this.extras = extras;
}
}
/**
* An error that is meant to be surfaced to the user.
*/
export class UserError extends UnexpectedStateError {}
type ToolParameters = z.ZodTypeAny;
type Literal = boolean | null | number | string | undefined;
type SerializableValue =
| Literal
| SerializableValue[]
| { [key: string]: SerializableValue };
type Progress = {
/**
* The progress thus far. This should increase every time progress is made, even if the total is unknown.
*/
progress: number;
/**
* Total number of items to process (or total progress required), if known.
*/
total?: number;
};
type Context<T extends FastMCPSessionAuth> = {
session: T | undefined;
reportProgress: (progress: Progress) => Promise<void>;
log: {
debug: (message: string, data?: SerializableValue) => void;
error: (message: string, data?: SerializableValue) => void;
info: (message: string, data?: SerializableValue) => void;
warn: (message: string, data?: SerializableValue) => void;
};
};
type TextContent = {
type: "text";
text: string;
};
const TextContentZodSchema = z
.object({
type: z.literal("text"),
/**
* The text content of the message.
*/
text: z.string(),
})
.strict() satisfies z.ZodType<TextContent>;
type ImageContent = {
type: "image";
data: string;
mimeType: string;
};
const ImageContentZodSchema = z
.object({
type: z.literal("image"),
/**
* The base64-encoded image data.
*/
data: z.string().base64(),
/**
* The MIME type of the image. Different providers may support different image types.
*/
mimeType: z.string(),
})
.strict() satisfies z.ZodType<ImageContent>;
type Content = TextContent | ImageContent;
const ContentZodSchema = z.discriminatedUnion("type", [
TextContentZodSchema,
ImageContentZodSchema,
]) satisfies z.ZodType<Content>;
type ContentResult = {
content: Content[];
isError?: boolean;
};
const ContentResultZodSchema = z
.object({
content: ContentZodSchema.array(),
isError: z.boolean().optional(),
})
.strict() satisfies z.ZodType<ContentResult>;
type Completion = {
values: string[];
total?: number;
hasMore?: boolean;
};
/**
* https://github.com/modelcontextprotocol/typescript-sdk/blob/3164da64d085ec4e022ae881329eee7b72f208d4/src/types.ts#L983-L1003
*/
const CompletionZodSchema = z.object({
/**
* An array of completion values. Must not exceed 100 items.
*/
values: z.array(z.string()).max(100),
/**
* The total number of completion options available. This can exceed the number of values actually sent in the response.
*/
total: z.optional(z.number().int()),
/**
* Indicates whether there are additional completion options beyond those provided in the current response, even if the exact total is unknown.
*/
hasMore: z.optional(z.boolean()),
}) satisfies z.ZodType<Completion>;
type Tool<T extends FastMCPSessionAuth, Params extends ToolParameters = ToolParameters> = {
name: string;
description?: string;
parameters?: Params;
execute: (
args: z.infer<Params>,
context: Context<T>,
) => Promise<string | ContentResult | TextContent | ImageContent>;
};
type ResourceResult =
| {
text: string;
}
| {
blob: string;
};
type InputResourceTemplateArgument = Readonly<{
name: string;
description?: string;
complete?: ArgumentValueCompleter;
}>;
type ResourceTemplateArgument = Readonly<{
name: string;
description?: string;
complete?: ArgumentValueCompleter;
}>;
type ResourceTemplate<
Arguments extends ResourceTemplateArgument[] = ResourceTemplateArgument[],
> = {
uriTemplate: string;
name: string;
description?: string;
mimeType?: string;
arguments: Arguments;
complete?: (name: string, value: string) => Promise<Completion>;
load: (
args: ResourceTemplateArgumentsToObject<Arguments>,
) => Promise<ResourceResult>;
};
type ResourceTemplateArgumentsToObject<T extends { name: string }[]> = {
[K in T[number]["name"]]: string;
};
type InputResourceTemplate<
Arguments extends ResourceTemplateArgument[] = ResourceTemplateArgument[],
> = {
uriTemplate: string;
name: string;
description?: string;
mimeType?: string;
arguments: Arguments;
load: (
args: ResourceTemplateArgumentsToObject<Arguments>,
) => Promise<ResourceResult>;
};
type Resource = {
uri: string;
name: string;
description?: string;
mimeType?: string;
load: () => Promise<ResourceResult | ResourceResult[]>;
complete?: (name: string, value: string) => Promise<Completion>;
};
type ArgumentValueCompleter = (value: string) => Promise<Completion>;
type InputPromptArgument = Readonly<{
name: string;
description?: string;
required?: boolean;
complete?: ArgumentValueCompleter;
enum?: string[];
}>;
type PromptArgumentsToObject<T extends { name: string; required?: boolean }[]> =
{
[K in T[number]["name"]]: Extract<
T[number],
{ name: K }
>["required"] extends true
? string
: string | undefined;
};
type InputPrompt<
Arguments extends InputPromptArgument[] = InputPromptArgument[],
Args = PromptArgumentsToObject<Arguments>,
> = {
name: string;
description?: string;
arguments?: InputPromptArgument[];
load: (args: Args) => Promise<string>;
};
type PromptArgument = Readonly<{
name: string;
description?: string;
required?: boolean;
complete?: ArgumentValueCompleter;
enum?: string[];
}>;
type Prompt<
Arguments extends PromptArgument[] = PromptArgument[],
Args = PromptArgumentsToObject<Arguments>,
> = {
arguments?: PromptArgument[];
complete?: (name: string, value: string) => Promise<Completion>;
description?: string;
load: (args: Args) => Promise<string>;
name: string;
};
type ServerOptions<T extends FastMCPSessionAuth> = {
name: string;
version: `${number}.${number}.${number}`;
authenticate?: Authenticate<T>;
};
type LoggingLevel =
| "debug"
| "info"
| "notice"
| "warning"
| "error"
| "critical"
| "alert"
| "emergency";
const FastMCPSessionEventEmitterBase: {
new (): StrictEventEmitter<EventEmitter, FastMCPSessionEvents>;
} = EventEmitter;
class FastMCPSessionEventEmitter extends FastMCPSessionEventEmitterBase {}
type SamplingResponse = {
model: string;
stopReason?: "endTurn" | "stopSequence" | "maxTokens" | string;
role: "user" | "assistant";
content: TextContent | ImageContent;
};
type FastMCPSessionAuth = Record<string, unknown> | undefined;
export class FastMCPSession<T extends FastMCPSessionAuth = FastMCPSessionAuth> extends FastMCPSessionEventEmitter {
#capabilities: ServerCapabilities = {};
#clientCapabilities?: ClientCapabilities;
#loggingLevel: LoggingLevel = "info";
#prompts: Prompt[] = [];
#resources: Resource[] = [];
#resourceTemplates: ResourceTemplate[] = [];
#roots: Root[] = [];
#server: Server;
#auth: T | undefined;
constructor({
auth,
name,
version,
tools,
resources,
resourcesTemplates,
prompts,
}: {
auth?: T;
name: string;
version: string;
tools: Tool<T>[];
resources: Resource[];
resourcesTemplates: InputResourceTemplate[];
prompts: Prompt[];
}) {
super();
this.#auth = auth;
if (tools.length) {
this.#capabilities.tools = {};
}
if (resources.length || resourcesTemplates.length) {
this.#capabilities.resources = {};
}
if (prompts.length) {
for (const prompt of prompts) {
this.addPrompt(prompt);
}
this.#capabilities.prompts = {};
}
this.#capabilities.logging = {};
this.#server = new Server(
{ name: name, version: version },
{ capabilities: this.#capabilities },
);
this.setupErrorHandling();
this.setupLoggingHandlers();
this.setupRootsHandlers();
this.setupCompleteHandlers();
if (tools.length) {
this.setupToolHandlers(tools);
}
if (resources.length || resourcesTemplates.length) {
for (const resource of resources) {
this.addResource(resource);
}
this.setupResourceHandlers(resources);
if (resourcesTemplates.length) {
for (const resourceTemplate of resourcesTemplates) {
this.addResourceTemplate(resourceTemplate);
}
this.setupResourceTemplateHandlers(resourcesTemplates);
}
}
if (prompts.length) {
this.setupPromptHandlers(prompts);
}
}
private addResource(inputResource: Resource) {
this.#resources.push(inputResource);
}
private addResourceTemplate(inputResourceTemplate: InputResourceTemplate) {
const completers: Record<string, ArgumentValueCompleter> = {};
for (const argument of inputResourceTemplate.arguments ?? []) {
if (argument.complete) {
completers[argument.name] = argument.complete;
}
}
const resourceTemplate = {
...inputResourceTemplate,
complete: async (name: string, value: string) => {
if (completers[name]) {
return await completers[name](value);
}
return {
values: [],
};
},
};
this.#resourceTemplates.push(resourceTemplate);
}
private addPrompt(inputPrompt: InputPrompt) {
const completers: Record<string, ArgumentValueCompleter> = {};
const enums: Record<string, string[]> = {};
for (const argument of inputPrompt.arguments ?? []) {
if (argument.complete) {
completers[argument.name] = argument.complete;
}
if (argument.enum) {
enums[argument.name] = argument.enum;
}
}
const prompt = {
...inputPrompt,
complete: async (name: string, value: string) => {
if (completers[name]) {
return await completers[name](value);
}
if (enums[name]) {
const fuse = new Fuse(enums[name], {
keys: ["value"],
});
const result = fuse.search(value);
return {
values: result.map((item) => item.item),
total: result.length,
};
}
return {
values: [],
};
},
};
this.#prompts.push(prompt);
}
public get clientCapabilities(): ClientCapabilities | null {
return this.#clientCapabilities ?? null;
}
public get server(): Server {
return this.#server;
}
#pingInterval: ReturnType<typeof setInterval> | null = null;
public async requestSampling(
message: z.infer<typeof CreateMessageRequestSchema>["params"],
): Promise<SamplingResponse> {
return this.#server.createMessage(message);
}
public async connect(transport: Transport) {
if (this.#server.transport) {
throw new UnexpectedStateError("Server is already connected");
}
await this.#server.connect(transport);
let attempt = 0;
while (attempt++ < 10) {
const capabilities = await this.#server.getClientCapabilities();
if (capabilities) {
this.#clientCapabilities = capabilities;
break;
}
await delay(100);
}
if (!this.#clientCapabilities) {
console.warn('[warning] FastMCP could not infer client capabilities')
}
if (this.#clientCapabilities?.roots?.listChanged) {
try {
const roots = await this.#server.listRoots();
this.#roots = roots.roots;
} catch(e) {
console.error(`[error] FastMCP received error listing roots.\n\n${e instanceof Error ? e.stack : JSON.stringify(e)}`)
}
}
this.#pingInterval = setInterval(async () => {
try {
await this.#server.ping();
} catch (error) {
this.emit("error", {
error: error as Error,
});
}
}, 1000);
}
public get roots(): Root[] {
return this.#roots;
}
public async close() {
if (this.#pingInterval) {
clearInterval(this.#pingInterval);
}
try {
await this.#server.close();
} catch (error) {
console.error("[MCP Error]", "could not close server", error);
}
}
private setupErrorHandling() {
this.#server.onerror = (error) => {
console.error("[MCP Error]", error);
};
}
public get loggingLevel(): LoggingLevel {
return this.#loggingLevel;
}
private setupCompleteHandlers() {
this.#server.setRequestHandler(CompleteRequestSchema, async (request) => {
if (request.params.ref.type === "ref/prompt") {
const prompt = this.#prompts.find(
(prompt) => prompt.name === request.params.ref.name,
);
if (!prompt) {
throw new UnexpectedStateError("Unknown prompt", {
request,
});
}
if (!prompt.complete) {
throw new UnexpectedStateError("Prompt does not support completion", {
request,
});
}
const completion = CompletionZodSchema.parse(
await prompt.complete(
request.params.argument.name,
request.params.argument.value,
),
);
return {
completion,
};
}
if (request.params.ref.type === "ref/resource") {
const resource = this.#resourceTemplates.find(
(resource) => resource.uriTemplate === request.params.ref.uri,
);
if (!resource) {
throw new UnexpectedStateError("Unknown resource", {
request,
});
}
if (!("uriTemplate" in resource)) {
throw new UnexpectedStateError("Unexpected resource");
}
if (!resource.complete) {
throw new UnexpectedStateError(
"Resource does not support completion",
{
request,
},
);
}
const completion = CompletionZodSchema.parse(
await resource.complete(
request.params.argument.name,
request.params.argument.value,
),
);
return {
completion,
};
}
throw new UnexpectedStateError("Unexpected completion request", {
request,
});
});
}
private setupRootsHandlers() {
this.#server.setNotificationHandler(
RootsListChangedNotificationSchema,
() => {
this.#server.listRoots().then((roots) => {
this.#roots = roots.roots;
this.emit("rootsChanged", {
roots: roots.roots,
});
});
},
);
}
private setupLoggingHandlers() {
this.#server.setRequestHandler(SetLevelRequestSchema, (request) => {
this.#loggingLevel = request.params.level;
return {};
});
}
private setupToolHandlers(tools: Tool<T>[]) {
this.#server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: tools.map((tool) => {
return {
name: tool.name,
description: tool.description,
inputSchema: tool.parameters
? zodToJsonSchema(tool.parameters)
: undefined,
};
}),
};
});
this.#server.setRequestHandler(CallToolRequestSchema, async (request) => {
const tool = tools.find((tool) => tool.name === request.params.name);
if (!tool) {
throw new McpError(
ErrorCode.MethodNotFound,
`Unknown tool: ${request.params.name}`,
);
}
let args: any = undefined;
if (tool.parameters) {
const parsed = tool.parameters.safeParse(request.params.arguments);
if (!parsed.success) {
throw new McpError(
ErrorCode.InvalidParams,
`Invalid ${request.params.name} parameters`,
);
}
args = parsed.data;
}
const progressToken = request.params?._meta?.progressToken;
let result: ContentResult;
try {
const reportProgress = async (progress: Progress) => {
await this.#server.notification({
method: "notifications/progress",
params: {
...progress,
progressToken,
},
});
};
const log = {
debug: (message: string, context?: SerializableValue) => {
this.#server.sendLoggingMessage({
level: "debug",
data: {
message,
context,
},
});
},
error: (message: string, context?: SerializableValue) => {
this.#server.sendLoggingMessage({
level: "error",
data: {
message,
context,
},
});
},
info: (message: string, context?: SerializableValue) => {
this.#server.sendLoggingMessage({
level: "info",
data: {
message,
context,
},
});
},
warn: (message: string, context?: SerializableValue) => {
this.#server.sendLoggingMessage({
level: "warning",
data: {
message,
context,
},
});
},
};
const maybeStringResult = await tool.execute(args, {
reportProgress,
log,
session: this.#auth,
});
if (typeof maybeStringResult === "string") {
result = ContentResultZodSchema.parse({
content: [{ type: "text", text: maybeStringResult }],
});
} else if ("type" in maybeStringResult) {
result = ContentResultZodSchema.parse({
content: [maybeStringResult],
});
} else {
result = ContentResultZodSchema.parse(maybeStringResult);
}
} catch (error) {
if (error instanceof UserError) {
return {
content: [{ type: "text", text: error.message }],
isError: true,
};
}
return {
content: [{ type: "text", text: `Error: ${error}` }],
isError: true,
};
}
return result;
});
}
private setupResourceHandlers(resources: Resource[]) {
this.#server.setRequestHandler(ListResourcesRequestSchema, async () => {
return {
resources: resources.map((resource) => {
return {
uri: resource.uri,
name: resource.name,
mimeType: resource.mimeType,
};
}),
};
});
this.#server.setRequestHandler(
ReadResourceRequestSchema,
async (request) => {
if ("uri" in request.params) {
const resource = resources.find(
(resource) =>
"uri" in resource && resource.uri === request.params.uri,
);
if (!resource) {
for (const resourceTemplate of this.#resourceTemplates) {
const uriTemplate = parseURITemplate(
resourceTemplate.uriTemplate,
);
const match = uriTemplate.fromUri(request.params.uri);
if (!match) {
continue;
}
const uri = uriTemplate.fill(match);
const result = await resourceTemplate.load(match);
return {
contents: [
{
uri: uri,
mimeType: resourceTemplate.mimeType,
name: resourceTemplate.name,
...result,
},
],
};
}
throw new McpError(
ErrorCode.MethodNotFound,
`Unknown resource: ${request.params.uri}`,
);
}
if (!("uri" in resource)) {
throw new UnexpectedStateError("Resource does not support reading");
}
let maybeArrayResult: Awaited<ReturnType<Resource["load"]>>;
try {
maybeArrayResult = await resource.load();
} catch (error) {
throw new McpError(
ErrorCode.InternalError,
`Error reading resource: ${error}`,
{
uri: resource.uri,
},
);
}
if (Array.isArray(maybeArrayResult)) {
return {
contents: maybeArrayResult.map((result) => ({
uri: resource.uri,
mimeType: resource.mimeType,
name: resource.name,
...result,
})),
};
} else {
return {
contents: [
{
uri: resource.uri,
mimeType: resource.mimeType,
name: resource.name,
...maybeArrayResult,
},
],
};
}
}
throw new UnexpectedStateError("Unknown resource request", {
request,
});
},
);
}
private setupResourceTemplateHandlers(resourceTemplates: ResourceTemplate[]) {
this.#server.setRequestHandler(
ListResourceTemplatesRequestSchema,
async () => {
return {
resourceTemplates: resourceTemplates.map((resourceTemplate) => {
return {
name: resourceTemplate.name,
uriTemplate: resourceTemplate.uriTemplate,
};
}),
};
},
);
}
private setupPromptHandlers(prompts: Prompt[]) {
this.#server.setRequestHandler(ListPromptsRequestSchema, async () => {
return {
prompts: prompts.map((prompt) => {
return {
name: prompt.name,
description: prompt.description,
arguments: prompt.arguments,
complete: prompt.complete,
};
}),
};
});
this.#server.setRequestHandler(GetPromptRequestSchema, async (request) => {
const prompt = prompts.find(
(prompt) => prompt.name === request.params.name,
);
if (!prompt) {
throw new McpError(
ErrorCode.MethodNotFound,
`Unknown prompt: ${request.params.name}`,
);
}
const args = request.params.arguments;
for (const arg of prompt.arguments ?? []) {
if (arg.required && !(args && arg.name in args)) {
throw new McpError(
ErrorCode.InvalidRequest,
`Missing required argument: ${arg.name}`,
);
}
}
let result: Awaited<ReturnType<Prompt["load"]>>;
try {
result = await prompt.load(args as Record<string, string | undefined>);
} catch (error) {
throw new McpError(
ErrorCode.InternalError,
`Error loading prompt: ${error}`,
);
}
return {
description: prompt.description,
messages: [
{
role: "user",
content: { type: "text", text: result },
},
],
};
});
}
}
const FastMCPEventEmitterBase: {
new (): StrictEventEmitter<EventEmitter, FastMCPEvents<FastMCPSessionAuth>>;
} = EventEmitter;
class FastMCPEventEmitter extends FastMCPEventEmitterBase {}
type Authenticate<T> = (request: http.IncomingMessage) => Promise<T>;
export class FastMCP<T extends Record<string, unknown> | undefined = undefined> extends FastMCPEventEmitter {
#options: ServerOptions<T>;
#prompts: InputPrompt[] = [];
#resources: Resource[] = [];
#resourcesTemplates: InputResourceTemplate[] = [];
#sessions: FastMCPSession<T>[] = [];
#sseServer: SSEServer | null = null;
#tools: Tool<T>[] = [];
#authenticate: Authenticate<T> | undefined;
constructor(public options: ServerOptions<T>) {
super();
this.#options = options;
this.#authenticate = options.authenticate;
}
public get sessions(): FastMCPSession<T>[] {
return this.#sessions;
}
/**
* Adds a tool to the server.
*/
public addTool<Params extends ToolParameters>(tool: Tool<T, Params>) {
this.#tools.push(tool as unknown as Tool<T>);
}
/**
* Adds a resource to the server.
*/
public addResource(resource: Resource) {
this.#resources.push(resource);
}
/**
* Adds a resource template to the server.
*/
public addResourceTemplate<
const Args extends InputResourceTemplateArgument[],
>(resource: InputResourceTemplate<Args>) {
this.#resourcesTemplates.push(resource);
}
/**
* Adds a prompt to the server.
*/
public addPrompt<const Args extends InputPromptArgument[]>(
prompt: InputPrompt<Args>,
) {
this.#prompts.push(prompt);
}
/**
* Starts the server.
*/
public async start(
options:
| { transportType: "stdio" }
| {
transportType: "sse";
sse: { endpoint: `/${string}`; port: number };
} = {
transportType: "stdio",
},
) {
if (options.transportType === "stdio") {
const transport = new StdioServerTransport();
const session = new FastMCPSession<T>({
name: this.#options.name,
version: this.#options.version,
tools: this.#tools,
resources: this.#resources,
resourcesTemplates: this.#resourcesTemplates,
prompts: this.#prompts,
});
await session.connect(transport);
this.#sessions.push(session);
this.emit("connect", {
session,
});
} else if (options.transportType === "sse") {
this.#sseServer = await startSSEServer<FastMCPSession<T>>({
endpoint: options.sse.endpoint as `/${string}`,
port: options.sse.port,
createServer: async (request) => {
let auth: T | undefined;
if (this.#authenticate) {
auth = await this.#authenticate(request);
}
return new FastMCPSession<T>({
auth,
name: this.#options.name,
version: this.#options.version,
tools: this.#tools,
resources: this.#resources,
resourcesTemplates: this.#resourcesTemplates,
prompts: this.#prompts,
});
},
onClose: (session) => {
this.emit("disconnect", {
session,
});
},
onConnect: async (session) => {
this.#sessions.push(session);
this.emit("connect", {
session,
});
},
});
console.info(
`server is running on SSE at http://localhost:${options.sse.port}${options.sse.endpoint}`,
);
} else {
throw new Error("Invalid transport type");
}
}
/**
* Stops the server.
*/
public async stop() {
if (this.#sseServer) {
this.#sseServer.close();
}
}
}
export type { Context };
export type { Tool, ToolParameters };
export type { Content, TextContent, ImageContent, ContentResult };
export type { Progress, SerializableValue };
export type { Resource, ResourceResult };
export type { ResourceTemplate, ResourceTemplateArgument };
export type { Prompt, PromptArgument };
export type { InputPrompt, InputPromptArgument };
export type { ServerOptions, LoggingLevel };
export type { FastMCPEvents, FastMCPSessionEvents };
---
File: /eslint.config.js
---
import perfectionist from "eslint-plugin-perfectionist";
export default [perfectionist.configs["recommended-alphabetical"]];
---
File: /package.json
---
{
"name": "fastmcp",
"version": "1.0.0",
"main": "dist/FastMCP.js",
"scripts": {
"build": "tsup",
"test": "vitest run && tsc && jsr publish --dry-run",
"format": "prettier --write . && eslint --fix ."
},
"bin": {
"fastmcp": "dist/bin/fastmcp.js"
},
"keywords": [
"MCP",
"SSE"
],
"type": "module",
"author": "Frank Fiegel <frank@glama.ai>",
"license": "MIT",
"description": "A TypeScript framework for building MCP servers.",
"module": "dist/FastMCP.js",
"types": "dist/FastMCP.d.ts",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.6.0",
"execa": "^9.5.2",
"file-type": "^20.3.0",
"fuse.js": "^7.1.0",
"mcp-proxy": "^2.10.4",
"strict-event-emitter-types": "^2.0.0",
"undici": "^7.4.0",
"uri-templates": "^0.2.0",
"yargs": "^17.7.2",
"zod": "^3.24.2",
"zod-to-json-schema": "^3.24.3"
},
"repository": {
"url": "https://github.com/punkpeye/fastmcp"
},
"homepage": "https://glama.ai/mcp",
"release": {
"branches": [
"main"
],
"plugins": [
"@semantic-release/commit-analyzer",
"@semantic-release/release-notes-generator",
"@semantic-release/npm",
"@semantic-release/github",
"@sebbo2002/semantic-release-jsr"
]
},
"devDependencies": {
"@sebbo2002/semantic-release-jsr": "^2.0.4",
"@tsconfig/node22": "^22.0.0",
"@types/node": "^22.13.5",
"@types/uri-templates": "^0.1.34",
"@types/yargs": "^17.0.33",
"eslint": "^9.21.0",
"eslint-plugin-perfectionist": "^4.9.0",
"eventsource-client": "^1.1.3",
"get-port-please": "^3.1.2",
"jsr": "^0.13.3",
"prettier": "^3.5.2",
"semantic-release": "^24.2.3",
"tsup": "^8.4.0",
"typescript": "^5.7.3",
"vitest": "^3.0.7"
},
"tsup": {
"entry": [
"src/FastMCP.ts",
"src/bin/fastmcp.ts"
],
"format": [
"esm"
],
"dts": true,
"splitting": true,
"sourcemap": true,
"clean": true
}
}
---
File: /README.md
---
# 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)
- [Sessions](#sessions)
- [Image content](#returning-an-image)
- [Logging](#logging)
- [Error handling](#errors)
- [SSE](#sse)
- CORS (enabled by default)
- [Progress notifications](#progress)
- [Typed server events](#typed-server-events)
- [Prompt argument auto-completion](#prompt-argument-auto-completion)
- [Sampling](#requestsampling)
- Automated SSE pings
- Roots
- CLI for [testing](#test-with-mcp-cli) and [debugging](#inspect-with-mcp-inspector)
## Installation
```bash
npm install fastmcp
```
## Quickstart
```ts
import { FastMCP } from "fastmcp";
import { z } from "zod";
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
npm install
# 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
```
### SSE
You can also run the server with SSE support:
```ts
server.start({
transportType: "sse",
sse: {
endpoint: "/sse",
port: 8080,
},
});
```
This will start the server and listen for SSE connections on `http://localhost:8080/sse`.
You can then use `SSEClientTransport` to connect to the server:
```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);
```
## 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.
```js
server.addTool({
name: "fetch",
description: "Fetch the content of a url",
parameters: z.object({
url: z.string(),
}),
execute: async (args) => {
return await fetchWebpageContent(args.url);
},
});
```
#### 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",
},
],
};
},
});
```
#### 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 }) => {
reportProgress({
progress: 0,
total: 100,
});
// ...
reportProgress({
progress: 100,
total: 100,
});
return "done";
},
});
```
### 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'
};
}
```
### 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}`,
};
},
});
```
#### 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}`,
};
},
});
```
### 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",
description: "Git diff or description of changes",
required: true,
},
],
load: async (args) => {
return `Generate a concise but descriptive commit message for these changes:\n\n${args.changes}`;
},
});
```
#### Prompt argument auto-completion
Prompts can provide auto-completion for their arguments:
```js
server.addPrompt({
name: "countryPoem",
description: "Writes a poem about a country",
load: async ({ name }) => {
return `Hello, ${name}!`;
},
arguments: [
{
name: "name",
description: "Name of the country",
required: true,
complete: async (value) => {
if (value === "Germ") {
return {
values: ["Germany"],
};
}
return {
values: [],
};
},
},
],
});
```
#### Prompt argument auto-completion using `enum`
If you provide an `enum` array for an argument, the server will automatically provide completions for the argument.
```js
server.addPrompt({
name: "countryPoem",
description: "Writes a poem about a country",
load: async ({ name }) => {
return `Hello, ${name}!`;
},
arguments: [
{
name: "name",
description: "Name of the country",
required: true,
enum: ["Germany", "France", "Italy"],
},
],
});
```
### Authentication
FastMCP allows you to `authenticate` clients using a custom function:
```ts
import { AuthError } from "fastmcp";
const server = new FastMCP({
name: "My Server",
version: "1.0.0",
authenticate: ({request}) => {
const apiKey = request.headers["x-api-key"];
if (apiKey !== '123') {
throw new Response(null, {
status: 401,
statusText: "Unauthorized",
});
}
// Whatever you return here will be accessible in the `context.session` object.
return {
id: 1,
}
},
});
```
Now you can access the authenticated session data in your tools:
```ts
server.addTool({
name: "sayHello",
execute: async (args, { session }) => {
return `Hello, ${session.id}!`;
},
});
```
### Sessions
The `session` object is an instance of `FastMCPSession` and it describes active client sessions.
```ts
server.sessions;
```
We allocate a new server instance for each client connection to enable 1:1 communication between a client and the server.
### Typed server events
You can listen to events emitted by the server using the `on` method:
```ts
server.on("connect", (event) => {
console.log("Client connected:", event.session);
});
server.on("disconnect", (event) => {
console.log("Client disconnected:", event.session);
});
```
## `FastMCPSession`
`FastMCPSession` represents a client session and provides methods to interact with the client.
Refer to [Sessions](#sessions) for examples of how to obtain a `FastMCPSession` instance.
### `requestSampling`
`requestSampling` creates a [sampling](https://modelcontextprotocol.io/docs/concepts/sampling) request and returns the response.
```ts
await session.requestSampling({
messages: [
{
role: "user",
content: {
type: "text",
text: "What files are in the current directory?",
},
},
],
systemPrompt: "You are a helpful file system assistant.",
includeContext: "thisServer",
maxTokens: 100,
});
```
### `clientCapabilities`
The `clientCapabilities` property contains the client capabilities.
```ts
session.clientCapabilities;
```
### `loggingLevel`
The `loggingLevel` property describes the logging level as set by the client.
```ts
session.loggingLevel;
```
### `roots`
The `roots` property contains the roots as set by the client.
```ts
session.roots;
```
### `server`
The `server` property contains an instance of MCP server that is associated with the session.
```ts
session.server;
```
### Typed session events
You can listen to events emitted by the session using the `on` method:
```ts
session.on("rootsChanged", (event) => {
console.log("Roots changed:", event.roots);
});
session.on("error", (event) => {
console.error("Error:", event.error);
});
```
## Running Your Server
### Test with `mcp-cli`
The fastest way to test and debug your server is with `fastmcp dev`:
```bash
npx fastmcp dev server.js
npx fastmcp dev server.ts
```
This will run your server with [`mcp-cli`](https://github.com/wong2/mcp-cli) for testing and debugging your MCP server in the terminal.
### Inspect with `MCP Inspector`
Another way is to use the official [`MCP Inspector`](https://modelcontextprotocol.io/docs/tools/inspector) to inspect your server with a Web UI:
```bash
npx fastmcp inspect server.ts
```
## FAQ
### How to use with Claude Desktop?
Follow the guide https://modelcontextprotocol.io/quickstart/user and add the following configuration:
```json
{
"mcpServers": {
"my-mcp-server": {
"command": "npx",
"args": [
"tsx",
"/PATH/TO/YOUR_PROJECT/src/index.ts"
],
"env": {
"YOUR_ENV_VAR": "value"
}
}
}
}
```
## Showcase
> [!NOTE]
>
> If you've developed a server using FastMCP, please [submit a PR](https://github.com/punkpeye/fastmcp) to showcase it here!
- https://github.com/apinetwork/piapi-mcp-server
- https://github.com/Meeting-Baas/meeting-mcp - Meeting BaaS MCP server that enables AI assistants to create meeting bots, search transcripts, and manage recording data
## Acknowledgements
- FastMCP is inspired by the [Python implementation](https://github.com/jlowin/fastmcp) by [Jonathan Lowin](https://github.com/jlowin).
- Parts of codebase were adopted from [LiteMCP](https://github.com/wong2/litemcp).
- Parts of codebase were adopted from [Model Context protocolでSSEをやってみる](https://dev.classmethod.jp/articles/mcp-sse/).
---
File: /vitest.config.js
---
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
poolOptions: {
forks: { execArgv: ["--experimental-eventsource"] },
},
},
});