fastmcp
Version:
A TypeScript framework for building MCP servers.
1,511 lines (1,492 loc) • 67.1 kB
JavaScript
"use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { newObj[key] = obj[key]; } } } newObj.default = obj; return newObj; } } function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }// src/FastMCP.ts
var _indexjs = require('@modelcontextprotocol/sdk/server/index.js');
var _stdiojs = require('@modelcontextprotocol/sdk/server/stdio.js');
var _typesjs = require('@modelcontextprotocol/sdk/types.js');
var _events = require('events');
var _promises = require('fs/promises');
var _fusejs = require('fuse.js'); var _fusejs2 = _interopRequireDefault(_fusejs);
var _mcpproxy = require('mcp-proxy');
var _promises3 = require('timers/promises');
var _undici = require('undici');
var _uritemplates = require('uri-templates'); var _uritemplates2 = _interopRequireDefault(_uritemplates);
var _xsschema = require('xsschema');
var _zod = require('zod');
// src/DiscoveryDocumentCache.ts
var DiscoveryDocumentCache = class {
get size() {
return this.#cache.size;
}
#cache = /* @__PURE__ */ new Map();
#inFlight = /* @__PURE__ */ new Map();
#ttl;
/**
* @param options - configuration options
* @param options.ttl - time-to-live in miliseconds
*/
constructor(options = {}) {
this.#ttl = _nullishCoalesce(options.ttl, () => ( 36e5));
}
/**
* @param url - optional URL to clear. if omitted, clears all cached documents.
*/
clear(url) {
if (url) {
this.#cache.delete(url);
} else {
this.#cache.clear();
}
}
/**
* fetches a discovery document from the given URL.
* uses cached value if available and not expired.
* coalesces concurrent requests for the same URL to prevent duplicate fetches.
*
* @param url - the discovery document URL (e.g., /.well-known/openid-configuration)
* @returns the discovery document as a JSON object
* @throws Error if the fetch fails or returns non-OK status
*/
async get(url) {
const now = Date.now();
const cached = this.#cache.get(url);
if (cached && cached.expiresAt > now) {
return cached.data;
}
const inFlight = this.#inFlight.get(url);
if (inFlight) {
return inFlight;
}
const fetchPromise = this.#fetchAndCache(url);
this.#inFlight.set(url, fetchPromise);
try {
const data = await fetchPromise;
return data;
} finally {
this.#inFlight.delete(url);
}
}
/**
* @param url - the URL to check
* @returns true if the URL is cached and nott expired
*/
has(url) {
const cached = this.#cache.get(url);
if (!cached) {
return false;
}
const now = Date.now();
if (cached.expiresAt <= now) {
this.#cache.delete(url);
return false;
}
return true;
}
async #fetchAndCache(url) {
const res = await fetch(url);
if (!res.ok) {
throw new Error(
`Failed to fetch discovery document from ${url}: ${res.status} ${res.statusText}`
);
}
const data = await res.json();
const expiresAt = Date.now() + this.#ttl;
this.#cache.set(url, {
data,
expiresAt
});
return data;
}
};
// src/FastMCP.ts
var imageContent = async (input) => {
let rawData;
try {
if ("url" in input) {
try {
const response = await _undici.fetch.call(void 0, 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 _promises.readFile.call(void 0, 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 { fileTypeFromBuffer } = await Promise.resolve().then(() => _interopRequireWildcard(require("file-type")));
const mimeType = await fileTypeFromBuffer(rawData);
if (!mimeType || !mimeType.mime.startsWith("image/")) {
console.warn(
`Warning: Content may not be a valid image. Detected MIME: ${_optionalChain([mimeType, 'optionalAccess', _2 => _2.mime]) || "unknown"}`
);
}
const base64Data = rawData.toString("base64");
return {
data: base64Data,
mimeType: _nullishCoalesce(_optionalChain([mimeType, 'optionalAccess', _3 => _3.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 _undici.fetch.call(void 0, 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 _promises.readFile.call(void 0, 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 { fileTypeFromBuffer } = await Promise.resolve().then(() => _interopRequireWildcard(require("file-type")));
const mimeType = await fileTypeFromBuffer(rawData);
if (!mimeType || !mimeType.mime.startsWith("audio/")) {
console.warn(
`Warning: Content may not be a valid audio file. Detected MIME: ${_optionalChain([mimeType, 'optionalAccess', _4 => _4.mime]) || "unknown"}`
);
}
const base64Data = rawData.toString("base64");
return {
data: base64Data,
mimeType: _nullishCoalesce(_optionalChain([mimeType, 'optionalAccess', _5 => _5.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 {
constructor(message, extras) {
super(message);
this.name = new.target.name;
this.extras = extras;
}
};
var UserError = class extends UnexpectedStateError {
};
var TextContentZodSchema = _zod.z.object({
/**
* The text content of the message.
*/
text: _zod.z.string(),
type: _zod.z.literal("text")
}).strict();
var ImageContentZodSchema = _zod.z.object({
/**
* The base64-encoded image data.
*/
data: _zod.z.string().base64(),
/**
* The MIME type of the image. Different providers may support different image types.
*/
mimeType: _zod.z.string(),
type: _zod.z.literal("image")
}).strict();
var AudioContentZodSchema = _zod.z.object({
/**
* The base64-encoded audio data.
*/
data: _zod.z.string().base64(),
mimeType: _zod.z.string(),
type: _zod.z.literal("audio")
}).strict();
var ResourceContentZodSchema = _zod.z.object({
resource: _zod.z.object({
blob: _zod.z.string().optional(),
mimeType: _zod.z.string().optional(),
text: _zod.z.string().optional(),
uri: _zod.z.string()
}),
type: _zod.z.literal("resource")
}).strict();
var ResourceLinkZodSchema = _zod.z.object({
description: _zod.z.string().optional(),
mimeType: _zod.z.string().optional(),
name: _zod.z.string(),
title: _zod.z.string().optional(),
type: _zod.z.literal("resource_link"),
uri: _zod.z.string()
});
var ContentZodSchema = _zod.z.discriminatedUnion("type", [
TextContentZodSchema,
ImageContentZodSchema,
AudioContentZodSchema,
ResourceContentZodSchema,
ResourceLinkZodSchema
]);
var ContentResultZodSchema = _zod.z.object({
content: ContentZodSchema.array(),
isError: _zod.z.boolean().optional()
}).strict();
var CompletionZodSchema = _zod.z.object({
/**
* Indicates whether there are additional completion options beyond those provided in the current response, even if the exact total is unknown.
*/
hasMore: _zod.z.optional(_zod.z.boolean()),
/**
* The total number of completion options available. This can exceed the number of values actually sent in the response.
*/
total: _zod.z.optional(_zod.z.number().int()),
/**
* An array of completion values. Must not exceed 100 items.
*/
values: _zod.z.array(_zod.z.string()).max(100)
});
var FastMCPSessionEventEmitterBase = _events.EventEmitter;
var ServerState = /* @__PURE__ */ ((ServerState2) => {
ServerState2["Error"] = "error";
ServerState2["Running"] = "running";
ServerState2["Stopped"] = "stopped";
return ServerState2;
})(ServerState || {});
var FastMCPSessionEventEmitter = class extends FastMCPSessionEventEmitterBase {
};
var FastMCPSession = class extends FastMCPSessionEventEmitter {
get clientCapabilities() {
return _nullishCoalesce(this.#clientCapabilities, () => ( null));
}
get isReady() {
return this.#connectionState === "ready";
}
get loggingLevel() {
return this.#loggingLevel;
}
get roots() {
return this.#roots;
}
get server() {
return this.#server;
}
get sessionId() {
return this.#sessionId;
}
set sessionId(value) {
this.#sessionId = value;
}
#auth;
#capabilities = {};
#clientCapabilities;
#connectionState = "connecting";
#logger;
#loggingLevel = "info";
#needsEventLoopFlush = false;
#pingConfig;
#pingInterval = null;
#prompts = [];
#resources = [];
#resourceTemplates = [];
#roots = [];
#rootsConfig;
#server;
/**
* Session ID from the Mcp-Session-Id header (HTTP transports only).
* Used to track per-session state across multiple requests.
*/
#sessionId;
#utils;
constructor({
auth,
instructions,
logger,
name,
ping,
prompts,
resources,
resourcesTemplates,
roots,
sessionId,
tools,
transportType,
utils,
version
}) {
super();
this.#auth = auth;
this.#logger = logger;
this.#pingConfig = ping;
this.#rootsConfig = roots;
this.#sessionId = sessionId;
this.#needsEventLoopFlush = transportType === "httpStream";
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.#capabilities.completions = {};
this.#server = new (0, _indexjs.Server)(
{ name, version },
{ capabilities: this.#capabilities, instructions }
);
this.#utils = utils;
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() {
this.#connectionState = "closed";
if (this.#pingInterval) {
clearInterval(this.#pingInterval);
}
try {
await this.#server.close();
} catch (error) {
this.#logger.error("[FastMCP error]", "could not close server", error);
}
}
async connect(transport) {
if (this.#server.transport) {
throw new UnexpectedStateError("Server is already connected");
}
this.#connectionState = "connecting";
try {
await this.#server.connect(transport);
if ("sessionId" in transport) {
const transportWithSessionId = transport;
if (typeof transportWithSessionId.sessionId === "string") {
this.#sessionId = transportWithSessionId.sessionId;
}
}
let attempt = 0;
const maxAttempts = 10;
const retryDelay = 100;
while (attempt++ < maxAttempts) {
const capabilities = this.#server.getClientCapabilities();
if (capabilities) {
this.#clientCapabilities = capabilities;
break;
}
await _promises3.setTimeout.call(void 0, retryDelay);
}
if (!this.#clientCapabilities) {
this.#logger.warn(
`[FastMCP warning] could not infer client capabilities after ${maxAttempts} attempts. Connection may be unstable.`
);
}
if (_optionalChain([this, 'access', _6 => _6.#rootsConfig, 'optionalAccess', _7 => _7.enabled]) !== false && _optionalChain([this, 'access', _8 => _8.#clientCapabilities, 'optionalAccess', _9 => _9.roots, 'optionalAccess', _10 => _10.listChanged]) && typeof this.#server.listRoots === "function") {
try {
const roots = await this.#server.listRoots();
this.#roots = _optionalChain([roots, 'optionalAccess', _11 => _11.roots]) || [];
} catch (e) {
if (e instanceof _typesjs.McpError && e.code === _typesjs.ErrorCode.MethodNotFound) {
this.#logger.debug(
"[FastMCP debug] listRoots method not supported by client"
);
} else {
this.#logger.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 (e2) {
const logLevel = pingConfig.logLevel;
if (logLevel === "debug") {
this.#logger.debug("[FastMCP debug] server ping failed");
} else if (logLevel === "warning") {
this.#logger.warn(
"[FastMCP warning] server is not responding to ping"
);
} else if (logLevel === "error") {
this.#logger.error(
"[FastMCP error] server is not responding to ping"
);
} else {
this.#logger.info("[FastMCP info] server ping failed");
}
}
}, pingConfig.intervalMs);
}
}
this.#connectionState = "ready";
this.emit("ready");
} catch (error) {
this.#connectionState = "error";
const errorEvent = {
error: error instanceof Error ? error : new Error(String(error))
};
this.emit("error", errorEvent);
throw error;
}
}
promptsListChanged(prompts) {
this.#prompts = [];
for (const prompt of prompts) {
this.addPrompt(prompt);
}
this.setupPromptHandlers(prompts);
this.triggerListChangedNotification("notifications/prompts/list_changed");
}
async requestSampling(message, options) {
return this.#server.createMessage(message, options);
}
resourcesListChanged(resources) {
this.#resources = [];
for (const resource of resources) {
this.addResource(resource);
}
this.setupResourceHandlers(resources);
this.triggerListChangedNotification("notifications/resources/list_changed");
}
resourceTemplatesListChanged(resourceTemplates) {
this.#resourceTemplates = [];
for (const resourceTemplate of resourceTemplates) {
this.addResourceTemplate(resourceTemplate);
}
this.setupResourceTemplateHandlers(resourceTemplates);
this.triggerListChangedNotification("notifications/resources/list_changed");
}
toolsListChanged(tools) {
const allowedTools = tools.filter(
(tool) => tool.canAccess ? tool.canAccess(this.#auth) : true
);
this.setupToolHandlers(allowedTools);
this.triggerListChangedNotification("notifications/tools/list_changed");
}
async triggerListChangedNotification(method) {
try {
await this.#server.notification({
method
});
} catch (error) {
this.#logger.error(
`[FastMCP error] failed to send ${method} notification.
${error instanceof Error ? error.stack : JSON.stringify(error)}`
);
}
}
waitForReady() {
if (this.isReady) {
return Promise.resolve();
}
if (this.#connectionState === "error" || this.#connectionState === "closed") {
return Promise.reject(
new Error(`Connection is in ${this.#connectionState} state`)
);
}
return new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
reject(
new Error(
"Connection timeout: Session failed to become ready within 5 seconds"
)
);
}, 5e3);
this.once("ready", () => {
clearTimeout(timeout);
resolve();
});
this.once("error", (event) => {
clearTimeout(timeout);
reject(event.error);
});
});
}
#getPingConfig(transport) {
const pingConfig = this.#pingConfig || {};
let defaultEnabled = false;
if ("type" in transport) {
if (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 = {};
const fuseInstances = {};
for (const argument of _nullishCoalesce(inputPrompt.arguments, () => ( []))) {
if (argument.complete) {
completers[argument.name] = argument.complete;
}
if (argument.enum) {
enums[argument.name] = argument.enum;
fuseInstances[argument.name] = new (0, _fusejs2.default)(argument.enum, {
includeScore: true,
threshold: 0.3
// More flexible matching!
});
}
}
const prompt = {
...inputPrompt,
complete: async (name, value, auth) => {
if (completers[name]) {
return await completers[name](value, auth);
}
if (inputPrompt.complete) {
return await inputPrompt.complete(name, value, auth);
}
if (fuseInstances[name]) {
const result = fuseInstances[name].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 _nullishCoalesce(inputResourceTemplate.arguments, () => ( []))) {
if (argument.complete) {
completers[argument.name] = argument.complete;
}
}
const resourceTemplate = {
...inputResourceTemplate,
complete: async (name, value, auth) => {
if (completers[name]) {
return await completers[name](value, auth);
}
if (inputResourceTemplate.complete) {
return await inputResourceTemplate.complete(name, value, auth);
}
return {
values: []
};
}
};
this.#resourceTemplates.push(resourceTemplate);
}
setupCompleteHandlers() {
this.#server.setRequestHandler(_typesjs.CompleteRequestSchema, async (request) => {
if (request.params.ref.type === "ref/prompt") {
const ref = request.params.ref;
const prompt = "name" in ref && this.#prompts.find((prompt2) => prompt2.name === 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,
this.#auth
)
);
return {
completion
};
}
if (request.params.ref.type === "ref/resource") {
const ref = request.params.ref;
const resource = "uri" in ref && this.#resourceTemplates.find(
(resource2) => resource2.uriTemplate === 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,
this.#auth
)
);
return {
completion
};
}
throw new UnexpectedStateError("Unexpected completion request", {
request
});
});
}
setupErrorHandling() {
this.#server.onerror = (error) => {
this.#logger.error("[FastMCP error]", error);
};
}
setupLoggingHandlers() {
this.#server.setRequestHandler(_typesjs.SetLevelRequestSchema, (request) => {
this.#loggingLevel = request.params.level;
return {};
});
}
setupPromptHandlers(prompts) {
this.#server.setRequestHandler(_typesjs.ListPromptsRequestSchema, async () => {
return {
prompts: prompts.map((prompt) => {
return {
arguments: prompt.arguments,
complete: prompt.complete,
description: prompt.description,
name: prompt.name
};
})
};
});
this.#server.setRequestHandler(_typesjs.GetPromptRequestSchema, async (request) => {
const prompt = prompts.find(
(prompt2) => prompt2.name === request.params.name
);
if (!prompt) {
throw new (0, _typesjs.McpError)(
_typesjs.ErrorCode.MethodNotFound,
`Unknown prompt: ${request.params.name}`
);
}
const args = request.params.arguments;
for (const arg of _nullishCoalesce(prompt.arguments, () => ( []))) {
if (arg.required && !(args && arg.name in args)) {
throw new (0, _typesjs.McpError)(
_typesjs.ErrorCode.InvalidRequest,
`Prompt '${request.params.name}' requires argument '${arg.name}': ${arg.description || "No description provided"}`
);
}
}
let result;
try {
result = await prompt.load(
args,
this.#auth
);
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
throw new (0, _typesjs.McpError)(
_typesjs.ErrorCode.InternalError,
`Failed to load prompt '${request.params.name}': ${errorMessage}`
);
}
if (typeof result === "string") {
return {
description: prompt.description,
messages: [
{
content: { text: result, type: "text" },
role: "user"
}
]
};
} else {
return {
description: prompt.description,
messages: result.messages
};
}
});
}
setupResourceHandlers(resources) {
this.#server.setRequestHandler(_typesjs.ListResourcesRequestSchema, async () => {
return {
resources: resources.map((resource) => ({
description: resource.description,
mimeType: resource.mimeType,
name: resource.name,
uri: resource.uri
}))
};
});
this.#server.setRequestHandler(
_typesjs.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 = _uritemplates2.default.call(void 0,
resourceTemplate.uriTemplate
);
const match = uriTemplate.fromUri(request.params.uri);
if (!match) {
continue;
}
const uri = uriTemplate.fill(match);
const result = await resourceTemplate.load(match, this.#auth);
const resources2 = Array.isArray(result) ? result : [result];
return {
contents: resources2.map((resource2) => ({
...resource2,
description: resourceTemplate.description,
mimeType: _nullishCoalesce(resource2.mimeType, () => ( resourceTemplate.mimeType)),
name: resourceTemplate.name,
uri: _nullishCoalesce(resource2.uri, () => ( uri))
}))
};
}
throw new (0, _typesjs.McpError)(
_typesjs.ErrorCode.MethodNotFound,
`Resource not found: '${request.params.uri}'. Available resources: ${resources.map((r) => r.uri).join(", ") || "none"}`
);
}
if (!("uri" in resource)) {
throw new UnexpectedStateError("Resource does not support reading");
}
let maybeArrayResult;
try {
maybeArrayResult = await resource.load(this.#auth);
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
throw new (0, _typesjs.McpError)(
_typesjs.ErrorCode.InternalError,
`Failed to load resource '${resource.name}' (${resource.uri}): ${errorMessage}`,
{
uri: resource.uri
}
);
}
const resourceResults = Array.isArray(maybeArrayResult) ? maybeArrayResult : [maybeArrayResult];
return {
contents: resourceResults.map((result) => ({
...result,
mimeType: _nullishCoalesce(result.mimeType, () => ( resource.mimeType)),
name: resource.name,
uri: _nullishCoalesce(result.uri, () => ( resource.uri))
}))
};
}
throw new UnexpectedStateError("Unknown resource request", {
request
});
}
);
}
setupResourceTemplateHandlers(resourceTemplates) {
this.#server.setRequestHandler(
_typesjs.ListResourceTemplatesRequestSchema,
async () => {
return {
resourceTemplates: resourceTemplates.map((resourceTemplate) => ({
description: resourceTemplate.description,
mimeType: resourceTemplate.mimeType,
name: resourceTemplate.name,
uriTemplate: resourceTemplate.uriTemplate
}))
};
}
);
}
setupRootsHandlers() {
if (_optionalChain([this, 'access', _12 => _12.#rootsConfig, 'optionalAccess', _13 => _13.enabled]) === false) {
this.#logger.debug(
"[FastMCP debug] roots capability explicitly disabled via config"
);
return;
}
if (typeof this.#server.listRoots === "function") {
this.#server.setNotificationHandler(
_typesjs.RootsListChangedNotificationSchema,
() => {
this.#server.listRoots().then((roots) => {
this.#roots = roots.roots;
this.emit("rootsChanged", {
roots: roots.roots
});
}).catch((error) => {
if (error instanceof _typesjs.McpError && error.code === _typesjs.ErrorCode.MethodNotFound) {
this.#logger.debug(
"[FastMCP debug] listRoots method not supported by client"
);
} else {
this.#logger.error(
`[FastMCP error] received error listing roots.
${error instanceof Error ? error.stack : JSON.stringify(error)}`
);
}
});
}
);
} else {
this.#logger.debug(
"[FastMCP debug] roots capability not available, not setting up notification handler"
);
}
}
setupToolHandlers(tools) {
this.#server.setRequestHandler(_typesjs.ListToolsRequestSchema, async () => {
return {
tools: await Promise.all(
tools.map(async (tool) => {
return {
annotations: tool.annotations,
description: tool.description,
inputSchema: tool.parameters ? await _xsschema.toJsonSchema.call(void 0, tool.parameters) : {
additionalProperties: false,
properties: {},
type: "object"
},
// More complete schema for Cursor compatibility
name: tool.name
};
})
)
};
});
this.#server.setRequestHandler(_typesjs.CallToolRequestSchema, async (request) => {
const tool = tools.find((tool2) => tool2.name === request.params.name);
if (!tool) {
throw new (0, _typesjs.McpError)(
_typesjs.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) {
const friendlyErrors = _optionalChain([this, 'access', _14 => _14.#utils, 'optionalAccess', _15 => _15.formatInvalidParamsErrorMessage]) ? this.#utils.formatInvalidParamsErrorMessage(parsed.issues) : parsed.issues.map((issue) => {
const path = _optionalChain([issue, 'access', _16 => _16.path, 'optionalAccess', _17 => _17.join, 'call', _18 => _18(".")]) || "root";
return `${path}: ${issue.message}`;
}).join(", ");
throw new (0, _typesjs.McpError)(
_typesjs.ErrorCode.InvalidParams,
`Tool '${request.params.name}' parameter validation failed: ${friendlyErrors}. Please check the parameter types and values according to the tool's schema.`
);
}
args = parsed.value;
}
const progressToken = _optionalChain([request, 'access', _19 => _19.params, 'optionalAccess', _20 => _20._meta, 'optionalAccess', _21 => _21.progressToken]);
let result;
try {
const reportProgress = async (progress) => {
try {
await this.#server.notification({
method: "notifications/progress",
params: {
...progress,
progressToken
}
});
if (this.#needsEventLoopFlush) {
await new Promise((resolve) => setImmediate(resolve));
}
} catch (progressError) {
this.#logger.warn(
`[FastMCP warning] Failed to report progress for tool '${request.params.name}':`,
progressError instanceof Error ? progressError.message : String(progressError)
);
}
};
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 streamContent = async (content) => {
const contentArray = Array.isArray(content) ? content : [content];
try {
await this.#server.notification({
method: "notifications/tool/streamContent",
params: {
content: contentArray,
toolName: request.params.name
}
});
if (this.#needsEventLoopFlush) {
await new Promise((resolve) => setImmediate(resolve));
}
} catch (streamError) {
this.#logger.warn(
`[FastMCP warning] Failed to stream content for tool '${request.params.name}':`,
streamError instanceof Error ? streamError.message : String(streamError)
);
}
};
const executeToolPromise = tool.execute(args, {
client: {
version: this.#server.getClientVersion()
},
log,
reportProgress,
requestId: typeof _optionalChain([request, 'access', _22 => _22.params, 'optionalAccess', _23 => _23._meta, 'optionalAccess', _24 => _24.requestId]) === "string" ? request.params._meta.requestId : void 0,
session: this.#auth,
sessionId: this.#sessionId,
streamContent
});
const maybeStringResult = await (tool.timeoutMs ? Promise.race([
executeToolPromise,
new Promise((_, reject) => {
const timeoutId = setTimeout(() => {
reject(
new UserError(
`Tool '${request.params.name}' timed out after ${tool.timeoutMs}ms. Consider increasing timeoutMs or optimizing the tool implementation.`
)
);
}, tool.timeoutMs);
executeToolPromise.finally(() => clearTimeout(timeoutId));
})
]) : executeToolPromise);
await _promises3.setTimeout.call(void 0, 1);
if (maybeStringResult === void 0 || maybeStringResult === null) {
result = ContentResultZodSchema.parse({
content: []
});
} else 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,
...error.extras ? { structuredContent: error.extras } : {}
};
}
const errorMessage = error instanceof Error ? error.message : String(error);
return {
content: [
{
text: `Tool '${request.params.name}' execution failed: ${errorMessage}`,
type: "text"
}
],
isError: true
};
}
return result;
});
}
};
function camelToSnakeCase(str) {
return str.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`);
}
function convertObjectToSnakeCase(obj) {
const result = {};
for (const [key, value] of Object.entries(obj)) {
const snakeKey = camelToSnakeCase(key);
result[snakeKey] = value;
}
return result;
}
function parseBasicAuthHeader(authHeader) {
const basicMatch = _optionalChain([authHeader, 'optionalAccess', _25 => _25.match, 'call', _26 => _26(/^Basic\s+(.+)$/)]);
if (!basicMatch) return null;
try {
const credentials = Buffer.from(basicMatch[1], "base64").toString("utf-8");
const credMatch = credentials.match(/^([^:]+):(.*)$/);
if (!credMatch) return null;
return { clientId: credMatch[1], clientSecret: credMatch[2] };
} catch (e3) {
return null;
}
}
var FastMCPEventEmitterBase = _events.EventEmitter;
var FastMCPEventEmitter = class extends FastMCPEventEmitterBase {
};
var FastMCP = class extends FastMCPEventEmitter {
constructor(options) {
super();
this.options = options;
this.#options = options;
this.#authenticate = options.authenticate;
this.#logger = options.logger || console;
}
get serverState() {
return this.#serverState;
}
get sessions() {
return this.#sessions;
}
#authenticate;
#httpStreamServer = null;
#logger;
#options;
#prompts = [];
#resources = [];
#resourcesTemplates = [];
#serverState = "stopped" /* Stopped */;
#sessions = [];
#tools = [];
/**
* Adds a prompt to the server.
*/
addPrompt(prompt) {
this.#prompts = this.#prompts.filter((p) => p.name !== prompt.name);
this.#prompts.push(prompt);
if (this.#serverState === "running" /* Running */) {
this.#promptsListChanged(this.#prompts);
}
}
/**
* Adds prompts to the server.
*/
addPrompts(prompts) {
const newPromptNames = new Set(prompts.map((prompt) => prompt.name));
this.#prompts = this.#prompts.filter((p) => !newPromptNames.has(p.name));
this.#prompts.push(...prompts);
if (this.#serverState === "running" /* Running */) {
this.#promptsListChanged(this.#prompts);
}
}
/**
* Adds a resource to the server.
*/
addResource(resource) {
this.#resources = this.#resources.filter((r) => r.name !== resource.name);
this.#resources.push(resource);
if (this.#serverState === "running" /* Running */) {
this.#resourcesListChanged(this.#resources);
}
}
/**
* Adds resources to the server.
*/
addResources(resources) {
const newResourceNames = new Set(
resources.map((resource) => resource.name)
);
this.#resources = this.#resources.filter(
(r) => !newResourceNames.has(r.name)
);
this.#resources.push(...resources);
if (this.#serverState === "running" /* Running */) {
this.#resourcesListChanged(this.#resources);
}
}
/**
* Adds a resource template to the server.
*/
addResourceTemplate(resource) {
this.#resourcesTemplates = this.#resourcesTemplates.filter(
(t) => t.name !== resource.name
);
this.#resourcesTemplates.push(resource);
if (this.#serverState === "running" /* Running */) {
this.#resourceTemplatesListChanged(this.#resourcesTemplates);
}
}
/**
* Adds resource templates to the server.
*/
addResourceTemplates(resources) {
const newResourceTemplateNames = new Set(
resources.map((resource) => resource.name)
);
this.#resourcesTemplates = this.#resourcesTemplates.filter(
(t) => !newResourceTemplateNames.has(t.name)
);
this.#resourcesTemplates.push(...resources);
if (this.#serverState === "running" /* Running */) {
this.#resourceTemplatesListChanged(this.#resourcesTemplates);
}
}
/**
* Adds a tool to the server.
*/
addTool(tool) {
this.#tools = this.#tools.filter((t) => t.name !== tool.name);
this.#tools.push(tool);
if (this.#serverState === "running" /* Running */) {
this.#toolsListChanged(this.#tools);
}
}
/**
* Adds tools to the server.
*/
addTools(tools) {
const newToolNames = new Set(tools.map((tool) => tool.name));
this.#tools = this.#tools.filter((t) => !newToolNames.has(t.name));
this.#tools.push(...tools);
if (this.#serverState === "running" /* Running */) {
this.#toolsListChanged(this.#tools);
}
}
/**
* Embeds a resource by URI, making it easy to include resources in tool responses.
*
* @param uri - The URI of the resource to embed
* @returns Promise<ResourceContent> - The embedded resource content
*/
async embedded(uri) {
const directResource = this.#resources.find(
(resource) => resource.uri === uri
);
if (directResource) {
const result = await directResource.load();
const results = Array.isArray(result) ? result : [result];
const firstResult = results[0];
const resourceData = {
mimeType: directResource.mimeType,
uri
};
if ("text" in firstResult) {
resourceData.text = firstResult.text;
}
if ("blob" in firstResult) {
resourceData.blob = firstResult.blob;
}
return resourceData;
}
for (const template of this.#resourcesTemplates) {
const parsedTemplate = _uritemplates2.default.call(void 0, template.uriTemplate);
const params = parsedTemplate.fromUri(uri);
if (!params) {
continue;
}
const result = await template.load(
params
);
const resourceData = {
mimeType: template.mimeType,
uri
};
if ("text" in result) {
resourceData.text = result.text;
}
if ("blob" in result) {
resourceData.blob = result.blob;
}
return resourceData;
}
throw new UnexpectedStateError(`Resource not found: ${uri}`, { uri });
}
/**
* Removes a prompt from the server.
*/
removePrompt(name) {
this.#prompts = this.#prompts.filter((p) => p.name !== name);
if (this.#serverState === "running" /* Running */) {
this.#promptsListChanged(this.#prompts);
}
}
/**
* Removes prompts from the server.
*/
removePrompts(names) {
for (const name of names) {
this.#prompts = this.#prompts.filter((p) => p.name !== name);
}
if (this.#serverState === "running" /* Running */) {
this.#promptsListChanged(this.#prompts);
}
}
/**
* Removes a resource from the server.
*/
removeResource(name) {
this.#resources = this.#resources.filter((r) => r.name !== name);
if (this.#serverState === "running" /* Running */) {
this.#resourcesListChanged(this.#resources);
}
}
/**
* Removes resources from the server.
*/
removeResources(names) {
for (const name of names) {
this.#resources = this.#resources.filter((r) => r.name !== name);
}
if (this.#serverState === "running" /* Running */) {
this.#resourcesListChanged(this.#resources);
}
}
/**
* Removes a resource template from the server.
*/
removeResourceTemplate(name) {
this.#resourcesTemplates = this.#resourcesTemplates.filter(
(t) => t.name !== name
);
if (this.#serverState === "running" /* Running */) {
this.#resourceTemplatesListChanged(this.#resourcesTemplates);
}
}
/**
* Removes resource templates from the server.
*/
removeResourceTemplates(names) {
for (const name of names) {
this.#resourcesTemplates = this.#resourcesTemplates.filter(
(t) => t.name !== name
);
}
if (this.#serverState === "running" /* Running */) {
this.#resourceTemplatesListChanged(this.#resourcesTemplates);
}
}
/**
* Removes a tool from the server.
*/
removeTool(name) {
this.#tools = this.#tools.filter((t) => t.name !== name);
if (this.#serverState === "running" /* Running */) {
this.#toolsListChanged(this.#tools);
}
}
/**
* Removes tools from the server.
*/
removeTools(names) {
for (const name of names) {
this.#tools = this.#tools.filter((t) => t.name !== name);
}
if (this.#serverState === "running" /* Running */) {
this.#toolsListChanged(this.#tools);
}
}
/**
* Starts the server.
*/
async start(options) {
const config = this.#parseRuntimeConfig(options);
if (config.transportType === "stdio") {
const transport = new (0, _stdiojs.StdioServerTransport)();
let auth;
if (this.#authenticate) {
try {
auth = await this.#authenticate(
void 0
);
} catch (error) {
this.#logger.error(
"[FastMCP error] Authentication failed for stdio transport:",
error instanceof Error ? error.message : String(error)
);
}
}
const session = new FastMCPSession({
auth,
instructions: this.#options.instructions,
logger: this.#logger,
name: this.#options.name,
ping: this.#options.ping,
prompts: this.#prompts,
resources: this.#resources,
resourcesTemplates: this.#resourcesTemplates,
roots: this.#options.roots,
tools: this.#tools,
transportType: "stdio",
utils: this.#options.utils,
version: this.#options.version
});
await session.connect(transport);
this.#sessions.push(session);
session.once("error", () => {
this.#removeSession(session);
});
if (transport.onclose) {
const originalOnClose = transport.onclose;
transport.onclose = () => {
this.#removeSession(session);
if (originalOnClose) {
originalOnClose();
}
};
} else {
transport.onclose = () => {
this.#removeSession(session);
};
}
this.emit("connect", {
session
});
this.#serverState = "running" /* Running */;
} else if (config.transportType === "httpStream") {
const httpConfig = config.httpStream;
if (httpConfig.stateless) {
this.#logger.info(
`[FastMCP info] Starting server in stateless mode on HTTP Stream at http://${httpConfig.host}:${httpConfig.port}${httpConfig.endpoint}`
);
this.#httpStreamServer = await _mcpproxy.startHTTPServer.call(void 0, {
...this.#authenticate ? { authenticate: this.#authenticate } : {},
createServer: async (request) => {
let auth;
if (this.#authenticate) {
auth = await this.#authenticate(request);
if (auth === void 0 || auth === null) {
throw new Error("Authentication required");
}
}
const sessionId = Array.isArray(request.headers["mcp-session-id"]) ? request.headers["mcp-session-id"][0] : request.headers["mcp-session-id"];
return this.#createSession(auth, sessionId);
},
enableJsonResponse: httpConfig.enableJsonResponse,
eventStore: httpConfig.eventStore,
host: httpConfig.host,
..._optionalChain([this, 'access', _27 => _27.#options, 'access', _28 => _28.oauth, 'optionalAccess', _29 => _29.enabled]) && _optionalChain([this, 'access', _30 => _30.#options, 'access', _31 => _31.oauth, 'access', _32 => _32.protectedResource, 'optionalAccess', _33 => _33.resource]) ? {
oauth: {
protectedResource: {
resource: this.#options.oauth.protectedResource.resource
}
}
} : {},
// In stateless mode, we don't track sessions
onClose: async () => {
},
onConnect: async () => {
this.#logger.debug(
`[FastMCP debug] Stateless HTTP Stream request handled`
);
},
onUnhandledRequest: async (req, res) => {
await this.#handleUnhandledRequest(
req,
res,
true,
httpConfig.host,
httpConfig.endpoint
);
},
port: httpConfig.port,
stateless: true,
streamEndpoint: httpConfig.endpoint
});
} else {
this.#httpStreamServer = await _mcpproxy.startHTTPServer.call(void 0, {
...this.#authenticate ? { authenticate: this.#authenticate } : {},
createServer: async (request)