fastmcp 3.20.1 → 3.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,359 +0,0 @@
1
- import { Client } from "@modelcontextprotocol/sdk/client/index.js";
2
- import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
3
- import { describe, expect, it } from "vitest";
4
- import { z } from "zod";
5
-
6
- import { FastMCP } from "./FastMCP.js";
7
-
8
- interface TestAuth {
9
- [key: string]: unknown;
10
- userId: string;
11
- }
12
-
13
- describe("FastMCP Session ID Support", () => {
14
- describe("HTTP Stream transport", () => {
15
- it("should expose sessionId to tool handlers from Mcp-Session-Id header", async () => {
16
- const server = new FastMCP<TestAuth>({
17
- authenticate: async () => ({
18
- userId: "test-user",
19
- }),
20
- name: "test-server",
21
- version: "1.0.0",
22
- });
23
-
24
- let capturedSessionId: string | undefined;
25
- let capturedRequestId: string | undefined;
26
-
27
- server.addTool({
28
- description: "Test tool that captures session and request IDs",
29
- execute: async (_args, context) => {
30
- capturedSessionId = context.sessionId;
31
- capturedRequestId = context.requestId;
32
- return `Session ID: ${context.sessionId || "none"}, Request ID: ${context.requestId || "none"}`;
33
- },
34
- name: "capture-ids",
35
- parameters: z.object({}),
36
- });
37
-
38
- const port = 3000 + Math.floor(Math.random() * 1000);
39
-
40
- await server.start({
41
- httpStream: {
42
- port,
43
- },
44
- transportType: "httpStream",
45
- });
46
-
47
- try {
48
- const transport = new StreamableHTTPClientTransport(
49
- new URL(`http://localhost:${port}/mcp`),
50
- );
51
-
52
- const client = new Client(
53
- {
54
- name: "test-client",
55
- version: "1.0.0",
56
- },
57
- {
58
- capabilities: {},
59
- },
60
- );
61
-
62
- await client.connect(transport);
63
-
64
- const result = await client.callTool({
65
- arguments: {},
66
- name: "capture-ids",
67
- });
68
-
69
- expect(result).toBeDefined();
70
- expect(capturedSessionId).toBeDefined();
71
- expect(typeof capturedSessionId).toBe("string");
72
- expect(capturedSessionId).toMatch(/^[0-9a-f-]+$/); // UUID format
73
-
74
- // Request ID may or may not be provided by the client
75
- // If provided, it should be a string
76
- if (capturedRequestId !== undefined) {
77
- expect(typeof capturedRequestId).toBe("string");
78
- }
79
-
80
- await client.close();
81
- } finally {
82
- await server.stop();
83
- }
84
- });
85
-
86
- it("should maintain the same sessionId across multiple requests", async () => {
87
- const server = new FastMCP<TestAuth>({
88
- authenticate: async () => ({
89
- userId: "test-user",
90
- }),
91
- name: "test-server",
92
- version: "1.0.0",
93
- });
94
-
95
- const capturedSessionIds: (string | undefined)[] = [];
96
-
97
- server.addTool({
98
- description: "Test tool that captures session ID",
99
- execute: async (_args, context) => {
100
- capturedSessionIds.push(context.sessionId);
101
- return `Session ID: ${context.sessionId}`;
102
- },
103
- name: "capture-session",
104
- parameters: z.object({}),
105
- });
106
-
107
- const port = 3000 + Math.floor(Math.random() * 1000);
108
-
109
- await server.start({
110
- httpStream: {
111
- port,
112
- },
113
- transportType: "httpStream",
114
- });
115
-
116
- try {
117
- const transport = new StreamableHTTPClientTransport(
118
- new URL(`http://localhost:${port}/mcp`),
119
- );
120
-
121
- const client = new Client(
122
- {
123
- name: "test-client",
124
- version: "1.0.0",
125
- },
126
- {
127
- capabilities: {},
128
- },
129
- );
130
-
131
- await client.connect(transport);
132
-
133
- // Make multiple requests
134
- await client.callTool({
135
- arguments: {},
136
- name: "capture-session",
137
- });
138
-
139
- await client.callTool({
140
- arguments: {},
141
- name: "capture-session",
142
- });
143
-
144
- await client.callTool({
145
- arguments: {},
146
- name: "capture-session",
147
- });
148
-
149
- // All requests should have the same session ID
150
- expect(capturedSessionIds).toHaveLength(3);
151
- expect(capturedSessionIds[0]).toBeDefined();
152
- expect(capturedSessionIds[0]).toBe(capturedSessionIds[1]);
153
- expect(capturedSessionIds[1]).toBe(capturedSessionIds[2]);
154
-
155
- await client.close();
156
- } finally {
157
- await server.stop();
158
- }
159
- });
160
-
161
- it("should support per-session state management using sessionId", async () => {
162
- const server = new FastMCP<TestAuth>({
163
- authenticate: async () => ({
164
- userId: "test-user",
165
- }),
166
- name: "test-server",
167
- version: "1.0.0",
168
- });
169
-
170
- // Per-session counter storage
171
- const sessionCounters = new Map<string, number>();
172
-
173
- server.addTool({
174
- description: "Increment a per-session counter",
175
- execute: async (_args, context) => {
176
- if (!context.sessionId) {
177
- return "No session ID available";
178
- }
179
-
180
- const currentCount = sessionCounters.get(context.sessionId) || 0;
181
- const newCount = currentCount + 1;
182
- sessionCounters.set(context.sessionId, newCount);
183
-
184
- return `Counter for session ${context.sessionId}: ${newCount}`;
185
- },
186
- name: "increment-counter",
187
- parameters: z.object({}),
188
- });
189
-
190
- const port = 3000 + Math.floor(Math.random() * 1000);
191
-
192
- await server.start({
193
- httpStream: {
194
- port,
195
- },
196
- transportType: "httpStream",
197
- });
198
-
199
- try {
200
- // Create two separate clients with different sessions
201
- const transport1 = new StreamableHTTPClientTransport(
202
- new URL(`http://localhost:${port}/mcp`),
203
- );
204
-
205
- const client1 = new Client(
206
- {
207
- name: "test-client-1",
208
- version: "1.0.0",
209
- },
210
- {
211
- capabilities: {},
212
- },
213
- );
214
-
215
- await client1.connect(transport1);
216
-
217
- const transport2 = new StreamableHTTPClientTransport(
218
- new URL(`http://localhost:${port}/mcp`),
219
- );
220
-
221
- const client2 = new Client(
222
- {
223
- name: "test-client-2",
224
- version: "1.0.0",
225
- },
226
- {
227
- capabilities: {},
228
- },
229
- );
230
-
231
- await client2.connect(transport2);
232
-
233
- // Increment counter for client 1 twice
234
- const result1a = await client1.callTool({
235
- arguments: {},
236
- name: "increment-counter",
237
- });
238
-
239
- const result1b = await client1.callTool({
240
- arguments: {},
241
- name: "increment-counter",
242
- });
243
-
244
- // Increment counter for client 2 once
245
- const result2 = await client2.callTool({
246
- arguments: {},
247
- name: "increment-counter",
248
- });
249
-
250
- // Verify counters are independent per session
251
- expect((result1a.content as Array<{ text: string }>)[0].text).toContain(
252
- ": 1",
253
- );
254
- expect((result1b.content as Array<{ text: string }>)[0].text).toContain(
255
- ": 2",
256
- );
257
- expect((result2.content as Array<{ text: string }>)[0].text).toContain(
258
- ": 1",
259
- );
260
-
261
- await client1.close();
262
- await client2.close();
263
- } finally {
264
- await server.stop();
265
- }
266
- });
267
-
268
- it("should work in stateless mode without persistent sessionId", async () => {
269
- const server = new FastMCP<TestAuth>({
270
- authenticate: async () => ({
271
- userId: "test-user",
272
- }),
273
- name: "test-server",
274
- version: "1.0.0",
275
- });
276
-
277
- let capturedSessionId: string | undefined;
278
-
279
- server.addTool({
280
- description: "Test tool in stateless mode",
281
- execute: async (_args, context) => {
282
- capturedSessionId = context.sessionId;
283
- return `Session ID: ${context.sessionId || "none"}`;
284
- },
285
- name: "test-stateless",
286
- parameters: z.object({}),
287
- });
288
-
289
- const port = 3000 + Math.floor(Math.random() * 1000);
290
-
291
- await server.start({
292
- httpStream: {
293
- port,
294
- stateless: true,
295
- },
296
- transportType: "httpStream",
297
- });
298
-
299
- try {
300
- const transport = new StreamableHTTPClientTransport(
301
- new URL(`http://localhost:${port}/mcp`),
302
- );
303
-
304
- const client = new Client(
305
- {
306
- name: "test-client",
307
- version: "1.0.0",
308
- },
309
- {
310
- capabilities: {},
311
- },
312
- );
313
-
314
- await client.connect(transport);
315
-
316
- await client.callTool({
317
- arguments: {},
318
- name: "test-stateless",
319
- });
320
-
321
- // In stateless mode, sessionId should be undefined
322
- expect(capturedSessionId).toBeUndefined();
323
-
324
- await client.close();
325
- } finally {
326
- await server.stop();
327
- }
328
- });
329
- });
330
-
331
- describe("stdio transport", () => {
332
- it("should not have sessionId in stdio transport", async () => {
333
- const server = new FastMCP<TestAuth>({
334
- authenticate: async () => ({
335
- userId: "test-user",
336
- }),
337
- name: "test-server",
338
- version: "1.0.0",
339
- });
340
-
341
- let capturedSessionId: string | undefined;
342
-
343
- server.addTool({
344
- description: "Test tool for stdio",
345
- execute: async (_args, context) => {
346
- capturedSessionId = context.sessionId;
347
- return `Session ID: ${context.sessionId || "none"}`;
348
- },
349
- name: "test-stdio",
350
- parameters: z.object({}),
351
- });
352
-
353
- await server.start({ transportType: "stdio" });
354
-
355
- // In stdio transport, sessionId should be undefined
356
- expect(capturedSessionId).toBeUndefined();
357
- });
358
- });
359
- });