fastmcp 3.12.0 → 3.13.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.
package/README.md CHANGED
@@ -18,6 +18,7 @@ A TypeScript framework for building [MCP](https://glama.ai/mcp) servers capable
18
18
  - [Logging](#logging)
19
19
  - [Error handling](#errors)
20
20
  - [HTTP Streaming](#http-streaming) (with SSE compatibility)
21
+ - [Stateless mode](#stateless-mode) for serverless deployments
21
22
  - CORS (enabled by default)
22
23
  - [Progress notifications](#progress)
23
24
  - [Streaming output](#streaming-output)
@@ -178,6 +179,52 @@ const transport = new SSEClientTransport(new URL(`http://localhost:8080/sse`));
178
179
  await client.connect(transport);
179
180
  ```
180
181
 
182
+ #### Stateless Mode
183
+
184
+ FastMCP supports stateless operation for HTTP streaming, where each request is handled independently without maintaining persistent sessions. This is ideal for serverless environments, load-balanced deployments, or when session state isn't required.
185
+
186
+ In stateless mode:
187
+
188
+ - No sessions are tracked on the server
189
+ - Each request creates a temporary session that's discarded after the response
190
+ - Reduced memory usage and better scalability
191
+ - Perfect for stateless deployment environments
192
+
193
+ You can enable stateless mode by adding the `stateless: true` option:
194
+
195
+ ```ts
196
+ server.start({
197
+ transportType: "httpStream",
198
+ httpStream: {
199
+ port: 8080,
200
+ stateless: true,
201
+ },
202
+ });
203
+ ```
204
+
205
+ > **Note:** Stateless mode is only available with HTTP streaming transport. Features that depend on persistent sessions (like session-specific state) will not be available in stateless mode.
206
+
207
+ You can also enable stateless mode using CLI arguments or environment variables:
208
+
209
+ ```bash
210
+ # Via CLI argument
211
+ npx fastmcp dev src/server.ts --transport http-stream --port 8080 --stateless true
212
+
213
+ # Via environment variable
214
+ FASTMCP_STATELESS=true npx fastmcp dev src/server.ts
215
+ ```
216
+
217
+ The `/ready` health check endpoint will indicate when the server is running in stateless mode:
218
+
219
+ ```json
220
+ {
221
+ "mode": "stateless",
222
+ "ready": 1,
223
+ "status": "ready",
224
+ "total": 1
225
+ }
226
+ ```
227
+
181
228
  ## Core Concepts
182
229
 
183
230
  ### Tools
package/dist/FastMCP.d.ts CHANGED
@@ -606,6 +606,7 @@ declare class FastMCP<T extends FastMCPSessionAuth = FastMCPSessionAuth> extends
606
606
  endpoint?: `/${string}`;
607
607
  eventStore?: EventStore;
608
608
  port: number;
609
+ stateless?: boolean;
609
610
  };
610
611
  transportType: "httpStream" | "stdio";
611
612
  }>): Promise<void>;
package/dist/FastMCP.js CHANGED
@@ -1091,109 +1091,71 @@ var FastMCP = class extends FastMCPEventEmitter {
1091
1091
  });
1092
1092
  } else if (config.transportType === "httpStream") {
1093
1093
  const httpConfig = config.httpStream;
1094
- this.#httpStreamServer = await startHTTPServer({
1095
- createServer: async (request) => {
1096
- let auth;
1097
- if (this.#authenticate) {
1098
- auth = await this.#authenticate(request);
1099
- }
1100
- const allowedTools = auth ? this.#tools.filter(
1101
- (tool) => tool.canAccess ? tool.canAccess(auth) : true
1102
- ) : this.#tools;
1103
- return new FastMCPSession({
1104
- auth,
1105
- name: this.#options.name,
1106
- ping: this.#options.ping,
1107
- prompts: this.#prompts,
1108
- resources: this.#resources,
1109
- resourcesTemplates: this.#resourcesTemplates,
1110
- roots: this.#options.roots,
1111
- tools: allowedTools,
1112
- transportType: "httpStream",
1113
- utils: this.#options.utils,
1114
- version: this.#options.version
1115
- });
1116
- },
1117
- enableJsonResponse: httpConfig.enableJsonResponse,
1118
- eventStore: httpConfig.eventStore,
1119
- onClose: async (session) => {
1120
- this.emit("disconnect", {
1121
- session
1122
- });
1123
- },
1124
- onConnect: async (session) => {
1125
- this.#sessions.push(session);
1126
- console.info(`[FastMCP info] HTTP Stream session established`);
1127
- this.emit("connect", {
1128
- session
1129
- });
1130
- },
1131
- onUnhandledRequest: async (req, res) => {
1132
- const healthConfig = this.#options.health ?? {};
1133
- const enabled = healthConfig.enabled === void 0 ? true : healthConfig.enabled;
1134
- if (enabled) {
1135
- const path = healthConfig.path ?? "/health";
1136
- const url = new URL(req.url || "", "http://localhost");
1137
- try {
1138
- if (req.method === "GET" && url.pathname === path) {
1139
- res.writeHead(healthConfig.status ?? 200, {
1140
- "Content-Type": "text/plain"
1141
- }).end(healthConfig.message ?? "\u2713 Ok");
1142
- return;
1143
- }
1144
- if (req.method === "GET" && url.pathname === "/ready") {
1145
- const readySessions = this.#sessions.filter(
1146
- (s) => s.isReady
1147
- ).length;
1148
- const totalSessions = this.#sessions.length;
1149
- const allReady = readySessions === totalSessions && totalSessions > 0;
1150
- const response = {
1151
- ready: readySessions,
1152
- status: allReady ? "ready" : totalSessions === 0 ? "no_sessions" : "initializing",
1153
- total: totalSessions
1154
- };
1155
- res.writeHead(allReady ? 200 : 503, {
1156
- "Content-Type": "application/json"
1157
- }).end(JSON.stringify(response));
1158
- return;
1159
- }
1160
- } catch (error) {
1161
- console.error("[FastMCP error] health endpoint error", error);
1162
- }
1163
- }
1164
- const oauthConfig = this.#options.oauth;
1165
- if (oauthConfig?.enabled && req.method === "GET") {
1166
- const url = new URL(req.url || "", "http://localhost");
1167
- if (url.pathname === "/.well-known/oauth-authorization-server" && oauthConfig.authorizationServer) {
1168
- const metadata = convertObjectToSnakeCase(
1169
- oauthConfig.authorizationServer
1170
- );
1171
- res.writeHead(200, {
1172
- "Content-Type": "application/json"
1173
- }).end(JSON.stringify(metadata));
1174
- return;
1094
+ if (httpConfig.stateless) {
1095
+ console.info(
1096
+ `[FastMCP info] Starting server in stateless mode on HTTP Stream at http://localhost:${httpConfig.port}${httpConfig.endpoint}`
1097
+ );
1098
+ this.#httpStreamServer = await startHTTPServer({
1099
+ createServer: async (request) => {
1100
+ let auth;
1101
+ if (this.#authenticate) {
1102
+ auth = await this.#authenticate(request);
1175
1103
  }
1176
- if (url.pathname === "/.well-known/oauth-protected-resource" && oauthConfig.protectedResource) {
1177
- const metadata = convertObjectToSnakeCase(
1178
- oauthConfig.protectedResource
1179
- );
1180
- res.writeHead(200, {
1181
- "Content-Type": "application/json"
1182
- }).end(JSON.stringify(metadata));
1183
- return;
1104
+ return this.#createSession(auth);
1105
+ },
1106
+ enableJsonResponse: httpConfig.enableJsonResponse,
1107
+ eventStore: httpConfig.eventStore,
1108
+ // In stateless mode, we don't track sessions
1109
+ onClose: async () => {
1110
+ },
1111
+ onConnect: async () => {
1112
+ console.debug(
1113
+ `[FastMCP debug] Stateless HTTP Stream request handled`
1114
+ );
1115
+ },
1116
+ onUnhandledRequest: async (req, res) => {
1117
+ await this.#handleUnhandledRequest(req, res, true);
1118
+ },
1119
+ port: httpConfig.port,
1120
+ stateless: true,
1121
+ streamEndpoint: httpConfig.endpoint
1122
+ });
1123
+ } else {
1124
+ this.#httpStreamServer = await startHTTPServer({
1125
+ createServer: async (request) => {
1126
+ let auth;
1127
+ if (this.#authenticate) {
1128
+ auth = await this.#authenticate(request);
1184
1129
  }
1185
- }
1186
- res.writeHead(404).end();
1187
- },
1188
- port: httpConfig.port,
1189
- streamEndpoint: httpConfig.endpoint
1190
- });
1191
- console.info(
1192
- `[FastMCP info] server is running on HTTP Stream at http://localhost:${httpConfig.port}${httpConfig.endpoint}`
1193
- );
1194
- console.info(
1195
- `[FastMCP info] Transport type: httpStream (Streamable HTTP, not SSE)`
1196
- );
1130
+ return this.#createSession(auth);
1131
+ },
1132
+ enableJsonResponse: httpConfig.enableJsonResponse,
1133
+ eventStore: httpConfig.eventStore,
1134
+ onClose: async (session) => {
1135
+ this.emit("disconnect", {
1136
+ session
1137
+ });
1138
+ },
1139
+ onConnect: async (session) => {
1140
+ this.#sessions.push(session);
1141
+ console.info(`[FastMCP info] HTTP Stream session established`);
1142
+ this.emit("connect", {
1143
+ session
1144
+ });
1145
+ },
1146
+ onUnhandledRequest: async (req, res) => {
1147
+ await this.#handleUnhandledRequest(req, res, false);
1148
+ },
1149
+ port: httpConfig.port,
1150
+ streamEndpoint: httpConfig.endpoint
1151
+ });
1152
+ console.info(
1153
+ `[FastMCP info] server is running on HTTP Stream at http://localhost:${httpConfig.port}${httpConfig.endpoint}`
1154
+ );
1155
+ console.info(
1156
+ `[FastMCP info] Transport type: httpStream (Streamable HTTP, not SSE)`
1157
+ );
1158
+ }
1197
1159
  } else {
1198
1160
  throw new Error("Invalid transport type");
1199
1161
  }
