import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { EventStore } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; import { RequestOptions } from "@modelcontextprotocol/sdk/shared/protocol.js"; import { Transport } from "@modelcontextprotocol/sdk/shared/transport.js"; import { CallToolRequestSchema, ClientCapabilities, CompleteRequestSchema, CreateMessageRequestSchema, ErrorCode, GetPromptRequestSchema, GetPromptResult, ListPromptsRequestSchema, ListPromptsResult, ListResourcesRequestSchema, ListResourcesResult, ListResourceTemplatesRequestSchema, ListResourceTemplatesResult, ListToolsRequestSchema, ListToolsResult, McpError, ReadResourceRequestSchema, ResourceLink, Root, RootsListChangedNotificationSchema, Tool as SDKTool, ServerCapabilities, SetLevelRequestSchema, } from "@modelcontextprotocol/sdk/types.js"; import { StandardSchemaV1 } from "@standard-schema/spec"; import { EventEmitter } from "events"; import { readFile } from "fs/promises"; import Fuse from "fuse.js"; import { Hono } from "hono"; import http from "http"; import { startHTTPServer } from "mcp-proxy"; import { StrictEventEmitter } from "strict-event-emitter-types"; import { setTimeout as delay } from "timers/promises"; import { fetch } from "undici"; import parseURITemplate from "uri-templates"; import { toJsonSchema } from "xsschema"; import { z } from "zod"; import type { OAuthProxy } from "./auth/OAuthProxy.js"; import type { AuthProvider, OAuthSession, } from "./auth/providers/AuthProvider.js"; export interface Logger { debug(...args: unknown[]): void; error(...args: unknown[]): void; info(...args: unknown[]): void; log(...args: unknown[]): void; warn(...args: unknown[]): void; } export type SSEServer = { close: () => Promise; }; type FastMCPEvents = { connect: (event: { session: FastMCPSession }) => void; disconnect: (event: { session: FastMCPSession }) => void; }; type FastMCPSessionEvents = { error: (event: { error: Error }) => void; ready: () => void; rootsChanged: (event: { roots: Root[] }) => void; }; export const imageContent = async ( input: { buffer: Buffer } | { path: string } | { url: string }, ): Promise => { let rawData: Buffer; 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 { fileTypeFromBuffer } = await import("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: ${ mimeType?.mime || "unknown" }`, ); } const base64Data = rawData.toString("base64"); return { data: base64Data, mimeType: mimeType?.mime ?? "image/png", type: "image", } as const; } catch (error) { if (error instanceof Error) { throw error; } else { throw new Error(`Unexpected error processing image: ${String(error)}`); } } }; export const audioContent = async ( input: { buffer: Buffer } | { path: string } | { url: string }, ): Promise => { let rawData: Buffer; 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 { fileTypeFromBuffer } = await import("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: ${ mimeType?.mime || "unknown" }`, ); } const base64Data = rawData.toString("base64"); return { data: base64Data, mimeType: mimeType?.mime ?? "audio/mpeg", type: "audio", } as const; } catch (error) { if (error instanceof Error) { throw error; } else { throw new Error(`Unexpected error processing audio: ${String(error)}`); } } }; type Context = { client: { version: ReturnType; }; 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; }; reportProgress: (progress: Progress) => Promise; /** * Request ID from the current MCP request. * Available for all transports when the client provides it. */ requestId?: string; session: T | undefined; /** * Session ID from the Mcp-Session-Id header. * Only available for HTTP-based transports (SSE, HTTP Stream). * Can be used to track per-session state, implement session-specific * counters, or maintain user-specific data across multiple requests. */ sessionId?: string; streamContent: (content: Content | Content[]) => Promise; }; type Extra = unknown; type Extras = Record; type Literal = boolean | null | number | string | undefined; 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 SerializableValue = | { [key: string]: SerializableValue } | Literal | SerializableValue[]; type TextContent = { text: string; type: "text"; }; type ToolParameters = StandardSchemaV1; abstract class FastMCPError extends Error { public constructor(message?: string) { super(message); this.name = new.target.name; } } 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 {} const TextContentZodSchema = z .object({ /** * The text content of the message. */ text: z.string(), type: z.literal("text"), }) .strict() satisfies z.ZodType; type ImageContent = { data: string; mimeType: string; type: "image"; }; const 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() satisfies z.ZodType; type AudioContent = { data: string; mimeType: string; type: "audio"; }; const AudioContentZodSchema = z .object({ /** * The base64-encoded audio data. */ data: z.string().base64(), mimeType: z.string(), type: z.literal("audio"), }) .strict() satisfies z.ZodType; type ResourceContent = { resource: { blob?: string; mimeType?: string; text?: string; uri: string; }; type: "resource"; }; const ResourceContentZodSchema = z .object({ resource: z.object({ blob: z.string().optional(), mimeType: z.string().optional(), text: z.string().optional(), uri: z.string(), }), type: z.literal("resource"), }) .strict() satisfies z.ZodType; const ResourceLinkZodSchema = z.object({ description: z.string().optional(), mimeType: z.string().optional(), name: z.string(), title: z.string().optional(), type: z.literal("resource_link"), uri: z.string(), }) satisfies z.ZodType; type Content = | AudioContent | ImageContent | ResourceContent | ResourceLink | TextContent; const ContentZodSchema = z.discriminatedUnion("type", [ TextContentZodSchema, ImageContentZodSchema, AudioContentZodSchema, ResourceContentZodSchema, ResourceLinkZodSchema, ]) 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 = { hasMore?: boolean; total?: number; values: string[]; }; /** * https://github.com/modelcontextprotocol/typescript-sdk/blob/3164da64d085ec4e022ae881329eee7b72f208d4/src/types.ts#L983-L1003 */ const 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), }) satisfies z.ZodType; type ArgumentValueCompleter = (value: string, auth?: T) => Promise; type InputPrompt< T extends FastMCPSessionAuth = FastMCPSessionAuth, Arguments extends InputPromptArgument[] = InputPromptArgument[], Args = PromptArgumentsToObject, > = { arguments?: InputPromptArgument[]; complete?: (name: string, value: string, auth?: T) => Promise; description?: string; load: (args: Args, auth?: T) => Promise; name: string; }; type InputPromptArgument = Readonly<{ complete?: ArgumentValueCompleter; description?: string; enum?: string[]; name: string; required?: boolean; }>; type InputResourceTemplate< T extends FastMCPSessionAuth, Arguments extends InputResourceTemplateArgument[] = InputResourceTemplateArgument[], > = { arguments: Arguments; complete?: (name: string, value: string, auth?: T) => Promise; description?: string; load: ( args: ResourceTemplateArgumentsToObject, auth?: T, ) => Promise; mimeType?: string; name: string; uriTemplate: string; }; type InputResourceTemplateArgument< T extends FastMCPSessionAuth = FastMCPSessionAuth, > = Readonly<{ complete?: ArgumentValueCompleter; description?: string; name: string; required?: boolean; }>; type LoggingLevel = | "alert" | "critical" | "debug" | "emergency" | "error" | "info" | "notice" | "warning"; type Prompt< T extends FastMCPSessionAuth = FastMCPSessionAuth, Arguments extends PromptArgument[] = PromptArgument[], Args = PromptArgumentsToObject, > = { arguments?: PromptArgument[]; complete?: (name: string, value: string, auth?: T) => Promise; description?: string; load: (args: Args, auth?: T) => Promise; name: string; }; type PromptArgument = Readonly<{ complete?: ArgumentValueCompleter; description?: string; enum?: string[]; name: string; required?: boolean; }>; type PromptArgumentsToObject = { [K in T[number]["name"]]: Extract< T[number], { name: K } >["required"] extends true ? string : string | undefined; }; type PromptResult = Pick | string; type Resource = { complete?: (name: string, value: string, auth?: T) => Promise; description?: string; load: (auth?: T) => Promise; mimeType?: string; name: string; uri: string; }; type ResourceResult = | { blob: string; mimeType?: string; uri?: string; } | { mimeType?: string; text: string; uri?: string; }; type ResourceTemplate< T extends FastMCPSessionAuth, Arguments extends ResourceTemplateArgument[] = ResourceTemplateArgument[], > = { arguments: Arguments; complete?: (name: string, value: string, auth?: T) => Promise; description?: string; load: ( args: ResourceTemplateArgumentsToObject, auth?: T, ) => Promise; mimeType?: string; name: string; uriTemplate: string; }; type ResourceTemplateArgument< T extends FastMCPSessionAuth = FastMCPSessionAuth, > = Readonly<{ complete?: ArgumentValueCompleter; description?: string; name: string; required?: boolean; }>; type ResourceTemplateArgumentsToObject = { [K in T[number]["name"]]: string; }; type SamplingResponse = { content: AudioContent | ImageContent | TextContent; model: string; role: "assistant" | "user"; stopReason?: "endTurn" | "maxTokens" | "stopSequence" | string; }; type ServerOptions = { /** * Authentication provider for OAuth flows. * When provided, automatically configures the `authenticate` function * and `oauth` settings. * * For custom authentication logic, use the `authenticate` option instead. * If both are provided, `authenticate` takes precedence. * * @example * ```typescript * import { FastMCP, GitHubProvider } from "fastmcp"; * * const server = new FastMCP({ * auth: new GitHubProvider({ * baseUrl: "http://localhost:8000", * clientId: process.env.GITHUB_CLIENT_ID!, * clientSecret: process.env.GITHUB_CLIENT_SECRET!, * }), * name: "My Server", * version: "1.0.0", * }); * ``` */ auth?: AuthProvider; authenticate?: Authenticate; /** * Configuration for the health-check endpoint that can be exposed when the * server is running using the HTTP Stream transport. When enabled, the * server will respond to an HTTP GET request with the configured path (by * default "/health") rendering a plain-text response (by default "ok") and * the configured status code (by default 200). * * The endpoint is only added when the server is started with * `transportType: "httpStream"` – it is ignored for the stdio transport. */ health?: { /** * When set to `false` the health-check endpoint is disabled. * @default true */ enabled?: boolean; /** * Plain-text body returned by the endpoint. * @default "ok" */ message?: string; /** * HTTP path that should be handled. * @default "/health" */ path?: string; /** * HTTP response status that will be returned. * @default 200 */ status?: number; }; instructions?: string; /** * Custom logger instance. If not provided, defaults to console. * Use this to integrate with your own logging system. */ logger?: Logger; name: string; /** * Configuration for OAuth well-known discovery endpoints that can be exposed * when the server is running using HTTP-based transports (SSE or HTTP Stream). * When enabled, the server will respond to requests for OAuth discovery endpoints * with the configured metadata. * * The endpoints are only added when the server is started with * `transportType: "httpStream"` – they are ignored for the stdio transport. * Both SSE and HTTP Stream transports support OAuth endpoints. */ oauth?: { /** * OAuth Authorization Server metadata for /.well-known/oauth-authorization-server * * This endpoint follows RFC 8414 (OAuth 2.0 Authorization Server Metadata) * and provides metadata about the OAuth 2.0 authorization server. * * Required by MCP Specification 2025-03-26 */ authorizationServer?: { authorizationEndpoint: string; codeChallengeMethodsSupported?: string[]; // DPoP support dpopSigningAlgValuesSupported?: string[]; grantTypesSupported?: string[]; introspectionEndpoint?: string; // Required issuer: string; // Common optional jwksUri?: string; opPolicyUri?: string; opTosUri?: string; registrationEndpoint?: string; responseModesSupported?: string[]; responseTypesSupported: string[]; revocationEndpoint?: string; scopesSupported?: string[]; serviceDocumentation?: string; tokenEndpoint: string; tokenEndpointAuthMethodsSupported?: string[]; tokenEndpointAuthSigningAlgValuesSupported?: string[]; uiLocalesSupported?: string[]; }; /** * Whether OAuth discovery endpoints should be enabled. */ enabled: boolean; /** * OAuth Protected Resource metadata for `/.well-known/oauth-protected-resource` * * This endpoint follows {@link https://www.rfc-editor.org/rfc/rfc9728.html | RFC 9728} * and provides metadata describing how an OAuth 2.0 protected resource (in this case, * an MCP server) expects to be accessed. * * When configured, FastMCP will automatically serve this metadata at the * `/.well-known/oauth-protected-resource` endpoint. The `authorizationServers` and `resource` * fields are required. All others are optional and will be omitted from the published * metadata if not specified. * * This satisfies the requirements of the MCP Authorization specification's * {@link https://modelcontextprotocol.io/specification/2025-06-18/basic/authorization#authorization-server-location | Authorization Server Location section}. * * Clients consuming this metadata MUST validate that any presented values comply with * RFC 9728, including strict validation of the `resource` identifier and intended audience * when access tokens are issued and presented (per RFC 8707 §2). * * @remarks Required by MCP Specification version 2025-06-18 */ protectedResource?: { /** * Allows for additional metadata fields beyond those defined in RFC 9728. * * @remarks This supports vendor-specific or experimental extensions. * @see {@link https://www.rfc-editor.org/rfc/rfc9728.html#section-2.3 | RFC 9728 §2.3} */ [key: string]: unknown; /** * Supported values for the `authorization_details` parameter (RFC 9396). * * @remarks Used when fine-grained access control is in play. * @see {@link https://www.rfc-editor.org/rfc/rfc9728.html#section-2-2.23 | RFC 9728 §2.2.23} */ authorizationDetailsTypesSupported?: string[]; /** * List of OAuth 2.0 authorization server issuer identifiers. * * These correspond to ASes that can issue access tokens for this protected resource. * MCP clients use these values to locate the relevant `/.well-known/oauth-authorization-server` * metadata for initiating the OAuth flow. * * @remarks Required by the MCP spec. MCP servers MUST provide at least one issuer. * Clients are responsible for choosing among them (see RFC 9728 §7.6). * @see {@link https://www.rfc-editor.org/rfc/rfc9728.html#section-2-2.3 | RFC 9728 §2.2.3} */ authorizationServers: string[]; /** * List of supported methods for presenting OAuth 2.0 bearer tokens. * * @remarks Valid values are `header`, `body`, and `query`. * If omitted, clients MAY assume only `header` is supported, per RFC 6750. * This is a client-side interpretation and not a serialization default. * @see {@link https://www.rfc-editor.org/rfc/rfc9728.html#section-2-2.9 | RFC 9728 §2.2.9} */ bearerMethodsSupported?: string[]; /** * Whether this resource requires all access tokens to be DPoP-bound. * * @remarks If omitted, clients SHOULD assume this is `false`. * @see {@link https://www.rfc-editor.org/rfc/rfc9728.html#section-2-2.27 | RFC 9728 §2.2.27} */ dpopBoundAccessTokensRequired?: boolean; /** * Supported algorithms for verifying DPoP proofs (RFC 9449). * * @see {@link https://www.rfc-editor.org/rfc/rfc9728.html#section-2-2.25 | RFC 9728 §2.2.25} */ dpopSigningAlgValuesSupported?: string[]; /** * JWKS URI of this resource. Used to validate access tokens or sign responses. * * @remarks When present, this MUST be an `https:` URI pointing to a valid JWK Set (RFC 7517). * @see {@link https://www.rfc-editor.org/rfc/rfc9728.html#section-2-2.5 | RFC 9728 §2.2.5} */ jwksUri?: string; /** * Canonical OAuth resource identifier for this protected resource (the MCP server). * * @remarks Typically the base URL of the MCP server. Clients MUST use this as the * `resource` parameter in authorization and token requests (per RFC 8707). * @see {@link https://www.rfc-editor.org/rfc/rfc9728.html#section-2-2.1 | RFC 9728 §2.2.1} */ resource: string; /** * URL to developer-accessible documentation for this resource. * * @remarks This field MAY be localized. * @see {@link https://www.rfc-editor.org/rfc/rfc9728.html#section-2-2.15 | RFC 9728 §2.2.15} */ resourceDocumentation?: string; /** * Human-readable name for display purposes (e.g., in UIs). * * @remarks This field MAY be localized using language tags (`resource_name#en`, etc.). * @see {@link https://www.rfc-editor.org/rfc/rfc9728.html#section-2-2.13 | RFC 9728 §2.2.13} */ resourceName?: string; /** * URL to a human-readable policy page describing acceptable use. * * @remarks This field MAY be localized. * @see {@link https://www.rfc-editor.org/rfc/rfc9728.html#section-2-2.17 | RFC 9728 §2.2.17} */ resourcePolicyUri?: string; /** * Supported JWS algorithms for signed responses from this resource (e.g., response signing). * * @remarks MUST NOT include `none`. * @see {@link https://www.rfc-editor.org/rfc/rfc9728.html#section-2-2.11 | RFC 9728 §2.2.11} */ resourceSigningAlgValuesSupported?: string[]; /** * URL to the protected resource’s Terms of Service. * * @remarks This field MAY be localized. * @see {@link https://www.rfc-editor.org/rfc/rfc9728.html#section-2-2.19 | RFC 9728 §2.2.19} */ resourceTosUri?: string; /** * Supported OAuth scopes for requesting access to this resource. * * @remarks Useful for discovery, but clients SHOULD still request the minimal scope required. * @see {@link https://www.rfc-editor.org/rfc/rfc9728.html#section-2-2.7 | RFC 9728 §2.2.7} */ scopesSupported?: string[]; /** * Developer-accessible documentation for how to use the service (not end-user docs). * * @remarks Semantically equivalent to `resourceDocumentation`, but included under its * alternate name for compatibility with tools or schemas expecting either. * @see {@link https://www.rfc-editor.org/rfc/rfc9728.html#section-2-2.15 | RFC 9728 §2.2.15} */ serviceDocumentation?: string; /** * Whether mutual-TLS-bound access tokens are required. * * @remarks If omitted, clients SHOULD assume this is `false` (client-side behavior). * @see {@link https://www.rfc-editor.org/rfc/rfc9728.html#section-2-2.21 | RFC 9728 §2.2.21} */ tlsClientCertificateBoundAccessTokens?: boolean; }; /** * OAuth Proxy instance for automatic OAuth flow handling. * When provided, FastMCP will automatically register OAuth endpoints: * - /oauth/register (DCR) * - /oauth/authorize * - /oauth/token * - /oauth/callback * - /oauth/consent */ proxy?: OAuthProxy; }; ping?: { /** * Whether ping should be enabled by default. * - true for SSE or HTTP Stream * - false for stdio */ enabled?: boolean; /** * Interval * @default 5000 (5s) */ intervalMs?: number; /** * Logging level for ping-related messages. * @default 'debug' */ logLevel?: LoggingLevel; }; /** * Configuration for roots capability */ roots?: { /** * Whether roots capability should be enabled * Set to false to completely disable roots support * @default true */ enabled?: boolean; }; /** * General utilities */ utils?: { formatInvalidParamsErrorMessage?: ( issues: readonly StandardSchemaV1.Issue[], ) => string; }; version: `${number}.${number}.${number}`; }; type Tool< T extends FastMCPSessionAuth, Params extends ToolParameters = ToolParameters, OutputParams extends ToolParameters = ToolParameters, > = { /** * MCP ext-apps metadata for linking interactive UI components. * This field is passed through to the tool listing response. * @see https://modelcontextprotocol.github.io/ext-apps/ */ _meta?: { /** Additional metadata fields */ [key: string]: unknown; /** UI component configuration */ ui?: { /** URI of the resource serving the UI (e.g., "ui://my-tool/app.html") */ resourceUri?: string; }; }; annotations?: { /** * When true, the tool leverages incremental content streaming * Return void for tools that handle all their output via streaming */ streamingHint?: boolean; } & ToolAnnotations; canAccess?: (auth: T) => boolean; description?: string; execute: ( args: StandardSchemaV1.InferOutput, context: Context, ) => Promise< | AudioContent | ContentResult | ImageContent | ResourceContent | ResourceLink | string | TextContent | void >; name: string; outputSchema?: OutputParams; parameters?: Params; timeoutMs?: number; }; /** * Tool annotations as defined in MCP Specification (2025-03-26) * These provide hints about a tool's behavior. */ type ToolAnnotations = { /** * If true, the tool may perform destructive updates * Only meaningful when readOnlyHint is false * @default true */ destructiveHint?: boolean; /** * If true, calling the tool repeatedly with the same arguments has no additional effect * Only meaningful when readOnlyHint is false * @default false */ idempotentHint?: boolean; /** * If true, the tool may interact with an "open world" of external entities * @default true */ openWorldHint?: boolean; /** * If true, indicates the tool does not modify its environment * @default false */ readOnlyHint?: boolean; /** * A human-readable title for the tool, useful for UI display */ title?: string; }; const FastMCPSessionEventEmitterBase: { new (): StrictEventEmitter; } = EventEmitter; export enum ServerState { Error = "error", Running = "running", Stopped = "stopped", } /** * Enhanced request object for custom routes */ export interface FastMCPRequest< T extends FastMCPSessionAuth = FastMCPSessionAuth, > { auth?: T; body?: unknown; headers: http.IncomingHttpHeaders; json(): Promise; method: string; params: Record; query: Record; text(): Promise; url: string; } /** * Enhanced response object for custom routes */ export interface FastMCPResponse { end(data?: Buffer | string): void; json(data: unknown): void; send(data: Buffer | string): void; setHeader(name: string, value: number | string | string[]): FastMCPResponse; status(code: number): FastMCPResponse; } /** * HTTP method types for custom routes */ export type HTTPMethod = | "DELETE" | "GET" | "OPTIONS" | "PATCH" | "POST" | "PUT"; /** * Route handler function type */ export type RouteHandler = ( req: FastMCPRequest, res: FastMCPResponse, ) => Promise | void; /** * Options for configuring custom routes */ export interface RouteOptions { /** * Whether this route should bypass authentication. * When true, the route handler will be called without authentication, * and req.auth will be undefined. * @default false */ public?: boolean; } type Authenticate = (request: http.IncomingMessage) => Promise; type FastMCPSessionAuth = Record | undefined; class FastMCPSessionEventEmitter extends FastMCPSessionEventEmitterBase {} export class FastMCPSession< T extends FastMCPSessionAuth = FastMCPSessionAuth, > extends FastMCPSessionEventEmitter { public get clientCapabilities(): ClientCapabilities | null { return this.#clientCapabilities ?? null; } public get isReady(): boolean { return this.#connectionState === "ready"; } public get loggingLevel(): LoggingLevel { return this.#loggingLevel; } public get roots(): Root[] { return this.#roots; } public get server(): Server { return this.#server; } public get sessionId(): string | undefined { return this.#sessionId; } public set sessionId(value: string | undefined) { this.#sessionId = value; } #auth: T | undefined; #capabilities: ServerCapabilities = {}; #clientCapabilities?: ClientCapabilities; #connectionState: "closed" | "connecting" | "error" | "ready" = "connecting"; #logger: Logger; #loggingLevel: LoggingLevel = "info"; #needsEventLoopFlush: boolean = false; #pingConfig?: ServerOptions["ping"]; #pingInterval: null | ReturnType = null; #prompts: Map> = new Map(); #resources: Map> = new Map(); #resourceTemplates: Map> = new Map(); #roots: Root[] = []; #rootsConfig?: ServerOptions["roots"]; #server: Server; /** * Session ID from the Mcp-Session-Id header (HTTP transports only). * Used to track per-session state across multiple requests. */ #sessionId?: string; #utils?: ServerOptions["utils"]; constructor({ auth, instructions, logger, name, ping, prompts, resources, resourcesTemplates, roots, sessionId, tools, transportType, utils, version, }: { auth?: T; instructions?: string; logger: Logger; name: string; ping?: ServerOptions["ping"]; prompts: Prompt[]; resources: Resource[]; resourcesTemplates: InputResourceTemplate[]; roots?: ServerOptions["roots"]; sessionId?: string; tools: Tool[]; transportType?: "httpStream" | "stdio"; utils?: ServerOptions["utils"]; version: string; }) { 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 Server( { name: name, version: version }, { capabilities: this.#capabilities, instructions: 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(); if (resourcesTemplates.length) { for (const resourceTemplate of resourcesTemplates) { this.addResourceTemplate(resourceTemplate); } this.setupResourceTemplateHandlers(); } } if (prompts.length) { this.setupPromptHandlers(); } } public 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); } } public async connect(transport: Transport) { if (this.#server.transport) { throw new UnexpectedStateError("Server is already connected"); } this.#connectionState = "connecting"; try { await this.#server.connect(transport); // Extract session ID from transport if available (HTTP transports only) if ("sessionId" in transport) { const transportWithSessionId = transport as { sessionId?: string; } & 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 delay(retryDelay); } if (!this.#clientCapabilities) { this.#logger.warn( `[FastMCP warning] could not infer client capabilities after ${maxAttempts} attempts. Connection may be unstable.`, ); } if ( this.#rootsConfig?.enabled !== false && 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) { this.#logger.debug( "[FastMCP debug] listRoots method not supported by client", ); } else { this.#logger.error( `[FastMCP error] received error listing roots.\n\n${ 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 { // The reason we are not emitting an error here is because some clients // seem to not respond to the ping request, and we don't want to crash the server, // e.g., https://github.com/punkpeye/fastmcp/issues/38. 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); } } // Mark connection as ready and emit event 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: Prompt[]) { this.#prompts.clear(); for (const prompt of prompts) { this.addPrompt(prompt); } this.setupPromptHandlers(); this.triggerListChangedNotification("notifications/prompts/list_changed"); } public async requestSampling( message: z.infer["params"], options?: RequestOptions, ): Promise { return this.#server.createMessage(message, options); } resourcesListChanged(resources: Resource[]) { this.#resources.clear(); for (const resource of resources) { this.addResource(resource); } this.setupResourceHandlers(); this.triggerListChangedNotification("notifications/resources/list_changed"); } resourceTemplatesListChanged(resourceTemplates: ResourceTemplate[]) { this.#resourceTemplates.clear(); for (const resourceTemplate of resourceTemplates) { this.addResourceTemplate(resourceTemplate); } this.setupResourceTemplateHandlers(); this.triggerListChangedNotification("notifications/resources/list_changed"); } toolsListChanged(tools: Tool[]) { const allowedTools = tools.filter((tool) => tool.canAccess ? tool.canAccess(this.#auth as T) : true, ); this.setupToolHandlers(allowedTools); this.triggerListChangedNotification("notifications/tools/list_changed"); } async triggerListChangedNotification(method: string) { try { await this.#server.notification({ method, }); } catch (error) { this.#logger.error( `[FastMCP error] failed to send ${method} notification.\n\n${ error instanceof Error ? error.stack : JSON.stringify(error) }`, ); } } /** * Update the session's authentication context. * Called by mcp-proxy when a new token is validated on subsequent requests. */ public updateAuth(auth: T): void { this.#auth = auth; } public waitForReady(): Promise { 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", ), ); }, 5000); this.once("ready", () => { clearTimeout(timeout); resolve(); }); this.once("error", (event) => { clearTimeout(timeout); reject(event.error); }); }); } #getPingConfig(transport: Transport): { enabled: boolean; intervalMs: number; logLevel: LoggingLevel; } { const pingConfig = this.#pingConfig || {}; let defaultEnabled = false; if ("type" in transport) { // Enable by default for SSE and HTTP streaming if (transport.type === "httpStream") { defaultEnabled = true; } } return { enabled: pingConfig.enabled !== undefined ? pingConfig.enabled : defaultEnabled, intervalMs: pingConfig.intervalMs || 5000, logLevel: pingConfig.logLevel || "debug", }; } private addPrompt(inputPrompt: InputPrompt) { const completers: Record> = {}; const enums: Record = {}; const fuseInstances: Record> = {}; for (const argument of inputPrompt.arguments ?? []) { if (argument.complete) { completers[argument.name] = argument.complete; } if (argument.enum) { enums[argument.name] = argument.enum; fuseInstances[argument.name] = new Fuse(argument.enum, { includeScore: true, threshold: 0.3, // More flexible matching! }); } } const prompt = { ...inputPrompt, complete: async (name: string, value: string, auth?: T) => { 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.set(prompt.name, prompt); } private addResource(inputResource: Resource) { this.#resources.set(inputResource.uri, 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, auth?: T) => { if (completers[name]) { return await completers[name](value, auth); } if (inputResourceTemplate.complete) { return await inputResourceTemplate.complete(name, value, auth); } return { values: [], }; }, }; this.#resourceTemplates.set(resourceTemplate.name, resourceTemplate); } private setupCompleteHandlers() { this.#server.setRequestHandler(CompleteRequestSchema, async (request) => { if (request.params.ref.type === "ref/prompt") { const ref = request.params.ref; const prompt = "name" in ref && this.#prompts.get(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 && Array.from(this.#resourceTemplates.values()).find( (resource) => resource.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, }); }); } private setupErrorHandling() { this.#server.onerror = (error) => { this.#logger.error("[FastMCP error]", error); }; } private setupLoggingHandlers() { this.#server.setRequestHandler(SetLevelRequestSchema, (request) => { this.#loggingLevel = request.params.level; return {}; }); } private setupPromptHandlers() { let cachedPromptsList: ListPromptsResult["prompts"] | null = null; this.#server.setRequestHandler(ListPromptsRequestSchema, async () => { if (cachedPromptsList) { return { prompts: cachedPromptsList, }; } cachedPromptsList = Array.from(this.#prompts.values()).map((prompt) => { return { arguments: prompt.arguments, complete: prompt.complete, description: prompt.description, name: prompt.name, }; }); return { prompts: cachedPromptsList, }; }); this.#server.setRequestHandler(GetPromptRequestSchema, async (request) => { const prompt = this.#prompts.get(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, `Prompt '${request.params.name}' requires argument '${arg.name}': ${ arg.description || "No description provided" }`, ); } } let result: Awaited["load"]>>; try { result = await prompt.load( args as Record, this.#auth, ); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); throw new McpError( 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, }; } }); } private setupResourceHandlers() { let cachedResourcesList: ListResourcesResult["resources"] | null = null; this.#server.setRequestHandler(ListResourcesRequestSchema, async () => { if (cachedResourcesList) { return { resources: cachedResourcesList, }; } cachedResourcesList = Array.from(this.#resources.values()).map( (resource) => ({ description: resource.description, mimeType: resource.mimeType, name: resource.name, uri: resource.uri, }), ); return { resources: cachedResourcesList, }; }); this.#server.setRequestHandler( ReadResourceRequestSchema, async (request) => { if ("uri" in request.params) { const resource = this.#resources.get(request.params.uri); if (!resource) { for (const resourceTemplate of this.#resourceTemplates.values()) { 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, this.#auth); const resources = Array.isArray(result) ? result : [result]; return { contents: resources.map((resource) => ({ ...resource, description: resourceTemplate.description, mimeType: resource.mimeType ?? resourceTemplate.mimeType, name: resourceTemplate.name, uri: resource.uri ?? uri, })), }; } throw new McpError( ErrorCode.MethodNotFound, `Resource not found: '${request.params.uri}'. Available resources: ${ Array.from(this.#resources.values()) .map((r) => r.uri) .join(", ") || "none" }`, ); } if (!("uri" in resource)) { throw new UnexpectedStateError("Resource does not support reading"); } let maybeArrayResult: Awaited["load"]>>; try { maybeArrayResult = await resource.load(this.#auth); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); throw new McpError( 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: result.mimeType ?? resource.mimeType, name: resource.name, uri: result.uri ?? resource.uri, })), }; } throw new UnexpectedStateError("Unknown resource request", { request, }); }, ); } private setupResourceTemplateHandlers() { let cachedResourceTemplatesList: | ListResourceTemplatesResult["resourceTemplates"] | null = null; this.#server.setRequestHandler( ListResourceTemplatesRequestSchema, async () => { if (cachedResourceTemplatesList) { return { resourceTemplates: cachedResourceTemplatesList, }; } cachedResourceTemplatesList = Array.from( this.#resourceTemplates.values(), ).map((resourceTemplate) => ({ description: resourceTemplate.description, mimeType: resourceTemplate.mimeType, name: resourceTemplate.name, uriTemplate: resourceTemplate.uriTemplate, })); return { resourceTemplates: cachedResourceTemplatesList, }; }, ); } private setupRootsHandlers() { if (this.#rootsConfig?.enabled === false) { this.#logger.debug( "[FastMCP debug] roots capability explicitly disabled via config", ); return; } // Only set up roots notification handling if the server supports it 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 ) { this.#logger.debug( "[FastMCP debug] listRoots method not supported by client", ); } else { this.#logger.error( `[FastMCP error] received error listing roots.\n\n${ error instanceof Error ? error.stack : JSON.stringify(error) }`, ); } }); }, ); } else { this.#logger.debug( "[FastMCP debug] roots capability not available, not setting up notification handler", ); } } private setupToolHandlers(tools: Tool[]) { const toolsMap = new Map(tools.map((tool) => [tool.name, tool])); let cachedToolsList: ListToolsResult["tools"] | null = null; this.#server.setRequestHandler(ListToolsRequestSchema, async () => { if (cachedToolsList) { return { tools: cachedToolsList, }; } cachedToolsList = 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", }) as SDKTool["inputSchema"], name: tool.name, ...(tool.outputSchema && { outputSchema: (await toJsonSchema( tool.outputSchema, )) as SDKTool["inputSchema"], }), // Pass through _meta for MCP ext-apps UI support (issue #229) ...(tool._meta && { _meta: tool._meta }), }; }), ); return { tools: cachedToolsList, }; }); this.#server.setRequestHandler(CallToolRequestSchema, async (request) => { const tool = toolsMap.get(request.params.name); if (!tool) { throw new McpError( ErrorCode.MethodNotFound, `Unknown tool: ${request.params.name}`, ); } let args: unknown = undefined; if (tool.parameters) { const parsed = await tool.parameters["~standard"].validate( request.params.arguments, ); if (parsed.issues) { const friendlyErrors = this.#utils?.formatInvalidParamsErrorMessage ? this.#utils.formatInvalidParamsErrorMessage(parsed.issues) : parsed.issues .map((issue) => { const path = issue.path?.join(".") || "root"; return `${path}: ${issue.message}`; }) .join(", "); throw new McpError( 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 = request.params?._meta?.progressToken; let result: ContentResult; try { const reportProgress = async (progress: 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: string, context?: SerializableValue) => { this.#server.sendLoggingMessage({ data: { context, message, }, level: "debug", }); }, error: (message: string, context?: SerializableValue) => { this.#server.sendLoggingMessage({ data: { context, message, }, level: "error", }); }, info: (message: string, context?: SerializableValue) => { this.#server.sendLoggingMessage({ data: { context, message, }, level: "info", }); }, warn: (message: string, context?: SerializableValue) => { this.#server.sendLoggingMessage({ data: { context, message, }, level: "warning", }); }, }; // Create a promise for tool execution // Streams partial results while a tool is still executing // Enables progressive rendering and real-time feedback const streamContent = async (content: Content | 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 request.params?._meta?.requestId === "string" ? request.params._meta.requestId : undefined, session: this.#auth, sessionId: this.#sessionId, streamContent, }); // Handle timeout if specified 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); // If promise resolves first executeToolPromise.then( () => clearTimeout(timeoutId), () => clearTimeout(timeoutId), ); }), ]) : executeToolPromise)) as | AudioContent | ContentResult | ImageContent | null | ResourceContent | ResourceLink | string | TextContent | undefined; // Without this test, we are running into situations where the last progress update is not reported. // See the 'reports multiple progress updates without buffering' test in FastMCP.test.ts before refactoring. await delay(1); if (maybeStringResult === undefined || 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; }); } } /** * Converts camelCase to snake_case for OAuth endpoint responses */ function camelToSnakeCase(str: string): string { return str.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`); } /** * Converts an object with camelCase keys to snake_case keys */ function convertObjectToSnakeCase( obj: Record, ): Record { const result: Record = {}; for (const [key, value] of Object.entries(obj)) { const snakeKey = camelToSnakeCase(key); result[snakeKey] = value; } return result; } /** * Parses Basic auth header (RFC 6749 Section 2.3.1) */ function parseBasicAuthHeader( authHeader: string | undefined, ): { clientId: string; clientSecret: string } | null { const basicMatch = authHeader?.match(/^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 { return null; } } const FastMCPEventEmitterBase: { new (): StrictEventEmitter>; } = EventEmitter; class FastMCPEventEmitter extends FastMCPEventEmitterBase {} export class FastMCP< T extends FastMCPSessionAuth = FastMCPSessionAuth, > extends FastMCPEventEmitter { public get serverState(): ServerState { return this.#serverState; } public get sessions(): FastMCPSession[] { return this.#sessions; } #authenticate: Authenticate | undefined; #honoApp = new Hono(); #httpStreamServer: null | SSEServer = null; #logger: Logger; #options: ServerOptions; #prompts: InputPrompt[] = []; #resources: Resource[] = []; #resourcesTemplates: InputResourceTemplate[] = []; #serverState: ServerState = ServerState.Stopped; #sessions: FastMCPSession[] = []; #tools: Tool[] = []; constructor(public options: ServerOptions) { super(); this.#options = options; this.#logger = options.logger || console; // If auth provider is specified, use it to configure authenticate and oauth if (options.auth) { // Use auth provider's authenticate if not explicitly overridden if (!options.authenticate) { this.#authenticate = ((request: http.IncomingMessage | undefined) => options.auth!.authenticate(request)) as Authenticate; } else { this.#authenticate = options.authenticate; } // Use auth provider's oauth config if not explicitly overridden if (!options.oauth) { this.#options = { ...options, oauth: options.auth.getOAuthConfig(), }; } } else { this.#authenticate = options.authenticate; } } /** * Adds a prompt to the server. */ public addPrompt[]>( prompt: InputPrompt, ) { this.#prompts = this.#prompts.filter((p) => p.name !== prompt.name); this.#prompts.push(prompt); if (this.#serverState === ServerState.Running) { this.#promptsListChanged(this.#prompts); } } /** * Adds prompts to the server. */ public addPrompts[]>( prompts: InputPrompt[], ) { 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 === ServerState.Running) { this.#promptsListChanged(this.#prompts); } } /** * Adds a resource to the server. */ public addResource(resource: Resource) { this.#resources = this.#resources.filter((r) => r.name !== resource.name); this.#resources.push(resource); if (this.#serverState === ServerState.Running) { this.#resourcesListChanged(this.#resources); } } /** * Adds resources to the server. */ public addResources(resources: Resource[]) { 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 === ServerState.Running) { this.#resourcesListChanged(this.#resources); } } /** * Adds a resource template to the server. */ public addResourceTemplate< const Args extends InputResourceTemplateArgument[], >(resource: InputResourceTemplate) { this.#resourcesTemplates = this.#resourcesTemplates.filter( (t) => t.name !== resource.name, ); this.#resourcesTemplates.push(resource); if (this.#serverState === ServerState.Running) { this.#resourceTemplatesListChanged(this.#resourcesTemplates); } } /** * Adds resource templates to the server. */ public addResourceTemplates< const Args extends InputResourceTemplateArgument[], >(resources: InputResourceTemplate[]) { 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 === ServerState.Running) { this.#resourceTemplatesListChanged(this.#resourcesTemplates); } } /** * Adds a tool to the server. */ public addTool(tool: Tool) { // Remove existing tool with the same name this.#tools = this.#tools.filter((t) => t.name !== tool.name); this.#tools.push(tool as unknown as Tool); if (this.#serverState === ServerState.Running) { this.#toolsListChanged(this.#tools); } } /** * Adds tools to the server. */ public addTools(tools: Tool[]) { const newToolNames = new Set(tools.map((tool) => tool.name)); this.#tools = this.#tools.filter((t) => !newToolNames.has(t.name)); this.#tools.push(...(tools as unknown as Tool[])); if (this.#serverState === ServerState.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 - The embedded resource content */ public async embedded(uri: string): Promise { // First, try to find a direct resource match 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: ResourceContent["resource"] = { mimeType: directResource.mimeType, uri, }; if ("text" in firstResult) { resourceData.text = firstResult.text; } if ("blob" in firstResult) { resourceData.blob = firstResult.blob; } return resourceData; } // Try to match against resource templates for (const template of this.#resourcesTemplates) { const parsedTemplate = parseURITemplate(template.uriTemplate); const params = parsedTemplate.fromUri(uri); if (!params) { continue; } const result = await template.load( params as ResourceTemplateArgumentsToObject, ); const resourceData: ResourceContent["resource"] = { mimeType: template.mimeType, uri, }; if ("text" in result) { resourceData.text = result.text; } if ("blob" in result) { resourceData.blob = result.blob; } return resourceData; // The resource we're looking for } throw new UnexpectedStateError(`Resource not found: ${uri}`, { uri }); } /** * Returns the underlying Hono app instance for direct access to Hono's native API. * This allows you to add custom routes, middleware, and handlers using Hono's standard methods. * * @returns The Hono app instance * * @example * ```typescript * const app = server.getApp(); * * // Add routes using native Hono API * app.get('/api/users', async (c) => { * return c.json({ users: [] }); * }); * * app.post('/api/users/:id', async (c) => { * const id = c.req.param('id'); * return c.json({ id }); * }); * ``` */ public getApp(): Hono { return this.#honoApp; } /** * Removes a prompt from the server. */ public removePrompt(name: string) { this.#prompts = this.#prompts.filter((p) => p.name !== name); if (this.#serverState === ServerState.Running) { this.#promptsListChanged(this.#prompts); } } /** * Removes prompts from the server. */ public removePrompts(names: string[]) { for (const name of names) { this.#prompts = this.#prompts.filter((p) => p.name !== name); } if (this.#serverState === ServerState.Running) { this.#promptsListChanged(this.#prompts); } } /** * Removes a resource from the server. */ public removeResource(name: string) { this.#resources = this.#resources.filter((r) => r.name !== name); if (this.#serverState === ServerState.Running) { this.#resourcesListChanged(this.#resources); } } /** * Removes resources from the server. */ public removeResources(names: string[]) { for (const name of names) { this.#resources = this.#resources.filter((r) => r.name !== name); } if (this.#serverState === ServerState.Running) { this.#resourcesListChanged(this.#resources); } } /** * Removes a resource template from the server. */ public removeResourceTemplate(name: string) { this.#resourcesTemplates = this.#resourcesTemplates.filter( (t) => t.name !== name, ); if (this.#serverState === ServerState.Running) { this.#resourceTemplatesListChanged(this.#resourcesTemplates); } } /** * Removes resource templates from the server. */ public removeResourceTemplates(names: string[]) { for (const name of names) { this.#resourcesTemplates = this.#resourcesTemplates.filter( (t) => t.name !== name, ); } if (this.#serverState === ServerState.Running) { this.#resourceTemplatesListChanged(this.#resourcesTemplates); } } /** * Removes a tool from the server. */ public removeTool(name: string) { // Remove existing tool with the same name this.#tools = this.#tools.filter((t) => t.name !== name); if (this.#serverState === ServerState.Running) { this.#toolsListChanged(this.#tools); } } /** * Removes tools from the server. */ public removeTools(names: string[]) { for (const name of names) { this.#tools = this.#tools.filter((t) => t.name !== name); } if (this.#serverState === ServerState.Running) { this.#toolsListChanged(this.#tools); } } /** * Starts the server. */ public async start( options?: Partial<{ httpStream: { enableJsonResponse?: boolean; endpoint?: `/${string}`; eventStore?: EventStore; host?: string; port: number; sslCa?: string; sslCert?: string; sslKey?: string; stateless?: boolean; }; transportType: "httpStream" | "stdio"; }>, ) { const config = this.#parseRuntimeConfig(options); if (config.transportType === "stdio") { const transport = new StdioServerTransport(); // For stdio transport, if authenticate function is provided, call it // with undefined request (since stdio doesn't have HTTP request context) let auth: T | undefined; if (this.#authenticate) { try { auth = await this.#authenticate( undefined as unknown as http.IncomingMessage, ); } catch (error) { this.#logger.error( "[FastMCP error] Authentication failed for stdio transport:", error instanceof Error ? error.message : String(error), ); // Continue without auth if authentication fails } } 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); }); // Monitor the underlying transport for close events 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: session as FastMCPSession, }); this.#serverState = ServerState.Running; } else if (config.transportType === "httpStream") { const httpConfig = config.httpStream; const protocol = httpConfig.sslCert || httpConfig.sslKey ? "https" : "http"; if (httpConfig.stateless) { // Stateless mode - create new server instance for each request this.#logger.info( `[FastMCP info] Starting server in stateless mode on HTTP Stream at ${protocol}://${httpConfig.host}:${httpConfig.port}${httpConfig.endpoint}`, ); this.#httpStreamServer = await startHTTPServer>({ ...(this.#authenticate ? { authenticate: this.#authenticate } : {}), createServer: async (request) => { let auth: T | undefined; if (this.#authenticate) { auth = await this.#authenticate(request); // In stateless mode, authentication is REQUIRED // mcp-proxy will catch this error and return 401 if (auth === undefined || auth === null) { throw new Error("Authentication required"); } } // Extract session ID from headers const sessionId = Array.isArray(request.headers["mcp-session-id"]) ? request.headers["mcp-session-id"][0] : request.headers["mcp-session-id"]; // In stateless mode, create a new session for each request // without persisting it in the sessions array return this.#createSession(auth, sessionId); }, enableJsonResponse: httpConfig.enableJsonResponse, eventStore: httpConfig.eventStore, host: httpConfig.host, ...(this.#options.oauth?.enabled && this.#options.oauth.protectedResource?.resource ? { oauth: { protectedResource: { resource: this.#options.oauth.protectedResource.resource, }, }, } : {}), // In stateless mode, we don't track sessions onClose: async () => { // No session tracking in stateless mode }, onConnect: async () => { // No persistent session tracking in stateless mode 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, sslCa: httpConfig.sslCa, sslCert: httpConfig.sslCert, sslKey: httpConfig.sslKey, stateless: true, streamEndpoint: httpConfig.endpoint, }); } else { // Regular mode with session management this.#httpStreamServer = await startHTTPServer>({ ...(this.#authenticate ? { authenticate: this.#authenticate } : {}), createServer: async (request) => { let auth: T | undefined; if (this.#authenticate) { auth = await this.#authenticate(request); } // Extract session ID from headers 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, ...(this.#options.oauth?.enabled && this.#options.oauth.protectedResource?.resource ? { oauth: { protectedResource: { resource: this.#options.oauth.protectedResource.resource, }, }, } : {}), onClose: async (session) => { const sessionIndex = this.#sessions.indexOf(session); if (sessionIndex !== -1) this.#sessions.splice(sessionIndex, 1); this.emit("disconnect", { session: session as FastMCPSession, }); }, onConnect: async (session) => { this.#sessions.push(session); this.#logger.info(`[FastMCP info] HTTP Stream session established`); this.emit("connect", { session: session as FastMCPSession, }); }, onUnhandledRequest: async (req, res) => { await this.#handleUnhandledRequest( req, res, false, httpConfig.host, httpConfig.endpoint, ); }, port: httpConfig.port, sslCa: httpConfig.sslCa, sslCert: httpConfig.sslCert, sslKey: httpConfig.sslKey, stateless: httpConfig.stateless, streamEndpoint: httpConfig.endpoint, }); this.#logger.info( `[FastMCP info] server is running on HTTP Stream at ${protocol}://${httpConfig.host}:${httpConfig.port}${httpConfig.endpoint}`, ); } this.#serverState = ServerState.Running; } else { throw new Error("Invalid transport type"); } } /** * Stops the server. */ public async stop() { if (this.#httpStreamServer) { await this.#httpStreamServer.close(); } this.#serverState = ServerState.Stopped; } /** * Creates a new FastMCPSession instance with the current configuration. * Used both for regular sessions and stateless requests. */ #createSession(auth?: T, sessionId?: string): FastMCPSession { // Check if authentication failed if ( auth && typeof auth === "object" && "authenticated" in auth && !(auth as { authenticated: unknown }).authenticated ) { const errorMessage = "error" in auth && typeof (auth as { error: unknown }).error === "string" ? (auth as { error: string }).error : "Authentication failed"; throw new Error(errorMessage); } const allowedTools = auth ? this.#tools.filter((tool) => tool.canAccess ? tool.canAccess(auth) : true, ) : this.#tools; return 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, sessionId, tools: allowedTools, transportType: "httpStream", utils: this.#options.utils, version: this.#options.version, }); } /** * Handles unhandled HTTP requests with health, readiness, OAuth endpoints, and custom routes */ #handleUnhandledRequest = async ( req: http.IncomingMessage, res: http.ServerResponse, isStateless = false, host: string, streamEndpoint?: string, ) => { const url = new URL(req.url || "", `http://${host}`); // Try Hono routes first - users may have added routes via getApp() try { // Convert Node.js IncomingMessage to Web Request const webRequest = this.#nodeRequestToWebRequest(req, url); // Call Hono's fetch handler const honoResponse = await this.#honoApp.fetch(webRequest, { incoming: req, outgoing: res, }); // If Hono handled it (not 404), write response and return if (honoResponse.status !== 404) { // Write Hono response to Node.js response if (!res.headersSent) { res.statusCode = honoResponse.status; honoResponse.headers.forEach((value, key) => { res.setHeader(key, value); }); if (honoResponse.body) { const reader = honoResponse.body.getReader(); try { while (true) { const { done, value } = await reader.read(); if (done) break; res.write(value); } } finally { reader.releaseLock(); } } res.end(); } return; } } catch (error) { // If Hono throws, log and continue to other endpoints this.#logger.debug("[FastMCP debug] Hono route not matched", error); } const healthConfig = this.#options.health ?? {}; const enabled = healthConfig.enabled === undefined ? true : healthConfig.enabled; if (enabled) { const path = healthConfig.path ?? "/health"; const url = new URL(req.url || "", `http://${host}`); try { if (req.method === "GET" && url.pathname === path) { res .writeHead(healthConfig.status ?? 200, { "Content-Type": "text/plain", }) .end(healthConfig.message ?? "✓ Ok"); return; } // Enhanced readiness check endpoint if (req.method === "GET" && url.pathname === "/ready") { if (isStateless) { // In stateless mode, we're always ready if the server is running const response = { mode: "stateless", ready: 1, status: "ready", total: 1, }; res .writeHead(200, { "Content-Type": "application/json", }) .end(JSON.stringify(response)); } else { const readySessions = this.#sessions.filter( (s) => s.isReady, ).length; const totalSessions = this.#sessions.length; const allReady = readySessions === totalSessions && totalSessions > 0; const response = { ready: readySessions, status: allReady ? "ready" : totalSessions === 0 ? "no_sessions" : "initializing", total: totalSessions, }; res .writeHead(allReady ? 200 : 503, { "Content-Type": "application/json", }) .end(JSON.stringify(response)); } return; } } catch (error) { this.#logger.error("[FastMCP error] health endpoint error", error); } } // Handle OAuth well-known endpoints const oauthConfig = this.#options.oauth; if (oauthConfig?.enabled && req.method === "GET") { const url = new URL(req.url || "", `http://${host}`); if ( url.pathname === "/.well-known/oauth-authorization-server" && oauthConfig.authorizationServer ) { const metadata = convertObjectToSnakeCase( oauthConfig.authorizationServer, ); res .writeHead(200, { "Content-Type": "application/json", }) .end(JSON.stringify(metadata)); return; } // Handle Protected Resource Metadata with MCP 2025-11-25 compliant discovery // Per spec, clients should search in order: // 1. WWW-Authenticate header (handled by mcp-proxy) // 2. /.well-known/oauth-protected-resource (e.g., /mcp) // 3. /.well-known/oauth-protected-resource (root) if (oauthConfig.protectedResource) { const wellKnownBase = "/.well-known/oauth-protected-resource"; let shouldServeMetadata = false; // Check for sub-path variant first (higher priority per MCP spec) if ( streamEndpoint && url.pathname === `${wellKnownBase}${streamEndpoint}` ) { shouldServeMetadata = true; } // Fall back to root path else if (url.pathname === wellKnownBase) { shouldServeMetadata = true; } if (shouldServeMetadata) { const metadata = convertObjectToSnakeCase( oauthConfig.protectedResource, ); res .writeHead(200, { "Content-Type": "application/json", }) .end(JSON.stringify(metadata)); return; } } } // Handle OAuth Proxy endpoints const oauthProxy = oauthConfig?.proxy; if (oauthProxy && oauthConfig?.enabled) { const url = new URL(req.url || "", `http://${host}`); try { // DCR endpoint - POST /oauth/register if (req.method === "POST" && url.pathname === "/oauth/register") { let body = ""; req.on("data", (chunk) => (body += chunk)); req.on("end", async () => { try { const request = JSON.parse(body); const response = await oauthProxy.registerClient(request); res .writeHead(201, { "Content-Type": "application/json" }) .end(JSON.stringify(response)); } catch (error) { const statusCode = (error as { statusCode?: number }).statusCode || 400; res .writeHead(statusCode, { "Content-Type": "application/json" }) .end( JSON.stringify( (error as { toJSON?: () => unknown }).toJSON?.() || { error: "invalid_request", }, ), ); } }); return; } // Authorization endpoint - GET /oauth/authorize if (req.method === "GET" && url.pathname === "/oauth/authorize") { try { const params = Object.fromEntries(url.searchParams.entries()); const response = await oauthProxy.authorize( params as { [key: string]: unknown; client_id: string; redirect_uri: string; response_type: string; }, ); // Response is a redirect const location = response.headers.get("Location"); if (location) { res.writeHead(response.status, { Location: location }).end(); } else { // HTML consent screen const html = await response.text(); res .writeHead(response.status, { "Content-Type": "text/html" }) .end(html); } } catch (error) { res.writeHead(400, { "Content-Type": "application/json" }).end( JSON.stringify( (error as { toJSON?: () => unknown }).toJSON?.() || { error: "invalid_request", }, ), ); } return; } // Callback endpoint - GET /oauth/callback if (req.method === "GET" && url.pathname === "/oauth/callback") { try { const mockRequest = new Request(`http://${host}${req.url}`); const response = await oauthProxy.handleCallback(mockRequest); const location = response.headers.get("Location"); if (location) { res.writeHead(response.status, { Location: location }).end(); } else { const text = await response.text(); res.writeHead(response.status).end(text); } } catch (error) { res.writeHead(400, { "Content-Type": "application/json" }).end( JSON.stringify( (error as { toJSON?: () => unknown }).toJSON?.() || { error: "server_error", }, ), ); } return; } // Consent endpoint - POST /oauth/consent if (req.method === "POST" && url.pathname === "/oauth/consent") { let body = ""; req.on("data", (chunk) => (body += chunk)); req.on("end", async () => { try { const mockRequest = new Request(`http://${host}/oauth/consent`, { body, headers: { "Content-Type": "application/x-www-form-urlencoded", }, method: "POST", }); const response = await oauthProxy.handleConsent(mockRequest); const location = response.headers.get("Location"); if (location) { res.writeHead(response.status, { Location: location }).end(); } else { const text = await response.text(); res.writeHead(response.status).end(text); } } catch (error) { res.writeHead(400, { "Content-Type": "application/json" }).end( JSON.stringify( (error as { toJSON?: () => unknown }).toJSON?.() || { error: "server_error", }, ), ); } }); return; } // Token endpoint - POST /oauth/token if (req.method === "POST" && url.pathname === "/oauth/token") { let body = ""; req.on("data", (chunk) => (body += chunk)); req.on("end", async () => { try { const params = new URLSearchParams(body); const grantType = params.get("grant_type"); // Parse Basic auth header (RFC 6749 Section 2.3.1) const basicAuth = parseBasicAuthHeader(req.headers.authorization); // Use Basic auth credentials if present, otherwise fall back to POST body const clientId = basicAuth?.clientId || params.get("client_id") || ""; const clientSecret = basicAuth?.clientSecret ?? params.get("client_secret") ?? undefined; let response; if (grantType === "authorization_code") { response = await oauthProxy.exchangeAuthorizationCode({ client_id: clientId, client_secret: clientSecret, code: params.get("code") || "", code_verifier: params.get("code_verifier") || undefined, grant_type: "authorization_code", redirect_uri: params.get("redirect_uri") || "", }); } else if (grantType === "refresh_token") { response = await oauthProxy.exchangeRefreshToken({ client_id: clientId, client_secret: clientSecret, grant_type: "refresh_token", refresh_token: params.get("refresh_token") || "", scope: params.get("scope") || undefined, }); } else { throw { statusCode: 400, toJSON: () => ({ error: "unsupported_grant_type" }), }; } res .writeHead(200, { "Content-Type": "application/json" }) .end(JSON.stringify(response)); } catch (error) { const statusCode = (error as { statusCode?: number }).statusCode || 400; res .writeHead(statusCode, { "Content-Type": "application/json" }) .end( JSON.stringify( (error as { toJSON?: () => unknown }).toJSON?.() || { error: "invalid_request", }, ), ); } }); return; } } catch (error) { this.#logger.error("[FastMCP error] OAuth Proxy endpoint error", error); res.writeHead(500).end(); return; } } // If the request was not handled above, return 404 res.writeHead(404).end(); }; /** * Converts Node.js IncomingMessage to Web Request for Hono */ #nodeRequestToWebRequest(req: http.IncomingMessage, url: URL): Request { const method = req.method || "GET"; // Build headers const headers = new Headers(); for (const [key, value] of Object.entries(req.headers)) { if (value) { if (Array.isArray(value)) { for (const v of value) { headers.append(key, v); } } else { headers.set(key, value); } } } // Create Web Request // For methods that can have a body, we need to pass the body const hasBody = method !== "GET" && method !== "HEAD"; if (hasBody) { return new Request(url.toString(), { // eslint-disable-next-line @typescript-eslint/no-explicit-any body: req as any, // Node.js IncomingMessage is readable stream duplex: "half", // Required for streaming bodies headers, method, } as RequestInit); } else { return new Request(url.toString(), { headers, method, }); } } #parseRuntimeConfig( overrides?: Partial<{ httpStream: { enableJsonResponse?: boolean; endpoint?: `/${string}`; eventStore?: EventStore; host?: string; port: number; sslCa?: string; sslCert?: string; sslKey?: string; stateless?: boolean; }; transportType: "httpStream" | "stdio"; }>, ): | { httpStream: { enableJsonResponse?: boolean; endpoint: `/${string}`; eventStore?: EventStore; host: string; port: number; sslCa?: string; sslCert?: string; sslKey?: string; stateless?: boolean; }; transportType: "httpStream"; } | { transportType: "stdio" } { const args = process.argv.slice(2); const getArg = (name: string) => { const index = args.findIndex((arg) => arg === `--${name}`); return index !== -1 && index + 1 < args.length ? args[index + 1] : undefined; }; const transportArg = getArg("transport"); const portArg = getArg("port"); const endpointArg = getArg("endpoint"); const statelessArg = getArg("stateless"); const hostArg = getArg("host"); const envTransport = process.env.FASTMCP_TRANSPORT; const envPort = process.env.FASTMCP_PORT; const envEndpoint = process.env.FASTMCP_ENDPOINT; const envStateless = process.env.FASTMCP_STATELESS; const envHost = process.env.FASTMCP_HOST; // Overrides > CLI > env > defaults const transportType = overrides?.transportType || (transportArg === "http-stream" ? "httpStream" : transportArg) || envTransport || "stdio"; if (transportType === "httpStream") { const port = parseInt( overrides?.httpStream?.port?.toString() || portArg || envPort || "8080", ); const host = overrides?.httpStream?.host || hostArg || envHost || "localhost"; const endpoint = overrides?.httpStream?.endpoint || endpointArg || envEndpoint || "/mcp"; const enableJsonResponse = overrides?.httpStream?.enableJsonResponse || false; const stateless = overrides?.httpStream?.stateless || statelessArg === "true" || envStateless === "true" || false; const eventStore = overrides?.httpStream?.eventStore; const sslCa = overrides?.httpStream?.sslCa; const sslCert = overrides?.httpStream?.sslCert; const sslKey = overrides?.httpStream?.sslKey; return { httpStream: { enableJsonResponse, endpoint: endpoint as `/${string}`, eventStore, host, port, sslCa, sslCert, sslKey, stateless, }, transportType: "httpStream" as const, }; } return { transportType: "stdio" as const }; } /** * Notifies all sessions that the prompts list has changed. */ #promptsListChanged(prompts: Prompt[]) { for (const session of this.#sessions) { session.promptsListChanged(prompts); } } #removeSession(session: FastMCPSession): void { const sessionIndex = this.#sessions.indexOf(session); if (sessionIndex !== -1) { this.#sessions.splice(sessionIndex, 1); this.emit("disconnect", { session: session as FastMCPSession, }); } } /** * Notifies all sessions that the resources list has changed. */ #resourcesListChanged(resources: Resource[]) { for (const session of this.#sessions) { session.resourcesListChanged(resources); } } /** * Notifies all sessions that the resource templates list has changed. */ #resourceTemplatesListChanged(templates: InputResourceTemplate[]) { for (const session of this.#sessions) { session.resourceTemplatesListChanged(templates); } } /** * Notifies all sessions that the tools list has changed. */ #toolsListChanged(tools: Tool[]) { for (const session of this.#sessions) { session.toolsListChanged(tools); } } } // Re-export commonly used auth utilities for convenience // Users can also import from "fastmcp/auth" for the full auth module export { // Auth providers AuthProvider, AzureProvider, // Auth helpers for canAccess getAuthSession, GitHubProvider, GoogleProvider, OAuthProvider, requireAll, requireAny, requireAuth, requireRole, requireScopes, } from "./auth/index.js"; export type { AuthProviderConfig, AzureProviderConfig, AzureSession, GenericOAuthProviderConfig, GitHubSession, GoogleSession, OAuthSession, } from "./auth/index.js"; export { DiscoveryDocumentCache } from "./DiscoveryDocumentCache.js"; export type { AudioContent, Content, ContentResult, Context, FastMCPEvents, FastMCPSessionAuth, FastMCPSessionEvents, ImageContent, InputPrompt, InputPromptArgument, LoggingLevel, Progress, Prompt, PromptArgument, Resource, ResourceContent, ResourceLink, ResourceResult, ResourceTemplate, ResourceTemplateArgument, SerializableValue, ServerOptions, TextContent, Tool, ToolParameters, };