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/README.md +110 -0
- package/dist/FastMCP.d.ts +21 -2
- package/dist/FastMCP.js +181 -48
- package/dist/FastMCP.js.map +1 -1
- package/jsr.json +1 -1
- package/package.json +1 -1
- package/src/FastMCP.test.ts +144 -5
- package/src/FastMCP.ts +257 -59
- package/src/examples/addition.ts +56 -0
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
|
|
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
|
-
|
|
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
|
-
|
|
765
|
+
this.#connectionState = "connecting";
|
|
730
766
|
|
|
731
|
-
|
|
767
|
+
try {
|
|
768
|
+
await this.#server.connect(transport);
|
|
732
769
|
|
|
733
|
-
|
|
734
|
-
const capabilities = await this.#server.getClientCapabilities();
|
|
770
|
+
let attempt = 0;
|
|
735
771
|
|
|
736
|
-
|
|
737
|
-
|
|
772
|
+
while (attempt++ < 10) {
|
|
773
|
+
const capabilities = this.#server.getClientCapabilities();
|
|
738
774
|
|
|
739
|
-
|
|
740
|
-
|
|
775
|
+
if (capabilities) {
|
|
776
|
+
this.#clientCapabilities = capabilities;
|
|
741
777
|
|
|
742
|
-
|
|
743
|
-
|
|
778
|
+
break;
|
|
779
|
+
}
|
|
744
780
|
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
}
|
|
781
|
+
await delay(100);
|
|
782
|
+
}
|
|
748
783
|
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
this.#
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
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
|
-
|
|
770
|
-
|
|
808
|
+
if (this.#clientCapabilities) {
|
|
809
|
+
const pingConfig = this.#getPingConfig(transport);
|
|
771
810
|
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
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
|
-
}
|
|
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
|
}
|
package/src/examples/addition.ts
CHANGED
|
@@ -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"
|