@@ -1206,6 +1168,100 @@ var FastMCP = class extends FastMCPEventEmitter {
1206
1168
  await this.#httpStreamServer.close();
1207
1169
  }
1208
1170
  }
1171
+ /**
1172
+ * Creates a new FastMCPSession instance with the current configuration.
1173
+ * Used both for regular sessions and stateless requests.
1174
+ */
1175
+ #createSession(auth) {
1176
+ const allowedTools = auth ? this.#tools.filter(
1177
+ (tool) => tool.canAccess ? tool.canAccess(auth) : true
1178
+ ) : this.#tools;
1179
+ return new FastMCPSession({
1180
+ auth,
1181
+ name: this.#options.name,
1182
+ ping: this.#options.ping,
1183
+ prompts: this.#prompts,
1184
+ resources: this.#resources,
1185
+ resourcesTemplates: this.#resourcesTemplates,
1186
+ roots: this.#options.roots,
1187
+ tools: allowedTools,
1188
+ transportType: "httpStream",
1189
+ utils: this.#options.utils,
1190
+ version: this.#options.version
1191
+ });
1192
+ }
1193
+ /**
1194
+ * Handles unhandled HTTP requests with health, readiness, and OAuth endpoints
1195
+ */
1196
+ #handleUnhandledRequest = async (req, res, isStateless = false) => {
1197
+ const healthConfig = this.#options.health ?? {};
1198
+ const enabled = healthConfig.enabled === void 0 ? true : healthConfig.enabled;
1199
+ if (enabled) {
1200
+ const path = healthConfig.path ?? "/health";
1201
+ const url = new URL(req.url || "", "http://localhost");
1202
+ try {
1203
+ if (req.method === "GET" && url.pathname === path) {
1204
+ res.writeHead(healthConfig.status ?? 200, {
1205
+ "Content-Type": "text/plain"
1206
+ }).end(healthConfig.message ?? "\u2713 Ok");
1207
+ return;
1208
+ }
1209
+ if (req.method === "GET" && url.pathname === "/ready") {
1210
+ if (isStateless) {
1211
+ const response = {
1212
+ mode: "stateless",
1213
+ ready: 1,
1214
+ status: "ready",
1215
+ total: 1
1216
+ };
1217
+ res.writeHead(200, {
1218
+ "Content-Type": "application/json"
1219
+ }).end(JSON.stringify(response));
1220
+ } else {
1221
+ const readySessions = this.#sessions.filter(
1222
+ (s) => s.isReady
1223
+ ).length;
1224
+ const totalSessions = this.#sessions.length;
1225
+ const allReady = readySessions === totalSessions && totalSessions > 0;
1226
+ const response = {
1227
+ ready: readySessions,
1228
+ status: allReady ? "ready" : totalSessions === 0 ? "no_sessions" : "initializing",
1229
+ total: totalSessions
1230
+ };
1231
+ res.writeHead(allReady ? 200 : 503, {
1232
+ "Content-Type": "application/json"
1233
+ }).end(JSON.stringify(response));
1234
+ }
1235
+ return;
1236
+ }
1237
+ } catch (error) {
1238
+ console.error("[FastMCP error] health endpoint error", error);
1239
+ }
1240
+ }
1241
+ const oauthConfig = this.#options.oauth;
1242
+ if (oauthConfig?.enabled && req.method === "GET") {
1243
+ const url = new URL(req.url || "", "http://localhost");
1244
+ if (url.pathname === "/.well-known/oauth-authorization-server" && oauthConfig.authorizationServer) {
1245
+ const metadata = convertObjectToSnakeCase(
1246
+ oauthConfig.authorizationServer
1247
+ );
1248
+ res.writeHead(200, {
1249
+ "Content-Type": "application/json"
1250
+ }).end(JSON.stringify(metadata));
1251
+ return;
1252
+ }
1253
+ if (url.pathname === "/.well-known/oauth-protected-resource" && oauthConfig.protectedResource) {
1254
+ const metadata = convertObjectToSnakeCase(
1255
+ oauthConfig.protectedResource
1256
+ );
1257
+ res.writeHead(200, {
1258
+ "Content-Type": "application/json"
1259
+ }).end(JSON.stringify(metadata));
1260
+ return;
1261
+ }
1262
+ }
1263
+ res.writeHead(404).end();
1264
+ };
1209
1265
  #parseRuntimeConfig(overrides) {
