fastmcp 1.6.1 → 1.7.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/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "fastmcp",
3
- "version": "1.6.1",
3
+ "version": "1.7.0",
4
4
  "main": "dist/FastMCP.js",
5
5
  "scripts": {
6
6
  "build": "tsup",
7
- "test": "vitest run && tsc",
7
+ "test": "vitest run && tsc && jsr publish --dry-run",
8
8
  "format": "prettier --write . && eslint --fix ."
9
9
  },
10
10
  "bin": {
@@ -24,7 +24,8 @@
24
24
  "@modelcontextprotocol/sdk": "^1.0.4",
25
25
  "execa": "^9.5.2",
26
26
  "file-type": "^19.6.0",
27
- "mcp-proxy": "^1.3.0",
27
+ "mcp-proxy": "^2.0.2",
28
+ "strict-event-emitter-types": "^2.0.0",
28
29
  "yargs": "^17.7.2",
29
30
  "zod": "^3.24.1",
30
31
  "zod-to-json-schema": "^3.24.1"
@@ -51,8 +52,8 @@
51
52
  "@types/yargs": "^17.0.33",
52
53
  "eslint": "^9.17.0",
53
54
  "eslint-plugin-perfectionist": "^4.4.0",
54
- "eventsource": "^3.0.2",
55
55
  "get-port-please": "^3.1.2",
56
+ "jsr": "^0.13.2",
56
57
  "prettier": "^3.4.2",
57
58
  "semantic-release": "^24.2.0",
58
59
  "tsup": "^8.3.5",
@@ -1,10 +1,9 @@
1
- import { FastMCP, UserError, imageContent } from "./FastMCP.js";
1
+ import { FastMCP, FastMCPSession, UserError, imageContent } from "./FastMCP.js";
2
2
  import { z } from "zod";
3
3
  import { test, expect, vi } from "vitest";
4
4
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
5
5
  import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
6
6
  import { getRandomPort } from "get-port-please";
7
- import { EventSource } from "eventsource";
8
7
  import { setTimeout as delay } from "timers/promises";
9
8
  import {
10
9
  ErrorCode,
@@ -12,9 +11,6 @@ import {
12
11
  McpError,
13
12
  } from "@modelcontextprotocol/sdk/types.js";
14
13
 
15
- // @ts-expect-error - figure out how to use --experimental-eventsource with vitest
16
- global.EventSource = EventSource;
17
-
18
14
  const runWithTestServer = async ({
19
15
  run,
20
16
  start,
@@ -26,6 +22,7 @@ const runWithTestServer = async ({
26
22
  }: {
27
23
  client: Client;
28
24
  server: FastMCP;
25
+ session: FastMCPSession;
29
26
  }) => Promise<void>;
30
27
  }) => {
31
28
  const port = await getRandomPort();
@@ -55,9 +52,15 @@ const runWithTestServer = async ({
55
52
  new URL(`http://localhost:${port}/sse`),
56
53
  );
57
54
 
58
- await client.connect(transport);
55
+ const session = await new Promise<FastMCPSession>((resolve) => {
56
+ server.on("connect", (event) => {
57
+ resolve(event.session);
58
+ });
59
+
60
+ client.connect(transport);
61
+ });
59
62
 
60
- await run({ client, server });
63
+ await run({ client, server, session });
61
64
  } finally {
62
65
  await server.stop();
63
66
  }
@@ -369,21 +372,19 @@ test("tracks tool progress", async () => {
369
372
  test("sets logging levels", async () => {
370
373
  await runWithTestServer({
371
374
  start: async () => {
372
- const server = new FastMCP({
375
+ return new FastMCP({
373
376
  name: "Test",
374
377
  version: "1.0.0",
375
378
  });
376
-
377
- return server;
378
379
  },
379
- run: async ({ client, server }) => {
380
+ run: async ({ client, session }) => {
380
381
  await client.setLoggingLevel("debug");
381
382
 
382
- expect(server.loggingLevel).toBe("debug");
383
+ expect(session.loggingLevel).toBe("debug");
383
384
 
384
385
  await client.setLoggingLevel("info");
385
386
 
386
- expect(server.loggingLevel).toBe("info");
387
+ expect(session.loggingLevel).toBe("info");
387
388
  },
388
389
  });
389
390
  });
@@ -543,3 +544,116 @@ test("adds prompts", async () => {
543
544
  },
544
545
  });
545
546
  });
547
+
548
+ test("uses events to notify server of client connect/disconnect", async () => {
549
+ const port = await getRandomPort();
550
+
551
+ const server = new FastMCP({
552
+ name: "Test",
553
+ version: "1.0.0",
554
+ });
555
+
556
+ const onConnect = vi.fn();
557
+ const onDisconnect = vi.fn();
558
+
559
+ server.on("connect", onConnect);
560
+ server.on("disconnect", onDisconnect);
561
+
562
+ await server.start({
563
+ transportType: "sse",
564
+ sse: {
565
+ endpoint: "/sse",
566
+ port,
567
+ },
568
+ });
569
+
570
+ const client = new Client(
571
+ {
572
+ name: "example-client",
573
+ version: "1.0.0",
574
+ },
575
+ {
576
+ capabilities: {},
577
+ },
578
+ );
579
+
580
+ const transport = new SSEClientTransport(
581
+ new URL(`http://localhost:${port}/sse`),
582
+ );
583
+
584
+ await client.connect(transport);
585
+
586
+ await delay(100);
587
+
588
+ expect(onConnect).toHaveBeenCalledTimes(1);
589
+ expect(onDisconnect).toHaveBeenCalledTimes(0);
590
+
591
+ expect(server.sessions).toEqual([expect.any(FastMCPSession)]);
592
+
593
+ await client.close();
594
+
595
+ await delay(100);
596
+
597
+ expect(onConnect).toHaveBeenCalledTimes(1);
598
+ expect(onDisconnect).toHaveBeenCalledTimes(1);
599
+
600
+ await server.stop();
601
+ });
602
+
603
+ test("handles multiple clients", async () => {
604
+ const port = await getRandomPort();
605
+
606
+ const server = new FastMCP({
607
+ name: "Test",
608
+ version: "1.0.0",
609
+ });
610
+
611
+ await server.start({
612
+ transportType: "sse",
613
+ sse: {
614
+ endpoint: "/sse",
615
+ port,
616
+ },
617
+ });
618
+
619
+ const client1 = new Client(
620
+ {
621
+ name: "example-client",
622
+ version: "1.0.0",
623
+ },
624
+ {
625
+ capabilities: {},
626
+ },
627
+ );
628
+
629
+ const transport1 = new SSEClientTransport(
630
+ new URL(`http://localhost:${port}/sse`),
631
+ );
632
+
633
+ await client1.connect(transport1);
634
+
635
+ const client2 = new Client(
636
+ {
637
+ name: "example-client",
638
+ version: "1.0.0",
639
+ },
640
+ {
641
+ capabilities: {},
642
+ },
643
+ );
644
+
645
+ const transport2 = new SSEClientTransport(
646
+ new URL(`http://localhost:${port}/sse`),
647
+ );
648
+
649
+ await client2.connect(transport2);
650
+
651
+ await delay(100);
652
+
653
+ expect(server.sessions).toEqual([
654
+ expect.any(FastMCPSession),
655
+ expect.any(FastMCPSession),
656
+ ]);
657
+
658
+ await server.stop();
659
+ });
package/src/FastMCP.ts CHANGED
@@ -16,7 +16,19 @@ import { zodToJsonSchema } from "zod-to-json-schema";
16
16
  import { z } from "zod";
17
17
  import { readFile } from "fs/promises";
18
18
  import { fileTypeFromBuffer } from "file-type";
19
- import { startSSEServer, type SSEServer } from "mcp-proxy";
19
+ import { StrictEventEmitter } from "strict-event-emitter-types";
20
+ import { EventEmitter } from "events";
21
+ import { startSSEServer } from "mcp-proxy";
22
+ import { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
23
+
24
+ export type SSEServer = {
25
+ close: () => Promise<void>;
26
+ };
27
+
28
+ type FastMCPEvents = {
29
+ connect: (event: { session: FastMCPSession }) => void;
30
+ disconnect: (event: { session: FastMCPSession }) => void;
31
+ };
20
32
 
21
33
  /**
22
34
  * Generates an image content object from a URL, file path, or buffer.
@@ -223,65 +235,89 @@ type LoggingLevel =
223
235
  | "alert"
224
236
  | "emergency";
225
237
 
226
- export class FastMCP {
227
- #tools: Tool[];
228
- #resources: Resource[];
229
- #prompts: Prompt[];
230
- #server: Server | null = null;
231
- #options: ServerOptions;
238
+ export class FastMCPSession {
239
+ #capabilities: ServerCapabilities = {};
232
240
  #loggingLevel: LoggingLevel = "info";
241
+ #server: Server;
242
+
243
+ constructor({
244
+ name,
245
+ version,
246
+ tools,
247
+ resources,
248
+ prompts,
249
+ }: {
250
+ name: string;
251
+ version: string;
252
+ tools: Tool[];
253
+ resources: Resource[];
254
+ prompts: Prompt[];
255
+ }) {
256
+ if (tools.length) {
257
+ this.#capabilities.tools = {};
258
+ }
233
259
 
234
- constructor(public options: ServerOptions) {
235
- this.#options = options;
236
- this.#tools = [];
237
- this.#resources = [];
238
- this.#prompts = [];
239
- }
260
+ if (resources.length) {
261
+ this.#capabilities.resources = {};
262
+ }
263
+
264
+ if (prompts.length) {
265
+ this.#capabilities.prompts = {};
266
+ }
240
267
 
241
- private setupHandlers(server: Server) {
242
- this.setupErrorHandling(server);
268
+ this.#capabilities.logging = {};
269
+
270
+ this.#server = new Server(
271
+ { name: name, version: version },
272
+ { capabilities: this.#capabilities },
273
+ );
243
274
 
244
- if (this.#tools.length) {
245
- this.setupToolHandlers(server);
275
+ this.setupErrorHandling();
276
+ this.setupLoggingHandlers();
277
+
278
+ if (tools.length) {
279
+ this.setupToolHandlers(tools);
246
280
  }
247
281
 
248
- if (this.#resources.length) {
249
- this.setupResourceHandlers(server);
282
+ if (resources.length) {
283
+ this.setupResourceHandlers(resources);
250
284
  }
251
285
 
252
- if (this.#prompts.length) {
253
- this.setupPromptHandlers(server);
286
+ if (prompts.length) {
287
+ this.setupPromptHandlers(prompts);
254
288
  }
289
+ }
255
290
 
256
- server.setRequestHandler(SetLevelRequestSchema, (request) => {
257
- this.#loggingLevel = request.params.level;
291
+ public get server(): Server {
292
+ return this.#server;
293
+ }
258
294
 
259
- return {};
260
- });
295
+ public connect(transport: Transport) {
296
+ this.#server.connect(transport);
261
297
  }
262
298
 
263
- private setupErrorHandling(server: Server) {
264
- server.onerror = (error) => {
299
+ private setupErrorHandling() {
300
+ this.#server.onerror = (error) => {
265
301
  console.error("[MCP Error]", error);
266
302
  };
267
-
268
- process.on("SIGINT", async () => {
269
- await server.close();
270
- process.exit(0);
271
- });
272
303
  }
273
304
 
274
- /**
275
- * Returns the current logging level.
276
- */
277
305
  public get loggingLevel(): LoggingLevel {
278
306
  return this.#loggingLevel;
279
307
  }
280
308
 
281
- private setupToolHandlers(server: Server) {
282
- server.setRequestHandler(ListToolsRequestSchema, async () => {
309
+ private setupLoggingHandlers() {
310
+ this.#server.setRequestHandler(SetLevelRequestSchema, (request) => {
311
+ this.#loggingLevel = request.params.level;
312
+
313
+ return {};
314
+ });
315
+ }
316
+
317
+ private setupToolHandlers(tools: Tool[]) {
318
+ this.#server.setRequestHandler(ListToolsRequestSchema, async () => {
283
319
  return {
284
- tools: this.#tools.map((tool) => {
320
+ tools: tools.map((tool) => {
285
321
  return {
286
322
  name: tool.name,
287
323
  description: tool.description,
@@ -293,10 +329,8 @@ export class FastMCP {
293
329
  };
294
330
  });
295
331
 
296
- server.setRequestHandler(CallToolRequestSchema, async (request) => {
297
- const tool = this.#tools.find(
298
- (tool) => tool.name === request.params.name,
299
- );
332
+ this.#server.setRequestHandler(CallToolRequestSchema, async (request) => {
333
+ const tool = tools.find((tool) => tool.name === request.params.name);
300
334
 
301
335
  if (!tool) {
302
336
  throw new McpError(
@@ -326,7 +360,7 @@ export class FastMCP {
326
360
 
327
361
  try {
328
362
  const reportProgress = async (progress: Progress) => {
329
- await server.notification({
363
+ await this.#server.notification({
330
364
  method: "notifications/progress",
331
365
  params: {
332
366
  ...progress,
@@ -337,7 +371,7 @@ export class FastMCP {
337
371
 
338
372
  const log = {
339
373
  debug: (message: string, context?: SerializableValue) => {
340
- server.sendLoggingMessage({
374
+ this.#server.sendLoggingMessage({
341
375
  level: "debug",
342
376
  data: {
343
377
  message,
@@ -346,7 +380,7 @@ export class FastMCP {
346
380
  });
347
381
  },
348
382
  error: (message: string, context?: SerializableValue) => {
349
- server.sendLoggingMessage({
383
+ this.#server.sendLoggingMessage({
350
384
  level: "error",
351
385
  data: {
352
386
  message,
@@ -355,7 +389,7 @@ export class FastMCP {
355
389
  });
356
390
  },
357
391
  info: (message: string, context?: SerializableValue) => {
358
- server.sendLoggingMessage({
392
+ this.#server.sendLoggingMessage({
359
393
  level: "info",
360
394
  data: {
361
395
  message,
@@ -364,7 +398,7 @@ export class FastMCP {
364
398
  });
365
399
  },
366
400
  warn: (message: string, context?: SerializableValue) => {
367
- server.sendLoggingMessage({
401
+ this.#server.sendLoggingMessage({
368
402
  level: "warning",
369
403
  data: {
370
404
  message,
@@ -408,10 +442,10 @@ export class FastMCP {
408
442
  });
409
443
  }
410
444
 
411
- private setupResourceHandlers(server: Server) {
412
- server.setRequestHandler(ListResourcesRequestSchema, async () => {
445
+ private setupResourceHandlers(resources: Resource[]) {
446
+ this.#server.setRequestHandler(ListResourcesRequestSchema, async () => {
413
447
  return {
414
- resources: this.#resources.map((resource) => {
448
+ resources: resources.map((resource) => {
415
449
  return {
416
450
  uri: resource.uri,
417
451
  name: resource.name,
@@ -421,48 +455,51 @@ export class FastMCP {
421
455
  };
422
456
  });
423
457
 
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}`,
458
+ this.#server.setRequestHandler(
459
+ ReadResourceRequestSchema,
460
+ async (request) => {
461
+ const resource = resources.find(
462
+ (resource) => resource.uri === request.params.uri,
433
463
  );
434
- }
435
464
 
436
- let result: Awaited<ReturnType<Resource["load"]>>;
465
+ if (!resource) {
466
+ throw new McpError(
467
+ ErrorCode.MethodNotFound,
468
+ `Unknown resource: ${request.params.uri}`,
469
+ );
470
+ }
437
471
 
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
- }
472
+ let result: Awaited<ReturnType<Resource["load"]>>;
449
473
 
450
- return {
451
- contents: [
452
- {
453
- uri: resource.uri,
454
- mimeType: resource.mimeType,
455
- ...result,
456
- },
457
- ],
458
- };
459
- });
474
+ try {
475
+ result = await resource.load();
476
+ } catch (error) {
477
+ throw new McpError(
478
+ ErrorCode.InternalError,
479
+ `Error reading resource: ${error}`,
480
+ {
481
+ uri: resource.uri,
482
+ },
483
+ );
484
+ }
485
+
486
+ return {
487
+ contents: [
488
+ {
489
+ uri: resource.uri,
490
+ mimeType: resource.mimeType,
491
+ ...result,
492
+ },
493
+ ],
494
+ };
495
+ },
496
+ );
460
497
  }
461
498
 
462
- private setupPromptHandlers(server: Server) {
463
- server.setRequestHandler(ListPromptsRequestSchema, async () => {
499
+ private setupPromptHandlers(prompts: Prompt[]) {
500
+ this.#server.setRequestHandler(ListPromptsRequestSchema, async () => {
464
501
  return {
465
- prompts: this.#prompts.map((prompt) => {
502
+ prompts: prompts.map((prompt) => {
466
503
  return {
467
504
  name: prompt.name,
468
505
  description: prompt.description,
@@ -472,8 +509,8 @@ export class FastMCP {
472
509
  };
473
510
  });
474
511
 
475
- server.setRequestHandler(GetPromptRequestSchema, async (request) => {
476
- const prompt = this.#prompts.find(
512
+ this.#server.setRequestHandler(GetPromptRequestSchema, async (request) => {
513
+ const prompt = prompts.find(
477
514
  (prompt) => prompt.name === request.params.name,
478
515
  );
479
516
 
@@ -519,6 +556,31 @@ export class FastMCP {
519
556
  };
520
557
  });
521
558
  }
559
+ }
560
+
561
+ const FastMCPEventEmitterBase: {
562
+ new (): StrictEventEmitter<EventEmitter, FastMCPEvents>;
563
+ } = EventEmitter;
564
+
565
+ class FastMCPEventEmitter extends FastMCPEventEmitterBase {}
566
+
567
+ export class FastMCP extends FastMCPEventEmitter {
568
+ #options: ServerOptions;
569
+ #prompts: Prompt[] = [];
570
+ #resources: Resource[] = [];
571
+ #sessions: FastMCPSession[] = [];
572
+ #sseServer: SSEServer | null = null;
573
+ #tools: Tool[] = [];
574
+
575
+ constructor(public options: ServerOptions) {
576
+ super();
577
+
578
+ this.#options = options;
579
+ }
580
+
581
+ public get sessions(): FastMCPSession[] {
582
+ return this.#sessions;
583
+ }
522
584
 
523
585
  /**
524
586
  * Adds a tool to the server.
@@ -541,8 +603,6 @@ export class FastMCP {
541
603
  this.#prompts.push(prompt);
542
604
  }
543
605
 
544
- #sseServer: SSEServer | null = null;
545
-
546
606
  /**
547
607
  * Starts the server.
548
608
  */
@@ -556,41 +616,81 @@ export class FastMCP {
556
616
  transportType: "stdio",
557
617
  },
558
618
  ) {
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 = {};
619
+ if (options.transportType === "stdio") {
620
+ const transport = new StdioServerTransport();
574
621
 
575
- this.#server = new Server(
576
- { name: this.#options.name, version: this.#options.version },
577
- { capabilities },
578
- );
622
+ const session = new FastMCPSession({
623
+ name: this.#options.name,
624
+ version: this.#options.version,
625
+ tools: this.#tools,
626
+ resources: this.#resources,
627
+ prompts: this.#prompts,
628
+ });
579
629
 
580
- this.setupHandlers(this.#server);
630
+ await session.connect(transport);
581
631
 
582
- if (options.transportType === "stdio") {
583
- const transport = new StdioServerTransport();
632
+ this.#sessions.push(session);
584
633
 
585
- await this.#server.connect(transport);
634
+ this.emit("connect", {
635
+ session,
636
+ });
586
637
 
587
638
  console.error(`server is running on stdio`);
588
639
  } else if (options.transportType === "sse") {
589
640
  this.#sseServer = await startSSEServer({
590
641
  endpoint: options.sse.endpoint as `/${string}`,
591
642
  port: options.sse.port,
592
- server: this.#server,
643
+ createServer: async () => {
644
+ const session = new FastMCPSession({
645
+ name: this.#options.name,
646
+ version: this.#options.version,
647
+ tools: this.#tools,
648
+ resources: this.#resources,
649
+ prompts: this.#prompts,
650
+ });
651
+
652
+ this.#sessions.push(session);
653
+
654
+ return session.server;
655
+ },
656
+ onClose: (server) => {
657
+ const session = this.#sessions.find(
658
+ (session) => session.server === server,
659
+ );
660
+
661
+ if (!session) {
662
+ throw new UnexpectedStateError("Server not found");
663
+ }
664
+
665
+ this.#sessions = this.#sessions.filter(
666
+ (maybeOurSession) => maybeOurSession !== session,
667
+ );
668
+
669
+ this.emit("disconnect", {
670
+ session,
671
+ });
672
+ },
673
+ onConnect: async (server) => {
674
+ const session = this.#sessions.find(
675
+ (session) => session.server === server,
676
+ );
677
+
678
+ if (!session) {
679
+ throw new UnexpectedStateError("Server not found");
680
+ }
681
+
682
+ // TODO investigate where is the race condition
683
+ setTimeout(() => {
684
+ this.emit("connect", {
685
+ session,
686
+ });
687
+ }, 100);
688
+ },
593
689
  });
690
+
691
+ console.error(
692
+ `server is running on SSE at http://localhost:${options.sse.port}${options.sse.endpoint}`,
693
+ );
594
694
  } else {
595
695
  throw new Error("Invalid transport type");
596
696
  }