From 081543c4d30619853053aa1b322c1e3d3514bd32 Mon Sep 17 00:00:00 2001 From: Matias Date: Mon, 20 Oct 2025 14:06:54 -0300 Subject: [PATCH 1/7] feat: add request.params._meta to Context --- README.md | 50 +++++++++- src/FastMCP.test.ts | 226 ++++++++++++++++++++++++++++++++++++++++---- src/FastMCP.ts | 17 +++- 3 files changed, 272 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index ca6aac9..679f29f 100644 --- a/README.md +++ b/README.md @@ -771,7 +771,53 @@ The `log` object has the following methods: #### Errors -The errors that are meant to be shown to the user should be thrown as `UserError` instances: +FastMCP supports two ways to handle errors in tool execution: + +##### MCP Errors (Recommended) + +For standards-compliant error handling, throw `McpError` with appropriate error codes: + +```js +import { ErrorCode, McpError } from "fastmcp"; + +server.addTool({ + name: "download", + description: "Download a file", + parameters: z.object({ + url: z.string(), + }), + execute: async (args) => { + if (args.url.startsWith("https://example.com")) { + // Throw MCP error with InvalidParams code + throw new McpError(ErrorCode.InvalidParams, "This URL is not allowed"); + } + + // Throw MCP error with custom data + if (!urlExists(args.url)) { + throw new McpError(ErrorCode.InvalidRequest, "Resource not found", { + url: args.url, + statusCode: 404, + }); + } + + return "done"; + }, +}); +``` + +**Available Error Codes:** + +- `ErrorCode.InvalidParams` - Invalid parameters provided +- `ErrorCode.InvalidRequest` - Invalid request +- `ErrorCode.InternalError` - Internal server error +- `ErrorCode.MethodNotFound` - Method/resource not found +- And other standard JSON-RPC error codes + +When a tool throws `McpError`, it's propagated through the MCP protocol as a proper JSON-RPC error, allowing clients to handle different error types appropriately. + +##### User Errors (Legacy) + +For backward compatibility, you can still use `UserError` for simple error messages: ```js import { UserError } from "fastmcp"; @@ -792,6 +838,8 @@ server.addTool({ }); ``` +`UserError` errors are converted to tool responses with `isError: true` and are displayed to the user as text content. + #### Progress Tools can report progress by calling `reportProgress` in the context object: diff --git a/src/FastMCP.test.ts b/src/FastMCP.test.ts index aa8ea3b..267c1eb 100644 --- a/src/FastMCP.test.ts +++ b/src/FastMCP.test.ts @@ -486,6 +486,132 @@ test("handles UserError errors with extras", async () => { }); }); +test("tool can throw McpError with InvalidParams error code", async () => { + await runWithTestServer({ + run: async ({ client }) => { + try { + await client.callTool({ + arguments: { value: "invalid" }, + name: "validate", + }); + throw new Error("Expected error to be thrown"); + } catch (error) { + expect(error).toBeInstanceOf(McpError); + + // @ts-expect-error - we know that error is an McpError + expect(error.code).toBe(ErrorCode.InvalidParams); + + // @ts-expect-error - we know that error is an McpError + expect(error.message).toContain("Invalid value provided"); + } + }, + server: async () => { + const server = new FastMCP({ + name: "Test", + version: "1.0.0", + }); + + server.addTool({ + description: "Validate input", + execute: async () => { + throw new McpError(ErrorCode.InvalidParams, "Invalid value provided"); + }, + name: "validate", + parameters: z.object({ value: z.string() }), + }); + + return server; + }, + }); +}); + +test("tool can throw McpError with InternalError error code", async () => { + await runWithTestServer({ + run: async ({ client }) => { + try { + await client.callTool({ + arguments: { value: "test" }, + name: "process", + }); + throw new Error("Expected error to be thrown"); + } catch (error) { + expect(error).toBeInstanceOf(McpError); + + // @ts-expect-error - we know that error is an McpError + expect(error.code).toBe(ErrorCode.InternalError); + + // @ts-expect-error - we know that error is an McpError + expect(error.message).toContain("Internal processing error"); + } + }, + server: async () => { + const server = new FastMCP({ + name: "Test", + version: "1.0.0", + }); + + server.addTool({ + description: "Process data", + execute: async () => { + throw new McpError( + ErrorCode.InternalError, + "Internal processing error", + ); + }, + name: "process", + parameters: z.object({ value: z.string() }), + }); + + return server; + }, + }); +}); + +test("tool can throw McpError with custom data", async () => { + await runWithTestServer({ + run: async ({ client }) => { + try { + await client.callTool({ + arguments: { id: "123" }, + name: "find", + }); + throw new Error("Expected error to be thrown"); + } catch (error) { + expect(error).toBeInstanceOf(McpError); + + // @ts-expect-error - we know that error is an McpError + expect(error.code).toBe(ErrorCode.InvalidRequest); + + // @ts-expect-error - we know that error is an McpError + expect(error.message).toContain("Resource not found"); + + // Note: Custom data may not be preserved through the MCP SDK transport layer + // The important part is that the error code and message are correct + } + }, + server: async () => { + const server = new FastMCP({ + name: "Test", + version: "1.0.0", + }); + + server.addTool({ + description: "Find resource", + execute: async (args) => { + throw new McpError(ErrorCode.InvalidRequest, "Resource not found", { + available: ["456", "789"], + id: args.id, + }); + }, + name: "find", + parameters: z.object({ id: z.string() }), + }); + + return server; + }, + }); +}); + test("calling an unknown tool throws McpError with MethodNotFound code", async () => { await runWithTestServer({ run: async ({ client }) => { @@ -570,6 +696,72 @@ test("tracks tool progress", async () => { }); }); +test("provides requestMetadata to tool context", async () => { + let capturedMetadata: Record | undefined = undefined; + const metadata = { foo: "bar" }; + + await runWithTestServer({ + run: async ({ client }) => { + await client.callTool({ + _meta: metadata, + name: "metadata-test", + }); + + expect(capturedMetadata).toBeDefined(); + expect(capturedMetadata).toEqual(metadata); + }, + server: async () => { + const server = new FastMCP({ + name: "Test", + version: "1.0.0", + }); + + server.addTool({ + execute: async (_args, context) => { + capturedMetadata = context.requestMetadata; + return "success"; + }, + name: "metadata-test", + }); + + return server; + }, + }); +}); + +test("allows tools to return _meta in CallToolResult", async () => { + const expectedMeta = { customField: "customValue", timestamp: 1234567890 }; + + await runWithTestServer({ + run: async ({ client }) => { + const result = await client.callTool({ + name: "meta-result-test", + }); + + expect(result._meta).toBeDefined(); + expect(result._meta).toEqual(expectedMeta); + }, + server: async () => { + const server = new FastMCP({ + name: "Test", + version: "1.0.0", + }); + + server.addTool({ + execute: async () => { + return { + _meta: expectedMeta, + content: [{ text: "success", type: "text" }], + }; + }, + name: "meta-result-test", + }); + + return server; + }, + }); +}); + test( "reports multiple progress updates without buffering", { @@ -667,26 +859,24 @@ test("sets logging levels", async () => { test("handles tool timeout", async () => { await runWithTestServer({ run: async ({ client }) => { - const result = await client.callTool({ - arguments: { - a: 1500, - b: 2, - }, - name: "add", - }); - - expect(result.isError).toBe(true); - - const result_typed = result as ContentResult; - - expect(Array.isArray(result_typed.content)).toBe(true); - expect(result_typed.content.length).toBe(1); + try { + await client.callTool({ + arguments: { + a: 1500, + b: 2, + }, + name: "add", + }); + throw new Error("Expected timeout error to be thrown"); + } catch (error) { + expect(error).toBeInstanceOf(McpError); - const firstItem = result_typed.content[0] as TextContent; + // @ts-expect-error - we know that error is an McpError + expect(error.code).toBe(ErrorCode.InternalError); - expect(firstItem.type).toBe("text"); - expect(firstItem.text).toBeDefined(); - expect(firstItem.text).toContain("timed out"); + // @ts-expect-error - we know that error is an McpError + expect(error.message).toContain("timed out"); + } }, server: async () => { const server = new FastMCP({ diff --git a/src/FastMCP.ts b/src/FastMCP.ts index 5e3f7bc..72ba505 100644 --- a/src/FastMCP.ts +++ b/src/FastMCP.ts @@ -19,6 +19,7 @@ import { ListToolsRequestSchema, McpError, ReadResourceRequestSchema, + RequestMeta, ResourceLink, Root, RootsListChangedNotificationSchema, @@ -215,6 +216,7 @@ type Context = { * Available for all transports when the client provides it. */ requestId?: string; + requestMetadata?: RequestMeta; session: T | undefined; /** * Session ID from the Mcp-Session-Id header. @@ -371,12 +373,14 @@ const ContentZodSchema = z.discriminatedUnion("type", [ ]) satisfies z.ZodType; type ContentResult = { + _meta?: Record; content: Content[]; isError?: boolean; }; const ContentResultZodSchema = z .object({ + _meta: z.record(z.unknown()).optional(), content: ContentZodSchema.array(), isError: z.boolean().optional(), }) @@ -1819,7 +1823,6 @@ export class FastMCPSession< ); } }; - const executeToolPromise = tool.execute(args, { client: { version: this.#server.getClientVersion(), @@ -1830,6 +1833,7 @@ export class FastMCPSession< typeof request.params?._meta?.requestId === "string" ? request.params._meta.requestId : undefined, + requestMetadata: request.params._meta, session: this.#auth, sessionId: this.#sessionId, streamContent, @@ -1842,7 +1846,8 @@ export class FastMCPSession< new Promise((_, reject) => { const timeoutId = setTimeout(() => { reject( - new UserError( + new McpError( + ErrorCode.InternalError, `Tool '${request.params.name}' timed out after ${tool.timeoutMs}ms. Consider increasing timeoutMs or optimizing the tool implementation.`, ), ); @@ -1883,6 +1888,11 @@ export class FastMCPSession< result = ContentResultZodSchema.parse(maybeStringResult); } } catch (error) { + // Re-throw McpError to let the MCP SDK handle it as a proper JSON-RPC error + if (error instanceof McpError) { + throw error; + } + if (error instanceof UserError) { return { content: [{ text: error.message, type: "text" }], @@ -2520,6 +2530,8 @@ export class FastMCP< } } +export { ErrorCode, McpError } from "@modelcontextprotocol/sdk/types.js"; + export type { AudioContent, Content, @@ -2534,6 +2546,7 @@ export type { Progress, Prompt, PromptArgument, + RequestMeta, Resource, ResourceContent, ResourceLink, From 6446ea7b56eb291a6d4d7321e4a41ff0aacba1ef Mon Sep 17 00:00:00 2001 From: Matias Date: Tue, 21 Oct 2025 08:14:48 -0300 Subject: [PATCH 2/7] chore: bump version, include build --- dist/FastMCP.d.ts | 653 ++++++++++++++++++ dist/FastMCP.js | 1401 +++++++++++++++++++++++++++++++++++++++ dist/FastMCP.js.map | 1 + dist/bin/fastmcp.d.ts | 1 + dist/bin/fastmcp.js | 152 +++++ dist/bin/fastmcp.js.map | 1 + package.json | 4 +- pnpm-lock.yaml | 26 +- 8 files changed, 2235 insertions(+), 4 deletions(-) create mode 100644 dist/FastMCP.d.ts create mode 100644 dist/FastMCP.js create mode 100644 dist/FastMCP.js.map create mode 100644 dist/bin/fastmcp.d.ts create mode 100755 dist/bin/fastmcp.js create mode 100644 dist/bin/fastmcp.js.map diff --git a/dist/FastMCP.d.ts b/dist/FastMCP.d.ts new file mode 100644 index 0000000..0e6931d --- /dev/null +++ b/dist/FastMCP.d.ts @@ -0,0 +1,653 @@ +import { Server } from '@modelcontextprotocol/sdk/server/index.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 { ResourceLink, RequestMeta, Root, ClientCapabilities, GetPromptResult, CreateMessageRequestSchema } from '@modelcontextprotocol/sdk/types.js'; +export { ErrorCode, McpError, RequestMeta, ResourceLink } from '@modelcontextprotocol/sdk/types.js'; +import { StandardSchemaV1 } from '@standard-schema/spec'; +import { EventEmitter } from 'events'; +import http from 'http'; +import { StrictEventEmitter } from 'strict-event-emitter-types'; +import { z } from 'zod'; + +interface Logger { + debug(...args: unknown[]): void; + error(...args: unknown[]): void; + info(...args: unknown[]): void; + log(...args: unknown[]): void; + warn(...args: unknown[]): void; +} +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; +}; +declare const imageContent: (input: { + buffer: Buffer; +} | { + path: string; +} | { + url: string; +}) => Promise; +declare const audioContent: (input: { + buffer: Buffer; +} | { + path: string; +} | { + url: string; +}) => Promise; +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; + requestMetadata?: RequestMeta; + 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; +declare abstract class FastMCPError extends Error { + constructor(message?: string); +} +declare class UnexpectedStateError extends FastMCPError { + extras?: Extras; + constructor(message: string, extras?: Extras); +} +/** + * An error that is meant to be surfaced to the user. + */ +declare class UserError extends UnexpectedStateError { +} +type ImageContent = { + data: string; + mimeType: string; + type: "image"; +}; +type AudioContent = { + data: string; + mimeType: string; + type: "audio"; +}; +type ResourceContent = { + resource: { + blob?: string; + mimeType?: string; + text?: string; + uri: string; + }; + type: "resource"; +}; +type Content = AudioContent | ImageContent | ResourceContent | ResourceLink | TextContent; +type ContentResult = { + _meta?: Record; + content: Content[]; + isError?: boolean; +}; +type Completion = { + hasMore?: boolean; + total?: number; + values: string[]; +}; +type ArgumentValueCompleter = (value: string, auth?: T) => Promise; +type InputPrompt[] = InputPromptArgument[], Args = PromptArgumentsToObject> = { + arguments?: InputPromptArgument[]; + 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[] = InputResourceTemplateArgument[]> = { + arguments: Arguments; + description?: string; + load: (args: ResourceTemplateArgumentsToObject, auth?: T) => Promise; + mimeType?: string; + name: string; + uriTemplate: string; +}; +type InputResourceTemplateArgument = Readonly<{ + complete?: ArgumentValueCompleter; + description?: string; + name: string; + required?: boolean; +}>; +type LoggingLevel = "alert" | "critical" | "debug" | "emergency" | "error" | "info" | "notice" | "warning"; +type Prompt[] = 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["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[] = 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 = 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 = { + 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[]; + dpopSigningAlgValuesSupported?: string[]; + grantTypesSupported?: string[]; + introspectionEndpoint?: string; + issuer: string; + 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; + }; + }; + 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 = { + 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; + name: string; + 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; +}; +declare const FastMCPSessionEventEmitterBase: { + new (): StrictEventEmitter; +}; +type Authenticate = (request: http.IncomingMessage) => Promise; +type FastMCPSessionAuth = Record | undefined; +declare class FastMCPSessionEventEmitter extends FastMCPSessionEventEmitterBase { +} +declare class FastMCPSession extends FastMCPSessionEventEmitter { + #private; + get clientCapabilities(): ClientCapabilities | null; + get isReady(): boolean; + get loggingLevel(): LoggingLevel; + get roots(): Root[]; + get server(): Server; + get sessionId(): string | undefined; + set sessionId(value: string | undefined); + 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; + }); + close(): Promise; + connect(transport: Transport): Promise; + requestSampling(message: z.infer["params"], options?: RequestOptions): Promise; + waitForReady(): Promise; + private addPrompt; + private addResource; + private addResourceTemplate; + private setupCompleteHandlers; + private setupErrorHandling; + private setupLoggingHandlers; + private setupPromptHandlers; + private setupResourceHandlers; + private setupResourceTemplateHandlers; + private setupRootsHandlers; + private setupToolHandlers; +} +declare const FastMCPEventEmitterBase: { + new (): StrictEventEmitter>; +}; +declare class FastMCPEventEmitter extends FastMCPEventEmitterBase { +} +declare class FastMCP extends FastMCPEventEmitter { + #private; + options: ServerOptions; + get sessions(): FastMCPSession[]; + constructor(options: ServerOptions); + /** + * Adds a prompt to the server. + */ + addPrompt[]>(prompt: InputPrompt): void; + /** + * Adds a resource to the server. + */ + addResource(resource: Resource): void; + /** + * Adds a resource template to the server. + */ + addResourceTemplate(resource: InputResourceTemplate): void; + /** + * Adds a tool to the server. + */ + addTool(tool: Tool): void; + /** + * 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 + */ + embedded(uri: string): Promise; + /** + * Starts the server. + */ + start(options?: Partial<{ + httpStream: { + enableJsonResponse?: boolean; + endpoint?: `/${string}`; + eventStore?: EventStore; + host?: string; + port: number; + stateless?: boolean; + }; + transportType: "httpStream" | "stdio"; + }>): Promise; + /** + * Stops the server. + */ + stop(): Promise; +} + +export { type AudioContent, type Content, type ContentResult, type Context, FastMCP, type FastMCPEvents, FastMCPSession, type FastMCPSessionEvents, type ImageContent, type InputPrompt, type InputPromptArgument, type Logger, type LoggingLevel, type Progress, type Prompt, type PromptArgument, type Resource, type ResourceContent, type ResourceResult, type ResourceTemplate, type ResourceTemplateArgument, type SSEServer, type SerializableValue, type ServerOptions, type TextContent, type Tool, type ToolParameters, UnexpectedStateError, UserError, audioContent, imageContent }; diff --git a/dist/FastMCP.js b/dist/FastMCP.js new file mode 100644 index 0000000..decccca --- /dev/null +++ b/dist/FastMCP.js @@ -0,0 +1,1401 @@ +// src/FastMCP.ts +import { Server } from "@modelcontextprotocol/sdk/server/index.js"; +import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; +import { + CallToolRequestSchema, + CompleteRequestSchema, + ErrorCode, + GetPromptRequestSchema, + ListPromptsRequestSchema, + ListResourcesRequestSchema, + ListResourceTemplatesRequestSchema, + ListToolsRequestSchema, + McpError, + ReadResourceRequestSchema, + RootsListChangedNotificationSchema, + SetLevelRequestSchema +} from "@modelcontextprotocol/sdk/types.js"; +import { EventEmitter } from "events"; +import { readFile } from "fs/promises"; +import Fuse from "fuse.js"; +import { startHTTPServer } from "mcp-proxy"; +import { setTimeout as delay } from "timers/promises"; +import { fetch } from "undici"; +import parseURITemplate from "uri-templates"; +import { toJsonSchema } from "xsschema"; +import { z } from "zod"; +import { ErrorCode as ErrorCode2, McpError as McpError2 } from "@modelcontextprotocol/sdk/types.js"; +var imageContent = async (input) => { + let rawData; + try { + if ("url" in input) { + try { + const response = await fetch(input.url); + if (!response.ok) { + throw new Error( + `Server responded with status: ${response.status} - ${response.statusText}` + ); + } + rawData = Buffer.from(await response.arrayBuffer()); + } catch (error) { + throw new Error( + `Failed to fetch image from URL (${input.url}): ${error instanceof Error ? error.message : String(error)}` + ); + } + } else if ("path" in input) { + try { + rawData = await readFile(input.path); + } catch (error) { + throw new Error( + `Failed to read image from path (${input.path}): ${error instanceof Error ? error.message : String(error)}` + ); + } + } else if ("buffer" in input) { + rawData = input.buffer; + } else { + throw new Error( + "Invalid input: Provide a valid 'url', 'path', or 'buffer'" + ); + } + const { 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" + }; + } catch (error) { + if (error instanceof Error) { + throw error; + } else { + throw new Error(`Unexpected error processing image: ${String(error)}`); + } + } +}; +var audioContent = async (input) => { + let rawData; + try { + if ("url" in input) { + try { + const response = await fetch(input.url); + if (!response.ok) { + throw new Error( + `Server responded with status: ${response.status} - ${response.statusText}` + ); + } + rawData = Buffer.from(await response.arrayBuffer()); + } catch (error) { + throw new Error( + `Failed to fetch audio from URL (${input.url}): ${error instanceof Error ? error.message : String(error)}` + ); + } + } else if ("path" in input) { + try { + rawData = await readFile(input.path); + } catch (error) { + throw new Error( + `Failed to read audio from path (${input.path}): ${error instanceof Error ? error.message : String(error)}` + ); + } + } else if ("buffer" in input) { + rawData = input.buffer; + } else { + throw new Error( + "Invalid input: Provide a valid 'url', 'path', or 'buffer'" + ); + } + const { 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" + }; + } catch (error) { + if (error instanceof Error) { + throw error; + } else { + throw new Error(`Unexpected error processing audio: ${String(error)}`); + } + } +}; +var FastMCPError = class extends Error { + constructor(message) { + super(message); + this.name = new.target.name; + } +}; +var UnexpectedStateError = class extends FastMCPError { + extras; + constructor(message, extras) { + super(message); + this.name = new.target.name; + this.extras = extras; + } +}; +var UserError = class extends UnexpectedStateError { +}; +var TextContentZodSchema = z.object({ + /** + * The text content of the message. + */ + text: z.string(), + type: z.literal("text") +}).strict(); +var ImageContentZodSchema = z.object({ + /** + * The base64-encoded image data. + */ + data: z.string().base64(), + /** + * The MIME type of the image. Different providers may support different image types. + */ + mimeType: z.string(), + type: z.literal("image") +}).strict(); +var AudioContentZodSchema = z.object({ + /** + * The base64-encoded audio data. + */ + data: z.string().base64(), + mimeType: z.string(), + type: z.literal("audio") +}).strict(); +var 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(); +var 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() +}); +var ContentZodSchema = z.discriminatedUnion("type", [ + TextContentZodSchema, + ImageContentZodSchema, + AudioContentZodSchema, + ResourceContentZodSchema, + ResourceLinkZodSchema +]); +var ContentResultZodSchema = z.object({ + _meta: z.record(z.unknown()).optional(), + content: ContentZodSchema.array(), + isError: z.boolean().optional() +}).strict(); +var CompletionZodSchema = z.object({ + /** + * Indicates whether there are additional completion options beyond those provided in the current response, even if the exact total is unknown. + */ + hasMore: z.optional(z.boolean()), + /** + * The total number of completion options available. This can exceed the number of values actually sent in the response. + */ + total: z.optional(z.number().int()), + /** + * An array of completion values. Must not exceed 100 items. + */ + values: z.array(z.string()).max(100) +}); +var FastMCPSessionEventEmitterBase = EventEmitter; +var FastMCPSessionEventEmitter = class extends FastMCPSessionEventEmitterBase { +}; +var FastMCPSession = class extends FastMCPSessionEventEmitter { + get clientCapabilities() { + return this.#clientCapabilities ?? null; + } + get 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.#server = new 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 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. + +${e instanceof Error ? e.stack : JSON.stringify(e)}` + ); + } + } + } + if (this.#clientCapabilities) { + const pingConfig = this.#getPingConfig(transport); + if (pingConfig.enabled) { + this.#pingInterval = setInterval(async () => { + try { + await this.#server.ping(); + } catch { + const logLevel = pingConfig.logLevel; + if (logLevel === "debug") { + 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; + } + } + async requestSampling(message, options) { + return this.#server.createMessage(message, options); + } + 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 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, value, auth) => { + if (completers[name]) { + return await completers[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 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); + } + return { + values: [] + }; + } + }; + this.#resourceTemplates.push(resourceTemplate); + } + setupCompleteHandlers() { + this.#server.setRequestHandler(CompleteRequestSchema, async (request) => { + if (request.params.ref.type === "ref/prompt") { + const prompt = this.#prompts.find( + (prompt2) => prompt2.name === request.params.ref.name + ); + if (!prompt) { + throw new UnexpectedStateError("Unknown prompt", { + request + }); + } + if (!prompt.complete) { + throw new UnexpectedStateError("Prompt does not support completion", { + request + }); + } + const completion = CompletionZodSchema.parse( + await prompt.complete( + request.params.argument.name, + request.params.argument.value, + this.#auth + ) + ); + return { + completion + }; + } + if (request.params.ref.type === "ref/resource") { + const resource = this.#resourceTemplates.find( + (resource2) => resource2.uriTemplate === request.params.ref.uri + ); + if (!resource) { + throw new UnexpectedStateError("Unknown resource", { + request + }); + } + if (!("uriTemplate" in resource)) { + throw new UnexpectedStateError("Unexpected resource"); + } + if (!resource.complete) { + throw new UnexpectedStateError( + "Resource does not support completion", + { + request + } + ); + } + const completion = CompletionZodSchema.parse( + await resource.complete( + request.params.argument.name, + request.params.argument.value, + 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(SetLevelRequestSchema, (request) => { + this.#loggingLevel = request.params.level; + return {}; + }); + } + setupPromptHandlers(prompts) { + this.#server.setRequestHandler(ListPromptsRequestSchema, async () => { + return { + prompts: prompts.map((prompt) => { + return { + arguments: prompt.arguments, + complete: prompt.complete, + description: prompt.description, + name: prompt.name + }; + }) + }; + }); + this.#server.setRequestHandler(GetPromptRequestSchema, async (request) => { + const prompt = prompts.find( + (prompt2) => prompt2.name === request.params.name + ); + if (!prompt) { + throw new McpError( + ErrorCode.MethodNotFound, + `Unknown prompt: ${request.params.name}` + ); + } + const args = request.params.arguments; + for (const arg of prompt.arguments ?? []) { + if (arg.required && !(args && arg.name in args)) { + throw new McpError( + ErrorCode.InvalidRequest, + `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 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 + }; + } + }); + } + setupResourceHandlers(resources) { + this.#server.setRequestHandler(ListResourcesRequestSchema, async () => { + return { + resources: resources.map((resource) => ({ + description: resource.description, + mimeType: resource.mimeType, + name: resource.name, + uri: resource.uri + })) + }; + }); + this.#server.setRequestHandler( + ReadResourceRequestSchema, + async (request) => { + if ("uri" in request.params) { + const resource = resources.find( + (resource2) => "uri" in resource2 && resource2.uri === request.params.uri + ); + if (!resource) { + for (const resourceTemplate of this.#resourceTemplates) { + const uriTemplate = parseURITemplate( + resourceTemplate.uriTemplate + ); + const match = uriTemplate.fromUri(request.params.uri); + if (!match) { + continue; + } + const uri = uriTemplate.fill(match); + const result = await resourceTemplate.load(match, this.#auth); + const resources2 = Array.isArray(result) ? result : [result]; + return { + contents: resources2.map((resource2) => ({ + ...resource2, + description: resourceTemplate.description, + mimeType: resource2.mimeType ?? resourceTemplate.mimeType, + name: resourceTemplate.name, + uri: resource2.uri ?? uri + })) + }; + } + throw new McpError( + 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 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 + }); + } + ); + } + setupResourceTemplateHandlers(resourceTemplates) { + this.#server.setRequestHandler( + ListResourceTemplatesRequestSchema, + async () => { + return { + resourceTemplates: resourceTemplates.map((resourceTemplate) => ({ + description: resourceTemplate.description, + mimeType: resourceTemplate.mimeType, + name: resourceTemplate.name, + uriTemplate: resourceTemplate.uriTemplate + })) + }; + } + ); + } + setupRootsHandlers() { + if (this.#rootsConfig?.enabled === false) { + this.#logger.debug( + "[FastMCP debug] roots capability explicitly disabled via config" + ); + return; + } + if (typeof this.#server.listRoots === "function") { + this.#server.setNotificationHandler( + RootsListChangedNotificationSchema, + () => { + this.#server.listRoots().then((roots) => { + this.#roots = roots.roots; + this.emit("rootsChanged", { + roots: roots.roots + }); + }).catch((error) => { + if (error instanceof McpError && error.code === ErrorCode.MethodNotFound) { + 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(ListToolsRequestSchema, async () => { + return { + tools: await Promise.all( + tools.map(async (tool) => { + return { + annotations: tool.annotations, + description: tool.description, + inputSchema: tool.parameters ? await toJsonSchema(tool.parameters) : { + additionalProperties: false, + properties: {}, + type: "object" + }, + // More complete schema for Cursor compatibility + name: tool.name + }; + }) + ) + }; + }); + this.#server.setRequestHandler(CallToolRequestSchema, async (request) => { + const tool = tools.find((tool2) => tool2.name === request.params.name); + if (!tool) { + throw new McpError( + ErrorCode.MethodNotFound, + `Unknown tool: ${request.params.name}` + ); + } + let args = void 0; + if (tool.parameters) { + const parsed = await tool.parameters["~standard"].validate( + request.params.arguments + ); + if (parsed.issues) { + 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; + 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 request.params?._meta?.requestId === "string" ? request.params._meta.requestId : void 0, + requestMetadata: request.params._meta, + session: this.#auth, + sessionId: this.#sessionId, + streamContent + }); + const maybeStringResult = await (tool.timeoutMs ? Promise.race([ + executeToolPromise, + new Promise((_, reject) => { + const timeoutId = setTimeout(() => { + reject( + new McpError( + ErrorCode.InternalError, + `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 delay(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 McpError) { + throw 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; +} +var FastMCPEventEmitterBase = EventEmitter; +var FastMCPEventEmitter = class extends FastMCPEventEmitterBase { +}; +var FastMCP = class extends FastMCPEventEmitter { + constructor(options) { + super(); + this.options = options; + this.#options = options; + this.#authenticate = options.authenticate; + this.#logger = options.logger || console; + } + get sessions() { + return this.#sessions; + } + #authenticate; + #httpStreamServer = null; + #logger; + #options; + #prompts = []; + #resources = []; + #resourcesTemplates = []; + #sessions = []; + #tools = []; + /** + * Adds a prompt to the server. + */ + addPrompt(prompt) { + this.#prompts.push(prompt); + } + /** + * Adds a resource to the server. + */ + addResource(resource) { + this.#resources.push(resource); + } + /** + * Adds a resource template to the server. + */ + addResourceTemplate(resource) { + this.#resourcesTemplates.push(resource); + } + /** + * Adds a tool to the server. + */ + addTool(tool) { + this.#tools.push(tool); + } + /** + * 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 + */ + 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 = parseURITemplate(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 }); + } + /** + * Starts the server. + */ + async start(options) { + const config = this.#parseRuntimeConfig(options); + if (config.transportType === "stdio") { + const transport = new 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 + }); + } 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 startHTTPServer({ + ...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, + // 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); + }, + port: httpConfig.port, + stateless: true, + streamEndpoint: httpConfig.endpoint + }); + } else { + this.#httpStreamServer = await startHTTPServer({ + ...this.#authenticate ? { authenticate: this.#authenticate } : {}, + createServer: async (request) => { + let auth; + if (this.#authenticate) { + auth = await this.#authenticate(request); + } + 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, + onClose: async (session) => { + const sessionIndex = this.#sessions.indexOf(session); + if (sessionIndex !== -1) this.#sessions.splice(sessionIndex, 1); + this.emit("disconnect", { + session + }); + }, + onConnect: async (session) => { + this.#sessions.push(session); + this.#logger.info(`[FastMCP info] HTTP Stream session established`); + this.emit("connect", { + session + }); + }, + onUnhandledRequest: async (req, res) => { + await this.#handleUnhandledRequest( + req, + res, + false, + httpConfig.host + ); + }, + port: httpConfig.port, + stateless: httpConfig.stateless, + streamEndpoint: httpConfig.endpoint + }); + this.#logger.info( + `[FastMCP info] server is running on HTTP Stream at http://${httpConfig.host}:${httpConfig.port}${httpConfig.endpoint}` + ); + } + } else { + throw new Error("Invalid transport type"); + } + } + /** + * Stops the server. + */ + async stop() { + if (this.#httpStreamServer) { + await this.#httpStreamServer.close(); + } + } + /** + * Creates a new FastMCPSession instance with the current configuration. + * Used both for regular sessions and stateless requests. + */ + #createSession(auth, sessionId) { + if (auth && typeof auth === "object" && "authenticated" in auth && !auth.authenticated) { + const errorMessage = "error" in auth && typeof auth.error === "string" ? auth.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, and OAuth endpoints + */ + #handleUnhandledRequest = async (req, res, isStateless = false, host) => { + const healthConfig = this.#options.health ?? {}; + const enabled = healthConfig.enabled === void 0 ? 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 ?? "\u2713 Ok"); + return; + } + if (req.method === "GET" && url.pathname === "/ready") { + if (isStateless) { + 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); + } + } + 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; + } + if (url.pathname === "/.well-known/oauth-protected-resource" && oauthConfig.protectedResource) { + const metadata = convertObjectToSnakeCase( + oauthConfig.protectedResource + ); + res.writeHead(200, { + "Content-Type": "application/json" + }).end(JSON.stringify(metadata)); + return; + } + } + res.writeHead(404).end(); + }; + #parseRuntimeConfig(overrides) { + const args = process.argv.slice(2); + const getArg = (name) => { + const index = args.findIndex((arg) => arg === `--${name}`); + return index !== -1 && index + 1 < args.length ? args[index + 1] : void 0; + }; + 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; + 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; + return { + httpStream: { + enableJsonResponse, + endpoint, + host, + port, + stateless + }, + transportType: "httpStream" + }; + } + return { transportType: "stdio" }; + } + #removeSession(session) { + const sessionIndex = this.#sessions.indexOf(session); + if (sessionIndex !== -1) { + this.#sessions.splice(sessionIndex, 1); + this.emit("disconnect", { + session + }); + } + } +}; +export { + ErrorCode2 as ErrorCode, + FastMCP, + FastMCPSession, + McpError2 as McpError, + UnexpectedStateError, + UserError, + audioContent, + imageContent +}; +//# sourceMappingURL=FastMCP.js.map \ No newline at end of file diff --git a/dist/FastMCP.js.map b/dist/FastMCP.js.map new file mode 100644 index 0000000..73ba8fa --- /dev/null +++ b/dist/FastMCP.js.map @@ -0,0 +1 @@ +{"version":3,"sources":["../src/FastMCP.ts"],"sourcesContent":["import { Server } from \"@modelcontextprotocol/sdk/server/index.js\";\nimport { StdioServerTransport } from \"@modelcontextprotocol/sdk/server/stdio.js\";\nimport { EventStore } from \"@modelcontextprotocol/sdk/server/streamableHttp.js\";\nimport { RequestOptions } from \"@modelcontextprotocol/sdk/shared/protocol.js\";\nimport { Transport } from \"@modelcontextprotocol/sdk/shared/transport.js\";\nimport {\n CallToolRequestSchema,\n ClientCapabilities,\n CompleteRequestSchema,\n CreateMessageRequestSchema,\n ErrorCode,\n GetPromptRequestSchema,\n GetPromptResult,\n ListPromptsRequestSchema,\n ListResourcesRequestSchema,\n ListResourcesResult,\n ListResourceTemplatesRequestSchema,\n ListResourceTemplatesResult,\n ListToolsRequestSchema,\n McpError,\n ReadResourceRequestSchema,\n RequestMeta,\n ResourceLink,\n Root,\n RootsListChangedNotificationSchema,\n ServerCapabilities,\n SetLevelRequestSchema,\n} from \"@modelcontextprotocol/sdk/types.js\";\nimport { StandardSchemaV1 } from \"@standard-schema/spec\";\nimport { EventEmitter } from \"events\";\nimport { readFile } from \"fs/promises\";\nimport Fuse from \"fuse.js\";\nimport http from \"http\";\nimport { startHTTPServer } from \"mcp-proxy\";\nimport { StrictEventEmitter } from \"strict-event-emitter-types\";\nimport { setTimeout as delay } from \"timers/promises\";\nimport { fetch } from \"undici\";\nimport parseURITemplate from \"uri-templates\";\nimport { toJsonSchema } from \"xsschema\";\nimport { z } from \"zod\";\n\nexport interface Logger {\n debug(...args: unknown[]): void;\n error(...args: unknown[]): void;\n info(...args: unknown[]): void;\n log(...args: unknown[]): void;\n warn(...args: unknown[]): void;\n}\n\nexport type SSEServer = {\n close: () => Promise;\n};\n\ntype FastMCPEvents = {\n connect: (event: { session: FastMCPSession }) => void;\n disconnect: (event: { session: FastMCPSession }) => void;\n};\n\ntype FastMCPSessionEvents = {\n error: (event: { error: Error }) => void;\n ready: () => void;\n rootsChanged: (event: { roots: Root[] }) => void;\n};\n\nexport const imageContent = async (\n input: { buffer: Buffer } | { path: string } | { url: string },\n): Promise => {\n let rawData: Buffer;\n\n try {\n if (\"url\" in input) {\n try {\n const response = await fetch(input.url);\n\n if (!response.ok) {\n throw new Error(\n `Server responded with status: ${response.status} - ${response.statusText}`,\n );\n }\n\n rawData = Buffer.from(await response.arrayBuffer());\n } catch (error) {\n throw new Error(\n `Failed to fetch image from URL (${input.url}): ${\n error instanceof Error ? error.message : String(error)\n }`,\n );\n }\n } else if (\"path\" in input) {\n try {\n rawData = await readFile(input.path);\n } catch (error) {\n throw new Error(\n `Failed to read image from path (${input.path}): ${\n error instanceof Error ? error.message : String(error)\n }`,\n );\n }\n } else if (\"buffer\" in input) {\n rawData = input.buffer;\n } else {\n throw new Error(\n \"Invalid input: Provide a valid 'url', 'path', or 'buffer'\",\n );\n }\n\n const { fileTypeFromBuffer } = await import(\"file-type\");\n const mimeType = await fileTypeFromBuffer(rawData);\n\n if (!mimeType || !mimeType.mime.startsWith(\"image/\")) {\n console.warn(\n `Warning: Content may not be a valid image. Detected MIME: ${\n mimeType?.mime || \"unknown\"\n }`,\n );\n }\n\n const base64Data = rawData.toString(\"base64\");\n\n return {\n data: base64Data,\n mimeType: mimeType?.mime ?? \"image/png\",\n type: \"image\",\n } as const;\n } catch (error) {\n if (error instanceof Error) {\n throw error;\n } else {\n throw new Error(`Unexpected error processing image: ${String(error)}`);\n }\n }\n};\n\nexport const audioContent = async (\n input: { buffer: Buffer } | { path: string } | { url: string },\n): Promise => {\n let rawData: Buffer;\n\n try {\n if (\"url\" in input) {\n try {\n const response = await fetch(input.url);\n\n if (!response.ok) {\n throw new Error(\n `Server responded with status: ${response.status} - ${response.statusText}`,\n );\n }\n\n rawData = Buffer.from(await response.arrayBuffer());\n } catch (error) {\n throw new Error(\n `Failed to fetch audio from URL (${input.url}): ${\n error instanceof Error ? error.message : String(error)\n }`,\n );\n }\n } else if (\"path\" in input) {\n try {\n rawData = await readFile(input.path);\n } catch (error) {\n throw new Error(\n `Failed to read audio from path (${input.path}): ${\n error instanceof Error ? error.message : String(error)\n }`,\n );\n }\n } else if (\"buffer\" in input) {\n rawData = input.buffer;\n } else {\n throw new Error(\n \"Invalid input: Provide a valid 'url', 'path', or 'buffer'\",\n );\n }\n\n const { fileTypeFromBuffer } = await import(\"file-type\");\n const mimeType = await fileTypeFromBuffer(rawData);\n\n if (!mimeType || !mimeType.mime.startsWith(\"audio/\")) {\n console.warn(\n `Warning: Content may not be a valid audio file. Detected MIME: ${\n mimeType?.mime || \"unknown\"\n }`,\n );\n }\n\n const base64Data = rawData.toString(\"base64\");\n\n return {\n data: base64Data,\n mimeType: mimeType?.mime ?? \"audio/mpeg\",\n type: \"audio\",\n } as const;\n } catch (error) {\n if (error instanceof Error) {\n throw error;\n } else {\n throw new Error(`Unexpected error processing audio: ${String(error)}`);\n }\n }\n};\n\ntype Context = {\n client: {\n version: ReturnType;\n };\n log: {\n debug: (message: string, data?: SerializableValue) => void;\n error: (message: string, data?: SerializableValue) => void;\n info: (message: string, data?: SerializableValue) => void;\n warn: (message: string, data?: SerializableValue) => void;\n };\n reportProgress: (progress: Progress) => Promise;\n /**\n * Request ID from the current MCP request.\n * Available for all transports when the client provides it.\n */\n requestId?: string;\n requestMetadata?: RequestMeta;\n session: T | undefined;\n /**\n * Session ID from the Mcp-Session-Id header.\n * Only available for HTTP-based transports (SSE, HTTP Stream).\n * Can be used to track per-session state, implement session-specific\n * counters, or maintain user-specific data across multiple requests.\n */\n sessionId?: string;\n streamContent: (content: Content | Content[]) => Promise;\n};\n\ntype Extra = unknown;\n\ntype Extras = Record;\n\ntype Literal = boolean | null | number | string | undefined;\n\ntype Progress = {\n /**\n * The progress thus far. This should increase every time progress is made, even if the total is unknown.\n */\n progress: number;\n /**\n * Total number of items to process (or total progress required), if known.\n */\n total?: number;\n};\n\ntype SerializableValue =\n | { [key: string]: SerializableValue }\n | Literal\n | SerializableValue[];\n\ntype TextContent = {\n text: string;\n type: \"text\";\n};\n\ntype ToolParameters = StandardSchemaV1;\n\nabstract class FastMCPError extends Error {\n public constructor(message?: string) {\n super(message);\n this.name = new.target.name;\n }\n}\n\nexport class UnexpectedStateError extends FastMCPError {\n public extras?: Extras;\n\n public constructor(message: string, extras?: Extras) {\n super(message);\n this.name = new.target.name;\n this.extras = extras;\n }\n}\n\n/**\n * An error that is meant to be surfaced to the user.\n */\nexport class UserError extends UnexpectedStateError {}\n\nconst TextContentZodSchema = z\n .object({\n /**\n * The text content of the message.\n */\n text: z.string(),\n type: z.literal(\"text\"),\n })\n .strict() satisfies z.ZodType;\n\ntype ImageContent = {\n data: string;\n mimeType: string;\n type: \"image\";\n};\n\nconst ImageContentZodSchema = z\n .object({\n /**\n * The base64-encoded image data.\n */\n data: z.string().base64(),\n /**\n * The MIME type of the image. Different providers may support different image types.\n */\n mimeType: z.string(),\n type: z.literal(\"image\"),\n })\n .strict() satisfies z.ZodType;\n\ntype AudioContent = {\n data: string;\n mimeType: string;\n type: \"audio\";\n};\n\nconst AudioContentZodSchema = z\n .object({\n /**\n * The base64-encoded audio data.\n */\n data: z.string().base64(),\n mimeType: z.string(),\n type: z.literal(\"audio\"),\n })\n .strict() satisfies z.ZodType;\n\ntype ResourceContent = {\n resource: {\n blob?: string;\n mimeType?: string;\n text?: string;\n uri: string;\n };\n type: \"resource\";\n};\n\nconst ResourceContentZodSchema = z\n .object({\n resource: z.object({\n blob: z.string().optional(),\n mimeType: z.string().optional(),\n text: z.string().optional(),\n uri: z.string(),\n }),\n type: z.literal(\"resource\"),\n })\n .strict() satisfies z.ZodType;\n\nconst ResourceLinkZodSchema = z.object({\n description: z.string().optional(),\n mimeType: z.string().optional(),\n name: z.string(),\n title: z.string().optional(),\n type: z.literal(\"resource_link\"),\n uri: z.string(),\n}) satisfies z.ZodType;\n\ntype Content =\n | AudioContent\n | ImageContent\n | ResourceContent\n | ResourceLink\n | TextContent;\n\nconst ContentZodSchema = z.discriminatedUnion(\"type\", [\n TextContentZodSchema,\n ImageContentZodSchema,\n AudioContentZodSchema,\n ResourceContentZodSchema,\n ResourceLinkZodSchema,\n]) satisfies z.ZodType;\n\ntype ContentResult = {\n _meta?: Record;\n content: Content[];\n isError?: boolean;\n};\n\nconst ContentResultZodSchema = z\n .object({\n _meta: z.record(z.unknown()).optional(),\n content: ContentZodSchema.array(),\n isError: z.boolean().optional(),\n })\n .strict() satisfies z.ZodType;\n\ntype Completion = {\n hasMore?: boolean;\n total?: number;\n values: string[];\n};\n\n/**\n * https://github.com/modelcontextprotocol/typescript-sdk/blob/3164da64d085ec4e022ae881329eee7b72f208d4/src/types.ts#L983-L1003\n */\nconst CompletionZodSchema = z.object({\n /**\n * Indicates whether there are additional completion options beyond those provided in the current response, even if the exact total is unknown.\n */\n hasMore: z.optional(z.boolean()),\n /**\n * The total number of completion options available. This can exceed the number of values actually sent in the response.\n */\n total: z.optional(z.number().int()),\n /**\n * An array of completion values. Must not exceed 100 items.\n */\n values: z.array(z.string()).max(100),\n}) satisfies z.ZodType;\n\ntype ArgumentValueCompleter =\n (value: string, auth?: T) => Promise;\n\ntype InputPrompt<\n T extends FastMCPSessionAuth = FastMCPSessionAuth,\n Arguments extends InputPromptArgument[] = InputPromptArgument[],\n Args = PromptArgumentsToObject,\n> = {\n arguments?: InputPromptArgument[];\n description?: string;\n load: (args: Args, auth?: T) => Promise;\n name: string;\n};\n\ntype InputPromptArgument =\n Readonly<{\n complete?: ArgumentValueCompleter;\n description?: string;\n enum?: string[];\n name: string;\n required?: boolean;\n }>;\n\ntype InputResourceTemplate<\n T extends FastMCPSessionAuth,\n Arguments extends\n InputResourceTemplateArgument[] = InputResourceTemplateArgument[],\n> = {\n arguments: Arguments;\n description?: string;\n load: (\n args: ResourceTemplateArgumentsToObject,\n auth?: T,\n ) => Promise;\n mimeType?: string;\n name: string;\n uriTemplate: string;\n};\n\ntype InputResourceTemplateArgument<\n T extends FastMCPSessionAuth = FastMCPSessionAuth,\n> = Readonly<{\n complete?: ArgumentValueCompleter;\n description?: string;\n name: string;\n required?: boolean;\n}>;\n\ntype LoggingLevel =\n | \"alert\"\n | \"critical\"\n | \"debug\"\n | \"emergency\"\n | \"error\"\n | \"info\"\n | \"notice\"\n | \"warning\";\n\ntype Prompt<\n T extends FastMCPSessionAuth = FastMCPSessionAuth,\n Arguments extends PromptArgument[] = PromptArgument[],\n Args = PromptArgumentsToObject,\n> = {\n arguments?: PromptArgument[];\n complete?: (name: string, value: string, auth?: T) => Promise;\n description?: string;\n load: (args: Args, auth?: T) => Promise;\n name: string;\n};\n\ntype PromptArgument =\n Readonly<{\n complete?: ArgumentValueCompleter;\n description?: string;\n enum?: string[];\n name: string;\n required?: boolean;\n }>;\n\ntype PromptArgumentsToObject =\n {\n [K in T[number][\"name\"]]: Extract<\n T[number],\n { name: K }\n >[\"required\"] extends true\n ? string\n : string | undefined;\n };\n\ntype PromptResult = Pick | string;\n\ntype Resource = {\n complete?: (name: string, value: string, auth?: T) => Promise;\n description?: string;\n load: (auth?: T) => Promise;\n mimeType?: string;\n name: string;\n uri: string;\n};\n\ntype ResourceResult =\n | {\n blob: string;\n mimeType?: string;\n uri?: string;\n }\n | {\n mimeType?: string;\n text: string;\n uri?: string;\n };\n\ntype ResourceTemplate<\n T extends FastMCPSessionAuth,\n Arguments extends\n ResourceTemplateArgument[] = ResourceTemplateArgument[],\n> = {\n arguments: Arguments;\n complete?: (name: string, value: string, auth?: T) => Promise;\n description?: string;\n load: (\n args: ResourceTemplateArgumentsToObject,\n auth?: T,\n ) => Promise;\n mimeType?: string;\n name: string;\n uriTemplate: string;\n};\n\ntype ResourceTemplateArgument<\n T extends FastMCPSessionAuth = FastMCPSessionAuth,\n> = Readonly<{\n complete?: ArgumentValueCompleter;\n description?: string;\n name: string;\n required?: boolean;\n}>;\n\ntype ResourceTemplateArgumentsToObject = {\n [K in T[number][\"name\"]]: string;\n};\n\ntype SamplingResponse = {\n content: AudioContent | ImageContent | TextContent;\n model: string;\n role: \"assistant\" | \"user\";\n stopReason?: \"endTurn\" | \"maxTokens\" | \"stopSequence\" | string;\n};\n\ntype ServerOptions = {\n authenticate?: Authenticate;\n /**\n * Configuration for the health-check endpoint that can be exposed when the\n * server is running using the HTTP Stream transport. When enabled, the\n * server will respond to an HTTP GET request with the configured path (by\n * default \"/health\") rendering a plain-text response (by default \"ok\") and\n * the configured status code (by default 200).\n *\n * The endpoint is only added when the server is started with\n * `transportType: \"httpStream\"` – it is ignored for the stdio transport.\n */\n health?: {\n /**\n * When set to `false` the health-check endpoint is disabled.\n * @default true\n */\n enabled?: boolean;\n\n /**\n * Plain-text body returned by the endpoint.\n * @default \"ok\"\n */\n message?: string;\n\n /**\n * HTTP path that should be handled.\n * @default \"/health\"\n */\n path?: string;\n\n /**\n * HTTP response status that will be returned.\n * @default 200\n */\n status?: number;\n };\n instructions?: string;\n /**\n * Custom logger instance. If not provided, defaults to console.\n * Use this to integrate with your own logging system.\n */\n logger?: Logger;\n name: string;\n\n /**\n * Configuration for OAuth well-known discovery endpoints that can be exposed\n * when the server is running using HTTP-based transports (SSE or HTTP Stream).\n * When enabled, the server will respond to requests for OAuth discovery endpoints\n * with the configured metadata.\n *\n * The endpoints are only added when the server is started with\n * `transportType: \"httpStream\"` – they are ignored for the stdio transport.\n * Both SSE and HTTP Stream transports support OAuth endpoints.\n */\n oauth?: {\n /**\n * OAuth Authorization Server metadata for /.well-known/oauth-authorization-server\n *\n * This endpoint follows RFC 8414 (OAuth 2.0 Authorization Server Metadata)\n * and provides metadata about the OAuth 2.0 authorization server.\n *\n * Required by MCP Specification 2025-03-26\n */\n authorizationServer?: {\n authorizationEndpoint: string;\n codeChallengeMethodsSupported?: string[];\n // DPoP support\n dpopSigningAlgValuesSupported?: string[];\n grantTypesSupported?: string[];\n\n introspectionEndpoint?: string;\n // Required\n issuer: string;\n // Common optional\n jwksUri?: string;\n opPolicyUri?: string;\n opTosUri?: string;\n registrationEndpoint?: string;\n responseModesSupported?: string[];\n responseTypesSupported: string[];\n revocationEndpoint?: string;\n scopesSupported?: string[];\n serviceDocumentation?: string;\n tokenEndpoint: string;\n tokenEndpointAuthMethodsSupported?: string[];\n tokenEndpointAuthSigningAlgValuesSupported?: string[];\n\n uiLocalesSupported?: string[];\n };\n\n /**\n * Whether OAuth discovery endpoints should be enabled.\n */\n enabled: boolean;\n\n /**\n * OAuth Protected Resource metadata for `/.well-known/oauth-protected-resource`\n *\n * This endpoint follows {@link https://www.rfc-editor.org/rfc/rfc9728.html | RFC 9728}\n * and provides metadata describing how an OAuth 2.0 protected resource (in this case,\n * an MCP server) expects to be accessed.\n *\n * When configured, FastMCP will automatically serve this metadata at the\n * `/.well-known/oauth-protected-resource` endpoint. The `authorizationServers` and `resource`\n * fields are required. All others are optional and will be omitted from the published\n * metadata if not specified.\n *\n * This satisfies the requirements of the MCP Authorization specification's\n * {@link https://modelcontextprotocol.io/specification/2025-06-18/basic/authorization#authorization-server-location | Authorization Server Location section}.\n *\n * Clients consuming this metadata MUST validate that any presented values comply with\n * RFC 9728, including strict validation of the `resource` identifier and intended audience\n * when access tokens are issued and presented (per RFC 8707 §2).\n *\n * @remarks Required by MCP Specification version 2025-06-18\n */\n protectedResource?: {\n /**\n * Allows for additional metadata fields beyond those defined in RFC 9728.\n *\n * @remarks This supports vendor-specific or experimental extensions.\n * @see {@link https://www.rfc-editor.org/rfc/rfc9728.html#section-2.3 | RFC 9728 §2.3}\n */\n [key: string]: unknown;\n\n /**\n * Supported values for the `authorization_details` parameter (RFC 9396).\n *\n * @remarks Used when fine-grained access control is in play.\n * @see {@link https://www.rfc-editor.org/rfc/rfc9728.html#section-2-2.23 | RFC 9728 §2.2.23}\n */\n authorizationDetailsTypesSupported?: string[];\n\n /**\n * List of OAuth 2.0 authorization server issuer identifiers.\n *\n * These correspond to ASes that can issue access tokens for this protected resource.\n * MCP clients use these values to locate the relevant `/.well-known/oauth-authorization-server`\n * metadata for initiating the OAuth flow.\n *\n * @remarks Required by the MCP spec. MCP servers MUST provide at least one issuer.\n * Clients are responsible for choosing among them (see RFC 9728 §7.6).\n * @see {@link https://www.rfc-editor.org/rfc/rfc9728.html#section-2-2.3 | RFC 9728 §2.2.3}\n */\n authorizationServers: string[];\n\n /**\n * List of supported methods for presenting OAuth 2.0 bearer tokens.\n *\n * @remarks Valid values are `header`, `body`, and `query`.\n * If omitted, clients MAY assume only `header` is supported, per RFC 6750.\n * This is a client-side interpretation and not a serialization default.\n * @see {@link https://www.rfc-editor.org/rfc/rfc9728.html#section-2-2.9 | RFC 9728 §2.2.9}\n */\n bearerMethodsSupported?: string[];\n\n /**\n * Whether this resource requires all access tokens to be DPoP-bound.\n *\n * @remarks If omitted, clients SHOULD assume this is `false`.\n * @see {@link https://www.rfc-editor.org/rfc/rfc9728.html#section-2-2.27 | RFC 9728 §2.2.27}\n */\n dpopBoundAccessTokensRequired?: boolean;\n\n /**\n * Supported algorithms for verifying DPoP proofs (RFC 9449).\n *\n * @see {@link https://www.rfc-editor.org/rfc/rfc9728.html#section-2-2.25 | RFC 9728 §2.2.25}\n */\n dpopSigningAlgValuesSupported?: string[];\n\n /**\n * JWKS URI of this resource. Used to validate access tokens or sign responses.\n *\n * @remarks When present, this MUST be an `https:` URI pointing to a valid JWK Set (RFC 7517).\n * @see {@link https://www.rfc-editor.org/rfc/rfc9728.html#section-2-2.5 | RFC 9728 §2.2.5}\n */\n jwksUri?: string;\n\n /**\n * Canonical OAuth resource identifier for this protected resource (the MCP server).\n *\n * @remarks Typically the base URL of the MCP server. Clients MUST use this as the\n * `resource` parameter in authorization and token requests (per RFC 8707).\n * @see {@link https://www.rfc-editor.org/rfc/rfc9728.html#section-2-2.1 | RFC 9728 §2.2.1}\n */\n resource: string;\n\n /**\n * URL to developer-accessible documentation for this resource.\n *\n * @remarks This field MAY be localized.\n * @see {@link https://www.rfc-editor.org/rfc/rfc9728.html#section-2-2.15 | RFC 9728 §2.2.15}\n */\n resourceDocumentation?: string;\n\n /**\n * Human-readable name for display purposes (e.g., in UIs).\n *\n * @remarks This field MAY be localized using language tags (`resource_name#en`, etc.).\n * @see {@link https://www.rfc-editor.org/rfc/rfc9728.html#section-2-2.13 | RFC 9728 §2.2.13}\n */\n resourceName?: string;\n\n /**\n * URL to a human-readable policy page describing acceptable use.\n *\n * @remarks This field MAY be localized.\n * @see {@link https://www.rfc-editor.org/rfc/rfc9728.html#section-2-2.17 | RFC 9728 §2.2.17}\n */\n resourcePolicyUri?: string;\n\n /**\n * Supported JWS algorithms for signed responses from this resource (e.g., response signing).\n *\n * @remarks MUST NOT include `none`.\n * @see {@link https://www.rfc-editor.org/rfc/rfc9728.html#section-2-2.11 | RFC 9728 §2.2.11}\n */\n resourceSigningAlgValuesSupported?: string[];\n\n /**\n * URL to the protected resource’s Terms of Service.\n *\n * @remarks This field MAY be localized.\n * @see {@link https://www.rfc-editor.org/rfc/rfc9728.html#section-2-2.19 | RFC 9728 §2.2.19}\n */\n resourceTosUri?: string;\n\n /**\n * Supported OAuth scopes for requesting access to this resource.\n *\n * @remarks Useful for discovery, but clients SHOULD still request the minimal scope required.\n * @see {@link https://www.rfc-editor.org/rfc/rfc9728.html#section-2-2.7 | RFC 9728 §2.2.7}\n */\n scopesSupported?: string[];\n\n /**\n * Developer-accessible documentation for how to use the service (not end-user docs).\n *\n * @remarks Semantically equivalent to `resourceDocumentation`, but included under its\n * alternate name for compatibility with tools or schemas expecting either.\n * @see {@link https://www.rfc-editor.org/rfc/rfc9728.html#section-2-2.15 | RFC 9728 §2.2.15}\n */\n serviceDocumentation?: string;\n\n /**\n * Whether mutual-TLS-bound access tokens are required.\n *\n * @remarks If omitted, clients SHOULD assume this is `false` (client-side behavior).\n * @see {@link https://www.rfc-editor.org/rfc/rfc9728.html#section-2-2.21 | RFC 9728 §2.2.21}\n */\n tlsClientCertificateBoundAccessTokens?: boolean;\n };\n };\n\n ping?: {\n /**\n * Whether ping should be enabled by default.\n * - true for SSE or HTTP Stream\n * - false for stdio\n */\n enabled?: boolean;\n /**\n * Interval\n * @default 5000 (5s)\n */\n intervalMs?: number;\n /**\n * Logging level for ping-related messages.\n * @default 'debug'\n */\n logLevel?: LoggingLevel;\n };\n /**\n * Configuration for roots capability\n */\n roots?: {\n /**\n * Whether roots capability should be enabled\n * Set to false to completely disable roots support\n * @default true\n */\n enabled?: boolean;\n };\n /**\n * General utilities\n */\n utils?: {\n formatInvalidParamsErrorMessage?: (\n issues: readonly StandardSchemaV1.Issue[],\n ) => string;\n };\n version: `${number}.${number}.${number}`;\n};\n\ntype Tool<\n T extends FastMCPSessionAuth,\n Params extends ToolParameters = ToolParameters,\n> = {\n annotations?: {\n /**\n * When true, the tool leverages incremental content streaming\n * Return void for tools that handle all their output via streaming\n */\n streamingHint?: boolean;\n } & ToolAnnotations;\n canAccess?: (auth: T) => boolean;\n description?: string;\n\n execute: (\n args: StandardSchemaV1.InferOutput,\n context: Context,\n ) => Promise<\n | AudioContent\n | ContentResult\n | ImageContent\n | ResourceContent\n | ResourceLink\n | string\n | TextContent\n | void\n >;\n name: string;\n parameters?: Params;\n timeoutMs?: number;\n};\n\n/**\n * Tool annotations as defined in MCP Specification (2025-03-26)\n * These provide hints about a tool's behavior.\n */\ntype ToolAnnotations = {\n /**\n * If true, the tool may perform destructive updates\n * Only meaningful when readOnlyHint is false\n * @default true\n */\n destructiveHint?: boolean;\n\n /**\n * If true, calling the tool repeatedly with the same arguments has no additional effect\n * Only meaningful when readOnlyHint is false\n * @default false\n */\n idempotentHint?: boolean;\n\n /**\n * If true, the tool may interact with an \"open world\" of external entities\n * @default true\n */\n openWorldHint?: boolean;\n\n /**\n * If true, indicates the tool does not modify its environment\n * @default false\n */\n readOnlyHint?: boolean;\n\n /**\n * A human-readable title for the tool, useful for UI display\n */\n title?: string;\n};\n\nconst FastMCPSessionEventEmitterBase: {\n new (): StrictEventEmitter;\n} = EventEmitter;\n\ntype Authenticate = (request: http.IncomingMessage) => Promise;\n\ntype FastMCPSessionAuth = Record | undefined;\n\nclass FastMCPSessionEventEmitter extends FastMCPSessionEventEmitterBase {}\n\nexport class FastMCPSession<\n T extends FastMCPSessionAuth = FastMCPSessionAuth,\n> extends FastMCPSessionEventEmitter {\n public get clientCapabilities(): ClientCapabilities | null {\n return this.#clientCapabilities ?? null;\n }\n public get isReady(): boolean {\n return this.#connectionState === \"ready\";\n }\n public get loggingLevel(): LoggingLevel {\n return this.#loggingLevel;\n }\n public get roots(): Root[] {\n return this.#roots;\n }\n public get server(): Server {\n return this.#server;\n }\n public get sessionId(): string | undefined {\n return this.#sessionId;\n }\n public set sessionId(value: string | undefined) {\n this.#sessionId = value;\n }\n #auth: T | undefined;\n #capabilities: ServerCapabilities = {};\n #clientCapabilities?: ClientCapabilities;\n #connectionState: \"closed\" | \"connecting\" | \"error\" | \"ready\" = \"connecting\";\n #logger: Logger;\n #loggingLevel: LoggingLevel = \"info\";\n #needsEventLoopFlush: boolean = false;\n #pingConfig?: ServerOptions[\"ping\"];\n\n #pingInterval: null | ReturnType = null;\n\n #prompts: Prompt[] = [];\n\n #resources: Resource[] = [];\n\n #resourceTemplates: ResourceTemplate[] = [];\n\n #roots: Root[] = [];\n\n #rootsConfig?: ServerOptions[\"roots\"];\n\n #server: Server;\n\n /**\n * Session ID from the Mcp-Session-Id header (HTTP transports only).\n * Used to track per-session state across multiple requests.\n */\n #sessionId?: string;\n\n #utils?: ServerOptions[\"utils\"];\n\n constructor({\n auth,\n instructions,\n logger,\n name,\n ping,\n prompts,\n resources,\n resourcesTemplates,\n roots,\n sessionId,\n tools,\n transportType,\n utils,\n version,\n }: {\n auth?: T;\n instructions?: string;\n logger: Logger;\n name: string;\n ping?: ServerOptions[\"ping\"];\n prompts: Prompt[];\n resources: Resource[];\n resourcesTemplates: InputResourceTemplate[];\n roots?: ServerOptions[\"roots\"];\n sessionId?: string;\n tools: Tool[];\n transportType?: \"httpStream\" | \"stdio\";\n utils?: ServerOptions[\"utils\"];\n version: string;\n }) {\n super();\n\n this.#auth = auth;\n this.#logger = logger;\n this.#pingConfig = ping;\n this.#rootsConfig = roots;\n this.#sessionId = sessionId;\n this.#needsEventLoopFlush = transportType === \"httpStream\";\n\n if (tools.length) {\n this.#capabilities.tools = {};\n }\n\n if (resources.length || resourcesTemplates.length) {\n this.#capabilities.resources = {};\n }\n\n if (prompts.length) {\n for (const prompt of prompts) {\n this.addPrompt(prompt);\n }\n\n this.#capabilities.prompts = {};\n }\n\n this.#capabilities.logging = {};\n\n this.#server = new Server(\n { name: name, version: version },\n { capabilities: this.#capabilities, instructions: instructions },\n );\n\n this.#utils = utils;\n\n this.setupErrorHandling();\n this.setupLoggingHandlers();\n this.setupRootsHandlers();\n this.setupCompleteHandlers();\n\n if (tools.length) {\n this.setupToolHandlers(tools);\n }\n\n if (resources.length || resourcesTemplates.length) {\n for (const resource of resources) {\n this.addResource(resource);\n }\n\n this.setupResourceHandlers(resources);\n\n if (resourcesTemplates.length) {\n for (const resourceTemplate of resourcesTemplates) {\n this.addResourceTemplate(resourceTemplate);\n }\n\n this.setupResourceTemplateHandlers(resourcesTemplates);\n }\n }\n\n if (prompts.length) {\n this.setupPromptHandlers(prompts);\n }\n }\n\n public async close() {\n this.#connectionState = \"closed\";\n\n if (this.#pingInterval) {\n clearInterval(this.#pingInterval);\n }\n\n try {\n await this.#server.close();\n } catch (error) {\n this.#logger.error(\"[FastMCP error]\", \"could not close server\", error);\n }\n }\n\n public async connect(transport: Transport) {\n if (this.#server.transport) {\n throw new UnexpectedStateError(\"Server is already connected\");\n }\n\n this.#connectionState = \"connecting\";\n\n try {\n await this.#server.connect(transport);\n\n // Extract session ID from transport if available (HTTP transports only)\n if (\"sessionId\" in transport) {\n const transportWithSessionId = transport as {\n sessionId?: string;\n } & Transport;\n if (typeof transportWithSessionId.sessionId === \"string\") {\n this.#sessionId = transportWithSessionId.sessionId;\n }\n }\n\n let attempt = 0;\n const maxAttempts = 10;\n const retryDelay = 100;\n\n while (attempt++ < maxAttempts) {\n const capabilities = this.#server.getClientCapabilities();\n\n if (capabilities) {\n this.#clientCapabilities = capabilities;\n break;\n }\n\n await delay(retryDelay);\n }\n\n if (!this.#clientCapabilities) {\n this.#logger.warn(\n `[FastMCP warning] could not infer client capabilities after ${maxAttempts} attempts. Connection may be unstable.`,\n );\n }\n\n if (\n this.#rootsConfig?.enabled !== false &&\n this.#clientCapabilities?.roots?.listChanged &&\n typeof this.#server.listRoots === \"function\"\n ) {\n try {\n const roots = await this.#server.listRoots();\n this.#roots = roots?.roots || [];\n } catch (e) {\n if (e instanceof McpError && e.code === ErrorCode.MethodNotFound) {\n this.#logger.debug(\n \"[FastMCP debug] listRoots method not supported by client\",\n );\n } else {\n this.#logger.error(\n `[FastMCP error] received error listing roots.\\n\\n${\n e instanceof Error ? e.stack : JSON.stringify(e)\n }`,\n );\n }\n }\n }\n\n if (this.#clientCapabilities) {\n const pingConfig = this.#getPingConfig(transport);\n\n if (pingConfig.enabled) {\n this.#pingInterval = setInterval(async () => {\n try {\n await this.#server.ping();\n } catch {\n // The reason we are not emitting an error here is because some clients\n // seem to not respond to the ping request, and we don't want to crash the server,\n // e.g., https://github.com/punkpeye/fastmcp/issues/38.\n const logLevel = pingConfig.logLevel;\n\n if (logLevel === \"debug\") {\n this.#logger.debug(\"[FastMCP debug] server ping failed\");\n } else if (logLevel === \"warning\") {\n this.#logger.warn(\n \"[FastMCP warning] server is not responding to ping\",\n );\n } else if (logLevel === \"error\") {\n this.#logger.error(\n \"[FastMCP error] server is not responding to ping\",\n );\n } else {\n this.#logger.info(\"[FastMCP info] server ping failed\");\n }\n }\n }, pingConfig.intervalMs);\n }\n }\n\n // Mark connection as ready and emit event\n this.#connectionState = \"ready\";\n this.emit(\"ready\");\n } catch (error) {\n this.#connectionState = \"error\";\n const errorEvent = {\n error: error instanceof Error ? error : new Error(String(error)),\n };\n this.emit(\"error\", errorEvent);\n throw error;\n }\n }\n\n public async requestSampling(\n message: z.infer[\"params\"],\n options?: RequestOptions,\n ): Promise {\n return this.#server.createMessage(message, options);\n }\n\n public waitForReady(): Promise {\n if (this.isReady) {\n return Promise.resolve();\n }\n\n if (\n this.#connectionState === \"error\" ||\n this.#connectionState === \"closed\"\n ) {\n return Promise.reject(\n new Error(`Connection is in ${this.#connectionState} state`),\n );\n }\n\n return new Promise((resolve, reject) => {\n const timeout = setTimeout(() => {\n reject(\n new Error(\n \"Connection timeout: Session failed to become ready within 5 seconds\",\n ),\n );\n }, 5000);\n\n this.once(\"ready\", () => {\n clearTimeout(timeout);\n resolve();\n });\n\n this.once(\"error\", (event) => {\n clearTimeout(timeout);\n reject(event.error);\n });\n });\n }\n\n #getPingConfig(transport: Transport): {\n enabled: boolean;\n intervalMs: number;\n logLevel: LoggingLevel;\n } {\n const pingConfig = this.#pingConfig || {};\n\n let defaultEnabled = false;\n\n if (\"type\" in transport) {\n // Enable by default for SSE and HTTP streaming\n if (transport.type === \"httpStream\") {\n defaultEnabled = true;\n }\n }\n\n return {\n enabled:\n pingConfig.enabled !== undefined ? pingConfig.enabled : defaultEnabled,\n intervalMs: pingConfig.intervalMs || 5000,\n logLevel: pingConfig.logLevel || \"debug\",\n };\n }\n\n private addPrompt(inputPrompt: InputPrompt) {\n const completers: Record> = {};\n const enums: Record = {};\n const fuseInstances: Record> = {};\n\n for (const argument of inputPrompt.arguments ?? []) {\n if (argument.complete) {\n completers[argument.name] = argument.complete;\n }\n\n if (argument.enum) {\n enums[argument.name] = argument.enum;\n fuseInstances[argument.name] = new Fuse(argument.enum, {\n includeScore: true,\n threshold: 0.3, // More flexible matching!\n });\n }\n }\n\n const prompt = {\n ...inputPrompt,\n complete: async (name: string, value: string, auth?: T) => {\n if (completers[name]) {\n return await completers[name](value, auth);\n }\n\n if (fuseInstances[name]) {\n const result = fuseInstances[name].search(value);\n\n return {\n total: result.length,\n values: result.map((item) => item.item),\n };\n }\n\n return {\n values: [],\n };\n },\n };\n\n this.#prompts.push(prompt);\n }\n\n private addResource(inputResource: Resource) {\n this.#resources.push(inputResource);\n }\n\n private addResourceTemplate(inputResourceTemplate: InputResourceTemplate) {\n const completers: Record> = {};\n\n for (const argument of inputResourceTemplate.arguments ?? []) {\n if (argument.complete) {\n completers[argument.name] = argument.complete;\n }\n }\n\n const resourceTemplate = {\n ...inputResourceTemplate,\n complete: async (name: string, value: string, auth?: T) => {\n if (completers[name]) {\n return await completers[name](value, auth);\n }\n\n return {\n values: [],\n };\n },\n };\n\n this.#resourceTemplates.push(resourceTemplate);\n }\n\n private setupCompleteHandlers() {\n this.#server.setRequestHandler(CompleteRequestSchema, async (request) => {\n if (request.params.ref.type === \"ref/prompt\") {\n const prompt = this.#prompts.find(\n (prompt) => prompt.name === request.params.ref.name,\n );\n\n if (!prompt) {\n throw new UnexpectedStateError(\"Unknown prompt\", {\n request,\n });\n }\n\n if (!prompt.complete) {\n throw new UnexpectedStateError(\"Prompt does not support completion\", {\n request,\n });\n }\n\n const completion = CompletionZodSchema.parse(\n await prompt.complete(\n request.params.argument.name,\n request.params.argument.value,\n this.#auth,\n ),\n );\n\n return {\n completion,\n };\n }\n\n if (request.params.ref.type === \"ref/resource\") {\n const resource = this.#resourceTemplates.find(\n (resource) => resource.uriTemplate === request.params.ref.uri,\n );\n\n if (!resource) {\n throw new UnexpectedStateError(\"Unknown resource\", {\n request,\n });\n }\n\n if (!(\"uriTemplate\" in resource)) {\n throw new UnexpectedStateError(\"Unexpected resource\");\n }\n\n if (!resource.complete) {\n throw new UnexpectedStateError(\n \"Resource does not support completion\",\n {\n request,\n },\n );\n }\n\n const completion = CompletionZodSchema.parse(\n await resource.complete(\n request.params.argument.name,\n request.params.argument.value,\n this.#auth,\n ),\n );\n\n return {\n completion,\n };\n }\n\n throw new UnexpectedStateError(\"Unexpected completion request\", {\n request,\n });\n });\n }\n\n private setupErrorHandling() {\n this.#server.onerror = (error) => {\n this.#logger.error(\"[FastMCP error]\", error);\n };\n }\n\n private setupLoggingHandlers() {\n this.#server.setRequestHandler(SetLevelRequestSchema, (request) => {\n this.#loggingLevel = request.params.level;\n\n return {};\n });\n }\n\n private setupPromptHandlers(prompts: Prompt[]) {\n this.#server.setRequestHandler(ListPromptsRequestSchema, async () => {\n return {\n prompts: prompts.map((prompt) => {\n return {\n arguments: prompt.arguments,\n complete: prompt.complete,\n description: prompt.description,\n name: prompt.name,\n };\n }),\n };\n });\n\n this.#server.setRequestHandler(GetPromptRequestSchema, async (request) => {\n const prompt = prompts.find(\n (prompt) => prompt.name === request.params.name,\n );\n\n if (!prompt) {\n throw new McpError(\n ErrorCode.MethodNotFound,\n `Unknown prompt: ${request.params.name}`,\n );\n }\n\n const args = request.params.arguments;\n\n for (const arg of prompt.arguments ?? []) {\n if (arg.required && !(args && arg.name in args)) {\n throw new McpError(\n ErrorCode.InvalidRequest,\n `Prompt '${request.params.name}' requires argument '${arg.name}': ${\n arg.description || \"No description provided\"\n }`,\n );\n }\n }\n\n let result: Awaited[\"load\"]>>;\n\n try {\n result = await prompt.load(\n args as Record,\n this.#auth,\n );\n } catch (error) {\n const errorMessage =\n error instanceof Error ? error.message : String(error);\n throw new McpError(\n ErrorCode.InternalError,\n `Failed to load prompt '${request.params.name}': ${errorMessage}`,\n );\n }\n\n if (typeof result === \"string\") {\n return {\n description: prompt.description,\n messages: [\n {\n content: { text: result, type: \"text\" },\n role: \"user\",\n },\n ],\n };\n } else {\n return {\n description: prompt.description,\n messages: result.messages,\n };\n }\n });\n }\n\n private setupResourceHandlers(resources: Resource[]) {\n this.#server.setRequestHandler(ListResourcesRequestSchema, async () => {\n return {\n resources: resources.map((resource) => ({\n description: resource.description,\n mimeType: resource.mimeType,\n name: resource.name,\n uri: resource.uri,\n })),\n } satisfies ListResourcesResult;\n });\n\n this.#server.setRequestHandler(\n ReadResourceRequestSchema,\n async (request) => {\n if (\"uri\" in request.params) {\n const resource = resources.find(\n (resource) =>\n \"uri\" in resource && resource.uri === request.params.uri,\n );\n\n if (!resource) {\n for (const resourceTemplate of this.#resourceTemplates) {\n const uriTemplate = parseURITemplate(\n resourceTemplate.uriTemplate,\n );\n\n const match = uriTemplate.fromUri(request.params.uri);\n\n if (!match) {\n continue;\n }\n\n const uri = uriTemplate.fill(match);\n\n const result = await resourceTemplate.load(match, this.#auth);\n\n const resources = Array.isArray(result) ? result : [result];\n return {\n contents: resources.map((resource) => ({\n ...resource,\n description: resourceTemplate.description,\n mimeType: resource.mimeType ?? resourceTemplate.mimeType,\n name: resourceTemplate.name,\n uri: resource.uri ?? uri,\n })),\n };\n }\n\n throw new McpError(\n ErrorCode.MethodNotFound,\n `Resource not found: '${request.params.uri}'. Available resources: ${\n resources.map((r) => r.uri).join(\", \") || \"none\"\n }`,\n );\n }\n\n if (!(\"uri\" in resource)) {\n throw new UnexpectedStateError(\"Resource does not support reading\");\n }\n\n let maybeArrayResult: Awaited[\"load\"]>>;\n\n try {\n maybeArrayResult = await resource.load(this.#auth);\n } catch (error) {\n const errorMessage =\n error instanceof Error ? error.message : String(error);\n throw new McpError(\n ErrorCode.InternalError,\n `Failed to load resource '${resource.name}' (${resource.uri}): ${errorMessage}`,\n {\n uri: resource.uri,\n },\n );\n }\n\n const resourceResults = Array.isArray(maybeArrayResult)\n ? maybeArrayResult\n : [maybeArrayResult];\n\n return {\n contents: resourceResults.map((result) => ({\n ...result,\n mimeType: result.mimeType ?? resource.mimeType,\n name: resource.name,\n uri: result.uri ?? resource.uri,\n })),\n };\n }\n\n throw new UnexpectedStateError(\"Unknown resource request\", {\n request,\n });\n },\n );\n }\n\n private setupResourceTemplateHandlers(\n resourceTemplates: ResourceTemplate[],\n ) {\n this.#server.setRequestHandler(\n ListResourceTemplatesRequestSchema,\n async () => {\n return {\n resourceTemplates: resourceTemplates.map((resourceTemplate) => ({\n description: resourceTemplate.description,\n mimeType: resourceTemplate.mimeType,\n name: resourceTemplate.name,\n uriTemplate: resourceTemplate.uriTemplate,\n })),\n } satisfies ListResourceTemplatesResult;\n },\n );\n }\n\n private setupRootsHandlers() {\n if (this.#rootsConfig?.enabled === false) {\n this.#logger.debug(\n \"[FastMCP debug] roots capability explicitly disabled via config\",\n );\n return;\n }\n\n // Only set up roots notification handling if the server supports it\n if (typeof this.#server.listRoots === \"function\") {\n this.#server.setNotificationHandler(\n RootsListChangedNotificationSchema,\n () => {\n this.#server\n .listRoots()\n .then((roots) => {\n this.#roots = roots.roots;\n\n this.emit(\"rootsChanged\", {\n roots: roots.roots,\n });\n })\n .catch((error) => {\n if (\n error instanceof McpError &&\n error.code === ErrorCode.MethodNotFound\n ) {\n this.#logger.debug(\n \"[FastMCP debug] listRoots method not supported by client\",\n );\n } else {\n this.#logger.error(\n `[FastMCP error] received error listing roots.\\n\\n${\n error instanceof Error ? error.stack : JSON.stringify(error)\n }`,\n );\n }\n });\n },\n );\n } else {\n this.#logger.debug(\n \"[FastMCP debug] roots capability not available, not setting up notification handler\",\n );\n }\n }\n\n private setupToolHandlers(tools: Tool[]) {\n this.#server.setRequestHandler(ListToolsRequestSchema, async () => {\n return {\n tools: await Promise.all(\n tools.map(async (tool) => {\n return {\n annotations: tool.annotations,\n description: tool.description,\n inputSchema: tool.parameters\n ? await toJsonSchema(tool.parameters)\n : {\n additionalProperties: false,\n properties: {},\n type: \"object\",\n }, // More complete schema for Cursor compatibility\n name: tool.name,\n };\n }),\n ),\n };\n });\n\n this.#server.setRequestHandler(CallToolRequestSchema, async (request) => {\n const tool = tools.find((tool) => tool.name === request.params.name);\n\n if (!tool) {\n throw new McpError(\n ErrorCode.MethodNotFound,\n `Unknown tool: ${request.params.name}`,\n );\n }\n\n let args: unknown = undefined;\n\n if (tool.parameters) {\n const parsed = await tool.parameters[\"~standard\"].validate(\n request.params.arguments,\n );\n\n if (parsed.issues) {\n const friendlyErrors = this.#utils?.formatInvalidParamsErrorMessage\n ? this.#utils.formatInvalidParamsErrorMessage(parsed.issues)\n : parsed.issues\n .map((issue) => {\n const path = issue.path?.join(\".\") || \"root\";\n return `${path}: ${issue.message}`;\n })\n .join(\", \");\n\n throw new McpError(\n ErrorCode.InvalidParams,\n `Tool '${request.params.name}' parameter validation failed: ${friendlyErrors}. Please check the parameter types and values according to the tool's schema.`,\n );\n }\n\n args = parsed.value;\n }\n\n const progressToken = request.params?._meta?.progressToken;\n\n let result: ContentResult;\n\n try {\n const reportProgress = async (progress: Progress) => {\n try {\n await this.#server.notification({\n method: \"notifications/progress\",\n params: {\n ...progress,\n progressToken,\n },\n });\n\n if (this.#needsEventLoopFlush) {\n await new Promise((resolve) => setImmediate(resolve));\n }\n } catch (progressError) {\n this.#logger.warn(\n `[FastMCP warning] Failed to report progress for tool '${request.params.name}':`,\n progressError instanceof Error\n ? progressError.message\n : String(progressError),\n );\n }\n };\n\n const log = {\n debug: (message: string, context?: SerializableValue) => {\n this.#server.sendLoggingMessage({\n data: {\n context,\n message,\n },\n level: \"debug\",\n });\n },\n error: (message: string, context?: SerializableValue) => {\n this.#server.sendLoggingMessage({\n data: {\n context,\n message,\n },\n level: \"error\",\n });\n },\n info: (message: string, context?: SerializableValue) => {\n this.#server.sendLoggingMessage({\n data: {\n context,\n message,\n },\n level: \"info\",\n });\n },\n warn: (message: string, context?: SerializableValue) => {\n this.#server.sendLoggingMessage({\n data: {\n context,\n message,\n },\n level: \"warning\",\n });\n },\n };\n\n // Create a promise for tool execution\n // Streams partial results while a tool is still executing\n // Enables progressive rendering and real-time feedback\n const streamContent = async (content: Content | Content[]) => {\n const contentArray = Array.isArray(content) ? content : [content];\n\n try {\n await this.#server.notification({\n method: \"notifications/tool/streamContent\",\n params: {\n content: contentArray,\n toolName: request.params.name,\n },\n });\n\n if (this.#needsEventLoopFlush) {\n await new Promise((resolve) => setImmediate(resolve));\n }\n } catch (streamError) {\n this.#logger.warn(\n `[FastMCP warning] Failed to stream content for tool '${request.params.name}':`,\n streamError instanceof Error\n ? streamError.message\n : String(streamError),\n );\n }\n };\n const executeToolPromise = tool.execute(args, {\n client: {\n version: this.#server.getClientVersion(),\n },\n log,\n reportProgress,\n requestId:\n typeof request.params?._meta?.requestId === \"string\"\n ? request.params._meta.requestId\n : undefined,\n requestMetadata: request.params._meta,\n session: this.#auth,\n sessionId: this.#sessionId,\n streamContent,\n });\n\n // Handle timeout if specified\n const maybeStringResult = (await (tool.timeoutMs\n ? Promise.race([\n executeToolPromise,\n new Promise((_, reject) => {\n const timeoutId = setTimeout(() => {\n reject(\n new McpError(\n ErrorCode.InternalError,\n `Tool '${request.params.name}' timed out after ${tool.timeoutMs}ms. Consider increasing timeoutMs or optimizing the tool implementation.`,\n ),\n );\n }, tool.timeoutMs);\n\n // If promise resolves first\n executeToolPromise.finally(() => clearTimeout(timeoutId));\n }),\n ])\n : executeToolPromise)) as\n | AudioContent\n | ContentResult\n | ImageContent\n | null\n | ResourceContent\n | ResourceLink\n | string\n | TextContent\n | undefined;\n\n // Without this test, we are running into situations where the last progress update is not reported.\n // See the 'reports multiple progress updates without buffering' test in FastMCP.test.ts before refactoring.\n await delay(1);\n\n if (maybeStringResult === undefined || maybeStringResult === null) {\n result = ContentResultZodSchema.parse({\n content: [],\n });\n } else if (typeof maybeStringResult === \"string\") {\n result = ContentResultZodSchema.parse({\n content: [{ text: maybeStringResult, type: \"text\" }],\n });\n } else if (\"type\" in maybeStringResult) {\n result = ContentResultZodSchema.parse({\n content: [maybeStringResult],\n });\n } else {\n result = ContentResultZodSchema.parse(maybeStringResult);\n }\n } catch (error) {\n // Re-throw McpError to let the MCP SDK handle it as a proper JSON-RPC error\n if (error instanceof McpError) {\n throw error;\n }\n\n if (error instanceof UserError) {\n return {\n content: [{ text: error.message, type: \"text\" }],\n isError: true,\n ...(error.extras ? { structuredContent: error.extras } : {}),\n };\n }\n\n const errorMessage =\n error instanceof Error ? error.message : String(error);\n return {\n content: [\n {\n text: `Tool '${request.params.name}' execution failed: ${errorMessage}`,\n type: \"text\",\n },\n ],\n isError: true,\n };\n }\n\n return result;\n });\n }\n}\n\n/**\n * Converts camelCase to snake_case for OAuth endpoint responses\n */\nfunction camelToSnakeCase(str: string): string {\n return str.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`);\n}\n\n/**\n * Converts an object with camelCase keys to snake_case keys\n */\nfunction convertObjectToSnakeCase(\n obj: Record,\n): Record {\n const result: Record = {};\n\n for (const [key, value] of Object.entries(obj)) {\n const snakeKey = camelToSnakeCase(key);\n result[snakeKey] = value;\n }\n\n return result;\n}\n\nconst FastMCPEventEmitterBase: {\n new (): StrictEventEmitter>;\n} = EventEmitter;\n\nclass FastMCPEventEmitter extends FastMCPEventEmitterBase {}\n\nexport class FastMCP<\n T extends FastMCPSessionAuth = FastMCPSessionAuth,\n> extends FastMCPEventEmitter {\n public get sessions(): FastMCPSession[] {\n return this.#sessions;\n }\n #authenticate: Authenticate | undefined;\n #httpStreamServer: null | SSEServer = null;\n #logger: Logger;\n #options: ServerOptions;\n #prompts: InputPrompt[] = [];\n #resources: Resource[] = [];\n #resourcesTemplates: InputResourceTemplate[] = [];\n #sessions: FastMCPSession[] = [];\n\n #tools: Tool[] = [];\n\n constructor(public options: ServerOptions) {\n super();\n\n this.#options = options;\n this.#authenticate = options.authenticate;\n this.#logger = options.logger || console;\n }\n\n /**\n * Adds a prompt to the server.\n */\n public addPrompt[]>(\n prompt: InputPrompt,\n ) {\n this.#prompts.push(prompt);\n }\n\n /**\n * Adds a resource to the server.\n */\n public addResource(resource: Resource) {\n this.#resources.push(resource);\n }\n\n /**\n * Adds a resource template to the server.\n */\n public addResourceTemplate<\n const Args extends InputResourceTemplateArgument[],\n >(resource: InputResourceTemplate) {\n this.#resourcesTemplates.push(resource);\n }\n\n /**\n * Adds a tool to the server.\n */\n public addTool(tool: Tool) {\n this.#tools.push(tool as unknown as Tool);\n }\n\n /**\n * Embeds a resource by URI, making it easy to include resources in tool responses.\n *\n * @param uri - The URI of the resource to embed\n * @returns Promise - The embedded resource content\n */\n public async embedded(uri: string): Promise {\n // First, try to find a direct resource match\n const directResource = this.#resources.find(\n (resource) => resource.uri === uri,\n );\n\n if (directResource) {\n const result = await directResource.load();\n const results = Array.isArray(result) ? result : [result];\n const firstResult = results[0];\n\n const resourceData: ResourceContent[\"resource\"] = {\n mimeType: directResource.mimeType,\n uri,\n };\n\n if (\"text\" in firstResult) {\n resourceData.text = firstResult.text;\n }\n\n if (\"blob\" in firstResult) {\n resourceData.blob = firstResult.blob;\n }\n\n return resourceData;\n }\n\n // Try to match against resource templates\n for (const template of this.#resourcesTemplates) {\n const parsedTemplate = parseURITemplate(template.uriTemplate);\n const params = parsedTemplate.fromUri(uri);\n if (!params) {\n continue;\n }\n\n const result = await template.load(\n params as ResourceTemplateArgumentsToObject,\n );\n\n const resourceData: ResourceContent[\"resource\"] = {\n mimeType: template.mimeType,\n uri,\n };\n\n if (\"text\" in result) {\n resourceData.text = result.text;\n }\n\n if (\"blob\" in result) {\n resourceData.blob = result.blob;\n }\n\n return resourceData; // The resource we're looking for\n }\n\n throw new UnexpectedStateError(`Resource not found: ${uri}`, { uri });\n }\n\n /**\n * Starts the server.\n */\n public async start(\n options?: Partial<{\n httpStream: {\n enableJsonResponse?: boolean;\n endpoint?: `/${string}`;\n eventStore?: EventStore;\n host?: string;\n port: number;\n stateless?: boolean;\n };\n transportType: \"httpStream\" | \"stdio\";\n }>,\n ) {\n const config = this.#parseRuntimeConfig(options);\n\n if (config.transportType === \"stdio\") {\n const transport = new StdioServerTransport();\n\n // For stdio transport, if authenticate function is provided, call it\n // with undefined request (since stdio doesn't have HTTP request context)\n let auth: T | undefined;\n\n if (this.#authenticate) {\n try {\n auth = await this.#authenticate(\n undefined as unknown as http.IncomingMessage,\n );\n } catch (error) {\n this.#logger.error(\n \"[FastMCP error] Authentication failed for stdio transport:\",\n error instanceof Error ? error.message : String(error),\n );\n // Continue without auth if authentication fails\n }\n }\n\n const session = new FastMCPSession({\n auth,\n instructions: this.#options.instructions,\n logger: this.#logger,\n name: this.#options.name,\n ping: this.#options.ping,\n prompts: this.#prompts,\n resources: this.#resources,\n resourcesTemplates: this.#resourcesTemplates,\n roots: this.#options.roots,\n tools: this.#tools,\n transportType: \"stdio\",\n utils: this.#options.utils,\n version: this.#options.version,\n });\n\n await session.connect(transport);\n\n this.#sessions.push(session);\n\n session.once(\"error\", () => {\n this.#removeSession(session);\n });\n\n // Monitor the underlying transport for close events\n if (transport.onclose) {\n const originalOnClose = transport.onclose;\n\n transport.onclose = () => {\n this.#removeSession(session);\n\n if (originalOnClose) {\n originalOnClose();\n }\n };\n } else {\n transport.onclose = () => {\n this.#removeSession(session);\n };\n }\n\n this.emit(\"connect\", {\n session: session as FastMCPSession,\n });\n } else if (config.transportType === \"httpStream\") {\n const httpConfig = config.httpStream;\n\n if (httpConfig.stateless) {\n // Stateless mode - create new server instance for each request\n this.#logger.info(\n `[FastMCP info] Starting server in stateless mode on HTTP Stream at http://${httpConfig.host}:${httpConfig.port}${httpConfig.endpoint}`,\n );\n\n this.#httpStreamServer = await startHTTPServer>({\n ...(this.#authenticate ? { authenticate: this.#authenticate } : {}),\n createServer: async (request) => {\n let auth: T | undefined;\n\n if (this.#authenticate) {\n auth = await this.#authenticate(request);\n\n // In stateless mode, authentication is REQUIRED\n // mcp-proxy will catch this error and return 401\n if (auth === undefined || auth === null) {\n throw new Error(\"Authentication required\");\n }\n }\n\n // Extract session ID from headers\n const sessionId = Array.isArray(request.headers[\"mcp-session-id\"])\n ? request.headers[\"mcp-session-id\"][0]\n : request.headers[\"mcp-session-id\"];\n\n // In stateless mode, create a new session for each request\n // without persisting it in the sessions array\n return this.#createSession(auth, sessionId);\n },\n enableJsonResponse: httpConfig.enableJsonResponse,\n eventStore: httpConfig.eventStore,\n host: httpConfig.host,\n // In stateless mode, we don't track sessions\n onClose: async () => {\n // No session tracking in stateless mode\n },\n onConnect: async () => {\n // No persistent session tracking in stateless mode\n this.#logger.debug(\n `[FastMCP debug] Stateless HTTP Stream request handled`,\n );\n },\n onUnhandledRequest: async (req, res) => {\n await this.#handleUnhandledRequest(req, res, true, httpConfig.host);\n },\n port: httpConfig.port,\n stateless: true,\n streamEndpoint: httpConfig.endpoint,\n });\n } else {\n // Regular mode with session management\n this.#httpStreamServer = await startHTTPServer>({\n ...(this.#authenticate ? { authenticate: this.#authenticate } : {}),\n createServer: async (request) => {\n let auth: T | undefined;\n\n if (this.#authenticate) {\n auth = await this.#authenticate(request);\n }\n\n // Extract session ID from headers\n const sessionId = Array.isArray(request.headers[\"mcp-session-id\"])\n ? request.headers[\"mcp-session-id\"][0]\n : request.headers[\"mcp-session-id\"];\n\n return this.#createSession(auth, sessionId);\n },\n enableJsonResponse: httpConfig.enableJsonResponse,\n eventStore: httpConfig.eventStore,\n host: httpConfig.host,\n onClose: async (session) => {\n const sessionIndex = this.#sessions.indexOf(session);\n\n if (sessionIndex !== -1) this.#sessions.splice(sessionIndex, 1);\n\n this.emit(\"disconnect\", {\n session: session as FastMCPSession,\n });\n },\n onConnect: async (session) => {\n this.#sessions.push(session);\n\n this.#logger.info(`[FastMCP info] HTTP Stream session established`);\n\n this.emit(\"connect\", {\n session: session as FastMCPSession,\n });\n },\n\n onUnhandledRequest: async (req, res) => {\n await this.#handleUnhandledRequest(\n req,\n res,\n false,\n httpConfig.host,\n );\n },\n port: httpConfig.port,\n stateless: httpConfig.stateless,\n streamEndpoint: httpConfig.endpoint,\n });\n\n this.#logger.info(\n `[FastMCP info] server is running on HTTP Stream at http://${httpConfig.host}:${httpConfig.port}${httpConfig.endpoint}`,\n );\n }\n } else {\n throw new Error(\"Invalid transport type\");\n }\n }\n\n /**\n * Stops the server.\n */\n public async stop() {\n if (this.#httpStreamServer) {\n await this.#httpStreamServer.close();\n }\n }\n\n /**\n * Creates a new FastMCPSession instance with the current configuration.\n * Used both for regular sessions and stateless requests.\n */\n #createSession(auth?: T, sessionId?: string): FastMCPSession {\n // Check if authentication failed\n if (\n auth &&\n typeof auth === \"object\" &&\n \"authenticated\" in auth &&\n !(auth as { authenticated: unknown }).authenticated\n ) {\n const errorMessage =\n \"error\" in auth &&\n typeof (auth as { error: unknown }).error === \"string\"\n ? (auth as { error: string }).error\n : \"Authentication failed\";\n throw new Error(errorMessage);\n }\n\n const allowedTools = auth\n ? this.#tools.filter((tool) =>\n tool.canAccess ? tool.canAccess(auth) : true,\n )\n : this.#tools;\n return new FastMCPSession({\n auth,\n instructions: this.#options.instructions,\n logger: this.#logger,\n name: this.#options.name,\n ping: this.#options.ping,\n prompts: this.#prompts,\n resources: this.#resources,\n resourcesTemplates: this.#resourcesTemplates,\n roots: this.#options.roots,\n sessionId,\n tools: allowedTools,\n transportType: \"httpStream\",\n utils: this.#options.utils,\n version: this.#options.version,\n });\n }\n\n /**\n * Handles unhandled HTTP requests with health, readiness, and OAuth endpoints\n */\n #handleUnhandledRequest = async (\n req: http.IncomingMessage,\n res: http.ServerResponse,\n isStateless = false,\n host: string,\n ) => {\n const healthConfig = this.#options.health ?? {};\n\n const enabled =\n healthConfig.enabled === undefined ? true : healthConfig.enabled;\n\n if (enabled) {\n const path = healthConfig.path ?? \"/health\";\n const url = new URL(req.url || \"\", `http://${host}`);\n\n try {\n if (req.method === \"GET\" && url.pathname === path) {\n res\n .writeHead(healthConfig.status ?? 200, {\n \"Content-Type\": \"text/plain\",\n })\n .end(healthConfig.message ?? \"✓ Ok\");\n\n return;\n }\n\n // Enhanced readiness check endpoint\n if (req.method === \"GET\" && url.pathname === \"/ready\") {\n if (isStateless) {\n // In stateless mode, we're always ready if the server is running\n const response = {\n mode: \"stateless\",\n ready: 1,\n status: \"ready\",\n total: 1,\n };\n\n res\n .writeHead(200, {\n \"Content-Type\": \"application/json\",\n })\n .end(JSON.stringify(response));\n } else {\n const readySessions = this.#sessions.filter(\n (s) => s.isReady,\n ).length;\n const totalSessions = this.#sessions.length;\n const allReady =\n readySessions === totalSessions && totalSessions > 0;\n\n const response = {\n ready: readySessions,\n status: allReady\n ? \"ready\"\n : totalSessions === 0\n ? \"no_sessions\"\n : \"initializing\",\n total: totalSessions,\n };\n\n res\n .writeHead(allReady ? 200 : 503, {\n \"Content-Type\": \"application/json\",\n })\n .end(JSON.stringify(response));\n }\n\n return;\n }\n } catch (error) {\n this.#logger.error(\"[FastMCP error] health endpoint error\", error);\n }\n }\n\n // Handle OAuth well-known endpoints\n const oauthConfig = this.#options.oauth;\n if (oauthConfig?.enabled && req.method === \"GET\") {\n const url = new URL(req.url || \"\", `http://${host}`);\n\n if (\n url.pathname === \"/.well-known/oauth-authorization-server\" &&\n oauthConfig.authorizationServer\n ) {\n const metadata = convertObjectToSnakeCase(\n oauthConfig.authorizationServer,\n );\n res\n .writeHead(200, {\n \"Content-Type\": \"application/json\",\n })\n .end(JSON.stringify(metadata));\n return;\n }\n\n if (\n url.pathname === \"/.well-known/oauth-protected-resource\" &&\n oauthConfig.protectedResource\n ) {\n const metadata = convertObjectToSnakeCase(\n oauthConfig.protectedResource,\n );\n res\n .writeHead(200, {\n \"Content-Type\": \"application/json\",\n })\n .end(JSON.stringify(metadata));\n return;\n }\n }\n\n // If the request was not handled above, return 404\n res.writeHead(404).end();\n };\n\n #parseRuntimeConfig(\n overrides?: Partial<{\n httpStream: {\n enableJsonResponse?: boolean;\n endpoint?: `/${string}`;\n host?: string;\n port: number;\n stateless?: boolean;\n };\n transportType: \"httpStream\" | \"stdio\";\n }>,\n ):\n | {\n httpStream: {\n enableJsonResponse?: boolean;\n endpoint: `/${string}`;\n eventStore?: EventStore;\n host: string;\n port: number;\n stateless?: boolean;\n };\n transportType: \"httpStream\";\n }\n | { transportType: \"stdio\" } {\n const args = process.argv.slice(2);\n const getArg = (name: string) => {\n const index = args.findIndex((arg) => arg === `--${name}`);\n\n return index !== -1 && index + 1 < args.length\n ? args[index + 1]\n : undefined;\n };\n\n const transportArg = getArg(\"transport\");\n const portArg = getArg(\"port\");\n const endpointArg = getArg(\"endpoint\");\n const statelessArg = getArg(\"stateless\");\n const hostArg = getArg(\"host\");\n\n const envTransport = process.env.FASTMCP_TRANSPORT;\n const envPort = process.env.FASTMCP_PORT;\n const envEndpoint = process.env.FASTMCP_ENDPOINT;\n const envStateless = process.env.FASTMCP_STATELESS;\n const envHost = process.env.FASTMCP_HOST;\n // Overrides > CLI > env > defaults\n const transportType =\n overrides?.transportType ||\n (transportArg === \"http-stream\" ? \"httpStream\" : transportArg) ||\n envTransport ||\n \"stdio\";\n\n if (transportType === \"httpStream\") {\n const port = parseInt(\n overrides?.httpStream?.port?.toString() || portArg || envPort || \"8080\",\n );\n const host =\n overrides?.httpStream?.host || hostArg || envHost || \"localhost\";\n const endpoint =\n overrides?.httpStream?.endpoint || endpointArg || envEndpoint || \"/mcp\";\n const enableJsonResponse =\n overrides?.httpStream?.enableJsonResponse || false;\n const stateless =\n overrides?.httpStream?.stateless ||\n statelessArg === \"true\" ||\n envStateless === \"true\" ||\n false;\n\n return {\n httpStream: {\n enableJsonResponse,\n endpoint: endpoint as `/${string}`,\n host,\n port,\n stateless,\n },\n transportType: \"httpStream\" as const,\n };\n }\n\n return { transportType: \"stdio\" as const };\n }\n\n #removeSession(session: FastMCPSession): void {\n const sessionIndex = this.#sessions.indexOf(session);\n\n if (sessionIndex !== -1) {\n this.#sessions.splice(sessionIndex, 1);\n this.emit(\"disconnect\", {\n session: session as FastMCPSession,\n });\n }\n }\n}\n\nexport { ErrorCode, McpError } from \"@modelcontextprotocol/sdk/types.js\";\n\nexport type {\n AudioContent,\n Content,\n ContentResult,\n Context,\n FastMCPEvents,\n FastMCPSessionEvents,\n ImageContent,\n InputPrompt,\n InputPromptArgument,\n LoggingLevel,\n Progress,\n Prompt,\n PromptArgument,\n RequestMeta,\n Resource,\n ResourceContent,\n ResourceLink,\n ResourceResult,\n ResourceTemplate,\n ResourceTemplateArgument,\n SerializableValue,\n ServerOptions,\n TextContent,\n Tool,\n ToolParameters,\n};\n"],"mappings":"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","names":["ErrorCode","McpError","prompt","resource","resources","tool"]} \ No newline at end of file diff --git a/dist/bin/fastmcp.d.ts b/dist/bin/fastmcp.d.ts new file mode 100644 index 0000000..908ba84 --- /dev/null +++ b/dist/bin/fastmcp.d.ts @@ -0,0 +1 @@ +#!/usr/bin/env node diff --git a/dist/bin/fastmcp.js b/dist/bin/fastmcp.js new file mode 100755 index 0000000..5c6478d --- /dev/null +++ b/dist/bin/fastmcp.js @@ -0,0 +1,152 @@ +#!/usr/bin/env node + +// src/bin/fastmcp.ts +import { execa } from "execa"; +import yargs from "yargs"; +import { hideBin } from "yargs/helpers"; +await yargs(hideBin(process.argv)).scriptName("fastmcp").command( + "dev ", + "Start a development server", + (yargs2) => { + return yargs2.positional("file", { + demandOption: true, + describe: "The path to the server file", + type: "string" + }).option("watch", { + alias: "w", + default: false, + describe: "Watch for file changes and restart server", + type: "boolean" + }).option("verbose", { + alias: "v", + default: false, + describe: "Enable verbose logging", + type: "boolean" + }); + }, + async (argv) => { + try { + const command = argv.watch ? `npx @wong2/mcp-cli npx tsx --watch ${argv.file}` : `npx @wong2/mcp-cli npx tsx ${argv.file}`; + if (argv.verbose) { + console.log(`[FastMCP] Starting server: ${command}`); + console.log(`[FastMCP] File: ${argv.file}`); + console.log( + `[FastMCP] Watch mode: ${argv.watch ? "enabled" : "disabled"}` + ); + } + await execa({ + shell: true, + stderr: "inherit", + stdin: "inherit", + stdout: "inherit" + })`${command}`; + } catch (error) { + console.error( + "[FastMCP Error] Failed to start development server:", + error instanceof Error ? error.message : String(error) + ); + if (argv.verbose && error instanceof Error && error.stack) { + console.error("[FastMCP Debug] Stack trace:", error.stack); + } + process.exit(1); + } + } +).command( + "inspect ", + "Inspect a server file", + (yargs2) => { + return yargs2.positional("file", { + demandOption: true, + describe: "The path to the server file", + type: "string" + }); + }, + async (argv) => { + try { + await execa({ + stderr: "inherit", + stdout: "inherit" + })`npx @modelcontextprotocol/inspector npx tsx ${argv.file}`; + } catch (error) { + console.error( + "[FastMCP Error] Failed to inspect server:", + error instanceof Error ? error.message : String(error) + ); + process.exit(1); + } + } +).command( + "validate ", + "Validate a FastMCP server file for syntax and basic structure", + (yargs2) => { + return yargs2.positional("file", { + demandOption: true, + describe: "The path to the server file", + type: "string" + }).option("strict", { + alias: "s", + default: false, + describe: "Enable strict validation (type checking)", + type: "boolean" + }); + }, + async (argv) => { + try { + const { existsSync } = await import("fs"); + const { resolve } = await import("path"); + const filePath = resolve(argv.file); + if (!existsSync(filePath)) { + console.error(`[FastMCP Error] File not found: ${filePath}`); + process.exit(1); + } + console.log(`[FastMCP] Validating server file: ${filePath}`); + const command = argv.strict ? `npx tsc --noEmit --strict ${filePath}` : `npx tsc --noEmit ${filePath}`; + try { + await execa({ + shell: true, + stderr: "pipe", + stdout: "pipe" + })`${command}`; + console.log("[FastMCP] \u2713 TypeScript compilation successful"); + } catch (tsError) { + console.error("[FastMCP] \u2717 TypeScript compilation failed"); + if (tsError instanceof Error && "stderr" in tsError) { + console.error(tsError.stderr); + } + process.exit(1); + } + try { + await execa({ + shell: true, + stderr: "pipe", + stdout: "pipe" + })`node -e " + (async () => { + try { + const { FastMCP } = await import('fastmcp'); + await import('file://${filePath}'); + console.log('[FastMCP] ✓ Server structure validation passed'); + } catch (error) { + console.error('[FastMCP] ✗ Server structure validation failed:', error.message); + process.exit(1); + } + })(); + "`; + } catch { + console.error("[FastMCP] \u2717 Server structure validation failed"); + console.error("Make sure the file properly imports and uses FastMCP"); + process.exit(1); + } + console.log( + "[FastMCP] \u2713 All validations passed! Server file looks good." + ); + } catch (error) { + console.error( + "[FastMCP Error] Validation failed:", + error instanceof Error ? error.message : String(error) + ); + process.exit(1); + } + } +).help().parseAsync(); +//# sourceMappingURL=fastmcp.js.map \ No newline at end of file diff --git a/dist/bin/fastmcp.js.map b/dist/bin/fastmcp.js.map new file mode 100644 index 0000000..b8fe21f --- /dev/null +++ b/dist/bin/fastmcp.js.map @@ -0,0 +1 @@ +{"version":3,"sources":["../../src/bin/fastmcp.ts"],"sourcesContent":["#!/usr/bin/env node\n\nimport { execa } from \"execa\";\nimport yargs from \"yargs\";\nimport { hideBin } from \"yargs/helpers\";\n\nawait yargs(hideBin(process.argv))\n .scriptName(\"fastmcp\")\n .command(\n \"dev \",\n \"Start a development server\",\n (yargs) => {\n return yargs\n .positional(\"file\", {\n demandOption: true,\n describe: \"The path to the server file\",\n type: \"string\",\n })\n\n .option(\"watch\", {\n alias: \"w\",\n default: false,\n describe: \"Watch for file changes and restart server\",\n type: \"boolean\",\n })\n\n .option(\"verbose\", {\n alias: \"v\",\n default: false,\n describe: \"Enable verbose logging\",\n type: \"boolean\",\n });\n },\n\n async (argv) => {\n try {\n const command = argv.watch\n ? `npx @wong2/mcp-cli npx tsx --watch ${argv.file}`\n : `npx @wong2/mcp-cli npx tsx ${argv.file}`;\n\n if (argv.verbose) {\n console.log(`[FastMCP] Starting server: ${command}`);\n console.log(`[FastMCP] File: ${argv.file}`);\n console.log(\n `[FastMCP] Watch mode: ${argv.watch ? \"enabled\" : \"disabled\"}`,\n );\n }\n\n await execa({\n shell: true,\n stderr: \"inherit\",\n stdin: \"inherit\",\n stdout: \"inherit\",\n })`${command}`;\n } catch (error) {\n console.error(\n \"[FastMCP Error] Failed to start development server:\",\n error instanceof Error ? error.message : String(error),\n );\n\n if (argv.verbose && error instanceof Error && error.stack) {\n console.error(\"[FastMCP Debug] Stack trace:\", error.stack);\n }\n\n process.exit(1);\n }\n },\n )\n\n .command(\n \"inspect \",\n \"Inspect a server file\",\n (yargs) => {\n return yargs.positional(\"file\", {\n demandOption: true,\n describe: \"The path to the server file\",\n type: \"string\",\n });\n },\n\n async (argv) => {\n try {\n await execa({\n stderr: \"inherit\",\n stdout: \"inherit\",\n })`npx @modelcontextprotocol/inspector npx tsx ${argv.file}`;\n } catch (error) {\n console.error(\n \"[FastMCP Error] Failed to inspect server:\",\n error instanceof Error ? error.message : String(error),\n );\n\n process.exit(1);\n }\n },\n )\n\n .command(\n \"validate \",\n \"Validate a FastMCP server file for syntax and basic structure\",\n (yargs) => {\n return yargs\n .positional(\"file\", {\n demandOption: true,\n describe: \"The path to the server file\",\n type: \"string\",\n })\n\n .option(\"strict\", {\n alias: \"s\",\n default: false,\n describe: \"Enable strict validation (type checking)\",\n type: \"boolean\",\n });\n },\n\n async (argv) => {\n try {\n const { existsSync } = await import(\"fs\");\n const { resolve } = await import(\"path\");\n const filePath = resolve(argv.file);\n\n if (!existsSync(filePath)) {\n console.error(`[FastMCP Error] File not found: ${filePath}`);\n process.exit(1);\n }\n\n console.log(`[FastMCP] Validating server file: ${filePath}`);\n\n const command = argv.strict\n ? `npx tsc --noEmit --strict ${filePath}`\n : `npx tsc --noEmit ${filePath}`;\n\n try {\n await execa({\n shell: true,\n stderr: \"pipe\",\n stdout: \"pipe\",\n })`${command}`;\n\n console.log(\"[FastMCP] ✓ TypeScript compilation successful\");\n } catch (tsError) {\n console.error(\"[FastMCP] ✗ TypeScript compilation failed\");\n\n if (tsError instanceof Error && \"stderr\" in tsError) {\n console.error(tsError.stderr);\n }\n\n process.exit(1);\n }\n\n try {\n await execa({\n shell: true,\n stderr: \"pipe\",\n stdout: \"pipe\",\n })`node -e \"\n (async () => {\n try {\n const { FastMCP } = await import('fastmcp');\n await import('file://${filePath}');\n console.log('[FastMCP] ✓ Server structure validation passed');\n } catch (error) {\n console.error('[FastMCP] ✗ Server structure validation failed:', error.message);\n process.exit(1);\n }\n })();\n \"`;\n } catch {\n console.error(\"[FastMCP] ✗ Server structure validation failed\");\n console.error(\"Make sure the file properly imports and uses FastMCP\");\n\n process.exit(1);\n }\n\n console.log(\n \"[FastMCP] ✓ All validations passed! Server file looks good.\",\n );\n } catch (error) {\n console.error(\n \"[FastMCP Error] Validation failed:\",\n error instanceof Error ? error.message : String(error),\n );\n\n process.exit(1);\n }\n },\n )\n\n .help()\n .parseAsync();\n"],"mappings":"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","names":["yargs"]} \ No newline at end of file diff --git a/package.json b/package.json index 852ee00..36c448b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "fastmcp", - "version": "1.0.0", + "version": "1.0.0-edgeandnode.1", "main": "dist/FastMCP.js", "scripts": { "build": "tsup", @@ -22,7 +22,7 @@ "module": "dist/FastMCP.js", "types": "dist/FastMCP.d.ts", "dependencies": { - "@modelcontextprotocol/sdk": "^1.17.2", + "@modelcontextprotocol/sdk": "github:edgeandnode/mcp-typescript-sdk#2de06543904483073d8cc13db1d0e08e16601081", "@standard-schema/spec": "^1.0.0", "execa": "^9.6.0", "file-type": "^21.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b662093..ce73745 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,8 +9,8 @@ importers: .: dependencies: '@modelcontextprotocol/sdk': - specifier: ^1.17.2 - version: 1.17.2 + specifier: github:edgeandnode/mcp-typescript-sdk#2de06543904483073d8cc13db1d0e08e16601081 + version: https://codeload.github.com/edgeandnode/mcp-typescript-sdk/tar.gz/2de06543904483073d8cc13db1d0e08e16601081 '@standard-schema/spec': specifier: ^1.0.0 version: 1.0.0 @@ -423,6 +423,11 @@ packages: resolution: {integrity: sha512-EFLRNXR/ixpXQWu6/3Cu30ndDFIFNaqUXcTqsGebujeMan9FzhAaFFswLRiFj61rgygDRr8WO1N+UijjgRxX9g==} engines: {node: '>=18'} + '@modelcontextprotocol/sdk@https://codeload.github.com/edgeandnode/mcp-typescript-sdk/tar.gz/2de06543904483073d8cc13db1d0e08e16601081': + resolution: {tarball: https://codeload.github.com/edgeandnode/mcp-typescript-sdk/tar.gz/2de06543904483073d8cc13db1d0e08e16601081} + version: 1.20.1-edgeandnode.1 + engines: {node: '>=18'} + '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -4010,6 +4015,23 @@ snapshots: transitivePeerDependencies: - supports-color + '@modelcontextprotocol/sdk@https://codeload.github.com/edgeandnode/mcp-typescript-sdk/tar.gz/2de06543904483073d8cc13db1d0e08e16601081': + dependencies: + ajv: 6.12.6 + content-type: 1.0.5 + cors: 2.8.5 + cross-spawn: 7.0.6 + eventsource: 3.0.7 + eventsource-parser: 3.0.3 + express: 5.1.0 + express-rate-limit: 7.5.1(express@5.1.0) + pkce-challenge: 5.0.0 + raw-body: 3.0.0 + zod: 3.25.76 + zod-to-json-schema: 3.24.6(zod@3.25.76) + transitivePeerDependencies: + - supports-color + '@nodelib/fs.scandir@2.1.5': dependencies: '@nodelib/fs.stat': 2.0.5 From d006d3e2285c48e456a0131e8e52f716e17f23b6 Mon Sep 17 00:00:00 2001 From: Pablo Carranza Velez Date: Wed, 10 Dec 2025 09:44:10 -0300 Subject: [PATCH 3/7] feat: add CustomMcpError with __isMcpError marker for robust error detection Add CustomMcpError class that extends McpError with a __isMcpError marker property to enable error detection across module boundaries. This solves instanceof failures when modules are loaded multiple times (e.g., with tsx, different bundlers, or ESM context isolation). Changes: - Add CustomMcpError class extending McpError with __isMcpError marker - Add isMcpErrorLike() type guard checking both instanceof and marker - Update tool error handling to use isMcpErrorLike() instead of instanceof - Export CustomMcpError and isMcpErrorLike for use in middleware This allows x402 payment middleware to throw errors that FastMCP will correctly re-throw as JSON-RPC errors instead of wrapping them in results. --- dist/FastMCP.d.ts | 28 +- dist/FastMCP.js | 342 +++----- dist/FastMCP.js.map | 2 +- dist/bin/fastmcp.js | 13 +- dist/bin/fastmcp.js.map | 2 +- src/FastMCP.ts | 1853 ++++++++++++++++++--------------------- 6 files changed, 1000 insertions(+), 1240 deletions(-) diff --git a/dist/FastMCP.d.ts b/dist/FastMCP.d.ts index 0e6931d..e6f0049 100644 --- a/dist/FastMCP.d.ts +++ b/dist/FastMCP.d.ts @@ -2,7 +2,7 @@ import { Server } from '@modelcontextprotocol/sdk/server/index.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 { ResourceLink, RequestMeta, Root, ClientCapabilities, GetPromptResult, CreateMessageRequestSchema } from '@modelcontextprotocol/sdk/types.js'; +import { ResourceLink, RequestMeta, Root, ClientCapabilities, GetPromptResult, CreateMessageRequestSchema, McpError } from '@modelcontextprotocol/sdk/types.js'; export { ErrorCode, McpError, RequestMeta, ResourceLink } from '@modelcontextprotocol/sdk/types.js'; import { StandardSchemaV1 } from '@standard-schema/spec'; import { EventEmitter } from 'events'; @@ -102,6 +102,22 @@ type ToolParameters = StandardSchemaV1; declare abstract class FastMCPError extends Error { constructor(message?: string); } +/** + * Custom MCP error with a marker property to enable robust error detection + * across module boundaries. + * + * This class extends McpError and adds a `__isMcpError` marker property. + * This allows error detection to work even when instanceof fails due to + * module instances being loaded multiple times (e.g., with tsx, different + * bundlers, or ESM/CommonJS mixing). + * + * Use this instead of McpError when you need the error to be re-thrown + * by FastMCP's error handling rather than wrapped in a result. + */ +declare class CustomMcpError extends McpError { + readonly __isMcpError = true; + constructor(code: number, message: string, data?: unknown); +} declare class UnexpectedStateError extends FastMCPError { extras?: Extras; constructor(message: string, extras?: Extras); @@ -111,6 +127,14 @@ declare class UnexpectedStateError extends FastMCPError { */ declare class UserError extends UnexpectedStateError { } +/** + * Type guard to check if an error should be re-thrown as an MCP error. + * Works across module boundaries by checking both instanceof and marker property. + * + * @param error - The error to check + * @returns true if error is an McpError or has the __isMcpError marker + */ +declare function isMcpErrorLike(error: unknown): error is McpError; type ImageContent = { data: string; mimeType: string; @@ -650,4 +674,4 @@ declare class FastMCP extends stop(): Promise; } -export { type AudioContent, type Content, type ContentResult, type Context, FastMCP, type FastMCPEvents, FastMCPSession, type FastMCPSessionEvents, type ImageContent, type InputPrompt, type InputPromptArgument, type Logger, type LoggingLevel, type Progress, type Prompt, type PromptArgument, type Resource, type ResourceContent, type ResourceResult, type ResourceTemplate, type ResourceTemplateArgument, type SSEServer, type SerializableValue, type ServerOptions, type TextContent, type Tool, type ToolParameters, UnexpectedStateError, UserError, audioContent, imageContent }; +export { type AudioContent, type Content, type ContentResult, type Context, CustomMcpError, FastMCP, type FastMCPEvents, FastMCPSession, type FastMCPSessionEvents, type ImageContent, type InputPrompt, type InputPromptArgument, type Logger, type LoggingLevel, type Progress, type Prompt, type PromptArgument, type Resource, type ResourceContent, type ResourceResult, type ResourceTemplate, type ResourceTemplateArgument, type SSEServer, type SerializableValue, type ServerOptions, type TextContent, type Tool, type ToolParameters, UnexpectedStateError, UserError, audioContent, imageContent, isMcpErrorLike }; diff --git a/dist/FastMCP.js b/dist/FastMCP.js index decccca..1378c0b 100644 --- a/dist/FastMCP.js +++ b/dist/FastMCP.js @@ -32,9 +32,7 @@ var imageContent = async (input) => { try { const response = await fetch(input.url); if (!response.ok) { - throw new Error( - `Server responded with status: ${response.status} - ${response.statusText}` - ); + throw new Error(`Server responded with status: ${response.status} - ${response.statusText}`); } rawData = Buffer.from(await response.arrayBuffer()); } catch (error) { @@ -53,16 +51,12 @@ var imageContent = async (input) => { } else if ("buffer" in input) { rawData = input.buffer; } else { - throw new Error( - "Invalid input: Provide a valid 'url', 'path', or 'buffer'" - ); + 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"}` - ); + console.warn(`Warning: Content may not be a valid image. Detected MIME: ${mimeType?.mime || "unknown"}`); } const base64Data = rawData.toString("base64"); return { @@ -85,9 +79,7 @@ var audioContent = async (input) => { try { const response = await fetch(input.url); if (!response.ok) { - throw new Error( - `Server responded with status: ${response.status} - ${response.statusText}` - ); + throw new Error(`Server responded with status: ${response.status} - ${response.statusText}`); } rawData = Buffer.from(await response.arrayBuffer()); } catch (error) { @@ -106,16 +98,12 @@ var audioContent = async (input) => { } else if ("buffer" in input) { rawData = input.buffer; } else { - throw new Error( - "Invalid input: Provide a valid 'url', 'path', or 'buffer'" - ); + 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"}` - ); + console.warn(`Warning: Content may not be a valid audio file. Detected MIME: ${mimeType?.mime || "unknown"}`); } const base64Data = rawData.toString("base64"); return { @@ -137,6 +125,12 @@ var FastMCPError = class extends Error { this.name = new.target.name; } }; +var CustomMcpError = class extends McpError { + __isMcpError = true; + constructor(code, message, data) { + super(code, message, data); + } +}; var UnexpectedStateError = class extends FastMCPError { extras; constructor(message, extras) { @@ -147,6 +141,9 @@ var UnexpectedStateError = class extends FastMCPError { }; var UserError = class extends UnexpectedStateError { }; +function isMcpErrorLike(error) { + return error instanceof McpError || typeof error === "object" && error !== null && "__isMcpError" in error && error.__isMcpError === true; +} var TextContentZodSchema = z.object({ /** * The text content of the message. @@ -372,9 +369,7 @@ var FastMCPSession = class extends FastMCPSessionEventEmitter { 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" - ); + this.#logger.debug("[FastMCP debug] listRoots method not supported by client"); } else { this.#logger.error( `[FastMCP error] received error listing roots. @@ -395,13 +390,9 @@ ${e instanceof Error ? e.stack : JSON.stringify(e)}` 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" - ); + 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" - ); + this.#logger.error("[FastMCP error] server is not responding to ping"); } else { this.#logger.info("[FastMCP info] server ping failed"); } @@ -428,17 +419,11 @@ ${e instanceof Error ? e.stack : JSON.stringify(e)}` return Promise.resolve(); } if (this.#connectionState === "error" || this.#connectionState === "closed") { - return Promise.reject( - new Error(`Connection is in ${this.#connectionState} state`) - ); + 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" - ) - ); + reject(new Error("Connection timeout: Session failed to become ready within 5 seconds")); }, 5e3); this.once("ready", () => { clearTimeout(timeout); @@ -527,9 +512,7 @@ ${e instanceof Error ? e.stack : JSON.stringify(e)}` setupCompleteHandlers() { this.#server.setRequestHandler(CompleteRequestSchema, async (request) => { if (request.params.ref.type === "ref/prompt") { - const prompt = this.#prompts.find( - (prompt2) => prompt2.name === request.params.ref.name - ); + const prompt = this.#prompts.find((prompt2) => prompt2.name === request.params.ref.name); if (!prompt) { throw new UnexpectedStateError("Unknown prompt", { request @@ -541,20 +524,14 @@ ${e instanceof Error ? e.stack : JSON.stringify(e)}` }); } const completion = CompletionZodSchema.parse( - await prompt.complete( - request.params.argument.name, - request.params.argument.value, - this.#auth - ) + await prompt.complete(request.params.argument.name, request.params.argument.value, this.#auth) ); return { completion }; } if (request.params.ref.type === "ref/resource") { - const resource = this.#resourceTemplates.find( - (resource2) => resource2.uriTemplate === request.params.ref.uri - ); + const resource = this.#resourceTemplates.find((resource2) => resource2.uriTemplate === request.params.ref.uri); if (!resource) { throw new UnexpectedStateError("Unknown resource", { request @@ -564,19 +541,12 @@ ${e instanceof Error ? e.stack : JSON.stringify(e)}` throw new UnexpectedStateError("Unexpected resource"); } if (!resource.complete) { - throw new UnexpectedStateError( - "Resource does not support completion", - { - request - } - ); + 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 - ) + await resource.complete(request.params.argument.name, request.params.argument.value, this.#auth) ); return { completion @@ -612,14 +582,9 @@ ${e instanceof Error ? e.stack : JSON.stringify(e)}` }; }); this.#server.setRequestHandler(GetPromptRequestSchema, async (request) => { - const prompt = prompts.find( - (prompt2) => prompt2.name === request.params.name - ); + const prompt = prompts.find((prompt2) => prompt2.name === request.params.name); if (!prompt) { - throw new McpError( - ErrorCode.MethodNotFound, - `Unknown prompt: ${request.params.name}` - ); + throw new McpError(ErrorCode.MethodNotFound, `Unknown prompt: ${request.params.name}`); } const args = request.params.arguments; for (const arg of prompt.arguments ?? []) { @@ -632,16 +597,10 @@ ${e instanceof Error ? e.stack : JSON.stringify(e)}` } let result; try { - result = await prompt.load( - args, - this.#auth - ); + result = await prompt.load(args, 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}` - ); + throw new McpError(ErrorCode.InternalError, `Failed to load prompt '${request.params.name}': ${errorMessage}`); } if (typeof result === "string") { return { @@ -672,122 +631,103 @@ ${e instanceof Error ? e.stack : JSON.stringify(e)}` })) }; }); - this.#server.setRequestHandler( - ReadResourceRequestSchema, - async (request) => { - if ("uri" in request.params) { - const resource = resources.find( - (resource2) => "uri" in resource2 && resource2.uri === request.params.uri - ); - if (!resource) { - for (const resourceTemplate of this.#resourceTemplates) { - const uriTemplate = parseURITemplate( - resourceTemplate.uriTemplate - ); - const match = uriTemplate.fromUri(request.params.uri); - if (!match) { - continue; - } - const uri = uriTemplate.fill(match); - const result = await resourceTemplate.load(match, this.#auth); - const resources2 = Array.isArray(result) ? result : [result]; - return { - contents: resources2.map((resource2) => ({ - ...resource2, - description: resourceTemplate.description, - mimeType: resource2.mimeType ?? resourceTemplate.mimeType, - name: resourceTemplate.name, - uri: resource2.uri ?? uri - })) - }; + this.#server.setRequestHandler(ReadResourceRequestSchema, async (request) => { + if ("uri" in request.params) { + const resource = resources.find((resource2) => "uri" in resource2 && resource2.uri === request.params.uri); + if (!resource) { + for (const resourceTemplate of this.#resourceTemplates) { + const uriTemplate = parseURITemplate(resourceTemplate.uriTemplate); + const match = uriTemplate.fromUri(request.params.uri); + if (!match) { + continue; } - throw new McpError( - 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 McpError( - ErrorCode.InternalError, - `Failed to load resource '${resource.name}' (${resource.uri}): ${errorMessage}`, - { - uri: resource.uri - } - ); + 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: resource2.mimeType ?? resourceTemplate.mimeType, + name: resourceTemplate.name, + uri: resource2.uri ?? 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 McpError( + ErrorCode.MethodNotFound, + `Resource not found: '${request.params.uri}'. Available resources: ${resources.map((r) => r.uri).join(", ") || "none"}` + ); } - throw new UnexpectedStateError("Unknown resource request", { - request - }); - } - ); - } - setupResourceTemplateHandlers(resourceTemplates) { - this.#server.setRequestHandler( - ListResourceTemplatesRequestSchema, - async () => { + 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 McpError( + ErrorCode.InternalError, + `Failed to load resource '${resource.name}' (${resource.uri}): ${errorMessage}`, + { + uri: resource.uri + } + ); + } + const resourceResults = Array.isArray(maybeArrayResult) ? maybeArrayResult : [maybeArrayResult]; return { - resourceTemplates: resourceTemplates.map((resourceTemplate) => ({ - description: resourceTemplate.description, - mimeType: resourceTemplate.mimeType, - name: resourceTemplate.name, - uriTemplate: resourceTemplate.uriTemplate + 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 + }); + }); + } + setupResourceTemplateHandlers(resourceTemplates) { + this.#server.setRequestHandler(ListResourceTemplatesRequestSchema, async () => { + return { + resourceTemplates: resourceTemplates.map((resourceTemplate) => ({ + description: resourceTemplate.description, + mimeType: resourceTemplate.mimeType, + name: resourceTemplate.name, + uriTemplate: resourceTemplate.uriTemplate + })) + }; + }); } setupRootsHandlers() { if (this.#rootsConfig?.enabled === false) { - this.#logger.debug( - "[FastMCP debug] roots capability explicitly disabled via config" - ); + this.#logger.debug("[FastMCP debug] roots capability explicitly disabled via config"); return; } if (typeof this.#server.listRoots === "function") { - this.#server.setNotificationHandler( - RootsListChangedNotificationSchema, - () => { - this.#server.listRoots().then((roots) => { - this.#roots = roots.roots; - this.emit("rootsChanged", { - roots: roots.roots - }); - }).catch((error) => { - if (error instanceof McpError && error.code === ErrorCode.MethodNotFound) { - this.#logger.debug( - "[FastMCP debug] listRoots method not supported by client" - ); - } else { - this.#logger.error( - `[FastMCP error] received error listing roots. + 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. ${error instanceof Error ? error.stack : JSON.stringify(error)}` - ); - } - }); - } - ); + ); + } + }); + }); } else { - this.#logger.debug( - "[FastMCP debug] roots capability not available, not setting up notification handler" - ); + this.#logger.debug("[FastMCP debug] roots capability not available, not setting up notification handler"); } } setupToolHandlers(tools) { @@ -813,16 +753,11 @@ ${error instanceof Error ? error.stack : JSON.stringify(error)}` this.#server.setRequestHandler(CallToolRequestSchema, async (request) => { const tool = tools.find((tool2) => tool2.name === request.params.name); if (!tool) { - throw new McpError( - ErrorCode.MethodNotFound, - `Unknown tool: ${request.params.name}` - ); + throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${request.params.name}`); } let args = void 0; if (tool.parameters) { - const parsed = await tool.parameters["~standard"].validate( - request.params.arguments - ); + 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"; @@ -958,7 +893,7 @@ ${error instanceof Error ? error.stack : JSON.stringify(error)}` result = ContentResultZodSchema.parse(maybeStringResult); } } catch (error) { - if (error instanceof McpError) { + if (isMcpErrorLike(error)) { throw error; } if (error instanceof UserError) { @@ -1048,9 +983,7 @@ var FastMCP = class extends FastMCPEventEmitter { * @returns Promise - The embedded resource content */ async embedded(uri) { - const directResource = this.#resources.find( - (resource) => resource.uri === uri - ); + const directResource = this.#resources.find((resource) => resource.uri === uri); if (directResource) { const result = await directResource.load(); const results = Array.isArray(result) ? result : [result]; @@ -1073,9 +1006,7 @@ var FastMCP = class extends FastMCPEventEmitter { if (!params) { continue; } - const result = await template.load( - params - ); + const result = await template.load(params); const resourceData = { mimeType: template.mimeType, uri @@ -1100,9 +1031,7 @@ var FastMCP = class extends FastMCPEventEmitter { let auth; if (this.#authenticate) { try { - auth = await this.#authenticate( - void 0 - ); + auth = await this.#authenticate(void 0); } catch (error) { this.#logger.error( "[FastMCP error] Authentication failed for stdio transport:", @@ -1172,9 +1101,7 @@ var FastMCP = class extends FastMCPEventEmitter { onClose: async () => { }, onConnect: async () => { - this.#logger.debug( - `[FastMCP debug] Stateless HTTP Stream request handled` - ); + this.#logger.debug(`[FastMCP debug] Stateless HTTP Stream request handled`); }, onUnhandledRequest: async (req, res) => { await this.#handleUnhandledRequest(req, res, true, httpConfig.host); @@ -1212,12 +1139,7 @@ var FastMCP = class extends FastMCPEventEmitter { }); }, onUnhandledRequest: async (req, res) => { - await this.#handleUnhandledRequest( - req, - res, - false, - httpConfig.host - ); + await this.#handleUnhandledRequest(req, res, false, httpConfig.host); }, port: httpConfig.port, stateless: httpConfig.stateless, @@ -1248,9 +1170,7 @@ var FastMCP = class extends FastMCPEventEmitter { const errorMessage = "error" in auth && typeof auth.error === "string" ? auth.error : "Authentication failed"; throw new Error(errorMessage); } - const allowedTools = auth ? this.#tools.filter( - (tool) => tool.canAccess ? tool.canAccess(auth) : true - ) : this.#tools; + const allowedTools = auth ? this.#tools.filter((tool) => tool.canAccess ? tool.canAccess(auth) : true) : this.#tools; return new FastMCPSession({ auth, instructions: this.#options.instructions, @@ -1296,9 +1216,7 @@ var FastMCP = class extends FastMCPEventEmitter { "Content-Type": "application/json" }).end(JSON.stringify(response)); } else { - const readySessions = this.#sessions.filter( - (s) => s.isReady - ).length; + const readySessions = this.#sessions.filter((s) => s.isReady).length; const totalSessions = this.#sessions.length; const allReady = readySessions === totalSessions && totalSessions > 0; const response = { @@ -1320,18 +1238,14 @@ var FastMCP = class extends FastMCPEventEmitter { 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 - ); + const metadata = convertObjectToSnakeCase(oauthConfig.authorizationServer); res.writeHead(200, { "Content-Type": "application/json" }).end(JSON.stringify(metadata)); return; } if (url.pathname === "/.well-known/oauth-protected-resource" && oauthConfig.protectedResource) { - const metadata = convertObjectToSnakeCase( - oauthConfig.protectedResource - ); + const metadata = convertObjectToSnakeCase(oauthConfig.protectedResource); res.writeHead(200, { "Content-Type": "application/json" }).end(JSON.stringify(metadata)); @@ -1358,9 +1272,7 @@ var FastMCP = class extends FastMCPEventEmitter { const envHost = process.env.FASTMCP_HOST; 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 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; @@ -1389,6 +1301,7 @@ var FastMCP = class extends FastMCPEventEmitter { } }; export { + CustomMcpError, ErrorCode2 as ErrorCode, FastMCP, FastMCPSession, @@ -1396,6 +1309,7 @@ export { UnexpectedStateError, UserError, audioContent, - imageContent + imageContent, + isMcpErrorLike }; //# sourceMappingURL=FastMCP.js.map \ No newline at end of file diff --git a/dist/FastMCP.js.map b/dist/FastMCP.js.map index 73ba8fa..645d742 100644 --- a/dist/FastMCP.js.map +++ b/dist/FastMCP.js.map @@ -1 +1 @@ -{"version":3,"sources":["../src/FastMCP.ts"],"sourcesContent":["import { Server } from \"@modelcontextprotocol/sdk/server/index.js\";\nimport { StdioServerTransport } from \"@modelcontextprotocol/sdk/server/stdio.js\";\nimport { EventStore } from \"@modelcontextprotocol/sdk/server/streamableHttp.js\";\nimport { RequestOptions } from \"@modelcontextprotocol/sdk/shared/protocol.js\";\nimport { Transport } from \"@modelcontextprotocol/sdk/shared/transport.js\";\nimport {\n CallToolRequestSchema,\n ClientCapabilities,\n CompleteRequestSchema,\n CreateMessageRequestSchema,\n ErrorCode,\n GetPromptRequestSchema,\n GetPromptResult,\n ListPromptsRequestSchema,\n ListResourcesRequestSchema,\n ListResourcesResult,\n ListResourceTemplatesRequestSchema,\n ListResourceTemplatesResult,\n ListToolsRequestSchema,\n McpError,\n ReadResourceRequestSchema,\n RequestMeta,\n ResourceLink,\n Root,\n RootsListChangedNotificationSchema,\n ServerCapabilities,\n SetLevelRequestSchema,\n} from \"@modelcontextprotocol/sdk/types.js\";\nimport { StandardSchemaV1 } from \"@standard-schema/spec\";\nimport { EventEmitter } from \"events\";\nimport { readFile } from \"fs/promises\";\nimport Fuse from \"fuse.js\";\nimport http from \"http\";\nimport { startHTTPServer } from \"mcp-proxy\";\nimport { StrictEventEmitter } from \"strict-event-emitter-types\";\nimport { setTimeout as delay } from \"timers/promises\";\nimport { fetch } from \"undici\";\nimport parseURITemplate from \"uri-templates\";\nimport { toJsonSchema } from \"xsschema\";\nimport { z } from \"zod\";\n\nexport interface Logger {\n debug(...args: unknown[]): void;\n error(...args: unknown[]): void;\n info(...args: unknown[]): void;\n log(...args: unknown[]): void;\n warn(...args: unknown[]): void;\n}\n\nexport type SSEServer = {\n close: () => Promise;\n};\n\ntype FastMCPEvents = {\n connect: (event: { session: FastMCPSession }) => void;\n disconnect: (event: { session: FastMCPSession }) => void;\n};\n\ntype FastMCPSessionEvents = {\n error: (event: { error: Error }) => void;\n ready: () => void;\n rootsChanged: (event: { roots: Root[] }) => void;\n};\n\nexport const imageContent = async (\n input: { buffer: Buffer } | { path: string } | { url: string },\n): Promise => {\n let rawData: Buffer;\n\n try {\n if (\"url\" in input) {\n try {\n const response = await fetch(input.url);\n\n if (!response.ok) {\n throw new Error(\n `Server responded with status: ${response.status} - ${response.statusText}`,\n );\n }\n\n rawData = Buffer.from(await response.arrayBuffer());\n } catch (error) {\n throw new Error(\n `Failed to fetch image from URL (${input.url}): ${\n error instanceof Error ? error.message : String(error)\n }`,\n );\n }\n } else if (\"path\" in input) {\n try {\n rawData = await readFile(input.path);\n } catch (error) {\n throw new Error(\n `Failed to read image from path (${input.path}): ${\n error instanceof Error ? error.message : String(error)\n }`,\n );\n }\n } else if (\"buffer\" in input) {\n rawData = input.buffer;\n } else {\n throw new Error(\n \"Invalid input: Provide a valid 'url', 'path', or 'buffer'\",\n );\n }\n\n const { fileTypeFromBuffer } = await import(\"file-type\");\n const mimeType = await fileTypeFromBuffer(rawData);\n\n if (!mimeType || !mimeType.mime.startsWith(\"image/\")) {\n console.warn(\n `Warning: Content may not be a valid image. Detected MIME: ${\n mimeType?.mime || \"unknown\"\n }`,\n );\n }\n\n const base64Data = rawData.toString(\"base64\");\n\n return {\n data: base64Data,\n mimeType: mimeType?.mime ?? \"image/png\",\n type: \"image\",\n } as const;\n } catch (error) {\n if (error instanceof Error) {\n throw error;\n } else {\n throw new Error(`Unexpected error processing image: ${String(error)}`);\n }\n }\n};\n\nexport const audioContent = async (\n input: { buffer: Buffer } | { path: string } | { url: string },\n): Promise => {\n let rawData: Buffer;\n\n try {\n if (\"url\" in input) {\n try {\n const response = await fetch(input.url);\n\n if (!response.ok) {\n throw new Error(\n `Server responded with status: ${response.status} - ${response.statusText}`,\n );\n }\n\n rawData = Buffer.from(await response.arrayBuffer());\n } catch (error) {\n throw new Error(\n `Failed to fetch audio from URL (${input.url}): ${\n error instanceof Error ? error.message : String(error)\n }`,\n );\n }\n } else if (\"path\" in input) {\n try {\n rawData = await readFile(input.path);\n } catch (error) {\n throw new Error(\n `Failed to read audio from path (${input.path}): ${\n error instanceof Error ? error.message : String(error)\n }`,\n );\n }\n } else if (\"buffer\" in input) {\n rawData = input.buffer;\n } else {\n throw new Error(\n \"Invalid input: Provide a valid 'url', 'path', or 'buffer'\",\n );\n }\n\n const { fileTypeFromBuffer } = await import(\"file-type\");\n const mimeType = await fileTypeFromBuffer(rawData);\n\n if (!mimeType || !mimeType.mime.startsWith(\"audio/\")) {\n console.warn(\n `Warning: Content may not be a valid audio file. Detected MIME: ${\n mimeType?.mime || \"unknown\"\n }`,\n );\n }\n\n const base64Data = rawData.toString(\"base64\");\n\n return {\n data: base64Data,\n mimeType: mimeType?.mime ?? \"audio/mpeg\",\n type: \"audio\",\n } as const;\n } catch (error) {\n if (error instanceof Error) {\n throw error;\n } else {\n throw new Error(`Unexpected error processing audio: ${String(error)}`);\n }\n }\n};\n\ntype Context = {\n client: {\n version: ReturnType;\n };\n log: {\n debug: (message: string, data?: SerializableValue) => void;\n error: (message: string, data?: SerializableValue) => void;\n info: (message: string, data?: SerializableValue) => void;\n warn: (message: string, data?: SerializableValue) => void;\n };\n reportProgress: (progress: Progress) => Promise;\n /**\n * Request ID from the current MCP request.\n * Available for all transports when the client provides it.\n */\n requestId?: string;\n requestMetadata?: RequestMeta;\n session: T | undefined;\n /**\n * Session ID from the Mcp-Session-Id header.\n * Only available for HTTP-based transports (SSE, HTTP Stream).\n * Can be used to track per-session state, implement session-specific\n * counters, or maintain user-specific data across multiple requests.\n */\n sessionId?: string;\n streamContent: (content: Content | Content[]) => Promise;\n};\n\ntype Extra = unknown;\n\ntype Extras = Record;\n\ntype Literal = boolean | null | number | string | undefined;\n\ntype Progress = {\n /**\n * The progress thus far. This should increase every time progress is made, even if the total is unknown.\n */\n progress: number;\n /**\n * Total number of items to process (or total progress required), if known.\n */\n total?: number;\n};\n\ntype SerializableValue =\n | { [key: string]: SerializableValue }\n | Literal\n | SerializableValue[];\n\ntype TextContent = {\n text: string;\n type: \"text\";\n};\n\ntype ToolParameters = StandardSchemaV1;\n\nabstract class FastMCPError extends Error {\n public constructor(message?: string) {\n super(message);\n this.name = new.target.name;\n }\n}\n\nexport class UnexpectedStateError extends FastMCPError {\n public extras?: Extras;\n\n public constructor(message: string, extras?: Extras) {\n super(message);\n this.name = new.target.name;\n this.extras = extras;\n }\n}\n\n/**\n * An error that is meant to be surfaced to the user.\n */\nexport class UserError extends UnexpectedStateError {}\n\nconst TextContentZodSchema = z\n .object({\n /**\n * The text content of the message.\n */\n text: z.string(),\n type: z.literal(\"text\"),\n })\n .strict() satisfies z.ZodType;\n\ntype ImageContent = {\n data: string;\n mimeType: string;\n type: \"image\";\n};\n\nconst ImageContentZodSchema = z\n .object({\n /**\n * The base64-encoded image data.\n */\n data: z.string().base64(),\n /**\n * The MIME type of the image. Different providers may support different image types.\n */\n mimeType: z.string(),\n type: z.literal(\"image\"),\n })\n .strict() satisfies z.ZodType;\n\ntype AudioContent = {\n data: string;\n mimeType: string;\n type: \"audio\";\n};\n\nconst AudioContentZodSchema = z\n .object({\n /**\n * The base64-encoded audio data.\n */\n data: z.string().base64(),\n mimeType: z.string(),\n type: z.literal(\"audio\"),\n })\n .strict() satisfies z.ZodType;\n\ntype ResourceContent = {\n resource: {\n blob?: string;\n mimeType?: string;\n text?: string;\n uri: string;\n };\n type: \"resource\";\n};\n\nconst ResourceContentZodSchema = z\n .object({\n resource: z.object({\n blob: z.string().optional(),\n mimeType: z.string().optional(),\n text: z.string().optional(),\n uri: z.string(),\n }),\n type: z.literal(\"resource\"),\n })\n .strict() satisfies z.ZodType;\n\nconst ResourceLinkZodSchema = z.object({\n description: z.string().optional(),\n mimeType: z.string().optional(),\n name: z.string(),\n title: z.string().optional(),\n type: z.literal(\"resource_link\"),\n uri: z.string(),\n}) satisfies z.ZodType;\n\ntype Content =\n | AudioContent\n | ImageContent\n | ResourceContent\n | ResourceLink\n | TextContent;\n\nconst ContentZodSchema = z.discriminatedUnion(\"type\", [\n TextContentZodSchema,\n ImageContentZodSchema,\n AudioContentZodSchema,\n ResourceContentZodSchema,\n ResourceLinkZodSchema,\n]) satisfies z.ZodType;\n\ntype ContentResult = {\n _meta?: Record;\n content: Content[];\n isError?: boolean;\n};\n\nconst ContentResultZodSchema = z\n .object({\n _meta: z.record(z.unknown()).optional(),\n content: ContentZodSchema.array(),\n isError: z.boolean().optional(),\n })\n .strict() satisfies z.ZodType;\n\ntype Completion = {\n hasMore?: boolean;\n total?: number;\n values: string[];\n};\n\n/**\n * https://github.com/modelcontextprotocol/typescript-sdk/blob/3164da64d085ec4e022ae881329eee7b72f208d4/src/types.ts#L983-L1003\n */\nconst CompletionZodSchema = z.object({\n /**\n * Indicates whether there are additional completion options beyond those provided in the current response, even if the exact total is unknown.\n */\n hasMore: z.optional(z.boolean()),\n /**\n * The total number of completion options available. This can exceed the number of values actually sent in the response.\n */\n total: z.optional(z.number().int()),\n /**\n * An array of completion values. Must not exceed 100 items.\n */\n values: z.array(z.string()).max(100),\n}) satisfies z.ZodType;\n\ntype ArgumentValueCompleter =\n (value: string, auth?: T) => Promise;\n\ntype InputPrompt<\n T extends FastMCPSessionAuth = FastMCPSessionAuth,\n Arguments extends InputPromptArgument[] = InputPromptArgument[],\n Args = PromptArgumentsToObject,\n> = {\n arguments?: InputPromptArgument[];\n description?: string;\n load: (args: Args, auth?: T) => Promise;\n name: string;\n};\n\ntype InputPromptArgument =\n Readonly<{\n complete?: ArgumentValueCompleter;\n description?: string;\n enum?: string[];\n name: string;\n required?: boolean;\n }>;\n\ntype InputResourceTemplate<\n T extends FastMCPSessionAuth,\n Arguments extends\n InputResourceTemplateArgument[] = InputResourceTemplateArgument[],\n> = {\n arguments: Arguments;\n description?: string;\n load: (\n args: ResourceTemplateArgumentsToObject,\n auth?: T,\n ) => Promise;\n mimeType?: string;\n name: string;\n uriTemplate: string;\n};\n\ntype InputResourceTemplateArgument<\n T extends FastMCPSessionAuth = FastMCPSessionAuth,\n> = Readonly<{\n complete?: ArgumentValueCompleter;\n description?: string;\n name: string;\n required?: boolean;\n}>;\n\ntype LoggingLevel =\n | \"alert\"\n | \"critical\"\n | \"debug\"\n | \"emergency\"\n | \"error\"\n | \"info\"\n | \"notice\"\n | \"warning\";\n\ntype Prompt<\n T extends FastMCPSessionAuth = FastMCPSessionAuth,\n Arguments extends PromptArgument[] = PromptArgument[],\n Args = PromptArgumentsToObject,\n> = {\n arguments?: PromptArgument[];\n complete?: (name: string, value: string, auth?: T) => Promise;\n description?: string;\n load: (args: Args, auth?: T) => Promise;\n name: string;\n};\n\ntype PromptArgument =\n Readonly<{\n complete?: ArgumentValueCompleter;\n description?: string;\n enum?: string[];\n name: string;\n required?: boolean;\n }>;\n\ntype PromptArgumentsToObject =\n {\n [K in T[number][\"name\"]]: Extract<\n T[number],\n { name: K }\n >[\"required\"] extends true\n ? string\n : string | undefined;\n };\n\ntype PromptResult = Pick | string;\n\ntype Resource = {\n complete?: (name: string, value: string, auth?: T) => Promise;\n description?: string;\n load: (auth?: T) => Promise;\n mimeType?: string;\n name: string;\n uri: string;\n};\n\ntype ResourceResult =\n | {\n blob: string;\n mimeType?: string;\n uri?: string;\n }\n | {\n mimeType?: string;\n text: string;\n uri?: string;\n };\n\ntype ResourceTemplate<\n T extends FastMCPSessionAuth,\n Arguments extends\n ResourceTemplateArgument[] = ResourceTemplateArgument[],\n> = {\n arguments: Arguments;\n complete?: (name: string, value: string, auth?: T) => Promise;\n description?: string;\n load: (\n args: ResourceTemplateArgumentsToObject,\n auth?: T,\n ) => Promise;\n mimeType?: string;\n name: string;\n uriTemplate: string;\n};\n\ntype ResourceTemplateArgument<\n T extends FastMCPSessionAuth = FastMCPSessionAuth,\n> = Readonly<{\n complete?: ArgumentValueCompleter;\n description?: string;\n name: string;\n required?: boolean;\n}>;\n\ntype ResourceTemplateArgumentsToObject = {\n [K in T[number][\"name\"]]: string;\n};\n\ntype SamplingResponse = {\n content: AudioContent | ImageContent | TextContent;\n model: string;\n role: \"assistant\" | \"user\";\n stopReason?: \"endTurn\" | \"maxTokens\" | \"stopSequence\" | string;\n};\n\ntype ServerOptions = {\n authenticate?: Authenticate;\n /**\n * Configuration for the health-check endpoint that can be exposed when the\n * server is running using the HTTP Stream transport. When enabled, the\n * server will respond to an HTTP GET request with the configured path (by\n * default \"/health\") rendering a plain-text response (by default \"ok\") and\n * the configured status code (by default 200).\n *\n * The endpoint is only added when the server is started with\n * `transportType: \"httpStream\"` – it is ignored for the stdio transport.\n */\n health?: {\n /**\n * When set to `false` the health-check endpoint is disabled.\n * @default true\n */\n enabled?: boolean;\n\n /**\n * Plain-text body returned by the endpoint.\n * @default \"ok\"\n */\n message?: string;\n\n /**\n * HTTP path that should be handled.\n * @default \"/health\"\n */\n path?: string;\n\n /**\n * HTTP response status that will be returned.\n * @default 200\n */\n status?: number;\n };\n instructions?: string;\n /**\n * Custom logger instance. If not provided, defaults to console.\n * Use this to integrate with your own logging system.\n */\n logger?: Logger;\n name: string;\n\n /**\n * Configuration for OAuth well-known discovery endpoints that can be exposed\n * when the server is running using HTTP-based transports (SSE or HTTP Stream).\n * When enabled, the server will respond to requests for OAuth discovery endpoints\n * with the configured metadata.\n *\n * The endpoints are only added when the server is started with\n * `transportType: \"httpStream\"` – they are ignored for the stdio transport.\n * Both SSE and HTTP Stream transports support OAuth endpoints.\n */\n oauth?: {\n /**\n * OAuth Authorization Server metadata for /.well-known/oauth-authorization-server\n *\n * This endpoint follows RFC 8414 (OAuth 2.0 Authorization Server Metadata)\n * and provides metadata about the OAuth 2.0 authorization server.\n *\n * Required by MCP Specification 2025-03-26\n */\n authorizationServer?: {\n authorizationEndpoint: string;\n codeChallengeMethodsSupported?: string[];\n // DPoP support\n dpopSigningAlgValuesSupported?: string[];\n grantTypesSupported?: string[];\n\n introspectionEndpoint?: string;\n // Required\n issuer: string;\n // Common optional\n jwksUri?: string;\n opPolicyUri?: string;\n opTosUri?: string;\n registrationEndpoint?: string;\n responseModesSupported?: string[];\n responseTypesSupported: string[];\n revocationEndpoint?: string;\n scopesSupported?: string[];\n serviceDocumentation?: string;\n tokenEndpoint: string;\n tokenEndpointAuthMethodsSupported?: string[];\n tokenEndpointAuthSigningAlgValuesSupported?: string[];\n\n uiLocalesSupported?: string[];\n };\n\n /**\n * Whether OAuth discovery endpoints should be enabled.\n */\n enabled: boolean;\n\n /**\n * OAuth Protected Resource metadata for `/.well-known/oauth-protected-resource`\n *\n * This endpoint follows {@link https://www.rfc-editor.org/rfc/rfc9728.html | RFC 9728}\n * and provides metadata describing how an OAuth 2.0 protected resource (in this case,\n * an MCP server) expects to be accessed.\n *\n * When configured, FastMCP will automatically serve this metadata at the\n * `/.well-known/oauth-protected-resource` endpoint. The `authorizationServers` and `resource`\n * fields are required. All others are optional and will be omitted from the published\n * metadata if not specified.\n *\n * This satisfies the requirements of the MCP Authorization specification's\n * {@link https://modelcontextprotocol.io/specification/2025-06-18/basic/authorization#authorization-server-location | Authorization Server Location section}.\n *\n * Clients consuming this metadata MUST validate that any presented values comply with\n * RFC 9728, including strict validation of the `resource` identifier and intended audience\n * when access tokens are issued and presented (per RFC 8707 §2).\n *\n * @remarks Required by MCP Specification version 2025-06-18\n */\n protectedResource?: {\n /**\n * Allows for additional metadata fields beyond those defined in RFC 9728.\n *\n * @remarks This supports vendor-specific or experimental extensions.\n * @see {@link https://www.rfc-editor.org/rfc/rfc9728.html#section-2.3 | RFC 9728 §2.3}\n */\n [key: string]: unknown;\n\n /**\n * Supported values for the `authorization_details` parameter (RFC 9396).\n *\n * @remarks Used when fine-grained access control is in play.\n * @see {@link https://www.rfc-editor.org/rfc/rfc9728.html#section-2-2.23 | RFC 9728 §2.2.23}\n */\n authorizationDetailsTypesSupported?: string[];\n\n /**\n * List of OAuth 2.0 authorization server issuer identifiers.\n *\n * These correspond to ASes that can issue access tokens for this protected resource.\n * MCP clients use these values to locate the relevant `/.well-known/oauth-authorization-server`\n * metadata for initiating the OAuth flow.\n *\n * @remarks Required by the MCP spec. MCP servers MUST provide at least one issuer.\n * Clients are responsible for choosing among them (see RFC 9728 §7.6).\n * @see {@link https://www.rfc-editor.org/rfc/rfc9728.html#section-2-2.3 | RFC 9728 §2.2.3}\n */\n authorizationServers: string[];\n\n /**\n * List of supported methods for presenting OAuth 2.0 bearer tokens.\n *\n * @remarks Valid values are `header`, `body`, and `query`.\n * If omitted, clients MAY assume only `header` is supported, per RFC 6750.\n * This is a client-side interpretation and not a serialization default.\n * @see {@link https://www.rfc-editor.org/rfc/rfc9728.html#section-2-2.9 | RFC 9728 §2.2.9}\n */\n bearerMethodsSupported?: string[];\n\n /**\n * Whether this resource requires all access tokens to be DPoP-bound.\n *\n * @remarks If omitted, clients SHOULD assume this is `false`.\n * @see {@link https://www.rfc-editor.org/rfc/rfc9728.html#section-2-2.27 | RFC 9728 §2.2.27}\n */\n dpopBoundAccessTokensRequired?: boolean;\n\n /**\n * Supported algorithms for verifying DPoP proofs (RFC 9449).\n *\n * @see {@link https://www.rfc-editor.org/rfc/rfc9728.html#section-2-2.25 | RFC 9728 §2.2.25}\n */\n dpopSigningAlgValuesSupported?: string[];\n\n /**\n * JWKS URI of this resource. Used to validate access tokens or sign responses.\n *\n * @remarks When present, this MUST be an `https:` URI pointing to a valid JWK Set (RFC 7517).\n * @see {@link https://www.rfc-editor.org/rfc/rfc9728.html#section-2-2.5 | RFC 9728 §2.2.5}\n */\n jwksUri?: string;\n\n /**\n * Canonical OAuth resource identifier for this protected resource (the MCP server).\n *\n * @remarks Typically the base URL of the MCP server. Clients MUST use this as the\n * `resource` parameter in authorization and token requests (per RFC 8707).\n * @see {@link https://www.rfc-editor.org/rfc/rfc9728.html#section-2-2.1 | RFC 9728 §2.2.1}\n */\n resource: string;\n\n /**\n * URL to developer-accessible documentation for this resource.\n *\n * @remarks This field MAY be localized.\n * @see {@link https://www.rfc-editor.org/rfc/rfc9728.html#section-2-2.15 | RFC 9728 §2.2.15}\n */\n resourceDocumentation?: string;\n\n /**\n * Human-readable name for display purposes (e.g., in UIs).\n *\n * @remarks This field MAY be localized using language tags (`resource_name#en`, etc.).\n * @see {@link https://www.rfc-editor.org/rfc/rfc9728.html#section-2-2.13 | RFC 9728 §2.2.13}\n */\n resourceName?: string;\n\n /**\n * URL to a human-readable policy page describing acceptable use.\n *\n * @remarks This field MAY be localized.\n * @see {@link https://www.rfc-editor.org/rfc/rfc9728.html#section-2-2.17 | RFC 9728 §2.2.17}\n */\n resourcePolicyUri?: string;\n\n /**\n * Supported JWS algorithms for signed responses from this resource (e.g., response signing).\n *\n * @remarks MUST NOT include `none`.\n * @see {@link https://www.rfc-editor.org/rfc/rfc9728.html#section-2-2.11 | RFC 9728 §2.2.11}\n */\n resourceSigningAlgValuesSupported?: string[];\n\n /**\n * URL to the protected resource’s Terms of Service.\n *\n * @remarks This field MAY be localized.\n * @see {@link https://www.rfc-editor.org/rfc/rfc9728.html#section-2-2.19 | RFC 9728 §2.2.19}\n */\n resourceTosUri?: string;\n\n /**\n * Supported OAuth scopes for requesting access to this resource.\n *\n * @remarks Useful for discovery, but clients SHOULD still request the minimal scope required.\n * @see {@link https://www.rfc-editor.org/rfc/rfc9728.html#section-2-2.7 | RFC 9728 §2.2.7}\n */\n scopesSupported?: string[];\n\n /**\n * Developer-accessible documentation for how to use the service (not end-user docs).\n *\n * @remarks Semantically equivalent to `resourceDocumentation`, but included under its\n * alternate name for compatibility with tools or schemas expecting either.\n * @see {@link https://www.rfc-editor.org/rfc/rfc9728.html#section-2-2.15 | RFC 9728 §2.2.15}\n */\n serviceDocumentation?: string;\n\n /**\n * Whether mutual-TLS-bound access tokens are required.\n *\n * @remarks If omitted, clients SHOULD assume this is `false` (client-side behavior).\n * @see {@link https://www.rfc-editor.org/rfc/rfc9728.html#section-2-2.21 | RFC 9728 §2.2.21}\n */\n tlsClientCertificateBoundAccessTokens?: boolean;\n };\n };\n\n ping?: {\n /**\n * Whether ping should be enabled by default.\n * - true for SSE or HTTP Stream\n * - false for stdio\n */\n enabled?: boolean;\n /**\n * Interval\n * @default 5000 (5s)\n */\n intervalMs?: number;\n /**\n * Logging level for ping-related messages.\n * @default 'debug'\n */\n logLevel?: LoggingLevel;\n };\n /**\n * Configuration for roots capability\n */\n roots?: {\n /**\n * Whether roots capability should be enabled\n * Set to false to completely disable roots support\n * @default true\n */\n enabled?: boolean;\n };\n /**\n * General utilities\n */\n utils?: {\n formatInvalidParamsErrorMessage?: (\n issues: readonly StandardSchemaV1.Issue[],\n ) => string;\n };\n version: `${number}.${number}.${number}`;\n};\n\ntype Tool<\n T extends FastMCPSessionAuth,\n Params extends ToolParameters = ToolParameters,\n> = {\n annotations?: {\n /**\n * When true, the tool leverages incremental content streaming\n * Return void for tools that handle all their output via streaming\n */\n streamingHint?: boolean;\n } & ToolAnnotations;\n canAccess?: (auth: T) => boolean;\n description?: string;\n\n execute: (\n args: StandardSchemaV1.InferOutput,\n context: Context,\n ) => Promise<\n | AudioContent\n | ContentResult\n | ImageContent\n | ResourceContent\n | ResourceLink\n | string\n | TextContent\n | void\n >;\n name: string;\n parameters?: Params;\n timeoutMs?: number;\n};\n\n/**\n * Tool annotations as defined in MCP Specification (2025-03-26)\n * These provide hints about a tool's behavior.\n */\ntype ToolAnnotations = {\n /**\n * If true, the tool may perform destructive updates\n * Only meaningful when readOnlyHint is false\n * @default true\n */\n destructiveHint?: boolean;\n\n /**\n * If true, calling the tool repeatedly with the same arguments has no additional effect\n * Only meaningful when readOnlyHint is false\n * @default false\n */\n idempotentHint?: boolean;\n\n /**\n * If true, the tool may interact with an \"open world\" of external entities\n * @default true\n */\n openWorldHint?: boolean;\n\n /**\n * If true, indicates the tool does not modify its environment\n * @default false\n */\n readOnlyHint?: boolean;\n\n /**\n * A human-readable title for the tool, useful for UI display\n */\n title?: string;\n};\n\nconst FastMCPSessionEventEmitterBase: {\n new (): StrictEventEmitter;\n} = EventEmitter;\n\ntype Authenticate = (request: http.IncomingMessage) => Promise;\n\ntype FastMCPSessionAuth = Record | undefined;\n\nclass FastMCPSessionEventEmitter extends FastMCPSessionEventEmitterBase {}\n\nexport class FastMCPSession<\n T extends FastMCPSessionAuth = FastMCPSessionAuth,\n> extends FastMCPSessionEventEmitter {\n public get clientCapabilities(): ClientCapabilities | null {\n return this.#clientCapabilities ?? null;\n }\n public get isReady(): boolean {\n return this.#connectionState === \"ready\";\n }\n public get loggingLevel(): LoggingLevel {\n return this.#loggingLevel;\n }\n public get roots(): Root[] {\n return this.#roots;\n }\n public get server(): Server {\n return this.#server;\n }\n public get sessionId(): string | undefined {\n return this.#sessionId;\n }\n public set sessionId(value: string | undefined) {\n this.#sessionId = value;\n }\n #auth: T | undefined;\n #capabilities: ServerCapabilities = {};\n #clientCapabilities?: ClientCapabilities;\n #connectionState: \"closed\" | \"connecting\" | \"error\" | \"ready\" = \"connecting\";\n #logger: Logger;\n #loggingLevel: LoggingLevel = \"info\";\n #needsEventLoopFlush: boolean = false;\n #pingConfig?: ServerOptions[\"ping\"];\n\n #pingInterval: null | ReturnType = null;\n\n #prompts: Prompt[] = [];\n\n #resources: Resource[] = [];\n\n #resourceTemplates: ResourceTemplate[] = [];\n\n #roots: Root[] = [];\n\n #rootsConfig?: ServerOptions[\"roots\"];\n\n #server: Server;\n\n /**\n * Session ID from the Mcp-Session-Id header (HTTP transports only).\n * Used to track per-session state across multiple requests.\n */\n #sessionId?: string;\n\n #utils?: ServerOptions[\"utils\"];\n\n constructor({\n auth,\n instructions,\n logger,\n name,\n ping,\n prompts,\n resources,\n resourcesTemplates,\n roots,\n sessionId,\n tools,\n transportType,\n utils,\n version,\n }: {\n auth?: T;\n instructions?: string;\n logger: Logger;\n name: string;\n ping?: ServerOptions[\"ping\"];\n prompts: Prompt[];\n resources: Resource[];\n resourcesTemplates: InputResourceTemplate[];\n roots?: ServerOptions[\"roots\"];\n sessionId?: string;\n tools: Tool[];\n transportType?: \"httpStream\" | \"stdio\";\n utils?: ServerOptions[\"utils\"];\n version: string;\n }) {\n super();\n\n this.#auth = auth;\n this.#logger = logger;\n this.#pingConfig = ping;\n this.#rootsConfig = roots;\n this.#sessionId = sessionId;\n this.#needsEventLoopFlush = transportType === \"httpStream\";\n\n if (tools.length) {\n this.#capabilities.tools = {};\n }\n\n if (resources.length || resourcesTemplates.length) {\n this.#capabilities.resources = {};\n }\n\n if (prompts.length) {\n for (const prompt of prompts) {\n this.addPrompt(prompt);\n }\n\n this.#capabilities.prompts = {};\n }\n\n this.#capabilities.logging = {};\n\n this.#server = new Server(\n { name: name, version: version },\n { capabilities: this.#capabilities, instructions: instructions },\n );\n\n this.#utils = utils;\n\n this.setupErrorHandling();\n this.setupLoggingHandlers();\n this.setupRootsHandlers();\n this.setupCompleteHandlers();\n\n if (tools.length) {\n this.setupToolHandlers(tools);\n }\n\n if (resources.length || resourcesTemplates.length) {\n for (const resource of resources) {\n this.addResource(resource);\n }\n\n this.setupResourceHandlers(resources);\n\n if (resourcesTemplates.length) {\n for (const resourceTemplate of resourcesTemplates) {\n this.addResourceTemplate(resourceTemplate);\n }\n\n this.setupResourceTemplateHandlers(resourcesTemplates);\n }\n }\n\n if (prompts.length) {\n this.setupPromptHandlers(prompts);\n }\n }\n\n public async close() {\n this.#connectionState = \"closed\";\n\n if (this.#pingInterval) {\n clearInterval(this.#pingInterval);\n }\n\n try {\n await this.#server.close();\n } catch (error) {\n this.#logger.error(\"[FastMCP error]\", \"could not close server\", error);\n }\n }\n\n public async connect(transport: Transport) {\n if (this.#server.transport) {\n throw new UnexpectedStateError(\"Server is already connected\");\n }\n\n this.#connectionState = \"connecting\";\n\n try {\n await this.#server.connect(transport);\n\n // Extract session ID from transport if available (HTTP transports only)\n if (\"sessionId\" in transport) {\n const transportWithSessionId = transport as {\n sessionId?: string;\n } & Transport;\n if (typeof transportWithSessionId.sessionId === \"string\") {\n this.#sessionId = transportWithSessionId.sessionId;\n }\n }\n\n let attempt = 0;\n const maxAttempts = 10;\n const retryDelay = 100;\n\n while (attempt++ < maxAttempts) {\n const capabilities = this.#server.getClientCapabilities();\n\n if (capabilities) {\n this.#clientCapabilities = capabilities;\n break;\n }\n\n await delay(retryDelay);\n }\n\n if (!this.#clientCapabilities) {\n this.#logger.warn(\n `[FastMCP warning] could not infer client capabilities after ${maxAttempts} attempts. Connection may be unstable.`,\n );\n }\n\n if (\n this.#rootsConfig?.enabled !== false &&\n this.#clientCapabilities?.roots?.listChanged &&\n typeof this.#server.listRoots === \"function\"\n ) {\n try {\n const roots = await this.#server.listRoots();\n this.#roots = roots?.roots || [];\n } catch (e) {\n if (e instanceof McpError && e.code === ErrorCode.MethodNotFound) {\n this.#logger.debug(\n \"[FastMCP debug] listRoots method not supported by client\",\n );\n } else {\n this.#logger.error(\n `[FastMCP error] received error listing roots.\\n\\n${\n e instanceof Error ? e.stack : JSON.stringify(e)\n }`,\n );\n }\n }\n }\n\n if (this.#clientCapabilities) {\n const pingConfig = this.#getPingConfig(transport);\n\n if (pingConfig.enabled) {\n this.#pingInterval = setInterval(async () => {\n try {\n await this.#server.ping();\n } catch {\n // The reason we are not emitting an error here is because some clients\n // seem to not respond to the ping request, and we don't want to crash the server,\n // e.g., https://github.com/punkpeye/fastmcp/issues/38.\n const logLevel = pingConfig.logLevel;\n\n if (logLevel === \"debug\") {\n this.#logger.debug(\"[FastMCP debug] server ping failed\");\n } else if (logLevel === \"warning\") {\n this.#logger.warn(\n \"[FastMCP warning] server is not responding to ping\",\n );\n } else if (logLevel === \"error\") {\n this.#logger.error(\n \"[FastMCP error] server is not responding to ping\",\n );\n } else {\n this.#logger.info(\"[FastMCP info] server ping failed\");\n }\n }\n }, pingConfig.intervalMs);\n }\n }\n\n // Mark connection as ready and emit event\n this.#connectionState = \"ready\";\n this.emit(\"ready\");\n } catch (error) {\n this.#connectionState = \"error\";\n const errorEvent = {\n error: error instanceof Error ? error : new Error(String(error)),\n };\n this.emit(\"error\", errorEvent);\n throw error;\n }\n }\n\n public async requestSampling(\n message: z.infer[\"params\"],\n options?: RequestOptions,\n ): Promise {\n return this.#server.createMessage(message, options);\n }\n\n public waitForReady(): Promise {\n if (this.isReady) {\n return Promise.resolve();\n }\n\n if (\n this.#connectionState === \"error\" ||\n this.#connectionState === \"closed\"\n ) {\n return Promise.reject(\n new Error(`Connection is in ${this.#connectionState} state`),\n );\n }\n\n return new Promise((resolve, reject) => {\n const timeout = setTimeout(() => {\n reject(\n new Error(\n \"Connection timeout: Session failed to become ready within 5 seconds\",\n ),\n );\n }, 5000);\n\n this.once(\"ready\", () => {\n clearTimeout(timeout);\n resolve();\n });\n\n this.once(\"error\", (event) => {\n clearTimeout(timeout);\n reject(event.error);\n });\n });\n }\n\n #getPingConfig(transport: Transport): {\n enabled: boolean;\n intervalMs: number;\n logLevel: LoggingLevel;\n } {\n const pingConfig = this.#pingConfig || {};\n\n let defaultEnabled = false;\n\n if (\"type\" in transport) {\n // Enable by default for SSE and HTTP streaming\n if (transport.type === \"httpStream\") {\n defaultEnabled = true;\n }\n }\n\n return {\n enabled:\n pingConfig.enabled !== undefined ? pingConfig.enabled : defaultEnabled,\n intervalMs: pingConfig.intervalMs || 5000,\n logLevel: pingConfig.logLevel || \"debug\",\n };\n }\n\n private addPrompt(inputPrompt: InputPrompt) {\n const completers: Record> = {};\n const enums: Record = {};\n const fuseInstances: Record> = {};\n\n for (const argument of inputPrompt.arguments ?? []) {\n if (argument.complete) {\n completers[argument.name] = argument.complete;\n }\n\n if (argument.enum) {\n enums[argument.name] = argument.enum;\n fuseInstances[argument.name] = new Fuse(argument.enum, {\n includeScore: true,\n threshold: 0.3, // More flexible matching!\n });\n }\n }\n\n const prompt = {\n ...inputPrompt,\n complete: async (name: string, value: string, auth?: T) => {\n if (completers[name]) {\n return await completers[name](value, auth);\n }\n\n if (fuseInstances[name]) {\n const result = fuseInstances[name].search(value);\n\n return {\n total: result.length,\n values: result.map((item) => item.item),\n };\n }\n\n return {\n values: [],\n };\n },\n };\n\n this.#prompts.push(prompt);\n }\n\n private addResource(inputResource: Resource) {\n this.#resources.push(inputResource);\n }\n\n private addResourceTemplate(inputResourceTemplate: InputResourceTemplate) {\n const completers: Record> = {};\n\n for (const argument of inputResourceTemplate.arguments ?? []) {\n if (argument.complete) {\n completers[argument.name] = argument.complete;\n }\n }\n\n const resourceTemplate = {\n ...inputResourceTemplate,\n complete: async (name: string, value: string, auth?: T) => {\n if (completers[name]) {\n return await completers[name](value, auth);\n }\n\n return {\n values: [],\n };\n },\n };\n\n this.#resourceTemplates.push(resourceTemplate);\n }\n\n private setupCompleteHandlers() {\n this.#server.setRequestHandler(CompleteRequestSchema, async (request) => {\n if (request.params.ref.type === \"ref/prompt\") {\n const prompt = this.#prompts.find(\n (prompt) => prompt.name === request.params.ref.name,\n );\n\n if (!prompt) {\n throw new UnexpectedStateError(\"Unknown prompt\", {\n request,\n });\n }\n\n if (!prompt.complete) {\n throw new UnexpectedStateError(\"Prompt does not support completion\", {\n request,\n });\n }\n\n const completion = CompletionZodSchema.parse(\n await prompt.complete(\n request.params.argument.name,\n request.params.argument.value,\n this.#auth,\n ),\n );\n\n return {\n completion,\n };\n }\n\n if (request.params.ref.type === \"ref/resource\") {\n const resource = this.#resourceTemplates.find(\n (resource) => resource.uriTemplate === request.params.ref.uri,\n );\n\n if (!resource) {\n throw new UnexpectedStateError(\"Unknown resource\", {\n request,\n });\n }\n\n if (!(\"uriTemplate\" in resource)) {\n throw new UnexpectedStateError(\"Unexpected resource\");\n }\n\n if (!resource.complete) {\n throw new UnexpectedStateError(\n \"Resource does not support completion\",\n {\n request,\n },\n );\n }\n\n const completion = CompletionZodSchema.parse(\n await resource.complete(\n request.params.argument.name,\n request.params.argument.value,\n this.#auth,\n ),\n );\n\n return {\n completion,\n };\n }\n\n throw new UnexpectedStateError(\"Unexpected completion request\", {\n request,\n });\n });\n }\n\n private setupErrorHandling() {\n this.#server.onerror = (error) => {\n this.#logger.error(\"[FastMCP error]\", error);\n };\n }\n\n private setupLoggingHandlers() {\n this.#server.setRequestHandler(SetLevelRequestSchema, (request) => {\n this.#loggingLevel = request.params.level;\n\n return {};\n });\n }\n\n private setupPromptHandlers(prompts: Prompt[]) {\n this.#server.setRequestHandler(ListPromptsRequestSchema, async () => {\n return {\n prompts: prompts.map((prompt) => {\n return {\n arguments: prompt.arguments,\n complete: prompt.complete,\n description: prompt.description,\n name: prompt.name,\n };\n }),\n };\n });\n\n this.#server.setRequestHandler(GetPromptRequestSchema, async (request) => {\n const prompt = prompts.find(\n (prompt) => prompt.name === request.params.name,\n );\n\n if (!prompt) {\n throw new McpError(\n ErrorCode.MethodNotFound,\n `Unknown prompt: ${request.params.name}`,\n );\n }\n\n const args = request.params.arguments;\n\n for (const arg of prompt.arguments ?? []) {\n if (arg.required && !(args && arg.name in args)) {\n throw new McpError(\n ErrorCode.InvalidRequest,\n `Prompt '${request.params.name}' requires argument '${arg.name}': ${\n arg.description || \"No description provided\"\n }`,\n );\n }\n }\n\n let result: Awaited[\"load\"]>>;\n\n try {\n result = await prompt.load(\n args as Record,\n this.#auth,\n );\n } catch (error) {\n const errorMessage =\n error instanceof Error ? error.message : String(error);\n throw new McpError(\n ErrorCode.InternalError,\n `Failed to load prompt '${request.params.name}': ${errorMessage}`,\n );\n }\n\n if (typeof result === \"string\") {\n return {\n description: prompt.description,\n messages: [\n {\n content: { text: result, type: \"text\" },\n role: \"user\",\n },\n ],\n };\n } else {\n return {\n description: prompt.description,\n messages: result.messages,\n };\n }\n });\n }\n\n private setupResourceHandlers(resources: Resource[]) {\n this.#server.setRequestHandler(ListResourcesRequestSchema, async () => {\n return {\n resources: resources.map((resource) => ({\n description: resource.description,\n mimeType: resource.mimeType,\n name: resource.name,\n uri: resource.uri,\n })),\n } satisfies ListResourcesResult;\n });\n\n this.#server.setRequestHandler(\n ReadResourceRequestSchema,\n async (request) => {\n if (\"uri\" in request.params) {\n const resource = resources.find(\n (resource) =>\n \"uri\" in resource && resource.uri === request.params.uri,\n );\n\n if (!resource) {\n for (const resourceTemplate of this.#resourceTemplates) {\n const uriTemplate = parseURITemplate(\n resourceTemplate.uriTemplate,\n );\n\n const match = uriTemplate.fromUri(request.params.uri);\n\n if (!match) {\n continue;\n }\n\n const uri = uriTemplate.fill(match);\n\n const result = await resourceTemplate.load(match, this.#auth);\n\n const resources = Array.isArray(result) ? result : [result];\n return {\n contents: resources.map((resource) => ({\n ...resource,\n description: resourceTemplate.description,\n mimeType: resource.mimeType ?? resourceTemplate.mimeType,\n name: resourceTemplate.name,\n uri: resource.uri ?? uri,\n })),\n };\n }\n\n throw new McpError(\n ErrorCode.MethodNotFound,\n `Resource not found: '${request.params.uri}'. Available resources: ${\n resources.map((r) => r.uri).join(\", \") || \"none\"\n }`,\n );\n }\n\n if (!(\"uri\" in resource)) {\n throw new UnexpectedStateError(\"Resource does not support reading\");\n }\n\n let maybeArrayResult: Awaited[\"load\"]>>;\n\n try {\n maybeArrayResult = await resource.load(this.#auth);\n } catch (error) {\n const errorMessage =\n error instanceof Error ? error.message : String(error);\n throw new McpError(\n ErrorCode.InternalError,\n `Failed to load resource '${resource.name}' (${resource.uri}): ${errorMessage}`,\n {\n uri: resource.uri,\n },\n );\n }\n\n const resourceResults = Array.isArray(maybeArrayResult)\n ? maybeArrayResult\n : [maybeArrayResult];\n\n return {\n contents: resourceResults.map((result) => ({\n ...result,\n mimeType: result.mimeType ?? resource.mimeType,\n name: resource.name,\n uri: result.uri ?? resource.uri,\n })),\n };\n }\n\n throw new UnexpectedStateError(\"Unknown resource request\", {\n request,\n });\n },\n );\n }\n\n private setupResourceTemplateHandlers(\n resourceTemplates: ResourceTemplate[],\n ) {\n this.#server.setRequestHandler(\n ListResourceTemplatesRequestSchema,\n async () => {\n return {\n resourceTemplates: resourceTemplates.map((resourceTemplate) => ({\n description: resourceTemplate.description,\n mimeType: resourceTemplate.mimeType,\n name: resourceTemplate.name,\n uriTemplate: resourceTemplate.uriTemplate,\n })),\n } satisfies ListResourceTemplatesResult;\n },\n );\n }\n\n private setupRootsHandlers() {\n if (this.#rootsConfig?.enabled === false) {\n this.#logger.debug(\n \"[FastMCP debug] roots capability explicitly disabled via config\",\n );\n return;\n }\n\n // Only set up roots notification handling if the server supports it\n if (typeof this.#server.listRoots === \"function\") {\n this.#server.setNotificationHandler(\n RootsListChangedNotificationSchema,\n () => {\n this.#server\n .listRoots()\n .then((roots) => {\n this.#roots = roots.roots;\n\n this.emit(\"rootsChanged\", {\n roots: roots.roots,\n });\n })\n .catch((error) => {\n if (\n error instanceof McpError &&\n error.code === ErrorCode.MethodNotFound\n ) {\n this.#logger.debug(\n \"[FastMCP debug] listRoots method not supported by client\",\n );\n } else {\n this.#logger.error(\n `[FastMCP error] received error listing roots.\\n\\n${\n error instanceof Error ? error.stack : JSON.stringify(error)\n }`,\n );\n }\n });\n },\n );\n } else {\n this.#logger.debug(\n \"[FastMCP debug] roots capability not available, not setting up notification handler\",\n );\n }\n }\n\n private setupToolHandlers(tools: Tool[]) {\n this.#server.setRequestHandler(ListToolsRequestSchema, async () => {\n return {\n tools: await Promise.all(\n tools.map(async (tool) => {\n return {\n annotations: tool.annotations,\n description: tool.description,\n inputSchema: tool.parameters\n ? await toJsonSchema(tool.parameters)\n : {\n additionalProperties: false,\n properties: {},\n type: \"object\",\n }, // More complete schema for Cursor compatibility\n name: tool.name,\n };\n }),\n ),\n };\n });\n\n this.#server.setRequestHandler(CallToolRequestSchema, async (request) => {\n const tool = tools.find((tool) => tool.name === request.params.name);\n\n if (!tool) {\n throw new McpError(\n ErrorCode.MethodNotFound,\n `Unknown tool: ${request.params.name}`,\n );\n }\n\n let args: unknown = undefined;\n\n if (tool.parameters) {\n const parsed = await tool.parameters[\"~standard\"].validate(\n request.params.arguments,\n );\n\n if (parsed.issues) {\n const friendlyErrors = this.#utils?.formatInvalidParamsErrorMessage\n ? this.#utils.formatInvalidParamsErrorMessage(parsed.issues)\n : parsed.issues\n .map((issue) => {\n const path = issue.path?.join(\".\") || \"root\";\n return `${path}: ${issue.message}`;\n })\n .join(\", \");\n\n throw new McpError(\n ErrorCode.InvalidParams,\n `Tool '${request.params.name}' parameter validation failed: ${friendlyErrors}. Please check the parameter types and values according to the tool's schema.`,\n );\n }\n\n args = parsed.value;\n }\n\n const progressToken = request.params?._meta?.progressToken;\n\n let result: ContentResult;\n\n try {\n const reportProgress = async (progress: Progress) => {\n try {\n await this.#server.notification({\n method: \"notifications/progress\",\n params: {\n ...progress,\n progressToken,\n },\n });\n\n if (this.#needsEventLoopFlush) {\n await new Promise((resolve) => setImmediate(resolve));\n }\n } catch (progressError) {\n this.#logger.warn(\n `[FastMCP warning] Failed to report progress for tool '${request.params.name}':`,\n progressError instanceof Error\n ? progressError.message\n : String(progressError),\n );\n }\n };\n\n const log = {\n debug: (message: string, context?: SerializableValue) => {\n this.#server.sendLoggingMessage({\n data: {\n context,\n message,\n },\n level: \"debug\",\n });\n },\n error: (message: string, context?: SerializableValue) => {\n this.#server.sendLoggingMessage({\n data: {\n context,\n message,\n },\n level: \"error\",\n });\n },\n info: (message: string, context?: SerializableValue) => {\n this.#server.sendLoggingMessage({\n data: {\n context,\n message,\n },\n level: \"info\",\n });\n },\n warn: (message: string, context?: SerializableValue) => {\n this.#server.sendLoggingMessage({\n data: {\n context,\n message,\n },\n level: \"warning\",\n });\n },\n };\n\n // Create a promise for tool execution\n // Streams partial results while a tool is still executing\n // Enables progressive rendering and real-time feedback\n const streamContent = async (content: Content | Content[]) => {\n const contentArray = Array.isArray(content) ? content : [content];\n\n try {\n await this.#server.notification({\n method: \"notifications/tool/streamContent\",\n params: {\n content: contentArray,\n toolName: request.params.name,\n },\n });\n\n if (this.#needsEventLoopFlush) {\n await new Promise((resolve) => setImmediate(resolve));\n }\n } catch (streamError) {\n this.#logger.warn(\n `[FastMCP warning] Failed to stream content for tool '${request.params.name}':`,\n streamError instanceof Error\n ? streamError.message\n : String(streamError),\n );\n }\n };\n const executeToolPromise = tool.execute(args, {\n client: {\n version: this.#server.getClientVersion(),\n },\n log,\n reportProgress,\n requestId:\n typeof request.params?._meta?.requestId === \"string\"\n ? request.params._meta.requestId\n : undefined,\n requestMetadata: request.params._meta,\n session: this.#auth,\n sessionId: this.#sessionId,\n streamContent,\n });\n\n // Handle timeout if specified\n const maybeStringResult = (await (tool.timeoutMs\n ? Promise.race([\n executeToolPromise,\n new Promise((_, reject) => {\n const timeoutId = setTimeout(() => {\n reject(\n new McpError(\n ErrorCode.InternalError,\n `Tool '${request.params.name}' timed out after ${tool.timeoutMs}ms. Consider increasing timeoutMs or optimizing the tool implementation.`,\n ),\n );\n }, tool.timeoutMs);\n\n // If promise resolves first\n executeToolPromise.finally(() => clearTimeout(timeoutId));\n }),\n ])\n : executeToolPromise)) as\n | AudioContent\n | ContentResult\n | ImageContent\n | null\n | ResourceContent\n | ResourceLink\n | string\n | TextContent\n | undefined;\n\n // Without this test, we are running into situations where the last progress update is not reported.\n // See the 'reports multiple progress updates without buffering' test in FastMCP.test.ts before refactoring.\n await delay(1);\n\n if (maybeStringResult === undefined || maybeStringResult === null) {\n result = ContentResultZodSchema.parse({\n content: [],\n });\n } else if (typeof maybeStringResult === \"string\") {\n result = ContentResultZodSchema.parse({\n content: [{ text: maybeStringResult, type: \"text\" }],\n });\n } else if (\"type\" in maybeStringResult) {\n result = ContentResultZodSchema.parse({\n content: [maybeStringResult],\n });\n } else {\n result = ContentResultZodSchema.parse(maybeStringResult);\n }\n } catch (error) {\n // Re-throw McpError to let the MCP SDK handle it as a proper JSON-RPC error\n if (error instanceof McpError) {\n throw error;\n }\n\n if (error instanceof UserError) {\n return {\n content: [{ text: error.message, type: \"text\" }],\n isError: true,\n ...(error.extras ? { structuredContent: error.extras } : {}),\n };\n }\n\n const errorMessage =\n error instanceof Error ? error.message : String(error);\n return {\n content: [\n {\n text: `Tool '${request.params.name}' execution failed: ${errorMessage}`,\n type: \"text\",\n },\n ],\n isError: true,\n };\n }\n\n return result;\n });\n }\n}\n\n/**\n * Converts camelCase to snake_case for OAuth endpoint responses\n */\nfunction camelToSnakeCase(str: string): string {\n return str.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`);\n}\n\n/**\n * Converts an object with camelCase keys to snake_case keys\n */\nfunction convertObjectToSnakeCase(\n obj: Record,\n): Record {\n const result: Record = {};\n\n for (const [key, value] of Object.entries(obj)) {\n const snakeKey = camelToSnakeCase(key);\n result[snakeKey] = value;\n }\n\n return result;\n}\n\nconst FastMCPEventEmitterBase: {\n new (): StrictEventEmitter>;\n} = EventEmitter;\n\nclass FastMCPEventEmitter extends FastMCPEventEmitterBase {}\n\nexport class FastMCP<\n T extends FastMCPSessionAuth = FastMCPSessionAuth,\n> extends FastMCPEventEmitter {\n public get sessions(): FastMCPSession[] {\n return this.#sessions;\n }\n #authenticate: Authenticate | undefined;\n #httpStreamServer: null | SSEServer = null;\n #logger: Logger;\n #options: ServerOptions;\n #prompts: InputPrompt[] = [];\n #resources: Resource[] = [];\n #resourcesTemplates: InputResourceTemplate[] = [];\n #sessions: FastMCPSession[] = [];\n\n #tools: Tool[] = [];\n\n constructor(public options: ServerOptions) {\n super();\n\n this.#options = options;\n this.#authenticate = options.authenticate;\n this.#logger = options.logger || console;\n }\n\n /**\n * Adds a prompt to the server.\n */\n public addPrompt[]>(\n prompt: InputPrompt,\n ) {\n this.#prompts.push(prompt);\n }\n\n /**\n * Adds a resource to the server.\n */\n public addResource(resource: Resource) {\n this.#resources.push(resource);\n }\n\n /**\n * Adds a resource template to the server.\n */\n public addResourceTemplate<\n const Args extends InputResourceTemplateArgument[],\n >(resource: InputResourceTemplate) {\n this.#resourcesTemplates.push(resource);\n }\n\n /**\n * Adds a tool to the server.\n */\n public addTool(tool: Tool) {\n this.#tools.push(tool as unknown as Tool);\n }\n\n /**\n * Embeds a resource by URI, making it easy to include resources in tool responses.\n *\n * @param uri - The URI of the resource to embed\n * @returns Promise - The embedded resource content\n */\n public async embedded(uri: string): Promise {\n // First, try to find a direct resource match\n const directResource = this.#resources.find(\n (resource) => resource.uri === uri,\n );\n\n if (directResource) {\n const result = await directResource.load();\n const results = Array.isArray(result) ? result : [result];\n const firstResult = results[0];\n\n const resourceData: ResourceContent[\"resource\"] = {\n mimeType: directResource.mimeType,\n uri,\n };\n\n if (\"text\" in firstResult) {\n resourceData.text = firstResult.text;\n }\n\n if (\"blob\" in firstResult) {\n resourceData.blob = firstResult.blob;\n }\n\n return resourceData;\n }\n\n // Try to match against resource templates\n for (const template of this.#resourcesTemplates) {\n const parsedTemplate = parseURITemplate(template.uriTemplate);\n const params = parsedTemplate.fromUri(uri);\n if (!params) {\n continue;\n }\n\n const result = await template.load(\n params as ResourceTemplateArgumentsToObject,\n );\n\n const resourceData: ResourceContent[\"resource\"] = {\n mimeType: template.mimeType,\n uri,\n };\n\n if (\"text\" in result) {\n resourceData.text = result.text;\n }\n\n if (\"blob\" in result) {\n resourceData.blob = result.blob;\n }\n\n return resourceData; // The resource we're looking for\n }\n\n throw new UnexpectedStateError(`Resource not found: ${uri}`, { uri });\n }\n\n /**\n * Starts the server.\n */\n public async start(\n options?: Partial<{\n httpStream: {\n enableJsonResponse?: boolean;\n endpoint?: `/${string}`;\n eventStore?: EventStore;\n host?: string;\n port: number;\n stateless?: boolean;\n };\n transportType: \"httpStream\" | \"stdio\";\n }>,\n ) {\n const config = this.#parseRuntimeConfig(options);\n\n if (config.transportType === \"stdio\") {\n const transport = new StdioServerTransport();\n\n // For stdio transport, if authenticate function is provided, call it\n // with undefined request (since stdio doesn't have HTTP request context)\n let auth: T | undefined;\n\n if (this.#authenticate) {\n try {\n auth = await this.#authenticate(\n undefined as unknown as http.IncomingMessage,\n );\n } catch (error) {\n this.#logger.error(\n \"[FastMCP error] Authentication failed for stdio transport:\",\n error instanceof Error ? error.message : String(error),\n );\n // Continue without auth if authentication fails\n }\n }\n\n const session = new FastMCPSession({\n auth,\n instructions: this.#options.instructions,\n logger: this.#logger,\n name: this.#options.name,\n ping: this.#options.ping,\n prompts: this.#prompts,\n resources: this.#resources,\n resourcesTemplates: this.#resourcesTemplates,\n roots: this.#options.roots,\n tools: this.#tools,\n transportType: \"stdio\",\n utils: this.#options.utils,\n version: this.#options.version,\n });\n\n await session.connect(transport);\n\n this.#sessions.push(session);\n\n session.once(\"error\", () => {\n this.#removeSession(session);\n });\n\n // Monitor the underlying transport for close events\n if (transport.onclose) {\n const originalOnClose = transport.onclose;\n\n transport.onclose = () => {\n this.#removeSession(session);\n\n if (originalOnClose) {\n originalOnClose();\n }\n };\n } else {\n transport.onclose = () => {\n this.#removeSession(session);\n };\n }\n\n this.emit(\"connect\", {\n session: session as FastMCPSession,\n });\n } else if (config.transportType === \"httpStream\") {\n const httpConfig = config.httpStream;\n\n if (httpConfig.stateless) {\n // Stateless mode - create new server instance for each request\n this.#logger.info(\n `[FastMCP info] Starting server in stateless mode on HTTP Stream at http://${httpConfig.host}:${httpConfig.port}${httpConfig.endpoint}`,\n );\n\n this.#httpStreamServer = await startHTTPServer>({\n ...(this.#authenticate ? { authenticate: this.#authenticate } : {}),\n createServer: async (request) => {\n let auth: T | undefined;\n\n if (this.#authenticate) {\n auth = await this.#authenticate(request);\n\n // In stateless mode, authentication is REQUIRED\n // mcp-proxy will catch this error and return 401\n if (auth === undefined || auth === null) {\n throw new Error(\"Authentication required\");\n }\n }\n\n // Extract session ID from headers\n const sessionId = Array.isArray(request.headers[\"mcp-session-id\"])\n ? request.headers[\"mcp-session-id\"][0]\n : request.headers[\"mcp-session-id\"];\n\n // In stateless mode, create a new session for each request\n // without persisting it in the sessions array\n return this.#createSession(auth, sessionId);\n },\n enableJsonResponse: httpConfig.enableJsonResponse,\n eventStore: httpConfig.eventStore,\n host: httpConfig.host,\n // In stateless mode, we don't track sessions\n onClose: async () => {\n // No session tracking in stateless mode\n },\n onConnect: async () => {\n // No persistent session tracking in stateless mode\n this.#logger.debug(\n `[FastMCP debug] Stateless HTTP Stream request handled`,\n );\n },\n onUnhandledRequest: async (req, res) => {\n await this.#handleUnhandledRequest(req, res, true, httpConfig.host);\n },\n port: httpConfig.port,\n stateless: true,\n streamEndpoint: httpConfig.endpoint,\n });\n } else {\n // Regular mode with session management\n this.#httpStreamServer = await startHTTPServer>({\n ...(this.#authenticate ? { authenticate: this.#authenticate } : {}),\n createServer: async (request) => {\n let auth: T | undefined;\n\n if (this.#authenticate) {\n auth = await this.#authenticate(request);\n }\n\n // Extract session ID from headers\n const sessionId = Array.isArray(request.headers[\"mcp-session-id\"])\n ? request.headers[\"mcp-session-id\"][0]\n : request.headers[\"mcp-session-id\"];\n\n return this.#createSession(auth, sessionId);\n },\n enableJsonResponse: httpConfig.enableJsonResponse,\n eventStore: httpConfig.eventStore,\n host: httpConfig.host,\n onClose: async (session) => {\n const sessionIndex = this.#sessions.indexOf(session);\n\n if (sessionIndex !== -1) this.#sessions.splice(sessionIndex, 1);\n\n this.emit(\"disconnect\", {\n session: session as FastMCPSession,\n });\n },\n onConnect: async (session) => {\n this.#sessions.push(session);\n\n this.#logger.info(`[FastMCP info] HTTP Stream session established`);\n\n this.emit(\"connect\", {\n session: session as FastMCPSession,\n });\n },\n\n onUnhandledRequest: async (req, res) => {\n await this.#handleUnhandledRequest(\n req,\n res,\n false,\n httpConfig.host,\n );\n },\n port: httpConfig.port,\n stateless: httpConfig.stateless,\n streamEndpoint: httpConfig.endpoint,\n });\n\n this.#logger.info(\n `[FastMCP info] server is running on HTTP Stream at http://${httpConfig.host}:${httpConfig.port}${httpConfig.endpoint}`,\n );\n }\n } else {\n throw new Error(\"Invalid transport type\");\n }\n }\n\n /**\n * Stops the server.\n */\n public async stop() {\n if (this.#httpStreamServer) {\n await this.#httpStreamServer.close();\n }\n }\n\n /**\n * Creates a new FastMCPSession instance with the current configuration.\n * Used both for regular sessions and stateless requests.\n */\n #createSession(auth?: T, sessionId?: string): FastMCPSession {\n // Check if authentication failed\n if (\n auth &&\n typeof auth === \"object\" &&\n \"authenticated\" in auth &&\n !(auth as { authenticated: unknown }).authenticated\n ) {\n const errorMessage =\n \"error\" in auth &&\n typeof (auth as { error: unknown }).error === \"string\"\n ? (auth as { error: string }).error\n : \"Authentication failed\";\n throw new Error(errorMessage);\n }\n\n const allowedTools = auth\n ? this.#tools.filter((tool) =>\n tool.canAccess ? tool.canAccess(auth) : true,\n )\n : this.#tools;\n return new FastMCPSession({\n auth,\n instructions: this.#options.instructions,\n logger: this.#logger,\n name: this.#options.name,\n ping: this.#options.ping,\n prompts: this.#prompts,\n resources: this.#resources,\n resourcesTemplates: this.#resourcesTemplates,\n roots: this.#options.roots,\n sessionId,\n tools: allowedTools,\n transportType: \"httpStream\",\n utils: this.#options.utils,\n version: this.#options.version,\n });\n }\n\n /**\n * Handles unhandled HTTP requests with health, readiness, and OAuth endpoints\n */\n #handleUnhandledRequest = async (\n req: http.IncomingMessage,\n res: http.ServerResponse,\n isStateless = false,\n host: string,\n ) => {\n const healthConfig = this.#options.health ?? {};\n\n const enabled =\n healthConfig.enabled === undefined ? true : healthConfig.enabled;\n\n if (enabled) {\n const path = healthConfig.path ?? \"/health\";\n const url = new URL(req.url || \"\", `http://${host}`);\n\n try {\n if (req.method === \"GET\" && url.pathname === path) {\n res\n .writeHead(healthConfig.status ?? 200, {\n \"Content-Type\": \"text/plain\",\n })\n .end(healthConfig.message ?? \"✓ Ok\");\n\n return;\n }\n\n // Enhanced readiness check endpoint\n if (req.method === \"GET\" && url.pathname === \"/ready\") {\n if (isStateless) {\n // In stateless mode, we're always ready if the server is running\n const response = {\n mode: \"stateless\",\n ready: 1,\n status: \"ready\",\n total: 1,\n };\n\n res\n .writeHead(200, {\n \"Content-Type\": \"application/json\",\n })\n .end(JSON.stringify(response));\n } else {\n const readySessions = this.#sessions.filter(\n (s) => s.isReady,\n ).length;\n const totalSessions = this.#sessions.length;\n const allReady =\n readySessions === totalSessions && totalSessions > 0;\n\n const response = {\n ready: readySessions,\n status: allReady\n ? \"ready\"\n : totalSessions === 0\n ? \"no_sessions\"\n : \"initializing\",\n total: totalSessions,\n };\n\n res\n .writeHead(allReady ? 200 : 503, {\n \"Content-Type\": \"application/json\",\n })\n .end(JSON.stringify(response));\n }\n\n return;\n }\n } catch (error) {\n this.#logger.error(\"[FastMCP error] health endpoint error\", error);\n }\n }\n\n // Handle OAuth well-known endpoints\n const oauthConfig = this.#options.oauth;\n if (oauthConfig?.enabled && req.method === \"GET\") {\n const url = new URL(req.url || \"\", `http://${host}`);\n\n if (\n url.pathname === \"/.well-known/oauth-authorization-server\" &&\n oauthConfig.authorizationServer\n ) {\n const metadata = convertObjectToSnakeCase(\n oauthConfig.authorizationServer,\n );\n res\n .writeHead(200, {\n \"Content-Type\": \"application/json\",\n })\n .end(JSON.stringify(metadata));\n return;\n }\n\n if (\n url.pathname === \"/.well-known/oauth-protected-resource\" &&\n oauthConfig.protectedResource\n ) {\n const metadata = convertObjectToSnakeCase(\n oauthConfig.protectedResource,\n );\n res\n .writeHead(200, {\n \"Content-Type\": \"application/json\",\n })\n .end(JSON.stringify(metadata));\n return;\n }\n }\n\n // If the request was not handled above, return 404\n res.writeHead(404).end();\n };\n\n #parseRuntimeConfig(\n overrides?: Partial<{\n httpStream: {\n enableJsonResponse?: boolean;\n endpoint?: `/${string}`;\n host?: string;\n port: number;\n stateless?: boolean;\n };\n transportType: \"httpStream\" | \"stdio\";\n }>,\n ):\n | {\n httpStream: {\n enableJsonResponse?: boolean;\n endpoint: `/${string}`;\n eventStore?: EventStore;\n host: string;\n port: number;\n stateless?: boolean;\n };\n transportType: \"httpStream\";\n }\n | { transportType: \"stdio\" } {\n const args = process.argv.slice(2);\n const getArg = (name: string) => {\n const index = args.findIndex((arg) => arg === `--${name}`);\n\n return index !== -1 && index + 1 < args.length\n ? args[index + 1]\n : undefined;\n };\n\n const transportArg = getArg(\"transport\");\n const portArg = getArg(\"port\");\n const endpointArg = getArg(\"endpoint\");\n const statelessArg = getArg(\"stateless\");\n const hostArg = getArg(\"host\");\n\n const envTransport = process.env.FASTMCP_TRANSPORT;\n const envPort = process.env.FASTMCP_PORT;\n const envEndpoint = process.env.FASTMCP_ENDPOINT;\n const envStateless = process.env.FASTMCP_STATELESS;\n const envHost = process.env.FASTMCP_HOST;\n // Overrides > CLI > env > defaults\n const transportType =\n overrides?.transportType ||\n (transportArg === \"http-stream\" ? \"httpStream\" : transportArg) ||\n envTransport ||\n \"stdio\";\n\n if (transportType === \"httpStream\") {\n const port = parseInt(\n overrides?.httpStream?.port?.toString() || portArg || envPort || \"8080\",\n );\n const host =\n overrides?.httpStream?.host || hostArg || envHost || \"localhost\";\n const endpoint =\n overrides?.httpStream?.endpoint || endpointArg || envEndpoint || \"/mcp\";\n const enableJsonResponse =\n overrides?.httpStream?.enableJsonResponse || false;\n const stateless =\n overrides?.httpStream?.stateless ||\n statelessArg === \"true\" ||\n envStateless === \"true\" ||\n false;\n\n return {\n httpStream: {\n enableJsonResponse,\n endpoint: endpoint as `/${string}`,\n host,\n port,\n stateless,\n },\n transportType: \"httpStream\" as const,\n };\n }\n\n return { transportType: \"stdio\" as const };\n }\n\n #removeSession(session: FastMCPSession): void {\n const sessionIndex = this.#sessions.indexOf(session);\n\n if (sessionIndex !== -1) {\n this.#sessions.splice(sessionIndex, 1);\n this.emit(\"disconnect\", {\n session: session as FastMCPSession,\n });\n }\n }\n}\n\nexport { ErrorCode, McpError } from \"@modelcontextprotocol/sdk/types.js\";\n\nexport type {\n AudioContent,\n Content,\n ContentResult,\n Context,\n FastMCPEvents,\n FastMCPSessionEvents,\n ImageContent,\n InputPrompt,\n InputPromptArgument,\n LoggingLevel,\n Progress,\n Prompt,\n PromptArgument,\n RequestMeta,\n Resource,\n ResourceContent,\n ResourceLink,\n ResourceResult,\n ResourceTemplate,\n ResourceTemplateArgument,\n SerializableValue,\n ServerOptions,\n TextContent,\n Tool,\n ToolParameters,\n};\n"],"mappings":"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","names":["ErrorCode","McpError","prompt","resource","resources","tool"]} \ No newline at end of file +{"version":3,"sources":["../src/FastMCP.ts"],"sourcesContent":["import { Server } from \"@modelcontextprotocol/sdk/server/index.js\"\nimport { StdioServerTransport } from \"@modelcontextprotocol/sdk/server/stdio.js\"\nimport { EventStore } from \"@modelcontextprotocol/sdk/server/streamableHttp.js\"\nimport { RequestOptions } from \"@modelcontextprotocol/sdk/shared/protocol.js\"\nimport { Transport } from \"@modelcontextprotocol/sdk/shared/transport.js\"\nimport {\n CallToolRequestSchema,\n ClientCapabilities,\n CompleteRequestSchema,\n CreateMessageRequestSchema,\n ErrorCode,\n GetPromptRequestSchema,\n GetPromptResult,\n ListPromptsRequestSchema,\n ListResourcesRequestSchema,\n ListResourcesResult,\n ListResourceTemplatesRequestSchema,\n ListResourceTemplatesResult,\n ListToolsRequestSchema,\n McpError,\n ReadResourceRequestSchema,\n RequestMeta,\n ResourceLink,\n Root,\n RootsListChangedNotificationSchema,\n ServerCapabilities,\n SetLevelRequestSchema,\n} from \"@modelcontextprotocol/sdk/types.js\"\nimport { StandardSchemaV1 } from \"@standard-schema/spec\"\nimport { EventEmitter } from \"events\"\nimport { readFile } from \"fs/promises\"\nimport Fuse from \"fuse.js\"\nimport http from \"http\"\nimport { startHTTPServer } from \"mcp-proxy\"\nimport { StrictEventEmitter } from \"strict-event-emitter-types\"\nimport { setTimeout as delay } from \"timers/promises\"\nimport { fetch } from \"undici\"\nimport parseURITemplate from \"uri-templates\"\nimport { toJsonSchema } from \"xsschema\"\nimport { z } from \"zod\"\n\nexport interface Logger {\n debug(...args: unknown[]): void\n error(...args: unknown[]): void\n info(...args: unknown[]): void\n log(...args: unknown[]): void\n warn(...args: unknown[]): void\n}\n\nexport type SSEServer = {\n close: () => Promise\n}\n\ntype FastMCPEvents = {\n connect: (event: { session: FastMCPSession }) => void\n disconnect: (event: { session: FastMCPSession }) => void\n}\n\ntype FastMCPSessionEvents = {\n error: (event: { error: Error }) => void\n ready: () => void\n rootsChanged: (event: { roots: Root[] }) => void\n}\n\nexport const imageContent = async (\n input: { buffer: Buffer } | { path: string } | { url: string },\n): Promise => {\n let rawData: Buffer\n\n try {\n if (\"url\" in input) {\n try {\n const response = await fetch(input.url)\n\n if (!response.ok) {\n throw new Error(`Server responded with status: ${response.status} - ${response.statusText}`)\n }\n\n rawData = Buffer.from(await response.arrayBuffer())\n } catch (error) {\n throw new Error(\n `Failed to fetch image from URL (${input.url}): ${error instanceof Error ? error.message : String(error)}`,\n )\n }\n } else if (\"path\" in input) {\n try {\n rawData = await readFile(input.path)\n } catch (error) {\n throw new Error(\n `Failed to read image from path (${input.path}): ${error instanceof Error ? error.message : String(error)}`,\n )\n }\n } else if (\"buffer\" in input) {\n rawData = input.buffer\n } else {\n throw new Error(\"Invalid input: Provide a valid 'url', 'path', or 'buffer'\")\n }\n\n const { fileTypeFromBuffer } = await import(\"file-type\")\n const mimeType = await fileTypeFromBuffer(rawData)\n\n if (!mimeType || !mimeType.mime.startsWith(\"image/\")) {\n console.warn(`Warning: Content may not be a valid image. Detected MIME: ${mimeType?.mime || \"unknown\"}`)\n }\n\n const base64Data = rawData.toString(\"base64\")\n\n return {\n data: base64Data,\n mimeType: mimeType?.mime ?? \"image/png\",\n type: \"image\",\n } as const\n } catch (error) {\n if (error instanceof Error) {\n throw error\n } else {\n throw new Error(`Unexpected error processing image: ${String(error)}`)\n }\n }\n}\n\nexport const audioContent = async (\n input: { buffer: Buffer } | { path: string } | { url: string },\n): Promise => {\n let rawData: Buffer\n\n try {\n if (\"url\" in input) {\n try {\n const response = await fetch(input.url)\n\n if (!response.ok) {\n throw new Error(`Server responded with status: ${response.status} - ${response.statusText}`)\n }\n\n rawData = Buffer.from(await response.arrayBuffer())\n } catch (error) {\n throw new Error(\n `Failed to fetch audio from URL (${input.url}): ${error instanceof Error ? error.message : String(error)}`,\n )\n }\n } else if (\"path\" in input) {\n try {\n rawData = await readFile(input.path)\n } catch (error) {\n throw new Error(\n `Failed to read audio from path (${input.path}): ${error instanceof Error ? error.message : String(error)}`,\n )\n }\n } else if (\"buffer\" in input) {\n rawData = input.buffer\n } else {\n throw new Error(\"Invalid input: Provide a valid 'url', 'path', or 'buffer'\")\n }\n\n const { fileTypeFromBuffer } = await import(\"file-type\")\n const mimeType = await fileTypeFromBuffer(rawData)\n\n if (!mimeType || !mimeType.mime.startsWith(\"audio/\")) {\n console.warn(`Warning: Content may not be a valid audio file. Detected MIME: ${mimeType?.mime || \"unknown\"}`)\n }\n\n const base64Data = rawData.toString(\"base64\")\n\n return {\n data: base64Data,\n mimeType: mimeType?.mime ?? \"audio/mpeg\",\n type: \"audio\",\n } as const\n } catch (error) {\n if (error instanceof Error) {\n throw error\n } else {\n throw new Error(`Unexpected error processing audio: ${String(error)}`)\n }\n }\n}\n\ntype Context = {\n client: {\n version: ReturnType\n }\n log: {\n debug: (message: string, data?: SerializableValue) => void\n error: (message: string, data?: SerializableValue) => void\n info: (message: string, data?: SerializableValue) => void\n warn: (message: string, data?: SerializableValue) => void\n }\n reportProgress: (progress: Progress) => Promise\n /**\n * Request ID from the current MCP request.\n * Available for all transports when the client provides it.\n */\n requestId?: string\n requestMetadata?: RequestMeta\n session: T | undefined\n /**\n * Session ID from the Mcp-Session-Id header.\n * Only available for HTTP-based transports (SSE, HTTP Stream).\n * Can be used to track per-session state, implement session-specific\n * counters, or maintain user-specific data across multiple requests.\n */\n sessionId?: string\n streamContent: (content: Content | Content[]) => Promise\n}\n\ntype Extra = unknown\n\ntype Extras = Record\n\ntype Literal = boolean | null | number | string | undefined\n\ntype Progress = {\n /**\n * The progress thus far. This should increase every time progress is made, even if the total is unknown.\n */\n progress: number\n /**\n * Total number of items to process (or total progress required), if known.\n */\n total?: number\n}\n\ntype SerializableValue = { [key: string]: SerializableValue } | Literal | SerializableValue[]\n\ntype TextContent = {\n text: string\n type: \"text\"\n}\n\ntype ToolParameters = StandardSchemaV1\n\nabstract class FastMCPError extends Error {\n public constructor(message?: string) {\n super(message)\n this.name = new.target.name\n }\n}\n\n/**\n * Custom MCP error with a marker property to enable robust error detection\n * across module boundaries.\n *\n * This class extends McpError and adds a `__isMcpError` marker property.\n * This allows error detection to work even when instanceof fails due to\n * module instances being loaded multiple times (e.g., with tsx, different\n * bundlers, or ESM/CommonJS mixing).\n *\n * Use this instead of McpError when you need the error to be re-thrown\n * by FastMCP's error handling rather than wrapped in a result.\n */\nexport class CustomMcpError extends McpError {\n readonly __isMcpError = true\n\n constructor(code: number, message: string, data?: unknown) {\n super(code, message, data)\n }\n}\n\nexport class UnexpectedStateError extends FastMCPError {\n public extras?: Extras\n\n public constructor(message: string, extras?: Extras) {\n super(message)\n this.name = new.target.name\n this.extras = extras\n }\n}\n\n/**\n * An error that is meant to be surfaced to the user.\n */\nexport class UserError extends UnexpectedStateError {}\n\n/**\n * Type guard to check if an error should be re-thrown as an MCP error.\n * Works across module boundaries by checking both instanceof and marker property.\n *\n * @param error - The error to check\n * @returns true if error is an McpError or has the __isMcpError marker\n */\nexport function isMcpErrorLike(error: unknown): error is McpError {\n return (\n error instanceof McpError ||\n (typeof error === \"object\" &&\n error !== null &&\n \"__isMcpError\" in error &&\n (error as { __isMcpError?: boolean }).__isMcpError === true)\n )\n}\n\nconst TextContentZodSchema = z\n .object({\n /**\n * The text content of the message.\n */\n text: z.string(),\n type: z.literal(\"text\"),\n })\n .strict() satisfies z.ZodType\n\ntype ImageContent = {\n data: string\n mimeType: string\n type: \"image\"\n}\n\nconst ImageContentZodSchema = z\n .object({\n /**\n * The base64-encoded image data.\n */\n data: z.string().base64(),\n /**\n * The MIME type of the image. Different providers may support different image types.\n */\n mimeType: z.string(),\n type: z.literal(\"image\"),\n })\n .strict() satisfies z.ZodType\n\ntype AudioContent = {\n data: string\n mimeType: string\n type: \"audio\"\n}\n\nconst AudioContentZodSchema = z\n .object({\n /**\n * The base64-encoded audio data.\n */\n data: z.string().base64(),\n mimeType: z.string(),\n type: z.literal(\"audio\"),\n })\n .strict() satisfies z.ZodType\n\ntype ResourceContent = {\n resource: {\n blob?: string\n mimeType?: string\n text?: string\n uri: string\n }\n type: \"resource\"\n}\n\nconst ResourceContentZodSchema = z\n .object({\n resource: z.object({\n blob: z.string().optional(),\n mimeType: z.string().optional(),\n text: z.string().optional(),\n uri: z.string(),\n }),\n type: z.literal(\"resource\"),\n })\n .strict() satisfies z.ZodType\n\nconst ResourceLinkZodSchema = z.object({\n description: z.string().optional(),\n mimeType: z.string().optional(),\n name: z.string(),\n title: z.string().optional(),\n type: z.literal(\"resource_link\"),\n uri: z.string(),\n}) satisfies z.ZodType\n\ntype Content = AudioContent | ImageContent | ResourceContent | ResourceLink | TextContent\n\nconst ContentZodSchema = z.discriminatedUnion(\"type\", [\n TextContentZodSchema,\n ImageContentZodSchema,\n AudioContentZodSchema,\n ResourceContentZodSchema,\n ResourceLinkZodSchema,\n]) satisfies z.ZodType\n\ntype ContentResult = {\n _meta?: Record\n content: Content[]\n isError?: boolean\n}\n\nconst ContentResultZodSchema = z\n .object({\n _meta: z.record(z.unknown()).optional(),\n content: ContentZodSchema.array(),\n isError: z.boolean().optional(),\n })\n .strict() satisfies z.ZodType\n\ntype Completion = {\n hasMore?: boolean\n total?: number\n values: string[]\n}\n\n/**\n * https://github.com/modelcontextprotocol/typescript-sdk/blob/3164da64d085ec4e022ae881329eee7b72f208d4/src/types.ts#L983-L1003\n */\nconst CompletionZodSchema = z.object({\n /**\n * Indicates whether there are additional completion options beyond those provided in the current response, even if the exact total is unknown.\n */\n hasMore: z.optional(z.boolean()),\n /**\n * The total number of completion options available. This can exceed the number of values actually sent in the response.\n */\n total: z.optional(z.number().int()),\n /**\n * An array of completion values. Must not exceed 100 items.\n */\n values: z.array(z.string()).max(100),\n}) satisfies z.ZodType\n\ntype ArgumentValueCompleter = (\n value: string,\n auth?: T,\n) => Promise\n\ntype InputPrompt<\n T extends FastMCPSessionAuth = FastMCPSessionAuth,\n Arguments extends InputPromptArgument[] = InputPromptArgument[],\n Args = PromptArgumentsToObject,\n> = {\n arguments?: InputPromptArgument[]\n description?: string\n load: (args: Args, auth?: T) => Promise\n name: string\n}\n\ntype InputPromptArgument = Readonly<{\n complete?: ArgumentValueCompleter\n description?: string\n enum?: string[]\n name: string\n required?: boolean\n}>\n\ntype InputResourceTemplate<\n T extends FastMCPSessionAuth,\n Arguments extends InputResourceTemplateArgument[] = InputResourceTemplateArgument[],\n> = {\n arguments: Arguments\n description?: string\n load: (args: ResourceTemplateArgumentsToObject, auth?: T) => Promise\n mimeType?: string\n name: string\n uriTemplate: string\n}\n\ntype InputResourceTemplateArgument = Readonly<{\n complete?: ArgumentValueCompleter\n description?: string\n name: string\n required?: boolean\n}>\n\ntype LoggingLevel = \"alert\" | \"critical\" | \"debug\" | \"emergency\" | \"error\" | \"info\" | \"notice\" | \"warning\"\n\ntype Prompt<\n T extends FastMCPSessionAuth = FastMCPSessionAuth,\n Arguments extends PromptArgument[] = PromptArgument[],\n Args = PromptArgumentsToObject,\n> = {\n arguments?: PromptArgument[]\n complete?: (name: string, value: string, auth?: T) => Promise\n description?: string\n load: (args: Args, auth?: T) => Promise\n name: string\n}\n\ntype PromptArgument = Readonly<{\n complete?: ArgumentValueCompleter\n description?: string\n enum?: string[]\n name: string\n required?: boolean\n}>\n\ntype PromptArgumentsToObject = {\n [K in T[number][\"name\"]]: Extract[\"required\"] extends true ? string : string | undefined\n}\n\ntype PromptResult = Pick | string\n\ntype Resource = {\n complete?: (name: string, value: string, auth?: T) => Promise\n description?: string\n load: (auth?: T) => Promise\n mimeType?: string\n name: string\n uri: string\n}\n\ntype ResourceResult =\n | {\n blob: string\n mimeType?: string\n uri?: string\n }\n | {\n mimeType?: string\n text: string\n uri?: string\n }\n\ntype ResourceTemplate<\n T extends FastMCPSessionAuth,\n Arguments extends ResourceTemplateArgument[] = ResourceTemplateArgument[],\n> = {\n arguments: Arguments\n complete?: (name: string, value: string, auth?: T) => Promise\n description?: string\n load: (args: ResourceTemplateArgumentsToObject, auth?: T) => Promise\n mimeType?: string\n name: string\n uriTemplate: string\n}\n\ntype ResourceTemplateArgument = Readonly<{\n complete?: ArgumentValueCompleter\n description?: string\n name: string\n required?: boolean\n}>\n\ntype ResourceTemplateArgumentsToObject = {\n [K in T[number][\"name\"]]: string\n}\n\ntype SamplingResponse = {\n content: AudioContent | ImageContent | TextContent\n model: string\n role: \"assistant\" | \"user\"\n stopReason?: \"endTurn\" | \"maxTokens\" | \"stopSequence\" | string\n}\n\ntype ServerOptions = {\n authenticate?: Authenticate\n /**\n * Configuration for the health-check endpoint that can be exposed when the\n * server is running using the HTTP Stream transport. When enabled, the\n * server will respond to an HTTP GET request with the configured path (by\n * default \"/health\") rendering a plain-text response (by default \"ok\") and\n * the configured status code (by default 200).\n *\n * The endpoint is only added when the server is started with\n * `transportType: \"httpStream\"` – it is ignored for the stdio transport.\n */\n health?: {\n /**\n * When set to `false` the health-check endpoint is disabled.\n * @default true\n */\n enabled?: boolean\n\n /**\n * Plain-text body returned by the endpoint.\n * @default \"ok\"\n */\n message?: string\n\n /**\n * HTTP path that should be handled.\n * @default \"/health\"\n */\n path?: string\n\n /**\n * HTTP response status that will be returned.\n * @default 200\n */\n status?: number\n }\n instructions?: string\n /**\n * Custom logger instance. If not provided, defaults to console.\n * Use this to integrate with your own logging system.\n */\n logger?: Logger\n name: string\n\n /**\n * Configuration for OAuth well-known discovery endpoints that can be exposed\n * when the server is running using HTTP-based transports (SSE or HTTP Stream).\n * When enabled, the server will respond to requests for OAuth discovery endpoints\n * with the configured metadata.\n *\n * The endpoints are only added when the server is started with\n * `transportType: \"httpStream\"` – they are ignored for the stdio transport.\n * Both SSE and HTTP Stream transports support OAuth endpoints.\n */\n oauth?: {\n /**\n * OAuth Authorization Server metadata for /.well-known/oauth-authorization-server\n *\n * This endpoint follows RFC 8414 (OAuth 2.0 Authorization Server Metadata)\n * and provides metadata about the OAuth 2.0 authorization server.\n *\n * Required by MCP Specification 2025-03-26\n */\n authorizationServer?: {\n authorizationEndpoint: string\n codeChallengeMethodsSupported?: string[]\n // DPoP support\n dpopSigningAlgValuesSupported?: string[]\n grantTypesSupported?: string[]\n\n introspectionEndpoint?: string\n // Required\n issuer: string\n // Common optional\n jwksUri?: string\n opPolicyUri?: string\n opTosUri?: string\n registrationEndpoint?: string\n responseModesSupported?: string[]\n responseTypesSupported: string[]\n revocationEndpoint?: string\n scopesSupported?: string[]\n serviceDocumentation?: string\n tokenEndpoint: string\n tokenEndpointAuthMethodsSupported?: string[]\n tokenEndpointAuthSigningAlgValuesSupported?: string[]\n\n uiLocalesSupported?: string[]\n }\n\n /**\n * Whether OAuth discovery endpoints should be enabled.\n */\n enabled: boolean\n\n /**\n * OAuth Protected Resource metadata for `/.well-known/oauth-protected-resource`\n *\n * This endpoint follows {@link https://www.rfc-editor.org/rfc/rfc9728.html | RFC 9728}\n * and provides metadata describing how an OAuth 2.0 protected resource (in this case,\n * an MCP server) expects to be accessed.\n *\n * When configured, FastMCP will automatically serve this metadata at the\n * `/.well-known/oauth-protected-resource` endpoint. The `authorizationServers` and `resource`\n * fields are required. All others are optional and will be omitted from the published\n * metadata if not specified.\n *\n * This satisfies the requirements of the MCP Authorization specification's\n * {@link https://modelcontextprotocol.io/specification/2025-06-18/basic/authorization#authorization-server-location | Authorization Server Location section}.\n *\n * Clients consuming this metadata MUST validate that any presented values comply with\n * RFC 9728, including strict validation of the `resource` identifier and intended audience\n * when access tokens are issued and presented (per RFC 8707 §2).\n *\n * @remarks Required by MCP Specification version 2025-06-18\n */\n protectedResource?: {\n /**\n * Allows for additional metadata fields beyond those defined in RFC 9728.\n *\n * @remarks This supports vendor-specific or experimental extensions.\n * @see {@link https://www.rfc-editor.org/rfc/rfc9728.html#section-2.3 | RFC 9728 §2.3}\n */\n [key: string]: unknown\n\n /**\n * Supported values for the `authorization_details` parameter (RFC 9396).\n *\n * @remarks Used when fine-grained access control is in play.\n * @see {@link https://www.rfc-editor.org/rfc/rfc9728.html#section-2-2.23 | RFC 9728 §2.2.23}\n */\n authorizationDetailsTypesSupported?: string[]\n\n /**\n * List of OAuth 2.0 authorization server issuer identifiers.\n *\n * These correspond to ASes that can issue access tokens for this protected resource.\n * MCP clients use these values to locate the relevant `/.well-known/oauth-authorization-server`\n * metadata for initiating the OAuth flow.\n *\n * @remarks Required by the MCP spec. MCP servers MUST provide at least one issuer.\n * Clients are responsible for choosing among them (see RFC 9728 §7.6).\n * @see {@link https://www.rfc-editor.org/rfc/rfc9728.html#section-2-2.3 | RFC 9728 §2.2.3}\n */\n authorizationServers: string[]\n\n /**\n * List of supported methods for presenting OAuth 2.0 bearer tokens.\n *\n * @remarks Valid values are `header`, `body`, and `query`.\n * If omitted, clients MAY assume only `header` is supported, per RFC 6750.\n * This is a client-side interpretation and not a serialization default.\n * @see {@link https://www.rfc-editor.org/rfc/rfc9728.html#section-2-2.9 | RFC 9728 §2.2.9}\n */\n bearerMethodsSupported?: string[]\n\n /**\n * Whether this resource requires all access tokens to be DPoP-bound.\n *\n * @remarks If omitted, clients SHOULD assume this is `false`.\n * @see {@link https://www.rfc-editor.org/rfc/rfc9728.html#section-2-2.27 | RFC 9728 §2.2.27}\n */\n dpopBoundAccessTokensRequired?: boolean\n\n /**\n * Supported algorithms for verifying DPoP proofs (RFC 9449).\n *\n * @see {@link https://www.rfc-editor.org/rfc/rfc9728.html#section-2-2.25 | RFC 9728 §2.2.25}\n */\n dpopSigningAlgValuesSupported?: string[]\n\n /**\n * JWKS URI of this resource. Used to validate access tokens or sign responses.\n *\n * @remarks When present, this MUST be an `https:` URI pointing to a valid JWK Set (RFC 7517).\n * @see {@link https://www.rfc-editor.org/rfc/rfc9728.html#section-2-2.5 | RFC 9728 §2.2.5}\n */\n jwksUri?: string\n\n /**\n * Canonical OAuth resource identifier for this protected resource (the MCP server).\n *\n * @remarks Typically the base URL of the MCP server. Clients MUST use this as the\n * `resource` parameter in authorization and token requests (per RFC 8707).\n * @see {@link https://www.rfc-editor.org/rfc/rfc9728.html#section-2-2.1 | RFC 9728 §2.2.1}\n */\n resource: string\n\n /**\n * URL to developer-accessible documentation for this resource.\n *\n * @remarks This field MAY be localized.\n * @see {@link https://www.rfc-editor.org/rfc/rfc9728.html#section-2-2.15 | RFC 9728 §2.2.15}\n */\n resourceDocumentation?: string\n\n /**\n * Human-readable name for display purposes (e.g., in UIs).\n *\n * @remarks This field MAY be localized using language tags (`resource_name#en`, etc.).\n * @see {@link https://www.rfc-editor.org/rfc/rfc9728.html#section-2-2.13 | RFC 9728 §2.2.13}\n */\n resourceName?: string\n\n /**\n * URL to a human-readable policy page describing acceptable use.\n *\n * @remarks This field MAY be localized.\n * @see {@link https://www.rfc-editor.org/rfc/rfc9728.html#section-2-2.17 | RFC 9728 §2.2.17}\n */\n resourcePolicyUri?: string\n\n /**\n * Supported JWS algorithms for signed responses from this resource (e.g., response signing).\n *\n * @remarks MUST NOT include `none`.\n * @see {@link https://www.rfc-editor.org/rfc/rfc9728.html#section-2-2.11 | RFC 9728 §2.2.11}\n */\n resourceSigningAlgValuesSupported?: string[]\n\n /**\n * URL to the protected resource’s Terms of Service.\n *\n * @remarks This field MAY be localized.\n * @see {@link https://www.rfc-editor.org/rfc/rfc9728.html#section-2-2.19 | RFC 9728 §2.2.19}\n */\n resourceTosUri?: string\n\n /**\n * Supported OAuth scopes for requesting access to this resource.\n *\n * @remarks Useful for discovery, but clients SHOULD still request the minimal scope required.\n * @see {@link https://www.rfc-editor.org/rfc/rfc9728.html#section-2-2.7 | RFC 9728 §2.2.7}\n */\n scopesSupported?: string[]\n\n /**\n * Developer-accessible documentation for how to use the service (not end-user docs).\n *\n * @remarks Semantically equivalent to `resourceDocumentation`, but included under its\n * alternate name for compatibility with tools or schemas expecting either.\n * @see {@link https://www.rfc-editor.org/rfc/rfc9728.html#section-2-2.15 | RFC 9728 §2.2.15}\n */\n serviceDocumentation?: string\n\n /**\n * Whether mutual-TLS-bound access tokens are required.\n *\n * @remarks If omitted, clients SHOULD assume this is `false` (client-side behavior).\n * @see {@link https://www.rfc-editor.org/rfc/rfc9728.html#section-2-2.21 | RFC 9728 §2.2.21}\n */\n tlsClientCertificateBoundAccessTokens?: boolean\n }\n }\n\n ping?: {\n /**\n * Whether ping should be enabled by default.\n * - true for SSE or HTTP Stream\n * - false for stdio\n */\n enabled?: boolean\n /**\n * Interval\n * @default 5000 (5s)\n */\n intervalMs?: number\n /**\n * Logging level for ping-related messages.\n * @default 'debug'\n */\n logLevel?: LoggingLevel\n }\n /**\n * Configuration for roots capability\n */\n roots?: {\n /**\n * Whether roots capability should be enabled\n * Set to false to completely disable roots support\n * @default true\n */\n enabled?: boolean\n }\n /**\n * General utilities\n */\n utils?: {\n formatInvalidParamsErrorMessage?: (issues: readonly StandardSchemaV1.Issue[]) => string\n }\n version: `${number}.${number}.${number}`\n}\n\ntype Tool = {\n annotations?: {\n /**\n * When true, the tool leverages incremental content streaming\n * Return void for tools that handle all their output via streaming\n */\n streamingHint?: boolean\n } & ToolAnnotations\n canAccess?: (auth: T) => boolean\n description?: string\n\n execute: (\n args: StandardSchemaV1.InferOutput,\n context: Context,\n ) => Promise<\n AudioContent | ContentResult | ImageContent | ResourceContent | ResourceLink | string | TextContent | void\n >\n name: string\n parameters?: Params\n timeoutMs?: number\n}\n\n/**\n * Tool annotations as defined in MCP Specification (2025-03-26)\n * These provide hints about a tool's behavior.\n */\ntype ToolAnnotations = {\n /**\n * If true, the tool may perform destructive updates\n * Only meaningful when readOnlyHint is false\n * @default true\n */\n destructiveHint?: boolean\n\n /**\n * If true, calling the tool repeatedly with the same arguments has no additional effect\n * Only meaningful when readOnlyHint is false\n * @default false\n */\n idempotentHint?: boolean\n\n /**\n * If true, the tool may interact with an \"open world\" of external entities\n * @default true\n */\n openWorldHint?: boolean\n\n /**\n * If true, indicates the tool does not modify its environment\n * @default false\n */\n readOnlyHint?: boolean\n\n /**\n * A human-readable title for the tool, useful for UI display\n */\n title?: string\n}\n\nconst FastMCPSessionEventEmitterBase: {\n new (): StrictEventEmitter\n} = EventEmitter\n\ntype Authenticate = (request: http.IncomingMessage) => Promise\n\ntype FastMCPSessionAuth = Record | undefined\n\nclass FastMCPSessionEventEmitter extends FastMCPSessionEventEmitterBase {}\n\nexport class FastMCPSession extends FastMCPSessionEventEmitter {\n public get clientCapabilities(): ClientCapabilities | null {\n return this.#clientCapabilities ?? null\n }\n public get isReady(): boolean {\n return this.#connectionState === \"ready\"\n }\n public get loggingLevel(): LoggingLevel {\n return this.#loggingLevel\n }\n public get roots(): Root[] {\n return this.#roots\n }\n public get server(): Server {\n return this.#server\n }\n public get sessionId(): string | undefined {\n return this.#sessionId\n }\n public set sessionId(value: string | undefined) {\n this.#sessionId = value\n }\n #auth: T | undefined\n #capabilities: ServerCapabilities = {}\n #clientCapabilities?: ClientCapabilities\n #connectionState: \"closed\" | \"connecting\" | \"error\" | \"ready\" = \"connecting\"\n #logger: Logger\n #loggingLevel: LoggingLevel = \"info\"\n #needsEventLoopFlush: boolean = false\n #pingConfig?: ServerOptions[\"ping\"]\n\n #pingInterval: null | ReturnType = null\n\n #prompts: Prompt[] = []\n\n #resources: Resource[] = []\n\n #resourceTemplates: ResourceTemplate[] = []\n\n #roots: Root[] = []\n\n #rootsConfig?: ServerOptions[\"roots\"]\n\n #server: Server\n\n /**\n * Session ID from the Mcp-Session-Id header (HTTP transports only).\n * Used to track per-session state across multiple requests.\n */\n #sessionId?: string\n\n #utils?: ServerOptions[\"utils\"]\n\n constructor({\n auth,\n instructions,\n logger,\n name,\n ping,\n prompts,\n resources,\n resourcesTemplates,\n roots,\n sessionId,\n tools,\n transportType,\n utils,\n version,\n }: {\n auth?: T\n instructions?: string\n logger: Logger\n name: string\n ping?: ServerOptions[\"ping\"]\n prompts: Prompt[]\n resources: Resource[]\n resourcesTemplates: InputResourceTemplate[]\n roots?: ServerOptions[\"roots\"]\n sessionId?: string\n tools: Tool[]\n transportType?: \"httpStream\" | \"stdio\"\n utils?: ServerOptions[\"utils\"]\n version: string\n }) {\n super()\n\n this.#auth = auth\n this.#logger = logger\n this.#pingConfig = ping\n this.#rootsConfig = roots\n this.#sessionId = sessionId\n this.#needsEventLoopFlush = transportType === \"httpStream\"\n\n if (tools.length) {\n this.#capabilities.tools = {}\n }\n\n if (resources.length || resourcesTemplates.length) {\n this.#capabilities.resources = {}\n }\n\n if (prompts.length) {\n for (const prompt of prompts) {\n this.addPrompt(prompt)\n }\n\n this.#capabilities.prompts = {}\n }\n\n this.#capabilities.logging = {}\n\n this.#server = new Server(\n { name: name, version: version },\n { capabilities: this.#capabilities, instructions: instructions },\n )\n\n this.#utils = utils\n\n this.setupErrorHandling()\n this.setupLoggingHandlers()\n this.setupRootsHandlers()\n this.setupCompleteHandlers()\n\n if (tools.length) {\n this.setupToolHandlers(tools)\n }\n\n if (resources.length || resourcesTemplates.length) {\n for (const resource of resources) {\n this.addResource(resource)\n }\n\n this.setupResourceHandlers(resources)\n\n if (resourcesTemplates.length) {\n for (const resourceTemplate of resourcesTemplates) {\n this.addResourceTemplate(resourceTemplate)\n }\n\n this.setupResourceTemplateHandlers(resourcesTemplates)\n }\n }\n\n if (prompts.length) {\n this.setupPromptHandlers(prompts)\n }\n }\n\n public async close() {\n this.#connectionState = \"closed\"\n\n if (this.#pingInterval) {\n clearInterval(this.#pingInterval)\n }\n\n try {\n await this.#server.close()\n } catch (error) {\n this.#logger.error(\"[FastMCP error]\", \"could not close server\", error)\n }\n }\n\n public async connect(transport: Transport) {\n if (this.#server.transport) {\n throw new UnexpectedStateError(\"Server is already connected\")\n }\n\n this.#connectionState = \"connecting\"\n\n try {\n await this.#server.connect(transport)\n\n // Extract session ID from transport if available (HTTP transports only)\n if (\"sessionId\" in transport) {\n const transportWithSessionId = transport as {\n sessionId?: string\n } & Transport\n if (typeof transportWithSessionId.sessionId === \"string\") {\n this.#sessionId = transportWithSessionId.sessionId\n }\n }\n\n let attempt = 0\n const maxAttempts = 10\n const retryDelay = 100\n\n while (attempt++ < maxAttempts) {\n const capabilities = this.#server.getClientCapabilities()\n\n if (capabilities) {\n this.#clientCapabilities = capabilities\n break\n }\n\n await delay(retryDelay)\n }\n\n if (!this.#clientCapabilities) {\n this.#logger.warn(\n `[FastMCP warning] could not infer client capabilities after ${maxAttempts} attempts. Connection may be unstable.`,\n )\n }\n\n if (\n this.#rootsConfig?.enabled !== false &&\n this.#clientCapabilities?.roots?.listChanged &&\n typeof this.#server.listRoots === \"function\"\n ) {\n try {\n const roots = await this.#server.listRoots()\n this.#roots = roots?.roots || []\n } catch (e) {\n if (e instanceof McpError && e.code === ErrorCode.MethodNotFound) {\n this.#logger.debug(\"[FastMCP debug] listRoots method not supported by client\")\n } else {\n this.#logger.error(\n `[FastMCP error] received error listing roots.\\n\\n${e instanceof Error ? e.stack : JSON.stringify(e)}`,\n )\n }\n }\n }\n\n if (this.#clientCapabilities) {\n const pingConfig = this.#getPingConfig(transport)\n\n if (pingConfig.enabled) {\n this.#pingInterval = setInterval(async () => {\n try {\n await this.#server.ping()\n } catch {\n // The reason we are not emitting an error here is because some clients\n // seem to not respond to the ping request, and we don't want to crash the server,\n // e.g., https://github.com/punkpeye/fastmcp/issues/38.\n const logLevel = pingConfig.logLevel\n\n if (logLevel === \"debug\") {\n this.#logger.debug(\"[FastMCP debug] server ping failed\")\n } else if (logLevel === \"warning\") {\n this.#logger.warn(\"[FastMCP warning] server is not responding to ping\")\n } else if (logLevel === \"error\") {\n this.#logger.error(\"[FastMCP error] server is not responding to ping\")\n } else {\n this.#logger.info(\"[FastMCP info] server ping failed\")\n }\n }\n }, pingConfig.intervalMs)\n }\n }\n\n // Mark connection as ready and emit event\n this.#connectionState = \"ready\"\n this.emit(\"ready\")\n } catch (error) {\n this.#connectionState = \"error\"\n const errorEvent = {\n error: error instanceof Error ? error : new Error(String(error)),\n }\n this.emit(\"error\", errorEvent)\n throw error\n }\n }\n\n public async requestSampling(\n message: z.infer[\"params\"],\n options?: RequestOptions,\n ): Promise {\n return this.#server.createMessage(message, options)\n }\n\n public waitForReady(): Promise {\n if (this.isReady) {\n return Promise.resolve()\n }\n\n if (this.#connectionState === \"error\" || this.#connectionState === \"closed\") {\n return Promise.reject(new Error(`Connection is in ${this.#connectionState} state`))\n }\n\n return new Promise((resolve, reject) => {\n const timeout = setTimeout(() => {\n reject(new Error(\"Connection timeout: Session failed to become ready within 5 seconds\"))\n }, 5000)\n\n this.once(\"ready\", () => {\n clearTimeout(timeout)\n resolve()\n })\n\n this.once(\"error\", (event) => {\n clearTimeout(timeout)\n reject(event.error)\n })\n })\n }\n\n #getPingConfig(transport: Transport): {\n enabled: boolean\n intervalMs: number\n logLevel: LoggingLevel\n } {\n const pingConfig = this.#pingConfig || {}\n\n let defaultEnabled = false\n\n if (\"type\" in transport) {\n // Enable by default for SSE and HTTP streaming\n if (transport.type === \"httpStream\") {\n defaultEnabled = true\n }\n }\n\n return {\n enabled: pingConfig.enabled !== undefined ? pingConfig.enabled : defaultEnabled,\n intervalMs: pingConfig.intervalMs || 5000,\n logLevel: pingConfig.logLevel || \"debug\",\n }\n }\n\n private addPrompt(inputPrompt: InputPrompt) {\n const completers: Record> = {}\n const enums: Record = {}\n const fuseInstances: Record> = {}\n\n for (const argument of inputPrompt.arguments ?? []) {\n if (argument.complete) {\n completers[argument.name] = argument.complete\n }\n\n if (argument.enum) {\n enums[argument.name] = argument.enum\n fuseInstances[argument.name] = new Fuse(argument.enum, {\n includeScore: true,\n threshold: 0.3, // More flexible matching!\n })\n }\n }\n\n const prompt = {\n ...inputPrompt,\n complete: async (name: string, value: string, auth?: T) => {\n if (completers[name]) {\n return await completers[name](value, auth)\n }\n\n if (fuseInstances[name]) {\n const result = fuseInstances[name].search(value)\n\n return {\n total: result.length,\n values: result.map((item) => item.item),\n }\n }\n\n return {\n values: [],\n }\n },\n }\n\n this.#prompts.push(prompt)\n }\n\n private addResource(inputResource: Resource) {\n this.#resources.push(inputResource)\n }\n\n private addResourceTemplate(inputResourceTemplate: InputResourceTemplate) {\n const completers: Record> = {}\n\n for (const argument of inputResourceTemplate.arguments ?? []) {\n if (argument.complete) {\n completers[argument.name] = argument.complete\n }\n }\n\n const resourceTemplate = {\n ...inputResourceTemplate,\n complete: async (name: string, value: string, auth?: T) => {\n if (completers[name]) {\n return await completers[name](value, auth)\n }\n\n return {\n values: [],\n }\n },\n }\n\n this.#resourceTemplates.push(resourceTemplate)\n }\n\n private setupCompleteHandlers() {\n this.#server.setRequestHandler(CompleteRequestSchema, async (request) => {\n if (request.params.ref.type === \"ref/prompt\") {\n const prompt = this.#prompts.find((prompt) => prompt.name === request.params.ref.name)\n\n if (!prompt) {\n throw new UnexpectedStateError(\"Unknown prompt\", {\n request,\n })\n }\n\n if (!prompt.complete) {\n throw new UnexpectedStateError(\"Prompt does not support completion\", {\n request,\n })\n }\n\n const completion = CompletionZodSchema.parse(\n await prompt.complete(request.params.argument.name, request.params.argument.value, this.#auth),\n )\n\n return {\n completion,\n }\n }\n\n if (request.params.ref.type === \"ref/resource\") {\n const resource = this.#resourceTemplates.find((resource) => resource.uriTemplate === request.params.ref.uri)\n\n if (!resource) {\n throw new UnexpectedStateError(\"Unknown resource\", {\n request,\n })\n }\n\n if (!(\"uriTemplate\" in resource)) {\n throw new UnexpectedStateError(\"Unexpected resource\")\n }\n\n if (!resource.complete) {\n throw new UnexpectedStateError(\"Resource does not support completion\", {\n request,\n })\n }\n\n const completion = CompletionZodSchema.parse(\n await resource.complete(request.params.argument.name, request.params.argument.value, this.#auth),\n )\n\n return {\n completion,\n }\n }\n\n throw new UnexpectedStateError(\"Unexpected completion request\", {\n request,\n })\n })\n }\n\n private setupErrorHandling() {\n this.#server.onerror = (error) => {\n this.#logger.error(\"[FastMCP error]\", error)\n }\n }\n\n private setupLoggingHandlers() {\n this.#server.setRequestHandler(SetLevelRequestSchema, (request) => {\n this.#loggingLevel = request.params.level\n\n return {}\n })\n }\n\n private setupPromptHandlers(prompts: Prompt[]) {\n this.#server.setRequestHandler(ListPromptsRequestSchema, async () => {\n return {\n prompts: prompts.map((prompt) => {\n return {\n arguments: prompt.arguments,\n complete: prompt.complete,\n description: prompt.description,\n name: prompt.name,\n }\n }),\n }\n })\n\n this.#server.setRequestHandler(GetPromptRequestSchema, async (request) => {\n const prompt = prompts.find((prompt) => prompt.name === request.params.name)\n\n if (!prompt) {\n throw new McpError(ErrorCode.MethodNotFound, `Unknown prompt: ${request.params.name}`)\n }\n\n const args = request.params.arguments\n\n for (const arg of prompt.arguments ?? []) {\n if (arg.required && !(args && arg.name in args)) {\n throw new McpError(\n ErrorCode.InvalidRequest,\n `Prompt '${request.params.name}' requires argument '${arg.name}': ${\n arg.description || \"No description provided\"\n }`,\n )\n }\n }\n\n let result: Awaited[\"load\"]>>\n\n try {\n result = await prompt.load(args as Record, this.#auth)\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error)\n throw new McpError(ErrorCode.InternalError, `Failed to load prompt '${request.params.name}': ${errorMessage}`)\n }\n\n if (typeof result === \"string\") {\n return {\n description: prompt.description,\n messages: [\n {\n content: { text: result, type: \"text\" },\n role: \"user\",\n },\n ],\n }\n } else {\n return {\n description: prompt.description,\n messages: result.messages,\n }\n }\n })\n }\n\n private setupResourceHandlers(resources: Resource[]) {\n this.#server.setRequestHandler(ListResourcesRequestSchema, async () => {\n return {\n resources: resources.map((resource) => ({\n description: resource.description,\n mimeType: resource.mimeType,\n name: resource.name,\n uri: resource.uri,\n })),\n } satisfies ListResourcesResult\n })\n\n this.#server.setRequestHandler(ReadResourceRequestSchema, async (request) => {\n if (\"uri\" in request.params) {\n const resource = resources.find((resource) => \"uri\" in resource && resource.uri === request.params.uri)\n\n if (!resource) {\n for (const resourceTemplate of this.#resourceTemplates) {\n const uriTemplate = parseURITemplate(resourceTemplate.uriTemplate)\n\n const match = uriTemplate.fromUri(request.params.uri)\n\n if (!match) {\n continue\n }\n\n const uri = uriTemplate.fill(match)\n\n const result = await resourceTemplate.load(match, this.#auth)\n\n const resources = Array.isArray(result) ? result : [result]\n return {\n contents: resources.map((resource) => ({\n ...resource,\n description: resourceTemplate.description,\n mimeType: resource.mimeType ?? resourceTemplate.mimeType,\n name: resourceTemplate.name,\n uri: resource.uri ?? uri,\n })),\n }\n }\n\n throw new McpError(\n ErrorCode.MethodNotFound,\n `Resource not found: '${request.params.uri}'. Available resources: ${\n resources.map((r) => r.uri).join(\", \") || \"none\"\n }`,\n )\n }\n\n if (!(\"uri\" in resource)) {\n throw new UnexpectedStateError(\"Resource does not support reading\")\n }\n\n let maybeArrayResult: Awaited[\"load\"]>>\n\n try {\n maybeArrayResult = await resource.load(this.#auth)\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error)\n throw new McpError(\n ErrorCode.InternalError,\n `Failed to load resource '${resource.name}' (${resource.uri}): ${errorMessage}`,\n {\n uri: resource.uri,\n },\n )\n }\n\n const resourceResults = Array.isArray(maybeArrayResult) ? maybeArrayResult : [maybeArrayResult]\n\n return {\n contents: resourceResults.map((result) => ({\n ...result,\n mimeType: result.mimeType ?? resource.mimeType,\n name: resource.name,\n uri: result.uri ?? resource.uri,\n })),\n }\n }\n\n throw new UnexpectedStateError(\"Unknown resource request\", {\n request,\n })\n })\n }\n\n private setupResourceTemplateHandlers(resourceTemplates: ResourceTemplate[]) {\n this.#server.setRequestHandler(ListResourceTemplatesRequestSchema, async () => {\n return {\n resourceTemplates: resourceTemplates.map((resourceTemplate) => ({\n description: resourceTemplate.description,\n mimeType: resourceTemplate.mimeType,\n name: resourceTemplate.name,\n uriTemplate: resourceTemplate.uriTemplate,\n })),\n } satisfies ListResourceTemplatesResult\n })\n }\n\n private setupRootsHandlers() {\n if (this.#rootsConfig?.enabled === false) {\n this.#logger.debug(\"[FastMCP debug] roots capability explicitly disabled via config\")\n return\n }\n\n // Only set up roots notification handling if the server supports it\n if (typeof this.#server.listRoots === \"function\") {\n this.#server.setNotificationHandler(RootsListChangedNotificationSchema, () => {\n this.#server\n .listRoots()\n .then((roots) => {\n this.#roots = roots.roots\n\n this.emit(\"rootsChanged\", {\n roots: roots.roots,\n })\n })\n .catch((error) => {\n if (error instanceof McpError && error.code === ErrorCode.MethodNotFound) {\n this.#logger.debug(\"[FastMCP debug] listRoots method not supported by client\")\n } else {\n this.#logger.error(\n `[FastMCP error] received error listing roots.\\n\\n${\n error instanceof Error ? error.stack : JSON.stringify(error)\n }`,\n )\n }\n })\n })\n } else {\n this.#logger.debug(\"[FastMCP debug] roots capability not available, not setting up notification handler\")\n }\n }\n\n private setupToolHandlers(tools: Tool[]) {\n this.#server.setRequestHandler(ListToolsRequestSchema, async () => {\n return {\n tools: await Promise.all(\n tools.map(async (tool) => {\n return {\n annotations: tool.annotations,\n description: tool.description,\n inputSchema: tool.parameters\n ? await toJsonSchema(tool.parameters)\n : {\n additionalProperties: false,\n properties: {},\n type: \"object\",\n }, // More complete schema for Cursor compatibility\n name: tool.name,\n }\n }),\n ),\n }\n })\n\n this.#server.setRequestHandler(CallToolRequestSchema, async (request) => {\n const tool = tools.find((tool) => tool.name === request.params.name)\n\n if (!tool) {\n throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${request.params.name}`)\n }\n\n let args: unknown = undefined\n\n if (tool.parameters) {\n const parsed = await tool.parameters[\"~standard\"].validate(request.params.arguments)\n\n if (parsed.issues) {\n const friendlyErrors = this.#utils?.formatInvalidParamsErrorMessage\n ? this.#utils.formatInvalidParamsErrorMessage(parsed.issues)\n : parsed.issues\n .map((issue) => {\n const path = issue.path?.join(\".\") || \"root\"\n return `${path}: ${issue.message}`\n })\n .join(\", \")\n\n throw new McpError(\n ErrorCode.InvalidParams,\n `Tool '${request.params.name}' parameter validation failed: ${friendlyErrors}. Please check the parameter types and values according to the tool's schema.`,\n )\n }\n\n args = parsed.value\n }\n\n const progressToken = request.params?._meta?.progressToken\n\n let result: ContentResult\n\n try {\n const reportProgress = async (progress: Progress) => {\n try {\n await this.#server.notification({\n method: \"notifications/progress\",\n params: {\n ...progress,\n progressToken,\n },\n })\n\n if (this.#needsEventLoopFlush) {\n await new Promise((resolve) => setImmediate(resolve))\n }\n } catch (progressError) {\n this.#logger.warn(\n `[FastMCP warning] Failed to report progress for tool '${request.params.name}':`,\n progressError instanceof Error ? progressError.message : String(progressError),\n )\n }\n }\n\n const log = {\n debug: (message: string, context?: SerializableValue) => {\n this.#server.sendLoggingMessage({\n data: {\n context,\n message,\n },\n level: \"debug\",\n })\n },\n error: (message: string, context?: SerializableValue) => {\n this.#server.sendLoggingMessage({\n data: {\n context,\n message,\n },\n level: \"error\",\n })\n },\n info: (message: string, context?: SerializableValue) => {\n this.#server.sendLoggingMessage({\n data: {\n context,\n message,\n },\n level: \"info\",\n })\n },\n warn: (message: string, context?: SerializableValue) => {\n this.#server.sendLoggingMessage({\n data: {\n context,\n message,\n },\n level: \"warning\",\n })\n },\n }\n\n // Create a promise for tool execution\n // Streams partial results while a tool is still executing\n // Enables progressive rendering and real-time feedback\n const streamContent = async (content: Content | Content[]) => {\n const contentArray = Array.isArray(content) ? content : [content]\n\n try {\n await this.#server.notification({\n method: \"notifications/tool/streamContent\",\n params: {\n content: contentArray,\n toolName: request.params.name,\n },\n })\n\n if (this.#needsEventLoopFlush) {\n await new Promise((resolve) => setImmediate(resolve))\n }\n } catch (streamError) {\n this.#logger.warn(\n `[FastMCP warning] Failed to stream content for tool '${request.params.name}':`,\n streamError instanceof Error ? streamError.message : String(streamError),\n )\n }\n }\n const executeToolPromise = tool.execute(args, {\n client: {\n version: this.#server.getClientVersion(),\n },\n log,\n reportProgress,\n requestId: typeof request.params?._meta?.requestId === \"string\" ? request.params._meta.requestId : undefined,\n requestMetadata: request.params._meta,\n session: this.#auth,\n sessionId: this.#sessionId,\n streamContent,\n })\n\n // Handle timeout if specified\n const maybeStringResult = (await (tool.timeoutMs\n ? Promise.race([\n executeToolPromise,\n new Promise((_, reject) => {\n const timeoutId = setTimeout(() => {\n reject(\n new McpError(\n ErrorCode.InternalError,\n `Tool '${request.params.name}' timed out after ${tool.timeoutMs}ms. Consider increasing timeoutMs or optimizing the tool implementation.`,\n ),\n )\n }, tool.timeoutMs)\n\n // If promise resolves first\n executeToolPromise.finally(() => clearTimeout(timeoutId))\n }),\n ])\n : executeToolPromise)) as\n | AudioContent\n | ContentResult\n | ImageContent\n | null\n | ResourceContent\n | ResourceLink\n | string\n | TextContent\n | undefined\n\n // Without this test, we are running into situations where the last progress update is not reported.\n // See the 'reports multiple progress updates without buffering' test in FastMCP.test.ts before refactoring.\n await delay(1)\n\n if (maybeStringResult === undefined || maybeStringResult === null) {\n result = ContentResultZodSchema.parse({\n content: [],\n })\n } else if (typeof maybeStringResult === \"string\") {\n result = ContentResultZodSchema.parse({\n content: [{ text: maybeStringResult, type: \"text\" }],\n })\n } else if (\"type\" in maybeStringResult) {\n result = ContentResultZodSchema.parse({\n content: [maybeStringResult],\n })\n } else {\n result = ContentResultZodSchema.parse(maybeStringResult)\n }\n } catch (error) {\n // Re-throw McpError to let the MCP SDK handle it as a proper JSON-RPC error\n // Use type guard to handle instanceof failures across module boundaries\n if (isMcpErrorLike(error)) {\n throw error\n }\n\n if (error instanceof UserError) {\n return {\n content: [{ text: error.message, type: \"text\" }],\n isError: true,\n ...(error.extras ? { structuredContent: error.extras } : {}),\n }\n }\n\n const errorMessage = error instanceof Error ? error.message : String(error)\n return {\n content: [\n {\n text: `Tool '${request.params.name}' execution failed: ${errorMessage}`,\n type: \"text\",\n },\n ],\n isError: true,\n }\n }\n\n return result\n })\n }\n}\n\n/**\n * Converts camelCase to snake_case for OAuth endpoint responses\n */\nfunction camelToSnakeCase(str: string): string {\n return str.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`)\n}\n\n/**\n * Converts an object with camelCase keys to snake_case keys\n */\nfunction convertObjectToSnakeCase(obj: Record): Record {\n const result: Record = {}\n\n for (const [key, value] of Object.entries(obj)) {\n const snakeKey = camelToSnakeCase(key)\n result[snakeKey] = value\n }\n\n return result\n}\n\nconst FastMCPEventEmitterBase: {\n new (): StrictEventEmitter>\n} = EventEmitter\n\nclass FastMCPEventEmitter extends FastMCPEventEmitterBase {}\n\nexport class FastMCP extends FastMCPEventEmitter {\n public get sessions(): FastMCPSession[] {\n return this.#sessions\n }\n #authenticate: Authenticate | undefined\n #httpStreamServer: null | SSEServer = null\n #logger: Logger\n #options: ServerOptions\n #prompts: InputPrompt[] = []\n #resources: Resource[] = []\n #resourcesTemplates: InputResourceTemplate[] = []\n #sessions: FastMCPSession[] = []\n\n #tools: Tool[] = []\n\n constructor(public options: ServerOptions) {\n super()\n\n this.#options = options\n this.#authenticate = options.authenticate\n this.#logger = options.logger || console\n }\n\n /**\n * Adds a prompt to the server.\n */\n public addPrompt[]>(prompt: InputPrompt) {\n this.#prompts.push(prompt)\n }\n\n /**\n * Adds a resource to the server.\n */\n public addResource(resource: Resource) {\n this.#resources.push(resource)\n }\n\n /**\n * Adds a resource template to the server.\n */\n public addResourceTemplate(\n resource: InputResourceTemplate,\n ) {\n this.#resourcesTemplates.push(resource)\n }\n\n /**\n * Adds a tool to the server.\n */\n public addTool(tool: Tool) {\n this.#tools.push(tool as unknown as Tool)\n }\n\n /**\n * Embeds a resource by URI, making it easy to include resources in tool responses.\n *\n * @param uri - The URI of the resource to embed\n * @returns Promise - The embedded resource content\n */\n public async embedded(uri: string): Promise {\n // First, try to find a direct resource match\n const directResource = this.#resources.find((resource) => resource.uri === uri)\n\n if (directResource) {\n const result = await directResource.load()\n const results = Array.isArray(result) ? result : [result]\n const firstResult = results[0]\n\n const resourceData: ResourceContent[\"resource\"] = {\n mimeType: directResource.mimeType,\n uri,\n }\n\n if (\"text\" in firstResult) {\n resourceData.text = firstResult.text\n }\n\n if (\"blob\" in firstResult) {\n resourceData.blob = firstResult.blob\n }\n\n return resourceData\n }\n\n // Try to match against resource templates\n for (const template of this.#resourcesTemplates) {\n const parsedTemplate = parseURITemplate(template.uriTemplate)\n const params = parsedTemplate.fromUri(uri)\n if (!params) {\n continue\n }\n\n const result = await template.load(params as ResourceTemplateArgumentsToObject)\n\n const resourceData: ResourceContent[\"resource\"] = {\n mimeType: template.mimeType,\n uri,\n }\n\n if (\"text\" in result) {\n resourceData.text = result.text\n }\n\n if (\"blob\" in result) {\n resourceData.blob = result.blob\n }\n\n return resourceData // The resource we're looking for\n }\n\n throw new UnexpectedStateError(`Resource not found: ${uri}`, { uri })\n }\n\n /**\n * Starts the server.\n */\n public async start(\n options?: Partial<{\n httpStream: {\n enableJsonResponse?: boolean\n endpoint?: `/${string}`\n eventStore?: EventStore\n host?: string\n port: number\n stateless?: boolean\n }\n transportType: \"httpStream\" | \"stdio\"\n }>,\n ) {\n const config = this.#parseRuntimeConfig(options)\n\n if (config.transportType === \"stdio\") {\n const transport = new StdioServerTransport()\n\n // For stdio transport, if authenticate function is provided, call it\n // with undefined request (since stdio doesn't have HTTP request context)\n let auth: T | undefined\n\n if (this.#authenticate) {\n try {\n auth = await this.#authenticate(undefined as unknown as http.IncomingMessage)\n } catch (error) {\n this.#logger.error(\n \"[FastMCP error] Authentication failed for stdio transport:\",\n error instanceof Error ? error.message : String(error),\n )\n // Continue without auth if authentication fails\n }\n }\n\n const session = new FastMCPSession({\n auth,\n instructions: this.#options.instructions,\n logger: this.#logger,\n name: this.#options.name,\n ping: this.#options.ping,\n prompts: this.#prompts,\n resources: this.#resources,\n resourcesTemplates: this.#resourcesTemplates,\n roots: this.#options.roots,\n tools: this.#tools,\n transportType: \"stdio\",\n utils: this.#options.utils,\n version: this.#options.version,\n })\n\n await session.connect(transport)\n\n this.#sessions.push(session)\n\n session.once(\"error\", () => {\n this.#removeSession(session)\n })\n\n // Monitor the underlying transport for close events\n if (transport.onclose) {\n const originalOnClose = transport.onclose\n\n transport.onclose = () => {\n this.#removeSession(session)\n\n if (originalOnClose) {\n originalOnClose()\n }\n }\n } else {\n transport.onclose = () => {\n this.#removeSession(session)\n }\n }\n\n this.emit(\"connect\", {\n session: session as FastMCPSession,\n })\n } else if (config.transportType === \"httpStream\") {\n const httpConfig = config.httpStream\n\n if (httpConfig.stateless) {\n // Stateless mode - create new server instance for each request\n this.#logger.info(\n `[FastMCP info] Starting server in stateless mode on HTTP Stream at http://${httpConfig.host}:${httpConfig.port}${httpConfig.endpoint}`,\n )\n\n this.#httpStreamServer = await startHTTPServer>({\n ...(this.#authenticate ? { authenticate: this.#authenticate } : {}),\n createServer: async (request) => {\n let auth: T | undefined\n\n if (this.#authenticate) {\n auth = await this.#authenticate(request)\n\n // In stateless mode, authentication is REQUIRED\n // mcp-proxy will catch this error and return 401\n if (auth === undefined || auth === null) {\n throw new Error(\"Authentication required\")\n }\n }\n\n // Extract session ID from headers\n const sessionId = Array.isArray(request.headers[\"mcp-session-id\"])\n ? request.headers[\"mcp-session-id\"][0]\n : request.headers[\"mcp-session-id\"]\n\n // In stateless mode, create a new session for each request\n // without persisting it in the sessions array\n return this.#createSession(auth, sessionId)\n },\n enableJsonResponse: httpConfig.enableJsonResponse,\n eventStore: httpConfig.eventStore,\n host: httpConfig.host,\n // In stateless mode, we don't track sessions\n onClose: async () => {\n // No session tracking in stateless mode\n },\n onConnect: async () => {\n // No persistent session tracking in stateless mode\n this.#logger.debug(`[FastMCP debug] Stateless HTTP Stream request handled`)\n },\n onUnhandledRequest: async (req, res) => {\n await this.#handleUnhandledRequest(req, res, true, httpConfig.host)\n },\n port: httpConfig.port,\n stateless: true,\n streamEndpoint: httpConfig.endpoint,\n })\n } else {\n // Regular mode with session management\n this.#httpStreamServer = await startHTTPServer>({\n ...(this.#authenticate ? { authenticate: this.#authenticate } : {}),\n createServer: async (request) => {\n let auth: T | undefined\n\n if (this.#authenticate) {\n auth = await this.#authenticate(request)\n }\n\n // Extract session ID from headers\n const sessionId = Array.isArray(request.headers[\"mcp-session-id\"])\n ? request.headers[\"mcp-session-id\"][0]\n : request.headers[\"mcp-session-id\"]\n\n return this.#createSession(auth, sessionId)\n },\n enableJsonResponse: httpConfig.enableJsonResponse,\n eventStore: httpConfig.eventStore,\n host: httpConfig.host,\n onClose: async (session) => {\n const sessionIndex = this.#sessions.indexOf(session)\n\n if (sessionIndex !== -1) this.#sessions.splice(sessionIndex, 1)\n\n this.emit(\"disconnect\", {\n session: session as FastMCPSession,\n })\n },\n onConnect: async (session) => {\n this.#sessions.push(session)\n\n this.#logger.info(`[FastMCP info] HTTP Stream session established`)\n\n this.emit(\"connect\", {\n session: session as FastMCPSession,\n })\n },\n\n onUnhandledRequest: async (req, res) => {\n await this.#handleUnhandledRequest(req, res, false, httpConfig.host)\n },\n port: httpConfig.port,\n stateless: httpConfig.stateless,\n streamEndpoint: httpConfig.endpoint,\n })\n\n this.#logger.info(\n `[FastMCP info] server is running on HTTP Stream at http://${httpConfig.host}:${httpConfig.port}${httpConfig.endpoint}`,\n )\n }\n } else {\n throw new Error(\"Invalid transport type\")\n }\n }\n\n /**\n * Stops the server.\n */\n public async stop() {\n if (this.#httpStreamServer) {\n await this.#httpStreamServer.close()\n }\n }\n\n /**\n * Creates a new FastMCPSession instance with the current configuration.\n * Used both for regular sessions and stateless requests.\n */\n #createSession(auth?: T, sessionId?: string): FastMCPSession {\n // Check if authentication failed\n if (\n auth &&\n typeof auth === \"object\" &&\n \"authenticated\" in auth &&\n !(auth as { authenticated: unknown }).authenticated\n ) {\n const errorMessage =\n \"error\" in auth && typeof (auth as { error: unknown }).error === \"string\"\n ? (auth as { error: string }).error\n : \"Authentication failed\"\n throw new Error(errorMessage)\n }\n\n const allowedTools = auth\n ? this.#tools.filter((tool) => (tool.canAccess ? tool.canAccess(auth) : true))\n : this.#tools\n return new FastMCPSession({\n auth,\n instructions: this.#options.instructions,\n logger: this.#logger,\n name: this.#options.name,\n ping: this.#options.ping,\n prompts: this.#prompts,\n resources: this.#resources,\n resourcesTemplates: this.#resourcesTemplates,\n roots: this.#options.roots,\n sessionId,\n tools: allowedTools,\n transportType: \"httpStream\",\n utils: this.#options.utils,\n version: this.#options.version,\n })\n }\n\n /**\n * Handles unhandled HTTP requests with health, readiness, and OAuth endpoints\n */\n #handleUnhandledRequest = async (\n req: http.IncomingMessage,\n res: http.ServerResponse,\n isStateless = false,\n host: string,\n ) => {\n const healthConfig = this.#options.health ?? {}\n\n const enabled = healthConfig.enabled === undefined ? true : healthConfig.enabled\n\n if (enabled) {\n const path = healthConfig.path ?? \"/health\"\n const url = new URL(req.url || \"\", `http://${host}`)\n\n try {\n if (req.method === \"GET\" && url.pathname === path) {\n res\n .writeHead(healthConfig.status ?? 200, {\n \"Content-Type\": \"text/plain\",\n })\n .end(healthConfig.message ?? \"✓ Ok\")\n\n return\n }\n\n // Enhanced readiness check endpoint\n if (req.method === \"GET\" && url.pathname === \"/ready\") {\n if (isStateless) {\n // In stateless mode, we're always ready if the server is running\n const response = {\n mode: \"stateless\",\n ready: 1,\n status: \"ready\",\n total: 1,\n }\n\n res\n .writeHead(200, {\n \"Content-Type\": \"application/json\",\n })\n .end(JSON.stringify(response))\n } else {\n const readySessions = this.#sessions.filter((s) => s.isReady).length\n const totalSessions = this.#sessions.length\n const allReady = readySessions === totalSessions && totalSessions > 0\n\n const response = {\n ready: readySessions,\n status: allReady ? \"ready\" : totalSessions === 0 ? \"no_sessions\" : \"initializing\",\n total: totalSessions,\n }\n\n res\n .writeHead(allReady ? 200 : 503, {\n \"Content-Type\": \"application/json\",\n })\n .end(JSON.stringify(response))\n }\n\n return\n }\n } catch (error) {\n this.#logger.error(\"[FastMCP error] health endpoint error\", error)\n }\n }\n\n // Handle OAuth well-known endpoints\n const oauthConfig = this.#options.oauth\n if (oauthConfig?.enabled && req.method === \"GET\") {\n const url = new URL(req.url || \"\", `http://${host}`)\n\n if (url.pathname === \"/.well-known/oauth-authorization-server\" && oauthConfig.authorizationServer) {\n const metadata = convertObjectToSnakeCase(oauthConfig.authorizationServer)\n res\n .writeHead(200, {\n \"Content-Type\": \"application/json\",\n })\n .end(JSON.stringify(metadata))\n return\n }\n\n if (url.pathname === \"/.well-known/oauth-protected-resource\" && oauthConfig.protectedResource) {\n const metadata = convertObjectToSnakeCase(oauthConfig.protectedResource)\n res\n .writeHead(200, {\n \"Content-Type\": \"application/json\",\n })\n .end(JSON.stringify(metadata))\n return\n }\n }\n\n // If the request was not handled above, return 404\n res.writeHead(404).end()\n }\n\n #parseRuntimeConfig(\n overrides?: Partial<{\n httpStream: {\n enableJsonResponse?: boolean\n endpoint?: `/${string}`\n host?: string\n port: number\n stateless?: boolean\n }\n transportType: \"httpStream\" | \"stdio\"\n }>,\n ):\n | {\n httpStream: {\n enableJsonResponse?: boolean\n endpoint: `/${string}`\n eventStore?: EventStore\n host: string\n port: number\n stateless?: boolean\n }\n transportType: \"httpStream\"\n }\n | { transportType: \"stdio\" } {\n const args = process.argv.slice(2)\n const getArg = (name: string) => {\n const index = args.findIndex((arg) => arg === `--${name}`)\n\n return index !== -1 && index + 1 < args.length ? args[index + 1] : undefined\n }\n\n const transportArg = getArg(\"transport\")\n const portArg = getArg(\"port\")\n const endpointArg = getArg(\"endpoint\")\n const statelessArg = getArg(\"stateless\")\n const hostArg = getArg(\"host\")\n\n const envTransport = process.env.FASTMCP_TRANSPORT\n const envPort = process.env.FASTMCP_PORT\n const envEndpoint = process.env.FASTMCP_ENDPOINT\n const envStateless = process.env.FASTMCP_STATELESS\n const envHost = process.env.FASTMCP_HOST\n // Overrides > CLI > env > defaults\n const transportType =\n overrides?.transportType ||\n (transportArg === \"http-stream\" ? \"httpStream\" : transportArg) ||\n envTransport ||\n \"stdio\"\n\n if (transportType === \"httpStream\") {\n const port = parseInt(overrides?.httpStream?.port?.toString() || portArg || envPort || \"8080\")\n const host = overrides?.httpStream?.host || hostArg || envHost || \"localhost\"\n const endpoint = overrides?.httpStream?.endpoint || endpointArg || envEndpoint || \"/mcp\"\n const enableJsonResponse = overrides?.httpStream?.enableJsonResponse || false\n const stateless = overrides?.httpStream?.stateless || statelessArg === \"true\" || envStateless === \"true\" || false\n\n return {\n httpStream: {\n enableJsonResponse,\n endpoint: endpoint as `/${string}`,\n host,\n port,\n stateless,\n },\n transportType: \"httpStream\" as const,\n }\n }\n\n return { transportType: \"stdio\" as const }\n }\n\n #removeSession(session: FastMCPSession): void {\n const sessionIndex = this.#sessions.indexOf(session)\n\n if (sessionIndex !== -1) {\n this.#sessions.splice(sessionIndex, 1)\n this.emit(\"disconnect\", {\n session: session as FastMCPSession,\n })\n }\n }\n}\n\nexport { ErrorCode, McpError } from \"@modelcontextprotocol/sdk/types.js\"\n\nexport type {\n AudioContent,\n Content,\n ContentResult,\n Context,\n FastMCPEvents,\n FastMCPSessionEvents,\n ImageContent,\n InputPrompt,\n InputPromptArgument,\n LoggingLevel,\n Progress,\n Prompt,\n PromptArgument,\n RequestMeta,\n Resource,\n ResourceContent,\n ResourceLink,\n ResourceResult,\n ResourceTemplate,\n ResourceTemplateArgument,\n SerializableValue,\n ServerOptions,\n TextContent,\n Tool,\n ToolParameters,\n}\n"],"mappings":";AAAA,SAAS,cAAc;AACvB,SAAS,4BAA4B;AAIrC;AAAA,EACE;AAAA,EAEA;AAAA,EAEA;AAAA,EACA;AAAA,EAEA;AAAA,EACA;AAAA,EAEA;AAAA,EAEA;AAAA,EACA;AAAA,EACA;AAAA,EAIA;AAAA,EAEA;AAAA,OACK;AAEP,SAAS,oBAAoB;AAC7B,SAAS,gBAAgB;AACzB,OAAO,UAAU;AAEjB,SAAS,uBAAuB;AAEhC,SAAS,cAAc,aAAa;AACpC,SAAS,aAAa;AACtB,OAAO,sBAAsB;AAC7B,SAAS,oBAAoB;AAC7B,SAAS,SAAS;AAkxElB,SAAS,aAAAA,YAAW,YAAAC,iBAAgB;AAzvE7B,IAAM,eAAe,OAC1B,UAC0B;AAC1B,MAAI;AAEJ,MAAI;AACF,QAAI,SAAS,OAAO;AAClB,UAAI;AACF,cAAM,WAAW,MAAM,MAAM,MAAM,GAAG;AAEtC,YAAI,CAAC,SAAS,IAAI;AAChB,gBAAM,IAAI,MAAM,iCAAiC,SAAS,MAAM,MAAM,SAAS,UAAU,EAAE;AAAA,QAC7F;AAEA,kBAAU,OAAO,KAAK,MAAM,SAAS,YAAY,CAAC;AAAA,MACpD,SAAS,OAAO;AACd,cAAM,IAAI;AAAA,UACR,mCAAmC,MAAM,GAAG,MAAM,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,CAAC;AAAA,QAC1G;AAAA,MACF;AAAA,IACF,WAAW,UAAU,OAAO;AAC1B,UAAI;AACF,kBAAU,MAAM,SAAS,MAAM,IAAI;AAAA,MACrC,SAAS,OAAO;AACd,cAAM,IAAI;AAAA,UACR,mCAAmC,MAAM,IAAI,MAAM,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,CAAC;AAAA,QAC3G;AAAA,MACF;AAAA,IACF,WAAW,YAAY,OAAO;AAC5B,gBAAU,MAAM;AAAA,IAClB,OAAO;AACL,YAAM,IAAI,MAAM,2DAA2D;AAAA,IAC7E;AAEA,UAAM,EAAE,mBAAmB,IAAI,MAAM,OAAO,WAAW;AACvD,UAAM,WAAW,MAAM,mBAAmB,OAAO;AAEjD,QAAI,CAAC,YAAY,CAAC,SAAS,KAAK,WAAW,QAAQ,GAAG;AACpD,cAAQ,KAAK,6DAA6D,UAAU,QAAQ,SAAS,EAAE;AAAA,IACzG;AAEA,UAAM,aAAa,QAAQ,SAAS,QAAQ;AAE5C,WAAO;AAAA,MACL,MAAM;AAAA,MACN,UAAU,UAAU,QAAQ;AAAA,MAC5B,MAAM;AAAA,IACR;AAAA,EACF,SAAS,OAAO;AACd,QAAI,iBAAiB,OAAO;AAC1B,YAAM;AAAA,IACR,OAAO;AACL,YAAM,IAAI,MAAM,sCAAsC,OAAO,KAAK,CAAC,EAAE;AAAA,IACvE;AAAA,EACF;AACF;AAEO,IAAM,eAAe,OAC1B,UAC0B;AAC1B,MAAI;AAEJ,MAAI;AACF,QAAI,SAAS,OAAO;AAClB,UAAI;AACF,cAAM,WAAW,MAAM,MAAM,MAAM,GAAG;AAEtC,YAAI,CAAC,SAAS,IAAI;AAChB,gBAAM,IAAI,MAAM,iCAAiC,SAAS,MAAM,MAAM,SAAS,UAAU,EAAE;AAAA,QAC7F;AAEA,kBAAU,OAAO,KAAK,MAAM,SAAS,YAAY,CAAC;AAAA,MACpD,SAAS,OAAO;AACd,cAAM,IAAI;AAAA,UACR,mCAAmC,MAAM,GAAG,MAAM,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,CAAC;AAAA,QAC1G;AAAA,MACF;AAAA,IACF,WAAW,UAAU,OAAO;AAC1B,UAAI;AACF,kBAAU,MAAM,SAAS,MAAM,IAAI;AAAA,MACrC,SAAS,OAAO;AACd,cAAM,IAAI;AAAA,UACR,mCAAmC,MAAM,IAAI,MAAM,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,CAAC;AAAA,QAC3G;AAAA,MACF;AAAA,IACF,WAAW,YAAY,OAAO;AAC5B,gBAAU,MAAM;AAAA,IAClB,OAAO;AACL,YAAM,IAAI,MAAM,2DAA2D;AAAA,IAC7E;AAEA,UAAM,EAAE,mBAAmB,IAAI,MAAM,OAAO,WAAW;AACvD,UAAM,WAAW,MAAM,mBAAmB,OAAO;AAEjD,QAAI,CAAC,YAAY,CAAC,SAAS,KAAK,WAAW,QAAQ,GAAG;AACpD,cAAQ,KAAK,kEAAkE,UAAU,QAAQ,SAAS,EAAE;AAAA,IAC9G;AAEA,UAAM,aAAa,QAAQ,SAAS,QAAQ;AAE5C,WAAO;AAAA,MACL,MAAM;AAAA,MACN,UAAU,UAAU,QAAQ;AAAA,MAC5B,MAAM;AAAA,IACR;AAAA,EACF,SAAS,OAAO;AACd,QAAI,iBAAiB,OAAO;AAC1B,YAAM;AAAA,IACR,OAAO;AACL,YAAM,IAAI,MAAM,sCAAsC,OAAO,KAAK,CAAC,EAAE;AAAA,IACvE;AAAA,EACF;AACF;AAwDA,IAAe,eAAf,cAAoC,MAAM;AAAA,EACjC,YAAY,SAAkB;AACnC,UAAM,OAAO;AACb,SAAK,OAAO,WAAW;AAAA,EACzB;AACF;AAcO,IAAM,iBAAN,cAA6B,SAAS;AAAA,EAClC,eAAe;AAAA,EAExB,YAAY,MAAc,SAAiB,MAAgB;AACzD,UAAM,MAAM,SAAS,IAAI;AAAA,EAC3B;AACF;AAEO,IAAM,uBAAN,cAAmC,aAAa;AAAA,EAC9C;AAAA,EAEA,YAAY,SAAiB,QAAiB;AACnD,UAAM,OAAO;AACb,SAAK,OAAO,WAAW;AACvB,SAAK,SAAS;AAAA,EAChB;AACF;AAKO,IAAM,YAAN,cAAwB,qBAAqB;AAAC;AAS9C,SAAS,eAAe,OAAmC;AAChE,SACE,iBAAiB,YAChB,OAAO,UAAU,YAChB,UAAU,QACV,kBAAkB,SACjB,MAAqC,iBAAiB;AAE7D;AAEA,IAAM,uBAAuB,EAC1B,OAAO;AAAA;AAAA;AAAA;AAAA,EAIN,MAAM,EAAE,OAAO;AAAA,EACf,MAAM,EAAE,QAAQ,MAAM;AACxB,CAAC,EACA,OAAO;AAQV,IAAM,wBAAwB,EAC3B,OAAO;AAAA;AAAA;AAAA;AAAA,EAIN,MAAM,EAAE,OAAO,EAAE,OAAO;AAAA;AAAA;AAAA;AAAA,EAIxB,UAAU,EAAE,OAAO;AAAA,EACnB,MAAM,EAAE,QAAQ,OAAO;AACzB,CAAC,EACA,OAAO;AAQV,IAAM,wBAAwB,EAC3B,OAAO;AAAA;AAAA;AAAA;AAAA,EAIN,MAAM,EAAE,OAAO,EAAE,OAAO;AAAA,EACxB,UAAU,EAAE,OAAO;AAAA,EACnB,MAAM,EAAE,QAAQ,OAAO;AACzB,CAAC,EACA,OAAO;AAYV,IAAM,2BAA2B,EAC9B,OAAO;AAAA,EACN,UAAU,EAAE,OAAO;AAAA,IACjB,MAAM,EAAE,OAAO,EAAE,SAAS;AAAA,IAC1B,UAAU,EAAE,OAAO,EAAE,SAAS;AAAA,IAC9B,MAAM,EAAE,OAAO,EAAE,SAAS;AAAA,IAC1B,KAAK,EAAE,OAAO;AAAA,EAChB,CAAC;AAAA,EACD,MAAM,EAAE,QAAQ,UAAU;AAC5B,CAAC,EACA,OAAO;AAEV,IAAM,wBAAwB,EAAE,OAAO;AAAA,EACrC,aAAa,EAAE,OAAO,EAAE,SAAS;AAAA,EACjC,UAAU,EAAE,OAAO,EAAE,SAAS;AAAA,EAC9B,MAAM,EAAE,OAAO;AAAA,EACf,OAAO,EAAE,OAAO,EAAE,SAAS;AAAA,EAC3B,MAAM,EAAE,QAAQ,eAAe;AAAA,EAC/B,KAAK,EAAE,OAAO;AAChB,CAAC;AAID,IAAM,mBAAmB,EAAE,mBAAmB,QAAQ;AAAA,EACpD;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,CAAC;AAQD,IAAM,yBAAyB,EAC5B,OAAO;AAAA,EACN,OAAO,EAAE,OAAO,EAAE,QAAQ,CAAC,EAAE,SAAS;AAAA,EACtC,SAAS,iBAAiB,MAAM;AAAA,EAChC,SAAS,EAAE,QAAQ,EAAE,SAAS;AAChC,CAAC,EACA,OAAO;AAWV,IAAM,sBAAsB,EAAE,OAAO;AAAA;AAAA;AAAA;AAAA,EAInC,SAAS,EAAE,SAAS,EAAE,QAAQ,CAAC;AAAA;AAAA;AAAA;AAAA,EAI/B,OAAO,EAAE,SAAS,EAAE,OAAO,EAAE,IAAI,CAAC;AAAA;AAAA;AAAA;AAAA,EAIlC,QAAQ,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,IAAI,GAAG;AACrC,CAAC;AA8dD,IAAM,iCAEF;AAMJ,IAAM,6BAAN,cAAyC,+BAA+B;AAAC;AAElE,IAAM,iBAAN,cAAgF,2BAA2B;AAAA,EAChH,IAAW,qBAAgD;AACzD,WAAO,KAAK,uBAAuB;AAAA,EACrC;AAAA,EACA,IAAW,UAAmB;AAC5B,WAAO,KAAK,qBAAqB;AAAA,EACnC;AAAA,EACA,IAAW,eAA6B;AACtC,WAAO,KAAK;AAAA,EACd;AAAA,EACA,IAAW,QAAgB;AACzB,WAAO,KAAK;AAAA,EACd;AAAA,EACA,IAAW,SAAiB;AAC1B,WAAO,KAAK;AAAA,EACd;AAAA,EACA,IAAW,YAAgC;AACzC,WAAO,KAAK;AAAA,EACd;AAAA,EACA,IAAW,UAAU,OAA2B;AAC9C,SAAK,aAAa;AAAA,EACpB;AAAA,EACA;AAAA,EACA,gBAAoC,CAAC;AAAA,EACrC;AAAA,EACA,mBAAgE;AAAA,EAChE;AAAA,EACA,gBAA8B;AAAA,EAC9B,uBAAgC;AAAA,EAChC;AAAA,EAEA,gBAAuD;AAAA,EAEvD,WAAwB,CAAC;AAAA,EAEzB,aAA4B,CAAC;AAAA,EAE7B,qBAA4C,CAAC;AAAA,EAE7C,SAAiB,CAAC;AAAA,EAElB;AAAA,EAEA;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA;AAAA,EAEA;AAAA,EAEA,YAAY;AAAA,IACV;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF,GAeG;AACD,UAAM;AAEN,SAAK,QAAQ;AACb,SAAK,UAAU;AACf,SAAK,cAAc;AACnB,SAAK,eAAe;AACpB,SAAK,aAAa;AAClB,SAAK,uBAAuB,kBAAkB;AAE9C,QAAI,MAAM,QAAQ;AAChB,WAAK,cAAc,QAAQ,CAAC;AAAA,IAC9B;AAEA,QAAI,UAAU,UAAU,mBAAmB,QAAQ;AACjD,WAAK,cAAc,YAAY,CAAC;AAAA,IAClC;AAEA,QAAI,QAAQ,QAAQ;AAClB,iBAAW,UAAU,SAAS;AAC5B,aAAK,UAAU,MAAM;AAAA,MACvB;AAEA,WAAK,cAAc,UAAU,CAAC;AAAA,IAChC;AAEA,SAAK,cAAc,UAAU,CAAC;AAE9B,SAAK,UAAU,IAAI;AAAA,MACjB,EAAE,MAAY,QAAiB;AAAA,MAC/B,EAAE,cAAc,KAAK,eAAe,aAA2B;AAAA,IACjE;AAEA,SAAK,SAAS;AAEd,SAAK,mBAAmB;AACxB,SAAK,qBAAqB;AAC1B,SAAK,mBAAmB;AACxB,SAAK,sBAAsB;AAE3B,QAAI,MAAM,QAAQ;AAChB,WAAK,kBAAkB,KAAK;AAAA,IAC9B;AAEA,QAAI,UAAU,UAAU,mBAAmB,QAAQ;AACjD,iBAAW,YAAY,WAAW;AAChC,aAAK,YAAY,QAAQ;AAAA,MAC3B;AAEA,WAAK,sBAAsB,SAAS;AAEpC,UAAI,mBAAmB,QAAQ;AAC7B,mBAAW,oBAAoB,oBAAoB;AACjD,eAAK,oBAAoB,gBAAgB;AAAA,QAC3C;AAEA,aAAK,8BAA8B,kBAAkB;AAAA,MACvD;AAAA,IACF;AAEA,QAAI,QAAQ,QAAQ;AAClB,WAAK,oBAAoB,OAAO;AAAA,IAClC;AAAA,EACF;AAAA,EAEA,MAAa,QAAQ;AACnB,SAAK,mBAAmB;AAExB,QAAI,KAAK,eAAe;AACtB,oBAAc,KAAK,aAAa;AAAA,IAClC;AAEA,QAAI;AACF,YAAM,KAAK,QAAQ,MAAM;AAAA,IAC3B,SAAS,OAAO;AACd,WAAK,QAAQ,MAAM,mBAAmB,0BAA0B,KAAK;AAAA,IACvE;AAAA,EACF;AAAA,EAEA,MAAa,QAAQ,WAAsB;AACzC,QAAI,KAAK,QAAQ,WAAW;AAC1B,YAAM,IAAI,qBAAqB,6BAA6B;AAAA,IAC9D;AAEA,SAAK,mBAAmB;AAExB,QAAI;AACF,YAAM,KAAK,QAAQ,QAAQ,SAAS;AAGpC,UAAI,eAAe,WAAW;AAC5B,cAAM,yBAAyB;AAG/B,YAAI,OAAO,uBAAuB,cAAc,UAAU;AACxD,eAAK,aAAa,uBAAuB;AAAA,QAC3C;AAAA,MACF;AAEA,UAAI,UAAU;AACd,YAAM,cAAc;AACpB,YAAM,aAAa;AAEnB,aAAO,YAAY,aAAa;AAC9B,cAAM,eAAe,KAAK,QAAQ,sBAAsB;AAExD,YAAI,cAAc;AAChB,eAAK,sBAAsB;AAC3B;AAAA,QACF;AAEA,cAAM,MAAM,UAAU;AAAA,MACxB;AAEA,UAAI,CAAC,KAAK,qBAAqB;AAC7B,aAAK,QAAQ;AAAA,UACX,+DAA+D,WAAW;AAAA,QAC5E;AAAA,MACF;AAEA,UACE,KAAK,cAAc,YAAY,SAC/B,KAAK,qBAAqB,OAAO,eACjC,OAAO,KAAK,QAAQ,cAAc,YAClC;AACA,YAAI;AACF,gBAAM,QAAQ,MAAM,KAAK,QAAQ,UAAU;AAC3C,eAAK,SAAS,OAAO,SAAS,CAAC;AAAA,QACjC,SAAS,GAAG;AACV,cAAI,aAAa,YAAY,EAAE,SAAS,UAAU,gBAAgB;AAChE,iBAAK,QAAQ,MAAM,0DAA0D;AAAA,UAC/E,OAAO;AACL,iBAAK,QAAQ;AAAA,cACX;AAAA;AAAA,EAAoD,aAAa,QAAQ,EAAE,QAAQ,KAAK,UAAU,CAAC,CAAC;AAAA,YACtG;AAAA,UACF;AAAA,QACF;AAAA,MACF;AAEA,UAAI,KAAK,qBAAqB;AAC5B,cAAM,aAAa,KAAK,eAAe,SAAS;AAEhD,YAAI,WAAW,SAAS;AACtB,eAAK,gBAAgB,YAAY,YAAY;AAC3C,gBAAI;AACF,oBAAM,KAAK,QAAQ,KAAK;AAAA,YAC1B,QAAQ;AAIN,oBAAM,WAAW,WAAW;AAE5B,kBAAI,aAAa,SAAS;AACxB,qBAAK,QAAQ,MAAM,oCAAoC;AAAA,cACzD,WAAW,aAAa,WAAW;AACjC,qBAAK,QAAQ,KAAK,oDAAoD;AAAA,cACxE,WAAW,aAAa,SAAS;AAC/B,qBAAK,QAAQ,MAAM,kDAAkD;AAAA,cACvE,OAAO;AACL,qBAAK,QAAQ,KAAK,mCAAmC;AAAA,cACvD;AAAA,YACF;AAAA,UACF,GAAG,WAAW,UAAU;AAAA,QAC1B;AAAA,MACF;AAGA,WAAK,mBAAmB;AACxB,WAAK,KAAK,OAAO;AAAA,IACnB,SAAS,OAAO;AACd,WAAK,mBAAmB;AACxB,YAAM,aAAa;AAAA,QACjB,OAAO,iBAAiB,QAAQ,QAAQ,IAAI,MAAM,OAAO,KAAK,CAAC;AAAA,MACjE;AACA,WAAK,KAAK,SAAS,UAAU;AAC7B,YAAM;AAAA,IACR;AAAA,EACF;AAAA,EAEA,MAAa,gBACX,SACA,SAC2B;AAC3B,WAAO,KAAK,QAAQ,cAAc,SAAS,OAAO;AAAA,EACpD;AAAA,EAEO,eAA8B;AACnC,QAAI,KAAK,SAAS;AAChB,aAAO,QAAQ,QAAQ;AAAA,IACzB;AAEA,QAAI,KAAK,qBAAqB,WAAW,KAAK,qBAAqB,UAAU;AAC3E,aAAO,QAAQ,OAAO,IAAI,MAAM,oBAAoB,KAAK,gBAAgB,QAAQ,CAAC;AAAA,IACpF;AAEA,WAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AACtC,YAAM,UAAU,WAAW,MAAM;AAC/B,eAAO,IAAI,MAAM,qEAAqE,CAAC;AAAA,MACzF,GAAG,GAAI;AAEP,WAAK,KAAK,SAAS,MAAM;AACvB,qBAAa,OAAO;AACpB,gBAAQ;AAAA,MACV,CAAC;AAED,WAAK,KAAK,SAAS,CAAC,UAAU;AAC5B,qBAAa,OAAO;AACpB,eAAO,MAAM,KAAK;AAAA,MACpB,CAAC;AAAA,IACH,CAAC;AAAA,EACH;AAAA,EAEA,eAAe,WAIb;AACA,UAAM,aAAa,KAAK,eAAe,CAAC;AAExC,QAAI,iBAAiB;AAErB,QAAI,UAAU,WAAW;AAEvB,UAAI,UAAU,SAAS,cAAc;AACnC,yBAAiB;AAAA,MACnB;AAAA,IACF;AAEA,WAAO;AAAA,MACL,SAAS,WAAW,YAAY,SAAY,WAAW,UAAU;AAAA,MACjE,YAAY,WAAW,cAAc;AAAA,MACrC,UAAU,WAAW,YAAY;AAAA,IACnC;AAAA,EACF;AAAA,EAEQ,UAAU,aAA6B;AAC7C,UAAM,aAAwD,CAAC;AAC/D,UAAM,QAAkC,CAAC;AACzC,UAAM,gBAA8C,CAAC;AAErD,eAAW,YAAY,YAAY,aAAa,CAAC,GAAG;AAClD,UAAI,SAAS,UAAU;AACrB,mBAAW,SAAS,IAAI,IAAI,SAAS;AAAA,MACvC;AAEA,UAAI,SAAS,MAAM;AACjB,cAAM,SAAS,IAAI,IAAI,SAAS;AAChC,sBAAc,SAAS,IAAI,IAAI,IAAI,KAAK,SAAS,MAAM;AAAA,UACrD,cAAc;AAAA,UACd,WAAW;AAAA;AAAA,QACb,CAAC;AAAA,MACH;AAAA,IACF;AAEA,UAAM,SAAS;AAAA,MACb,GAAG;AAAA,MACH,UAAU,OAAO,MAAc,OAAe,SAAa;AACzD,YAAI,WAAW,IAAI,GAAG;AACpB,iBAAO,MAAM,WAAW,IAAI,EAAE,OAAO,IAAI;AAAA,QAC3C;AAEA,YAAI,cAAc,IAAI,GAAG;AACvB,gBAAM,SAAS,cAAc,IAAI,EAAE,OAAO,KAAK;AAE/C,iBAAO;AAAA,YACL,OAAO,OAAO;AAAA,YACd,QAAQ,OAAO,IAAI,CAAC,SAAS,KAAK,IAAI;AAAA,UACxC;AAAA,QACF;AAEA,eAAO;AAAA,UACL,QAAQ,CAAC;AAAA,QACX;AAAA,MACF;AAAA,IACF;AAEA,SAAK,SAAS,KAAK,MAAM;AAAA,EAC3B;AAAA,EAEQ,YAAY,eAA4B;AAC9C,SAAK,WAAW,KAAK,aAAa;AAAA,EACpC;AAAA,EAEQ,oBAAoB,uBAAiD;AAC3E,UAAM,aAAwD,CAAC;AAE/D,eAAW,YAAY,sBAAsB,aAAa,CAAC,GAAG;AAC5D,UAAI,SAAS,UAAU;AACrB,mBAAW,SAAS,IAAI,IAAI,SAAS;AAAA,MACvC;AAAA,IACF;AAEA,UAAM,mBAAmB;AAAA,MACvB,GAAG;AAAA,MACH,UAAU,OAAO,MAAc,OAAe,SAAa;AACzD,YAAI,WAAW,IAAI,GAAG;AACpB,iBAAO,MAAM,WAAW,IAAI,EAAE,OAAO,IAAI;AAAA,QAC3C;AAEA,eAAO;AAAA,UACL,QAAQ,CAAC;AAAA,QACX;AAAA,MACF;AAAA,IACF;AAEA,SAAK,mBAAmB,KAAK,gBAAgB;AAAA,EAC/C;AAAA,EAEQ,wBAAwB;AAC9B,SAAK,QAAQ,kBAAkB,uBAAuB,OAAO,YAAY;AACvE,UAAI,QAAQ,OAAO,IAAI,SAAS,cAAc;AAC5C,cAAM,SAAS,KAAK,SAAS,KAAK,CAACC,YAAWA,QAAO,SAAS,QAAQ,OAAO,IAAI,IAAI;AAErF,YAAI,CAAC,QAAQ;AACX,gBAAM,IAAI,qBAAqB,kBAAkB;AAAA,YAC/C;AAAA,UACF,CAAC;AAAA,QACH;AAEA,YAAI,CAAC,OAAO,UAAU;AACpB,gBAAM,IAAI,qBAAqB,sCAAsC;AAAA,YACnE;AAAA,UACF,CAAC;AAAA,QACH;AAEA,cAAM,aAAa,oBAAoB;AAAA,UACrC,MAAM,OAAO,SAAS,QAAQ,OAAO,SAAS,MAAM,QAAQ,OAAO,SAAS,OAAO,KAAK,KAAK;AAAA,QAC/F;AAEA,eAAO;AAAA,UACL;AAAA,QACF;AAAA,MACF;AAEA,UAAI,QAAQ,OAAO,IAAI,SAAS,gBAAgB;AAC9C,cAAM,WAAW,KAAK,mBAAmB,KAAK,CAACC,cAAaA,UAAS,gBAAgB,QAAQ,OAAO,IAAI,GAAG;AAE3G,YAAI,CAAC,UAAU;AACb,gBAAM,IAAI,qBAAqB,oBAAoB;AAAA,YACjD;AAAA,UACF,CAAC;AAAA,QACH;AAEA,YAAI,EAAE,iBAAiB,WAAW;AAChC,gBAAM,IAAI,qBAAqB,qBAAqB;AAAA,QACtD;AAEA,YAAI,CAAC,SAAS,UAAU;AACtB,gBAAM,IAAI,qBAAqB,wCAAwC;AAAA,YACrE;AAAA,UACF,CAAC;AAAA,QACH;AAEA,cAAM,aAAa,oBAAoB;AAAA,UACrC,MAAM,SAAS,SAAS,QAAQ,OAAO,SAAS,MAAM,QAAQ,OAAO,SAAS,OAAO,KAAK,KAAK;AAAA,QACjG;AAEA,eAAO;AAAA,UACL;AAAA,QACF;AAAA,MACF;AAEA,YAAM,IAAI,qBAAqB,iCAAiC;AAAA,QAC9D;AAAA,MACF,CAAC;AAAA,IACH,CAAC;AAAA,EACH;AAAA,EAEQ,qBAAqB;AAC3B,SAAK,QAAQ,UAAU,CAAC,UAAU;AAChC,WAAK,QAAQ,MAAM,mBAAmB,KAAK;AAAA,IAC7C;AAAA,EACF;AAAA,EAEQ,uBAAuB;AAC7B,SAAK,QAAQ,kBAAkB,uBAAuB,CAAC,YAAY;AACjE,WAAK,gBAAgB,QAAQ,OAAO;AAEpC,aAAO,CAAC;AAAA,IACV,CAAC;AAAA,EACH;AAAA,EAEQ,oBAAoB,SAAsB;AAChD,SAAK,QAAQ,kBAAkB,0BAA0B,YAAY;AACnE,aAAO;AAAA,QACL,SAAS,QAAQ,IAAI,CAAC,WAAW;AAC/B,iBAAO;AAAA,YACL,WAAW,OAAO;AAAA,YAClB,UAAU,OAAO;AAAA,YACjB,aAAa,OAAO;AAAA,YACpB,MAAM,OAAO;AAAA,UACf;AAAA,QACF,CAAC;AAAA,MACH;AAAA,IACF,CAAC;AAED,SAAK,QAAQ,kBAAkB,wBAAwB,OAAO,YAAY;AACxE,YAAM,SAAS,QAAQ,KAAK,CAACD,YAAWA,QAAO,SAAS,QAAQ,OAAO,IAAI;AAE3E,UAAI,CAAC,QAAQ;AACX,cAAM,IAAI,SAAS,UAAU,gBAAgB,mBAAmB,QAAQ,OAAO,IAAI,EAAE;AAAA,MACvF;AAEA,YAAM,OAAO,QAAQ,OAAO;AAE5B,iBAAW,OAAO,OAAO,aAAa,CAAC,GAAG;AACxC,YAAI,IAAI,YAAY,EAAE,QAAQ,IAAI,QAAQ,OAAO;AAC/C,gBAAM,IAAI;AAAA,YACR,UAAU;AAAA,YACV,WAAW,QAAQ,OAAO,IAAI,wBAAwB,IAAI,IAAI,MAC5D,IAAI,eAAe,yBACrB;AAAA,UACF;AAAA,QACF;AAAA,MACF;AAEA,UAAI;AAEJ,UAAI;AACF,iBAAS,MAAM,OAAO,KAAK,MAA4C,KAAK,KAAK;AAAA,MACnF,SAAS,OAAO;AACd,cAAM,eAAe,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AAC1E,cAAM,IAAI,SAAS,UAAU,eAAe,0BAA0B,QAAQ,OAAO,IAAI,MAAM,YAAY,EAAE;AAAA,MAC/G;AAEA,UAAI,OAAO,WAAW,UAAU;AAC9B,eAAO;AAAA,UACL,aAAa,OAAO;AAAA,UACpB,UAAU;AAAA,YACR;AAAA,cACE,SAAS,EAAE,MAAM,QAAQ,MAAM,OAAO;AAAA,cACtC,MAAM;AAAA,YACR;AAAA,UACF;AAAA,QACF;AAAA,MACF,OAAO;AACL,eAAO;AAAA,UACL,aAAa,OAAO;AAAA,UACpB,UAAU,OAAO;AAAA,QACnB;AAAA,MACF;AAAA,IACF,CAAC;AAAA,EACH;AAAA,EAEQ,sBAAsB,WAA0B;AACtD,SAAK,QAAQ,kBAAkB,4BAA4B,YAAY;AACrE,aAAO;AAAA,QACL,WAAW,UAAU,IAAI,CAAC,cAAc;AAAA,UACtC,aAAa,SAAS;AAAA,UACtB,UAAU,SAAS;AAAA,UACnB,MAAM,SAAS;AAAA,UACf,KAAK,SAAS;AAAA,QAChB,EAAE;AAAA,MACJ;AAAA,IACF,CAAC;AAED,SAAK,QAAQ,kBAAkB,2BAA2B,OAAO,YAAY;AAC3E,UAAI,SAAS,QAAQ,QAAQ;AAC3B,cAAM,WAAW,UAAU,KAAK,CAACC,cAAa,SAASA,aAAYA,UAAS,QAAQ,QAAQ,OAAO,GAAG;AAEtG,YAAI,CAAC,UAAU;AACb,qBAAW,oBAAoB,KAAK,oBAAoB;AACtD,kBAAM,cAAc,iBAAiB,iBAAiB,WAAW;AAEjE,kBAAM,QAAQ,YAAY,QAAQ,QAAQ,OAAO,GAAG;AAEpD,gBAAI,CAAC,OAAO;AACV;AAAA,YACF;AAEA,kBAAM,MAAM,YAAY,KAAK,KAAK;AAElC,kBAAM,SAAS,MAAM,iBAAiB,KAAK,OAAO,KAAK,KAAK;AAE5D,kBAAMC,aAAY,MAAM,QAAQ,MAAM,IAAI,SAAS,CAAC,MAAM;AAC1D,mBAAO;AAAA,cACL,UAAUA,WAAU,IAAI,CAACD,eAAc;AAAA,gBACrC,GAAGA;AAAA,gBACH,aAAa,iBAAiB;AAAA,gBAC9B,UAAUA,UAAS,YAAY,iBAAiB;AAAA,gBAChD,MAAM,iBAAiB;AAAA,gBACvB,KAAKA,UAAS,OAAO;AAAA,cACvB,EAAE;AAAA,YACJ;AAAA,UACF;AAEA,gBAAM,IAAI;AAAA,YACR,UAAU;AAAA,YACV,wBAAwB,QAAQ,OAAO,GAAG,2BACxC,UAAU,IAAI,CAAC,MAAM,EAAE,GAAG,EAAE,KAAK,IAAI,KAAK,MAC5C;AAAA,UACF;AAAA,QACF;AAEA,YAAI,EAAE,SAAS,WAAW;AACxB,gBAAM,IAAI,qBAAqB,mCAAmC;AAAA,QACpE;AAEA,YAAI;AAEJ,YAAI;AACF,6BAAmB,MAAM,SAAS,KAAK,KAAK,KAAK;AAAA,QACnD,SAAS,OAAO;AACd,gBAAM,eAAe,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AAC1E,gBAAM,IAAI;AAAA,YACR,UAAU;AAAA,YACV,4BAA4B,SAAS,IAAI,MAAM,SAAS,GAAG,MAAM,YAAY;AAAA,YAC7E;AAAA,cACE,KAAK,SAAS;AAAA,YAChB;AAAA,UACF;AAAA,QACF;AAEA,cAAM,kBAAkB,MAAM,QAAQ,gBAAgB,IAAI,mBAAmB,CAAC,gBAAgB;AAE9F,eAAO;AAAA,UACL,UAAU,gBAAgB,IAAI,CAAC,YAAY;AAAA,YACzC,GAAG;AAAA,YACH,UAAU,OAAO,YAAY,SAAS;AAAA,YACtC,MAAM,SAAS;AAAA,YACf,KAAK,OAAO,OAAO,SAAS;AAAA,UAC9B,EAAE;AAAA,QACJ;AAAA,MACF;AAEA,YAAM,IAAI,qBAAqB,4BAA4B;AAAA,QACzD;AAAA,MACF,CAAC;AAAA,IACH,CAAC;AAAA,EACH;AAAA,EAEQ,8BAA8B,mBAA0C;AAC9E,SAAK,QAAQ,kBAAkB,oCAAoC,YAAY;AAC7E,aAAO;AAAA,QACL,mBAAmB,kBAAkB,IAAI,CAAC,sBAAsB;AAAA,UAC9D,aAAa,iBAAiB;AAAA,UAC9B,UAAU,iBAAiB;AAAA,UAC3B,MAAM,iBAAiB;AAAA,UACvB,aAAa,iBAAiB;AAAA,QAChC,EAAE;AAAA,MACJ;AAAA,IACF,CAAC;AAAA,EACH;AAAA,EAEQ,qBAAqB;AAC3B,QAAI,KAAK,cAAc,YAAY,OAAO;AACxC,WAAK,QAAQ,MAAM,iEAAiE;AACpF;AAAA,IACF;AAGA,QAAI,OAAO,KAAK,QAAQ,cAAc,YAAY;AAChD,WAAK,QAAQ,uBAAuB,oCAAoC,MAAM;AAC5E,aAAK,QACF,UAAU,EACV,KAAK,CAAC,UAAU;AACf,eAAK,SAAS,MAAM;AAEpB,eAAK,KAAK,gBAAgB;AAAA,YACxB,OAAO,MAAM;AAAA,UACf,CAAC;AAAA,QACH,CAAC,EACA,MAAM,CAAC,UAAU;AAChB,cAAI,iBAAiB,YAAY,MAAM,SAAS,UAAU,gBAAgB;AACxE,iBAAK,QAAQ,MAAM,0DAA0D;AAAA,UAC/E,OAAO;AACL,iBAAK,QAAQ;AAAA,cACX;AAAA;AAAA,EACE,iBAAiB,QAAQ,MAAM,QAAQ,KAAK,UAAU,KAAK,CAC7D;AAAA,YACF;AAAA,UACF;AAAA,QACF,CAAC;AAAA,MACL,CAAC;AAAA,IACH,OAAO;AACL,WAAK,QAAQ,MAAM,qFAAqF;AAAA,IAC1G;AAAA,EACF;AAAA,EAEQ,kBAAkB,OAAkB;AAC1C,SAAK,QAAQ,kBAAkB,wBAAwB,YAAY;AACjE,aAAO;AAAA,QACL,OAAO,MAAM,QAAQ;AAAA,UACnB,MAAM,IAAI,OAAO,SAAS;AACxB,mBAAO;AAAA,cACL,aAAa,KAAK;AAAA,cAClB,aAAa,KAAK;AAAA,cAClB,aAAa,KAAK,aACd,MAAM,aAAa,KAAK,UAAU,IAClC;AAAA,gBACE,sBAAsB;AAAA,gBACtB,YAAY,CAAC;AAAA,gBACb,MAAM;AAAA,cACR;AAAA;AAAA,cACJ,MAAM,KAAK;AAAA,YACb;AAAA,UACF,CAAC;AAAA,QACH;AAAA,MACF;AAAA,IACF,CAAC;AAED,SAAK,QAAQ,kBAAkB,uBAAuB,OAAO,YAAY;AACvE,YAAM,OAAO,MAAM,KAAK,CAACE,UAASA,MAAK,SAAS,QAAQ,OAAO,IAAI;AAEnE,UAAI,CAAC,MAAM;AACT,cAAM,IAAI,SAAS,UAAU,gBAAgB,iBAAiB,QAAQ,OAAO,IAAI,EAAE;AAAA,MACrF;AAEA,UAAI,OAAgB;AAEpB,UAAI,KAAK,YAAY;AACnB,cAAM,SAAS,MAAM,KAAK,WAAW,WAAW,EAAE,SAAS,QAAQ,OAAO,SAAS;AAEnF,YAAI,OAAO,QAAQ;AACjB,gBAAM,iBAAiB,KAAK,QAAQ,kCAChC,KAAK,OAAO,gCAAgC,OAAO,MAAM,IACzD,OAAO,OACJ,IAAI,CAAC,UAAU;AACd,kBAAM,OAAO,MAAM,MAAM,KAAK,GAAG,KAAK;AACtC,mBAAO,GAAG,IAAI,KAAK,MAAM,OAAO;AAAA,UAClC,CAAC,EACA,KAAK,IAAI;AAEhB,gBAAM,IAAI;AAAA,YACR,UAAU;AAAA,YACV,SAAS,QAAQ,OAAO,IAAI,kCAAkC,cAAc;AAAA,UAC9E;AAAA,QACF;AAEA,eAAO,OAAO;AAAA,MAChB;AAEA,YAAM,gBAAgB,QAAQ,QAAQ,OAAO;AAE7C,UAAI;AAEJ,UAAI;AACF,cAAM,iBAAiB,OAAO,aAAuB;AACnD,cAAI;AACF,kBAAM,KAAK,QAAQ,aAAa;AAAA,cAC9B,QAAQ;AAAA,cACR,QAAQ;AAAA,gBACN,GAAG;AAAA,gBACH;AAAA,cACF;AAAA,YACF,CAAC;AAED,gBAAI,KAAK,sBAAsB;AAC7B,oBAAM,IAAI,QAAQ,CAAC,YAAY,aAAa,OAAO,CAAC;AAAA,YACtD;AAAA,UACF,SAAS,eAAe;AACtB,iBAAK,QAAQ;AAAA,cACX,yDAAyD,QAAQ,OAAO,IAAI;AAAA,cAC5E,yBAAyB,QAAQ,cAAc,UAAU,OAAO,aAAa;AAAA,YAC/E;AAAA,UACF;AAAA,QACF;AAEA,cAAM,MAAM;AAAA,UACV,OAAO,CAAC,SAAiB,YAAgC;AACvD,iBAAK,QAAQ,mBAAmB;AAAA,cAC9B,MAAM;AAAA,gBACJ;AAAA,gBACA;AAAA,cACF;AAAA,cACA,OAAO;AAAA,YACT,CAAC;AAAA,UACH;AAAA,UACA,OAAO,CAAC,SAAiB,YAAgC;AACvD,iBAAK,QAAQ,mBAAmB;AAAA,cAC9B,MAAM;AAAA,gBACJ;AAAA,gBACA;AAAA,cACF;AAAA,cACA,OAAO;AAAA,YACT,CAAC;AAAA,UACH;AAAA,UACA,MAAM,CAAC,SAAiB,YAAgC;AACtD,iBAAK,QAAQ,mBAAmB;AAAA,cAC9B,MAAM;AAAA,gBACJ;AAAA,gBACA;AAAA,cACF;AAAA,cACA,OAAO;AAAA,YACT,CAAC;AAAA,UACH;AAAA,UACA,MAAM,CAAC,SAAiB,YAAgC;AACtD,iBAAK,QAAQ,mBAAmB;AAAA,cAC9B,MAAM;AAAA,gBACJ;AAAA,gBACA;AAAA,cACF;AAAA,cACA,OAAO;AAAA,YACT,CAAC;AAAA,UACH;AAAA,QACF;AAKA,cAAM,gBAAgB,OAAO,YAAiC;AAC5D,gBAAM,eAAe,MAAM,QAAQ,OAAO,IAAI,UAAU,CAAC,OAAO;AAEhE,cAAI;AACF,kBAAM,KAAK,QAAQ,aAAa;AAAA,cAC9B,QAAQ;AAAA,cACR,QAAQ;AAAA,gBACN,SAAS;AAAA,gBACT,UAAU,QAAQ,OAAO;AAAA,cAC3B;AAAA,YACF,CAAC;AAED,gBAAI,KAAK,sBAAsB;AAC7B,oBAAM,IAAI,QAAQ,CAAC,YAAY,aAAa,OAAO,CAAC;AAAA,YACtD;AAAA,UACF,SAAS,aAAa;AACpB,iBAAK,QAAQ;AAAA,cACX,wDAAwD,QAAQ,OAAO,IAAI;AAAA,cAC3E,uBAAuB,QAAQ,YAAY,UAAU,OAAO,WAAW;AAAA,YACzE;AAAA,UACF;AAAA,QACF;AACA,cAAM,qBAAqB,KAAK,QAAQ,MAAM;AAAA,UAC5C,QAAQ;AAAA,YACN,SAAS,KAAK,QAAQ,iBAAiB;AAAA,UACzC;AAAA,UACA;AAAA,UACA;AAAA,UACA,WAAW,OAAO,QAAQ,QAAQ,OAAO,cAAc,WAAW,QAAQ,OAAO,MAAM,YAAY;AAAA,UACnG,iBAAiB,QAAQ,OAAO;AAAA,UAChC,SAAS,KAAK;AAAA,UACd,WAAW,KAAK;AAAA,UAChB;AAAA,QACF,CAAC;AAGD,cAAM,oBAAqB,OAAO,KAAK,YACnC,QAAQ,KAAK;AAAA,UACX;AAAA,UACA,IAAI,QAAe,CAAC,GAAG,WAAW;AAChC,kBAAM,YAAY,WAAW,MAAM;AACjC;AAAA,gBACE,IAAI;AAAA,kBACF,UAAU;AAAA,kBACV,SAAS,QAAQ,OAAO,IAAI,qBAAqB,KAAK,SAAS;AAAA,gBACjE;AAAA,cACF;AAAA,YACF,GAAG,KAAK,SAAS;AAGjB,+BAAmB,QAAQ,MAAM,aAAa,SAAS,CAAC;AAAA,UAC1D,CAAC;AAAA,QACH,CAAC,IACD;AAaJ,cAAM,MAAM,CAAC;AAEb,YAAI,sBAAsB,UAAa,sBAAsB,MAAM;AACjE,mBAAS,uBAAuB,MAAM;AAAA,YACpC,SAAS,CAAC;AAAA,UACZ,CAAC;AAAA,QACH,WAAW,OAAO,sBAAsB,UAAU;AAChD,mBAAS,uBAAuB,MAAM;AAAA,YACpC,SAAS,CAAC,EAAE,MAAM,mBAAmB,MAAM,OAAO,CAAC;AAAA,UACrD,CAAC;AAAA,QACH,WAAW,UAAU,mBAAmB;AACtC,mBAAS,uBAAuB,MAAM;AAAA,YACpC,SAAS,CAAC,iBAAiB;AAAA,UAC7B,CAAC;AAAA,QACH,OAAO;AACL,mBAAS,uBAAuB,MAAM,iBAAiB;AAAA,QACzD;AAAA,MACF,SAAS,OAAO;AAGd,YAAI,eAAe,KAAK,GAAG;AACzB,gBAAM;AAAA,QACR;AAEA,YAAI,iBAAiB,WAAW;AAC9B,iBAAO;AAAA,YACL,SAAS,CAAC,EAAE,MAAM,MAAM,SAAS,MAAM,OAAO,CAAC;AAAA,YAC/C,SAAS;AAAA,YACT,GAAI,MAAM,SAAS,EAAE,mBAAmB,MAAM,OAAO,IAAI,CAAC;AAAA,UAC5D;AAAA,QACF;AAEA,cAAM,eAAe,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AAC1E,eAAO;AAAA,UACL,SAAS;AAAA,YACP;AAAA,cACE,MAAM,SAAS,QAAQ,OAAO,IAAI,uBAAuB,YAAY;AAAA,cACrE,MAAM;AAAA,YACR;AAAA,UACF;AAAA,UACA,SAAS;AAAA,QACX;AAAA,MACF;AAEA,aAAO;AAAA,IACT,CAAC;AAAA,EACH;AACF;AAKA,SAAS,iBAAiB,KAAqB;AAC7C,SAAO,IAAI,QAAQ,UAAU,CAAC,WAAW,IAAI,OAAO,YAAY,CAAC,EAAE;AACrE;AAKA,SAAS,yBAAyB,KAAuD;AACvF,QAAM,SAAkC,CAAC;AAEzC,aAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,GAAG,GAAG;AAC9C,UAAM,WAAW,iBAAiB,GAAG;AACrC,WAAO,QAAQ,IAAI;AAAA,EACrB;AAEA,SAAO;AACT;AAEA,IAAM,0BAEF;AAEJ,IAAM,sBAAN,cAAkC,wBAAwB;AAAC;AAEpD,IAAM,UAAN,cAAyE,oBAAoB;AAAA,EAelG,YAAmB,SAA2B;AAC5C,UAAM;AADW;AAGjB,SAAK,WAAW;AAChB,SAAK,gBAAgB,QAAQ;AAC7B,SAAK,UAAU,QAAQ,UAAU;AAAA,EACnC;AAAA,EApBA,IAAW,WAAgC;AACzC,WAAO,KAAK;AAAA,EACd;AAAA,EACA;AAAA,EACA,oBAAsC;AAAA,EACtC;AAAA,EACA;AAAA,EACA,WAA6B,CAAC;AAAA,EAC9B,aAA4B,CAAC;AAAA,EAC7B,sBAAkD,CAAC;AAAA,EACnD,YAAiC,CAAC;AAAA,EAElC,SAAoB,CAAC;AAAA;AAAA;AAAA;AAAA,EAad,UAAuD,QAA8B;AAC1F,SAAK,SAAS,KAAK,MAAM;AAAA,EAC3B;AAAA;AAAA;AAAA;AAAA,EAKO,YAAY,UAAuB;AACxC,SAAK,WAAW,KAAK,QAAQ;AAAA,EAC/B;AAAA;AAAA;AAAA;AAAA,EAKO,oBACL,UACA;AACA,SAAK,oBAAoB,KAAK,QAAQ;AAAA,EACxC;AAAA;AAAA;AAAA;AAAA,EAKO,QAAuC,MAAuB;AACnE,SAAK,OAAO,KAAK,IAA0B;AAAA,EAC7C;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,MAAa,SAAS,KAAmD;AAEvE,UAAM,iBAAiB,KAAK,WAAW,KAAK,CAAC,aAAa,SAAS,QAAQ,GAAG;AAE9E,QAAI,gBAAgB;AAClB,YAAM,SAAS,MAAM,eAAe,KAAK;AACzC,YAAM,UAAU,MAAM,QAAQ,MAAM,IAAI,SAAS,CAAC,MAAM;AACxD,YAAM,cAAc,QAAQ,CAAC;AAE7B,YAAM,eAA4C;AAAA,QAChD,UAAU,eAAe;AAAA,QACzB;AAAA,MACF;AAEA,UAAI,UAAU,aAAa;AACzB,qBAAa,OAAO,YAAY;AAAA,MAClC;AAEA,UAAI,UAAU,aAAa;AACzB,qBAAa,OAAO,YAAY;AAAA,MAClC;AAEA,aAAO;AAAA,IACT;AAGA,eAAW,YAAY,KAAK,qBAAqB;AAC/C,YAAM,iBAAiB,iBAAiB,SAAS,WAAW;AAC5D,YAAM,SAAS,eAAe,QAAQ,GAAG;AACzC,UAAI,CAAC,QAAQ;AACX;AAAA,MACF;AAEA,YAAM,SAAS,MAAM,SAAS,KAAK,MAAsE;AAEzG,YAAM,eAA4C;AAAA,QAChD,UAAU,SAAS;AAAA,QACnB;AAAA,MACF;AAEA,UAAI,UAAU,QAAQ;AACpB,qBAAa,OAAO,OAAO;AAAA,MAC7B;AAEA,UAAI,UAAU,QAAQ;AACpB,qBAAa,OAAO,OAAO;AAAA,MAC7B;AAEA,aAAO;AAAA,IACT;AAEA,UAAM,IAAI,qBAAqB,uBAAuB,GAAG,IAAI,EAAE,IAAI,CAAC;AAAA,EACtE;AAAA;AAAA;AAAA;AAAA,EAKA,MAAa,MACX,SAWA;AACA,UAAM,SAAS,KAAK,oBAAoB,OAAO;AAE/C,QAAI,OAAO,kBAAkB,SAAS;AACpC,YAAM,YAAY,IAAI,qBAAqB;AAI3C,UAAI;AAEJ,UAAI,KAAK,eAAe;AACtB,YAAI;AACF,iBAAO,MAAM,KAAK,cAAc,MAA4C;AAAA,QAC9E,SAAS,OAAO;AACd,eAAK,QAAQ;AAAA,YACX;AAAA,YACA,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AAAA,UACvD;AAAA,QAEF;AAAA,MACF;AAEA,YAAM,UAAU,IAAI,eAAkB;AAAA,QACpC;AAAA,QACA,cAAc,KAAK,SAAS;AAAA,QAC5B,QAAQ,KAAK;AAAA,QACb,MAAM,KAAK,SAAS;AAAA,QACpB,MAAM,KAAK,SAAS;AAAA,QACpB,SAAS,KAAK;AAAA,QACd,WAAW,KAAK;AAAA,QAChB,oBAAoB,KAAK;AAAA,QACzB,OAAO,KAAK,SAAS;AAAA,QACrB,OAAO,KAAK;AAAA,QACZ,eAAe;AAAA,QACf,OAAO,KAAK,SAAS;AAAA,QACrB,SAAS,KAAK,SAAS;AAAA,MACzB,CAAC;AAED,YAAM,QAAQ,QAAQ,SAAS;AAE/B,WAAK,UAAU,KAAK,OAAO;AAE3B,cAAQ,KAAK,SAAS,MAAM;AAC1B,aAAK,eAAe,OAAO;AAAA,MAC7B,CAAC;AAGD,UAAI,UAAU,SAAS;AACrB,cAAM,kBAAkB,UAAU;AAElC,kBAAU,UAAU,MAAM;AACxB,eAAK,eAAe,OAAO;AAE3B,cAAI,iBAAiB;AACnB,4BAAgB;AAAA,UAClB;AAAA,QACF;AAAA,MACF,OAAO;AACL,kBAAU,UAAU,MAAM;AACxB,eAAK,eAAe,OAAO;AAAA,QAC7B;AAAA,MACF;AAEA,WAAK,KAAK,WAAW;AAAA,QACnB;AAAA,MACF,CAAC;AAAA,IACH,WAAW,OAAO,kBAAkB,cAAc;AAChD,YAAM,aAAa,OAAO;AAE1B,UAAI,WAAW,WAAW;AAExB,aAAK,QAAQ;AAAA,UACX,6EAA6E,WAAW,IAAI,IAAI,WAAW,IAAI,GAAG,WAAW,QAAQ;AAAA,QACvI;AAEA,aAAK,oBAAoB,MAAM,gBAAmC;AAAA,UAChE,GAAI,KAAK,gBAAgB,EAAE,cAAc,KAAK,cAAc,IAAI,CAAC;AAAA,UACjE,cAAc,OAAO,YAAY;AAC/B,gBAAI;AAEJ,gBAAI,KAAK,eAAe;AACtB,qBAAO,MAAM,KAAK,cAAc,OAAO;AAIvC,kBAAI,SAAS,UAAa,SAAS,MAAM;AACvC,sBAAM,IAAI,MAAM,yBAAyB;AAAA,cAC3C;AAAA,YACF;AAGA,kBAAM,YAAY,MAAM,QAAQ,QAAQ,QAAQ,gBAAgB,CAAC,IAC7D,QAAQ,QAAQ,gBAAgB,EAAE,CAAC,IACnC,QAAQ,QAAQ,gBAAgB;AAIpC,mBAAO,KAAK,eAAe,MAAM,SAAS;AAAA,UAC5C;AAAA,UACA,oBAAoB,WAAW;AAAA,UAC/B,YAAY,WAAW;AAAA,UACvB,MAAM,WAAW;AAAA;AAAA,UAEjB,SAAS,YAAY;AAAA,UAErB;AAAA,UACA,WAAW,YAAY;AAErB,iBAAK,QAAQ,MAAM,uDAAuD;AAAA,UAC5E;AAAA,UACA,oBAAoB,OAAO,KAAK,QAAQ;AACtC,kBAAM,KAAK,wBAAwB,KAAK,KAAK,MAAM,WAAW,IAAI;AAAA,UACpE;AAAA,UACA,MAAM,WAAW;AAAA,UACjB,WAAW;AAAA,UACX,gBAAgB,WAAW;AAAA,QAC7B,CAAC;AAAA,MACH,OAAO;AAEL,aAAK,oBAAoB,MAAM,gBAAmC;AAAA,UAChE,GAAI,KAAK,gBAAgB,EAAE,cAAc,KAAK,cAAc,IAAI,CAAC;AAAA,UACjE,cAAc,OAAO,YAAY;AAC/B,gBAAI;AAEJ,gBAAI,KAAK,eAAe;AACtB,qBAAO,MAAM,KAAK,cAAc,OAAO;AAAA,YACzC;AAGA,kBAAM,YAAY,MAAM,QAAQ,QAAQ,QAAQ,gBAAgB,CAAC,IAC7D,QAAQ,QAAQ,gBAAgB,EAAE,CAAC,IACnC,QAAQ,QAAQ,gBAAgB;AAEpC,mBAAO,KAAK,eAAe,MAAM,SAAS;AAAA,UAC5C;AAAA,UACA,oBAAoB,WAAW;AAAA,UAC/B,YAAY,WAAW;AAAA,UACvB,MAAM,WAAW;AAAA,UACjB,SAAS,OAAO,YAAY;AAC1B,kBAAM,eAAe,KAAK,UAAU,QAAQ,OAAO;AAEnD,gBAAI,iBAAiB,GAAI,MAAK,UAAU,OAAO,cAAc,CAAC;AAE9D,iBAAK,KAAK,cAAc;AAAA,cACtB;AAAA,YACF,CAAC;AAAA,UACH;AAAA,UACA,WAAW,OAAO,YAAY;AAC5B,iBAAK,UAAU,KAAK,OAAO;AAE3B,iBAAK,QAAQ,KAAK,gDAAgD;AAElE,iBAAK,KAAK,WAAW;AAAA,cACnB;AAAA,YACF,CAAC;AAAA,UACH;AAAA,UAEA,oBAAoB,OAAO,KAAK,QAAQ;AACtC,kBAAM,KAAK,wBAAwB,KAAK,KAAK,OAAO,WAAW,IAAI;AAAA,UACrE;AAAA,UACA,MAAM,WAAW;AAAA,UACjB,WAAW,WAAW;AAAA,UACtB,gBAAgB,WAAW;AAAA,QAC7B,CAAC;AAED,aAAK,QAAQ;AAAA,UACX,6DAA6D,WAAW,IAAI,IAAI,WAAW,IAAI,GAAG,WAAW,QAAQ;AAAA,QACvH;AAAA,MACF;AAAA,IACF,OAAO;AACL,YAAM,IAAI,MAAM,wBAAwB;AAAA,IAC1C;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,MAAa,OAAO;AAClB,QAAI,KAAK,mBAAmB;AAC1B,YAAM,KAAK,kBAAkB,MAAM;AAAA,IACrC;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,eAAe,MAAU,WAAuC;AAE9D,QACE,QACA,OAAO,SAAS,YAChB,mBAAmB,QACnB,CAAE,KAAoC,eACtC;AACA,YAAM,eACJ,WAAW,QAAQ,OAAQ,KAA4B,UAAU,WAC5D,KAA2B,QAC5B;AACN,YAAM,IAAI,MAAM,YAAY;AAAA,IAC9B;AAEA,UAAM,eAAe,OACjB,KAAK,OAAO,OAAO,CAAC,SAAU,KAAK,YAAY,KAAK,UAAU,IAAI,IAAI,IAAK,IAC3E,KAAK;AACT,WAAO,IAAI,eAAkB;AAAA,MAC3B;AAAA,MACA,cAAc,KAAK,SAAS;AAAA,MAC5B,QAAQ,KAAK;AAAA,MACb,MAAM,KAAK,SAAS;AAAA,MACpB,MAAM,KAAK,SAAS;AAAA,MACpB,SAAS,KAAK;AAAA,MACd,WAAW,KAAK;AAAA,MAChB,oBAAoB,KAAK;AAAA,MACzB,OAAO,KAAK,SAAS;AAAA,MACrB;AAAA,MACA,OAAO;AAAA,MACP,eAAe;AAAA,MACf,OAAO,KAAK,SAAS;AAAA,MACrB,SAAS,KAAK,SAAS;AAAA,IACzB,CAAC;AAAA,EACH;AAAA;AAAA;AAAA;AAAA,EAKA,0BAA0B,OACxB,KACA,KACA,cAAc,OACd,SACG;AACH,UAAM,eAAe,KAAK,SAAS,UAAU,CAAC;AAE9C,UAAM,UAAU,aAAa,YAAY,SAAY,OAAO,aAAa;AAEzE,QAAI,SAAS;AACX,YAAM,OAAO,aAAa,QAAQ;AAClC,YAAM,MAAM,IAAI,IAAI,IAAI,OAAO,IAAI,UAAU,IAAI,EAAE;AAEnD,UAAI;AACF,YAAI,IAAI,WAAW,SAAS,IAAI,aAAa,MAAM;AACjD,cACG,UAAU,aAAa,UAAU,KAAK;AAAA,YACrC,gBAAgB;AAAA,UAClB,CAAC,EACA,IAAI,aAAa,WAAW,WAAM;AAErC;AAAA,QACF;AAGA,YAAI,IAAI,WAAW,SAAS,IAAI,aAAa,UAAU;AACrD,cAAI,aAAa;AAEf,kBAAM,WAAW;AAAA,cACf,MAAM;AAAA,cACN,OAAO;AAAA,cACP,QAAQ;AAAA,cACR,OAAO;AAAA,YACT;AAEA,gBACG,UAAU,KAAK;AAAA,cACd,gBAAgB;AAAA,YAClB,CAAC,EACA,IAAI,KAAK,UAAU,QAAQ,CAAC;AAAA,UACjC,OAAO;AACL,kBAAM,gBAAgB,KAAK,UAAU,OAAO,CAAC,MAAM,EAAE,OAAO,EAAE;AAC9D,kBAAM,gBAAgB,KAAK,UAAU;AACrC,kBAAM,WAAW,kBAAkB,iBAAiB,gBAAgB;AAEpE,kBAAM,WAAW;AAAA,cACf,OAAO;AAAA,cACP,QAAQ,WAAW,UAAU,kBAAkB,IAAI,gBAAgB;AAAA,cACnE,OAAO;AAAA,YACT;AAEA,gBACG,UAAU,WAAW,MAAM,KAAK;AAAA,cAC/B,gBAAgB;AAAA,YAClB,CAAC,EACA,IAAI,KAAK,UAAU,QAAQ,CAAC;AAAA,UACjC;AAEA;AAAA,QACF;AAAA,MACF,SAAS,OAAO;AACd,aAAK,QAAQ,MAAM,yCAAyC,KAAK;AAAA,MACnE;AAAA,IACF;AAGA,UAAM,cAAc,KAAK,SAAS;AAClC,QAAI,aAAa,WAAW,IAAI,WAAW,OAAO;AAChD,YAAM,MAAM,IAAI,IAAI,IAAI,OAAO,IAAI,UAAU,IAAI,EAAE;AAEnD,UAAI,IAAI,aAAa,6CAA6C,YAAY,qBAAqB;AACjG,cAAM,WAAW,yBAAyB,YAAY,mBAAmB;AACzE,YACG,UAAU,KAAK;AAAA,UACd,gBAAgB;AAAA,QAClB,CAAC,EACA,IAAI,KAAK,UAAU,QAAQ,CAAC;AAC/B;AAAA,MACF;AAEA,UAAI,IAAI,aAAa,2CAA2C,YAAY,mBAAmB;AAC7F,cAAM,WAAW,yBAAyB,YAAY,iBAAiB;AACvE,YACG,UAAU,KAAK;AAAA,UACd,gBAAgB;AAAA,QAClB,CAAC,EACA,IAAI,KAAK,UAAU,QAAQ,CAAC;AAC/B;AAAA,MACF;AAAA,IACF;AAGA,QAAI,UAAU,GAAG,EAAE,IAAI;AAAA,EACzB;AAAA,EAEA,oBACE,WAsB6B;AAC7B,UAAM,OAAO,QAAQ,KAAK,MAAM,CAAC;AACjC,UAAM,SAAS,CAAC,SAAiB;AAC/B,YAAM,QAAQ,KAAK,UAAU,CAAC,QAAQ,QAAQ,KAAK,IAAI,EAAE;AAEzD,aAAO,UAAU,MAAM,QAAQ,IAAI,KAAK,SAAS,KAAK,QAAQ,CAAC,IAAI;AAAA,IACrE;AAEA,UAAM,eAAe,OAAO,WAAW;AACvC,UAAM,UAAU,OAAO,MAAM;AAC7B,UAAM,cAAc,OAAO,UAAU;AACrC,UAAM,eAAe,OAAO,WAAW;AACvC,UAAM,UAAU,OAAO,MAAM;AAE7B,UAAM,eAAe,QAAQ,IAAI;AACjC,UAAM,UAAU,QAAQ,IAAI;AAC5B,UAAM,cAAc,QAAQ,IAAI;AAChC,UAAM,eAAe,QAAQ,IAAI;AACjC,UAAM,UAAU,QAAQ,IAAI;AAE5B,UAAM,gBACJ,WAAW,kBACV,iBAAiB,gBAAgB,eAAe,iBACjD,gBACA;AAEF,QAAI,kBAAkB,cAAc;AAClC,YAAM,OAAO,SAAS,WAAW,YAAY,MAAM,SAAS,KAAK,WAAW,WAAW,MAAM;AAC7F,YAAM,OAAO,WAAW,YAAY,QAAQ,WAAW,WAAW;AAClE,YAAM,WAAW,WAAW,YAAY,YAAY,eAAe,eAAe;AAClF,YAAM,qBAAqB,WAAW,YAAY,sBAAsB;AACxE,YAAM,YAAY,WAAW,YAAY,aAAa,iBAAiB,UAAU,iBAAiB,UAAU;AAE5G,aAAO;AAAA,QACL,YAAY;AAAA,UACV;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,QACF;AAAA,QACA,eAAe;AAAA,MACjB;AAAA,IACF;AAEA,WAAO,EAAE,eAAe,QAAiB;AAAA,EAC3C;AAAA,EAEA,eAAe,SAAkC;AAC/C,UAAM,eAAe,KAAK,UAAU,QAAQ,OAAO;AAEnD,QAAI,iBAAiB,IAAI;AACvB,WAAK,UAAU,OAAO,cAAc,CAAC;AACrC,WAAK,KAAK,cAAc;AAAA,QACtB;AAAA,MACF,CAAC;AAAA,IACH;AAAA,EACF;AACF;","names":["ErrorCode","McpError","prompt","resource","resources","tool"]} \ No newline at end of file diff --git a/dist/bin/fastmcp.js b/dist/bin/fastmcp.js index 5c6478d..db531b0 100755 --- a/dist/bin/fastmcp.js +++ b/dist/bin/fastmcp.js @@ -30,9 +30,7 @@ await yargs(hideBin(process.argv)).scriptName("fastmcp").command( if (argv.verbose) { console.log(`[FastMCP] Starting server: ${command}`); console.log(`[FastMCP] File: ${argv.file}`); - console.log( - `[FastMCP] Watch mode: ${argv.watch ? "enabled" : "disabled"}` - ); + console.log(`[FastMCP] Watch mode: ${argv.watch ? "enabled" : "disabled"}`); } await execa({ shell: true, @@ -137,14 +135,9 @@ await yargs(hideBin(process.argv)).scriptName("fastmcp").command( console.error("Make sure the file properly imports and uses FastMCP"); process.exit(1); } - console.log( - "[FastMCP] \u2713 All validations passed! Server file looks good." - ); + console.log("[FastMCP] \u2713 All validations passed! Server file looks good."); } catch (error) { - console.error( - "[FastMCP Error] Validation failed:", - error instanceof Error ? error.message : String(error) - ); + console.error("[FastMCP Error] Validation failed:", error instanceof Error ? error.message : String(error)); process.exit(1); } } diff --git a/dist/bin/fastmcp.js.map b/dist/bin/fastmcp.js.map index b8fe21f..767ec2e 100644 --- a/dist/bin/fastmcp.js.map +++ b/dist/bin/fastmcp.js.map @@ -1 +1 @@ -{"version":3,"sources":["../../src/bin/fastmcp.ts"],"sourcesContent":["#!/usr/bin/env node\n\nimport { execa } from \"execa\";\nimport yargs from \"yargs\";\nimport { hideBin } from \"yargs/helpers\";\n\nawait yargs(hideBin(process.argv))\n .scriptName(\"fastmcp\")\n .command(\n \"dev \",\n \"Start a development server\",\n (yargs) => {\n return yargs\n .positional(\"file\", {\n demandOption: true,\n describe: \"The path to the server file\",\n type: \"string\",\n })\n\n .option(\"watch\", {\n alias: \"w\",\n default: false,\n describe: \"Watch for file changes and restart server\",\n type: \"boolean\",\n })\n\n .option(\"verbose\", {\n alias: \"v\",\n default: false,\n describe: \"Enable verbose logging\",\n type: \"boolean\",\n });\n },\n\n async (argv) => {\n try {\n const command = argv.watch\n ? `npx @wong2/mcp-cli npx tsx --watch ${argv.file}`\n : `npx @wong2/mcp-cli npx tsx ${argv.file}`;\n\n if (argv.verbose) {\n console.log(`[FastMCP] Starting server: ${command}`);\n console.log(`[FastMCP] File: ${argv.file}`);\n console.log(\n `[FastMCP] Watch mode: ${argv.watch ? \"enabled\" : \"disabled\"}`,\n );\n }\n\n await execa({\n shell: true,\n stderr: \"inherit\",\n stdin: \"inherit\",\n stdout: \"inherit\",\n })`${command}`;\n } catch (error) {\n console.error(\n \"[FastMCP Error] Failed to start development server:\",\n error instanceof Error ? error.message : String(error),\n );\n\n if (argv.verbose && error instanceof Error && error.stack) {\n console.error(\"[FastMCP Debug] Stack trace:\", error.stack);\n }\n\n process.exit(1);\n }\n },\n )\n\n .command(\n \"inspect \",\n \"Inspect a server file\",\n (yargs) => {\n return yargs.positional(\"file\", {\n demandOption: true,\n describe: \"The path to the server file\",\n type: \"string\",\n });\n },\n\n async (argv) => {\n try {\n await execa({\n stderr: \"inherit\",\n stdout: \"inherit\",\n })`npx @modelcontextprotocol/inspector npx tsx ${argv.file}`;\n } catch (error) {\n console.error(\n \"[FastMCP Error] Failed to inspect server:\",\n error instanceof Error ? error.message : String(error),\n );\n\n process.exit(1);\n }\n },\n )\n\n .command(\n \"validate \",\n \"Validate a FastMCP server file for syntax and basic structure\",\n (yargs) => {\n return yargs\n .positional(\"file\", {\n demandOption: true,\n describe: \"The path to the server file\",\n type: \"string\",\n })\n\n .option(\"strict\", {\n alias: \"s\",\n default: false,\n describe: \"Enable strict validation (type checking)\",\n type: \"boolean\",\n });\n },\n\n async (argv) => {\n try {\n const { existsSync } = await import(\"fs\");\n const { resolve } = await import(\"path\");\n const filePath = resolve(argv.file);\n\n if (!existsSync(filePath)) {\n console.error(`[FastMCP Error] File not found: ${filePath}`);\n process.exit(1);\n }\n\n console.log(`[FastMCP] Validating server file: ${filePath}`);\n\n const command = argv.strict\n ? `npx tsc --noEmit --strict ${filePath}`\n : `npx tsc --noEmit ${filePath}`;\n\n try {\n await execa({\n shell: true,\n stderr: \"pipe\",\n stdout: \"pipe\",\n })`${command}`;\n\n console.log(\"[FastMCP] ✓ TypeScript compilation successful\");\n } catch (tsError) {\n console.error(\"[FastMCP] ✗ TypeScript compilation failed\");\n\n if (tsError instanceof Error && \"stderr\" in tsError) {\n console.error(tsError.stderr);\n }\n\n process.exit(1);\n }\n\n try {\n await execa({\n shell: true,\n stderr: \"pipe\",\n stdout: \"pipe\",\n })`node -e \"\n (async () => {\n try {\n const { FastMCP } = await import('fastmcp');\n await import('file://${filePath}');\n console.log('[FastMCP] ✓ Server structure validation passed');\n } catch (error) {\n console.error('[FastMCP] ✗ Server structure validation failed:', error.message);\n process.exit(1);\n }\n })();\n \"`;\n } catch {\n console.error(\"[FastMCP] ✗ Server structure validation failed\");\n console.error(\"Make sure the file properly imports and uses FastMCP\");\n\n process.exit(1);\n }\n\n console.log(\n \"[FastMCP] ✓ All validations passed! Server file looks good.\",\n );\n } catch (error) {\n console.error(\n \"[FastMCP Error] Validation failed:\",\n error instanceof Error ? error.message : String(error),\n );\n\n process.exit(1);\n }\n },\n )\n\n .help()\n .parseAsync();\n"],"mappings":"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","names":["yargs"]} \ No newline at end of file +{"version":3,"sources":["../../src/bin/fastmcp.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { execa } from \"execa\"\nimport yargs from \"yargs\"\nimport { hideBin } from \"yargs/helpers\"\n\nawait yargs(hideBin(process.argv))\n .scriptName(\"fastmcp\")\n .command(\n \"dev \",\n \"Start a development server\",\n (yargs) => {\n return yargs\n .positional(\"file\", {\n demandOption: true,\n describe: \"The path to the server file\",\n type: \"string\",\n })\n\n .option(\"watch\", {\n alias: \"w\",\n default: false,\n describe: \"Watch for file changes and restart server\",\n type: \"boolean\",\n })\n\n .option(\"verbose\", {\n alias: \"v\",\n default: false,\n describe: \"Enable verbose logging\",\n type: \"boolean\",\n })\n },\n\n async (argv) => {\n try {\n const command = argv.watch\n ? `npx @wong2/mcp-cli npx tsx --watch ${argv.file}`\n : `npx @wong2/mcp-cli npx tsx ${argv.file}`\n\n if (argv.verbose) {\n console.log(`[FastMCP] Starting server: ${command}`)\n console.log(`[FastMCP] File: ${argv.file}`)\n console.log(`[FastMCP] Watch mode: ${argv.watch ? \"enabled\" : \"disabled\"}`)\n }\n\n await execa({\n shell: true,\n stderr: \"inherit\",\n stdin: \"inherit\",\n stdout: \"inherit\",\n })`${command}`\n } catch (error) {\n console.error(\n \"[FastMCP Error] Failed to start development server:\",\n error instanceof Error ? error.message : String(error),\n )\n\n if (argv.verbose && error instanceof Error && error.stack) {\n console.error(\"[FastMCP Debug] Stack trace:\", error.stack)\n }\n\n process.exit(1)\n }\n },\n )\n\n .command(\n \"inspect \",\n \"Inspect a server file\",\n (yargs) => {\n return yargs.positional(\"file\", {\n demandOption: true,\n describe: \"The path to the server file\",\n type: \"string\",\n })\n },\n\n async (argv) => {\n try {\n await execa({\n stderr: \"inherit\",\n stdout: \"inherit\",\n })`npx @modelcontextprotocol/inspector npx tsx ${argv.file}`\n } catch (error) {\n console.error(\n \"[FastMCP Error] Failed to inspect server:\",\n error instanceof Error ? error.message : String(error),\n )\n\n process.exit(1)\n }\n },\n )\n\n .command(\n \"validate \",\n \"Validate a FastMCP server file for syntax and basic structure\",\n (yargs) => {\n return yargs\n .positional(\"file\", {\n demandOption: true,\n describe: \"The path to the server file\",\n type: \"string\",\n })\n\n .option(\"strict\", {\n alias: \"s\",\n default: false,\n describe: \"Enable strict validation (type checking)\",\n type: \"boolean\",\n })\n },\n\n async (argv) => {\n try {\n const { existsSync } = await import(\"fs\")\n const { resolve } = await import(\"path\")\n const filePath = resolve(argv.file)\n\n if (!existsSync(filePath)) {\n console.error(`[FastMCP Error] File not found: ${filePath}`)\n process.exit(1)\n }\n\n console.log(`[FastMCP] Validating server file: ${filePath}`)\n\n const command = argv.strict ? `npx tsc --noEmit --strict ${filePath}` : `npx tsc --noEmit ${filePath}`\n\n try {\n await execa({\n shell: true,\n stderr: \"pipe\",\n stdout: \"pipe\",\n })`${command}`\n\n console.log(\"[FastMCP] ✓ TypeScript compilation successful\")\n } catch (tsError) {\n console.error(\"[FastMCP] ✗ TypeScript compilation failed\")\n\n if (tsError instanceof Error && \"stderr\" in tsError) {\n console.error(tsError.stderr)\n }\n\n process.exit(1)\n }\n\n try {\n await execa({\n shell: true,\n stderr: \"pipe\",\n stdout: \"pipe\",\n })`node -e \"\n (async () => {\n try {\n const { FastMCP } = await import('fastmcp');\n await import('file://${filePath}');\n console.log('[FastMCP] ✓ Server structure validation passed');\n } catch (error) {\n console.error('[FastMCP] ✗ Server structure validation failed:', error.message);\n process.exit(1);\n }\n })();\n \"`\n } catch {\n console.error(\"[FastMCP] ✗ Server structure validation failed\")\n console.error(\"Make sure the file properly imports and uses FastMCP\")\n\n process.exit(1)\n }\n\n console.log(\"[FastMCP] ✓ All validations passed! Server file looks good.\")\n } catch (error) {\n console.error(\"[FastMCP Error] Validation failed:\", error instanceof Error ? error.message : String(error))\n\n process.exit(1)\n }\n },\n )\n\n .help()\n .parseAsync()\n"],"mappings":"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","names":["yargs"]} \ No newline at end of file diff --git a/src/FastMCP.ts b/src/FastMCP.ts index 72ba505..d382cfc 100644 --- a/src/FastMCP.ts +++ b/src/FastMCP.ts @@ -1,8 +1,8 @@ -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 { 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, @@ -25,252 +25,245 @@ import { RootsListChangedNotificationSchema, 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 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"; +} 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 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" export interface Logger { - debug(...args: unknown[]): void; - error(...args: unknown[]): void; - info(...args: unknown[]): void; - log(...args: unknown[]): void; - warn(...args: unknown[]): void; + 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; -}; + close: () => Promise +} type FastMCPEvents = { - connect: (event: { session: FastMCPSession }) => void; - disconnect: (event: { session: FastMCPSession }) => void; -}; + connect: (event: { session: FastMCPSession }) => void + disconnect: (event: { session: FastMCPSession }) => void +} type FastMCPSessionEvents = { - error: (event: { error: Error }) => void; - ready: () => void; - rootsChanged: (event: { roots: Root[] }) => void; -}; + 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; + let rawData: Buffer try { if ("url" in input) { try { - const response = await fetch(input.url); + const response = await fetch(input.url) if (!response.ok) { - throw new Error( - `Server responded with status: ${response.status} - ${response.statusText}`, - ); + throw new Error(`Server responded with status: ${response.status} - ${response.statusText}`) } - rawData = Buffer.from(await response.arrayBuffer()); + 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) - }`, - ); + `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); + 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) - }`, - ); + `Failed to read image from path (${input.path}): ${error instanceof Error ? error.message : String(error)}`, + ) } } else if ("buffer" in input) { - rawData = input.buffer; + rawData = input.buffer } else { - throw new Error( - "Invalid input: Provide a valid 'url', 'path', or 'buffer'", - ); + throw new Error("Invalid input: Provide a valid 'url', 'path', or 'buffer'") } - const { fileTypeFromBuffer } = await import("file-type"); - const mimeType = await fileTypeFromBuffer(rawData); + 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" - }`, - ); + console.warn(`Warning: Content may not be a valid image. Detected MIME: ${mimeType?.mime || "unknown"}`) } - const base64Data = rawData.toString("base64"); + const base64Data = rawData.toString("base64") return { data: base64Data, mimeType: mimeType?.mime ?? "image/png", type: "image", - } as const; + } as const } catch (error) { if (error instanceof Error) { - throw error; + throw error } else { - throw new Error(`Unexpected error processing image: ${String(error)}`); + throw new Error(`Unexpected error processing image: ${String(error)}`) } } -}; +} export const audioContent = async ( input: { buffer: Buffer } | { path: string } | { url: string }, ): Promise => { - let rawData: Buffer; + let rawData: Buffer try { if ("url" in input) { try { - const response = await fetch(input.url); + const response = await fetch(input.url) if (!response.ok) { - throw new Error( - `Server responded with status: ${response.status} - ${response.statusText}`, - ); + throw new Error(`Server responded with status: ${response.status} - ${response.statusText}`) } - rawData = Buffer.from(await response.arrayBuffer()); + 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) - }`, - ); + `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); + 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) - }`, - ); + `Failed to read audio from path (${input.path}): ${error instanceof Error ? error.message : String(error)}`, + ) } } else if ("buffer" in input) { - rawData = input.buffer; + rawData = input.buffer } else { - throw new Error( - "Invalid input: Provide a valid 'url', 'path', or 'buffer'", - ); + throw new Error("Invalid input: Provide a valid 'url', 'path', or 'buffer'") } - const { fileTypeFromBuffer } = await import("file-type"); - const mimeType = await fileTypeFromBuffer(rawData); + 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" - }`, - ); + console.warn(`Warning: Content may not be a valid audio file. Detected MIME: ${mimeType?.mime || "unknown"}`) } - const base64Data = rawData.toString("base64"); + const base64Data = rawData.toString("base64") return { data: base64Data, mimeType: mimeType?.mime ?? "audio/mpeg", type: "audio", - } as const; + } as const } catch (error) { if (error instanceof Error) { - throw error; + throw error } else { - throw new Error(`Unexpected error processing audio: ${String(error)}`); + throw new Error(`Unexpected error processing audio: ${String(error)}`) } } -}; +} type Context = { client: { - version: ReturnType; - }; + 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; + 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; - requestMetadata?: RequestMeta; - session: T | undefined; + requestId?: string + requestMetadata?: RequestMeta + 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; -}; + sessionId?: string + streamContent: (content: Content | Content[]) => Promise +} -type Extra = unknown; +type Extra = unknown -type Extras = Record; +type Extras = Record -type Literal = boolean | null | number | string | undefined; +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; + progress: number /** * Total number of items to process (or total progress required), if known. */ - total?: number; -}; + total?: number +} -type SerializableValue = - | { [key: string]: SerializableValue } - | Literal - | SerializableValue[]; +type SerializableValue = { [key: string]: SerializableValue } | Literal | SerializableValue[] type TextContent = { - text: string; - type: "text"; -}; + text: string + type: "text" +} -type ToolParameters = StandardSchemaV1; +type ToolParameters = StandardSchemaV1 abstract class FastMCPError extends Error { public constructor(message?: string) { - super(message); - this.name = new.target.name; + super(message) + this.name = new.target.name + } +} + +/** + * Custom MCP error with a marker property to enable robust error detection + * across module boundaries. + * + * This class extends McpError and adds a `__isMcpError` marker property. + * This allows error detection to work even when instanceof fails due to + * module instances being loaded multiple times (e.g., with tsx, different + * bundlers, or ESM/CommonJS mixing). + * + * Use this instead of McpError when you need the error to be re-thrown + * by FastMCP's error handling rather than wrapped in a result. + */ +export class CustomMcpError extends McpError { + readonly __isMcpError = true + + constructor(code: number, message: string, data?: unknown) { + super(code, message, data) } } export class UnexpectedStateError extends FastMCPError { - public extras?: Extras; + public extras?: Extras public constructor(message: string, extras?: Extras) { - super(message); - this.name = new.target.name; - this.extras = extras; + super(message) + this.name = new.target.name + this.extras = extras } } @@ -279,6 +272,23 @@ export class UnexpectedStateError extends FastMCPError { */ export class UserError extends UnexpectedStateError {} +/** + * Type guard to check if an error should be re-thrown as an MCP error. + * Works across module boundaries by checking both instanceof and marker property. + * + * @param error - The error to check + * @returns true if error is an McpError or has the __isMcpError marker + */ +export function isMcpErrorLike(error: unknown): error is McpError { + return ( + error instanceof McpError || + (typeof error === "object" && + error !== null && + "__isMcpError" in error && + (error as { __isMcpError?: boolean }).__isMcpError === true) + ) +} + const TextContentZodSchema = z .object({ /** @@ -287,13 +297,13 @@ const TextContentZodSchema = z text: z.string(), type: z.literal("text"), }) - .strict() satisfies z.ZodType; + .strict() satisfies z.ZodType type ImageContent = { - data: string; - mimeType: string; - type: "image"; -}; + data: string + mimeType: string + type: "image" +} const ImageContentZodSchema = z .object({ @@ -307,13 +317,13 @@ const ImageContentZodSchema = z mimeType: z.string(), type: z.literal("image"), }) - .strict() satisfies z.ZodType; + .strict() satisfies z.ZodType type AudioContent = { - data: string; - mimeType: string; - type: "audio"; -}; + data: string + mimeType: string + type: "audio" +} const AudioContentZodSchema = z .object({ @@ -324,17 +334,17 @@ const AudioContentZodSchema = z mimeType: z.string(), type: z.literal("audio"), }) - .strict() satisfies z.ZodType; + .strict() satisfies z.ZodType type ResourceContent = { resource: { - blob?: string; - mimeType?: string; - text?: string; - uri: string; - }; - type: "resource"; -}; + blob?: string + mimeType?: string + text?: string + uri: string + } + type: "resource" +} const ResourceContentZodSchema = z .object({ @@ -346,7 +356,7 @@ const ResourceContentZodSchema = z }), type: z.literal("resource"), }) - .strict() satisfies z.ZodType; + .strict() satisfies z.ZodType const ResourceLinkZodSchema = z.object({ description: z.string().optional(), @@ -355,14 +365,9 @@ const ResourceLinkZodSchema = z.object({ title: z.string().optional(), type: z.literal("resource_link"), uri: z.string(), -}) satisfies z.ZodType; +}) satisfies z.ZodType -type Content = - | AudioContent - | ImageContent - | ResourceContent - | ResourceLink - | TextContent; +type Content = AudioContent | ImageContent | ResourceContent | ResourceLink | TextContent const ContentZodSchema = z.discriminatedUnion("type", [ TextContentZodSchema, @@ -370,13 +375,13 @@ const ContentZodSchema = z.discriminatedUnion("type", [ AudioContentZodSchema, ResourceContentZodSchema, ResourceLinkZodSchema, -]) satisfies z.ZodType; +]) satisfies z.ZodType type ContentResult = { - _meta?: Record; - content: Content[]; - isError?: boolean; -}; + _meta?: Record + content: Content[] + isError?: boolean +} const ContentResultZodSchema = z .object({ @@ -384,13 +389,13 @@ const ContentResultZodSchema = z content: ContentZodSchema.array(), isError: z.boolean().optional(), }) - .strict() satisfies z.ZodType; + .strict() satisfies z.ZodType type Completion = { - hasMore?: boolean; - total?: number; - values: string[]; -}; + hasMore?: boolean + total?: number + values: string[] +} /** * https://github.com/modelcontextprotocol/typescript-sdk/blob/3164da64d085ec4e022ae881329eee7b72f208d4/src/types.ts#L983-L1003 @@ -408,159 +413,133 @@ const CompletionZodSchema = z.object({ * An array of completion values. Must not exceed 100 items. */ values: z.array(z.string()).max(100), -}) satisfies z.ZodType; +}) satisfies z.ZodType -type ArgumentValueCompleter = - (value: string, auth?: T) => Promise; +type ArgumentValueCompleter = ( + value: string, + auth?: T, +) => Promise type InputPrompt< T extends FastMCPSessionAuth = FastMCPSessionAuth, Arguments extends InputPromptArgument[] = InputPromptArgument[], Args = PromptArgumentsToObject, > = { - arguments?: InputPromptArgument[]; - description?: string; - load: (args: Args, auth?: T) => Promise; - name: string; -}; - -type InputPromptArgument = - Readonly<{ - complete?: ArgumentValueCompleter; - description?: string; - enum?: string[]; - name: string; - required?: boolean; - }>; + arguments?: InputPromptArgument[] + 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 extends InputResourceTemplateArgument[] = InputResourceTemplateArgument[], > = { - arguments: Arguments; - 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"; + arguments: Arguments + description?: string + load: (args: ResourceTemplateArgumentsToObject, auth?: T) => Promise + mimeType?: string + name: string + uriTemplate: string +} + +type InputResourceTemplateArgument = 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; + 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["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; -}; + 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; + blob: string + mimeType?: string + uri?: string } | { - mimeType?: string; - text: string; - uri?: string; - }; + mimeType?: string + text: string + uri?: string + } type ResourceTemplate< T extends FastMCPSessionAuth, - Arguments extends - ResourceTemplateArgument[] = ResourceTemplateArgument[], + 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; -}>; + 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 = Readonly<{ + complete?: ArgumentValueCompleter + description?: string + name: string + required?: boolean +}> type ResourceTemplateArgumentsToObject = { - [K in T[number]["name"]]: string; -}; + [K in T[number]["name"]]: string +} type SamplingResponse = { - content: AudioContent | ImageContent | TextContent; - model: string; - role: "assistant" | "user"; - stopReason?: "endTurn" | "maxTokens" | "stopSequence" | string; -}; + content: AudioContent | ImageContent | TextContent + model: string + role: "assistant" | "user" + stopReason?: "endTurn" | "maxTokens" | "stopSequence" | string +} type ServerOptions = { - authenticate?: Authenticate; + 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 @@ -576,33 +555,33 @@ type ServerOptions = { * When set to `false` the health-check endpoint is disabled. * @default true */ - enabled?: boolean; + enabled?: boolean /** * Plain-text body returned by the endpoint. * @default "ok" */ - message?: string; + message?: string /** * HTTP path that should be handled. * @default "/health" */ - path?: string; + path?: string /** * HTTP response status that will be returned. * @default 200 */ - status?: number; - }; - instructions?: string; + 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; + logger?: Logger + name: string /** * Configuration for OAuth well-known discovery endpoints that can be exposed @@ -624,36 +603,36 @@ type ServerOptions = { * Required by MCP Specification 2025-03-26 */ authorizationServer?: { - authorizationEndpoint: string; - codeChallengeMethodsSupported?: string[]; + authorizationEndpoint: string + codeChallengeMethodsSupported?: string[] // DPoP support - dpopSigningAlgValuesSupported?: string[]; - grantTypesSupported?: string[]; + dpopSigningAlgValuesSupported?: string[] + grantTypesSupported?: string[] - introspectionEndpoint?: string; + introspectionEndpoint?: string // Required - issuer: string; + 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[]; - }; + 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; + enabled: boolean /** * OAuth Protected Resource metadata for `/.well-known/oauth-protected-resource` @@ -683,7 +662,7 @@ type ServerOptions = { * @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; + [key: string]: unknown /** * Supported values for the `authorization_details` parameter (RFC 9396). @@ -691,7 +670,7 @@ type ServerOptions = { * @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[]; + authorizationDetailsTypesSupported?: string[] /** * List of OAuth 2.0 authorization server issuer identifiers. @@ -704,7 +683,7 @@ type ServerOptions = { * 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[]; + authorizationServers: string[] /** * List of supported methods for presenting OAuth 2.0 bearer tokens. @@ -714,7 +693,7 @@ type ServerOptions = { * 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[]; + bearerMethodsSupported?: string[] /** * Whether this resource requires all access tokens to be DPoP-bound. @@ -722,14 +701,14 @@ type ServerOptions = { * @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; + 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[]; + dpopSigningAlgValuesSupported?: string[] /** * JWKS URI of this resource. Used to validate access tokens or sign responses. @@ -737,7 +716,7 @@ type ServerOptions = { * @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; + jwksUri?: string /** * Canonical OAuth resource identifier for this protected resource (the MCP server). @@ -746,7 +725,7 @@ type ServerOptions = { * `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; + resource: string /** * URL to developer-accessible documentation for this resource. @@ -754,7 +733,7 @@ type ServerOptions = { * @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; + resourceDocumentation?: string /** * Human-readable name for display purposes (e.g., in UIs). @@ -762,7 +741,7 @@ type ServerOptions = { * @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; + resourceName?: string /** * URL to a human-readable policy page describing acceptable use. @@ -770,7 +749,7 @@ type ServerOptions = { * @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; + resourcePolicyUri?: string /** * Supported JWS algorithms for signed responses from this resource (e.g., response signing). @@ -778,7 +757,7 @@ type ServerOptions = { * @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[]; + resourceSigningAlgValuesSupported?: string[] /** * URL to the protected resource’s Terms of Service. @@ -786,7 +765,7 @@ type ServerOptions = { * @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; + resourceTosUri?: string /** * Supported OAuth scopes for requesting access to this resource. @@ -794,7 +773,7 @@ type ServerOptions = { * @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[]; + scopesSupported?: string[] /** * Developer-accessible documentation for how to use the service (not end-user docs). @@ -803,7 +782,7 @@ type ServerOptions = { * 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; + serviceDocumentation?: string /** * Whether mutual-TLS-bound access tokens are required. @@ -811,9 +790,9 @@ type ServerOptions = { * @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; - }; - }; + tlsClientCertificateBoundAccessTokens?: boolean + } + } ping?: { /** @@ -821,18 +800,18 @@ type ServerOptions = { * - true for SSE or HTTP Stream * - false for stdio */ - enabled?: boolean; + enabled?: boolean /** * Interval * @default 5000 (5s) */ - intervalMs?: number; + intervalMs?: number /** * Logging level for ping-related messages. * @default 'debug' */ - logLevel?: LoggingLevel; - }; + logLevel?: LoggingLevel + } /** * Configuration for roots capability */ @@ -842,50 +821,38 @@ type ServerOptions = { * Set to false to completely disable roots support * @default true */ - enabled?: boolean; - }; + enabled?: boolean + } /** * General utilities */ utils?: { - formatInvalidParamsErrorMessage?: ( - issues: readonly StandardSchemaV1.Issue[], - ) => string; - }; - version: `${number}.${number}.${number}`; -}; - -type Tool< - T extends FastMCPSessionAuth, - Params extends ToolParameters = ToolParameters, -> = { + formatInvalidParamsErrorMessage?: (issues: readonly StandardSchemaV1.Issue[]) => string + } + version: `${number}.${number}.${number}` +} + +type Tool = { 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; + 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; - parameters?: Params; - timeoutMs?: number; -}; + AudioContent | ContentResult | ImageContent | ResourceContent | ResourceLink | string | TextContent | void + > + name: string + parameters?: Params + timeoutMs?: number +} /** * Tool annotations as defined in MCP Specification (2025-03-26) @@ -897,97 +864,95 @@ type ToolAnnotations = { * Only meaningful when readOnlyHint is false * @default true */ - destructiveHint?: boolean; + 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; + idempotentHint?: boolean /** * If true, the tool may interact with an "open world" of external entities * @default true */ - openWorldHint?: boolean; + openWorldHint?: boolean /** * If true, indicates the tool does not modify its environment * @default false */ - readOnlyHint?: boolean; + readOnlyHint?: boolean /** * A human-readable title for the tool, useful for UI display */ - title?: string; -}; + title?: string +} const FastMCPSessionEventEmitterBase: { - new (): StrictEventEmitter; -} = EventEmitter; + new (): StrictEventEmitter +} = EventEmitter -type Authenticate = (request: http.IncomingMessage) => Promise; +type Authenticate = (request: http.IncomingMessage) => Promise -type FastMCPSessionAuth = Record | undefined; +type FastMCPSessionAuth = Record | undefined class FastMCPSessionEventEmitter extends FastMCPSessionEventEmitterBase {} -export class FastMCPSession< - T extends FastMCPSessionAuth = FastMCPSessionAuth, -> extends FastMCPSessionEventEmitter { +export class FastMCPSession extends FastMCPSessionEventEmitter { public get clientCapabilities(): ClientCapabilities | null { - return this.#clientCapabilities ?? null; + return this.#clientCapabilities ?? null } public get isReady(): boolean { - return this.#connectionState === "ready"; + return this.#connectionState === "ready" } public get loggingLevel(): LoggingLevel { - return this.#loggingLevel; + return this.#loggingLevel } public get roots(): Root[] { - return this.#roots; + return this.#roots } public get server(): Server { - return this.#server; + return this.#server } public get sessionId(): string | undefined { - return this.#sessionId; + return this.#sessionId } public set sessionId(value: string | undefined) { - this.#sessionId = value; + 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"]; + #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; + #pingInterval: null | ReturnType = null - #prompts: Prompt[] = []; + #prompts: Prompt[] = [] - #resources: Resource[] = []; + #resources: Resource[] = [] - #resourceTemplates: ResourceTemplate[] = []; + #resourceTemplates: ResourceTemplate[] = [] - #roots: Root[] = []; + #roots: Root[] = [] - #rootsConfig?: ServerOptions["roots"]; + #rootsConfig?: ServerOptions["roots"] - #server: Server; + #server: Server /** * Session ID from the Mcp-Session-Id header (HTTP transports only). * Used to track per-session state across multiple requests. */ - #sessionId?: string; + #sessionId?: string - #utils?: ServerOptions["utils"]; + #utils?: ServerOptions["utils"] constructor({ auth, @@ -1005,138 +970,138 @@ export class FastMCPSession< 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; + 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(); + super() - this.#auth = auth; - this.#logger = logger; - this.#pingConfig = ping; - this.#rootsConfig = roots; - this.#sessionId = sessionId; - this.#needsEventLoopFlush = transportType === "httpStream"; + 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 = {}; + this.#capabilities.tools = {} } if (resources.length || resourcesTemplates.length) { - this.#capabilities.resources = {}; + this.#capabilities.resources = {} } if (prompts.length) { for (const prompt of prompts) { - this.addPrompt(prompt); + this.addPrompt(prompt) } - this.#capabilities.prompts = {}; + this.#capabilities.prompts = {} } - this.#capabilities.logging = {}; + this.#capabilities.logging = {} this.#server = new Server( { name: name, version: version }, { capabilities: this.#capabilities, instructions: instructions }, - ); + ) - this.#utils = utils; + this.#utils = utils - this.setupErrorHandling(); - this.setupLoggingHandlers(); - this.setupRootsHandlers(); - this.setupCompleteHandlers(); + this.setupErrorHandling() + this.setupLoggingHandlers() + this.setupRootsHandlers() + this.setupCompleteHandlers() if (tools.length) { - this.setupToolHandlers(tools); + this.setupToolHandlers(tools) } if (resources.length || resourcesTemplates.length) { for (const resource of resources) { - this.addResource(resource); + this.addResource(resource) } - this.setupResourceHandlers(resources); + this.setupResourceHandlers(resources) if (resourcesTemplates.length) { for (const resourceTemplate of resourcesTemplates) { - this.addResourceTemplate(resourceTemplate); + this.addResourceTemplate(resourceTemplate) } - this.setupResourceTemplateHandlers(resourcesTemplates); + this.setupResourceTemplateHandlers(resourcesTemplates) } } if (prompts.length) { - this.setupPromptHandlers(prompts); + this.setupPromptHandlers(prompts) } } public async close() { - this.#connectionState = "closed"; + this.#connectionState = "closed" if (this.#pingInterval) { - clearInterval(this.#pingInterval); + clearInterval(this.#pingInterval) } try { - await this.#server.close(); + await this.#server.close() } catch (error) { - this.#logger.error("[FastMCP error]", "could not close server", 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"); + throw new UnexpectedStateError("Server is already connected") } - this.#connectionState = "connecting"; + this.#connectionState = "connecting" try { - await this.#server.connect(transport); + 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; + sessionId?: string + } & Transport if (typeof transportWithSessionId.sessionId === "string") { - this.#sessionId = transportWithSessionId.sessionId; + this.#sessionId = transportWithSessionId.sessionId } } - let attempt = 0; - const maxAttempts = 10; - const retryDelay = 100; + let attempt = 0 + const maxAttempts = 10 + const retryDelay = 100 while (attempt++ < maxAttempts) { - const capabilities = this.#server.getClientCapabilities(); + const capabilities = this.#server.getClientCapabilities() if (capabilities) { - this.#clientCapabilities = capabilities; - break; + this.#clientCapabilities = capabilities + break } - await delay(retryDelay); + await delay(retryDelay) } if (!this.#clientCapabilities) { this.#logger.warn( `[FastMCP warning] could not infer client capabilities after ${maxAttempts} attempts. Connection may be unstable.`, - ); + ) } if ( @@ -1145,64 +1110,56 @@ export class FastMCPSession< typeof this.#server.listRoots === "function" ) { try { - const roots = await this.#server.listRoots(); - this.#roots = roots?.roots || []; + 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", - ); + 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) - }`, - ); + `[FastMCP error] received error listing roots.\n\n${e instanceof Error ? e.stack : JSON.stringify(e)}`, + ) } } } if (this.#clientCapabilities) { - const pingConfig = this.#getPingConfig(transport); + const pingConfig = this.#getPingConfig(transport) if (pingConfig.enabled) { this.#pingInterval = setInterval(async () => { try { - await this.#server.ping(); + 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; + const logLevel = pingConfig.logLevel if (logLevel === "debug") { - this.#logger.debug("[FastMCP debug] server ping failed"); + this.#logger.debug("[FastMCP debug] server ping failed") } else if (logLevel === "warning") { - this.#logger.warn( - "[FastMCP warning] server is not responding to ping", - ); + 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", - ); + this.#logger.error("[FastMCP error] server is not responding to ping") } else { - this.#logger.info("[FastMCP info] server ping failed"); + this.#logger.info("[FastMCP info] server ping failed") } } - }, pingConfig.intervalMs); + }, pingConfig.intervalMs) } } // Mark connection as ready and emit event - this.#connectionState = "ready"; - this.emit("ready"); + this.#connectionState = "ready" + this.emit("ready") } catch (error) { - this.#connectionState = "error"; + this.#connectionState = "error" const errorEvent = { error: error instanceof Error ? error : new Error(String(error)), - }; - this.emit("error", errorEvent); - throw error; + } + this.emit("error", errorEvent) + throw error } } @@ -1210,84 +1167,74 @@ export class FastMCPSession< message: z.infer["params"], options?: RequestOptions, ): Promise { - return this.#server.createMessage(message, options); + return this.#server.createMessage(message, options) } public waitForReady(): Promise { if (this.isReady) { - return Promise.resolve(); + return Promise.resolve() } - if ( - this.#connectionState === "error" || - this.#connectionState === "closed" - ) { - return Promise.reject( - new Error(`Connection is in ${this.#connectionState} state`), - ); + 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); + reject(new Error("Connection timeout: Session failed to become ready within 5 seconds")) + }, 5000) this.once("ready", () => { - clearTimeout(timeout); - resolve(); - }); + clearTimeout(timeout) + resolve() + }) this.once("error", (event) => { - clearTimeout(timeout); - reject(event.error); - }); - }); + clearTimeout(timeout) + reject(event.error) + }) + }) } #getPingConfig(transport: Transport): { - enabled: boolean; - intervalMs: number; - logLevel: LoggingLevel; + enabled: boolean + intervalMs: number + logLevel: LoggingLevel } { - const pingConfig = this.#pingConfig || {}; + const pingConfig = this.#pingConfig || {} - let defaultEnabled = false; + let defaultEnabled = false if ("type" in transport) { // Enable by default for SSE and HTTP streaming if (transport.type === "httpStream") { - defaultEnabled = true; + defaultEnabled = true } } return { - enabled: - pingConfig.enabled !== undefined ? pingConfig.enabled : defaultEnabled, + 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> = {}; + const completers: Record> = {} + const enums: Record = {} + const fuseInstances: Record> = {} for (const argument of inputPrompt.arguments ?? []) { if (argument.complete) { - completers[argument.name] = argument.complete; + completers[argument.name] = argument.complete } if (argument.enum) { - enums[argument.name] = argument.enum; + enums[argument.name] = argument.enum fuseInstances[argument.name] = new Fuse(argument.enum, { includeScore: true, threshold: 0.3, // More flexible matching! - }); + }) } } @@ -1295,37 +1242,37 @@ export class FastMCPSession< ...inputPrompt, complete: async (name: string, value: string, auth?: T) => { if (completers[name]) { - return await completers[name](value, auth); + return await completers[name](value, auth) } if (fuseInstances[name]) { - const result = fuseInstances[name].search(value); + const result = fuseInstances[name].search(value) return { total: result.length, values: result.map((item) => item.item), - }; + } } return { values: [], - }; + } }, - }; + } - this.#prompts.push(prompt); + this.#prompts.push(prompt) } private addResource(inputResource: Resource) { - this.#resources.push(inputResource); + this.#resources.push(inputResource) } private addResourceTemplate(inputResourceTemplate: InputResourceTemplate) { - const completers: Record> = {}; + const completers: Record> = {} for (const argument of inputResourceTemplate.arguments ?? []) { if (argument.complete) { - completers[argument.name] = argument.complete; + completers[argument.name] = argument.complete } } @@ -1333,105 +1280,90 @@ export class FastMCPSession< ...inputResourceTemplate, complete: async (name: string, value: string, auth?: T) => { if (completers[name]) { - return await completers[name](value, auth); + return await completers[name](value, auth) } return { values: [], - }; + } }, - }; + } - this.#resourceTemplates.push(resourceTemplate); + this.#resourceTemplates.push(resourceTemplate) } private setupCompleteHandlers() { this.#server.setRequestHandler(CompleteRequestSchema, async (request) => { if (request.params.ref.type === "ref/prompt") { - const prompt = this.#prompts.find( - (prompt) => prompt.name === request.params.ref.name, - ); + const prompt = this.#prompts.find((prompt) => prompt.name === request.params.ref.name) if (!prompt) { throw new UnexpectedStateError("Unknown prompt", { request, - }); + }) } if (!prompt.complete) { throw new UnexpectedStateError("Prompt does not support completion", { request, - }); + }) } const completion = CompletionZodSchema.parse( - await prompt.complete( - request.params.argument.name, - request.params.argument.value, - this.#auth, - ), - ); + await prompt.complete(request.params.argument.name, request.params.argument.value, this.#auth), + ) return { completion, - }; + } } if (request.params.ref.type === "ref/resource") { - const resource = this.#resourceTemplates.find( - (resource) => resource.uriTemplate === request.params.ref.uri, - ); + const resource = this.#resourceTemplates.find((resource) => resource.uriTemplate === request.params.ref.uri) if (!resource) { throw new UnexpectedStateError("Unknown resource", { request, - }); + }) } if (!("uriTemplate" in resource)) { - throw new UnexpectedStateError("Unexpected resource"); + throw new UnexpectedStateError("Unexpected resource") } if (!resource.complete) { - throw new UnexpectedStateError( - "Resource does not support completion", - { - request, - }, - ); + 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, - ), - ); + 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); - }; + this.#logger.error("[FastMCP error]", error) + } } private setupLoggingHandlers() { this.#server.setRequestHandler(SetLevelRequestSchema, (request) => { - this.#loggingLevel = request.params.level; + this.#loggingLevel = request.params.level - return {}; - }); + return {} + }) } private setupPromptHandlers(prompts: Prompt[]) { @@ -1443,24 +1375,19 @@ export class FastMCPSession< complete: prompt.complete, description: prompt.description, name: prompt.name, - }; + } }), - }; - }); + } + }) this.#server.setRequestHandler(GetPromptRequestSchema, async (request) => { - const prompt = prompts.find( - (prompt) => prompt.name === request.params.name, - ); + const prompt = prompts.find((prompt) => prompt.name === request.params.name) if (!prompt) { - throw new McpError( - ErrorCode.MethodNotFound, - `Unknown prompt: ${request.params.name}`, - ); + throw new McpError(ErrorCode.MethodNotFound, `Unknown prompt: ${request.params.name}`) } - const args = request.params.arguments; + const args = request.params.arguments for (const arg of prompt.arguments ?? []) { if (arg.required && !(args && arg.name in args)) { @@ -1469,24 +1396,17 @@ export class FastMCPSession< `Prompt '${request.params.name}' requires argument '${arg.name}': ${ arg.description || "No description provided" }`, - ); + ) } } - let result: Awaited["load"]>>; + let result: Awaited["load"]>> try { - result = await prompt.load( - args as Record, - this.#auth, - ); + 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}`, - ); + 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") { @@ -1498,14 +1418,14 @@ export class FastMCPSession< role: "user", }, ], - }; + } } else { return { description: prompt.description, messages: result.messages, - }; + } } - }); + }) } private setupResourceHandlers(resources: Resource[]) { @@ -1517,157 +1437,129 @@ export class FastMCPSession< name: resource.name, uri: resource.uri, })), - } satisfies ListResourcesResult; - }); - - this.#server.setRequestHandler( - ReadResourceRequestSchema, - async (request) => { - if ("uri" in request.params) { - const resource = resources.find( - (resource) => - "uri" in resource && resource.uri === request.params.uri, - ); - - if (!resource) { - for (const resourceTemplate of this.#resourceTemplates) { - const uriTemplate = parseURITemplate( - resourceTemplate.uriTemplate, - ); - - const match = uriTemplate.fromUri(request.params.uri); - - if (!match) { - continue; - } + } satisfies ListResourcesResult + }) + + this.#server.setRequestHandler(ReadResourceRequestSchema, async (request) => { + if ("uri" in request.params) { + const resource = resources.find((resource) => "uri" in resource && resource.uri === request.params.uri) - const uri = uriTemplate.fill(match); + if (!resource) { + for (const resourceTemplate of this.#resourceTemplates) { + const uriTemplate = parseURITemplate(resourceTemplate.uriTemplate) - const result = await resourceTemplate.load(match, this.#auth); + const match = uriTemplate.fromUri(request.params.uri) - 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, - })), - }; + if (!match) { + continue } - throw new McpError( - ErrorCode.MethodNotFound, - `Resource not found: '${request.params.uri}'. Available resources: ${ - resources.map((r) => r.uri).join(", ") || "none" - }`, - ); - } + const uri = uriTemplate.fill(match) + + const result = await resourceTemplate.load(match, this.#auth) - if (!("uri" in resource)) { - throw new UnexpectedStateError("Resource does not support reading"); + 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, + })), + } } - let maybeArrayResult: Awaited["load"]>>; + throw new McpError( + ErrorCode.MethodNotFound, + `Resource not found: '${request.params.uri}'. Available resources: ${ + resources.map((r) => r.uri).join(", ") || "none" + }`, + ) + } - 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, - }, - ); - } + if (!("uri" in resource)) { + throw new UnexpectedStateError("Resource does not support reading") + } - const resourceResults = Array.isArray(maybeArrayResult) - ? maybeArrayResult - : [maybeArrayResult]; + let maybeArrayResult: Awaited["load"]>> - return { - contents: resourceResults.map((result) => ({ - ...result, - mimeType: result.mimeType ?? resource.mimeType, - name: resource.name, - uri: result.uri ?? resource.uri, - })), - }; + 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, + }, + ) } - throw new UnexpectedStateError("Unknown resource request", { - request, - }); - }, - ); - } + const resourceResults = Array.isArray(maybeArrayResult) ? maybeArrayResult : [maybeArrayResult] - private setupResourceTemplateHandlers( - resourceTemplates: ResourceTemplate[], - ) { - this.#server.setRequestHandler( - ListResourceTemplatesRequestSchema, - async () => { return { - resourceTemplates: resourceTemplates.map((resourceTemplate) => ({ - description: resourceTemplate.description, - mimeType: resourceTemplate.mimeType, - name: resourceTemplate.name, - uriTemplate: resourceTemplate.uriTemplate, + contents: resourceResults.map((result) => ({ + ...result, + mimeType: result.mimeType ?? resource.mimeType, + name: resource.name, + uri: result.uri ?? resource.uri, })), - } satisfies ListResourceTemplatesResult; - }, - ); + } + } + + throw new UnexpectedStateError("Unknown resource request", { + request, + }) + }) + } + + private setupResourceTemplateHandlers(resourceTemplates: ResourceTemplate[]) { + this.#server.setRequestHandler(ListResourceTemplatesRequestSchema, async () => { + return { + resourceTemplates: resourceTemplates.map((resourceTemplate) => ({ + description: resourceTemplate.description, + mimeType: resourceTemplate.mimeType, + name: resourceTemplate.name, + uriTemplate: resourceTemplate.uriTemplate, + })), + } satisfies ListResourceTemplatesResult + }) } private setupRootsHandlers() { if (this.#rootsConfig?.enabled === false) { - this.#logger.debug( - "[FastMCP debug] roots capability explicitly disabled via config", - ); - return; + 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, - }); + 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) - }`, - ); - } - }); - }, - ); + }) + .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", - ); + this.#logger.debug("[FastMCP debug] roots capability not available, not setting up notification handler") } } @@ -1687,51 +1579,46 @@ export class FastMCPSession< type: "object", }, // More complete schema for Cursor compatibility name: tool.name, - }; + } }), ), - }; - }); + } + }) this.#server.setRequestHandler(CallToolRequestSchema, async (request) => { - const tool = tools.find((tool) => tool.name === request.params.name); + const tool = tools.find((tool) => tool.name === request.params.name) if (!tool) { - throw new McpError( - ErrorCode.MethodNotFound, - `Unknown tool: ${request.params.name}`, - ); + throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${request.params.name}`) } - let args: unknown = undefined; + let args: unknown = undefined if (tool.parameters) { - const parsed = await tool.parameters["~standard"].validate( - request.params.arguments, - ); + 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}`; + const path = issue.path?.join(".") || "root" + return `${path}: ${issue.message}` }) - .join(", "); + .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; + args = parsed.value } - const progressToken = request.params?._meta?.progressToken; + const progressToken = request.params?._meta?.progressToken - let result: ContentResult; + let result: ContentResult try { const reportProgress = async (progress: Progress) => { @@ -1742,20 +1629,18 @@ export class FastMCPSession< ...progress, progressToken, }, - }); + }) if (this.#needsEventLoopFlush) { - await new Promise((resolve) => setImmediate(resolve)); + 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), - ); + progressError instanceof Error ? progressError.message : String(progressError), + ) } - }; + } const log = { debug: (message: string, context?: SerializableValue) => { @@ -1765,7 +1650,7 @@ export class FastMCPSession< message, }, level: "debug", - }); + }) }, error: (message: string, context?: SerializableValue) => { this.#server.sendLoggingMessage({ @@ -1774,7 +1659,7 @@ export class FastMCPSession< message, }, level: "error", - }); + }) }, info: (message: string, context?: SerializableValue) => { this.#server.sendLoggingMessage({ @@ -1783,7 +1668,7 @@ export class FastMCPSession< message, }, level: "info", - }); + }) }, warn: (message: string, context?: SerializableValue) => { this.#server.sendLoggingMessage({ @@ -1792,15 +1677,15 @@ export class FastMCPSession< 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]; + const contentArray = Array.isArray(content) ? content : [content] try { await this.#server.notification({ @@ -1809,35 +1694,30 @@ export class FastMCPSession< content: contentArray, toolName: request.params.name, }, - }); + }) if (this.#needsEventLoopFlush) { - await new Promise((resolve) => setImmediate(resolve)); + 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), - ); + 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, + requestId: typeof request.params?._meta?.requestId === "string" ? request.params._meta.requestId : undefined, requestMetadata: request.params._meta, session: this.#auth, sessionId: this.#sessionId, streamContent, - }); + }) // Handle timeout if specified const maybeStringResult = (await (tool.timeoutMs @@ -1850,11 +1730,11 @@ export class FastMCPSession< ErrorCode.InternalError, `Tool '${request.params.name}' timed out after ${tool.timeoutMs}ms. Consider increasing timeoutMs or optimizing the tool implementation.`, ), - ); - }, tool.timeoutMs); + ) + }, tool.timeoutMs) // If promise resolves first - executeToolPromise.finally(() => clearTimeout(timeoutId)); + executeToolPromise.finally(() => clearTimeout(timeoutId)) }), ]) : executeToolPromise)) as @@ -1866,31 +1746,32 @@ export class FastMCPSession< | ResourceLink | string | TextContent - | undefined; + | 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); + 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); + result = ContentResultZodSchema.parse(maybeStringResult) } } catch (error) { // Re-throw McpError to let the MCP SDK handle it as a proper JSON-RPC error - if (error instanceof McpError) { - throw error; + // Use type guard to handle instanceof failures across module boundaries + if (isMcpErrorLike(error)) { + throw error } if (error instanceof UserError) { @@ -1898,11 +1779,10 @@ export class FastMCPSession< content: [{ text: error.message, type: "text" }], isError: true, ...(error.extras ? { structuredContent: error.extras } : {}), - }; + } } - const errorMessage = - error instanceof Error ? error.message : String(error); + const errorMessage = error instanceof Error ? error.message : String(error) return { content: [ { @@ -1911,11 +1791,11 @@ export class FastMCPSession< }, ], isError: true, - }; + } } - return result; - }); + return result + }) } } @@ -1923,86 +1803,80 @@ export class FastMCPSession< * Converts camelCase to snake_case for OAuth endpoint responses */ function camelToSnakeCase(str: string): string { - return str.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`); + 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 = {}; +function convertObjectToSnakeCase(obj: Record): Record { + const result: Record = {} for (const [key, value] of Object.entries(obj)) { - const snakeKey = camelToSnakeCase(key); - result[snakeKey] = value; + const snakeKey = camelToSnakeCase(key) + result[snakeKey] = value } - return result; + return result } const FastMCPEventEmitterBase: { - new (): StrictEventEmitter>; -} = EventEmitter; + new (): StrictEventEmitter> +} = EventEmitter class FastMCPEventEmitter extends FastMCPEventEmitterBase {} -export class FastMCP< - T extends FastMCPSessionAuth = FastMCPSessionAuth, -> extends FastMCPEventEmitter { +export class FastMCP extends FastMCPEventEmitter { public get sessions(): FastMCPSession[] { - return this.#sessions; + return this.#sessions } - #authenticate: Authenticate | undefined; - #httpStreamServer: null | SSEServer = null; - #logger: Logger; - #options: ServerOptions; - #prompts: InputPrompt[] = []; - #resources: Resource[] = []; - #resourcesTemplates: InputResourceTemplate[] = []; - #sessions: FastMCPSession[] = []; + #authenticate: Authenticate | undefined + #httpStreamServer: null | SSEServer = null + #logger: Logger + #options: ServerOptions + #prompts: InputPrompt[] = [] + #resources: Resource[] = [] + #resourcesTemplates: InputResourceTemplate[] = [] + #sessions: FastMCPSession[] = [] - #tools: Tool[] = []; + #tools: Tool[] = [] constructor(public options: ServerOptions) { - super(); + super() - this.#options = options; - this.#authenticate = options.authenticate; - this.#logger = options.logger || console; + this.#options = options + this.#authenticate = options.authenticate + this.#logger = options.logger || console } /** * Adds a prompt to the server. */ - public addPrompt[]>( - prompt: InputPrompt, - ) { - this.#prompts.push(prompt); + public addPrompt[]>(prompt: InputPrompt) { + this.#prompts.push(prompt) } /** * Adds a resource to the server. */ public addResource(resource: Resource) { - this.#resources.push(resource); + this.#resources.push(resource) } /** * Adds a resource template to the server. */ - public addResourceTemplate< - const Args extends InputResourceTemplateArgument[], - >(resource: InputResourceTemplate) { - this.#resourcesTemplates.push(resource); + public addResourceTemplate( + resource: InputResourceTemplate, + ) { + this.#resourcesTemplates.push(resource) } /** * Adds a tool to the server. */ public addTool(tool: Tool) { - this.#tools.push(tool as unknown as Tool); + this.#tools.push(tool as unknown as Tool) } /** @@ -2013,60 +1887,56 @@ export class FastMCP< */ public async embedded(uri: string): Promise { // First, try to find a direct resource match - const directResource = this.#resources.find( - (resource) => resource.uri === 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 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; + resourceData.text = firstResult.text } if ("blob" in firstResult) { - resourceData.blob = firstResult.blob; + resourceData.blob = firstResult.blob } - return resourceData; + return resourceData } // Try to match against resource templates for (const template of this.#resourcesTemplates) { - const parsedTemplate = parseURITemplate(template.uriTemplate); - const params = parsedTemplate.fromUri(uri); + const parsedTemplate = parseURITemplate(template.uriTemplate) + const params = parsedTemplate.fromUri(uri) if (!params) { - continue; + continue } - const result = await template.load( - params as ResourceTemplateArgumentsToObject, - ); + const result = await template.load(params as ResourceTemplateArgumentsToObject) const resourceData: ResourceContent["resource"] = { mimeType: template.mimeType, uri, - }; + } if ("text" in result) { - resourceData.text = result.text; + resourceData.text = result.text } if ("blob" in result) { - resourceData.blob = result.blob; + resourceData.blob = result.blob } - return resourceData; // The resource we're looking for + return resourceData // The resource we're looking for } - throw new UnexpectedStateError(`Resource not found: ${uri}`, { uri }); + throw new UnexpectedStateError(`Resource not found: ${uri}`, { uri }) } /** @@ -2075,35 +1945,33 @@ export class FastMCP< public async start( options?: Partial<{ httpStream: { - enableJsonResponse?: boolean; - endpoint?: `/${string}`; - eventStore?: EventStore; - host?: string; - port: number; - stateless?: boolean; - }; - transportType: "httpStream" | "stdio"; + enableJsonResponse?: boolean + endpoint?: `/${string}` + eventStore?: EventStore + host?: string + port: number + stateless?: boolean + } + transportType: "httpStream" | "stdio" }>, ) { - const config = this.#parseRuntimeConfig(options); + const config = this.#parseRuntimeConfig(options) if (config.transportType === "stdio") { - const transport = new StdioServerTransport(); + 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; + let auth: T | undefined if (this.#authenticate) { try { - auth = await this.#authenticate( - undefined as unknown as http.IncomingMessage, - ); + 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 } } @@ -2122,68 +1990,68 @@ export class FastMCP< transportType: "stdio", utils: this.#options.utils, version: this.#options.version, - }); + }) - await session.connect(transport); + await session.connect(transport) - this.#sessions.push(session); + this.#sessions.push(session) session.once("error", () => { - this.#removeSession(session); - }); + this.#removeSession(session) + }) // Monitor the underlying transport for close events if (transport.onclose) { - const originalOnClose = transport.onclose; + const originalOnClose = transport.onclose transport.onclose = () => { - this.#removeSession(session); + this.#removeSession(session) if (originalOnClose) { - originalOnClose(); + originalOnClose() } - }; + } } else { transport.onclose = () => { - this.#removeSession(session); - }; + this.#removeSession(session) + } } this.emit("connect", { session: session as FastMCPSession, - }); + }) } else if (config.transportType === "httpStream") { - const httpConfig = config.httpStream; + const httpConfig = config.httpStream 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 http://${httpConfig.host}:${httpConfig.port}${httpConfig.endpoint}`, - ); + ) this.#httpStreamServer = await startHTTPServer>({ ...(this.#authenticate ? { authenticate: this.#authenticate } : {}), createServer: async (request) => { - let auth: T | undefined; + let auth: T | undefined if (this.#authenticate) { - auth = await this.#authenticate(request); + 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"); + 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"]; + : 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); + return this.#createSession(auth, sessionId) }, enableJsonResponse: httpConfig.enableJsonResponse, eventStore: httpConfig.eventStore, @@ -2194,76 +2062,69 @@ export class FastMCP< }, onConnect: async () => { // No persistent session tracking in stateless mode - this.#logger.debug( - `[FastMCP debug] Stateless HTTP Stream request handled`, - ); + this.#logger.debug(`[FastMCP debug] Stateless HTTP Stream request handled`) }, onUnhandledRequest: async (req, res) => { - await this.#handleUnhandledRequest(req, res, true, httpConfig.host); + await this.#handleUnhandledRequest(req, res, true, httpConfig.host) }, port: httpConfig.port, 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; + let auth: T | undefined if (this.#authenticate) { - auth = await this.#authenticate(request); + 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"]; + : request.headers["mcp-session-id"] - return this.#createSession(auth, sessionId); + return this.#createSession(auth, sessionId) }, enableJsonResponse: httpConfig.enableJsonResponse, eventStore: httpConfig.eventStore, host: httpConfig.host, onClose: async (session) => { - const sessionIndex = this.#sessions.indexOf(session); + const sessionIndex = this.#sessions.indexOf(session) - if (sessionIndex !== -1) this.#sessions.splice(sessionIndex, 1); + if (sessionIndex !== -1) this.#sessions.splice(sessionIndex, 1) this.emit("disconnect", { session: session as FastMCPSession, - }); + }) }, onConnect: async (session) => { - this.#sessions.push(session); + this.#sessions.push(session) - this.#logger.info(`[FastMCP info] HTTP Stream session established`); + 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, - ); + await this.#handleUnhandledRequest(req, res, false, httpConfig.host) }, port: httpConfig.port, stateless: httpConfig.stateless, streamEndpoint: httpConfig.endpoint, - }); + }) this.#logger.info( `[FastMCP info] server is running on HTTP Stream at http://${httpConfig.host}:${httpConfig.port}${httpConfig.endpoint}`, - ); + ) } } else { - throw new Error("Invalid transport type"); + throw new Error("Invalid transport type") } } @@ -2272,7 +2133,7 @@ export class FastMCP< */ public async stop() { if (this.#httpStreamServer) { - await this.#httpStreamServer.close(); + await this.#httpStreamServer.close() } } @@ -2289,18 +2150,15 @@ export class FastMCP< !(auth as { authenticated: unknown }).authenticated ) { const errorMessage = - "error" in auth && - typeof (auth as { error: unknown }).error === "string" + "error" in auth && typeof (auth as { error: unknown }).error === "string" ? (auth as { error: string }).error - : "Authentication failed"; - throw new Error(errorMessage); + : "Authentication failed" + throw new Error(errorMessage) } const allowedTools = auth - ? this.#tools.filter((tool) => - tool.canAccess ? tool.canAccess(auth) : true, - ) - : this.#tools; + ? this.#tools.filter((tool) => (tool.canAccess ? tool.canAccess(auth) : true)) + : this.#tools return new FastMCPSession({ auth, instructions: this.#options.instructions, @@ -2316,7 +2174,7 @@ export class FastMCP< transportType: "httpStream", utils: this.#options.utils, version: this.#options.version, - }); + }) } /** @@ -2328,14 +2186,13 @@ export class FastMCP< isStateless = false, host: string, ) => { - const healthConfig = this.#options.health ?? {}; + const healthConfig = this.#options.health ?? {} - const enabled = - healthConfig.enabled === undefined ? true : healthConfig.enabled; + const enabled = healthConfig.enabled === undefined ? true : healthConfig.enabled if (enabled) { - const path = healthConfig.path ?? "/health"; - const url = new URL(req.url || "", `http://${host}`); + const path = healthConfig.path ?? "/health" + const url = new URL(req.url || "", `http://${host}`) try { if (req.method === "GET" && url.pathname === path) { @@ -2343,9 +2200,9 @@ export class FastMCP< .writeHead(healthConfig.status ?? 200, { "Content-Type": "text/plain", }) - .end(healthConfig.message ?? "✓ Ok"); + .end(healthConfig.message ?? "✓ Ok") - return; + return } // Enhanced readiness check endpoint @@ -2357,151 +2214,123 @@ export class FastMCP< ready: 1, status: "ready", total: 1, - }; + } res .writeHead(200, { "Content-Type": "application/json", }) - .end(JSON.stringify(response)); + .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 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", + status: allReady ? "ready" : totalSessions === 0 ? "no_sessions" : "initializing", total: totalSessions, - }; + } res .writeHead(allReady ? 200 : 503, { "Content-Type": "application/json", }) - .end(JSON.stringify(response)); + .end(JSON.stringify(response)) } - return; + return } } catch (error) { - this.#logger.error("[FastMCP error] health endpoint error", error); + this.#logger.error("[FastMCP error] health endpoint error", error) } } // Handle OAuth well-known endpoints - const oauthConfig = this.#options.oauth; + const oauthConfig = this.#options.oauth if (oauthConfig?.enabled && req.method === "GET") { - const url = new URL(req.url || "", `http://${host}`); + const url = new URL(req.url || "", `http://${host}`) - if ( - url.pathname === "/.well-known/oauth-authorization-server" && - oauthConfig.authorizationServer - ) { - const metadata = convertObjectToSnakeCase( - oauthConfig.authorizationServer, - ); + 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; + .end(JSON.stringify(metadata)) + return } - if ( - url.pathname === "/.well-known/oauth-protected-resource" && - oauthConfig.protectedResource - ) { - const metadata = convertObjectToSnakeCase( - oauthConfig.protectedResource, - ); + if (url.pathname === "/.well-known/oauth-protected-resource" && oauthConfig.protectedResource) { + const metadata = convertObjectToSnakeCase(oauthConfig.protectedResource) res .writeHead(200, { "Content-Type": "application/json", }) - .end(JSON.stringify(metadata)); - return; + .end(JSON.stringify(metadata)) + return } } // If the request was not handled above, return 404 - res.writeHead(404).end(); - }; + res.writeHead(404).end() + } #parseRuntimeConfig( overrides?: Partial<{ httpStream: { - enableJsonResponse?: boolean; - endpoint?: `/${string}`; - host?: string; - port: number; - stateless?: boolean; - }; - transportType: "httpStream" | "stdio"; + enableJsonResponse?: boolean + endpoint?: `/${string}` + host?: string + port: number + stateless?: boolean + } + transportType: "httpStream" | "stdio" }>, ): | { httpStream: { - enableJsonResponse?: boolean; - endpoint: `/${string}`; - eventStore?: EventStore; - host: string; - port: number; - stateless?: boolean; - }; - transportType: "httpStream"; + enableJsonResponse?: boolean + endpoint: `/${string}` + eventStore?: EventStore + host: string + port: number + stateless?: boolean + } + transportType: "httpStream" } | { transportType: "stdio" } { - const args = process.argv.slice(2); + 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; + 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"; + "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 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 return { httpStream: { @@ -2512,25 +2341,25 @@ export class FastMCP< stateless, }, transportType: "httpStream" as const, - }; + } } - return { transportType: "stdio" as const }; + return { transportType: "stdio" as const } } #removeSession(session: FastMCPSession): void { - const sessionIndex = this.#sessions.indexOf(session); + const sessionIndex = this.#sessions.indexOf(session) if (sessionIndex !== -1) { - this.#sessions.splice(sessionIndex, 1); + this.#sessions.splice(sessionIndex, 1) this.emit("disconnect", { session: session as FastMCPSession, - }); + }) } } } -export { ErrorCode, McpError } from "@modelcontextprotocol/sdk/types.js"; +export { ErrorCode, McpError } from "@modelcontextprotocol/sdk/types.js" export type { AudioContent, @@ -2558,4 +2387,4 @@ export type { TextContent, Tool, ToolParameters, -}; +} From 358855f0fb94c9537fd0f1c71c6ccb2a92725218 Mon Sep 17 00:00:00 2001 From: Pablo Carranza Velez Date: Wed, 10 Dec 2025 15:35:25 -0300 Subject: [PATCH 4/7] chore: apply prettier formatting --- README.md | 435 +++-- eslint.config.ts | 10 +- src/FastMCP.oauth.test.ts | 177 +- src/FastMCP.session-context.test.ts | 88 +- src/FastMCP.session-id.test.ts | 218 ++- src/FastMCP.test.ts | 2305 +++++++++++++-------------- src/bin/fastmcp.ts | 84 +- src/examples/addition.ts | 135 +- src/examples/custom-logger.ts | 28 +- src/examples/oauth-server.ts | 22 +- src/examples/session-context.ts | 113 +- src/examples/session-id-counter.ts | 98 +- vitest.config.js | 4 +- 13 files changed, 1750 insertions(+), 1967 deletions(-) diff --git a/README.md b/README.md index 679f29f..e616abf 100644 --- a/README.md +++ b/README.md @@ -69,13 +69,13 @@ npm install fastmcp > There are many real-world examples of using FastMCP in the wild. See the [Showcase](#showcase) for examples. ```ts -import { FastMCP } from "fastmcp"; -import { z } from "zod"; // Or any validation library that supports Standard Schema +import { FastMCP } from "fastmcp" +import { z } from "zod" // Or any validation library that supports Standard Schema const server = new FastMCP({ name: "My Server", version: "1.0.0", -}); +}) server.addTool({ name: "add", @@ -85,13 +85,13 @@ server.addTool({ b: z.number(), }), execute: async (args) => { - return String(args.a + args.b); + return String(args.a + args.b) }, -}); +}) server.start({ transportType: "stdio", -}); +}) ``` _That's it!_ You have a working MCP server. @@ -129,7 +129,7 @@ server.start({ httpStream: { port: 8080, }, -}); +}) ``` This will start the server and listen for HTTP streaming connections on `http://localhost:8080/mcp`. @@ -143,7 +143,7 @@ You can connect to these servers using the appropriate client transport. For HTTP streaming connections: ```ts -import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; +import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js" const client = new Client( { @@ -153,19 +153,17 @@ const client = new Client( { capabilities: {}, }, -); +) -const transport = new StreamableHTTPClientTransport( - new URL(`http://localhost:8080/mcp`), -); +const transport = new StreamableHTTPClientTransport(new URL(`http://localhost:8080/mcp`)) -await client.connect(transport); +await client.connect(transport) ``` For SSE connections: ```ts -import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js"; +import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js" const client = new Client( { @@ -175,11 +173,11 @@ const client = new Client( { capabilities: {}, }, -); +) -const transport = new SSEClientTransport(new URL(`http://localhost:8080/sse`)); +const transport = new SSEClientTransport(new URL(`http://localhost:8080/sse`)) -await client.connect(transport); +await client.connect(transport) ``` #### Stateless Mode @@ -202,7 +200,7 @@ server.start({ port: 8080, stateless: true, }, -}); +}) ``` > **Note:** Stateless mode is only available with HTTP streaming transport. Features that depend on persistent sessions (like session-specific state) will not be available in stateless mode. @@ -239,7 +237,7 @@ FastMCP uses the [Standard Schema](https://standardschema.dev) specification for **Zod Example:** ```typescript -import { z } from "zod"; +import { z } from "zod" server.addTool({ name: "fetch-zod", @@ -248,15 +246,15 @@ server.addTool({ url: z.string(), }), execute: async (args) => { - return await fetchWebpageContent(args.url); + return await fetchWebpageContent(args.url) }, -}); +}) ``` **ArkType Example:** ```typescript -import { type } from "arktype"; +import { type } from "arktype" server.addTool({ name: "fetch-arktype", @@ -265,9 +263,9 @@ server.addTool({ url: "string", }), execute: async (args) => { - return await fetchWebpageContent(args.url); + return await fetchWebpageContent(args.url) }, -}); +}) ``` **Valibot Example:** @@ -275,7 +273,7 @@ server.addTool({ Valibot requires the peer dependency @valibot/to-json-schema. ```typescript -import * as v from "valibot"; +import * as v from "valibot" server.addTool({ name: "fetch-valibot", @@ -284,9 +282,9 @@ server.addTool({ url: v.string(), }), execute: async (args) => { - return await fetchWebpageContent(args.url); + return await fetchWebpageContent(args.url) }, -}); +}) ``` #### Tools Without Parameters @@ -301,24 +299,24 @@ When creating tools that don't require parameters, you have two options: description: "Say hello", // No parameters property execute: async () => { - return "Hello, world!"; + return "Hello, world!" }, - }); + }) ``` 2. Explicitly define empty parameters: ```typescript - import { z } from "zod"; + import { z } from "zod" server.addTool({ name: "sayHello", description: "Say hello", parameters: z.object({}), // Empty object execute: async () => { - return "Hello, world!"; + return "Hello, world!" }, - }); + }) ``` > [!NOTE] @@ -335,7 +333,7 @@ server.addTool({ description: "An admin-only tool", canAccess: (auth) => auth?.role === "admin", execute: async () => "Welcome, admin!", -}); +}) ``` #### Returning a string @@ -350,9 +348,9 @@ server.addTool({ url: z.string(), }), execute: async (args) => { - return "Hello, world!"; + return "Hello, world!" }, -}); +}) ``` The latter is equivalent to: @@ -372,9 +370,9 @@ server.addTool({ text: "Hello, world!", }, ], - }; + } }, -}); +}) ``` #### Returning a list @@ -394,9 +392,9 @@ server.addTool({ { type: "text", text: "First message" }, { type: "text", text: "Second message" }, ], - }; + } }, -}); +}) ``` #### Returning an image @@ -404,7 +402,7 @@ server.addTool({ Use the `imageContent` to create a content object for an image: ```js -import { imageContent } from "fastmcp"; +import { imageContent } from "fastmcp" server.addTool({ name: "download", @@ -415,7 +413,7 @@ server.addTool({ execute: async (args) => { return imageContent({ url: "https://example.com/image.png", - }); + }) // or... // return imageContent({ @@ -434,7 +432,7 @@ server.addTool({ // ], // }; }, -}); +}) ``` The `imageContent` function takes the following options: @@ -463,9 +461,9 @@ server.addTool({ mimeType: "image/png", }, ], - }; + } }, -}); +}) ``` #### Configurable Ping Behavior @@ -484,7 +482,7 @@ const server = new FastMCP({ // Set log level for ping-related messages (default: 'debug') logLevel: "debug", }, -}); +}) ``` By default, ping behavior is optimized for each transport type: @@ -516,12 +514,12 @@ const server = new FastMCP({ // HTTP status code to return (default: 200) status: 200, }, -}); +}) await server.start({ transportType: "httpStream", httpStream: { port: 8080 }, -}); +}) ``` Now a request to `http://localhost:8080/healthz` will return: @@ -548,7 +546,7 @@ const server = new FastMCP({ enabled: false, // By default, roots support is enabled (true) }, -}); +}) ``` This provides the following benefits: @@ -562,16 +560,16 @@ You can listen for root changes in your server: ```ts server.on("connect", (event) => { - const session = event.session; + const session = event.session // Access the current roots - console.log("Initial roots:", session.roots); + console.log("Initial roots:", session.roots) // Listen for changes to the roots session.on("rootsChanged", (event) => { - console.log("Roots changed:", event.roots); - }); -}); + console.log("Roots changed:", event.roots) + }) +}) ``` When a client doesn't support roots or when roots functionality is explicitly disabled, these operations will gracefully handle the situation without throwing errors. @@ -581,7 +579,7 @@ When a client doesn't support roots or when roots functionality is explicitly di Use the `audioContent` to create a content object for an audio: ```js -import { audioContent } from "fastmcp"; +import { audioContent } from "fastmcp" server.addTool({ name: "download", @@ -592,7 +590,7 @@ server.addTool({ execute: async (args) => { return audioContent({ url: "https://example.com/audio.mp3", - }); + }) // or... // return audioContent({ @@ -611,7 +609,7 @@ server.addTool({ // ], // }; }, -}); +}) ``` The `audioContent` function takes the following options: @@ -640,9 +638,9 @@ server.addTool({ mimeType: "audio/mpeg", }, ], - }; + } }, -}); +}) ``` #### Return combination type @@ -674,7 +672,7 @@ server.addTool({ mimeType: "audio/mpeg", }, ], - }; + } }, // or... @@ -696,7 +694,7 @@ server.addTool({ // ], // }; // }, -}); +}) ``` #### Custom Logger @@ -704,27 +702,27 @@ server.addTool({ FastMCP allows you to provide a custom logger implementation to control how the server logs messages. This is useful for integrating with existing logging infrastructure or customizing log formatting. ```ts -import { FastMCP, Logger } from "fastmcp"; +import { FastMCP, Logger } from "fastmcp" class CustomLogger implements Logger { debug(...args: unknown[]): void { - console.log("[DEBUG]", new Date().toISOString(), ...args); + console.log("[DEBUG]", new Date().toISOString(), ...args) } error(...args: unknown[]): void { - console.error("[ERROR]", new Date().toISOString(), ...args); + console.error("[ERROR]", new Date().toISOString(), ...args) } info(...args: unknown[]): void { - console.info("[INFO]", new Date().toISOString(), ...args); + console.info("[INFO]", new Date().toISOString(), ...args) } log(...args: unknown[]): void { - console.log("[LOG]", new Date().toISOString(), ...args); + console.log("[LOG]", new Date().toISOString(), ...args) } warn(...args: unknown[]): void { - console.warn("[WARN]", new Date().toISOString(), ...args); + console.warn("[WARN]", new Date().toISOString(), ...args) } } @@ -732,7 +730,7 @@ const server = new FastMCP({ name: "My Server", version: "1.0.0", logger: new CustomLogger(), -}); +}) ``` See `src/examples/custom-logger.ts` for examples with Winston, Pino, and file-based logging. @@ -751,15 +749,15 @@ server.addTool({ execute: async (args, { log }) => { log.info("Downloading file...", { url, - }); + }) // ... - log.info("Downloaded file"); + log.info("Downloaded file") - return "done"; + return "done" }, -}); +}) ``` The `log` object has the following methods: @@ -778,7 +776,7 @@ FastMCP supports two ways to handle errors in tool execution: For standards-compliant error handling, throw `McpError` with appropriate error codes: ```js -import { ErrorCode, McpError } from "fastmcp"; +import { ErrorCode, McpError } from "fastmcp" server.addTool({ name: "download", @@ -789,7 +787,7 @@ server.addTool({ execute: async (args) => { if (args.url.startsWith("https://example.com")) { // Throw MCP error with InvalidParams code - throw new McpError(ErrorCode.InvalidParams, "This URL is not allowed"); + throw new McpError(ErrorCode.InvalidParams, "This URL is not allowed") } // Throw MCP error with custom data @@ -797,12 +795,12 @@ server.addTool({ throw new McpError(ErrorCode.InvalidRequest, "Resource not found", { url: args.url, statusCode: 404, - }); + }) } - return "done"; + return "done" }, -}); +}) ``` **Available Error Codes:** @@ -820,7 +818,7 @@ When a tool throws `McpError`, it's propagated through the MCP protocol as a pro For backward compatibility, you can still use `UserError` for simple error messages: ```js -import { UserError } from "fastmcp"; +import { UserError } from "fastmcp" server.addTool({ name: "download", @@ -830,12 +828,12 @@ server.addTool({ }), execute: async (args) => { if (args.url.startsWith("https://example.com")) { - throw new UserError("This URL is not allowed"); + throw new UserError("This URL is not allowed") } - return "done"; + return "done" }, -}); +}) ``` `UserError` errors are converted to tool responses with `isError: true` and are displayed to the user as text content. @@ -855,18 +853,18 @@ server.addTool({ await reportProgress({ progress: 0, total: 100, - }); + }) // ... await reportProgress({ progress: 100, total: 100, - }); + }) - return "done"; + return "done" }, -}); +}) ``` #### Streaming Output @@ -892,13 +890,13 @@ server.addTool({ }, execute: async (args, { streamContent }) => { // Send initial content immediately - await streamContent({ type: "text", text: "Starting generation...\n" }); + await streamContent({ type: "text", text: "Starting generation...\n" }) // Simulate incremental content generation - const words = "The quick brown fox jumps over the lazy dog.".split(" "); + const words = "The quick brown fox jumps over the lazy dog.".split(" ") for (const word of words) { - await streamContent({ type: "text", text: word + " " }); - await new Promise((resolve) => setTimeout(resolve, 300)); // Simulate delay + await streamContent({ type: "text", text: word + " " }) + await new Promise((resolve) => setTimeout(resolve, 300)) // Simulate delay } // When using streamContent, you can: @@ -906,12 +904,12 @@ server.addTool({ // 2. Return a final result (which will be appended to streamed content) // Option 1: All content was streamed, so return void - return; + return // Option 2: Return final content that will be appended // return "Generation complete!"; }, -}); +}) ``` Streaming works with all content types (text, image, audio) and can be combined with progress reporting: @@ -927,26 +925,26 @@ server.addTool({ streamingHint: true, }, execute: async (args, { streamContent, reportProgress }) => { - const total = args.datasetSize; + const total = args.datasetSize for (let i = 0; i < total; i++) { // Report numeric progress - await reportProgress({ progress: i, total }); + await reportProgress({ progress: i, total }) // Stream intermediate results if (i % 10 === 0) { await streamContent({ type: "text", text: `Processed ${i} of ${total} items...\n`, - }); + }) } - await new Promise((resolve) => setTimeout(resolve, 50)); + await new Promise((resolve) => setTimeout(resolve, 50)) } - return "Processing complete!"; + return "Processing complete!" }, -}); +}) ``` #### Tool Annotations @@ -966,9 +964,9 @@ server.addTool({ openWorldHint: true, // Tool interacts with external entities }, execute: async (args) => { - return await fetchWebpageContent(args.url); + return await fetchWebpageContent(args.url) }, -}); +}) ``` The available annotations are: @@ -1002,9 +1000,9 @@ server.addResource({ async load() { return { text: await readLogFile(), - }; + } }, -}); +}) ``` > [!NOTE] @@ -1053,9 +1051,9 @@ server.addResourceTemplate({ async load({ name }) { return { text: `Example log content for ${name}`, - }; + } }, -}); +}) ``` #### Resource template argument auto-completion @@ -1076,21 +1074,21 @@ server.addResourceTemplate({ if (value === "Example") { return { values: ["Example Log"], - }; + } } return { values: [], - }; + } }, }, ], async load({ name }) { return { text: `Example log content for ${name}`, - }; + } }, -}); +}) ``` ### Embedded Resources @@ -1114,9 +1112,9 @@ server.addTool({ resource: await server.embedded(`user://profile/${args.userId}`), }, ], - }; + } }, -}); +}) ``` #### Working with Resource Templates @@ -1139,12 +1137,12 @@ server.addResourceTemplate({ const docs = { "getting-started": "# Getting Started\n\nWelcome to our project!", "api-reference": "# API Reference\n\nAuthentication is required.", - }; + } return { text: docs[args.section] || "Documentation not found", - }; + } }, -}); +}) // Use embedded resources in a tool server.addTool({ @@ -1161,9 +1159,9 @@ server.addTool({ resource: await server.embedded(`docs://project/${args.section}`), }, ], - }; + } }, -}); +}) ``` #### Working with Direct Resources @@ -1179,9 +1177,9 @@ server.addResource({ async load() { return { text: "System operational", - }; + } }, -}); +}) // Use in a tool server.addTool({ @@ -1196,9 +1194,9 @@ server.addTool({ resource: await server.embedded("system://status"), }, ], - }; + } }, -}); +}) ``` ### Prompts @@ -1217,9 +1215,9 @@ server.addPrompt({ }, ], load: async (args) => { - return `Generate a concise but descriptive commit message for these changes:\n\n${args.changes}`; + return `Generate a concise but descriptive commit message for these changes:\n\n${args.changes}` }, -}); +}) ``` #### Prompt argument auto-completion @@ -1231,7 +1229,7 @@ server.addPrompt({ name: "countryPoem", description: "Writes a poem about a country", load: async ({ name }) => { - return `Hello, ${name}!`; + return `Hello, ${name}!` }, arguments: [ { @@ -1242,16 +1240,16 @@ server.addPrompt({ if (value === "Germ") { return { values: ["Germany"], - }; + } } return { values: [], - }; + } }, }, ], -}); +}) ``` #### Prompt argument auto-completion using `enum` @@ -1263,7 +1261,7 @@ server.addPrompt({ name: "countryPoem", description: "Writes a poem about a country", load: async ({ name }) => { - return `Hello, ${name}!`; + return `Hello, ${name}!` }, arguments: [ { @@ -1273,7 +1271,7 @@ server.addPrompt({ enum: ["Germany", "France", "Italy"], }, ], -}); +}) ``` ### Authentication @@ -1292,21 +1290,21 @@ const server = new FastMCP({ name: "My Server", version: "1.0.0", authenticate: (request) => { - const apiKey = request.headers["x-api-key"]; + const apiKey = request.headers["x-api-key"] if (apiKey !== "123") { throw new Response(null, { status: 401, statusText: "Unauthorized", - }); + }) } // Whatever you return here will be accessible in the `context.session` object. return { id: 1, - }; + } }, -}); +}) ``` Now you can access the authenticated session data in your tools: @@ -1315,9 +1313,9 @@ Now you can access the authenticated session data in your tools: server.addTool({ name: "sayHello", execute: async (args, { session }) => { - return `Hello, ${session.id}!`; + return `Hello, ${session.id}!` }, -}); +}) ``` #### Tool Authorization @@ -1331,12 +1329,12 @@ If `canAccess` is not provided, the tool is accessible to all authenticated user ```typescript const server = new FastMCP<{ role: "admin" | "user" }>({ authenticate: async (request) => { - const role = request.headers["x-role"] as string; - return { role: role === "admin" ? "admin" : "user" }; + const role = request.headers["x-role"] as string + return { role: role === "admin" ? "admin" : "user" } }, name: "My Server", version: "1.0.0", -}); +}) server.addTool({ name: "admin-dashboard", @@ -1344,17 +1342,17 @@ server.addTool({ // Only users with the 'admin' role can see and execute this tool canAccess: (auth) => auth?.role === "admin", execute: async () => { - return "Welcome to the admin dashboard!"; + return "Welcome to the admin dashboard!" }, -}); +}) server.addTool({ name: "public-info", description: "A tool available to everyone", execute: async () => { - return "This is public information."; + return "This is public information." }, -}); +}) ``` In this example, only clients authenticating with the `admin` role will be able to list or call the `admin-dashboard` tool. The `public-info` tool will be available to all authenticated users. @@ -1364,9 +1362,9 @@ In this example, only clients authenticating with the `admin` role will be able FastMCP includes built-in support for OAuth discovery endpoints, supporting both **MCP Specification 2025-03-26** and **MCP Specification 2025-06-18** for OAuth integration. This makes it easy to integrate with OAuth authorization flows by providing standard discovery endpoints that comply with RFC 8414 (OAuth 2.0 Authorization Server Metadata) and RFC 9470 (OAuth 2.0 Protected Resource Metadata): ```ts -import { FastMCP } from "fastmcp"; -import { buildGetJwks } from "get-jwks"; -import fastJwt from "fast-jwt"; +import fastJwt from "fast-jwt" +import { FastMCP } from "fastmcp" +import { buildGetJwks } from "get-jwks" const server = new FastMCP({ name: "My Server", @@ -1386,74 +1384,73 @@ const server = new FastMCP({ }, }, authenticate: async (request) => { - const authHeader = request.headers.authorization; + const authHeader = request.headers.authorization if (!authHeader?.startsWith("Bearer ")) { throw new Response(null, { status: 401, statusText: "Missing or invalid authorization header", - }); + }) } - const token = authHeader.slice(7); // Remove 'Bearer ' prefix + const token = authHeader.slice(7) // Remove 'Bearer ' prefix // Validate OAuth JWT access token using OpenID Connect discovery try { // TODO: Cache the discovery document to avoid repeated requests // Discover OAuth/OpenID configuration from well-known endpoint - const discoveryUrl = - "https://auth.example.com/.well-known/openid-configuration"; + const discoveryUrl = "https://auth.example.com/.well-known/openid-configuration" // Alternative: Use OAuth authorization server metadata endpoint // const discoveryUrl = 'https://auth.example.com/.well-known/oauth-authorization-server'; - const discoveryResponse = await fetch(discoveryUrl); + const discoveryResponse = await fetch(discoveryUrl) if (!discoveryResponse.ok) { - throw new Error("Failed to fetch OAuth discovery document"); + throw new Error("Failed to fetch OAuth discovery document") } - const config = await discoveryResponse.json(); - const jwksUri = config.jwks_uri; - const issuer = config.issuer; + const config = await discoveryResponse.json() + const jwksUri = config.jwks_uri + const issuer = config.issuer // Create JWKS client for token verification using discovered endpoint const getJwks = buildGetJwks({ jwksUrl: jwksUri, cache: true, rateLimit: true, - }); + }) // Create JWT verifier with JWKS and discovered issuer const verify = fastJwt.createVerifier({ key: async (token) => { - const { header } = fastJwt.decode(token, { complete: true }); + const { header } = fastJwt.decode(token, { complete: true }) const jwk = await getJwks.getJwk({ kid: header.kid, alg: header.alg, - }); - return jwk; + }) + return jwk }, algorithms: ["RS256", "ES256"], issuer: issuer, audience: "mcp://my-server", - }); + }) // Verify the JWT token - const payload = await verify(token); + const payload = await verify(token) return { userId: payload.sub, scope: payload.scope, email: payload.email, // Include other claims as needed - }; + } } catch (error) { throw new Response(null, { status: 401, statusText: "Invalid OAuth token", - }); + }) } }, -}); +}) ``` This configuration automatically exposes OAuth discovery endpoints: @@ -1468,13 +1465,14 @@ For JWT token validation, you can use libraries like [`get-jwks`](https://github If you are exposing your MCP server via HTTP, you may wish to allow clients to supply sensitive keys via headers, which can then be passed along to APIs that your tools interact with, allowing each client to supply their own API keys. This can be done by capturing the HTTP headers in the `authenticate` section and storing them in the session to be referenced by the tools later. ```ts -import { FastMCP } from "fastmcp"; -import { IncomingHttpHeaders } from "http"; +import { IncomingHttpHeaders } from "http" + +import { FastMCP } from "fastmcp" // Define the session data type interface SessionData { - headers: IncomingHttpHeaders; - [key: string]: unknown; // Add index signature to satisfy Record + headers: IncomingHttpHeaders + [key: string]: unknown // Add index signature to satisfy Record } // Create a server instance @@ -1485,26 +1483,26 @@ const server = new FastMCP({ // Authentication logic return { headers: request.headers, - }; + } }, -}); +}) // Tool to display HTTP headers server.addTool({ name: "headerTool", description: "Reads HTTP headers from the request", execute: async (args: any, context: any) => { - const session = context.session as SessionData; - const headers = session?.headers ?? {}; + const session = context.session as SessionData + const headers = session?.headers ?? {} const getHeaderString = (header: string | string[] | undefined) => - Array.isArray(header) ? header.join(", ") : (header ?? "N/A"); + Array.isArray(header) ? header.join(", ") : (header ?? "N/A") - const userAgent = getHeaderString(headers["user-agent"]); - const authorization = getHeaderString(headers["authorization"]); - return `User-Agent: ${userAgent}\nAuthorization: ${authorization}\nAll Headers: ${JSON.stringify(headers, null, 2)}`; + const userAgent = getHeaderString(headers["user-agent"]) + const authorization = getHeaderString(headers["authorization"]) + return `User-Agent: ${userAgent}\nAuthorization: ${authorization}\nAll Headers: ${JSON.stringify(headers, null, 2)}` }, -}); +}) // Start the server server.start({ @@ -1512,33 +1510,30 @@ server.start({ httpStream: { port: 8080, }, -}); +}) ``` A client that would connect to this may look something like this: ```ts -import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; -import { Client } from "@modelcontextprotocol/sdk/client/index.js"; +import { Client } from "@modelcontextprotocol/sdk/client/index.js" +import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js" -const transport = new StreamableHTTPClientTransport( - new URL(`http://localhost:8080/mcp`), - { - requestInit: { - headers: { - Authorization: "Test 123", - }, +const transport = new StreamableHTTPClientTransport(new URL(`http://localhost:8080/mcp`), { + requestInit: { + headers: { + Authorization: "Test 123", }, }, -); +}) const client = new Client({ name: "example-client", version: "1.0.0", -}); +}) -(async () => { - await client.connect(transport); +;(async () => { + await client.connect(transport) // Call a tool const result = await client.callTool({ @@ -1546,10 +1541,10 @@ const client = new Client({ arguments: { arg1: "value", }, - }); + }) - console.log("Tool result:", result); -})().catch(console.error); + console.log("Tool result:", result) +})().catch(console.error) ``` What would show up in the console after the client runs is something like this: @@ -1596,16 +1591,16 @@ FastMCP automatically exposes session and request IDs to tool handlers through t - Useful for request tracing and debugging ```ts -import { FastMCP } from "fastmcp"; -import { z } from "zod"; +import { FastMCP } from "fastmcp" +import { z } from "zod" const server = new FastMCP({ name: "Session Counter Server", version: "1.0.0", -}); +}) // Per-session counter storage -const sessionCounters = new Map(); +const sessionCounters = new Map() server.addTool({ name: "increment_counter", @@ -1613,16 +1608,16 @@ server.addTool({ parameters: z.object({}), execute: async (args, context) => { if (!context.sessionId) { - return "Session ID not available (requires HTTP transport)"; + return "Session ID not available (requires HTTP transport)" } - const counter = sessionCounters.get(context.sessionId) || 0; - const newCounter = counter + 1; - sessionCounters.set(context.sessionId, newCounter); + const counter = sessionCounters.get(context.sessionId) || 0 + const newCounter = counter + 1 + sessionCounters.set(context.sessionId, newCounter) - return `Counter for session ${context.sessionId}: ${newCounter}`; + return `Counter for session ${context.sessionId}: ${newCounter}` }, -}); +}) server.addTool({ name: "show_ids", @@ -1630,16 +1625,16 @@ server.addTool({ parameters: z.object({}), execute: async (args, context) => { return `Session ID: ${context.sessionId || "N/A"} -Request ID: ${context.requestId || "N/A"}`; +Request ID: ${context.requestId || "N/A"}` }, -}); +}) server.start({ transportType: "httpStream", httpStream: { port: 8080, }, -}); +}) ``` **Use Cases:** @@ -1670,7 +1665,7 @@ const server = new FastMCP({ version: "1.0.0", instructions: 'Instructions describing how to use the server and its features.\n\nThis can be used by clients to improve the LLM\'s understanding of available tools, resources, etc. It can be thought of like a "hint" to the model. For example, this information MAY be added to the system prompt.', -}); +}) ``` ### Sessions @@ -1678,7 +1673,7 @@ const server = new FastMCP({ The `session` object is an instance of `FastMCPSession` and it describes active client sessions. ```ts -server.sessions; +server.sessions ``` We allocate a new server instance for each client connection to enable 1:1 communication between a client and the server. @@ -1689,12 +1684,12 @@ You can listen to events emitted by the server using the `on` method: ```ts server.on("connect", (event) => { - console.log("Client connected:", event.session); -}); + console.log("Client connected:", event.session) +}) server.on("disconnect", (event) => { - console.log("Client disconnected:", event.session); -}); + console.log("Client disconnected:", event.session) +}) ``` ## `FastMCPSession` @@ -1721,7 +1716,7 @@ await session.requestSampling({ systemPrompt: "You are a helpful file system assistant.", includeContext: "thisServer", maxTokens: 100, -}); +}) ``` #### Options @@ -1747,7 +1742,7 @@ await session.requestSampling( { // Progress callback - called when progress notifications are received onprogress: (progress) => { - console.log(`Progress: ${progress.progress}/${progress.total}`); + console.log(`Progress: ${progress.progress}/${progress.total}`) }, // Abort signal for cancelling the request @@ -1762,7 +1757,7 @@ await session.requestSampling( // Maximum total timeout regardless of progress (no default) maxTotalTimeout: 60000, }, -); +) ``` **Options:** @@ -1778,7 +1773,7 @@ await session.requestSampling( The `clientCapabilities` property contains the client capabilities. ```ts -session.clientCapabilities; +session.clientCapabilities ``` ### `loggingLevel` @@ -1786,7 +1781,7 @@ session.clientCapabilities; The `loggingLevel` property describes the logging level as set by the client. ```ts -session.loggingLevel; +session.loggingLevel ``` ### `roots` @@ -1794,7 +1789,7 @@ session.loggingLevel; The `roots` property contains the roots as set by the client. ```ts -session.roots; +session.roots ``` ### `server` @@ -1802,7 +1797,7 @@ session.roots; The `server` property contains an instance of MCP server that is associated with the session. ```ts -session.server; +session.server ``` ### Typed session events @@ -1811,12 +1806,12 @@ You can listen to events emitted by the session using the `on` method: ```ts session.on("rootsChanged", (event) => { - console.log("Roots changed:", event.roots); -}); + console.log("Roots changed:", event.roots) +}) session.on("error", (event) => { - console.error("Error:", event.error); -}); + console.error("Error:", event.error) +}) ``` ## Running Your Server diff --git a/eslint.config.ts b/eslint.config.ts index 421bce1..f08d128 100644 --- a/eslint.config.ts +++ b/eslint.config.ts @@ -1,7 +1,7 @@ -import eslint from "@eslint/js"; -import eslintConfigPrettier from "eslint-config-prettier/flat"; -import perfectionist from "eslint-plugin-perfectionist"; -import tseslint from "typescript-eslint"; +import eslint from "@eslint/js" +import eslintConfigPrettier from "eslint-config-prettier/flat" +import perfectionist from "eslint-plugin-perfectionist" +import tseslint from "typescript-eslint" export default tseslint.config( eslint.configs.recommended, @@ -11,4 +11,4 @@ export default tseslint.config( { ignores: ["**/*.js", "dist/**"], }, -); +) diff --git a/src/FastMCP.oauth.test.ts b/src/FastMCP.oauth.test.ts index 589132f..2db8bd7 100644 --- a/src/FastMCP.oauth.test.ts +++ b/src/FastMCP.oauth.test.ts @@ -1,11 +1,11 @@ -import { getRandomPort } from "get-port-please"; -import { describe, expect, it } from "vitest"; +import { getRandomPort } from "get-port-please" +import { describe, expect, it } from "vitest" -import { FastMCP } from "./FastMCP.js"; +import { FastMCP } from "./FastMCP.js" describe("FastMCP OAuth Support", () => { it("should serve OAuth authorization server metadata", async () => { - const port = await getRandomPort(); + const port = await getRandomPort() const server = new FastMCP({ name: "Test Server", @@ -23,51 +23,37 @@ describe("FastMCP OAuth Support", () => { enabled: true, }, version: "1.0.0", - }); + }) await server.start({ httpStream: { port }, transportType: "httpStream", - }); + }) try { // Test the OAuth authorization server endpoint - const response = await fetch( - `http://localhost:${port}/.well-known/oauth-authorization-server`, - ); - expect(response.status).toBe(200); - expect(response.headers.get("content-type")).toBe("application/json"); + const response = await fetch(`http://localhost:${port}/.well-known/oauth-authorization-server`) + expect(response.status).toBe(200) + expect(response.headers.get("content-type")).toBe("application/json") - const metadata = (await response.json()) as Record; + const metadata = (await response.json()) as Record // Check that camelCase was converted to snake_case - expect(metadata.issuer).toBe("https://auth.example.com"); - expect(metadata.authorization_endpoint).toBe( - "https://auth.example.com/oauth/authorize", - ); - expect(metadata.token_endpoint).toBe( - "https://auth.example.com/oauth/token", - ); - expect(metadata.response_types_supported).toEqual(["code"]); - expect(metadata.jwks_uri).toBe( - "https://auth.example.com/.well-known/jwks.json", - ); - expect(metadata.scopes_supported).toEqual(["read", "write"]); - expect(metadata.grant_types_supported).toEqual([ - "authorization_code", - "refresh_token", - ]); - expect(metadata.dpop_signing_alg_values_supported).toEqual([ - "ES256", - "RS256", - ]); + expect(metadata.issuer).toBe("https://auth.example.com") + expect(metadata.authorization_endpoint).toBe("https://auth.example.com/oauth/authorize") + expect(metadata.token_endpoint).toBe("https://auth.example.com/oauth/token") + expect(metadata.response_types_supported).toEqual(["code"]) + expect(metadata.jwks_uri).toBe("https://auth.example.com/.well-known/jwks.json") + expect(metadata.scopes_supported).toEqual(["read", "write"]) + expect(metadata.grant_types_supported).toEqual(["authorization_code", "refresh_token"]) + expect(metadata.dpop_signing_alg_values_supported).toEqual(["ES256", "RS256"]) } finally { - await server.stop(); + await server.stop() } - }); + }) it("should serve OAuth protected resource metadata", async () => { - const port = await getRandomPort(); + const port = await getRandomPort() const server = new FastMCP({ name: "Test Server", @@ -99,72 +85,53 @@ describe("FastMCP OAuth Support", () => { }, }, version: "1.0.0", - }); + }) await server.start({ httpStream: { port }, transportType: "httpStream", - }); + }) try { - const response = await fetch( - `http://localhost:${port}/.well-known/oauth-protected-resource`, - ); - expect(response.status).toBe(200); - expect(response.headers.get("content-type")).toBe("application/json"); + const response = await fetch(`http://localhost:${port}/.well-known/oauth-protected-resource`) + expect(response.status).toBe(200) + expect(response.headers.get("content-type")).toBe("application/json") - const metadata = (await response.json()) as Record; + const metadata = (await response.json()) as Record // Check that camelCase was converted to snake_case - expect(metadata.resource).toBe("mcp://test-server"); - expect(metadata.authorization_servers).toEqual([ - "https://auth.example.com", - ]); - expect(metadata.jwks_uri).toBe( - "https://test-server.example.com/.well-known/jwks.json", - ); - expect(metadata.bearer_methods_supported).toEqual(["header"]); - expect(metadata.resource_documentation).toBe( - "https://docs.example.com/api", - ); + expect(metadata.resource).toBe("mcp://test-server") + expect(metadata.authorization_servers).toEqual(["https://auth.example.com"]) + expect(metadata.jwks_uri).toBe("https://test-server.example.com/.well-known/jwks.json") + expect(metadata.bearer_methods_supported).toEqual(["header"]) + expect(metadata.resource_documentation).toBe("https://docs.example.com/api") // New fields added for RFC 9728 compliance - expect(metadata.authorization_details_types_supported).toEqual([ - "payment_initiation", - ]); - expect(metadata.dpop_bound_access_tokens_required).toBe(true); - expect(metadata.dpop_signing_alg_values_supported).toEqual([ - "ES256", - "RS256", - ]); - expect(metadata.resource_name).toBe("Test API"); - expect(metadata.resource_policy_uri).toBe( - "https://test-server.example.com/policy", - ); - expect(metadata.resource_signing_alg_values_supported).toEqual(["RS256"]); - expect(metadata.resource_tos_uri).toBe( - "https://test-server.example.com/tos", - ); - expect(metadata.scopes_supported).toEqual(["read", "write", "admin"]); - expect(metadata.service_documentation).toBe( - "https://developer.example.com/api", - ); - expect(metadata.tls_client_certificate_bound_access_tokens).toBe(false); + expect(metadata.authorization_details_types_supported).toEqual(["payment_initiation"]) + expect(metadata.dpop_bound_access_tokens_required).toBe(true) + expect(metadata.dpop_signing_alg_values_supported).toEqual(["ES256", "RS256"]) + expect(metadata.resource_name).toBe("Test API") + expect(metadata.resource_policy_uri).toBe("https://test-server.example.com/policy") + expect(metadata.resource_signing_alg_values_supported).toEqual(["RS256"]) + expect(metadata.resource_tos_uri).toBe("https://test-server.example.com/tos") + expect(metadata.scopes_supported).toEqual(["read", "write", "admin"]) + expect(metadata.service_documentation).toBe("https://developer.example.com/api") + expect(metadata.tls_client_certificate_bound_access_tokens).toBe(false) // Vendor extensions (dynamic properties) - expect(metadata.vendor_prefix_custom_field).toBe("custom value"); + expect(metadata.vendor_prefix_custom_field).toBe("custom value") expect(metadata.vendor_prefix_complex_object).toEqual({ nestedArray: [1, 2, 3], nestedProperty: "nested value", - }); - expect(metadata.x_api_version).toBe("2.0"); + }) + expect(metadata.x_api_version).toBe("2.0") } finally { - await server.stop(); + await server.stop() } - }); + }) it("should return 404 for OAuth endpoints when disabled", async () => { - const port = await getRandomPort(); + const port = await getRandomPort() const server = new FastMCP({ name: "Test Server", @@ -172,54 +139,46 @@ describe("FastMCP OAuth Support", () => { enabled: false, }, version: "1.0.0", - }); + }) await server.start({ httpStream: { port }, transportType: "httpStream", - }); + }) try { - const authServerResponse = await fetch( - `http://localhost:${port}/.well-known/oauth-authorization-server`, - ); - expect(authServerResponse.status).toBe(404); - - const protectedResourceResponse = await fetch( - `http://localhost:${port}/.well-known/oauth-protected-resource`, - ); - expect(protectedResourceResponse.status).toBe(404); + const authServerResponse = await fetch(`http://localhost:${port}/.well-known/oauth-authorization-server`) + expect(authServerResponse.status).toBe(404) + + const protectedResourceResponse = await fetch(`http://localhost:${port}/.well-known/oauth-protected-resource`) + expect(protectedResourceResponse.status).toBe(404) } finally { - await server.stop(); + await server.stop() } - }); + }) it("should return 404 for OAuth endpoints when not configured", async () => { - const port = await getRandomPort(); + const port = await getRandomPort() const server = new FastMCP({ name: "Test Server", version: "1.0.0", // No oauth configuration - }); + }) await server.start({ httpStream: { port }, transportType: "httpStream", - }); + }) try { - const authServerResponse = await fetch( - `http://localhost:${port}/.well-known/oauth-authorization-server`, - ); - expect(authServerResponse.status).toBe(404); - - const protectedResourceResponse = await fetch( - `http://localhost:${port}/.well-known/oauth-protected-resource`, - ); - expect(protectedResourceResponse.status).toBe(404); + const authServerResponse = await fetch(`http://localhost:${port}/.well-known/oauth-authorization-server`) + expect(authServerResponse.status).toBe(404) + + const protectedResourceResponse = await fetch(`http://localhost:${port}/.well-known/oauth-protected-resource`) + expect(protectedResourceResponse.status).toBe(404) } finally { - await server.stop(); + await server.stop() } - }); -}); + }) +}) diff --git a/src/FastMCP.session-context.test.ts b/src/FastMCP.session-context.test.ts index 53497a8..1d1a325 100644 --- a/src/FastMCP.session-context.test.ts +++ b/src/FastMCP.session-context.test.ts @@ -1,43 +1,43 @@ -import { describe, expect, it, vi } from "vitest"; -import { z } from "zod"; +import { describe, expect, it, vi } from "vitest" +import { z } from "zod" -import { FastMCP } from "./FastMCP.js"; +import { FastMCP } from "./FastMCP.js" interface TestAuth { - [key: string]: unknown; // Required for FastMCPSessionAuth compatibility - role: "admin" | "user"; - userId: string; + [key: string]: unknown // Required for FastMCPSessionAuth compatibility + role: "admin" | "user" + userId: string } describe("FastMCP Session Context", () => { describe("stdio transport", () => { it("should pass session context to tool execution when authenticate is provided", async () => { - const mockAuth: TestAuth = { role: "admin", userId: "test-user" }; + const mockAuth: TestAuth = { role: "admin", userId: "test-user" } const server = new FastMCP({ authenticate: async (request) => { - if (!request) return mockAuth; + if (!request) return mockAuth - throw new Error("Unexpected request in test"); + throw new Error("Unexpected request in test") }, name: "test-server", version: "1.0.0", - }); + }) server.addTool({ description: "Test tool to verify session context", execute: async (_args, context) => { - return `Session received: ${context.session ? "yes" : "no"}`; + return `Session received: ${context.session ? "yes" : "no"}` }, name: "test-session-context", parameters: z.object({ message: z.string(), }), - }); + }) - await server.start({ transportType: "stdio" }); + await server.start({ transportType: "stdio" }) - expect(server).toBeDefined(); - }); + expect(server).toBeDefined() + }) it("should handle authentication errors gracefully in stdio transport", async () => { const mockLogger = { @@ -46,57 +46,57 @@ describe("FastMCP Session Context", () => { info: vi.fn(), log: vi.fn(), warn: vi.fn(), - }; + } const server = new FastMCP({ authenticate: async () => { - throw new Error("Auth failed"); + throw new Error("Auth failed") }, logger: mockLogger, name: "test-server", version: "1.0.0", - }); + }) server.addTool({ description: "Test tool", execute: async (_args, context) => { - return `Session: ${context.session ? "present" : "undefined"}`; + return `Session: ${context.session ? "present" : "undefined"}` }, name: "test-tool", - }); + }) - await server.start({ transportType: "stdio" }); + await server.start({ transportType: "stdio" }) expect(mockLogger.error).toHaveBeenCalledWith( "[FastMCP error] Authentication failed for stdio transport:", "Auth failed", - ); - }); + ) + }) it("should work without authenticate function", async () => { const server = new FastMCP({ name: "test-server", version: "1.0.0", - }); + }) server.addTool({ description: "Test tool without auth", execute: async (_args, context) => { - return `Session: ${context.session ? "present" : "undefined"}`; + return `Session: ${context.session ? "present" : "undefined"}` }, name: "test-tool", - }); + }) - await server.start({ transportType: "stdio" }); + await server.start({ transportType: "stdio" }) - expect(server).toBeDefined(); - }); - }); + expect(server).toBeDefined() + }) + }) describe("environment variable based authentication", () => { it("should support reading from environment variables in stdio mode", async () => { - const originalEnv = process.env.TEST_USER_ID; + const originalEnv = process.env.TEST_USER_ID - process.env.TEST_USER_ID = "env-user-123"; + process.env.TEST_USER_ID = "env-user-123" try { const server = new FastMCP({ @@ -105,32 +105,32 @@ describe("FastMCP Session Context", () => { return { role: "user" as const, userId: process.env.TEST_USER_ID || "default-user", - }; + } } - throw new Error("HTTP not supported in this test"); + throw new Error("HTTP not supported in this test") }, name: "test-server", version: "1.0.0", - }); + }) server.addTool({ description: "Tool using env-based auth", execute: async (_args, context) => { - return `Environment user: ${context.session?.userId}`; + return `Environment user: ${context.session?.userId}` }, name: "env-test-tool", - }); + }) - await server.start({ transportType: "stdio" }); + await server.start({ transportType: "stdio" }) - expect(server).toBeDefined(); + expect(server).toBeDefined() } finally { if (originalEnv !== undefined) { - process.env.TEST_USER_ID = originalEnv; + process.env.TEST_USER_ID = originalEnv } else { - delete process.env.TEST_USER_ID; + delete process.env.TEST_USER_ID } } - }); - }); -}); + }) + }) +}) diff --git a/src/FastMCP.session-id.test.ts b/src/FastMCP.session-id.test.ts index 392d437..8803df4 100644 --- a/src/FastMCP.session-id.test.ts +++ b/src/FastMCP.session-id.test.ts @@ -1,13 +1,13 @@ -import { Client } from "@modelcontextprotocol/sdk/client/index.js"; -import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; -import { describe, expect, it } from "vitest"; -import { z } from "zod"; +import { Client } from "@modelcontextprotocol/sdk/client/index.js" +import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js" +import { describe, expect, it } from "vitest" +import { z } from "zod" -import { FastMCP } from "./FastMCP.js"; +import { FastMCP } from "./FastMCP.js" interface TestAuth { - [key: string]: unknown; - userId: string; + [key: string]: unknown + userId: string } describe("FastMCP Session ID Support", () => { @@ -19,35 +19,33 @@ describe("FastMCP Session ID Support", () => { }), name: "test-server", version: "1.0.0", - }); + }) - let capturedSessionId: string | undefined; - let capturedRequestId: string | undefined; + let capturedSessionId: string | undefined + let capturedRequestId: string | undefined server.addTool({ description: "Test tool that captures session and request IDs", execute: async (_args, context) => { - capturedSessionId = context.sessionId; - capturedRequestId = context.requestId; - return `Session ID: ${context.sessionId || "none"}, Request ID: ${context.requestId || "none"}`; + capturedSessionId = context.sessionId + capturedRequestId = context.requestId + return `Session ID: ${context.sessionId || "none"}, Request ID: ${context.requestId || "none"}` }, name: "capture-ids", parameters: z.object({}), - }); + }) - const port = 3000 + Math.floor(Math.random() * 1000); + const port = 3000 + Math.floor(Math.random() * 1000) await server.start({ httpStream: { port, }, transportType: "httpStream", - }); + }) try { - const transport = new StreamableHTTPClientTransport( - new URL(`http://localhost:${port}/mcp`), - ); + const transport = new StreamableHTTPClientTransport(new URL(`http://localhost:${port}/mcp`)) const client = new Client( { @@ -57,31 +55,31 @@ describe("FastMCP Session ID Support", () => { { capabilities: {}, }, - ); + ) - await client.connect(transport); + await client.connect(transport) const result = await client.callTool({ arguments: {}, name: "capture-ids", - }); + }) - expect(result).toBeDefined(); - expect(capturedSessionId).toBeDefined(); - expect(typeof capturedSessionId).toBe("string"); - expect(capturedSessionId).toMatch(/^[0-9a-f-]+$/); // UUID format + expect(result).toBeDefined() + expect(capturedSessionId).toBeDefined() + expect(typeof capturedSessionId).toBe("string") + expect(capturedSessionId).toMatch(/^[0-9a-f-]+$/) // UUID format // Request ID may or may not be provided by the client // If provided, it should be a string if (capturedRequestId !== undefined) { - expect(typeof capturedRequestId).toBe("string"); + expect(typeof capturedRequestId).toBe("string") } - await client.close(); + await client.close() } finally { - await server.stop(); + await server.stop() } - }); + }) it("should maintain the same sessionId across multiple requests", async () => { const server = new FastMCP({ @@ -90,33 +88,31 @@ describe("FastMCP Session ID Support", () => { }), name: "test-server", version: "1.0.0", - }); + }) - const capturedSessionIds: (string | undefined)[] = []; + const capturedSessionIds: (string | undefined)[] = [] server.addTool({ description: "Test tool that captures session ID", execute: async (_args, context) => { - capturedSessionIds.push(context.sessionId); - return `Session ID: ${context.sessionId}`; + capturedSessionIds.push(context.sessionId) + return `Session ID: ${context.sessionId}` }, name: "capture-session", parameters: z.object({}), - }); + }) - const port = 3000 + Math.floor(Math.random() * 1000); + const port = 3000 + Math.floor(Math.random() * 1000) await server.start({ httpStream: { port, }, transportType: "httpStream", - }); + }) try { - const transport = new StreamableHTTPClientTransport( - new URL(`http://localhost:${port}/mcp`), - ); + const transport = new StreamableHTTPClientTransport(new URL(`http://localhost:${port}/mcp`)) const client = new Client( { @@ -126,37 +122,37 @@ describe("FastMCP Session ID Support", () => { { capabilities: {}, }, - ); + ) - await client.connect(transport); + await client.connect(transport) // Make multiple requests await client.callTool({ arguments: {}, name: "capture-session", - }); + }) await client.callTool({ arguments: {}, name: "capture-session", - }); + }) await client.callTool({ arguments: {}, name: "capture-session", - }); + }) // All requests should have the same session ID - expect(capturedSessionIds).toHaveLength(3); - expect(capturedSessionIds[0]).toBeDefined(); - expect(capturedSessionIds[0]).toBe(capturedSessionIds[1]); - expect(capturedSessionIds[1]).toBe(capturedSessionIds[2]); + expect(capturedSessionIds).toHaveLength(3) + expect(capturedSessionIds[0]).toBeDefined() + expect(capturedSessionIds[0]).toBe(capturedSessionIds[1]) + expect(capturedSessionIds[1]).toBe(capturedSessionIds[2]) - await client.close(); + await client.close() } finally { - await server.stop(); + await server.stop() } - }); + }) it("should support per-session state management using sessionId", async () => { const server = new FastMCP({ @@ -165,42 +161,40 @@ describe("FastMCP Session ID Support", () => { }), name: "test-server", version: "1.0.0", - }); + }) // Per-session counter storage - const sessionCounters = new Map(); + const sessionCounters = new Map() server.addTool({ description: "Increment a per-session counter", execute: async (_args, context) => { if (!context.sessionId) { - return "No session ID available"; + return "No session ID available" } - const currentCount = sessionCounters.get(context.sessionId) || 0; - const newCount = currentCount + 1; - sessionCounters.set(context.sessionId, newCount); + const currentCount = sessionCounters.get(context.sessionId) || 0 + const newCount = currentCount + 1 + sessionCounters.set(context.sessionId, newCount) - return `Counter for session ${context.sessionId}: ${newCount}`; + return `Counter for session ${context.sessionId}: ${newCount}` }, name: "increment-counter", parameters: z.object({}), - }); + }) - const port = 3000 + Math.floor(Math.random() * 1000); + const port = 3000 + Math.floor(Math.random() * 1000) await server.start({ httpStream: { port, }, transportType: "httpStream", - }); + }) try { // Create two separate clients with different sessions - const transport1 = new StreamableHTTPClientTransport( - new URL(`http://localhost:${port}/mcp`), - ); + const transport1 = new StreamableHTTPClientTransport(new URL(`http://localhost:${port}/mcp`)) const client1 = new Client( { @@ -210,13 +204,11 @@ describe("FastMCP Session ID Support", () => { { capabilities: {}, }, - ); + ) - await client1.connect(transport1); + await client1.connect(transport1) - const transport2 = new StreamableHTTPClientTransport( - new URL(`http://localhost:${port}/mcp`), - ); + const transport2 = new StreamableHTTPClientTransport(new URL(`http://localhost:${port}/mcp`)) const client2 = new Client( { @@ -226,44 +218,38 @@ describe("FastMCP Session ID Support", () => { { capabilities: {}, }, - ); + ) - await client2.connect(transport2); + await client2.connect(transport2) // Increment counter for client 1 twice const result1a = await client1.callTool({ arguments: {}, name: "increment-counter", - }); + }) const result1b = await client1.callTool({ arguments: {}, name: "increment-counter", - }); + }) // Increment counter for client 2 once const result2 = await client2.callTool({ arguments: {}, name: "increment-counter", - }); + }) // Verify counters are independent per session - expect((result1a.content as Array<{ text: string }>)[0].text).toContain( - ": 1", - ); - expect((result1b.content as Array<{ text: string }>)[0].text).toContain( - ": 2", - ); - expect((result2.content as Array<{ text: string }>)[0].text).toContain( - ": 1", - ); - - await client1.close(); - await client2.close(); + expect((result1a.content as Array<{ text: string }>)[0].text).toContain(": 1") + expect((result1b.content as Array<{ text: string }>)[0].text).toContain(": 2") + expect((result2.content as Array<{ text: string }>)[0].text).toContain(": 1") + + await client1.close() + await client2.close() } finally { - await server.stop(); + await server.stop() } - }); + }) it("should work in stateless mode without persistent sessionId", async () => { const server = new FastMCP({ @@ -272,21 +258,21 @@ describe("FastMCP Session ID Support", () => { }), name: "test-server", version: "1.0.0", - }); + }) - let capturedSessionId: string | undefined; + let capturedSessionId: string | undefined server.addTool({ description: "Test tool in stateless mode", execute: async (_args, context) => { - capturedSessionId = context.sessionId; - return `Session ID: ${context.sessionId || "none"}`; + capturedSessionId = context.sessionId + return `Session ID: ${context.sessionId || "none"}` }, name: "test-stateless", parameters: z.object({}), - }); + }) - const port = 3000 + Math.floor(Math.random() * 1000); + const port = 3000 + Math.floor(Math.random() * 1000) await server.start({ httpStream: { @@ -294,12 +280,10 @@ describe("FastMCP Session ID Support", () => { stateless: true, }, transportType: "httpStream", - }); + }) try { - const transport = new StreamableHTTPClientTransport( - new URL(`http://localhost:${port}/mcp`), - ); + const transport = new StreamableHTTPClientTransport(new URL(`http://localhost:${port}/mcp`)) const client = new Client( { @@ -309,24 +293,24 @@ describe("FastMCP Session ID Support", () => { { capabilities: {}, }, - ); + ) - await client.connect(transport); + await client.connect(transport) await client.callTool({ arguments: {}, name: "test-stateless", - }); + }) // In stateless mode, sessionId should be undefined - expect(capturedSessionId).toBeUndefined(); + expect(capturedSessionId).toBeUndefined() - await client.close(); + await client.close() } finally { - await server.stop(); + await server.stop() } - }); - }); + }) + }) describe("stdio transport", () => { it("should not have sessionId in stdio transport", async () => { @@ -336,24 +320,24 @@ describe("FastMCP Session ID Support", () => { }), name: "test-server", version: "1.0.0", - }); + }) - let capturedSessionId: string | undefined; + let capturedSessionId: string | undefined server.addTool({ description: "Test tool for stdio", execute: async (_args, context) => { - capturedSessionId = context.sessionId; - return `Session ID: ${context.sessionId || "none"}`; + capturedSessionId = context.sessionId + return `Session ID: ${context.sessionId || "none"}` }, name: "test-stdio", parameters: z.object({}), - }); + }) - await server.start({ transportType: "stdio" }); + await server.start({ transportType: "stdio" }) // In stdio transport, sessionId should be undefined - expect(capturedSessionId).toBeUndefined(); - }); - }); -}); + expect(capturedSessionId).toBeUndefined() + }) + }) +}) diff --git a/src/FastMCP.test.ts b/src/FastMCP.test.ts index 267c1eb..952a83e 100644 --- a/src/FastMCP.test.ts +++ b/src/FastMCP.test.ts @@ -1,6 +1,8 @@ -import { Client } from "@modelcontextprotocol/sdk/client/index.js"; -import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js"; -import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; +import { setTimeout as delay } from "timers/promises" + +import { Client } from "@modelcontextprotocol/sdk/client/index.js" +import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js" +import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js" import { CreateMessageRequestSchema, ErrorCode, @@ -9,56 +11,48 @@ import { McpError, PingRequestSchema, Root, -} from "@modelcontextprotocol/sdk/types.js"; -import { createEventSource, EventSourceClient } from "eventsource-client"; -import { getRandomPort } from "get-port-please"; -import { setTimeout as delay } from "timers/promises"; -import { fetch } from "undici"; -import { expect, test, vi } from "vitest"; -import { z } from "zod"; -import { z as z4 } from "zod/v4"; +} from "@modelcontextprotocol/sdk/types.js" +import { createEventSource, EventSourceClient } from "eventsource-client" +import { getRandomPort } from "get-port-please" +import { fetch } from "undici" +import { expect, test, vi } from "vitest" +import { z } from "zod" +import { z as z4 } from "zod/v4" import { audioContent, - type ContentResult, FastMCP, FastMCPSession, imageContent, - type TextContent, UserError, -} from "./FastMCP.js"; + type ContentResult, + type TextContent, +} from "./FastMCP.js" const runWithTestServer = async ({ client: createClient, run, server: createServer, }: { - client?: () => Promise; - run: ({ - client, - server, - }: { - client: Client; - server: FastMCP; - session: FastMCPSession; - }) => Promise; - server?: () => Promise; + client?: () => Promise + run: ({ client, server }: { client: Client; server: FastMCP; session: FastMCPSession }) => Promise + server?: () => Promise }) => { - const port = await getRandomPort(); + const port = await getRandomPort() const server = createServer ? await createServer() : new FastMCP({ name: "Test", version: "1.0.0", - }); + }) await server.start({ httpStream: { port, }, transportType: "httpStream", - }); + }) try { const client = createClient @@ -71,29 +65,27 @@ const runWithTestServer = async ({ { capabilities: {}, }, - ); + ) - const transport = new SSEClientTransport( - new URL(`http://localhost:${port}/sse`), - ); + const transport = new SSEClientTransport(new URL(`http://localhost:${port}/sse`)) const session = await new Promise((resolve) => { server.on("connect", async (event) => { // Wait for session to be fully ready before resolving - await event.session.waitForReady(); - resolve(event.session); - }); + await event.session.waitForReady() + resolve(event.session) + }) - client.connect(transport); - }); + client.connect(transport) + }) - await run({ client, server, session }); + await run({ client, server, session }) } finally { - await server.stop(); + await server.stop() } - return port; -}; + return port +} test("adds tools", async () => { await runWithTestServer({ @@ -115,30 +107,30 @@ test("adds tools", async () => { name: "add", }, ], - }); + }) }, server: async () => { const server = new FastMCP({ name: "Test", version: "1.0.0", - }); + }) server.addTool({ description: "Add two numbers", execute: async (args) => { - return String(args.a + args.b); + return String(args.a + args.b) }, name: "add", parameters: z.object({ a: z.number(), b: z.number(), }), - }); + }) - return server; + return server }, - }); -}); + }) +}) test("adds tools with Zod v4 schema", async () => { await runWithTestServer({ @@ -160,55 +152,55 @@ test("adds tools with Zod v4 schema", async () => { name: "add-zod-v4", }, ], - }); + }) }, server: async () => { const server = new FastMCP({ name: "Test", version: "1.0.0", - }); + }) const AddParamsZod4 = z4.object({ a: z4.number(), b: z4.number(), - }); + }) server.addTool({ description: "Add two numbers (using Zod v4 schema)", execute: async (args) => { - return String(args.a + args.b); + return String(args.a + args.b) }, name: "add-zod-v4", parameters: AddParamsZod4, - }); + }) - return server; + return server }, - }); -}); + }) +}) test("health endpoint returns ok", async () => { - const port = await getRandomPort(); + const port = await getRandomPort() const server = new FastMCP({ health: { message: "healthy", path: "/healthz" }, name: "Test", version: "1.0.0", - }); + }) await server.start({ httpStream: { port }, transportType: "httpStream", - }); + }) try { - const response = await fetch(`http://localhost:${port}/healthz`); - expect(response.status).toBe(200); - expect(await response.text()).toBe("healthy"); + const response = await fetch(`http://localhost:${port}/healthz`) + expect(response.status).toBe(200) + expect(await response.text()).toBe("healthy") } finally { - await server.stop(); + await server.stop() } -}); +}) test("calls a tool", async () => { await runWithTestServer({ @@ -223,30 +215,30 @@ test("calls a tool", async () => { }), ).toEqual({ content: [{ text: "3", type: "text" }], - }); + }) }, server: async () => { const server = new FastMCP({ name: "Test", version: "1.0.0", - }); + }) server.addTool({ description: "Add two numbers", execute: async (args) => { - return String(args.a + args.b); + return String(args.a + args.b) }, name: "add", parameters: z.object({ a: z.number(), b: z.number(), }), - }); + }) - return server; + return server }, - }); -}); + }) +}) test("returns a list", async () => { await runWithTestServer({ @@ -264,13 +256,13 @@ test("returns a list", async () => { { text: "a", type: "text" }, { text: "b", type: "text" }, ], - }); + }) }, server: async () => { const server = new FastMCP({ name: "Test", version: "1.0.0", - }); + }) server.addTool({ description: "Add two numbers", @@ -280,19 +272,19 @@ test("returns a list", async () => { { text: "a", type: "text" }, { text: "b", type: "text" }, ], - }; + } }, name: "add", parameters: z.object({ a: z.number(), b: z.number(), }), - }); + }) - return server; + return server }, - }); -}); + }) +}) test("returns an image", async () => { await runWithTestServer({ @@ -313,13 +305,13 @@ test("returns an image", async () => { type: "image", }, ], - }); + }) }, server: async () => { const server = new FastMCP({ name: "Test", version: "1.0.0", - }); + }) server.addTool({ description: "Add two numbers", @@ -329,19 +321,19 @@ test("returns an image", async () => { "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=", "base64", ), - }); + }) }, name: "add", parameters: z.object({ a: z.number(), b: z.number(), }), - }); + }) - return server; + return server }, - }); -}); + }) +}) test("returns an audio", async () => { await runWithTestServer({ @@ -362,13 +354,13 @@ test("returns an audio", async () => { type: "audio", }, ], - }); + }) }, server: async () => { const server = new FastMCP({ name: "Test", version: "1.0.0", - }); + }) server.addTool({ description: "Add two numbers", @@ -378,19 +370,19 @@ test("returns an audio", async () => { "UklGRhwMAABXQVZFZm10IBAAAAABAAEAgD4AAIA+AAABAAgAZGF0Ya4LAACAgICAgICAgICAgICAgICAgICAgICAgICAf3hxeH+AfXZ1eHx6dnR5fYGFgoOKi42aloubq6GOjI2Op7ythXJ0eYF5aV1AOFFib32HmZSHhpCalIiYi4SRkZaLfnhxaWptb21qaWBea2BRYmZTVmFgWFNXVVVhaGdbYGhZbXh1gXZ1goeIlot1k6yxtKaOkaWhq7KonKCZoaCjoKWuqqmurK6ztrO7tbTAvru/vb68vbW6vLGqsLOfm5yal5KKhoyBeHt2dXBnbmljVlJWUEBBPDw9Mi4zKRwhIBYaGRQcHBURGB0XFxwhGxocJSstMjg6PTc6PUxVV1lWV2JqaXN0coCHhIyPjpOenqWppK6xu72yxMu9us7Pw83Wy9nY29ve6OPr6uvs6ezu6ejk6erm3uPj3dbT1sjBzdDFuMHAt7m1r7W6qaCupJOTkpWPgHqAd3JrbGlnY1peX1hTUk9PTFRKR0RFQkRBRUVEQkdBPjs9Pzo6NT04Njs+PTxAPzo/Ojk6PEA5PUJAQD04PkRCREZLUk1KT1BRUVdXU1VRV1tZV1xgXltcXF9hXl9eY2VmZmlna3J0b3F3eHyBfX+JgIWJiouTlZCTmpybnqSgnqyrqrO3srK2uL2/u7jAwMLFxsfEv8XLzcrIy83JzcrP0s3M0dTP0drY1dPR1dzc19za19XX2dnU1NjU0dXPzdHQy8rMysfGxMLBvLu3ta+sraeioJ2YlI+MioeFfX55cnJsaWVjXVlbVE5RTktHRUVAPDw3NC8uLyknKSIiJiUdHiEeGx4eHRwZHB8cHiAfHh8eHSEhISMoJyMnKisrLCszNy8yOTg9QEJFRUVITVFOTlJVWltaXmNfX2ZqZ21xb3R3eHqAhoeJkZKTlZmhpJ6kqKeur6yxtLW1trW4t6+us7axrbK2tLa6ury7u7u9u7vCwb+/vr7Ev7y9v8G8vby6vru4uLq+tri8ubi5t7W4uLW5uLKxs7G0tLGwt7Wvs7avr7O0tLW4trS4uLO1trW1trm1tLm0r7Kyr66wramsqaKlp52bmpeWl5KQkImEhIB8fXh3eHJrbW5mYGNcWFhUUE1LRENDQUI9ODcxLy8vMCsqLCgoKCgpKScoKCYoKygpKyssLi0sLi0uMDIwMTIuLzQ0Njg4Njc8ODlBQ0A/RUdGSU5RUVFUV1pdXWFjZGdpbG1vcXJ2eXh6fICAgIWIio2OkJGSlJWanJqbnZ2cn6Kkp6enq62srbCysrO1uLy4uL+/vL7CwMHAvb/Cvbq9vLm5uba2t7Sysq+urqyqqaalpqShoJ+enZuamZqXlZWTkpGSkpCNjpCMioqLioiHhoeGhYSGg4GDhoKDg4GBg4GBgoGBgoOChISChISChIWDg4WEgoSEgYODgYGCgYGAgICAgX99f398fX18e3p6e3t7enp7fHx4e3x6e3x7fHx9fX59fn1+fX19fH19fnx9fn19fX18fHx7fHx6fH18fXx8fHx7fH1+fXx+f319fn19fn1+gH9+f4B/fn+AgICAgH+AgICAgIGAgICAgH9+f4B+f35+fn58e3t8e3p5eXh4d3Z1dHRzcXBvb21sbmxqaWhlZmVjYmFfX2BfXV1cXFxaWVlaWVlYV1hYV1hYWVhZWFlaWllbXFpbXV5fX15fYWJhYmNiYWJhYWJjZGVmZ2hqbG1ub3Fxc3V3dnd6e3t8e3x+f3+AgICAgoGBgoKDhISFh4aHiYqKi4uMjYyOj4+QkZKUlZWXmJmbm52enqCioqSlpqeoqaqrrK2ur7CxsrGys7O0tbW2tba3t7i3uLe4t7a3t7i3tre2tba1tLSzsrKysbCvrq2sq6qop6alo6OioJ+dnJqZmJeWlJKSkI+OjoyLioiIh4WEg4GBgH9+fXt6eXh3d3V0c3JxcG9ubWxsamppaWhnZmVlZGRjYmNiYWBhYGBfYF9fXl5fXl1dXVxdXF1dXF1cXF1cXF1dXV5dXV5fXl9eX19gYGFgYWJhYmFiY2NiY2RjZGNkZWRlZGVmZmVmZmVmZ2dmZ2hnaGhnaGloZ2hpaWhpamlqaWpqa2pra2xtbGxtbm1ubm5vcG9wcXBxcnFycnN0c3N0dXV2d3d4eHh5ent6e3x9fn5/f4CAgIGCg4SEhYaGh4iIiYqLi4uMjY2Oj5CQkZGSk5OUlJWWlpeYl5iZmZqbm5ybnJ2cnZ6en56fn6ChoKChoqGio6KjpKOko6SjpKWkpaSkpKSlpKWkpaSlpKSlpKOkpKOko6KioaKhoaCfoJ+enp2dnJybmpmZmJeXlpWUk5STkZGQj4+OjYyLioqJh4eGhYSEgoKBgIB/fn59fHt7enl5eHd3dnZ1dHRzc3JycXBxcG9vbm5tbWxrbGxraWppaWhpaGdnZ2dmZ2ZlZmVmZWRlZGVkY2RjZGNkZGRkZGRkZGRkZGRjZGRkY2RjZGNkZWRlZGVmZWZmZ2ZnZ2doaWhpaWpra2xsbW5tbm9ub29wcXFycnNzdHV1dXZ2d3d4eXl6enp7fHx9fX5+f4CAgIGAgYGCgoOEhISFhoWGhoeIh4iJiImKiYqLiouLjI2MjI2OjY6Pj46PkI+QkZCRkJGQkZGSkZKRkpGSkZGRkZKRkpKRkpGSkZKRkpGSkZKRkpGSkZCRkZCRkI+Qj5CPkI+Pjo+OjY6Njo2MjYyLjIuMi4qLioqJiomJiImIh4iHh4aHhoaFhoWFhIWEg4SDg4KDgoKBgoGAgYCBgICAgICAf4CAf39+f35/fn1+fX59fHx9fH18e3x7fHt6e3p7ent6e3p5enl6enl6eXp5eXl4eXh5eHl4eXh5eHl4eXh5eHh3eHh4d3h4d3h3d3h4d3l4eHd4d3h3eHd4d3h3eHh4eXh5eHl4eHl4eXh5enl6eXp5enl6eXp5ent6ent6e3x7fHx9fH18fX19fn1+fX5/fn9+f4B/gH+Af4CAgICAgIGAgYCBgoGCgYKCgoKDgoOEg4OEg4SFhIWEhYSFhoWGhYaHhoeHhoeGh4iHiIiHiImIiImKiYqJiYqJiouKi4qLiouKi4qLiouKi4qLiouKi4qLi4qLiouKi4qLiomJiomIiYiJiImIh4iIh4iHhoeGhYWGhYaFhIWEg4OEg4KDgoOCgYKBgIGAgICAgH+Af39+f359fn18fX19fHx8e3t6e3p7enl6eXp5enl6enl5eXh5eHh5eHl4eXh5eHl4eHd5eHd3eHl4d3h3eHd4d3h3eHh4d3h4d3h3d3h5eHl4eXh5eHl5eXp5enl6eXp7ent6e3p7e3t7fHt8e3x8fHx9fH1+fX59fn9+f35/gH+AgICAgICAgYGAgYKBgoGCgoKDgoOEg4SEhIWFhIWFhoWGhYaGhoaHhoeGh4aHhoeIh4iHiIeHiIeIh4iHiIeIiIiHiIeIh4iHiIiHiIeIh4iHiIeIh4eIh4eIh4aHh4aHhoeGh4aHhoWGhYaFhoWFhIWEhYSFhIWEhISDhIOEg4OCg4OCg4KDgYKCgYKCgYCBgIGAgYCBgICAgICAgICAf4B/f4B/gH+Af35/fn9+f35/fn1+fn19fn1+fX59fn19fX19fH18fXx9fH18fXx9fH18fXx8fHt8e3x7fHt8e3x7fHt8e3x7fHt8e3x7fHt8e3x7fHt8e3x8e3x7fHt8e3x7fHx8fXx9fH18fX5+fX59fn9+f35+f35/gH+Af4B/gICAgICAgICAgICAgYCBgIGAgIGAgYGBgoGCgYKBgoGCgYKBgoGCgoKDgoOCg4KDgoOCg4KDgoOCg4KDgoOCg4KDgoOCg4KDgoOCg4KDgoOCg4KDgoOCg4KDgoOCg4KDgoOCg4KCgoGCgYKBgoGCgYKBgoGCgYKBgoGCgYKBgoGCgYKBgoGCgYKBgoGCgYKBgoGBgYCBgIGAgYCBgIGAgYCBgIGAgYCBgIGAgYCBgIGAgYCAgICBgIGAgYCBgIGAgYCBgIGAgYCBgExJU1RCAAAASU5GT0lDUkQMAAAAMjAwOC0wOS0yMQAASUVORwMAAAAgAAABSVNGVBYAAABTb255IFNvdW5kIEZvcmdlIDguMAAA", "base64", ), - }); + }) }, name: "add", parameters: z.object({ a: z.number(), b: z.number(), }), - }); + }) - return server; + return server }, - }); -}); + }) +}) test("handles UserError errors", async () => { await runWithTestServer({ @@ -406,30 +398,30 @@ test("handles UserError errors", async () => { ).toEqual({ content: [{ text: "Something went wrong", type: "text" }], isError: true, - }); + }) }, server: async () => { const server = new FastMCP({ name: "Test", version: "1.0.0", - }); + }) server.addTool({ description: "Add two numbers", execute: async () => { - throw new UserError("Something went wrong"); + throw new UserError("Something went wrong") }, name: "add", parameters: z.object({ a: z.number(), b: z.number(), }), - }); + }) - return server; + return server }, - }); -}); + }) +}) test("handles UserError errors with extras", async () => { await runWithTestServer({ @@ -444,7 +436,7 @@ test("handles UserError errors with extras", async () => { content: [{ text: "Something went wrong", type: "text" }], isError: true, structuredContent: { foo: "bar", num: 42 }, - }); + }) // Should NOT include structuredContent if extras is not present expect( @@ -455,36 +447,36 @@ test("handles UserError errors with extras", async () => { ).toEqual({ content: [{ text: "Something went wrong", type: "text" }], isError: true, - }); + }) }, server: async () => { const server = new FastMCP({ name: "Test", version: "1.0.0", - }); + }) server.addTool({ description: "Throws UserError with extras", execute: async () => { - throw new UserError("Something went wrong", { foo: "bar", num: 42 }); + throw new UserError("Something went wrong", { foo: "bar", num: 42 }) }, name: "add_with_extras", parameters: z.object({ a: z.number(), b: z.number() }), - }); + }) server.addTool({ description: "Throws UserError without extras", execute: async () => { - throw new UserError("Something went wrong"); + throw new UserError("Something went wrong") }, name: "add_without_extras", parameters: z.object({ a: z.number(), b: z.number() }), - }); + }) - return server; + return server }, - }); -}); + }) +}) test("tool can throw McpError with InvalidParams error code", async () => { await runWithTestServer({ @@ -493,37 +485,37 @@ test("tool can throw McpError with InvalidParams error code", async () => { await client.callTool({ arguments: { value: "invalid" }, name: "validate", - }); - throw new Error("Expected error to be thrown"); + }) + throw new Error("Expected error to be thrown") } catch (error) { - expect(error).toBeInstanceOf(McpError); + expect(error).toBeInstanceOf(McpError) // @ts-expect-error - we know that error is an McpError - expect(error.code).toBe(ErrorCode.InvalidParams); + expect(error.code).toBe(ErrorCode.InvalidParams) // @ts-expect-error - we know that error is an McpError - expect(error.message).toContain("Invalid value provided"); + expect(error.message).toContain("Invalid value provided") } }, server: async () => { const server = new FastMCP({ name: "Test", version: "1.0.0", - }); + }) server.addTool({ description: "Validate input", execute: async () => { - throw new McpError(ErrorCode.InvalidParams, "Invalid value provided"); + throw new McpError(ErrorCode.InvalidParams, "Invalid value provided") }, name: "validate", parameters: z.object({ value: z.string() }), - }); + }) - return server; + return server }, - }); -}); + }) +}) test("tool can throw McpError with InternalError error code", async () => { await runWithTestServer({ @@ -532,40 +524,37 @@ test("tool can throw McpError with InternalError error code", async () => { await client.callTool({ arguments: { value: "test" }, name: "process", - }); - throw new Error("Expected error to be thrown"); + }) + throw new Error("Expected error to be thrown") } catch (error) { - expect(error).toBeInstanceOf(McpError); + expect(error).toBeInstanceOf(McpError) // @ts-expect-error - we know that error is an McpError - expect(error.code).toBe(ErrorCode.InternalError); + expect(error.code).toBe(ErrorCode.InternalError) // @ts-expect-error - we know that error is an McpError - expect(error.message).toContain("Internal processing error"); + expect(error.message).toContain("Internal processing error") } }, server: async () => { const server = new FastMCP({ name: "Test", version: "1.0.0", - }); + }) server.addTool({ description: "Process data", execute: async () => { - throw new McpError( - ErrorCode.InternalError, - "Internal processing error", - ); + throw new McpError(ErrorCode.InternalError, "Internal processing error") }, name: "process", parameters: z.object({ value: z.string() }), - }); + }) - return server; + return server }, - }); -}); + }) +}) test("tool can throw McpError with custom data", async () => { await runWithTestServer({ @@ -574,16 +563,16 @@ test("tool can throw McpError with custom data", async () => { await client.callTool({ arguments: { id: "123" }, name: "find", - }); - throw new Error("Expected error to be thrown"); + }) + throw new Error("Expected error to be thrown") } catch (error) { - expect(error).toBeInstanceOf(McpError); + expect(error).toBeInstanceOf(McpError) // @ts-expect-error - we know that error is an McpError - expect(error.code).toBe(ErrorCode.InvalidRequest); + expect(error.code).toBe(ErrorCode.InvalidRequest) // @ts-expect-error - we know that error is an McpError - expect(error.message).toContain("Resource not found"); + expect(error.message).toContain("Resource not found") // Note: Custom data may not be preserved through the MCP SDK transport layer // The important part is that the error code and message are correct @@ -593,7 +582,7 @@ test("tool can throw McpError with custom data", async () => { const server = new FastMCP({ name: "Test", version: "1.0.0", - }); + }) server.addTool({ description: "Find resource", @@ -601,16 +590,16 @@ test("tool can throw McpError with custom data", async () => { throw new McpError(ErrorCode.InvalidRequest, "Resource not found", { available: ["456", "789"], id: args.id, - }); + }) }, name: "find", parameters: z.object({ id: z.string() }), - }); + }) - return server; + return server }, - }); -}); + }) +}) test("calling an unknown tool throws McpError with MethodNotFound code", async () => { await runWithTestServer({ @@ -622,29 +611,29 @@ test("calling an unknown tool throws McpError with MethodNotFound code", async ( b: 2, }, name: "add", - }); + }) } catch (error) { - expect(error).toBeInstanceOf(McpError); + expect(error).toBeInstanceOf(McpError) // @ts-expect-error - we know that error is an McpError - expect(error.code).toBe(ErrorCode.MethodNotFound); + expect(error.code).toBe(ErrorCode.MethodNotFound) } }, server: async () => { const server = new FastMCP({ name: "Test", version: "1.0.0", - }); + }) - return server; + return server }, - }); -}); + }) +}) test("tracks tool progress", async () => { await runWithTestServer({ run: async ({ client }) => { - const onProgress = vi.fn(); + const onProgress = vi.fn() await client.callTool( { @@ -658,19 +647,19 @@ test("tracks tool progress", async () => { { onprogress: onProgress, }, - ); + ) - expect(onProgress).toHaveBeenCalledTimes(1); + expect(onProgress).toHaveBeenCalledTimes(1) expect(onProgress).toHaveBeenCalledWith({ progress: 0, total: 10, - }); + }) }, server: async () => { const server = new FastMCP({ name: "Test", version: "1.0.0", - }); + }) server.addTool({ description: "Add two numbers", @@ -678,89 +667,89 @@ test("tracks tool progress", async () => { reportProgress({ progress: 0, total: 10, - }); + }) - await delay(100); + await delay(100) - return String(args.a + args.b); + return String(args.a + args.b) }, name: "add", parameters: z.object({ a: z.number(), b: z.number(), }), - }); + }) - return server; + return server }, - }); -}); + }) +}) test("provides requestMetadata to tool context", async () => { - let capturedMetadata: Record | undefined = undefined; - const metadata = { foo: "bar" }; + let capturedMetadata: Record | undefined = undefined + const metadata = { foo: "bar" } await runWithTestServer({ run: async ({ client }) => { await client.callTool({ _meta: metadata, name: "metadata-test", - }); + }) - expect(capturedMetadata).toBeDefined(); - expect(capturedMetadata).toEqual(metadata); + expect(capturedMetadata).toBeDefined() + expect(capturedMetadata).toEqual(metadata) }, server: async () => { const server = new FastMCP({ name: "Test", version: "1.0.0", - }); + }) server.addTool({ execute: async (_args, context) => { - capturedMetadata = context.requestMetadata; - return "success"; + capturedMetadata = context.requestMetadata + return "success" }, name: "metadata-test", - }); + }) - return server; + return server }, - }); -}); + }) +}) test("allows tools to return _meta in CallToolResult", async () => { - const expectedMeta = { customField: "customValue", timestamp: 1234567890 }; + const expectedMeta = { customField: "customValue", timestamp: 1234567890 } await runWithTestServer({ run: async ({ client }) => { const result = await client.callTool({ name: "meta-result-test", - }); + }) - expect(result._meta).toBeDefined(); - expect(result._meta).toEqual(expectedMeta); + expect(result._meta).toBeDefined() + expect(result._meta).toEqual(expectedMeta) }, server: async () => { const server = new FastMCP({ name: "Test", version: "1.0.0", - }); + }) server.addTool({ execute: async () => { return { _meta: expectedMeta, content: [{ text: "success", type: "text" }], - }; + } }, name: "meta-result-test", - }); + }) - return server; + return server }, - }); -}); + }) +}) test( "reports multiple progress updates without buffering", @@ -772,11 +761,11 @@ test( async () => { await runWithTestServer({ run: async ({ client }) => { - const progressCalls: Array<{ progress: number; total: number }> = []; + const progressCalls: Array<{ progress: number; total: number }> = [] const onProgress = vi.fn((data) => { - progressCalls.push(data); - }); + progressCalls.push(data) + }) await client.callTool( { @@ -789,72 +778,72 @@ test( { onprogress: onProgress, }, - ); + ) - expect(onProgress).toHaveBeenCalledTimes(4); + expect(onProgress).toHaveBeenCalledTimes(4) expect(progressCalls).toEqual([ { progress: 0, total: 100 }, { progress: 50, total: 100 }, { progress: 90, total: 100 }, { progress: 100, total: 100 }, // This was previously lost due to buffering - ]); + ]) }, server: async () => { const server = new FastMCP({ name: "Test", version: "1.0.0", - }); + }) server.addTool({ description: "Test tool for progress buffering fix", execute: async (args, { reportProgress }) => { - const { steps } = args; + const { steps } = args // Initial - await reportProgress({ progress: 0, total: 100 }); + await reportProgress({ progress: 0, total: 100 }) for (let i = 1; i <= steps; i++) { - await delay(50); // Small delay to simulate work + await delay(50) // Small delay to simulate work if (i === 1) { - await reportProgress({ progress: 50, total: 100 }); + await reportProgress({ progress: 50, total: 100 }) } else if (i === 2) { - await reportProgress({ progress: 90, total: 100 }); + await reportProgress({ progress: 90, total: 100 }) } } // This was the critical test case that failed before the fix // because there's no await after it, causing it to be buffered - await reportProgress({ progress: 100, total: 100 }); + await reportProgress({ progress: 100, total: 100 }) - return "Progress test completed"; + return "Progress test completed" }, name: "progress-test", parameters: z.object({ steps: z.number(), }), - }); + }) - return server; + return server }, - }); + }) }, -); +) test("sets logging levels", async () => { await runWithTestServer({ run: async ({ client, session }) => { - await client.setLoggingLevel("debug"); + await client.setLoggingLevel("debug") - expect(session.loggingLevel).toBe("debug"); + expect(session.loggingLevel).toBe("debug") - await client.setLoggingLevel("info"); + await client.setLoggingLevel("info") - expect(session.loggingLevel).toBe("info"); + expect(session.loggingLevel).toBe("info") }, - }); -}); + }) +}) test("handles tool timeout", async () => { await runWithTestServer({ @@ -866,34 +855,34 @@ test("handles tool timeout", async () => { b: 2, }, name: "add", - }); - throw new Error("Expected timeout error to be thrown"); + }) + throw new Error("Expected timeout error to be thrown") } catch (error) { - expect(error).toBeInstanceOf(McpError); + expect(error).toBeInstanceOf(McpError) // @ts-expect-error - we know that error is an McpError - expect(error.code).toBe(ErrorCode.InternalError); + expect(error.code).toBe(ErrorCode.InternalError) // @ts-expect-error - we know that error is an McpError - expect(error.message).toContain("timed out"); + expect(error.message).toContain("timed out") } }, server: async () => { const server = new FastMCP({ name: "Test", version: "1.0.0", - }); + }) server.addTool({ description: "Add two numbers with potential timeout", execute: async (args) => { - console.log(`Adding ${args.a} and ${args.b}`); + console.log(`Adding ${args.a} and ${args.b}`) if (args.a > 1000 || args.b > 1000) { - await new Promise((resolve) => setTimeout(resolve, 3000)); + await new Promise((resolve) => setTimeout(resolve, 3000)) } - return String(args.a + args.b); + return String(args.a + args.b) }, name: "add", parameters: z.object({ @@ -901,29 +890,26 @@ test("handles tool timeout", async () => { b: z.number(), }), timeoutMs: 1000, - }); + }) - return server; + return server }, - }); -}); + }) +}) test("sends logging messages to the client", async () => { await runWithTestServer({ run: async ({ client }) => { - const onLog = vi.fn(); - - client.setNotificationHandler( - LoggingMessageNotificationSchema, - (message) => { - if (message.method === "notifications/message") { - onLog({ - level: message.params.level, - ...(message.params.data ?? {}), - }); - } - }, - ); + const onLog = vi.fn() + + client.setNotificationHandler(LoggingMessageNotificationSchema, (message) => { + if (message.method === "notifications/message") { + onLog({ + level: message.params.level, + ...(message.params.data ?? {}), + }) + } + }) await client.callTool({ arguments: { @@ -931,58 +917,58 @@ test("sends logging messages to the client", async () => { b: 2, }, name: "add", - }); + }) - expect(onLog).toHaveBeenCalledTimes(4); + expect(onLog).toHaveBeenCalledTimes(4) expect(onLog).toHaveBeenNthCalledWith(1, { context: { foo: "bar", }, level: "debug", message: "debug message", - }); + }) expect(onLog).toHaveBeenNthCalledWith(2, { level: "error", message: "error message", - }); + }) expect(onLog).toHaveBeenNthCalledWith(3, { level: "info", message: "info message", - }); + }) expect(onLog).toHaveBeenNthCalledWith(4, { level: "warning", message: "warn message", - }); + }) }, server: async () => { const server = new FastMCP({ name: "Test", version: "1.0.0", - }); + }) server.addTool({ description: "Add two numbers", execute: async (args, { log }) => { log.debug("debug message", { foo: "bar", - }); - log.error("error message"); - log.info("info message"); - log.warn("warn message"); + }) + log.error("error message") + log.info("info message") + log.warn("warn message") - return String(args.a + args.b); + return String(args.a + args.b) }, name: "add", parameters: z.object({ a: z.number(), b: z.number(), }), - }); + }) - return server; + return server }, - }); -}); + }) +}) test("adds resources", async () => { await runWithTestServer({ @@ -995,29 +981,29 @@ test("adds resources", async () => { uri: "file:///logs/app.log", }, ], - }); + }) }, server: async () => { const server = new FastMCP({ name: "Test", version: "1.0.0", - }); + }) server.addResource({ async load() { return { text: "Example log content", - }; + } }, mimeType: "text/plain", name: "Application Logs", uri: "file:///logs/app.log", - }); + }) - return server; + return server }, - }); -}); + }) +}) test("clients reads a resource", async () => { await runWithTestServer({ @@ -1035,29 +1021,29 @@ test("clients reads a resource", async () => { uri: "file:///logs/app.log", }, ], - }); + }) }, server: async () => { const server = new FastMCP({ name: "Test", version: "1.0.0", - }); + }) server.addResource({ async load() { return { text: "Example log content", - }; + } }, mimeType: "text/plain", name: "Application Logs", uri: "file:///logs/app.log", - }); + }) - return server; + return server }, - }); -}); + }) +}) test("clients reads a resource that returns multiple resources", async () => { await runWithTestServer({ @@ -1081,13 +1067,13 @@ test("clients reads a resource that returns multiple resources", async () => { uri: "file:///logs/app.log", }, ], - }); + }) }, server: async () => { const server = new FastMCP({ name: "Test", version: "1.0.0", - }); + }) server.addResource({ async load() { @@ -1098,17 +1084,17 @@ test("clients reads a resource that returns multiple resources", async () => { { text: "b", }, - ]; + ] }, mimeType: "text/plain", name: "Application Logs", uri: "file:///logs/app.log", - }); + }) - return server; + return server }, - }); -}); + }) +}) test("embedded resources work in tools", async () => { await runWithTestServer({ @@ -1131,14 +1117,14 @@ test("embedded resources work in tools", async () => { type: "resource", }, ], - }); + }) }, server: async () => { const server = new FastMCP({ name: "Test", version: "1.0.0", - }); + }) server.addResourceTemplate({ arguments: [ @@ -1150,12 +1136,12 @@ test("embedded resources work in tools", async () => { async load(args) { return { text: `{"id":"${args.userId}","name":"User","email":"user@example.com"}`, - }; + } }, mimeType: "application/json", name: "User Profile", uriTemplate: "user://profile/{userId}", - }); + }) server.addTool({ description: "Get user profile data", @@ -1163,24 +1149,22 @@ test("embedded resources work in tools", async () => { return { content: [ { - resource: await server.embedded( - `user://profile/${args.userId}`, - ), + resource: await server.embedded(`user://profile/${args.userId}`), type: "resource", }, ], - }; + } }, name: "get_user_profile", parameters: z.object({ userId: z.string(), }), - }); + }) - return server; + return server }, - }); -}); + }) +}) test("embedded resources work with direct resources", async () => { await runWithTestServer({ @@ -1201,25 +1185,25 @@ test("embedded resources work with direct resources", async () => { type: "resource", }, ], - }); + }) }, server: async () => { const server = new FastMCP({ name: "Test", version: "1.0.0", - }); + }) server.addResource({ async load() { return { text: "Example log content", - }; + } }, mimeType: "text/plain", name: "Application Logs", uri: "file:///logs/app.log", - }); + }) server.addTool({ description: "Get application logs", @@ -1231,16 +1215,16 @@ test("embedded resources work with direct resources", async () => { type: "resource", }, ], - }; + } }, name: "get_logs", parameters: z.object({}), - }); + }) - return server; + return server }, - }); -}); + }) +}) test("embedded resources work with URI templates and query parameters", async () => { await runWithTestServer({ @@ -1264,7 +1248,7 @@ test("embedded resources work with URI templates and query parameters", async () type: "resource", }, ], - }); + }) // Test case 2: Query parameters with different order expect( @@ -1285,7 +1269,7 @@ test("embedded resources work with URI templates and query parameters", async () type: "resource", }, ], - }); + }) // Test case 3: Query parameters with encoded values expect( @@ -1306,14 +1290,14 @@ test("embedded resources work with URI templates and query parameters", async () type: "resource", }, ], - }); + }) }, server: async () => { const server = new FastMCP({ name: "Test", version: "1.0.0", - }); + }) server.addResourceTemplate({ arguments: [ @@ -1333,16 +1317,15 @@ test("embedded resources work with URI templates and query parameters", async () query: args.q, type: "search", }), - }; + } }, mimeType: "application/json", name: "Search Resource", uriTemplate: "ui://search{?location,q}", - }); + }) server.addTool({ - description: - "Get search resource data using embedded function with query parameters", + description: "Get search resource data using embedded function with query parameters", execute: async (args) => { return { content: [ @@ -1351,18 +1334,18 @@ test("embedded resources work with URI templates and query parameters", async () type: "resource", }, ], - }; + } }, name: "get_search_resource", parameters: z.object({ uri: z.string(), }), - }); + }) - return server; + return server }, - }); -}); + }) +}) test("embedded resources work with complex URI template patterns", async () => { await runWithTestServer({ @@ -1386,7 +1369,7 @@ test("embedded resources work with complex URI template patterns", async () => { type: "resource", }, ], - }); + }) // Test case 2: Optional query parameters (some missing) expect( @@ -1407,14 +1390,14 @@ test("embedded resources work with complex URI template patterns", async () => { type: "resource", }, ], - }); + }) }, server: async () => { const server = new FastMCP({ name: "Test", version: "1.0.0", - }); + }) server.addResourceTemplate({ arguments: [ @@ -1434,25 +1417,24 @@ test("embedded resources work with complex URI template patterns", async () => { async load(args) { const result: Record = { userId: args.userId, - }; + } if (args.fields) { - result.fields = args.fields; + result.fields = args.fields } if (args.format) { - result.format = args.format; + result.format = args.format } return { text: JSON.stringify(result), - }; + } }, mimeType: "application/json", name: "User Data API", uriTemplate: "api://users/{userId}{?fields,format}", - }); + }) server.addTool({ - description: - "Get user data using complex URI templates with path and query parameters", + description: "Get user data using complex URI templates with path and query parameters", execute: async (args) => { return { content: [ @@ -1461,18 +1443,18 @@ test("embedded resources work with complex URI template patterns", async () => { type: "resource", }, ], - }; + } }, name: "get_user_data", parameters: z.object({ uri: z.string(), }), - }); + }) - return server; + return server }, - }); -}); + }) +}) test("adds prompts", async () => { await runWithTestServer({ @@ -1495,7 +1477,7 @@ test("adds prompts", async () => { role: "user", }, ], - }); + }) expect(await client.listPrompts()).toEqual({ prompts: [ @@ -1511,13 +1493,13 @@ test("adds prompts", async () => { name: "git-commit", }, ], - }); + }) }, server: async () => { const server = new FastMCP({ name: "Test", version: "1.0.0", - }); + }) server.addPrompt({ arguments: [ @@ -1529,36 +1511,36 @@ test("adds prompts", async () => { ], description: "Generate a Git commit message", load: async (args) => { - return `Generate a concise but descriptive commit message for these changes:\n\n${args.changes}`; + return `Generate a concise but descriptive commit message for these changes:\n\n${args.changes}` }, name: "git-commit", - }); + }) - return server; + return server }, - }); -}); + }) +}) test("uses events to notify server of client connect/disconnect", async () => { - const port = await getRandomPort(); + const port = await getRandomPort() const server = new FastMCP({ name: "Test", version: "1.0.0", - }); + }) - const onConnect = vi.fn().mockResolvedValue(undefined); - const onDisconnect = vi.fn().mockResolvedValue(undefined); + const onConnect = vi.fn().mockResolvedValue(undefined) + const onDisconnect = vi.fn().mockResolvedValue(undefined) - server.on("connect", onConnect); - server.on("disconnect", onDisconnect); + server.on("connect", onConnect) + server.on("disconnect", onDisconnect) await server.start({ httpStream: { port, }, transportType: "httpStream", - }); + }) const client = new Client( { @@ -1568,45 +1550,43 @@ test("uses events to notify server of client connect/disconnect", async () => { { capabilities: {}, }, - ); + ) - const transport = new SSEClientTransport( - new URL(`http://localhost:${port}/sse`), - ); + const transport = new SSEClientTransport(new URL(`http://localhost:${port}/sse`)) - await client.connect(transport); + await client.connect(transport) - await delay(100); + await delay(100) - expect(onConnect).toHaveBeenCalledTimes(1); - expect(onDisconnect).toHaveBeenCalledTimes(0); + expect(onConnect).toHaveBeenCalledTimes(1) + expect(onDisconnect).toHaveBeenCalledTimes(0) - expect(server.sessions).toEqual([expect.any(FastMCPSession)]); + expect(server.sessions).toEqual([expect.any(FastMCPSession)]) - await client.close(); + await client.close() - await delay(100); + await delay(100) - expect(onConnect).toHaveBeenCalledTimes(1); - expect(onDisconnect).toHaveBeenCalledTimes(1); + expect(onConnect).toHaveBeenCalledTimes(1) + expect(onDisconnect).toHaveBeenCalledTimes(1) - await server.stop(); -}); + await server.stop() +}) test("handles multiple clients", async () => { - const port = await getRandomPort(); + const port = await getRandomPort() const server = new FastMCP({ name: "Test", version: "1.0.0", - }); + }) await server.start({ httpStream: { port, }, transportType: "httpStream", - }); + }) const client1 = new Client( { @@ -1616,13 +1596,11 @@ test("handles multiple clients", async () => { { capabilities: {}, }, - ); + ) - const transport1 = new SSEClientTransport( - new URL(`http://localhost:${port}/sse`), - ); + const transport1 = new SSEClientTransport(new URL(`http://localhost:${port}/sse`)) - await client1.connect(transport1); + await client1.connect(transport1) const client2 = new Client( { @@ -1632,23 +1610,18 @@ test("handles multiple clients", async () => { { capabilities: {}, }, - ); + ) - const transport2 = new SSEClientTransport( - new URL(`http://localhost:${port}/sse`), - ); + const transport2 = new SSEClientTransport(new URL(`http://localhost:${port}/sse`)) - await client2.connect(transport2); + await client2.connect(transport2) - await delay(100); + await delay(100) - expect(server.sessions).toEqual([ - expect.any(FastMCPSession), - expect.any(FastMCPSession), - ]); + expect(server.sessions).toEqual([expect.any(FastMCPSession), expect.any(FastMCPSession)]) - await server.stop(); -}); + await server.stop() +}) test("session knows about client capabilities", async () => { await runWithTestServer({ @@ -1665,7 +1638,7 @@ test("session knows about client capabilities", async () => { }, }, }, - ); + ) client.setRequestHandler(ListRootsRequestSchema, () => { return { @@ -1675,20 +1648,20 @@ test("session knows about client capabilities", async () => { uri: "file:///home/user/projects/frontend", }, ], - }; - }); + } + }) - return client; + return client }, run: async ({ session }) => { expect(session.clientCapabilities).toEqual({ roots: { listChanged: true, }, - }); + }) }, - }); -}); + }) +}) test("session knows about roots", async () => { await runWithTestServer({ @@ -1705,7 +1678,7 @@ test("session knows about roots", async () => { }, }, }, - ); + ) client.setRequestHandler(ListRootsRequestSchema, () => { return { @@ -1715,10 +1688,10 @@ test("session knows about roots", async () => { uri: "file:///home/user/projects/frontend", }, ], - }; - }); + } + }) - return client; + return client }, run: async ({ session }) => { expect(session.roots).toEqual([ @@ -1726,10 +1699,10 @@ test("session knows about roots", async () => { name: "Frontend Repository", uri: "file:///home/user/projects/frontend", }, - ]); + ]) }, - }); -}); + }) +}) test("session listens to roots changes", async () => { const clientRoots: Root[] = [ @@ -1737,7 +1710,7 @@ test("session listens to roots changes", async () => { name: "Frontend Repository", uri: "file:///home/user/projects/frontend", }, - ]; + ] await runWithTestServer({ client: async () => { @@ -1753,15 +1726,15 @@ test("session listens to roots changes", async () => { }, }, }, - ); + ) client.setRequestHandler(ListRootsRequestSchema, () => { return { roots: clientRoots, - }; - }); + } + }) - return client; + return client }, run: async ({ client, session }) => { expect(session.roots).toEqual([ @@ -1769,20 +1742,20 @@ test("session listens to roots changes", async () => { name: "Frontend Repository", uri: "file:///home/user/projects/frontend", }, - ]); + ]) clientRoots.push({ name: "Backend Repository", uri: "file:///home/user/projects/backend", - }); + }) - await client.sendRootsListChanged(); + await client.sendRootsListChanged() - const onRootsChanged = vi.fn(); + const onRootsChanged = vi.fn() - session.on("rootsChanged", onRootsChanged); + session.on("rootsChanged", onRootsChanged) - await delay(100); + await delay(100) expect(session.roots).toEqual([ { @@ -1793,9 +1766,9 @@ test("session listens to roots changes", async () => { name: "Backend Repository", uri: "file:///home/user/projects/backend", }, - ]); + ]) - expect(onRootsChanged).toHaveBeenCalledTimes(1); + expect(onRootsChanged).toHaveBeenCalledTimes(1) expect(onRootsChanged).toHaveBeenCalledWith({ roots: [ { @@ -1807,22 +1780,22 @@ test("session listens to roots changes", async () => { uri: "file:///home/user/projects/backend", }, ], - }); + }) }, - }); -}); + }) +}) test("session sends pings to the client", async () => { await runWithTestServer({ run: async ({ client }) => { - const onPing = vi.fn().mockReturnValue({}); + const onPing = vi.fn().mockReturnValue({}) - client.setRequestHandler(PingRequestSchema, onPing); + client.setRequestHandler(PingRequestSchema, onPing) - await delay(2000); + await delay(2000) - expect(onPing.mock.calls.length).toBeGreaterThanOrEqual(1); - expect(onPing.mock.calls.length).toBeLessThanOrEqual(3); + expect(onPing.mock.calls.length).toBeGreaterThanOrEqual(1) + expect(onPing.mock.calls.length).toBeLessThanOrEqual(3) }, server: async () => { const server = new FastMCP({ @@ -1832,11 +1805,11 @@ test("session sends pings to the client", async () => { intervalMs: 1000, }, version: "1.0.0", - }); - return server; + }) + return server }, - }); -}); + }) +}) test("completes prompt arguments", async () => { await runWithTestServer({ @@ -1850,19 +1823,19 @@ test("completes prompt arguments", async () => { name: "countryPoem", type: "ref/prompt", }, - }); + }) expect(response).toEqual({ completion: { values: ["Germany"], }, - }); + }) }, server: async () => { const server = new FastMCP({ name: "Test", version: "1.0.0", - }); + }) server.addPrompt({ arguments: [ @@ -1871,12 +1844,12 @@ test("completes prompt arguments", async () => { if (value === "Germ") { return { values: ["Germany"], - }; + } } return { values: [], - }; + } }, description: "Name of the country", name: "name", @@ -1885,15 +1858,15 @@ test("completes prompt arguments", async () => { ], description: "Writes a poem about a country", load: async ({ name }) => { - return `Hello, ${name}!`; + return `Hello, ${name}!` }, name: "countryPoem", - }); + }) - return server; + return server }, - }); -}); + }) +}) test("adds automatic prompt argument completion when enum is provided", async () => { await runWithTestServer({ @@ -1907,20 +1880,20 @@ test("adds automatic prompt argument completion when enum is provided", async () name: "countryPoem", type: "ref/prompt", }, - }); + }) expect(response).toEqual({ completion: { total: 1, values: ["Germany"], }, - }); + }) }, server: async () => { const server = new FastMCP({ name: "Test", version: "1.0.0", - }); + }) server.addPrompt({ arguments: [ @@ -1933,15 +1906,15 @@ test("adds automatic prompt argument completion when enum is provided", async () ], description: "Writes a poem about a country", load: async ({ name }) => { - return `Hello, ${name}!`; + return `Hello, ${name}!` }, name: "countryPoem", - }); + }) - return server; + return server }, - }); -}); + }) +}) test("completes template resource arguments", async () => { await runWithTestServer({ @@ -1955,19 +1928,19 @@ test("completes template resource arguments", async () => { type: "ref/resource", uri: "issue:///{issueId}", }, - }); + }) expect(response).toEqual({ completion: { values: ["123456"], }, - }); + }) }, server: async () => { const server = new FastMCP({ name: "Test", version: "1.0.0", - }); + }) server.addResourceTemplate({ arguments: [ @@ -1976,12 +1949,12 @@ test("completes template resource arguments", async () => { if (value === "123") { return { values: ["123456"], - }; + } } return { values: [], - }; + } }, description: "ID of the issue", name: "issueId", @@ -1990,17 +1963,17 @@ test("completes template resource arguments", async () => { load: async ({ issueId }) => { return { text: `Issue ${issueId}`, - }; + } }, mimeType: "text/plain", name: "Issue", uriTemplate: "issue:///{issueId}", - }); + }) - return server; + return server }, - }); -}); + }) +}) test("lists resource templates", async () => { await runWithTestServer({ @@ -2013,13 +1986,13 @@ test("lists resource templates", async () => { uriTemplate: "file:///logs/{name}.log", }, ], - }); + }) }, server: async () => { const server = new FastMCP({ name: "Test", version: "1.0.0", - }); + }) server.addResourceTemplate({ arguments: [ @@ -2032,107 +2005,101 @@ test("lists resource templates", async () => { load: async ({ name }) => { return { text: `Example log content for ${name}`, - }; + } }, mimeType: "text/plain", name: "Application Logs", uriTemplate: "file:///logs/{name}.log", - }); + }) - return server; + return server }, - }); -}); + }) +}) -test( - "HTTP Stream: custom endpoint works with /another-mcp", - { timeout: 20000 }, - async () => { - const port = await getRandomPort(); +test("HTTP Stream: custom endpoint works with /another-mcp", { timeout: 20000 }, async () => { + const port = await getRandomPort() - // Create server with custom endpoint - const server = new FastMCP({ - name: "Test", - version: "1.0.0", - }); + // Create server with custom endpoint + const server = new FastMCP({ + name: "Test", + version: "1.0.0", + }) - server.addTool({ - description: "Add two numbers", - execute: async (args) => { - return String(args.a + args.b); - }, - name: "add", - parameters: z.object({ - a: z.number(), - b: z.number(), - }), - }); + server.addTool({ + description: "Add two numbers", + execute: async (args) => { + return String(args.a + args.b) + }, + name: "add", + parameters: z.object({ + a: z.number(), + b: z.number(), + }), + }) - await server.start({ - httpStream: { - endpoint: "/another-mcp", - port, + await server.start({ + httpStream: { + endpoint: "/another-mcp", + port, + }, + transportType: "httpStream", + }) + + try { + // Create client + const client = new Client( + { + name: "example-client", + version: "1.0.0", + }, + { + capabilities: {}, }, - transportType: "httpStream", - }); + ) - try { - // Create client - const client = new Client( - { - name: "example-client", - version: "1.0.0", - }, - { - capabilities: {}, - }, - ); + const transport = new StreamableHTTPClientTransport(new URL(`http://localhost:${port}/another-mcp`)) - const transport = new StreamableHTTPClientTransport( - new URL(`http://localhost:${port}/another-mcp`), - ); + // Connect client to server and wait for session to be ready + const sessionPromise = new Promise((resolve) => { + server.on("connect", async (event) => { + await event.session.waitForReady() + resolve(event.session) + }) + }) - // Connect client to server and wait for session to be ready - const sessionPromise = new Promise((resolve) => { - server.on("connect", async (event) => { - await event.session.waitForReady(); - resolve(event.session); - }); - }); + await client.connect(transport) + await sessionPromise - await client.connect(transport); - await sessionPromise; + // Call tool + const result = await client.callTool({ + arguments: { + a: 5, + b: 7, + }, + name: "add", + }) - // Call tool - const result = await client.callTool({ - arguments: { - a: 5, - b: 7, - }, - name: "add", - }); + // Check result + expect(result).toEqual({ + content: [{ text: "12", type: "text" }], + }) - // Check result - expect(result).toEqual({ - content: [{ text: "12", type: "text" }], - }); - - // Clean up connection - await transport.terminateSession(); - await client.close(); - } finally { - await server.stop(); - } - }, -); + // Clean up connection + await transport.terminateSession() + await client.close() + } finally { + await server.stop() + } +}) test("clients reads a resource accessed via a resource template", async () => { // eslint-disable-next-line @typescript-eslint/no-unused-vars const loadSpy = vi.fn((_args) => { return { text: "Example log content", - }; - }); + } + }) await runWithTestServer({ run: async ({ client }) => { @@ -2149,17 +2116,17 @@ test("clients reads a resource accessed via a resource template", async () => { uri: "file:///logs/app.log", }, ], - }); + }) expect(loadSpy).toHaveBeenCalledWith({ name: "app", - }); + }) }, server: async () => { const server = new FastMCP({ name: "Test", version: "1.0.0", - }); + }) server.addResourceTemplate({ arguments: [ @@ -2169,17 +2136,17 @@ test("clients reads a resource accessed via a resource template", async () => { }, ], async load(args) { - return loadSpy(args); + return loadSpy(args) }, mimeType: "text/plain", name: "Application Logs", uriTemplate: "file:///logs/{name}.log", - }); + }) - return server; + return server }, - }); -}); + }) +}) test("makes a sampling request", async () => { const onMessageRequest = vi.fn(() => { @@ -2190,8 +2157,8 @@ test("makes a sampling request", async () => { }, model: "gpt-3.5-turbo", role: "assistant", - }; - }); + } + }) await runWithTestServer({ client: async () => { @@ -2205,11 +2172,11 @@ test("makes a sampling request", async () => { sampling: {}, }, }, - ); - return client; + ) + return client }, run: async ({ client, session }) => { - client.setRequestHandler(CreateMessageRequestSchema, onMessageRequest); + client.setRequestHandler(CreateMessageRequestSchema, onMessageRequest) const response = await session.requestSampling({ includeContext: "thisServer", @@ -2224,7 +2191,7 @@ test("makes a sampling request", async () => { }, ], systemPrompt: "You are a helpful file system assistant.", - }); + }) expect(response).toEqual({ content: { @@ -2233,12 +2200,12 @@ test("makes a sampling request", async () => { }, model: "gpt-3.5-turbo", role: "assistant", - }); + }) - expect(onMessageRequest).toHaveBeenCalledTimes(1); + expect(onMessageRequest).toHaveBeenCalledTimes(1) }, - }); -}); + }) +}) test("throws ErrorCode.InvalidParams if tool parameters do not match zod schema", async () => { await runWithTestServer({ @@ -2250,41 +2217,41 @@ test("throws ErrorCode.InvalidParams if tool parameters do not match zod schema" b: "invalid", }, name: "add", - }); + }) } catch (error) { - expect(error).toBeInstanceOf(McpError); + expect(error).toBeInstanceOf(McpError) // @ts-expect-error - we know that error is an McpError - expect(error.code).toBe(ErrorCode.InvalidParams); + expect(error.code).toBe(ErrorCode.InvalidParams) // @ts-expect-error - we know that error is an McpError expect(error.message).toBe( "MCP error -32602: MCP error -32602: Tool 'add' parameter validation failed: b: Expected number, received string. Please check the parameter types and values according to the tool's schema.", - ); + ) } }, server: async () => { const server = new FastMCP({ name: "Test", version: "1.0.0", - }); + }) server.addTool({ description: "Add two numbers", execute: async (args) => { - return String(args.a + args.b); + return String(args.a + args.b) }, name: "add", parameters: z.object({ a: z.number(), b: z.number(), }), - }); + }) - return server; + return server }, - }); -}); + }) +}) test("server remains usable after InvalidParams error", async () => { await runWithTestServer({ @@ -2296,17 +2263,17 @@ test("server remains usable after InvalidParams error", async () => { b: "invalid", }, name: "add", - }); + }) } catch (error) { - expect(error).toBeInstanceOf(McpError); + expect(error).toBeInstanceOf(McpError) // @ts-expect-error - we know that error is an McpError - expect(error.code).toBe(ErrorCode.InvalidParams); + expect(error.code).toBe(ErrorCode.InvalidParams) // @ts-expect-error - we know that error is an McpError expect(error.message).toBe( "MCP error -32602: MCP error -32602: Tool 'add' parameter validation failed: b: Expected number, received string. Please check the parameter types and values according to the tool's schema.", - ); + ) } expect( @@ -2319,57 +2286,57 @@ test("server remains usable after InvalidParams error", async () => { }), ).toEqual({ content: [{ text: "3", type: "text" }], - }); + }) }, server: async () => { const server = new FastMCP({ name: "Test", version: "1.0.0", - }); + }) server.addTool({ description: "Add two numbers", execute: async (args) => { - return String(args.a + args.b); + return String(args.a + args.b) }, name: "add", parameters: z.object({ a: z.number(), b: z.number(), }), - }); + }) - return server; + return server }, - }); -}); + }) +}) test("allows new clients to connect after a client disconnects", async () => { - const port = await getRandomPort(); + const port = await getRandomPort() const server = new FastMCP({ name: "Test", version: "1.0.0", - }); + }) server.addTool({ description: "Add two numbers", execute: async (args) => { - return String(args.a + args.b); + return String(args.a + args.b) }, name: "add", parameters: z.object({ a: z.number(), b: z.number(), }), - }); + }) await server.start({ httpStream: { port, }, transportType: "httpStream", - }); + }) const client1 = new Client( { @@ -2379,13 +2346,11 @@ test("allows new clients to connect after a client disconnects", async () => { { capabilities: {}, }, - ); + ) - const transport1 = new SSEClientTransport( - new URL(`http://localhost:${port}/sse`), - ); + const transport1 = new SSEClientTransport(new URL(`http://localhost:${port}/sse`)) - await client1.connect(transport1); + await client1.connect(transport1) expect( await client1.callTool({ @@ -2397,9 +2362,9 @@ test("allows new clients to connect after a client disconnects", async () => { }), ).toEqual({ content: [{ text: "3", type: "text" }], - }); + }) - await client1.close(); + await client1.close() const client2 = new Client( { @@ -2409,13 +2374,11 @@ test("allows new clients to connect after a client disconnects", async () => { { capabilities: {}, }, - ); + ) - const transport2 = new SSEClientTransport( - new URL(`http://localhost:${port}/sse`), - ); + const transport2 = new SSEClientTransport(new URL(`http://localhost:${port}/sse`)) - await client2.connect(transport2); + await client2.connect(transport2) expect( await client2.callTool({ @@ -2427,52 +2390,52 @@ test("allows new clients to connect after a client disconnects", async () => { }), ).toEqual({ content: [{ text: "3", type: "text" }], - }); + }) - await client2.close(); + await client2.close() - await server.stop(); -}); + await server.stop() +}) test("able to close server immediately after starting it", async () => { - const port = await getRandomPort(); + const port = await getRandomPort() const server = new FastMCP({ name: "Test", version: "1.0.0", - }); + }) await server.start({ httpStream: { port, }, transportType: "httpStream", - }); + }) // We were previously not waiting for the server to start. // Therefore, this would have caused error 'Server is not running.'. - await server.stop(); -}); + await server.stop() +}) test("closing event source does not produce error", async () => { - const port = await getRandomPort(); + const port = await getRandomPort() const server = new FastMCP({ name: "Test", version: "1.0.0", - }); + }) server.addTool({ description: "Add two numbers", execute: async (args) => { - return String(args.a + args.b); + return String(args.a + args.b) }, name: "add", parameters: z.object({ a: z.number(), b: z.number(), }), - }); + }) await server.start({ httpStream: { @@ -2480,52 +2443,52 @@ test("closing event source does not produce error", async () => { port, }, transportType: "httpStream", - }); + }) const eventSource = await new Promise((onMessage) => { const eventSource = createEventSource({ onConnect: () => { - console.info("connected"); + console.info("connected") }, onDisconnect: () => { - console.info("disconnected"); + console.info("disconnected") }, onMessage: () => { - onMessage(eventSource); + onMessage(eventSource) }, url: `http://127.0.0.1:${port}/sse`, - }); - }); + }) + }) - expect(eventSource.readyState).toBe("open"); + expect(eventSource.readyState).toBe("open") - eventSource.close(); + eventSource.close() // We were getting unhandled error 'Not connected' // https://github.com/punkpeye/mcp-proxy/commit/62cf27d5e3dfcbc353e8d03c7714a62c37177b52 - await delay(1000); + await delay(1000) - await server.stop(); -}); + await server.stop() +}) test("provides auth to tools", async () => { - const port = await getRandomPort(); + const port = await getRandomPort() const authenticate = vi.fn(async () => { return { id: 1, - }; - }); + } + }) const server = new FastMCP<{ id: number }>({ authenticate, name: "Test", version: "1.0.0", - }); + }) const execute = vi.fn(async (args) => { - return String(args.a + args.b); - }); + return String(args.a + args.b) + }) server.addTool({ description: "Add two numbers", @@ -2535,14 +2498,14 @@ test("provides auth to tools", async () => { a: z.number(), b: z.number(), }), - }); + }) await server.start({ httpStream: { port, }, transportType: "httpStream", - }); + }) const client = new Client( { @@ -2552,31 +2515,25 @@ test("provides auth to tools", async () => { { capabilities: {}, }, - ); + ) - const transport = new SSEClientTransport( - new URL(`http://localhost:${port}/sse`), - { - eventSourceInit: { - fetch: async (url, init) => { - return fetch(url, { - ...init, - headers: { - ...init?.headers, - "x-api-key": "123", - }, - }); - }, + const transport = new SSEClientTransport(new URL(`http://localhost:${port}/sse`), { + eventSourceInit: { + fetch: async (url, init) => { + return fetch(url, { + ...init, + headers: { + ...init?.headers, + "x-api-key": "123", + }, + }) }, }, - ); + }) - await client.connect(transport); + await client.connect(transport) - expect( - authenticate, - "authenticate should have been called", - ).toHaveBeenCalledTimes(1); + expect(authenticate, "authenticate should have been called").toHaveBeenCalledTimes(1) expect( await client.callTool({ @@ -2588,9 +2545,9 @@ test("provides auth to tools", async () => { }), ).toEqual({ content: [{ text: "3", type: "text" }], - }); + }) - expect(execute, "execute should have been called").toHaveBeenCalledTimes(1); + expect(execute, "execute should have been called").toHaveBeenCalledTimes(1) expect(execute).toHaveBeenCalledWith( { @@ -2611,44 +2568,44 @@ test("provides auth to tools", async () => { sessionId: expect.any(String), streamContent: expect.any(Function), }, - ); -}); + ) +}) test("provides auth to resources", async () => { - const port = await getRandomPort(); + const port = await getRandomPort() const authenticate = vi.fn(async () => { return { role: "admin", userId: 42, - }; - }); + } + }) const server = new FastMCP<{ role: string; userId: number }>({ authenticate, name: "Test", version: "1.0.0", - }); + }) const resourceLoad = vi.fn(async (auth) => { return { text: `User ${auth?.userId} with role ${auth?.role} loaded this resource`, - }; - }); + } + }) server.addResource({ load: resourceLoad, mimeType: "text/plain", name: "Auth Resource", uri: "auth://resource", - }); + }) await server.start({ httpStream: { port, }, transportType: "httpStream", - }); + }) const client = new Client( { @@ -2658,36 +2615,33 @@ test("provides auth to resources", async () => { { capabilities: {}, }, - ); + ) - const transport = new SSEClientTransport( - new URL(`http://localhost:${port}/sse`), - { - eventSourceInit: { - fetch: async (url, init) => { - return fetch(url, { - ...init, - headers: { - ...init?.headers, - "x-api-key": "123", - }, - }); - }, + const transport = new SSEClientTransport(new URL(`http://localhost:${port}/sse`), { + eventSourceInit: { + fetch: async (url, init) => { + return fetch(url, { + ...init, + headers: { + ...init?.headers, + "x-api-key": "123", + }, + }) }, }, - ); + }) - await client.connect(transport); + await client.connect(transport) const result = await client.readResource({ uri: "auth://resource", - }); + }) - expect(resourceLoad).toHaveBeenCalledTimes(1); + expect(resourceLoad).toHaveBeenCalledTimes(1) expect(resourceLoad).toHaveBeenCalledWith({ role: "admin", userId: 42, - }); + }) expect(result).toEqual({ contents: [ @@ -2698,30 +2652,30 @@ test("provides auth to resources", async () => { uri: "auth://resource", }, ], - }); -}); + }) +}) test("provides auth to resource templates", async () => { - const port = await getRandomPort(); + const port = await getRandomPort() const authenticate = vi.fn(async () => { return { permissions: ["read", "write"], userId: 99, - }; - }); + } + }) const server = new FastMCP<{ permissions: string[]; userId: number }>({ authenticate, name: "Test", version: "1.0.0", - }); + }) const templateLoad = vi.fn(async (args, auth) => { return { text: `Resource ${args.resourceId} accessed by user ${auth?.userId} with permissions: ${auth?.permissions?.join(", ")}`, - }; - }); + } + }) server.addResourceTemplate({ arguments: [ @@ -2734,14 +2688,14 @@ test("provides auth to resource templates", async () => { mimeType: "text/plain", name: "Auth Template", uriTemplate: "auth://template/{resourceId}", - }); + }) await server.start({ httpStream: { port, }, transportType: "httpStream", - }); + }) const client = new Client( { @@ -2751,36 +2705,33 @@ test("provides auth to resource templates", async () => { { capabilities: {}, }, - ); + ) - const transport = new SSEClientTransport( - new URL(`http://localhost:${port}/sse`), - { - eventSourceInit: { - fetch: async (url, init) => { - return fetch(url, { - ...init, - headers: { - ...init?.headers, - "x-api-key": "123", - }, - }); - }, + const transport = new SSEClientTransport(new URL(`http://localhost:${port}/sse`), { + eventSourceInit: { + fetch: async (url, init) => { + return fetch(url, { + ...init, + headers: { + ...init?.headers, + "x-api-key": "123", + }, + }) }, }, - ); + }) - await client.connect(transport); + await client.connect(transport) const result = await client.readResource({ uri: "auth://template/resource-123", - }); + }) - expect(templateLoad).toHaveBeenCalledTimes(1); + expect(templateLoad).toHaveBeenCalledTimes(1) expect(templateLoad).toHaveBeenCalledWith( { resourceId: "resource-123" }, { permissions: ["read", "write"], userId: 99 }, - ); + ) expect(result).toEqual({ contents: [ @@ -2791,24 +2742,24 @@ test("provides auth to resource templates", async () => { uri: "auth://template/resource-123", }, ], - }); -}); + }) +}) test("provides auth to resource templates returning arrays", async () => { - const port = await getRandomPort(); + const port = await getRandomPort() const authenticate = vi.fn(async () => { return { accessLevel: 3, teamId: "team-alpha", - }; - }); + } + }) const server = new FastMCP<{ accessLevel: number; teamId: string }>({ authenticate, name: "Test", version: "1.0.0", - }); + }) const templateLoad = vi.fn(async (args, auth) => { return [ @@ -2818,8 +2769,8 @@ test("provides auth to resource templates returning arrays", async () => { { text: `Document 2 for ${args.category} - Access Level: ${auth?.accessLevel}`, }, - ]; - }); + ] + }) server.addResourceTemplate({ arguments: [ @@ -2832,14 +2783,14 @@ test("provides auth to resource templates returning arrays", async () => { mimeType: "text/plain", name: "Multi Doc Template", uriTemplate: "docs://category/{category}", - }); + }) await server.start({ httpStream: { port, }, transportType: "httpStream", - }); + }) const client = new Client( { @@ -2849,36 +2800,30 @@ test("provides auth to resource templates returning arrays", async () => { { capabilities: {}, }, - ); + ) - const transport = new SSEClientTransport( - new URL(`http://localhost:${port}/sse`), - { - eventSourceInit: { - fetch: async (url, init) => { - return fetch(url, { - ...init, - headers: { - ...init?.headers, - "x-api-key": "123", - }, - }); - }, + const transport = new SSEClientTransport(new URL(`http://localhost:${port}/sse`), { + eventSourceInit: { + fetch: async (url, init) => { + return fetch(url, { + ...init, + headers: { + ...init?.headers, + "x-api-key": "123", + }, + }) }, }, - ); + }) - await client.connect(transport); + await client.connect(transport) const result = await client.readResource({ uri: "docs://category/reports", - }); + }) - expect(templateLoad).toHaveBeenCalledTimes(1); - expect(templateLoad).toHaveBeenCalledWith( - { category: "reports" }, - { accessLevel: 3, teamId: "team-alpha" }, - ); + expect(templateLoad).toHaveBeenCalledTimes(1) + expect(templateLoad).toHaveBeenCalledWith({ category: "reports" }, { accessLevel: 3, teamId: "team-alpha" }) expect(result).toEqual({ contents: [ @@ -2895,33 +2840,30 @@ test("provides auth to resource templates returning arrays", async () => { uri: "docs://category/reports", }, ], - }); -}); + }) +}) test("provides auth to prompt argument completion", async () => { - const port = await getRandomPort(); + const port = await getRandomPort() const authenticate = vi.fn(async () => { return { department: "engineering", userId: 100, - }; - }); + } + }) const server = new FastMCP<{ department: string; userId: number }>({ authenticate, name: "Test", version: "1.0.0", - }); + }) const promptCompleter = vi.fn(async (value: string, auth) => { return { - values: [ - `${value}_user${auth?.userId}`, - `${value}_dept${auth?.department}`, - ], - }; - }); + values: [`${value}_user${auth?.userId}`, `${value}_dept${auth?.department}`], + } + }) server.addPrompt({ arguments: [ @@ -2933,17 +2875,17 @@ test("provides auth to prompt argument completion", async () => { }, ], async load(args) { - return `Loading project: ${args.project}`; + return `Loading project: ${args.project}` }, name: "load-project", - }); + }) await server.start({ httpStream: { port, }, transportType: "httpStream", - }); + }) const client = new Client( { @@ -2953,26 +2895,23 @@ test("provides auth to prompt argument completion", async () => { { capabilities: {}, }, - ); + ) - const transport = new SSEClientTransport( - new URL(`http://localhost:${port}/sse`), - { - eventSourceInit: { - fetch: async (url, init) => { - return fetch(url, { - ...init, - headers: { - ...init?.headers, - "x-api-key": "123", - }, - }); - }, + const transport = new SSEClientTransport(new URL(`http://localhost:${port}/sse`), { + eventSourceInit: { + fetch: async (url, init) => { + return fetch(url, { + ...init, + headers: { + ...init?.headers, + "x-api-key": "123", + }, + }) }, }, - ); + }) - await client.connect(transport); + await client.connect(transport) const completionResult = await client.complete({ argument: { @@ -2983,40 +2922,40 @@ test("provides auth to prompt argument completion", async () => { name: "load-project", type: "ref/prompt", }, - }); + }) - expect(promptCompleter).toHaveBeenCalledTimes(1); + expect(promptCompleter).toHaveBeenCalledTimes(1) expect(promptCompleter).toHaveBeenCalledWith("test", { department: "engineering", userId: 100, - }); + }) expect(completionResult).toEqual({ completion: { values: ["test_user100", "test_deptengineering"], }, - }); -}); + }) +}) test("provides auth to prompt load function", async () => { - const port = await getRandomPort(); + const port = await getRandomPort() const authenticate = vi.fn(async () => { return { level: "admin", username: "testuser", - }; - }); + } + }) const server = new FastMCP<{ level: string; username: string }>({ authenticate, name: "Test", version: "1.0.0", - }); + }) const promptLoad = vi.fn(async (args, auth) => { - return `Welcome ${auth?.username} (${auth?.level}): You selected ${args.option}`; - }); + return `Welcome ${auth?.username} (${auth?.level}): You selected ${args.option}` + }) server.addPrompt({ arguments: [ @@ -3028,14 +2967,14 @@ test("provides auth to prompt load function", async () => { ], load: promptLoad, name: "auth-prompt", - }); + }) await server.start({ httpStream: { port, }, transportType: "httpStream", - }); + }) const client = new Client( { @@ -3045,37 +2984,31 @@ test("provides auth to prompt load function", async () => { { capabilities: {}, }, - ); + ) - const transport = new SSEClientTransport( - new URL(`http://localhost:${port}/sse`), - { - eventSourceInit: { - fetch: async (url, init) => { - return fetch(url, { - ...init, - headers: { - ...init?.headers, - "x-api-key": "123", - }, - }); - }, + const transport = new SSEClientTransport(new URL(`http://localhost:${port}/sse`), { + eventSourceInit: { + fetch: async (url, init) => { + return fetch(url, { + ...init, + headers: { + ...init?.headers, + "x-api-key": "123", + }, + }) }, }, - ); + }) - await client.connect(transport); + await client.connect(transport) const result = await client.getPrompt({ arguments: { option: "dashboard" }, name: "auth-prompt", - }); + }) - expect(promptLoad).toHaveBeenCalledTimes(1); - expect(promptLoad).toHaveBeenCalledWith( - { option: "dashboard" }, - { level: "admin", username: "testuser" }, - ); + expect(promptLoad).toHaveBeenCalledTimes(1) + expect(promptLoad).toHaveBeenCalledWith({ option: "dashboard" }, { level: "admin", username: "testuser" }) expect(result).toEqual({ messages: [ @@ -3087,30 +3020,30 @@ test("provides auth to prompt load function", async () => { role: "user", }, ], - }); -}); + }) +}) test("provides auth to resource template argument completion", async () => { - const port = await getRandomPort(); + const port = await getRandomPort() const authenticate = vi.fn(async () => { return { region: "us-west", teamId: "alpha", - }; - }); + } + }) const server = new FastMCP<{ region: string; teamId: string }>({ authenticate, name: "Test", version: "1.0.0", - }); + }) const resourceCompleter = vi.fn(async (value: string, auth) => { return { values: [`${value}_${auth?.region}`, `${value}_team_${auth?.teamId}`], - }; - }); + } + }) server.addResourceTemplate({ arguments: [ @@ -3124,19 +3057,19 @@ test("provides auth to resource template argument completion", async () => { async load(args) { return { text: `Service ${args.serviceId} data`, - }; + } }, mimeType: "text/plain", name: "Service Resource", uriTemplate: "service://{serviceId}", - }); + }) await server.start({ httpStream: { port, }, transportType: "httpStream", - }); + }) const client = new Client( { @@ -3146,26 +3079,23 @@ test("provides auth to resource template argument completion", async () => { { capabilities: {}, }, - ); + ) - const transport = new SSEClientTransport( - new URL(`http://localhost:${port}/sse`), - { - eventSourceInit: { - fetch: async (url, init) => { - return fetch(url, { - ...init, - headers: { - ...init?.headers, - "x-api-key": "123", - }, - }); - }, + const transport = new SSEClientTransport(new URL(`http://localhost:${port}/sse`), { + eventSourceInit: { + fetch: async (url, init) => { + return fetch(url, { + ...init, + headers: { + ...init?.headers, + "x-api-key": "123", + }, + }) }, }, - ); + }) - await client.connect(transport); + await client.connect(transport) const completionResult = await client.complete({ argument: { @@ -3176,49 +3106,49 @@ test("provides auth to resource template argument completion", async () => { type: "ref/resource", uri: "service://{serviceId}", }, - }); + }) - expect(resourceCompleter).toHaveBeenCalledTimes(1); + expect(resourceCompleter).toHaveBeenCalledTimes(1) expect(resourceCompleter).toHaveBeenCalledWith("api", { region: "us-west", teamId: "alpha", - }); + }) expect(completionResult).toEqual({ completion: { values: ["api_us-west", "api_team_alpha"], }, - }); -}); + }) +}) test("supports streaming output from tools", async () => { - let streamResult: { content: Array<{ text: string; type: string }> }; + let streamResult: { content: Array<{ text: string; type: string }> } await runWithTestServer({ run: async ({ client }) => { const result = await client.callTool({ arguments: {}, name: "streaming-void-tool", - }); + }) expect(result).toEqual({ content: [], - }); + }) streamResult = (await client.callTool({ arguments: {}, name: "streaming-with-result", - })) as { content: Array<{ text: string; type: string }> }; + })) as { content: Array<{ text: string; type: string }> } expect(streamResult).toEqual({ content: [{ text: "Final result after streaming", type: "text" }], - }); + }) }, server: async () => { const server = new FastMCP({ name: "Test", version: "1.0.0", - }); + }) server.addTool({ annotations: { @@ -3229,19 +3159,19 @@ test("supports streaming output from tools", async () => { await context.streamContent({ text: "Streaming content 1", type: "text", - }); + }) await context.streamContent({ text: "Streaming content 2", type: "text", - }); + }) // Return void - return; + return }, name: "streaming-void-tool", parameters: z.object({}), - }); + }) server.addTool({ annotations: { @@ -3252,44 +3182,44 @@ test("supports streaming output from tools", async () => { await context.streamContent({ text: "Streaming content 1", type: "text", - }); + }) await context.streamContent({ text: "Streaming content 2", type: "text", - }); + }) - return "Final result after streaming"; + return "Final result after streaming" }, name: "streaming-with-result", parameters: z.object({}), - }); + }) - return server; + return server }, - }); -}); + }) +}) test("blocks unauthorized requests", async () => { - const port = await getRandomPort(); + const port = await getRandomPort() const server = new FastMCP<{ id: number }>({ authenticate: async () => { throw new Response(null, { status: 401, statusText: "Unauthorized", - }); + }) }, name: "Test", version: "1.0.0", - }); + }) await server.start({ httpStream: { port, }, transportType: "httpStream", - }); + }) const client = new Client( { @@ -3299,202 +3229,175 @@ test("blocks unauthorized requests", async () => { { capabilities: {}, }, - ); + ) - const transport = new SSEClientTransport( - new URL(`http://localhost:${port}/sse`), - ); + const transport = new SSEClientTransport(new URL(`http://localhost:${port}/sse`)) expect(async () => { - await client.connect(transport); - }).rejects.toThrow("SSE error: Non-200 status code (401)"); -}); + await client.connect(transport) + }).rejects.toThrow("SSE error: Non-200 status code (401)") +}) test("filters tools based on canAccess property", async () => { - const port = await getRandomPort(); + const port = await getRandomPort() const server = new FastMCP<{ role: string }>({ authenticate: async (request) => { - const role = request.headers["x-role"] as string; - return { role: role || "user" }; + const role = request.headers["x-role"] as string + return { role: role || "user" } }, name: "Test", version: "1.0.0", - }); + }) server.addTool({ canAccess: (auth) => auth?.role === "admin", description: "Admin only", execute: async () => "admin", name: "admin-tool", - }); + }) server.addTool({ description: "Available to all", execute: async () => "public", name: "public-tool", - }); + }) - await server.start({ httpStream: { port }, transportType: "httpStream" }); + await server.start({ httpStream: { port }, transportType: "httpStream" }) try { // Admin gets both tools - const adminClient = new Client( - { name: "admin", version: "1.0.0" }, - { capabilities: {} }, - ); - const adminTransport = new SSEClientTransport( - new URL(`http://localhost:${port}/sse`), - { - eventSourceInit: { - fetch: (url, init) => - fetch(url, { - ...init, - headers: { ...init?.headers, "x-role": "admin" }, - }), - }, + const adminClient = new Client({ name: "admin", version: "1.0.0" }, { capabilities: {} }) + const adminTransport = new SSEClientTransport(new URL(`http://localhost:${port}/sse`), { + eventSourceInit: { + fetch: (url, init) => + fetch(url, { + ...init, + headers: { ...init?.headers, "x-role": "admin" }, + }), }, - ); - await adminClient.connect(adminTransport); + }) + await adminClient.connect(adminTransport) - const adminTools = await adminClient.listTools(); - expect(adminTools.tools.map((t) => t.name).sort()).toEqual([ - "admin-tool", - "public-tool", - ]); + const adminTools = await adminClient.listTools() + expect(adminTools.tools.map((t) => t.name).sort()).toEqual(["admin-tool", "public-tool"]) // User gets only public tool - const userClient = new Client( - { name: "user", version: "1.0.0" }, - { capabilities: {} }, - ); - const userTransport = new SSEClientTransport( - new URL(`http://localhost:${port}/sse`), - { - eventSourceInit: { - fetch: (url, init) => - fetch(url, { - ...init, - headers: { ...init?.headers, "x-role": "user" }, - }), - }, + const userClient = new Client({ name: "user", version: "1.0.0" }, { capabilities: {} }) + const userTransport = new SSEClientTransport(new URL(`http://localhost:${port}/sse`), { + eventSourceInit: { + fetch: (url, init) => + fetch(url, { + ...init, + headers: { ...init?.headers, "x-role": "user" }, + }), }, - ); - await userClient.connect(userTransport); + }) + await userClient.connect(userTransport) - const userTools = await userClient.listTools(); - expect(userTools.tools.map((t) => t.name)).toEqual(["public-tool"]); + const userTools = await userClient.listTools() + expect(userTools.tools.map((t) => t.name)).toEqual(["public-tool"]) - await adminClient.close(); - await userClient.close(); + await adminClient.close() + await userClient.close() } finally { - await server.stop(); + await server.stop() } -}); +}) test("tools without canAccess are accessible to all", async () => { await runWithTestServer({ run: async ({ client }) => { - const tools = await client.listTools(); - expect(tools.tools).toHaveLength(1); - expect(tools.tools[0].name).toBe("test-tool"); + const tools = await client.listTools() + expect(tools.tools).toHaveLength(1) + expect(tools.tools[0].name).toBe("test-tool") const result = await client.callTool({ arguments: {}, name: "test-tool", - }); - expect( - (result.content as Array<{ text: string; type: string }>)[0], - ).toEqual({ text: "success", type: "text" }); + }) + expect((result.content as Array<{ text: string; type: string }>)[0]).toEqual({ text: "success", type: "text" }) }, server: async () => { - const server = new FastMCP({ name: "Test", version: "1.0.0" }); + const server = new FastMCP({ name: "Test", version: "1.0.0" }) server.addTool({ description: "Test tool", execute: async () => "success", name: "test-tool", - }); - return server; + }) + return server }, - }); -}); + }) +}) test("canAccess works without authentication", async () => { - const port = await getRandomPort(); + const port = await getRandomPort() const server = new FastMCP<{ role: string }>({ name: "Test", version: "1.0.0", - }); + }) server.addTool({ canAccess: (auth) => auth?.role === "admin", execute: async () => "admin", name: "admin-tool", - }); + }) server.addTool({ execute: async () => "public", name: "public-tool", - }); + }) - await server.start({ httpStream: { port }, transportType: "httpStream" }); + await server.start({ httpStream: { port }, transportType: "httpStream" }) try { - const client = new Client( - { name: "test-client", version: "1.0.0" }, - { capabilities: {} }, - ); - const transport = new SSEClientTransport( - new URL(`http://localhost:${port}/sse`), - ); - await client.connect(transport); - - const tools = await client.listTools(); - expect(tools.tools.map((t) => t.name).sort()).toEqual([ - "admin-tool", - "public-tool", - ]); - - await client.close(); + const client = new Client({ name: "test-client", version: "1.0.0" }, { capabilities: {} }) + const transport = new SSEClientTransport(new URL(`http://localhost:${port}/sse`)) + await client.connect(transport) + + const tools = await client.listTools() + expect(tools.tools.map((t) => t.name).sort()).toEqual(["admin-tool", "public-tool"]) + + await client.close() } finally { - await server.stop(); + await server.stop() } -}); +}) // We now use a direct approach for testing HTTP Stream functionality // rather than a helper function // Set longer timeout for HTTP Stream tests test("HTTP Stream: calls a tool", { timeout: 20000 }, async () => { - console.log("Starting HTTP Stream test..."); + console.log("Starting HTTP Stream test...") - const port = await getRandomPort(); + const port = await getRandomPort() // Create server directly (don't use helper function) const server = new FastMCP({ name: "Test", version: "1.0.0", - }); + }) server.addTool({ description: "Add two numbers", execute: async (args) => { - return String(args.a + args.b); + return String(args.a + args.b) }, name: "add", parameters: z.object({ a: z.number(), b: z.number(), }), - }); + }) await server.start({ httpStream: { port, }, transportType: "httpStream", - }); + }) try { // Create client @@ -3506,24 +3409,22 @@ test("HTTP Stream: calls a tool", { timeout: 20000 }, async () => { { capabilities: {}, }, - ); + ) // IMPORTANT: Don't provide sessionId manually with HTTP streaming // The server will generate a session ID automatically - const transport = new StreamableHTTPClientTransport( - new URL(`http://localhost:${port}/mcp`), - ); + const transport = new StreamableHTTPClientTransport(new URL(`http://localhost:${port}/mcp`)) // Connect client to server and wait for session to be ready const sessionPromise = new Promise((resolve) => { server.on("connect", async (event) => { - await event.session.waitForReady(); - resolve(event.session); - }); - }); + await event.session.waitForReady() + resolve(event.session) + }) + }) - await client.connect(transport); - await sessionPromise; + await client.connect(transport) + await sessionPromise // Call tool const result = await client.callTool({ @@ -3532,21 +3433,21 @@ test("HTTP Stream: calls a tool", { timeout: 20000 }, async () => { b: 2, }, name: "add", - }); + }) // Check result expect(result).toEqual({ content: [{ text: "3", type: "text" }], - }); + }) // Clean up connection - await transport.terminateSession(); + await transport.terminateSession() - await client.close(); + await client.close() } finally { - await server.stop(); + await server.stop() } -}); +}) test("uses `formatInvalidParamsErrorMessage` callback to build ErrorCode.InvalidParams error message", async () => { await runWithTestServer({ @@ -3558,17 +3459,17 @@ test("uses `formatInvalidParamsErrorMessage` callback to build ErrorCode.Invalid b: "invalid", }, name: "add", - }); + }) } catch (error) { - expect(error).toBeInstanceOf(McpError); + expect(error).toBeInstanceOf(McpError) // @ts-expect-error - we know that error is an McpError - expect(error.code).toBe(ErrorCode.InvalidParams); + expect(error.code).toBe(ErrorCode.InvalidParams) // @ts-expect-error - we know that error is an McpError expect(error.message).toBe( `MCP error -32602: MCP error -32602: Tool 'add' parameter validation failed: My custom error message: Field b failed with error 'Expected number, received string'. Please check the parameter types and values according to the tool's schema.`, - ); + ) } }, server: async () => { @@ -3578,52 +3479,52 @@ test("uses `formatInvalidParamsErrorMessage` callback to build ErrorCode.Invalid formatInvalidParamsErrorMessage: (issues) => { const message = issues .map((issue) => { - const path = issue.path?.join(".") || "root"; - return `Field ${path} failed with error '${issue.message}'`; + const path = issue.path?.join(".") || "root" + return `Field ${path} failed with error '${issue.message}'` }) - .join(", "); - return `My custom error message: ${message}`; + .join(", ") + return `My custom error message: ${message}` }, }, version: "1.0.0", - }); + }) server.addTool({ description: "Add two numbers", execute: async (args) => { - return String(args.a + args.b); + return String(args.a + args.b) }, name: "add", parameters: z.object({ a: z.number(), b: z.number(), }), - }); + }) - return server; + return server }, - }); -}); + }) +}) test("stateless mode works correctly", async () => { - const port = await getRandomPort(); + const port = await getRandomPort() const server = new FastMCP({ name: "Test server", version: "1.0.0", - }); + }) server.addTool({ description: "Add two numbers", execute: async (args) => { - return String(args.a + args.b); + return String(args.a + args.b) }, name: "add", parameters: z.object({ a: z.number(), b: z.number(), }), - }); + }) await server.start({ httpStream: { @@ -3631,7 +3532,7 @@ test("stateless mode works correctly", async () => { stateless: true, }, transportType: "httpStream", - }); + }) try { const client = new Client( @@ -3642,56 +3543,54 @@ test("stateless mode works correctly", async () => { { capabilities: {}, }, - ); + ) - const transport = new StreamableHTTPClientTransport( - new URL(`http://localhost:${port}/mcp`), - ); + const transport = new StreamableHTTPClientTransport(new URL(`http://localhost:${port}/mcp`)) - await client.connect(transport); + await client.connect(transport) // Tool call should work in stateless mode const result = await client.callTool({ arguments: { a: 5, b: 7 }, name: "add", - }); + }) expect(result.content).toEqual([ { text: "12", type: "text", }, - ]); + ]) // Multiple calls should work independently in stateless mode const result2 = await client.callTool({ arguments: { a: 10, b: 20 }, name: "add", - }); + }) expect(result2.content).toEqual([ { text: "30", type: "text", }, - ]); + ]) // Server should not track sessions in stateless mode - expect(server.sessions.length).toBe(0); + expect(server.sessions.length).toBe(0) - await client.close(); + await client.close() } finally { - await server.stop(); + await server.stop() } -}); +}) test("stateless mode health check includes mode indicator", async () => { - const port = await getRandomPort(); + const port = await getRandomPort() const server = new FastMCP({ name: "Test server", version: "1.0.0", - }); + }) await server.start({ httpStream: { @@ -3699,44 +3598,44 @@ test("stateless mode health check includes mode indicator", async () => { stateless: true, }, transportType: "httpStream", - }); + }) try { - const response = await fetch(`http://localhost:${port}/ready`); - expect(response.status).toBe(200); + const response = await fetch(`http://localhost:${port}/ready`) + expect(response.status).toBe(200) - const json = await response.json(); + const json = await response.json() expect(json).toEqual({ mode: "stateless", ready: 1, status: "ready", total: 1, - }); + }) } finally { - await server.stop(); + await server.stop() } -}); +}) test("stateless mode with valid authentication allows access", async () => { - const port = await getRandomPort(); + const port = await getRandomPort() const server = new FastMCP<{ userId: string }>({ authenticate: async () => { // Always authenticate successfully for this test - return { userId: "123" }; + return { userId: "123" } }, name: "Test server", version: "1.0.0", - }); + }) server.addTool({ description: "Test tool", execute: async () => { - return "pong"; + return "pong" }, name: "ping", parameters: z.object({}), - }); + }) await server.start({ httpStream: { @@ -3744,7 +3643,7 @@ test("stateless mode with valid authentication allows access", async () => { stateless: true, }, transportType: "httpStream", - }); + }) try { const client = new Client( @@ -3755,63 +3654,61 @@ test("stateless mode with valid authentication allows access", async () => { { capabilities: {}, }, - ); + ) - const transport = new StreamableHTTPClientTransport( - new URL(`http://localhost:${port}/mcp`), - ); + const transport = new StreamableHTTPClientTransport(new URL(`http://localhost:${port}/mcp`)) - await client.connect(transport); + await client.connect(transport) const result = await client.callTool({ arguments: {}, name: "ping", - }); + }) expect(result.content).toEqual([ { text: "pong", type: "text", }, - ]); + ]) // Server should not track sessions in stateless mode - expect(server.sessions.length).toBe(0); + expect(server.sessions.length).toBe(0) - await client.close(); + await client.close() } finally { - await server.stop(); + await server.stop() } -}); +}) test("stateless mode rejects missing Authorization header", async () => { - const port = await getRandomPort(); + const port = await getRandomPort() const server = new FastMCP<{ userId: string }>({ authenticate: async (req) => { - const authHeader = req.headers.authorization; + const authHeader = req.headers.authorization if (!authHeader || !authHeader.startsWith("Bearer ")) { throw new Response(null, { status: 401, statusText: "Unauthorized", - }); + }) } - return { userId: "123" }; + return { userId: "123" } }, name: "Test server", version: "1.0.0", - }); + }) server.addTool({ description: "Test tool", execute: async () => { - return "pong"; + return "pong" }, name: "ping", parameters: z.object({}), - }); + }) await server.start({ httpStream: { @@ -3819,7 +3716,7 @@ test("stateless mode rejects missing Authorization header", async () => { stateless: true, }, transportType: "httpStream", - }); + }) try { // Send a raw HTTP request without Authorization header @@ -3837,56 +3734,56 @@ test("stateless mode rejects missing Authorization header", async () => { "Content-Type": "application/json", }, method: "POST", - }); + }) - expect(response.status).toBe(401); + expect(response.status).toBe(401) - const body = (await response.json()) as { error?: { message?: string } }; - expect(body.error?.message).toContain("Unauthorized"); + const body = (await response.json()) as { error?: { message?: string } } + expect(body.error?.message).toContain("Unauthorized") } finally { - await server.stop(); + await server.stop() } -}); +}) test("stateless mode rejects invalid authentication token", async () => { - const port = await getRandomPort(); - const VALID_TOKEN = "valid_jwt_token"; - const INVALID_TOKEN = "invalid_jwt_token"; + const port = await getRandomPort() + const VALID_TOKEN = "valid_jwt_token" + const INVALID_TOKEN = "invalid_jwt_token" const server = new FastMCP<{ userId: string }>({ authenticate: async (req) => { - const authHeader = req.headers.authorization; + const authHeader = req.headers.authorization if (!authHeader || !authHeader.startsWith("Bearer ")) { throw new Response(null, { status: 401, statusText: "Unauthorized", - }); + }) } - const token = authHeader.split(" ")[1]; + const token = authHeader.split(" ")[1] if (token === VALID_TOKEN) { - return { userId: "123" }; + return { userId: "123" } } throw new Response(null, { status: 401, statusText: "Unauthorized", - }); + }) }, name: "Test server", version: "1.0.0", - }); + }) server.addTool({ description: "Test tool", execute: async () => { - return "pong"; + return "pong" }, name: "ping", parameters: z.object({}), - }); + }) await server.start({ httpStream: { @@ -3894,7 +3791,7 @@ test("stateless mode rejects invalid authentication token", async () => { stateless: true, }, transportType: "httpStream", - }); + }) try { // Send a raw HTTP request with invalid token @@ -3913,37 +3810,37 @@ test("stateless mode rejects invalid authentication token", async () => { "Content-Type": "application/json", }, method: "POST", - }); + }) - expect(response.status).toBe(401); + expect(response.status).toBe(401) - const body = (await response.json()) as { error?: { message?: string } }; - expect(body.error?.message).toContain("Unauthorized"); + const body = (await response.json()) as { error?: { message?: string } } + expect(body.error?.message).toContain("Unauthorized") } finally { - await server.stop(); + await server.stop() } -}); +}) test("stateless mode handles authentication function throwing errors", async () => { - const port = await getRandomPort(); + const port = await getRandomPort() const server = new FastMCP<{ userId: string }>({ authenticate: async () => { // Simulate an internal error during token validation - throw new Error("JWT validation service is down"); + throw new Error("JWT validation service is down") }, name: "Test server", version: "1.0.0", - }); + }) server.addTool({ description: "Test tool", execute: async () => { - return "pong"; + return "pong" }, name: "ping", parameters: z.object({}), - }); + }) await server.start({ httpStream: { @@ -3951,7 +3848,7 @@ test("stateless mode handles authentication function throwing errors", async () stateless: true, }, transportType: "httpStream", - }); + }) try { // Send a raw HTTP request @@ -3970,40 +3867,40 @@ test("stateless mode handles authentication function throwing errors", async () "Content-Type": "application/json", }, method: "POST", - }); + }) - expect(response.status).toBe(401); + expect(response.status).toBe(401) - const body = (await response.json()) as { error?: { message?: string } }; + const body = (await response.json()) as { error?: { message?: string } } // The actual error message should be passed through - expect(body.error?.message).toContain("JWT validation service is down"); + expect(body.error?.message).toContain("JWT validation service is down") } finally { - await server.stop(); + await server.stop() } -}); +}) test("stateless mode handles concurrent requests with authentication", async () => { - const port = await getRandomPort(); - let requestCount = 0; + const port = await getRandomPort() + let requestCount = 0 const server = new FastMCP<{ requestId: number }>({ authenticate: async () => { // Track each authentication request - requestCount++; - return { requestId: requestCount }; + requestCount++ + return { requestId: requestCount } }, name: "Test server", version: "1.0.0", - }); + }) server.addTool({ description: "Echo request ID", execute: async (_args, context) => { - return `Request ${context.session?.requestId}`; + return `Request ${context.session?.requestId}` }, name: "whoami", parameters: z.object({}), - }); + }) await server.start({ httpStream: { @@ -4011,7 +3908,7 @@ test("stateless mode handles concurrent requests with authentication", async () stateless: true, }, transportType: "httpStream", - }); + }) try { // Create two clients to test concurrent stateless requests @@ -4023,7 +3920,7 @@ test("stateless mode handles concurrent requests with authentication", async () { capabilities: {}, }, - ); + ) const client2 = new Client( { @@ -4033,67 +3930,63 @@ test("stateless mode handles concurrent requests with authentication", async () { capabilities: {}, }, - ); + ) - const transport1 = new StreamableHTTPClientTransport( - new URL(`http://localhost:${port}/mcp`), - ); + const transport1 = new StreamableHTTPClientTransport(new URL(`http://localhost:${port}/mcp`)) - const transport2 = new StreamableHTTPClientTransport( - new URL(`http://localhost:${port}/mcp`), - ); + const transport2 = new StreamableHTTPClientTransport(new URL(`http://localhost:${port}/mcp`)) - await client1.connect(transport1); - await client2.connect(transport2); + await client1.connect(transport1) + await client2.connect(transport2) // Both clients should work independently const result1 = await client1.callTool({ arguments: {}, name: "whoami", - }); + }) const result2 = await client2.callTool({ arguments: {}, name: "whoami", - }); + }) // Each request should have been authenticated - expect((result1.content as unknown[])[0]).toHaveProperty("text"); - expect((result2.content as unknown[])[0]).toHaveProperty("text"); + expect((result1.content as unknown[])[0]).toHaveProperty("text") + expect((result2.content as unknown[])[0]).toHaveProperty("text") // Server should not track sessions in stateless mode - expect(server.sessions.length).toBe(0); + expect(server.sessions.length).toBe(0) - await client1.close(); - await client2.close(); + await client1.close() + await client2.close() } finally { - await server.stop(); + await server.stop() } -}); +}) // Tests for GitHub Issue: FastMCP authentication fix // Testing the fix for session creation despite authentication failure test("authentication failure handling: should throw error when auth.authenticated is false", async () => { - const port = await getRandomPort(); + const port = await getRandomPort() const server = new FastMCP<{ authenticated: boolean; error?: string }>({ authenticate: async () => { // Simulate authentication failure with { authenticated: false } - return { authenticated: false, error: "Invalid JWT token" }; + return { authenticated: false, error: "Invalid JWT token" } }, name: "Test server", version: "1.0.0", - }); + }) server.addTool({ description: "Test tool", execute: async () => { - return "pong"; + return "pong" }, name: "ping", parameters: z.object({}), - }); + }) await server.start({ httpStream: { @@ -4101,7 +3994,7 @@ test("authentication failure handling: should throw error when auth.authenticate stateless: true, }, transportType: "httpStream", - }); + }) try { // Send a raw HTTP request that should be rejected @@ -4122,43 +4015,43 @@ test("authentication failure handling: should throw error when auth.authenticate "Content-Type": "application/json", }, method: "POST", - }); + }) // Should return 401 Unauthorized (handled by mcp-proxy) - expect(response.status).toBe(401); + expect(response.status).toBe(401) const body = (await response.json()) as { - error?: { code?: number; message?: string }; - }; - expect(body.error?.message).toContain("Invalid JWT token"); + error?: { code?: number; message?: string } + } + expect(body.error?.message).toContain("Invalid JWT token") } finally { - await server.stop(); + await server.stop() } -}); +}) test("authentication failure handling: should create session when auth.authenticated is true", async () => { - const port = await getRandomPort(); + const port = await getRandomPort() const server = new FastMCP<{ - authenticated: boolean; - session?: { userId: string }; + authenticated: boolean + session?: { userId: string } }>({ authenticate: async () => { // Simulate successful authentication - return { authenticated: true, session: { userId: "123" } }; + return { authenticated: true, session: { userId: "123" } } }, name: "Test server", version: "1.0.0", - }); + }) server.addTool({ description: "Test tool", execute: async (_args, context) => { - return `User: ${context.session?.session?.userId}`; + return `User: ${context.session?.session?.userId}` }, name: "whoami", parameters: z.object({}), - }); + }) await server.start({ httpStream: { @@ -4166,7 +4059,7 @@ test("authentication failure handling: should create session when auth.authentic stateless: true, }, transportType: "httpStream", - }); + }) try { const client = new Client( @@ -4177,49 +4070,47 @@ test("authentication failure handling: should create session when auth.authentic { capabilities: {}, }, - ); + ) - const transport = new StreamableHTTPClientTransport( - new URL(`http://localhost:${port}/mcp`), - ); + const transport = new StreamableHTTPClientTransport(new URL(`http://localhost:${port}/mcp`)) - await client.connect(transport); + await client.connect(transport) const result = await client.callTool({ arguments: {}, name: "whoami", - }); + }) expect(result.content).toEqual([ { text: "User: 123", type: "text", }, - ]); + ]) - await client.close(); + await client.close() } finally { - await server.stop(); + await server.stop() } -}); +}) test("authentication failure handling: should create session when auth is null/undefined (anonymous)", async () => { - const port = await getRandomPort(); + const port = await getRandomPort() const server = new FastMCP({ // No authenticate function - anonymous access name: "Test server", version: "1.0.0", - }); + }) server.addTool({ description: "Test tool", execute: async (_args, context) => { - return `Anonymous: ${context.session === undefined}`; + return `Anonymous: ${context.session === undefined}` }, name: "ping", parameters: z.object({}), - }); + }) await server.start({ httpStream: { @@ -4227,7 +4118,7 @@ test("authentication failure handling: should create session when auth is null/u stateless: true, }, transportType: "httpStream", - }); + }) try { const client = new Client( @@ -4238,52 +4129,50 @@ test("authentication failure handling: should create session when auth is null/u { capabilities: {}, }, - ); + ) - const transport = new StreamableHTTPClientTransport( - new URL(`http://localhost:${port}/mcp`), - ); + const transport = new StreamableHTTPClientTransport(new URL(`http://localhost:${port}/mcp`)) - await client.connect(transport); + await client.connect(transport) const result = await client.callTool({ arguments: {}, name: "ping", - }); + }) expect(result.content).toEqual([ { text: "Anonymous: true", type: "text", }, - ]); + ]) - await client.close(); + await client.close() } finally { - await server.stop(); + await server.stop() } -}); +}) test("authentication failure handling: should use default error message when auth.error is not provided", async () => { - const port = await getRandomPort(); + const port = await getRandomPort() const server = new FastMCP<{ authenticated: boolean }>({ authenticate: async () => { // Return authenticated: false without custom error message - return { authenticated: false }; + return { authenticated: false } }, name: "Test server", version: "1.0.0", - }); + }) server.addTool({ description: "Test tool", execute: async () => { - return "pong"; + return "pong" }, name: "ping", parameters: z.object({}), - }); + }) await server.start({ httpStream: { @@ -4291,7 +4180,7 @@ test("authentication failure handling: should use default error message when aut stateless: true, }, transportType: "httpStream", - }); + }) try { const response = await fetch(`http://localhost:${port}/mcp`, { @@ -4310,39 +4199,39 @@ test("authentication failure handling: should use default error message when aut "Content-Type": "application/json", }, method: "POST", - }); + }) - expect(response.status).toBe(401); + expect(response.status).toBe(401) const body = (await response.json()) as { - error?: { message?: string }; - }; - expect(body.error?.message).toContain("Authentication failed"); + error?: { message?: string } + } + expect(body.error?.message).toContain("Authentication failed") } finally { - await server.stop(); + await server.stop() } -}); +}) test("authentication failure handling: should preserve existing behavior for truthy auth results", async () => { - const port = await getRandomPort(); + const port = await getRandomPort() const server = new FastMCP<{ role: string; userId: string }>({ authenticate: async () => { // Return a truthy object without 'authenticated' field (legacy pattern) - return { role: "admin", userId: "456" }; + return { role: "admin", userId: "456" } }, name: "Test server", version: "1.0.0", - }); + }) server.addTool({ description: "Test tool", execute: async (_args, context) => { - return `User: ${context.session?.userId}, Role: ${context.session?.role}`; + return `User: ${context.session?.userId}, Role: ${context.session?.role}` }, name: "whoami", parameters: z.object({}), - }); + }) await server.start({ httpStream: { @@ -4350,7 +4239,7 @@ test("authentication failure handling: should preserve existing behavior for tru stateless: true, }, transportType: "httpStream", - }); + }) try { const client = new Client( @@ -4361,52 +4250,50 @@ test("authentication failure handling: should preserve existing behavior for tru { capabilities: {}, }, - ); + ) - const transport = new StreamableHTTPClientTransport( - new URL(`http://localhost:${port}/mcp`), - ); + const transport = new StreamableHTTPClientTransport(new URL(`http://localhost:${port}/mcp`)) - await client.connect(transport); + await client.connect(transport) const result = await client.callTool({ arguments: {}, name: "whoami", - }); + }) expect(result.content).toEqual([ { text: "User: 456, Role: admin", type: "text", }, - ]); + ]) - await client.close(); + await client.close() } finally { - await server.stop(); + await server.stop() } -}); +}) test("authentication failure handling: should handle authentication with custom error messages", async () => { - const port = await getRandomPort(); - const CUSTOM_ERROR_MSG = "Token expired at 2025-10-07T12:00:00Z"; + const port = await getRandomPort() + const CUSTOM_ERROR_MSG = "Token expired at 2025-10-07T12:00:00Z" const server = new FastMCP<{ authenticated: boolean; error?: string }>({ authenticate: async () => { - return { authenticated: false, error: CUSTOM_ERROR_MSG }; + return { authenticated: false, error: CUSTOM_ERROR_MSG } }, name: "Test server", version: "1.0.0", - }); + }) server.addTool({ description: "Test tool", execute: async () => { - return "pong"; + return "pong" }, name: "ping", parameters: z.object({}), - }); + }) await server.start({ httpStream: { @@ -4414,7 +4301,7 @@ test("authentication failure handling: should handle authentication with custom stateless: true, }, transportType: "httpStream", - }); + }) try { const response = await fetch(`http://localhost:${port}/mcp`, { @@ -4433,26 +4320,26 @@ test("authentication failure handling: should handle authentication with custom "Content-Type": "application/json", }, method: "POST", - }); + }) - expect(response.status).toBe(401); + expect(response.status).toBe(401) const body = (await response.json()) as { - error?: { message?: string }; - }; - expect(body.error?.message).toBe(CUSTOM_ERROR_MSG); + error?: { message?: string } + } + expect(body.error?.message).toBe(CUSTOM_ERROR_MSG) } finally { - await server.stop(); + await server.stop() } -}); +}) test("authentication failure handling: should not create session for authenticated=false even with session data", async () => { - const port = await getRandomPort(); + const port = await getRandomPort() const server = new FastMCP<{ - authenticated: boolean; - error?: string; - session?: { userId: string }; + authenticated: boolean + error?: string + session?: { userId: string } }>({ authenticate: async () => { // Even if session data is present, authenticated: false should reject @@ -4460,20 +4347,20 @@ test("authentication failure handling: should not create session for authenticat authenticated: false, error: "Insufficient permissions", session: { userId: "hacker" }, - }; + } }, name: "Test server", version: "1.0.0", - }); + }) server.addTool({ description: "Test tool", execute: async () => { - return "pong"; + return "pong" }, name: "ping", parameters: z.object({}), - }); + }) await server.start({ httpStream: { @@ -4481,7 +4368,7 @@ test("authentication failure handling: should not create session for authenticat stateless: true, }, transportType: "httpStream", - }); + }) try { const response = await fetch(`http://localhost:${port}/mcp`, { @@ -4500,29 +4387,29 @@ test("authentication failure handling: should not create session for authenticat "Content-Type": "application/json", }, method: "POST", - }); + }) - expect(response.status).toBe(401); + expect(response.status).toBe(401) const body = (await response.json()) as { - error?: { message?: string }; - }; - expect(body.error?.message).toContain("Insufficient permissions"); + error?: { message?: string } + } + expect(body.error?.message).toContain("Insufficient permissions") // Verify session was never created - expect(server.sessions.length).toBe(0); + expect(server.sessions.length).toBe(0) } finally { - await server.stop(); + await server.stop() } -}); +}) test("host configuration works with 0.0.0.0", async () => { - const port = await getRandomPort(); + const port = await getRandomPort() const server = new FastMCP({ name: "Test server", version: "1.0.0", - }); + }) await server.start({ httpStream: { @@ -4530,50 +4417,50 @@ test("host configuration works with 0.0.0.0", async () => { port, }, transportType: "httpStream", - }); + }) try { - const healthResponse = await fetch(`http://0.0.0.0:${port}/health`); - expect(healthResponse.status).toBe(200); - expect(await healthResponse.text()).toBe("✓ Ok"); + const healthResponse = await fetch(`http://0.0.0.0:${port}/health`) + expect(healthResponse.status).toBe(200) + expect(await healthResponse.text()).toBe("✓ Ok") } finally { - await server.stop(); + await server.stop() } -}); +}) test("tools can access client info", async () => { await runWithTestServer({ run: async ({ client }) => { const result = (await client.callTool({ name: "get-client-info", - })) as ContentResult; + })) as ContentResult - expect(result.content).toHaveLength(1); - expect(result.content[0]).toHaveProperty("type", "text"); + expect(result.content).toHaveLength(1) + expect(result.content[0]).toHaveProperty("type", "text") - const text = (result.content[0] as TextContent).text; - expect(text).toContain("Client name:"); - expect(text).toContain("Client version:"); + const text = (result.content[0] as TextContent).text + expect(text).toContain("Client name:") + expect(text).toContain("Client version:") // The client info should contain some actual client information - expect(text).toMatch(/Client name:\s+\w+/); - expect(text).toMatch(/Client version:\s+[\d.]+/); + expect(text).toMatch(/Client name:\s+\w+/) + expect(text).toMatch(/Client version:\s+[\d.]+/) }, server: async () => { const server = new FastMCP({ name: "Test", version: "1.0.0", - }); + }) server.addTool({ description: "Get client information", execute: async (_args, context) => { - const clientInfo = context.client.version; - return `Client name: ${clientInfo?.name || "unknown"}\nClient version: ${clientInfo?.version || "unknown"}`; + const clientInfo = context.client.version + return `Client name: ${clientInfo?.name || "unknown"}\nClient version: ${clientInfo?.version || "unknown"}` }, name: "get-client-info", - }); + }) - return server; + return server }, - }); -}); + }) +}) diff --git a/src/bin/fastmcp.ts b/src/bin/fastmcp.ts index 537330c..2bf39be 100644 --- a/src/bin/fastmcp.ts +++ b/src/bin/fastmcp.ts @@ -1,8 +1,7 @@ #!/usr/bin/env node - -import { execa } from "execa"; -import yargs from "yargs"; -import { hideBin } from "yargs/helpers"; +import { execa } from "execa" +import yargs from "yargs" +import { hideBin } from "yargs/helpers" await yargs(hideBin(process.argv)) .scriptName("fastmcp") @@ -29,21 +28,19 @@ await yargs(hideBin(process.argv)) default: false, describe: "Enable verbose logging", type: "boolean", - }); + }) }, async (argv) => { try { const command = argv.watch ? `npx @wong2/mcp-cli npx tsx --watch ${argv.file}` - : `npx @wong2/mcp-cli npx tsx ${argv.file}`; + : `npx @wong2/mcp-cli npx tsx ${argv.file}` if (argv.verbose) { - console.log(`[FastMCP] Starting server: ${command}`); - console.log(`[FastMCP] File: ${argv.file}`); - console.log( - `[FastMCP] Watch mode: ${argv.watch ? "enabled" : "disabled"}`, - ); + console.log(`[FastMCP] Starting server: ${command}`) + console.log(`[FastMCP] File: ${argv.file}`) + console.log(`[FastMCP] Watch mode: ${argv.watch ? "enabled" : "disabled"}`) } await execa({ @@ -51,18 +48,18 @@ await yargs(hideBin(process.argv)) stderr: "inherit", stdin: "inherit", stdout: "inherit", - })`${command}`; + })`${command}` } catch (error) { console.error( "[FastMCP Error] Failed to start development server:", error instanceof Error ? error.message : String(error), - ); + ) if (argv.verbose && error instanceof Error && error.stack) { - console.error("[FastMCP Debug] Stack trace:", error.stack); + console.error("[FastMCP Debug] Stack trace:", error.stack) } - process.exit(1); + process.exit(1) } }, ) @@ -75,7 +72,7 @@ await yargs(hideBin(process.argv)) demandOption: true, describe: "The path to the server file", type: "string", - }); + }) }, async (argv) => { @@ -83,14 +80,14 @@ await yargs(hideBin(process.argv)) await execa({ stderr: "inherit", stdout: "inherit", - })`npx @modelcontextprotocol/inspector npx tsx ${argv.file}`; + })`npx @modelcontextprotocol/inspector npx tsx ${argv.file}` } catch (error) { console.error( "[FastMCP Error] Failed to inspect server:", error instanceof Error ? error.message : String(error), - ); + ) - process.exit(1); + process.exit(1) } }, ) @@ -111,42 +108,40 @@ await yargs(hideBin(process.argv)) default: false, describe: "Enable strict validation (type checking)", type: "boolean", - }); + }) }, async (argv) => { try { - const { existsSync } = await import("fs"); - const { resolve } = await import("path"); - const filePath = resolve(argv.file); + const { existsSync } = await import("fs") + const { resolve } = await import("path") + const filePath = resolve(argv.file) if (!existsSync(filePath)) { - console.error(`[FastMCP Error] File not found: ${filePath}`); - process.exit(1); + console.error(`[FastMCP Error] File not found: ${filePath}`) + process.exit(1) } - console.log(`[FastMCP] Validating server file: ${filePath}`); + console.log(`[FastMCP] Validating server file: ${filePath}`) - const command = argv.strict - ? `npx tsc --noEmit --strict ${filePath}` - : `npx tsc --noEmit ${filePath}`; + const command = argv.strict ? `npx tsc --noEmit --strict ${filePath}` : `npx tsc --noEmit ${filePath}` try { await execa({ shell: true, stderr: "pipe", stdout: "pipe", - })`${command}`; + })`${command}` - console.log("[FastMCP] ✓ TypeScript compilation successful"); + console.log("[FastMCP] ✓ TypeScript compilation successful") } catch (tsError) { - console.error("[FastMCP] ✗ TypeScript compilation failed"); + console.error("[FastMCP] ✗ TypeScript compilation failed") if (tsError instanceof Error && "stderr" in tsError) { - console.error(tsError.stderr); + console.error(tsError.stderr) } - process.exit(1); + process.exit(1) } try { @@ -165,27 +160,22 @@ await yargs(hideBin(process.argv)) process.exit(1); } })(); - "`; + "` } catch { - console.error("[FastMCP] ✗ Server structure validation failed"); - console.error("Make sure the file properly imports and uses FastMCP"); + console.error("[FastMCP] ✗ Server structure validation failed") + console.error("Make sure the file properly imports and uses FastMCP") - process.exit(1); + process.exit(1) } - console.log( - "[FastMCP] ✓ All validations passed! Server file looks good.", - ); + console.log("[FastMCP] ✓ All validations passed! Server file looks good.") } catch (error) { - console.error( - "[FastMCP Error] Validation failed:", - error instanceof Error ? error.message : String(error), - ); + console.error("[FastMCP Error] Validation failed:", error instanceof Error ? error.message : String(error)) - process.exit(1); + process.exit(1) } }, ) .help() - .parseAsync(); + .parseAsync() diff --git a/src/examples/addition.ts b/src/examples/addition.ts index 69a0fdc..1a9cacb 100644 --- a/src/examples/addition.ts +++ b/src/examples/addition.ts @@ -8,11 +8,11 @@ * * For a complete project template, see https://github.com/punkpeye/fastmcp-boilerplate */ -import { type } from "arktype"; -import * as v from "valibot"; -import { z } from "zod"; +import { type } from "arktype" +import * as v from "valibot" +import { z } from "zod" -import { FastMCP } from "../FastMCP.js"; +import { FastMCP } from "../FastMCP.js" const server = new FastMCP({ name: "Addition", @@ -29,13 +29,13 @@ const server = new FastMCP({ // enabled: false, }, version: "1.0.0", -}); +}) // --- Zod Example --- const AddParamsZod = z.object({ a: z.number().describe("The first number"), b: z.number().describe("The second number"), -}); +}) server.addTool({ annotations: { @@ -46,18 +46,18 @@ server.addTool({ description: "Add two numbers (using Zod schema)", execute: async (args) => { // args is typed as { a: number, b: number } - console.log(`[Zod] Adding ${args.a} and ${args.b}`); - return String(args.a + args.b); + console.log(`[Zod] Adding ${args.a} and ${args.b}`) + return String(args.a + args.b) }, name: "add-zod", parameters: AddParamsZod, -}); +}) // --- ArkType Example --- const AddParamsArkType = type({ a: "number", b: "number", -}); +}) server.addTool({ annotations: { @@ -70,31 +70,31 @@ server.addTool({ description: "Add two numbers (using ArkType schema)", execute: async (args, { log }) => { // args is typed as { a: number, b: number } based on AddParamsArkType.infer - console.log(`[ArkType] Adding ${args.a} and ${args.b}`); + console.log(`[ArkType] Adding ${args.a} and ${args.b}`) // Demonstrate long-running operation that might need a timeout - log.info("Starting calculation with potential delay..."); + log.info("Starting calculation with potential delay...") // Simulate a complex calculation process if (args.a > 1000 || args.b > 1000) { - log.warn("Large numbers detected, operation might take longer"); + log.warn("Large numbers detected, operation might take longer") // In a real implementation, this delay might be a slow operation - await new Promise((resolve) => setTimeout(resolve, 3000)); + await new Promise((resolve) => setTimeout(resolve, 3000)) } - return String(args.a + args.b); + return String(args.a + args.b) }, name: "add-arktype", parameters: AddParamsArkType, // Will abort execution after 2s timeoutMs: 2000, -}); +}) // --- Valibot Example --- const AddParamsValibot = v.object({ a: v.number("The first number"), b: v.number("The second number"), -}); +}) server.addTool({ annotations: { @@ -104,23 +104,23 @@ server.addTool({ }, description: "Add two numbers (using Valibot schema)", execute: async (args) => { - console.log(`[Valibot] Adding ${args.a} and ${args.b}`); - return String(args.a + args.b); + console.log(`[Valibot] Adding ${args.a} and ${args.b}`) + return String(args.a + args.b) }, name: "add-valibot", parameters: AddParamsValibot, -}); +}) server.addResource({ async load() { return { text: "Example log content", - }; + } }, mimeType: "text/plain", name: "Application Logs", uri: "file:///logs/app.log", -}); +}) server.addTool({ annotations: { @@ -130,30 +130,30 @@ server.addTool({ }, description: "Generate a poem line by line with streaming output", execute: async (args, context) => { - const { theme } = args; + const { theme } = args const lines = [ `Poem about ${theme} - line 1`, `Poem about ${theme} - line 2`, `Poem about ${theme} - line 3`, `Poem about ${theme} - line 4`, - ]; + ] for (const line of lines) { await context.streamContent({ text: line, type: "text", - }); + }) - await new Promise((resolve) => setTimeout(resolve, 1000)); + await new Promise((resolve) => setTimeout(resolve, 1000)) } - return; + return }, name: "stream-poem", parameters: z.object({ theme: z.string().describe("Theme for the poem"), }), -}); +}) server.addTool({ annotations: { @@ -162,27 +162,27 @@ server.addTool({ }, description: "Test progress reporting without buffering delays", execute: async (args, { reportProgress }) => { - console.log("Testing progress reporting fix for HTTP Stream buffering..."); + console.log("Testing progress reporting fix for HTTP Stream buffering...") - await reportProgress({ progress: 0, total: 100 }); - await new Promise((resolve) => setTimeout(resolve, 500)); + await reportProgress({ progress: 0, total: 100 }) + await new Promise((resolve) => setTimeout(resolve, 500)) - await reportProgress({ progress: 25, total: 100 }); - await new Promise((resolve) => setTimeout(resolve, 500)); + await reportProgress({ progress: 25, total: 100 }) + await new Promise((resolve) => setTimeout(resolve, 500)) - await reportProgress({ progress: 75, total: 100 }); - await new Promise((resolve) => setTimeout(resolve, 500)); + await reportProgress({ progress: 75, total: 100 }) + await new Promise((resolve) => setTimeout(resolve, 500)) // This progress should be received immediately - await reportProgress({ progress: 100, total: 100 }); + await reportProgress({ progress: 100, total: 100 }) - return `Buffering test completed for ${args.testCase}`; + return `Buffering test completed for ${args.testCase}` }, name: "test-buffering-fix", parameters: z.object({ testCase: z.string().describe("Test case description"), }), -}); +}) server.addPrompt({ arguments: [ @@ -194,10 +194,10 @@ server.addPrompt({ ], description: "Generate a Git commit message", load: async (args) => { - return `Generate a concise but descriptive commit message for these changes:\n\n${args.changes}`; + return `Generate a concise but descriptive commit message for these changes:\n\n${args.changes}` }, name: "git-commit", -}); +}) server.addResourceTemplate({ arguments: [ @@ -216,18 +216,16 @@ server.addResourceTemplate({ "# Deployment Guide\n\nTo deploy this application:\n\n1. Build the project: `npm run build`\n2. Set environment variables\n3. Deploy to your hosting platform", "getting-started": "# Getting Started\n\nWelcome to our project! Follow these steps to set up your development environment:\n\n1. Clone the repository\n2. Install dependencies with `npm install`\n3. Run `npm start` to begin", - }; + } return { - text: - docs[args.section as keyof typeof docs] || - "Documentation section not found", - }; + text: docs[args.section as keyof typeof docs] || "Documentation section not found", + } }, mimeType: "text/markdown", name: "Project Documentation", uriTemplate: "docs://project/{section}", -}); +}) server.addTool({ annotations: { @@ -235,8 +233,7 @@ server.addTool({ readOnlyHint: true, title: "Get Documentation (Embedded)", }, - description: - "Retrieve project documentation using embedded resources - demonstrates the new embedded() feature", + description: "Retrieve project documentation using embedded resources - demonstrates the new embedded() feature", execute: async (args) => { return { content: [ @@ -245,37 +242,31 @@ server.addTool({ type: "resource", }, ], - }; + } }, name: "get-documentation", parameters: z.object({ - section: z - .enum(["getting-started", "api-reference", "deployment"]) - .describe("Documentation section to retrieve"), + section: z.enum(["getting-started", "api-reference", "deployment"]).describe("Documentation section to retrieve"), }), -}); +}) // Select transport type based on command line arguments -const transportType = process.argv.includes("--http-stream") - ? "httpStream" - : "stdio"; +const transportType = process.argv.includes("--http-stream") ? "httpStream" : "stdio" if (transportType === "httpStream") { // Start with HTTP streaming transport - const PORT = process.env.PORT ? parseInt(process.env.PORT, 10) : 8080; + const PORT = process.env.PORT ? parseInt(process.env.PORT, 10) : 8080 server.start({ httpStream: { port: PORT, }, transportType: "httpStream", - }); + }) - console.log( - `HTTP Stream MCP server is running at http://localhost:${PORT}/mcp`, - ); - console.log("Use StreamableHTTPClientTransport to connect to this server"); - console.log("For example:"); + console.log(`HTTP Stream MCP server is running at http://localhost:${PORT}/mcp`) + console.log("Use StreamableHTTPClientTransport to connect to this server") + console.log("For example:") console.log(` import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; @@ -295,15 +286,13 @@ if (transportType === "httpStream") { ); await client.connect(transport); - `); + `) } else if (process.argv.includes("--explicit-ping-config")) { server.start({ transportType: "stdio", - }); + }) - console.log( - "Started stdio transport with explicit ping configuration from server options", - ); + console.log("Started stdio transport with explicit ping configuration from server options") } else if (process.argv.includes("--disable-roots")) { // Example of disabling roots at runtime const serverWithDisabledRoots = new FastMCP({ @@ -316,18 +305,18 @@ if (transportType === "httpStream") { enabled: false, }, version: "1.0.0", - }); + }) serverWithDisabledRoots.start({ transportType: "stdio", - }); + }) - console.log("Started stdio transport with roots support disabled"); + console.log("Started stdio transport with roots support disabled") } else { // Disable by default for: server.start({ transportType: "stdio", - }); + }) - console.log("Started stdio transport with ping disabled by default"); + console.log("Started stdio transport with ping disabled by default") } diff --git a/src/examples/custom-logger.ts b/src/examples/custom-logger.ts index f204636..026ecd8 100644 --- a/src/examples/custom-logger.ts +++ b/src/examples/custom-logger.ts @@ -9,30 +9,30 @@ * */ -import { z } from "zod"; +import { z } from "zod" -import { FastMCP, Logger } from "../FastMCP.js"; +import { FastMCP, Logger } from "../FastMCP.js" // Example 1: Simple Custom Logger Implementation class SimpleCustomLogger implements Logger { debug(...args: unknown[]): void { - console.log("[CUSTOM DEBUG]", new Date().toISOString(), ...args); + console.log("[CUSTOM DEBUG]", new Date().toISOString(), ...args) } error(...args: unknown[]): void { - console.error("[CUSTOM ERROR]", new Date().toISOString(), ...args); + console.error("[CUSTOM ERROR]", new Date().toISOString(), ...args) } info(...args: unknown[]): void { - console.info("[CUSTOM INFO]", new Date().toISOString(), ...args); + console.info("[CUSTOM INFO]", new Date().toISOString(), ...args) } log(...args: unknown[]): void { - console.log("[CUSTOM LOG]", new Date().toISOString(), ...args); + console.log("[CUSTOM LOG]", new Date().toISOString(), ...args) } warn(...args: unknown[]): void { - console.warn("[CUSTOM WARN]", new Date().toISOString(), ...args); + console.warn("[CUSTOM WARN]", new Date().toISOString(), ...args) } } @@ -172,7 +172,7 @@ class SimpleCustomLogger implements Logger { // } // Choose which logger to use (uncomment the one you want to use) -const logger = new SimpleCustomLogger(); +const logger = new SimpleCustomLogger() // const logger = new FileLogger(); // const logger = new WinstonLoggerAdapter(); // const logger = new PinoLoggerAdapter(); @@ -181,21 +181,21 @@ const server = new FastMCP({ logger: logger, name: "custom-logger-example", version: "1.0.0", -}); +}) server.addTool({ description: "A test tool that demonstrates custom logging", execute: async (args) => { - return `Received: ${args.message}`; + return `Received: ${args.message}` }, name: "test_tool", parameters: z.object({ message: z.string().describe("A message to log"), }), -}); +}) // Start the server with stdio transport server.start({ transportType: "stdio" }).catch((error: unknown) => { - console.error("Failed to start server:", error); - process.exit(1); -}); + console.error("Failed to start server:", error) + process.exit(1) +}) diff --git a/src/examples/oauth-server.ts b/src/examples/oauth-server.ts index 80b23ce..589d707 100644 --- a/src/examples/oauth-server.ts +++ b/src/examples/oauth-server.ts @@ -10,7 +10,7 @@ * - http://localhost:4111/.well-known/oauth-protected-resource */ -import { FastMCP } from "../FastMCP.js"; +import { FastMCP } from "../FastMCP.js" const server = new FastMCP({ name: "OAuth Example Server", @@ -36,20 +36,14 @@ const server = new FastMCP({ scopesSupported: ["read", "write", "admin"], serviceDocumentation: "https://docs.example.com/oauth", tokenEndpoint: "https://auth.example.com/oauth/token", - tokenEndpointAuthMethodsSupported: [ - "client_secret_basic", - "client_secret_post", - ], + tokenEndpointAuthMethodsSupported: ["client_secret_basic", "client_secret_post"], tokenEndpointAuthSigningAlgValuesSupported: ["RS256", "ES256"], uiLocalesSupported: ["en-US", "es-ES"], }, enabled: true, protectedResource: { - authorizationDetailsTypesSupported: [ - "payment_initiation", - "account_access", - ], + authorizationDetailsTypesSupported: ["payment_initiation", "account_access"], authorizationServers: ["https://auth.example.com"], bearerMethodsSupported: ["header"], dpopBoundAccessTokensRequired: false, @@ -67,7 +61,7 @@ const server = new FastMCP({ }, }, version: "1.0.0", -}); +}) // Add a simple tool to demonstrate the server functionality server.addTool({ @@ -87,16 +81,16 @@ that need to integrate with OAuth 2.0 authorization flows.`, type: "text", }, ], - }; + } }, name: "get-server-info", -}); +}) // Start the server await server.start({ httpStream: { port: 4111 }, transportType: "httpStream", -}); +}) console.log(` 🚀 OAuth Example Server is running! @@ -110,4 +104,4 @@ Try these endpoints: The OAuth endpoints work with both SSE and HTTP Stream transports and return JSON metadata following RFC 8414 standards. -`); +`) diff --git a/src/examples/session-context.ts b/src/examples/session-context.ts index a1c2318..b14a661 100644 --- a/src/examples/session-context.ts +++ b/src/examples/session-context.ts @@ -9,61 +9,54 @@ * npx fastmcp dev src/examples/session-context.ts */ -import { z } from "zod"; +import { z } from "zod" -import { FastMCP } from "../FastMCP.js"; +import { FastMCP } from "../FastMCP.js" interface UserSession { - [key: string]: unknown; - permissions: string[]; - role: "admin" | "guest" | "user"; - userId: string; - username: string; + [key: string]: unknown + permissions: string[] + role: "admin" | "guest" | "user" + userId: string + username: string } const server = new FastMCP({ authenticate: async (request) => { if (!request) { - console.log( - "[Auth] Authenticating stdio transport using environment variables", - ); - - const userId = process.env.USER_ID || "default-user"; - const username = process.env.USERNAME || "Anonymous"; - const role = - (process.env.USER_ROLE as "admin" | "guest" | "user") || "guest"; + console.log("[Auth] Authenticating stdio transport using environment variables") + + const userId = process.env.USER_ID || "default-user" + const username = process.env.USERNAME || "Anonymous" + const role = (process.env.USER_ROLE as "admin" | "guest" | "user") || "guest" // Mock permissions based on role const permissions = - role === "admin" - ? ["read", "write", "delete", "admin"] - : role === "user" - ? ["read", "write"] - : ["read"]; + role === "admin" ? ["read", "write", "delete", "admin"] : role === "user" ? ["read", "write"] : ["read"] const session: UserSession = { authenticatedAt: new Date().toISOString(), permissions, role, userId, username, - }; + } - console.log(`[Auth] Authenticated user: ${username} (${role})`); + console.log(`[Auth] Authenticated user: ${username} (${role})`) - return session; + return session } // For HTTP transport (request contains headers) - console.log("[Auth] Authenticating HTTP transport using headers"); + console.log("[Auth] Authenticating HTTP transport using headers") - const authHeader = request.headers["authorization"] as string; + const authHeader = request.headers["authorization"] as string if (!authHeader || !authHeader.startsWith("Bearer ")) { throw new Response("Missing or invalid authorization header", { status: 401, - }); + }) } - const token = authHeader.substring(7); + const token = authHeader.substring(7) // Mock token validation (in real implementation, validate against your auth service) if (token === "admin-token") { @@ -73,7 +66,7 @@ const server = new FastMCP({ role: "admin" as const, userId: "admin-001", username: "Administrator", - }; + } } else if (token === "user-token") { return { authenticatedAt: new Date().toISOString(), @@ -81,23 +74,22 @@ const server = new FastMCP({ role: "user" as const, userId: "user-001", username: "Regular User", - }; + } } - throw new Response("Invalid token", { status: 401 }); + throw new Response("Invalid token", { status: 401 }) }, name: "Session Context Demo", version: "1.0.0", -}); +}) // Tool that demonstrates session context access server.addTool({ description: "Get information about the current authenticated user", execute: async (_args, context) => { - if (!context.session) - return "No session context available (this shouldn't happen after the fix!)"; + if (!context.session) return "No session context available (this shouldn't happen after the fix!)" - const { session } = context; + const { session } = context return `✓ Session Context Available! @@ -106,49 +98,46 @@ User Info: - Username: ${session.username} - Role: ${session.role} - Permissions: ${session.permissions.join(", ")} -- Authenticated At: ${session.authenticatedAt}`; +- Authenticated At: ${session.authenticatedAt}` }, name: "whoami", -}); +}) // Tool that demonstrates role-based access server.addTool({ description: "Perform an admin-only operation (requires admin role)", execute: async (args, context) => { - if (!context.session) - return "No session context - cannot verify permissions"; - if (context.session.role !== "admin") - return `Access denied. Current role: ${context.session.role}, required: admin`; - if (!context.session.permissions.includes("admin")) - return "Insufficient permissions for admin operations"; - - return `✓ Admin operation "${args.action}" executed successfully by ${context.session.username}`; + if (!context.session) return "No session context - cannot verify permissions" + if (context.session.role !== "admin") return `Access denied. Current role: ${context.session.role}, required: admin` + if (!context.session.permissions.includes("admin")) return "Insufficient permissions for admin operations" + + return `✓ Admin operation "${args.action}" executed successfully by ${context.session.username}` }, name: "admin-operation", parameters: z.object({ action: z.string().describe("The admin action to perform"), }), -}); +}) // Tool that demonstrates permission checks server.addTool({ description: "Check what permissions the current user has", execute: async (args, context) => { - if (!context.session) return "No session context available"; + if (!context.session) return "No session context available" - const hasPermission = context.session.permissions.includes(args.operation); + const hasPermission = context.session.permissions.includes(args.operation) return `Permission Check for "${args.operation}": ${hasPermission ? "✓ ALLOWED" : "! DENIED"} Your permissions: ${context.session.permissions.join(", ")} -Your role: ${context.session.role}`; +Your role: ${context.session.role}` }, name: "check-permissions", parameters: z.object({ operation: z.string().describe("Operation to check permission for"), }), -}); +}) // Resource that uses session context server.addResource({ @@ -165,7 +154,7 @@ server.addResource({ null, 2, ), - }; + } } return { @@ -184,12 +173,12 @@ server.addResource({ null, 2, ), - }; + } }, mimeType: "application/json", name: "Current User Information", uri: "session://current-user", -}); +}) // Prompt that uses session context server.addPrompt({ @@ -202,31 +191,31 @@ server.addPrompt({ ], description: "Generate a personalized greeting based on the current user", load: async (args, auth) => { - const style = args.style || "friendly"; + const style = args.style || "friendly" if (!auth) { - return "Hello! I don't have access to your session information."; + return "Hello! I don't have access to your session information." } const greetings = { casual: `Hey ${auth.username}! Nice to see you again.`, formal: `Good day, ${auth.username}. You are logged in with ${auth.role} privileges.`, friendly: `Hello ${auth.username}! 😊 You're logged in as a ${auth.role}. How can I help you today?`, - }; + } - return greetings[style as keyof typeof greetings] || greetings.friendly; + return greetings[style as keyof typeof greetings] || greetings.friendly }, name: "personalized-greeting", -}); +}) // Start the server if (process.argv.includes("--http-stream")) { - const PORT = process.env.PORT ? parseInt(process.env.PORT, 10) : 3000; + const PORT = process.env.PORT ? parseInt(process.env.PORT, 10) : 3000 server.start({ httpStream: { port: PORT }, transportType: "httpStream", - }); + }) console.log(` 🚀 Session Context Demo server running on HTTP Stream! @@ -240,9 +229,9 @@ curl -H "Authorization: Bearer admin-token" \\ -H "Content-Type: application/json" \\ -d '{"method":"tools/call","params":{"name":"whoami","arguments":{}}}' \\ http://localhost:${PORT}/mcp -`); +`) } else { - server.start({ transportType: "stdio" }); + server.start({ transportType: "stdio" }) console.log(` 🚀 Session Context Demo server started with stdio transport! @@ -265,5 +254,5 @@ Available resources: Available prompts: - personalized-greeting: Get a personalized greeting -`); +`) } diff --git a/src/examples/session-id-counter.ts b/src/examples/session-id-counter.ts index 9534463..2c052bd 100644 --- a/src/examples/session-id-counter.ts +++ b/src/examples/session-id-counter.ts @@ -11,14 +11,14 @@ * Then test with multiple clients to see how each session maintains its own state. */ -import { z } from "zod"; +import { z } from "zod" -import { FastMCP } from "../FastMCP.js"; +import { FastMCP } from "../FastMCP.js" interface UserSession { - [key: string]: unknown; - role: "admin" | "user"; - userId: string; + [key: string]: unknown + role: "admin" | "user" + userId: string } const server = new FastMCP({ @@ -28,46 +28,43 @@ const server = new FastMCP({ return { role: "user" as const, userId: process.env.USER_ID || "default-user", - }; + } } // HTTP transport - check authorization header - const authHeader = request.headers["authorization"] as string; + const authHeader = request.headers["authorization"] as string if (!authHeader || !authHeader.startsWith("Bearer ")) { throw new Response("Missing or invalid authorization header", { status: 401, - }); + }) } - const token = authHeader.substring(7); + const token = authHeader.substring(7) // Mock token validation if (token === "admin-token") { return { role: "admin" as const, userId: "admin-001", - }; + } } else if (token === "user-token") { return { role: "user" as const, userId: "user-001", - }; + } } - throw new Response("Invalid token", { status: 401 }); + throw new Response("Invalid token", { status: 401 }) }, name: "Session ID Counter Demo", version: "1.0.0", -}); +}) // Per-session counter storage // In a real application, this could be Redis, a database, or any other storage -const sessionCounters = new Map(); -const sessionData = new Map< - string, - { createdAt: Date; lastAccessed: Date; requestCount: number } ->(); +const sessionCounters = new Map() +const sessionData = new Map() // Tool to increment a per-session counter server.addTool({ @@ -75,22 +72,22 @@ server.addTool({ "Increment a counter that is unique to your session. Each client session maintains its own independent counter.", execute: async (_args, context) => { if (!context.sessionId) { - return "❌ No session ID available. This tool requires HTTP transport with session tracking."; + return "❌ No session ID available. This tool requires HTTP transport with session tracking." } - const currentCount = sessionCounters.get(context.sessionId) || 0; - const newCount = currentCount + 1; - sessionCounters.set(context.sessionId, newCount); + const currentCount = sessionCounters.get(context.sessionId) || 0 + const newCount = currentCount + 1 + sessionCounters.set(context.sessionId, newCount) // Update session metadata const metadata = sessionData.get(context.sessionId) || { createdAt: new Date(), lastAccessed: new Date(), requestCount: 0, - }; - metadata.lastAccessed = new Date(); - metadata.requestCount += 1; - sessionData.set(context.sessionId, metadata); + } + metadata.lastAccessed = new Date() + metadata.requestCount += 1 + sessionData.set(context.sessionId, metadata) return `✓ Counter incremented! @@ -102,80 +99,79 @@ Role: ${context.session?.role} Session Info: - Created: ${metadata.createdAt.toISOString()} - Last Accessed: ${metadata.lastAccessed.toISOString()} -- Total Requests: ${metadata.requestCount}`; +- Total Requests: ${metadata.requestCount}` }, name: "increment-counter", parameters: z.object({}), -}); +}) // Tool to get the current counter value server.addTool({ description: "Get the current value of your session's counter", execute: async (_args, context) => { if (!context.sessionId) { - return "❌ No session ID available. This tool requires HTTP transport with session tracking."; + return "❌ No session ID available. This tool requires HTTP transport with session tracking." } - const currentCount = sessionCounters.get(context.sessionId) || 0; - const metadata = sessionData.get(context.sessionId); + const currentCount = sessionCounters.get(context.sessionId) || 0 + const metadata = sessionData.get(context.sessionId) return `Session ID: ${context.sessionId} Counter Value: ${currentCount} User: ${context.session?.userId} -${metadata ? `\nSession created: ${metadata.createdAt.toISOString()}\nTotal requests: ${metadata.requestCount}` : ""}`; +${metadata ? `\nSession created: ${metadata.createdAt.toISOString()}\nTotal requests: ${metadata.requestCount}` : ""}` }, name: "get-counter", parameters: z.object({}), -}); +}) // Tool to reset the counter server.addTool({ description: "Reset your session's counter to zero", execute: async (_args, context) => { if (!context.sessionId) { - return "❌ No session ID available. This tool requires HTTP transport with session tracking."; + return "❌ No session ID available. This tool requires HTTP transport with session tracking." } - sessionCounters.set(context.sessionId, 0); + sessionCounters.set(context.sessionId, 0) - return `✓ Counter reset to 0 for session ${context.sessionId}`; + return `✓ Counter reset to 0 for session ${context.sessionId}` }, name: "reset-counter", parameters: z.object({}), -}); +}) // Tool to list all active sessions (admin only) server.addTool({ description: "List all active sessions and their counter values (admin only)", execute: async (_args, context) => { if (context.session?.role !== "admin") { - return "❌ Access denied. This tool requires admin role."; + return "❌ Access denied. This tool requires admin role." } if (sessionCounters.size === 0) { - return "No active sessions with counters."; + return "No active sessions with counters." } const sessions = Array.from(sessionCounters.entries()) .map(([sessionId, count]) => { - const metadata = sessionData.get(sessionId); + const metadata = sessionData.get(sessionId) return `- Session: ${sessionId.substring(0, 8)}... Counter: ${count} Created: ${metadata?.createdAt.toISOString() || "unknown"} - Requests: ${metadata?.requestCount || 0}`; + Requests: ${metadata?.requestCount || 0}` }) - .join("\n\n"); + .join("\n\n") - return `Active Sessions (${sessionCounters.size}):\n\n${sessions}`; + return `Active Sessions (${sessionCounters.size}):\n\n${sessions}` }, name: "list-sessions", parameters: z.object({}), -}); +}) // Tool to demonstrate request ID tracking server.addTool({ - description: - "Show both session ID and request ID to demonstrate per-request tracking", + description: "Show both session ID and request ID to demonstrate per-request tracking", execute: async (_args, context) => { return `Session & Request Information: @@ -185,19 +181,19 @@ User ID: ${context.session?.userId || "N/A"} Role: ${context.session?.role || "N/A"} The session ID remains constant across multiple requests from the same client, -while the request ID is unique for each individual request.`; +while the request ID is unique for each individual request.` }, name: "show-ids", parameters: z.object({}), -}); +}) // Start the server -const PORT = process.env.PORT ? parseInt(process.env.PORT, 10) : 3000; +const PORT = process.env.PORT ? parseInt(process.env.PORT, 10) : 3000 server.start({ httpStream: { port: PORT }, transportType: "httpStream", -}); +}) console.log(` 🚀 Session ID Counter Demo server running! @@ -227,4 +223,4 @@ Available tools: - show-ids: Display session and request IDs Try connecting with multiple clients to see how each maintains its own counter! -`); +`) diff --git a/vitest.config.js b/vitest.config.js index 2e668db..f76abd6 100644 --- a/vitest.config.js +++ b/vitest.config.js @@ -1,4 +1,4 @@ -import { defineConfig } from "vitest/config"; +import { defineConfig } from "vitest/config" export default defineConfig({ test: { @@ -6,4 +6,4 @@ export default defineConfig({ forks: { execArgv: ["--experimental-eventsource"] }, }, }, -}); +}) From cf5db74d3f316e350ce60e067d9a6377414d6e0c Mon Sep 17 00:00:00 2001 From: Matias Date: Tue, 24 Feb 2026 14:55:45 -0300 Subject: [PATCH 5/7] chore: remove ./dist --- dist/FastMCP.d.ts | 677 -------------------- dist/FastMCP.js | 1315 --------------------------------------- dist/FastMCP.js.map | 1 - dist/bin/fastmcp.d.ts | 1 - dist/bin/fastmcp.js | 145 ----- dist/bin/fastmcp.js.map | 1 - 6 files changed, 2140 deletions(-) delete mode 100644 dist/FastMCP.d.ts delete mode 100644 dist/FastMCP.js delete mode 100644 dist/FastMCP.js.map delete mode 100644 dist/bin/fastmcp.d.ts delete mode 100755 dist/bin/fastmcp.js delete mode 100644 dist/bin/fastmcp.js.map diff --git a/dist/FastMCP.d.ts b/dist/FastMCP.d.ts deleted file mode 100644 index e6f0049..0000000 --- a/dist/FastMCP.d.ts +++ /dev/null @@ -1,677 +0,0 @@ -import { Server } from '@modelcontextprotocol/sdk/server/index.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 { ResourceLink, RequestMeta, Root, ClientCapabilities, GetPromptResult, CreateMessageRequestSchema, McpError } from '@modelcontextprotocol/sdk/types.js'; -export { ErrorCode, McpError, RequestMeta, ResourceLink } from '@modelcontextprotocol/sdk/types.js'; -import { StandardSchemaV1 } from '@standard-schema/spec'; -import { EventEmitter } from 'events'; -import http from 'http'; -import { StrictEventEmitter } from 'strict-event-emitter-types'; -import { z } from 'zod'; - -interface Logger { - debug(...args: unknown[]): void; - error(...args: unknown[]): void; - info(...args: unknown[]): void; - log(...args: unknown[]): void; - warn(...args: unknown[]): void; -} -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; -}; -declare const imageContent: (input: { - buffer: Buffer; -} | { - path: string; -} | { - url: string; -}) => Promise; -declare const audioContent: (input: { - buffer: Buffer; -} | { - path: string; -} | { - url: string; -}) => Promise; -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; - requestMetadata?: RequestMeta; - 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; -declare abstract class FastMCPError extends Error { - constructor(message?: string); -} -/** - * Custom MCP error with a marker property to enable robust error detection - * across module boundaries. - * - * This class extends McpError and adds a `__isMcpError` marker property. - * This allows error detection to work even when instanceof fails due to - * module instances being loaded multiple times (e.g., with tsx, different - * bundlers, or ESM/CommonJS mixing). - * - * Use this instead of McpError when you need the error to be re-thrown - * by FastMCP's error handling rather than wrapped in a result. - */ -declare class CustomMcpError extends McpError { - readonly __isMcpError = true; - constructor(code: number, message: string, data?: unknown); -} -declare class UnexpectedStateError extends FastMCPError { - extras?: Extras; - constructor(message: string, extras?: Extras); -} -/** - * An error that is meant to be surfaced to the user. - */ -declare class UserError extends UnexpectedStateError { -} -/** - * Type guard to check if an error should be re-thrown as an MCP error. - * Works across module boundaries by checking both instanceof and marker property. - * - * @param error - The error to check - * @returns true if error is an McpError or has the __isMcpError marker - */ -declare function isMcpErrorLike(error: unknown): error is McpError; -type ImageContent = { - data: string; - mimeType: string; - type: "image"; -}; -type AudioContent = { - data: string; - mimeType: string; - type: "audio"; -}; -type ResourceContent = { - resource: { - blob?: string; - mimeType?: string; - text?: string; - uri: string; - }; - type: "resource"; -}; -type Content = AudioContent | ImageContent | ResourceContent | ResourceLink | TextContent; -type ContentResult = { - _meta?: Record; - content: Content[]; - isError?: boolean; -}; -type Completion = { - hasMore?: boolean; - total?: number; - values: string[]; -}; -type ArgumentValueCompleter = (value: string, auth?: T) => Promise; -type InputPrompt[] = InputPromptArgument[], Args = PromptArgumentsToObject> = { - arguments?: InputPromptArgument[]; - 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[] = InputResourceTemplateArgument[]> = { - arguments: Arguments; - description?: string; - load: (args: ResourceTemplateArgumentsToObject, auth?: T) => Promise; - mimeType?: string; - name: string; - uriTemplate: string; -}; -type InputResourceTemplateArgument = Readonly<{ - complete?: ArgumentValueCompleter; - description?: string; - name: string; - required?: boolean; -}>; -type LoggingLevel = "alert" | "critical" | "debug" | "emergency" | "error" | "info" | "notice" | "warning"; -type Prompt[] = 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["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[] = 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 = 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 = { - 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[]; - dpopSigningAlgValuesSupported?: string[]; - grantTypesSupported?: string[]; - introspectionEndpoint?: string; - issuer: string; - 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; - }; - }; - 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 = { - 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; - name: string; - 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; -}; -declare const FastMCPSessionEventEmitterBase: { - new (): StrictEventEmitter; -}; -type Authenticate = (request: http.IncomingMessage) => Promise; -type FastMCPSessionAuth = Record | undefined; -declare class FastMCPSessionEventEmitter extends FastMCPSessionEventEmitterBase { -} -declare class FastMCPSession extends FastMCPSessionEventEmitter { - #private; - get clientCapabilities(): ClientCapabilities | null; - get isReady(): boolean; - get loggingLevel(): LoggingLevel; - get roots(): Root[]; - get server(): Server; - get sessionId(): string | undefined; - set sessionId(value: string | undefined); - 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; - }); - close(): Promise; - connect(transport: Transport): Promise; - requestSampling(message: z.infer["params"], options?: RequestOptions): Promise; - waitForReady(): Promise; - private addPrompt; - private addResource; - private addResourceTemplate; - private setupCompleteHandlers; - private setupErrorHandling; - private setupLoggingHandlers; - private setupPromptHandlers; - private setupResourceHandlers; - private setupResourceTemplateHandlers; - private setupRootsHandlers; - private setupToolHandlers; -} -declare const FastMCPEventEmitterBase: { - new (): StrictEventEmitter>; -}; -declare class FastMCPEventEmitter extends FastMCPEventEmitterBase { -} -declare class FastMCP extends FastMCPEventEmitter { - #private; - options: ServerOptions; - get sessions(): FastMCPSession[]; - constructor(options: ServerOptions); - /** - * Adds a prompt to the server. - */ - addPrompt[]>(prompt: InputPrompt): void; - /** - * Adds a resource to the server. - */ - addResource(resource: Resource): void; - /** - * Adds a resource template to the server. - */ - addResourceTemplate(resource: InputResourceTemplate): void; - /** - * Adds a tool to the server. - */ - addTool(tool: Tool): void; - /** - * 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 - */ - embedded(uri: string): Promise; - /** - * Starts the server. - */ - start(options?: Partial<{ - httpStream: { - enableJsonResponse?: boolean; - endpoint?: `/${string}`; - eventStore?: EventStore; - host?: string; - port: number; - stateless?: boolean; - }; - transportType: "httpStream" | "stdio"; - }>): Promise; - /** - * Stops the server. - */ - stop(): Promise; -} - -export { type AudioContent, type Content, type ContentResult, type Context, CustomMcpError, FastMCP, type FastMCPEvents, FastMCPSession, type FastMCPSessionEvents, type ImageContent, type InputPrompt, type InputPromptArgument, type Logger, type LoggingLevel, type Progress, type Prompt, type PromptArgument, type Resource, type ResourceContent, type ResourceResult, type ResourceTemplate, type ResourceTemplateArgument, type SSEServer, type SerializableValue, type ServerOptions, type TextContent, type Tool, type ToolParameters, UnexpectedStateError, UserError, audioContent, imageContent, isMcpErrorLike }; diff --git a/dist/FastMCP.js b/dist/FastMCP.js deleted file mode 100644 index 1378c0b..0000000 --- a/dist/FastMCP.js +++ /dev/null @@ -1,1315 +0,0 @@ -// src/FastMCP.ts -import { Server } from "@modelcontextprotocol/sdk/server/index.js"; -import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; -import { - CallToolRequestSchema, - CompleteRequestSchema, - ErrorCode, - GetPromptRequestSchema, - ListPromptsRequestSchema, - ListResourcesRequestSchema, - ListResourceTemplatesRequestSchema, - ListToolsRequestSchema, - McpError, - ReadResourceRequestSchema, - RootsListChangedNotificationSchema, - SetLevelRequestSchema -} from "@modelcontextprotocol/sdk/types.js"; -import { EventEmitter } from "events"; -import { readFile } from "fs/promises"; -import Fuse from "fuse.js"; -import { startHTTPServer } from "mcp-proxy"; -import { setTimeout as delay } from "timers/promises"; -import { fetch } from "undici"; -import parseURITemplate from "uri-templates"; -import { toJsonSchema } from "xsschema"; -import { z } from "zod"; -import { ErrorCode as ErrorCode2, McpError as McpError2 } from "@modelcontextprotocol/sdk/types.js"; -var imageContent = async (input) => { - let rawData; - try { - if ("url" in input) { - try { - const response = await fetch(input.url); - if (!response.ok) { - throw new Error(`Server responded with status: ${response.status} - ${response.statusText}`); - } - rawData = Buffer.from(await response.arrayBuffer()); - } catch (error) { - throw new Error( - `Failed to fetch image from URL (${input.url}): ${error instanceof Error ? error.message : String(error)}` - ); - } - } else if ("path" in input) { - try { - rawData = await readFile(input.path); - } catch (error) { - throw new Error( - `Failed to read image from path (${input.path}): ${error instanceof Error ? error.message : String(error)}` - ); - } - } else if ("buffer" in input) { - rawData = input.buffer; - } else { - throw new Error("Invalid input: Provide a valid 'url', 'path', or 'buffer'"); - } - const { 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" - }; - } catch (error) { - if (error instanceof Error) { - throw error; - } else { - throw new Error(`Unexpected error processing image: ${String(error)}`); - } - } -}; -var audioContent = async (input) => { - let rawData; - try { - if ("url" in input) { - try { - const response = await fetch(input.url); - if (!response.ok) { - throw new Error(`Server responded with status: ${response.status} - ${response.statusText}`); - } - rawData = Buffer.from(await response.arrayBuffer()); - } catch (error) { - throw new Error( - `Failed to fetch audio from URL (${input.url}): ${error instanceof Error ? error.message : String(error)}` - ); - } - } else if ("path" in input) { - try { - rawData = await readFile(input.path); - } catch (error) { - throw new Error( - `Failed to read audio from path (${input.path}): ${error instanceof Error ? error.message : String(error)}` - ); - } - } else if ("buffer" in input) { - rawData = input.buffer; - } else { - throw new Error("Invalid input: Provide a valid 'url', 'path', or 'buffer'"); - } - const { 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" - }; - } 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 CustomMcpError = class extends McpError { - __isMcpError = true; - constructor(code, message, data) { - super(code, message, data); - } -}; -var UnexpectedStateError = class extends FastMCPError { - extras; - constructor(message, extras) { - super(message); - this.name = new.target.name; - this.extras = extras; - } -}; -var UserError = class extends UnexpectedStateError { -}; -function isMcpErrorLike(error) { - return error instanceof McpError || typeof error === "object" && error !== null && "__isMcpError" in error && error.__isMcpError === true; -} -var TextContentZodSchema = z.object({ - /** - * The text content of the message. - */ - text: z.string(), - type: z.literal("text") -}).strict(); -var ImageContentZodSchema = z.object({ - /** - * The base64-encoded image data. - */ - data: z.string().base64(), - /** - * The MIME type of the image. Different providers may support different image types. - */ - mimeType: z.string(), - type: z.literal("image") -}).strict(); -var AudioContentZodSchema = z.object({ - /** - * The base64-encoded audio data. - */ - data: z.string().base64(), - mimeType: z.string(), - type: z.literal("audio") -}).strict(); -var 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(); -var 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() -}); -var ContentZodSchema = z.discriminatedUnion("type", [ - TextContentZodSchema, - ImageContentZodSchema, - AudioContentZodSchema, - ResourceContentZodSchema, - ResourceLinkZodSchema -]); -var ContentResultZodSchema = z.object({ - _meta: z.record(z.unknown()).optional(), - content: ContentZodSchema.array(), - isError: z.boolean().optional() -}).strict(); -var CompletionZodSchema = z.object({ - /** - * Indicates whether there are additional completion options beyond those provided in the current response, even if the exact total is unknown. - */ - hasMore: z.optional(z.boolean()), - /** - * The total number of completion options available. This can exceed the number of values actually sent in the response. - */ - total: z.optional(z.number().int()), - /** - * An array of completion values. Must not exceed 100 items. - */ - values: z.array(z.string()).max(100) -}); -var FastMCPSessionEventEmitterBase = EventEmitter; -var FastMCPSessionEventEmitter = class extends FastMCPSessionEventEmitterBase { -}; -var FastMCPSession = class extends FastMCPSessionEventEmitter { - get clientCapabilities() { - return this.#clientCapabilities ?? null; - } - get 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.#server = new 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 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. - -${e instanceof Error ? e.stack : JSON.stringify(e)}` - ); - } - } - } - if (this.#clientCapabilities) { - const pingConfig = this.#getPingConfig(transport); - if (pingConfig.enabled) { - this.#pingInterval = setInterval(async () => { - try { - await this.#server.ping(); - } catch { - const logLevel = pingConfig.logLevel; - if (logLevel === "debug") { - 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; - } - } - async requestSampling(message, options) { - return this.#server.createMessage(message, options); - } - 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 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, value, auth) => { - if (completers[name]) { - return await completers[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 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); - } - return { - values: [] - }; - } - }; - this.#resourceTemplates.push(resourceTemplate); - } - setupCompleteHandlers() { - this.#server.setRequestHandler(CompleteRequestSchema, async (request) => { - if (request.params.ref.type === "ref/prompt") { - const prompt = this.#prompts.find((prompt2) => prompt2.name === request.params.ref.name); - if (!prompt) { - throw new UnexpectedStateError("Unknown prompt", { - request - }); - } - if (!prompt.complete) { - throw new UnexpectedStateError("Prompt does not support completion", { - request - }); - } - const completion = CompletionZodSchema.parse( - await prompt.complete(request.params.argument.name, request.params.argument.value, this.#auth) - ); - return { - completion - }; - } - if (request.params.ref.type === "ref/resource") { - const resource = this.#resourceTemplates.find((resource2) => resource2.uriTemplate === request.params.ref.uri); - if (!resource) { - throw new UnexpectedStateError("Unknown resource", { - request - }); - } - if (!("uriTemplate" in resource)) { - throw new UnexpectedStateError("Unexpected resource"); - } - if (!resource.complete) { - throw new UnexpectedStateError("Resource does not support completion", { - request - }); - } - const completion = CompletionZodSchema.parse( - await resource.complete(request.params.argument.name, request.params.argument.value, 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(SetLevelRequestSchema, (request) => { - this.#loggingLevel = request.params.level; - return {}; - }); - } - setupPromptHandlers(prompts) { - this.#server.setRequestHandler(ListPromptsRequestSchema, async () => { - return { - prompts: prompts.map((prompt) => { - return { - arguments: prompt.arguments, - complete: prompt.complete, - description: prompt.description, - name: prompt.name - }; - }) - }; - }); - this.#server.setRequestHandler(GetPromptRequestSchema, async (request) => { - const prompt = prompts.find((prompt2) => prompt2.name === request.params.name); - if (!prompt) { - throw new McpError(ErrorCode.MethodNotFound, `Unknown prompt: ${request.params.name}`); - } - const args = request.params.arguments; - for (const arg of prompt.arguments ?? []) { - if (arg.required && !(args && arg.name in args)) { - throw new McpError( - ErrorCode.InvalidRequest, - `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 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 - }; - } - }); - } - setupResourceHandlers(resources) { - this.#server.setRequestHandler(ListResourcesRequestSchema, async () => { - return { - resources: resources.map((resource) => ({ - description: resource.description, - mimeType: resource.mimeType, - name: resource.name, - uri: resource.uri - })) - }; - }); - this.#server.setRequestHandler(ReadResourceRequestSchema, async (request) => { - if ("uri" in request.params) { - const resource = resources.find((resource2) => "uri" in resource2 && resource2.uri === request.params.uri); - if (!resource) { - for (const resourceTemplate of this.#resourceTemplates) { - const uriTemplate = parseURITemplate(resourceTemplate.uriTemplate); - const match = uriTemplate.fromUri(request.params.uri); - if (!match) { - continue; - } - const uri = uriTemplate.fill(match); - const result = await resourceTemplate.load(match, this.#auth); - const resources2 = Array.isArray(result) ? result : [result]; - return { - contents: resources2.map((resource2) => ({ - ...resource2, - description: resourceTemplate.description, - mimeType: resource2.mimeType ?? resourceTemplate.mimeType, - name: resourceTemplate.name, - uri: resource2.uri ?? uri - })) - }; - } - throw new McpError( - 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 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 - }); - }); - } - setupResourceTemplateHandlers(resourceTemplates) { - this.#server.setRequestHandler(ListResourceTemplatesRequestSchema, async () => { - return { - resourceTemplates: resourceTemplates.map((resourceTemplate) => ({ - description: resourceTemplate.description, - mimeType: resourceTemplate.mimeType, - name: resourceTemplate.name, - uriTemplate: resourceTemplate.uriTemplate - })) - }; - }); - } - setupRootsHandlers() { - if (this.#rootsConfig?.enabled === false) { - this.#logger.debug("[FastMCP debug] roots capability explicitly disabled via config"); - return; - } - if (typeof this.#server.listRoots === "function") { - this.#server.setNotificationHandler(RootsListChangedNotificationSchema, () => { - this.#server.listRoots().then((roots) => { - this.#roots = roots.roots; - this.emit("rootsChanged", { - roots: roots.roots - }); - }).catch((error) => { - if (error instanceof McpError && error.code === ErrorCode.MethodNotFound) { - 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(ListToolsRequestSchema, async () => { - return { - tools: await Promise.all( - tools.map(async (tool) => { - return { - annotations: tool.annotations, - description: tool.description, - inputSchema: tool.parameters ? await toJsonSchema(tool.parameters) : { - additionalProperties: false, - properties: {}, - type: "object" - }, - // More complete schema for Cursor compatibility - name: tool.name - }; - }) - ) - }; - }); - this.#server.setRequestHandler(CallToolRequestSchema, async (request) => { - const tool = tools.find((tool2) => tool2.name === request.params.name); - if (!tool) { - throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${request.params.name}`); - } - let args = void 0; - if (tool.parameters) { - const parsed = await tool.parameters["~standard"].validate(request.params.arguments); - if (parsed.issues) { - 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; - 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 request.params?._meta?.requestId === "string" ? request.params._meta.requestId : void 0, - requestMetadata: request.params._meta, - session: this.#auth, - sessionId: this.#sessionId, - streamContent - }); - const maybeStringResult = await (tool.timeoutMs ? Promise.race([ - executeToolPromise, - new Promise((_, reject) => { - const timeoutId = setTimeout(() => { - reject( - new McpError( - ErrorCode.InternalError, - `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 delay(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 (isMcpErrorLike(error)) { - throw 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; -} -var FastMCPEventEmitterBase = EventEmitter; -var FastMCPEventEmitter = class extends FastMCPEventEmitterBase { -}; -var FastMCP = class extends FastMCPEventEmitter { - constructor(options) { - super(); - this.options = options; - this.#options = options; - this.#authenticate = options.authenticate; - this.#logger = options.logger || console; - } - get sessions() { - return this.#sessions; - } - #authenticate; - #httpStreamServer = null; - #logger; - #options; - #prompts = []; - #resources = []; - #resourcesTemplates = []; - #sessions = []; - #tools = []; - /** - * Adds a prompt to the server. - */ - addPrompt(prompt) { - this.#prompts.push(prompt); - } - /** - * Adds a resource to the server. - */ - addResource(resource) { - this.#resources.push(resource); - } - /** - * Adds a resource template to the server. - */ - addResourceTemplate(resource) { - this.#resourcesTemplates.push(resource); - } - /** - * Adds a tool to the server. - */ - addTool(tool) { - this.#tools.push(tool); - } - /** - * 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 - */ - 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 = parseURITemplate(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 }); - } - /** - * Starts the server. - */ - async start(options) { - const config = this.#parseRuntimeConfig(options); - if (config.transportType === "stdio") { - const transport = new 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 - }); - } 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 startHTTPServer({ - ...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, - // 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); - }, - port: httpConfig.port, - stateless: true, - streamEndpoint: httpConfig.endpoint - }); - } else { - this.#httpStreamServer = await startHTTPServer({ - ...this.#authenticate ? { authenticate: this.#authenticate } : {}, - createServer: async (request) => { - let auth; - if (this.#authenticate) { - auth = await this.#authenticate(request); - } - 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, - onClose: async (session) => { - const sessionIndex = this.#sessions.indexOf(session); - if (sessionIndex !== -1) this.#sessions.splice(sessionIndex, 1); - this.emit("disconnect", { - session - }); - }, - onConnect: async (session) => { - this.#sessions.push(session); - this.#logger.info(`[FastMCP info] HTTP Stream session established`); - this.emit("connect", { - session - }); - }, - onUnhandledRequest: async (req, res) => { - await this.#handleUnhandledRequest(req, res, false, httpConfig.host); - }, - port: httpConfig.port, - stateless: httpConfig.stateless, - streamEndpoint: httpConfig.endpoint - }); - this.#logger.info( - `[FastMCP info] server is running on HTTP Stream at http://${httpConfig.host}:${httpConfig.port}${httpConfig.endpoint}` - ); - } - } else { - throw new Error("Invalid transport type"); - } - } - /** - * Stops the server. - */ - async stop() { - if (this.#httpStreamServer) { - await this.#httpStreamServer.close(); - } - } - /** - * Creates a new FastMCPSession instance with the current configuration. - * Used both for regular sessions and stateless requests. - */ - #createSession(auth, sessionId) { - if (auth && typeof auth === "object" && "authenticated" in auth && !auth.authenticated) { - const errorMessage = "error" in auth && typeof auth.error === "string" ? auth.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, and OAuth endpoints - */ - #handleUnhandledRequest = async (req, res, isStateless = false, host) => { - const healthConfig = this.#options.health ?? {}; - const enabled = healthConfig.enabled === void 0 ? 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 ?? "\u2713 Ok"); - return; - } - if (req.method === "GET" && url.pathname === "/ready") { - if (isStateless) { - 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); - } - } - 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; - } - if (url.pathname === "/.well-known/oauth-protected-resource" && oauthConfig.protectedResource) { - const metadata = convertObjectToSnakeCase(oauthConfig.protectedResource); - res.writeHead(200, { - "Content-Type": "application/json" - }).end(JSON.stringify(metadata)); - return; - } - } - res.writeHead(404).end(); - }; - #parseRuntimeConfig(overrides) { - const args = process.argv.slice(2); - const getArg = (name) => { - const index = args.findIndex((arg) => arg === `--${name}`); - return index !== -1 && index + 1 < args.length ? args[index + 1] : void 0; - }; - 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; - 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; - return { - httpStream: { - enableJsonResponse, - endpoint, - host, - port, - stateless - }, - transportType: "httpStream" - }; - } - return { transportType: "stdio" }; - } - #removeSession(session) { - const sessionIndex = this.#sessions.indexOf(session); - if (sessionIndex !== -1) { - this.#sessions.splice(sessionIndex, 1); - this.emit("disconnect", { - session - }); - } - } -}; -export { - CustomMcpError, - ErrorCode2 as ErrorCode, - FastMCP, - FastMCPSession, - McpError2 as McpError, - UnexpectedStateError, - UserError, - audioContent, - imageContent, - isMcpErrorLike -}; -//# sourceMappingURL=FastMCP.js.map \ No newline at end of file diff --git a/dist/FastMCP.js.map b/dist/FastMCP.js.map deleted file mode 100644 index 645d742..0000000 --- a/dist/FastMCP.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"sources":["../src/FastMCP.ts"],"sourcesContent":["import { Server } from \"@modelcontextprotocol/sdk/server/index.js\"\nimport { StdioServerTransport } from \"@modelcontextprotocol/sdk/server/stdio.js\"\nimport { EventStore } from \"@modelcontextprotocol/sdk/server/streamableHttp.js\"\nimport { RequestOptions } from \"@modelcontextprotocol/sdk/shared/protocol.js\"\nimport { Transport } from \"@modelcontextprotocol/sdk/shared/transport.js\"\nimport {\n CallToolRequestSchema,\n ClientCapabilities,\n CompleteRequestSchema,\n CreateMessageRequestSchema,\n ErrorCode,\n GetPromptRequestSchema,\n GetPromptResult,\n ListPromptsRequestSchema,\n ListResourcesRequestSchema,\n ListResourcesResult,\n ListResourceTemplatesRequestSchema,\n ListResourceTemplatesResult,\n ListToolsRequestSchema,\n McpError,\n ReadResourceRequestSchema,\n RequestMeta,\n ResourceLink,\n Root,\n RootsListChangedNotificationSchema,\n ServerCapabilities,\n SetLevelRequestSchema,\n} from \"@modelcontextprotocol/sdk/types.js\"\nimport { StandardSchemaV1 } from \"@standard-schema/spec\"\nimport { EventEmitter } from \"events\"\nimport { readFile } from \"fs/promises\"\nimport Fuse from \"fuse.js\"\nimport http from \"http\"\nimport { startHTTPServer } from \"mcp-proxy\"\nimport { StrictEventEmitter } from \"strict-event-emitter-types\"\nimport { setTimeout as delay } from \"timers/promises\"\nimport { fetch } from \"undici\"\nimport parseURITemplate from \"uri-templates\"\nimport { toJsonSchema } from \"xsschema\"\nimport { z } from \"zod\"\n\nexport interface Logger {\n debug(...args: unknown[]): void\n error(...args: unknown[]): void\n info(...args: unknown[]): void\n log(...args: unknown[]): void\n warn(...args: unknown[]): void\n}\n\nexport type SSEServer = {\n close: () => Promise\n}\n\ntype FastMCPEvents = {\n connect: (event: { session: FastMCPSession }) => void\n disconnect: (event: { session: FastMCPSession }) => void\n}\n\ntype FastMCPSessionEvents = {\n error: (event: { error: Error }) => void\n ready: () => void\n rootsChanged: (event: { roots: Root[] }) => void\n}\n\nexport const imageContent = async (\n input: { buffer: Buffer } | { path: string } | { url: string },\n): Promise => {\n let rawData: Buffer\n\n try {\n if (\"url\" in input) {\n try {\n const response = await fetch(input.url)\n\n if (!response.ok) {\n throw new Error(`Server responded with status: ${response.status} - ${response.statusText}`)\n }\n\n rawData = Buffer.from(await response.arrayBuffer())\n } catch (error) {\n throw new Error(\n `Failed to fetch image from URL (${input.url}): ${error instanceof Error ? error.message : String(error)}`,\n )\n }\n } else if (\"path\" in input) {\n try {\n rawData = await readFile(input.path)\n } catch (error) {\n throw new Error(\n `Failed to read image from path (${input.path}): ${error instanceof Error ? error.message : String(error)}`,\n )\n }\n } else if (\"buffer\" in input) {\n rawData = input.buffer\n } else {\n throw new Error(\"Invalid input: Provide a valid 'url', 'path', or 'buffer'\")\n }\n\n const { fileTypeFromBuffer } = await import(\"file-type\")\n const mimeType = await fileTypeFromBuffer(rawData)\n\n if (!mimeType || !mimeType.mime.startsWith(\"image/\")) {\n console.warn(`Warning: Content may not be a valid image. Detected MIME: ${mimeType?.mime || \"unknown\"}`)\n }\n\n const base64Data = rawData.toString(\"base64\")\n\n return {\n data: base64Data,\n mimeType: mimeType?.mime ?? \"image/png\",\n type: \"image\",\n } as const\n } catch (error) {\n if (error instanceof Error) {\n throw error\n } else {\n throw new Error(`Unexpected error processing image: ${String(error)}`)\n }\n }\n}\n\nexport const audioContent = async (\n input: { buffer: Buffer } | { path: string } | { url: string },\n): Promise => {\n let rawData: Buffer\n\n try {\n if (\"url\" in input) {\n try {\n const response = await fetch(input.url)\n\n if (!response.ok) {\n throw new Error(`Server responded with status: ${response.status} - ${response.statusText}`)\n }\n\n rawData = Buffer.from(await response.arrayBuffer())\n } catch (error) {\n throw new Error(\n `Failed to fetch audio from URL (${input.url}): ${error instanceof Error ? error.message : String(error)}`,\n )\n }\n } else if (\"path\" in input) {\n try {\n rawData = await readFile(input.path)\n } catch (error) {\n throw new Error(\n `Failed to read audio from path (${input.path}): ${error instanceof Error ? error.message : String(error)}`,\n )\n }\n } else if (\"buffer\" in input) {\n rawData = input.buffer\n } else {\n throw new Error(\"Invalid input: Provide a valid 'url', 'path', or 'buffer'\")\n }\n\n const { fileTypeFromBuffer } = await import(\"file-type\")\n const mimeType = await fileTypeFromBuffer(rawData)\n\n if (!mimeType || !mimeType.mime.startsWith(\"audio/\")) {\n console.warn(`Warning: Content may not be a valid audio file. Detected MIME: ${mimeType?.mime || \"unknown\"}`)\n }\n\n const base64Data = rawData.toString(\"base64\")\n\n return {\n data: base64Data,\n mimeType: mimeType?.mime ?? \"audio/mpeg\",\n type: \"audio\",\n } as const\n } catch (error) {\n if (error instanceof Error) {\n throw error\n } else {\n throw new Error(`Unexpected error processing audio: ${String(error)}`)\n }\n }\n}\n\ntype Context = {\n client: {\n version: ReturnType\n }\n log: {\n debug: (message: string, data?: SerializableValue) => void\n error: (message: string, data?: SerializableValue) => void\n info: (message: string, data?: SerializableValue) => void\n warn: (message: string, data?: SerializableValue) => void\n }\n reportProgress: (progress: Progress) => Promise\n /**\n * Request ID from the current MCP request.\n * Available for all transports when the client provides it.\n */\n requestId?: string\n requestMetadata?: RequestMeta\n session: T | undefined\n /**\n * Session ID from the Mcp-Session-Id header.\n * Only available for HTTP-based transports (SSE, HTTP Stream).\n * Can be used to track per-session state, implement session-specific\n * counters, or maintain user-specific data across multiple requests.\n */\n sessionId?: string\n streamContent: (content: Content | Content[]) => Promise\n}\n\ntype Extra = unknown\n\ntype Extras = Record\n\ntype Literal = boolean | null | number | string | undefined\n\ntype Progress = {\n /**\n * The progress thus far. This should increase every time progress is made, even if the total is unknown.\n */\n progress: number\n /**\n * Total number of items to process (or total progress required), if known.\n */\n total?: number\n}\n\ntype SerializableValue = { [key: string]: SerializableValue } | Literal | SerializableValue[]\n\ntype TextContent = {\n text: string\n type: \"text\"\n}\n\ntype ToolParameters = StandardSchemaV1\n\nabstract class FastMCPError extends Error {\n public constructor(message?: string) {\n super(message)\n this.name = new.target.name\n }\n}\n\n/**\n * Custom MCP error with a marker property to enable robust error detection\n * across module boundaries.\n *\n * This class extends McpError and adds a `__isMcpError` marker property.\n * This allows error detection to work even when instanceof fails due to\n * module instances being loaded multiple times (e.g., with tsx, different\n * bundlers, or ESM/CommonJS mixing).\n *\n * Use this instead of McpError when you need the error to be re-thrown\n * by FastMCP's error handling rather than wrapped in a result.\n */\nexport class CustomMcpError extends McpError {\n readonly __isMcpError = true\n\n constructor(code: number, message: string, data?: unknown) {\n super(code, message, data)\n }\n}\n\nexport class UnexpectedStateError extends FastMCPError {\n public extras?: Extras\n\n public constructor(message: string, extras?: Extras) {\n super(message)\n this.name = new.target.name\n this.extras = extras\n }\n}\n\n/**\n * An error that is meant to be surfaced to the user.\n */\nexport class UserError extends UnexpectedStateError {}\n\n/**\n * Type guard to check if an error should be re-thrown as an MCP error.\n * Works across module boundaries by checking both instanceof and marker property.\n *\n * @param error - The error to check\n * @returns true if error is an McpError or has the __isMcpError marker\n */\nexport function isMcpErrorLike(error: unknown): error is McpError {\n return (\n error instanceof McpError ||\n (typeof error === \"object\" &&\n error !== null &&\n \"__isMcpError\" in error &&\n (error as { __isMcpError?: boolean }).__isMcpError === true)\n )\n}\n\nconst TextContentZodSchema = z\n .object({\n /**\n * The text content of the message.\n */\n text: z.string(),\n type: z.literal(\"text\"),\n })\n .strict() satisfies z.ZodType\n\ntype ImageContent = {\n data: string\n mimeType: string\n type: \"image\"\n}\n\nconst ImageContentZodSchema = z\n .object({\n /**\n * The base64-encoded image data.\n */\n data: z.string().base64(),\n /**\n * The MIME type of the image. Different providers may support different image types.\n */\n mimeType: z.string(),\n type: z.literal(\"image\"),\n })\n .strict() satisfies z.ZodType\n\ntype AudioContent = {\n data: string\n mimeType: string\n type: \"audio\"\n}\n\nconst AudioContentZodSchema = z\n .object({\n /**\n * The base64-encoded audio data.\n */\n data: z.string().base64(),\n mimeType: z.string(),\n type: z.literal(\"audio\"),\n })\n .strict() satisfies z.ZodType\n\ntype ResourceContent = {\n resource: {\n blob?: string\n mimeType?: string\n text?: string\n uri: string\n }\n type: \"resource\"\n}\n\nconst ResourceContentZodSchema = z\n .object({\n resource: z.object({\n blob: z.string().optional(),\n mimeType: z.string().optional(),\n text: z.string().optional(),\n uri: z.string(),\n }),\n type: z.literal(\"resource\"),\n })\n .strict() satisfies z.ZodType\n\nconst ResourceLinkZodSchema = z.object({\n description: z.string().optional(),\n mimeType: z.string().optional(),\n name: z.string(),\n title: z.string().optional(),\n type: z.literal(\"resource_link\"),\n uri: z.string(),\n}) satisfies z.ZodType\n\ntype Content = AudioContent | ImageContent | ResourceContent | ResourceLink | TextContent\n\nconst ContentZodSchema = z.discriminatedUnion(\"type\", [\n TextContentZodSchema,\n ImageContentZodSchema,\n AudioContentZodSchema,\n ResourceContentZodSchema,\n ResourceLinkZodSchema,\n]) satisfies z.ZodType\n\ntype ContentResult = {\n _meta?: Record\n content: Content[]\n isError?: boolean\n}\n\nconst ContentResultZodSchema = z\n .object({\n _meta: z.record(z.unknown()).optional(),\n content: ContentZodSchema.array(),\n isError: z.boolean().optional(),\n })\n .strict() satisfies z.ZodType\n\ntype Completion = {\n hasMore?: boolean\n total?: number\n values: string[]\n}\n\n/**\n * https://github.com/modelcontextprotocol/typescript-sdk/blob/3164da64d085ec4e022ae881329eee7b72f208d4/src/types.ts#L983-L1003\n */\nconst CompletionZodSchema = z.object({\n /**\n * Indicates whether there are additional completion options beyond those provided in the current response, even if the exact total is unknown.\n */\n hasMore: z.optional(z.boolean()),\n /**\n * The total number of completion options available. This can exceed the number of values actually sent in the response.\n */\n total: z.optional(z.number().int()),\n /**\n * An array of completion values. Must not exceed 100 items.\n */\n values: z.array(z.string()).max(100),\n}) satisfies z.ZodType\n\ntype ArgumentValueCompleter = (\n value: string,\n auth?: T,\n) => Promise\n\ntype InputPrompt<\n T extends FastMCPSessionAuth = FastMCPSessionAuth,\n Arguments extends InputPromptArgument[] = InputPromptArgument[],\n Args = PromptArgumentsToObject,\n> = {\n arguments?: InputPromptArgument[]\n description?: string\n load: (args: Args, auth?: T) => Promise\n name: string\n}\n\ntype InputPromptArgument = Readonly<{\n complete?: ArgumentValueCompleter\n description?: string\n enum?: string[]\n name: string\n required?: boolean\n}>\n\ntype InputResourceTemplate<\n T extends FastMCPSessionAuth,\n Arguments extends InputResourceTemplateArgument[] = InputResourceTemplateArgument[],\n> = {\n arguments: Arguments\n description?: string\n load: (args: ResourceTemplateArgumentsToObject, auth?: T) => Promise\n mimeType?: string\n name: string\n uriTemplate: string\n}\n\ntype InputResourceTemplateArgument = Readonly<{\n complete?: ArgumentValueCompleter\n description?: string\n name: string\n required?: boolean\n}>\n\ntype LoggingLevel = \"alert\" | \"critical\" | \"debug\" | \"emergency\" | \"error\" | \"info\" | \"notice\" | \"warning\"\n\ntype Prompt<\n T extends FastMCPSessionAuth = FastMCPSessionAuth,\n Arguments extends PromptArgument[] = PromptArgument[],\n Args = PromptArgumentsToObject,\n> = {\n arguments?: PromptArgument[]\n complete?: (name: string, value: string, auth?: T) => Promise\n description?: string\n load: (args: Args, auth?: T) => Promise\n name: string\n}\n\ntype PromptArgument = Readonly<{\n complete?: ArgumentValueCompleter\n description?: string\n enum?: string[]\n name: string\n required?: boolean\n}>\n\ntype PromptArgumentsToObject = {\n [K in T[number][\"name\"]]: Extract[\"required\"] extends true ? string : string | undefined\n}\n\ntype PromptResult = Pick | string\n\ntype Resource = {\n complete?: (name: string, value: string, auth?: T) => Promise\n description?: string\n load: (auth?: T) => Promise\n mimeType?: string\n name: string\n uri: string\n}\n\ntype ResourceResult =\n | {\n blob: string\n mimeType?: string\n uri?: string\n }\n | {\n mimeType?: string\n text: string\n uri?: string\n }\n\ntype ResourceTemplate<\n T extends FastMCPSessionAuth,\n Arguments extends ResourceTemplateArgument[] = ResourceTemplateArgument[],\n> = {\n arguments: Arguments\n complete?: (name: string, value: string, auth?: T) => Promise\n description?: string\n load: (args: ResourceTemplateArgumentsToObject, auth?: T) => Promise\n mimeType?: string\n name: string\n uriTemplate: string\n}\n\ntype ResourceTemplateArgument = Readonly<{\n complete?: ArgumentValueCompleter\n description?: string\n name: string\n required?: boolean\n}>\n\ntype ResourceTemplateArgumentsToObject = {\n [K in T[number][\"name\"]]: string\n}\n\ntype SamplingResponse = {\n content: AudioContent | ImageContent | TextContent\n model: string\n role: \"assistant\" | \"user\"\n stopReason?: \"endTurn\" | \"maxTokens\" | \"stopSequence\" | string\n}\n\ntype ServerOptions = {\n authenticate?: Authenticate\n /**\n * Configuration for the health-check endpoint that can be exposed when the\n * server is running using the HTTP Stream transport. When enabled, the\n * server will respond to an HTTP GET request with the configured path (by\n * default \"/health\") rendering a plain-text response (by default \"ok\") and\n * the configured status code (by default 200).\n *\n * The endpoint is only added when the server is started with\n * `transportType: \"httpStream\"` – it is ignored for the stdio transport.\n */\n health?: {\n /**\n * When set to `false` the health-check endpoint is disabled.\n * @default true\n */\n enabled?: boolean\n\n /**\n * Plain-text body returned by the endpoint.\n * @default \"ok\"\n */\n message?: string\n\n /**\n * HTTP path that should be handled.\n * @default \"/health\"\n */\n path?: string\n\n /**\n * HTTP response status that will be returned.\n * @default 200\n */\n status?: number\n }\n instructions?: string\n /**\n * Custom logger instance. If not provided, defaults to console.\n * Use this to integrate with your own logging system.\n */\n logger?: Logger\n name: string\n\n /**\n * Configuration for OAuth well-known discovery endpoints that can be exposed\n * when the server is running using HTTP-based transports (SSE or HTTP Stream).\n * When enabled, the server will respond to requests for OAuth discovery endpoints\n * with the configured metadata.\n *\n * The endpoints are only added when the server is started with\n * `transportType: \"httpStream\"` – they are ignored for the stdio transport.\n * Both SSE and HTTP Stream transports support OAuth endpoints.\n */\n oauth?: {\n /**\n * OAuth Authorization Server metadata for /.well-known/oauth-authorization-server\n *\n * This endpoint follows RFC 8414 (OAuth 2.0 Authorization Server Metadata)\n * and provides metadata about the OAuth 2.0 authorization server.\n *\n * Required by MCP Specification 2025-03-26\n */\n authorizationServer?: {\n authorizationEndpoint: string\n codeChallengeMethodsSupported?: string[]\n // DPoP support\n dpopSigningAlgValuesSupported?: string[]\n grantTypesSupported?: string[]\n\n introspectionEndpoint?: string\n // Required\n issuer: string\n // Common optional\n jwksUri?: string\n opPolicyUri?: string\n opTosUri?: string\n registrationEndpoint?: string\n responseModesSupported?: string[]\n responseTypesSupported: string[]\n revocationEndpoint?: string\n scopesSupported?: string[]\n serviceDocumentation?: string\n tokenEndpoint: string\n tokenEndpointAuthMethodsSupported?: string[]\n tokenEndpointAuthSigningAlgValuesSupported?: string[]\n\n uiLocalesSupported?: string[]\n }\n\n /**\n * Whether OAuth discovery endpoints should be enabled.\n */\n enabled: boolean\n\n /**\n * OAuth Protected Resource metadata for `/.well-known/oauth-protected-resource`\n *\n * This endpoint follows {@link https://www.rfc-editor.org/rfc/rfc9728.html | RFC 9728}\n * and provides metadata describing how an OAuth 2.0 protected resource (in this case,\n * an MCP server) expects to be accessed.\n *\n * When configured, FastMCP will automatically serve this metadata at the\n * `/.well-known/oauth-protected-resource` endpoint. The `authorizationServers` and `resource`\n * fields are required. All others are optional and will be omitted from the published\n * metadata if not specified.\n *\n * This satisfies the requirements of the MCP Authorization specification's\n * {@link https://modelcontextprotocol.io/specification/2025-06-18/basic/authorization#authorization-server-location | Authorization Server Location section}.\n *\n * Clients consuming this metadata MUST validate that any presented values comply with\n * RFC 9728, including strict validation of the `resource` identifier and intended audience\n * when access tokens are issued and presented (per RFC 8707 §2).\n *\n * @remarks Required by MCP Specification version 2025-06-18\n */\n protectedResource?: {\n /**\n * Allows for additional metadata fields beyond those defined in RFC 9728.\n *\n * @remarks This supports vendor-specific or experimental extensions.\n * @see {@link https://www.rfc-editor.org/rfc/rfc9728.html#section-2.3 | RFC 9728 §2.3}\n */\n [key: string]: unknown\n\n /**\n * Supported values for the `authorization_details` parameter (RFC 9396).\n *\n * @remarks Used when fine-grained access control is in play.\n * @see {@link https://www.rfc-editor.org/rfc/rfc9728.html#section-2-2.23 | RFC 9728 §2.2.23}\n */\n authorizationDetailsTypesSupported?: string[]\n\n /**\n * List of OAuth 2.0 authorization server issuer identifiers.\n *\n * These correspond to ASes that can issue access tokens for this protected resource.\n * MCP clients use these values to locate the relevant `/.well-known/oauth-authorization-server`\n * metadata for initiating the OAuth flow.\n *\n * @remarks Required by the MCP spec. MCP servers MUST provide at least one issuer.\n * Clients are responsible for choosing among them (see RFC 9728 §7.6).\n * @see {@link https://www.rfc-editor.org/rfc/rfc9728.html#section-2-2.3 | RFC 9728 §2.2.3}\n */\n authorizationServers: string[]\n\n /**\n * List of supported methods for presenting OAuth 2.0 bearer tokens.\n *\n * @remarks Valid values are `header`, `body`, and `query`.\n * If omitted, clients MAY assume only `header` is supported, per RFC 6750.\n * This is a client-side interpretation and not a serialization default.\n * @see {@link https://www.rfc-editor.org/rfc/rfc9728.html#section-2-2.9 | RFC 9728 §2.2.9}\n */\n bearerMethodsSupported?: string[]\n\n /**\n * Whether this resource requires all access tokens to be DPoP-bound.\n *\n * @remarks If omitted, clients SHOULD assume this is `false`.\n * @see {@link https://www.rfc-editor.org/rfc/rfc9728.html#section-2-2.27 | RFC 9728 §2.2.27}\n */\n dpopBoundAccessTokensRequired?: boolean\n\n /**\n * Supported algorithms for verifying DPoP proofs (RFC 9449).\n *\n * @see {@link https://www.rfc-editor.org/rfc/rfc9728.html#section-2-2.25 | RFC 9728 §2.2.25}\n */\n dpopSigningAlgValuesSupported?: string[]\n\n /**\n * JWKS URI of this resource. Used to validate access tokens or sign responses.\n *\n * @remarks When present, this MUST be an `https:` URI pointing to a valid JWK Set (RFC 7517).\n * @see {@link https://www.rfc-editor.org/rfc/rfc9728.html#section-2-2.5 | RFC 9728 §2.2.5}\n */\n jwksUri?: string\n\n /**\n * Canonical OAuth resource identifier for this protected resource (the MCP server).\n *\n * @remarks Typically the base URL of the MCP server. Clients MUST use this as the\n * `resource` parameter in authorization and token requests (per RFC 8707).\n * @see {@link https://www.rfc-editor.org/rfc/rfc9728.html#section-2-2.1 | RFC 9728 §2.2.1}\n */\n resource: string\n\n /**\n * URL to developer-accessible documentation for this resource.\n *\n * @remarks This field MAY be localized.\n * @see {@link https://www.rfc-editor.org/rfc/rfc9728.html#section-2-2.15 | RFC 9728 §2.2.15}\n */\n resourceDocumentation?: string\n\n /**\n * Human-readable name for display purposes (e.g., in UIs).\n *\n * @remarks This field MAY be localized using language tags (`resource_name#en`, etc.).\n * @see {@link https://www.rfc-editor.org/rfc/rfc9728.html#section-2-2.13 | RFC 9728 §2.2.13}\n */\n resourceName?: string\n\n /**\n * URL to a human-readable policy page describing acceptable use.\n *\n * @remarks This field MAY be localized.\n * @see {@link https://www.rfc-editor.org/rfc/rfc9728.html#section-2-2.17 | RFC 9728 §2.2.17}\n */\n resourcePolicyUri?: string\n\n /**\n * Supported JWS algorithms for signed responses from this resource (e.g., response signing).\n *\n * @remarks MUST NOT include `none`.\n * @see {@link https://www.rfc-editor.org/rfc/rfc9728.html#section-2-2.11 | RFC 9728 §2.2.11}\n */\n resourceSigningAlgValuesSupported?: string[]\n\n /**\n * URL to the protected resource’s Terms of Service.\n *\n * @remarks This field MAY be localized.\n * @see {@link https://www.rfc-editor.org/rfc/rfc9728.html#section-2-2.19 | RFC 9728 §2.2.19}\n */\n resourceTosUri?: string\n\n /**\n * Supported OAuth scopes for requesting access to this resource.\n *\n * @remarks Useful for discovery, but clients SHOULD still request the minimal scope required.\n * @see {@link https://www.rfc-editor.org/rfc/rfc9728.html#section-2-2.7 | RFC 9728 §2.2.7}\n */\n scopesSupported?: string[]\n\n /**\n * Developer-accessible documentation for how to use the service (not end-user docs).\n *\n * @remarks Semantically equivalent to `resourceDocumentation`, but included under its\n * alternate name for compatibility with tools or schemas expecting either.\n * @see {@link https://www.rfc-editor.org/rfc/rfc9728.html#section-2-2.15 | RFC 9728 §2.2.15}\n */\n serviceDocumentation?: string\n\n /**\n * Whether mutual-TLS-bound access tokens are required.\n *\n * @remarks If omitted, clients SHOULD assume this is `false` (client-side behavior).\n * @see {@link https://www.rfc-editor.org/rfc/rfc9728.html#section-2-2.21 | RFC 9728 §2.2.21}\n */\n tlsClientCertificateBoundAccessTokens?: boolean\n }\n }\n\n ping?: {\n /**\n * Whether ping should be enabled by default.\n * - true for SSE or HTTP Stream\n * - false for stdio\n */\n enabled?: boolean\n /**\n * Interval\n * @default 5000 (5s)\n */\n intervalMs?: number\n /**\n * Logging level for ping-related messages.\n * @default 'debug'\n */\n logLevel?: LoggingLevel\n }\n /**\n * Configuration for roots capability\n */\n roots?: {\n /**\n * Whether roots capability should be enabled\n * Set to false to completely disable roots support\n * @default true\n */\n enabled?: boolean\n }\n /**\n * General utilities\n */\n utils?: {\n formatInvalidParamsErrorMessage?: (issues: readonly StandardSchemaV1.Issue[]) => string\n }\n version: `${number}.${number}.${number}`\n}\n\ntype Tool = {\n annotations?: {\n /**\n * When true, the tool leverages incremental content streaming\n * Return void for tools that handle all their output via streaming\n */\n streamingHint?: boolean\n } & ToolAnnotations\n canAccess?: (auth: T) => boolean\n description?: string\n\n execute: (\n args: StandardSchemaV1.InferOutput,\n context: Context,\n ) => Promise<\n AudioContent | ContentResult | ImageContent | ResourceContent | ResourceLink | string | TextContent | void\n >\n name: string\n parameters?: Params\n timeoutMs?: number\n}\n\n/**\n * Tool annotations as defined in MCP Specification (2025-03-26)\n * These provide hints about a tool's behavior.\n */\ntype ToolAnnotations = {\n /**\n * If true, the tool may perform destructive updates\n * Only meaningful when readOnlyHint is false\n * @default true\n */\n destructiveHint?: boolean\n\n /**\n * If true, calling the tool repeatedly with the same arguments has no additional effect\n * Only meaningful when readOnlyHint is false\n * @default false\n */\n idempotentHint?: boolean\n\n /**\n * If true, the tool may interact with an \"open world\" of external entities\n * @default true\n */\n openWorldHint?: boolean\n\n /**\n * If true, indicates the tool does not modify its environment\n * @default false\n */\n readOnlyHint?: boolean\n\n /**\n * A human-readable title for the tool, useful for UI display\n */\n title?: string\n}\n\nconst FastMCPSessionEventEmitterBase: {\n new (): StrictEventEmitter\n} = EventEmitter\n\ntype Authenticate = (request: http.IncomingMessage) => Promise\n\ntype FastMCPSessionAuth = Record | undefined\n\nclass FastMCPSessionEventEmitter extends FastMCPSessionEventEmitterBase {}\n\nexport class FastMCPSession extends FastMCPSessionEventEmitter {\n public get clientCapabilities(): ClientCapabilities | null {\n return this.#clientCapabilities ?? null\n }\n public get isReady(): boolean {\n return this.#connectionState === \"ready\"\n }\n public get loggingLevel(): LoggingLevel {\n return this.#loggingLevel\n }\n public get roots(): Root[] {\n return this.#roots\n }\n public get server(): Server {\n return this.#server\n }\n public get sessionId(): string | undefined {\n return this.#sessionId\n }\n public set sessionId(value: string | undefined) {\n this.#sessionId = value\n }\n #auth: T | undefined\n #capabilities: ServerCapabilities = {}\n #clientCapabilities?: ClientCapabilities\n #connectionState: \"closed\" | \"connecting\" | \"error\" | \"ready\" = \"connecting\"\n #logger: Logger\n #loggingLevel: LoggingLevel = \"info\"\n #needsEventLoopFlush: boolean = false\n #pingConfig?: ServerOptions[\"ping\"]\n\n #pingInterval: null | ReturnType = null\n\n #prompts: Prompt[] = []\n\n #resources: Resource[] = []\n\n #resourceTemplates: ResourceTemplate[] = []\n\n #roots: Root[] = []\n\n #rootsConfig?: ServerOptions[\"roots\"]\n\n #server: Server\n\n /**\n * Session ID from the Mcp-Session-Id header (HTTP transports only).\n * Used to track per-session state across multiple requests.\n */\n #sessionId?: string\n\n #utils?: ServerOptions[\"utils\"]\n\n constructor({\n auth,\n instructions,\n logger,\n name,\n ping,\n prompts,\n resources,\n resourcesTemplates,\n roots,\n sessionId,\n tools,\n transportType,\n utils,\n version,\n }: {\n auth?: T\n instructions?: string\n logger: Logger\n name: string\n ping?: ServerOptions[\"ping\"]\n prompts: Prompt[]\n resources: Resource[]\n resourcesTemplates: InputResourceTemplate[]\n roots?: ServerOptions[\"roots\"]\n sessionId?: string\n tools: Tool[]\n transportType?: \"httpStream\" | \"stdio\"\n utils?: ServerOptions[\"utils\"]\n version: string\n }) {\n super()\n\n this.#auth = auth\n this.#logger = logger\n this.#pingConfig = ping\n this.#rootsConfig = roots\n this.#sessionId = sessionId\n this.#needsEventLoopFlush = transportType === \"httpStream\"\n\n if (tools.length) {\n this.#capabilities.tools = {}\n }\n\n if (resources.length || resourcesTemplates.length) {\n this.#capabilities.resources = {}\n }\n\n if (prompts.length) {\n for (const prompt of prompts) {\n this.addPrompt(prompt)\n }\n\n this.#capabilities.prompts = {}\n }\n\n this.#capabilities.logging = {}\n\n this.#server = new Server(\n { name: name, version: version },\n { capabilities: this.#capabilities, instructions: instructions },\n )\n\n this.#utils = utils\n\n this.setupErrorHandling()\n this.setupLoggingHandlers()\n this.setupRootsHandlers()\n this.setupCompleteHandlers()\n\n if (tools.length) {\n this.setupToolHandlers(tools)\n }\n\n if (resources.length || resourcesTemplates.length) {\n for (const resource of resources) {\n this.addResource(resource)\n }\n\n this.setupResourceHandlers(resources)\n\n if (resourcesTemplates.length) {\n for (const resourceTemplate of resourcesTemplates) {\n this.addResourceTemplate(resourceTemplate)\n }\n\n this.setupResourceTemplateHandlers(resourcesTemplates)\n }\n }\n\n if (prompts.length) {\n this.setupPromptHandlers(prompts)\n }\n }\n\n public async close() {\n this.#connectionState = \"closed\"\n\n if (this.#pingInterval) {\n clearInterval(this.#pingInterval)\n }\n\n try {\n await this.#server.close()\n } catch (error) {\n this.#logger.error(\"[FastMCP error]\", \"could not close server\", error)\n }\n }\n\n public async connect(transport: Transport) {\n if (this.#server.transport) {\n throw new UnexpectedStateError(\"Server is already connected\")\n }\n\n this.#connectionState = \"connecting\"\n\n try {\n await this.#server.connect(transport)\n\n // Extract session ID from transport if available (HTTP transports only)\n if (\"sessionId\" in transport) {\n const transportWithSessionId = transport as {\n sessionId?: string\n } & Transport\n if (typeof transportWithSessionId.sessionId === \"string\") {\n this.#sessionId = transportWithSessionId.sessionId\n }\n }\n\n let attempt = 0\n const maxAttempts = 10\n const retryDelay = 100\n\n while (attempt++ < maxAttempts) {\n const capabilities = this.#server.getClientCapabilities()\n\n if (capabilities) {\n this.#clientCapabilities = capabilities\n break\n }\n\n await delay(retryDelay)\n }\n\n if (!this.#clientCapabilities) {\n this.#logger.warn(\n `[FastMCP warning] could not infer client capabilities after ${maxAttempts} attempts. Connection may be unstable.`,\n )\n }\n\n if (\n this.#rootsConfig?.enabled !== false &&\n this.#clientCapabilities?.roots?.listChanged &&\n typeof this.#server.listRoots === \"function\"\n ) {\n try {\n const roots = await this.#server.listRoots()\n this.#roots = roots?.roots || []\n } catch (e) {\n if (e instanceof McpError && e.code === ErrorCode.MethodNotFound) {\n this.#logger.debug(\"[FastMCP debug] listRoots method not supported by client\")\n } else {\n this.#logger.error(\n `[FastMCP error] received error listing roots.\\n\\n${e instanceof Error ? e.stack : JSON.stringify(e)}`,\n )\n }\n }\n }\n\n if (this.#clientCapabilities) {\n const pingConfig = this.#getPingConfig(transport)\n\n if (pingConfig.enabled) {\n this.#pingInterval = setInterval(async () => {\n try {\n await this.#server.ping()\n } catch {\n // The reason we are not emitting an error here is because some clients\n // seem to not respond to the ping request, and we don't want to crash the server,\n // e.g., https://github.com/punkpeye/fastmcp/issues/38.\n const logLevel = pingConfig.logLevel\n\n if (logLevel === \"debug\") {\n this.#logger.debug(\"[FastMCP debug] server ping failed\")\n } else if (logLevel === \"warning\") {\n this.#logger.warn(\"[FastMCP warning] server is not responding to ping\")\n } else if (logLevel === \"error\") {\n this.#logger.error(\"[FastMCP error] server is not responding to ping\")\n } else {\n this.#logger.info(\"[FastMCP info] server ping failed\")\n }\n }\n }, pingConfig.intervalMs)\n }\n }\n\n // Mark connection as ready and emit event\n this.#connectionState = \"ready\"\n this.emit(\"ready\")\n } catch (error) {\n this.#connectionState = \"error\"\n const errorEvent = {\n error: error instanceof Error ? error : new Error(String(error)),\n }\n this.emit(\"error\", errorEvent)\n throw error\n }\n }\n\n public async requestSampling(\n message: z.infer[\"params\"],\n options?: RequestOptions,\n ): Promise {\n return this.#server.createMessage(message, options)\n }\n\n public waitForReady(): Promise {\n if (this.isReady) {\n return Promise.resolve()\n }\n\n if (this.#connectionState === \"error\" || this.#connectionState === \"closed\") {\n return Promise.reject(new Error(`Connection is in ${this.#connectionState} state`))\n }\n\n return new Promise((resolve, reject) => {\n const timeout = setTimeout(() => {\n reject(new Error(\"Connection timeout: Session failed to become ready within 5 seconds\"))\n }, 5000)\n\n this.once(\"ready\", () => {\n clearTimeout(timeout)\n resolve()\n })\n\n this.once(\"error\", (event) => {\n clearTimeout(timeout)\n reject(event.error)\n })\n })\n }\n\n #getPingConfig(transport: Transport): {\n enabled: boolean\n intervalMs: number\n logLevel: LoggingLevel\n } {\n const pingConfig = this.#pingConfig || {}\n\n let defaultEnabled = false\n\n if (\"type\" in transport) {\n // Enable by default for SSE and HTTP streaming\n if (transport.type === \"httpStream\") {\n defaultEnabled = true\n }\n }\n\n return {\n enabled: pingConfig.enabled !== undefined ? pingConfig.enabled : defaultEnabled,\n intervalMs: pingConfig.intervalMs || 5000,\n logLevel: pingConfig.logLevel || \"debug\",\n }\n }\n\n private addPrompt(inputPrompt: InputPrompt) {\n const completers: Record> = {}\n const enums: Record = {}\n const fuseInstances: Record> = {}\n\n for (const argument of inputPrompt.arguments ?? []) {\n if (argument.complete) {\n completers[argument.name] = argument.complete\n }\n\n if (argument.enum) {\n enums[argument.name] = argument.enum\n fuseInstances[argument.name] = new Fuse(argument.enum, {\n includeScore: true,\n threshold: 0.3, // More flexible matching!\n })\n }\n }\n\n const prompt = {\n ...inputPrompt,\n complete: async (name: string, value: string, auth?: T) => {\n if (completers[name]) {\n return await completers[name](value, auth)\n }\n\n if (fuseInstances[name]) {\n const result = fuseInstances[name].search(value)\n\n return {\n total: result.length,\n values: result.map((item) => item.item),\n }\n }\n\n return {\n values: [],\n }\n },\n }\n\n this.#prompts.push(prompt)\n }\n\n private addResource(inputResource: Resource) {\n this.#resources.push(inputResource)\n }\n\n private addResourceTemplate(inputResourceTemplate: InputResourceTemplate) {\n const completers: Record> = {}\n\n for (const argument of inputResourceTemplate.arguments ?? []) {\n if (argument.complete) {\n completers[argument.name] = argument.complete\n }\n }\n\n const resourceTemplate = {\n ...inputResourceTemplate,\n complete: async (name: string, value: string, auth?: T) => {\n if (completers[name]) {\n return await completers[name](value, auth)\n }\n\n return {\n values: [],\n }\n },\n }\n\n this.#resourceTemplates.push(resourceTemplate)\n }\n\n private setupCompleteHandlers() {\n this.#server.setRequestHandler(CompleteRequestSchema, async (request) => {\n if (request.params.ref.type === \"ref/prompt\") {\n const prompt = this.#prompts.find((prompt) => prompt.name === request.params.ref.name)\n\n if (!prompt) {\n throw new UnexpectedStateError(\"Unknown prompt\", {\n request,\n })\n }\n\n if (!prompt.complete) {\n throw new UnexpectedStateError(\"Prompt does not support completion\", {\n request,\n })\n }\n\n const completion = CompletionZodSchema.parse(\n await prompt.complete(request.params.argument.name, request.params.argument.value, this.#auth),\n )\n\n return {\n completion,\n }\n }\n\n if (request.params.ref.type === \"ref/resource\") {\n const resource = this.#resourceTemplates.find((resource) => resource.uriTemplate === request.params.ref.uri)\n\n if (!resource) {\n throw new UnexpectedStateError(\"Unknown resource\", {\n request,\n })\n }\n\n if (!(\"uriTemplate\" in resource)) {\n throw new UnexpectedStateError(\"Unexpected resource\")\n }\n\n if (!resource.complete) {\n throw new UnexpectedStateError(\"Resource does not support completion\", {\n request,\n })\n }\n\n const completion = CompletionZodSchema.parse(\n await resource.complete(request.params.argument.name, request.params.argument.value, this.#auth),\n )\n\n return {\n completion,\n }\n }\n\n throw new UnexpectedStateError(\"Unexpected completion request\", {\n request,\n })\n })\n }\n\n private setupErrorHandling() {\n this.#server.onerror = (error) => {\n this.#logger.error(\"[FastMCP error]\", error)\n }\n }\n\n private setupLoggingHandlers() {\n this.#server.setRequestHandler(SetLevelRequestSchema, (request) => {\n this.#loggingLevel = request.params.level\n\n return {}\n })\n }\n\n private setupPromptHandlers(prompts: Prompt[]) {\n this.#server.setRequestHandler(ListPromptsRequestSchema, async () => {\n return {\n prompts: prompts.map((prompt) => {\n return {\n arguments: prompt.arguments,\n complete: prompt.complete,\n description: prompt.description,\n name: prompt.name,\n }\n }),\n }\n })\n\n this.#server.setRequestHandler(GetPromptRequestSchema, async (request) => {\n const prompt = prompts.find((prompt) => prompt.name === request.params.name)\n\n if (!prompt) {\n throw new McpError(ErrorCode.MethodNotFound, `Unknown prompt: ${request.params.name}`)\n }\n\n const args = request.params.arguments\n\n for (const arg of prompt.arguments ?? []) {\n if (arg.required && !(args && arg.name in args)) {\n throw new McpError(\n ErrorCode.InvalidRequest,\n `Prompt '${request.params.name}' requires argument '${arg.name}': ${\n arg.description || \"No description provided\"\n }`,\n )\n }\n }\n\n let result: Awaited[\"load\"]>>\n\n try {\n result = await prompt.load(args as Record, this.#auth)\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error)\n throw new McpError(ErrorCode.InternalError, `Failed to load prompt '${request.params.name}': ${errorMessage}`)\n }\n\n if (typeof result === \"string\") {\n return {\n description: prompt.description,\n messages: [\n {\n content: { text: result, type: \"text\" },\n role: \"user\",\n },\n ],\n }\n } else {\n return {\n description: prompt.description,\n messages: result.messages,\n }\n }\n })\n }\n\n private setupResourceHandlers(resources: Resource[]) {\n this.#server.setRequestHandler(ListResourcesRequestSchema, async () => {\n return {\n resources: resources.map((resource) => ({\n description: resource.description,\n mimeType: resource.mimeType,\n name: resource.name,\n uri: resource.uri,\n })),\n } satisfies ListResourcesResult\n })\n\n this.#server.setRequestHandler(ReadResourceRequestSchema, async (request) => {\n if (\"uri\" in request.params) {\n const resource = resources.find((resource) => \"uri\" in resource && resource.uri === request.params.uri)\n\n if (!resource) {\n for (const resourceTemplate of this.#resourceTemplates) {\n const uriTemplate = parseURITemplate(resourceTemplate.uriTemplate)\n\n const match = uriTemplate.fromUri(request.params.uri)\n\n if (!match) {\n continue\n }\n\n const uri = uriTemplate.fill(match)\n\n const result = await resourceTemplate.load(match, this.#auth)\n\n const resources = Array.isArray(result) ? result : [result]\n return {\n contents: resources.map((resource) => ({\n ...resource,\n description: resourceTemplate.description,\n mimeType: resource.mimeType ?? resourceTemplate.mimeType,\n name: resourceTemplate.name,\n uri: resource.uri ?? uri,\n })),\n }\n }\n\n throw new McpError(\n ErrorCode.MethodNotFound,\n `Resource not found: '${request.params.uri}'. Available resources: ${\n resources.map((r) => r.uri).join(\", \") || \"none\"\n }`,\n )\n }\n\n if (!(\"uri\" in resource)) {\n throw new UnexpectedStateError(\"Resource does not support reading\")\n }\n\n let maybeArrayResult: Awaited[\"load\"]>>\n\n try {\n maybeArrayResult = await resource.load(this.#auth)\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error)\n throw new McpError(\n ErrorCode.InternalError,\n `Failed to load resource '${resource.name}' (${resource.uri}): ${errorMessage}`,\n {\n uri: resource.uri,\n },\n )\n }\n\n const resourceResults = Array.isArray(maybeArrayResult) ? maybeArrayResult : [maybeArrayResult]\n\n return {\n contents: resourceResults.map((result) => ({\n ...result,\n mimeType: result.mimeType ?? resource.mimeType,\n name: resource.name,\n uri: result.uri ?? resource.uri,\n })),\n }\n }\n\n throw new UnexpectedStateError(\"Unknown resource request\", {\n request,\n })\n })\n }\n\n private setupResourceTemplateHandlers(resourceTemplates: ResourceTemplate[]) {\n this.#server.setRequestHandler(ListResourceTemplatesRequestSchema, async () => {\n return {\n resourceTemplates: resourceTemplates.map((resourceTemplate) => ({\n description: resourceTemplate.description,\n mimeType: resourceTemplate.mimeType,\n name: resourceTemplate.name,\n uriTemplate: resourceTemplate.uriTemplate,\n })),\n } satisfies ListResourceTemplatesResult\n })\n }\n\n private setupRootsHandlers() {\n if (this.#rootsConfig?.enabled === false) {\n this.#logger.debug(\"[FastMCP debug] roots capability explicitly disabled via config\")\n return\n }\n\n // Only set up roots notification handling if the server supports it\n if (typeof this.#server.listRoots === \"function\") {\n this.#server.setNotificationHandler(RootsListChangedNotificationSchema, () => {\n this.#server\n .listRoots()\n .then((roots) => {\n this.#roots = roots.roots\n\n this.emit(\"rootsChanged\", {\n roots: roots.roots,\n })\n })\n .catch((error) => {\n if (error instanceof McpError && error.code === ErrorCode.MethodNotFound) {\n this.#logger.debug(\"[FastMCP debug] listRoots method not supported by client\")\n } else {\n this.#logger.error(\n `[FastMCP error] received error listing roots.\\n\\n${\n error instanceof Error ? error.stack : JSON.stringify(error)\n }`,\n )\n }\n })\n })\n } else {\n this.#logger.debug(\"[FastMCP debug] roots capability not available, not setting up notification handler\")\n }\n }\n\n private setupToolHandlers(tools: Tool[]) {\n this.#server.setRequestHandler(ListToolsRequestSchema, async () => {\n return {\n tools: await Promise.all(\n tools.map(async (tool) => {\n return {\n annotations: tool.annotations,\n description: tool.description,\n inputSchema: tool.parameters\n ? await toJsonSchema(tool.parameters)\n : {\n additionalProperties: false,\n properties: {},\n type: \"object\",\n }, // More complete schema for Cursor compatibility\n name: tool.name,\n }\n }),\n ),\n }\n })\n\n this.#server.setRequestHandler(CallToolRequestSchema, async (request) => {\n const tool = tools.find((tool) => tool.name === request.params.name)\n\n if (!tool) {\n throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${request.params.name}`)\n }\n\n let args: unknown = undefined\n\n if (tool.parameters) {\n const parsed = await tool.parameters[\"~standard\"].validate(request.params.arguments)\n\n if (parsed.issues) {\n const friendlyErrors = this.#utils?.formatInvalidParamsErrorMessage\n ? this.#utils.formatInvalidParamsErrorMessage(parsed.issues)\n : parsed.issues\n .map((issue) => {\n const path = issue.path?.join(\".\") || \"root\"\n return `${path}: ${issue.message}`\n })\n .join(\", \")\n\n throw new McpError(\n ErrorCode.InvalidParams,\n `Tool '${request.params.name}' parameter validation failed: ${friendlyErrors}. Please check the parameter types and values according to the tool's schema.`,\n )\n }\n\n args = parsed.value\n }\n\n const progressToken = request.params?._meta?.progressToken\n\n let result: ContentResult\n\n try {\n const reportProgress = async (progress: Progress) => {\n try {\n await this.#server.notification({\n method: \"notifications/progress\",\n params: {\n ...progress,\n progressToken,\n },\n })\n\n if (this.#needsEventLoopFlush) {\n await new Promise((resolve) => setImmediate(resolve))\n }\n } catch (progressError) {\n this.#logger.warn(\n `[FastMCP warning] Failed to report progress for tool '${request.params.name}':`,\n progressError instanceof Error ? progressError.message : String(progressError),\n )\n }\n }\n\n const log = {\n debug: (message: string, context?: SerializableValue) => {\n this.#server.sendLoggingMessage({\n data: {\n context,\n message,\n },\n level: \"debug\",\n })\n },\n error: (message: string, context?: SerializableValue) => {\n this.#server.sendLoggingMessage({\n data: {\n context,\n message,\n },\n level: \"error\",\n })\n },\n info: (message: string, context?: SerializableValue) => {\n this.#server.sendLoggingMessage({\n data: {\n context,\n message,\n },\n level: \"info\",\n })\n },\n warn: (message: string, context?: SerializableValue) => {\n this.#server.sendLoggingMessage({\n data: {\n context,\n message,\n },\n level: \"warning\",\n })\n },\n }\n\n // Create a promise for tool execution\n // Streams partial results while a tool is still executing\n // Enables progressive rendering and real-time feedback\n const streamContent = async (content: Content | Content[]) => {\n const contentArray = Array.isArray(content) ? content : [content]\n\n try {\n await this.#server.notification({\n method: \"notifications/tool/streamContent\",\n params: {\n content: contentArray,\n toolName: request.params.name,\n },\n })\n\n if (this.#needsEventLoopFlush) {\n await new Promise((resolve) => setImmediate(resolve))\n }\n } catch (streamError) {\n this.#logger.warn(\n `[FastMCP warning] Failed to stream content for tool '${request.params.name}':`,\n streamError instanceof Error ? streamError.message : String(streamError),\n )\n }\n }\n const executeToolPromise = tool.execute(args, {\n client: {\n version: this.#server.getClientVersion(),\n },\n log,\n reportProgress,\n requestId: typeof request.params?._meta?.requestId === \"string\" ? request.params._meta.requestId : undefined,\n requestMetadata: request.params._meta,\n session: this.#auth,\n sessionId: this.#sessionId,\n streamContent,\n })\n\n // Handle timeout if specified\n const maybeStringResult = (await (tool.timeoutMs\n ? Promise.race([\n executeToolPromise,\n new Promise((_, reject) => {\n const timeoutId = setTimeout(() => {\n reject(\n new McpError(\n ErrorCode.InternalError,\n `Tool '${request.params.name}' timed out after ${tool.timeoutMs}ms. Consider increasing timeoutMs or optimizing the tool implementation.`,\n ),\n )\n }, tool.timeoutMs)\n\n // If promise resolves first\n executeToolPromise.finally(() => clearTimeout(timeoutId))\n }),\n ])\n : executeToolPromise)) as\n | AudioContent\n | ContentResult\n | ImageContent\n | null\n | ResourceContent\n | ResourceLink\n | string\n | TextContent\n | undefined\n\n // Without this test, we are running into situations where the last progress update is not reported.\n // See the 'reports multiple progress updates without buffering' test in FastMCP.test.ts before refactoring.\n await delay(1)\n\n if (maybeStringResult === undefined || maybeStringResult === null) {\n result = ContentResultZodSchema.parse({\n content: [],\n })\n } else if (typeof maybeStringResult === \"string\") {\n result = ContentResultZodSchema.parse({\n content: [{ text: maybeStringResult, type: \"text\" }],\n })\n } else if (\"type\" in maybeStringResult) {\n result = ContentResultZodSchema.parse({\n content: [maybeStringResult],\n })\n } else {\n result = ContentResultZodSchema.parse(maybeStringResult)\n }\n } catch (error) {\n // Re-throw McpError to let the MCP SDK handle it as a proper JSON-RPC error\n // Use type guard to handle instanceof failures across module boundaries\n if (isMcpErrorLike(error)) {\n throw error\n }\n\n if (error instanceof UserError) {\n return {\n content: [{ text: error.message, type: \"text\" }],\n isError: true,\n ...(error.extras ? { structuredContent: error.extras } : {}),\n }\n }\n\n const errorMessage = error instanceof Error ? error.message : String(error)\n return {\n content: [\n {\n text: `Tool '${request.params.name}' execution failed: ${errorMessage}`,\n type: \"text\",\n },\n ],\n isError: true,\n }\n }\n\n return result\n })\n }\n}\n\n/**\n * Converts camelCase to snake_case for OAuth endpoint responses\n */\nfunction camelToSnakeCase(str: string): string {\n return str.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`)\n}\n\n/**\n * Converts an object with camelCase keys to snake_case keys\n */\nfunction convertObjectToSnakeCase(obj: Record): Record {\n const result: Record = {}\n\n for (const [key, value] of Object.entries(obj)) {\n const snakeKey = camelToSnakeCase(key)\n result[snakeKey] = value\n }\n\n return result\n}\n\nconst FastMCPEventEmitterBase: {\n new (): StrictEventEmitter>\n} = EventEmitter\n\nclass FastMCPEventEmitter extends FastMCPEventEmitterBase {}\n\nexport class FastMCP extends FastMCPEventEmitter {\n public get sessions(): FastMCPSession[] {\n return this.#sessions\n }\n #authenticate: Authenticate | undefined\n #httpStreamServer: null | SSEServer = null\n #logger: Logger\n #options: ServerOptions\n #prompts: InputPrompt[] = []\n #resources: Resource[] = []\n #resourcesTemplates: InputResourceTemplate[] = []\n #sessions: FastMCPSession[] = []\n\n #tools: Tool[] = []\n\n constructor(public options: ServerOptions) {\n super()\n\n this.#options = options\n this.#authenticate = options.authenticate\n this.#logger = options.logger || console\n }\n\n /**\n * Adds a prompt to the server.\n */\n public addPrompt[]>(prompt: InputPrompt) {\n this.#prompts.push(prompt)\n }\n\n /**\n * Adds a resource to the server.\n */\n public addResource(resource: Resource) {\n this.#resources.push(resource)\n }\n\n /**\n * Adds a resource template to the server.\n */\n public addResourceTemplate(\n resource: InputResourceTemplate,\n ) {\n this.#resourcesTemplates.push(resource)\n }\n\n /**\n * Adds a tool to the server.\n */\n public addTool(tool: Tool) {\n this.#tools.push(tool as unknown as Tool)\n }\n\n /**\n * Embeds a resource by URI, making it easy to include resources in tool responses.\n *\n * @param uri - The URI of the resource to embed\n * @returns Promise - The embedded resource content\n */\n public async embedded(uri: string): Promise {\n // First, try to find a direct resource match\n const directResource = this.#resources.find((resource) => resource.uri === uri)\n\n if (directResource) {\n const result = await directResource.load()\n const results = Array.isArray(result) ? result : [result]\n const firstResult = results[0]\n\n const resourceData: ResourceContent[\"resource\"] = {\n mimeType: directResource.mimeType,\n uri,\n }\n\n if (\"text\" in firstResult) {\n resourceData.text = firstResult.text\n }\n\n if (\"blob\" in firstResult) {\n resourceData.blob = firstResult.blob\n }\n\n return resourceData\n }\n\n // Try to match against resource templates\n for (const template of this.#resourcesTemplates) {\n const parsedTemplate = parseURITemplate(template.uriTemplate)\n const params = parsedTemplate.fromUri(uri)\n if (!params) {\n continue\n }\n\n const result = await template.load(params as ResourceTemplateArgumentsToObject)\n\n const resourceData: ResourceContent[\"resource\"] = {\n mimeType: template.mimeType,\n uri,\n }\n\n if (\"text\" in result) {\n resourceData.text = result.text\n }\n\n if (\"blob\" in result) {\n resourceData.blob = result.blob\n }\n\n return resourceData // The resource we're looking for\n }\n\n throw new UnexpectedStateError(`Resource not found: ${uri}`, { uri })\n }\n\n /**\n * Starts the server.\n */\n public async start(\n options?: Partial<{\n httpStream: {\n enableJsonResponse?: boolean\n endpoint?: `/${string}`\n eventStore?: EventStore\n host?: string\n port: number\n stateless?: boolean\n }\n transportType: \"httpStream\" | \"stdio\"\n }>,\n ) {\n const config = this.#parseRuntimeConfig(options)\n\n if (config.transportType === \"stdio\") {\n const transport = new StdioServerTransport()\n\n // For stdio transport, if authenticate function is provided, call it\n // with undefined request (since stdio doesn't have HTTP request context)\n let auth: T | undefined\n\n if (this.#authenticate) {\n try {\n auth = await this.#authenticate(undefined as unknown as http.IncomingMessage)\n } catch (error) {\n this.#logger.error(\n \"[FastMCP error] Authentication failed for stdio transport:\",\n error instanceof Error ? error.message : String(error),\n )\n // Continue without auth if authentication fails\n }\n }\n\n const session = new FastMCPSession({\n auth,\n instructions: this.#options.instructions,\n logger: this.#logger,\n name: this.#options.name,\n ping: this.#options.ping,\n prompts: this.#prompts,\n resources: this.#resources,\n resourcesTemplates: this.#resourcesTemplates,\n roots: this.#options.roots,\n tools: this.#tools,\n transportType: \"stdio\",\n utils: this.#options.utils,\n version: this.#options.version,\n })\n\n await session.connect(transport)\n\n this.#sessions.push(session)\n\n session.once(\"error\", () => {\n this.#removeSession(session)\n })\n\n // Monitor the underlying transport for close events\n if (transport.onclose) {\n const originalOnClose = transport.onclose\n\n transport.onclose = () => {\n this.#removeSession(session)\n\n if (originalOnClose) {\n originalOnClose()\n }\n }\n } else {\n transport.onclose = () => {\n this.#removeSession(session)\n }\n }\n\n this.emit(\"connect\", {\n session: session as FastMCPSession,\n })\n } else if (config.transportType === \"httpStream\") {\n const httpConfig = config.httpStream\n\n if (httpConfig.stateless) {\n // Stateless mode - create new server instance for each request\n this.#logger.info(\n `[FastMCP info] Starting server in stateless mode on HTTP Stream at http://${httpConfig.host}:${httpConfig.port}${httpConfig.endpoint}`,\n )\n\n this.#httpStreamServer = await startHTTPServer>({\n ...(this.#authenticate ? { authenticate: this.#authenticate } : {}),\n createServer: async (request) => {\n let auth: T | undefined\n\n if (this.#authenticate) {\n auth = await this.#authenticate(request)\n\n // In stateless mode, authentication is REQUIRED\n // mcp-proxy will catch this error and return 401\n if (auth === undefined || auth === null) {\n throw new Error(\"Authentication required\")\n }\n }\n\n // Extract session ID from headers\n const sessionId = Array.isArray(request.headers[\"mcp-session-id\"])\n ? request.headers[\"mcp-session-id\"][0]\n : request.headers[\"mcp-session-id\"]\n\n // In stateless mode, create a new session for each request\n // without persisting it in the sessions array\n return this.#createSession(auth, sessionId)\n },\n enableJsonResponse: httpConfig.enableJsonResponse,\n eventStore: httpConfig.eventStore,\n host: httpConfig.host,\n // In stateless mode, we don't track sessions\n onClose: async () => {\n // No session tracking in stateless mode\n },\n onConnect: async () => {\n // No persistent session tracking in stateless mode\n this.#logger.debug(`[FastMCP debug] Stateless HTTP Stream request handled`)\n },\n onUnhandledRequest: async (req, res) => {\n await this.#handleUnhandledRequest(req, res, true, httpConfig.host)\n },\n port: httpConfig.port,\n stateless: true,\n streamEndpoint: httpConfig.endpoint,\n })\n } else {\n // Regular mode with session management\n this.#httpStreamServer = await startHTTPServer>({\n ...(this.#authenticate ? { authenticate: this.#authenticate } : {}),\n createServer: async (request) => {\n let auth: T | undefined\n\n if (this.#authenticate) {\n auth = await this.#authenticate(request)\n }\n\n // Extract session ID from headers\n const sessionId = Array.isArray(request.headers[\"mcp-session-id\"])\n ? request.headers[\"mcp-session-id\"][0]\n : request.headers[\"mcp-session-id\"]\n\n return this.#createSession(auth, sessionId)\n },\n enableJsonResponse: httpConfig.enableJsonResponse,\n eventStore: httpConfig.eventStore,\n host: httpConfig.host,\n onClose: async (session) => {\n const sessionIndex = this.#sessions.indexOf(session)\n\n if (sessionIndex !== -1) this.#sessions.splice(sessionIndex, 1)\n\n this.emit(\"disconnect\", {\n session: session as FastMCPSession,\n })\n },\n onConnect: async (session) => {\n this.#sessions.push(session)\n\n this.#logger.info(`[FastMCP info] HTTP Stream session established`)\n\n this.emit(\"connect\", {\n session: session as FastMCPSession,\n })\n },\n\n onUnhandledRequest: async (req, res) => {\n await this.#handleUnhandledRequest(req, res, false, httpConfig.host)\n },\n port: httpConfig.port,\n stateless: httpConfig.stateless,\n streamEndpoint: httpConfig.endpoint,\n })\n\n this.#logger.info(\n `[FastMCP info] server is running on HTTP Stream at http://${httpConfig.host}:${httpConfig.port}${httpConfig.endpoint}`,\n )\n }\n } else {\n throw new Error(\"Invalid transport type\")\n }\n }\n\n /**\n * Stops the server.\n */\n public async stop() {\n if (this.#httpStreamServer) {\n await this.#httpStreamServer.close()\n }\n }\n\n /**\n * Creates a new FastMCPSession instance with the current configuration.\n * Used both for regular sessions and stateless requests.\n */\n #createSession(auth?: T, sessionId?: string): FastMCPSession {\n // Check if authentication failed\n if (\n auth &&\n typeof auth === \"object\" &&\n \"authenticated\" in auth &&\n !(auth as { authenticated: unknown }).authenticated\n ) {\n const errorMessage =\n \"error\" in auth && typeof (auth as { error: unknown }).error === \"string\"\n ? (auth as { error: string }).error\n : \"Authentication failed\"\n throw new Error(errorMessage)\n }\n\n const allowedTools = auth\n ? this.#tools.filter((tool) => (tool.canAccess ? tool.canAccess(auth) : true))\n : this.#tools\n return new FastMCPSession({\n auth,\n instructions: this.#options.instructions,\n logger: this.#logger,\n name: this.#options.name,\n ping: this.#options.ping,\n prompts: this.#prompts,\n resources: this.#resources,\n resourcesTemplates: this.#resourcesTemplates,\n roots: this.#options.roots,\n sessionId,\n tools: allowedTools,\n transportType: \"httpStream\",\n utils: this.#options.utils,\n version: this.#options.version,\n })\n }\n\n /**\n * Handles unhandled HTTP requests with health, readiness, and OAuth endpoints\n */\n #handleUnhandledRequest = async (\n req: http.IncomingMessage,\n res: http.ServerResponse,\n isStateless = false,\n host: string,\n ) => {\n const healthConfig = this.#options.health ?? {}\n\n const enabled = healthConfig.enabled === undefined ? true : healthConfig.enabled\n\n if (enabled) {\n const path = healthConfig.path ?? \"/health\"\n const url = new URL(req.url || \"\", `http://${host}`)\n\n try {\n if (req.method === \"GET\" && url.pathname === path) {\n res\n .writeHead(healthConfig.status ?? 200, {\n \"Content-Type\": \"text/plain\",\n })\n .end(healthConfig.message ?? \"✓ Ok\")\n\n return\n }\n\n // Enhanced readiness check endpoint\n if (req.method === \"GET\" && url.pathname === \"/ready\") {\n if (isStateless) {\n // In stateless mode, we're always ready if the server is running\n const response = {\n mode: \"stateless\",\n ready: 1,\n status: \"ready\",\n total: 1,\n }\n\n res\n .writeHead(200, {\n \"Content-Type\": \"application/json\",\n })\n .end(JSON.stringify(response))\n } else {\n const readySessions = this.#sessions.filter((s) => s.isReady).length\n const totalSessions = this.#sessions.length\n const allReady = readySessions === totalSessions && totalSessions > 0\n\n const response = {\n ready: readySessions,\n status: allReady ? \"ready\" : totalSessions === 0 ? \"no_sessions\" : \"initializing\",\n total: totalSessions,\n }\n\n res\n .writeHead(allReady ? 200 : 503, {\n \"Content-Type\": \"application/json\",\n })\n .end(JSON.stringify(response))\n }\n\n return\n }\n } catch (error) {\n this.#logger.error(\"[FastMCP error] health endpoint error\", error)\n }\n }\n\n // Handle OAuth well-known endpoints\n const oauthConfig = this.#options.oauth\n if (oauthConfig?.enabled && req.method === \"GET\") {\n const url = new URL(req.url || \"\", `http://${host}`)\n\n if (url.pathname === \"/.well-known/oauth-authorization-server\" && oauthConfig.authorizationServer) {\n const metadata = convertObjectToSnakeCase(oauthConfig.authorizationServer)\n res\n .writeHead(200, {\n \"Content-Type\": \"application/json\",\n })\n .end(JSON.stringify(metadata))\n return\n }\n\n if (url.pathname === \"/.well-known/oauth-protected-resource\" && oauthConfig.protectedResource) {\n const metadata = convertObjectToSnakeCase(oauthConfig.protectedResource)\n res\n .writeHead(200, {\n \"Content-Type\": \"application/json\",\n })\n .end(JSON.stringify(metadata))\n return\n }\n }\n\n // If the request was not handled above, return 404\n res.writeHead(404).end()\n }\n\n #parseRuntimeConfig(\n overrides?: Partial<{\n httpStream: {\n enableJsonResponse?: boolean\n endpoint?: `/${string}`\n host?: string\n port: number\n stateless?: boolean\n }\n transportType: \"httpStream\" | \"stdio\"\n }>,\n ):\n | {\n httpStream: {\n enableJsonResponse?: boolean\n endpoint: `/${string}`\n eventStore?: EventStore\n host: string\n port: number\n stateless?: boolean\n }\n transportType: \"httpStream\"\n }\n | { transportType: \"stdio\" } {\n const args = process.argv.slice(2)\n const getArg = (name: string) => {\n const index = args.findIndex((arg) => arg === `--${name}`)\n\n return index !== -1 && index + 1 < args.length ? args[index + 1] : undefined\n }\n\n const transportArg = getArg(\"transport\")\n const portArg = getArg(\"port\")\n const endpointArg = getArg(\"endpoint\")\n const statelessArg = getArg(\"stateless\")\n const hostArg = getArg(\"host\")\n\n const envTransport = process.env.FASTMCP_TRANSPORT\n const envPort = process.env.FASTMCP_PORT\n const envEndpoint = process.env.FASTMCP_ENDPOINT\n const envStateless = process.env.FASTMCP_STATELESS\n const envHost = process.env.FASTMCP_HOST\n // Overrides > CLI > env > defaults\n const transportType =\n overrides?.transportType ||\n (transportArg === \"http-stream\" ? \"httpStream\" : transportArg) ||\n envTransport ||\n \"stdio\"\n\n if (transportType === \"httpStream\") {\n const port = parseInt(overrides?.httpStream?.port?.toString() || portArg || envPort || \"8080\")\n const host = overrides?.httpStream?.host || hostArg || envHost || \"localhost\"\n const endpoint = overrides?.httpStream?.endpoint || endpointArg || envEndpoint || \"/mcp\"\n const enableJsonResponse = overrides?.httpStream?.enableJsonResponse || false\n const stateless = overrides?.httpStream?.stateless || statelessArg === \"true\" || envStateless === \"true\" || false\n\n return {\n httpStream: {\n enableJsonResponse,\n endpoint: endpoint as `/${string}`,\n host,\n port,\n stateless,\n },\n transportType: \"httpStream\" as const,\n }\n }\n\n return { transportType: \"stdio\" as const }\n }\n\n #removeSession(session: FastMCPSession): void {\n const sessionIndex = this.#sessions.indexOf(session)\n\n if (sessionIndex !== -1) {\n this.#sessions.splice(sessionIndex, 1)\n this.emit(\"disconnect\", {\n session: session as FastMCPSession,\n })\n }\n }\n}\n\nexport { ErrorCode, McpError } from \"@modelcontextprotocol/sdk/types.js\"\n\nexport type {\n AudioContent,\n Content,\n ContentResult,\n Context,\n FastMCPEvents,\n FastMCPSessionEvents,\n ImageContent,\n InputPrompt,\n InputPromptArgument,\n LoggingLevel,\n Progress,\n Prompt,\n PromptArgument,\n RequestMeta,\n Resource,\n ResourceContent,\n ResourceLink,\n ResourceResult,\n ResourceTemplate,\n ResourceTemplateArgument,\n SerializableValue,\n ServerOptions,\n TextContent,\n Tool,\n ToolParameters,\n}\n"],"mappings":";AAAA,SAAS,cAAc;AACvB,SAAS,4BAA4B;AAIrC;AAAA,EACE;AAAA,EAEA;AAAA,EAEA;AAAA,EACA;AAAA,EAEA;AAAA,EACA;AAAA,EAEA;AAAA,EAEA;AAAA,EACA;AAAA,EACA;AAAA,EAIA;AAAA,EAEA;AAAA,OACK;AAEP,SAAS,oBAAoB;AAC7B,SAAS,gBAAgB;AACzB,OAAO,UAAU;AAEjB,SAAS,uBAAuB;AAEhC,SAAS,cAAc,aAAa;AACpC,SAAS,aAAa;AACtB,OAAO,sBAAsB;AAC7B,SAAS,oBAAoB;AAC7B,SAAS,SAAS;AAkxElB,SAAS,aAAAA,YAAW,YAAAC,iBAAgB;AAzvE7B,IAAM,eAAe,OAC1B,UAC0B;AAC1B,MAAI;AAEJ,MAAI;AACF,QAAI,SAAS,OAAO;AAClB,UAAI;AACF,cAAM,WAAW,MAAM,MAAM,MAAM,GAAG;AAEtC,YAAI,CAAC,SAAS,IAAI;AAChB,gBAAM,IAAI,MAAM,iCAAiC,SAAS,MAAM,MAAM,SAAS,UAAU,EAAE;AAAA,QAC7F;AAEA,kBAAU,OAAO,KAAK,MAAM,SAAS,YAAY,CAAC;AAAA,MACpD,SAAS,OAAO;AACd,cAAM,IAAI;AAAA,UACR,mCAAmC,MAAM,GAAG,MAAM,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,CAAC;AAAA,QAC1G;AAAA,MACF;AAAA,IACF,WAAW,UAAU,OAAO;AAC1B,UAAI;AACF,kBAAU,MAAM,SAAS,MAAM,IAAI;AAAA,MACrC,SAAS,OAAO;AACd,cAAM,IAAI;AAAA,UACR,mCAAmC,MAAM,IAAI,MAAM,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,CAAC;AAAA,QAC3G;AAAA,MACF;AAAA,IACF,WAAW,YAAY,OAAO;AAC5B,gBAAU,MAAM;AAAA,IAClB,OAAO;AACL,YAAM,IAAI,MAAM,2DAA2D;AAAA,IAC7E;AAEA,UAAM,EAAE,mBAAmB,IAAI,MAAM,OAAO,WAAW;AACvD,UAAM,WAAW,MAAM,mBAAmB,OAAO;AAEjD,QAAI,CAAC,YAAY,CAAC,SAAS,KAAK,WAAW,QAAQ,GAAG;AACpD,cAAQ,KAAK,6DAA6D,UAAU,QAAQ,SAAS,EAAE;AAAA,IACzG;AAEA,UAAM,aAAa,QAAQ,SAAS,QAAQ;AAE5C,WAAO;AAAA,MACL,MAAM;AAAA,MACN,UAAU,UAAU,QAAQ;AAAA,MAC5B,MAAM;AAAA,IACR;AAAA,EACF,SAAS,OAAO;AACd,QAAI,iBAAiB,OAAO;AAC1B,YAAM;AAAA,IACR,OAAO;AACL,YAAM,IAAI,MAAM,sCAAsC,OAAO,KAAK,CAAC,EAAE;AAAA,IACvE;AAAA,EACF;AACF;AAEO,IAAM,eAAe,OAC1B,UAC0B;AAC1B,MAAI;AAEJ,MAAI;AACF,QAAI,SAAS,OAAO;AAClB,UAAI;AACF,cAAM,WAAW,MAAM,MAAM,MAAM,GAAG;AAEtC,YAAI,CAAC,SAAS,IAAI;AAChB,gBAAM,IAAI,MAAM,iCAAiC,SAAS,MAAM,MAAM,SAAS,UAAU,EAAE;AAAA,QAC7F;AAEA,kBAAU,OAAO,KAAK,MAAM,SAAS,YAAY,CAAC;AAAA,MACpD,SAAS,OAAO;AACd,cAAM,IAAI;AAAA,UACR,mCAAmC,MAAM,GAAG,MAAM,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,CAAC;AAAA,QAC1G;AAAA,MACF;AAAA,IACF,WAAW,UAAU,OAAO;AAC1B,UAAI;AACF,kBAAU,MAAM,SAAS,MAAM,IAAI;AAAA,MACrC,SAAS,OAAO;AACd,cAAM,IAAI;AAAA,UACR,mCAAmC,MAAM,IAAI,MAAM,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,CAAC;AAAA,QAC3G;AAAA,MACF;AAAA,IACF,WAAW,YAAY,OAAO;AAC5B,gBAAU,MAAM;AAAA,IAClB,OAAO;AACL,YAAM,IAAI,MAAM,2DAA2D;AAAA,IAC7E;AAEA,UAAM,EAAE,mBAAmB,IAAI,MAAM,OAAO,WAAW;AACvD,UAAM,WAAW,MAAM,mBAAmB,OAAO;AAEjD,QAAI,CAAC,YAAY,CAAC,SAAS,KAAK,WAAW,QAAQ,GAAG;AACpD,cAAQ,KAAK,kEAAkE,UAAU,QAAQ,SAAS,EAAE;AAAA,IAC9G;AAEA,UAAM,aAAa,QAAQ,SAAS,QAAQ;AAE5C,WAAO;AAAA,MACL,MAAM;AAAA,MACN,UAAU,UAAU,QAAQ;AAAA,MAC5B,MAAM;AAAA,IACR;AAAA,EACF,SAAS,OAAO;AACd,QAAI,iBAAiB,OAAO;AAC1B,YAAM;AAAA,IACR,OAAO;AACL,YAAM,IAAI,MAAM,sCAAsC,OAAO,KAAK,CAAC,EAAE;AAAA,IACvE;AAAA,EACF;AACF;AAwDA,IAAe,eAAf,cAAoC,MAAM;AAAA,EACjC,YAAY,SAAkB;AACnC,UAAM,OAAO;AACb,SAAK,OAAO,WAAW;AAAA,EACzB;AACF;AAcO,IAAM,iBAAN,cAA6B,SAAS;AAAA,EAClC,eAAe;AAAA,EAExB,YAAY,MAAc,SAAiB,MAAgB;AACzD,UAAM,MAAM,SAAS,IAAI;AAAA,EAC3B;AACF;AAEO,IAAM,uBAAN,cAAmC,aAAa;AAAA,EAC9C;AAAA,EAEA,YAAY,SAAiB,QAAiB;AACnD,UAAM,OAAO;AACb,SAAK,OAAO,WAAW;AACvB,SAAK,SAAS;AAAA,EAChB;AACF;AAKO,IAAM,YAAN,cAAwB,qBAAqB;AAAC;AAS9C,SAAS,eAAe,OAAmC;AAChE,SACE,iBAAiB,YAChB,OAAO,UAAU,YAChB,UAAU,QACV,kBAAkB,SACjB,MAAqC,iBAAiB;AAE7D;AAEA,IAAM,uBAAuB,EAC1B,OAAO;AAAA;AAAA;AAAA;AAAA,EAIN,MAAM,EAAE,OAAO;AAAA,EACf,MAAM,EAAE,QAAQ,MAAM;AACxB,CAAC,EACA,OAAO;AAQV,IAAM,wBAAwB,EAC3B,OAAO;AAAA;AAAA;AAAA;AAAA,EAIN,MAAM,EAAE,OAAO,EAAE,OAAO;AAAA;AAAA;AAAA;AAAA,EAIxB,UAAU,EAAE,OAAO;AAAA,EACnB,MAAM,EAAE,QAAQ,OAAO;AACzB,CAAC,EACA,OAAO;AAQV,IAAM,wBAAwB,EAC3B,OAAO;AAAA;AAAA;AAAA;AAAA,EAIN,MAAM,EAAE,OAAO,EAAE,OAAO;AAAA,EACxB,UAAU,EAAE,OAAO;AAAA,EACnB,MAAM,EAAE,QAAQ,OAAO;AACzB,CAAC,EACA,OAAO;AAYV,IAAM,2BAA2B,EAC9B,OAAO;AAAA,EACN,UAAU,EAAE,OAAO;AAAA,IACjB,MAAM,EAAE,OAAO,EAAE,SAAS;AAAA,IAC1B,UAAU,EAAE,OAAO,EAAE,SAAS;AAAA,IAC9B,MAAM,EAAE,OAAO,EAAE,SAAS;AAAA,IAC1B,KAAK,EAAE,OAAO;AAAA,EAChB,CAAC;AAAA,EACD,MAAM,EAAE,QAAQ,UAAU;AAC5B,CAAC,EACA,OAAO;AAEV,IAAM,wBAAwB,EAAE,OAAO;AAAA,EACrC,aAAa,EAAE,OAAO,EAAE,SAAS;AAAA,EACjC,UAAU,EAAE,OAAO,EAAE,SAAS;AAAA,EAC9B,MAAM,EAAE,OAAO;AAAA,EACf,OAAO,EAAE,OAAO,EAAE,SAAS;AAAA,EAC3B,MAAM,EAAE,QAAQ,eAAe;AAAA,EAC/B,KAAK,EAAE,OAAO;AAChB,CAAC;AAID,IAAM,mBAAmB,EAAE,mBAAmB,QAAQ;AAAA,EACpD;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,CAAC;AAQD,IAAM,yBAAyB,EAC5B,OAAO;AAAA,EACN,OAAO,EAAE,OAAO,EAAE,QAAQ,CAAC,EAAE,SAAS;AAAA,EACtC,SAAS,iBAAiB,MAAM;AAAA,EAChC,SAAS,EAAE,QAAQ,EAAE,SAAS;AAChC,CAAC,EACA,OAAO;AAWV,IAAM,sBAAsB,EAAE,OAAO;AAAA;AAAA;AAAA;AAAA,EAInC,SAAS,EAAE,SAAS,EAAE,QAAQ,CAAC;AAAA;AAAA;AAAA;AAAA,EAI/B,OAAO,EAAE,SAAS,EAAE,OAAO,EAAE,IAAI,CAAC;AAAA;AAAA;AAAA;AAAA,EAIlC,QAAQ,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,IAAI,GAAG;AACrC,CAAC;AA8dD,IAAM,iCAEF;AAMJ,IAAM,6BAAN,cAAyC,+BAA+B;AAAC;AAElE,IAAM,iBAAN,cAAgF,2BAA2B;AAAA,EAChH,IAAW,qBAAgD;AACzD,WAAO,KAAK,uBAAuB;AAAA,EACrC;AAAA,EACA,IAAW,UAAmB;AAC5B,WAAO,KAAK,qBAAqB;AAAA,EACnC;AAAA,EACA,IAAW,eAA6B;AACtC,WAAO,KAAK;AAAA,EACd;AAAA,EACA,IAAW,QAAgB;AACzB,WAAO,KAAK;AAAA,EACd;AAAA,EACA,IAAW,SAAiB;AAC1B,WAAO,KAAK;AAAA,EACd;AAAA,EACA,IAAW,YAAgC;AACzC,WAAO,KAAK;AAAA,EACd;AAAA,EACA,IAAW,UAAU,OAA2B;AAC9C,SAAK,aAAa;AAAA,EACpB;AAAA,EACA;AAAA,EACA,gBAAoC,CAAC;AAAA,EACrC;AAAA,EACA,mBAAgE;AAAA,EAChE;AAAA,EACA,gBAA8B;AAAA,EAC9B,uBAAgC;AAAA,EAChC;AAAA,EAEA,gBAAuD;AAAA,EAEvD,WAAwB,CAAC;AAAA,EAEzB,aAA4B,CAAC;AAAA,EAE7B,qBAA4C,CAAC;AAAA,EAE7C,SAAiB,CAAC;AAAA,EAElB;AAAA,EAEA;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA;AAAA,EAEA;AAAA,EAEA,YAAY;AAAA,IACV;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF,GAeG;AACD,UAAM;AAEN,SAAK,QAAQ;AACb,SAAK,UAAU;AACf,SAAK,cAAc;AACnB,SAAK,eAAe;AACpB,SAAK,aAAa;AAClB,SAAK,uBAAuB,kBAAkB;AAE9C,QAAI,MAAM,QAAQ;AAChB,WAAK,cAAc,QAAQ,CAAC;AAAA,IAC9B;AAEA,QAAI,UAAU,UAAU,mBAAmB,QAAQ;AACjD,WAAK,cAAc,YAAY,CAAC;AAAA,IAClC;AAEA,QAAI,QAAQ,QAAQ;AAClB,iBAAW,UAAU,SAAS;AAC5B,aAAK,UAAU,MAAM;AAAA,MACvB;AAEA,WAAK,cAAc,UAAU,CAAC;AAAA,IAChC;AAEA,SAAK,cAAc,UAAU,CAAC;AAE9B,SAAK,UAAU,IAAI;AAAA,MACjB,EAAE,MAAY,QAAiB;AAAA,MAC/B,EAAE,cAAc,KAAK,eAAe,aAA2B;AAAA,IACjE;AAEA,SAAK,SAAS;AAEd,SAAK,mBAAmB;AACxB,SAAK,qBAAqB;AAC1B,SAAK,mBAAmB;AACxB,SAAK,sBAAsB;AAE3B,QAAI,MAAM,QAAQ;AAChB,WAAK,kBAAkB,KAAK;AAAA,IAC9B;AAEA,QAAI,UAAU,UAAU,mBAAmB,QAAQ;AACjD,iBAAW,YAAY,WAAW;AAChC,aAAK,YAAY,QAAQ;AAAA,MAC3B;AAEA,WAAK,sBAAsB,SAAS;AAEpC,UAAI,mBAAmB,QAAQ;AAC7B,mBAAW,oBAAoB,oBAAoB;AACjD,eAAK,oBAAoB,gBAAgB;AAAA,QAC3C;AAEA,aAAK,8BAA8B,kBAAkB;AAAA,MACvD;AAAA,IACF;AAEA,QAAI,QAAQ,QAAQ;AAClB,WAAK,oBAAoB,OAAO;AAAA,IAClC;AAAA,EACF;AAAA,EAEA,MAAa,QAAQ;AACnB,SAAK,mBAAmB;AAExB,QAAI,KAAK,eAAe;AACtB,oBAAc,KAAK,aAAa;AAAA,IAClC;AAEA,QAAI;AACF,YAAM,KAAK,QAAQ,MAAM;AAAA,IAC3B,SAAS,OAAO;AACd,WAAK,QAAQ,MAAM,mBAAmB,0BAA0B,KAAK;AAAA,IACvE;AAAA,EACF;AAAA,EAEA,MAAa,QAAQ,WAAsB;AACzC,QAAI,KAAK,QAAQ,WAAW;AAC1B,YAAM,IAAI,qBAAqB,6BAA6B;AAAA,IAC9D;AAEA,SAAK,mBAAmB;AAExB,QAAI;AACF,YAAM,KAAK,QAAQ,QAAQ,SAAS;AAGpC,UAAI,eAAe,WAAW;AAC5B,cAAM,yBAAyB;AAG/B,YAAI,OAAO,uBAAuB,cAAc,UAAU;AACxD,eAAK,aAAa,uBAAuB;AAAA,QAC3C;AAAA,MACF;AAEA,UAAI,UAAU;AACd,YAAM,cAAc;AACpB,YAAM,aAAa;AAEnB,aAAO,YAAY,aAAa;AAC9B,cAAM,eAAe,KAAK,QAAQ,sBAAsB;AAExD,YAAI,cAAc;AAChB,eAAK,sBAAsB;AAC3B;AAAA,QACF;AAEA,cAAM,MAAM,UAAU;AAAA,MACxB;AAEA,UAAI,CAAC,KAAK,qBAAqB;AAC7B,aAAK,QAAQ;AAAA,UACX,+DAA+D,WAAW;AAAA,QAC5E;AAAA,MACF;AAEA,UACE,KAAK,cAAc,YAAY,SAC/B,KAAK,qBAAqB,OAAO,eACjC,OAAO,KAAK,QAAQ,cAAc,YAClC;AACA,YAAI;AACF,gBAAM,QAAQ,MAAM,KAAK,QAAQ,UAAU;AAC3C,eAAK,SAAS,OAAO,SAAS,CAAC;AAAA,QACjC,SAAS,GAAG;AACV,cAAI,aAAa,YAAY,EAAE,SAAS,UAAU,gBAAgB;AAChE,iBAAK,QAAQ,MAAM,0DAA0D;AAAA,UAC/E,OAAO;AACL,iBAAK,QAAQ;AAAA,cACX;AAAA;AAAA,EAAoD,aAAa,QAAQ,EAAE,QAAQ,KAAK,UAAU,CAAC,CAAC;AAAA,YACtG;AAAA,UACF;AAAA,QACF;AAAA,MACF;AAEA,UAAI,KAAK,qBAAqB;AAC5B,cAAM,aAAa,KAAK,eAAe,SAAS;AAEhD,YAAI,WAAW,SAAS;AACtB,eAAK,gBAAgB,YAAY,YAAY;AAC3C,gBAAI;AACF,oBAAM,KAAK,QAAQ,KAAK;AAAA,YAC1B,QAAQ;AAIN,oBAAM,WAAW,WAAW;AAE5B,kBAAI,aAAa,SAAS;AACxB,qBAAK,QAAQ,MAAM,oCAAoC;AAAA,cACzD,WAAW,aAAa,WAAW;AACjC,qBAAK,QAAQ,KAAK,oDAAoD;AAAA,cACxE,WAAW,aAAa,SAAS;AAC/B,qBAAK,QAAQ,MAAM,kDAAkD;AAAA,cACvE,OAAO;AACL,qBAAK,QAAQ,KAAK,mCAAmC;AAAA,cACvD;AAAA,YACF;AAAA,UACF,GAAG,WAAW,UAAU;AAAA,QAC1B;AAAA,MACF;AAGA,WAAK,mBAAmB;AACxB,WAAK,KAAK,OAAO;AAAA,IACnB,SAAS,OAAO;AACd,WAAK,mBAAmB;AACxB,YAAM,aAAa;AAAA,QACjB,OAAO,iBAAiB,QAAQ,QAAQ,IAAI,MAAM,OAAO,KAAK,CAAC;AAAA,MACjE;AACA,WAAK,KAAK,SAAS,UAAU;AAC7B,YAAM;AAAA,IACR;AAAA,EACF;AAAA,EAEA,MAAa,gBACX,SACA,SAC2B;AAC3B,WAAO,KAAK,QAAQ,cAAc,SAAS,OAAO;AAAA,EACpD;AAAA,EAEO,eAA8B;AACnC,QAAI,KAAK,SAAS;AAChB,aAAO,QAAQ,QAAQ;AAAA,IACzB;AAEA,QAAI,KAAK,qBAAqB,WAAW,KAAK,qBAAqB,UAAU;AAC3E,aAAO,QAAQ,OAAO,IAAI,MAAM,oBAAoB,KAAK,gBAAgB,QAAQ,CAAC;AAAA,IACpF;AAEA,WAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AACtC,YAAM,UAAU,WAAW,MAAM;AAC/B,eAAO,IAAI,MAAM,qEAAqE,CAAC;AAAA,MACzF,GAAG,GAAI;AAEP,WAAK,KAAK,SAAS,MAAM;AACvB,qBAAa,OAAO;AACpB,gBAAQ;AAAA,MACV,CAAC;AAED,WAAK,KAAK,SAAS,CAAC,UAAU;AAC5B,qBAAa,OAAO;AACpB,eAAO,MAAM,KAAK;AAAA,MACpB,CAAC;AAAA,IACH,CAAC;AAAA,EACH;AAAA,EAEA,eAAe,WAIb;AACA,UAAM,aAAa,KAAK,eAAe,CAAC;AAExC,QAAI,iBAAiB;AAErB,QAAI,UAAU,WAAW;AAEvB,UAAI,UAAU,SAAS,cAAc;AACnC,yBAAiB;AAAA,MACnB;AAAA,IACF;AAEA,WAAO;AAAA,MACL,SAAS,WAAW,YAAY,SAAY,WAAW,UAAU;AAAA,MACjE,YAAY,WAAW,cAAc;AAAA,MACrC,UAAU,WAAW,YAAY;AAAA,IACnC;AAAA,EACF;AAAA,EAEQ,UAAU,aAA6B;AAC7C,UAAM,aAAwD,CAAC;AAC/D,UAAM,QAAkC,CAAC;AACzC,UAAM,gBAA8C,CAAC;AAErD,eAAW,YAAY,YAAY,aAAa,CAAC,GAAG;AAClD,UAAI,SAAS,UAAU;AACrB,mBAAW,SAAS,IAAI,IAAI,SAAS;AAAA,MACvC;AAEA,UAAI,SAAS,MAAM;AACjB,cAAM,SAAS,IAAI,IAAI,SAAS;AAChC,sBAAc,SAAS,IAAI,IAAI,IAAI,KAAK,SAAS,MAAM;AAAA,UACrD,cAAc;AAAA,UACd,WAAW;AAAA;AAAA,QACb,CAAC;AAAA,MACH;AAAA,IACF;AAEA,UAAM,SAAS;AAAA,MACb,GAAG;AAAA,MACH,UAAU,OAAO,MAAc,OAAe,SAAa;AACzD,YAAI,WAAW,IAAI,GAAG;AACpB,iBAAO,MAAM,WAAW,IAAI,EAAE,OAAO,IAAI;AAAA,QAC3C;AAEA,YAAI,cAAc,IAAI,GAAG;AACvB,gBAAM,SAAS,cAAc,IAAI,EAAE,OAAO,KAAK;AAE/C,iBAAO;AAAA,YACL,OAAO,OAAO;AAAA,YACd,QAAQ,OAAO,IAAI,CAAC,SAAS,KAAK,IAAI;AAAA,UACxC;AAAA,QACF;AAEA,eAAO;AAAA,UACL,QAAQ,CAAC;AAAA,QACX;AAAA,MACF;AAAA,IACF;AAEA,SAAK,SAAS,KAAK,MAAM;AAAA,EAC3B;AAAA,EAEQ,YAAY,eAA4B;AAC9C,SAAK,WAAW,KAAK,aAAa;AAAA,EACpC;AAAA,EAEQ,oBAAoB,uBAAiD;AAC3E,UAAM,aAAwD,CAAC;AAE/D,eAAW,YAAY,sBAAsB,aAAa,CAAC,GAAG;AAC5D,UAAI,SAAS,UAAU;AACrB,mBAAW,SAAS,IAAI,IAAI,SAAS;AAAA,MACvC;AAAA,IACF;AAEA,UAAM,mBAAmB;AAAA,MACvB,GAAG;AAAA,MACH,UAAU,OAAO,MAAc,OAAe,SAAa;AACzD,YAAI,WAAW,IAAI,GAAG;AACpB,iBAAO,MAAM,WAAW,IAAI,EAAE,OAAO,IAAI;AAAA,QAC3C;AAEA,eAAO;AAAA,UACL,QAAQ,CAAC;AAAA,QACX;AAAA,MACF;AAAA,IACF;AAEA,SAAK,mBAAmB,KAAK,gBAAgB;AAAA,EAC/C;AAAA,EAEQ,wBAAwB;AAC9B,SAAK,QAAQ,kBAAkB,uBAAuB,OAAO,YAAY;AACvE,UAAI,QAAQ,OAAO,IAAI,SAAS,cAAc;AAC5C,cAAM,SAAS,KAAK,SAAS,KAAK,CAACC,YAAWA,QAAO,SAAS,QAAQ,OAAO,IAAI,IAAI;AAErF,YAAI,CAAC,QAAQ;AACX,gBAAM,IAAI,qBAAqB,kBAAkB;AAAA,YAC/C;AAAA,UACF,CAAC;AAAA,QACH;AAEA,YAAI,CAAC,OAAO,UAAU;AACpB,gBAAM,IAAI,qBAAqB,sCAAsC;AAAA,YACnE;AAAA,UACF,CAAC;AAAA,QACH;AAEA,cAAM,aAAa,oBAAoB;AAAA,UACrC,MAAM,OAAO,SAAS,QAAQ,OAAO,SAAS,MAAM,QAAQ,OAAO,SAAS,OAAO,KAAK,KAAK;AAAA,QAC/F;AAEA,eAAO;AAAA,UACL;AAAA,QACF;AAAA,MACF;AAEA,UAAI,QAAQ,OAAO,IAAI,SAAS,gBAAgB;AAC9C,cAAM,WAAW,KAAK,mBAAmB,KAAK,CAACC,cAAaA,UAAS,gBAAgB,QAAQ,OAAO,IAAI,GAAG;AAE3G,YAAI,CAAC,UAAU;AACb,gBAAM,IAAI,qBAAqB,oBAAoB;AAAA,YACjD;AAAA,UACF,CAAC;AAAA,QACH;AAEA,YAAI,EAAE,iBAAiB,WAAW;AAChC,gBAAM,IAAI,qBAAqB,qBAAqB;AAAA,QACtD;AAEA,YAAI,CAAC,SAAS,UAAU;AACtB,gBAAM,IAAI,qBAAqB,wCAAwC;AAAA,YACrE;AAAA,UACF,CAAC;AAAA,QACH;AAEA,cAAM,aAAa,oBAAoB;AAAA,UACrC,MAAM,SAAS,SAAS,QAAQ,OAAO,SAAS,MAAM,QAAQ,OAAO,SAAS,OAAO,KAAK,KAAK;AAAA,QACjG;AAEA,eAAO;AAAA,UACL;AAAA,QACF;AAAA,MACF;AAEA,YAAM,IAAI,qBAAqB,iCAAiC;AAAA,QAC9D;AAAA,MACF,CAAC;AAAA,IACH,CAAC;AAAA,EACH;AAAA,EAEQ,qBAAqB;AAC3B,SAAK,QAAQ,UAAU,CAAC,UAAU;AAChC,WAAK,QAAQ,MAAM,mBAAmB,KAAK;AAAA,IAC7C;AAAA,EACF;AAAA,EAEQ,uBAAuB;AAC7B,SAAK,QAAQ,kBAAkB,uBAAuB,CAAC,YAAY;AACjE,WAAK,gBAAgB,QAAQ,OAAO;AAEpC,aAAO,CAAC;AAAA,IACV,CAAC;AAAA,EACH;AAAA,EAEQ,oBAAoB,SAAsB;AAChD,SAAK,QAAQ,kBAAkB,0BAA0B,YAAY;AACnE,aAAO;AAAA,QACL,SAAS,QAAQ,IAAI,CAAC,WAAW;AAC/B,iBAAO;AAAA,YACL,WAAW,OAAO;AAAA,YAClB,UAAU,OAAO;AAAA,YACjB,aAAa,OAAO;AAAA,YACpB,MAAM,OAAO;AAAA,UACf;AAAA,QACF,CAAC;AAAA,MACH;AAAA,IACF,CAAC;AAED,SAAK,QAAQ,kBAAkB,wBAAwB,OAAO,YAAY;AACxE,YAAM,SAAS,QAAQ,KAAK,CAACD,YAAWA,QAAO,SAAS,QAAQ,OAAO,IAAI;AAE3E,UAAI,CAAC,QAAQ;AACX,cAAM,IAAI,SAAS,UAAU,gBAAgB,mBAAmB,QAAQ,OAAO,IAAI,EAAE;AAAA,MACvF;AAEA,YAAM,OAAO,QAAQ,OAAO;AAE5B,iBAAW,OAAO,OAAO,aAAa,CAAC,GAAG;AACxC,YAAI,IAAI,YAAY,EAAE,QAAQ,IAAI,QAAQ,OAAO;AAC/C,gBAAM,IAAI;AAAA,YACR,UAAU;AAAA,YACV,WAAW,QAAQ,OAAO,IAAI,wBAAwB,IAAI,IAAI,MAC5D,IAAI,eAAe,yBACrB;AAAA,UACF;AAAA,QACF;AAAA,MACF;AAEA,UAAI;AAEJ,UAAI;AACF,iBAAS,MAAM,OAAO,KAAK,MAA4C,KAAK,KAAK;AAAA,MACnF,SAAS,OAAO;AACd,cAAM,eAAe,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AAC1E,cAAM,IAAI,SAAS,UAAU,eAAe,0BAA0B,QAAQ,OAAO,IAAI,MAAM,YAAY,EAAE;AAAA,MAC/G;AAEA,UAAI,OAAO,WAAW,UAAU;AAC9B,eAAO;AAAA,UACL,aAAa,OAAO;AAAA,UACpB,UAAU;AAAA,YACR;AAAA,cACE,SAAS,EAAE,MAAM,QAAQ,MAAM,OAAO;AAAA,cACtC,MAAM;AAAA,YACR;AAAA,UACF;AAAA,QACF;AAAA,MACF,OAAO;AACL,eAAO;AAAA,UACL,aAAa,OAAO;AAAA,UACpB,UAAU,OAAO;AAAA,QACnB;AAAA,MACF;AAAA,IACF,CAAC;AAAA,EACH;AAAA,EAEQ,sBAAsB,WAA0B;AACtD,SAAK,QAAQ,kBAAkB,4BAA4B,YAAY;AACrE,aAAO;AAAA,QACL,WAAW,UAAU,IAAI,CAAC,cAAc;AAAA,UACtC,aAAa,SAAS;AAAA,UACtB,UAAU,SAAS;AAAA,UACnB,MAAM,SAAS;AAAA,UACf,KAAK,SAAS;AAAA,QAChB,EAAE;AAAA,MACJ;AAAA,IACF,CAAC;AAED,SAAK,QAAQ,kBAAkB,2BAA2B,OAAO,YAAY;AAC3E,UAAI,SAAS,QAAQ,QAAQ;AAC3B,cAAM,WAAW,UAAU,KAAK,CAACC,cAAa,SAASA,aAAYA,UAAS,QAAQ,QAAQ,OAAO,GAAG;AAEtG,YAAI,CAAC,UAAU;AACb,qBAAW,oBAAoB,KAAK,oBAAoB;AACtD,kBAAM,cAAc,iBAAiB,iBAAiB,WAAW;AAEjE,kBAAM,QAAQ,YAAY,QAAQ,QAAQ,OAAO,GAAG;AAEpD,gBAAI,CAAC,OAAO;AACV;AAAA,YACF;AAEA,kBAAM,MAAM,YAAY,KAAK,KAAK;AAElC,kBAAM,SAAS,MAAM,iBAAiB,KAAK,OAAO,KAAK,KAAK;AAE5D,kBAAMC,aAAY,MAAM,QAAQ,MAAM,IAAI,SAAS,CAAC,MAAM;AAC1D,mBAAO;AAAA,cACL,UAAUA,WAAU,IAAI,CAACD,eAAc;AAAA,gBACrC,GAAGA;AAAA,gBACH,aAAa,iBAAiB;AAAA,gBAC9B,UAAUA,UAAS,YAAY,iBAAiB;AAAA,gBAChD,MAAM,iBAAiB;AAAA,gBACvB,KAAKA,UAAS,OAAO;AAAA,cACvB,EAAE;AAAA,YACJ;AAAA,UACF;AAEA,gBAAM,IAAI;AAAA,YACR,UAAU;AAAA,YACV,wBAAwB,QAAQ,OAAO,GAAG,2BACxC,UAAU,IAAI,CAAC,MAAM,EAAE,GAAG,EAAE,KAAK,IAAI,KAAK,MAC5C;AAAA,UACF;AAAA,QACF;AAEA,YAAI,EAAE,SAAS,WAAW;AACxB,gBAAM,IAAI,qBAAqB,mCAAmC;AAAA,QACpE;AAEA,YAAI;AAEJ,YAAI;AACF,6BAAmB,MAAM,SAAS,KAAK,KAAK,KAAK;AAAA,QACnD,SAAS,OAAO;AACd,gBAAM,eAAe,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AAC1E,gBAAM,IAAI;AAAA,YACR,UAAU;AAAA,YACV,4BAA4B,SAAS,IAAI,MAAM,SAAS,GAAG,MAAM,YAAY;AAAA,YAC7E;AAAA,cACE,KAAK,SAAS;AAAA,YAChB;AAAA,UACF;AAAA,QACF;AAEA,cAAM,kBAAkB,MAAM,QAAQ,gBAAgB,IAAI,mBAAmB,CAAC,gBAAgB;AAE9F,eAAO;AAAA,UACL,UAAU,gBAAgB,IAAI,CAAC,YAAY;AAAA,YACzC,GAAG;AAAA,YACH,UAAU,OAAO,YAAY,SAAS;AAAA,YACtC,MAAM,SAAS;AAAA,YACf,KAAK,OAAO,OAAO,SAAS;AAAA,UAC9B,EAAE;AAAA,QACJ;AAAA,MACF;AAEA,YAAM,IAAI,qBAAqB,4BAA4B;AAAA,QACzD;AAAA,MACF,CAAC;AAAA,IACH,CAAC;AAAA,EACH;AAAA,EAEQ,8BAA8B,mBAA0C;AAC9E,SAAK,QAAQ,kBAAkB,oCAAoC,YAAY;AAC7E,aAAO;AAAA,QACL,mBAAmB,kBAAkB,IAAI,CAAC,sBAAsB;AAAA,UAC9D,aAAa,iBAAiB;AAAA,UAC9B,UAAU,iBAAiB;AAAA,UAC3B,MAAM,iBAAiB;AAAA,UACvB,aAAa,iBAAiB;AAAA,QAChC,EAAE;AAAA,MACJ;AAAA,IACF,CAAC;AAAA,EACH;AAAA,EAEQ,qBAAqB;AAC3B,QAAI,KAAK,cAAc,YAAY,OAAO;AACxC,WAAK,QAAQ,MAAM,iEAAiE;AACpF;AAAA,IACF;AAGA,QAAI,OAAO,KAAK,QAAQ,cAAc,YAAY;AAChD,WAAK,QAAQ,uBAAuB,oCAAoC,MAAM;AAC5E,aAAK,QACF,UAAU,EACV,KAAK,CAAC,UAAU;AACf,eAAK,SAAS,MAAM;AAEpB,eAAK,KAAK,gBAAgB;AAAA,YACxB,OAAO,MAAM;AAAA,UACf,CAAC;AAAA,QACH,CAAC,EACA,MAAM,CAAC,UAAU;AAChB,cAAI,iBAAiB,YAAY,MAAM,SAAS,UAAU,gBAAgB;AACxE,iBAAK,QAAQ,MAAM,0DAA0D;AAAA,UAC/E,OAAO;AACL,iBAAK,QAAQ;AAAA,cACX;AAAA;AAAA,EACE,iBAAiB,QAAQ,MAAM,QAAQ,KAAK,UAAU,KAAK,CAC7D;AAAA,YACF;AAAA,UACF;AAAA,QACF,CAAC;AAAA,MACL,CAAC;AAAA,IACH,OAAO;AACL,WAAK,QAAQ,MAAM,qFAAqF;AAAA,IAC1G;AAAA,EACF;AAAA,EAEQ,kBAAkB,OAAkB;AAC1C,SAAK,QAAQ,kBAAkB,wBAAwB,YAAY;AACjE,aAAO;AAAA,QACL,OAAO,MAAM,QAAQ;AAAA,UACnB,MAAM,IAAI,OAAO,SAAS;AACxB,mBAAO;AAAA,cACL,aAAa,KAAK;AAAA,cAClB,aAAa,KAAK;AAAA,cAClB,aAAa,KAAK,aACd,MAAM,aAAa,KAAK,UAAU,IAClC;AAAA,gBACE,sBAAsB;AAAA,gBACtB,YAAY,CAAC;AAAA,gBACb,MAAM;AAAA,cACR;AAAA;AAAA,cACJ,MAAM,KAAK;AAAA,YACb;AAAA,UACF,CAAC;AAAA,QACH;AAAA,MACF;AAAA,IACF,CAAC;AAED,SAAK,QAAQ,kBAAkB,uBAAuB,OAAO,YAAY;AACvE,YAAM,OAAO,MAAM,KAAK,CAACE,UAASA,MAAK,SAAS,QAAQ,OAAO,IAAI;AAEnE,UAAI,CAAC,MAAM;AACT,cAAM,IAAI,SAAS,UAAU,gBAAgB,iBAAiB,QAAQ,OAAO,IAAI,EAAE;AAAA,MACrF;AAEA,UAAI,OAAgB;AAEpB,UAAI,KAAK,YAAY;AACnB,cAAM,SAAS,MAAM,KAAK,WAAW,WAAW,EAAE,SAAS,QAAQ,OAAO,SAAS;AAEnF,YAAI,OAAO,QAAQ;AACjB,gBAAM,iBAAiB,KAAK,QAAQ,kCAChC,KAAK,OAAO,gCAAgC,OAAO,MAAM,IACzD,OAAO,OACJ,IAAI,CAAC,UAAU;AACd,kBAAM,OAAO,MAAM,MAAM,KAAK,GAAG,KAAK;AACtC,mBAAO,GAAG,IAAI,KAAK,MAAM,OAAO;AAAA,UAClC,CAAC,EACA,KAAK,IAAI;AAEhB,gBAAM,IAAI;AAAA,YACR,UAAU;AAAA,YACV,SAAS,QAAQ,OAAO,IAAI,kCAAkC,cAAc;AAAA,UAC9E;AAAA,QACF;AAEA,eAAO,OAAO;AAAA,MAChB;AAEA,YAAM,gBAAgB,QAAQ,QAAQ,OAAO;AAE7C,UAAI;AAEJ,UAAI;AACF,cAAM,iBAAiB,OAAO,aAAuB;AACnD,cAAI;AACF,kBAAM,KAAK,QAAQ,aAAa;AAAA,cAC9B,QAAQ;AAAA,cACR,QAAQ;AAAA,gBACN,GAAG;AAAA,gBACH;AAAA,cACF;AAAA,YACF,CAAC;AAED,gBAAI,KAAK,sBAAsB;AAC7B,oBAAM,IAAI,QAAQ,CAAC,YAAY,aAAa,OAAO,CAAC;AAAA,YACtD;AAAA,UACF,SAAS,eAAe;AACtB,iBAAK,QAAQ;AAAA,cACX,yDAAyD,QAAQ,OAAO,IAAI;AAAA,cAC5E,yBAAyB,QAAQ,cAAc,UAAU,OAAO,aAAa;AAAA,YAC/E;AAAA,UACF;AAAA,QACF;AAEA,cAAM,MAAM;AAAA,UACV,OAAO,CAAC,SAAiB,YAAgC;AACvD,iBAAK,QAAQ,mBAAmB;AAAA,cAC9B,MAAM;AAAA,gBACJ;AAAA,gBACA;AAAA,cACF;AAAA,cACA,OAAO;AAAA,YACT,CAAC;AAAA,UACH;AAAA,UACA,OAAO,CAAC,SAAiB,YAAgC;AACvD,iBAAK,QAAQ,mBAAmB;AAAA,cAC9B,MAAM;AAAA,gBACJ;AAAA,gBACA;AAAA,cACF;AAAA,cACA,OAAO;AAAA,YACT,CAAC;AAAA,UACH;AAAA,UACA,MAAM,CAAC,SAAiB,YAAgC;AACtD,iBAAK,QAAQ,mBAAmB;AAAA,cAC9B,MAAM;AAAA,gBACJ;AAAA,gBACA;AAAA,cACF;AAAA,cACA,OAAO;AAAA,YACT,CAAC;AAAA,UACH;AAAA,UACA,MAAM,CAAC,SAAiB,YAAgC;AACtD,iBAAK,QAAQ,mBAAmB;AAAA,cAC9B,MAAM;AAAA,gBACJ;AAAA,gBACA;AAAA,cACF;AAAA,cACA,OAAO;AAAA,YACT,CAAC;AAAA,UACH;AAAA,QACF;AAKA,cAAM,gBAAgB,OAAO,YAAiC;AAC5D,gBAAM,eAAe,MAAM,QAAQ,OAAO,IAAI,UAAU,CAAC,OAAO;AAEhE,cAAI;AACF,kBAAM,KAAK,QAAQ,aAAa;AAAA,cAC9B,QAAQ;AAAA,cACR,QAAQ;AAAA,gBACN,SAAS;AAAA,gBACT,UAAU,QAAQ,OAAO;AAAA,cAC3B;AAAA,YACF,CAAC;AAED,gBAAI,KAAK,sBAAsB;AAC7B,oBAAM,IAAI,QAAQ,CAAC,YAAY,aAAa,OAAO,CAAC;AAAA,YACtD;AAAA,UACF,SAAS,aAAa;AACpB,iBAAK,QAAQ;AAAA,cACX,wDAAwD,QAAQ,OAAO,IAAI;AAAA,cAC3E,uBAAuB,QAAQ,YAAY,UAAU,OAAO,WAAW;AAAA,YACzE;AAAA,UACF;AAAA,QACF;AACA,cAAM,qBAAqB,KAAK,QAAQ,MAAM;AAAA,UAC5C,QAAQ;AAAA,YACN,SAAS,KAAK,QAAQ,iBAAiB;AAAA,UACzC;AAAA,UACA;AAAA,UACA;AAAA,UACA,WAAW,OAAO,QAAQ,QAAQ,OAAO,cAAc,WAAW,QAAQ,OAAO,MAAM,YAAY;AAAA,UACnG,iBAAiB,QAAQ,OAAO;AAAA,UAChC,SAAS,KAAK;AAAA,UACd,WAAW,KAAK;AAAA,UAChB;AAAA,QACF,CAAC;AAGD,cAAM,oBAAqB,OAAO,KAAK,YACnC,QAAQ,KAAK;AAAA,UACX;AAAA,UACA,IAAI,QAAe,CAAC,GAAG,WAAW;AAChC,kBAAM,YAAY,WAAW,MAAM;AACjC;AAAA,gBACE,IAAI;AAAA,kBACF,UAAU;AAAA,kBACV,SAAS,QAAQ,OAAO,IAAI,qBAAqB,KAAK,SAAS;AAAA,gBACjE;AAAA,cACF;AAAA,YACF,GAAG,KAAK,SAAS;AAGjB,+BAAmB,QAAQ,MAAM,aAAa,SAAS,CAAC;AAAA,UAC1D,CAAC;AAAA,QACH,CAAC,IACD;AAaJ,cAAM,MAAM,CAAC;AAEb,YAAI,sBAAsB,UAAa,sBAAsB,MAAM;AACjE,mBAAS,uBAAuB,MAAM;AAAA,YACpC,SAAS,CAAC;AAAA,UACZ,CAAC;AAAA,QACH,WAAW,OAAO,sBAAsB,UAAU;AAChD,mBAAS,uBAAuB,MAAM;AAAA,YACpC,SAAS,CAAC,EAAE,MAAM,mBAAmB,MAAM,OAAO,CAAC;AAAA,UACrD,CAAC;AAAA,QACH,WAAW,UAAU,mBAAmB;AACtC,mBAAS,uBAAuB,MAAM;AAAA,YACpC,SAAS,CAAC,iBAAiB;AAAA,UAC7B,CAAC;AAAA,QACH,OAAO;AACL,mBAAS,uBAAuB,MAAM,iBAAiB;AAAA,QACzD;AAAA,MACF,SAAS,OAAO;AAGd,YAAI,eAAe,KAAK,GAAG;AACzB,gBAAM;AAAA,QACR;AAEA,YAAI,iBAAiB,WAAW;AAC9B,iBAAO;AAAA,YACL,SAAS,CAAC,EAAE,MAAM,MAAM,SAAS,MAAM,OAAO,CAAC;AAAA,YAC/C,SAAS;AAAA,YACT,GAAI,MAAM,SAAS,EAAE,mBAAmB,MAAM,OAAO,IAAI,CAAC;AAAA,UAC5D;AAAA,QACF;AAEA,cAAM,eAAe,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AAC1E,eAAO;AAAA,UACL,SAAS;AAAA,YACP;AAAA,cACE,MAAM,SAAS,QAAQ,OAAO,IAAI,uBAAuB,YAAY;AAAA,cACrE,MAAM;AAAA,YACR;AAAA,UACF;AAAA,UACA,SAAS;AAAA,QACX;AAAA,MACF;AAEA,aAAO;AAAA,IACT,CAAC;AAAA,EACH;AACF;AAKA,SAAS,iBAAiB,KAAqB;AAC7C,SAAO,IAAI,QAAQ,UAAU,CAAC,WAAW,IAAI,OAAO,YAAY,CAAC,EAAE;AACrE;AAKA,SAAS,yBAAyB,KAAuD;AACvF,QAAM,SAAkC,CAAC;AAEzC,aAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,GAAG,GAAG;AAC9C,UAAM,WAAW,iBAAiB,GAAG;AACrC,WAAO,QAAQ,IAAI;AAAA,EACrB;AAEA,SAAO;AACT;AAEA,IAAM,0BAEF;AAEJ,IAAM,sBAAN,cAAkC,wBAAwB;AAAC;AAEpD,IAAM,UAAN,cAAyE,oBAAoB;AAAA,EAelG,YAAmB,SAA2B;AAC5C,UAAM;AADW;AAGjB,SAAK,WAAW;AAChB,SAAK,gBAAgB,QAAQ;AAC7B,SAAK,UAAU,QAAQ,UAAU;AAAA,EACnC;AAAA,EApBA,IAAW,WAAgC;AACzC,WAAO,KAAK;AAAA,EACd;AAAA,EACA;AAAA,EACA,oBAAsC;AAAA,EACtC;AAAA,EACA;AAAA,EACA,WAA6B,CAAC;AAAA,EAC9B,aAA4B,CAAC;AAAA,EAC7B,sBAAkD,CAAC;AAAA,EACnD,YAAiC,CAAC;AAAA,EAElC,SAAoB,CAAC;AAAA;AAAA;AAAA;AAAA,EAad,UAAuD,QAA8B;AAC1F,SAAK,SAAS,KAAK,MAAM;AAAA,EAC3B;AAAA;AAAA;AAAA;AAAA,EAKO,YAAY,UAAuB;AACxC,SAAK,WAAW,KAAK,QAAQ;AAAA,EAC/B;AAAA;AAAA;AAAA;AAAA,EAKO,oBACL,UACA;AACA,SAAK,oBAAoB,KAAK,QAAQ;AAAA,EACxC;AAAA;AAAA;AAAA;AAAA,EAKO,QAAuC,MAAuB;AACnE,SAAK,OAAO,KAAK,IAA0B;AAAA,EAC7C;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,MAAa,SAAS,KAAmD;AAEvE,UAAM,iBAAiB,KAAK,WAAW,KAAK,CAAC,aAAa,SAAS,QAAQ,GAAG;AAE9E,QAAI,gBAAgB;AAClB,YAAM,SAAS,MAAM,eAAe,KAAK;AACzC,YAAM,UAAU,MAAM,QAAQ,MAAM,IAAI,SAAS,CAAC,MAAM;AACxD,YAAM,cAAc,QAAQ,CAAC;AAE7B,YAAM,eAA4C;AAAA,QAChD,UAAU,eAAe;AAAA,QACzB;AAAA,MACF;AAEA,UAAI,UAAU,aAAa;AACzB,qBAAa,OAAO,YAAY;AAAA,MAClC;AAEA,UAAI,UAAU,aAAa;AACzB,qBAAa,OAAO,YAAY;AAAA,MAClC;AAEA,aAAO;AAAA,IACT;AAGA,eAAW,YAAY,KAAK,qBAAqB;AAC/C,YAAM,iBAAiB,iBAAiB,SAAS,WAAW;AAC5D,YAAM,SAAS,eAAe,QAAQ,GAAG;AACzC,UAAI,CAAC,QAAQ;AACX;AAAA,MACF;AAEA,YAAM,SAAS,MAAM,SAAS,KAAK,MAAsE;AAEzG,YAAM,eAA4C;AAAA,QAChD,UAAU,SAAS;AAAA,QACnB;AAAA,MACF;AAEA,UAAI,UAAU,QAAQ;AACpB,qBAAa,OAAO,OAAO;AAAA,MAC7B;AAEA,UAAI,UAAU,QAAQ;AACpB,qBAAa,OAAO,OAAO;AAAA,MAC7B;AAEA,aAAO;AAAA,IACT;AAEA,UAAM,IAAI,qBAAqB,uBAAuB,GAAG,IAAI,EAAE,IAAI,CAAC;AAAA,EACtE;AAAA;AAAA;AAAA;AAAA,EAKA,MAAa,MACX,SAWA;AACA,UAAM,SAAS,KAAK,oBAAoB,OAAO;AAE/C,QAAI,OAAO,kBAAkB,SAAS;AACpC,YAAM,YAAY,IAAI,qBAAqB;AAI3C,UAAI;AAEJ,UAAI,KAAK,eAAe;AACtB,YAAI;AACF,iBAAO,MAAM,KAAK,cAAc,MAA4C;AAAA,QAC9E,SAAS,OAAO;AACd,eAAK,QAAQ;AAAA,YACX;AAAA,YACA,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AAAA,UACvD;AAAA,QAEF;AAAA,MACF;AAEA,YAAM,UAAU,IAAI,eAAkB;AAAA,QACpC;AAAA,QACA,cAAc,KAAK,SAAS;AAAA,QAC5B,QAAQ,KAAK;AAAA,QACb,MAAM,KAAK,SAAS;AAAA,QACpB,MAAM,KAAK,SAAS;AAAA,QACpB,SAAS,KAAK;AAAA,QACd,WAAW,KAAK;AAAA,QAChB,oBAAoB,KAAK;AAAA,QACzB,OAAO,KAAK,SAAS;AAAA,QACrB,OAAO,KAAK;AAAA,QACZ,eAAe;AAAA,QACf,OAAO,KAAK,SAAS;AAAA,QACrB,SAAS,KAAK,SAAS;AAAA,MACzB,CAAC;AAED,YAAM,QAAQ,QAAQ,SAAS;AAE/B,WAAK,UAAU,KAAK,OAAO;AAE3B,cAAQ,KAAK,SAAS,MAAM;AAC1B,aAAK,eAAe,OAAO;AAAA,MAC7B,CAAC;AAGD,UAAI,UAAU,SAAS;AACrB,cAAM,kBAAkB,UAAU;AAElC,kBAAU,UAAU,MAAM;AACxB,eAAK,eAAe,OAAO;AAE3B,cAAI,iBAAiB;AACnB,4BAAgB;AAAA,UAClB;AAAA,QACF;AAAA,MACF,OAAO;AACL,kBAAU,UAAU,MAAM;AACxB,eAAK,eAAe,OAAO;AAAA,QAC7B;AAAA,MACF;AAEA,WAAK,KAAK,WAAW;AAAA,QACnB;AAAA,MACF,CAAC;AAAA,IACH,WAAW,OAAO,kBAAkB,cAAc;AAChD,YAAM,aAAa,OAAO;AAE1B,UAAI,WAAW,WAAW;AAExB,aAAK,QAAQ;AAAA,UACX,6EAA6E,WAAW,IAAI,IAAI,WAAW,IAAI,GAAG,WAAW,QAAQ;AAAA,QACvI;AAEA,aAAK,oBAAoB,MAAM,gBAAmC;AAAA,UAChE,GAAI,KAAK,gBAAgB,EAAE,cAAc,KAAK,cAAc,IAAI,CAAC;AAAA,UACjE,cAAc,OAAO,YAAY;AAC/B,gBAAI;AAEJ,gBAAI,KAAK,eAAe;AACtB,qBAAO,MAAM,KAAK,cAAc,OAAO;AAIvC,kBAAI,SAAS,UAAa,SAAS,MAAM;AACvC,sBAAM,IAAI,MAAM,yBAAyB;AAAA,cAC3C;AAAA,YACF;AAGA,kBAAM,YAAY,MAAM,QAAQ,QAAQ,QAAQ,gBAAgB,CAAC,IAC7D,QAAQ,QAAQ,gBAAgB,EAAE,CAAC,IACnC,QAAQ,QAAQ,gBAAgB;AAIpC,mBAAO,KAAK,eAAe,MAAM,SAAS;AAAA,UAC5C;AAAA,UACA,oBAAoB,WAAW;AAAA,UAC/B,YAAY,WAAW;AAAA,UACvB,MAAM,WAAW;AAAA;AAAA,UAEjB,SAAS,YAAY;AAAA,UAErB;AAAA,UACA,WAAW,YAAY;AAErB,iBAAK,QAAQ,MAAM,uDAAuD;AAAA,UAC5E;AAAA,UACA,oBAAoB,OAAO,KAAK,QAAQ;AACtC,kBAAM,KAAK,wBAAwB,KAAK,KAAK,MAAM,WAAW,IAAI;AAAA,UACpE;AAAA,UACA,MAAM,WAAW;AAAA,UACjB,WAAW;AAAA,UACX,gBAAgB,WAAW;AAAA,QAC7B,CAAC;AAAA,MACH,OAAO;AAEL,aAAK,oBAAoB,MAAM,gBAAmC;AAAA,UAChE,GAAI,KAAK,gBAAgB,EAAE,cAAc,KAAK,cAAc,IAAI,CAAC;AAAA,UACjE,cAAc,OAAO,YAAY;AAC/B,gBAAI;AAEJ,gBAAI,KAAK,eAAe;AACtB,qBAAO,MAAM,KAAK,cAAc,OAAO;AAAA,YACzC;AAGA,kBAAM,YAAY,MAAM,QAAQ,QAAQ,QAAQ,gBAAgB,CAAC,IAC7D,QAAQ,QAAQ,gBAAgB,EAAE,CAAC,IACnC,QAAQ,QAAQ,gBAAgB;AAEpC,mBAAO,KAAK,eAAe,MAAM,SAAS;AAAA,UAC5C;AAAA,UACA,oBAAoB,WAAW;AAAA,UAC/B,YAAY,WAAW;AAAA,UACvB,MAAM,WAAW;AAAA,UACjB,SAAS,OAAO,YAAY;AAC1B,kBAAM,eAAe,KAAK,UAAU,QAAQ,OAAO;AAEnD,gBAAI,iBAAiB,GAAI,MAAK,UAAU,OAAO,cAAc,CAAC;AAE9D,iBAAK,KAAK,cAAc;AAAA,cACtB;AAAA,YACF,CAAC;AAAA,UACH;AAAA,UACA,WAAW,OAAO,YAAY;AAC5B,iBAAK,UAAU,KAAK,OAAO;AAE3B,iBAAK,QAAQ,KAAK,gDAAgD;AAElE,iBAAK,KAAK,WAAW;AAAA,cACnB;AAAA,YACF,CAAC;AAAA,UACH;AAAA,UAEA,oBAAoB,OAAO,KAAK,QAAQ;AACtC,kBAAM,KAAK,wBAAwB,KAAK,KAAK,OAAO,WAAW,IAAI;AAAA,UACrE;AAAA,UACA,MAAM,WAAW;AAAA,UACjB,WAAW,WAAW;AAAA,UACtB,gBAAgB,WAAW;AAAA,QAC7B,CAAC;AAED,aAAK,QAAQ;AAAA,UACX,6DAA6D,WAAW,IAAI,IAAI,WAAW,IAAI,GAAG,WAAW,QAAQ;AAAA,QACvH;AAAA,MACF;AAAA,IACF,OAAO;AACL,YAAM,IAAI,MAAM,wBAAwB;AAAA,IAC1C;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,MAAa,OAAO;AAClB,QAAI,KAAK,mBAAmB;AAC1B,YAAM,KAAK,kBAAkB,MAAM;AAAA,IACrC;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,eAAe,MAAU,WAAuC;AAE9D,QACE,QACA,OAAO,SAAS,YAChB,mBAAmB,QACnB,CAAE,KAAoC,eACtC;AACA,YAAM,eACJ,WAAW,QAAQ,OAAQ,KAA4B,UAAU,WAC5D,KAA2B,QAC5B;AACN,YAAM,IAAI,MAAM,YAAY;AAAA,IAC9B;AAEA,UAAM,eAAe,OACjB,KAAK,OAAO,OAAO,CAAC,SAAU,KAAK,YAAY,KAAK,UAAU,IAAI,IAAI,IAAK,IAC3E,KAAK;AACT,WAAO,IAAI,eAAkB;AAAA,MAC3B;AAAA,MACA,cAAc,KAAK,SAAS;AAAA,MAC5B,QAAQ,KAAK;AAAA,MACb,MAAM,KAAK,SAAS;AAAA,MACpB,MAAM,KAAK,SAAS;AAAA,MACpB,SAAS,KAAK;AAAA,MACd,WAAW,KAAK;AAAA,MAChB,oBAAoB,KAAK;AAAA,MACzB,OAAO,KAAK,SAAS;AAAA,MACrB;AAAA,MACA,OAAO;AAAA,MACP,eAAe;AAAA,MACf,OAAO,KAAK,SAAS;AAAA,MACrB,SAAS,KAAK,SAAS;AAAA,IACzB,CAAC;AAAA,EACH;AAAA;AAAA;AAAA;AAAA,EAKA,0BAA0B,OACxB,KACA,KACA,cAAc,OACd,SACG;AACH,UAAM,eAAe,KAAK,SAAS,UAAU,CAAC;AAE9C,UAAM,UAAU,aAAa,YAAY,SAAY,OAAO,aAAa;AAEzE,QAAI,SAAS;AACX,YAAM,OAAO,aAAa,QAAQ;AAClC,YAAM,MAAM,IAAI,IAAI,IAAI,OAAO,IAAI,UAAU,IAAI,EAAE;AAEnD,UAAI;AACF,YAAI,IAAI,WAAW,SAAS,IAAI,aAAa,MAAM;AACjD,cACG,UAAU,aAAa,UAAU,KAAK;AAAA,YACrC,gBAAgB;AAAA,UAClB,CAAC,EACA,IAAI,aAAa,WAAW,WAAM;AAErC;AAAA,QACF;AAGA,YAAI,IAAI,WAAW,SAAS,IAAI,aAAa,UAAU;AACrD,cAAI,aAAa;AAEf,kBAAM,WAAW;AAAA,cACf,MAAM;AAAA,cACN,OAAO;AAAA,cACP,QAAQ;AAAA,cACR,OAAO;AAAA,YACT;AAEA,gBACG,UAAU,KAAK;AAAA,cACd,gBAAgB;AAAA,YAClB,CAAC,EACA,IAAI,KAAK,UAAU,QAAQ,CAAC;AAAA,UACjC,OAAO;AACL,kBAAM,gBAAgB,KAAK,UAAU,OAAO,CAAC,MAAM,EAAE,OAAO,EAAE;AAC9D,kBAAM,gBAAgB,KAAK,UAAU;AACrC,kBAAM,WAAW,kBAAkB,iBAAiB,gBAAgB;AAEpE,kBAAM,WAAW;AAAA,cACf,OAAO;AAAA,cACP,QAAQ,WAAW,UAAU,kBAAkB,IAAI,gBAAgB;AAAA,cACnE,OAAO;AAAA,YACT;AAEA,gBACG,UAAU,WAAW,MAAM,KAAK;AAAA,cAC/B,gBAAgB;AAAA,YAClB,CAAC,EACA,IAAI,KAAK,UAAU,QAAQ,CAAC;AAAA,UACjC;AAEA;AAAA,QACF;AAAA,MACF,SAAS,OAAO;AACd,aAAK,QAAQ,MAAM,yCAAyC,KAAK;AAAA,MACnE;AAAA,IACF;AAGA,UAAM,cAAc,KAAK,SAAS;AAClC,QAAI,aAAa,WAAW,IAAI,WAAW,OAAO;AAChD,YAAM,MAAM,IAAI,IAAI,IAAI,OAAO,IAAI,UAAU,IAAI,EAAE;AAEnD,UAAI,IAAI,aAAa,6CAA6C,YAAY,qBAAqB;AACjG,cAAM,WAAW,yBAAyB,YAAY,mBAAmB;AACzE,YACG,UAAU,KAAK;AAAA,UACd,gBAAgB;AAAA,QAClB,CAAC,EACA,IAAI,KAAK,UAAU,QAAQ,CAAC;AAC/B;AAAA,MACF;AAEA,UAAI,IAAI,aAAa,2CAA2C,YAAY,mBAAmB;AAC7F,cAAM,WAAW,yBAAyB,YAAY,iBAAiB;AACvE,YACG,UAAU,KAAK;AAAA,UACd,gBAAgB;AAAA,QAClB,CAAC,EACA,IAAI,KAAK,UAAU,QAAQ,CAAC;AAC/B;AAAA,MACF;AAAA,IACF;AAGA,QAAI,UAAU,GAAG,EAAE,IAAI;AAAA,EACzB;AAAA,EAEA,oBACE,WAsB6B;AAC7B,UAAM,OAAO,QAAQ,KAAK,MAAM,CAAC;AACjC,UAAM,SAAS,CAAC,SAAiB;AAC/B,YAAM,QAAQ,KAAK,UAAU,CAAC,QAAQ,QAAQ,KAAK,IAAI,EAAE;AAEzD,aAAO,UAAU,MAAM,QAAQ,IAAI,KAAK,SAAS,KAAK,QAAQ,CAAC,IAAI;AAAA,IACrE;AAEA,UAAM,eAAe,OAAO,WAAW;AACvC,UAAM,UAAU,OAAO,MAAM;AAC7B,UAAM,cAAc,OAAO,UAAU;AACrC,UAAM,eAAe,OAAO,WAAW;AACvC,UAAM,UAAU,OAAO,MAAM;AAE7B,UAAM,eAAe,QAAQ,IAAI;AACjC,UAAM,UAAU,QAAQ,IAAI;AAC5B,UAAM,cAAc,QAAQ,IAAI;AAChC,UAAM,eAAe,QAAQ,IAAI;AACjC,UAAM,UAAU,QAAQ,IAAI;AAE5B,UAAM,gBACJ,WAAW,kBACV,iBAAiB,gBAAgB,eAAe,iBACjD,gBACA;AAEF,QAAI,kBAAkB,cAAc;AAClC,YAAM,OAAO,SAAS,WAAW,YAAY,MAAM,SAAS,KAAK,WAAW,WAAW,MAAM;AAC7F,YAAM,OAAO,WAAW,YAAY,QAAQ,WAAW,WAAW;AAClE,YAAM,WAAW,WAAW,YAAY,YAAY,eAAe,eAAe;AAClF,YAAM,qBAAqB,WAAW,YAAY,sBAAsB;AACxE,YAAM,YAAY,WAAW,YAAY,aAAa,iBAAiB,UAAU,iBAAiB,UAAU;AAE5G,aAAO;AAAA,QACL,YAAY;AAAA,UACV;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,QACF;AAAA,QACA,eAAe;AAAA,MACjB;AAAA,IACF;AAEA,WAAO,EAAE,eAAe,QAAiB;AAAA,EAC3C;AAAA,EAEA,eAAe,SAAkC;AAC/C,UAAM,eAAe,KAAK,UAAU,QAAQ,OAAO;AAEnD,QAAI,iBAAiB,IAAI;AACvB,WAAK,UAAU,OAAO,cAAc,CAAC;AACrC,WAAK,KAAK,cAAc;AAAA,QACtB;AAAA,MACF,CAAC;AAAA,IACH;AAAA,EACF;AACF;","names":["ErrorCode","McpError","prompt","resource","resources","tool"]} \ No newline at end of file diff --git a/dist/bin/fastmcp.d.ts b/dist/bin/fastmcp.d.ts deleted file mode 100644 index 908ba84..0000000 --- a/dist/bin/fastmcp.d.ts +++ /dev/null @@ -1 +0,0 @@ -#!/usr/bin/env node diff --git a/dist/bin/fastmcp.js b/dist/bin/fastmcp.js deleted file mode 100755 index db531b0..0000000 --- a/dist/bin/fastmcp.js +++ /dev/null @@ -1,145 +0,0 @@ -#!/usr/bin/env node - -// src/bin/fastmcp.ts -import { execa } from "execa"; -import yargs from "yargs"; -import { hideBin } from "yargs/helpers"; -await yargs(hideBin(process.argv)).scriptName("fastmcp").command( - "dev ", - "Start a development server", - (yargs2) => { - return yargs2.positional("file", { - demandOption: true, - describe: "The path to the server file", - type: "string" - }).option("watch", { - alias: "w", - default: false, - describe: "Watch for file changes and restart server", - type: "boolean" - }).option("verbose", { - alias: "v", - default: false, - describe: "Enable verbose logging", - type: "boolean" - }); - }, - async (argv) => { - try { - const command = argv.watch ? `npx @wong2/mcp-cli npx tsx --watch ${argv.file}` : `npx @wong2/mcp-cli npx tsx ${argv.file}`; - if (argv.verbose) { - console.log(`[FastMCP] Starting server: ${command}`); - console.log(`[FastMCP] File: ${argv.file}`); - console.log(`[FastMCP] Watch mode: ${argv.watch ? "enabled" : "disabled"}`); - } - await execa({ - shell: true, - stderr: "inherit", - stdin: "inherit", - stdout: "inherit" - })`${command}`; - } catch (error) { - console.error( - "[FastMCP Error] Failed to start development server:", - error instanceof Error ? error.message : String(error) - ); - if (argv.verbose && error instanceof Error && error.stack) { - console.error("[FastMCP Debug] Stack trace:", error.stack); - } - process.exit(1); - } - } -).command( - "inspect ", - "Inspect a server file", - (yargs2) => { - return yargs2.positional("file", { - demandOption: true, - describe: "The path to the server file", - type: "string" - }); - }, - async (argv) => { - try { - await execa({ - stderr: "inherit", - stdout: "inherit" - })`npx @modelcontextprotocol/inspector npx tsx ${argv.file}`; - } catch (error) { - console.error( - "[FastMCP Error] Failed to inspect server:", - error instanceof Error ? error.message : String(error) - ); - process.exit(1); - } - } -).command( - "validate ", - "Validate a FastMCP server file for syntax and basic structure", - (yargs2) => { - return yargs2.positional("file", { - demandOption: true, - describe: "The path to the server file", - type: "string" - }).option("strict", { - alias: "s", - default: false, - describe: "Enable strict validation (type checking)", - type: "boolean" - }); - }, - async (argv) => { - try { - const { existsSync } = await import("fs"); - const { resolve } = await import("path"); - const filePath = resolve(argv.file); - if (!existsSync(filePath)) { - console.error(`[FastMCP Error] File not found: ${filePath}`); - process.exit(1); - } - console.log(`[FastMCP] Validating server file: ${filePath}`); - const command = argv.strict ? `npx tsc --noEmit --strict ${filePath}` : `npx tsc --noEmit ${filePath}`; - try { - await execa({ - shell: true, - stderr: "pipe", - stdout: "pipe" - })`${command}`; - console.log("[FastMCP] \u2713 TypeScript compilation successful"); - } catch (tsError) { - console.error("[FastMCP] \u2717 TypeScript compilation failed"); - if (tsError instanceof Error && "stderr" in tsError) { - console.error(tsError.stderr); - } - process.exit(1); - } - try { - await execa({ - shell: true, - stderr: "pipe", - stdout: "pipe" - })`node -e " - (async () => { - try { - const { FastMCP } = await import('fastmcp'); - await import('file://${filePath}'); - console.log('[FastMCP] ✓ Server structure validation passed'); - } catch (error) { - console.error('[FastMCP] ✗ Server structure validation failed:', error.message); - process.exit(1); - } - })(); - "`; - } catch { - console.error("[FastMCP] \u2717 Server structure validation failed"); - console.error("Make sure the file properly imports and uses FastMCP"); - process.exit(1); - } - console.log("[FastMCP] \u2713 All validations passed! Server file looks good."); - } catch (error) { - console.error("[FastMCP Error] Validation failed:", error instanceof Error ? error.message : String(error)); - process.exit(1); - } - } -).help().parseAsync(); -//# sourceMappingURL=fastmcp.js.map \ No newline at end of file diff --git a/dist/bin/fastmcp.js.map b/dist/bin/fastmcp.js.map deleted file mode 100644 index 767ec2e..0000000 --- a/dist/bin/fastmcp.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"sources":["../../src/bin/fastmcp.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { execa } from \"execa\"\nimport yargs from \"yargs\"\nimport { hideBin } from \"yargs/helpers\"\n\nawait yargs(hideBin(process.argv))\n .scriptName(\"fastmcp\")\n .command(\n \"dev \",\n \"Start a development server\",\n (yargs) => {\n return yargs\n .positional(\"file\", {\n demandOption: true,\n describe: \"The path to the server file\",\n type: \"string\",\n })\n\n .option(\"watch\", {\n alias: \"w\",\n default: false,\n describe: \"Watch for file changes and restart server\",\n type: \"boolean\",\n })\n\n .option(\"verbose\", {\n alias: \"v\",\n default: false,\n describe: \"Enable verbose logging\",\n type: \"boolean\",\n })\n },\n\n async (argv) => {\n try {\n const command = argv.watch\n ? `npx @wong2/mcp-cli npx tsx --watch ${argv.file}`\n : `npx @wong2/mcp-cli npx tsx ${argv.file}`\n\n if (argv.verbose) {\n console.log(`[FastMCP] Starting server: ${command}`)\n console.log(`[FastMCP] File: ${argv.file}`)\n console.log(`[FastMCP] Watch mode: ${argv.watch ? \"enabled\" : \"disabled\"}`)\n }\n\n await execa({\n shell: true,\n stderr: \"inherit\",\n stdin: \"inherit\",\n stdout: \"inherit\",\n })`${command}`\n } catch (error) {\n console.error(\n \"[FastMCP Error] Failed to start development server:\",\n error instanceof Error ? error.message : String(error),\n )\n\n if (argv.verbose && error instanceof Error && error.stack) {\n console.error(\"[FastMCP Debug] Stack trace:\", error.stack)\n }\n\n process.exit(1)\n }\n },\n )\n\n .command(\n \"inspect \",\n \"Inspect a server file\",\n (yargs) => {\n return yargs.positional(\"file\", {\n demandOption: true,\n describe: \"The path to the server file\",\n type: \"string\",\n })\n },\n\n async (argv) => {\n try {\n await execa({\n stderr: \"inherit\",\n stdout: \"inherit\",\n })`npx @modelcontextprotocol/inspector npx tsx ${argv.file}`\n } catch (error) {\n console.error(\n \"[FastMCP Error] Failed to inspect server:\",\n error instanceof Error ? error.message : String(error),\n )\n\n process.exit(1)\n }\n },\n )\n\n .command(\n \"validate \",\n \"Validate a FastMCP server file for syntax and basic structure\",\n (yargs) => {\n return yargs\n .positional(\"file\", {\n demandOption: true,\n describe: \"The path to the server file\",\n type: \"string\",\n })\n\n .option(\"strict\", {\n alias: \"s\",\n default: false,\n describe: \"Enable strict validation (type checking)\",\n type: \"boolean\",\n })\n },\n\n async (argv) => {\n try {\n const { existsSync } = await import(\"fs\")\n const { resolve } = await import(\"path\")\n const filePath = resolve(argv.file)\n\n if (!existsSync(filePath)) {\n console.error(`[FastMCP Error] File not found: ${filePath}`)\n process.exit(1)\n }\n\n console.log(`[FastMCP] Validating server file: ${filePath}`)\n\n const command = argv.strict ? `npx tsc --noEmit --strict ${filePath}` : `npx tsc --noEmit ${filePath}`\n\n try {\n await execa({\n shell: true,\n stderr: \"pipe\",\n stdout: \"pipe\",\n })`${command}`\n\n console.log(\"[FastMCP] ✓ TypeScript compilation successful\")\n } catch (tsError) {\n console.error(\"[FastMCP] ✗ TypeScript compilation failed\")\n\n if (tsError instanceof Error && \"stderr\" in tsError) {\n console.error(tsError.stderr)\n }\n\n process.exit(1)\n }\n\n try {\n await execa({\n shell: true,\n stderr: \"pipe\",\n stdout: \"pipe\",\n })`node -e \"\n (async () => {\n try {\n const { FastMCP } = await import('fastmcp');\n await import('file://${filePath}');\n console.log('[FastMCP] ✓ Server structure validation passed');\n } catch (error) {\n console.error('[FastMCP] ✗ Server structure validation failed:', error.message);\n process.exit(1);\n }\n })();\n \"`\n } catch {\n console.error(\"[FastMCP] ✗ Server structure validation failed\")\n console.error(\"Make sure the file properly imports and uses FastMCP\")\n\n process.exit(1)\n }\n\n console.log(\"[FastMCP] ✓ All validations passed! Server file looks good.\")\n } catch (error) {\n console.error(\"[FastMCP Error] Validation failed:\", error instanceof Error ? error.message : String(error))\n\n process.exit(1)\n }\n },\n )\n\n .help()\n .parseAsync()\n"],"mappings":"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","names":["yargs"]} \ No newline at end of file From fdb711204e4748f1aeca2a82c166e0a823c17cc1 Mon Sep 17 00:00:00 2001 From: Matias Date: Tue, 24 Feb 2026 14:57:15 -0300 Subject: [PATCH 6/7] feat: namespace package to @ampersend_ai and add npm release workflow - Rename package to @ampersend_ai/fastmcp - Add release.yml workflow for tag-based npm publishing - Update jsr.json namespace - Switch to @ampersend_ai/modelcontextprotocol-sdk dependency - Update repository URL to edgeandnode/fastmcp --- .github/workflows/release.yml | 100 ++++++++++++++++++++++++++++++++++ jsr.json | 2 +- package.json | 8 +-- pnpm-lock.yaml | 47 ++++++++-------- 4 files changed, 128 insertions(+), 29 deletions(-) create mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..3c566e7 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,100 @@ +name: Release + +on: + push: + tags: + - "v*.*.*" + - "v*.*.*-alpha.*" + - "v*.*.*-beta.*" + +permissions: + contents: read + id-token: write # Required for npm provenance + +env: + NODE_VERSION: "22.x" + +jobs: + verify-version: + name: Verify version + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Extract version from tag + id: tag + run: | + TAG=${GITHUB_REF#refs/tags/v} + echo "version=$TAG" >> $GITHUB_OUTPUT + echo "Tagged version: $TAG" + + - name: Read package.json version + id: package + run: | + VERSION=$(node -p "require('./package.json').version") + echo "version=$VERSION" >> $GITHUB_OUTPUT + echo "Package version: $VERSION" + + - name: Compare versions + run: | + if [ "${{ steps.tag.outputs.version }}" != "${{ steps.package.outputs.version }}" ]; then + echo "ERROR: Tag version (${{ steps.tag.outputs.version }}) does not match package.json version (${{ steps.package.outputs.version }})" + exit 1 + fi + echo "Versions match: ${{ steps.tag.outputs.version }}" + + publish: + name: Publish to npm + runs-on: ubuntu-latest + needs: verify-version + environment: + name: npm + url: https://www.npmjs.com/package/@ampersend_ai/fastmcp + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + package_json_file: package.json + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + registry-url: "https://registry.npmjs.org" + cache: "pnpm" + cache-dependency-path: pnpm-lock.yaml + + - name: Update npm + run: npm install -g npm@latest + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Build package + run: pnpm build + + - name: Determine publish tag + id: publish-tag + run: | + VERSION="${GITHUB_REF#refs/tags/v}" + if [[ "$VERSION" == *"-alpha."* ]]; then + echo "tag=alpha" >> $GITHUB_OUTPUT + echo "Publishing with tag: alpha" + elif [[ "$VERSION" == *"-beta."* ]]; then + echo "tag=beta" >> $GITHUB_OUTPUT + echo "Publishing with tag: beta" + else + echo "tag=latest" >> $GITHUB_OUTPUT + echo "Publishing with tag: latest" + fi + + - name: Publish to npm + run: npm publish --access public --tag ${{ steps.publish-tag.outputs.tag }} + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/jsr.json b/jsr.json index 3ab8734..e0104bb 100644 --- a/jsr.json +++ b/jsr.json @@ -2,6 +2,6 @@ "exports": "./src/FastMCP.ts", "include": ["src/FastMCP.ts", "src/bin/fastmcp.ts"], "license": "MIT", - "name": "@punkpeye/fastmcp", + "name": "@ampersend_ai/fastmcp", "version": "1.0.0" } diff --git a/package.json b/package.json index 36c448b..e50c2cd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { - "name": "fastmcp", - "version": "1.0.0-edgeandnode.1", + "name": "@ampersend_ai/fastmcp", + "version": "1.0.0", "main": "dist/FastMCP.js", "scripts": { "build": "tsup", @@ -22,7 +22,7 @@ "module": "dist/FastMCP.js", "types": "dist/FastMCP.d.ts", "dependencies": { - "@modelcontextprotocol/sdk": "github:edgeandnode/mcp-typescript-sdk#2de06543904483073d8cc13db1d0e08e16601081", + "@modelcontextprotocol/sdk": "npm:@ampersend_ai/modelcontextprotocol-sdk@^1.20.1", "@standard-schema/spec": "^1.0.0", "execa": "^9.6.0", "file-type": "^21.0.0", @@ -37,7 +37,7 @@ "zod-to-json-schema": "^3.24.6" }, "repository": { - "url": "https://github.com/punkpeye/fastmcp" + "url": "https://github.com/edgeandnode/fastmcp" }, "homepage": "https://glama.ai/mcp", "release": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ce73745..d67aea1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,8 +9,8 @@ importers: .: dependencies: '@modelcontextprotocol/sdk': - specifier: github:edgeandnode/mcp-typescript-sdk#2de06543904483073d8cc13db1d0e08e16601081 - version: https://codeload.github.com/edgeandnode/mcp-typescript-sdk/tar.gz/2de06543904483073d8cc13db1d0e08e16601081 + specifier: npm:@ampersend_ai/modelcontextprotocol-sdk@^1.20.1 + version: '@ampersend_ai/modelcontextprotocol-sdk@1.20.1' '@standard-schema/spec': specifier: ^1.0.0 version: 1.0.0 @@ -126,6 +126,10 @@ importers: packages: + '@ampersend_ai/modelcontextprotocol-sdk@1.20.1': + resolution: {integrity: sha512-u28xfWkgMyYsFW9jyJWuU8BndOFGJVPGRXu6g7t1iuOOTDA76lBAB5a6cbWzL4Y852gHYNyit3qTSL4I3EHCuw==} + engines: {node: '>=18'} + '@ark/schema@0.46.0': resolution: {integrity: sha512-c2UQdKgP2eqqDArfBqQIJppxJHvNNXuQPeuSPlDML4rjw+f1cu0qAlzOG4b8ujgm9ctIDWwhpyw6gjG5ledIVQ==} @@ -423,11 +427,6 @@ packages: resolution: {integrity: sha512-EFLRNXR/ixpXQWu6/3Cu30ndDFIFNaqUXcTqsGebujeMan9FzhAaFFswLRiFj61rgygDRr8WO1N+UijjgRxX9g==} engines: {node: '>=18'} - '@modelcontextprotocol/sdk@https://codeload.github.com/edgeandnode/mcp-typescript-sdk/tar.gz/2de06543904483073d8cc13db1d0e08e16601081': - resolution: {tarball: https://codeload.github.com/edgeandnode/mcp-typescript-sdk/tar.gz/2de06543904483073d8cc13db1d0e08e16601081} - version: 1.20.1-edgeandnode.1 - engines: {node: '>=18'} - '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -3716,6 +3715,23 @@ packages: snapshots: + '@ampersend_ai/modelcontextprotocol-sdk@1.20.1': + dependencies: + ajv: 6.12.6 + content-type: 1.0.5 + cors: 2.8.5 + cross-spawn: 7.0.6 + eventsource: 3.0.7 + eventsource-parser: 3.0.3 + express: 5.1.0 + express-rate-limit: 7.5.1(express@5.1.0) + pkce-challenge: 5.0.0 + raw-body: 3.0.0 + zod: 3.25.76 + zod-to-json-schema: 3.24.6(zod@3.25.76) + transitivePeerDependencies: + - supports-color + '@ark/schema@0.46.0': dependencies: '@ark/util': 0.46.0 @@ -4015,23 +4031,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@modelcontextprotocol/sdk@https://codeload.github.com/edgeandnode/mcp-typescript-sdk/tar.gz/2de06543904483073d8cc13db1d0e08e16601081': - dependencies: - ajv: 6.12.6 - content-type: 1.0.5 - cors: 2.8.5 - cross-spawn: 7.0.6 - eventsource: 3.0.7 - eventsource-parser: 3.0.3 - express: 5.1.0 - express-rate-limit: 7.5.1(express@5.1.0) - pkce-challenge: 5.0.0 - raw-body: 3.0.0 - zod: 3.25.76 - zod-to-json-schema: 3.24.6(zod@3.25.76) - transitivePeerDependencies: - - supports-color - '@nodelib/fs.scandir@2.1.5': dependencies: '@nodelib/fs.stat': 2.0.5 From cbca41cb964ecb967b6e55efc96298721c5aeb63 Mon Sep 17 00:00:00 2001 From: Matias Date: Tue, 24 Feb 2026 15:19:47 -0300 Subject: [PATCH 7/7] chore: apply prettier formatting --- README.md | 434 ++--- eslint.config.ts | 10 +- src/FastMCP.oauth.test.ts | 177 +- src/FastMCP.session-context.test.ts | 88 +- src/FastMCP.session-id.test.ts | 218 +-- src/FastMCP.test.ts | 2305 ++++++++++++++------------- src/FastMCP.ts | 1795 +++++++++++---------- src/bin/fastmcp.ts | 83 +- src/examples/addition.ts | 135 +- src/examples/custom-logger.ts | 28 +- src/examples/oauth-server.ts | 22 +- src/examples/session-context.ts | 113 +- src/examples/session-id-counter.ts | 98 +- vitest.config.js | 4 +- 14 files changed, 2961 insertions(+), 2549 deletions(-) diff --git a/README.md b/README.md index e616abf..b2fda06 100644 --- a/README.md +++ b/README.md @@ -69,13 +69,13 @@ npm install fastmcp > There are many real-world examples of using FastMCP in the wild. See the [Showcase](#showcase) for examples. ```ts -import { FastMCP } from "fastmcp" -import { z } from "zod" // Or any validation library that supports Standard Schema +import { FastMCP } from "fastmcp"; +import { z } from "zod"; // Or any validation library that supports Standard Schema const server = new FastMCP({ name: "My Server", version: "1.0.0", -}) +}); server.addTool({ name: "add", @@ -85,13 +85,13 @@ server.addTool({ b: z.number(), }), execute: async (args) => { - return String(args.a + args.b) + return String(args.a + args.b); }, -}) +}); server.start({ transportType: "stdio", -}) +}); ``` _That's it!_ You have a working MCP server. @@ -129,7 +129,7 @@ server.start({ httpStream: { port: 8080, }, -}) +}); ``` This will start the server and listen for HTTP streaming connections on `http://localhost:8080/mcp`. @@ -143,7 +143,7 @@ You can connect to these servers using the appropriate client transport. For HTTP streaming connections: ```ts -import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js" +import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; const client = new Client( { @@ -153,17 +153,19 @@ const client = new Client( { capabilities: {}, }, -) +); -const transport = new StreamableHTTPClientTransport(new URL(`http://localhost:8080/mcp`)) +const transport = new StreamableHTTPClientTransport( + new URL(`http://localhost:8080/mcp`), +); -await client.connect(transport) +await client.connect(transport); ``` For SSE connections: ```ts -import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js" +import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js"; const client = new Client( { @@ -173,11 +175,11 @@ const client = new Client( { capabilities: {}, }, -) +); -const transport = new SSEClientTransport(new URL(`http://localhost:8080/sse`)) +const transport = new SSEClientTransport(new URL(`http://localhost:8080/sse`)); -await client.connect(transport) +await client.connect(transport); ``` #### Stateless Mode @@ -200,7 +202,7 @@ server.start({ port: 8080, stateless: true, }, -}) +}); ``` > **Note:** Stateless mode is only available with HTTP streaming transport. Features that depend on persistent sessions (like session-specific state) will not be available in stateless mode. @@ -237,7 +239,7 @@ FastMCP uses the [Standard Schema](https://standardschema.dev) specification for **Zod Example:** ```typescript -import { z } from "zod" +import { z } from "zod"; server.addTool({ name: "fetch-zod", @@ -246,15 +248,15 @@ server.addTool({ url: z.string(), }), execute: async (args) => { - return await fetchWebpageContent(args.url) + return await fetchWebpageContent(args.url); }, -}) +}); ``` **ArkType Example:** ```typescript -import { type } from "arktype" +import { type } from "arktype"; server.addTool({ name: "fetch-arktype", @@ -263,9 +265,9 @@ server.addTool({ url: "string", }), execute: async (args) => { - return await fetchWebpageContent(args.url) + return await fetchWebpageContent(args.url); }, -}) +}); ``` **Valibot Example:** @@ -273,7 +275,7 @@ server.addTool({ Valibot requires the peer dependency @valibot/to-json-schema. ```typescript -import * as v from "valibot" +import * as v from "valibot"; server.addTool({ name: "fetch-valibot", @@ -282,9 +284,9 @@ server.addTool({ url: v.string(), }), execute: async (args) => { - return await fetchWebpageContent(args.url) + return await fetchWebpageContent(args.url); }, -}) +}); ``` #### Tools Without Parameters @@ -299,24 +301,24 @@ When creating tools that don't require parameters, you have two options: description: "Say hello", // No parameters property execute: async () => { - return "Hello, world!" + return "Hello, world!"; }, - }) + }); ``` 2. Explicitly define empty parameters: ```typescript - import { z } from "zod" + import { z } from "zod"; server.addTool({ name: "sayHello", description: "Say hello", parameters: z.object({}), // Empty object execute: async () => { - return "Hello, world!" + return "Hello, world!"; }, - }) + }); ``` > [!NOTE] @@ -333,7 +335,7 @@ server.addTool({ description: "An admin-only tool", canAccess: (auth) => auth?.role === "admin", execute: async () => "Welcome, admin!", -}) +}); ``` #### Returning a string @@ -348,9 +350,9 @@ server.addTool({ url: z.string(), }), execute: async (args) => { - return "Hello, world!" + return "Hello, world!"; }, -}) +}); ``` The latter is equivalent to: @@ -370,9 +372,9 @@ server.addTool({ text: "Hello, world!", }, ], - } + }; }, -}) +}); ``` #### Returning a list @@ -392,9 +394,9 @@ server.addTool({ { type: "text", text: "First message" }, { type: "text", text: "Second message" }, ], - } + }; }, -}) +}); ``` #### Returning an image @@ -402,7 +404,7 @@ server.addTool({ Use the `imageContent` to create a content object for an image: ```js -import { imageContent } from "fastmcp" +import { imageContent } from "fastmcp"; server.addTool({ name: "download", @@ -413,7 +415,7 @@ server.addTool({ execute: async (args) => { return imageContent({ url: "https://example.com/image.png", - }) + }); // or... // return imageContent({ @@ -432,7 +434,7 @@ server.addTool({ // ], // }; }, -}) +}); ``` The `imageContent` function takes the following options: @@ -461,9 +463,9 @@ server.addTool({ mimeType: "image/png", }, ], - } + }; }, -}) +}); ``` #### Configurable Ping Behavior @@ -482,7 +484,7 @@ const server = new FastMCP({ // Set log level for ping-related messages (default: 'debug') logLevel: "debug", }, -}) +}); ``` By default, ping behavior is optimized for each transport type: @@ -514,12 +516,12 @@ const server = new FastMCP({ // HTTP status code to return (default: 200) status: 200, }, -}) +}); await server.start({ transportType: "httpStream", httpStream: { port: 8080 }, -}) +}); ``` Now a request to `http://localhost:8080/healthz` will return: @@ -546,7 +548,7 @@ const server = new FastMCP({ enabled: false, // By default, roots support is enabled (true) }, -}) +}); ``` This provides the following benefits: @@ -560,16 +562,16 @@ You can listen for root changes in your server: ```ts server.on("connect", (event) => { - const session = event.session + const session = event.session; // Access the current roots - console.log("Initial roots:", session.roots) + console.log("Initial roots:", session.roots); // Listen for changes to the roots session.on("rootsChanged", (event) => { - console.log("Roots changed:", event.roots) - }) -}) + console.log("Roots changed:", event.roots); + }); +}); ``` When a client doesn't support roots or when roots functionality is explicitly disabled, these operations will gracefully handle the situation without throwing errors. @@ -579,7 +581,7 @@ When a client doesn't support roots or when roots functionality is explicitly di Use the `audioContent` to create a content object for an audio: ```js -import { audioContent } from "fastmcp" +import { audioContent } from "fastmcp"; server.addTool({ name: "download", @@ -590,7 +592,7 @@ server.addTool({ execute: async (args) => { return audioContent({ url: "https://example.com/audio.mp3", - }) + }); // or... // return audioContent({ @@ -609,7 +611,7 @@ server.addTool({ // ], // }; }, -}) +}); ``` The `audioContent` function takes the following options: @@ -638,9 +640,9 @@ server.addTool({ mimeType: "audio/mpeg", }, ], - } + }; }, -}) +}); ``` #### Return combination type @@ -672,7 +674,7 @@ server.addTool({ mimeType: "audio/mpeg", }, ], - } + }; }, // or... @@ -694,7 +696,7 @@ server.addTool({ // ], // }; // }, -}) +}); ``` #### Custom Logger @@ -702,27 +704,27 @@ server.addTool({ FastMCP allows you to provide a custom logger implementation to control how the server logs messages. This is useful for integrating with existing logging infrastructure or customizing log formatting. ```ts -import { FastMCP, Logger } from "fastmcp" +import { FastMCP, Logger } from "fastmcp"; class CustomLogger implements Logger { debug(...args: unknown[]): void { - console.log("[DEBUG]", new Date().toISOString(), ...args) + console.log("[DEBUG]", new Date().toISOString(), ...args); } error(...args: unknown[]): void { - console.error("[ERROR]", new Date().toISOString(), ...args) + console.error("[ERROR]", new Date().toISOString(), ...args); } info(...args: unknown[]): void { - console.info("[INFO]", new Date().toISOString(), ...args) + console.info("[INFO]", new Date().toISOString(), ...args); } log(...args: unknown[]): void { - console.log("[LOG]", new Date().toISOString(), ...args) + console.log("[LOG]", new Date().toISOString(), ...args); } warn(...args: unknown[]): void { - console.warn("[WARN]", new Date().toISOString(), ...args) + console.warn("[WARN]", new Date().toISOString(), ...args); } } @@ -730,7 +732,7 @@ const server = new FastMCP({ name: "My Server", version: "1.0.0", logger: new CustomLogger(), -}) +}); ``` See `src/examples/custom-logger.ts` for examples with Winston, Pino, and file-based logging. @@ -749,15 +751,15 @@ server.addTool({ execute: async (args, { log }) => { log.info("Downloading file...", { url, - }) + }); // ... - log.info("Downloaded file") + log.info("Downloaded file"); - return "done" + return "done"; }, -}) +}); ``` The `log` object has the following methods: @@ -776,7 +778,7 @@ FastMCP supports two ways to handle errors in tool execution: For standards-compliant error handling, throw `McpError` with appropriate error codes: ```js -import { ErrorCode, McpError } from "fastmcp" +import { ErrorCode, McpError } from "fastmcp"; server.addTool({ name: "download", @@ -787,7 +789,7 @@ server.addTool({ execute: async (args) => { if (args.url.startsWith("https://example.com")) { // Throw MCP error with InvalidParams code - throw new McpError(ErrorCode.InvalidParams, "This URL is not allowed") + throw new McpError(ErrorCode.InvalidParams, "This URL is not allowed"); } // Throw MCP error with custom data @@ -795,12 +797,12 @@ server.addTool({ throw new McpError(ErrorCode.InvalidRequest, "Resource not found", { url: args.url, statusCode: 404, - }) + }); } - return "done" + return "done"; }, -}) +}); ``` **Available Error Codes:** @@ -818,7 +820,7 @@ When a tool throws `McpError`, it's propagated through the MCP protocol as a pro For backward compatibility, you can still use `UserError` for simple error messages: ```js -import { UserError } from "fastmcp" +import { UserError } from "fastmcp"; server.addTool({ name: "download", @@ -828,12 +830,12 @@ server.addTool({ }), execute: async (args) => { if (args.url.startsWith("https://example.com")) { - throw new UserError("This URL is not allowed") + throw new UserError("This URL is not allowed"); } - return "done" + return "done"; }, -}) +}); ``` `UserError` errors are converted to tool responses with `isError: true` and are displayed to the user as text content. @@ -853,18 +855,18 @@ server.addTool({ await reportProgress({ progress: 0, total: 100, - }) + }); // ... await reportProgress({ progress: 100, total: 100, - }) + }); - return "done" + return "done"; }, -}) +}); ``` #### Streaming Output @@ -890,13 +892,13 @@ server.addTool({ }, execute: async (args, { streamContent }) => { // Send initial content immediately - await streamContent({ type: "text", text: "Starting generation...\n" }) + await streamContent({ type: "text", text: "Starting generation...\n" }); // Simulate incremental content generation - const words = "The quick brown fox jumps over the lazy dog.".split(" ") + const words = "The quick brown fox jumps over the lazy dog.".split(" "); for (const word of words) { - await streamContent({ type: "text", text: word + " " }) - await new Promise((resolve) => setTimeout(resolve, 300)) // Simulate delay + await streamContent({ type: "text", text: word + " " }); + await new Promise((resolve) => setTimeout(resolve, 300)); // Simulate delay } // When using streamContent, you can: @@ -904,12 +906,12 @@ server.addTool({ // 2. Return a final result (which will be appended to streamed content) // Option 1: All content was streamed, so return void - return + return; // Option 2: Return final content that will be appended // return "Generation complete!"; }, -}) +}); ``` Streaming works with all content types (text, image, audio) and can be combined with progress reporting: @@ -925,26 +927,26 @@ server.addTool({ streamingHint: true, }, execute: async (args, { streamContent, reportProgress }) => { - const total = args.datasetSize + const total = args.datasetSize; for (let i = 0; i < total; i++) { // Report numeric progress - await reportProgress({ progress: i, total }) + await reportProgress({ progress: i, total }); // Stream intermediate results if (i % 10 === 0) { await streamContent({ type: "text", text: `Processed ${i} of ${total} items...\n`, - }) + }); } - await new Promise((resolve) => setTimeout(resolve, 50)) + await new Promise((resolve) => setTimeout(resolve, 50)); } - return "Processing complete!" + return "Processing complete!"; }, -}) +}); ``` #### Tool Annotations @@ -964,9 +966,9 @@ server.addTool({ openWorldHint: true, // Tool interacts with external entities }, execute: async (args) => { - return await fetchWebpageContent(args.url) + return await fetchWebpageContent(args.url); }, -}) +}); ``` The available annotations are: @@ -1000,9 +1002,9 @@ server.addResource({ async load() { return { text: await readLogFile(), - } + }; }, -}) +}); ``` > [!NOTE] @@ -1051,9 +1053,9 @@ server.addResourceTemplate({ async load({ name }) { return { text: `Example log content for ${name}`, - } + }; }, -}) +}); ``` #### Resource template argument auto-completion @@ -1074,21 +1076,21 @@ server.addResourceTemplate({ if (value === "Example") { return { values: ["Example Log"], - } + }; } return { values: [], - } + }; }, }, ], async load({ name }) { return { text: `Example log content for ${name}`, - } + }; }, -}) +}); ``` ### Embedded Resources @@ -1112,9 +1114,9 @@ server.addTool({ resource: await server.embedded(`user://profile/${args.userId}`), }, ], - } + }; }, -}) +}); ``` #### Working with Resource Templates @@ -1137,12 +1139,12 @@ server.addResourceTemplate({ const docs = { "getting-started": "# Getting Started\n\nWelcome to our project!", "api-reference": "# API Reference\n\nAuthentication is required.", - } + }; return { text: docs[args.section] || "Documentation not found", - } + }; }, -}) +}); // Use embedded resources in a tool server.addTool({ @@ -1159,9 +1161,9 @@ server.addTool({ resource: await server.embedded(`docs://project/${args.section}`), }, ], - } + }; }, -}) +}); ``` #### Working with Direct Resources @@ -1177,9 +1179,9 @@ server.addResource({ async load() { return { text: "System operational", - } + }; }, -}) +}); // Use in a tool server.addTool({ @@ -1194,9 +1196,9 @@ server.addTool({ resource: await server.embedded("system://status"), }, ], - } + }; }, -}) +}); ``` ### Prompts @@ -1215,9 +1217,9 @@ server.addPrompt({ }, ], load: async (args) => { - return `Generate a concise but descriptive commit message for these changes:\n\n${args.changes}` + return `Generate a concise but descriptive commit message for these changes:\n\n${args.changes}`; }, -}) +}); ``` #### Prompt argument auto-completion @@ -1229,7 +1231,7 @@ server.addPrompt({ name: "countryPoem", description: "Writes a poem about a country", load: async ({ name }) => { - return `Hello, ${name}!` + return `Hello, ${name}!`; }, arguments: [ { @@ -1240,16 +1242,16 @@ server.addPrompt({ if (value === "Germ") { return { values: ["Germany"], - } + }; } return { values: [], - } + }; }, }, ], -}) +}); ``` #### Prompt argument auto-completion using `enum` @@ -1261,7 +1263,7 @@ server.addPrompt({ name: "countryPoem", description: "Writes a poem about a country", load: async ({ name }) => { - return `Hello, ${name}!` + return `Hello, ${name}!`; }, arguments: [ { @@ -1271,7 +1273,7 @@ server.addPrompt({ enum: ["Germany", "France", "Italy"], }, ], -}) +}); ``` ### Authentication @@ -1290,21 +1292,21 @@ const server = new FastMCP({ name: "My Server", version: "1.0.0", authenticate: (request) => { - const apiKey = request.headers["x-api-key"] + const apiKey = request.headers["x-api-key"]; if (apiKey !== "123") { throw new Response(null, { status: 401, statusText: "Unauthorized", - }) + }); } // Whatever you return here will be accessible in the `context.session` object. return { id: 1, - } + }; }, -}) +}); ``` Now you can access the authenticated session data in your tools: @@ -1313,9 +1315,9 @@ Now you can access the authenticated session data in your tools: server.addTool({ name: "sayHello", execute: async (args, { session }) => { - return `Hello, ${session.id}!` + return `Hello, ${session.id}!`; }, -}) +}); ``` #### Tool Authorization @@ -1329,12 +1331,12 @@ If `canAccess` is not provided, the tool is accessible to all authenticated user ```typescript const server = new FastMCP<{ role: "admin" | "user" }>({ authenticate: async (request) => { - const role = request.headers["x-role"] as string - return { role: role === "admin" ? "admin" : "user" } + const role = request.headers["x-role"] as string; + return { role: role === "admin" ? "admin" : "user" }; }, name: "My Server", version: "1.0.0", -}) +}); server.addTool({ name: "admin-dashboard", @@ -1342,17 +1344,17 @@ server.addTool({ // Only users with the 'admin' role can see and execute this tool canAccess: (auth) => auth?.role === "admin", execute: async () => { - return "Welcome to the admin dashboard!" + return "Welcome to the admin dashboard!"; }, -}) +}); server.addTool({ name: "public-info", description: "A tool available to everyone", execute: async () => { - return "This is public information." + return "This is public information."; }, -}) +}); ``` In this example, only clients authenticating with the `admin` role will be able to list or call the `admin-dashboard` tool. The `public-info` tool will be available to all authenticated users. @@ -1362,9 +1364,9 @@ In this example, only clients authenticating with the `admin` role will be able FastMCP includes built-in support for OAuth discovery endpoints, supporting both **MCP Specification 2025-03-26** and **MCP Specification 2025-06-18** for OAuth integration. This makes it easy to integrate with OAuth authorization flows by providing standard discovery endpoints that comply with RFC 8414 (OAuth 2.0 Authorization Server Metadata) and RFC 9470 (OAuth 2.0 Protected Resource Metadata): ```ts -import fastJwt from "fast-jwt" -import { FastMCP } from "fastmcp" -import { buildGetJwks } from "get-jwks" +import fastJwt from "fast-jwt"; +import { FastMCP } from "fastmcp"; +import { buildGetJwks } from "get-jwks"; const server = new FastMCP({ name: "My Server", @@ -1384,73 +1386,74 @@ const server = new FastMCP({ }, }, authenticate: async (request) => { - const authHeader = request.headers.authorization + const authHeader = request.headers.authorization; if (!authHeader?.startsWith("Bearer ")) { throw new Response(null, { status: 401, statusText: "Missing or invalid authorization header", - }) + }); } - const token = authHeader.slice(7) // Remove 'Bearer ' prefix + const token = authHeader.slice(7); // Remove 'Bearer ' prefix // Validate OAuth JWT access token using OpenID Connect discovery try { // TODO: Cache the discovery document to avoid repeated requests // Discover OAuth/OpenID configuration from well-known endpoint - const discoveryUrl = "https://auth.example.com/.well-known/openid-configuration" + const discoveryUrl = + "https://auth.example.com/.well-known/openid-configuration"; // Alternative: Use OAuth authorization server metadata endpoint // const discoveryUrl = 'https://auth.example.com/.well-known/oauth-authorization-server'; - const discoveryResponse = await fetch(discoveryUrl) + const discoveryResponse = await fetch(discoveryUrl); if (!discoveryResponse.ok) { - throw new Error("Failed to fetch OAuth discovery document") + throw new Error("Failed to fetch OAuth discovery document"); } - const config = await discoveryResponse.json() - const jwksUri = config.jwks_uri - const issuer = config.issuer + const config = await discoveryResponse.json(); + const jwksUri = config.jwks_uri; + const issuer = config.issuer; // Create JWKS client for token verification using discovered endpoint const getJwks = buildGetJwks({ jwksUrl: jwksUri, cache: true, rateLimit: true, - }) + }); // Create JWT verifier with JWKS and discovered issuer const verify = fastJwt.createVerifier({ key: async (token) => { - const { header } = fastJwt.decode(token, { complete: true }) + const { header } = fastJwt.decode(token, { complete: true }); const jwk = await getJwks.getJwk({ kid: header.kid, alg: header.alg, - }) - return jwk + }); + return jwk; }, algorithms: ["RS256", "ES256"], issuer: issuer, audience: "mcp://my-server", - }) + }); // Verify the JWT token - const payload = await verify(token) + const payload = await verify(token); return { userId: payload.sub, scope: payload.scope, email: payload.email, // Include other claims as needed - } + }; } catch (error) { throw new Response(null, { status: 401, statusText: "Invalid OAuth token", - }) + }); } }, -}) +}); ``` This configuration automatically exposes OAuth discovery endpoints: @@ -1465,14 +1468,14 @@ For JWT token validation, you can use libraries like [`get-jwks`](https://github If you are exposing your MCP server via HTTP, you may wish to allow clients to supply sensitive keys via headers, which can then be passed along to APIs that your tools interact with, allowing each client to supply their own API keys. This can be done by capturing the HTTP headers in the `authenticate` section and storing them in the session to be referenced by the tools later. ```ts -import { IncomingHttpHeaders } from "http" +import { IncomingHttpHeaders } from "http"; -import { FastMCP } from "fastmcp" +import { FastMCP } from "fastmcp"; // Define the session data type interface SessionData { - headers: IncomingHttpHeaders - [key: string]: unknown // Add index signature to satisfy Record + headers: IncomingHttpHeaders; + [key: string]: unknown; // Add index signature to satisfy Record } // Create a server instance @@ -1483,26 +1486,26 @@ const server = new FastMCP({ // Authentication logic return { headers: request.headers, - } + }; }, -}) +}); // Tool to display HTTP headers server.addTool({ name: "headerTool", description: "Reads HTTP headers from the request", execute: async (args: any, context: any) => { - const session = context.session as SessionData - const headers = session?.headers ?? {} + const session = context.session as SessionData; + const headers = session?.headers ?? {}; const getHeaderString = (header: string | string[] | undefined) => - Array.isArray(header) ? header.join(", ") : (header ?? "N/A") + Array.isArray(header) ? header.join(", ") : (header ?? "N/A"); - const userAgent = getHeaderString(headers["user-agent"]) - const authorization = getHeaderString(headers["authorization"]) - return `User-Agent: ${userAgent}\nAuthorization: ${authorization}\nAll Headers: ${JSON.stringify(headers, null, 2)}` + const userAgent = getHeaderString(headers["user-agent"]); + const authorization = getHeaderString(headers["authorization"]); + return `User-Agent: ${userAgent}\nAuthorization: ${authorization}\nAll Headers: ${JSON.stringify(headers, null, 2)}`; }, -}) +}); // Start the server server.start({ @@ -1510,30 +1513,33 @@ server.start({ httpStream: { port: 8080, }, -}) +}); ``` A client that would connect to this may look something like this: ```ts -import { Client } from "@modelcontextprotocol/sdk/client/index.js" -import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js" +import { Client } from "@modelcontextprotocol/sdk/client/index.js"; +import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; -const transport = new StreamableHTTPClientTransport(new URL(`http://localhost:8080/mcp`), { - requestInit: { - headers: { - Authorization: "Test 123", +const transport = new StreamableHTTPClientTransport( + new URL(`http://localhost:8080/mcp`), + { + requestInit: { + headers: { + Authorization: "Test 123", + }, }, }, -}) +); const client = new Client({ name: "example-client", version: "1.0.0", -}) +}); -;(async () => { - await client.connect(transport) +(async () => { + await client.connect(transport); // Call a tool const result = await client.callTool({ @@ -1541,10 +1547,10 @@ const client = new Client({ arguments: { arg1: "value", }, - }) + }); - console.log("Tool result:", result) -})().catch(console.error) + console.log("Tool result:", result); +})().catch(console.error); ``` What would show up in the console after the client runs is something like this: @@ -1591,16 +1597,16 @@ FastMCP automatically exposes session and request IDs to tool handlers through t - Useful for request tracing and debugging ```ts -import { FastMCP } from "fastmcp" -import { z } from "zod" +import { FastMCP } from "fastmcp"; +import { z } from "zod"; const server = new FastMCP({ name: "Session Counter Server", version: "1.0.0", -}) +}); // Per-session counter storage -const sessionCounters = new Map() +const sessionCounters = new Map(); server.addTool({ name: "increment_counter", @@ -1608,16 +1614,16 @@ server.addTool({ parameters: z.object({}), execute: async (args, context) => { if (!context.sessionId) { - return "Session ID not available (requires HTTP transport)" + return "Session ID not available (requires HTTP transport)"; } - const counter = sessionCounters.get(context.sessionId) || 0 - const newCounter = counter + 1 - sessionCounters.set(context.sessionId, newCounter) + const counter = sessionCounters.get(context.sessionId) || 0; + const newCounter = counter + 1; + sessionCounters.set(context.sessionId, newCounter); - return `Counter for session ${context.sessionId}: ${newCounter}` + return `Counter for session ${context.sessionId}: ${newCounter}`; }, -}) +}); server.addTool({ name: "show_ids", @@ -1625,16 +1631,16 @@ server.addTool({ parameters: z.object({}), execute: async (args, context) => { return `Session ID: ${context.sessionId || "N/A"} -Request ID: ${context.requestId || "N/A"}` +Request ID: ${context.requestId || "N/A"}`; }, -}) +}); server.start({ transportType: "httpStream", httpStream: { port: 8080, }, -}) +}); ``` **Use Cases:** @@ -1665,7 +1671,7 @@ const server = new FastMCP({ version: "1.0.0", instructions: 'Instructions describing how to use the server and its features.\n\nThis can be used by clients to improve the LLM\'s understanding of available tools, resources, etc. It can be thought of like a "hint" to the model. For example, this information MAY be added to the system prompt.', -}) +}); ``` ### Sessions @@ -1673,7 +1679,7 @@ const server = new FastMCP({ The `session` object is an instance of `FastMCPSession` and it describes active client sessions. ```ts -server.sessions +server.sessions; ``` We allocate a new server instance for each client connection to enable 1:1 communication between a client and the server. @@ -1684,12 +1690,12 @@ You can listen to events emitted by the server using the `on` method: ```ts server.on("connect", (event) => { - console.log("Client connected:", event.session) -}) + console.log("Client connected:", event.session); +}); server.on("disconnect", (event) => { - console.log("Client disconnected:", event.session) -}) + console.log("Client disconnected:", event.session); +}); ``` ## `FastMCPSession` @@ -1716,7 +1722,7 @@ await session.requestSampling({ systemPrompt: "You are a helpful file system assistant.", includeContext: "thisServer", maxTokens: 100, -}) +}); ``` #### Options @@ -1742,7 +1748,7 @@ await session.requestSampling( { // Progress callback - called when progress notifications are received onprogress: (progress) => { - console.log(`Progress: ${progress.progress}/${progress.total}`) + console.log(`Progress: ${progress.progress}/${progress.total}`); }, // Abort signal for cancelling the request @@ -1757,7 +1763,7 @@ await session.requestSampling( // Maximum total timeout regardless of progress (no default) maxTotalTimeout: 60000, }, -) +); ``` **Options:** @@ -1773,7 +1779,7 @@ await session.requestSampling( The `clientCapabilities` property contains the client capabilities. ```ts -session.clientCapabilities +session.clientCapabilities; ``` ### `loggingLevel` @@ -1781,7 +1787,7 @@ session.clientCapabilities The `loggingLevel` property describes the logging level as set by the client. ```ts -session.loggingLevel +session.loggingLevel; ``` ### `roots` @@ -1789,7 +1795,7 @@ session.loggingLevel The `roots` property contains the roots as set by the client. ```ts -session.roots +session.roots; ``` ### `server` @@ -1797,7 +1803,7 @@ session.roots The `server` property contains an instance of MCP server that is associated with the session. ```ts -session.server +session.server; ``` ### Typed session events @@ -1806,12 +1812,12 @@ You can listen to events emitted by the session using the `on` method: ```ts session.on("rootsChanged", (event) => { - console.log("Roots changed:", event.roots) -}) + console.log("Roots changed:", event.roots); +}); session.on("error", (event) => { - console.error("Error:", event.error) -}) + console.error("Error:", event.error); +}); ``` ## Running Your Server diff --git a/eslint.config.ts b/eslint.config.ts index f08d128..421bce1 100644 --- a/eslint.config.ts +++ b/eslint.config.ts @@ -1,7 +1,7 @@ -import eslint from "@eslint/js" -import eslintConfigPrettier from "eslint-config-prettier/flat" -import perfectionist from "eslint-plugin-perfectionist" -import tseslint from "typescript-eslint" +import eslint from "@eslint/js"; +import eslintConfigPrettier from "eslint-config-prettier/flat"; +import perfectionist from "eslint-plugin-perfectionist"; +import tseslint from "typescript-eslint"; export default tseslint.config( eslint.configs.recommended, @@ -11,4 +11,4 @@ export default tseslint.config( { ignores: ["**/*.js", "dist/**"], }, -) +); diff --git a/src/FastMCP.oauth.test.ts b/src/FastMCP.oauth.test.ts index 2db8bd7..589132f 100644 --- a/src/FastMCP.oauth.test.ts +++ b/src/FastMCP.oauth.test.ts @@ -1,11 +1,11 @@ -import { getRandomPort } from "get-port-please" -import { describe, expect, it } from "vitest" +import { getRandomPort } from "get-port-please"; +import { describe, expect, it } from "vitest"; -import { FastMCP } from "./FastMCP.js" +import { FastMCP } from "./FastMCP.js"; describe("FastMCP OAuth Support", () => { it("should serve OAuth authorization server metadata", async () => { - const port = await getRandomPort() + const port = await getRandomPort(); const server = new FastMCP({ name: "Test Server", @@ -23,37 +23,51 @@ describe("FastMCP OAuth Support", () => { enabled: true, }, version: "1.0.0", - }) + }); await server.start({ httpStream: { port }, transportType: "httpStream", - }) + }); try { // Test the OAuth authorization server endpoint - const response = await fetch(`http://localhost:${port}/.well-known/oauth-authorization-server`) - expect(response.status).toBe(200) - expect(response.headers.get("content-type")).toBe("application/json") + const response = await fetch( + `http://localhost:${port}/.well-known/oauth-authorization-server`, + ); + expect(response.status).toBe(200); + expect(response.headers.get("content-type")).toBe("application/json"); - const metadata = (await response.json()) as Record + const metadata = (await response.json()) as Record; // Check that camelCase was converted to snake_case - expect(metadata.issuer).toBe("https://auth.example.com") - expect(metadata.authorization_endpoint).toBe("https://auth.example.com/oauth/authorize") - expect(metadata.token_endpoint).toBe("https://auth.example.com/oauth/token") - expect(metadata.response_types_supported).toEqual(["code"]) - expect(metadata.jwks_uri).toBe("https://auth.example.com/.well-known/jwks.json") - expect(metadata.scopes_supported).toEqual(["read", "write"]) - expect(metadata.grant_types_supported).toEqual(["authorization_code", "refresh_token"]) - expect(metadata.dpop_signing_alg_values_supported).toEqual(["ES256", "RS256"]) + expect(metadata.issuer).toBe("https://auth.example.com"); + expect(metadata.authorization_endpoint).toBe( + "https://auth.example.com/oauth/authorize", + ); + expect(metadata.token_endpoint).toBe( + "https://auth.example.com/oauth/token", + ); + expect(metadata.response_types_supported).toEqual(["code"]); + expect(metadata.jwks_uri).toBe( + "https://auth.example.com/.well-known/jwks.json", + ); + expect(metadata.scopes_supported).toEqual(["read", "write"]); + expect(metadata.grant_types_supported).toEqual([ + "authorization_code", + "refresh_token", + ]); + expect(metadata.dpop_signing_alg_values_supported).toEqual([ + "ES256", + "RS256", + ]); } finally { - await server.stop() + await server.stop(); } - }) + }); it("should serve OAuth protected resource metadata", async () => { - const port = await getRandomPort() + const port = await getRandomPort(); const server = new FastMCP({ name: "Test Server", @@ -85,53 +99,72 @@ describe("FastMCP OAuth Support", () => { }, }, version: "1.0.0", - }) + }); await server.start({ httpStream: { port }, transportType: "httpStream", - }) + }); try { - const response = await fetch(`http://localhost:${port}/.well-known/oauth-protected-resource`) - expect(response.status).toBe(200) - expect(response.headers.get("content-type")).toBe("application/json") + const response = await fetch( + `http://localhost:${port}/.well-known/oauth-protected-resource`, + ); + expect(response.status).toBe(200); + expect(response.headers.get("content-type")).toBe("application/json"); - const metadata = (await response.json()) as Record + const metadata = (await response.json()) as Record; // Check that camelCase was converted to snake_case - expect(metadata.resource).toBe("mcp://test-server") - expect(metadata.authorization_servers).toEqual(["https://auth.example.com"]) - expect(metadata.jwks_uri).toBe("https://test-server.example.com/.well-known/jwks.json") - expect(metadata.bearer_methods_supported).toEqual(["header"]) - expect(metadata.resource_documentation).toBe("https://docs.example.com/api") + expect(metadata.resource).toBe("mcp://test-server"); + expect(metadata.authorization_servers).toEqual([ + "https://auth.example.com", + ]); + expect(metadata.jwks_uri).toBe( + "https://test-server.example.com/.well-known/jwks.json", + ); + expect(metadata.bearer_methods_supported).toEqual(["header"]); + expect(metadata.resource_documentation).toBe( + "https://docs.example.com/api", + ); // New fields added for RFC 9728 compliance - expect(metadata.authorization_details_types_supported).toEqual(["payment_initiation"]) - expect(metadata.dpop_bound_access_tokens_required).toBe(true) - expect(metadata.dpop_signing_alg_values_supported).toEqual(["ES256", "RS256"]) - expect(metadata.resource_name).toBe("Test API") - expect(metadata.resource_policy_uri).toBe("https://test-server.example.com/policy") - expect(metadata.resource_signing_alg_values_supported).toEqual(["RS256"]) - expect(metadata.resource_tos_uri).toBe("https://test-server.example.com/tos") - expect(metadata.scopes_supported).toEqual(["read", "write", "admin"]) - expect(metadata.service_documentation).toBe("https://developer.example.com/api") - expect(metadata.tls_client_certificate_bound_access_tokens).toBe(false) + expect(metadata.authorization_details_types_supported).toEqual([ + "payment_initiation", + ]); + expect(metadata.dpop_bound_access_tokens_required).toBe(true); + expect(metadata.dpop_signing_alg_values_supported).toEqual([ + "ES256", + "RS256", + ]); + expect(metadata.resource_name).toBe("Test API"); + expect(metadata.resource_policy_uri).toBe( + "https://test-server.example.com/policy", + ); + expect(metadata.resource_signing_alg_values_supported).toEqual(["RS256"]); + expect(metadata.resource_tos_uri).toBe( + "https://test-server.example.com/tos", + ); + expect(metadata.scopes_supported).toEqual(["read", "write", "admin"]); + expect(metadata.service_documentation).toBe( + "https://developer.example.com/api", + ); + expect(metadata.tls_client_certificate_bound_access_tokens).toBe(false); // Vendor extensions (dynamic properties) - expect(metadata.vendor_prefix_custom_field).toBe("custom value") + expect(metadata.vendor_prefix_custom_field).toBe("custom value"); expect(metadata.vendor_prefix_complex_object).toEqual({ nestedArray: [1, 2, 3], nestedProperty: "nested value", - }) - expect(metadata.x_api_version).toBe("2.0") + }); + expect(metadata.x_api_version).toBe("2.0"); } finally { - await server.stop() + await server.stop(); } - }) + }); it("should return 404 for OAuth endpoints when disabled", async () => { - const port = await getRandomPort() + const port = await getRandomPort(); const server = new FastMCP({ name: "Test Server", @@ -139,46 +172,54 @@ describe("FastMCP OAuth Support", () => { enabled: false, }, version: "1.0.0", - }) + }); await server.start({ httpStream: { port }, transportType: "httpStream", - }) + }); try { - const authServerResponse = await fetch(`http://localhost:${port}/.well-known/oauth-authorization-server`) - expect(authServerResponse.status).toBe(404) - - const protectedResourceResponse = await fetch(`http://localhost:${port}/.well-known/oauth-protected-resource`) - expect(protectedResourceResponse.status).toBe(404) + const authServerResponse = await fetch( + `http://localhost:${port}/.well-known/oauth-authorization-server`, + ); + expect(authServerResponse.status).toBe(404); + + const protectedResourceResponse = await fetch( + `http://localhost:${port}/.well-known/oauth-protected-resource`, + ); + expect(protectedResourceResponse.status).toBe(404); } finally { - await server.stop() + await server.stop(); } - }) + }); it("should return 404 for OAuth endpoints when not configured", async () => { - const port = await getRandomPort() + const port = await getRandomPort(); const server = new FastMCP({ name: "Test Server", version: "1.0.0", // No oauth configuration - }) + }); await server.start({ httpStream: { port }, transportType: "httpStream", - }) + }); try { - const authServerResponse = await fetch(`http://localhost:${port}/.well-known/oauth-authorization-server`) - expect(authServerResponse.status).toBe(404) - - const protectedResourceResponse = await fetch(`http://localhost:${port}/.well-known/oauth-protected-resource`) - expect(protectedResourceResponse.status).toBe(404) + const authServerResponse = await fetch( + `http://localhost:${port}/.well-known/oauth-authorization-server`, + ); + expect(authServerResponse.status).toBe(404); + + const protectedResourceResponse = await fetch( + `http://localhost:${port}/.well-known/oauth-protected-resource`, + ); + expect(protectedResourceResponse.status).toBe(404); } finally { - await server.stop() + await server.stop(); } - }) -}) + }); +}); diff --git a/src/FastMCP.session-context.test.ts b/src/FastMCP.session-context.test.ts index 1d1a325..53497a8 100644 --- a/src/FastMCP.session-context.test.ts +++ b/src/FastMCP.session-context.test.ts @@ -1,43 +1,43 @@ -import { describe, expect, it, vi } from "vitest" -import { z } from "zod" +import { describe, expect, it, vi } from "vitest"; +import { z } from "zod"; -import { FastMCP } from "./FastMCP.js" +import { FastMCP } from "./FastMCP.js"; interface TestAuth { - [key: string]: unknown // Required for FastMCPSessionAuth compatibility - role: "admin" | "user" - userId: string + [key: string]: unknown; // Required for FastMCPSessionAuth compatibility + role: "admin" | "user"; + userId: string; } describe("FastMCP Session Context", () => { describe("stdio transport", () => { it("should pass session context to tool execution when authenticate is provided", async () => { - const mockAuth: TestAuth = { role: "admin", userId: "test-user" } + const mockAuth: TestAuth = { role: "admin", userId: "test-user" }; const server = new FastMCP({ authenticate: async (request) => { - if (!request) return mockAuth + if (!request) return mockAuth; - throw new Error("Unexpected request in test") + throw new Error("Unexpected request in test"); }, name: "test-server", version: "1.0.0", - }) + }); server.addTool({ description: "Test tool to verify session context", execute: async (_args, context) => { - return `Session received: ${context.session ? "yes" : "no"}` + return `Session received: ${context.session ? "yes" : "no"}`; }, name: "test-session-context", parameters: z.object({ message: z.string(), }), - }) + }); - await server.start({ transportType: "stdio" }) + await server.start({ transportType: "stdio" }); - expect(server).toBeDefined() - }) + expect(server).toBeDefined(); + }); it("should handle authentication errors gracefully in stdio transport", async () => { const mockLogger = { @@ -46,57 +46,57 @@ describe("FastMCP Session Context", () => { info: vi.fn(), log: vi.fn(), warn: vi.fn(), - } + }; const server = new FastMCP({ authenticate: async () => { - throw new Error("Auth failed") + throw new Error("Auth failed"); }, logger: mockLogger, name: "test-server", version: "1.0.0", - }) + }); server.addTool({ description: "Test tool", execute: async (_args, context) => { - return `Session: ${context.session ? "present" : "undefined"}` + return `Session: ${context.session ? "present" : "undefined"}`; }, name: "test-tool", - }) + }); - await server.start({ transportType: "stdio" }) + await server.start({ transportType: "stdio" }); expect(mockLogger.error).toHaveBeenCalledWith( "[FastMCP error] Authentication failed for stdio transport:", "Auth failed", - ) - }) + ); + }); it("should work without authenticate function", async () => { const server = new FastMCP({ name: "test-server", version: "1.0.0", - }) + }); server.addTool({ description: "Test tool without auth", execute: async (_args, context) => { - return `Session: ${context.session ? "present" : "undefined"}` + return `Session: ${context.session ? "present" : "undefined"}`; }, name: "test-tool", - }) + }); - await server.start({ transportType: "stdio" }) + await server.start({ transportType: "stdio" }); - expect(server).toBeDefined() - }) - }) + expect(server).toBeDefined(); + }); + }); describe("environment variable based authentication", () => { it("should support reading from environment variables in stdio mode", async () => { - const originalEnv = process.env.TEST_USER_ID + const originalEnv = process.env.TEST_USER_ID; - process.env.TEST_USER_ID = "env-user-123" + process.env.TEST_USER_ID = "env-user-123"; try { const server = new FastMCP({ @@ -105,32 +105,32 @@ describe("FastMCP Session Context", () => { return { role: "user" as const, userId: process.env.TEST_USER_ID || "default-user", - } + }; } - throw new Error("HTTP not supported in this test") + throw new Error("HTTP not supported in this test"); }, name: "test-server", version: "1.0.0", - }) + }); server.addTool({ description: "Tool using env-based auth", execute: async (_args, context) => { - return `Environment user: ${context.session?.userId}` + return `Environment user: ${context.session?.userId}`; }, name: "env-test-tool", - }) + }); - await server.start({ transportType: "stdio" }) + await server.start({ transportType: "stdio" }); - expect(server).toBeDefined() + expect(server).toBeDefined(); } finally { if (originalEnv !== undefined) { - process.env.TEST_USER_ID = originalEnv + process.env.TEST_USER_ID = originalEnv; } else { - delete process.env.TEST_USER_ID + delete process.env.TEST_USER_ID; } } - }) - }) -}) + }); + }); +}); diff --git a/src/FastMCP.session-id.test.ts b/src/FastMCP.session-id.test.ts index 8803df4..392d437 100644 --- a/src/FastMCP.session-id.test.ts +++ b/src/FastMCP.session-id.test.ts @@ -1,13 +1,13 @@ -import { Client } from "@modelcontextprotocol/sdk/client/index.js" -import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js" -import { describe, expect, it } from "vitest" -import { z } from "zod" +import { Client } from "@modelcontextprotocol/sdk/client/index.js"; +import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; +import { describe, expect, it } from "vitest"; +import { z } from "zod"; -import { FastMCP } from "./FastMCP.js" +import { FastMCP } from "./FastMCP.js"; interface TestAuth { - [key: string]: unknown - userId: string + [key: string]: unknown; + userId: string; } describe("FastMCP Session ID Support", () => { @@ -19,33 +19,35 @@ describe("FastMCP Session ID Support", () => { }), name: "test-server", version: "1.0.0", - }) + }); - let capturedSessionId: string | undefined - let capturedRequestId: string | undefined + let capturedSessionId: string | undefined; + let capturedRequestId: string | undefined; server.addTool({ description: "Test tool that captures session and request IDs", execute: async (_args, context) => { - capturedSessionId = context.sessionId - capturedRequestId = context.requestId - return `Session ID: ${context.sessionId || "none"}, Request ID: ${context.requestId || "none"}` + capturedSessionId = context.sessionId; + capturedRequestId = context.requestId; + return `Session ID: ${context.sessionId || "none"}, Request ID: ${context.requestId || "none"}`; }, name: "capture-ids", parameters: z.object({}), - }) + }); - const port = 3000 + Math.floor(Math.random() * 1000) + const port = 3000 + Math.floor(Math.random() * 1000); await server.start({ httpStream: { port, }, transportType: "httpStream", - }) + }); try { - const transport = new StreamableHTTPClientTransport(new URL(`http://localhost:${port}/mcp`)) + const transport = new StreamableHTTPClientTransport( + new URL(`http://localhost:${port}/mcp`), + ); const client = new Client( { @@ -55,31 +57,31 @@ describe("FastMCP Session ID Support", () => { { capabilities: {}, }, - ) + ); - await client.connect(transport) + await client.connect(transport); const result = await client.callTool({ arguments: {}, name: "capture-ids", - }) + }); - expect(result).toBeDefined() - expect(capturedSessionId).toBeDefined() - expect(typeof capturedSessionId).toBe("string") - expect(capturedSessionId).toMatch(/^[0-9a-f-]+$/) // UUID format + expect(result).toBeDefined(); + expect(capturedSessionId).toBeDefined(); + expect(typeof capturedSessionId).toBe("string"); + expect(capturedSessionId).toMatch(/^[0-9a-f-]+$/); // UUID format // Request ID may or may not be provided by the client // If provided, it should be a string if (capturedRequestId !== undefined) { - expect(typeof capturedRequestId).toBe("string") + expect(typeof capturedRequestId).toBe("string"); } - await client.close() + await client.close(); } finally { - await server.stop() + await server.stop(); } - }) + }); it("should maintain the same sessionId across multiple requests", async () => { const server = new FastMCP({ @@ -88,31 +90,33 @@ describe("FastMCP Session ID Support", () => { }), name: "test-server", version: "1.0.0", - }) + }); - const capturedSessionIds: (string | undefined)[] = [] + const capturedSessionIds: (string | undefined)[] = []; server.addTool({ description: "Test tool that captures session ID", execute: async (_args, context) => { - capturedSessionIds.push(context.sessionId) - return `Session ID: ${context.sessionId}` + capturedSessionIds.push(context.sessionId); + return `Session ID: ${context.sessionId}`; }, name: "capture-session", parameters: z.object({}), - }) + }); - const port = 3000 + Math.floor(Math.random() * 1000) + const port = 3000 + Math.floor(Math.random() * 1000); await server.start({ httpStream: { port, }, transportType: "httpStream", - }) + }); try { - const transport = new StreamableHTTPClientTransport(new URL(`http://localhost:${port}/mcp`)) + const transport = new StreamableHTTPClientTransport( + new URL(`http://localhost:${port}/mcp`), + ); const client = new Client( { @@ -122,37 +126,37 @@ describe("FastMCP Session ID Support", () => { { capabilities: {}, }, - ) + ); - await client.connect(transport) + await client.connect(transport); // Make multiple requests await client.callTool({ arguments: {}, name: "capture-session", - }) + }); await client.callTool({ arguments: {}, name: "capture-session", - }) + }); await client.callTool({ arguments: {}, name: "capture-session", - }) + }); // All requests should have the same session ID - expect(capturedSessionIds).toHaveLength(3) - expect(capturedSessionIds[0]).toBeDefined() - expect(capturedSessionIds[0]).toBe(capturedSessionIds[1]) - expect(capturedSessionIds[1]).toBe(capturedSessionIds[2]) + expect(capturedSessionIds).toHaveLength(3); + expect(capturedSessionIds[0]).toBeDefined(); + expect(capturedSessionIds[0]).toBe(capturedSessionIds[1]); + expect(capturedSessionIds[1]).toBe(capturedSessionIds[2]); - await client.close() + await client.close(); } finally { - await server.stop() + await server.stop(); } - }) + }); it("should support per-session state management using sessionId", async () => { const server = new FastMCP({ @@ -161,40 +165,42 @@ describe("FastMCP Session ID Support", () => { }), name: "test-server", version: "1.0.0", - }) + }); // Per-session counter storage - const sessionCounters = new Map() + const sessionCounters = new Map(); server.addTool({ description: "Increment a per-session counter", execute: async (_args, context) => { if (!context.sessionId) { - return "No session ID available" + return "No session ID available"; } - const currentCount = sessionCounters.get(context.sessionId) || 0 - const newCount = currentCount + 1 - sessionCounters.set(context.sessionId, newCount) + const currentCount = sessionCounters.get(context.sessionId) || 0; + const newCount = currentCount + 1; + sessionCounters.set(context.sessionId, newCount); - return `Counter for session ${context.sessionId}: ${newCount}` + return `Counter for session ${context.sessionId}: ${newCount}`; }, name: "increment-counter", parameters: z.object({}), - }) + }); - const port = 3000 + Math.floor(Math.random() * 1000) + const port = 3000 + Math.floor(Math.random() * 1000); await server.start({ httpStream: { port, }, transportType: "httpStream", - }) + }); try { // Create two separate clients with different sessions - const transport1 = new StreamableHTTPClientTransport(new URL(`http://localhost:${port}/mcp`)) + const transport1 = new StreamableHTTPClientTransport( + new URL(`http://localhost:${port}/mcp`), + ); const client1 = new Client( { @@ -204,11 +210,13 @@ describe("FastMCP Session ID Support", () => { { capabilities: {}, }, - ) + ); - await client1.connect(transport1) + await client1.connect(transport1); - const transport2 = new StreamableHTTPClientTransport(new URL(`http://localhost:${port}/mcp`)) + const transport2 = new StreamableHTTPClientTransport( + new URL(`http://localhost:${port}/mcp`), + ); const client2 = new Client( { @@ -218,38 +226,44 @@ describe("FastMCP Session ID Support", () => { { capabilities: {}, }, - ) + ); - await client2.connect(transport2) + await client2.connect(transport2); // Increment counter for client 1 twice const result1a = await client1.callTool({ arguments: {}, name: "increment-counter", - }) + }); const result1b = await client1.callTool({ arguments: {}, name: "increment-counter", - }) + }); // Increment counter for client 2 once const result2 = await client2.callTool({ arguments: {}, name: "increment-counter", - }) + }); // Verify counters are independent per session - expect((result1a.content as Array<{ text: string }>)[0].text).toContain(": 1") - expect((result1b.content as Array<{ text: string }>)[0].text).toContain(": 2") - expect((result2.content as Array<{ text: string }>)[0].text).toContain(": 1") - - await client1.close() - await client2.close() + expect((result1a.content as Array<{ text: string }>)[0].text).toContain( + ": 1", + ); + expect((result1b.content as Array<{ text: string }>)[0].text).toContain( + ": 2", + ); + expect((result2.content as Array<{ text: string }>)[0].text).toContain( + ": 1", + ); + + await client1.close(); + await client2.close(); } finally { - await server.stop() + await server.stop(); } - }) + }); it("should work in stateless mode without persistent sessionId", async () => { const server = new FastMCP({ @@ -258,21 +272,21 @@ describe("FastMCP Session ID Support", () => { }), name: "test-server", version: "1.0.0", - }) + }); - let capturedSessionId: string | undefined + let capturedSessionId: string | undefined; server.addTool({ description: "Test tool in stateless mode", execute: async (_args, context) => { - capturedSessionId = context.sessionId - return `Session ID: ${context.sessionId || "none"}` + capturedSessionId = context.sessionId; + return `Session ID: ${context.sessionId || "none"}`; }, name: "test-stateless", parameters: z.object({}), - }) + }); - const port = 3000 + Math.floor(Math.random() * 1000) + const port = 3000 + Math.floor(Math.random() * 1000); await server.start({ httpStream: { @@ -280,10 +294,12 @@ describe("FastMCP Session ID Support", () => { stateless: true, }, transportType: "httpStream", - }) + }); try { - const transport = new StreamableHTTPClientTransport(new URL(`http://localhost:${port}/mcp`)) + const transport = new StreamableHTTPClientTransport( + new URL(`http://localhost:${port}/mcp`), + ); const client = new Client( { @@ -293,24 +309,24 @@ describe("FastMCP Session ID Support", () => { { capabilities: {}, }, - ) + ); - await client.connect(transport) + await client.connect(transport); await client.callTool({ arguments: {}, name: "test-stateless", - }) + }); // In stateless mode, sessionId should be undefined - expect(capturedSessionId).toBeUndefined() + expect(capturedSessionId).toBeUndefined(); - await client.close() + await client.close(); } finally { - await server.stop() + await server.stop(); } - }) - }) + }); + }); describe("stdio transport", () => { it("should not have sessionId in stdio transport", async () => { @@ -320,24 +336,24 @@ describe("FastMCP Session ID Support", () => { }), name: "test-server", version: "1.0.0", - }) + }); - let capturedSessionId: string | undefined + let capturedSessionId: string | undefined; server.addTool({ description: "Test tool for stdio", execute: async (_args, context) => { - capturedSessionId = context.sessionId - return `Session ID: ${context.sessionId || "none"}` + capturedSessionId = context.sessionId; + return `Session ID: ${context.sessionId || "none"}`; }, name: "test-stdio", parameters: z.object({}), - }) + }); - await server.start({ transportType: "stdio" }) + await server.start({ transportType: "stdio" }); // In stdio transport, sessionId should be undefined - expect(capturedSessionId).toBeUndefined() - }) - }) -}) + expect(capturedSessionId).toBeUndefined(); + }); + }); +}); diff --git a/src/FastMCP.test.ts b/src/FastMCP.test.ts index 952a83e..267c1eb 100644 --- a/src/FastMCP.test.ts +++ b/src/FastMCP.test.ts @@ -1,8 +1,6 @@ -import { setTimeout as delay } from "timers/promises" - -import { Client } from "@modelcontextprotocol/sdk/client/index.js" -import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js" -import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js" +import { Client } from "@modelcontextprotocol/sdk/client/index.js"; +import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js"; +import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; import { CreateMessageRequestSchema, ErrorCode, @@ -11,48 +9,56 @@ import { McpError, PingRequestSchema, Root, -} from "@modelcontextprotocol/sdk/types.js" -import { createEventSource, EventSourceClient } from "eventsource-client" -import { getRandomPort } from "get-port-please" -import { fetch } from "undici" -import { expect, test, vi } from "vitest" -import { z } from "zod" -import { z as z4 } from "zod/v4" +} from "@modelcontextprotocol/sdk/types.js"; +import { createEventSource, EventSourceClient } from "eventsource-client"; +import { getRandomPort } from "get-port-please"; +import { setTimeout as delay } from "timers/promises"; +import { fetch } from "undici"; +import { expect, test, vi } from "vitest"; +import { z } from "zod"; +import { z as z4 } from "zod/v4"; import { audioContent, + type ContentResult, FastMCP, FastMCPSession, imageContent, - UserError, - type ContentResult, type TextContent, -} from "./FastMCP.js" + UserError, +} from "./FastMCP.js"; const runWithTestServer = async ({ client: createClient, run, server: createServer, }: { - client?: () => Promise - run: ({ client, server }: { client: Client; server: FastMCP; session: FastMCPSession }) => Promise - server?: () => Promise + client?: () => Promise; + run: ({ + client, + server, + }: { + client: Client; + server: FastMCP; + session: FastMCPSession; + }) => Promise; + server?: () => Promise; }) => { - const port = await getRandomPort() + const port = await getRandomPort(); const server = createServer ? await createServer() : new FastMCP({ name: "Test", version: "1.0.0", - }) + }); await server.start({ httpStream: { port, }, transportType: "httpStream", - }) + }); try { const client = createClient @@ -65,27 +71,29 @@ const runWithTestServer = async ({ { capabilities: {}, }, - ) + ); - const transport = new SSEClientTransport(new URL(`http://localhost:${port}/sse`)) + const transport = new SSEClientTransport( + new URL(`http://localhost:${port}/sse`), + ); const session = await new Promise((resolve) => { server.on("connect", async (event) => { // Wait for session to be fully ready before resolving - await event.session.waitForReady() - resolve(event.session) - }) + await event.session.waitForReady(); + resolve(event.session); + }); - client.connect(transport) - }) + client.connect(transport); + }); - await run({ client, server, session }) + await run({ client, server, session }); } finally { - await server.stop() + await server.stop(); } - return port -} + return port; +}; test("adds tools", async () => { await runWithTestServer({ @@ -107,30 +115,30 @@ test("adds tools", async () => { name: "add", }, ], - }) + }); }, server: async () => { const server = new FastMCP({ name: "Test", version: "1.0.0", - }) + }); server.addTool({ description: "Add two numbers", execute: async (args) => { - return String(args.a + args.b) + return String(args.a + args.b); }, name: "add", parameters: z.object({ a: z.number(), b: z.number(), }), - }) + }); - return server + return server; }, - }) -}) + }); +}); test("adds tools with Zod v4 schema", async () => { await runWithTestServer({ @@ -152,55 +160,55 @@ test("adds tools with Zod v4 schema", async () => { name: "add-zod-v4", }, ], - }) + }); }, server: async () => { const server = new FastMCP({ name: "Test", version: "1.0.0", - }) + }); const AddParamsZod4 = z4.object({ a: z4.number(), b: z4.number(), - }) + }); server.addTool({ description: "Add two numbers (using Zod v4 schema)", execute: async (args) => { - return String(args.a + args.b) + return String(args.a + args.b); }, name: "add-zod-v4", parameters: AddParamsZod4, - }) + }); - return server + return server; }, - }) -}) + }); +}); test("health endpoint returns ok", async () => { - const port = await getRandomPort() + const port = await getRandomPort(); const server = new FastMCP({ health: { message: "healthy", path: "/healthz" }, name: "Test", version: "1.0.0", - }) + }); await server.start({ httpStream: { port }, transportType: "httpStream", - }) + }); try { - const response = await fetch(`http://localhost:${port}/healthz`) - expect(response.status).toBe(200) - expect(await response.text()).toBe("healthy") + const response = await fetch(`http://localhost:${port}/healthz`); + expect(response.status).toBe(200); + expect(await response.text()).toBe("healthy"); } finally { - await server.stop() + await server.stop(); } -}) +}); test("calls a tool", async () => { await runWithTestServer({ @@ -215,30 +223,30 @@ test("calls a tool", async () => { }), ).toEqual({ content: [{ text: "3", type: "text" }], - }) + }); }, server: async () => { const server = new FastMCP({ name: "Test", version: "1.0.0", - }) + }); server.addTool({ description: "Add two numbers", execute: async (args) => { - return String(args.a + args.b) + return String(args.a + args.b); }, name: "add", parameters: z.object({ a: z.number(), b: z.number(), }), - }) + }); - return server + return server; }, - }) -}) + }); +}); test("returns a list", async () => { await runWithTestServer({ @@ -256,13 +264,13 @@ test("returns a list", async () => { { text: "a", type: "text" }, { text: "b", type: "text" }, ], - }) + }); }, server: async () => { const server = new FastMCP({ name: "Test", version: "1.0.0", - }) + }); server.addTool({ description: "Add two numbers", @@ -272,19 +280,19 @@ test("returns a list", async () => { { text: "a", type: "text" }, { text: "b", type: "text" }, ], - } + }; }, name: "add", parameters: z.object({ a: z.number(), b: z.number(), }), - }) + }); - return server + return server; }, - }) -}) + }); +}); test("returns an image", async () => { await runWithTestServer({ @@ -305,13 +313,13 @@ test("returns an image", async () => { type: "image", }, ], - }) + }); }, server: async () => { const server = new FastMCP({ name: "Test", version: "1.0.0", - }) + }); server.addTool({ description: "Add two numbers", @@ -321,19 +329,19 @@ test("returns an image", async () => { "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=", "base64", ), - }) + }); }, name: "add", parameters: z.object({ a: z.number(), b: z.number(), }), - }) + }); - return server + return server; }, - }) -}) + }); +}); test("returns an audio", async () => { await runWithTestServer({ @@ -354,13 +362,13 @@ test("returns an audio", async () => { type: "audio", }, ], - }) + }); }, server: async () => { const server = new FastMCP({ name: "Test", version: "1.0.0", - }) + }); server.addTool({ description: "Add two numbers", @@ -370,19 +378,19 @@ test("returns an audio", async () => { "UklGRhwMAABXQVZFZm10IBAAAAABAAEAgD4AAIA+AAABAAgAZGF0Ya4LAACAgICAgICAgICAgICAgICAgICAgICAgICAf3hxeH+AfXZ1eHx6dnR5fYGFgoOKi42aloubq6GOjI2Op7ythXJ0eYF5aV1AOFFib32HmZSHhpCalIiYi4SRkZaLfnhxaWptb21qaWBea2BRYmZTVmFgWFNXVVVhaGdbYGhZbXh1gXZ1goeIlot1k6yxtKaOkaWhq7KonKCZoaCjoKWuqqmurK6ztrO7tbTAvru/vb68vbW6vLGqsLOfm5yal5KKhoyBeHt2dXBnbmljVlJWUEBBPDw9Mi4zKRwhIBYaGRQcHBURGB0XFxwhGxocJSstMjg6PTc6PUxVV1lWV2JqaXN0coCHhIyPjpOenqWppK6xu72yxMu9us7Pw83Wy9nY29ve6OPr6uvs6ezu6ejk6erm3uPj3dbT1sjBzdDFuMHAt7m1r7W6qaCupJOTkpWPgHqAd3JrbGlnY1peX1hTUk9PTFRKR0RFQkRBRUVEQkdBPjs9Pzo6NT04Njs+PTxAPzo/Ojk6PEA5PUJAQD04PkRCREZLUk1KT1BRUVdXU1VRV1tZV1xgXltcXF9hXl9eY2VmZmlna3J0b3F3eHyBfX+JgIWJiouTlZCTmpybnqSgnqyrqrO3srK2uL2/u7jAwMLFxsfEv8XLzcrIy83JzcrP0s3M0dTP0drY1dPR1dzc19za19XX2dnU1NjU0dXPzdHQy8rMysfGxMLBvLu3ta+sraeioJ2YlI+MioeFfX55cnJsaWVjXVlbVE5RTktHRUVAPDw3NC8uLyknKSIiJiUdHiEeGx4eHRwZHB8cHiAfHh8eHSEhISMoJyMnKisrLCszNy8yOTg9QEJFRUVITVFOTlJVWltaXmNfX2ZqZ21xb3R3eHqAhoeJkZKTlZmhpJ6kqKeur6yxtLW1trW4t6+us7axrbK2tLa6ury7u7u9u7vCwb+/vr7Ev7y9v8G8vby6vru4uLq+tri8ubi5t7W4uLW5uLKxs7G0tLGwt7Wvs7avr7O0tLW4trS4uLO1trW1trm1tLm0r7Kyr66wramsqaKlp52bmpeWl5KQkImEhIB8fXh3eHJrbW5mYGNcWFhUUE1LRENDQUI9ODcxLy8vMCsqLCgoKCgpKScoKCYoKygpKyssLi0sLi0uMDIwMTIuLzQ0Njg4Njc8ODlBQ0A/RUdGSU5RUVFUV1pdXWFjZGdpbG1vcXJ2eXh6fICAgIWIio2OkJGSlJWanJqbnZ2cn6Kkp6enq62srbCysrO1uLy4uL+/vL7CwMHAvb/Cvbq9vLm5uba2t7Sysq+urqyqqaalpqShoJ+enZuamZqXlZWTkpGSkpCNjpCMioqLioiHhoeGhYSGg4GDhoKDg4GBg4GBgoGBgoOChISChISChIWDg4WEgoSEgYODgYGCgYGAgICAgX99f398fX18e3p6e3t7enp7fHx4e3x6e3x7fHx9fX59fn1+fX19fH19fnx9fn19fX18fHx7fHx6fH18fXx8fHx7fH1+fXx+f319fn19fn1+gH9+f4B/fn+AgICAgH+AgICAgIGAgICAgH9+f4B+f35+fn58e3t8e3p5eXh4d3Z1dHRzcXBvb21sbmxqaWhlZmVjYmFfX2BfXV1cXFxaWVlaWVlYV1hYV1hYWVhZWFlaWllbXFpbXV5fX15fYWJhYmNiYWJhYWJjZGVmZ2hqbG1ub3Fxc3V3dnd6e3t8e3x+f3+AgICAgoGBgoKDhISFh4aHiYqKi4uMjYyOj4+QkZKUlZWXmJmbm52enqCioqSlpqeoqaqrrK2ur7CxsrGys7O0tbW2tba3t7i3uLe4t7a3t7i3tre2tba1tLSzsrKysbCvrq2sq6qop6alo6OioJ+dnJqZmJeWlJKSkI+OjoyLioiIh4WEg4GBgH9+fXt6eXh3d3V0c3JxcG9ubWxsamppaWhnZmVlZGRjYmNiYWBhYGBfYF9fXl5fXl1dXVxdXF1dXF1cXF1cXF1dXV5dXV5fXl9eX19gYGFgYWJhYmFiY2NiY2RjZGNkZWRlZGVmZmVmZmVmZ2dmZ2hnaGhnaGloZ2hpaWhpamlqaWpqa2pra2xtbGxtbm1ubm5vcG9wcXBxcnFycnN0c3N0dXV2d3d4eHh5ent6e3x9fn5/f4CAgIGCg4SEhYaGh4iIiYqLi4uMjY2Oj5CQkZGSk5OUlJWWlpeYl5iZmZqbm5ybnJ2cnZ6en56fn6ChoKChoqGio6KjpKOko6SjpKWkpaSkpKSlpKWkpaSlpKSlpKOkpKOko6KioaKhoaCfoJ+enp2dnJybmpmZmJeXlpWUk5STkZGQj4+OjYyLioqJh4eGhYSEgoKBgIB/fn59fHt7enl5eHd3dnZ1dHRzc3JycXBxcG9vbm5tbWxrbGxraWppaWhpaGdnZ2dmZ2ZlZmVmZWRlZGVkY2RjZGNkZGRkZGRkZGRkZGRjZGRkY2RjZGNkZWRlZGVmZWZmZ2ZnZ2doaWhpaWpra2xsbW5tbm9ub29wcXFycnNzdHV1dXZ2d3d4eXl6enp7fHx9fX5+f4CAgIGAgYGCgoOEhISFhoWGhoeIh4iJiImKiYqLiouLjI2MjI2OjY6Pj46PkI+QkZCRkJGQkZGSkZKRkpGSkZGRkZKRkpKRkpGSkZKRkpGSkZKRkpGSkZCRkZCRkI+Qj5CPkI+Pjo+OjY6Njo2MjYyLjIuMi4qLioqJiomJiImIh4iHh4aHhoaFhoWFhIWEg4SDg4KDgoKBgoGAgYCBgICAgICAf4CAf39+f35/fn1+fX59fHx9fH18e3x7fHt6e3p7ent6e3p5enl6enl6eXp5eXl4eXh5eHl4eXh5eHl4eXh5eHh3eHh4d3h4d3h3d3h4d3l4eHd4d3h3eHd4d3h3eHh4eXh5eHl4eHl4eXh5enl6eXp5enl6eXp5ent6ent6e3x7fHx9fH18fX19fn1+fX5/fn9+f4B/gH+Af4CAgICAgIGAgYCBgoGCgYKCgoKDgoOEg4OEg4SFhIWEhYSFhoWGhYaHhoeHhoeGh4iHiIiHiImIiImKiYqJiYqJiouKi4qLiouKi4qLiouKi4qLiouKi4qLi4qLiouKi4qLiomJiomIiYiJiImIh4iIh4iHhoeGhYWGhYaFhIWEg4OEg4KDgoOCgYKBgIGAgICAgH+Af39+f359fn18fX19fHx8e3t6e3p7enl6eXp5enl6enl5eXh5eHh5eHl4eXh5eHl4eHd5eHd3eHl4d3h3eHd4d3h3eHh4d3h4d3h3d3h5eHl4eXh5eHl5eXp5enl6eXp7ent6e3p7e3t7fHt8e3x8fHx9fH1+fX59fn9+f35/gH+AgICAgICAgYGAgYKBgoGCgoKDgoOEg4SEhIWFhIWFhoWGhYaGhoaHhoeGh4aHhoeIh4iHiIeHiIeIh4iHiIeIiIiHiIeIh4iHiIiHiIeIh4iHiIeIh4eIh4eIh4aHh4aHhoeGh4aHhoWGhYaFhoWFhIWEhYSFhIWEhISDhIOEg4OCg4OCg4KDgYKCgYKCgYCBgIGAgYCBgICAgICAgICAf4B/f4B/gH+Af35/fn9+f35/fn1+fn19fn1+fX59fn19fX19fH18fXx9fH18fXx9fH18fXx8fHt8e3x7fHt8e3x7fHt8e3x7fHt8e3x7fHt8e3x7fHt8e3x8e3x7fHt8e3x7fHx8fXx9fH18fX5+fX59fn9+f35+f35/gH+Af4B/gICAgICAgICAgICAgYCBgIGAgIGAgYGBgoGCgYKBgoGCgYKBgoGCgoKDgoOCg4KDgoOCg4KDgoOCg4KDgoOCg4KDgoOCg4KDgoOCg4KDgoOCg4KDgoOCg4KDgoOCg4KDgoOCg4KCgoGCgYKBgoGCgYKBgoGCgYKBgoGCgYKBgoGCgYKBgoGCgYKBgoGCgYKBgoGBgYCBgIGAgYCBgIGAgYCBgIGAgYCBgIGAgYCBgIGAgYCAgICBgIGAgYCBgIGAgYCBgIGAgYCBgExJU1RCAAAASU5GT0lDUkQMAAAAMjAwOC0wOS0yMQAASUVORwMAAAAgAAABSVNGVBYAAABTb255IFNvdW5kIEZvcmdlIDguMAAA", "base64", ), - }) + }); }, name: "add", parameters: z.object({ a: z.number(), b: z.number(), }), - }) + }); - return server + return server; }, - }) -}) + }); +}); test("handles UserError errors", async () => { await runWithTestServer({ @@ -398,30 +406,30 @@ test("handles UserError errors", async () => { ).toEqual({ content: [{ text: "Something went wrong", type: "text" }], isError: true, - }) + }); }, server: async () => { const server = new FastMCP({ name: "Test", version: "1.0.0", - }) + }); server.addTool({ description: "Add two numbers", execute: async () => { - throw new UserError("Something went wrong") + throw new UserError("Something went wrong"); }, name: "add", parameters: z.object({ a: z.number(), b: z.number(), }), - }) + }); - return server + return server; }, - }) -}) + }); +}); test("handles UserError errors with extras", async () => { await runWithTestServer({ @@ -436,7 +444,7 @@ test("handles UserError errors with extras", async () => { content: [{ text: "Something went wrong", type: "text" }], isError: true, structuredContent: { foo: "bar", num: 42 }, - }) + }); // Should NOT include structuredContent if extras is not present expect( @@ -447,36 +455,36 @@ test("handles UserError errors with extras", async () => { ).toEqual({ content: [{ text: "Something went wrong", type: "text" }], isError: true, - }) + }); }, server: async () => { const server = new FastMCP({ name: "Test", version: "1.0.0", - }) + }); server.addTool({ description: "Throws UserError with extras", execute: async () => { - throw new UserError("Something went wrong", { foo: "bar", num: 42 }) + throw new UserError("Something went wrong", { foo: "bar", num: 42 }); }, name: "add_with_extras", parameters: z.object({ a: z.number(), b: z.number() }), - }) + }); server.addTool({ description: "Throws UserError without extras", execute: async () => { - throw new UserError("Something went wrong") + throw new UserError("Something went wrong"); }, name: "add_without_extras", parameters: z.object({ a: z.number(), b: z.number() }), - }) + }); - return server + return server; }, - }) -}) + }); +}); test("tool can throw McpError with InvalidParams error code", async () => { await runWithTestServer({ @@ -485,37 +493,37 @@ test("tool can throw McpError with InvalidParams error code", async () => { await client.callTool({ arguments: { value: "invalid" }, name: "validate", - }) - throw new Error("Expected error to be thrown") + }); + throw new Error("Expected error to be thrown"); } catch (error) { - expect(error).toBeInstanceOf(McpError) + expect(error).toBeInstanceOf(McpError); // @ts-expect-error - we know that error is an McpError - expect(error.code).toBe(ErrorCode.InvalidParams) + expect(error.code).toBe(ErrorCode.InvalidParams); // @ts-expect-error - we know that error is an McpError - expect(error.message).toContain("Invalid value provided") + expect(error.message).toContain("Invalid value provided"); } }, server: async () => { const server = new FastMCP({ name: "Test", version: "1.0.0", - }) + }); server.addTool({ description: "Validate input", execute: async () => { - throw new McpError(ErrorCode.InvalidParams, "Invalid value provided") + throw new McpError(ErrorCode.InvalidParams, "Invalid value provided"); }, name: "validate", parameters: z.object({ value: z.string() }), - }) + }); - return server + return server; }, - }) -}) + }); +}); test("tool can throw McpError with InternalError error code", async () => { await runWithTestServer({ @@ -524,37 +532,40 @@ test("tool can throw McpError with InternalError error code", async () => { await client.callTool({ arguments: { value: "test" }, name: "process", - }) - throw new Error("Expected error to be thrown") + }); + throw new Error("Expected error to be thrown"); } catch (error) { - expect(error).toBeInstanceOf(McpError) + expect(error).toBeInstanceOf(McpError); // @ts-expect-error - we know that error is an McpError - expect(error.code).toBe(ErrorCode.InternalError) + expect(error.code).toBe(ErrorCode.InternalError); // @ts-expect-error - we know that error is an McpError - expect(error.message).toContain("Internal processing error") + expect(error.message).toContain("Internal processing error"); } }, server: async () => { const server = new FastMCP({ name: "Test", version: "1.0.0", - }) + }); server.addTool({ description: "Process data", execute: async () => { - throw new McpError(ErrorCode.InternalError, "Internal processing error") + throw new McpError( + ErrorCode.InternalError, + "Internal processing error", + ); }, name: "process", parameters: z.object({ value: z.string() }), - }) + }); - return server + return server; }, - }) -}) + }); +}); test("tool can throw McpError with custom data", async () => { await runWithTestServer({ @@ -563,16 +574,16 @@ test("tool can throw McpError with custom data", async () => { await client.callTool({ arguments: { id: "123" }, name: "find", - }) - throw new Error("Expected error to be thrown") + }); + throw new Error("Expected error to be thrown"); } catch (error) { - expect(error).toBeInstanceOf(McpError) + expect(error).toBeInstanceOf(McpError); // @ts-expect-error - we know that error is an McpError - expect(error.code).toBe(ErrorCode.InvalidRequest) + expect(error.code).toBe(ErrorCode.InvalidRequest); // @ts-expect-error - we know that error is an McpError - expect(error.message).toContain("Resource not found") + expect(error.message).toContain("Resource not found"); // Note: Custom data may not be preserved through the MCP SDK transport layer // The important part is that the error code and message are correct @@ -582,7 +593,7 @@ test("tool can throw McpError with custom data", async () => { const server = new FastMCP({ name: "Test", version: "1.0.0", - }) + }); server.addTool({ description: "Find resource", @@ -590,16 +601,16 @@ test("tool can throw McpError with custom data", async () => { throw new McpError(ErrorCode.InvalidRequest, "Resource not found", { available: ["456", "789"], id: args.id, - }) + }); }, name: "find", parameters: z.object({ id: z.string() }), - }) + }); - return server + return server; }, - }) -}) + }); +}); test("calling an unknown tool throws McpError with MethodNotFound code", async () => { await runWithTestServer({ @@ -611,29 +622,29 @@ test("calling an unknown tool throws McpError with MethodNotFound code", async ( b: 2, }, name: "add", - }) + }); } catch (error) { - expect(error).toBeInstanceOf(McpError) + expect(error).toBeInstanceOf(McpError); // @ts-expect-error - we know that error is an McpError - expect(error.code).toBe(ErrorCode.MethodNotFound) + expect(error.code).toBe(ErrorCode.MethodNotFound); } }, server: async () => { const server = new FastMCP({ name: "Test", version: "1.0.0", - }) + }); - return server + return server; }, - }) -}) + }); +}); test("tracks tool progress", async () => { await runWithTestServer({ run: async ({ client }) => { - const onProgress = vi.fn() + const onProgress = vi.fn(); await client.callTool( { @@ -647,19 +658,19 @@ test("tracks tool progress", async () => { { onprogress: onProgress, }, - ) + ); - expect(onProgress).toHaveBeenCalledTimes(1) + expect(onProgress).toHaveBeenCalledTimes(1); expect(onProgress).toHaveBeenCalledWith({ progress: 0, total: 10, - }) + }); }, server: async () => { const server = new FastMCP({ name: "Test", version: "1.0.0", - }) + }); server.addTool({ description: "Add two numbers", @@ -667,89 +678,89 @@ test("tracks tool progress", async () => { reportProgress({ progress: 0, total: 10, - }) + }); - await delay(100) + await delay(100); - return String(args.a + args.b) + return String(args.a + args.b); }, name: "add", parameters: z.object({ a: z.number(), b: z.number(), }), - }) + }); - return server + return server; }, - }) -}) + }); +}); test("provides requestMetadata to tool context", async () => { - let capturedMetadata: Record | undefined = undefined - const metadata = { foo: "bar" } + let capturedMetadata: Record | undefined = undefined; + const metadata = { foo: "bar" }; await runWithTestServer({ run: async ({ client }) => { await client.callTool({ _meta: metadata, name: "metadata-test", - }) + }); - expect(capturedMetadata).toBeDefined() - expect(capturedMetadata).toEqual(metadata) + expect(capturedMetadata).toBeDefined(); + expect(capturedMetadata).toEqual(metadata); }, server: async () => { const server = new FastMCP({ name: "Test", version: "1.0.0", - }) + }); server.addTool({ execute: async (_args, context) => { - capturedMetadata = context.requestMetadata - return "success" + capturedMetadata = context.requestMetadata; + return "success"; }, name: "metadata-test", - }) + }); - return server + return server; }, - }) -}) + }); +}); test("allows tools to return _meta in CallToolResult", async () => { - const expectedMeta = { customField: "customValue", timestamp: 1234567890 } + const expectedMeta = { customField: "customValue", timestamp: 1234567890 }; await runWithTestServer({ run: async ({ client }) => { const result = await client.callTool({ name: "meta-result-test", - }) + }); - expect(result._meta).toBeDefined() - expect(result._meta).toEqual(expectedMeta) + expect(result._meta).toBeDefined(); + expect(result._meta).toEqual(expectedMeta); }, server: async () => { const server = new FastMCP({ name: "Test", version: "1.0.0", - }) + }); server.addTool({ execute: async () => { return { _meta: expectedMeta, content: [{ text: "success", type: "text" }], - } + }; }, name: "meta-result-test", - }) + }); - return server + return server; }, - }) -}) + }); +}); test( "reports multiple progress updates without buffering", @@ -761,11 +772,11 @@ test( async () => { await runWithTestServer({ run: async ({ client }) => { - const progressCalls: Array<{ progress: number; total: number }> = [] + const progressCalls: Array<{ progress: number; total: number }> = []; const onProgress = vi.fn((data) => { - progressCalls.push(data) - }) + progressCalls.push(data); + }); await client.callTool( { @@ -778,72 +789,72 @@ test( { onprogress: onProgress, }, - ) + ); - expect(onProgress).toHaveBeenCalledTimes(4) + expect(onProgress).toHaveBeenCalledTimes(4); expect(progressCalls).toEqual([ { progress: 0, total: 100 }, { progress: 50, total: 100 }, { progress: 90, total: 100 }, { progress: 100, total: 100 }, // This was previously lost due to buffering - ]) + ]); }, server: async () => { const server = new FastMCP({ name: "Test", version: "1.0.0", - }) + }); server.addTool({ description: "Test tool for progress buffering fix", execute: async (args, { reportProgress }) => { - const { steps } = args + const { steps } = args; // Initial - await reportProgress({ progress: 0, total: 100 }) + await reportProgress({ progress: 0, total: 100 }); for (let i = 1; i <= steps; i++) { - await delay(50) // Small delay to simulate work + await delay(50); // Small delay to simulate work if (i === 1) { - await reportProgress({ progress: 50, total: 100 }) + await reportProgress({ progress: 50, total: 100 }); } else if (i === 2) { - await reportProgress({ progress: 90, total: 100 }) + await reportProgress({ progress: 90, total: 100 }); } } // This was the critical test case that failed before the fix // because there's no await after it, causing it to be buffered - await reportProgress({ progress: 100, total: 100 }) + await reportProgress({ progress: 100, total: 100 }); - return "Progress test completed" + return "Progress test completed"; }, name: "progress-test", parameters: z.object({ steps: z.number(), }), - }) + }); - return server + return server; }, - }) + }); }, -) +); test("sets logging levels", async () => { await runWithTestServer({ run: async ({ client, session }) => { - await client.setLoggingLevel("debug") + await client.setLoggingLevel("debug"); - expect(session.loggingLevel).toBe("debug") + expect(session.loggingLevel).toBe("debug"); - await client.setLoggingLevel("info") + await client.setLoggingLevel("info"); - expect(session.loggingLevel).toBe("info") + expect(session.loggingLevel).toBe("info"); }, - }) -}) + }); +}); test("handles tool timeout", async () => { await runWithTestServer({ @@ -855,34 +866,34 @@ test("handles tool timeout", async () => { b: 2, }, name: "add", - }) - throw new Error("Expected timeout error to be thrown") + }); + throw new Error("Expected timeout error to be thrown"); } catch (error) { - expect(error).toBeInstanceOf(McpError) + expect(error).toBeInstanceOf(McpError); // @ts-expect-error - we know that error is an McpError - expect(error.code).toBe(ErrorCode.InternalError) + expect(error.code).toBe(ErrorCode.InternalError); // @ts-expect-error - we know that error is an McpError - expect(error.message).toContain("timed out") + expect(error.message).toContain("timed out"); } }, server: async () => { const server = new FastMCP({ name: "Test", version: "1.0.0", - }) + }); server.addTool({ description: "Add two numbers with potential timeout", execute: async (args) => { - console.log(`Adding ${args.a} and ${args.b}`) + console.log(`Adding ${args.a} and ${args.b}`); if (args.a > 1000 || args.b > 1000) { - await new Promise((resolve) => setTimeout(resolve, 3000)) + await new Promise((resolve) => setTimeout(resolve, 3000)); } - return String(args.a + args.b) + return String(args.a + args.b); }, name: "add", parameters: z.object({ @@ -890,26 +901,29 @@ test("handles tool timeout", async () => { b: z.number(), }), timeoutMs: 1000, - }) + }); - return server + return server; }, - }) -}) + }); +}); test("sends logging messages to the client", async () => { await runWithTestServer({ run: async ({ client }) => { - const onLog = vi.fn() - - client.setNotificationHandler(LoggingMessageNotificationSchema, (message) => { - if (message.method === "notifications/message") { - onLog({ - level: message.params.level, - ...(message.params.data ?? {}), - }) - } - }) + const onLog = vi.fn(); + + client.setNotificationHandler( + LoggingMessageNotificationSchema, + (message) => { + if (message.method === "notifications/message") { + onLog({ + level: message.params.level, + ...(message.params.data ?? {}), + }); + } + }, + ); await client.callTool({ arguments: { @@ -917,58 +931,58 @@ test("sends logging messages to the client", async () => { b: 2, }, name: "add", - }) + }); - expect(onLog).toHaveBeenCalledTimes(4) + expect(onLog).toHaveBeenCalledTimes(4); expect(onLog).toHaveBeenNthCalledWith(1, { context: { foo: "bar", }, level: "debug", message: "debug message", - }) + }); expect(onLog).toHaveBeenNthCalledWith(2, { level: "error", message: "error message", - }) + }); expect(onLog).toHaveBeenNthCalledWith(3, { level: "info", message: "info message", - }) + }); expect(onLog).toHaveBeenNthCalledWith(4, { level: "warning", message: "warn message", - }) + }); }, server: async () => { const server = new FastMCP({ name: "Test", version: "1.0.0", - }) + }); server.addTool({ description: "Add two numbers", execute: async (args, { log }) => { log.debug("debug message", { foo: "bar", - }) - log.error("error message") - log.info("info message") - log.warn("warn message") + }); + log.error("error message"); + log.info("info message"); + log.warn("warn message"); - return String(args.a + args.b) + return String(args.a + args.b); }, name: "add", parameters: z.object({ a: z.number(), b: z.number(), }), - }) + }); - return server + return server; }, - }) -}) + }); +}); test("adds resources", async () => { await runWithTestServer({ @@ -981,29 +995,29 @@ test("adds resources", async () => { uri: "file:///logs/app.log", }, ], - }) + }); }, server: async () => { const server = new FastMCP({ name: "Test", version: "1.0.0", - }) + }); server.addResource({ async load() { return { text: "Example log content", - } + }; }, mimeType: "text/plain", name: "Application Logs", uri: "file:///logs/app.log", - }) + }); - return server + return server; }, - }) -}) + }); +}); test("clients reads a resource", async () => { await runWithTestServer({ @@ -1021,29 +1035,29 @@ test("clients reads a resource", async () => { uri: "file:///logs/app.log", }, ], - }) + }); }, server: async () => { const server = new FastMCP({ name: "Test", version: "1.0.0", - }) + }); server.addResource({ async load() { return { text: "Example log content", - } + }; }, mimeType: "text/plain", name: "Application Logs", uri: "file:///logs/app.log", - }) + }); - return server + return server; }, - }) -}) + }); +}); test("clients reads a resource that returns multiple resources", async () => { await runWithTestServer({ @@ -1067,13 +1081,13 @@ test("clients reads a resource that returns multiple resources", async () => { uri: "file:///logs/app.log", }, ], - }) + }); }, server: async () => { const server = new FastMCP({ name: "Test", version: "1.0.0", - }) + }); server.addResource({ async load() { @@ -1084,17 +1098,17 @@ test("clients reads a resource that returns multiple resources", async () => { { text: "b", }, - ] + ]; }, mimeType: "text/plain", name: "Application Logs", uri: "file:///logs/app.log", - }) + }); - return server + return server; }, - }) -}) + }); +}); test("embedded resources work in tools", async () => { await runWithTestServer({ @@ -1117,14 +1131,14 @@ test("embedded resources work in tools", async () => { type: "resource", }, ], - }) + }); }, server: async () => { const server = new FastMCP({ name: "Test", version: "1.0.0", - }) + }); server.addResourceTemplate({ arguments: [ @@ -1136,12 +1150,12 @@ test("embedded resources work in tools", async () => { async load(args) { return { text: `{"id":"${args.userId}","name":"User","email":"user@example.com"}`, - } + }; }, mimeType: "application/json", name: "User Profile", uriTemplate: "user://profile/{userId}", - }) + }); server.addTool({ description: "Get user profile data", @@ -1149,22 +1163,24 @@ test("embedded resources work in tools", async () => { return { content: [ { - resource: await server.embedded(`user://profile/${args.userId}`), + resource: await server.embedded( + `user://profile/${args.userId}`, + ), type: "resource", }, ], - } + }; }, name: "get_user_profile", parameters: z.object({ userId: z.string(), }), - }) + }); - return server + return server; }, - }) -}) + }); +}); test("embedded resources work with direct resources", async () => { await runWithTestServer({ @@ -1185,25 +1201,25 @@ test("embedded resources work with direct resources", async () => { type: "resource", }, ], - }) + }); }, server: async () => { const server = new FastMCP({ name: "Test", version: "1.0.0", - }) + }); server.addResource({ async load() { return { text: "Example log content", - } + }; }, mimeType: "text/plain", name: "Application Logs", uri: "file:///logs/app.log", - }) + }); server.addTool({ description: "Get application logs", @@ -1215,16 +1231,16 @@ test("embedded resources work with direct resources", async () => { type: "resource", }, ], - } + }; }, name: "get_logs", parameters: z.object({}), - }) + }); - return server + return server; }, - }) -}) + }); +}); test("embedded resources work with URI templates and query parameters", async () => { await runWithTestServer({ @@ -1248,7 +1264,7 @@ test("embedded resources work with URI templates and query parameters", async () type: "resource", }, ], - }) + }); // Test case 2: Query parameters with different order expect( @@ -1269,7 +1285,7 @@ test("embedded resources work with URI templates and query parameters", async () type: "resource", }, ], - }) + }); // Test case 3: Query parameters with encoded values expect( @@ -1290,14 +1306,14 @@ test("embedded resources work with URI templates and query parameters", async () type: "resource", }, ], - }) + }); }, server: async () => { const server = new FastMCP({ name: "Test", version: "1.0.0", - }) + }); server.addResourceTemplate({ arguments: [ @@ -1317,15 +1333,16 @@ test("embedded resources work with URI templates and query parameters", async () query: args.q, type: "search", }), - } + }; }, mimeType: "application/json", name: "Search Resource", uriTemplate: "ui://search{?location,q}", - }) + }); server.addTool({ - description: "Get search resource data using embedded function with query parameters", + description: + "Get search resource data using embedded function with query parameters", execute: async (args) => { return { content: [ @@ -1334,18 +1351,18 @@ test("embedded resources work with URI templates and query parameters", async () type: "resource", }, ], - } + }; }, name: "get_search_resource", parameters: z.object({ uri: z.string(), }), - }) + }); - return server + return server; }, - }) -}) + }); +}); test("embedded resources work with complex URI template patterns", async () => { await runWithTestServer({ @@ -1369,7 +1386,7 @@ test("embedded resources work with complex URI template patterns", async () => { type: "resource", }, ], - }) + }); // Test case 2: Optional query parameters (some missing) expect( @@ -1390,14 +1407,14 @@ test("embedded resources work with complex URI template patterns", async () => { type: "resource", }, ], - }) + }); }, server: async () => { const server = new FastMCP({ name: "Test", version: "1.0.0", - }) + }); server.addResourceTemplate({ arguments: [ @@ -1417,24 +1434,25 @@ test("embedded resources work with complex URI template patterns", async () => { async load(args) { const result: Record = { userId: args.userId, - } + }; if (args.fields) { - result.fields = args.fields + result.fields = args.fields; } if (args.format) { - result.format = args.format + result.format = args.format; } return { text: JSON.stringify(result), - } + }; }, mimeType: "application/json", name: "User Data API", uriTemplate: "api://users/{userId}{?fields,format}", - }) + }); server.addTool({ - description: "Get user data using complex URI templates with path and query parameters", + description: + "Get user data using complex URI templates with path and query parameters", execute: async (args) => { return { content: [ @@ -1443,18 +1461,18 @@ test("embedded resources work with complex URI template patterns", async () => { type: "resource", }, ], - } + }; }, name: "get_user_data", parameters: z.object({ uri: z.string(), }), - }) + }); - return server + return server; }, - }) -}) + }); +}); test("adds prompts", async () => { await runWithTestServer({ @@ -1477,7 +1495,7 @@ test("adds prompts", async () => { role: "user", }, ], - }) + }); expect(await client.listPrompts()).toEqual({ prompts: [ @@ -1493,13 +1511,13 @@ test("adds prompts", async () => { name: "git-commit", }, ], - }) + }); }, server: async () => { const server = new FastMCP({ name: "Test", version: "1.0.0", - }) + }); server.addPrompt({ arguments: [ @@ -1511,36 +1529,36 @@ test("adds prompts", async () => { ], description: "Generate a Git commit message", load: async (args) => { - return `Generate a concise but descriptive commit message for these changes:\n\n${args.changes}` + return `Generate a concise but descriptive commit message for these changes:\n\n${args.changes}`; }, name: "git-commit", - }) + }); - return server + return server; }, - }) -}) + }); +}); test("uses events to notify server of client connect/disconnect", async () => { - const port = await getRandomPort() + const port = await getRandomPort(); const server = new FastMCP({ name: "Test", version: "1.0.0", - }) + }); - const onConnect = vi.fn().mockResolvedValue(undefined) - const onDisconnect = vi.fn().mockResolvedValue(undefined) + const onConnect = vi.fn().mockResolvedValue(undefined); + const onDisconnect = vi.fn().mockResolvedValue(undefined); - server.on("connect", onConnect) - server.on("disconnect", onDisconnect) + server.on("connect", onConnect); + server.on("disconnect", onDisconnect); await server.start({ httpStream: { port, }, transportType: "httpStream", - }) + }); const client = new Client( { @@ -1550,43 +1568,45 @@ test("uses events to notify server of client connect/disconnect", async () => { { capabilities: {}, }, - ) + ); - const transport = new SSEClientTransport(new URL(`http://localhost:${port}/sse`)) + const transport = new SSEClientTransport( + new URL(`http://localhost:${port}/sse`), + ); - await client.connect(transport) + await client.connect(transport); - await delay(100) + await delay(100); - expect(onConnect).toHaveBeenCalledTimes(1) - expect(onDisconnect).toHaveBeenCalledTimes(0) + expect(onConnect).toHaveBeenCalledTimes(1); + expect(onDisconnect).toHaveBeenCalledTimes(0); - expect(server.sessions).toEqual([expect.any(FastMCPSession)]) + expect(server.sessions).toEqual([expect.any(FastMCPSession)]); - await client.close() + await client.close(); - await delay(100) + await delay(100); - expect(onConnect).toHaveBeenCalledTimes(1) - expect(onDisconnect).toHaveBeenCalledTimes(1) + expect(onConnect).toHaveBeenCalledTimes(1); + expect(onDisconnect).toHaveBeenCalledTimes(1); - await server.stop() -}) + await server.stop(); +}); test("handles multiple clients", async () => { - const port = await getRandomPort() + const port = await getRandomPort(); const server = new FastMCP({ name: "Test", version: "1.0.0", - }) + }); await server.start({ httpStream: { port, }, transportType: "httpStream", - }) + }); const client1 = new Client( { @@ -1596,11 +1616,13 @@ test("handles multiple clients", async () => { { capabilities: {}, }, - ) + ); - const transport1 = new SSEClientTransport(new URL(`http://localhost:${port}/sse`)) + const transport1 = new SSEClientTransport( + new URL(`http://localhost:${port}/sse`), + ); - await client1.connect(transport1) + await client1.connect(transport1); const client2 = new Client( { @@ -1610,18 +1632,23 @@ test("handles multiple clients", async () => { { capabilities: {}, }, - ) + ); - const transport2 = new SSEClientTransport(new URL(`http://localhost:${port}/sse`)) + const transport2 = new SSEClientTransport( + new URL(`http://localhost:${port}/sse`), + ); - await client2.connect(transport2) + await client2.connect(transport2); - await delay(100) + await delay(100); - expect(server.sessions).toEqual([expect.any(FastMCPSession), expect.any(FastMCPSession)]) + expect(server.sessions).toEqual([ + expect.any(FastMCPSession), + expect.any(FastMCPSession), + ]); - await server.stop() -}) + await server.stop(); +}); test("session knows about client capabilities", async () => { await runWithTestServer({ @@ -1638,7 +1665,7 @@ test("session knows about client capabilities", async () => { }, }, }, - ) + ); client.setRequestHandler(ListRootsRequestSchema, () => { return { @@ -1648,20 +1675,20 @@ test("session knows about client capabilities", async () => { uri: "file:///home/user/projects/frontend", }, ], - } - }) + }; + }); - return client + return client; }, run: async ({ session }) => { expect(session.clientCapabilities).toEqual({ roots: { listChanged: true, }, - }) + }); }, - }) -}) + }); +}); test("session knows about roots", async () => { await runWithTestServer({ @@ -1678,7 +1705,7 @@ test("session knows about roots", async () => { }, }, }, - ) + ); client.setRequestHandler(ListRootsRequestSchema, () => { return { @@ -1688,10 +1715,10 @@ test("session knows about roots", async () => { uri: "file:///home/user/projects/frontend", }, ], - } - }) + }; + }); - return client + return client; }, run: async ({ session }) => { expect(session.roots).toEqual([ @@ -1699,10 +1726,10 @@ test("session knows about roots", async () => { name: "Frontend Repository", uri: "file:///home/user/projects/frontend", }, - ]) + ]); }, - }) -}) + }); +}); test("session listens to roots changes", async () => { const clientRoots: Root[] = [ @@ -1710,7 +1737,7 @@ test("session listens to roots changes", async () => { name: "Frontend Repository", uri: "file:///home/user/projects/frontend", }, - ] + ]; await runWithTestServer({ client: async () => { @@ -1726,15 +1753,15 @@ test("session listens to roots changes", async () => { }, }, }, - ) + ); client.setRequestHandler(ListRootsRequestSchema, () => { return { roots: clientRoots, - } - }) + }; + }); - return client + return client; }, run: async ({ client, session }) => { expect(session.roots).toEqual([ @@ -1742,20 +1769,20 @@ test("session listens to roots changes", async () => { name: "Frontend Repository", uri: "file:///home/user/projects/frontend", }, - ]) + ]); clientRoots.push({ name: "Backend Repository", uri: "file:///home/user/projects/backend", - }) + }); - await client.sendRootsListChanged() + await client.sendRootsListChanged(); - const onRootsChanged = vi.fn() + const onRootsChanged = vi.fn(); - session.on("rootsChanged", onRootsChanged) + session.on("rootsChanged", onRootsChanged); - await delay(100) + await delay(100); expect(session.roots).toEqual([ { @@ -1766,9 +1793,9 @@ test("session listens to roots changes", async () => { name: "Backend Repository", uri: "file:///home/user/projects/backend", }, - ]) + ]); - expect(onRootsChanged).toHaveBeenCalledTimes(1) + expect(onRootsChanged).toHaveBeenCalledTimes(1); expect(onRootsChanged).toHaveBeenCalledWith({ roots: [ { @@ -1780,22 +1807,22 @@ test("session listens to roots changes", async () => { uri: "file:///home/user/projects/backend", }, ], - }) + }); }, - }) -}) + }); +}); test("session sends pings to the client", async () => { await runWithTestServer({ run: async ({ client }) => { - const onPing = vi.fn().mockReturnValue({}) + const onPing = vi.fn().mockReturnValue({}); - client.setRequestHandler(PingRequestSchema, onPing) + client.setRequestHandler(PingRequestSchema, onPing); - await delay(2000) + await delay(2000); - expect(onPing.mock.calls.length).toBeGreaterThanOrEqual(1) - expect(onPing.mock.calls.length).toBeLessThanOrEqual(3) + expect(onPing.mock.calls.length).toBeGreaterThanOrEqual(1); + expect(onPing.mock.calls.length).toBeLessThanOrEqual(3); }, server: async () => { const server = new FastMCP({ @@ -1805,11 +1832,11 @@ test("session sends pings to the client", async () => { intervalMs: 1000, }, version: "1.0.0", - }) - return server + }); + return server; }, - }) -}) + }); +}); test("completes prompt arguments", async () => { await runWithTestServer({ @@ -1823,19 +1850,19 @@ test("completes prompt arguments", async () => { name: "countryPoem", type: "ref/prompt", }, - }) + }); expect(response).toEqual({ completion: { values: ["Germany"], }, - }) + }); }, server: async () => { const server = new FastMCP({ name: "Test", version: "1.0.0", - }) + }); server.addPrompt({ arguments: [ @@ -1844,12 +1871,12 @@ test("completes prompt arguments", async () => { if (value === "Germ") { return { values: ["Germany"], - } + }; } return { values: [], - } + }; }, description: "Name of the country", name: "name", @@ -1858,15 +1885,15 @@ test("completes prompt arguments", async () => { ], description: "Writes a poem about a country", load: async ({ name }) => { - return `Hello, ${name}!` + return `Hello, ${name}!`; }, name: "countryPoem", - }) + }); - return server + return server; }, - }) -}) + }); +}); test("adds automatic prompt argument completion when enum is provided", async () => { await runWithTestServer({ @@ -1880,20 +1907,20 @@ test("adds automatic prompt argument completion when enum is provided", async () name: "countryPoem", type: "ref/prompt", }, - }) + }); expect(response).toEqual({ completion: { total: 1, values: ["Germany"], }, - }) + }); }, server: async () => { const server = new FastMCP({ name: "Test", version: "1.0.0", - }) + }); server.addPrompt({ arguments: [ @@ -1906,15 +1933,15 @@ test("adds automatic prompt argument completion when enum is provided", async () ], description: "Writes a poem about a country", load: async ({ name }) => { - return `Hello, ${name}!` + return `Hello, ${name}!`; }, name: "countryPoem", - }) + }); - return server + return server; }, - }) -}) + }); +}); test("completes template resource arguments", async () => { await runWithTestServer({ @@ -1928,19 +1955,19 @@ test("completes template resource arguments", async () => { type: "ref/resource", uri: "issue:///{issueId}", }, - }) + }); expect(response).toEqual({ completion: { values: ["123456"], }, - }) + }); }, server: async () => { const server = new FastMCP({ name: "Test", version: "1.0.0", - }) + }); server.addResourceTemplate({ arguments: [ @@ -1949,12 +1976,12 @@ test("completes template resource arguments", async () => { if (value === "123") { return { values: ["123456"], - } + }; } return { values: [], - } + }; }, description: "ID of the issue", name: "issueId", @@ -1963,17 +1990,17 @@ test("completes template resource arguments", async () => { load: async ({ issueId }) => { return { text: `Issue ${issueId}`, - } + }; }, mimeType: "text/plain", name: "Issue", uriTemplate: "issue:///{issueId}", - }) + }); - return server + return server; }, - }) -}) + }); +}); test("lists resource templates", async () => { await runWithTestServer({ @@ -1986,13 +2013,13 @@ test("lists resource templates", async () => { uriTemplate: "file:///logs/{name}.log", }, ], - }) + }); }, server: async () => { const server = new FastMCP({ name: "Test", version: "1.0.0", - }) + }); server.addResourceTemplate({ arguments: [ @@ -2005,101 +2032,107 @@ test("lists resource templates", async () => { load: async ({ name }) => { return { text: `Example log content for ${name}`, - } + }; }, mimeType: "text/plain", name: "Application Logs", uriTemplate: "file:///logs/{name}.log", - }) + }); - return server + return server; }, - }) -}) - -test("HTTP Stream: custom endpoint works with /another-mcp", { timeout: 20000 }, async () => { - const port = await getRandomPort() + }); +}); - // Create server with custom endpoint - const server = new FastMCP({ - name: "Test", - version: "1.0.0", - }) - - server.addTool({ - description: "Add two numbers", - execute: async (args) => { - return String(args.a + args.b) - }, - name: "add", - parameters: z.object({ - a: z.number(), - b: z.number(), - }), - }) +test( + "HTTP Stream: custom endpoint works with /another-mcp", + { timeout: 20000 }, + async () => { + const port = await getRandomPort(); - await server.start({ - httpStream: { - endpoint: "/another-mcp", - port, - }, - transportType: "httpStream", - }) + // Create server with custom endpoint + const server = new FastMCP({ + name: "Test", + version: "1.0.0", + }); - try { - // Create client - const client = new Client( - { - name: "example-client", - version: "1.0.0", + server.addTool({ + description: "Add two numbers", + execute: async (args) => { + return String(args.a + args.b); }, - { - capabilities: {}, + name: "add", + parameters: z.object({ + a: z.number(), + b: z.number(), + }), + }); + + await server.start({ + httpStream: { + endpoint: "/another-mcp", + port, }, - ) + transportType: "httpStream", + }); - const transport = new StreamableHTTPClientTransport(new URL(`http://localhost:${port}/another-mcp`)) + try { + // Create client + const client = new Client( + { + name: "example-client", + version: "1.0.0", + }, + { + capabilities: {}, + }, + ); - // Connect client to server and wait for session to be ready - const sessionPromise = new Promise((resolve) => { - server.on("connect", async (event) => { - await event.session.waitForReady() - resolve(event.session) - }) - }) + const transport = new StreamableHTTPClientTransport( + new URL(`http://localhost:${port}/another-mcp`), + ); - await client.connect(transport) - await sessionPromise + // Connect client to server and wait for session to be ready + const sessionPromise = new Promise((resolve) => { + server.on("connect", async (event) => { + await event.session.waitForReady(); + resolve(event.session); + }); + }); - // Call tool - const result = await client.callTool({ - arguments: { - a: 5, - b: 7, - }, - name: "add", - }) + await client.connect(transport); + await sessionPromise; - // Check result - expect(result).toEqual({ - content: [{ text: "12", type: "text" }], - }) + // Call tool + const result = await client.callTool({ + arguments: { + a: 5, + b: 7, + }, + name: "add", + }); - // Clean up connection - await transport.terminateSession() - await client.close() - } finally { - await server.stop() - } -}) + // Check result + expect(result).toEqual({ + content: [{ text: "12", type: "text" }], + }); + + // Clean up connection + await transport.terminateSession(); + await client.close(); + } finally { + await server.stop(); + } + }, +); test("clients reads a resource accessed via a resource template", async () => { // eslint-disable-next-line @typescript-eslint/no-unused-vars const loadSpy = vi.fn((_args) => { return { text: "Example log content", - } - }) + }; + }); await runWithTestServer({ run: async ({ client }) => { @@ -2116,17 +2149,17 @@ test("clients reads a resource accessed via a resource template", async () => { uri: "file:///logs/app.log", }, ], - }) + }); expect(loadSpy).toHaveBeenCalledWith({ name: "app", - }) + }); }, server: async () => { const server = new FastMCP({ name: "Test", version: "1.0.0", - }) + }); server.addResourceTemplate({ arguments: [ @@ -2136,17 +2169,17 @@ test("clients reads a resource accessed via a resource template", async () => { }, ], async load(args) { - return loadSpy(args) + return loadSpy(args); }, mimeType: "text/plain", name: "Application Logs", uriTemplate: "file:///logs/{name}.log", - }) + }); - return server + return server; }, - }) -}) + }); +}); test("makes a sampling request", async () => { const onMessageRequest = vi.fn(() => { @@ -2157,8 +2190,8 @@ test("makes a sampling request", async () => { }, model: "gpt-3.5-turbo", role: "assistant", - } - }) + }; + }); await runWithTestServer({ client: async () => { @@ -2172,11 +2205,11 @@ test("makes a sampling request", async () => { sampling: {}, }, }, - ) - return client + ); + return client; }, run: async ({ client, session }) => { - client.setRequestHandler(CreateMessageRequestSchema, onMessageRequest) + client.setRequestHandler(CreateMessageRequestSchema, onMessageRequest); const response = await session.requestSampling({ includeContext: "thisServer", @@ -2191,7 +2224,7 @@ test("makes a sampling request", async () => { }, ], systemPrompt: "You are a helpful file system assistant.", - }) + }); expect(response).toEqual({ content: { @@ -2200,12 +2233,12 @@ test("makes a sampling request", async () => { }, model: "gpt-3.5-turbo", role: "assistant", - }) + }); - expect(onMessageRequest).toHaveBeenCalledTimes(1) + expect(onMessageRequest).toHaveBeenCalledTimes(1); }, - }) -}) + }); +}); test("throws ErrorCode.InvalidParams if tool parameters do not match zod schema", async () => { await runWithTestServer({ @@ -2217,41 +2250,41 @@ test("throws ErrorCode.InvalidParams if tool parameters do not match zod schema" b: "invalid", }, name: "add", - }) + }); } catch (error) { - expect(error).toBeInstanceOf(McpError) + expect(error).toBeInstanceOf(McpError); // @ts-expect-error - we know that error is an McpError - expect(error.code).toBe(ErrorCode.InvalidParams) + expect(error.code).toBe(ErrorCode.InvalidParams); // @ts-expect-error - we know that error is an McpError expect(error.message).toBe( "MCP error -32602: MCP error -32602: Tool 'add' parameter validation failed: b: Expected number, received string. Please check the parameter types and values according to the tool's schema.", - ) + ); } }, server: async () => { const server = new FastMCP({ name: "Test", version: "1.0.0", - }) + }); server.addTool({ description: "Add two numbers", execute: async (args) => { - return String(args.a + args.b) + return String(args.a + args.b); }, name: "add", parameters: z.object({ a: z.number(), b: z.number(), }), - }) + }); - return server + return server; }, - }) -}) + }); +}); test("server remains usable after InvalidParams error", async () => { await runWithTestServer({ @@ -2263,17 +2296,17 @@ test("server remains usable after InvalidParams error", async () => { b: "invalid", }, name: "add", - }) + }); } catch (error) { - expect(error).toBeInstanceOf(McpError) + expect(error).toBeInstanceOf(McpError); // @ts-expect-error - we know that error is an McpError - expect(error.code).toBe(ErrorCode.InvalidParams) + expect(error.code).toBe(ErrorCode.InvalidParams); // @ts-expect-error - we know that error is an McpError expect(error.message).toBe( "MCP error -32602: MCP error -32602: Tool 'add' parameter validation failed: b: Expected number, received string. Please check the parameter types and values according to the tool's schema.", - ) + ); } expect( @@ -2286,57 +2319,57 @@ test("server remains usable after InvalidParams error", async () => { }), ).toEqual({ content: [{ text: "3", type: "text" }], - }) + }); }, server: async () => { const server = new FastMCP({ name: "Test", version: "1.0.0", - }) + }); server.addTool({ description: "Add two numbers", execute: async (args) => { - return String(args.a + args.b) + return String(args.a + args.b); }, name: "add", parameters: z.object({ a: z.number(), b: z.number(), }), - }) + }); - return server + return server; }, - }) -}) + }); +}); test("allows new clients to connect after a client disconnects", async () => { - const port = await getRandomPort() + const port = await getRandomPort(); const server = new FastMCP({ name: "Test", version: "1.0.0", - }) + }); server.addTool({ description: "Add two numbers", execute: async (args) => { - return String(args.a + args.b) + return String(args.a + args.b); }, name: "add", parameters: z.object({ a: z.number(), b: z.number(), }), - }) + }); await server.start({ httpStream: { port, }, transportType: "httpStream", - }) + }); const client1 = new Client( { @@ -2346,11 +2379,13 @@ test("allows new clients to connect after a client disconnects", async () => { { capabilities: {}, }, - ) + ); - const transport1 = new SSEClientTransport(new URL(`http://localhost:${port}/sse`)) + const transport1 = new SSEClientTransport( + new URL(`http://localhost:${port}/sse`), + ); - await client1.connect(transport1) + await client1.connect(transport1); expect( await client1.callTool({ @@ -2362,9 +2397,9 @@ test("allows new clients to connect after a client disconnects", async () => { }), ).toEqual({ content: [{ text: "3", type: "text" }], - }) + }); - await client1.close() + await client1.close(); const client2 = new Client( { @@ -2374,11 +2409,13 @@ test("allows new clients to connect after a client disconnects", async () => { { capabilities: {}, }, - ) + ); - const transport2 = new SSEClientTransport(new URL(`http://localhost:${port}/sse`)) + const transport2 = new SSEClientTransport( + new URL(`http://localhost:${port}/sse`), + ); - await client2.connect(transport2) + await client2.connect(transport2); expect( await client2.callTool({ @@ -2390,52 +2427,52 @@ test("allows new clients to connect after a client disconnects", async () => { }), ).toEqual({ content: [{ text: "3", type: "text" }], - }) + }); - await client2.close() + await client2.close(); - await server.stop() -}) + await server.stop(); +}); test("able to close server immediately after starting it", async () => { - const port = await getRandomPort() + const port = await getRandomPort(); const server = new FastMCP({ name: "Test", version: "1.0.0", - }) + }); await server.start({ httpStream: { port, }, transportType: "httpStream", - }) + }); // We were previously not waiting for the server to start. // Therefore, this would have caused error 'Server is not running.'. - await server.stop() -}) + await server.stop(); +}); test("closing event source does not produce error", async () => { - const port = await getRandomPort() + const port = await getRandomPort(); const server = new FastMCP({ name: "Test", version: "1.0.0", - }) + }); server.addTool({ description: "Add two numbers", execute: async (args) => { - return String(args.a + args.b) + return String(args.a + args.b); }, name: "add", parameters: z.object({ a: z.number(), b: z.number(), }), - }) + }); await server.start({ httpStream: { @@ -2443,52 +2480,52 @@ test("closing event source does not produce error", async () => { port, }, transportType: "httpStream", - }) + }); const eventSource = await new Promise((onMessage) => { const eventSource = createEventSource({ onConnect: () => { - console.info("connected") + console.info("connected"); }, onDisconnect: () => { - console.info("disconnected") + console.info("disconnected"); }, onMessage: () => { - onMessage(eventSource) + onMessage(eventSource); }, url: `http://127.0.0.1:${port}/sse`, - }) - }) + }); + }); - expect(eventSource.readyState).toBe("open") + expect(eventSource.readyState).toBe("open"); - eventSource.close() + eventSource.close(); // We were getting unhandled error 'Not connected' // https://github.com/punkpeye/mcp-proxy/commit/62cf27d5e3dfcbc353e8d03c7714a62c37177b52 - await delay(1000) + await delay(1000); - await server.stop() -}) + await server.stop(); +}); test("provides auth to tools", async () => { - const port = await getRandomPort() + const port = await getRandomPort(); const authenticate = vi.fn(async () => { return { id: 1, - } - }) + }; + }); const server = new FastMCP<{ id: number }>({ authenticate, name: "Test", version: "1.0.0", - }) + }); const execute = vi.fn(async (args) => { - return String(args.a + args.b) - }) + return String(args.a + args.b); + }); server.addTool({ description: "Add two numbers", @@ -2498,14 +2535,14 @@ test("provides auth to tools", async () => { a: z.number(), b: z.number(), }), - }) + }); await server.start({ httpStream: { port, }, transportType: "httpStream", - }) + }); const client = new Client( { @@ -2515,25 +2552,31 @@ test("provides auth to tools", async () => { { capabilities: {}, }, - ) + ); - const transport = new SSEClientTransport(new URL(`http://localhost:${port}/sse`), { - eventSourceInit: { - fetch: async (url, init) => { - return fetch(url, { - ...init, - headers: { - ...init?.headers, - "x-api-key": "123", - }, - }) + const transport = new SSEClientTransport( + new URL(`http://localhost:${port}/sse`), + { + eventSourceInit: { + fetch: async (url, init) => { + return fetch(url, { + ...init, + headers: { + ...init?.headers, + "x-api-key": "123", + }, + }); + }, }, }, - }) + ); - await client.connect(transport) + await client.connect(transport); - expect(authenticate, "authenticate should have been called").toHaveBeenCalledTimes(1) + expect( + authenticate, + "authenticate should have been called", + ).toHaveBeenCalledTimes(1); expect( await client.callTool({ @@ -2545,9 +2588,9 @@ test("provides auth to tools", async () => { }), ).toEqual({ content: [{ text: "3", type: "text" }], - }) + }); - expect(execute, "execute should have been called").toHaveBeenCalledTimes(1) + expect(execute, "execute should have been called").toHaveBeenCalledTimes(1); expect(execute).toHaveBeenCalledWith( { @@ -2568,44 +2611,44 @@ test("provides auth to tools", async () => { sessionId: expect.any(String), streamContent: expect.any(Function), }, - ) -}) + ); +}); test("provides auth to resources", async () => { - const port = await getRandomPort() + const port = await getRandomPort(); const authenticate = vi.fn(async () => { return { role: "admin", userId: 42, - } - }) + }; + }); const server = new FastMCP<{ role: string; userId: number }>({ authenticate, name: "Test", version: "1.0.0", - }) + }); const resourceLoad = vi.fn(async (auth) => { return { text: `User ${auth?.userId} with role ${auth?.role} loaded this resource`, - } - }) + }; + }); server.addResource({ load: resourceLoad, mimeType: "text/plain", name: "Auth Resource", uri: "auth://resource", - }) + }); await server.start({ httpStream: { port, }, transportType: "httpStream", - }) + }); const client = new Client( { @@ -2615,33 +2658,36 @@ test("provides auth to resources", async () => { { capabilities: {}, }, - ) + ); - const transport = new SSEClientTransport(new URL(`http://localhost:${port}/sse`), { - eventSourceInit: { - fetch: async (url, init) => { - return fetch(url, { - ...init, - headers: { - ...init?.headers, - "x-api-key": "123", - }, - }) + const transport = new SSEClientTransport( + new URL(`http://localhost:${port}/sse`), + { + eventSourceInit: { + fetch: async (url, init) => { + return fetch(url, { + ...init, + headers: { + ...init?.headers, + "x-api-key": "123", + }, + }); + }, }, }, - }) + ); - await client.connect(transport) + await client.connect(transport); const result = await client.readResource({ uri: "auth://resource", - }) + }); - expect(resourceLoad).toHaveBeenCalledTimes(1) + expect(resourceLoad).toHaveBeenCalledTimes(1); expect(resourceLoad).toHaveBeenCalledWith({ role: "admin", userId: 42, - }) + }); expect(result).toEqual({ contents: [ @@ -2652,30 +2698,30 @@ test("provides auth to resources", async () => { uri: "auth://resource", }, ], - }) -}) + }); +}); test("provides auth to resource templates", async () => { - const port = await getRandomPort() + const port = await getRandomPort(); const authenticate = vi.fn(async () => { return { permissions: ["read", "write"], userId: 99, - } - }) + }; + }); const server = new FastMCP<{ permissions: string[]; userId: number }>({ authenticate, name: "Test", version: "1.0.0", - }) + }); const templateLoad = vi.fn(async (args, auth) => { return { text: `Resource ${args.resourceId} accessed by user ${auth?.userId} with permissions: ${auth?.permissions?.join(", ")}`, - } - }) + }; + }); server.addResourceTemplate({ arguments: [ @@ -2688,14 +2734,14 @@ test("provides auth to resource templates", async () => { mimeType: "text/plain", name: "Auth Template", uriTemplate: "auth://template/{resourceId}", - }) + }); await server.start({ httpStream: { port, }, transportType: "httpStream", - }) + }); const client = new Client( { @@ -2705,33 +2751,36 @@ test("provides auth to resource templates", async () => { { capabilities: {}, }, - ) + ); - const transport = new SSEClientTransport(new URL(`http://localhost:${port}/sse`), { - eventSourceInit: { - fetch: async (url, init) => { - return fetch(url, { - ...init, - headers: { - ...init?.headers, - "x-api-key": "123", - }, - }) + const transport = new SSEClientTransport( + new URL(`http://localhost:${port}/sse`), + { + eventSourceInit: { + fetch: async (url, init) => { + return fetch(url, { + ...init, + headers: { + ...init?.headers, + "x-api-key": "123", + }, + }); + }, }, }, - }) + ); - await client.connect(transport) + await client.connect(transport); const result = await client.readResource({ uri: "auth://template/resource-123", - }) + }); - expect(templateLoad).toHaveBeenCalledTimes(1) + expect(templateLoad).toHaveBeenCalledTimes(1); expect(templateLoad).toHaveBeenCalledWith( { resourceId: "resource-123" }, { permissions: ["read", "write"], userId: 99 }, - ) + ); expect(result).toEqual({ contents: [ @@ -2742,24 +2791,24 @@ test("provides auth to resource templates", async () => { uri: "auth://template/resource-123", }, ], - }) -}) + }); +}); test("provides auth to resource templates returning arrays", async () => { - const port = await getRandomPort() + const port = await getRandomPort(); const authenticate = vi.fn(async () => { return { accessLevel: 3, teamId: "team-alpha", - } - }) + }; + }); const server = new FastMCP<{ accessLevel: number; teamId: string }>({ authenticate, name: "Test", version: "1.0.0", - }) + }); const templateLoad = vi.fn(async (args, auth) => { return [ @@ -2769,8 +2818,8 @@ test("provides auth to resource templates returning arrays", async () => { { text: `Document 2 for ${args.category} - Access Level: ${auth?.accessLevel}`, }, - ] - }) + ]; + }); server.addResourceTemplate({ arguments: [ @@ -2783,14 +2832,14 @@ test("provides auth to resource templates returning arrays", async () => { mimeType: "text/plain", name: "Multi Doc Template", uriTemplate: "docs://category/{category}", - }) + }); await server.start({ httpStream: { port, }, transportType: "httpStream", - }) + }); const client = new Client( { @@ -2800,30 +2849,36 @@ test("provides auth to resource templates returning arrays", async () => { { capabilities: {}, }, - ) + ); - const transport = new SSEClientTransport(new URL(`http://localhost:${port}/sse`), { - eventSourceInit: { - fetch: async (url, init) => { - return fetch(url, { - ...init, - headers: { - ...init?.headers, - "x-api-key": "123", - }, - }) + const transport = new SSEClientTransport( + new URL(`http://localhost:${port}/sse`), + { + eventSourceInit: { + fetch: async (url, init) => { + return fetch(url, { + ...init, + headers: { + ...init?.headers, + "x-api-key": "123", + }, + }); + }, }, }, - }) + ); - await client.connect(transport) + await client.connect(transport); const result = await client.readResource({ uri: "docs://category/reports", - }) + }); - expect(templateLoad).toHaveBeenCalledTimes(1) - expect(templateLoad).toHaveBeenCalledWith({ category: "reports" }, { accessLevel: 3, teamId: "team-alpha" }) + expect(templateLoad).toHaveBeenCalledTimes(1); + expect(templateLoad).toHaveBeenCalledWith( + { category: "reports" }, + { accessLevel: 3, teamId: "team-alpha" }, + ); expect(result).toEqual({ contents: [ @@ -2840,30 +2895,33 @@ test("provides auth to resource templates returning arrays", async () => { uri: "docs://category/reports", }, ], - }) -}) + }); +}); test("provides auth to prompt argument completion", async () => { - const port = await getRandomPort() + const port = await getRandomPort(); const authenticate = vi.fn(async () => { return { department: "engineering", userId: 100, - } - }) + }; + }); const server = new FastMCP<{ department: string; userId: number }>({ authenticate, name: "Test", version: "1.0.0", - }) + }); const promptCompleter = vi.fn(async (value: string, auth) => { return { - values: [`${value}_user${auth?.userId}`, `${value}_dept${auth?.department}`], - } - }) + values: [ + `${value}_user${auth?.userId}`, + `${value}_dept${auth?.department}`, + ], + }; + }); server.addPrompt({ arguments: [ @@ -2875,17 +2933,17 @@ test("provides auth to prompt argument completion", async () => { }, ], async load(args) { - return `Loading project: ${args.project}` + return `Loading project: ${args.project}`; }, name: "load-project", - }) + }); await server.start({ httpStream: { port, }, transportType: "httpStream", - }) + }); const client = new Client( { @@ -2895,23 +2953,26 @@ test("provides auth to prompt argument completion", async () => { { capabilities: {}, }, - ) + ); - const transport = new SSEClientTransport(new URL(`http://localhost:${port}/sse`), { - eventSourceInit: { - fetch: async (url, init) => { - return fetch(url, { - ...init, - headers: { - ...init?.headers, - "x-api-key": "123", - }, - }) + const transport = new SSEClientTransport( + new URL(`http://localhost:${port}/sse`), + { + eventSourceInit: { + fetch: async (url, init) => { + return fetch(url, { + ...init, + headers: { + ...init?.headers, + "x-api-key": "123", + }, + }); + }, }, }, - }) + ); - await client.connect(transport) + await client.connect(transport); const completionResult = await client.complete({ argument: { @@ -2922,40 +2983,40 @@ test("provides auth to prompt argument completion", async () => { name: "load-project", type: "ref/prompt", }, - }) + }); - expect(promptCompleter).toHaveBeenCalledTimes(1) + expect(promptCompleter).toHaveBeenCalledTimes(1); expect(promptCompleter).toHaveBeenCalledWith("test", { department: "engineering", userId: 100, - }) + }); expect(completionResult).toEqual({ completion: { values: ["test_user100", "test_deptengineering"], }, - }) -}) + }); +}); test("provides auth to prompt load function", async () => { - const port = await getRandomPort() + const port = await getRandomPort(); const authenticate = vi.fn(async () => { return { level: "admin", username: "testuser", - } - }) + }; + }); const server = new FastMCP<{ level: string; username: string }>({ authenticate, name: "Test", version: "1.0.0", - }) + }); const promptLoad = vi.fn(async (args, auth) => { - return `Welcome ${auth?.username} (${auth?.level}): You selected ${args.option}` - }) + return `Welcome ${auth?.username} (${auth?.level}): You selected ${args.option}`; + }); server.addPrompt({ arguments: [ @@ -2967,14 +3028,14 @@ test("provides auth to prompt load function", async () => { ], load: promptLoad, name: "auth-prompt", - }) + }); await server.start({ httpStream: { port, }, transportType: "httpStream", - }) + }); const client = new Client( { @@ -2984,31 +3045,37 @@ test("provides auth to prompt load function", async () => { { capabilities: {}, }, - ) + ); - const transport = new SSEClientTransport(new URL(`http://localhost:${port}/sse`), { - eventSourceInit: { - fetch: async (url, init) => { - return fetch(url, { - ...init, - headers: { - ...init?.headers, - "x-api-key": "123", - }, - }) + const transport = new SSEClientTransport( + new URL(`http://localhost:${port}/sse`), + { + eventSourceInit: { + fetch: async (url, init) => { + return fetch(url, { + ...init, + headers: { + ...init?.headers, + "x-api-key": "123", + }, + }); + }, }, }, - }) + ); - await client.connect(transport) + await client.connect(transport); const result = await client.getPrompt({ arguments: { option: "dashboard" }, name: "auth-prompt", - }) + }); - expect(promptLoad).toHaveBeenCalledTimes(1) - expect(promptLoad).toHaveBeenCalledWith({ option: "dashboard" }, { level: "admin", username: "testuser" }) + expect(promptLoad).toHaveBeenCalledTimes(1); + expect(promptLoad).toHaveBeenCalledWith( + { option: "dashboard" }, + { level: "admin", username: "testuser" }, + ); expect(result).toEqual({ messages: [ @@ -3020,30 +3087,30 @@ test("provides auth to prompt load function", async () => { role: "user", }, ], - }) -}) + }); +}); test("provides auth to resource template argument completion", async () => { - const port = await getRandomPort() + const port = await getRandomPort(); const authenticate = vi.fn(async () => { return { region: "us-west", teamId: "alpha", - } - }) + }; + }); const server = new FastMCP<{ region: string; teamId: string }>({ authenticate, name: "Test", version: "1.0.0", - }) + }); const resourceCompleter = vi.fn(async (value: string, auth) => { return { values: [`${value}_${auth?.region}`, `${value}_team_${auth?.teamId}`], - } - }) + }; + }); server.addResourceTemplate({ arguments: [ @@ -3057,19 +3124,19 @@ test("provides auth to resource template argument completion", async () => { async load(args) { return { text: `Service ${args.serviceId} data`, - } + }; }, mimeType: "text/plain", name: "Service Resource", uriTemplate: "service://{serviceId}", - }) + }); await server.start({ httpStream: { port, }, transportType: "httpStream", - }) + }); const client = new Client( { @@ -3079,23 +3146,26 @@ test("provides auth to resource template argument completion", async () => { { capabilities: {}, }, - ) + ); - const transport = new SSEClientTransport(new URL(`http://localhost:${port}/sse`), { - eventSourceInit: { - fetch: async (url, init) => { - return fetch(url, { - ...init, - headers: { - ...init?.headers, - "x-api-key": "123", - }, - }) + const transport = new SSEClientTransport( + new URL(`http://localhost:${port}/sse`), + { + eventSourceInit: { + fetch: async (url, init) => { + return fetch(url, { + ...init, + headers: { + ...init?.headers, + "x-api-key": "123", + }, + }); + }, }, }, - }) + ); - await client.connect(transport) + await client.connect(transport); const completionResult = await client.complete({ argument: { @@ -3106,49 +3176,49 @@ test("provides auth to resource template argument completion", async () => { type: "ref/resource", uri: "service://{serviceId}", }, - }) + }); - expect(resourceCompleter).toHaveBeenCalledTimes(1) + expect(resourceCompleter).toHaveBeenCalledTimes(1); expect(resourceCompleter).toHaveBeenCalledWith("api", { region: "us-west", teamId: "alpha", - }) + }); expect(completionResult).toEqual({ completion: { values: ["api_us-west", "api_team_alpha"], }, - }) -}) + }); +}); test("supports streaming output from tools", async () => { - let streamResult: { content: Array<{ text: string; type: string }> } + let streamResult: { content: Array<{ text: string; type: string }> }; await runWithTestServer({ run: async ({ client }) => { const result = await client.callTool({ arguments: {}, name: "streaming-void-tool", - }) + }); expect(result).toEqual({ content: [], - }) + }); streamResult = (await client.callTool({ arguments: {}, name: "streaming-with-result", - })) as { content: Array<{ text: string; type: string }> } + })) as { content: Array<{ text: string; type: string }> }; expect(streamResult).toEqual({ content: [{ text: "Final result after streaming", type: "text" }], - }) + }); }, server: async () => { const server = new FastMCP({ name: "Test", version: "1.0.0", - }) + }); server.addTool({ annotations: { @@ -3159,19 +3229,19 @@ test("supports streaming output from tools", async () => { await context.streamContent({ text: "Streaming content 1", type: "text", - }) + }); await context.streamContent({ text: "Streaming content 2", type: "text", - }) + }); // Return void - return + return; }, name: "streaming-void-tool", parameters: z.object({}), - }) + }); server.addTool({ annotations: { @@ -3182,44 +3252,44 @@ test("supports streaming output from tools", async () => { await context.streamContent({ text: "Streaming content 1", type: "text", - }) + }); await context.streamContent({ text: "Streaming content 2", type: "text", - }) + }); - return "Final result after streaming" + return "Final result after streaming"; }, name: "streaming-with-result", parameters: z.object({}), - }) + }); - return server + return server; }, - }) -}) + }); +}); test("blocks unauthorized requests", async () => { - const port = await getRandomPort() + const port = await getRandomPort(); const server = new FastMCP<{ id: number }>({ authenticate: async () => { throw new Response(null, { status: 401, statusText: "Unauthorized", - }) + }); }, name: "Test", version: "1.0.0", - }) + }); await server.start({ httpStream: { port, }, transportType: "httpStream", - }) + }); const client = new Client( { @@ -3229,175 +3299,202 @@ test("blocks unauthorized requests", async () => { { capabilities: {}, }, - ) + ); - const transport = new SSEClientTransport(new URL(`http://localhost:${port}/sse`)) + const transport = new SSEClientTransport( + new URL(`http://localhost:${port}/sse`), + ); expect(async () => { - await client.connect(transport) - }).rejects.toThrow("SSE error: Non-200 status code (401)") -}) + await client.connect(transport); + }).rejects.toThrow("SSE error: Non-200 status code (401)"); +}); test("filters tools based on canAccess property", async () => { - const port = await getRandomPort() + const port = await getRandomPort(); const server = new FastMCP<{ role: string }>({ authenticate: async (request) => { - const role = request.headers["x-role"] as string - return { role: role || "user" } + const role = request.headers["x-role"] as string; + return { role: role || "user" }; }, name: "Test", version: "1.0.0", - }) + }); server.addTool({ canAccess: (auth) => auth?.role === "admin", description: "Admin only", execute: async () => "admin", name: "admin-tool", - }) + }); server.addTool({ description: "Available to all", execute: async () => "public", name: "public-tool", - }) + }); - await server.start({ httpStream: { port }, transportType: "httpStream" }) + await server.start({ httpStream: { port }, transportType: "httpStream" }); try { // Admin gets both tools - const adminClient = new Client({ name: "admin", version: "1.0.0" }, { capabilities: {} }) - const adminTransport = new SSEClientTransport(new URL(`http://localhost:${port}/sse`), { - eventSourceInit: { - fetch: (url, init) => - fetch(url, { - ...init, - headers: { ...init?.headers, "x-role": "admin" }, - }), + const adminClient = new Client( + { name: "admin", version: "1.0.0" }, + { capabilities: {} }, + ); + const adminTransport = new SSEClientTransport( + new URL(`http://localhost:${port}/sse`), + { + eventSourceInit: { + fetch: (url, init) => + fetch(url, { + ...init, + headers: { ...init?.headers, "x-role": "admin" }, + }), + }, }, - }) - await adminClient.connect(adminTransport) + ); + await adminClient.connect(adminTransport); - const adminTools = await adminClient.listTools() - expect(adminTools.tools.map((t) => t.name).sort()).toEqual(["admin-tool", "public-tool"]) + const adminTools = await adminClient.listTools(); + expect(adminTools.tools.map((t) => t.name).sort()).toEqual([ + "admin-tool", + "public-tool", + ]); // User gets only public tool - const userClient = new Client({ name: "user", version: "1.0.0" }, { capabilities: {} }) - const userTransport = new SSEClientTransport(new URL(`http://localhost:${port}/sse`), { - eventSourceInit: { - fetch: (url, init) => - fetch(url, { - ...init, - headers: { ...init?.headers, "x-role": "user" }, - }), + const userClient = new Client( + { name: "user", version: "1.0.0" }, + { capabilities: {} }, + ); + const userTransport = new SSEClientTransport( + new URL(`http://localhost:${port}/sse`), + { + eventSourceInit: { + fetch: (url, init) => + fetch(url, { + ...init, + headers: { ...init?.headers, "x-role": "user" }, + }), + }, }, - }) - await userClient.connect(userTransport) + ); + await userClient.connect(userTransport); - const userTools = await userClient.listTools() - expect(userTools.tools.map((t) => t.name)).toEqual(["public-tool"]) + const userTools = await userClient.listTools(); + expect(userTools.tools.map((t) => t.name)).toEqual(["public-tool"]); - await adminClient.close() - await userClient.close() + await adminClient.close(); + await userClient.close(); } finally { - await server.stop() + await server.stop(); } -}) +}); test("tools without canAccess are accessible to all", async () => { await runWithTestServer({ run: async ({ client }) => { - const tools = await client.listTools() - expect(tools.tools).toHaveLength(1) - expect(tools.tools[0].name).toBe("test-tool") + const tools = await client.listTools(); + expect(tools.tools).toHaveLength(1); + expect(tools.tools[0].name).toBe("test-tool"); const result = await client.callTool({ arguments: {}, name: "test-tool", - }) - expect((result.content as Array<{ text: string; type: string }>)[0]).toEqual({ text: "success", type: "text" }) + }); + expect( + (result.content as Array<{ text: string; type: string }>)[0], + ).toEqual({ text: "success", type: "text" }); }, server: async () => { - const server = new FastMCP({ name: "Test", version: "1.0.0" }) + const server = new FastMCP({ name: "Test", version: "1.0.0" }); server.addTool({ description: "Test tool", execute: async () => "success", name: "test-tool", - }) - return server + }); + return server; }, - }) -}) + }); +}); test("canAccess works without authentication", async () => { - const port = await getRandomPort() + const port = await getRandomPort(); const server = new FastMCP<{ role: string }>({ name: "Test", version: "1.0.0", - }) + }); server.addTool({ canAccess: (auth) => auth?.role === "admin", execute: async () => "admin", name: "admin-tool", - }) + }); server.addTool({ execute: async () => "public", name: "public-tool", - }) + }); - await server.start({ httpStream: { port }, transportType: "httpStream" }) + await server.start({ httpStream: { port }, transportType: "httpStream" }); try { - const client = new Client({ name: "test-client", version: "1.0.0" }, { capabilities: {} }) - const transport = new SSEClientTransport(new URL(`http://localhost:${port}/sse`)) - await client.connect(transport) - - const tools = await client.listTools() - expect(tools.tools.map((t) => t.name).sort()).toEqual(["admin-tool", "public-tool"]) - - await client.close() + const client = new Client( + { name: "test-client", version: "1.0.0" }, + { capabilities: {} }, + ); + const transport = new SSEClientTransport( + new URL(`http://localhost:${port}/sse`), + ); + await client.connect(transport); + + const tools = await client.listTools(); + expect(tools.tools.map((t) => t.name).sort()).toEqual([ + "admin-tool", + "public-tool", + ]); + + await client.close(); } finally { - await server.stop() + await server.stop(); } -}) +}); // We now use a direct approach for testing HTTP Stream functionality // rather than a helper function // Set longer timeout for HTTP Stream tests test("HTTP Stream: calls a tool", { timeout: 20000 }, async () => { - console.log("Starting HTTP Stream test...") + console.log("Starting HTTP Stream test..."); - const port = await getRandomPort() + const port = await getRandomPort(); // Create server directly (don't use helper function) const server = new FastMCP({ name: "Test", version: "1.0.0", - }) + }); server.addTool({ description: "Add two numbers", execute: async (args) => { - return String(args.a + args.b) + return String(args.a + args.b); }, name: "add", parameters: z.object({ a: z.number(), b: z.number(), }), - }) + }); await server.start({ httpStream: { port, }, transportType: "httpStream", - }) + }); try { // Create client @@ -3409,22 +3506,24 @@ test("HTTP Stream: calls a tool", { timeout: 20000 }, async () => { { capabilities: {}, }, - ) + ); // IMPORTANT: Don't provide sessionId manually with HTTP streaming // The server will generate a session ID automatically - const transport = new StreamableHTTPClientTransport(new URL(`http://localhost:${port}/mcp`)) + const transport = new StreamableHTTPClientTransport( + new URL(`http://localhost:${port}/mcp`), + ); // Connect client to server and wait for session to be ready const sessionPromise = new Promise((resolve) => { server.on("connect", async (event) => { - await event.session.waitForReady() - resolve(event.session) - }) - }) + await event.session.waitForReady(); + resolve(event.session); + }); + }); - await client.connect(transport) - await sessionPromise + await client.connect(transport); + await sessionPromise; // Call tool const result = await client.callTool({ @@ -3433,21 +3532,21 @@ test("HTTP Stream: calls a tool", { timeout: 20000 }, async () => { b: 2, }, name: "add", - }) + }); // Check result expect(result).toEqual({ content: [{ text: "3", type: "text" }], - }) + }); // Clean up connection - await transport.terminateSession() + await transport.terminateSession(); - await client.close() + await client.close(); } finally { - await server.stop() + await server.stop(); } -}) +}); test("uses `formatInvalidParamsErrorMessage` callback to build ErrorCode.InvalidParams error message", async () => { await runWithTestServer({ @@ -3459,17 +3558,17 @@ test("uses `formatInvalidParamsErrorMessage` callback to build ErrorCode.Invalid b: "invalid", }, name: "add", - }) + }); } catch (error) { - expect(error).toBeInstanceOf(McpError) + expect(error).toBeInstanceOf(McpError); // @ts-expect-error - we know that error is an McpError - expect(error.code).toBe(ErrorCode.InvalidParams) + expect(error.code).toBe(ErrorCode.InvalidParams); // @ts-expect-error - we know that error is an McpError expect(error.message).toBe( `MCP error -32602: MCP error -32602: Tool 'add' parameter validation failed: My custom error message: Field b failed with error 'Expected number, received string'. Please check the parameter types and values according to the tool's schema.`, - ) + ); } }, server: async () => { @@ -3479,52 +3578,52 @@ test("uses `formatInvalidParamsErrorMessage` callback to build ErrorCode.Invalid formatInvalidParamsErrorMessage: (issues) => { const message = issues .map((issue) => { - const path = issue.path?.join(".") || "root" - return `Field ${path} failed with error '${issue.message}'` + const path = issue.path?.join(".") || "root"; + return `Field ${path} failed with error '${issue.message}'`; }) - .join(", ") - return `My custom error message: ${message}` + .join(", "); + return `My custom error message: ${message}`; }, }, version: "1.0.0", - }) + }); server.addTool({ description: "Add two numbers", execute: async (args) => { - return String(args.a + args.b) + return String(args.a + args.b); }, name: "add", parameters: z.object({ a: z.number(), b: z.number(), }), - }) + }); - return server + return server; }, - }) -}) + }); +}); test("stateless mode works correctly", async () => { - const port = await getRandomPort() + const port = await getRandomPort(); const server = new FastMCP({ name: "Test server", version: "1.0.0", - }) + }); server.addTool({ description: "Add two numbers", execute: async (args) => { - return String(args.a + args.b) + return String(args.a + args.b); }, name: "add", parameters: z.object({ a: z.number(), b: z.number(), }), - }) + }); await server.start({ httpStream: { @@ -3532,7 +3631,7 @@ test("stateless mode works correctly", async () => { stateless: true, }, transportType: "httpStream", - }) + }); try { const client = new Client( @@ -3543,54 +3642,56 @@ test("stateless mode works correctly", async () => { { capabilities: {}, }, - ) + ); - const transport = new StreamableHTTPClientTransport(new URL(`http://localhost:${port}/mcp`)) + const transport = new StreamableHTTPClientTransport( + new URL(`http://localhost:${port}/mcp`), + ); - await client.connect(transport) + await client.connect(transport); // Tool call should work in stateless mode const result = await client.callTool({ arguments: { a: 5, b: 7 }, name: "add", - }) + }); expect(result.content).toEqual([ { text: "12", type: "text", }, - ]) + ]); // Multiple calls should work independently in stateless mode const result2 = await client.callTool({ arguments: { a: 10, b: 20 }, name: "add", - }) + }); expect(result2.content).toEqual([ { text: "30", type: "text", }, - ]) + ]); // Server should not track sessions in stateless mode - expect(server.sessions.length).toBe(0) + expect(server.sessions.length).toBe(0); - await client.close() + await client.close(); } finally { - await server.stop() + await server.stop(); } -}) +}); test("stateless mode health check includes mode indicator", async () => { - const port = await getRandomPort() + const port = await getRandomPort(); const server = new FastMCP({ name: "Test server", version: "1.0.0", - }) + }); await server.start({ httpStream: { @@ -3598,44 +3699,44 @@ test("stateless mode health check includes mode indicator", async () => { stateless: true, }, transportType: "httpStream", - }) + }); try { - const response = await fetch(`http://localhost:${port}/ready`) - expect(response.status).toBe(200) + const response = await fetch(`http://localhost:${port}/ready`); + expect(response.status).toBe(200); - const json = await response.json() + const json = await response.json(); expect(json).toEqual({ mode: "stateless", ready: 1, status: "ready", total: 1, - }) + }); } finally { - await server.stop() + await server.stop(); } -}) +}); test("stateless mode with valid authentication allows access", async () => { - const port = await getRandomPort() + const port = await getRandomPort(); const server = new FastMCP<{ userId: string }>({ authenticate: async () => { // Always authenticate successfully for this test - return { userId: "123" } + return { userId: "123" }; }, name: "Test server", version: "1.0.0", - }) + }); server.addTool({ description: "Test tool", execute: async () => { - return "pong" + return "pong"; }, name: "ping", parameters: z.object({}), - }) + }); await server.start({ httpStream: { @@ -3643,7 +3744,7 @@ test("stateless mode with valid authentication allows access", async () => { stateless: true, }, transportType: "httpStream", - }) + }); try { const client = new Client( @@ -3654,61 +3755,63 @@ test("stateless mode with valid authentication allows access", async () => { { capabilities: {}, }, - ) + ); - const transport = new StreamableHTTPClientTransport(new URL(`http://localhost:${port}/mcp`)) + const transport = new StreamableHTTPClientTransport( + new URL(`http://localhost:${port}/mcp`), + ); - await client.connect(transport) + await client.connect(transport); const result = await client.callTool({ arguments: {}, name: "ping", - }) + }); expect(result.content).toEqual([ { text: "pong", type: "text", }, - ]) + ]); // Server should not track sessions in stateless mode - expect(server.sessions.length).toBe(0) + expect(server.sessions.length).toBe(0); - await client.close() + await client.close(); } finally { - await server.stop() + await server.stop(); } -}) +}); test("stateless mode rejects missing Authorization header", async () => { - const port = await getRandomPort() + const port = await getRandomPort(); const server = new FastMCP<{ userId: string }>({ authenticate: async (req) => { - const authHeader = req.headers.authorization + const authHeader = req.headers.authorization; if (!authHeader || !authHeader.startsWith("Bearer ")) { throw new Response(null, { status: 401, statusText: "Unauthorized", - }) + }); } - return { userId: "123" } + return { userId: "123" }; }, name: "Test server", version: "1.0.0", - }) + }); server.addTool({ description: "Test tool", execute: async () => { - return "pong" + return "pong"; }, name: "ping", parameters: z.object({}), - }) + }); await server.start({ httpStream: { @@ -3716,7 +3819,7 @@ test("stateless mode rejects missing Authorization header", async () => { stateless: true, }, transportType: "httpStream", - }) + }); try { // Send a raw HTTP request without Authorization header @@ -3734,56 +3837,56 @@ test("stateless mode rejects missing Authorization header", async () => { "Content-Type": "application/json", }, method: "POST", - }) + }); - expect(response.status).toBe(401) + expect(response.status).toBe(401); - const body = (await response.json()) as { error?: { message?: string } } - expect(body.error?.message).toContain("Unauthorized") + const body = (await response.json()) as { error?: { message?: string } }; + expect(body.error?.message).toContain("Unauthorized"); } finally { - await server.stop() + await server.stop(); } -}) +}); test("stateless mode rejects invalid authentication token", async () => { - const port = await getRandomPort() - const VALID_TOKEN = "valid_jwt_token" - const INVALID_TOKEN = "invalid_jwt_token" + const port = await getRandomPort(); + const VALID_TOKEN = "valid_jwt_token"; + const INVALID_TOKEN = "invalid_jwt_token"; const server = new FastMCP<{ userId: string }>({ authenticate: async (req) => { - const authHeader = req.headers.authorization + const authHeader = req.headers.authorization; if (!authHeader || !authHeader.startsWith("Bearer ")) { throw new Response(null, { status: 401, statusText: "Unauthorized", - }) + }); } - const token = authHeader.split(" ")[1] + const token = authHeader.split(" ")[1]; if (token === VALID_TOKEN) { - return { userId: "123" } + return { userId: "123" }; } throw new Response(null, { status: 401, statusText: "Unauthorized", - }) + }); }, name: "Test server", version: "1.0.0", - }) + }); server.addTool({ description: "Test tool", execute: async () => { - return "pong" + return "pong"; }, name: "ping", parameters: z.object({}), - }) + }); await server.start({ httpStream: { @@ -3791,7 +3894,7 @@ test("stateless mode rejects invalid authentication token", async () => { stateless: true, }, transportType: "httpStream", - }) + }); try { // Send a raw HTTP request with invalid token @@ -3810,37 +3913,37 @@ test("stateless mode rejects invalid authentication token", async () => { "Content-Type": "application/json", }, method: "POST", - }) + }); - expect(response.status).toBe(401) + expect(response.status).toBe(401); - const body = (await response.json()) as { error?: { message?: string } } - expect(body.error?.message).toContain("Unauthorized") + const body = (await response.json()) as { error?: { message?: string } }; + expect(body.error?.message).toContain("Unauthorized"); } finally { - await server.stop() + await server.stop(); } -}) +}); test("stateless mode handles authentication function throwing errors", async () => { - const port = await getRandomPort() + const port = await getRandomPort(); const server = new FastMCP<{ userId: string }>({ authenticate: async () => { // Simulate an internal error during token validation - throw new Error("JWT validation service is down") + throw new Error("JWT validation service is down"); }, name: "Test server", version: "1.0.0", - }) + }); server.addTool({ description: "Test tool", execute: async () => { - return "pong" + return "pong"; }, name: "ping", parameters: z.object({}), - }) + }); await server.start({ httpStream: { @@ -3848,7 +3951,7 @@ test("stateless mode handles authentication function throwing errors", async () stateless: true, }, transportType: "httpStream", - }) + }); try { // Send a raw HTTP request @@ -3867,40 +3970,40 @@ test("stateless mode handles authentication function throwing errors", async () "Content-Type": "application/json", }, method: "POST", - }) + }); - expect(response.status).toBe(401) + expect(response.status).toBe(401); - const body = (await response.json()) as { error?: { message?: string } } + const body = (await response.json()) as { error?: { message?: string } }; // The actual error message should be passed through - expect(body.error?.message).toContain("JWT validation service is down") + expect(body.error?.message).toContain("JWT validation service is down"); } finally { - await server.stop() + await server.stop(); } -}) +}); test("stateless mode handles concurrent requests with authentication", async () => { - const port = await getRandomPort() - let requestCount = 0 + const port = await getRandomPort(); + let requestCount = 0; const server = new FastMCP<{ requestId: number }>({ authenticate: async () => { // Track each authentication request - requestCount++ - return { requestId: requestCount } + requestCount++; + return { requestId: requestCount }; }, name: "Test server", version: "1.0.0", - }) + }); server.addTool({ description: "Echo request ID", execute: async (_args, context) => { - return `Request ${context.session?.requestId}` + return `Request ${context.session?.requestId}`; }, name: "whoami", parameters: z.object({}), - }) + }); await server.start({ httpStream: { @@ -3908,7 +4011,7 @@ test("stateless mode handles concurrent requests with authentication", async () stateless: true, }, transportType: "httpStream", - }) + }); try { // Create two clients to test concurrent stateless requests @@ -3920,7 +4023,7 @@ test("stateless mode handles concurrent requests with authentication", async () { capabilities: {}, }, - ) + ); const client2 = new Client( { @@ -3930,63 +4033,67 @@ test("stateless mode handles concurrent requests with authentication", async () { capabilities: {}, }, - ) + ); - const transport1 = new StreamableHTTPClientTransport(new URL(`http://localhost:${port}/mcp`)) + const transport1 = new StreamableHTTPClientTransport( + new URL(`http://localhost:${port}/mcp`), + ); - const transport2 = new StreamableHTTPClientTransport(new URL(`http://localhost:${port}/mcp`)) + const transport2 = new StreamableHTTPClientTransport( + new URL(`http://localhost:${port}/mcp`), + ); - await client1.connect(transport1) - await client2.connect(transport2) + await client1.connect(transport1); + await client2.connect(transport2); // Both clients should work independently const result1 = await client1.callTool({ arguments: {}, name: "whoami", - }) + }); const result2 = await client2.callTool({ arguments: {}, name: "whoami", - }) + }); // Each request should have been authenticated - expect((result1.content as unknown[])[0]).toHaveProperty("text") - expect((result2.content as unknown[])[0]).toHaveProperty("text") + expect((result1.content as unknown[])[0]).toHaveProperty("text"); + expect((result2.content as unknown[])[0]).toHaveProperty("text"); // Server should not track sessions in stateless mode - expect(server.sessions.length).toBe(0) + expect(server.sessions.length).toBe(0); - await client1.close() - await client2.close() + await client1.close(); + await client2.close(); } finally { - await server.stop() + await server.stop(); } -}) +}); // Tests for GitHub Issue: FastMCP authentication fix // Testing the fix for session creation despite authentication failure test("authentication failure handling: should throw error when auth.authenticated is false", async () => { - const port = await getRandomPort() + const port = await getRandomPort(); const server = new FastMCP<{ authenticated: boolean; error?: string }>({ authenticate: async () => { // Simulate authentication failure with { authenticated: false } - return { authenticated: false, error: "Invalid JWT token" } + return { authenticated: false, error: "Invalid JWT token" }; }, name: "Test server", version: "1.0.0", - }) + }); server.addTool({ description: "Test tool", execute: async () => { - return "pong" + return "pong"; }, name: "ping", parameters: z.object({}), - }) + }); await server.start({ httpStream: { @@ -3994,7 +4101,7 @@ test("authentication failure handling: should throw error when auth.authenticate stateless: true, }, transportType: "httpStream", - }) + }); try { // Send a raw HTTP request that should be rejected @@ -4015,43 +4122,43 @@ test("authentication failure handling: should throw error when auth.authenticate "Content-Type": "application/json", }, method: "POST", - }) + }); // Should return 401 Unauthorized (handled by mcp-proxy) - expect(response.status).toBe(401) + expect(response.status).toBe(401); const body = (await response.json()) as { - error?: { code?: number; message?: string } - } - expect(body.error?.message).toContain("Invalid JWT token") + error?: { code?: number; message?: string }; + }; + expect(body.error?.message).toContain("Invalid JWT token"); } finally { - await server.stop() + await server.stop(); } -}) +}); test("authentication failure handling: should create session when auth.authenticated is true", async () => { - const port = await getRandomPort() + const port = await getRandomPort(); const server = new FastMCP<{ - authenticated: boolean - session?: { userId: string } + authenticated: boolean; + session?: { userId: string }; }>({ authenticate: async () => { // Simulate successful authentication - return { authenticated: true, session: { userId: "123" } } + return { authenticated: true, session: { userId: "123" } }; }, name: "Test server", version: "1.0.0", - }) + }); server.addTool({ description: "Test tool", execute: async (_args, context) => { - return `User: ${context.session?.session?.userId}` + return `User: ${context.session?.session?.userId}`; }, name: "whoami", parameters: z.object({}), - }) + }); await server.start({ httpStream: { @@ -4059,7 +4166,7 @@ test("authentication failure handling: should create session when auth.authentic stateless: true, }, transportType: "httpStream", - }) + }); try { const client = new Client( @@ -4070,47 +4177,49 @@ test("authentication failure handling: should create session when auth.authentic { capabilities: {}, }, - ) + ); - const transport = new StreamableHTTPClientTransport(new URL(`http://localhost:${port}/mcp`)) + const transport = new StreamableHTTPClientTransport( + new URL(`http://localhost:${port}/mcp`), + ); - await client.connect(transport) + await client.connect(transport); const result = await client.callTool({ arguments: {}, name: "whoami", - }) + }); expect(result.content).toEqual([ { text: "User: 123", type: "text", }, - ]) + ]); - await client.close() + await client.close(); } finally { - await server.stop() + await server.stop(); } -}) +}); test("authentication failure handling: should create session when auth is null/undefined (anonymous)", async () => { - const port = await getRandomPort() + const port = await getRandomPort(); const server = new FastMCP({ // No authenticate function - anonymous access name: "Test server", version: "1.0.0", - }) + }); server.addTool({ description: "Test tool", execute: async (_args, context) => { - return `Anonymous: ${context.session === undefined}` + return `Anonymous: ${context.session === undefined}`; }, name: "ping", parameters: z.object({}), - }) + }); await server.start({ httpStream: { @@ -4118,7 +4227,7 @@ test("authentication failure handling: should create session when auth is null/u stateless: true, }, transportType: "httpStream", - }) + }); try { const client = new Client( @@ -4129,50 +4238,52 @@ test("authentication failure handling: should create session when auth is null/u { capabilities: {}, }, - ) + ); - const transport = new StreamableHTTPClientTransport(new URL(`http://localhost:${port}/mcp`)) + const transport = new StreamableHTTPClientTransport( + new URL(`http://localhost:${port}/mcp`), + ); - await client.connect(transport) + await client.connect(transport); const result = await client.callTool({ arguments: {}, name: "ping", - }) + }); expect(result.content).toEqual([ { text: "Anonymous: true", type: "text", }, - ]) + ]); - await client.close() + await client.close(); } finally { - await server.stop() + await server.stop(); } -}) +}); test("authentication failure handling: should use default error message when auth.error is not provided", async () => { - const port = await getRandomPort() + const port = await getRandomPort(); const server = new FastMCP<{ authenticated: boolean }>({ authenticate: async () => { // Return authenticated: false without custom error message - return { authenticated: false } + return { authenticated: false }; }, name: "Test server", version: "1.0.0", - }) + }); server.addTool({ description: "Test tool", execute: async () => { - return "pong" + return "pong"; }, name: "ping", parameters: z.object({}), - }) + }); await server.start({ httpStream: { @@ -4180,7 +4291,7 @@ test("authentication failure handling: should use default error message when aut stateless: true, }, transportType: "httpStream", - }) + }); try { const response = await fetch(`http://localhost:${port}/mcp`, { @@ -4199,39 +4310,39 @@ test("authentication failure handling: should use default error message when aut "Content-Type": "application/json", }, method: "POST", - }) + }); - expect(response.status).toBe(401) + expect(response.status).toBe(401); const body = (await response.json()) as { - error?: { message?: string } - } - expect(body.error?.message).toContain("Authentication failed") + error?: { message?: string }; + }; + expect(body.error?.message).toContain("Authentication failed"); } finally { - await server.stop() + await server.stop(); } -}) +}); test("authentication failure handling: should preserve existing behavior for truthy auth results", async () => { - const port = await getRandomPort() + const port = await getRandomPort(); const server = new FastMCP<{ role: string; userId: string }>({ authenticate: async () => { // Return a truthy object without 'authenticated' field (legacy pattern) - return { role: "admin", userId: "456" } + return { role: "admin", userId: "456" }; }, name: "Test server", version: "1.0.0", - }) + }); server.addTool({ description: "Test tool", execute: async (_args, context) => { - return `User: ${context.session?.userId}, Role: ${context.session?.role}` + return `User: ${context.session?.userId}, Role: ${context.session?.role}`; }, name: "whoami", parameters: z.object({}), - }) + }); await server.start({ httpStream: { @@ -4239,7 +4350,7 @@ test("authentication failure handling: should preserve existing behavior for tru stateless: true, }, transportType: "httpStream", - }) + }); try { const client = new Client( @@ -4250,50 +4361,52 @@ test("authentication failure handling: should preserve existing behavior for tru { capabilities: {}, }, - ) + ); - const transport = new StreamableHTTPClientTransport(new URL(`http://localhost:${port}/mcp`)) + const transport = new StreamableHTTPClientTransport( + new URL(`http://localhost:${port}/mcp`), + ); - await client.connect(transport) + await client.connect(transport); const result = await client.callTool({ arguments: {}, name: "whoami", - }) + }); expect(result.content).toEqual([ { text: "User: 456, Role: admin", type: "text", }, - ]) + ]); - await client.close() + await client.close(); } finally { - await server.stop() + await server.stop(); } -}) +}); test("authentication failure handling: should handle authentication with custom error messages", async () => { - const port = await getRandomPort() - const CUSTOM_ERROR_MSG = "Token expired at 2025-10-07T12:00:00Z" + const port = await getRandomPort(); + const CUSTOM_ERROR_MSG = "Token expired at 2025-10-07T12:00:00Z"; const server = new FastMCP<{ authenticated: boolean; error?: string }>({ authenticate: async () => { - return { authenticated: false, error: CUSTOM_ERROR_MSG } + return { authenticated: false, error: CUSTOM_ERROR_MSG }; }, name: "Test server", version: "1.0.0", - }) + }); server.addTool({ description: "Test tool", execute: async () => { - return "pong" + return "pong"; }, name: "ping", parameters: z.object({}), - }) + }); await server.start({ httpStream: { @@ -4301,7 +4414,7 @@ test("authentication failure handling: should handle authentication with custom stateless: true, }, transportType: "httpStream", - }) + }); try { const response = await fetch(`http://localhost:${port}/mcp`, { @@ -4320,26 +4433,26 @@ test("authentication failure handling: should handle authentication with custom "Content-Type": "application/json", }, method: "POST", - }) + }); - expect(response.status).toBe(401) + expect(response.status).toBe(401); const body = (await response.json()) as { - error?: { message?: string } - } - expect(body.error?.message).toBe(CUSTOM_ERROR_MSG) + error?: { message?: string }; + }; + expect(body.error?.message).toBe(CUSTOM_ERROR_MSG); } finally { - await server.stop() + await server.stop(); } -}) +}); test("authentication failure handling: should not create session for authenticated=false even with session data", async () => { - const port = await getRandomPort() + const port = await getRandomPort(); const server = new FastMCP<{ - authenticated: boolean - error?: string - session?: { userId: string } + authenticated: boolean; + error?: string; + session?: { userId: string }; }>({ authenticate: async () => { // Even if session data is present, authenticated: false should reject @@ -4347,20 +4460,20 @@ test("authentication failure handling: should not create session for authenticat authenticated: false, error: "Insufficient permissions", session: { userId: "hacker" }, - } + }; }, name: "Test server", version: "1.0.0", - }) + }); server.addTool({ description: "Test tool", execute: async () => { - return "pong" + return "pong"; }, name: "ping", parameters: z.object({}), - }) + }); await server.start({ httpStream: { @@ -4368,7 +4481,7 @@ test("authentication failure handling: should not create session for authenticat stateless: true, }, transportType: "httpStream", - }) + }); try { const response = await fetch(`http://localhost:${port}/mcp`, { @@ -4387,29 +4500,29 @@ test("authentication failure handling: should not create session for authenticat "Content-Type": "application/json", }, method: "POST", - }) + }); - expect(response.status).toBe(401) + expect(response.status).toBe(401); const body = (await response.json()) as { - error?: { message?: string } - } - expect(body.error?.message).toContain("Insufficient permissions") + error?: { message?: string }; + }; + expect(body.error?.message).toContain("Insufficient permissions"); // Verify session was never created - expect(server.sessions.length).toBe(0) + expect(server.sessions.length).toBe(0); } finally { - await server.stop() + await server.stop(); } -}) +}); test("host configuration works with 0.0.0.0", async () => { - const port = await getRandomPort() + const port = await getRandomPort(); const server = new FastMCP({ name: "Test server", version: "1.0.0", - }) + }); await server.start({ httpStream: { @@ -4417,50 +4530,50 @@ test("host configuration works with 0.0.0.0", async () => { port, }, transportType: "httpStream", - }) + }); try { - const healthResponse = await fetch(`http://0.0.0.0:${port}/health`) - expect(healthResponse.status).toBe(200) - expect(await healthResponse.text()).toBe("✓ Ok") + const healthResponse = await fetch(`http://0.0.0.0:${port}/health`); + expect(healthResponse.status).toBe(200); + expect(await healthResponse.text()).toBe("✓ Ok"); } finally { - await server.stop() + await server.stop(); } -}) +}); test("tools can access client info", async () => { await runWithTestServer({ run: async ({ client }) => { const result = (await client.callTool({ name: "get-client-info", - })) as ContentResult + })) as ContentResult; - expect(result.content).toHaveLength(1) - expect(result.content[0]).toHaveProperty("type", "text") + expect(result.content).toHaveLength(1); + expect(result.content[0]).toHaveProperty("type", "text"); - const text = (result.content[0] as TextContent).text - expect(text).toContain("Client name:") - expect(text).toContain("Client version:") + const text = (result.content[0] as TextContent).text; + expect(text).toContain("Client name:"); + expect(text).toContain("Client version:"); // The client info should contain some actual client information - expect(text).toMatch(/Client name:\s+\w+/) - expect(text).toMatch(/Client version:\s+[\d.]+/) + expect(text).toMatch(/Client name:\s+\w+/); + expect(text).toMatch(/Client version:\s+[\d.]+/); }, server: async () => { const server = new FastMCP({ name: "Test", version: "1.0.0", - }) + }); server.addTool({ description: "Get client information", execute: async (_args, context) => { - const clientInfo = context.client.version - return `Client name: ${clientInfo?.name || "unknown"}\nClient version: ${clientInfo?.version || "unknown"}` + const clientInfo = context.client.version; + return `Client name: ${clientInfo?.name || "unknown"}\nClient version: ${clientInfo?.version || "unknown"}`; }, name: "get-client-info", - }) + }); - return server + return server; }, - }) -}) + }); +}); diff --git a/src/FastMCP.ts b/src/FastMCP.ts index d382cfc..ebbefd0 100644 --- a/src/FastMCP.ts +++ b/src/FastMCP.ts @@ -1,8 +1,8 @@ -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 { 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, @@ -25,215 +25,230 @@ import { RootsListChangedNotificationSchema, 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 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" +} 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 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"; export interface Logger { - debug(...args: unknown[]): void - error(...args: unknown[]): void - info(...args: unknown[]): void - log(...args: unknown[]): void - warn(...args: unknown[]): void + 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 -} + close: () => Promise; +}; type FastMCPEvents = { - connect: (event: { session: FastMCPSession }) => void - disconnect: (event: { session: FastMCPSession }) => void -} + connect: (event: { session: FastMCPSession }) => void; + disconnect: (event: { session: FastMCPSession }) => void; +}; type FastMCPSessionEvents = { - error: (event: { error: Error }) => void - ready: () => void - rootsChanged: (event: { roots: Root[] }) => void -} + 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 + let rawData: Buffer; try { if ("url" in input) { try { - const response = await fetch(input.url) + const response = await fetch(input.url); if (!response.ok) { - throw new Error(`Server responded with status: ${response.status} - ${response.statusText}`) + throw new Error( + `Server responded with status: ${response.status} - ${response.statusText}`, + ); } - rawData = Buffer.from(await response.arrayBuffer()) + 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) + 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 + rawData = input.buffer; } else { - throw new Error("Invalid input: Provide a valid 'url', 'path', or 'buffer'") + throw new Error( + "Invalid input: Provide a valid 'url', 'path', or 'buffer'", + ); } - const { fileTypeFromBuffer } = await import("file-type") - const mimeType = await fileTypeFromBuffer(rawData) + 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"}`) + console.warn( + `Warning: Content may not be a valid image. Detected MIME: ${mimeType?.mime || "unknown"}`, + ); } - const base64Data = rawData.toString("base64") + const base64Data = rawData.toString("base64"); return { data: base64Data, mimeType: mimeType?.mime ?? "image/png", type: "image", - } as const + } as const; } catch (error) { if (error instanceof Error) { - throw error + throw error; } else { - throw new Error(`Unexpected error processing image: ${String(error)}`) + throw new Error(`Unexpected error processing image: ${String(error)}`); } } -} +}; export const audioContent = async ( input: { buffer: Buffer } | { path: string } | { url: string }, ): Promise => { - let rawData: Buffer + let rawData: Buffer; try { if ("url" in input) { try { - const response = await fetch(input.url) + const response = await fetch(input.url); if (!response.ok) { - throw new Error(`Server responded with status: ${response.status} - ${response.statusText}`) + throw new Error( + `Server responded with status: ${response.status} - ${response.statusText}`, + ); } - rawData = Buffer.from(await response.arrayBuffer()) + 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) + 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 + rawData = input.buffer; } else { - throw new Error("Invalid input: Provide a valid 'url', 'path', or 'buffer'") + throw new Error( + "Invalid input: Provide a valid 'url', 'path', or 'buffer'", + ); } - const { fileTypeFromBuffer } = await import("file-type") - const mimeType = await fileTypeFromBuffer(rawData) + 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"}`) + console.warn( + `Warning: Content may not be a valid audio file. Detected MIME: ${mimeType?.mime || "unknown"}`, + ); } - const base64Data = rawData.toString("base64") + const base64Data = rawData.toString("base64"); return { data: base64Data, mimeType: mimeType?.mime ?? "audio/mpeg", type: "audio", - } as const + } as const; } catch (error) { if (error instanceof Error) { - throw error + throw error; } else { - throw new Error(`Unexpected error processing audio: ${String(error)}`) + throw new Error(`Unexpected error processing audio: ${String(error)}`); } } -} +}; type Context = { client: { - version: ReturnType - } + 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 + 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 - requestMetadata?: RequestMeta - session: T | undefined + requestId?: string; + requestMetadata?: RequestMeta; + 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 -} + sessionId?: string; + streamContent: (content: Content | Content[]) => Promise; +}; -type Extra = unknown +type Extra = unknown; -type Extras = Record +type Extras = Record; -type Literal = boolean | null | number | string | undefined +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 + progress: number; /** * Total number of items to process (or total progress required), if known. */ - total?: number -} + total?: number; +}; -type SerializableValue = { [key: string]: SerializableValue } | Literal | SerializableValue[] +type SerializableValue = + | { [key: string]: SerializableValue } + | Literal + | SerializableValue[]; type TextContent = { - text: string - type: "text" -} + text: string; + type: "text"; +}; -type ToolParameters = StandardSchemaV1 +type ToolParameters = StandardSchemaV1; abstract class FastMCPError extends Error { public constructor(message?: string) { - super(message) - this.name = new.target.name + super(message); + this.name = new.target.name; } } @@ -250,20 +265,20 @@ abstract class FastMCPError extends Error { * by FastMCP's error handling rather than wrapped in a result. */ export class CustomMcpError extends McpError { - readonly __isMcpError = true + readonly __isMcpError = true; constructor(code: number, message: string, data?: unknown) { - super(code, message, data) + super(code, message, data); } } export class UnexpectedStateError extends FastMCPError { - public extras?: Extras + public extras?: Extras; public constructor(message: string, extras?: Extras) { - super(message) - this.name = new.target.name - this.extras = extras + super(message); + this.name = new.target.name; + this.extras = extras; } } @@ -286,7 +301,7 @@ export function isMcpErrorLike(error: unknown): error is McpError { error !== null && "__isMcpError" in error && (error as { __isMcpError?: boolean }).__isMcpError === true) - ) + ); } const TextContentZodSchema = z @@ -297,13 +312,13 @@ const TextContentZodSchema = z text: z.string(), type: z.literal("text"), }) - .strict() satisfies z.ZodType + .strict() satisfies z.ZodType; type ImageContent = { - data: string - mimeType: string - type: "image" -} + data: string; + mimeType: string; + type: "image"; +}; const ImageContentZodSchema = z .object({ @@ -317,13 +332,13 @@ const ImageContentZodSchema = z mimeType: z.string(), type: z.literal("image"), }) - .strict() satisfies z.ZodType + .strict() satisfies z.ZodType; type AudioContent = { - data: string - mimeType: string - type: "audio" -} + data: string; + mimeType: string; + type: "audio"; +}; const AudioContentZodSchema = z .object({ @@ -334,17 +349,17 @@ const AudioContentZodSchema = z mimeType: z.string(), type: z.literal("audio"), }) - .strict() satisfies z.ZodType + .strict() satisfies z.ZodType; type ResourceContent = { resource: { - blob?: string - mimeType?: string - text?: string - uri: string - } - type: "resource" -} + blob?: string; + mimeType?: string; + text?: string; + uri: string; + }; + type: "resource"; +}; const ResourceContentZodSchema = z .object({ @@ -356,7 +371,7 @@ const ResourceContentZodSchema = z }), type: z.literal("resource"), }) - .strict() satisfies z.ZodType + .strict() satisfies z.ZodType; const ResourceLinkZodSchema = z.object({ description: z.string().optional(), @@ -365,9 +380,14 @@ const ResourceLinkZodSchema = z.object({ title: z.string().optional(), type: z.literal("resource_link"), uri: z.string(), -}) satisfies z.ZodType +}) satisfies z.ZodType; -type Content = AudioContent | ImageContent | ResourceContent | ResourceLink | TextContent +type Content = + | AudioContent + | ImageContent + | ResourceContent + | ResourceLink + | TextContent; const ContentZodSchema = z.discriminatedUnion("type", [ TextContentZodSchema, @@ -375,13 +395,13 @@ const ContentZodSchema = z.discriminatedUnion("type", [ AudioContentZodSchema, ResourceContentZodSchema, ResourceLinkZodSchema, -]) satisfies z.ZodType +]) satisfies z.ZodType; type ContentResult = { - _meta?: Record - content: Content[] - isError?: boolean -} + _meta?: Record; + content: Content[]; + isError?: boolean; +}; const ContentResultZodSchema = z .object({ @@ -389,13 +409,13 @@ const ContentResultZodSchema = z content: ContentZodSchema.array(), isError: z.boolean().optional(), }) - .strict() satisfies z.ZodType + .strict() satisfies z.ZodType; type Completion = { - hasMore?: boolean - total?: number - values: string[] -} + hasMore?: boolean; + total?: number; + values: string[]; +}; /** * https://github.com/modelcontextprotocol/typescript-sdk/blob/3164da64d085ec4e022ae881329eee7b72f208d4/src/types.ts#L983-L1003 @@ -413,133 +433,159 @@ const CompletionZodSchema = z.object({ * An array of completion values. Must not exceed 100 items. */ values: z.array(z.string()).max(100), -}) satisfies z.ZodType +}) satisfies z.ZodType; -type ArgumentValueCompleter = ( - value: string, - auth?: T, -) => Promise +type ArgumentValueCompleter = + (value: string, auth?: T) => Promise; type InputPrompt< T extends FastMCPSessionAuth = FastMCPSessionAuth, Arguments extends InputPromptArgument[] = InputPromptArgument[], Args = PromptArgumentsToObject, > = { - arguments?: InputPromptArgument[] - description?: string - load: (args: Args, auth?: T) => Promise - name: string -} - -type InputPromptArgument = Readonly<{ - complete?: ArgumentValueCompleter - description?: string - enum?: string[] - name: string - required?: boolean -}> + arguments?: InputPromptArgument[]; + 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 extends + InputResourceTemplateArgument[] = InputResourceTemplateArgument[], > = { - arguments: Arguments - description?: string - load: (args: ResourceTemplateArgumentsToObject, auth?: T) => Promise - mimeType?: string - name: string - uriTemplate: string -} - -type InputResourceTemplateArgument = Readonly<{ - complete?: ArgumentValueCompleter - description?: string - name: string - required?: boolean -}> - -type LoggingLevel = "alert" | "critical" | "debug" | "emergency" | "error" | "info" | "notice" | "warning" + arguments: Arguments; + 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["required"] extends true ? string : string | undefined -} - -type PromptResult = Pick | string + 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 -} + 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 + blob: string; + mimeType?: string; + uri?: string; } | { - mimeType?: string - text: string - uri?: string - } + mimeType?: string; + text: string; + uri?: string; + }; type ResourceTemplate< T extends FastMCPSessionAuth, - Arguments extends ResourceTemplateArgument[] = ResourceTemplateArgument[], + 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 = Readonly<{ - complete?: ArgumentValueCompleter - description?: string - name: string - required?: boolean -}> + 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 -} + [K in T[number]["name"]]: string; +}; type SamplingResponse = { - content: AudioContent | ImageContent | TextContent - model: string - role: "assistant" | "user" - stopReason?: "endTurn" | "maxTokens" | "stopSequence" | string -} + content: AudioContent | ImageContent | TextContent; + model: string; + role: "assistant" | "user"; + stopReason?: "endTurn" | "maxTokens" | "stopSequence" | string; +}; type ServerOptions = { - authenticate?: Authenticate + 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 @@ -555,33 +601,33 @@ type ServerOptions = { * When set to `false` the health-check endpoint is disabled. * @default true */ - enabled?: boolean + enabled?: boolean; /** * Plain-text body returned by the endpoint. * @default "ok" */ - message?: string + message?: string; /** * HTTP path that should be handled. * @default "/health" */ - path?: string + path?: string; /** * HTTP response status that will be returned. * @default 200 */ - status?: number - } - instructions?: string + 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 + logger?: Logger; + name: string; /** * Configuration for OAuth well-known discovery endpoints that can be exposed @@ -603,36 +649,36 @@ type ServerOptions = { * Required by MCP Specification 2025-03-26 */ authorizationServer?: { - authorizationEndpoint: string - codeChallengeMethodsSupported?: string[] + authorizationEndpoint: string; + codeChallengeMethodsSupported?: string[]; // DPoP support - dpopSigningAlgValuesSupported?: string[] - grantTypesSupported?: string[] + dpopSigningAlgValuesSupported?: string[]; + grantTypesSupported?: string[]; - introspectionEndpoint?: string + introspectionEndpoint?: string; // Required - issuer: string + 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[] - } + 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 + enabled: boolean; /** * OAuth Protected Resource metadata for `/.well-known/oauth-protected-resource` @@ -662,7 +708,7 @@ type ServerOptions = { * @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 + [key: string]: unknown; /** * Supported values for the `authorization_details` parameter (RFC 9396). @@ -670,7 +716,7 @@ type ServerOptions = { * @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[] + authorizationDetailsTypesSupported?: string[]; /** * List of OAuth 2.0 authorization server issuer identifiers. @@ -683,7 +729,7 @@ type ServerOptions = { * 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[] + authorizationServers: string[]; /** * List of supported methods for presenting OAuth 2.0 bearer tokens. @@ -693,7 +739,7 @@ type ServerOptions = { * 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[] + bearerMethodsSupported?: string[]; /** * Whether this resource requires all access tokens to be DPoP-bound. @@ -701,14 +747,14 @@ type ServerOptions = { * @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 + 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[] + dpopSigningAlgValuesSupported?: string[]; /** * JWKS URI of this resource. Used to validate access tokens or sign responses. @@ -716,7 +762,7 @@ type ServerOptions = { * @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 + jwksUri?: string; /** * Canonical OAuth resource identifier for this protected resource (the MCP server). @@ -725,7 +771,7 @@ type ServerOptions = { * `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 + resource: string; /** * URL to developer-accessible documentation for this resource. @@ -733,7 +779,7 @@ type ServerOptions = { * @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 + resourceDocumentation?: string; /** * Human-readable name for display purposes (e.g., in UIs). @@ -741,7 +787,7 @@ type ServerOptions = { * @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 + resourceName?: string; /** * URL to a human-readable policy page describing acceptable use. @@ -749,7 +795,7 @@ type ServerOptions = { * @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 + resourcePolicyUri?: string; /** * Supported JWS algorithms for signed responses from this resource (e.g., response signing). @@ -757,7 +803,7 @@ type ServerOptions = { * @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[] + resourceSigningAlgValuesSupported?: string[]; /** * URL to the protected resource’s Terms of Service. @@ -765,7 +811,7 @@ type ServerOptions = { * @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 + resourceTosUri?: string; /** * Supported OAuth scopes for requesting access to this resource. @@ -773,7 +819,7 @@ type ServerOptions = { * @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[] + scopesSupported?: string[]; /** * Developer-accessible documentation for how to use the service (not end-user docs). @@ -782,7 +828,7 @@ type ServerOptions = { * 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 + serviceDocumentation?: string; /** * Whether mutual-TLS-bound access tokens are required. @@ -790,9 +836,9 @@ type ServerOptions = { * @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 - } - } + tlsClientCertificateBoundAccessTokens?: boolean; + }; + }; ping?: { /** @@ -800,18 +846,18 @@ type ServerOptions = { * - true for SSE or HTTP Stream * - false for stdio */ - enabled?: boolean + enabled?: boolean; /** * Interval * @default 5000 (5s) */ - intervalMs?: number + intervalMs?: number; /** * Logging level for ping-related messages. * @default 'debug' */ - logLevel?: LoggingLevel - } + logLevel?: LoggingLevel; + }; /** * Configuration for roots capability */ @@ -821,38 +867,50 @@ type ServerOptions = { * Set to false to completely disable roots support * @default true */ - enabled?: boolean - } + enabled?: boolean; + }; /** * General utilities */ utils?: { - formatInvalidParamsErrorMessage?: (issues: readonly StandardSchemaV1.Issue[]) => string - } - version: `${number}.${number}.${number}` -} - -type Tool = { + formatInvalidParamsErrorMessage?: ( + issues: readonly StandardSchemaV1.Issue[], + ) => string; + }; + version: `${number}.${number}.${number}`; +}; + +type Tool< + T extends FastMCPSessionAuth, + Params extends ToolParameters = ToolParameters, +> = { 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 + 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 - parameters?: Params - timeoutMs?: number -} + | AudioContent + | ContentResult + | ImageContent + | ResourceContent + | ResourceLink + | string + | TextContent + | void + >; + name: string; + parameters?: Params; + timeoutMs?: number; +}; /** * Tool annotations as defined in MCP Specification (2025-03-26) @@ -864,95 +922,97 @@ type ToolAnnotations = { * Only meaningful when readOnlyHint is false * @default true */ - destructiveHint?: boolean + 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 + idempotentHint?: boolean; /** * If true, the tool may interact with an "open world" of external entities * @default true */ - openWorldHint?: boolean + openWorldHint?: boolean; /** * If true, indicates the tool does not modify its environment * @default false */ - readOnlyHint?: boolean + readOnlyHint?: boolean; /** * A human-readable title for the tool, useful for UI display */ - title?: string -} + title?: string; +}; const FastMCPSessionEventEmitterBase: { - new (): StrictEventEmitter -} = EventEmitter + new (): StrictEventEmitter; +} = EventEmitter; -type Authenticate = (request: http.IncomingMessage) => Promise +type Authenticate = (request: http.IncomingMessage) => Promise; -type FastMCPSessionAuth = Record | undefined +type FastMCPSessionAuth = Record | undefined; class FastMCPSessionEventEmitter extends FastMCPSessionEventEmitterBase {} -export class FastMCPSession extends FastMCPSessionEventEmitter { +export class FastMCPSession< + T extends FastMCPSessionAuth = FastMCPSessionAuth, +> extends FastMCPSessionEventEmitter { public get clientCapabilities(): ClientCapabilities | null { - return this.#clientCapabilities ?? null + return this.#clientCapabilities ?? null; } public get isReady(): boolean { - return this.#connectionState === "ready" + return this.#connectionState === "ready"; } public get loggingLevel(): LoggingLevel { - return this.#loggingLevel + return this.#loggingLevel; } public get roots(): Root[] { - return this.#roots + return this.#roots; } public get server(): Server { - return this.#server + return this.#server; } public get sessionId(): string | undefined { - return this.#sessionId + return this.#sessionId; } public set sessionId(value: string | undefined) { - this.#sessionId = value + 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"] + #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 + #pingInterval: null | ReturnType = null; - #prompts: Prompt[] = [] + #prompts: Prompt[] = []; - #resources: Resource[] = [] + #resources: Resource[] = []; - #resourceTemplates: ResourceTemplate[] = [] + #resourceTemplates: ResourceTemplate[] = []; - #roots: Root[] = [] + #roots: Root[] = []; - #rootsConfig?: ServerOptions["roots"] + #rootsConfig?: ServerOptions["roots"]; - #server: Server + #server: Server; /** * Session ID from the Mcp-Session-Id header (HTTP transports only). * Used to track per-session state across multiple requests. */ - #sessionId?: string + #sessionId?: string; - #utils?: ServerOptions["utils"] + #utils?: ServerOptions["utils"]; constructor({ auth, @@ -970,138 +1030,138 @@ export class FastMCPSession e 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 + 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() + super(); - this.#auth = auth - this.#logger = logger - this.#pingConfig = ping - this.#rootsConfig = roots - this.#sessionId = sessionId - this.#needsEventLoopFlush = transportType === "httpStream" + 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 = {} + this.#capabilities.tools = {}; } if (resources.length || resourcesTemplates.length) { - this.#capabilities.resources = {} + this.#capabilities.resources = {}; } if (prompts.length) { for (const prompt of prompts) { - this.addPrompt(prompt) + this.addPrompt(prompt); } - this.#capabilities.prompts = {} + this.#capabilities.prompts = {}; } - this.#capabilities.logging = {} + this.#capabilities.logging = {}; this.#server = new Server( { name: name, version: version }, { capabilities: this.#capabilities, instructions: instructions }, - ) + ); - this.#utils = utils + this.#utils = utils; - this.setupErrorHandling() - this.setupLoggingHandlers() - this.setupRootsHandlers() - this.setupCompleteHandlers() + this.setupErrorHandling(); + this.setupLoggingHandlers(); + this.setupRootsHandlers(); + this.setupCompleteHandlers(); if (tools.length) { - this.setupToolHandlers(tools) + this.setupToolHandlers(tools); } if (resources.length || resourcesTemplates.length) { for (const resource of resources) { - this.addResource(resource) + this.addResource(resource); } - this.setupResourceHandlers(resources) + this.setupResourceHandlers(resources); if (resourcesTemplates.length) { for (const resourceTemplate of resourcesTemplates) { - this.addResourceTemplate(resourceTemplate) + this.addResourceTemplate(resourceTemplate); } - this.setupResourceTemplateHandlers(resourcesTemplates) + this.setupResourceTemplateHandlers(resourcesTemplates); } } if (prompts.length) { - this.setupPromptHandlers(prompts) + this.setupPromptHandlers(prompts); } } public async close() { - this.#connectionState = "closed" + this.#connectionState = "closed"; if (this.#pingInterval) { - clearInterval(this.#pingInterval) + clearInterval(this.#pingInterval); } try { - await this.#server.close() + await this.#server.close(); } catch (error) { - this.#logger.error("[FastMCP error]", "could not close server", 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") + throw new UnexpectedStateError("Server is already connected"); } - this.#connectionState = "connecting" + this.#connectionState = "connecting"; try { - await this.#server.connect(transport) + 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 + sessionId?: string; + } & Transport; if (typeof transportWithSessionId.sessionId === "string") { - this.#sessionId = transportWithSessionId.sessionId + this.#sessionId = transportWithSessionId.sessionId; } } - let attempt = 0 - const maxAttempts = 10 - const retryDelay = 100 + let attempt = 0; + const maxAttempts = 10; + const retryDelay = 100; while (attempt++ < maxAttempts) { - const capabilities = this.#server.getClientCapabilities() + const capabilities = this.#server.getClientCapabilities(); if (capabilities) { - this.#clientCapabilities = capabilities - break + this.#clientCapabilities = capabilities; + break; } - await delay(retryDelay) + await delay(retryDelay); } if (!this.#clientCapabilities) { this.#logger.warn( `[FastMCP warning] could not infer client capabilities after ${maxAttempts} attempts. Connection may be unstable.`, - ) + ); } if ( @@ -1110,56 +1170,62 @@ export class FastMCPSession e typeof this.#server.listRoots === "function" ) { try { - const roots = await this.#server.listRoots() - this.#roots = roots?.roots || [] + 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") + 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) + const pingConfig = this.#getPingConfig(transport); if (pingConfig.enabled) { this.#pingInterval = setInterval(async () => { try { - await this.#server.ping() + 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 + const logLevel = pingConfig.logLevel; if (logLevel === "debug") { - this.#logger.debug("[FastMCP debug] server ping failed") + this.#logger.debug("[FastMCP debug] server ping failed"); } else if (logLevel === "warning") { - this.#logger.warn("[FastMCP warning] server is not responding to ping") + 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") + this.#logger.error( + "[FastMCP error] server is not responding to ping", + ); } else { - this.#logger.info("[FastMCP info] server ping failed") + this.#logger.info("[FastMCP info] server ping failed"); } } - }, pingConfig.intervalMs) + }, pingConfig.intervalMs); } } // Mark connection as ready and emit event - this.#connectionState = "ready" - this.emit("ready") + this.#connectionState = "ready"; + this.emit("ready"); } catch (error) { - this.#connectionState = "error" + this.#connectionState = "error"; const errorEvent = { error: error instanceof Error ? error : new Error(String(error)), - } - this.emit("error", errorEvent) - throw error + }; + this.emit("error", errorEvent); + throw error; } } @@ -1167,74 +1233,84 @@ export class FastMCPSession e message: z.infer["params"], options?: RequestOptions, ): Promise { - return this.#server.createMessage(message, options) + return this.#server.createMessage(message, options); } public waitForReady(): Promise { if (this.isReady) { - return Promise.resolve() + return Promise.resolve(); } - if (this.#connectionState === "error" || this.#connectionState === "closed") { - return Promise.reject(new Error(`Connection is in ${this.#connectionState} state`)) + 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) + reject( + new Error( + "Connection timeout: Session failed to become ready within 5 seconds", + ), + ); + }, 5000); this.once("ready", () => { - clearTimeout(timeout) - resolve() - }) + clearTimeout(timeout); + resolve(); + }); this.once("error", (event) => { - clearTimeout(timeout) - reject(event.error) - }) - }) + clearTimeout(timeout); + reject(event.error); + }); + }); } #getPingConfig(transport: Transport): { - enabled: boolean - intervalMs: number - logLevel: LoggingLevel + enabled: boolean; + intervalMs: number; + logLevel: LoggingLevel; } { - const pingConfig = this.#pingConfig || {} + const pingConfig = this.#pingConfig || {}; - let defaultEnabled = false + let defaultEnabled = false; if ("type" in transport) { // Enable by default for SSE and HTTP streaming if (transport.type === "httpStream") { - defaultEnabled = true + defaultEnabled = true; } } return { - enabled: pingConfig.enabled !== undefined ? pingConfig.enabled : defaultEnabled, + 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> = {} + const completers: Record> = {}; + const enums: Record = {}; + const fuseInstances: Record> = {}; for (const argument of inputPrompt.arguments ?? []) { if (argument.complete) { - completers[argument.name] = argument.complete + completers[argument.name] = argument.complete; } if (argument.enum) { - enums[argument.name] = argument.enum + enums[argument.name] = argument.enum; fuseInstances[argument.name] = new Fuse(argument.enum, { includeScore: true, threshold: 0.3, // More flexible matching! - }) + }); } } @@ -1242,37 +1318,37 @@ export class FastMCPSession e ...inputPrompt, complete: async (name: string, value: string, auth?: T) => { if (completers[name]) { - return await completers[name](value, auth) + return await completers[name](value, auth); } if (fuseInstances[name]) { - const result = fuseInstances[name].search(value) + const result = fuseInstances[name].search(value); return { total: result.length, values: result.map((item) => item.item), - } + }; } return { values: [], - } + }; }, - } + }; - this.#prompts.push(prompt) + this.#prompts.push(prompt); } private addResource(inputResource: Resource) { - this.#resources.push(inputResource) + this.#resources.push(inputResource); } private addResourceTemplate(inputResourceTemplate: InputResourceTemplate) { - const completers: Record> = {} + const completers: Record> = {}; for (const argument of inputResourceTemplate.arguments ?? []) { if (argument.complete) { - completers[argument.name] = argument.complete + completers[argument.name] = argument.complete; } } @@ -1280,90 +1356,105 @@ export class FastMCPSession e ...inputResourceTemplate, complete: async (name: string, value: string, auth?: T) => { if (completers[name]) { - return await completers[name](value, auth) + return await completers[name](value, auth); } return { values: [], - } + }; }, - } + }; - this.#resourceTemplates.push(resourceTemplate) + this.#resourceTemplates.push(resourceTemplate); } private setupCompleteHandlers() { this.#server.setRequestHandler(CompleteRequestSchema, async (request) => { if (request.params.ref.type === "ref/prompt") { - const prompt = this.#prompts.find((prompt) => prompt.name === request.params.ref.name) + const prompt = this.#prompts.find( + (prompt) => prompt.name === request.params.ref.name, + ); if (!prompt) { throw new UnexpectedStateError("Unknown prompt", { request, - }) + }); } if (!prompt.complete) { throw new UnexpectedStateError("Prompt does not support completion", { request, - }) + }); } const completion = CompletionZodSchema.parse( - await prompt.complete(request.params.argument.name, request.params.argument.value, this.#auth), - ) + await prompt.complete( + request.params.argument.name, + request.params.argument.value, + this.#auth, + ), + ); return { completion, - } + }; } if (request.params.ref.type === "ref/resource") { - const resource = this.#resourceTemplates.find((resource) => resource.uriTemplate === request.params.ref.uri) + const resource = this.#resourceTemplates.find( + (resource) => resource.uriTemplate === request.params.ref.uri, + ); if (!resource) { throw new UnexpectedStateError("Unknown resource", { request, - }) + }); } if (!("uriTemplate" in resource)) { - throw new UnexpectedStateError("Unexpected resource") + throw new UnexpectedStateError("Unexpected resource"); } if (!resource.complete) { - throw new UnexpectedStateError("Resource does not support completion", { - request, - }) + 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), - ) + 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) - } + this.#logger.error("[FastMCP error]", error); + }; } private setupLoggingHandlers() { this.#server.setRequestHandler(SetLevelRequestSchema, (request) => { - this.#loggingLevel = request.params.level + this.#loggingLevel = request.params.level; - return {} - }) + return {}; + }); } private setupPromptHandlers(prompts: Prompt[]) { @@ -1375,19 +1466,24 @@ export class FastMCPSession e complete: prompt.complete, description: prompt.description, name: prompt.name, - } + }; }), - } - }) + }; + }); this.#server.setRequestHandler(GetPromptRequestSchema, async (request) => { - const prompt = prompts.find((prompt) => prompt.name === request.params.name) + const prompt = prompts.find( + (prompt) => prompt.name === request.params.name, + ); if (!prompt) { - throw new McpError(ErrorCode.MethodNotFound, `Unknown prompt: ${request.params.name}`) + throw new McpError( + ErrorCode.MethodNotFound, + `Unknown prompt: ${request.params.name}`, + ); } - const args = request.params.arguments + const args = request.params.arguments; for (const arg of prompt.arguments ?? []) { if (arg.required && !(args && arg.name in args)) { @@ -1396,17 +1492,24 @@ export class FastMCPSession e `Prompt '${request.params.name}' requires argument '${arg.name}': ${ arg.description || "No description provided" }`, - ) + ); } } - let result: Awaited["load"]>> + let result: Awaited["load"]>>; try { - result = await prompt.load(args as Record, this.#auth) + 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}`) + 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") { @@ -1418,14 +1521,14 @@ export class FastMCPSession e role: "user", }, ], - } + }; } else { return { description: prompt.description, messages: result.messages, - } + }; } - }) + }); } private setupResourceHandlers(resources: Resource[]) { @@ -1437,129 +1540,157 @@ export class FastMCPSession e name: resource.name, uri: resource.uri, })), - } satisfies ListResourcesResult - }) + } satisfies ListResourcesResult; + }); + + this.#server.setRequestHandler( + ReadResourceRequestSchema, + async (request) => { + if ("uri" in request.params) { + const resource = resources.find( + (resource) => + "uri" in resource && resource.uri === request.params.uri, + ); + + if (!resource) { + for (const resourceTemplate of this.#resourceTemplates) { + const uriTemplate = parseURITemplate( + resourceTemplate.uriTemplate, + ); + + const match = uriTemplate.fromUri(request.params.uri); + + if (!match) { + continue; + } - this.#server.setRequestHandler(ReadResourceRequestSchema, async (request) => { - if ("uri" in request.params) { - const resource = resources.find((resource) => "uri" in resource && resource.uri === request.params.uri) + const uri = uriTemplate.fill(match); - if (!resource) { - for (const resourceTemplate of this.#resourceTemplates) { - const uriTemplate = parseURITemplate(resourceTemplate.uriTemplate) + const result = await resourceTemplate.load(match, this.#auth); - const match = uriTemplate.fromUri(request.params.uri) - - if (!match) { - continue + 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, + })), + }; } - const uri = uriTemplate.fill(match) - - const result = await resourceTemplate.load(match, this.#auth) + throw new McpError( + ErrorCode.MethodNotFound, + `Resource not found: '${request.params.uri}'. Available resources: ${ + resources.map((r) => r.uri).join(", ") || "none" + }`, + ); + } - 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, - })), - } + if (!("uri" in resource)) { + throw new UnexpectedStateError("Resource does not support reading"); } - throw new McpError( - ErrorCode.MethodNotFound, - `Resource not found: '${request.params.uri}'. Available resources: ${ - resources.map((r) => r.uri).join(", ") || "none" - }`, - ) - } + let maybeArrayResult: Awaited["load"]>>; - if (!("uri" in resource)) { - throw new UnexpectedStateError("Resource does not support reading") - } + 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, + }, + ); + } - let maybeArrayResult: Awaited["load"]>> + const resourceResults = Array.isArray(maybeArrayResult) + ? maybeArrayResult + : [maybeArrayResult]; - 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, - }, - ) + return { + contents: resourceResults.map((result) => ({ + ...result, + mimeType: result.mimeType ?? resource.mimeType, + name: resource.name, + uri: result.uri ?? resource.uri, + })), + }; } - const resourceResults = Array.isArray(maybeArrayResult) ? maybeArrayResult : [maybeArrayResult] + throw new UnexpectedStateError("Unknown resource request", { + request, + }); + }, + ); + } + private setupResourceTemplateHandlers( + resourceTemplates: ResourceTemplate[], + ) { + this.#server.setRequestHandler( + ListResourceTemplatesRequestSchema, + async () => { return { - contents: resourceResults.map((result) => ({ - ...result, - mimeType: result.mimeType ?? resource.mimeType, - name: resource.name, - uri: result.uri ?? resource.uri, + resourceTemplates: resourceTemplates.map((resourceTemplate) => ({ + description: resourceTemplate.description, + mimeType: resourceTemplate.mimeType, + name: resourceTemplate.name, + uriTemplate: resourceTemplate.uriTemplate, })), - } - } - - throw new UnexpectedStateError("Unknown resource request", { - request, - }) - }) - } - - private setupResourceTemplateHandlers(resourceTemplates: ResourceTemplate[]) { - this.#server.setRequestHandler(ListResourceTemplatesRequestSchema, async () => { - return { - resourceTemplates: resourceTemplates.map((resourceTemplate) => ({ - description: resourceTemplate.description, - mimeType: resourceTemplate.mimeType, - name: resourceTemplate.name, - uriTemplate: resourceTemplate.uriTemplate, - })), - } satisfies ListResourceTemplatesResult - }) + } satisfies ListResourceTemplatesResult; + }, + ); } private setupRootsHandlers() { if (this.#rootsConfig?.enabled === false) { - this.#logger.debug("[FastMCP debug] roots capability explicitly disabled via config") - return + 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, + 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) - }`, - ) - } - }) - }) + .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") + this.#logger.debug( + "[FastMCP debug] roots capability not available, not setting up notification handler", + ); } } @@ -1579,46 +1710,51 @@ export class FastMCPSession e type: "object", }, // More complete schema for Cursor compatibility name: tool.name, - } + }; }), ), - } - }) + }; + }); this.#server.setRequestHandler(CallToolRequestSchema, async (request) => { - const tool = tools.find((tool) => tool.name === request.params.name) + const tool = tools.find((tool) => tool.name === request.params.name); if (!tool) { - throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${request.params.name}`) + throw new McpError( + ErrorCode.MethodNotFound, + `Unknown tool: ${request.params.name}`, + ); } - let args: unknown = undefined + let args: unknown = undefined; if (tool.parameters) { - const parsed = await tool.parameters["~standard"].validate(request.params.arguments) + 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}` + const path = issue.path?.join(".") || "root"; + return `${path}: ${issue.message}`; }) - .join(", ") + .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 + args = parsed.value; } - const progressToken = request.params?._meta?.progressToken + const progressToken = request.params?._meta?.progressToken; - let result: ContentResult + let result: ContentResult; try { const reportProgress = async (progress: Progress) => { @@ -1629,18 +1765,20 @@ export class FastMCPSession e ...progress, progressToken, }, - }) + }); if (this.#needsEventLoopFlush) { - await new Promise((resolve) => setImmediate(resolve)) + 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), - ) + progressError instanceof Error + ? progressError.message + : String(progressError), + ); } - } + }; const log = { debug: (message: string, context?: SerializableValue) => { @@ -1650,7 +1788,7 @@ export class FastMCPSession e message, }, level: "debug", - }) + }); }, error: (message: string, context?: SerializableValue) => { this.#server.sendLoggingMessage({ @@ -1659,7 +1797,7 @@ export class FastMCPSession e message, }, level: "error", - }) + }); }, info: (message: string, context?: SerializableValue) => { this.#server.sendLoggingMessage({ @@ -1668,7 +1806,7 @@ export class FastMCPSession e message, }, level: "info", - }) + }); }, warn: (message: string, context?: SerializableValue) => { this.#server.sendLoggingMessage({ @@ -1677,15 +1815,15 @@ export class FastMCPSession e 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] + const contentArray = Array.isArray(content) ? content : [content]; try { await this.#server.notification({ @@ -1694,30 +1832,35 @@ export class FastMCPSession e content: contentArray, toolName: request.params.name, }, - }) + }); if (this.#needsEventLoopFlush) { - await new Promise((resolve) => setImmediate(resolve)) + 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), - ) + 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, + requestId: + typeof request.params?._meta?.requestId === "string" + ? request.params._meta.requestId + : undefined, requestMetadata: request.params._meta, session: this.#auth, sessionId: this.#sessionId, streamContent, - }) + }); // Handle timeout if specified const maybeStringResult = (await (tool.timeoutMs @@ -1730,11 +1873,11 @@ export class FastMCPSession e ErrorCode.InternalError, `Tool '${request.params.name}' timed out after ${tool.timeoutMs}ms. Consider increasing timeoutMs or optimizing the tool implementation.`, ), - ) - }, tool.timeoutMs) + ); + }, tool.timeoutMs); // If promise resolves first - executeToolPromise.finally(() => clearTimeout(timeoutId)) + executeToolPromise.finally(() => clearTimeout(timeoutId)); }), ]) : executeToolPromise)) as @@ -1746,32 +1889,32 @@ export class FastMCPSession e | ResourceLink | string | TextContent - | undefined + | 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) + 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) + result = ContentResultZodSchema.parse(maybeStringResult); } } catch (error) { // Re-throw McpError to let the MCP SDK handle it as a proper JSON-RPC error // Use type guard to handle instanceof failures across module boundaries if (isMcpErrorLike(error)) { - throw error + throw error; } if (error instanceof UserError) { @@ -1779,10 +1922,11 @@ export class FastMCPSession e content: [{ text: error.message, type: "text" }], isError: true, ...(error.extras ? { structuredContent: error.extras } : {}), - } + }; } - const errorMessage = error instanceof Error ? error.message : String(error) + const errorMessage = + error instanceof Error ? error.message : String(error); return { content: [ { @@ -1791,11 +1935,11 @@ export class FastMCPSession e }, ], isError: true, - } + }; } - return result - }) + return result; + }); } } @@ -1803,80 +1947,86 @@ export class FastMCPSession e * Converts camelCase to snake_case for OAuth endpoint responses */ function camelToSnakeCase(str: string): string { - return str.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`) + 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 = {} +function convertObjectToSnakeCase( + obj: Record, +): Record { + const result: Record = {}; for (const [key, value] of Object.entries(obj)) { - const snakeKey = camelToSnakeCase(key) - result[snakeKey] = value + const snakeKey = camelToSnakeCase(key); + result[snakeKey] = value; } - return result + return result; } const FastMCPEventEmitterBase: { - new (): StrictEventEmitter> -} = EventEmitter + new (): StrictEventEmitter>; +} = EventEmitter; class FastMCPEventEmitter extends FastMCPEventEmitterBase {} -export class FastMCP extends FastMCPEventEmitter { +export class FastMCP< + T extends FastMCPSessionAuth = FastMCPSessionAuth, +> extends FastMCPEventEmitter { public get sessions(): FastMCPSession[] { - return this.#sessions + return this.#sessions; } - #authenticate: Authenticate | undefined - #httpStreamServer: null | SSEServer = null - #logger: Logger - #options: ServerOptions - #prompts: InputPrompt[] = [] - #resources: Resource[] = [] - #resourcesTemplates: InputResourceTemplate[] = [] - #sessions: FastMCPSession[] = [] + #authenticate: Authenticate | undefined; + #httpStreamServer: null | SSEServer = null; + #logger: Logger; + #options: ServerOptions; + #prompts: InputPrompt[] = []; + #resources: Resource[] = []; + #resourcesTemplates: InputResourceTemplate[] = []; + #sessions: FastMCPSession[] = []; - #tools: Tool[] = [] + #tools: Tool[] = []; constructor(public options: ServerOptions) { - super() + super(); - this.#options = options - this.#authenticate = options.authenticate - this.#logger = options.logger || console + this.#options = options; + this.#authenticate = options.authenticate; + this.#logger = options.logger || console; } /** * Adds a prompt to the server. */ - public addPrompt[]>(prompt: InputPrompt) { - this.#prompts.push(prompt) + public addPrompt[]>( + prompt: InputPrompt, + ) { + this.#prompts.push(prompt); } /** * Adds a resource to the server. */ public addResource(resource: Resource) { - this.#resources.push(resource) + this.#resources.push(resource); } /** * Adds a resource template to the server. */ - public addResourceTemplate( - resource: InputResourceTemplate, - ) { - this.#resourcesTemplates.push(resource) + public addResourceTemplate< + const Args extends InputResourceTemplateArgument[], + >(resource: InputResourceTemplate) { + this.#resourcesTemplates.push(resource); } /** * Adds a tool to the server. */ public addTool(tool: Tool) { - this.#tools.push(tool as unknown as Tool) + this.#tools.push(tool as unknown as Tool); } /** @@ -1887,56 +2037,60 @@ export class FastMCP extends */ public async embedded(uri: string): Promise { // First, try to find a direct resource match - const directResource = this.#resources.find((resource) => resource.uri === 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 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 + resourceData.text = firstResult.text; } if ("blob" in firstResult) { - resourceData.blob = firstResult.blob + resourceData.blob = firstResult.blob; } - return resourceData + return resourceData; } // Try to match against resource templates for (const template of this.#resourcesTemplates) { - const parsedTemplate = parseURITemplate(template.uriTemplate) - const params = parsedTemplate.fromUri(uri) + const parsedTemplate = parseURITemplate(template.uriTemplate); + const params = parsedTemplate.fromUri(uri); if (!params) { - continue + continue; } - const result = await template.load(params as ResourceTemplateArgumentsToObject) + const result = await template.load( + params as ResourceTemplateArgumentsToObject, + ); const resourceData: ResourceContent["resource"] = { mimeType: template.mimeType, uri, - } + }; if ("text" in result) { - resourceData.text = result.text + resourceData.text = result.text; } if ("blob" in result) { - resourceData.blob = result.blob + resourceData.blob = result.blob; } - return resourceData // The resource we're looking for + return resourceData; // The resource we're looking for } - throw new UnexpectedStateError(`Resource not found: ${uri}`, { uri }) + throw new UnexpectedStateError(`Resource not found: ${uri}`, { uri }); } /** @@ -1945,33 +2099,35 @@ export class FastMCP extends public async start( options?: Partial<{ httpStream: { - enableJsonResponse?: boolean - endpoint?: `/${string}` - eventStore?: EventStore - host?: string - port: number - stateless?: boolean - } - transportType: "httpStream" | "stdio" + enableJsonResponse?: boolean; + endpoint?: `/${string}`; + eventStore?: EventStore; + host?: string; + port: number; + stateless?: boolean; + }; + transportType: "httpStream" | "stdio"; }>, ) { - const config = this.#parseRuntimeConfig(options) + const config = this.#parseRuntimeConfig(options); if (config.transportType === "stdio") { - const transport = new StdioServerTransport() + 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 + let auth: T | undefined; if (this.#authenticate) { try { - auth = await this.#authenticate(undefined as unknown as http.IncomingMessage) + 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 } } @@ -1990,68 +2146,68 @@ export class FastMCP extends transportType: "stdio", utils: this.#options.utils, version: this.#options.version, - }) + }); - await session.connect(transport) + await session.connect(transport); - this.#sessions.push(session) + this.#sessions.push(session); session.once("error", () => { - this.#removeSession(session) - }) + this.#removeSession(session); + }); // Monitor the underlying transport for close events if (transport.onclose) { - const originalOnClose = transport.onclose + const originalOnClose = transport.onclose; transport.onclose = () => { - this.#removeSession(session) + this.#removeSession(session); if (originalOnClose) { - originalOnClose() + originalOnClose(); } - } + }; } else { transport.onclose = () => { - this.#removeSession(session) - } + this.#removeSession(session); + }; } this.emit("connect", { session: session as FastMCPSession, - }) + }); } else if (config.transportType === "httpStream") { - const httpConfig = config.httpStream + const httpConfig = config.httpStream; 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 http://${httpConfig.host}:${httpConfig.port}${httpConfig.endpoint}`, - ) + ); this.#httpStreamServer = await startHTTPServer>({ ...(this.#authenticate ? { authenticate: this.#authenticate } : {}), createServer: async (request) => { - let auth: T | undefined + let auth: T | undefined; if (this.#authenticate) { - auth = await this.#authenticate(request) + 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") + 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"] + : 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) + return this.#createSession(auth, sessionId); }, enableJsonResponse: httpConfig.enableJsonResponse, eventStore: httpConfig.eventStore, @@ -2062,69 +2218,76 @@ export class FastMCP extends }, onConnect: async () => { // No persistent session tracking in stateless mode - this.#logger.debug(`[FastMCP debug] Stateless HTTP Stream request handled`) + this.#logger.debug( + `[FastMCP debug] Stateless HTTP Stream request handled`, + ); }, onUnhandledRequest: async (req, res) => { - await this.#handleUnhandledRequest(req, res, true, httpConfig.host) + await this.#handleUnhandledRequest(req, res, true, httpConfig.host); }, port: httpConfig.port, 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 + let auth: T | undefined; if (this.#authenticate) { - auth = await this.#authenticate(request) + 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"] + : request.headers["mcp-session-id"]; - return this.#createSession(auth, sessionId) + return this.#createSession(auth, sessionId); }, enableJsonResponse: httpConfig.enableJsonResponse, eventStore: httpConfig.eventStore, host: httpConfig.host, onClose: async (session) => { - const sessionIndex = this.#sessions.indexOf(session) + const sessionIndex = this.#sessions.indexOf(session); - if (sessionIndex !== -1) this.#sessions.splice(sessionIndex, 1) + if (sessionIndex !== -1) this.#sessions.splice(sessionIndex, 1); this.emit("disconnect", { session: session as FastMCPSession, - }) + }); }, onConnect: async (session) => { - this.#sessions.push(session) + this.#sessions.push(session); - this.#logger.info(`[FastMCP info] HTTP Stream session established`) + 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) + await this.#handleUnhandledRequest( + req, + res, + false, + httpConfig.host, + ); }, port: httpConfig.port, stateless: httpConfig.stateless, streamEndpoint: httpConfig.endpoint, - }) + }); this.#logger.info( `[FastMCP info] server is running on HTTP Stream at http://${httpConfig.host}:${httpConfig.port}${httpConfig.endpoint}`, - ) + ); } } else { - throw new Error("Invalid transport type") + throw new Error("Invalid transport type"); } } @@ -2133,7 +2296,7 @@ export class FastMCP extends */ public async stop() { if (this.#httpStreamServer) { - await this.#httpStreamServer.close() + await this.#httpStreamServer.close(); } } @@ -2150,15 +2313,18 @@ export class FastMCP extends !(auth as { authenticated: unknown }).authenticated ) { const errorMessage = - "error" in auth && typeof (auth as { error: unknown }).error === "string" + "error" in auth && + typeof (auth as { error: unknown }).error === "string" ? (auth as { error: string }).error - : "Authentication failed" - throw new Error(errorMessage) + : "Authentication failed"; + throw new Error(errorMessage); } const allowedTools = auth - ? this.#tools.filter((tool) => (tool.canAccess ? tool.canAccess(auth) : true)) - : this.#tools + ? this.#tools.filter((tool) => + tool.canAccess ? tool.canAccess(auth) : true, + ) + : this.#tools; return new FastMCPSession({ auth, instructions: this.#options.instructions, @@ -2174,7 +2340,7 @@ export class FastMCP extends transportType: "httpStream", utils: this.#options.utils, version: this.#options.version, - }) + }); } /** @@ -2186,13 +2352,14 @@ export class FastMCP extends isStateless = false, host: string, ) => { - const healthConfig = this.#options.health ?? {} + const healthConfig = this.#options.health ?? {}; - const enabled = healthConfig.enabled === undefined ? true : healthConfig.enabled + const enabled = + healthConfig.enabled === undefined ? true : healthConfig.enabled; if (enabled) { - const path = healthConfig.path ?? "/health" - const url = new URL(req.url || "", `http://${host}`) + const path = healthConfig.path ?? "/health"; + const url = new URL(req.url || "", `http://${host}`); try { if (req.method === "GET" && url.pathname === path) { @@ -2200,9 +2367,9 @@ export class FastMCP extends .writeHead(healthConfig.status ?? 200, { "Content-Type": "text/plain", }) - .end(healthConfig.message ?? "✓ Ok") + .end(healthConfig.message ?? "✓ Ok"); - return + return; } // Enhanced readiness check endpoint @@ -2214,123 +2381,151 @@ export class FastMCP extends ready: 1, status: "ready", total: 1, - } + }; res .writeHead(200, { "Content-Type": "application/json", }) - .end(JSON.stringify(response)) + .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 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", + status: allReady + ? "ready" + : totalSessions === 0 + ? "no_sessions" + : "initializing", total: totalSessions, - } + }; res .writeHead(allReady ? 200 : 503, { "Content-Type": "application/json", }) - .end(JSON.stringify(response)) + .end(JSON.stringify(response)); } - return + return; } } catch (error) { - this.#logger.error("[FastMCP error] health endpoint error", error) + this.#logger.error("[FastMCP error] health endpoint error", error); } } // Handle OAuth well-known endpoints - const oauthConfig = this.#options.oauth + const oauthConfig = this.#options.oauth; if (oauthConfig?.enabled && req.method === "GET") { - const url = new URL(req.url || "", `http://${host}`) + const url = new URL(req.url || "", `http://${host}`); - if (url.pathname === "/.well-known/oauth-authorization-server" && oauthConfig.authorizationServer) { - const metadata = convertObjectToSnakeCase(oauthConfig.authorizationServer) + 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 + .end(JSON.stringify(metadata)); + return; } - if (url.pathname === "/.well-known/oauth-protected-resource" && oauthConfig.protectedResource) { - const metadata = convertObjectToSnakeCase(oauthConfig.protectedResource) + if ( + url.pathname === "/.well-known/oauth-protected-resource" && + oauthConfig.protectedResource + ) { + const metadata = convertObjectToSnakeCase( + oauthConfig.protectedResource, + ); res .writeHead(200, { "Content-Type": "application/json", }) - .end(JSON.stringify(metadata)) - return + .end(JSON.stringify(metadata)); + return; } } // If the request was not handled above, return 404 - res.writeHead(404).end() - } + res.writeHead(404).end(); + }; #parseRuntimeConfig( overrides?: Partial<{ httpStream: { - enableJsonResponse?: boolean - endpoint?: `/${string}` - host?: string - port: number - stateless?: boolean - } - transportType: "httpStream" | "stdio" + enableJsonResponse?: boolean; + endpoint?: `/${string}`; + host?: string; + port: number; + stateless?: boolean; + }; + transportType: "httpStream" | "stdio"; }>, ): | { httpStream: { - enableJsonResponse?: boolean - endpoint: `/${string}` - eventStore?: EventStore - host: string - port: number - stateless?: boolean - } - transportType: "httpStream" + enableJsonResponse?: boolean; + endpoint: `/${string}`; + eventStore?: EventStore; + host: string; + port: number; + stateless?: boolean; + }; + transportType: "httpStream"; } | { transportType: "stdio" } { - const args = process.argv.slice(2) + 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 + 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" + "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 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; return { httpStream: { @@ -2341,25 +2536,25 @@ export class FastMCP extends stateless, }, transportType: "httpStream" as const, - } + }; } - return { transportType: "stdio" as const } + return { transportType: "stdio" as const }; } #removeSession(session: FastMCPSession): void { - const sessionIndex = this.#sessions.indexOf(session) + const sessionIndex = this.#sessions.indexOf(session); if (sessionIndex !== -1) { - this.#sessions.splice(sessionIndex, 1) + this.#sessions.splice(sessionIndex, 1); this.emit("disconnect", { session: session as FastMCPSession, - }) + }); } } } -export { ErrorCode, McpError } from "@modelcontextprotocol/sdk/types.js" +export { ErrorCode, McpError } from "@modelcontextprotocol/sdk/types.js"; export type { AudioContent, @@ -2387,4 +2582,4 @@ export type { TextContent, Tool, ToolParameters, -} +}; diff --git a/src/bin/fastmcp.ts b/src/bin/fastmcp.ts index 2bf39be..769f20e 100644 --- a/src/bin/fastmcp.ts +++ b/src/bin/fastmcp.ts @@ -1,7 +1,7 @@ #!/usr/bin/env node -import { execa } from "execa" -import yargs from "yargs" -import { hideBin } from "yargs/helpers" +import { execa } from "execa"; +import yargs from "yargs"; +import { hideBin } from "yargs/helpers"; await yargs(hideBin(process.argv)) .scriptName("fastmcp") @@ -28,19 +28,21 @@ await yargs(hideBin(process.argv)) default: false, describe: "Enable verbose logging", type: "boolean", - }) + }); }, async (argv) => { try { const command = argv.watch ? `npx @wong2/mcp-cli npx tsx --watch ${argv.file}` - : `npx @wong2/mcp-cli npx tsx ${argv.file}` + : `npx @wong2/mcp-cli npx tsx ${argv.file}`; if (argv.verbose) { - console.log(`[FastMCP] Starting server: ${command}`) - console.log(`[FastMCP] File: ${argv.file}`) - console.log(`[FastMCP] Watch mode: ${argv.watch ? "enabled" : "disabled"}`) + console.log(`[FastMCP] Starting server: ${command}`); + console.log(`[FastMCP] File: ${argv.file}`); + console.log( + `[FastMCP] Watch mode: ${argv.watch ? "enabled" : "disabled"}`, + ); } await execa({ @@ -48,18 +50,18 @@ await yargs(hideBin(process.argv)) stderr: "inherit", stdin: "inherit", stdout: "inherit", - })`${command}` + })`${command}`; } catch (error) { console.error( "[FastMCP Error] Failed to start development server:", error instanceof Error ? error.message : String(error), - ) + ); if (argv.verbose && error instanceof Error && error.stack) { - console.error("[FastMCP Debug] Stack trace:", error.stack) + console.error("[FastMCP Debug] Stack trace:", error.stack); } - process.exit(1) + process.exit(1); } }, ) @@ -72,7 +74,7 @@ await yargs(hideBin(process.argv)) demandOption: true, describe: "The path to the server file", type: "string", - }) + }); }, async (argv) => { @@ -80,14 +82,14 @@ await yargs(hideBin(process.argv)) await execa({ stderr: "inherit", stdout: "inherit", - })`npx @modelcontextprotocol/inspector npx tsx ${argv.file}` + })`npx @modelcontextprotocol/inspector npx tsx ${argv.file}`; } catch (error) { console.error( "[FastMCP Error] Failed to inspect server:", error instanceof Error ? error.message : String(error), - ) + ); - process.exit(1) + process.exit(1); } }, ) @@ -108,40 +110,42 @@ await yargs(hideBin(process.argv)) default: false, describe: "Enable strict validation (type checking)", type: "boolean", - }) + }); }, async (argv) => { try { - const { existsSync } = await import("fs") - const { resolve } = await import("path") - const filePath = resolve(argv.file) + const { existsSync } = await import("fs"); + const { resolve } = await import("path"); + const filePath = resolve(argv.file); if (!existsSync(filePath)) { - console.error(`[FastMCP Error] File not found: ${filePath}`) - process.exit(1) + console.error(`[FastMCP Error] File not found: ${filePath}`); + process.exit(1); } - console.log(`[FastMCP] Validating server file: ${filePath}`) + console.log(`[FastMCP] Validating server file: ${filePath}`); - const command = argv.strict ? `npx tsc --noEmit --strict ${filePath}` : `npx tsc --noEmit ${filePath}` + const command = argv.strict + ? `npx tsc --noEmit --strict ${filePath}` + : `npx tsc --noEmit ${filePath}`; try { await execa({ shell: true, stderr: "pipe", stdout: "pipe", - })`${command}` + })`${command}`; - console.log("[FastMCP] ✓ TypeScript compilation successful") + console.log("[FastMCP] ✓ TypeScript compilation successful"); } catch (tsError) { - console.error("[FastMCP] ✗ TypeScript compilation failed") + console.error("[FastMCP] ✗ TypeScript compilation failed"); if (tsError instanceof Error && "stderr" in tsError) { - console.error(tsError.stderr) + console.error(tsError.stderr); } - process.exit(1) + process.exit(1); } try { @@ -160,22 +164,27 @@ await yargs(hideBin(process.argv)) process.exit(1); } })(); - "` + "`; } catch { - console.error("[FastMCP] ✗ Server structure validation failed") - console.error("Make sure the file properly imports and uses FastMCP") + console.error("[FastMCP] ✗ Server structure validation failed"); + console.error("Make sure the file properly imports and uses FastMCP"); - process.exit(1) + process.exit(1); } - console.log("[FastMCP] ✓ All validations passed! Server file looks good.") + console.log( + "[FastMCP] ✓ All validations passed! Server file looks good.", + ); } catch (error) { - console.error("[FastMCP Error] Validation failed:", error instanceof Error ? error.message : String(error)) + console.error( + "[FastMCP Error] Validation failed:", + error instanceof Error ? error.message : String(error), + ); - process.exit(1) + process.exit(1); } }, ) .help() - .parseAsync() + .parseAsync(); diff --git a/src/examples/addition.ts b/src/examples/addition.ts index 1a9cacb..69a0fdc 100644 --- a/src/examples/addition.ts +++ b/src/examples/addition.ts @@ -8,11 +8,11 @@ * * For a complete project template, see https://github.com/punkpeye/fastmcp-boilerplate */ -import { type } from "arktype" -import * as v from "valibot" -import { z } from "zod" +import { type } from "arktype"; +import * as v from "valibot"; +import { z } from "zod"; -import { FastMCP } from "../FastMCP.js" +import { FastMCP } from "../FastMCP.js"; const server = new FastMCP({ name: "Addition", @@ -29,13 +29,13 @@ const server = new FastMCP({ // enabled: false, }, version: "1.0.0", -}) +}); // --- Zod Example --- const AddParamsZod = z.object({ a: z.number().describe("The first number"), b: z.number().describe("The second number"), -}) +}); server.addTool({ annotations: { @@ -46,18 +46,18 @@ server.addTool({ description: "Add two numbers (using Zod schema)", execute: async (args) => { // args is typed as { a: number, b: number } - console.log(`[Zod] Adding ${args.a} and ${args.b}`) - return String(args.a + args.b) + console.log(`[Zod] Adding ${args.a} and ${args.b}`); + return String(args.a + args.b); }, name: "add-zod", parameters: AddParamsZod, -}) +}); // --- ArkType Example --- const AddParamsArkType = type({ a: "number", b: "number", -}) +}); server.addTool({ annotations: { @@ -70,31 +70,31 @@ server.addTool({ description: "Add two numbers (using ArkType schema)", execute: async (args, { log }) => { // args is typed as { a: number, b: number } based on AddParamsArkType.infer - console.log(`[ArkType] Adding ${args.a} and ${args.b}`) + console.log(`[ArkType] Adding ${args.a} and ${args.b}`); // Demonstrate long-running operation that might need a timeout - log.info("Starting calculation with potential delay...") + log.info("Starting calculation with potential delay..."); // Simulate a complex calculation process if (args.a > 1000 || args.b > 1000) { - log.warn("Large numbers detected, operation might take longer") + log.warn("Large numbers detected, operation might take longer"); // In a real implementation, this delay might be a slow operation - await new Promise((resolve) => setTimeout(resolve, 3000)) + await new Promise((resolve) => setTimeout(resolve, 3000)); } - return String(args.a + args.b) + return String(args.a + args.b); }, name: "add-arktype", parameters: AddParamsArkType, // Will abort execution after 2s timeoutMs: 2000, -}) +}); // --- Valibot Example --- const AddParamsValibot = v.object({ a: v.number("The first number"), b: v.number("The second number"), -}) +}); server.addTool({ annotations: { @@ -104,23 +104,23 @@ server.addTool({ }, description: "Add two numbers (using Valibot schema)", execute: async (args) => { - console.log(`[Valibot] Adding ${args.a} and ${args.b}`) - return String(args.a + args.b) + console.log(`[Valibot] Adding ${args.a} and ${args.b}`); + return String(args.a + args.b); }, name: "add-valibot", parameters: AddParamsValibot, -}) +}); server.addResource({ async load() { return { text: "Example log content", - } + }; }, mimeType: "text/plain", name: "Application Logs", uri: "file:///logs/app.log", -}) +}); server.addTool({ annotations: { @@ -130,30 +130,30 @@ server.addTool({ }, description: "Generate a poem line by line with streaming output", execute: async (args, context) => { - const { theme } = args + const { theme } = args; const lines = [ `Poem about ${theme} - line 1`, `Poem about ${theme} - line 2`, `Poem about ${theme} - line 3`, `Poem about ${theme} - line 4`, - ] + ]; for (const line of lines) { await context.streamContent({ text: line, type: "text", - }) + }); - await new Promise((resolve) => setTimeout(resolve, 1000)) + await new Promise((resolve) => setTimeout(resolve, 1000)); } - return + return; }, name: "stream-poem", parameters: z.object({ theme: z.string().describe("Theme for the poem"), }), -}) +}); server.addTool({ annotations: { @@ -162,27 +162,27 @@ server.addTool({ }, description: "Test progress reporting without buffering delays", execute: async (args, { reportProgress }) => { - console.log("Testing progress reporting fix for HTTP Stream buffering...") + console.log("Testing progress reporting fix for HTTP Stream buffering..."); - await reportProgress({ progress: 0, total: 100 }) - await new Promise((resolve) => setTimeout(resolve, 500)) + await reportProgress({ progress: 0, total: 100 }); + await new Promise((resolve) => setTimeout(resolve, 500)); - await reportProgress({ progress: 25, total: 100 }) - await new Promise((resolve) => setTimeout(resolve, 500)) + await reportProgress({ progress: 25, total: 100 }); + await new Promise((resolve) => setTimeout(resolve, 500)); - await reportProgress({ progress: 75, total: 100 }) - await new Promise((resolve) => setTimeout(resolve, 500)) + await reportProgress({ progress: 75, total: 100 }); + await new Promise((resolve) => setTimeout(resolve, 500)); // This progress should be received immediately - await reportProgress({ progress: 100, total: 100 }) + await reportProgress({ progress: 100, total: 100 }); - return `Buffering test completed for ${args.testCase}` + return `Buffering test completed for ${args.testCase}`; }, name: "test-buffering-fix", parameters: z.object({ testCase: z.string().describe("Test case description"), }), -}) +}); server.addPrompt({ arguments: [ @@ -194,10 +194,10 @@ server.addPrompt({ ], description: "Generate a Git commit message", load: async (args) => { - return `Generate a concise but descriptive commit message for these changes:\n\n${args.changes}` + return `Generate a concise but descriptive commit message for these changes:\n\n${args.changes}`; }, name: "git-commit", -}) +}); server.addResourceTemplate({ arguments: [ @@ -216,16 +216,18 @@ server.addResourceTemplate({ "# Deployment Guide\n\nTo deploy this application:\n\n1. Build the project: `npm run build`\n2. Set environment variables\n3. Deploy to your hosting platform", "getting-started": "# Getting Started\n\nWelcome to our project! Follow these steps to set up your development environment:\n\n1. Clone the repository\n2. Install dependencies with `npm install`\n3. Run `npm start` to begin", - } + }; return { - text: docs[args.section as keyof typeof docs] || "Documentation section not found", - } + text: + docs[args.section as keyof typeof docs] || + "Documentation section not found", + }; }, mimeType: "text/markdown", name: "Project Documentation", uriTemplate: "docs://project/{section}", -}) +}); server.addTool({ annotations: { @@ -233,7 +235,8 @@ server.addTool({ readOnlyHint: true, title: "Get Documentation (Embedded)", }, - description: "Retrieve project documentation using embedded resources - demonstrates the new embedded() feature", + description: + "Retrieve project documentation using embedded resources - demonstrates the new embedded() feature", execute: async (args) => { return { content: [ @@ -242,31 +245,37 @@ server.addTool({ type: "resource", }, ], - } + }; }, name: "get-documentation", parameters: z.object({ - section: z.enum(["getting-started", "api-reference", "deployment"]).describe("Documentation section to retrieve"), + section: z + .enum(["getting-started", "api-reference", "deployment"]) + .describe("Documentation section to retrieve"), }), -}) +}); // Select transport type based on command line arguments -const transportType = process.argv.includes("--http-stream") ? "httpStream" : "stdio" +const transportType = process.argv.includes("--http-stream") + ? "httpStream" + : "stdio"; if (transportType === "httpStream") { // Start with HTTP streaming transport - const PORT = process.env.PORT ? parseInt(process.env.PORT, 10) : 8080 + const PORT = process.env.PORT ? parseInt(process.env.PORT, 10) : 8080; server.start({ httpStream: { port: PORT, }, transportType: "httpStream", - }) + }); - console.log(`HTTP Stream MCP server is running at http://localhost:${PORT}/mcp`) - console.log("Use StreamableHTTPClientTransport to connect to this server") - console.log("For example:") + console.log( + `HTTP Stream MCP server is running at http://localhost:${PORT}/mcp`, + ); + console.log("Use StreamableHTTPClientTransport to connect to this server"); + console.log("For example:"); console.log(` import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; @@ -286,13 +295,15 @@ if (transportType === "httpStream") { ); await client.connect(transport); - `) + `); } else if (process.argv.includes("--explicit-ping-config")) { server.start({ transportType: "stdio", - }) + }); - console.log("Started stdio transport with explicit ping configuration from server options") + console.log( + "Started stdio transport with explicit ping configuration from server options", + ); } else if (process.argv.includes("--disable-roots")) { // Example of disabling roots at runtime const serverWithDisabledRoots = new FastMCP({ @@ -305,18 +316,18 @@ if (transportType === "httpStream") { enabled: false, }, version: "1.0.0", - }) + }); serverWithDisabledRoots.start({ transportType: "stdio", - }) + }); - console.log("Started stdio transport with roots support disabled") + console.log("Started stdio transport with roots support disabled"); } else { // Disable by default for: server.start({ transportType: "stdio", - }) + }); - console.log("Started stdio transport with ping disabled by default") + console.log("Started stdio transport with ping disabled by default"); } diff --git a/src/examples/custom-logger.ts b/src/examples/custom-logger.ts index 026ecd8..f204636 100644 --- a/src/examples/custom-logger.ts +++ b/src/examples/custom-logger.ts @@ -9,30 +9,30 @@ * */ -import { z } from "zod" +import { z } from "zod"; -import { FastMCP, Logger } from "../FastMCP.js" +import { FastMCP, Logger } from "../FastMCP.js"; // Example 1: Simple Custom Logger Implementation class SimpleCustomLogger implements Logger { debug(...args: unknown[]): void { - console.log("[CUSTOM DEBUG]", new Date().toISOString(), ...args) + console.log("[CUSTOM DEBUG]", new Date().toISOString(), ...args); } error(...args: unknown[]): void { - console.error("[CUSTOM ERROR]", new Date().toISOString(), ...args) + console.error("[CUSTOM ERROR]", new Date().toISOString(), ...args); } info(...args: unknown[]): void { - console.info("[CUSTOM INFO]", new Date().toISOString(), ...args) + console.info("[CUSTOM INFO]", new Date().toISOString(), ...args); } log(...args: unknown[]): void { - console.log("[CUSTOM LOG]", new Date().toISOString(), ...args) + console.log("[CUSTOM LOG]", new Date().toISOString(), ...args); } warn(...args: unknown[]): void { - console.warn("[CUSTOM WARN]", new Date().toISOString(), ...args) + console.warn("[CUSTOM WARN]", new Date().toISOString(), ...args); } } @@ -172,7 +172,7 @@ class SimpleCustomLogger implements Logger { // } // Choose which logger to use (uncomment the one you want to use) -const logger = new SimpleCustomLogger() +const logger = new SimpleCustomLogger(); // const logger = new FileLogger(); // const logger = new WinstonLoggerAdapter(); // const logger = new PinoLoggerAdapter(); @@ -181,21 +181,21 @@ const server = new FastMCP({ logger: logger, name: "custom-logger-example", version: "1.0.0", -}) +}); server.addTool({ description: "A test tool that demonstrates custom logging", execute: async (args) => { - return `Received: ${args.message}` + return `Received: ${args.message}`; }, name: "test_tool", parameters: z.object({ message: z.string().describe("A message to log"), }), -}) +}); // Start the server with stdio transport server.start({ transportType: "stdio" }).catch((error: unknown) => { - console.error("Failed to start server:", error) - process.exit(1) -}) + console.error("Failed to start server:", error); + process.exit(1); +}); diff --git a/src/examples/oauth-server.ts b/src/examples/oauth-server.ts index 589d707..80b23ce 100644 --- a/src/examples/oauth-server.ts +++ b/src/examples/oauth-server.ts @@ -10,7 +10,7 @@ * - http://localhost:4111/.well-known/oauth-protected-resource */ -import { FastMCP } from "../FastMCP.js" +import { FastMCP } from "../FastMCP.js"; const server = new FastMCP({ name: "OAuth Example Server", @@ -36,14 +36,20 @@ const server = new FastMCP({ scopesSupported: ["read", "write", "admin"], serviceDocumentation: "https://docs.example.com/oauth", tokenEndpoint: "https://auth.example.com/oauth/token", - tokenEndpointAuthMethodsSupported: ["client_secret_basic", "client_secret_post"], + tokenEndpointAuthMethodsSupported: [ + "client_secret_basic", + "client_secret_post", + ], tokenEndpointAuthSigningAlgValuesSupported: ["RS256", "ES256"], uiLocalesSupported: ["en-US", "es-ES"], }, enabled: true, protectedResource: { - authorizationDetailsTypesSupported: ["payment_initiation", "account_access"], + authorizationDetailsTypesSupported: [ + "payment_initiation", + "account_access", + ], authorizationServers: ["https://auth.example.com"], bearerMethodsSupported: ["header"], dpopBoundAccessTokensRequired: false, @@ -61,7 +67,7 @@ const server = new FastMCP({ }, }, version: "1.0.0", -}) +}); // Add a simple tool to demonstrate the server functionality server.addTool({ @@ -81,16 +87,16 @@ that need to integrate with OAuth 2.0 authorization flows.`, type: "text", }, ], - } + }; }, name: "get-server-info", -}) +}); // Start the server await server.start({ httpStream: { port: 4111 }, transportType: "httpStream", -}) +}); console.log(` 🚀 OAuth Example Server is running! @@ -104,4 +110,4 @@ Try these endpoints: The OAuth endpoints work with both SSE and HTTP Stream transports and return JSON metadata following RFC 8414 standards. -`) +`); diff --git a/src/examples/session-context.ts b/src/examples/session-context.ts index b14a661..a1c2318 100644 --- a/src/examples/session-context.ts +++ b/src/examples/session-context.ts @@ -9,54 +9,61 @@ * npx fastmcp dev src/examples/session-context.ts */ -import { z } from "zod" +import { z } from "zod"; -import { FastMCP } from "../FastMCP.js" +import { FastMCP } from "../FastMCP.js"; interface UserSession { - [key: string]: unknown - permissions: string[] - role: "admin" | "guest" | "user" - userId: string - username: string + [key: string]: unknown; + permissions: string[]; + role: "admin" | "guest" | "user"; + userId: string; + username: string; } const server = new FastMCP({ authenticate: async (request) => { if (!request) { - console.log("[Auth] Authenticating stdio transport using environment variables") - - const userId = process.env.USER_ID || "default-user" - const username = process.env.USERNAME || "Anonymous" - const role = (process.env.USER_ROLE as "admin" | "guest" | "user") || "guest" + console.log( + "[Auth] Authenticating stdio transport using environment variables", + ); + + const userId = process.env.USER_ID || "default-user"; + const username = process.env.USERNAME || "Anonymous"; + const role = + (process.env.USER_ROLE as "admin" | "guest" | "user") || "guest"; // Mock permissions based on role const permissions = - role === "admin" ? ["read", "write", "delete", "admin"] : role === "user" ? ["read", "write"] : ["read"] + role === "admin" + ? ["read", "write", "delete", "admin"] + : role === "user" + ? ["read", "write"] + : ["read"]; const session: UserSession = { authenticatedAt: new Date().toISOString(), permissions, role, userId, username, - } + }; - console.log(`[Auth] Authenticated user: ${username} (${role})`) + console.log(`[Auth] Authenticated user: ${username} (${role})`); - return session + return session; } // For HTTP transport (request contains headers) - console.log("[Auth] Authenticating HTTP transport using headers") + console.log("[Auth] Authenticating HTTP transport using headers"); - const authHeader = request.headers["authorization"] as string + const authHeader = request.headers["authorization"] as string; if (!authHeader || !authHeader.startsWith("Bearer ")) { throw new Response("Missing or invalid authorization header", { status: 401, - }) + }); } - const token = authHeader.substring(7) + const token = authHeader.substring(7); // Mock token validation (in real implementation, validate against your auth service) if (token === "admin-token") { @@ -66,7 +73,7 @@ const server = new FastMCP({ role: "admin" as const, userId: "admin-001", username: "Administrator", - } + }; } else if (token === "user-token") { return { authenticatedAt: new Date().toISOString(), @@ -74,22 +81,23 @@ const server = new FastMCP({ role: "user" as const, userId: "user-001", username: "Regular User", - } + }; } - throw new Response("Invalid token", { status: 401 }) + throw new Response("Invalid token", { status: 401 }); }, name: "Session Context Demo", version: "1.0.0", -}) +}); // Tool that demonstrates session context access server.addTool({ description: "Get information about the current authenticated user", execute: async (_args, context) => { - if (!context.session) return "No session context available (this shouldn't happen after the fix!)" + if (!context.session) + return "No session context available (this shouldn't happen after the fix!)"; - const { session } = context + const { session } = context; return `✓ Session Context Available! @@ -98,46 +106,49 @@ User Info: - Username: ${session.username} - Role: ${session.role} - Permissions: ${session.permissions.join(", ")} -- Authenticated At: ${session.authenticatedAt}` +- Authenticated At: ${session.authenticatedAt}`; }, name: "whoami", -}) +}); // Tool that demonstrates role-based access server.addTool({ description: "Perform an admin-only operation (requires admin role)", execute: async (args, context) => { - if (!context.session) return "No session context - cannot verify permissions" - if (context.session.role !== "admin") return `Access denied. Current role: ${context.session.role}, required: admin` - if (!context.session.permissions.includes("admin")) return "Insufficient permissions for admin operations" - - return `✓ Admin operation "${args.action}" executed successfully by ${context.session.username}` + if (!context.session) + return "No session context - cannot verify permissions"; + if (context.session.role !== "admin") + return `Access denied. Current role: ${context.session.role}, required: admin`; + if (!context.session.permissions.includes("admin")) + return "Insufficient permissions for admin operations"; + + return `✓ Admin operation "${args.action}" executed successfully by ${context.session.username}`; }, name: "admin-operation", parameters: z.object({ action: z.string().describe("The admin action to perform"), }), -}) +}); // Tool that demonstrates permission checks server.addTool({ description: "Check what permissions the current user has", execute: async (args, context) => { - if (!context.session) return "No session context available" + if (!context.session) return "No session context available"; - const hasPermission = context.session.permissions.includes(args.operation) + const hasPermission = context.session.permissions.includes(args.operation); return `Permission Check for "${args.operation}": ${hasPermission ? "✓ ALLOWED" : "! DENIED"} Your permissions: ${context.session.permissions.join(", ")} -Your role: ${context.session.role}` +Your role: ${context.session.role}`; }, name: "check-permissions", parameters: z.object({ operation: z.string().describe("Operation to check permission for"), }), -}) +}); // Resource that uses session context server.addResource({ @@ -154,7 +165,7 @@ server.addResource({ null, 2, ), - } + }; } return { @@ -173,12 +184,12 @@ server.addResource({ null, 2, ), - } + }; }, mimeType: "application/json", name: "Current User Information", uri: "session://current-user", -}) +}); // Prompt that uses session context server.addPrompt({ @@ -191,31 +202,31 @@ server.addPrompt({ ], description: "Generate a personalized greeting based on the current user", load: async (args, auth) => { - const style = args.style || "friendly" + const style = args.style || "friendly"; if (!auth) { - return "Hello! I don't have access to your session information." + return "Hello! I don't have access to your session information."; } const greetings = { casual: `Hey ${auth.username}! Nice to see you again.`, formal: `Good day, ${auth.username}. You are logged in with ${auth.role} privileges.`, friendly: `Hello ${auth.username}! 😊 You're logged in as a ${auth.role}. How can I help you today?`, - } + }; - return greetings[style as keyof typeof greetings] || greetings.friendly + return greetings[style as keyof typeof greetings] || greetings.friendly; }, name: "personalized-greeting", -}) +}); // Start the server if (process.argv.includes("--http-stream")) { - const PORT = process.env.PORT ? parseInt(process.env.PORT, 10) : 3000 + const PORT = process.env.PORT ? parseInt(process.env.PORT, 10) : 3000; server.start({ httpStream: { port: PORT }, transportType: "httpStream", - }) + }); console.log(` 🚀 Session Context Demo server running on HTTP Stream! @@ -229,9 +240,9 @@ curl -H "Authorization: Bearer admin-token" \\ -H "Content-Type: application/json" \\ -d '{"method":"tools/call","params":{"name":"whoami","arguments":{}}}' \\ http://localhost:${PORT}/mcp -`) +`); } else { - server.start({ transportType: "stdio" }) + server.start({ transportType: "stdio" }); console.log(` 🚀 Session Context Demo server started with stdio transport! @@ -254,5 +265,5 @@ Available resources: Available prompts: - personalized-greeting: Get a personalized greeting -`) +`); } diff --git a/src/examples/session-id-counter.ts b/src/examples/session-id-counter.ts index 2c052bd..9534463 100644 --- a/src/examples/session-id-counter.ts +++ b/src/examples/session-id-counter.ts @@ -11,14 +11,14 @@ * Then test with multiple clients to see how each session maintains its own state. */ -import { z } from "zod" +import { z } from "zod"; -import { FastMCP } from "../FastMCP.js" +import { FastMCP } from "../FastMCP.js"; interface UserSession { - [key: string]: unknown - role: "admin" | "user" - userId: string + [key: string]: unknown; + role: "admin" | "user"; + userId: string; } const server = new FastMCP({ @@ -28,43 +28,46 @@ const server = new FastMCP({ return { role: "user" as const, userId: process.env.USER_ID || "default-user", - } + }; } // HTTP transport - check authorization header - const authHeader = request.headers["authorization"] as string + const authHeader = request.headers["authorization"] as string; if (!authHeader || !authHeader.startsWith("Bearer ")) { throw new Response("Missing or invalid authorization header", { status: 401, - }) + }); } - const token = authHeader.substring(7) + const token = authHeader.substring(7); // Mock token validation if (token === "admin-token") { return { role: "admin" as const, userId: "admin-001", - } + }; } else if (token === "user-token") { return { role: "user" as const, userId: "user-001", - } + }; } - throw new Response("Invalid token", { status: 401 }) + throw new Response("Invalid token", { status: 401 }); }, name: "Session ID Counter Demo", version: "1.0.0", -}) +}); // Per-session counter storage // In a real application, this could be Redis, a database, or any other storage -const sessionCounters = new Map() -const sessionData = new Map() +const sessionCounters = new Map(); +const sessionData = new Map< + string, + { createdAt: Date; lastAccessed: Date; requestCount: number } +>(); // Tool to increment a per-session counter server.addTool({ @@ -72,22 +75,22 @@ server.addTool({ "Increment a counter that is unique to your session. Each client session maintains its own independent counter.", execute: async (_args, context) => { if (!context.sessionId) { - return "❌ No session ID available. This tool requires HTTP transport with session tracking." + return "❌ No session ID available. This tool requires HTTP transport with session tracking."; } - const currentCount = sessionCounters.get(context.sessionId) || 0 - const newCount = currentCount + 1 - sessionCounters.set(context.sessionId, newCount) + const currentCount = sessionCounters.get(context.sessionId) || 0; + const newCount = currentCount + 1; + sessionCounters.set(context.sessionId, newCount); // Update session metadata const metadata = sessionData.get(context.sessionId) || { createdAt: new Date(), lastAccessed: new Date(), requestCount: 0, - } - metadata.lastAccessed = new Date() - metadata.requestCount += 1 - sessionData.set(context.sessionId, metadata) + }; + metadata.lastAccessed = new Date(); + metadata.requestCount += 1; + sessionData.set(context.sessionId, metadata); return `✓ Counter incremented! @@ -99,79 +102,80 @@ Role: ${context.session?.role} Session Info: - Created: ${metadata.createdAt.toISOString()} - Last Accessed: ${metadata.lastAccessed.toISOString()} -- Total Requests: ${metadata.requestCount}` +- Total Requests: ${metadata.requestCount}`; }, name: "increment-counter", parameters: z.object({}), -}) +}); // Tool to get the current counter value server.addTool({ description: "Get the current value of your session's counter", execute: async (_args, context) => { if (!context.sessionId) { - return "❌ No session ID available. This tool requires HTTP transport with session tracking." + return "❌ No session ID available. This tool requires HTTP transport with session tracking."; } - const currentCount = sessionCounters.get(context.sessionId) || 0 - const metadata = sessionData.get(context.sessionId) + const currentCount = sessionCounters.get(context.sessionId) || 0; + const metadata = sessionData.get(context.sessionId); return `Session ID: ${context.sessionId} Counter Value: ${currentCount} User: ${context.session?.userId} -${metadata ? `\nSession created: ${metadata.createdAt.toISOString()}\nTotal requests: ${metadata.requestCount}` : ""}` +${metadata ? `\nSession created: ${metadata.createdAt.toISOString()}\nTotal requests: ${metadata.requestCount}` : ""}`; }, name: "get-counter", parameters: z.object({}), -}) +}); // Tool to reset the counter server.addTool({ description: "Reset your session's counter to zero", execute: async (_args, context) => { if (!context.sessionId) { - return "❌ No session ID available. This tool requires HTTP transport with session tracking." + return "❌ No session ID available. This tool requires HTTP transport with session tracking."; } - sessionCounters.set(context.sessionId, 0) + sessionCounters.set(context.sessionId, 0); - return `✓ Counter reset to 0 for session ${context.sessionId}` + return `✓ Counter reset to 0 for session ${context.sessionId}`; }, name: "reset-counter", parameters: z.object({}), -}) +}); // Tool to list all active sessions (admin only) server.addTool({ description: "List all active sessions and their counter values (admin only)", execute: async (_args, context) => { if (context.session?.role !== "admin") { - return "❌ Access denied. This tool requires admin role." + return "❌ Access denied. This tool requires admin role."; } if (sessionCounters.size === 0) { - return "No active sessions with counters." + return "No active sessions with counters."; } const sessions = Array.from(sessionCounters.entries()) .map(([sessionId, count]) => { - const metadata = sessionData.get(sessionId) + const metadata = sessionData.get(sessionId); return `- Session: ${sessionId.substring(0, 8)}... Counter: ${count} Created: ${metadata?.createdAt.toISOString() || "unknown"} - Requests: ${metadata?.requestCount || 0}` + Requests: ${metadata?.requestCount || 0}`; }) - .join("\n\n") + .join("\n\n"); - return `Active Sessions (${sessionCounters.size}):\n\n${sessions}` + return `Active Sessions (${sessionCounters.size}):\n\n${sessions}`; }, name: "list-sessions", parameters: z.object({}), -}) +}); // Tool to demonstrate request ID tracking server.addTool({ - description: "Show both session ID and request ID to demonstrate per-request tracking", + description: + "Show both session ID and request ID to demonstrate per-request tracking", execute: async (_args, context) => { return `Session & Request Information: @@ -181,19 +185,19 @@ User ID: ${context.session?.userId || "N/A"} Role: ${context.session?.role || "N/A"} The session ID remains constant across multiple requests from the same client, -while the request ID is unique for each individual request.` +while the request ID is unique for each individual request.`; }, name: "show-ids", parameters: z.object({}), -}) +}); // Start the server -const PORT = process.env.PORT ? parseInt(process.env.PORT, 10) : 3000 +const PORT = process.env.PORT ? parseInt(process.env.PORT, 10) : 3000; server.start({ httpStream: { port: PORT }, transportType: "httpStream", -}) +}); console.log(` 🚀 Session ID Counter Demo server running! @@ -223,4 +227,4 @@ Available tools: - show-ids: Display session and request IDs Try connecting with multiple clients to see how each maintains its own counter! -`) +`); diff --git a/vitest.config.js b/vitest.config.js index f76abd6..2e668db 100644 --- a/vitest.config.js +++ b/vitest.config.js @@ -1,4 +1,4 @@ -import { defineConfig } from "vitest/config" +import { defineConfig } from "vitest/config"; export default defineConfig({ test: { @@ -6,4 +6,4 @@ export default defineConfig({ forks: { execArgv: ["--experimental-eventsource"] }, }, }, -}) +});