1210
1266
  const args = process.argv.slice(2);
1211
1267
  const getArg = (name) => {
@@ -1215,9 +1271,11 @@ var FastMCP = class extends FastMCPEventEmitter {
1215
1271
  const transportArg = getArg("transport");
1216
1272
  const portArg = getArg("port");
1217
1273
  const endpointArg = getArg("endpoint");
1274
+ const statelessArg = getArg("stateless");
1218
1275
  const envTransport = process.env.FASTMCP_TRANSPORT;
1219
1276
  const envPort = process.env.FASTMCP_PORT;
1220
1277
  const envEndpoint = process.env.FASTMCP_ENDPOINT;
1278
+ const envStateless = process.env.FASTMCP_STATELESS;
1221
1279
  const transportType = overrides?.transportType || (transportArg === "http-stream" ? "httpStream" : transportArg) || envTransport || "stdio";
1222
1280
  if (transportType === "httpStream") {
1223
1281
  const port = parseInt(
@@ -1225,11 +1283,13 @@ var FastMCP = class extends FastMCPEventEmitter {
1225
1283
  );
1226
1284
  const endpoint = overrides?.httpStream?.endpoint || endpointArg || envEndpoint || "/mcp";
1227
1285
  const enableJsonResponse = overrides?.httpStream?.enableJsonResponse || false;
1286
+ const stateless = overrides?.httpStream?.stateless || statelessArg === "true" || envStateless === "true" || false;
1228
1287
  return {
1229
1288
  httpStream: {
1230
1289
  enableJsonResponse,
1231
1290
  endpoint,
1232
- port
1291
+ port,
1292
+ stateless
1233
1293
  },
1234
1294
  transportType: "httpStream"
1235
1295
  };