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/src/FastMCP.ts CHANGED
@@ -2018,6 +2018,7 @@ export class FastMCP<
2018
2018
  endpoint?: `/${string}`;
2019
2019
  eventStore?: EventStore;
2020
2020
  port: number;
2021
+ stateless?: boolean;
2021
2022
  };
2022
2023
  transportType: "httpStream" | "stdio";
2023
2024
  }>,
@@ -2050,151 +2051,86 @@ export class FastMCP<
2050
2051
  } else if (config.transportType === "httpStream") {
2051
2052
  const httpConfig = config.httpStream;
2052
2053
 
2053
- this.#httpStreamServer = await startHTTPServer<FastMCPSession<T>>({
2054
- createServer: async (request) => {
2055
- let auth: T | undefined;
2056
-
2057
- if (this.#authenticate) {
2058
- auth = await this.#authenticate(request);
2059
- }
2060
- const allowedTools = auth
2061
- ? this.#tools.filter((tool) =>
2062
- tool.canAccess ? tool.canAccess(auth) : true,
2063
- )
2064
- : this.#tools;
2065
- return new FastMCPSession<T>({
2066
- auth,
2067
- name: this.#options.name,
2068
- ping: this.#options.ping,
2069
- prompts: this.#prompts,
2070
- resources: this.#resources,
2071
- resourcesTemplates: this.#resourcesTemplates,
2072
- roots: this.#options.roots,
2073
- tools: allowedTools,
2074
- transportType: "httpStream",
2075
- utils: this.#options.utils,
2076
- version: this.#options.version,
2077
- });
2078
- },
2079
- enableJsonResponse: httpConfig.enableJsonResponse,
2080
- eventStore: httpConfig.eventStore,
2081
- onClose: async (session) => {
2082
- this.emit("disconnect", {
2083
- session: session as FastMCPSession<FastMCPSessionAuth>,
2084
- });
2085
- },
2086
- onConnect: async (session) => {
2087
- this.#sessions.push(session);
2088
-
2089
- console.info(`[FastMCP info] HTTP Stream session established`);
2090
-
2091
- this.emit("connect", {
2092
- session: session as FastMCPSession<FastMCPSessionAuth>,
2093
- });
2094
- },
2095
-
2096
- onUnhandledRequest: async (req, res) => {
2097
- const healthConfig = this.#options.health ?? {};
2054
+ if (httpConfig.stateless) {
2055
+ // Stateless mode - create new server instance for each request
2056
+ console.info(
2057
+ `[FastMCP info] Starting server in stateless mode on HTTP Stream at http://localhost:${httpConfig.port}${httpConfig.endpoint}`,
2058
+ );
2098
2059
 
2099
- const enabled =
2100
- healthConfig.enabled === undefined ? true : healthConfig.enabled;
2060
+ this.#httpStreamServer = await startHTTPServer<FastMCPSession<T>>({
2061
+ createServer: async (request) => {
2062
+ let auth: T | undefined;
2101
2063
 
2102
- if (enabled) {
2103
- const path = healthConfig.path ?? "/health";
2104
- const url = new URL(req.url || "", "http://localhost");
2064
+ if (this.#authenticate) {
2065
+ auth = await this.#authenticate(request);
2066
+ }
2105
2067
 
2106
- try {
2107
- if (req.method === "GET" && url.pathname === path) {
2108
- res
2109
- .writeHead(healthConfig.status ?? 200, {
2110
- "Content-Type": "text/plain",
2111
- })
2112
- .end(healthConfig.message ?? "✓ Ok");
2113
-
2114
- return;
2115
- }
2068
+ // In stateless mode, create a new session for each request
2069
+ // without persisting it in the sessions array
2070
+ return this.#createSession(auth);
2071
+ },
2072
+ enableJsonResponse: httpConfig.enableJsonResponse,
2073
+ eventStore: httpConfig.eventStore,
2074
+ // In stateless mode, we don't track sessions
2075
+ onClose: async () => {
2076
+ // No session tracking in stateless mode
2077
+ },
2078
+ onConnect: async () => {
2079
+ // No persistent session tracking in stateless mode
2080
+ console.debug(
2081
+ `[FastMCP debug] Stateless HTTP Stream request handled`,
2082
+ );
2083
+ },
2084
+ onUnhandledRequest: async (req, res) => {
2085
+ await this.#handleUnhandledRequest(req, res, true);
2086
+ },
2087
+ port: httpConfig.port,
2088
+ stateless: true,
2089
+ streamEndpoint: httpConfig.endpoint,
2090
+ });
2091
+ } else {
2092
+ // Regular mode with session management
2093
+ this.#httpStreamServer = await startHTTPServer<FastMCPSession<T>>({
2094
+ createServer: async (request) => {
2095
+ let auth: T | undefined;
2116
2096
 
2117
- // Enhanced readiness check endpoint
2118
- if (req.method === "GET" && url.pathname === "/ready") {
2119
- const readySessions = this.#sessions.filter(
2120
- (s) => s.isReady,
2121
- ).length;
2122
- const totalSessions = this.#sessions.length;
2123
- const allReady =
2124
- readySessions === totalSessions && totalSessions > 0;
2125
-
2126
- const response = {
2127
- ready: readySessions,
2128
- status: allReady
2129
- ? "ready"
2130
- : totalSessions === 0
2131
- ? "no_sessions"
2132
- : "initializing",
2133
- total: totalSessions,
2134
- };
2135
-
2136
- res
2137
- .writeHead(allReady ? 200 : 503, {
2138
- "Content-Type": "application/json",
2139
- })
2140
- .end(JSON.stringify(response));
2141
-
2142
- return;
2143
- }
2144
- } catch (error) {
2145
- console.error("[FastMCP error] health endpoint error", error);
2097
+ if (this.#authenticate) {
2098
+ auth = await this.#authenticate(request);
2146
2099
  }
2147
- }
2148
2100
 
2149
- // Handle OAuth well-known endpoints
2150
- const oauthConfig = this.#options.oauth;
2151
- if (oauthConfig?.enabled && req.method === "GET") {
2152
- const url = new URL(req.url || "", "http://localhost");
2153
-
2154
- if (
2155
- url.pathname === "/.well-known/oauth-authorization-server" &&
2156
- oauthConfig.authorizationServer
2157
- ) {
2158
- const metadata = convertObjectToSnakeCase(
2159
- oauthConfig.authorizationServer,
2160
- );
2161
- res
2162
- .writeHead(200, {
2163
- "Content-Type": "application/json",
2164
- })
2165
- .end(JSON.stringify(metadata));
2166
- return;
2167
- }
2101
+ return this.#createSession(auth);
2102
+ },
2103
+ enableJsonResponse: httpConfig.enableJsonResponse,
2104
+ eventStore: httpConfig.eventStore,
2105
+ onClose: async (session) => {
2106
+ this.emit("disconnect", {
2107
+ session: session as FastMCPSession<FastMCPSessionAuth>,
2108
+ });
2109
+ },
2110
+ onConnect: async (session) => {
2111
+ this.#sessions.push(session);
2168
2112
 
2169
- if (
2170
- url.pathname === "/.well-known/oauth-protected-resource" &&
2171
- oauthConfig.protectedResource
2172
- ) {
2173
- const metadata = convertObjectToSnakeCase(
2174
- oauthConfig.protectedResource,
2175
- );
2176
- res
2177
- .writeHead(200, {
2178
- "Content-Type": "application/json",
2179
- })
2180
- .end(JSON.stringify(metadata));
2181
- return;
2182
- }
2183
- }
2113
+ console.info(`[FastMCP info] HTTP Stream session established`);
2184
2114
 
2185
- // If the request was not handled above, return 404
2186
- res.writeHead(404).end();
2187
- },
2188
- port: httpConfig.port,
2189
- streamEndpoint: httpConfig.endpoint,
2190
- });
2115
+ this.emit("connect", {
2116
+ session: session as FastMCPSession<FastMCPSessionAuth>,
2117
+ });
2118
+ },
2191
2119
 
