fastmcp 2.1.4 → 2.2.1

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
@@ -44,6 +44,7 @@ type FastMCPEvents<T extends FastMCPSessionAuth> = {
44
44
 
45
45
  type FastMCPSessionEvents = {
46
46
  error: (event: { error: Error }) => void;
47
+ ready: () => void;
47
48
  rootsChanged: (event: { roots: Root[] }) => void;
48
49
  };
49
50
 
@@ -281,12 +282,35 @@ const AudioContentZodSchema = z
281
282
  })
282
283
  .strict() satisfies z.ZodType<AudioContent>;
283
284
 
284
- type Content = AudioContent | ImageContent | TextContent;
285
+ type ResourceContent = {
286
+ resource: {
287
+ blob?: string;
288
+ mimeType?: string;
289
+ text?: string;
290
+ uri: string;
291
+ };
292
+ type: "resource";
293
+ };
294
+
295
+ const ResourceContentZodSchema = z
296
+ .object({
297
+ resource: z.object({
298
+ blob: z.string().optional(),
299
+ mimeType: z.string().optional(),
300
+ text: z.string().optional(),
301
+ uri: z.string(),
302
+ }),
303
+ type: z.literal("resource"),
304
+ })
305
+ .strict() satisfies z.ZodType<ResourceContent>;
306
+
307
+ type Content = AudioContent | ImageContent | ResourceContent | TextContent;
285
308
 
286
309
  const ContentZodSchema = z.discriminatedUnion("type", [
287
310
  TextContentZodSchema,
288
311
  ImageContentZodSchema,
289
312
  AudioContentZodSchema,
313
+ ResourceContentZodSchema,
290
314
  ]) satisfies z.ZodType<Content>;
291
315
 
292
316
  type ContentResult = {
@@ -534,7 +558,13 @@ type Tool<
534
558
  args: StandardSchemaV1.InferOutput<Params>,
535
559
  context: Context<T>,
536
560
  ) => Promise<
537
- AudioContent | ContentResult | ImageContent | string | TextContent | void
561
+ | AudioContent
562
+ | ContentResult
563
+ | ImageContent
564
+ | ResourceContent
565
+ | string
566
+ | TextContent
567
+ | void
538
568
  >;
539
569
  name: string;
540
570
  parameters?: Params;
@@ -599,6 +629,9 @@ export class FastMCPSession<
599
629
  public get clientCapabilities(): ClientCapabilities | null {
600
630
  return this.#clientCapabilities ?? null;
601
631
  }
632
+ public get isReady(): boolean {
633
+ return this.#connectionState === "ready";
634
+ }
602
635
  public get loggingLevel(): LoggingLevel {
603
636
  return this.#loggingLevel;
604
637
  }
@@ -611,6 +644,7 @@ export class FastMCPSession<
611
644
  #auth: T | undefined;
612
645
  #capabilities: ServerCapabilities = {};
613
646
  #clientCapabilities?: ClientCapabilities;
647
+ #connectionState: "closed" | "connecting" | "error" | "ready" = "connecting";
614
648
  #loggingLevel: LoggingLevel = "info";
615
649
  #pingConfig?: ServerOptions<T>["ping"];
616
650
  #pingInterval: null | ReturnType<typeof setInterval> = null;
@@ -710,6 +744,8 @@ export class FastMCPSession<
710
744
  }
711
745
 
