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.
@@ -1,4389 +0,0 @@
1
- import { Client } from "@modelcontextprotocol/sdk/client/index.js";
2
- import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
3
- import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
4
- import {
5
- CreateMessageRequestSchema,
6
- ErrorCode,
7
- ListRootsRequestSchema,
8
- LoggingMessageNotificationSchema,
9
- McpError,
10
- PingRequestSchema,
11
- Root,
12
- } from "@modelcontextprotocol/sdk/types.js";
13
- import { createEventSource, EventSourceClient } from "eventsource-client";
14
- import { getRandomPort } from "get-port-please";
15
- import { setTimeout as delay } from "timers/promises";
16
- import { fetch } from "undici";
17
- import { expect, test, vi } from "vitest";
18
- import { z } from "zod";
19
- import { z as z4 } from "zod/v4";
20
-
21
- import {
22
- audioContent,
23
- type ContentResult,
24
- FastMCP,
25
- FastMCPSession,
26
- imageContent,
27
- type TextContent,
28
- UserError,
29
- } from "./FastMCP.js";
30
-
31
- const runWithTestServer = async ({
32
- client: createClient,
33
- run,
34
- server: createServer,
35
- }: {
36
- client?: () => Promise<Client>;
37
- run: ({
38
- client,
39
- server,
40
- }: {
41
- client: Client;
42
- server: FastMCP;
43
- session: FastMCPSession;
44
- }) => Promise<void>;
45
- server?: () => Promise<FastMCP>;
46
- }) => {
47
- const port = await getRandomPort();
48
-
49
- const server = createServer
50
- ? await createServer()
51
- : new FastMCP({
52
- name: "Test",
53
- version: "1.0.0",
54
- });
55
-
56
- await server.start({
57
- httpStream: {
58
- port,
59
- },
60
- transportType: "httpStream",
61
- });
62
-
63
- try {
64
- const client = createClient
65
- ? await createClient()
66
- : new Client(
67
- {
68
- name: "example-client",
69
- version: "1.0.0",
70
- },
71
- {
72
- capabilities: {},
73
- },
74
- );
75
-
76
- const transport = new SSEClientTransport(
77
- new URL(`http://localhost:${port}/sse`),
78
- );
79
-
80
- const session = await new Promise<FastMCPSession>((resolve) => {
81
- server.on("connect", async (event) => {
82
- // Wait for session to be fully ready before resolving
83
- await event.session.waitForReady();
84
- resolve(event.session);
85
- });
86
-
87
- client.connect(transport);
88
- });
89
-
90
- await run({ client, server, session });
91
- } finally {
92
- await server.stop();
93
- }
94
-
95
- return port;
96
- };
97
-
98
- test("adds tools", async () => {
99
- await runWithTestServer({
100
- run: async ({ client }) => {
101
- expect(await client.listTools()).toEqual({
102
- tools: [
103
- {
104
- description: "Add two numbers",
105
- inputSchema: {
106
- $schema: "http://json-schema.org/draft-07/schema#",
107
- additionalProperties: false,
108
- properties: {
109
- a: { type: "number" },
110
- b: { type: "number" },
111
- },
112
- required: ["a", "b"],
113
- type: "object",
114
- },
115
- name: "add",
116
- },
117
- ],
118
- });
119
- },
120
- server: async () => {
121
- const server = new FastMCP({
122
- name: "Test",
123
- version: "1.0.0",
124
- });
125
-
126
- server.addTool({
127
- description: "Add two numbers",
128
- execute: async (args) => {
129
- return String(args.a + args.b);
130
- },
131
- name: "add",
132
- parameters: z.object({
133
- a: z.number(),
134
- b: z.number(),
135
- }),
136
- });
137
-
138
- return server;
139
- },
140
- });
141
- });
142
-
143
- test("adds tools with Zod v4 schema", async () => {
144
- await runWithTestServer({
145
- run: async ({ client }) => {
146
- expect(await client.listTools()).toEqual({
147
- tools: [
148
- {
149
- description: "Add two numbers (using Zod v4 schema)",
150
- inputSchema: {
151
- $schema: "https://json-schema.org/draft/2020-12/schema",
152
- additionalProperties: false,
153
- properties: {
154
- a: { type: "number" },
155
- b: { type: "number" },
156
- },
157
- required: ["a", "b"],
158
- type: "object",
159
- },
160
- name: "add-zod-v4",
161
- },
162
- ],
163
- });
164
- },
165
- server: async () => {
166
- const server = new FastMCP({
167
- name: "Test",
168
- version: "1.0.0",
169
- });
170
-
171
- const AddParamsZod4 = z4.object({
172
- a: z4.number(),
173
- b: z4.number(),
174
- });
175
-
176
- server.addTool({
177
- description: "Add two numbers (using Zod v4 schema)",
178
- execute: async (args) => {
179
- return String(args.a + args.b);
180
- },
181
- name: "add-zod-v4",
182
- parameters: AddParamsZod4,
183
- });
184
-
185
- return server;
186
- },
187
- });
188
- });
189
-
190
- test("health endpoint returns ok", async () => {
191
- const port = await getRandomPort();
192
-
193
- const server = new FastMCP({
194
- health: { message: "healthy", path: "/healthz" },
195
- name: "Test",
196
- version: "1.0.0",
197
- });
198
-
199
- await server.start({
200
- httpStream: { port },
201
- transportType: "httpStream",
202
- });
203
-
204
- try {
205
- const response = await fetch(`http://localhost:${port}/healthz`);
206
- expect(response.status).toBe(200);
207
- expect(await response.text()).toBe("healthy");
208
- } finally {
209
- await server.stop();
210
- }
211
- });
212
-
213
- test("calls a tool", async () => {
214
- await runWithTestServer({
215
- run: async ({ client }) => {
216
- expect(
217
- await client.callTool({
218
- arguments: {
219
- a: 1,
220
- b: 2,
221
- },
222
- name: "add",
223
- }),
224
- ).toEqual({
225
- content: [{ text: "3", type: "text" }],
226
- });
227
- },
228
- server: async () => {
229
- const server = new FastMCP({
230
- name: "Test",
231
- version: "1.0.0",
232
- });
233
-
234
- server.addTool({
235
- description: "Add two numbers",
236
- execute: async (args) => {
237
- return String(args.a + args.b);
238
- },
239
- name: "add",
240
- parameters: z.object({
241
- a: z.number(),
242
- b: z.number(),
243
- }),
244
- });
245
-
246
- return server;
247
- },
248
- });
249
- });
250
-
251
- test("returns a list", async () => {
252
- await runWithTestServer({
253
- run: async ({ client }) => {
254
- expect(
255
- await client.callTool({
256
- arguments: {
257
- a: 1,
258
- b: 2,
259
- },
260
- name: "add",
261
- }),
262
- ).toEqual({
263
- content: [
264
- { text: "a", type: "text" },
265
- { text: "b", type: "text" },
266
- ],
267
- });
268
- },
269
- server: async () => {
270
- const server = new FastMCP({
271
- name: "Test",
272
- version: "1.0.0",
273
- });
274
-
275
- server.addTool({
276
- description: "Add two numbers",
277
- execute: async () => {
278
- return {
279
- content: [
280
- { text: "a", type: "text" },
281
- { text: "b", type: "text" },
282
- ],
283
- };
284
- },
285
- name: "add",
286
- parameters: z.object({
287
- a: z.number(),
288
- b: z.number(),
289
- }),
290
- });
291
-
292
- return server;
293
- },
294
- });
295
- });
296
-
297
- test("returns an image", async () => {
298
- await runWithTestServer({
299
- run: async ({ client }) => {
300
- expect(
301
- await client.callTool({
302
- arguments: {
303
- a: 1,
304
- b: 2,
305
- },
306
- name: "add",
307
- }),
308
- ).toEqual({
309
- content: [
310
- {
311
- data: "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=",
312
- mimeType: "image/png",
313
- type: "image",
314
- },
315
- ],
316
- });
317
- },
318
- server: async () => {
319
- const server = new FastMCP({
320
- name: "Test",
321
- version: "1.0.0",
322
- });
323
-
324
- server.addTool({
325
- description: "Add two numbers",
326
- execute: async () => {
327
- return imageContent({
328
- buffer: Buffer.from(
329
- "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=",
330
- "base64",
331
- ),
332
- });
333
- },
334
- name: "add",
335
- parameters: z.object({
336
- a: z.number(),
337
- b: z.number(),
338
- }),
339
- });
340
-
341
- return server;
342
- },
343
- });
344
- });
345
-
346
- test("returns an audio", async () => {
347
- await runWithTestServer({
348
- run: async ({ client }) => {
349
- expect(
350
- await client.callTool({
351
- arguments: {
352
- a: 1,
353
- b: 2,
354
- },
355
- name: "add",
356
- }),
357
- ).toEqual({
358
- content: [
359
- {
360
- data: "UklGRhwMAABXQVZFZm10IBAAAAABAAEAgD4AAIA+AAABAAgAZGF0Ya4LAACAgICAgICAgICAgICAgICAgICAgICAgICAf3hxeH+AfXZ1eHx6dnR5fYGFgoOKi42aloubq6GOjI2Op7ythXJ0eYF5aV1AOFFib32HmZSHhpCalIiYi4SRkZaLfnhxaWptb21qaWBea2BRYmZTVmFgWFNXVVVhaGdbYGhZbXh1gXZ1goeIlot1k6yxtKaOkaWhq7KonKCZoaCjoKWuqqmurK6ztrO7tbTAvru/vb68vbW6vLGqsLOfm5yal5KKhoyBeHt2dXBnbmljVlJWUEBBPDw9Mi4zKRwhIBYaGRQcHBURGB0XFxwhGxocJSstMjg6PTc6PUxVV1lWV2JqaXN0coCHhIyPjpOenqWppK6xu72yxMu9us7Pw83Wy9nY29ve6OPr6uvs6ezu6ejk6erm3uPj3dbT1sjBzdDFuMHAt7m1r7W6qaCupJOTkpWPgHqAd3JrbGlnY1peX1hTUk9PTFRKR0RFQkRBRUVEQkdBPjs9Pzo6NT04Njs+PTxAPzo/Ojk6PEA5PUJAQD04PkRCREZLUk1KT1BRUVdXU1VRV1tZV1xgXltcXF9hXl9eY2VmZmlna3J0b3F3eHyBfX+JgIWJiouTlZCTmpybnqSgnqyrqrO3srK2uL2/u7jAwMLFxsfEv8XLzcrIy83JzcrP0s3M0dTP0drY1dPR1dzc19za19XX2dnU1NjU0dXPzdHQy8rMysfGxMLBvLu3ta+sraeioJ2YlI+MioeFfX55cnJsaWVjXVlbVE5RTktHRUVAPDw3NC8uLyknKSIiJiUdHiEeGx4eHRwZHB8cHiAfHh8eHSEhISMoJyMnKisrLCszNy8yOTg9QEJFRUVITVFOTlJVWltaXmNfX2ZqZ21xb3R3eHqAhoeJkZKTlZmhpJ6kqKeur6yxtLW1trW4t6+us7axrbK2tLa6ury7u7u9u7vCwb+/vr7Ev7y9v8G8vby6vru4uLq+tri8ubi5t7W4uLW5uLKxs7G0tLGwt7Wvs7avr7O0tLW4trS4uLO1trW1trm1tLm0r7Kyr66wramsqaKlp52bmpeWl5KQkImEhIB8fXh3eHJrbW5mYGNcWFhUUE1LRENDQUI9ODcxLy8vMCsqLCgoKCgpKScoKCYoKygpKyssLi0sLi0uMDIwMTIuLzQ0Njg4Njc8ODlBQ0A/RUdGSU5RUVFUV1pdXWFjZGdpbG1vcXJ2eXh6fICAgIWIio2OkJGSlJWanJqbnZ2cn6Kkp6enq62srbCysrO1uLy4uL+/vL7CwMHAvb/Cvbq9vLm5uba2t7Sysq+urqyqqaalpqShoJ+enZuamZqXlZWTkpGSkpCNjpCMioqLioiHhoeGhYSGg4GDhoKDg4GBg4GBgoGBgoOChISChISChIWDg4WEgoSEgYODgYGCgYGAgICAgX99f398fX18e3p6e3t7enp7fHx4e3x6e3x7fHx9fX59fn1+fX19fH19fnx9fn19fX18fHx7fHx6fH18fXx8fHx7fH1+fXx+f319fn19fn1+gH9+f4B/fn+AgICAgH+AgICAgIGAgICAgH9+f4B+f35+fn58e3t8e3p5eXh4d3Z1dHRzcXBvb21sbmxqaWhlZmVjYmFfX2BfXV1cXFxaWVlaWVlYV1hYV1hYWVhZWFlaWllbXFpbXV5fX15fYWJhYmNiYWJhYWJjZGVmZ2hqbG1ub3Fxc3V3dnd6e3t8e3x+f3+AgICAgoGBgoKDhISFh4aHiYqKi4uMjYyOj4+QkZKUlZWXmJmbm52enqCioqSlpqeoqaqrrK2ur7CxsrGys7O0tbW2tba3t7i3uLe4t7a3t7i3tre2tba1tLSzsrKysbCvrq2sq6qop6alo6OioJ+dnJqZmJeWlJKSkI+OjoyLioiIh4WEg4GBgH9+fXt6eXh3d3V0c3JxcG9ubWxsamppaWhnZmVlZGRjYmNiYWBhYGBfYF9fXl5fXl1dXVxdXF1dXF1cXF1cXF1dXV5dXV5fXl9eX19gYGFgYWJhYmFiY2NiY2RjZGNkZWRlZGVmZmVmZmVmZ2dmZ2hnaGhnaGloZ2hpaWhpamlqaWpqa2pra2xtbGxtbm1ubm5vcG9wcXBxcnFycnN0c3N0dXV2d3d4eHh5ent6e3x9fn5/f4CAgIGCg4SEhYaGh4iIiYqLi4uMjY2Oj5CQkZGSk5OUlJWWlpeYl5iZmZqbm5ybnJ2cnZ6en56fn6ChoKChoqGio6KjpKOko6SjpKWkpaSkpKSlpKWkpaSlpKSlpKOkpKOko6KioaKhoaCfoJ+enp2dnJybmpmZmJeXlpWUk5STkZGQj4+OjYyLioqJh4eGhYSEgoKBgIB/fn59fHt7enl5eHd3dnZ1dHRzc3JycXBxcG9vbm5tbWxrbGxraWppaWhpaGdnZ2dmZ2ZlZmVmZWRlZGVkY2RjZGNkZGRkZGRkZGRkZGRjZGRkY2RjZGNkZWRlZGVmZWZmZ2ZnZ2doaWhpaWpra2xsbW5tbm9ub29wcXFycnNzdHV1dXZ2d3d4eXl6enp7fHx9fX5+f4CAgIGAgYGCgoOEhISFhoWGhoeIh4iJiImKiYqLiouLjI2MjI2OjY6Pj46PkI+QkZCRkJGQkZGSkZKRkpGSkZGRkZKRkpKRkpGSkZKRkpGSkZKRkpGSkZCRkZCRkI+Qj5CPkI+Pjo+OjY6Njo2MjYyLjIuMi4qLioqJiomJiImIh4iHh4aHhoaFhoWFhIWEg4SDg4KDgoKBgoGAgYCBgICAgICAf4CAf39+f35/fn1+fX59fHx9fH18e3x7fHt6e3p7ent6e3p5enl6enl6eXp5eXl4eXh5eHl4eXh5eHl4eXh5eHh3eHh4d3h4d3h3d3h4d3l4eHd4d3h3eHd4d3h3eHh4eXh5eHl4eHl4eXh5enl6eXp5enl6eXp5ent6ent6e3x7fHx9fH18fX19fn1+fX5/fn9+f4B/gH+Af4CAgICAgIGAgYCBgoGCgYKCgoKDgoOEg4OEg4SFhIWEhYSFhoWGhYaHhoeHhoeGh4iHiIiHiImIiImKiYqJiYqJiouKi4qLiouKi4qLiouKi4qLiouKi4qLi4qLiouKi4qLiomJiomIiYiJiImIh4iIh4iHhoeGhYWGhYaFhIWEg4OEg4KDgoOCgYKBgIGAgICAgH+Af39+f359fn18fX19fHx8e3t6e3p7enl6eXp5enl6enl5eXh5eHh5eHl4eXh5eHl4eHd5eHd3eHl4d3h3eHd4d3h3eHh4d3h4d3h3d3h5eHl4eXh5eHl5eXp5enl6eXp7ent6e3p7e3t7fHt8e3x8fHx9fH1+fX59fn9+f35/gH+AgICAgICAgYGAgYKBgoGCgoKDgoOEg4SEhIWFhIWFhoWGhYaGhoaHhoeGh4aHhoeIh4iHiIeHiIeIh4iHiIeIiIiHiIeIh4iHiIiHiIeIh4iHiIeIh4eIh4eIh4aHh4aHhoeGh4aHhoWGhYaFhoWFhIWEhYSFhIWEhISDhIOEg4OCg4OCg4KDgYKCgYKCgYCBgIGAgYCBgICAgICAgICAf4B/f4B/gH+Af35/fn9+f35/fn1+fn19fn1+fX59fn19fX19fH18fXx9fH18fXx9fH18fXx8fHt8e3x7fHt8e3x7fHt8e3x7fHt8e3x7fHt8e3x7fHt8e3x8e3x7fHt8e3x7fHx8fXx9fH18fX5+fX59fn9+f35+f35/gH+Af4B/gICAgICAgICAgICAgYCBgIGAgIGAgYGBgoGCgYKBgoGCgYKBgoGCgoKDgoOCg4KDgoOCg4KDgoOCg4KDgoOCg4KDgoOCg4KDgoOCg4KDgoOCg4KDgoOCg4KDgoOCg4KDgoOCg4KCgoGCgYKBgoGCgYKBgoGCgYKBgoGCgYKBgoGCgYKBgoGCgYKBgoGCgYKBgoGBgYCBgIGAgYCBgIGAgYCBgIGAgYCBgIGAgYCBgIGAgYCAgICBgIGAgYCBgIGAgYCBgIGAgYCBgExJU1RCAAAASU5GT0lDUkQMAAAAMjAwOC0wOS0yMQAASUVORwMAAAAgAAABSVNGVBYAAABTb255IFNvdW5kIEZvcmdlIDguMAAA",
361
- mimeType: "audio/wav",
362
- type: "audio",
363
- },
364
- ],
365
- });
366
- },
367
- server: async () => {
368
- const server = new FastMCP({
369
- name: "Test",
370
- version: "1.0.0",
371
- });
372
-
373
- server.addTool({
374
- description: "Add two numbers",
375
- execute: async () => {
376
- return audioContent({
377
- buffer: Buffer.from(
378
- "UklGRhwMAABXQVZFZm10IBAAAAABAAEAgD4AAIA+AAABAAgAZGF0Ya4LAACAgICAgICAgICAgICAgICAgICAgICAgICAf3hxeH+AfXZ1eHx6dnR5fYGFgoOKi42aloubq6GOjI2Op7ythXJ0eYF5aV1AOFFib32HmZSHhpCalIiYi4SRkZaLfnhxaWptb21qaWBea2BRYmZTVmFgWFNXVVVhaGdbYGhZbXh1gXZ1goeIlot1k6yxtKaOkaWhq7KonKCZoaCjoKWuqqmurK6ztrO7tbTAvru/vb68vbW6vLGqsLOfm5yal5KKhoyBeHt2dXBnbmljVlJWUEBBPDw9Mi4zKRwhIBYaGRQcHBURGB0XFxwhGxocJSstMjg6PTc6PUxVV1lWV2JqaXN0coCHhIyPjpOenqWppK6xu72yxMu9us7Pw83Wy9nY29ve6OPr6uvs6ezu6ejk6erm3uPj3dbT1sjBzdDFuMHAt7m1r7W6qaCupJOTkpWPgHqAd3JrbGlnY1peX1hTUk9PTFRKR0RFQkRBRUVEQkdBPjs9Pzo6NT04Njs+PTxAPzo/Ojk6PEA5PUJAQD04PkRCREZLUk1KT1BRUVdXU1VRV1tZV1xgXltcXF9hXl9eY2VmZmlna3J0b3F3eHyBfX+JgIWJiouTlZCTmpybnqSgnqyrqrO3srK2uL2/u7jAwMLFxsfEv8XLzcrIy83JzcrP0s3M0dTP0drY1dPR1dzc19za19XX2dnU1NjU0dXPzdHQy8rMysfGxMLBvLu3ta+sraeioJ2YlI+MioeFfX55cnJsaWVjXVlbVE5RTktHRUVAPDw3NC8uLyknKSIiJiUdHiEeGx4eHRwZHB8cHiAfHh8eHSEhISMoJyMnKisrLCszNy8yOTg9QEJFRUVITVFOTlJVWltaXmNfX2ZqZ21xb3R3eHqAhoeJkZKTlZmhpJ6kqKeur6yxtLW1trW4t6+us7axrbK2tLa6ury7u7u9u7vCwb+/vr7Ev7y9v8G8vby6vru4uLq+tri8ubi5t7W4uLW5uLKxs7G0tLGwt7Wvs7avr7O0tLW4trS4uLO1trW1trm1tLm0r7Kyr66wramsqaKlp52bmpeWl5KQkImEhIB8fXh3eHJrbW5mYGNcWFhUUE1LRENDQUI9ODcxLy8vMCsqLCgoKCgpKScoKCYoKygpKyssLi0sLi0uMDIwMTIuLzQ0Njg4Njc8ODlBQ0A/RUdGSU5RUVFUV1pdXWFjZGdpbG1vcXJ2eXh6fICAgIWIio2OkJGSlJWanJqbnZ2cn6Kkp6enq62srbCysrO1uLy4uL+/vL7CwMHAvb/Cvbq9vLm5uba2t7Sysq+urqyqqaalpqShoJ+enZuamZqXlZWTkpGSkpCNjpCMioqLioiHhoeGhYSGg4GDhoKDg4GBg4GBgoGBgoOChISChISChIWDg4WEgoSEgYODgYGCgYGAgICAgX99f398fX18e3p6e3t7enp7fHx4e3x6e3x7fHx9fX59fn1+fX19fH19fnx9fn19fX18fHx7fHx6fH18fXx8fHx7fH1+fXx+f319fn19fn1+gH9+f4B/fn+AgICAgH+AgICAgIGAgICAgH9+f4B+f35+fn58e3t8e3p5eXh4d3Z1dHRzcXBvb21sbmxqaWhlZmVjYmFfX2BfXV1cXFxaWVlaWVlYV1hYV1hYWVhZWFlaWllbXFpbXV5fX15fYWJhYmNiYWJhYWJjZGVmZ2hqbG1ub3Fxc3V3dnd6e3t8e3x+f3+AgICAgoGBgoKDhISFh4aHiYqKi4uMjYyOj4+QkZKUlZWXmJmbm52enqCioqSlpqeoqaqrrK2ur7CxsrGys7O0tbW2tba3t7i3uLe4t7a3t7i3tre2tba1tLSzsrKysbCvrq2sq6qop6alo6OioJ+dnJqZmJeWlJKSkI+OjoyLioiIh4WEg4GBgH9+fXt6eXh3d3V0c3JxcG9ubWxsamppaWhnZmVlZGRjYmNiYWBhYGBfYF9fXl5fXl1dXVxdXF1dXF1cXF1cXF1dXV5dXV5fXl9eX19gYGFgYWJhYmFiY2NiY2RjZGNkZWRlZGVmZmVmZmVmZ2dmZ2hnaGhnaGloZ2hpaWhpamlqaWpqa2pra2xtbGxtbm1ubm5vcG9wcXBxcnFycnN0c3N0dXV2d3d4eHh5ent6e3x9fn5/f4CAgIGCg4SEhYaGh4iIiYqLi4uMjY2Oj5CQkZGSk5OUlJWWlpeYl5iZmZqbm5ybnJ2cnZ6en56fn6ChoKChoqGio6KjpKOko6SjpKWkpaSkpKSlpKWkpaSlpKSlpKOkpKOko6KioaKhoaCfoJ+enp2dnJybmpmZmJeXlpWUk5STkZGQj4+OjYyLioqJh4eGhYSEgoKBgIB/fn59fHt7enl5eHd3dnZ1dHRzc3JycXBxcG9vbm5tbWxrbGxraWppaWhpaGdnZ2dmZ2ZlZmVmZWRlZGVkY2RjZGNkZGRkZGRkZGRkZGRjZGRkY2RjZGNkZWRlZGVmZWZmZ2ZnZ2doaWhpaWpra2xsbW5tbm9ub29wcXFycnNzdHV1dXZ2d3d4eXl6enp7fHx9fX5+f4CAgIGAgYGCgoOEhISFhoWGhoeIh4iJiImKiYqLiouLjI2MjI2OjY6Pj46PkI+QkZCRkJGQkZGSkZKRkpGSkZGRkZKRkpKRkpGSkZKRkpGSkZKRkpGSkZCRkZCRkI+Qj5CPkI+Pjo+OjY6Njo2MjYyLjIuMi4qLioqJiomJiImIh4iHh4aHhoaFhoWFhIWEg4SDg4KDgoKBgoGAgYCBgICAgICAf4CAf39+f35/fn1+fX59fHx9fH18e3x7fHt6e3p7ent6e3p5enl6enl6eXp5eXl4eXh5eHl4eXh5eHl4eXh5eHh3eHh4d3h4d3h3d3h4d3l4eHd4d3h3eHd4d3h3eHh4eXh5eHl4eHl4eXh5enl6eXp5enl6eXp5ent6ent6e3x7fHx9fH18fX19fn1+fX5/fn9+f4B/gH+Af4CAgICAgIGAgYCBgoGCgYKCgoKDgoOEg4OEg4SFhIWEhYSFhoWGhYaHhoeHhoeGh4iHiIiHiImIiImKiYqJiYqJiouKi4qLiouKi4qLiouKi4qLiouKi4qLi4qLiouKi4qLiomJiomIiYiJiImIh4iIh4iHhoeGhYWGhYaFhIWEg4OEg4KDgoOCgYKBgIGAgICAgH+Af39+f359fn18fX19fHx8e3t6e3p7enl6eXp5enl6enl5eXh5eHh5eHl4eXh5eHl4eHd5eHd3eHl4d3h3eHd4d3h3eHh4d3h4d3h3d3h5eHl4eXh5eHl5eXp5enl6eXp7ent6e3p7e3t7fHt8e3x8fHx9fH1+fX59fn9+f35/gH+AgICAgICAgYGAgYKBgoGCgoKDgoOEg4SEhIWFhIWFhoWGhYaGhoaHhoeGh4aHhoeIh4iHiIeHiIeIh4iHiIeIiIiHiIeIh4iHiIiHiIeIh4iHiIeIh4eIh4eIh4aHh4aHhoeGh4aHhoWGhYaFhoWFhIWEhYSFhIWEhISDhIOEg4OCg4OCg4KDgYKCgYKCgYCBgIGAgYCBgICAgICAgICAf4B/f4B/gH+Af35/fn9+f35/fn1+fn19fn1+fX59fn19fX19fH18fXx9fH18fXx9fH18fXx8fHt8e3x7fHt8e3x7fHt8e3x7fHt8e3x7fHt8e3x7fHt8e3x8e3x7fHt8e3x7fHx8fXx9fH18fX5+fX59fn9+f35+f35/gH+Af4B/gICAgICAgICAgICAgYCBgIGAgIGAgYGBgoGCgYKBgoGCgYKBgoGCgoKDgoOCg4KDgoOCg4KDgoOCg4KDgoOCg4KDgoOCg4KDgoOCg4KDgoOCg4KDgoOCg4KDgoOCg4KDgoOCg4KCgoGCgYKBgoGCgYKBgoGCgYKBgoGCgYKBgoGCgYKBgoGCgYKBgoGCgYKBgoGBgYCBgIGAgYCBgIGAgYCBgIGAgYCBgIGAgYCBgIGAgYCAgICBgIGAgYCBgIGAgYCBgIGAgYCBgExJU1RCAAAASU5GT0lDUkQMAAAAMjAwOC0wOS0yMQAASUVORwMAAAAgAAABSVNGVBYAAABTb255IFNvdW5kIEZvcmdlIDguMAAA",
379
- "base64",
380
- ),
381
- });
382
- },
383
- name: "add",
384
- parameters: z.object({
385
- a: z.number(),
386
- b: z.number(),
387
- }),
388
- });
389
-
390
- return server;
391
- },
392
- });
393
- });
394
-
395
- test("handles UserError errors", async () => {
396
- await runWithTestServer({
397
- run: async ({ client }) => {
398
- expect(
399
- await client.callTool({
400
- arguments: {
401
- a: 1,
402
- b: 2,
403
- },
404
- name: "add",
405
- }),
406
- ).toEqual({
407
- content: [{ text: "Something went wrong", type: "text" }],
408
- isError: true,
409
- });
410
- },
411
- server: async () => {
412
- const server = new FastMCP({
413
- name: "Test",
414
- version: "1.0.0",
415
- });
416
-
417
- server.addTool({
418
- description: "Add two numbers",
419
- execute: async () => {
420
- throw new UserError("Something went wrong");
421
- },
422
- name: "add",
423
- parameters: z.object({
424
- a: z.number(),
425
- b: z.number(),
426
- }),
427
- });
428
-
429
- return server;
430
- },
431
- });
432
- });
433
-
434
- test("handles UserError errors with extras", async () => {
435
- await runWithTestServer({
436
- run: async ({ client }) => {
437
- // Should include structuredContent if extras is present
438
- expect(
439
- await client.callTool({
440
- arguments: { a: 1, b: 2 },
441
- name: "add_with_extras",
442
- }),
443
- ).toEqual({
444
- content: [{ text: "Something went wrong", type: "text" }],
445
- isError: true,
446
- structuredContent: { foo: "bar", num: 42 },
447
- });
448
-
449
- // Should NOT include structuredContent if extras is not present
450
- expect(
451
- await client.callTool({
452
- arguments: { a: 1, b: 2 },
453
- name: "add_without_extras",
454
- }),
455
- ).toEqual({
456
- content: [{ text: "Something went wrong", type: "text" }],
457
- isError: true,
458
- });
459
- },
460
- server: async () => {
461
- const server = new FastMCP({
462
- name: "Test",
463
- version: "1.0.0",
464
- });
465
-
466
- server.addTool({
467
- description: "Throws UserError with extras",
468
- execute: async () => {
469
- throw new UserError("Something went wrong", { foo: "bar", num: 42 });
470
- },
471
- name: "add_with_extras",
472
- parameters: z.object({ a: z.number(), b: z.number() }),
473
- });
474
-
475
- server.addTool({
476
- description: "Throws UserError without extras",
477
- execute: async () => {
478
- throw new UserError("Something went wrong");
479
- },
480
- name: "add_without_extras",
481
- parameters: z.object({ a: z.number(), b: z.number() }),
482
- });
483
-
484
- return server;
485
- },
486
- });
487
- });
488
-
489
- test("calling an unknown tool throws McpError with MethodNotFound code", async () => {
490
- await runWithTestServer({
491
- run: async ({ client }) => {
492
- try {
493
- await client.callTool({
494
- arguments: {
495
- a: 1,
496
- b: 2,
497
- },
498
- name: "add",
499
- });
500
- } catch (error) {
501
- expect(error).toBeInstanceOf(McpError);
502
-
503
- // @ts-expect-error - we know that error is an McpError
504
- expect(error.code).toBe(ErrorCode.MethodNotFound);
505
- }
506
- },
507
- server: async () => {
508
- const server = new FastMCP({
509
- name: "Test",
510
- version: "1.0.0",
511
- });
512
-
513
- return server;
514
- },
515
- });
516
- });
517
-
518
- test("tracks tool progress", async () => {
519
- await runWithTestServer({
520
- run: async ({ client }) => {
521
- const onProgress = vi.fn();
522
-
523
- await client.callTool(
524
- {
525
- arguments: {
526
- a: 1,
527
- b: 2,
528
- },
529
- name: "add",
530
- },
531
- undefined,
532
- {
533
- onprogress: onProgress,
534
- },
535
- );
536
-
537
- expect(onProgress).toHaveBeenCalledTimes(1);
538
- expect(onProgress).toHaveBeenCalledWith({
539
- progress: 0,
540
- total: 10,
541
- });
542
- },
543
- server: async () => {
544
- const server = new FastMCP({
545
- name: "Test",
546
- version: "1.0.0",
547
- });
548
-
549
- server.addTool({
550
- description: "Add two numbers",
551
- execute: async (args, { reportProgress }) => {
552
- reportProgress({
553
- progress: 0,
554
- total: 10,
555
- });
556
-
557
- await delay(100);
558
-
559
- return String(args.a + args.b);
560
- },
561
- name: "add",
562
- parameters: z.object({
563
- a: z.number(),
564
- b: z.number(),
565
- }),
566
- });
567
-
568
- return server;
569
- },
570
- });
571
- });
572
-
573
- test(
574
- "reports multiple progress updates without buffering",
575
- {
576
- // Earlier this test was flaky because the last progress update was not reported.
577
- // We are now running it 10 times to make sure that future updates do not regress.
578
- repeats: 10,
579
- },
580
- async () => {
581
- await runWithTestServer({
582
- run: async ({ client }) => {
583
- const progressCalls: Array<{ progress: number; total: number }> = [];
584
-
585
- const onProgress = vi.fn((data) => {
586
- progressCalls.push(data);
587
- });
588
-
589
- await client.callTool(
590
- {
591
- arguments: {
592
- steps: 3,
593
- },
594
- name: "progress-test",
595
- },
596
- undefined,
597
- {
598
- onprogress: onProgress,
599
- },
600
- );
601
-
602
- expect(onProgress).toHaveBeenCalledTimes(4);
603
-
604
- expect(progressCalls).toEqual([
605
- { progress: 0, total: 100 },
606
- { progress: 50, total: 100 },
607
- { progress: 90, total: 100 },
608
- { progress: 100, total: 100 }, // This was previously lost due to buffering
609
- ]);
610
- },
611
- server: async () => {
612
- const server = new FastMCP({
613
- name: "Test",
614
- version: "1.0.0",
615
- });
616
-
617
- server.addTool({
618
- description: "Test tool for progress buffering fix",
619
- execute: async (args, { reportProgress }) => {
620
- const { steps } = args;
621
-
622
- // Initial
623
- await reportProgress({ progress: 0, total: 100 });
624
-
625
- for (let i = 1; i <= steps; i++) {
626
- await delay(50); // Small delay to simulate work
627
-
628
- if (i === 1) {
629
- await reportProgress({ progress: 50, total: 100 });
630
- } else if (i === 2) {
631
- await reportProgress({ progress: 90, total: 100 });
632
- }
633
- }
634
-
635
- // This was the critical test case that failed before the fix
636
- // because there's no await after it, causing it to be buffered
637
- await reportProgress({ progress: 100, total: 100 });
638
-
639
- return "Progress test completed";
640
- },
641
- name: "progress-test",
642
- parameters: z.object({
643
- steps: z.number(),
644
- }),
645
- });
646
-
647
- return server;
648
- },
649
- });
650
- },
651
- );
652
-
653
- test("sets logging levels", async () => {
654
- await runWithTestServer({
655
- run: async ({ client, session }) => {
656
- await client.setLoggingLevel("debug");
657
-
658
- expect(session.loggingLevel).toBe("debug");
659
-
660
- await client.setLoggingLevel("info");
661
-
662
- expect(session.loggingLevel).toBe("info");
663
- },
664
- });
665
- });
666
-
667
- test("handles tool timeout", async () => {
668
- await runWithTestServer({
669
- run: async ({ client }) => {
670
- const result = await client.callTool({
671
- arguments: {
672
- a: 1500,
673
- b: 2,
674
- },
675
- name: "add",
676
- });
677
-
678
- expect(result.isError).toBe(true);
679
-
680
- const result_typed = result as ContentResult;
681
-
682
- expect(Array.isArray(result_typed.content)).toBe(true);
683
- expect(result_typed.content.length).toBe(1);
684
-
685
- const firstItem = result_typed.content[0] as TextContent;
686
-
687
- expect(firstItem.type).toBe("text");
688
- expect(firstItem.text).toBeDefined();
689
- expect(firstItem.text).toContain("timed out");
690
- },
691
- server: async () => {
692
- const server = new FastMCP({
693
- name: "Test",
694
- version: "1.0.0",
695
- });
696
-
697
- server.addTool({
698
- description: "Add two numbers with potential timeout",
699
- execute: async (args) => {
700
- console.log(`Adding ${args.a} and ${args.b}`);
701
-
702
- if (args.a > 1000 || args.b > 1000) {
703
- await new Promise((resolve) => setTimeout(resolve, 3000));
704
- }
705
-
706
- return String(args.a + args.b);
707
- },
708
- name: "add",
709
- parameters: z.object({
710
- a: z.number(),
711
- b: z.number(),
712
- }),
713
- timeoutMs: 1000,
714
- });
715
-
716
- return server;
717
- },
718
- });
719
- });
720
-
721
- test("sends logging messages to the client", async () => {
722
- await runWithTestServer({
723
- run: async ({ client }) => {
724
- const onLog = vi.fn();
725
-
726
- client.setNotificationHandler(
727
- LoggingMessageNotificationSchema,
728
- (message) => {
729
- if (message.method === "notifications/message") {
730
- onLog({
731
- level: message.params.level,
732
- ...(message.params.data ?? {}),
733
- });
734
- }
735
- },
736
- );
737
-
738
- await client.callTool({
739
- arguments: {
740
- a: 1,
741
- b: 2,
742
- },
743
- name: "add",
744
- });
745
-
746
- expect(onLog).toHaveBeenCalledTimes(4);
747
- expect(onLog).toHaveBeenNthCalledWith(1, {
748
- context: {
749
- foo: "bar",
750
- },
751
- level: "debug",
752
- message: "debug message",
753
- });
754
- expect(onLog).toHaveBeenNthCalledWith(2, {
755
- level: "error",
756
- message: "error message",
757
- });
758
- expect(onLog).toHaveBeenNthCalledWith(3, {
759
- level: "info",
760
- message: "info message",
761
- });
762
- expect(onLog).toHaveBeenNthCalledWith(4, {
763
- level: "warning",
764
- message: "warn message",
765
- });
766
- },
767
- server: async () => {
768
- const server = new FastMCP({
769
- name: "Test",
770
- version: "1.0.0",
771
- });
772
-
773
- server.addTool({
774
- description: "Add two numbers",
775
- execute: async (args, { log }) => {
776
- log.debug("debug message", {
777
- foo: "bar",
778
- });
779
- log.error("error message");
780
- log.info("info message");
781
- log.warn("warn message");
782
-
783
- return String(args.a + args.b);
784
- },
785
- name: "add",
786
- parameters: z.object({
787
- a: z.number(),
788
- b: z.number(),
789
- }),
790
- });
791
-
792
- return server;
793
- },
794
- });
795
- });
796
-
797
- test("adds resources", async () => {
798
- await runWithTestServer({
799
- run: async ({ client }) => {
800
- expect(await client.listResources()).toEqual({
801
- resources: [
802
- {
803
- mimeType: "text/plain",
804
- name: "Application Logs",
805
- uri: "file:///logs/app.log",
806
- },
807
- ],
808
- });
809
- },
810
- server: async () => {
811
- const server = new FastMCP({
812
- name: "Test",
813
- version: "1.0.0",
814
- });
815
-
816
- server.addResource({
817
- async load() {
818
- return {
819
- text: "Example log content",
820
- };
821
- },
822
- mimeType: "text/plain",
823
- name: "Application Logs",
824
- uri: "file:///logs/app.log",
825
- });
826
-
827
- return server;
828
- },
829
- });
830
- });
831
-
832
- test("clients reads a resource", async () => {
833
- await runWithTestServer({
834
- run: async ({ client }) => {
835
- expect(
836
- await client.readResource({
837
- uri: "file:///logs/app.log",
838
- }),
839
- ).toEqual({
840
- contents: [
841
- {
842
- mimeType: "text/plain",
843
- name: "Application Logs",
844
- text: "Example log content",
845
- uri: "file:///logs/app.log",
846
- },
847
- ],
848
- });
849
- },
850
- server: async () => {
851
- const server = new FastMCP({
852
- name: "Test",
853
- version: "1.0.0",
854
- });
855
-
856
- server.addResource({
857
- async load() {
858
- return {
859
- text: "Example log content",
860
- };
861
- },
862
- mimeType: "text/plain",
863
- name: "Application Logs",
864
- uri: "file:///logs/app.log",
865
- });
866
-
867
- return server;
868
- },
869
- });
870
- });
871
-
872
- test("clients reads a resource that returns multiple resources", async () => {
873
- await runWithTestServer({
874
- run: async ({ client }) => {
875
- expect(
876
- await client.readResource({
877
- uri: "file:///logs/app.log",
878
- }),
879
- ).toEqual({
880
- contents: [
881
- {
882
- mimeType: "text/plain",
883
- name: "Application Logs",
884
- text: "a",
885
- uri: "file:///logs/app.log",
886
- },
887
- {
888
- mimeType: "text/plain",
889
- name: "Application Logs",
890
- text: "b",
891
- uri: "file:///logs/app.log",
892
- },
893
- ],
894
- });
895
- },
896
- server: async () => {
897
- const server = new FastMCP({
898
- name: "Test",
899
- version: "1.0.0",
900
- });
901
-
902
- server.addResource({
903
- async load() {
904
- return [
905
- {
906
- text: "a",
907
- },
908
- {
909
- text: "b",
910
- },
911
- ];
912
- },
913
- mimeType: "text/plain",
914
- name: "Application Logs",
915
- uri: "file:///logs/app.log",
916
- });
917
-
918
- return server;
919
- },
920
- });
921
- });
922
-
923
- test("embedded resources work in tools", async () => {
924
- await runWithTestServer({
925
- run: async ({ client }) => {
926
- expect(
927
- await client.callTool({
928
- arguments: {
929
- userId: "123",
930
- },
931
- name: "get_user_profile",
932
- }),
933
- ).toEqual({
934
- content: [
935
- {
936
- resource: {
937
- mimeType: "application/json",
938
- text: '{"id":"123","name":"User","email":"user@example.com"}',
939
- uri: "user://profile/123",
940
- },
941
- type: "resource",
942
- },
943
- ],
944
- });
945
- },
946
-
947
- server: async () => {
948
- const server = new FastMCP({
949
- name: "Test",
950
- version: "1.0.0",
951
- });
952
-
953
- server.addResourceTemplate({
954
- arguments: [
955
- {
956
- name: "userId",
957
- required: true,
958
- },
959
- ],
960
- async load(args) {
961
- return {
962
- text: `{"id":"${args.userId}","name":"User","email":"user@example.com"}`,
963
- };
964
- },
965
- mimeType: "application/json",
966
- name: "User Profile",
967
- uriTemplate: "user://profile/{userId}",
968
- });
969
-
970
- server.addTool({
971
- description: "Get user profile data",
972
- execute: async (args) => {
973
- return {
974
- content: [
975
- {
976
- resource: await server.embedded(
977
- `user://profile/${args.userId}`,
978
- ),
979
- type: "resource",
980
- },
981
- ],
982
- };
983
- },
984
- name: "get_user_profile",
985
- parameters: z.object({
986
- userId: z.string(),
987
- }),
988
- });
989
-
990
- return server;
991
- },
992
- });
993
- });
994
-
995
- test("embedded resources work with direct resources", async () => {
996
- await runWithTestServer({
997
- run: async ({ client }) => {
998
- expect(
999
- await client.callTool({
1000
- arguments: {},
1001
- name: "get_logs",
1002
- }),
1003
- ).toEqual({
1004
- content: [
1005
- {
1006
- resource: {
1007
- mimeType: "text/plain",
1008
- text: "Example log content",
1009
- uri: "file:///logs/app.log",
1010
- },
1011
- type: "resource",
1012
- },
1013
- ],
1014
- });
1015
- },
1016
-
1017
- server: async () => {
1018
- const server = new FastMCP({
1019
- name: "Test",
1020
- version: "1.0.0",
1021
- });
1022
-
1023
- server.addResource({
1024
- async load() {
1025
- return {
1026
- text: "Example log content",
1027
- };
1028
- },
1029
- mimeType: "text/plain",
1030
- name: "Application Logs",
1031
- uri: "file:///logs/app.log",
1032
- });
1033
-
1034
- server.addTool({
1035
- description: "Get application logs",
1036
- execute: async () => {
1037
- return {
1038
- content: [
1039
- {
1040
- resource: await server.embedded("file:///logs/app.log"),
1041
- type: "resource",
1042
- },
1043
- ],
1044
- };
1045
- },
1046
- name: "get_logs",
1047
- parameters: z.object({}),
1048
- });
1049
-
1050
- return server;
1051
- },
1052
- });
1053
- });
1054
-
1055
- test("embedded resources work with URI templates and query parameters", async () => {
1056
- await runWithTestServer({
1057
- run: async ({ client }) => {
1058
- // Test case 1: Simple query parameter extraction
1059
- expect(
1060
- await client.callTool({
1061
- arguments: {
1062
- uri: "ui://search?location=a&q=b",
1063
- },
1064
- name: "get_search_resource",
1065
- }),
1066
- ).toEqual({
1067
- content: [
1068
- {
1069
- resource: {
1070
- mimeType: "application/json",
1071
- text: '{"location":"a","query":"b","type":"search"}',
1072
- uri: "ui://search?location=a&q=b",
1073
- },
1074
- type: "resource",
1075
- },
1076
- ],
1077
- });
1078
-
1079
- // Test case 2: Query parameters with different order
1080
- expect(
1081
- await client.callTool({
1082
- arguments: {
1083
- uri: "ui://search?q=test&location=home",
1084
- },
1085
- name: "get_search_resource",
1086
- }),
1087
- ).toEqual({
1088
- content: [
1089
- {
1090
- resource: {
1091
- mimeType: "application/json",
1092
- text: '{"location":"home","query":"test","type":"search"}',
1093
- uri: "ui://search?q=test&location=home",
1094
- },
1095
- type: "resource",
1096
- },
1097
- ],
1098
- });
1099
-
1100
- // Test case 3: Query parameters with encoded values
1101
- expect(
1102
- await client.callTool({
1103
- arguments: {
1104
- uri: "ui://search?location=new%20york&q=hello%20world",
1105
- },
1106
- name: "get_search_resource",
1107
- }),
1108
- ).toEqual({
1109
- content: [
1110
- {
1111
- resource: {
1112
- mimeType: "application/json",
1113
- text: '{"location":"new york","query":"hello world","type":"search"}',
1114
- uri: "ui://search?location=new%20york&q=hello%20world",
1115
- },
1116
- type: "resource",
1117
- },
1118
- ],
1119
- });
1120
- },
1121
-
1122
- server: async () => {
1123
- const server = new FastMCP({
1124
- name: "Test",
1125
- version: "1.0.0",
1126
- });
1127
-
1128
- server.addResourceTemplate({
1129
- arguments: [
1130
- {
1131
- name: "location",
1132
- required: true,
1133
- },
1134
- {
1135
- name: "q",
1136
- required: true,
1137
- },
1138
- ],
1139
- async load(args) {
1140
- return {
1141
- text: JSON.stringify({
1142
- location: args.location,
1143
- query: args.q,
1144
- type: "search",
1145
- }),
1146
- };
1147
- },
1148
- mimeType: "application/json",
1149
- name: "Search Resource",
1150
- uriTemplate: "ui://search{?location,q}",
1151
- });
1152
-
1153
- server.addTool({
1154
- description:
1155
- "Get search resource data using embedded function with query parameters",
1156
- execute: async (args) => {
1157
- return {
1158
- content: [
1159
- {
1160
- resource: await server.embedded(args.uri),
1161
- type: "resource",
1162
- },
1163
- ],
1164
- };
1165
- },
1166
- name: "get_search_resource",
1167
- parameters: z.object({
1168
- uri: z.string(),
1169
- }),
1170
- });
1171
-
1172
- return server;
1173
- },
1174
- });
1175
- });
1176
-
1177
- test("embedded resources work with complex URI template patterns", async () => {
1178
- await runWithTestServer({
1179
- run: async ({ client }) => {
1180
- // Test case 1: Path and query parameters combined
1181
- expect(
1182
- await client.callTool({
1183
- arguments: {
1184
- uri: "api://users/123?fields=name,email&format=json",
1185
- },
1186
- name: "get_user_data",
1187
- }),
1188
- ).toEqual({
1189
- content: [
1190
- {
1191
- resource: {
1192
- mimeType: "application/json",
1193
- text: '{"userId":"123","fields":["name","email"],"format":"json"}',
1194
- uri: "api://users/123?fields=name,email&format=json",
1195
- },
1196
- type: "resource",
1197
- },
1198
- ],
1199
- });
1200
-
1201
- // Test case 2: Optional query parameters (some missing)
1202
- expect(
1203
- await client.callTool({
1204
- arguments: {
1205
- uri: "api://users/456?format=xml",
1206
- },
1207
- name: "get_user_data",
1208
- }),
1209
- ).toEqual({
1210
- content: [
1211
- {
1212
- resource: {
1213
- mimeType: "application/json",
1214
- text: '{"userId":"456","format":"xml"}',
1215
- uri: "api://users/456?format=xml",
1216
- },
1217
- type: "resource",
1218
- },
1219
- ],
1220
- });
1221
- },
1222
-
1223
- server: async () => {
1224
- const server = new FastMCP({
1225
- name: "Test",
1226
- version: "1.0.0",
1227
- });
1228
-
1229
- server.addResourceTemplate({
1230
- arguments: [
1231
- {
1232
- name: "userId",
1233
- required: true,
1234
- },
1235
- {
1236
- name: "fields",
1237
- required: false,
1238
- },
1239
- {
1240
- name: "format",
1241
- required: false,
1242
- },
1243
- ],
1244
- async load(args) {
1245
- const result: Record<string, string> = {
1246
- userId: args.userId,
1247
- };
1248
- if (args.fields) {
1249
- result.fields = args.fields;
1250
- }
1251
- if (args.format) {
1252
- result.format = args.format;
1253
- }
1254
- return {
1255
- text: JSON.stringify(result),
1256
- };
1257
- },
1258
- mimeType: "application/json",
1259
- name: "User Data API",
1260
- uriTemplate: "api://users/{userId}{?fields,format}",
1261
- });
1262
-
1263
- server.addTool({
1264
- description:
1265
- "Get user data using complex URI templates with path and query parameters",
1266
- execute: async (args) => {
1267
- return {
1268
- content: [
1269
- {
1270
- resource: await server.embedded(args.uri),
1271
- type: "resource",
1272
- },
1273
- ],
1274
- };
1275
- },
1276
- name: "get_user_data",
1277
- parameters: z.object({
1278
- uri: z.string(),
1279
- }),
1280
- });
1281
-
1282
- return server;
1283
- },
1284
- });
1285
- });
1286
-
1287
- test("adds prompts", async () => {
1288
- await runWithTestServer({
1289
- run: async ({ client }) => {
1290
- expect(
1291
- await client.getPrompt({
1292
- arguments: {
1293
- changes: "foo",
1294
- },
1295
- name: "git-commit",
1296
- }),
1297
- ).toEqual({
1298
- description: "Generate a Git commit message",
1299
- messages: [
1300
- {
1301
- content: {
1302
- text: "Generate a concise but descriptive commit message for these changes:\n\nfoo",
1303
- type: "text",
1304
- },
1305
- role: "user",
1306
- },
1307
- ],
1308
- });
1309
-
1310
- expect(await client.listPrompts()).toEqual({
1311
- prompts: [
1312
- {
1313
- arguments: [
1314
- {
1315
- description: "Git diff or description of changes",
1316
- name: "changes",
1317
- required: true,
1318
- },
1319
- ],
1320
- description: "Generate a Git commit message",
1321
- name: "git-commit",
1322
- },
1323
- ],
1324
- });
1325
- },
1326
- server: async () => {
1327
- const server = new FastMCP({
1328
- name: "Test",
1329
- version: "1.0.0",
1330
- });
1331
-
1332
- server.addPrompt({
1333
- arguments: [
1334
- {
1335
- description: "Git diff or description of changes",
1336
- name: "changes",
1337
- required: true,
1338
- },
1339
- ],
1340
- description: "Generate a Git commit message",
1341
- load: async (args) => {
1342
- return `Generate a concise but descriptive commit message for these changes:\n\n${args.changes}`;
1343
- },
1344
- name: "git-commit",
1345
- });
1346
-
1347
- return server;
1348
- },
1349
- });
1350
- });
1351
-
1352
- test("uses events to notify server of client connect/disconnect", async () => {
1353
- const port = await getRandomPort();
1354
-
1355
- const server = new FastMCP({
1356
- name: "Test",
1357
- version: "1.0.0",
1358
- });
1359
-
1360
- const onConnect = vi.fn().mockResolvedValue(undefined);
1361
- const onDisconnect = vi.fn().mockResolvedValue(undefined);
1362
-
1363
- server.on("connect", onConnect);
1364
- server.on("disconnect", onDisconnect);
1365
-
1366
- await server.start({
1367
- httpStream: {
1368
- port,
1369
- },
1370
- transportType: "httpStream",
1371
- });
1372
-
1373
- const client = new Client(
1374
- {
1375
- name: "example-client",
1376
- version: "1.0.0",
1377
- },
1378
- {
1379
- capabilities: {},
1380
- },
1381
- );
1382
-
1383
- const transport = new SSEClientTransport(
1384
- new URL(`http://localhost:${port}/sse`),
1385
- );
1386
-
1387
- await client.connect(transport);
1388
-
1389
- await delay(100);
1390
-
1391
- expect(onConnect).toHaveBeenCalledTimes(1);
1392
- expect(onDisconnect).toHaveBeenCalledTimes(0);
1393
-
1394
- expect(server.sessions).toEqual([expect.any(FastMCPSession)]);
1395
-
1396
- await client.close();
1397
-
1398
- await delay(100);
1399
-
1400
- expect(onConnect).toHaveBeenCalledTimes(1);
1401
- expect(onDisconnect).toHaveBeenCalledTimes(1);
1402
-
1403
- await server.stop();
1404
- });
1405
-
1406
- test("handles multiple clients", async () => {
1407
- const port = await getRandomPort();
1408
-
1409
- const server = new FastMCP({
1410
- name: "Test",
1411
- version: "1.0.0",
1412
- });
1413
-
1414
- await server.start({
1415
- httpStream: {
1416
- port,
1417
- },
1418
- transportType: "httpStream",
1419
- });
1420
-
1421
- const client1 = new Client(
1422
- {
1423
- name: "example-client",
1424
- version: "1.0.0",
1425
- },
1426
- {
1427
- capabilities: {},
1428
- },
1429
- );
1430
-
1431
- const transport1 = new SSEClientTransport(
1432
- new URL(`http://localhost:${port}/sse`),
1433
- );
1434
-
1435
- await client1.connect(transport1);
1436
-
1437
- const client2 = new Client(
1438
- {
1439
- name: "example-client",
1440
- version: "1.0.0",
1441
- },
1442
- {
1443
- capabilities: {},
1444
- },
1445
- );
1446
-
1447
- const transport2 = new SSEClientTransport(
1448
- new URL(`http://localhost:${port}/sse`),
1449
- );
1450
-
1451
- await client2.connect(transport2);
1452
-
1453
- await delay(100);
1454
-
1455
- expect(server.sessions).toEqual([
1456
- expect.any(FastMCPSession),
1457
- expect.any(FastMCPSession),
1458
- ]);
1459
-
1460
- await server.stop();
1461
- });
1462
-
1463
- test("session knows about client capabilities", async () => {
1464
- await runWithTestServer({
1465
- client: async () => {
1466
- const client = new Client(
1467
- {
1468
- name: "example-client",
1469
- version: "1.0.0",
1470
- },
1471
- {
1472
- capabilities: {
1473
- roots: {
1474
- listChanged: true,
1475
- },
1476
- },
1477
- },
1478
- );
1479
-
1480
- client.setRequestHandler(ListRootsRequestSchema, () => {
1481
- return {
1482
- roots: [
1483
- {
1484
- name: "Frontend Repository",
1485
- uri: "file:///home/user/projects/frontend",
1486
- },
1487
- ],
1488
- };
1489
- });
1490
-
1491
- return client;
1492
- },
1493
- run: async ({ session }) => {
1494
- expect(session.clientCapabilities).toEqual({
1495
- roots: {
1496
- listChanged: true,
1497
- },
1498
- });
1499
- },
1500
- });
1501
- });
1502
-
1503
- test("session knows about roots", async () => {
1504
- await runWithTestServer({
1505
- client: async () => {
1506
- const client = new Client(
1507
- {
1508
- name: "example-client",
1509
- version: "1.0.0",
1510
- },
1511
- {
1512
- capabilities: {
1513
- roots: {
1514
- listChanged: true,
1515
- },
1516
- },
1517
- },
1518
- );
1519
-
1520
- client.setRequestHandler(ListRootsRequestSchema, () => {
1521
- return {
1522
- roots: [
1523
- {
1524
- name: "Frontend Repository",
1525
- uri: "file:///home/user/projects/frontend",
1526
- },
1527
- ],
1528
- };
1529
- });
1530
-
1531
- return client;
1532
- },
1533
- run: async ({ session }) => {
1534
- expect(session.roots).toEqual([
1535
- {
1536
- name: "Frontend Repository",
1537
- uri: "file:///home/user/projects/frontend",
1538
- },
1539
- ]);
1540
- },
1541
- });
1542
- });
1543
-
1544
- test("session listens to roots changes", async () => {
1545
- const clientRoots: Root[] = [
1546
- {
1547
- name: "Frontend Repository",
1548
- uri: "file:///home/user/projects/frontend",
1549
- },
1550
- ];
1551
-
1552
- await runWithTestServer({
1553
- client: async () => {
1554
- const client = new Client(
1555
- {
1556
- name: "example-client",
1557
- version: "1.0.0",
1558
- },
1559
- {
1560
- capabilities: {
1561
- roots: {
1562
- listChanged: true,
1563
- },
1564
- },
1565
- },
1566
- );
1567
-
1568
- client.setRequestHandler(ListRootsRequestSchema, () => {
1569
- return {
1570
- roots: clientRoots,
1571
- };
1572
- });
1573
-
1574
- return client;
1575
- },
1576
- run: async ({ client, session }) => {
1577
- expect(session.roots).toEqual([
1578
- {
1579
- name: "Frontend Repository",
1580
- uri: "file:///home/user/projects/frontend",
1581
- },
1582
- ]);
1583
-
1584
- clientRoots.push({
1585
- name: "Backend Repository",
1586
- uri: "file:///home/user/projects/backend",
1587
- });
1588
-
1589
- await client.sendRootsListChanged();
1590
-
1591
- const onRootsChanged = vi.fn();
1592
-
1593
- session.on("rootsChanged", onRootsChanged);
1594
-
1595
- await delay(100);
1596
-
1597
- expect(session.roots).toEqual([
1598
- {
1599
- name: "Frontend Repository",
1600
- uri: "file:///home/user/projects/frontend",
1601
- },
1602
- {
1603
- name: "Backend Repository",
1604
- uri: "file:///home/user/projects/backend",
1605
- },
1606
- ]);
1607
-
1608
- expect(onRootsChanged).toHaveBeenCalledTimes(1);
1609
- expect(onRootsChanged).toHaveBeenCalledWith({
1610
- roots: [
1611
- {
1612
- name: "Frontend Repository",
1613
- uri: "file:///home/user/projects/frontend",
1614
- },
1615
- {
1616
- name: "Backend Repository",
1617
- uri: "file:///home/user/projects/backend",
1618
- },
1619
- ],
1620
- });
1621
- },
1622
- });
1623
- });
1624
-
1625
- test("session sends pings to the client", async () => {
1626
- await runWithTestServer({
1627
- run: async ({ client }) => {
1628
- const onPing = vi.fn().mockReturnValue({});
1629
-
1630
- client.setRequestHandler(PingRequestSchema, onPing);
1631
-
1632
- await delay(2000);
1633
-
1634
- expect(onPing.mock.calls.length).toBeGreaterThanOrEqual(1);
1635
- expect(onPing.mock.calls.length).toBeLessThanOrEqual(3);
1636
- },
1637
- server: async () => {
1638
- const server = new FastMCP({
1639
- name: "Test",
1640
- ping: {
1641
- enabled: true,
1642
- intervalMs: 1000,
1643
- },
1644
- version: "1.0.0",
1645
- });
1646
- return server;
1647
- },
1648
- });
1649
- });
1650
-
1651
- test("completes prompt arguments", async () => {
1652
- await runWithTestServer({
1653
- run: async ({ client }) => {
1654
- const response = await client.complete({
1655
- argument: {
1656
- name: "name",
1657
- value: "Germ",
1658
- },
1659
- ref: {
1660
- name: "countryPoem",
1661
- type: "ref/prompt",
1662
- },
1663
- });
1664
-
1665
- expect(response).toEqual({
1666
- completion: {
1667
- values: ["Germany"],
1668
- },
1669
- });
1670
- },
1671
- server: async () => {
1672
- const server = new FastMCP({
1673
- name: "Test",
1674
- version: "1.0.0",
1675
- });
1676
-
1677
- server.addPrompt({
1678
- arguments: [
1679
- {
1680
- complete: async (value) => {
1681
- if (value === "Germ") {
1682
- return {
1683
- values: ["Germany"],
1684
- };
1685
- }
1686
-
1687
- return {
1688
- values: [],
1689
- };
1690
- },
1691
- description: "Name of the country",
1692
- name: "name",
1693
- required: true,
1694
- },
1695
- ],
1696
- description: "Writes a poem about a country",
1697
- load: async ({ name }) => {
1698
- return `Hello, ${name}!`;
1699
- },
1700
- name: "countryPoem",
1701
- });
1702
-
1703
- return server;
1704
- },
1705
- });
1706
- });
1707
-
1708
- test("adds automatic prompt argument completion when enum is provided", async () => {
1709
- await runWithTestServer({
1710
- run: async ({ client }) => {
1711
- const response = await client.complete({
1712
- argument: {
1713
- name: "name",
1714
- value: "Germ",
1715
- },
1716
- ref: {
1717
- name: "countryPoem",
1718
- type: "ref/prompt",
1719
- },
1720
- });
1721
-
1722
- expect(response).toEqual({
1723
- completion: {
1724
- total: 1,
1725
- values: ["Germany"],
1726
- },
1727
- });
1728
- },
1729
- server: async () => {
1730
- const server = new FastMCP({
1731
- name: "Test",
1732
- version: "1.0.0",
1733
- });
1734
-
1735
- server.addPrompt({
1736
- arguments: [
1737
- {
1738
- description: "Name of the country",
1739
- enum: ["Germany", "France", "Italy"],
1740
- name: "name",
1741
- required: true,
1742
- },
1743
- ],
1744
- description: "Writes a poem about a country",
1745
- load: async ({ name }) => {
1746
- return `Hello, ${name}!`;
1747
- },
1748
- name: "countryPoem",
1749
- });
1750
-
1751
- return server;
1752
- },
1753
- });
1754
- });
1755
-
1756
- test("completes template resource arguments", async () => {
1757
- await runWithTestServer({
1758
- run: async ({ client }) => {
1759
- const response = await client.complete({
1760
- argument: {
1761
- name: "issueId",
1762
- value: "123",
1763
- },
1764
- ref: {
1765
- type: "ref/resource",
1766
- uri: "issue:///{issueId}",
1767
- },
1768
- });
1769
-
1770
- expect(response).toEqual({
1771
- completion: {
1772
- values: ["123456"],
1773
- },
1774
- });
1775
- },
1776
- server: async () => {
1777
- const server = new FastMCP({
1778
- name: "Test",
1779
- version: "1.0.0",
1780
- });
1781
-
1782
- server.addResourceTemplate({
1783
- arguments: [
1784
- {
1785
- complete: async (value) => {
1786
- if (value === "123") {
1787
- return {
1788
- values: ["123456"],
1789
- };
1790
- }
1791
-
1792
- return {
1793
- values: [],
1794
- };
1795
- },
1796
- description: "ID of the issue",
1797
- name: "issueId",
1798
- },
1799
- ],
1800
- load: async ({ issueId }) => {
1801
- return {
1802
- text: `Issue ${issueId}`,
1803
- };
1804
- },
1805
- mimeType: "text/plain",
1806
- name: "Issue",
1807
- uriTemplate: "issue:///{issueId}",
1808
- });
1809
-
1810
- return server;
1811
- },
1812
- });
1813
- });
1814
-
1815
- test("lists resource templates", async () => {
1816
- await runWithTestServer({
1817
- run: async ({ client }) => {
1818
- expect(await client.listResourceTemplates()).toEqual({
1819
- resourceTemplates: [
1820
- {
1821
- mimeType: "text/plain",
1822
- name: "Application Logs",
1823
- uriTemplate: "file:///logs/{name}.log",
1824
- },
1825
- ],
1826
- });
1827
- },
1828
- server: async () => {
1829
- const server = new FastMCP({
1830
- name: "Test",
1831
- version: "1.0.0",
1832
- });
1833
-
1834
- server.addResourceTemplate({
1835
- arguments: [
1836
- {
1837
- description: "Name of the log",
1838
- name: "name",
1839
- required: true,
1840
- },
1841
- ],
1842
- load: async ({ name }) => {
1843
- return {
1844
- text: `Example log content for ${name}`,
1845
- };
1846
- },
1847
- mimeType: "text/plain",
1848
- name: "Application Logs",
1849
- uriTemplate: "file:///logs/{name}.log",
1850
- });
1851
-
1852
- return server;
1853
- },
1854
- });
1855
- });
1856
-
1857
- test(
1858
- "HTTP Stream: custom endpoint works with /another-mcp",
1859
- { timeout: 20000 },
1860
- async () => {
1861
- const port = await getRandomPort();
1862
-
1863
- // Create server with custom endpoint
1864
- const server = new FastMCP({
1865
- name: "Test",
1866
- version: "1.0.0",
1867
- });
1868
-
1869
- server.addTool({
1870
- description: "Add two numbers",
1871
- execute: async (args) => {
1872
- return String(args.a + args.b);
1873
- },
1874
- name: "add",
1875
- parameters: z.object({
1876
- a: z.number(),
1877
- b: z.number(),
1878
- }),
1879
- });
1880
-
1881
- await server.start({
1882
- httpStream: {
1883
- endpoint: "/another-mcp",
1884
- port,
1885
- },
1886
- transportType: "httpStream",
1887
- });
1888
-
1889
- try {
1890
- // Create client
1891
- const client = new Client(
1892
- {
1893
- name: "example-client",
1894
- version: "1.0.0",
1895
- },
1896
- {
1897
- capabilities: {},
1898
- },
1899
- );
1900
-
1901
- const transport = new StreamableHTTPClientTransport(
1902
- new URL(`http://localhost:${port}/another-mcp`),
1903
- );
1904
-
1905
- // Connect client to server and wait for session to be ready
1906
- const sessionPromise = new Promise<FastMCPSession>((resolve) => {
1907
- server.on("connect", async (event) => {
1908
- await event.session.waitForReady();
1909
- resolve(event.session);
1910
- });
1911
- });
1912
-
1913
- await client.connect(transport);
1914
- await sessionPromise;
1915
-
1916
- // Call tool
1917
- const result = await client.callTool({
1918
- arguments: {
1919
- a: 5,
1920
- b: 7,
1921
- },
1922
- name: "add",
1923
- });
1924
-
1925
- // Check result
1926
- expect(result).toEqual({
1927
- content: [{ text: "12", type: "text" }],
1928
- });
1929
-
1930
- // Clean up connection
1931
- await transport.terminateSession();
1932
- await client.close();
1933
- } finally {
1934
- await server.stop();
1935
- }
1936
- },
1937
- );
1938
-
1939
- test("clients reads a resource accessed via a resource template", async () => {
1940
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
1941
- const loadSpy = vi.fn((_args) => {
1942
- return {
1943
- text: "Example log content",
1944
- };
1945
- });
1946
-
1947
- await runWithTestServer({
1948
- run: async ({ client }) => {
1949
- expect(
1950
- await client.readResource({
1951
- uri: "file:///logs/app.log",
1952
- }),
1953
- ).toEqual({
1954
- contents: [
1955
- {
1956
- mimeType: "text/plain",
1957
- name: "Application Logs",
1958
- text: "Example log content",
1959
- uri: "file:///logs/app.log",
1960
- },
1961
- ],
1962
- });
1963
-
1964
- expect(loadSpy).toHaveBeenCalledWith({
1965
- name: "app",
1966
- });
1967
- },
1968
- server: async () => {
1969
- const server = new FastMCP({
1970
- name: "Test",
1971
- version: "1.0.0",
1972
- });
1973
-
1974
- server.addResourceTemplate({
1975
- arguments: [
1976
- {
1977
- description: "Name of the log",
1978
- name: "name",
1979
- },
1980
- ],
1981
- async load(args) {
1982
- return loadSpy(args);
1983
- },
1984
- mimeType: "text/plain",
1985
- name: "Application Logs",
1986
- uriTemplate: "file:///logs/{name}.log",
1987
- });
1988
-
1989
- return server;
1990
- },
1991
- });
1992
- });
1993
-
1994
- test("makes a sampling request", async () => {
1995
- const onMessageRequest = vi.fn(() => {
1996
- return {
1997
- content: {
1998
- text: "The files are in the current directory.",
1999
- type: "text",
2000
- },
2001
- model: "gpt-3.5-turbo",
2002
- role: "assistant",
2003
- };
2004
- });
2005
-
2006
- await runWithTestServer({
2007
- client: async () => {
2008
- const client = new Client(
2009
- {
2010
- name: "example-client",
2011
- version: "1.0.0",
2012
- },
2013
- {
2014
- capabilities: {
2015
- sampling: {},
2016
- },
2017
- },
2018
- );
2019
- return client;
2020
- },
2021
- run: async ({ client, session }) => {
2022
- client.setRequestHandler(CreateMessageRequestSchema, onMessageRequest);
2023
-
2024
- const response = await session.requestSampling({
2025
- includeContext: "thisServer",
2026
- maxTokens: 100,
2027
- messages: [
2028
- {
2029
- content: {
2030
- text: "What files are in the current directory?",
2031
- type: "text",
2032
- },
2033
- role: "user",
2034
- },
2035
- ],
2036
- systemPrompt: "You are a helpful file system assistant.",
2037
- });
2038
-
2039
- expect(response).toEqual({
2040
- content: {
2041
- text: "The files are in the current directory.",
2042
- type: "text",
2043
- },
2044
- model: "gpt-3.5-turbo",
2045
- role: "assistant",
2046
- });
2047
-
2048
- expect(onMessageRequest).toHaveBeenCalledTimes(1);
2049
- },
2050
- });
2051
- });
2052
-
2053
- test("throws ErrorCode.InvalidParams if tool parameters do not match zod schema", async () => {
2054
- await runWithTestServer({
2055
- run: async ({ client }) => {
2056
- try {
2057
- await client.callTool({
2058
- arguments: {
2059
- a: 1,
2060
- b: "invalid",
2061
- },
2062
- name: "add",
2063
- });
2064
- } catch (error) {
2065
- expect(error).toBeInstanceOf(McpError);
2066
-
2067
- // @ts-expect-error - we know that error is an McpError
2068
- expect(error.code).toBe(ErrorCode.InvalidParams);
2069
-
2070
- // @ts-expect-error - we know that error is an McpError
2071
- expect(error.message).toBe(
2072
- "MCP error -32602: MCP error -32602: Tool 'add' parameter validation failed: b: Expected number, received string. Please check the parameter types and values according to the tool's schema.",
2073
- );
2074
- }
2075
- },
2076
- server: async () => {
2077
- const server = new FastMCP({
2078
- name: "Test",
2079
- version: "1.0.0",
2080
- });
2081
-
2082
- server.addTool({
2083
- description: "Add two numbers",
2084
- execute: async (args) => {
2085
- return String(args.a + args.b);
2086
- },
2087
- name: "add",
2088
- parameters: z.object({
2089
- a: z.number(),
2090
- b: z.number(),
2091
- }),
2092
- });
2093
-
2094
- return server;
2095
- },
2096
- });
2097
- });
2098
-
2099
- test("server remains usable after InvalidParams error", async () => {
2100
- await runWithTestServer({
2101
- run: async ({ client }) => {
2102
- try {
2103
- await client.callTool({
2104
- arguments: {
2105
- a: 1,
2106
- b: "invalid",
2107
- },
2108
- name: "add",
2109
- });
2110
- } catch (error) {
2111
- expect(error).toBeInstanceOf(McpError);
2112
-
2113
- // @ts-expect-error - we know that error is an McpError
2114
- expect(error.code).toBe(ErrorCode.InvalidParams);
2115
-
2116
- // @ts-expect-error - we know that error is an McpError
2117
- expect(error.message).toBe(
2118
- "MCP error -32602: MCP error -32602: Tool 'add' parameter validation failed: b: Expected number, received string. Please check the parameter types and values according to the tool's schema.",
2119
- );
2120
- }
2121
-
2122
- expect(
2123
- await client.callTool({
2124
- arguments: {
2125
- a: 1,
2126
- b: 2,
2127
- },
2128
- name: "add",
2129
- }),
2130
- ).toEqual({
2131
- content: [{ text: "3", type: "text" }],
2132
- });
2133
- },
2134
- server: async () => {
2135
- const server = new FastMCP({
2136
- name: "Test",
2137
- version: "1.0.0",
2138
- });
2139
-
2140
- server.addTool({
2141
- description: "Add two numbers",
2142
- execute: async (args) => {
2143
- return String(args.a + args.b);
2144
- },
2145
- name: "add",
2146
- parameters: z.object({
2147
- a: z.number(),
2148
- b: z.number(),
2149
- }),
2150
- });
2151
-
2152
- return server;
2153
- },
2154
- });
2155
- });
2156
-
2157
- test("allows new clients to connect after a client disconnects", async () => {
2158
- const port = await getRandomPort();
2159
-
2160
- const server = new FastMCP({
2161
- name: "Test",
2162
- version: "1.0.0",
2163
- });
2164
-
2165
- server.addTool({
2166
- description: "Add two numbers",
2167
- execute: async (args) => {
2168
- return String(args.a + args.b);
2169
- },
2170
- name: "add",
2171
- parameters: z.object({
2172
- a: z.number(),
2173
- b: z.number(),
2174
- }),
2175
- });
2176
-
2177
- await server.start({
2178
- httpStream: {
2179
- port,
2180
- },
2181
- transportType: "httpStream",
2182
- });
2183
-
2184
- const client1 = new Client(
2185
- {
2186
- name: "example-client",
2187
- version: "1.0.0",
2188
- },
2189
- {
2190
- capabilities: {},
2191
- },
2192
- );
2193
-
2194
- const transport1 = new SSEClientTransport(
2195
- new URL(`http://localhost:${port}/sse`),
2196
- );
2197
-
2198
- await client1.connect(transport1);
2199
-
2200
- expect(
2201
- await client1.callTool({
2202
- arguments: {
2203
- a: 1,
2204
- b: 2,
2205
- },
2206
- name: "add",
2207
- }),
2208
- ).toEqual({
2209
- content: [{ text: "3", type: "text" }],
2210
- });
2211
-
2212
- await client1.close();
2213
-
2214
- const client2 = new Client(
2215
- {
2216
- name: "example-client",
2217
- version: "1.0.0",
2218
- },
2219
- {
2220
- capabilities: {},
2221
- },
2222
- );
2223
-
2224
- const transport2 = new SSEClientTransport(
2225
- new URL(`http://localhost:${port}/sse`),
2226
- );
2227
-
2228
- await client2.connect(transport2);
2229
-
2230
- expect(
2231
- await client2.callTool({
2232
- arguments: {
2233
- a: 1,
2234
- b: 2,
2235
- },
2236
- name: "add",
2237
- }),
2238
- ).toEqual({
2239
- content: [{ text: "3", type: "text" }],
2240
- });
2241
-
2242
- await client2.close();
2243
-
2244
- await server.stop();
2245
- });
2246
-
2247
- test("able to close server immediately after starting it", async () => {
2248
- const port = await getRandomPort();
2249
-
2250
- const server = new FastMCP({
2251
- name: "Test",
2252
- version: "1.0.0",
2253
- });
2254
-
2255
- await server.start({
2256
- httpStream: {
2257
- port,
2258
- },
2259
- transportType: "httpStream",
2260
- });
2261
-
2262
- // We were previously not waiting for the server to start.
2263
- // Therefore, this would have caused error 'Server is not running.'.
2264
- await server.stop();
2265
- });
2266
-
2267
- test("closing event source does not produce error", async () => {
2268
- const port = await getRandomPort();
2269
-
2270
- const server = new FastMCP({
2271
- name: "Test",
2272
- version: "1.0.0",
2273
- });
2274
-
2275
- server.addTool({
2276
- description: "Add two numbers",
2277
- execute: async (args) => {
2278
- return String(args.a + args.b);
2279
- },
2280
- name: "add",
2281
- parameters: z.object({
2282
- a: z.number(),
2283
- b: z.number(),
2284
- }),
2285
- });
2286
-
2287
- await server.start({
2288
- httpStream: {
2289
- host: "127.0.0.1",
2290
- port,
2291
- },
2292
- transportType: "httpStream",
2293
- });
2294
-
2295
- const eventSource = await new Promise<EventSourceClient>((onMessage) => {
2296
- const eventSource = createEventSource({
2297
- onConnect: () => {
2298
- console.info("connected");
2299
- },
2300
- onDisconnect: () => {
2301
- console.info("disconnected");
2302
- },
2303
- onMessage: () => {
2304
- onMessage(eventSource);
2305
- },
2306
- url: `http://127.0.0.1:${port}/sse`,
2307
- });
2308
- });
2309
-
2310
- expect(eventSource.readyState).toBe("open");
2311
-
2312
- eventSource.close();
2313
-
2314
- // We were getting unhandled error 'Not connected'
2315
- // https://github.com/punkpeye/mcp-proxy/commit/62cf27d5e3dfcbc353e8d03c7714a62c37177b52
2316
- await delay(1000);
2317
-
2318
- await server.stop();
2319
- });
2320
-
2321
- test("provides auth to tools", async () => {
2322
- const port = await getRandomPort();
2323
-
2324
- const authenticate = vi.fn(async () => {
2325
- return {
2326
- id: 1,
2327
- };
2328
- });
2329
-
2330
- const server = new FastMCP<{ id: number }>({
2331
- authenticate,
2332
- name: "Test",
2333
- version: "1.0.0",
2334
- });
2335
-
2336
- const execute = vi.fn(async (args) => {
2337
- return String(args.a + args.b);
2338
- });
2339
-
2340
- server.addTool({
2341
- description: "Add two numbers",
2342
- execute,
2343
- name: "add",
2344
- parameters: z.object({
2345
- a: z.number(),
2346
- b: z.number(),
2347
- }),
2348
- });
2349
-
2350
- await server.start({
2351
- httpStream: {
2352
- port,
2353
- },
2354
- transportType: "httpStream",
2355
- });
2356
-
2357
- const client = new Client(
2358
- {
2359
- name: "example-client",
2360
- version: "1.0.0",
2361
- },
2362
- {
2363
- capabilities: {},
2364
- },
2365
- );
2366
-
2367
- const transport = new SSEClientTransport(
2368
- new URL(`http://localhost:${port}/sse`),
2369
- {
2370
- eventSourceInit: {
2371
- fetch: async (url, init) => {
2372
- return fetch(url, {
2373
- ...init,
2374
- headers: {
2375
- ...init?.headers,
2376
- "x-api-key": "123",
2377
- },
2378
- });
2379
- },
2380
- },
2381
- },
2382
- );
2383
-
2384
- await client.connect(transport);
2385
-
2386
- expect(
2387
- authenticate,
2388
- "authenticate should have been called",
2389
- ).toHaveBeenCalledTimes(1);
2390
-
2391
- expect(
2392
- await client.callTool({
2393
- arguments: {
2394
- a: 1,
2395
- b: 2,
2396
- },
2397
- name: "add",
2398
- }),
2399
- ).toEqual({
2400
- content: [{ text: "3", type: "text" }],
2401
- });
2402
-
2403
- expect(execute, "execute should have been called").toHaveBeenCalledTimes(1);
2404
-
2405
- expect(execute).toHaveBeenCalledWith(
2406
- {
2407
- a: 1,
2408
- b: 2,
2409
- },
2410
- {
2411
- client: expect.any(Object),
2412
- log: {
2413
- debug: expect.any(Function),
2414
- error: expect.any(Function),
2415
- info: expect.any(Function),
2416
- warn: expect.any(Function),
2417
- },
2418
- reportProgress: expect.any(Function),
2419
- requestId: undefined,
2420
- session: { id: 1 },
2421
- sessionId: expect.any(String),
2422
- streamContent: expect.any(Function),
2423
- },
2424
- );
2425
- });
2426
-
2427
- test("provides auth to resources", async () => {
2428
- const port = await getRandomPort();
2429
-
2430
- const authenticate = vi.fn(async () => {
2431
- return {
2432
- role: "admin",
2433
- userId: 42,
2434
- };
2435
- });
2436
-
2437
- const server = new FastMCP<{ role: string; userId: number }>({
2438
- authenticate,
2439
- name: "Test",
2440
- version: "1.0.0",
2441
- });
2442
-
2443
- const resourceLoad = vi.fn(async (auth) => {
2444
- return {
2445
- text: `User ${auth?.userId} with role ${auth?.role} loaded this resource`,
2446
- };
2447
- });
2448
-
2449
- server.addResource({
2450
- load: resourceLoad,
2451
- mimeType: "text/plain",
2452
- name: "Auth Resource",
2453
- uri: "auth://resource",
2454
- });
2455
-
2456
- await server.start({
2457
- httpStream: {
2458
- port,
2459
- },
2460
- transportType: "httpStream",
2461
- });
2462
-
2463
- const client = new Client(
2464
- {
2465
- name: "example-client",
2466
- version: "1.0.0",
2467
- },
2468
- {
2469
- capabilities: {},
2470
- },
2471
- );
2472
-
2473
- const transport = new SSEClientTransport(
2474
- new URL(`http://localhost:${port}/sse`),
2475
- {
2476
- eventSourceInit: {
2477
- fetch: async (url, init) => {
2478
- return fetch(url, {
2479
- ...init,
2480
- headers: {
2481
- ...init?.headers,
2482
- "x-api-key": "123",
2483
- },
2484
- });
2485
- },
2486
- },
2487
- },
2488
- );
2489
-
2490
- await client.connect(transport);
2491
-
2492
- const result = await client.readResource({
2493
- uri: "auth://resource",
2494
- });
2495
-
2496
- expect(resourceLoad).toHaveBeenCalledTimes(1);
2497
- expect(resourceLoad).toHaveBeenCalledWith({
2498
- role: "admin",
2499
- userId: 42,
2500
- });
2501
-
2502
- expect(result).toEqual({
2503
- contents: [
2504
- {
2505
- mimeType: "text/plain",
2506
- name: "Auth Resource",
2507
- text: "User 42 with role admin loaded this resource",
2508
- uri: "auth://resource",
2509
- },
2510
- ],
2511
- });
2512
- });
2513
-
2514
- test("provides auth to resource templates", async () => {
2515
- const port = await getRandomPort();
2516
-
2517
- const authenticate = vi.fn(async () => {
2518
- return {
2519
- permissions: ["read", "write"],
2520
- userId: 99,
2521
- };
2522
- });
2523
-
2524
- const server = new FastMCP<{ permissions: string[]; userId: number }>({
2525
- authenticate,
2526
- name: "Test",
2527
- version: "1.0.0",
2528
- });
2529
-
2530
- const templateLoad = vi.fn(async (args, auth) => {
2531
- return {
2532
- text: `Resource ${args.resourceId} accessed by user ${auth?.userId} with permissions: ${auth?.permissions?.join(", ")}`,
2533
- };
2534
- });
2535
-
2536
- server.addResourceTemplate({
2537
- arguments: [
2538
- {
2539
- name: "resourceId",
2540
- required: true,
2541
- },
2542
- ],
2543
- load: templateLoad,
2544
- mimeType: "text/plain",
2545
- name: "Auth Template",
2546
- uriTemplate: "auth://template/{resourceId}",
2547
- });
2548
-
2549
- await server.start({
2550
- httpStream: {
2551
- port,
2552
- },
2553
- transportType: "httpStream",
2554
- });
2555
-
2556
- const client = new Client(
2557
- {
2558
- name: "example-client",
2559
- version: "1.0.0",
2560
- },
2561
- {
2562
- capabilities: {},
2563
- },
2564
- );
2565
-
2566
- const transport = new SSEClientTransport(
2567
- new URL(`http://localhost:${port}/sse`),
2568
- {
2569
- eventSourceInit: {
2570
- fetch: async (url, init) => {
2571
- return fetch(url, {
2572
- ...init,
2573
- headers: {
2574
- ...init?.headers,
2575
- "x-api-key": "123",
2576
- },
2577
- });
2578
- },
2579
- },
2580
- },
2581
- );
2582
-
2583
- await client.connect(transport);
2584
-
2585
- const result = await client.readResource({
2586
- uri: "auth://template/resource-123",
2587
- });
2588
-
2589
- expect(templateLoad).toHaveBeenCalledTimes(1);
2590
- expect(templateLoad).toHaveBeenCalledWith(
2591
- { resourceId: "resource-123" },
2592
- { permissions: ["read", "write"], userId: 99 },
2593
- );
2594
-
2595
- expect(result).toEqual({
2596
- contents: [
2597
- {
2598
- mimeType: "text/plain",
2599
- name: "Auth Template",
2600
- text: "Resource resource-123 accessed by user 99 with permissions: read, write",
2601
- uri: "auth://template/resource-123",
2602
- },
2603
- ],
2604
- });
2605
- });
2606
-
2607
- test("provides auth to resource templates returning arrays", async () => {
2608
- const port = await getRandomPort();
2609
-
2610
- const authenticate = vi.fn(async () => {
2611
- return {
2612
- accessLevel: 3,
2613
- teamId: "team-alpha",
2614
- };
2615
- });
2616
-
2617
- const server = new FastMCP<{ accessLevel: number; teamId: string }>({
2618
- authenticate,
2619
- name: "Test",
2620
- version: "1.0.0",
2621
- });
2622
-
2623
- const templateLoad = vi.fn(async (args, auth) => {
2624
- return [
2625
- {
2626
- text: `Document 1 for ${args.category} - Team: ${auth?.teamId}`,
2627
- },
2628
- {
2629
- text: `Document 2 for ${args.category} - Access Level: ${auth?.accessLevel}`,
2630
- },
2631
- ];
2632
- });
2633
-
2634
- server.addResourceTemplate({
2635
- arguments: [
2636
- {
2637
- name: "category",
2638
- required: true,
2639
- },
2640
- ],
2641
- load: templateLoad,
2642
- mimeType: "text/plain",
2643
- name: "Multi Doc Template",
2644
- uriTemplate: "docs://category/{category}",
2645
- });
2646
-
2647
- await server.start({
2648
- httpStream: {
2649
- port,
2650
- },
2651
- transportType: "httpStream",
2652
- });
2653
-
2654
- const client = new Client(
2655
- {
2656
- name: "example-client",
2657
- version: "1.0.0",
2658
- },
2659
- {
2660
- capabilities: {},
2661
- },
2662
- );
2663
-
2664
- const transport = new SSEClientTransport(
2665
- new URL(`http://localhost:${port}/sse`),
2666
- {
2667
- eventSourceInit: {
2668
- fetch: async (url, init) => {
2669
- return fetch(url, {
2670
- ...init,
2671
- headers: {
2672
- ...init?.headers,
2673
- "x-api-key": "123",
2674
- },
2675
- });
2676
- },
2677
- },
2678
- },
2679
- );
2680
-
2681
- await client.connect(transport);
2682
-
2683
- const result = await client.readResource({
2684
- uri: "docs://category/reports",
2685
- });
2686
-
2687
- expect(templateLoad).toHaveBeenCalledTimes(1);
2688
- expect(templateLoad).toHaveBeenCalledWith(
2689
- { category: "reports" },
2690
- { accessLevel: 3, teamId: "team-alpha" },
2691
- );
2692
-
2693
- expect(result).toEqual({
2694
- contents: [
2695
- {
2696
- mimeType: "text/plain",
2697
- name: "Multi Doc Template",
2698
- text: "Document 1 for reports - Team: team-alpha",
2699
- uri: "docs://category/reports",
2700
- },
2701
- {
2702
- mimeType: "text/plain",
2703
- name: "Multi Doc Template",
2704
- text: "Document 2 for reports - Access Level: 3",
2705
- uri: "docs://category/reports",
2706
- },
2707
- ],
2708
- });
2709
- });
2710
-
2711
- test("provides auth to prompt argument completion", async () => {
2712
- const port = await getRandomPort();
2713
-
2714
- const authenticate = vi.fn(async () => {
2715
- return {
2716
- department: "engineering",
2717
- userId: 100,
2718
- };
2719
- });
2720
-
2721
- const server = new FastMCP<{ department: string; userId: number }>({
2722
- authenticate,
2723
- name: "Test",
2724
- version: "1.0.0",
2725
- });
2726
-
2727
- const promptCompleter = vi.fn(async (value: string, auth) => {
2728
- return {
2729
- values: [
2730
- `${value}_user${auth?.userId}`,
2731
- `${value}_dept${auth?.department}`,
2732
- ],
2733
- };
2734
- });
2735
-
2736
- server.addPrompt({
2737
- arguments: [
2738
- {
2739
- complete: promptCompleter,
2740
- description: "Project name",
2741
- name: "project",
2742
- required: true,
2743
- },
2744
- ],
2745
- async load(args) {
2746
- return `Loading project: ${args.project}`;
2747
- },
2748
- name: "load-project",
2749
- });
2750
-
2751
- await server.start({
2752
- httpStream: {
2753
- port,
2754
- },
2755
- transportType: "httpStream",
2756
- });
2757
-
2758
- const client = new Client(
2759
- {
2760
- name: "example-client",
2761
- version: "1.0.0",
2762
- },
2763
- {
2764
- capabilities: {},
2765
- },
2766
- );
2767
-
2768
- const transport = new SSEClientTransport(
2769
- new URL(`http://localhost:${port}/sse`),
2770
- {
2771
- eventSourceInit: {
2772
- fetch: async (url, init) => {
2773
- return fetch(url, {
2774
- ...init,
2775
- headers: {
2776
- ...init?.headers,
2777
- "x-api-key": "123",
2778
- },
2779
- });
2780
- },
2781
- },
2782
- },
2783
- );
2784
-
2785
- await client.connect(transport);
2786
-
2787
- const completionResult = await client.complete({
2788
- argument: {
2789
- name: "project",
2790
- value: "test",
2791
- },
2792
- ref: {
2793
- name: "load-project",
2794
- type: "ref/prompt",
2795
- },
2796
- });
2797
-
2798
- expect(promptCompleter).toHaveBeenCalledTimes(1);
2799
- expect(promptCompleter).toHaveBeenCalledWith("test", {
2800
- department: "engineering",
2801
- userId: 100,
2802
- });
2803
-
2804
- expect(completionResult).toEqual({
2805
- completion: {
2806
- values: ["test_user100", "test_deptengineering"],
2807
- },
2808
- });
2809
- });
2810
-
2811
- test("provides auth to prompt load function", async () => {
2812
- const port = await getRandomPort();
2813
-
2814
- const authenticate = vi.fn(async () => {
2815
- return {
2816
- level: "admin",
2817
- username: "testuser",
2818
- };
2819
- });
2820
-
2821
- const server = new FastMCP<{ level: string; username: string }>({
2822
- authenticate,
2823
- name: "Test",
2824
- version: "1.0.0",
2825
- });
2826
-
2827
- const promptLoad = vi.fn(async (args, auth) => {
2828
- return `Welcome ${auth?.username} (${auth?.level}): You selected ${args.option}`;
2829
- });
2830
-
2831
- server.addPrompt({
2832
- arguments: [
2833
- {
2834
- description: "Option to select",
2835
- name: "option",
2836
- required: true,
2837
- },
2838
- ],
2839
- load: promptLoad,
2840
- name: "auth-prompt",
2841
- });
2842
-
2843
- await server.start({
2844
- httpStream: {
2845
- port,
2846
- },
2847
- transportType: "httpStream",
2848
- });
2849
-
2850
- const client = new Client(
2851
- {
2852
- name: "example-client",
2853
- version: "1.0.0",
2854
- },
2855
- {
2856
- capabilities: {},
2857
- },
2858
- );
2859
-
2860
- const transport = new SSEClientTransport(
2861
- new URL(`http://localhost:${port}/sse`),
2862
- {
2863
- eventSourceInit: {
2864
- fetch: async (url, init) => {
2865
- return fetch(url, {
2866
- ...init,
2867
- headers: {
2868
- ...init?.headers,
2869
- "x-api-key": "123",
2870
- },
2871
- });
2872
- },
2873
- },
2874
- },
2875
- );
2876
-
2877
- await client.connect(transport);
2878
-
2879
- const result = await client.getPrompt({
2880
- arguments: { option: "dashboard" },
2881
- name: "auth-prompt",
2882
- });
2883
-
2884
- expect(promptLoad).toHaveBeenCalledTimes(1);
2885
- expect(promptLoad).toHaveBeenCalledWith(
2886
- { option: "dashboard" },
2887
- { level: "admin", username: "testuser" },
2888
- );
2889
-
2890
- expect(result).toEqual({
2891
- messages: [
2892
- {
2893
- content: {
2894
- text: "Welcome testuser (admin): You selected dashboard",
2895
- type: "text",
2896
- },
2897
- role: "user",
2898
- },
2899
- ],
2900
- });
2901
- });
2902
-
2903
- test("provides auth to resource template argument completion", async () => {
2904
- const port = await getRandomPort();
2905
-
2906
- const authenticate = vi.fn(async () => {
2907
- return {
2908
- region: "us-west",
2909
- teamId: "alpha",
2910
- };
2911
- });
2912
-
2913
- const server = new FastMCP<{ region: string; teamId: string }>({
2914
- authenticate,
2915
- name: "Test",
2916
- version: "1.0.0",
2917
- });
2918
-
2919
- const resourceCompleter = vi.fn(async (value: string, auth) => {
2920
- return {
2921
- values: [`${value}_${auth?.region}`, `${value}_team_${auth?.teamId}`],
2922
- };
2923
- });
2924
-
2925
- server.addResourceTemplate({
2926
- arguments: [
2927
- {
2928
- complete: resourceCompleter,
2929
- description: "Service ID",
2930
- name: "serviceId",
2931
- required: true,
2932
- },
2933
- ],
2934
- async load(args) {
2935
- return {
2936
- text: `Service ${args.serviceId} data`,
2937
- };
2938
- },
2939
- mimeType: "text/plain",
2940
- name: "Service Resource",
2941
- uriTemplate: "service://{serviceId}",
2942
- });
2943
-
2944
- await server.start({
2945
- httpStream: {
2946
- port,
2947
- },
2948
- transportType: "httpStream",
2949
- });
2950
-
2951
- const client = new Client(
2952
- {
2953
- name: "example-client",
2954
- version: "1.0.0",
2955
- },
2956
- {
2957
- capabilities: {},
2958
- },
2959
- );
2960
-
2961
- const transport = new SSEClientTransport(
2962
- new URL(`http://localhost:${port}/sse`),
2963
- {
2964
- eventSourceInit: {
2965
- fetch: async (url, init) => {
2966
- return fetch(url, {
2967
- ...init,
2968
- headers: {
2969
- ...init?.headers,
2970
- "x-api-key": "123",
2971
- },
2972
- });
2973
- },
2974
- },
2975
- },
2976
- );
2977
-
2978
- await client.connect(transport);
2979
-
2980
- const completionResult = await client.complete({
2981
- argument: {
2982
- name: "serviceId",
2983
- value: "api",
2984
- },
2985
- ref: {
2986
- type: "ref/resource",
2987
- uri: "service://{serviceId}",
2988
- },
2989
- });
2990
-
2991
- expect(resourceCompleter).toHaveBeenCalledTimes(1);
2992
- expect(resourceCompleter).toHaveBeenCalledWith("api", {
2993
- region: "us-west",
2994
- teamId: "alpha",
2995
- });
2996
-
2997
- expect(completionResult).toEqual({
2998
- completion: {
2999
- values: ["api_us-west", "api_team_alpha"],
3000
- },
3001
- });
3002
- });
3003
-
3004
- test("supports streaming output from tools", async () => {
3005
- let streamResult: { content: Array<{ text: string; type: string }> };
3006
-
3007
- await runWithTestServer({
3008
- run: async ({ client }) => {
3009
- const result = await client.callTool({
3010
- arguments: {},
3011
- name: "streaming-void-tool",
3012
- });
3013
-
3014
- expect(result).toEqual({
3015
- content: [],
3016
- });
3017
-
3018
- streamResult = (await client.callTool({
3019
- arguments: {},
3020
- name: "streaming-with-result",
3021
- })) as { content: Array<{ text: string; type: string }> };
3022
-
3023
- expect(streamResult).toEqual({
3024
- content: [{ text: "Final result after streaming", type: "text" }],
3025
- });
3026
- },
3027
- server: async () => {
3028
- const server = new FastMCP({
3029
- name: "Test",
3030
- version: "1.0.0",
3031
- });
3032
-
3033
- server.addTool({
3034
- annotations: {
3035
- streamingHint: true,
3036
- },
3037
- description: "A streaming tool that returns void",
3038
- execute: async (_args, context) => {
3039
- await context.streamContent({
3040
- text: "Streaming content 1",
3041
- type: "text",
3042
- });
3043
-
3044
- await context.streamContent({
3045
- text: "Streaming content 2",
3046
- type: "text",
3047
- });
3048
-
3049
- // Return void
3050
- return;
3051
- },
3052
- name: "streaming-void-tool",
3053
- parameters: z.object({}),
3054
- });
3055
-
3056
- server.addTool({
3057
- annotations: {
3058
- streamingHint: true,
3059
- },
3060
- description: "A streaming tool that returns a result.",
3061
- execute: async (_args, context) => {
3062
- await context.streamContent({
3063
- text: "Streaming content 1",
3064
- type: "text",
3065
- });
3066
-
3067
- await context.streamContent({
3068
- text: "Streaming content 2",
3069
- type: "text",
3070
- });
3071
-
3072
- return "Final result after streaming";
3073
- },
3074
- name: "streaming-with-result",
3075
- parameters: z.object({}),
3076
- });
3077
-
3078
- return server;
3079
- },
3080
- });
3081
- });
3082
-
3083
- test("blocks unauthorized requests", async () => {
3084
- const port = await getRandomPort();
3085
-
3086
- const server = new FastMCP<{ id: number }>({
3087
- authenticate: async () => {
3088
- throw new Response(null, {
3089
- status: 401,
3090
- statusText: "Unauthorized",
3091
- });
3092
- },
3093
- name: "Test",
3094
- version: "1.0.0",
3095
- });
3096
-
3097
- await server.start({
3098
- httpStream: {
3099
- port,
3100
- },
3101
- transportType: "httpStream",
3102
- });
3103
-
3104
- const client = new Client(
3105
- {
3106
- name: "example-client",
3107
- version: "1.0.0",
3108
- },
3109
- {
3110
- capabilities: {},
3111
- },
3112
- );
3113
-
3114
- const transport = new SSEClientTransport(
3115
- new URL(`http://localhost:${port}/sse`),
3116
- );
3117
-
3118
- expect(async () => {
3119
- await client.connect(transport);
3120
- }).rejects.toThrow("SSE error: Non-200 status code (401)");
3121
- });
3122
-
3123
- test("filters tools based on canAccess property", async () => {
3124
- const port = await getRandomPort();
3125
-
3126
- const server = new FastMCP<{ role: string }>({
3127
- authenticate: async (request) => {
3128
- const role = request.headers["x-role"] as string;
3129
- return { role: role || "user" };
3130
- },
3131
- name: "Test",
3132
- version: "1.0.0",
3133
- });
3134
-
3135
- server.addTool({
3136
- canAccess: (auth) => auth?.role === "admin",
3137
- description: "Admin only",
3138
- execute: async () => "admin",
3139
- name: "admin-tool",
3140
- });
3141
-
3142
- server.addTool({
3143
- description: "Available to all",
3144
- execute: async () => "public",
3145
- name: "public-tool",
3146
- });
3147
-
3148
- await server.start({ httpStream: { port }, transportType: "httpStream" });
3149
-
3150
- try {
3151
- // Admin gets both tools
3152
- const adminClient = new Client(
3153
- { name: "admin", version: "1.0.0" },
3154
- { capabilities: {} },
3155
- );
3156
- const adminTransport = new SSEClientTransport(
3157
- new URL(`http://localhost:${port}/sse`),
3158
- {
3159
- eventSourceInit: {
3160
- fetch: (url, init) =>
3161
- fetch(url, {
3162
- ...init,
3163
- headers: { ...init?.headers, "x-role": "admin" },
3164
- }),
3165
- },
3166
- },
3167
- );
3168
- await adminClient.connect(adminTransport);
3169
-
3170
- const adminTools = await adminClient.listTools();
3171
- expect(adminTools.tools.map((t) => t.name).sort()).toEqual([
3172
- "admin-tool",
3173
- "public-tool",
3174
- ]);
3175
-
3176
- // User gets only public tool
3177
- const userClient = new Client(
3178
- { name: "user", version: "1.0.0" },
3179
- { capabilities: {} },
3180
- );
3181
- const userTransport = new SSEClientTransport(
3182
- new URL(`http://localhost:${port}/sse`),
3183
- {
3184
- eventSourceInit: {
3185
- fetch: (url, init) =>
3186
- fetch(url, {
3187
- ...init,
3188
- headers: { ...init?.headers, "x-role": "user" },
3189
- }),
3190
- },
3191
- },
3192
- );
3193
- await userClient.connect(userTransport);
3194
-
3195
- const userTools = await userClient.listTools();
3196
- expect(userTools.tools.map((t) => t.name)).toEqual(["public-tool"]);
3197
-
3198
- await adminClient.close();
3199
- await userClient.close();
3200
- } finally {
3201
- await server.stop();
3202
- }
3203
- });
3204
-
3205
- test("tools without canAccess are accessible to all", async () => {
3206
- await runWithTestServer({
3207
- run: async ({ client }) => {
3208
- const tools = await client.listTools();
3209
- expect(tools.tools).toHaveLength(1);
3210
- expect(tools.tools[0].name).toBe("test-tool");
3211
-
3212
- const result = await client.callTool({
3213
- arguments: {},
3214
- name: "test-tool",
3215
- });
3216
- expect(
3217
- (result.content as Array<{ text: string; type: string }>)[0],
3218
- ).toEqual({ text: "success", type: "text" });
3219
- },
3220
- server: async () => {
3221
- const server = new FastMCP({ name: "Test", version: "1.0.0" });
3222
- server.addTool({
3223
- description: "Test tool",
3224
- execute: async () => "success",
3225
- name: "test-tool",
3226
- });
3227
- return server;
3228
- },
3229
- });
3230
- });
3231
-
3232
- test("canAccess works without authentication", async () => {
3233
- const port = await getRandomPort();
3234
-
3235
- const server = new FastMCP<{ role: string }>({
3236
- name: "Test",
3237
- version: "1.0.0",
3238
- });
3239
-
3240
- server.addTool({
3241
- canAccess: (auth) => auth?.role === "admin",
3242
- execute: async () => "admin",
3243
- name: "admin-tool",
3244
- });
3245
-
3246
- server.addTool({
3247
- execute: async () => "public",
3248
- name: "public-tool",
3249
- });
3250
-
3251
- await server.start({ httpStream: { port }, transportType: "httpStream" });
3252
-
3253
- try {
3254
- const client = new Client(
3255
- { name: "test-client", version: "1.0.0" },
3256
- { capabilities: {} },
3257
- );
3258
- const transport = new SSEClientTransport(
3259
- new URL(`http://localhost:${port}/sse`),
3260
- );
3261
- await client.connect(transport);
3262
-
3263
- const tools = await client.listTools();
3264
- expect(tools.tools.map((t) => t.name).sort()).toEqual([
3265
- "admin-tool",
3266
- "public-tool",
3267
- ]);
3268
-
3269
- await client.close();
3270
- } finally {
3271
- await server.stop();
3272
- }
3273
- });
3274
-
3275
- // We now use a direct approach for testing HTTP Stream functionality
3276
- // rather than a helper function
3277
-
3278
- // Set longer timeout for HTTP Stream tests
3279
- test("HTTP Stream: calls a tool", { timeout: 20000 }, async () => {
3280
- console.log("Starting HTTP Stream test...");
3281
-
3282
- const port = await getRandomPort();
3283
-
3284
- // Create server directly (don't use helper function)
3285
- const server = new FastMCP({
3286
- name: "Test",
3287
- version: "1.0.0",
3288
- });
3289
-
3290
- server.addTool({
3291
- description: "Add two numbers",
3292
- execute: async (args) => {
3293
- return String(args.a + args.b);
3294
- },
3295
- name: "add",
3296
- parameters: z.object({
3297
- a: z.number(),
3298
- b: z.number(),
3299
- }),
3300
- });
3301
-
3302
- await server.start({
3303
- httpStream: {
3304
- port,
3305
- },
3306
- transportType: "httpStream",
3307
- });
3308
-
3309
- try {
3310
- // Create client
3311
- const client = new Client(
3312
- {
3313
- name: "example-client",
3314
- version: "1.0.0",
3315
- },
3316
- {
3317
- capabilities: {},
3318
- },
3319
- );
3320
-
3321
- // IMPORTANT: Don't provide sessionId manually with HTTP streaming
3322
- // The server will generate a session ID automatically
3323
- const transport = new StreamableHTTPClientTransport(
3324
- new URL(`http://localhost:${port}/mcp`),
3325
- );
3326
-
3327
- // Connect client to server and wait for session to be ready
3328
- const sessionPromise = new Promise<FastMCPSession>((resolve) => {
3329
- server.on("connect", async (event) => {
3330
- await event.session.waitForReady();
3331
- resolve(event.session);
3332
- });
3333
- });
3334
-
3335
- await client.connect(transport);
3336
- await sessionPromise;
3337
-
3338
- // Call tool
3339
- const result = await client.callTool({
3340
- arguments: {
3341
- a: 1,
3342
- b: 2,
3343
- },
3344
- name: "add",
3345
- });
3346
-
3347
- // Check result
3348
- expect(result).toEqual({
3349
- content: [{ text: "3", type: "text" }],
3350
- });
3351
-
3352
- // Clean up connection
3353
- await transport.terminateSession();
3354
-
3355
- await client.close();
3356
- } finally {
3357
- await server.stop();
3358
- }
3359
- });
3360
-
3361
- test("uses `formatInvalidParamsErrorMessage` callback to build ErrorCode.InvalidParams error message", async () => {
3362
- await runWithTestServer({
3363
- run: async ({ client }) => {
3364
- try {
3365
- await client.callTool({
3366
- arguments: {
3367
- a: 1,
3368
- b: "invalid",
3369
- },
3370
- name: "add",
3371
- });
3372
- } catch (error) {
3373
- expect(error).toBeInstanceOf(McpError);
3374
-
3375
- // @ts-expect-error - we know that error is an McpError
3376
- expect(error.code).toBe(ErrorCode.InvalidParams);
3377
-
3378
- // @ts-expect-error - we know that error is an McpError
3379
- expect(error.message).toBe(
3380
- `MCP error -32602: MCP error -32602: Tool 'add' parameter validation failed: My custom error message: Field b failed with error 'Expected number, received string'. Please check the parameter types and values according to the tool's schema.`,
3381
- );
3382
- }
3383
- },
3384
- server: async () => {
3385
- const server = new FastMCP({
3386
- name: "Test",
3387
- utils: {
3388
- formatInvalidParamsErrorMessage: (issues) => {
3389
- const message = issues
3390
- .map((issue) => {
3391
- const path = issue.path?.join(".") || "root";
3392
- return `Field ${path} failed with error '${issue.message}'`;
3393
- })
3394
- .join(", ");
3395
- return `My custom error message: ${message}`;
3396
- },
3397
- },
3398
- version: "1.0.0",
3399
- });
3400
-
3401
- server.addTool({
3402
- description: "Add two numbers",
3403
- execute: async (args) => {
3404
- return String(args.a + args.b);
3405
- },
3406
- name: "add",
3407
- parameters: z.object({
3408
- a: z.number(),
3409
- b: z.number(),
3410
- }),
3411
- });
3412
-
3413
- return server;
3414
- },
3415
- });
3416
- });
3417
-
3418
- test("stateless mode works correctly", async () => {
3419
- const port = await getRandomPort();
3420
-
3421
- const server = new FastMCP({
3422
- name: "Test server",
3423
- version: "1.0.0",
3424
- });
3425
-
3426
- server.addTool({
3427
- description: "Add two numbers",
3428
- execute: async (args) => {
3429
- return String(args.a + args.b);
3430
- },
3431
- name: "add",
3432
- parameters: z.object({
3433
- a: z.number(),
3434
- b: z.number(),
3435
- }),
3436
- });
3437
-
3438
- await server.start({
3439
- httpStream: {
3440
- port,
3441
- stateless: true,
3442
- },
3443
- transportType: "httpStream",
3444
- });
3445
-
3446
- try {
3447
- const client = new Client(
3448
- {
3449
- name: "Test client",
3450
- version: "1.0.0",
3451
- },
3452
- {
3453
- capabilities: {},
3454
- },
3455
- );
3456
-
3457
- const transport = new StreamableHTTPClientTransport(
3458
- new URL(`http://localhost:${port}/mcp`),
3459
- );
3460
-
3461
- await client.connect(transport);
3462
-
3463
- // Tool call should work in stateless mode
3464
- const result = await client.callTool({
3465
- arguments: { a: 5, b: 7 },
3466
- name: "add",
3467
- });
3468
-
3469
- expect(result.content).toEqual([
3470
- {
3471
- text: "12",
3472
- type: "text",
3473
- },
3474
- ]);
3475
-
3476
- // Multiple calls should work independently in stateless mode
3477
- const result2 = await client.callTool({
3478
- arguments: { a: 10, b: 20 },
3479
- name: "add",
3480
- });
3481
-
3482
- expect(result2.content).toEqual([
3483
- {
3484
- text: "30",
3485
- type: "text",
3486
- },
3487
- ]);
3488
-
3489
- // Server should not track sessions in stateless mode
3490
- expect(server.sessions.length).toBe(0);
3491
-
3492
- await client.close();
3493
- } finally {
3494
- await server.stop();
3495
- }
3496
- });
3497
-
3498
- test("stateless mode health check includes mode indicator", async () => {
3499
- const port = await getRandomPort();
3500
-
3501
- const server = new FastMCP({
3502
- name: "Test server",
3503
- version: "1.0.0",
3504
- });
3505
-
3506
- await server.start({
3507
- httpStream: {
3508
- port,
3509
- stateless: true,
3510
- },
3511
- transportType: "httpStream",
3512
- });
3513
-
3514
- try {
3515
- const response = await fetch(`http://localhost:${port}/ready`);
3516
- expect(response.status).toBe(200);
3517
-
3518
- const json = await response.json();
3519
- expect(json).toEqual({
3520
- mode: "stateless",
3521
- ready: 1,
3522
- status: "ready",
3523
- total: 1,
3524
- });
3525
- } finally {
3526
- await server.stop();
3527
- }
3528
- });
3529
-
3530
- test("stateless mode with valid authentication allows access", async () => {
3531
- const port = await getRandomPort();
3532
-
3533
- const server = new FastMCP<{ userId: string }>({
3534
- authenticate: async () => {
3535
- // Always authenticate successfully for this test
3536
- return { userId: "123" };
3537
- },
3538
- name: "Test server",
3539
- version: "1.0.0",
3540
- });
3541
-
3542
- server.addTool({
3543
- description: "Test tool",
3544
- execute: async () => {
3545
- return "pong";
3546
- },
3547
- name: "ping",
3548
- parameters: z.object({}),
3549
- });
3550
-
3551
- await server.start({
3552
- httpStream: {
3553
- port,
3554
- stateless: true,
3555
- },
3556
- transportType: "httpStream",
3557
- });
3558
-
3559
- try {
3560
- const client = new Client(
3561
- {
3562
- name: "Test client",
3563
- version: "1.0.0",
3564
- },
3565
- {
3566
- capabilities: {},
3567
- },
3568
- );
3569
-
3570
- const transport = new StreamableHTTPClientTransport(
3571
- new URL(`http://localhost:${port}/mcp`),
3572
- );
3573
-
3574
- await client.connect(transport);
3575
-
3576
- const result = await client.callTool({
3577
- arguments: {},
3578
- name: "ping",
3579
- });
3580
-
3581
- expect(result.content).toEqual([
3582
- {
3583
- text: "pong",
3584
- type: "text",
3585
- },
3586
- ]);
3587
-
3588
- // Server should not track sessions in stateless mode
3589
- expect(server.sessions.length).toBe(0);
3590
-
3591
- await client.close();
3592
- } finally {
3593
- await server.stop();
3594
- }
3595
- });
3596
-
3597
- test("stateless mode rejects missing Authorization header", async () => {
3598
- const port = await getRandomPort();
3599
-
3600
- const server = new FastMCP<{ userId: string }>({
3601
- authenticate: async (req) => {
3602
- const authHeader = req.headers.authorization;
3603
-
3604
- if (!authHeader || !authHeader.startsWith("Bearer ")) {
3605
- throw new Response(null, {
3606
- status: 401,
3607
- statusText: "Unauthorized",
3608
- });
3609
- }
3610
-
3611
- return { userId: "123" };
3612
- },
3613
- name: "Test server",
3614
- version: "1.0.0",
3615
- });
3616
-
3617
- server.addTool({
3618
- description: "Test tool",
3619
- execute: async () => {
3620
- return "pong";
3621
- },
3622
- name: "ping",
3623
- parameters: z.object({}),
3624
- });
3625
-
3626
- await server.start({
3627
- httpStream: {
3628
- port,
3629
- stateless: true,
3630
- },
3631
- transportType: "httpStream",
3632
- });
3633
-
3634
- try {
3635
- // Send a raw HTTP request without Authorization header
3636
- const response = await fetch(`http://localhost:${port}/mcp`, {
3637
- body: JSON.stringify({
3638
- id: 1,
3639
- jsonrpc: "2.0",
3640
- method: "tools/call",
3641
- params: {
3642
- arguments: {},
3643
- name: "ping",
3644
- },
3645
- }),
3646
- headers: {
3647
- "Content-Type": "application/json",
3648
- },
3649
- method: "POST",
3650
- });
3651
-
3652
- expect(response.status).toBe(401);
3653
-
3654
- const body = (await response.json()) as { error?: { message?: string } };
3655
- expect(body.error?.message).toContain("Unauthorized");
3656
- } finally {
3657
- await server.stop();
3658
- }
3659
- });
3660
-
3661
- test("stateless mode rejects invalid authentication token", async () => {
3662
- const port = await getRandomPort();
3663
- const VALID_TOKEN = "valid_jwt_token";
3664
- const INVALID_TOKEN = "invalid_jwt_token";
3665
-
3666
- const server = new FastMCP<{ userId: string }>({
3667
- authenticate: async (req) => {
3668
- const authHeader = req.headers.authorization;
3669
-
3670
- if (!authHeader || !authHeader.startsWith("Bearer ")) {
3671
- throw new Response(null, {
3672
- status: 401,
3673
- statusText: "Unauthorized",
3674
- });
3675
- }
3676
-
3677
- const token = authHeader.split(" ")[1];
3678
-
3679
- if (token === VALID_TOKEN) {
3680
- return { userId: "123" };
3681
- }
3682
-
3683
- throw new Response(null, {
3684
- status: 401,
3685
- statusText: "Unauthorized",
3686
- });
3687
- },
3688
- name: "Test server",
3689
- version: "1.0.0",
3690
- });
3691
-
3692
- server.addTool({
3693
- description: "Test tool",
3694
- execute: async () => {
3695
- return "pong";
3696
- },
3697
- name: "ping",
3698
- parameters: z.object({}),
3699
- });
3700
-
3701
- await server.start({
3702
- httpStream: {
3703
- port,
3704
- stateless: true,
3705
- },
3706
- transportType: "httpStream",
3707
- });
3708
-
3709
- try {
3710
- // Send a raw HTTP request with invalid token
3711
- const response = await fetch(`http://localhost:${port}/mcp`, {
3712
- body: JSON.stringify({
3713
- id: 1,
3714
- jsonrpc: "2.0",
3715
- method: "tools/call",
3716
- params: {
3717
- arguments: {},
3718
- name: "ping",
3719
- },
3720
- }),
3721
- headers: {
3722
- Authorization: `Bearer ${INVALID_TOKEN}`,
3723
- "Content-Type": "application/json",
3724
- },
3725
- method: "POST",
3726
- });
3727
-
3728
- expect(response.status).toBe(401);
3729
-
3730
- const body = (await response.json()) as { error?: { message?: string } };
3731
- expect(body.error?.message).toContain("Unauthorized");
3732
- } finally {
3733
- await server.stop();
3734
- }
3735
- });
3736
-
3737
- test("stateless mode handles authentication function throwing errors", async () => {
3738
- const port = await getRandomPort();
3739
-
3740
- const server = new FastMCP<{ userId: string }>({
3741
- authenticate: async () => {
3742
- // Simulate an internal error during token validation
3743
- throw new Error("JWT validation service is down");
3744
- },
3745
- name: "Test server",
3746
- version: "1.0.0",
3747
- });
3748
-
3749
- server.addTool({
3750
- description: "Test tool",
3751
- execute: async () => {
3752
- return "pong";
3753
- },
3754
- name: "ping",
3755
- parameters: z.object({}),
3756
- });
3757
-
3758
- await server.start({
3759
- httpStream: {
3760
- port,
3761
- stateless: true,
3762
- },
3763
- transportType: "httpStream",
3764
- });
3765
-
3766
- try {
3767
- // Send a raw HTTP request
3768
- const response = await fetch(`http://localhost:${port}/mcp`, {
3769
- body: JSON.stringify({
3770
- id: 1,
3771
- jsonrpc: "2.0",
3772
- method: "tools/call",
3773
- params: {
3774
- arguments: {},
3775
- name: "ping",
3776
- },
3777
- }),
3778
- headers: {
3779
- Authorization: "Bearer any_token",
3780
- "Content-Type": "application/json",
3781
- },
3782
- method: "POST",
3783
- });
3784
-
3785
- expect(response.status).toBe(401);
3786
-
3787
- const body = (await response.json()) as { error?: { message?: string } };
3788
- // The actual error message should be passed through
3789
- expect(body.error?.message).toContain("JWT validation service is down");
3790
- } finally {
3791
- await server.stop();
3792
- }
3793
- });
3794
-
3795
- test("stateless mode handles concurrent requests with authentication", async () => {
3796
- const port = await getRandomPort();
3797
- let requestCount = 0;
3798
-
3799
- const server = new FastMCP<{ requestId: number }>({
3800
- authenticate: async () => {
3801
- // Track each authentication request
3802
- requestCount++;
3803
- return { requestId: requestCount };
3804
- },
3805
- name: "Test server",
3806
- version: "1.0.0",
3807
- });
3808
-
3809
- server.addTool({
3810
- description: "Echo request ID",
3811
- execute: async (_args, context) => {
3812
- return `Request ${context.session?.requestId}`;
3813
- },
3814
- name: "whoami",
3815
- parameters: z.object({}),
3816
- });
3817
-
3818
- await server.start({
3819
- httpStream: {
3820
- port,
3821
- stateless: true,
3822
- },
3823
- transportType: "httpStream",
3824
- });
3825
-
3826
- try {
3827
- // Create two clients to test concurrent stateless requests
3828
- const client1 = new Client(
3829
- {
3830
- name: "Client 1",
3831
- version: "1.0.0",
3832
- },
3833
- {
3834
- capabilities: {},
3835
- },
3836
- );
3837
-
3838
- const client2 = new Client(
3839
- {
3840
- name: "Client 2",
3841
- version: "1.0.0",
3842
- },
3843
- {
3844
- capabilities: {},
3845
- },
3846
- );
3847
-
3848
- const transport1 = new StreamableHTTPClientTransport(
3849
- new URL(`http://localhost:${port}/mcp`),
3850
- );
3851
-
3852
- const transport2 = new StreamableHTTPClientTransport(
3853
- new URL(`http://localhost:${port}/mcp`),
3854
- );
3855
-
3856
- await client1.connect(transport1);
3857
- await client2.connect(transport2);
3858
-
3859
- // Both clients should work independently
3860
- const result1 = await client1.callTool({
3861
- arguments: {},
3862
- name: "whoami",
3863
- });
3864
-
3865
- const result2 = await client2.callTool({
3866
- arguments: {},
3867
- name: "whoami",
3868
- });
3869
-
3870
- // Each request should have been authenticated
3871
- expect((result1.content as unknown[])[0]).toHaveProperty("text");
3872
- expect((result2.content as unknown[])[0]).toHaveProperty("text");
3873
-
3874
- // Server should not track sessions in stateless mode
3875
- expect(server.sessions.length).toBe(0);
3876
-
3877
- await client1.close();
3878
- await client2.close();
3879
- } finally {
3880
- await server.stop();
3881
- }
3882
- });
3883
-
3884
- // Tests for GitHub Issue: FastMCP authentication fix
3885
- // Testing the fix for session creation despite authentication failure
3886
-
3887
- test("authentication failure handling: should throw error when auth.authenticated is false", async () => {
3888
- const port = await getRandomPort();
3889
-
3890
- const server = new FastMCP<{ authenticated: boolean; error?: string }>({
3891
- authenticate: async () => {
3892
- // Simulate authentication failure with { authenticated: false }
3893
- return { authenticated: false, error: "Invalid JWT token" };
3894
- },
3895
- name: "Test server",
3896
- version: "1.0.0",
3897
- });
3898
-
3899
- server.addTool({
3900
- description: "Test tool",
3901
- execute: async () => {
3902
- return "pong";
3903
- },
3904
- name: "ping",
3905
- parameters: z.object({}),
3906
- });
3907
-
3908
- await server.start({
3909
- httpStream: {
3910
- port,
3911
- stateless: true,
3912
- },
3913
- transportType: "httpStream",
3914
- });
3915
-
3916
- try {
3917
- // Send a raw HTTP request that should be rejected
3918
- const response = await fetch(`http://localhost:${port}/mcp`, {
3919
- body: JSON.stringify({
3920
- id: 1,
3921
- jsonrpc: "2.0",
3922
- method: "initialize",
3923
- params: {
3924
- capabilities: {},
3925
- clientInfo: { name: "test", version: "1.0" },
3926
- protocolVersion: "2024-11-05",
3927
- },
3928
- }),
3929
- headers: {
3930
- Accept: "application/json, text/event-stream",
3931
- Authorization: "Bearer invalid.jwt.token",
3932
- "Content-Type": "application/json",
3933
- },
3934
- method: "POST",
3935
- });
3936
-
3937
- // Should return 401 Unauthorized (handled by mcp-proxy)
3938
- expect(response.status).toBe(401);
3939
-
3940
- const body = (await response.json()) as {
3941
- error?: { code?: number; message?: string };
3942
- };
3943
- expect(body.error?.message).toContain("Invalid JWT token");
3944
- } finally {
3945
- await server.stop();
3946
- }
3947
- });
3948
-
3949
- test("authentication failure handling: should create session when auth.authenticated is true", async () => {
3950
- const port = await getRandomPort();
3951
-
3952
- const server = new FastMCP<{
3953
- authenticated: boolean;
3954
- session?: { userId: string };
3955
- }>({
3956
- authenticate: async () => {
3957
- // Simulate successful authentication
3958
- return { authenticated: true, session: { userId: "123" } };
3959
- },
3960
- name: "Test server",
3961
- version: "1.0.0",
3962
- });
3963
-
3964
- server.addTool({
3965
- description: "Test tool",
3966
- execute: async (_args, context) => {
3967
- return `User: ${context.session?.session?.userId}`;
3968
- },
3969
- name: "whoami",
3970
- parameters: z.object({}),
3971
- });
3972
-
3973
- await server.start({
3974
- httpStream: {
3975
- port,
3976
- stateless: true,
3977
- },
3978
- transportType: "httpStream",
3979
- });
3980
-
3981
- try {
3982
- const client = new Client(
3983
- {
3984
- name: "Test client",
3985
- version: "1.0.0",
3986
- },
3987
- {
3988
- capabilities: {},
3989
- },
3990
- );
3991
-
3992
- const transport = new StreamableHTTPClientTransport(
3993
- new URL(`http://localhost:${port}/mcp`),
3994
- );
3995
-
3996
- await client.connect(transport);
3997
-
3998
- const result = await client.callTool({
3999
- arguments: {},
4000
- name: "whoami",
4001
- });
4002
-
4003
- expect(result.content).toEqual([
4004
- {
4005
- text: "User: 123",
4006
- type: "text",
4007
- },
4008
- ]);
4009
-
4010
- await client.close();
4011
- } finally {
4012
- await server.stop();
4013
- }
4014
- });
4015
-
4016
- test("authentication failure handling: should create session when auth is null/undefined (anonymous)", async () => {
4017
- const port = await getRandomPort();
4018
-
4019
- const server = new FastMCP({
4020
- // No authenticate function - anonymous access
4021
- name: "Test server",
4022
- version: "1.0.0",
4023
- });
4024
-
4025
- server.addTool({
4026
- description: "Test tool",
4027
- execute: async (_args, context) => {
4028
- return `Anonymous: ${context.session === undefined}`;
4029
- },
4030
- name: "ping",
4031
- parameters: z.object({}),
4032
- });
4033
-
4034
- await server.start({
4035
- httpStream: {
4036
- port,
4037
- stateless: true,
4038
- },
4039
- transportType: "httpStream",
4040
- });
4041
-
4042
- try {
4043
- const client = new Client(
4044
- {
4045
- name: "Test client",
4046
- version: "1.0.0",
4047
- },
4048
- {
4049
- capabilities: {},
4050
- },
4051
- );
4052
-
4053
- const transport = new StreamableHTTPClientTransport(
4054
- new URL(`http://localhost:${port}/mcp`),
4055
- );
4056
-
4057
- await client.connect(transport);
4058
-
4059
- const result = await client.callTool({
4060
- arguments: {},
4061
- name: "ping",
4062
- });
4063
-
4064
- expect(result.content).toEqual([
4065
- {
4066
- text: "Anonymous: true",
4067
- type: "text",
4068
- },
4069
- ]);
4070
-
4071
- await client.close();
4072
- } finally {
4073
- await server.stop();
4074
- }
4075
- });
4076
-
4077
- test("authentication failure handling: should use default error message when auth.error is not provided", async () => {
4078
- const port = await getRandomPort();
4079
-
4080
- const server = new FastMCP<{ authenticated: boolean }>({
4081
- authenticate: async () => {
4082
- // Return authenticated: false without custom error message
4083
- return { authenticated: false };
4084
- },
4085
- name: "Test server",
4086
- version: "1.0.0",
4087
- });
4088
-
4089
- server.addTool({
4090
- description: "Test tool",
4091
- execute: async () => {
4092
- return "pong";
4093
- },
4094
- name: "ping",
4095
- parameters: z.object({}),
4096
- });
4097
-
4098
- await server.start({
4099
- httpStream: {
4100
- port,
4101
- stateless: true,
4102
- },
4103
- transportType: "httpStream",
4104
- });
4105
-
4106
- try {
4107
- const response = await fetch(`http://localhost:${port}/mcp`, {
4108
- body: JSON.stringify({
4109
- id: 1,
4110
- jsonrpc: "2.0",
4111
- method: "initialize",
4112
- params: {
4113
- capabilities: {},
4114
- clientInfo: { name: "test", version: "1.0" },
4115
- protocolVersion: "2024-11-05",
4116
- },
4117
- }),
4118
- headers: {
4119
- Accept: "application/json, text/event-stream",
4120
- "Content-Type": "application/json",
4121
- },
4122
- method: "POST",
4123
- });
4124
-
4125
- expect(response.status).toBe(401);
4126
-
4127
- const body = (await response.json()) as {
4128
- error?: { message?: string };
4129
- };
4130
- expect(body.error?.message).toContain("Authentication failed");
4131
- } finally {
4132
- await server.stop();
4133
- }
4134
- });
4135
-
4136
- test("authentication failure handling: should preserve existing behavior for truthy auth results", async () => {
4137
- const port = await getRandomPort();
4138
-
4139
- const server = new FastMCP<{ role: string; userId: string }>({
4140
- authenticate: async () => {
4141
- // Return a truthy object without 'authenticated' field (legacy pattern)
4142
- return { role: "admin", userId: "456" };
4143
- },
4144
- name: "Test server",
4145
- version: "1.0.0",
4146
- });
4147
-
4148
- server.addTool({
4149
- description: "Test tool",
4150
- execute: async (_args, context) => {
4151
- return `User: ${context.session?.userId}, Role: ${context.session?.role}`;
4152
- },
4153
- name: "whoami",
4154
- parameters: z.object({}),
4155
- });
4156
-
4157
- await server.start({
4158
- httpStream: {
4159
- port,
4160
- stateless: true,
4161
- },
4162
- transportType: "httpStream",
4163
- });
4164
-
4165
- try {
4166
- const client = new Client(
4167
- {
4168
- name: "Test client",
4169
- version: "1.0.0",
4170
- },
4171
- {
4172
- capabilities: {},
4173
- },
4174
- );
4175
-
4176
- const transport = new StreamableHTTPClientTransport(
4177
- new URL(`http://localhost:${port}/mcp`),
4178
- );
4179
-
4180
- await client.connect(transport);
4181
-
4182
- const result = await client.callTool({
4183
- arguments: {},
4184
- name: "whoami",
4185
- });
4186
-
4187
- expect(result.content).toEqual([
4188
- {
4189
- text: "User: 456, Role: admin",
4190
- type: "text",
4191
- },
4192
- ]);
4193
-
4194
- await client.close();
4195
- } finally {
4196
- await server.stop();
4197
- }
4198
- });
4199
-
4200
- test("authentication failure handling: should handle authentication with custom error messages", async () => {
4201
- const port = await getRandomPort();
4202
- const CUSTOM_ERROR_MSG = "Token expired at 2025-10-07T12:00:00Z";
4203
-
4204
- const server = new FastMCP<{ authenticated: boolean; error?: string }>({
4205
- authenticate: async () => {
4206
- return { authenticated: false, error: CUSTOM_ERROR_MSG };
4207
- },
4208
- name: "Test server",
4209
- version: "1.0.0",
4210
- });
4211
-
4212
- server.addTool({
4213
- description: "Test tool",
4214
- execute: async () => {
4215
- return "pong";
4216
- },
4217
- name: "ping",
4218
- parameters: z.object({}),
4219
- });
4220
-
4221
- await server.start({
4222
- httpStream: {
4223
- port,
4224
- stateless: true,
4225
- },
4226
- transportType: "httpStream",
4227
- });
4228
-
4229
- try {
4230
- const response = await fetch(`http://localhost:${port}/mcp`, {
4231
- body: JSON.stringify({
4232
- id: 1,
4233
- jsonrpc: "2.0",
4234
- method: "initialize",
4235
- params: {
4236
- capabilities: {},
4237
- clientInfo: { name: "test", version: "1.0" },
4238
- protocolVersion: "2024-11-05",
4239
- },
4240
- }),
4241
- headers: {
4242
- Accept: "application/json, text/event-stream",
4243
- "Content-Type": "application/json",
4244
- },
4245
- method: "POST",
4246
- });
4247
-
4248
- expect(response.status).toBe(401);
4249
-
4250
- const body = (await response.json()) as {
4251
- error?: { message?: string };
4252
- };
4253
- expect(body.error?.message).toBe(CUSTOM_ERROR_MSG);
4254
- } finally {
4255
- await server.stop();
4256
- }
4257
- });
4258
-
4259
- test("authentication failure handling: should not create session for authenticated=false even with session data", async () => {
4260
- const port = await getRandomPort();
4261
-
4262
- const server = new FastMCP<{
4263
- authenticated: boolean;
4264
- error?: string;
4265
- session?: { userId: string };
4266
- }>({
4267
- authenticate: async () => {
4268
- // Even if session data is present, authenticated: false should reject
4269
- return {
4270
- authenticated: false,
4271
- error: "Insufficient permissions",
4272
- session: { userId: "hacker" },
4273
- };
4274
- },
4275
- name: "Test server",
4276
- version: "1.0.0",
4277
- });
4278
-
4279
- server.addTool({
4280
- description: "Test tool",
4281
- execute: async () => {
4282
- return "pong";
4283
- },
4284
- name: "ping",
4285
- parameters: z.object({}),
4286
- });
4287
-
4288
- await server.start({
4289
- httpStream: {
4290
- port,
4291
- stateless: true,
4292
- },
4293
- transportType: "httpStream",
4294
- });
4295
-
4296
- try {
4297
- const response = await fetch(`http://localhost:${port}/mcp`, {
4298
- body: JSON.stringify({
4299
- id: 1,
4300
- jsonrpc: "2.0",
4301
- method: "initialize",
4302
- params: {
4303
- capabilities: {},
4304
- clientInfo: { name: "test", version: "1.0" },
4305
- protocolVersion: "2024-11-05",
4306
- },
4307
- }),
4308
- headers: {
4309
- Accept: "application/json, text/event-stream",
4310
- "Content-Type": "application/json",
4311
- },
4312
- method: "POST",
4313
- });
4314
-
4315
- expect(response.status).toBe(401);
4316
-
4317
- const body = (await response.json()) as {
4318
- error?: { message?: string };
4319
- };
4320
- expect(body.error?.message).toContain("Insufficient permissions");
4321
-
4322
- // Verify session was never created
4323
- expect(server.sessions.length).toBe(0);
4324
- } finally {
4325
- await server.stop();
4326
- }
4327
- });
4328
-
4329
- test("host configuration works with 0.0.0.0", async () => {
4330
- const port = await getRandomPort();
4331
-
4332
- const server = new FastMCP({
4333
- name: "Test server",
4334
- version: "1.0.0",
4335
- });
4336
-
4337
- await server.start({
4338
- httpStream: {
4339
- host: "0.0.0.0",
4340
- port,
4341
- },
4342
- transportType: "httpStream",
4343
- });
4344
-
4345
- try {
4346
- const healthResponse = await fetch(`http://0.0.0.0:${port}/health`);
4347
- expect(healthResponse.status).toBe(200);
4348
- expect(await healthResponse.text()).toBe("✓ Ok");
4349
- } finally {
4350
- await server.stop();
4351
- }
4352
- });
4353
-
4354
- test("tools can access client info", async () => {
4355
- await runWithTestServer({
4356
- run: async ({ client }) => {
4357
- const result = (await client.callTool({
4358
- name: "get-client-info",
4359
- })) as ContentResult;
4360
-
4361
- expect(result.content).toHaveLength(1);
4362
- expect(result.content[0]).toHaveProperty("type", "text");
4363
-
4364
- const text = (result.content[0] as TextContent).text;
4365
- expect(text).toContain("Client name:");
4366
- expect(text).toContain("Client version:");
4367
- // The client info should contain some actual client information
4368
- expect(text).toMatch(/Client name:\s+\w+/);
4369
- expect(text).toMatch(/Client version:\s+[\d.]+/);
4370
- },
4371
- server: async () => {
4372
- const server = new FastMCP({
4373
- name: "Test",
4374
- version: "1.0.0",
4375
- });
4376
-
4377
- server.addTool({
4378
- description: "Get client information",
4379
- execute: async (_args, context) => {
4380
- const clientInfo = context.client.version;
4381
- return `Client name: ${clientInfo?.name || "unknown"}\nClient version: ${clientInfo?.version || "unknown"}`;
4382
- },
4383
- name: "get-client-info",
4384
- });
4385
-
4386
- return server;
4387
- },
4388
- });
4389
- });