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; }; type FastMCPEvents = { connect: (event: { session: FastMCPSession }) => void; disconnect: (event: { session: FastMCPSession }) => 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 => { 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; 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 = { session: T | undefined; reportProgress: (progress: Progress) => Promise; 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; 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; type Content = TextContent | ImageContent; const ContentZodSchema = z.discriminatedUnion("type", [ TextContentZodSchema, ImageContentZodSchema, ]) satisfies z.ZodType; type ContentResult = { content: Content[]; isError?: boolean; }; const ContentResultZodSchema = z .object({ content: ContentZodSchema.array(), isError: z.boolean().optional(), }) .strict() satisfies z.ZodType; 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; type Tool = { name: string; description?: string; parameters?: Params; execute: ( args: z.infer, context: Context, ) => Promise; }; 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; load: ( args: ResourceTemplateArgumentsToObject, ) => Promise; }; type ResourceTemplateArgumentsToObject = { [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, ) => Promise; }; type Resource = { uri: string; name: string; description?: string; mimeType?: string; load: () => Promise; complete?: (name: string, value: string) => Promise; }; type ArgumentValueCompleter = (value: string) => Promise; type InputPromptArgument = Readonly<{ name: string; description?: string; required?: boolean; complete?: ArgumentValueCompleter; enum?: string[]; }>; type PromptArgumentsToObject = { [K in T[number]["name"]]: Extract< T[number], { name: K } >["required"] extends true ? string : string | undefined; }; type InputPrompt< Arguments extends InputPromptArgument[] = InputPromptArgument[], Args = PromptArgumentsToObject, > = { name: string; description?: string; arguments?: InputPromptArgument[]; load: (args: Args) => Promise; }; type PromptArgument = Readonly<{ name: string; description?: string; required?: boolean; complete?: ArgumentValueCompleter; enum?: string[]; }>; type Prompt< Arguments extends PromptArgument[] = PromptArgument[], Args = PromptArgumentsToObject, > = { arguments?: PromptArgument[]; complete?: (name: string, value: string) => Promise; description?: string; load: (args: Args) => Promise; name: string; }; type ServerOptions = { name: string; version: `${number}.${number}.${number}`; authenticate?: Authenticate; }; type LoggingLevel = | "debug" | "info" | "notice" | "warning" | "error" | "critical" | "alert" | "emergency"; const FastMCPSessionEventEmitterBase: { new (): StrictEventEmitter; } = EventEmitter; class FastMCPSessionEventEmitter extends FastMCPSessionEventEmitterBase {} type SamplingResponse = { model: string; stopReason?: "endTurn" | "stopSequence" | "maxTokens" | string; role: "user" | "assistant"; content: TextContent | ImageContent; }; type FastMCPSessionAuth = Record | undefined; export class FastMCPSession 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[]; 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 = {}; 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 = {}; const enums: Record = {}; 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 | null = null; public async requestSampling( message: z.infer["params"], ): Promise { 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[]) { 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>; 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>; try { result = await prompt.load(args as Record); } 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; class FastMCPEventEmitter extends FastMCPEventEmitterBase {} type Authenticate = (request: http.IncomingMessage) => Promise; export class FastMCP | undefined = undefined> extends FastMCPEventEmitter { #options: ServerOptions; #prompts: InputPrompt[] = []; #resources: Resource[] = []; #resourcesTemplates: InputResourceTemplate[] = []; #sessions: FastMCPSession[] = []; #sseServer: SSEServer | null = null; #tools: Tool[] = []; #authenticate: Authenticate | undefined; constructor(public options: ServerOptions) { super(); this.#options = options; this.#authenticate = options.authenticate; } public get sessions(): FastMCPSession[] { return this.#sessions; } /** * Adds a tool to the server. */ public addTool(tool: Tool) { this.#tools.push(tool as unknown as Tool); } /** * 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) { this.#resourcesTemplates.push(resource); } /** * Adds a prompt to the server. */ public addPrompt( prompt: InputPrompt, ) { 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({ 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>({ 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({ 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 };