712
746
  public async close() {
747
+ this.#connectionState = "closed";
748
+
713
749
  if (this.#pingInterval) {
714
750
  clearInterval(this.#pingInterval);
715
751
  }
@@ -726,72 +762,90 @@ export class FastMCPSession<
726
762
  throw new UnexpectedStateError("Server is already connected");
727
763
  }
728
764
 
729
- await this.#server.connect(transport);
765
+ this.#connectionState = "connecting";
730
766
 
731
- let attempt = 0;
767
+ try {
768
+ await this.#server.connect(transport);
732
769
 
733
- while (attempt++ < 10) {
734
- const capabilities = await this.#server.getClientCapabilities();
770
+ let attempt = 0;
735
771
 
736
- if (capabilities) {
737
- this.#clientCapabilities = capabilities;
772
+ while (attempt++ < 10) {
773
+ const capabilities = this.#server.getClientCapabilities();
738
774
 
739
- break;
740
- }
775
+ if (capabilities) {
776
+ this.#clientCapabilities = capabilities;
741
777
 
742
- await delay(100);
743
- }
778
+ break;
779
+ }
744
780
 
745
- if (!this.#clientCapabilities) {
746
- console.warn("[FastMCP warning] could not infer client capabilities");
747
- }
781
+ await delay(100);
782
+ }
748
783
 
749
- if (
750
- this.#clientCapabilities?.roots?.listChanged &&
751
- typeof this.#server.listRoots === "function"
752
- ) {
753
- try {
754
- const roots = await this.#server.listRoots();
755
- this.#roots = roots.roots;
756
- } catch (e) {
757
- if (e instanceof McpError && e.code === ErrorCode.MethodNotFound) {
758
- console.debug(
759
- "[FastMCP debug] listRoots method not supported by client",
760
- );
761
- } else {
762
- console.error(
763
- `[FastMCP error] received error listing roots.\n\n${e instanceof Error ? e.stack : JSON.stringify(e)}`,
764
- );
784
+ if (!this.#clientCapabilities) {
785
+ console.warn("[FastMCP warning] could not infer client capabilities");
786
+ }
787
+
788
+ if (
789
+ this.#clientCapabilities?.roots?.listChanged &&
790
+ typeof this.#server.listRoots === "function"
791
+ ) {
792
+ try {
793
+ const roots = await this.#server.listRoots();
794
+ this.#roots = roots.roots;
795
+ } catch (e) {
796
+ if (e instanceof McpError && e.code === ErrorCode.MethodNotFound) {
797
+ console.debug(
798
+ "[FastMCP debug] listRoots method not supported by client",
799
+ );
800
+ } else {
801
+ console.error(
802
+ `[FastMCP error] received error listing roots.\n\n${e instanceof Error ? e.stack : JSON.stringify(e)}`,
803
+ );
804
+ }
765
805
  }
766
806
  }
767
- }
768
807
 
769
- if (this.#clientCapabilities) {
770
- const pingConfig = this.#getPingConfig(transport);
808
+ if (this.#clientCapabilities) {
809
+ const pingConfig = this.#getPingConfig(transport);
771
810
 
772
- if (pingConfig.enabled) {
773
- this.#pingInterval = setInterval(async () => {
774
- try {
775
- await this.#server.ping();
776
- } catch {
777
- // The reason we are not emitting an error here is because some clients
778
- // seem to not respond to the ping request, and we don't want to crash the server,
779
- // e.g., https://github.com/punkpeye/fastmcp/issues/38.
780
- const logLevel = pingConfig.logLevel;
781
- if (logLevel === "debug") {
782
- console.debug("[FastMCP debug] server ping failed");
783
- } else if (logLevel === "warning") {
784
- console.warn(
785
- "[FastMCP warning] server is not responding to ping",
786
- );
787
- } else if (logLevel === "error") {
788
- console.error("[FastMCP error] server is not responding to ping");
789
- } else {
790
- console.info("[FastMCP info] server ping failed");
811
+ if (pingConfig.enabled) {
812
+ this.#pingInterval = setInterval(async () => {
813
+ try {
814
+ await this.#server.ping();
815
+ } catch {
816
+ // The reason we are not emitting an error here is because some clients
817
+ // seem to not respond to the ping request, and we don't want to crash the server,
818
+ // e.g., https://github.com/punkpeye/fastmcp/issues/38.
819
+ const logLevel = pingConfig.logLevel;
820
+
821
+ if (logLevel === "debug") {
822
+ console.debug("[FastMCP debug] server ping failed");
823
+ } else if (logLevel === "warning") {
824
+ console.warn(
825
+ "[FastMCP warning] server is not responding to ping",
826
+ );
827
+ } else if (logLevel === "error") {
828
+ console.error(
829
+ "[FastMCP error] server is not responding to ping",
830
+ );
831
+ } else {
832
+ console.info("[FastMCP info] server ping failed");
833
+ }
791
834
  }
792
- }
793
- }, pingConfig.intervalMs);
835
+ }, pingConfig.intervalMs);
836
+ }
794
837
  }
838
+
839
+ // Mark connection as ready and emit event
840
+ this.#connectionState = "ready";
841
+ this.emit("ready");
842
+ } catch (error) {
843
+ this.#connectionState = "error";
844
+ const errorEvent = {
845
+ error: error instanceof Error ? error : new Error(String(error)),
846
+ };
847
+ this.emit("error", errorEvent);
848
+ throw error;
795
849
  }
796
850
  }
797
851
 
@@ -801,6 +855,41 @@ export class FastMCPSession<
801
855
  return this.#server.createMessage(message);
802
856
  }
