// src/FastMCP.ts
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequestSchema,
CompleteRequestSchema,
ErrorCode,
GetPromptRequestSchema,
ListPromptsRequestSchema,
ListResourcesRequestSchema,
ListResourceTemplatesRequestSchema,
ListToolsRequestSchema,
McpError,
ReadResourceRequestSchema,
RootsListChangedNotificationSchema,
SetLevelRequestSchema
} from "@modelcontextprotocol/sdk/types.js";
import { EventEmitter } from "events";
import { fileTypeFromBuffer } from "file-type";
import { readFile } from "fs/promises";
import Fuse from "fuse.js";
import { startHTTPStreamServer, startSSEServer } from "mcp-proxy";
import { setTimeout as delay } from "timers/promises";
import { fetch } from "undici";
import parseURITemplate from "uri-templates";
import { toJsonSchema } from "xsschema";
import { z } from "zod";
var imageContent = async (input) => {
let rawData;
try {
if ("url" in input) {
try {
const response = await fetch(input.url);
if (!response.ok) {
throw new Error(
`Server responded with status: ${response.status} - ${response.statusText}`
);
}
rawData = Buffer.from(await response.arrayBuffer());
} catch (error) {
throw new Error(
`Failed to fetch image from URL (${input.url}): ${error instanceof Error ? error.message : String(error)}`
);
}
} else if ("path" in input) {
try {
rawData = await readFile(input.path);
} catch (error) {
throw new Error(
`Failed to read image from path (${input.path}): ${error instanceof Error ? error.message : String(error)}`
);
}
} 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);
if (!mimeType || !mimeType.mime.startsWith("image/")) {
console.warn(
`Warning: Content may not be a valid image. Detected MIME: ${mimeType?.mime || "unknown"}`
);
}
const base64Data = rawData.toString("base64");
return {
data: base64Data,
mimeType: mimeType?.mime ?? "image/png",
type: "image"
};
} catch (error) {
if (error instanceof Error) {
throw error;
} else {
throw new Error(`Unexpected error processing image: ${String(error)}`);
}
}
};
var audioContent = async (input) => {
let rawData;
try {
if ("url" in input) {
try {
const response = await fetch(input.url);
if (!response.ok) {
throw new Error(
`Server responded with status: ${response.status} - ${response.statusText}`
);
}
rawData = Buffer.from(await response.arrayBuffer());
} catch (error) {
throw new Error(
`Failed to fetch audio from URL (${input.url}): ${error instanceof Error ? error.message : String(error)}`
);
}
} else if ("path" in input) {
try {
rawData = await readFile(input.path);
} catch (error) {
throw new Error(
`Failed to read audio from path (${input.path}): ${error instanceof Error ? error.message : String(error)}`
);
}
} 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);
if (!mimeType || !mimeType.mime.startsWith("audio/")) {
console.warn(
`Warning: Content may not be a valid audio file. Detected MIME: ${mimeType?.mime || "unknown"}`
);
}
const base64Data = rawData.toString("base64");
return {
data: base64Data,
mimeType: mimeType?.mime ?? "audio/mpeg",
type: "audio"
};
} catch (error) {
if (error instanceof Error) {
throw error;
} else {
throw new Error(`Unexpected error processing audio: ${String(error)}`);
}
}
};
var FastMCPError = class extends Error {
constructor(message) {
super(message);
this.name = new.target.name;
}
};
var UnexpectedStateError = class extends FastMCPError {
extras;
constructor(message, extras) {
super(message);
this.name = new.target.name;
this.extras = extras;
}
};
var UserError = class extends UnexpectedStateError {
};
var TextContentZodSchema = z.object({
/**
* The text content of the message.
*/
text: z.string(),
type: z.literal("text")
}).strict();
var ImageContentZodSchema = z.object({
/**
* 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(),
type: z.literal("image")
}).strict();
var AudioContentZodSchema = z.object({
/**
* The base64-encoded audio data.
*/
data: z.string().base64(),
mimeType: z.string(),
type: z.literal("audio")
}).strict();
var ContentZodSchema = z.discriminatedUnion("type", [
TextContentZodSchema,
ImageContentZodSchema,
AudioContentZodSchema
]);
var ContentResultZodSchema = z.object({
content: ContentZodSchema.array(),
isError: z.boolean().optional()
}).strict();
var CompletionZodSchema = z.object({
/**
* 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()),
/**
* 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()),
/**
* An array of completion values. Must not exceed 100 items.
*/
values: z.array(z.string()).max(100)
});
var FastMCPSessionEventEmitterBase = EventEmitter;
var FastMCPSessionEventEmitter = class extends FastMCPSessionEventEmitterBase {
};
var FastMCPSession = class extends FastMCPSessionEventEmitter {
get clientCapabilities() {
return this.#clientCapabilities ?? null;
}
get loggingLevel() {
return this.#loggingLevel;
}
get roots() {
return this.#roots;
}
get server() {
return this.#server;
}
#auth;
#capabilities = {};
#clientCapabilities;
#loggingLevel = "info";
#pingConfig;
#pingInterval = null;
#prompts = [];
#resources = [];
#resourceTemplates = [];
#roots = [];
#rootsConfig;
#server;
constructor({
auth,
instructions,
name,
ping,
prompts,
resources,
resourcesTemplates,
roots,
tools,
version
}) {
super();
this.#auth = auth;
this.#pingConfig = ping;
this.#rootsConfig = roots;
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, version },
{ capabilities: this.#capabilities, instructions }
);
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);
}
}
async close() {
if (this.#pingInterval) {
clearInterval(this.#pingInterval);
}
try {
await this.#server.close();
} catch (error) {
console.error("[FastMCP error]", "could not close server", error);
}
}
async connect(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("[FastMCP warning] could not infer client capabilities");
}
if (this.#clientCapabilities?.roots?.listChanged && typeof this.#server.listRoots === "function") {
try {
const roots = await this.#server.listRoots();
this.#roots = roots.roots;
} catch (e) {
if (e instanceof McpError && e.code === ErrorCode.MethodNotFound) {
console.debug(
"[FastMCP debug] listRoots method not supported by client"
);
} else {
console.error(
`[FastMCP error] received error listing roots.
${e instanceof Error ? e.stack : JSON.stringify(e)}`
);
}
}
}
if (this.#clientCapabilities) {
const pingConfig = this.#getPingConfig(transport);
if (pingConfig.enabled) {
this.#pingInterval = setInterval(async () => {
try {
await this.#server.ping();
} catch {
const logLevel = pingConfig.logLevel;
if (logLevel === "debug") {
console.debug("[FastMCP debug] server ping failed");
} else if (logLevel === "warning") {
console.warn(
"[FastMCP warning] server is not responding to ping"
);
} else if (logLevel === "error") {
console.error("[FastMCP error] server is not responding to ping");
} else {
console.info("[FastMCP info] server ping failed");
}
}
}, pingConfig.intervalMs);
}
}
}
async requestSampling(message) {
return this.#server.createMessage(message);
}
#getPingConfig(transport) {
const pingConfig = this.#pingConfig || {};
let defaultEnabled = false;
if ("type" in transport) {
if (transport.type === "sse" || transport.type === "httpStream") {
defaultEnabled = true;
}
}
return {
enabled: pingConfig.enabled !== void 0 ? pingConfig.enabled : defaultEnabled,
intervalMs: pingConfig.intervalMs || 5e3,
logLevel: pingConfig.logLevel || "debug"
};
}
addPrompt(inputPrompt) {
const completers = {};
const enums = {};
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, value) => {
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 {
total: result.length,
values: result.map((item) => item.item)
};
}
return {
values: []
};
}
};
this.#prompts.push(prompt);
}
addResource(inputResource) {
this.#resources.push(inputResource);
}
addResourceTemplate(inputResourceTemplate) {
const completers = {};
for (const argument of inputResourceTemplate.arguments ?? []) {
if (argument.complete) {
completers[argument.name] = argument.complete;
}
}
const resourceTemplate = {
...inputResourceTemplate,
complete: async (name, value) => {
if (completers[name]) {
return await completers[name](value);
}
return {
values: []
};
}
};
this.#resourceTemplates.push(resourceTemplate);
}
setupCompleteHandlers() {
this.#server.setRequestHandler(CompleteRequestSchema, async (request) => {
if (request.params.ref.type === "ref/prompt") {
const prompt = this.#prompts.find(
(prompt2) => prompt2.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(
(resource2) => resource2.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
});
});
}
setupErrorHandling() {
this.#server.onerror = (error) => {
console.error("[FastMCP error]", error);
};
}
setupLoggingHandlers() {
this.#server.setRequestHandler(SetLevelRequestSchema, (request) => {
this.#loggingLevel = request.params.level;
return {};
});
}
setupPromptHandlers(prompts) {
this.#server.setRequestHandler(ListPromptsRequestSchema, async () => {
return {
prompts: prompts.map((prompt) => {
return {
arguments: prompt.arguments,
complete: prompt.complete,
description: prompt.description,
name: prompt.name
};
})
};
});
this.#server.setRequestHandler(GetPromptRequestSchema, async (request) => {
const prompt = prompts.find(
(prompt2) => prompt2.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;
try {
result = await prompt.load(args);
} catch (error) {
throw new McpError(
ErrorCode.InternalError,
`Error loading prompt: ${error}`
);
}
return {
description: prompt.description,
messages: [
{
content: { text: result, type: "text" },
role: "user"
}
]
};
});
}
setupResourceHandlers(resources) {
this.#server.setRequestHandler(ListResourcesRequestSchema, async () => {
return {
resources: resources.map((resource) => {
return {
mimeType: resource.mimeType,
name: resource.name,
uri: resource.uri
};
})
};
});
this.#server.setRequestHandler(
ReadResourceRequestSchema,
async (request) => {
if ("uri" in request.params) {
const resource = resources.find(
(resource2) => "uri" in resource2 && resource2.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: [
{
mimeType: resourceTemplate.mimeType,
name: resourceTemplate.name,
uri,
...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;
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) => ({
mimeType: resource.mimeType,
name: resource.name,
uri: resource.uri,
...result
}))
};
} else {
return {
contents: [
{
mimeType: resource.mimeType,
name: resource.name,
uri: resource.uri,
...maybeArrayResult
}
]
};
}
}
throw new UnexpectedStateError("Unknown resource request", {
request
});
}
);
}
setupResourceTemplateHandlers(resourceTemplates) {
this.#server.setRequestHandler(
ListResourceTemplatesRequestSchema,
async () => {
return {
resourceTemplates: resourceTemplates.map((resourceTemplate) => {
return {
name: resourceTemplate.name,
uriTemplate: resourceTemplate.uriTemplate
};
})
};
}
);
}
setupRootsHandlers() {
if (this.#rootsConfig?.enabled === false) {
console.debug(
"[FastMCP debug] roots capability explicitly disabled via config"
);
return;
}
if (typeof this.#server.listRoots === "function") {
this.#server.setNotificationHandler(
RootsListChangedNotificationSchema,
() => {
this.#server.listRoots().then((roots) => {
this.#roots = roots.roots;
this.emit("rootsChanged", {
roots: roots.roots
});
}).catch((error) => {
if (error instanceof McpError && error.code === ErrorCode.MethodNotFound) {
console.debug(
"[FastMCP debug] listRoots method not supported by client"
);
} else {
console.error("[FastMCP error] Error listing roots", error);
}
});
}
);
} else {
console.debug(
"[FastMCP debug] roots capability not available, not setting up notification handler"
);
}
}
setupToolHandlers(tools) {
this.#server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: await Promise.all(
tools.map(async (tool) => {
return {
annotations: tool.annotations,
description: tool.description,
inputSchema: tool.parameters ? await toJsonSchema(tool.parameters) : {
additionalProperties: false,
properties: {},
type: "object"
},
// More complete schema for Cursor compatibility
name: tool.name
};
})
)
};
});
this.#server.setRequestHandler(CallToolRequestSchema, async (request) => {
const tool = tools.find((tool2) => tool2.name === request.params.name);
if (!tool) {
throw new McpError(
ErrorCode.MethodNotFound,
`Unknown tool: ${request.params.name}`
);
}
let args = void 0;
if (tool.parameters) {
const parsed = await tool.parameters["~standard"].validate(
request.params.arguments
);
if (parsed.issues) {
throw new McpError(
ErrorCode.InvalidParams,
`Invalid ${request.params.name} parameters: ${JSON.stringify(parsed.issues)}`
);
}
args = parsed.value;
}
const progressToken = request.params?._meta?.progressToken;
let result;
try {
const reportProgress = async (progress) => {
await this.#server.notification({
method: "notifications/progress",
params: {
...progress,
progressToken
}
});
};
const log = {
debug: (message, context) => {
this.#server.sendLoggingMessage({
data: {
context,
message
},
level: "debug"
});
},
error: (message, context) => {
this.#server.sendLoggingMessage({
data: {
context,
message
},
level: "error"
});
},
info: (message, context) => {
this.#server.sendLoggingMessage({
data: {
context,
message
},
level: "info"
});
},
warn: (message, context) => {
this.#server.sendLoggingMessage({
data: {
context,
message
},
level: "warning"
});
}
};
const executeToolPromise = tool.execute(args, {
log,
reportProgress,
session: this.#auth
});
const maybeStringResult = await (tool.timeoutMs ? Promise.race([
executeToolPromise,
new Promise((_, reject) => {
setTimeout(() => {
reject(
new UserError(
`Tool execution timed out after ${tool.timeoutMs}ms`
)
);
}, tool.timeoutMs);
})
]) : executeToolPromise);
if (typeof maybeStringResult === "string") {
result = ContentResultZodSchema.parse({
content: [{ text: maybeStringResult, type: "text" }]
});
} else if ("type" in maybeStringResult) {
result = ContentResultZodSchema.parse({
content: [maybeStringResult]
});
} else {
result = ContentResultZodSchema.parse(maybeStringResult);
}
} catch (error) {
if (error instanceof UserError) {
return {
content: [{ text: error.message, type: "text" }],
isError: true
};
}
return {
content: [{ text: `Error: ${error}`, type: "text" }],
isError: true
};
}
return result;
});
}
};
var FastMCPEventEmitterBase = EventEmitter;
var FastMCPEventEmitter = class extends FastMCPEventEmitterBase {
};
var FastMCP = class extends FastMCPEventEmitter {
constructor(options) {
super();
this.options = options;
this.#options = options;
this.#authenticate = options.authenticate;
}
get sessions() {
return this.#sessions;
}
#authenticate;
#httpStreamServer = null;
#options;
#prompts = [];
#resources = [];
#resourcesTemplates = [];
#sessions = [];
#sseServer = null;
#tools = [];
/**
* Adds a prompt to the server.
*/
addPrompt(prompt) {
this.#prompts.push(prompt);
}
/**
* Adds a resource to the server.
*/
addResource(resource) {
this.#resources.push(resource);
}
/**
* Adds a resource template to the server.
*/
addResourceTemplate(resource) {
this.#resourcesTemplates.push(resource);
}
/**
* Adds a tool to the server.
*/
addTool(tool) {
this.#tools.push(tool);
}
/**
* Starts the server.
*/
async start(options = {
transportType: "stdio"
}) {
if (options.transportType === "stdio") {
const transport = new StdioServerTransport();
const session = new FastMCPSession({
instructions: this.#options.instructions,
name: this.#options.name,
ping: this.#options.ping,
prompts: this.#prompts,
resources: this.#resources,
resourcesTemplates: this.#resourcesTemplates,
roots: this.#options.roots,
tools: this.#tools,
version: this.#options.version
});
await session.connect(transport);
this.#sessions.push(session);
this.emit("connect", {
session
});
} else if (options.transportType === "sse") {
this.#sseServer = await startSSEServer({
createServer: async (request) => {
let auth;
if (this.#authenticate) {
auth = await this.#authenticate(request);
}
return new FastMCPSession({
auth,
name: this.#options.name,
ping: this.#options.ping,
prompts: this.#prompts,
resources: this.#resources,
resourcesTemplates: this.#resourcesTemplates,
roots: this.#options.roots,
tools: this.#tools,
version: this.#options.version
});
},
endpoint: options.sse.endpoint,
onClose: (session) => {
this.emit("disconnect", {
session
});
},
onConnect: async (session) => {
this.#sessions.push(session);
this.emit("connect", {
session
});
},
port: options.sse.port
});
console.info(
`[FastMCP info] server is running on SSE at http://localhost:${options.sse.port}${options.sse.endpoint}`
);
} else if (options.transportType === "httpStream") {
this.#httpStreamServer = await startHTTPStreamServer({
createServer: async (request) => {
let auth;
if (this.#authenticate) {
auth = await this.#authenticate(request);
}
return new FastMCPSession({
auth,
name: this.#options.name,
ping: this.#options.ping,
prompts: this.#prompts,
resources: this.#resources,
resourcesTemplates: this.#resourcesTemplates,
roots: this.#options.roots,
tools: this.#tools,
version: this.#options.version
});
},
endpoint: options.httpStream.endpoint,
onClose: (session) => {
this.emit("disconnect", {
session
});
},
onConnect: async (session) => {
this.#sessions.push(session);
this.emit("connect", {
session
});
},
port: options.httpStream.port
});
console.info(
`[FastMCP info] server is running on HTTP Stream at http://localhost:${options.httpStream.port}${options.httpStream.endpoint}`
);
} else {
throw new Error("Invalid transport type");
}
}
/**
* Stops the server.
*/
async stop() {
if (this.#sseServer) {
await this.#sseServer.close();
}
if (this.#httpStreamServer) {
await this.#httpStreamServer.close();
}
}
};
export {
FastMCP,
FastMCPSession,
UnexpectedStateError,
UserError,
audioContent,
imageContent
};
//# sourceMappingURL=FastMCP.js.map