fastmcp 3.19.3 → 3.20.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,359 @@
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
+ });
@@ -148,7 +148,7 @@ test("adds tools with Zod v4 schema", async () => {
148
148
  {
149
149
  description: "Add two numbers (using Zod v4 schema)",
150
150
  inputSchema: {
151
- $schema: "https://json-schema.org/draft/2020-12/schema",
151
+ $schema: "http://json-schema.org/draft-07/schema#",
152
152
  additionalProperties: false,
153
153
  properties: {
154
154
  a: { type: "number" },
@@ -2416,7 +2416,9 @@ test("provides auth to tools", async () => {
2416
2416
  warn: expect.any(Function),
2417
2417
  },
2418
2418
  reportProgress: expect.any(Function),
2419
+ requestId: undefined,
2419
2420
  session: { id: 1 },
2421
+ sessionId: expect.any(String),
2420
2422
  streamContent: expect.any(Function),
2421
2423
  },
2422
2424
  );
package/src/FastMCP.ts CHANGED
@@ -210,7 +210,19 @@ type Context<T extends FastMCPSessionAuth> = {
210
210
  warn: (message: string, data?: SerializableValue) => void;
211
211
  };
212
212
  reportProgress: (progress: Progress) => Promise<void>;
213
+ /**
214
+ * Request ID from the current MCP request.
215
+ * Available for all transports when the client provides it.
216
+ */
217
+ requestId?: string;
213
218
  session: T | undefined;
219
+ /**
220
+ * Session ID from the Mcp-Session-Id header.
221
+ * Only available for HTTP-based transports (SSE, HTTP Stream).
222
+ * Can be used to track per-session state, implement session-specific
223
+ * counters, or maintain user-specific data across multiple requests.
224
+ */
225
+ sessionId?: string;
214
226
  streamContent: (content: Content | Content[]) => Promise<void>;
215
227
  };
216
228
 
@@ -936,6 +948,12 @@ export class FastMCPSession<
936
948
  public get server(): Server {
937
949
  return this.#server;
938
950
  }
951
+ public get sessionId(): string | undefined {
952
+ return this.#sessionId;
953
+ }
954
+ public set sessionId(value: string | undefined) {
955
+ this.#sessionId = value;
956
+ }
939
957
  #auth: T | undefined;
940
958
  #capabilities: ServerCapabilities = {};
941
959
  #clientCapabilities?: ClientCapabilities;
@@ -959,6 +977,12 @@ export class FastMCPSession<
959
977
 
960
978
  #server: Server;
961
979
 
980
+ /**
981
+ * Session ID from the Mcp-Session-Id header (HTTP transports only).
982
+ * Used to track per-session state across multiple requests.
983
+ */
984
+ #sessionId?: string;
985
+
962
986
  #utils?: ServerOptions<T>["utils"];
963
987
 
964
988
  constructor({
@@ -971,6 +995,7 @@ export class FastMCPSession<
971
995
  resources,
972
996
  resourcesTemplates,
973
997
  roots,
998
+ sessionId,
974
999
  tools,
975
1000
  transportType,
976
1001
  utils,
@@ -985,6 +1010,7 @@ export class FastMCPSession<
985
1010
  resources: Resource<T>[];
986
1011
  resourcesTemplates: InputResourceTemplate<T>[];
987
1012
  roots?: ServerOptions<T>["roots"];
1013
+ sessionId?: string;
988
1014
  tools: Tool<T>[];
989
1015
  transportType?: "httpStream" | "stdio";
990
1016
  utils?: ServerOptions<T>["utils"];
@@ -996,6 +1022,7 @@ export class FastMCPSession<
996
1022
  this.#logger = logger;
997
1023
  this.#pingConfig = ping;
998
1024
  this.#rootsConfig = roots;
1025
+ this.#sessionId = sessionId;
999
1026
  this.#needsEventLoopFlush = transportType === "httpStream";
1000
1027
 
1001
1028
  if (tools.length) {
@@ -1077,6 +1104,16 @@ export class FastMCPSession<
1077
1104
  try {
1078
1105
  await this.#server.connect(transport);
1079
1106
 
1107
+ // Extract session ID from transport if available (HTTP transports only)
1108
+ if ("sessionId" in transport) {
1109
+ const transportWithSessionId = transport as {
1110
+ sessionId?: string;
1111
+ } & Transport;
1112
+ if (typeof transportWithSessionId.sessionId === "string") {
1113
+ this.#sessionId = transportWithSessionId.sessionId;
1114
+ }
1115
+ }
1116
+
1080
1117
  let attempt = 0;
1081
1118
  const maxAttempts = 10;
1082
1119
  const retryDelay = 100;
@@ -1789,7 +1826,12 @@ export class FastMCPSession<
1789
1826
  },
1790
1827
  log,
1791
1828
  reportProgress,
1829
+ requestId:
1830
+ typeof request.params?._meta?.requestId === "string"
1831
+ ? request.params._meta.requestId
1832
+ : undefined,
1792
1833
  session: this.#auth,
1834
+ sessionId: this.#sessionId,
1793
1835
  streamContent,
1794
1836
  });
1795
1837
 
@@ -2110,7 +2152,7 @@ export class FastMCP<
2110
2152
  );
2111
2153
 
2112
2154
  this.#httpStreamServer = await startHTTPServer<FastMCPSession<T>>({
2113
- authenticate: this.#authenticate,
2155
+ ...(this.#authenticate ? { authenticate: this.#authenticate } : {}),
2114
2156
  createServer: async (request) => {
2115
2157
  let auth: T | undefined;
2116
2158
 
@@ -2124,9 +2166,14 @@ export class FastMCP<
2124
2166
  }
2125
2167
  }
2126
2168
 
2169
+ // Extract session ID from headers
2170
+ const sessionId = Array.isArray(request.headers["mcp-session-id"])
2171
+ ? request.headers["mcp-session-id"][0]
2172
+ : request.headers["mcp-session-id"];
2173
+
2127
2174
  // In stateless mode, create a new session for each request
2128
2175
  // without persisting it in the sessions array
2129
- return this.#createSession(auth);
2176
+ return this.#createSession(auth, sessionId);
2130
2177
  },
2131
2178
  enableJsonResponse: httpConfig.enableJsonResponse,
2132
2179
  eventStore: httpConfig.eventStore,
@@ -2151,7 +2198,7 @@ export class FastMCP<
2151
2198
  } else {
2152
2199
  // Regular mode with session management
2153
2200
  this.#httpStreamServer = await startHTTPServer<FastMCPSession<T>>({
2154
- authenticate: this.#authenticate,
2201
+ ...(this.#authenticate ? { authenticate: this.#authenticate } : {}),
2155
2202
  createServer: async (request) => {
2156
2203
  let auth: T | undefined;
2157
2204
 
@@ -2159,7 +2206,12 @@ export class FastMCP<
2159
2206
  auth = await this.#authenticate(request);
2160
2207
  }
2161
2208
 
2162
- return this.#createSession(auth);
2209
+ // Extract session ID from headers
2210
+ const sessionId = Array.isArray(request.headers["mcp-session-id"])
2211
+ ? request.headers["mcp-session-id"][0]
2212
+ : request.headers["mcp-session-id"];
2213
+
2214
+ return this.#createSession(auth, sessionId);
2163
2215
  },
2164
2216
  enableJsonResponse: httpConfig.enableJsonResponse,
2165
2217
  eventStore: httpConfig.eventStore,
@@ -2218,7 +2270,7 @@ export class FastMCP<
2218
2270
  * Creates a new FastMCPSession instance with the current configuration.
2219
2271
  * Used both for regular sessions and stateless requests.
2220
2272
  */
2221
- #createSession(auth?: T): FastMCPSession<T> {
2273
+ #createSession(auth?: T, sessionId?: string): FastMCPSession<T> {
2222
2274
  // Check if authentication failed
2223
2275
  if (
2224
2276
  auth &&
@@ -2249,6 +2301,7 @@ export class FastMCP<
2249
2301
  resources: this.#resources,
2250
2302
  resourcesTemplates: this.#resourcesTemplates,
2251
2303
  roots: this.#options.roots,
2304
+ sessionId,
2252
2305
  tools: allowedTools,
2253
2306
  transportType: "httpStream",
2254
2307
  utils: this.#options.utils,