fastmcp 3.20.0 → 3.20.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/FastMCP.ts DELETED
@@ -1,2548 +0,0 @@
1
- import { Server } from "@modelcontextprotocol/sdk/server/index.js";
2
- import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
- import { EventStore } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
4
- import { RequestOptions } from "@modelcontextprotocol/sdk/shared/protocol.js";
5
- import { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
6
- import {
7
- CallToolRequestSchema,
8
- ClientCapabilities,
9
- CompleteRequestSchema,
10
- CreateMessageRequestSchema,
11
- ErrorCode,
12
- GetPromptRequestSchema,
13
- GetPromptResult,
14
- ListPromptsRequestSchema,
15
- ListResourcesRequestSchema,
16
- ListResourcesResult,
17
- ListResourceTemplatesRequestSchema,
18
- ListResourceTemplatesResult,
19
- ListToolsRequestSchema,
20
- McpError,
21
- ReadResourceRequestSchema,
22
- ResourceLink,
23
- Root,
24
- RootsListChangedNotificationSchema,
25
- ServerCapabilities,
26
- SetLevelRequestSchema,
27
- } from "@modelcontextprotocol/sdk/types.js";
28
- import { StandardSchemaV1 } from "@standard-schema/spec";
29
- import { EventEmitter } from "events";
30
- import { readFile } from "fs/promises";
31
- import Fuse from "fuse.js";
32
- import http from "http";
33
- import { startHTTPServer } from "mcp-proxy";
34
- import { StrictEventEmitter } from "strict-event-emitter-types";
35
- import { setTimeout as delay } from "timers/promises";
36
- import { fetch } from "undici";
37
- import parseURITemplate from "uri-templates";
38
- import { toJsonSchema } from "xsschema";
39
- import { z } from "zod";
40
-
41
- export interface Logger {
42
- debug(...args: unknown[]): void;
43
- error(...args: unknown[]): void;
44
- info(...args: unknown[]): void;
45
- log(...args: unknown[]): void;
46
- warn(...args: unknown[]): void;
47
- }
48
-
49
- export type SSEServer = {
50
- close: () => Promise<void>;
51
- };
52
-
53
- type FastMCPEvents<T extends FastMCPSessionAuth> = {
54
- connect: (event: { session: FastMCPSession<T> }) => void;
55
- disconnect: (event: { session: FastMCPSession<T> }) => void;
56
- };
57
-
58
- type FastMCPSessionEvents = {
59
- error: (event: { error: Error }) => void;
60
- ready: () => void;
61
- rootsChanged: (event: { roots: Root[] }) => void;
62
- };
63
-
64
- export const imageContent = async (
65
- input: { buffer: Buffer } | { path: string } | { url: string },
66
- ): Promise<ImageContent> => {
67
- let rawData: Buffer;
68
-
69
- try {
70
- if ("url" in input) {
71
- try {
72
- const response = await fetch(input.url);
73
-
74
- if (!response.ok) {
75
- throw new Error(
76
- `Server responded with status: ${response.status} - ${response.statusText}`,
77
- );
78
- }
79
-
80
- rawData = Buffer.from(await response.arrayBuffer());
81
- } catch (error) {
82
- throw new Error(
83
- `Failed to fetch image from URL (${input.url}): ${
84
- error instanceof Error ? error.message : String(error)
85
- }`,
86
- );
87
- }
88
- } else if ("path" in input) {
89
- try {
90
- rawData = await readFile(input.path);
91
- } catch (error) {
92
- throw new Error(
93
- `Failed to read image from path (${input.path}): ${
94
- error instanceof Error ? error.message : String(error)
95
- }`,
96
- );
97
- }
98
- } else if ("buffer" in input) {
99
- rawData = input.buffer;
100
- } else {
101
- throw new Error(
102
- "Invalid input: Provide a valid 'url', 'path', or 'buffer'",
103
- );
104
- }
105
-
106
- const { fileTypeFromBuffer } = await import("file-type");
107
- const mimeType = await fileTypeFromBuffer(rawData);
108
-
109
- if (!mimeType || !mimeType.mime.startsWith("image/")) {
110
- console.warn(
111
- `Warning: Content may not be a valid image. Detected MIME: ${
112
- mimeType?.mime || "unknown"
113
- }`,
114
- );
115
- }
116
-
117
- const base64Data = rawData.toString("base64");
118
-
119
- return {
120
- data: base64Data,
121
- mimeType: mimeType?.mime ?? "image/png",
122
- type: "image",
123
- } as const;
124
- } catch (error) {
125
- if (error instanceof Error) {
126
- throw error;
127
- } else {
128
- throw new Error(`Unexpected error processing image: ${String(error)}`);
129
- }
130
- }
131
- };
132
-
133
- export const audioContent = async (
134
- input: { buffer: Buffer } | { path: string } | { url: string },
135
- ): Promise<AudioContent> => {
136
- let rawData: Buffer;
137
-
138
- try {
139
- if ("url" in input) {
140
- try {
141
- const response = await fetch(input.url);
142
-
143
- if (!response.ok) {
144
- throw new Error(
145
- `Server responded with status: ${response.status} - ${response.statusText}`,
146
- );
147
- }
148
-
149
- rawData = Buffer.from(await response.arrayBuffer());
150
- } catch (error) {
151
- throw new Error(
152
- `Failed to fetch audio from URL (${input.url}): ${
153
- error instanceof Error ? error.message : String(error)
154
- }`,
155
- );
156
- }
157
- } else if ("path" in input) {
158
- try {
159
- rawData = await readFile(input.path);
160
- } catch (error) {
161
- throw new Error(
162
- `Failed to read audio from path (${input.path}): ${
163
- error instanceof Error ? error.message : String(error)
164
- }`,
165
- );
166
- }
167
- } else if ("buffer" in input) {
168
- rawData = input.buffer;
169
- } else {
170
- throw new Error(
171
- "Invalid input: Provide a valid 'url', 'path', or 'buffer'",
172
- );
173
- }
174
-
175
- const { fileTypeFromBuffer } = await import("file-type");
176
- const mimeType = await fileTypeFromBuffer(rawData);
177
-
178
- if (!mimeType || !mimeType.mime.startsWith("audio/")) {
179
- console.warn(
180
- `Warning: Content may not be a valid audio file. Detected MIME: ${
181
- mimeType?.mime || "unknown"
182
- }`,
183
- );
184
- }
185
-
186
- const base64Data = rawData.toString("base64");
187
-
188
- return {
189
- data: base64Data,
190
- mimeType: mimeType?.mime ?? "audio/mpeg",
191
- type: "audio",
192
- } as const;
193
- } catch (error) {
194
- if (error instanceof Error) {
195
- throw error;
196
- } else {
197
- throw new Error(`Unexpected error processing audio: ${String(error)}`);
198
- }
199
- }
200
- };
201
-
202
- type Context<T extends FastMCPSessionAuth> = {
203
- client: {
204
- version: ReturnType<Server["getClientVersion"]>;
205
- };
206
- log: {
207
- debug: (message: string, data?: SerializableValue) => void;
208
- error: (message: string, data?: SerializableValue) => void;
209
- info: (message: string, data?: SerializableValue) => void;
210
- warn: (message: string, data?: SerializableValue) => void;
211
- };
212
- reportProgress: (progress: Progress) => Promise<void>;
213
- /**
214
- * Request ID from the current MCP request.
215
- * Available for all transports when the client provides it.
216
- */
217
- requestId?: string;
218
- session: T | undefined;
219
- /**
220
- * Session ID from the Mcp-Session-Id header.
221
- * Only available for HTTP-based transports (SSE, HTTP Stream).
222
- * Can be used to track per-session state, implement session-specific
223
- * counters, or maintain user-specific data across multiple requests.
224
- */
225
- sessionId?: string;
226
- streamContent: (content: Content | Content[]) => Promise<void>;
227
- };
228
-
229
- type Extra = unknown;
230
-
231
- type Extras = Record<string, Extra>;
232
-
233
- type Literal = boolean | null | number | string | undefined;
234
-
235
- type Progress = {
236
- /**
237
- * The progress thus far. This should increase every time progress is made, even if the total is unknown.
238
- */
239
- progress: number;
240
- /**
241
- * Total number of items to process (or total progress required), if known.
242
- */
243
- total?: number;
244
- };
245
-
246
- type SerializableValue =
247
- | { [key: string]: SerializableValue }
248
- | Literal
249
- | SerializableValue[];
250
-
251
- type TextContent = {
252
- text: string;
253
- type: "text";
254
- };
255
-
256
- type ToolParameters = StandardSchemaV1;
257
-
258
- abstract class FastMCPError extends Error {
259
- public constructor(message?: string) {
260
- super(message);
261
- this.name = new.target.name;
262
- }
263
- }
264
-
265
- export class UnexpectedStateError extends FastMCPError {
266
- public extras?: Extras;
267
-
268
- public constructor(message: string, extras?: Extras) {
269
- super(message);
270
- this.name = new.target.name;
271
- this.extras = extras;
272
- }
273
- }
274
-
275
- /**
276
- * An error that is meant to be surfaced to the user.
277
- */
278
- export class UserError extends UnexpectedStateError {}
279
-
280
- const TextContentZodSchema = z
281
- .object({
282
- /**
283
- * The text content of the message.
284
- */
285
- text: z.string(),
286
- type: z.literal("text"),
287
- })
288
- .strict() satisfies z.ZodType<TextContent>;
289
-
290
- type ImageContent = {
291
- data: string;
292
- mimeType: string;
293
- type: "image";
294
- };
295
-
296
- const ImageContentZodSchema = z
297
- .object({
298
- /**
299
- * The base64-encoded image data.
300
- */
301
- data: z.string().base64(),
302
- /**
303
- * The MIME type of the image. Different providers may support different image types.
304
- */
305
- mimeType: z.string(),
306
- type: z.literal("image"),
307
- })
308
- .strict() satisfies z.ZodType<ImageContent>;
309
-
310
- type AudioContent = {
311
- data: string;
312
- mimeType: string;
313
- type: "audio";
314
- };
315
-
316
- const AudioContentZodSchema = z
317
- .object({
318
- /**
319
- * The base64-encoded audio data.
320
- */
321
- data: z.string().base64(),
322
- mimeType: z.string(),
323
- type: z.literal("audio"),
324
- })
325
- .strict() satisfies z.ZodType<AudioContent>;
326
-
327
- type ResourceContent = {
328
- resource: {
329
- blob?: string;
330
- mimeType?: string;
331
- text?: string;
332
- uri: string;
333
- };
334
- type: "resource";
335
- };
336
-
337
- const ResourceContentZodSchema = z
338
- .object({
339
- resource: z.object({
340
- blob: z.string().optional(),
341
- mimeType: z.string().optional(),
342
- text: z.string().optional(),
343
- uri: z.string(),
344
- }),
345
- type: z.literal("resource"),
346
- })
347
- .strict() satisfies z.ZodType<ResourceContent>;
348
-
349
- const ResourceLinkZodSchema = z.object({
350
- description: z.string().optional(),
351
- mimeType: z.string().optional(),
352
- name: z.string(),
353
- title: z.string().optional(),
354
- type: z.literal("resource_link"),
355
- uri: z.string(),
356
- }) satisfies z.ZodType<ResourceLink>;
357
-
358
- type Content =
359
- | AudioContent
360
- | ImageContent
361
- | ResourceContent
362
- | ResourceLink
363
- | TextContent;
364
-
365
- const ContentZodSchema = z.discriminatedUnion("type", [
366
- TextContentZodSchema,
367
- ImageContentZodSchema,
368
- AudioContentZodSchema,
369
- ResourceContentZodSchema,
370
- ResourceLinkZodSchema,
371
- ]) satisfies z.ZodType<Content>;
372
-
373
- type ContentResult = {
374
- content: Content[];
375
- isError?: boolean;
376
- };
377
-
378
- const ContentResultZodSchema = z
379
- .object({
380
- content: ContentZodSchema.array(),
381
- isError: z.boolean().optional(),
382
- })
383
- .strict() satisfies z.ZodType<ContentResult>;
384
-
385
- type Completion = {
386
- hasMore?: boolean;
387
- total?: number;
388
- values: string[];
389
- };
390
-
391
- /**
392
- * https://github.com/modelcontextprotocol/typescript-sdk/blob/3164da64d085ec4e022ae881329eee7b72f208d4/src/types.ts#L983-L1003
393
- */
394
- const CompletionZodSchema = z.object({
395
- /**
396
- * Indicates whether there are additional completion options beyond those provided in the current response, even if the exact total is unknown.
397
- */
398
- hasMore: z.optional(z.boolean()),
399
- /**
400
- * The total number of completion options available. This can exceed the number of values actually sent in the response.
401
- */
402
- total: z.optional(z.number().int()),
403
- /**
404
- * An array of completion values. Must not exceed 100 items.
405
- */
406
- values: z.array(z.string()).max(100),
407
- }) satisfies z.ZodType<Completion>;
408
-
409
- type ArgumentValueCompleter<T extends FastMCPSessionAuth = FastMCPSessionAuth> =
410
- (value: string, auth?: T) => Promise<Completion>;
411
-
412
- type InputPrompt<
413
- T extends FastMCPSessionAuth = FastMCPSessionAuth,
414
- Arguments extends InputPromptArgument<T>[] = InputPromptArgument<T>[],
415
- Args = PromptArgumentsToObject<Arguments>,
416
- > = {
417
- arguments?: InputPromptArgument<T>[];
418
- description?: string;
419
- load: (args: Args, auth?: T) => Promise<PromptResult>;
420
- name: string;
421
- };
422
-
423
- type InputPromptArgument<T extends FastMCPSessionAuth = FastMCPSessionAuth> =
424
- Readonly<{
425
- complete?: ArgumentValueCompleter<T>;
426
- description?: string;
427
- enum?: string[];
428
- name: string;
429
- required?: boolean;
430
- }>;
431
-
432
- type InputResourceTemplate<
433
- T extends FastMCPSessionAuth,
434
- Arguments extends
435
- InputResourceTemplateArgument<T>[] = InputResourceTemplateArgument<T>[],
436
- > = {
437
- arguments: Arguments;
438
- description?: string;
439
- load: (
440
- args: ResourceTemplateArgumentsToObject<Arguments>,
441
- auth?: T,
442
- ) => Promise<ResourceResult | ResourceResult[]>;
443
- mimeType?: string;
444
- name: string;
445
- uriTemplate: string;
446
- };
447
-
448
- type InputResourceTemplateArgument<
449
- T extends FastMCPSessionAuth = FastMCPSessionAuth,
450
- > = Readonly<{
451
- complete?: ArgumentValueCompleter<T>;
452
- description?: string;
453
- name: string;
454
- required?: boolean;
455
- }>;
456
-
457
- type LoggingLevel =
458
- | "alert"
459
- | "critical"
460
- | "debug"
461
- | "emergency"
462
- | "error"
463
- | "info"
464
- | "notice"
465
- | "warning";
466
-
467
- type Prompt<
468
- T extends FastMCPSessionAuth = FastMCPSessionAuth,
469
- Arguments extends PromptArgument<T>[] = PromptArgument<T>[],
470
- Args = PromptArgumentsToObject<Arguments>,
471
- > = {
472
- arguments?: PromptArgument<T>[];
473
- complete?: (name: string, value: string, auth?: T) => Promise<Completion>;
474
- description?: string;
475
- load: (args: Args, auth?: T) => Promise<PromptResult>;
476
- name: string;
477
- };
478
-
479
- type PromptArgument<T extends FastMCPSessionAuth = FastMCPSessionAuth> =
480
- Readonly<{
481
- complete?: ArgumentValueCompleter<T>;
482
- description?: string;
483
- enum?: string[];
484
- name: string;
485
- required?: boolean;
486
- }>;
487
-
488
- type PromptArgumentsToObject<T extends { name: string; required?: boolean }[]> =
489
- {
490
- [K in T[number]["name"]]: Extract<
491
- T[number],
492
- { name: K }
493
- >["required"] extends true
494
- ? string
495
- : string | undefined;
496
- };
497
-
498
- type PromptResult = Pick<GetPromptResult, "messages"> | string;
499
-
500
- type Resource<T extends FastMCPSessionAuth> = {
501
- complete?: (name: string, value: string, auth?: T) => Promise<Completion>;
502
- description?: string;
503
- load: (auth?: T) => Promise<ResourceResult | ResourceResult[]>;
504
- mimeType?: string;
505
- name: string;
506
- uri: string;
507
- };
508
-
509
- type ResourceResult =
510
- | {
511
- blob: string;
512
- mimeType?: string;
513
- uri?: string;
514
- }
515
- | {
516
- mimeType?: string;
517
- text: string;
518
- uri?: string;
519
- };
520
-
521
- type ResourceTemplate<
522
- T extends FastMCPSessionAuth,
523
- Arguments extends
524
- ResourceTemplateArgument<T>[] = ResourceTemplateArgument<T>[],
525
- > = {
526
- arguments: Arguments;
527
- complete?: (name: string, value: string, auth?: T) => Promise<Completion>;
528
- description?: string;
529
- load: (
530
- args: ResourceTemplateArgumentsToObject<Arguments>,
531
- auth?: T,
532
- ) => Promise<ResourceResult | ResourceResult[]>;
533
- mimeType?: string;
534
- name: string;
535
- uriTemplate: string;
536
- };
537
-
538
- type ResourceTemplateArgument<
539
- T extends FastMCPSessionAuth = FastMCPSessionAuth,
540
- > = Readonly<{
541
- complete?: ArgumentValueCompleter<T>;
542
- description?: string;
543
- name: string;
544
- required?: boolean;
545
- }>;
546
-
547
- type ResourceTemplateArgumentsToObject<T extends { name: string }[]> = {
548
- [K in T[number]["name"]]: string;
549
- };
550
-
551
- type SamplingResponse = {
552
- content: AudioContent | ImageContent | TextContent;
553
- model: string;
554
- role: "assistant" | "user";
555
- stopReason?: "endTurn" | "maxTokens" | "stopSequence" | string;
556
- };
557
-
558
- type ServerOptions<T extends FastMCPSessionAuth> = {
559
- authenticate?: Authenticate<T>;
560
- /**
561
- * Configuration for the health-check endpoint that can be exposed when the
562
- * server is running using the HTTP Stream transport. When enabled, the
563
- * server will respond to an HTTP GET request with the configured path (by
564
- * default "/health") rendering a plain-text response (by default "ok") and
565
- * the configured status code (by default 200).
566
- *
567
- * The endpoint is only added when the server is started with
568
- * `transportType: "httpStream"` – it is ignored for the stdio transport.
569
- */
570
- health?: {
571
- /**
572
- * When set to `false` the health-check endpoint is disabled.
573
- * @default true
574
- */
575
- enabled?: boolean;
576
-
577
- /**
578
- * Plain-text body returned by the endpoint.
579
- * @default "ok"
580
- */
581
- message?: string;
582
-
583
- /**
584
- * HTTP path that should be handled.
585
- * @default "/health"
586
- */
587
- path?: string;
588
-
589
- /**
590
- * HTTP response status that will be returned.
591
- * @default 200
592
- */
593
- status?: number;
594
- };
595
- instructions?: string;
596
- /**
597
- * Custom logger instance. If not provided, defaults to console.
598
- * Use this to integrate with your own logging system.
599
- */
600
- logger?: Logger;
601
- name: string;
602
-
603
- /**
604
- * Configuration for OAuth well-known discovery endpoints that can be exposed
605
- * when the server is running using HTTP-based transports (SSE or HTTP Stream).
606
- * When enabled, the server will respond to requests for OAuth discovery endpoints
607
- * with the configured metadata.
608
- *
609
- * The endpoints are only added when the server is started with
610
- * `transportType: "httpStream"` – they are ignored for the stdio transport.
611
- * Both SSE and HTTP Stream transports support OAuth endpoints.
612
- */
613
- oauth?: {
614
- /**
615
- * OAuth Authorization Server metadata for /.well-known/oauth-authorization-server
616
- *
617
- * This endpoint follows RFC 8414 (OAuth 2.0 Authorization Server Metadata)
618
- * and provides metadata about the OAuth 2.0 authorization server.
619
- *
620
- * Required by MCP Specification 2025-03-26
621
- */
622
- authorizationServer?: {
623
- authorizationEndpoint: string;
624
- codeChallengeMethodsSupported?: string[];
625
- // DPoP support
626
- dpopSigningAlgValuesSupported?: string[];
627
- grantTypesSupported?: string[];
628
-
629
- introspectionEndpoint?: string;
630
- // Required
631
- issuer: string;
632
- // Common optional
633
- jwksUri?: string;
634
- opPolicyUri?: string;
635
- opTosUri?: string;
636
- registrationEndpoint?: string;
637
- responseModesSupported?: string[];
638
- responseTypesSupported: string[];
639
- revocationEndpoint?: string;
640
- scopesSupported?: string[];
641
- serviceDocumentation?: string;
642
- tokenEndpoint: string;
643
- tokenEndpointAuthMethodsSupported?: string[];
644
- tokenEndpointAuthSigningAlgValuesSupported?: string[];
645
-
646
- uiLocalesSupported?: string[];
647
- };
648
-
649
- /**
650
- * Whether OAuth discovery endpoints should be enabled.
651
- */
652
- enabled: boolean;
653
-
654
- /**
655
- * OAuth Protected Resource metadata for `/.well-known/oauth-protected-resource`
656
- *
657
- * This endpoint follows {@link https://www.rfc-editor.org/rfc/rfc9728.html | RFC 9728}
658
- * and provides metadata describing how an OAuth 2.0 protected resource (in this case,
659
- * an MCP server) expects to be accessed.
660
- *
661
- * When configured, FastMCP will automatically serve this metadata at the
662
- * `/.well-known/oauth-protected-resource` endpoint. The `authorizationServers` and `resource`
663
- * fields are required. All others are optional and will be omitted from the published
664
- * metadata if not specified.
665
- *
666
- * This satisfies the requirements of the MCP Authorization specification's
667
- * {@link https://modelcontextprotocol.io/specification/2025-06-18/basic/authorization#authorization-server-location | Authorization Server Location section}.
668
- *
669
- * Clients consuming this metadata MUST validate that any presented values comply with
670
- * RFC 9728, including strict validation of the `resource` identifier and intended audience
671
- * when access tokens are issued and presented (per RFC 8707 §2).
672
- *
673
- * @remarks Required by MCP Specification version 2025-06-18
674
- */
675
- protectedResource?: {
676
- /**
677
- * Allows for additional metadata fields beyond those defined in RFC 9728.
678
- *
679
- * @remarks This supports vendor-specific or experimental extensions.
680
- * @see {@link https://www.rfc-editor.org/rfc/rfc9728.html#section-2.3 | RFC 9728 §2.3}
681
- */
682
- [key: string]: unknown;
683
-
684
- /**
685
- * Supported values for the `authorization_details` parameter (RFC 9396).
686
- *
687
- * @remarks Used when fine-grained access control is in play.
688
- * @see {@link https://www.rfc-editor.org/rfc/rfc9728.html#section-2-2.23 | RFC 9728 §2.2.23}
689
- */
690
- authorizationDetailsTypesSupported?: string[];
691
-
692
- /**
693
- * List of OAuth 2.0 authorization server issuer identifiers.
694
- *
695
- * These correspond to ASes that can issue access tokens for this protected resource.
696
- * MCP clients use these values to locate the relevant `/.well-known/oauth-authorization-server`
697
- * metadata for initiating the OAuth flow.
698
- *
699
- * @remarks Required by the MCP spec. MCP servers MUST provide at least one issuer.
700
- * Clients are responsible for choosing among them (see RFC 9728 §7.6).
701
- * @see {@link https://www.rfc-editor.org/rfc/rfc9728.html#section-2-2.3 | RFC 9728 §2.2.3}
702
- */
703
- authorizationServers: string[];
704
-
705
- /**
706
- * List of supported methods for presenting OAuth 2.0 bearer tokens.
707
- *
708
- * @remarks Valid values are `header`, `body`, and `query`.
709
- * If omitted, clients MAY assume only `header` is supported, per RFC 6750.
710
- * This is a client-side interpretation and not a serialization default.
711
- * @see {@link https://www.rfc-editor.org/rfc/rfc9728.html#section-2-2.9 | RFC 9728 §2.2.9}
712
- */
713
- bearerMethodsSupported?: string[];
714
-
715
- /**
716
- * Whether this resource requires all access tokens to be DPoP-bound.
717
- *
718
- * @remarks If omitted, clients SHOULD assume this is `false`.
719
- * @see {@link https://www.rfc-editor.org/rfc/rfc9728.html#section-2-2.27 | RFC 9728 §2.2.27}
720
- */
721
- dpopBoundAccessTokensRequired?: boolean;
722
-
723
- /**
724
- * Supported algorithms for verifying DPoP proofs (RFC 9449).
725
- *
726
- * @see {@link https://www.rfc-editor.org/rfc/rfc9728.html#section-2-2.25 | RFC 9728 §2.2.25}
727
- */
728
- dpopSigningAlgValuesSupported?: string[];
729
-
730
- /**
731
- * JWKS URI of this resource. Used to validate access tokens or sign responses.
732
- *
733
- * @remarks When present, this MUST be an `https:` URI pointing to a valid JWK Set (RFC 7517).
734
- * @see {@link https://www.rfc-editor.org/rfc/rfc9728.html#section-2-2.5 | RFC 9728 §2.2.5}
735
- */
736
- jwksUri?: string;
737
-
738
- /**
739
- * Canonical OAuth resource identifier for this protected resource (the MCP server).
740
- *
741
- * @remarks Typically the base URL of the MCP server. Clients MUST use this as the
742
- * `resource` parameter in authorization and token requests (per RFC 8707).
743
- * @see {@link https://www.rfc-editor.org/rfc/rfc9728.html#section-2-2.1 | RFC 9728 §2.2.1}
744
- */
745
- resource: string;
746
-
747
- /**
748
- * URL to developer-accessible documentation for this resource.
749
- *
750
- * @remarks This field MAY be localized.
751
- * @see {@link https://www.rfc-editor.org/rfc/rfc9728.html#section-2-2.15 | RFC 9728 §2.2.15}
752
- */
753
- resourceDocumentation?: string;
754
-
755
- /**
756
- * Human-readable name for display purposes (e.g., in UIs).
757
- *
758
- * @remarks This field MAY be localized using language tags (`resource_name#en`, etc.).
759
- * @see {@link https://www.rfc-editor.org/rfc/rfc9728.html#section-2-2.13 | RFC 9728 §2.2.13}
760
- */
761
- resourceName?: string;
762
-
763
- /**
764
- * URL to a human-readable policy page describing acceptable use.
765
- *
766
- * @remarks This field MAY be localized.
767
- * @see {@link https://www.rfc-editor.org/rfc/rfc9728.html#section-2-2.17 | RFC 9728 §2.2.17}
768
- */
769
- resourcePolicyUri?: string;
770
-
771
- /**
772
- * Supported JWS algorithms for signed responses from this resource (e.g., response signing).
773
- *
774
- * @remarks MUST NOT include `none`.
775
- * @see {@link https://www.rfc-editor.org/rfc/rfc9728.html#section-2-2.11 | RFC 9728 §2.2.11}
776
- */
777
- resourceSigningAlgValuesSupported?: string[];
778
-
779
- /**
780
- * URL to the protected resource’s Terms of Service.
781
- *
782
- * @remarks This field MAY be localized.
783
- * @see {@link https://www.rfc-editor.org/rfc/rfc9728.html#section-2-2.19 | RFC 9728 §2.2.19}
784
- */
785
- resourceTosUri?: string;
786
-
787
- /**
788
- * Supported OAuth scopes for requesting access to this resource.
789
- *
790
- * @remarks Useful for discovery, but clients SHOULD still request the minimal scope required.
791
- * @see {@link https://www.rfc-editor.org/rfc/rfc9728.html#section-2-2.7 | RFC 9728 §2.2.7}
792
- */
793
- scopesSupported?: string[];
794
-
795
- /**
796
- * Developer-accessible documentation for how to use the service (not end-user docs).
797
- *
798
- * @remarks Semantically equivalent to `resourceDocumentation`, but included under its
799
- * alternate name for compatibility with tools or schemas expecting either.
800
- * @see {@link https://www.rfc-editor.org/rfc/rfc9728.html#section-2-2.15 | RFC 9728 §2.2.15}
801
- */
802
- serviceDocumentation?: string;
803
-
804
- /**
805
- * Whether mutual-TLS-bound access tokens are required.
806
- *
807
- * @remarks If omitted, clients SHOULD assume this is `false` (client-side behavior).
808
- * @see {@link https://www.rfc-editor.org/rfc/rfc9728.html#section-2-2.21 | RFC 9728 §2.2.21}
809
- */
810
- tlsClientCertificateBoundAccessTokens?: boolean;
811
- };
812
- };
813
-
814
- ping?: {
815
- /**
816
- * Whether ping should be enabled by default.
817
- * - true for SSE or HTTP Stream
818
- * - false for stdio
819
- */
820
- enabled?: boolean;
821
- /**
822
- * Interval
823
- * @default 5000 (5s)
824
- */
825
- intervalMs?: number;
826
- /**
827
- * Logging level for ping-related messages.
828
- * @default 'debug'
829
- */
830
- logLevel?: LoggingLevel;
831
- };
832
- /**
833
- * Configuration for roots capability
834
- */
835
- roots?: {
836
- /**
837
- * Whether roots capability should be enabled
838
- * Set to false to completely disable roots support
839
- * @default true
840
- */
841
- enabled?: boolean;
842
- };
843
- /**
844
- * General utilities
845
- */
846
- utils?: {
847
- formatInvalidParamsErrorMessage?: (
848
- issues: readonly StandardSchemaV1.Issue[],
849
- ) => string;
850
- };
851
- version: `${number}.${number}.${number}`;
852
- };
853
-
854
- type Tool<
855
- T extends FastMCPSessionAuth,
856
- Params extends ToolParameters = ToolParameters,
857
- > = {
858
- annotations?: {
859
- /**
860
- * When true, the tool leverages incremental content streaming
861
- * Return void for tools that handle all their output via streaming
862
- */
863
- streamingHint?: boolean;
864
- } & ToolAnnotations;
865
- canAccess?: (auth: T) => boolean;
866
- description?: string;
867
-
868
- execute: (
869
- args: StandardSchemaV1.InferOutput<Params>,
870
- context: Context<T>,
871
- ) => Promise<
872
- | AudioContent
873
- | ContentResult
874
- | ImageContent
875
- | ResourceContent
876
- | ResourceLink
877
- | string
878
- | TextContent
879
- | void
880
- >;
881
- name: string;
882
- parameters?: Params;
883
- timeoutMs?: number;
884
- };
885
-
886
- /**
887
- * Tool annotations as defined in MCP Specification (2025-03-26)
888
- * These provide hints about a tool's behavior.
889
- */
890
- type ToolAnnotations = {
891
- /**
892
- * If true, the tool may perform destructive updates
893
- * Only meaningful when readOnlyHint is false
894
- * @default true
895
- */
896
- destructiveHint?: boolean;
897
-
898
- /**
899
- * If true, calling the tool repeatedly with the same arguments has no additional effect
900
- * Only meaningful when readOnlyHint is false
901
- * @default false
902
- */
903
- idempotentHint?: boolean;
904
-
905
- /**
906
- * If true, the tool may interact with an "open world" of external entities
907
- * @default true
908
- */
909
- openWorldHint?: boolean;
910
-
911
- /**
912
- * If true, indicates the tool does not modify its environment
913
- * @default false
914
- */
915
- readOnlyHint?: boolean;
916
-
917
- /**
918
- * A human-readable title for the tool, useful for UI display
919
- */
920
- title?: string;
921
- };
922
-
923
- const FastMCPSessionEventEmitterBase: {
924
- new (): StrictEventEmitter<EventEmitter, FastMCPSessionEvents>;
925
- } = EventEmitter;
926
-
927
- type Authenticate<T> = (request: http.IncomingMessage) => Promise<T>;
928
-
929
- type FastMCPSessionAuth = Record<string, unknown> | undefined;
930
-
931
- class FastMCPSessionEventEmitter extends FastMCPSessionEventEmitterBase {}
932
-
933
- export class FastMCPSession<
934
- T extends FastMCPSessionAuth = FastMCPSessionAuth,
935
- > extends FastMCPSessionEventEmitter {
936
- public get clientCapabilities(): ClientCapabilities | null {
937
- return this.#clientCapabilities ?? null;
938
- }
939
- public get isReady(): boolean {
940
- return this.#connectionState === "ready";
941
- }
942
- public get loggingLevel(): LoggingLevel {
943
- return this.#loggingLevel;
944
- }
945
- public get roots(): Root[] {
946
- return this.#roots;
947
- }
948
- public get server(): Server {
949
- return this.#server;
950
- }
951
- public get sessionId(): string | undefined {
952
- return this.#sessionId;
953
- }
954
- public set sessionId(value: string | undefined) {
955
- this.#sessionId = value;
956
- }
957
- #auth: T | undefined;
958
- #capabilities: ServerCapabilities = {};
959
- #clientCapabilities?: ClientCapabilities;
960
- #connectionState: "closed" | "connecting" | "error" | "ready" = "connecting";
961
- #logger: Logger;
962
- #loggingLevel: LoggingLevel = "info";
963
- #needsEventLoopFlush: boolean = false;
964
- #pingConfig?: ServerOptions<T>["ping"];
965
-
966
- #pingInterval: null | ReturnType<typeof setInterval> = null;
967
-
968
- #prompts: Prompt<T>[] = [];
969
-
970
- #resources: Resource<T>[] = [];
971
-
972
- #resourceTemplates: ResourceTemplate<T>[] = [];
973
-
974
- #roots: Root[] = [];
975
-
976
- #rootsConfig?: ServerOptions<T>["roots"];
977
-
978
- #server: Server;
979
-
980
- /**
981
- * Session ID from the Mcp-Session-Id header (HTTP transports only).
982
- * Used to track per-session state across multiple requests.
983
- */
984
- #sessionId?: string;
985
-
986
- #utils?: ServerOptions<T>["utils"];
987
-
988
- constructor({
989
- auth,
990
- instructions,
991
- logger,
992
- name,
993
- ping,
994
- prompts,
995
- resources,
996
- resourcesTemplates,
997
- roots,
998
- sessionId,
999
- tools,
1000
- transportType,
1001
- utils,
1002
- version,
1003
- }: {
1004
- auth?: T;
1005
- instructions?: string;
1006
- logger: Logger;
1007
- name: string;
1008
- ping?: ServerOptions<T>["ping"];
1009
- prompts: Prompt<T>[];
1010
- resources: Resource<T>[];
1011
- resourcesTemplates: InputResourceTemplate<T>[];
1012
- roots?: ServerOptions<T>["roots"];
1013
- sessionId?: string;
1014
- tools: Tool<T>[];
1015
- transportType?: "httpStream" | "stdio";
1016
- utils?: ServerOptions<T>["utils"];
1017
- version: string;
1018
- }) {
1019
- super();
1020
-
1021
- this.#auth = auth;
1022
- this.#logger = logger;
1023
- this.#pingConfig = ping;
1024
- this.#rootsConfig = roots;
1025
- this.#sessionId = sessionId;
1026
- this.#needsEventLoopFlush = transportType === "httpStream";
1027
-
1028
- if (tools.length) {
1029
- this.#capabilities.tools = {};
1030
- }
1031
-
1032
- if (resources.length || resourcesTemplates.length) {
1033
- this.#capabilities.resources = {};
1034
- }
1035
-
1036
- if (prompts.length) {
1037
- for (const prompt of prompts) {
1038
- this.addPrompt(prompt);
1039
- }
1040
-
1041
- this.#capabilities.prompts = {};
1042
- }
1043
-
1044
- this.#capabilities.logging = {};
1045
-
1046
- this.#server = new Server(
1047
- { name: name, version: version },
1048
- { capabilities: this.#capabilities, instructions: instructions },
1049
- );
1050
-
1051
- this.#utils = utils;
1052
-
1053
- this.setupErrorHandling();
1054
- this.setupLoggingHandlers();
1055
- this.setupRootsHandlers();
1056
- this.setupCompleteHandlers();
1057
-
1058
- if (tools.length) {
1059
- this.setupToolHandlers(tools);
1060
- }
1061
-
1062
- if (resources.length || resourcesTemplates.length) {
1063
- for (const resource of resources) {
1064
- this.addResource(resource);
1065
- }
1066
-
1067
- this.setupResourceHandlers(resources);
1068
-
1069
- if (resourcesTemplates.length) {
1070
- for (const resourceTemplate of resourcesTemplates) {
1071
- this.addResourceTemplate(resourceTemplate);
1072
- }
1073
-
1074
- this.setupResourceTemplateHandlers(resourcesTemplates);
1075
- }
1076
- }
1077
-
1078
- if (prompts.length) {
1079
- this.setupPromptHandlers(prompts);
1080
- }
1081
- }
1082
-
1083
- public async close() {
1084
- this.#connectionState = "closed";
1085
-
1086
- if (this.#pingInterval) {
1087
- clearInterval(this.#pingInterval);
1088
- }
1089
-
1090
- try {
1091
- await this.#server.close();
1092
- } catch (error) {
1093
- this.#logger.error("[FastMCP error]", "could not close server", error);
1094
- }
1095
- }
1096
-
1097
- public async connect(transport: Transport) {
1098
- if (this.#server.transport) {
1099
- throw new UnexpectedStateError("Server is already connected");
1100
- }
1101
-
1102
- this.#connectionState = "connecting";
1103
-
1104
- try {
1105
- await this.#server.connect(transport);
1106
-
1107
- // Extract session ID from transport if available (HTTP transports only)
1108
- if ("sessionId" in transport) {
1109
- const transportWithSessionId = transport as {
1110
- sessionId?: string;
1111
- } & Transport;
1112
- if (typeof transportWithSessionId.sessionId === "string") {
1113
- this.#sessionId = transportWithSessionId.sessionId;
1114
- }
1115
- }
1116
-
1117
- let attempt = 0;
1118
- const maxAttempts = 10;
1119
- const retryDelay = 100;
1120
-
1121
- while (attempt++ < maxAttempts) {
1122
- const capabilities = this.#server.getClientCapabilities();
1123
-
1124
- if (capabilities) {
1125
- this.#clientCapabilities = capabilities;
1126
- break;
1127
- }
1128
-
1129
- await delay(retryDelay);
1130
- }
1131
-
1132
- if (!this.#clientCapabilities) {
1133
- this.#logger.warn(
1134
- `[FastMCP warning] could not infer client capabilities after ${maxAttempts} attempts. Connection may be unstable.`,
1135
- );
1136
- }
1137
-
1138
- if (
1139
- this.#rootsConfig?.enabled !== false &&
1140
- this.#clientCapabilities?.roots?.listChanged &&
1141
- typeof this.#server.listRoots === "function"
1142
- ) {
1143
- try {
1144
- const roots = await this.#server.listRoots();
1145
- this.#roots = roots?.roots || [];
1146
- } catch (e) {
1147
- if (e instanceof McpError && e.code === ErrorCode.MethodNotFound) {
1148
- this.#logger.debug(
1149
- "[FastMCP debug] listRoots method not supported by client",
1150
- );
1151
- } else {
1152
- this.#logger.error(
1153
- `[FastMCP error] received error listing roots.\n\n${
1154
- e instanceof Error ? e.stack : JSON.stringify(e)
1155
- }`,
1156
- );
1157
- }
1158
- }
1159
- }
1160
-
1161
- if (this.#clientCapabilities) {
1162
- const pingConfig = this.#getPingConfig(transport);
1163
-
1164
- if (pingConfig.enabled) {
1165
- this.#pingInterval = setInterval(async () => {
1166
- try {
1167
- await this.#server.ping();
1168
- } catch {
1169
- // The reason we are not emitting an error here is because some clients
1170
- // seem to not respond to the ping request, and we don't want to crash the server,
1171
- // e.g., https://github.com/punkpeye/fastmcp/issues/38.
1172
- const logLevel = pingConfig.logLevel;
1173
-
1174
- if (logLevel === "debug") {
1175
- this.#logger.debug("[FastMCP debug] server ping failed");
1176
- } else if (logLevel === "warning") {
1177
- this.#logger.warn(
1178
- "[FastMCP warning] server is not responding to ping",
1179
- );
1180
- } else if (logLevel === "error") {
1181
- this.#logger.error(
1182
- "[FastMCP error] server is not responding to ping",
1183
- );
1184
- } else {
1185
- this.#logger.info("[FastMCP info] server ping failed");
1186
- }
1187
- }
1188
- }, pingConfig.intervalMs);
1189
- }
1190
- }
1191
-
1192
- // Mark connection as ready and emit event
1193
- this.#connectionState = "ready";
1194
- this.emit("ready");
1195
- } catch (error) {
1196
- this.#connectionState = "error";
1197
- const errorEvent = {
1198
- error: error instanceof Error ? error : new Error(String(error)),
1199
- };
1200
- this.emit("error", errorEvent);
1201
- throw error;
1202
- }
1203
- }
1204
-
1205
- public async requestSampling(
1206
- message: z.infer<typeof CreateMessageRequestSchema>["params"],
1207
- options?: RequestOptions,
1208
- ): Promise<SamplingResponse> {
1209
- return this.#server.createMessage(message, options);
1210
- }
1211
-
1212
- public waitForReady(): Promise<void> {
1213
- if (this.isReady) {
1214
- return Promise.resolve();
1215
- }
1216
-
1217
- if (
1218
- this.#connectionState === "error" ||
1219
- this.#connectionState === "closed"
1220
- ) {
1221
- return Promise.reject(
1222
- new Error(`Connection is in ${this.#connectionState} state`),
1223
- );
1224
- }
1225
-
1226
- return new Promise((resolve, reject) => {
1227
- const timeout = setTimeout(() => {
1228
- reject(
1229
- new Error(
1230
- "Connection timeout: Session failed to become ready within 5 seconds",
1231
- ),
1232
- );
1233
- }, 5000);
1234
-
1235
- this.once("ready", () => {
1236
- clearTimeout(timeout);
1237
- resolve();
1238
- });
1239
-
1240
- this.once("error", (event) => {
1241
- clearTimeout(timeout);
1242
- reject(event.error);
1243
- });
1244
- });
1245
- }
1246
-
1247
- #getPingConfig(transport: Transport): {
1248
- enabled: boolean;
1249
- intervalMs: number;
1250
- logLevel: LoggingLevel;
1251
- } {
1252
- const pingConfig = this.#pingConfig || {};
1253
-
1254
- let defaultEnabled = false;
1255
-
1256
- if ("type" in transport) {
1257
- // Enable by default for SSE and HTTP streaming
1258
- if (transport.type === "httpStream") {
1259
- defaultEnabled = true;
1260
- }
1261
- }
1262
-
1263
- return {
1264
- enabled:
1265
- pingConfig.enabled !== undefined ? pingConfig.enabled : defaultEnabled,
1266
- intervalMs: pingConfig.intervalMs || 5000,
1267
- logLevel: pingConfig.logLevel || "debug",
1268
- };
1269
- }
1270
-
1271
- private addPrompt(inputPrompt: InputPrompt<T>) {
1272
- const completers: Record<string, ArgumentValueCompleter<T>> = {};
1273
- const enums: Record<string, string[]> = {};
1274
- const fuseInstances: Record<string, Fuse<string>> = {};
1275
-
1276
- for (const argument of inputPrompt.arguments ?? []) {
1277
- if (argument.complete) {
1278
- completers[argument.name] = argument.complete;
1279
- }
1280
-
1281
- if (argument.enum) {
1282
- enums[argument.name] = argument.enum;
1283
- fuseInstances[argument.name] = new Fuse(argument.enum, {
1284
- includeScore: true,
1285
- threshold: 0.3, // More flexible matching!
1286
- });
1287
- }
1288
- }
1289
-
1290
- const prompt = {
1291
- ...inputPrompt,
1292
- complete: async (name: string, value: string, auth?: T) => {
1293
- if (completers[name]) {
1294
- return await completers[name](value, auth);
1295
- }
1296
-
1297
- if (fuseInstances[name]) {
1298
- const result = fuseInstances[name].search(value);
1299
-
1300
- return {
1301
- total: result.length,
1302
- values: result.map((item) => item.item),
1303
- };
1304
- }
1305
-
1306
- return {
1307
- values: [],
1308
- };
1309
- },
1310
- };
1311
-
1312
- this.#prompts.push(prompt);
1313
- }
1314
-
1315
- private addResource(inputResource: Resource<T>) {
1316
- this.#resources.push(inputResource);
1317
- }
1318
-
1319
- private addResourceTemplate(inputResourceTemplate: InputResourceTemplate<T>) {
1320
- const completers: Record<string, ArgumentValueCompleter<T>> = {};
1321
-
1322
- for (const argument of inputResourceTemplate.arguments ?? []) {
1323
- if (argument.complete) {
1324
- completers[argument.name] = argument.complete;
1325
- }
1326
- }
1327
-
1328
- const resourceTemplate = {
1329
- ...inputResourceTemplate,
1330
- complete: async (name: string, value: string, auth?: T) => {
1331
- if (completers[name]) {
1332
- return await completers[name](value, auth);
1333
- }
1334
-
1335
- return {
1336
- values: [],
1337
- };
1338
- },
1339
- };
1340
-
1341
- this.#resourceTemplates.push(resourceTemplate);
1342
- }
1343
-
1344
- private setupCompleteHandlers() {
1345
- this.#server.setRequestHandler(CompleteRequestSchema, async (request) => {
1346
- if (request.params.ref.type === "ref/prompt") {
1347
- const prompt = this.#prompts.find(
1348
- (prompt) => prompt.name === request.params.ref.name,
1349
- );
1350
-
1351
- if (!prompt) {
1352
- throw new UnexpectedStateError("Unknown prompt", {
1353
- request,
1354
- });
1355
- }
1356
-
1357
- if (!prompt.complete) {
1358
- throw new UnexpectedStateError("Prompt does not support completion", {
1359
- request,
1360
- });
1361
- }
1362
-
1363
- const completion = CompletionZodSchema.parse(
1364
- await prompt.complete(
1365
- request.params.argument.name,
1366
- request.params.argument.value,
1367
- this.#auth,
1368
- ),
1369
- );
1370
-
1371
- return {
1372
- completion,
1373
- };
1374
- }
1375
-
1376
- if (request.params.ref.type === "ref/resource") {
1377
- const resource = this.#resourceTemplates.find(
1378
- (resource) => resource.uriTemplate === request.params.ref.uri,
1379
- );
1380
-
1381
- if (!resource) {
1382
- throw new UnexpectedStateError("Unknown resource", {
1383
- request,
1384
- });
1385
- }
1386
-
1387
- if (!("uriTemplate" in resource)) {
1388
- throw new UnexpectedStateError("Unexpected resource");
1389
- }
1390
-
1391
- if (!resource.complete) {
1392
- throw new UnexpectedStateError(
1393
- "Resource does not support completion",
1394
- {
1395
- request,
1396
- },
1397
- );
1398
- }
1399
-
1400
- const completion = CompletionZodSchema.parse(
1401
- await resource.complete(
1402
- request.params.argument.name,
1403
- request.params.argument.value,
1404
- this.#auth,
1405
- ),
1406
- );
1407
-
1408
- return {
1409
- completion,
1410
- };
1411
- }
1412
-
1413
- throw new UnexpectedStateError("Unexpected completion request", {
1414
- request,
1415
- });
1416
- });
1417
- }
1418
-
1419
- private setupErrorHandling() {
1420
- this.#server.onerror = (error) => {
1421
- this.#logger.error("[FastMCP error]", error);
1422
- };
1423
- }
1424
-
1425
- private setupLoggingHandlers() {
1426
- this.#server.setRequestHandler(SetLevelRequestSchema, (request) => {
1427
- this.#loggingLevel = request.params.level;
1428
-
1429
- return {};
1430
- });
1431
- }
1432
-
1433
- private setupPromptHandlers(prompts: Prompt<T>[]) {
1434
- this.#server.setRequestHandler(ListPromptsRequestSchema, async () => {
1435
- return {
1436
- prompts: prompts.map((prompt) => {
1437
- return {
1438
- arguments: prompt.arguments,
1439
- complete: prompt.complete,
1440
- description: prompt.description,
1441
- name: prompt.name,
1442
- };
1443
- }),
1444
- };
1445
- });
1446
-
1447
- this.#server.setRequestHandler(GetPromptRequestSchema, async (request) => {
1448
- const prompt = prompts.find(
1449
- (prompt) => prompt.name === request.params.name,
1450
- );
1451
-
1452
- if (!prompt) {
1453
- throw new McpError(
1454
- ErrorCode.MethodNotFound,
1455
- `Unknown prompt: ${request.params.name}`,
1456
- );
1457
- }
1458
-
1459
- const args = request.params.arguments;
1460
-
1461
- for (const arg of prompt.arguments ?? []) {
1462
- if (arg.required && !(args && arg.name in args)) {
1463
- throw new McpError(
1464
- ErrorCode.InvalidRequest,
1465
- `Prompt '${request.params.name}' requires argument '${arg.name}': ${
1466
- arg.description || "No description provided"
1467
- }`,
1468
- );
1469
- }
1470
- }
1471
-
1472
- let result: Awaited<ReturnType<Prompt<T>["load"]>>;
1473
-
1474
- try {
1475
- result = await prompt.load(
1476
- args as Record<string, string | undefined>,
1477
- this.#auth,
1478
- );
1479
- } catch (error) {
1480
- const errorMessage =
1481
- error instanceof Error ? error.message : String(error);
1482
- throw new McpError(
1483
- ErrorCode.InternalError,
1484
- `Failed to load prompt '${request.params.name}': ${errorMessage}`,
1485
- );
1486
- }
1487
-
1488
- if (typeof result === "string") {
1489
- return {
1490
- description: prompt.description,
1491
- messages: [
1492
- {
1493
- content: { text: result, type: "text" },
1494
- role: "user",
1495
- },
1496
- ],
1497
- };
1498
- } else {
1499
- return {
1500
- description: prompt.description,
1501
- messages: result.messages,
1502
- };
1503
- }
1504
- });
1505
- }
1506
-
1507
- private setupResourceHandlers(resources: Resource<T>[]) {
1508
- this.#server.setRequestHandler(ListResourcesRequestSchema, async () => {
1509
- return {
1510
- resources: resources.map((resource) => ({
1511
- description: resource.description,
1512
- mimeType: resource.mimeType,
1513
- name: resource.name,
1514
- uri: resource.uri,
1515
- })),
1516
- } satisfies ListResourcesResult;
1517
- });
1518
-
1519
- this.#server.setRequestHandler(
1520
- ReadResourceRequestSchema,
1521
- async (request) => {
1522
- if ("uri" in request.params) {
1523
- const resource = resources.find(
1524
- (resource) =>
1525
- "uri" in resource && resource.uri === request.params.uri,
1526
- );
1527
-
1528
- if (!resource) {
1529
- for (const resourceTemplate of this.#resourceTemplates) {
1530
- const uriTemplate = parseURITemplate(
1531
- resourceTemplate.uriTemplate,
1532
- );
1533
-
1534
- const match = uriTemplate.fromUri(request.params.uri);
1535
-
1536
- if (!match) {
1537
- continue;
1538
- }
1539
-
1540
- const uri = uriTemplate.fill(match);
1541
-
1542
- const result = await resourceTemplate.load(match, this.#auth);
1543
-
1544
- const resources = Array.isArray(result) ? result : [result];
1545
- return {
1546
- contents: resources.map((resource) => ({
1547
- ...resource,
1548
- description: resourceTemplate.description,
1549
- mimeType: resource.mimeType ?? resourceTemplate.mimeType,
1550
- name: resourceTemplate.name,
1551
- uri: resource.uri ?? uri,
1552
- })),
1553
- };
1554
- }
1555
-
1556
- throw new McpError(
1557
- ErrorCode.MethodNotFound,
1558
- `Resource not found: '${request.params.uri}'. Available resources: ${
1559
- resources.map((r) => r.uri).join(", ") || "none"
1560
- }`,
1561
- );
1562
- }
1563
-
1564
- if (!("uri" in resource)) {
1565
- throw new UnexpectedStateError("Resource does not support reading");
1566
- }
1567
-
1568
- let maybeArrayResult: Awaited<ReturnType<Resource<T>["load"]>>;
1569
-
1570
- try {
1571
- maybeArrayResult = await resource.load(this.#auth);
1572
- } catch (error) {
1573
- const errorMessage =
1574
- error instanceof Error ? error.message : String(error);
1575
- throw new McpError(
1576
- ErrorCode.InternalError,
1577
- `Failed to load resource '${resource.name}' (${resource.uri}): ${errorMessage}`,
1578
- {
1579
- uri: resource.uri,
1580
- },
1581
- );
1582
- }
1583
-
1584
- const resourceResults = Array.isArray(maybeArrayResult)
1585
- ? maybeArrayResult
1586
- : [maybeArrayResult];
1587
-
1588
- return {
1589
- contents: resourceResults.map((result) => ({
1590
- ...result,
1591
- mimeType: result.mimeType ?? resource.mimeType,
1592
- name: resource.name,
1593
- uri: result.uri ?? resource.uri,
1594
- })),
1595
- };
1596
- }
1597
-
1598
- throw new UnexpectedStateError("Unknown resource request", {
1599
- request,
1600
- });
1601
- },
1602
- );
1603
- }
1604
-
1605
- private setupResourceTemplateHandlers(
1606
- resourceTemplates: ResourceTemplate<T>[],
1607
- ) {
1608
- this.#server.setRequestHandler(
1609
- ListResourceTemplatesRequestSchema,
1610
- async () => {
1611
- return {
1612
- resourceTemplates: resourceTemplates.map((resourceTemplate) => ({
1613
- description: resourceTemplate.description,
1614
- mimeType: resourceTemplate.mimeType,
1615
- name: resourceTemplate.name,
1616
- uriTemplate: resourceTemplate.uriTemplate,
1617
- })),
1618
- } satisfies ListResourceTemplatesResult;
1619
- },
1620
- );
1621
- }
1622
-
1623
- private setupRootsHandlers() {
1624
- if (this.#rootsConfig?.enabled === false) {
1625
- this.#logger.debug(
1626
- "[FastMCP debug] roots capability explicitly disabled via config",
1627
- );
1628
- return;
1629
- }
1630
-
1631
- // Only set up roots notification handling if the server supports it
1632
- if (typeof this.#server.listRoots === "function") {
1633
- this.#server.setNotificationHandler(
1634
- RootsListChangedNotificationSchema,
1635
- () => {
1636
- this.#server
1637
- .listRoots()
1638
- .then((roots) => {
1639
- this.#roots = roots.roots;
1640
-
1641
- this.emit("rootsChanged", {
1642
- roots: roots.roots,
1643
- });
1644
- })
1645
- .catch((error) => {
1646
- if (
1647
- error instanceof McpError &&
1648
- error.code === ErrorCode.MethodNotFound
1649
- ) {
1650
- this.#logger.debug(
1651
- "[FastMCP debug] listRoots method not supported by client",
1652
- );
1653
- } else {
1654
- this.#logger.error(
1655
- `[FastMCP error] received error listing roots.\n\n${
1656
- error instanceof Error ? error.stack : JSON.stringify(error)
1657
- }`,
1658
- );
1659
- }
1660
- });
1661
- },
1662
- );
1663
- } else {
1664
- this.#logger.debug(
1665
- "[FastMCP debug] roots capability not available, not setting up notification handler",
1666
- );
1667
- }
1668
- }
1669
-
1670
- private setupToolHandlers(tools: Tool<T>[]) {
1671
- this.#server.setRequestHandler(ListToolsRequestSchema, async () => {
1672
- return {
1673
- tools: await Promise.all(
1674
- tools.map(async (tool) => {
1675
- return {
1676
- annotations: tool.annotations,
1677
- description: tool.description,
1678
- inputSchema: tool.parameters
1679
- ? await toJsonSchema(tool.parameters)
1680
- : {
1681
- additionalProperties: false,
1682
- properties: {},
1683
- type: "object",
1684
- }, // More complete schema for Cursor compatibility
1685
- name: tool.name,
1686
- };
1687
- }),
1688
- ),
1689
- };
1690
- });
1691
-
1692
- this.#server.setRequestHandler(CallToolRequestSchema, async (request) => {
1693
- const tool = tools.find((tool) => tool.name === request.params.name);
1694
-
1695
- if (!tool) {
1696
- throw new McpError(
1697
- ErrorCode.MethodNotFound,
1698
- `Unknown tool: ${request.params.name}`,
1699
- );
1700
- }
1701
-
1702
- let args: unknown = undefined;
1703
-
1704
- if (tool.parameters) {
1705
- const parsed = await tool.parameters["~standard"].validate(
1706
- request.params.arguments,
1707
- );
1708
-
1709
- if (parsed.issues) {
1710
- const friendlyErrors = this.#utils?.formatInvalidParamsErrorMessage
1711
- ? this.#utils.formatInvalidParamsErrorMessage(parsed.issues)
1712
- : parsed.issues
1713
- .map((issue) => {
1714
- const path = issue.path?.join(".") || "root";
1715
- return `${path}: ${issue.message}`;
1716
- })
1717
- .join(", ");
1718
-
1719
- throw new McpError(
1720
- ErrorCode.InvalidParams,
1721
- `Tool '${request.params.name}' parameter validation failed: ${friendlyErrors}. Please check the parameter types and values according to the tool's schema.`,
1722
- );
1723
- }
1724
-
1725
- args = parsed.value;
1726
- }
1727
-
1728
- const progressToken = request.params?._meta?.progressToken;
1729
-
1730
- let result: ContentResult;
1731
-
1732
- try {
1733
- const reportProgress = async (progress: Progress) => {
1734
- try {
1735
- await this.#server.notification({
1736
- method: "notifications/progress",
1737
- params: {
1738
- ...progress,
1739
- progressToken,
1740
- },
1741
- });
1742
-
1743
- if (this.#needsEventLoopFlush) {
1744
- await new Promise((resolve) => setImmediate(resolve));
1745
- }
1746
- } catch (progressError) {
1747
- this.#logger.warn(
1748
- `[FastMCP warning] Failed to report progress for tool '${request.params.name}':`,
1749
- progressError instanceof Error
1750
- ? progressError.message
1751
- : String(progressError),
1752
- );
1753
- }
1754
- };
1755
-
1756
- const log = {
1757
- debug: (message: string, context?: SerializableValue) => {
1758
- this.#server.sendLoggingMessage({
1759
- data: {
1760
- context,
1761
- message,
1762
- },
1763
- level: "debug",
1764
- });
1765
- },
1766
- error: (message: string, context?: SerializableValue) => {
1767
- this.#server.sendLoggingMessage({
1768
- data: {
1769
- context,
1770
- message,
1771
- },
1772
- level: "error",
1773
- });
1774
- },
1775
- info: (message: string, context?: SerializableValue) => {
1776
- this.#server.sendLoggingMessage({
1777
- data: {
1778
- context,
1779
- message,
1780
- },
1781
- level: "info",
1782
- });
1783
- },
1784
- warn: (message: string, context?: SerializableValue) => {
1785
- this.#server.sendLoggingMessage({
1786
- data: {
1787
- context,
1788
- message,
1789
- },
1790
- level: "warning",
1791
- });
1792
- },
1793
- };
1794
-
1795
- // Create a promise for tool execution
1796
- // Streams partial results while a tool is still executing
1797
- // Enables progressive rendering and real-time feedback
1798
- const streamContent = async (content: Content | Content[]) => {
1799
- const contentArray = Array.isArray(content) ? content : [content];
1800
-
1801
- try {
1802
- await this.#server.notification({
1803
- method: "notifications/tool/streamContent",
1804
- params: {
1805
- content: contentArray,
1806
- toolName: request.params.name,
1807
- },
1808
- });
1809
-
1810
- if (this.#needsEventLoopFlush) {
1811
- await new Promise((resolve) => setImmediate(resolve));
1812
- }
1813
- } catch (streamError) {
1814
- this.#logger.warn(
1815
- `[FastMCP warning] Failed to stream content for tool '${request.params.name}':`,
1816
- streamError instanceof Error
1817
- ? streamError.message
1818
- : String(streamError),
1819
- );
1820
- }
1821
- };
1822
-
1823
- const executeToolPromise = tool.execute(args, {
1824
- client: {
1825
- version: this.#server.getClientVersion(),
1826
- },
1827
- log,
1828
- reportProgress,
1829
- requestId:
1830
- typeof request.params?._meta?.requestId === "string"
1831
- ? request.params._meta.requestId
1832
- : undefined,
1833
- session: this.#auth,
1834
- sessionId: this.#sessionId,
1835
- streamContent,
1836
- });
1837
-
1838
- // Handle timeout if specified
1839
- const maybeStringResult = (await (tool.timeoutMs
1840
- ? Promise.race([
1841
- executeToolPromise,
1842
- new Promise<never>((_, reject) => {
1843
- const timeoutId = setTimeout(() => {
1844
- reject(
1845
- new UserError(
1846
- `Tool '${request.params.name}' timed out after ${tool.timeoutMs}ms. Consider increasing timeoutMs or optimizing the tool implementation.`,
1847
- ),
1848
- );
1849
- }, tool.timeoutMs);
1850
-
1851
- // If promise resolves first
1852
- executeToolPromise.finally(() => clearTimeout(timeoutId));
1853
- }),
1854
- ])
1855
- : executeToolPromise)) as
1856
- | AudioContent
1857
- | ContentResult
1858
- | ImageContent
1859
- | null
1860
- | ResourceContent
1861
- | ResourceLink
1862
- | string
1863
- | TextContent
1864
- | undefined;
1865
-
1866
- // Without this test, we are running into situations where the last progress update is not reported.
1867
- // See the 'reports multiple progress updates without buffering' test in FastMCP.test.ts before refactoring.
1868
- await delay(1);
1869
-
1870
- if (maybeStringResult === undefined || maybeStringResult === null) {
1871
- result = ContentResultZodSchema.parse({
1872
- content: [],
1873
- });
1874
- } else if (typeof maybeStringResult === "string") {
1875
- result = ContentResultZodSchema.parse({
1876
- content: [{ text: maybeStringResult, type: "text" }],
1877
- });
1878
- } else if ("type" in maybeStringResult) {
1879
- result = ContentResultZodSchema.parse({
1880
- content: [maybeStringResult],
1881
- });
1882
- } else {
1883
- result = ContentResultZodSchema.parse(maybeStringResult);
1884
- }
1885
- } catch (error) {
1886
- if (error instanceof UserError) {
1887
- return {
1888
- content: [{ text: error.message, type: "text" }],
1889
- isError: true,
1890
- ...(error.extras ? { structuredContent: error.extras } : {}),
1891
- };
1892
- }
1893
-
1894
- const errorMessage =
1895
- error instanceof Error ? error.message : String(error);
1896
- return {
1897
- content: [
1898
- {
1899
- text: `Tool '${request.params.name}' execution failed: ${errorMessage}`,
1900
- type: "text",
1901
- },
1902
- ],
1903
- isError: true,
1904
- };
1905
- }
1906
-
1907
- return result;
1908
- });
1909
- }
1910
- }
1911
-
1912
- /**
1913
- * Converts camelCase to snake_case for OAuth endpoint responses
1914
- */
1915
- function camelToSnakeCase(str: string): string {
1916
- return str.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`);
1917
- }
1918
-
1919
- /**
1920
- * Converts an object with camelCase keys to snake_case keys
1921
- */
1922
- function convertObjectToSnakeCase(
1923
- obj: Record<string, unknown>,
1924
- ): Record<string, unknown> {
1925
- const result: Record<string, unknown> = {};
1926
-
1927
- for (const [key, value] of Object.entries(obj)) {
1928
- const snakeKey = camelToSnakeCase(key);
1929
- result[snakeKey] = value;
1930
- }
1931
-
1932
- return result;
1933
- }
1934
-
1935
- const FastMCPEventEmitterBase: {
1936
- new (): StrictEventEmitter<EventEmitter, FastMCPEvents<FastMCPSessionAuth>>;
1937
- } = EventEmitter;
1938
-
1939
- class FastMCPEventEmitter extends FastMCPEventEmitterBase {}
1940
-
1941
- export class FastMCP<
1942
- T extends FastMCPSessionAuth = FastMCPSessionAuth,
1943
- > extends FastMCPEventEmitter {
1944
- public get sessions(): FastMCPSession<T>[] {
1945
- return this.#sessions;
1946
- }
1947
- #authenticate: Authenticate<T> | undefined;
1948
- #httpStreamServer: null | SSEServer = null;
1949
- #logger: Logger;
1950
- #options: ServerOptions<T>;
1951
- #prompts: InputPrompt<T>[] = [];
1952
- #resources: Resource<T>[] = [];
1953
- #resourcesTemplates: InputResourceTemplate<T>[] = [];
1954
- #sessions: FastMCPSession<T>[] = [];
1955
-
1956
- #tools: Tool<T>[] = [];
1957
-
1958
- constructor(public options: ServerOptions<T>) {
1959
- super();
1960
-
1961
- this.#options = options;
1962
- this.#authenticate = options.authenticate;
1963
- this.#logger = options.logger || console;
1964
- }
1965
-
1966
- /**
1967
- * Adds a prompt to the server.
1968
- */
1969
- public addPrompt<const Args extends InputPromptArgument<T>[]>(
1970
- prompt: InputPrompt<T, Args>,
1971
- ) {
1972
- this.#prompts.push(prompt);
1973
- }
1974
-
1975
- /**
1976
- * Adds a resource to the server.
1977
- */
1978
- public addResource(resource: Resource<T>) {
1979
- this.#resources.push(resource);
1980
- }
1981
-
1982
- /**
1983
- * Adds a resource template to the server.
1984
- */
1985
- public addResourceTemplate<
1986
- const Args extends InputResourceTemplateArgument[],
1987
- >(resource: InputResourceTemplate<T, Args>) {
1988
- this.#resourcesTemplates.push(resource);
1989
- }
1990
-
1991
- /**
1992
- * Adds a tool to the server.
1993
- */
1994
- public addTool<Params extends ToolParameters>(tool: Tool<T, Params>) {
1995
- this.#tools.push(tool as unknown as Tool<T>);
1996
- }
1997
-
1998
- /**
1999
- * Embeds a resource by URI, making it easy to include resources in tool responses.
2000
- *
2001
- * @param uri - The URI of the resource to embed
2002
- * @returns Promise<ResourceContent> - The embedded resource content
2003
- */
2004
- public async embedded(uri: string): Promise<ResourceContent["resource"]> {
2005
- // First, try to find a direct resource match
2006
- const directResource = this.#resources.find(
2007
- (resource) => resource.uri === uri,
2008
- );
2009
-
2010
- if (directResource) {
2011
- const result = await directResource.load();
2012
- const results = Array.isArray(result) ? result : [result];
2013
- const firstResult = results[0];
2014
-
2015
- const resourceData: ResourceContent["resource"] = {
2016
- mimeType: directResource.mimeType,
2017
- uri,
2018
- };
2019
-
2020
- if ("text" in firstResult) {
2021
- resourceData.text = firstResult.text;
2022
- }
2023
-
2024
- if ("blob" in firstResult) {
2025
- resourceData.blob = firstResult.blob;
2026
- }
2027
-
2028
- return resourceData;
2029
- }
2030
-
2031
- // Try to match against resource templates
2032
- for (const template of this.#resourcesTemplates) {
2033
- const parsedTemplate = parseURITemplate(template.uriTemplate);
2034
- const params = parsedTemplate.fromUri(uri);
2035
- if (!params) {
2036
- continue;
2037
- }
2038
-
2039
- const result = await template.load(
2040
- params as ResourceTemplateArgumentsToObject<typeof template.arguments>,
2041
- );
2042
-
2043
- const resourceData: ResourceContent["resource"] = {
2044
- mimeType: template.mimeType,
2045
- uri,
2046
- };
2047
-
2048
- if ("text" in result) {
2049
- resourceData.text = result.text;
2050
- }
2051
-
2052
- if ("blob" in result) {
2053
- resourceData.blob = result.blob;
2054
- }
2055
-
2056
- return resourceData; // The resource we're looking for
2057
- }
2058
-
2059
- throw new UnexpectedStateError(`Resource not found: ${uri}`, { uri });
2060
- }
2061
-
2062
- /**
2063
- * Starts the server.
2064
- */
2065
- public async start(
2066
- options?: Partial<{
2067
- httpStream: {
2068
- enableJsonResponse?: boolean;
2069
- endpoint?: `/${string}`;
2070
- eventStore?: EventStore;
2071
- host?: string;
2072
- port: number;
2073
- stateless?: boolean;
2074
- };
2075
- transportType: "httpStream" | "stdio";
2076
- }>,
2077
- ) {
2078
- const config = this.#parseRuntimeConfig(options);
2079
-
2080
- if (config.transportType === "stdio") {
2081
- const transport = new StdioServerTransport();
2082
-
2083
- // For stdio transport, if authenticate function is provided, call it
2084
- // with undefined request (since stdio doesn't have HTTP request context)
2085
- let auth: T | undefined;
2086
-
2087
- if (this.#authenticate) {
2088
- try {
2089
- auth = await this.#authenticate(
2090
- undefined as unknown as http.IncomingMessage,
2091
- );
2092
- } catch (error) {
2093
- this.#logger.error(
2094
- "[FastMCP error] Authentication failed for stdio transport:",
2095
- error instanceof Error ? error.message : String(error),
2096
- );
2097
- // Continue without auth if authentication fails
2098
- }
2099
- }
2100
-
2101
- const session = new FastMCPSession<T>({
2102
- auth,
2103
- instructions: this.#options.instructions,
2104
- logger: this.#logger,
2105
- name: this.#options.name,
2106
- ping: this.#options.ping,
2107
- prompts: this.#prompts,
2108
- resources: this.#resources,
2109
- resourcesTemplates: this.#resourcesTemplates,
2110
- roots: this.#options.roots,
2111
- tools: this.#tools,
2112
- transportType: "stdio",
2113
- utils: this.#options.utils,
2114
- version: this.#options.version,
2115
- });
2116
-
2117
- await session.connect(transport);
2118
-
2119
- this.#sessions.push(session);
2120
-
2121
- session.once("error", () => {
2122
- this.#removeSession(session);
2123
- });
2124
-
2125
- // Monitor the underlying transport for close events
2126
- if (transport.onclose) {
2127
- const originalOnClose = transport.onclose;
2128
-
2129
- transport.onclose = () => {
2130
- this.#removeSession(session);
2131
-
2132
- if (originalOnClose) {
2133
- originalOnClose();
2134
- }
2135
- };
2136
- } else {
2137
- transport.onclose = () => {
2138
- this.#removeSession(session);
2139
- };
2140
- }
2141
-
2142
- this.emit("connect", {
2143
- session: session as FastMCPSession<FastMCPSessionAuth>,
2144
- });
2145
- } else if (config.transportType === "httpStream") {
2146
- const httpConfig = config.httpStream;
2147
-
2148
- if (httpConfig.stateless) {
2149
- // Stateless mode - create new server instance for each request
2150
- this.#logger.info(
2151
- `[FastMCP info] Starting server in stateless mode on HTTP Stream at http://${httpConfig.host}:${httpConfig.port}${httpConfig.endpoint}`,
2152
- );
2153
-
2154
- this.#httpStreamServer = await startHTTPServer<FastMCPSession<T>>({
2155
- ...(this.#authenticate ? { authenticate: this.#authenticate } : {}),
2156
- createServer: async (request) => {
2157
- let auth: T | undefined;
2158
-
2159
- if (this.#authenticate) {
2160
- auth = await this.#authenticate(request);
2161
-
2162
- // In stateless mode, authentication is REQUIRED
2163
- // mcp-proxy will catch this error and return 401
2164
- if (auth === undefined || auth === null) {
2165
- throw new Error("Authentication required");
2166
- }
2167
- }
2168
-
2169
- // Extract session ID from headers
2170
- const sessionId = Array.isArray(request.headers["mcp-session-id"])
2171
- ? request.headers["mcp-session-id"][0]
2172
- : request.headers["mcp-session-id"];
2173
-
2174
- // In stateless mode, create a new session for each request
2175
- // without persisting it in the sessions array
2176
- return this.#createSession(auth, sessionId);
2177
- },
2178
- enableJsonResponse: httpConfig.enableJsonResponse,
2179
- eventStore: httpConfig.eventStore,
2180
- host: httpConfig.host,
2181
- // In stateless mode, we don't track sessions
2182
- onClose: async () => {
2183
- // No session tracking in stateless mode
2184
- },
2185
- onConnect: async () => {
2186
- // No persistent session tracking in stateless mode
2187
- this.#logger.debug(
2188
- `[FastMCP debug] Stateless HTTP Stream request handled`,
2189
- );
2190
- },
2191
- onUnhandledRequest: async (req, res) => {
2192
- await this.#handleUnhandledRequest(req, res, true, httpConfig.host);
2193
- },
2194
- port: httpConfig.port,
2195
- stateless: true,
2196
- streamEndpoint: httpConfig.endpoint,
2197
- });
2198
- } else {
2199
- // Regular mode with session management
2200
- this.#httpStreamServer = await startHTTPServer<FastMCPSession<T>>({
2201
- ...(this.#authenticate ? { authenticate: this.#authenticate } : {}),
2202
- createServer: async (request) => {
2203
- let auth: T | undefined;
2204
-
2205
- if (this.#authenticate) {
2206
- auth = await this.#authenticate(request);
2207
- }
2208
-
2209
- // Extract session ID from headers
2210
- const sessionId = Array.isArray(request.headers["mcp-session-id"])
2211
- ? request.headers["mcp-session-id"][0]
2212
- : request.headers["mcp-session-id"];
2213
-
2214
- return this.#createSession(auth, sessionId);
2215
- },
2216
- enableJsonResponse: httpConfig.enableJsonResponse,
2217
- eventStore: httpConfig.eventStore,
2218
- host: httpConfig.host,
2219
- onClose: async (session) => {
2220
- const sessionIndex = this.#sessions.indexOf(session);
2221
-
2222
- if (sessionIndex !== -1) this.#sessions.splice(sessionIndex, 1);
2223
-
2224
- this.emit("disconnect", {
2225
- session: session as FastMCPSession<FastMCPSessionAuth>,
2226
- });
2227
- },
2228
- onConnect: async (session) => {
2229
- this.#sessions.push(session);
2230
-
2231
- this.#logger.info(`[FastMCP info] HTTP Stream session established`);
2232
-
2233
- this.emit("connect", {
2234
- session: session as FastMCPSession<FastMCPSessionAuth>,
2235
- });
2236
- },
2237
-
2238
- onUnhandledRequest: async (req, res) => {
2239
- await this.#handleUnhandledRequest(
2240
- req,
2241
- res,
2242
- false,
2243
- httpConfig.host,
2244
- );
2245
- },
2246
- port: httpConfig.port,
2247
- stateless: httpConfig.stateless,
2248
- streamEndpoint: httpConfig.endpoint,
2249
- });
2250
-
2251
- this.#logger.info(
2252
- `[FastMCP info] server is running on HTTP Stream at http://${httpConfig.host}:${httpConfig.port}${httpConfig.endpoint}`,
2253
- );
2254
- }
2255
- } else {
2256
- throw new Error("Invalid transport type");
2257
- }
2258
- }
2259
-
2260
- /**
2261
- * Stops the server.
2262
- */
2263
- public async stop() {
2264
- if (this.#httpStreamServer) {
2265
- await this.#httpStreamServer.close();
2266
- }
2267
- }
2268
-
2269
- /**
2270
- * Creates a new FastMCPSession instance with the current configuration.
2271
- * Used both for regular sessions and stateless requests.
2272
- */
2273
- #createSession(auth?: T, sessionId?: string): FastMCPSession<T> {
2274
- // Check if authentication failed
2275
- if (
2276
- auth &&
2277
- typeof auth === "object" &&
2278
- "authenticated" in auth &&
2279
- !(auth as { authenticated: unknown }).authenticated
2280
- ) {
2281
- const errorMessage =
2282
- "error" in auth &&
2283
- typeof (auth as { error: unknown }).error === "string"
2284
- ? (auth as { error: string }).error
2285
- : "Authentication failed";
2286
- throw new Error(errorMessage);
2287
- }
2288
-
2289
- const allowedTools = auth
2290
- ? this.#tools.filter((tool) =>
2291
- tool.canAccess ? tool.canAccess(auth) : true,
2292
- )
2293
- : this.#tools;
2294
- return new FastMCPSession<T>({
2295
- auth,
2296
- instructions: this.#options.instructions,
2297
- logger: this.#logger,
2298
- name: this.#options.name,
2299
- ping: this.#options.ping,
2300
- prompts: this.#prompts,
2301
- resources: this.#resources,
2302
- resourcesTemplates: this.#resourcesTemplates,
2303
- roots: this.#options.roots,
2304
- sessionId,
2305
- tools: allowedTools,
2306
- transportType: "httpStream",
2307
- utils: this.#options.utils,
2308
- version: this.#options.version,
2309
- });
2310
- }
2311
-
2312
- /**
2313
- * Handles unhandled HTTP requests with health, readiness, and OAuth endpoints
2314
- */
2315
- #handleUnhandledRequest = async (
2316
- req: http.IncomingMessage,
2317
- res: http.ServerResponse,
2318
- isStateless = false,
2319
- host: string,
2320
- ) => {
2321
- const healthConfig = this.#options.health ?? {};
2322
-
2323
- const enabled =
2324
- healthConfig.enabled === undefined ? true : healthConfig.enabled;
2325
-
2326
- if (enabled) {
2327
- const path = healthConfig.path ?? "/health";
2328
- const url = new URL(req.url || "", `http://${host}`);
2329
-
2330
- try {
2331
- if (req.method === "GET" && url.pathname === path) {
2332
- res
2333
- .writeHead(healthConfig.status ?? 200, {
2334
- "Content-Type": "text/plain",
2335
- })
2336
- .end(healthConfig.message ?? "✓ Ok");
2337
-
2338
- return;
2339
- }
2340
-
2341
- // Enhanced readiness check endpoint
2342
- if (req.method === "GET" && url.pathname === "/ready") {
2343
- if (isStateless) {
2344
- // In stateless mode, we're always ready if the server is running
2345
- const response = {
2346
- mode: "stateless",
2347
- ready: 1,
2348
- status: "ready",
2349
- total: 1,
2350
- };
2351
-
2352
- res
2353
- .writeHead(200, {
2354
- "Content-Type": "application/json",
2355
- })
2356
- .end(JSON.stringify(response));
2357
- } else {
2358
- const readySessions = this.#sessions.filter(
2359
- (s) => s.isReady,
2360
- ).length;
2361
- const totalSessions = this.#sessions.length;
2362
- const allReady =
2363
- readySessions === totalSessions && totalSessions > 0;
2364
-
2365
- const response = {
2366
- ready: readySessions,
2367
- status: allReady
2368
- ? "ready"
2369
- : totalSessions === 0
2370
- ? "no_sessions"
2371
- : "initializing",
2372
- total: totalSessions,
2373
- };
2374
-
2375
- res
2376
- .writeHead(allReady ? 200 : 503, {
2377
- "Content-Type": "application/json",
2378
- })
2379
- .end(JSON.stringify(response));
2380
- }
2381
-
2382
- return;
2383
- }
2384
- } catch (error) {
2385
- this.#logger.error("[FastMCP error] health endpoint error", error);
2386
- }
2387
- }
2388
-
2389
- // Handle OAuth well-known endpoints
2390
- const oauthConfig = this.#options.oauth;
2391
- if (oauthConfig?.enabled && req.method === "GET") {
2392
- const url = new URL(req.url || "", `http://${host}`);
2393
-
2394
- if (
2395
- url.pathname === "/.well-known/oauth-authorization-server" &&
2396
- oauthConfig.authorizationServer
2397
- ) {
2398
- const metadata = convertObjectToSnakeCase(
2399
- oauthConfig.authorizationServer,
2400
- );
2401
- res
2402
- .writeHead(200, {
2403
- "Content-Type": "application/json",
2404
- })
2405
- .end(JSON.stringify(metadata));
2406
- return;
2407
- }
2408
-
2409
- if (
2410
- url.pathname === "/.well-known/oauth-protected-resource" &&
2411
- oauthConfig.protectedResource
2412
- ) {
2413
- const metadata = convertObjectToSnakeCase(
2414
- oauthConfig.protectedResource,
2415
- );
2416
- res
2417
- .writeHead(200, {
2418
- "Content-Type": "application/json",
2419
- })
2420
- .end(JSON.stringify(metadata));
2421
- return;
2422
- }
2423
- }
2424
-
2425
- // If the request was not handled above, return 404
2426
- res.writeHead(404).end();
2427
- };
2428
-
2429
- #parseRuntimeConfig(
2430
- overrides?: Partial<{
2431
- httpStream: {
2432
- enableJsonResponse?: boolean;
2433
- endpoint?: `/${string}`;
2434
- host?: string;
2435
- port: number;
2436
- stateless?: boolean;
2437
- };
2438
- transportType: "httpStream" | "stdio";
2439
- }>,
2440
- ):
2441
- | {
2442
- httpStream: {
2443
- enableJsonResponse?: boolean;
2444
- endpoint: `/${string}`;
2445
- eventStore?: EventStore;
2446
- host: string;
2447
- port: number;
2448
- stateless?: boolean;
2449
- };
2450
- transportType: "httpStream";
2451
- }
2452
- | { transportType: "stdio" } {
2453
- const args = process.argv.slice(2);
2454
- const getArg = (name: string) => {
2455
- const index = args.findIndex((arg) => arg === `--${name}`);
2456
-
2457
- return index !== -1 && index + 1 < args.length
2458
- ? args[index + 1]
2459
- : undefined;
2460
- };
2461
-
2462
- const transportArg = getArg("transport");
2463
- const portArg = getArg("port");
2464
- const endpointArg = getArg("endpoint");
2465
- const statelessArg = getArg("stateless");
2466
- const hostArg = getArg("host");
2467
-
2468
- const envTransport = process.env.FASTMCP_TRANSPORT;
2469
- const envPort = process.env.FASTMCP_PORT;
2470
- const envEndpoint = process.env.FASTMCP_ENDPOINT;
2471
- const envStateless = process.env.FASTMCP_STATELESS;
2472
- const envHost = process.env.FASTMCP_HOST;
2473
- // Overrides > CLI > env > defaults
2474
- const transportType =
2475
- overrides?.transportType ||
2476
- (transportArg === "http-stream" ? "httpStream" : transportArg) ||
2477
- envTransport ||
2478
- "stdio";
2479
-
2480
- if (transportType === "httpStream") {
2481
- const port = parseInt(
2482
- overrides?.httpStream?.port?.toString() || portArg || envPort || "8080",
2483
- );
2484
- const host =
2485
- overrides?.httpStream?.host || hostArg || envHost || "localhost";
2486
- const endpoint =
2487
- overrides?.httpStream?.endpoint || endpointArg || envEndpoint || "/mcp";
2488
- const enableJsonResponse =
2489
- overrides?.httpStream?.enableJsonResponse || false;
2490
- const stateless =
2491
- overrides?.httpStream?.stateless ||
2492
- statelessArg === "true" ||
2493
- envStateless === "true" ||
2494
- false;
2495
-
2496
- return {
2497
- httpStream: {
2498
- enableJsonResponse,
2499
- endpoint: endpoint as `/${string}`,
2500
- host,
2501
- port,
2502
- stateless,
2503
- },
2504
- transportType: "httpStream" as const,
2505
- };
2506
- }
2507
-
2508
- return { transportType: "stdio" as const };
2509
- }
2510
-
2511
- #removeSession(session: FastMCPSession<T>): void {
2512
- const sessionIndex = this.#sessions.indexOf(session);
2513
-
2514
- if (sessionIndex !== -1) {
2515
- this.#sessions.splice(sessionIndex, 1);
2516
- this.emit("disconnect", {
2517
- session: session as FastMCPSession<FastMCPSessionAuth>,
2518
- });
2519
- }
2520
- }
2521
- }
2522
-
2523
- export type {
2524
- AudioContent,
2525
- Content,
2526
- ContentResult,
2527
- Context,
2528
- FastMCPEvents,
2529
- FastMCPSessionEvents,
2530
- ImageContent,
2531
- InputPrompt,
2532
- InputPromptArgument,
2533
- LoggingLevel,
2534
- Progress,
2535
- Prompt,
2536
- PromptArgument,
2537
- Resource,
2538
- ResourceContent,
2539
- ResourceLink,
2540
- ResourceResult,
2541
- ResourceTemplate,
2542
- ResourceTemplateArgument,
2543
- SerializableValue,
2544
- ServerOptions,
2545
- TextContent,
2546
- Tool,
2547
- ToolParameters,
2548
- };