2192
- console.info(
2193
- `[FastMCP info] server is running on HTTP Stream at http://localhost:${httpConfig.port}${httpConfig.endpoint}`,
2194
- );
2195
- console.info(
2196
- `[FastMCP info] Transport type: httpStream (Streamable HTTP, not SSE)`,
2197
- );
2120
+ onUnhandledRequest: async (req, res) => {
2121
+ await this.#handleUnhandledRequest(req, res, false);
2122
+ },
2123
+ port: httpConfig.port,
2124
+ streamEndpoint: httpConfig.endpoint,
2125
+ });
2126
+
2127
+ console.info(
2128
+ `[FastMCP info] server is running on HTTP Stream at http://localhost:${httpConfig.port}${httpConfig.endpoint}`,
2129
+ );
2130
+ console.info(
2131
+ `[FastMCP info] Transport type: httpStream (Streamable HTTP, not SSE)`,
2132
+ );
2133
+ }
2198
2134
  } else {
2199
2135
  throw new Error("Invalid transport type");
2200
2136
  }
@@ -2209,12 +2145,153 @@ export class FastMCP<
2209
2145
  }
2210
2146
  }
2211
2147
 
2148
+ /**
2149
+ * Creates a new FastMCPSession instance with the current configuration.
2150
+ * Used both for regular sessions and stateless requests.
2151
+ */
2152
+ #createSession(auth?: T): FastMCPSession<T> {
2153
+ const allowedTools = auth
2154
+ ? this.#tools.filter((tool) =>
2155
+ tool.canAccess ? tool.canAccess(auth) : true,
2156
+ )
2157
+ : this.#tools;
2158
+ return new FastMCPSession<T>({
2159
+ auth,
2160
+ name: this.#options.name,
2161
+ ping: this.#options.ping,
2162
+ prompts: this.#prompts,
2163
+ resources: this.#resources,
2164
+ resourcesTemplates: this.#resourcesTemplates,
2165
+ roots: this.#options.roots,
2166
+ tools: allowedTools,
2167
+ transportType: "httpStream",
2168
+ utils: this.#options.utils,
2169
+ version: this.#options.version,
2170
+ });
2171
+ }
2172
+
2173
+ /**
2174
+ * Handles unhandled HTTP requests with health, readiness, and OAuth endpoints
2175
+ */
2176
+ #handleUnhandledRequest = async (
2177
+ req: http.IncomingMessage,
2178
+ res: http.ServerResponse,
2179
+ isStateless = false,
2180
+ ) => {
2181
+ const healthConfig = this.#options.health ?? {};
2182
+
2183
+ const enabled =
2184
+ healthConfig.enabled === undefined ? true : healthConfig.enabled;
2185
+
2186
+ if (enabled) {
2187
+ const path = healthConfig.path ?? "/health";
2188
+ const url = new URL(req.url || "", "http://localhost");
2189
+
2190
+ try {
2191
+ if (req.method === "GET" && url.pathname === path) {
2192
+ res
2193
+ .writeHead(healthConfig.status ?? 200, {
2194
+ "Content-Type": "text/plain",
2195
+ })
2196
+ .end(healthConfig.message ?? "✓ Ok");
2197
+
2198
+ return;
2199
+ }
2200
+
2201
+ // Enhanced readiness check endpoint
2202
+ if (req.method === "GET" && url.pathname === "/ready") {
2203
+ if (isStateless) {
2204
+ // In stateless mode, we're always ready if the server is running
2205
+ const response = {
2206
+ mode: "stateless",
2207
+ ready: 1,
2208
+ status: "ready",
2209
+ total: 1,
2210
+ };
2211
+
2212
+ res
2213
+ .writeHead(200, {
2214
+ "Content-Type": "application/json",
2215
+ })
2216
+ .end(JSON.stringify(response));
2217
+ } else {
2218
+ const readySessions = this.#sessions.filter(
2219
+ (s) => s.isReady,
2220
+ ).length;
2221
+ const totalSessions = this.#sessions.length;
2222
+ const allReady =
2223
+ readySessions === totalSessions && totalSessions > 0;
2224
+
2225
+ const response = {
2226
+ ready: readySessions,
2227
+ status: allReady
2228
+ ? "ready"
2229
+ : totalSessions === 0
2230
+ ? "no_sessions"
2231
+ : "initializing",
2232
+ total: totalSessions,
2233
+ };
2234
+
2235
+ res
2236
+ .writeHead(allReady ? 200 : 503, {
2237
+ "Content-Type": "application/json",
2238
+ })
2239
+ .end(JSON.stringify(response));
2240
+ }
2241
+
2242
+ return;
2243
+ }
2244
+ } catch (error) {
2245
+ console.error("[FastMCP error] health endpoint error", error);
2246
+ }
2247
+ }
2248
+
2249
+ // Handle OAuth well-known endpoints
2250
+ const oauthConfig = this.#options.oauth;
2251
+ if (oauthConfig?.enabled && req.method === "GET") {
2252
+ const url = new URL(req.url || "", "http://localhost");
2253
+
2254
+ if (
2255
+ url.pathname === "/.well-known/oauth-authorization-server" &&
2256
+ oauthConfig.authorizationServer
2257
+ ) {
2258
+ const metadata = convertObjectToSnakeCase(
2259
+ oauthConfig.authorizationServer,
2260
+ );
2261
+ res
2262
+ .writeHead(200, {
2263
+ "Content-Type": "application/json",
2264
+ })
2265
+ .end(JSON.stringify(metadata));
2266
+ return;
2267
+ }
2268
+
2269
+ if (
2270
+ url.pathname === "/.well-known/oauth-protected-resource" &&
2271
+ oauthConfig.protectedResource
2272
+ ) {
2273
+ const metadata = convertObjectToSnakeCase(
2274
+ oauthConfig.protectedResource,
2275
+ );
2276
+ res
2277
+ .writeHead(200, {
2278
+ "Content-Type": "application/json",
2279
+ })
2280
+ .end(JSON.stringify(metadata));
2281
+ return;
2282
+ }
2283
+ }
2284
+
2285
+ // If the request was not handled above, return 404
2286
+ res.writeHead(404).end();
2287
+ };
2212
2288
  #parseRuntimeConfig(