803
857
 
858
+ public waitForReady(): Promise<void> {
859
+ if (this.isReady) {
860
+ return Promise.resolve();
861
+ }
862
+
863
+ if (
864
+ this.#connectionState === "error" ||
865
+ this.#connectionState === "closed"
866
+ ) {
867
+ return Promise.reject(
868
+ new Error(`Connection is in ${this.#connectionState} state`),
869
+ );
870
+ }
871
+
872
+ return new Promise((resolve, reject) => {
873
+ const timeout = setTimeout(() => {
874
+ reject(
875
+ new Error(
876
+ "Connection timeout: Session failed to become ready within 5 seconds",
877
+ ),
878
+ );
879
+ }, 5000);
880
+
881
+ this.once("ready", () => {
882
+ clearTimeout(timeout);
883
+ resolve();
884
+ });
885
+
886
+ this.once("error", (event) => {
887
+ clearTimeout(timeout);
888
+ reject(event.error);
889
+ });
890
+ });
891
+ }
892
+
804
893
  #getPingConfig(transport: Transport): {
805
894
  enabled: boolean;
806
895
  intervalMs: number;
@@ -1362,6 +1451,7 @@ export class FastMCPSession<
1362
1451
  | ContentResult
1363
1452
  | ImageContent
1364
1453
  | null
1454
+ | ResourceContent
1365
1455
  | string
1366
1456
  | TextContent
1367
1457
  | undefined;
@@ -1470,6 +1560,88 @@ export class FastMCP<
1470
1560
  this.#tools.push(tool as unknown as Tool<T>);
1471
1561
  }
1472
1562
 
1563
+ /**
1564
+ * Embeds a resource by URI, making it easy to include resources in tool responses.
1565
+ *
1566
+ * @param uri - The URI of the resource to embed
1567
+ * @returns Promise<ResourceContent> - The embedded resource content
1568
+ */
1569
+ public async embedded(uri: string): Promise<ResourceContent["resource"]> {
1570
+ // First, try to find a direct resource match
1571
+ const directResource = this.#resources.find(
1572
+ (resource) => resource.uri === uri,
1573
+ );
1574
+
1575
+ if (directResource) {
1576
+ const result = await directResource.load();
1577
+ const results = Array.isArray(result) ? result : [result];
1578
+ const firstResult = results[0];
1579
+
1580
+ const resourceData: ResourceContent["resource"] = {
1581
+ mimeType: directResource.mimeType,
1582
+ uri,
1583
+ };
1584
+
1585
+ if ("text" in firstResult) {
1586
+ resourceData.text = firstResult.text;
1587
+ }
1588
+
1589
+ if ("blob" in firstResult) {
1590
+ resourceData.blob = firstResult.blob;
1591
+ }
1592
+
1593
+ return resourceData;
1594
+ }
1595
+
1596
+ // Try to match against resource templates
1597
+ for (const template of this.#resourcesTemplates) {
1598
+ // Check if the URI starts with the template base
1599
+ const templateBase = template.uriTemplate.split("{")[0];
1600
+
1601
+ if (uri.startsWith(templateBase)) {
1602
+ const params: Record<string, string> = {};
1603
+ const templateParts = template.uriTemplate.split("/");
1604
+ const uriParts = uri.split("/");
1605
+
1606
+ for (let i = 0; i < templateParts.length; i++) {
1607
+ const templatePart = templateParts[i];
1608
+
1609
+ if (templatePart?.startsWith("{") && templatePart.endsWith("}")) {
1610
+ const paramName = templatePart.slice(1, -1);
1611
+ const paramValue = uriParts[i];
1612
+
1613
+ if (paramValue) {
1614
+ params[paramName] = paramValue;
1615
+ }
1616
+ }
1617
+ }
1618
+
1619
+ const result = await template.load(
1620
+ params as ResourceTemplateArgumentsToObject<
1621
+ typeof template.arguments
1622
+ >,
1623
+ );
1624
+
1625
+ const resourceData: ResourceContent["resource"] = {
1626
+ mimeType: template.mimeType,
1627
+ uri,
1628
+ };
1629
+
1630
+ if ("text" in result) {
1631
+ resourceData.text = result.text;
1632
+ }
1633
+
1634
+ if ("blob" in result) {
1635
+ resourceData.blob = result.blob;
1636
+ }
1637
+
1638
+ return resourceData; // The resource we're looking for
1639
+ }
1640
+ }
1641
+
1642
+ throw new UnexpectedStateError(`Resource not found: ${uri}`, { uri });
1643
+ }
1644
+
1473
1645
  /**
1474
1646
  * Starts the server.
1475
1647
  */
