fastmcp 1.6.1 → 1.8.0

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 CHANGED
@@ -2,6 +2,7 @@ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
2
2
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
3
  import {
4
4
  CallToolRequestSchema,
5
+ ClientCapabilities,
5
6
  ErrorCode,
6
7
  GetPromptRequestSchema,
7
8
  ListPromptsRequestSchema,
@@ -9,14 +10,28 @@ import {
9
10
  ListToolsRequestSchema,
10
11
  McpError,
11
12
  ReadResourceRequestSchema,
13
+ Root,
12
14
  ServerCapabilities,
13
15
  SetLevelRequestSchema,
14
16
  } from "@modelcontextprotocol/sdk/types.js";
15
17
  import { zodToJsonSchema } from "zod-to-json-schema";
16
18
  import { z } from "zod";
19
+ import { setTimeout as delay } from "timers/promises";
17
20
  import { readFile } from "fs/promises";
18
21
  import { fileTypeFromBuffer } from "file-type";
19
- import { startSSEServer, type SSEServer } from "mcp-proxy";
22
+ import { StrictEventEmitter } from "strict-event-emitter-types";
23
+ import { EventEmitter } from "events";
24
+ import { startSSEServer } from "mcp-proxy";
25
+ import { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
26
+
27
+ export type SSEServer = {
28
+ close: () => Promise<void>;
29
+ };
30
+
31
+ type FastMCPEvents = {
32
+ connect: (event: { session: FastMCPSession }) => void;
33
+ disconnect: (event: { session: FastMCPSession }) => void;
34
+ };
20
35
 
21
36
  /**
22
37
  * Generates an image content object from a URL, file path, or buffer.
@@ -223,65 +238,131 @@ type LoggingLevel =
223
238
  | "alert"
224
239
  | "emergency";
225
240
 
226
- export class FastMCP {
227
- #tools: Tool[];
228
- #resources: Resource[];
229
- #prompts: Prompt[];
230
- #server: Server | null = null;
231
- #options: ServerOptions;
241
+ export class FastMCPSession {
242
+ #capabilities: ServerCapabilities = {};
232
243
  #loggingLevel: LoggingLevel = "info";
244
+ #server: Server;
245
+ #clientCapabilities?: ClientCapabilities;
246
+ #roots: Root[] = [];
247
+
248
+ constructor({
249
+ name,
250
+ version,
251
+ tools,
252
+ resources,
253
+ prompts,
254
+ }: {
255
+ name: string;
256
+ version: string;
257
+ tools: Tool[];
258
+ resources: Resource[];
259
+ prompts: Prompt[];
260
+ }) {
261
+ if (tools.length) {
262
+ this.#capabilities.tools = {};
263
+ }
233
264
 
234
- constructor(public options: ServerOptions) {
235
- this.#options = options;
236
- this.#tools = [];
237
- this.#resources = [];
238
- this.#prompts = [];
265
+ if (resources.length) {
266
+ this.#capabilities.resources = {};
267
+ }
268
+
269
+ if (prompts.length) {
270
+ this.#capabilities.prompts = {};
271
+ }
272
+
273
+ this.#capabilities.logging = {};
274
+
275
+ this.#server = new Server(
276
+ { name: name, version: version },
277
+ { capabilities: this.#capabilities },
278
+ );
279
+
280
+ this.setupErrorHandling();
281
+ this.setupLoggingHandlers();
282
+
283
+ if (tools.length) {
284
+ this.setupToolHandlers(tools);
285
+ }
286
+
287
+ if (resources.length) {
288
+ this.setupResourceHandlers(resources);
289
+ }
290
+
291
+ if (prompts.length) {
292
+ this.setupPromptHandlers(prompts);
293
+ }
239
294
  }
240
295
 
241
- private setupHandlers(server: Server) {
242
- this.setupErrorHandling(server);
296
+ public get clientCapabilities(): ClientCapabilities | null {
297
+ return this.#clientCapabilities ?? null;
298
+ }
299
+
300
+ public get server(): Server {
301
+ return this.#server;
302
+ }
303
+
304
+ public async connect(transport: Transport) {
305
+ if (this.#server.transport) {
306
+ throw new UnexpectedStateError("Server is already connected");
307
+ }
308
+
309
+ await this.#server.connect(transport);
310
+
311
+ let attempt = 0;
312
+
313
+ while (attempt++ < 10) {
314
+ const capabilities = await this.#server.getClientCapabilities();
243
315
 
244
- if (this.#tools.length) {
245
- this.setupToolHandlers(server);
316
+ if (capabilities) {
317
+ this.#clientCapabilities = capabilities;
318
+
319
+ break;
320
+ }
321
+
322
+ await delay(100);
246
323
  }
247
324
 
248
- if (this.#resources.length) {
249
- this.setupResourceHandlers(server);
325
+ if (this.#clientCapabilities?.roots) {
326
+ const roots = await this.#server.listRoots();
327
+
328
+ this.#roots = roots.roots;
250
329
  }
251
330
 
252
- if (this.#prompts.length) {
253
- this.setupPromptHandlers(server);
331
+ if (!this.#clientCapabilities) {
332
+ throw new UnexpectedStateError("Server did not connect");
254
333
  }
334
+ }
255
335
 
256
- server.setRequestHandler(SetLevelRequestSchema, (request) => {
257
- this.#loggingLevel = request.params.level;
336
+ public get roots(): Root[] {
337
+ return this.#roots;
338
+ }
258
339
 
259
- return {};
260
- });
340
+ public async close() {
341
+ await this.#server.close();
261
342
  }
262
343
 
263
- private setupErrorHandling(server: Server) {
264
- server.onerror = (error) => {
344
+ private setupErrorHandling() {
345
+ this.#server.onerror = (error) => {
265
346
  console.error("[MCP Error]", error);
266
347
  };
267
-
268
- process.on("SIGINT", async () => {
269
- await server.close();
270
- process.exit(0);
271
- });
272
348
  }
273
349
 
274
- /**
275
- * Returns the current logging level.
276
- */
277
350
  public get loggingLevel(): LoggingLevel {
278
351
  return this.#loggingLevel;
279
352
  }
280
353
 
281
- private setupToolHandlers(server: Server) {
282
- server.setRequestHandler(ListToolsRequestSchema, async () => {
354
+ private setupLoggingHandlers() {
355
+ this.#server.setRequestHandler(SetLevelRequestSchema, (request) => {
356
+ this.#loggingLevel = request.params.level;
357
+
358
+ return {};
359
+ });
360
+ }
361
+
362
+ private setupToolHandlers(tools: Tool[]) {
363
+ this.#server.setRequestHandler(ListToolsRequestSchema, async () => {
283
364
  return {
284
- tools: this.#tools.map((tool) => {
365
+ tools: tools.map((tool) => {
285
366
  return {
286
367
  name: tool.name,
287
368
  description: tool.description,
@@ -293,10 +374,8 @@ export class FastMCP {
293
374
  };
294
375
  });
295
376
 
296
- server.setRequestHandler(CallToolRequestSchema, async (request) => {
297
- const tool = this.#tools.find(
298
- (tool) => tool.name === request.params.name,
299
- );
377
+ this.#server.setRequestHandler(CallToolRequestSchema, async (request) => {
378
+ const tool = tools.find((tool) => tool.name === request.params.name);
300
379
 
301
380
  if (!tool) {
302
381
  throw new McpError(
@@ -326,7 +405,7 @@ export class FastMCP {
326
405
 
327
406
  try {
328
407
  const reportProgress = async (progress: Progress) => {
329
- await server.notification({
408
+ await this.#server.notification({
330
409
  method: "notifications/progress",
331
410
  params: {
332
411
  ...progress,
@@ -337,7 +416,7 @@ export class FastMCP {
337
416
 
338
417
  const log = {
339
418
  debug: (message: string, context?: SerializableValue) => {
340
- server.sendLoggingMessage({
419
+ this.#server.sendLoggingMessage({
341
420
  level: "debug",
342
421
  data: {
343
422
  message,
@@ -346,7 +425,7 @@ export class FastMCP {
346
425
  });
347
426
  },
348
427
  error: (message: string, context?: SerializableValue) => {
349
- server.sendLoggingMessage({
428
+ this.#server.sendLoggingMessage({
350
429
  level: "error",
351
430
  data: {
352
431
  message,
@@ -355,7 +434,7 @@ export class FastMCP {
355
434
  });
356
435
  },
357
436
  info: (message: string, context?: SerializableValue) => {
358
- server.sendLoggingMessage({
437
+ this.#server.sendLoggingMessage({
359
438
  level: "info",
360
439
  data: {
361
440
  message,
@@ -364,7 +443,7 @@ export class FastMCP {
364
443
  });
365
444
  },
366
445
  warn: (message: string, context?: SerializableValue) => {
367
- server.sendLoggingMessage({
446
+ this.#server.sendLoggingMessage({
368
447
  level: "warning",
369
448
  data: {
370
449
  message,
@@ -408,10 +487,10 @@ export class FastMCP {
408
487
  });
409
488
  }
410
489
 
411
- private setupResourceHandlers(server: Server) {
412
- server.setRequestHandler(ListResourcesRequestSchema, async () => {
490
+ private setupResourceHandlers(resources: Resource[]) {
491
+ this.#server.setRequestHandler(ListResourcesRequestSchema, async () => {
413
492
  return {
414
- resources: this.#resources.map((resource) => {
493
+ resources: resources.map((resource) => {
415
494
  return {
416
495
  uri: resource.uri,
417
496
  name: resource.name,
@@ -421,48 +500,51 @@ export class FastMCP {
421
500
  };
422
501
  });
423
502
 
424
- server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
425
- const resource = this.#resources.find(
426
- (resource) => resource.uri === request.params.uri,
427
- );
428
-
429
- if (!resource) {
430
- throw new McpError(
431
- ErrorCode.MethodNotFound,
432
- `Unknown resource: ${request.params.uri}`,
503
+ this.#server.setRequestHandler(
504
+ ReadResourceRequestSchema,
505
+ async (request) => {
506
+ const resource = resources.find(
507
+ (resource) => resource.uri === request.params.uri,
433
508
  );
434
- }
435
509
 
436
- let result: Awaited<ReturnType<Resource["load"]>>;
510
+ if (!resource) {
511
+ throw new McpError(
512
+ ErrorCode.MethodNotFound,
513
+ `Unknown resource: ${request.params.uri}`,
514
+ );
515
+ }
437
516
 
438
- try {
439
- result = await resource.load();
440
- } catch (error) {
441
- throw new McpError(
442
- ErrorCode.InternalError,
443
- `Error reading resource: ${error}`,
444
- {
445
- uri: resource.uri,
446
- },
447
- );
448
- }
517
+ let result: Awaited<ReturnType<Resource["load"]>>;
449
518
 
450
- return {
451
- contents: [
452
- {
453
- uri: resource.uri,
454
- mimeType: resource.mimeType,
455
- ...result,
456
- },
457
- ],
458
- };
459
- });
519
+ try {
520
+ result = await resource.load();
521
+ } catch (error) {
522
+ throw new McpError(
523
+ ErrorCode.InternalError,
524
+ `Error reading resource: ${error}`,
525
+ {
526
+ uri: resource.uri,
527
+ },
528
+ );
529
+ }
530
+
531
+ return {
532
+ contents: [
533
+ {
534
+ uri: resource.uri,
535
+ mimeType: resource.mimeType,
536
+ ...result,
537
+ },
538
+ ],
539
+ };
540
+ },
541
+ );
460
542
  }
461
543
 
462
- private setupPromptHandlers(server: Server) {
463
- server.setRequestHandler(ListPromptsRequestSchema, async () => {
544
+ private setupPromptHandlers(prompts: Prompt[]) {
545
+ this.#server.setRequestHandler(ListPromptsRequestSchema, async () => {
464
546
  return {
465
- prompts: this.#prompts.map((prompt) => {
547
+ prompts: prompts.map((prompt) => {
466
548
  return {
467
549
  name: prompt.name,
468
550
  description: prompt.description,
@@ -472,8 +554,8 @@ export class FastMCP {
472
554
  };
473
555
  });
474
556
 
475
- server.setRequestHandler(GetPromptRequestSchema, async (request) => {
476
- const prompt = this.#prompts.find(
557
+ this.#server.setRequestHandler(GetPromptRequestSchema, async (request) => {
558
+ const prompt = prompts.find(
477
559
  (prompt) => prompt.name === request.params.name,
478
560
  );
479
561
 
@@ -519,6 +601,31 @@ export class FastMCP {
519
601
  };
520
602
  });
521
603
  }
604
+ }
605
+
606
+ const FastMCPEventEmitterBase: {
607
+ new (): StrictEventEmitter<EventEmitter, FastMCPEvents>;
608
+ } = EventEmitter;
609
+
610
+ class FastMCPEventEmitter extends FastMCPEventEmitterBase {}
611
+
612
+ export class FastMCP extends FastMCPEventEmitter {
613
+ #options: ServerOptions;
614
+ #prompts: Prompt[] = [];
615
+ #resources: Resource[] = [];
616
+ #sessions: FastMCPSession[] = [];
617
+ #sseServer: SSEServer | null = null;
618
+ #tools: Tool[] = [];
619
+
620
+ constructor(public options: ServerOptions) {
621
+ super();
622
+
623
+ this.#options = options;
624
+ }
625
+
626
+ public get sessions(): FastMCPSession[] {
627
+ return this.#sessions;
628
+ }
522
629
 
523
630
  /**
524
631
  * Adds a tool to the server.
@@ -541,8 +648,6 @@ export class FastMCP {
541
648
  this.#prompts.push(prompt);
542
649
  }
543
650
 
544
- #sseServer: SSEServer | null = null;
545
-
546
651
  /**
547
652
  * Starts the server.
548
653
  */
@@ -556,41 +661,56 @@ export class FastMCP {
556
661
  transportType: "stdio",
557
662
  },
558
663
  ) {
559
- const capabilities: ServerCapabilities = {};
560
-
561
- if (this.#tools.length) {
562
- capabilities.tools = {};
563
- }
564
-
565
- if (this.#resources.length) {
566
- capabilities.resources = {};
567
- }
568
-
569
- if (this.#prompts.length) {
570
- capabilities.prompts = {};
571
- }
572
-
573
- capabilities.logging = {};
664
+ if (options.transportType === "stdio") {
665
+ const transport = new StdioServerTransport();
574
666
 
575
- this.#server = new Server(
576
- { name: this.#options.name, version: this.#options.version },
577
- { capabilities },
578
- );
667
+ const session = new FastMCPSession({
668
+ name: this.#options.name,
669
+ version: this.#options.version,
670
+ tools: this.#tools,
671
+ resources: this.#resources,
672
+ prompts: this.#prompts,
673
+ });
579
674
 
580
- this.setupHandlers(this.#server);
675
+ await session.connect(transport);
581
676
 
582
- if (options.transportType === "stdio") {
583
- const transport = new StdioServerTransport();
677
+ this.#sessions.push(session);
584
678
 
585
- await this.#server.connect(transport);
679
+ this.emit("connect", {
680
+ session,
681
+ });
586
682
 
587
683
  console.error(`server is running on stdio`);
588
684
  } else if (options.transportType === "sse") {
589
- this.#sseServer = await startSSEServer({
685
+ this.#sseServer = await startSSEServer<FastMCPSession>({
590
686
  endpoint: options.sse.endpoint as `/${string}`,
591
687
  port: options.sse.port,
592
- server: this.#server,
688
+ createServer: async () => {
689
+ return new FastMCPSession({
690
+ name: this.#options.name,
691
+ version: this.#options.version,
692
+ tools: this.#tools,
693
+ resources: this.#resources,
694
+ prompts: this.#prompts,
695
+ });
696
+ },
697
+ onClose: (session) => {
698
+ this.emit("disconnect", {
699
+ session,
700
+ });
701
+ },
702
+ onConnect: async (session) => {
703
+ this.#sessions.push(session);
704
+
705
+ this.emit("connect", {
706
+ session,
707
+ });
708
+ },
593
709
  });
710
+
711
+ console.error(
712
+ `server is running on SSE at http://localhost:${options.sse.port}${options.sse.endpoint}`,
713
+ );
594
714
  } else {
595
715
  throw new Error("Invalid transport type");
596
716
  }
@@ -21,6 +21,32 @@ server.addTool({
21
21
  },
22
22
  });
23
23
 
24
+ server.addResource({
25
+ uri: "file:///logs/app.log",
26
+ name: "Application Logs",
27
+ mimeType: "text/plain",
28
+ async load() {
29
+ return {
30
+ text: "Example log content",
31
+ };
32
+ },
33
+ });
34
+
35
+ server.addPrompt({
36
+ name: "git-commit",
37
+ description: "Generate a Git commit message",
38
+ arguments: [
39
+ {
40
+ name: "changes",
41
+ description: "Git diff or description of changes",
42
+ required: true,
43
+ },
44
+ ],
45
+ load: async (args) => {
46
+ return `Generate a concise but descriptive commit message for these changes:\n\n${args.changes}`;
47
+ },
48
+ });
49
+
24
50
  server.start({
25
51
  transportType: "stdio",
26
52
  });
@@ -0,0 +1,9 @@
1
+ import { defineConfig } from "vitest/config";
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ poolOptions: {
6
+ forks: { execArgv: ["--experimental-eventsource"] },
7
+ },
8
+ },
9
+ });