2213
2289
  overrides?: Partial<{
2214
2290
  httpStream: {
2215
2291
  enableJsonResponse?: boolean;
2216
2292
  endpoint?: `/${string}`;
2217
2293
  port: number;
2294
+ stateless?: boolean;
2218
2295
  };
2219
2296
  transportType: "httpStream" | "stdio";
2220
2297
  }>,
@@ -2225,6 +2302,7 @@ export class FastMCP<
2225
2302
  endpoint: `/${string}`;
2226
2303
  eventStore?: EventStore;
2227
2304
  port: number;
2305
+ stateless?: boolean;
2228
2306
  };
2229
2307
  transportType: "httpStream";
2230
2308
  }
@@ -2241,10 +2319,12 @@ export class FastMCP<
2241
2319
  const transportArg = getArg("transport");
2242
2320
  const portArg = getArg("port");
2243
2321
  const endpointArg = getArg("endpoint");
2322
+ const statelessArg = getArg("stateless");
2244
2323
 
2245
2324
  const envTransport = process.env.FASTMCP_TRANSPORT;
2246
2325
  const envPort = process.env.FASTMCP_PORT;
2247
2326
  const envEndpoint = process.env.FASTMCP_ENDPOINT;
2327
+ const envStateless = process.env.FASTMCP_STATELESS;
2248
2328
 
2249
2329
  // Overrides > CLI > env > defaults
2250
2330
  const transportType =
@@ -2261,12 +2341,18 @@ export class FastMCP<
2261
2341
  overrides?.httpStream?.endpoint || endpointArg || envEndpoint || "/mcp";
2262
2342
  const enableJsonResponse =
2263
2343
  overrides?.httpStream?.enableJsonResponse || false;
2344
+ const stateless =
2345
+ overrides?.httpStream?.stateless ||
2346
+ statelessArg === "true" ||
2347
+ envStateless === "true" ||
2348
+ false;
2264
2349
 
2265
2350
  return {
2266
2351
  httpStream: {
2267
2352
  enableJsonResponse,
2268
2353
  endpoint: endpoint as `/${string}`,
2269
2354
  port,
2355
+ stateless,
2270
2356
  },
2271
2357
  transportType: "httpStream" as const,
2272
2358
  };