@@ -1546,12 +1718,10 @@ export class FastMCP<
1546
1718
 
1547
1719
  if (enabled) {
1548
1720
  const path = healthConfig.path ?? "/health";
1721
+ const url = new URL(req.url || "", "http://localhost");
1549
1722
 
1550
1723
  try {
1551
- if (
1552
- req.method === "GET" &&
1553
- new URL(req.url || "", "http://localhost").pathname === path
1554
- ) {
1724
+ if (req.method === "GET" && url.pathname === path) {
1555
1725
  res
1556
1726
  .writeHead(healthConfig.status ?? 200, {
1557
1727
  "Content-Type": "text/plain",
@@ -1560,6 +1730,34 @@ export class FastMCP<
1560
1730
 
1561
1731
  return;
1562
1732
  }
1733
+
1734
+ // Enhanced readiness check endpoint
1735
+ if (req.method === "GET" && url.pathname === "/ready") {
1736
+ const readySessions = this.#sessions.filter(
1737
+ (s) => s.isReady,
1738
+ ).length;
1739
+ const totalSessions = this.#sessions.length;
1740
+ const allReady =
1741
+ readySessions === totalSessions && totalSessions > 0;
1742
+
1743
+ const response = {
1744
+ ready: readySessions,
1745
+ status: allReady
1746
+ ? "ready"
1747
+ : totalSessions === 0
1748
+ ? "no_sessions"
1749
+ : "initializing",
1750
+ total: totalSessions,
1751
+ };
1752
+
1753
+ res
1754
+ .writeHead(allReady ? 200 : 503, {
1755
+ "Content-Type": "application/json",
1756
+ })
1757
+ .end(JSON.stringify(response));
1758
+
1759
+ return;
1760
+ }
1563
1761
  } catch (error) {
1564
1762
  console.error("[FastMCP error] health endpoint error", error);
1565
1763
  }
@@ -170,6 +170,62 @@ server.addPrompt({
170
170
  name: "git-commit",
171
171
  });
172
172
 
173
+ server.addResourceTemplate({
174
+ arguments: [
175
+ {
176
+ description: "Documentation section to retrieve",
177
+ name: "section",
178
+ required: true,
179
+ },
180
+ ],
181
+ description: "Get project documentation",
182
+ load: async (args) => {
183
+ const docs = {
184
+ "api-reference":
185
+ "# API Reference\n\n## Authentication\nAll API requests require a valid API key in the Authorization header.\n\n## Endpoints\n- GET /users - List all users\n- POST /users - Create new user",
186
+ deployment:
187
+ "# Deployment Guide\n\nTo deploy this application:\n\n1. Build the project: `npm run build`\n2. Set environment variables\n3. Deploy to your hosting platform",
188
+ "getting-started":
189
+ "# Getting Started\n\nWelcome to our project! Follow these steps to set up your development environment:\n\n1. Clone the repository\n2. Install dependencies with `npm install`\n3. Run `npm start` to begin",
190
+ };
191
+
192
+ return {
193
+ text:
194
+ docs[args.section as keyof typeof docs] ||
195
+ "Documentation section not found",
196
+ };
197
+ },
198
+ mimeType: "text/markdown",
199
+ name: "Project Documentation",
200
+ uriTemplate: "docs://project/{section}",
201
+ });
202
+
203
+ server.addTool({
204
+ annotations: {
205
+ openWorldHint: false,
206
+ readOnlyHint: true,
207
+ title: "Get Documentation (Embedded)",
208
+ },
209
+ description:
210
+ "Retrieve project documentation using embedded resources - demonstrates the new embedded() feature",
211
+ execute: async (args) => {
212
+ return {
213
+ content: [
214
+ {
215
+ resource: await server.embedded(`docs://project/${args.section}`),
216
+ type: "resource",
217
+ },
218
+ ],
219
+ };
220
+ },
221
+ name: "get-documentation",
222
+ parameters: z.object({
223
+ section: z
224
+ .enum(["getting-started", "api-reference", "deployment"])
225
+ .describe("Documentation section to retrieve"),
226
+ }),
227
+ });
228
+
173
229
  // Select transport type based on command line arguments
174
230
  const transportType = process.argv.includes("--http-stream")
175
231
  ? "httpStream"