fastmcp 3.12.0 → 3.14.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
@@ -38,6 +38,14 @@ import parseURITemplate from "uri-templates";
38
38
  import { toJsonSchema } from "xsschema";
39
39
  import { z } from "zod";
40
40
 
41
+ export interface Logger {
42
+ debug(...args: unknown[]): void;
43
+ error(...args: unknown[]): void;
44
+ info(...args: unknown[]): void;
45
+ log(...args: unknown[]): void;
46
+ warn(...args: unknown[]): void;
47
+ }
48
+
41
49
  export type SSEServer = {
42
50
  close: () => Promise<void>;
43
51
  };
@@ -570,6 +578,11 @@ type ServerOptions<T extends FastMCPSessionAuth> = {
570
578
  status?: number;
571
579
  };
572
580
  instructions?: string;
581
+ /**
582
+ * Custom logger instance. If not provided, defaults to console.
583
+ * Use this to integrate with your own logging system.
584
+ */
585
+ logger?: Logger;
573
586
  name: string;
574
587
 
575
588
  /**
@@ -924,6 +937,7 @@ export class FastMCPSession<
924
937
  #capabilities: ServerCapabilities = {};
925
938
  #clientCapabilities?: ClientCapabilities;
926
939
  #connectionState: "closed" | "connecting" | "error" | "ready" = "connecting";
940
+ #logger: Logger;
927
941
  #loggingLevel: LoggingLevel = "info";
928
942
  #needsEventLoopFlush: boolean = false;
929
943
  #pingConfig?: ServerOptions<T>["ping"];
@@ -947,6 +961,7 @@ export class FastMCPSession<
947
961
  constructor({
948
962
  auth,
949
963
  instructions,
964
+ logger,
950
965
  name,
951
966
  ping,
952
967
  prompts,
@@ -960,6 +975,7 @@ export class FastMCPSession<
960
975
  }: {
961
976
  auth?: T;
962
977
  instructions?: string;
978
+ logger: Logger;
963
979
  name: string;
964
980
  ping?: ServerOptions<T>["ping"];
965
981
  prompts: Prompt<T>[];
@@ -974,6 +990,7 @@ export class FastMCPSession<
974
990
  super();
975
991
 
976
992
  this.#auth = auth;
993
+ this.#logger = logger;
977
994
  this.#pingConfig = ping;
978
995
  this.#rootsConfig = roots;
979
996
  this.#needsEventLoopFlush = transportType === "httpStream";
@@ -1043,7 +1060,7 @@ export class FastMCPSession<
1043
1060
  try {
1044
1061
  await this.#server.close();
1045
1062
  } catch (error) {
1046
- console.error("[FastMCP error]", "could not close server", error);
1063
+ this.#logger.error("[FastMCP error]", "could not close server", error);
1047
1064
  }
1048
1065
  }
1049
1066
 
@@ -1073,7 +1090,7 @@ export class FastMCPSession<
1073
1090
  }
1074
1091
 
1075
1092
  if (!this.#clientCapabilities) {
1076
- console.warn(
1093
+ this.#logger.warn(
1077
1094
  `[FastMCP warning] could not infer client capabilities after ${maxAttempts} attempts. Connection may be unstable.`,
1078
1095
  );
1079
1096
  }
@@ -1087,11 +1104,11 @@ export class FastMCPSession<
1087
1104
  this.#roots = roots?.roots || [];
1088
1105
  } catch (e) {
1089
1106
  if (e instanceof McpError && e.code === ErrorCode.MethodNotFound) {
1090
- console.debug(
1107
+ this.#logger.debug(
1091
1108
  "[FastMCP debug] listRoots method not supported by client",
1092
1109
  );
1093
1110
  } else {
1094
- console.error(
1111
+ this.#logger.error(
1095
1112
  `[FastMCP error] received error listing roots.\n\n${
1096
1113
  e instanceof Error ? e.stack : JSON.stringify(e)
1097
1114
  }`,
@@ -1114,17 +1131,17 @@ export class FastMCPSession<
1114
1131
  const logLevel = pingConfig.logLevel;
1115
1132
 
1116
1133
  if (logLevel === "debug") {
1117
- console.debug("[FastMCP debug] server ping failed");
1134
+ this.#logger.debug("[FastMCP debug] server ping failed");
1118
1135
  } else if (logLevel === "warning") {
1119
- console.warn(
1136
+ this.#logger.warn(
1120
1137
  "[FastMCP warning] server is not responding to ping",
1121
1138
  );
1122
1139
  } else if (logLevel === "error") {
1123
- console.error(
1140
+ this.#logger.error(
1124
1141
  "[FastMCP error] server is not responding to ping",
1125
1142
  );
1126
1143
  } else {
1127
- console.info("[FastMCP info] server ping failed");
1144
+ this.#logger.info("[FastMCP info] server ping failed");
1128
1145
  }
1129
1146
  }
1130
1147
  }, pingConfig.intervalMs);
@@ -1360,7 +1377,7 @@ export class FastMCPSession<
1360
1377
 
1361
1378
  private setupErrorHandling() {
1362
1379
  this.#server.onerror = (error) => {
1363
- console.error("[FastMCP error]", error);
1380
+ this.#logger.error("[FastMCP error]", error);
1364
1381
  };
1365
1382
  }
1366
1383
 
@@ -1564,7 +1581,7 @@ export class FastMCPSession<
1564
1581
 
1565
1582
  private setupRootsHandlers() {
1566
1583
  if (this.#rootsConfig?.enabled === false) {
1567
- console.debug(
1584
+ this.#logger.debug(
1568
1585
  "[FastMCP debug] roots capability explicitly disabled via config",
1569
1586
  );
1570
1587
  return;
@@ -1589,11 +1606,11 @@ export class FastMCPSession<
1589
1606
  error instanceof McpError &&
1590
1607
  error.code === ErrorCode.MethodNotFound
1591
1608
  ) {
1592
- console.debug(
1609
+ this.#logger.debug(
1593
1610
  "[FastMCP debug] listRoots method not supported by client",
1594
1611
  );
1595
1612
  } else {
1596
- console.error(
1613
+ this.#logger.error(
1597
1614
  `[FastMCP error] received error listing roots.\n\n${
1598
1615
  error instanceof Error ? error.stack : JSON.stringify(error)
1599
1616
  }`,
@@ -1603,7 +1620,7 @@ export class FastMCPSession<
1603
1620
  },
1604
1621
  );
1605
1622
  } else {
1606
- console.debug(
1623
+ this.#logger.debug(
1607
1624
  "[FastMCP debug] roots capability not available, not setting up notification handler",
1608
1625
  );
1609
1626
  }
@@ -1686,7 +1703,7 @@ export class FastMCPSession<
1686
1703
  await new Promise((resolve) => setImmediate(resolve));
1687
1704
  }
1688
1705
  } catch (progressError) {
1689
- console.warn(
1706
+ this.#logger.warn(
1690
1707
  `[FastMCP warning] Failed to report progress for tool '${request.params.name}':`,
1691
1708
  progressError instanceof Error
1692
1709
  ? progressError.message
@@ -1753,7 +1770,7 @@ export class FastMCPSession<
1753
1770
  await new Promise((resolve) => setImmediate(resolve));
1754
1771
  }
1755
1772
  } catch (streamError) {
1756
- console.warn(
1773
+ this.#logger.warn(
1757
1774
  `[FastMCP warning] Failed to stream content for tool '${request.params.name}':`,
1758
1775
  streamError instanceof Error
1759
1776
  ? streamError.message
@@ -1879,6 +1896,7 @@ export class FastMCP<
1879
1896
  }
1880
1897
  #authenticate: Authenticate<T> | undefined;
1881
1898
  #httpStreamServer: null | SSEServer = null;
1899
+ #logger: Logger;
1882
1900
  #options: ServerOptions<T>;
1883
1901
  #prompts: InputPrompt<T>[] = [];
1884
1902
  #resources: Resource<T>[] = [];
@@ -1892,6 +1910,7 @@ export class FastMCP<
1892
1910
 
1893
1911
  this.#options = options;
1894
1912
  this.#authenticate = options.authenticate;
1913
+ this.#logger = options.logger || console;
1895
1914
  }
1896
1915
 
1897
1916
  /**
@@ -2018,6 +2037,7 @@ export class FastMCP<
2018
2037
  endpoint?: `/${string}`;
2019
2038
  eventStore?: EventStore;
2020
2039
  port: number;
2040
+ stateless?: boolean;
2021
2041
  };
2022
2042
  transportType: "httpStream" | "stdio";
2023
2043
  }>,
@@ -2028,6 +2048,7 @@ export class FastMCP<
2028
2048
  const transport = new StdioServerTransport();
2029
2049
  const session = new FastMCPSession<T>({
2030
2050
  instructions: this.#options.instructions,
2051
+ logger: this.#logger,
2031
2052
  name: this.#options.name,
2032
2053
  ping: this.#options.ping,
2033
2054
  prompts: this.#prompts,
@@ -2050,151 +2071,86 @@ export class FastMCP<
2050
2071
  } else if (config.transportType === "httpStream") {
2051
2072
  const httpConfig = config.httpStream;
2052
2073
 
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 ?? {};
2074
+ if (httpConfig.stateless) {
2075
+ // Stateless mode - create new server instance for each request
2076
+ this.#logger.info(
2077
+ `[FastMCP info] Starting server in stateless mode on HTTP Stream at http://localhost:${httpConfig.port}${httpConfig.endpoint}`,
2078
+ );
2098
2079
 
2099
- const enabled =
2100
- healthConfig.enabled === undefined ? true : healthConfig.enabled;
2080
+ this.#httpStreamServer = await startHTTPServer<FastMCPSession<T>>({
2081
+ createServer: async (request) => {
2082
+ let auth: T | undefined;
2101
2083
 
2102
- if (enabled) {
2103
- const path = healthConfig.path ?? "/health";
2104
- const url = new URL(req.url || "", "http://localhost");
2084
+ if (this.#authenticate) {
2085
+ auth = await this.#authenticate(request);
2086
+ }
2105
2087
 
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
- }
2088
+ // In stateless mode, create a new session for each request
2089
+ // without persisting it in the sessions array
2090
+ return this.#createSession(auth);
2091
+ },
2092
+ enableJsonResponse: httpConfig.enableJsonResponse,
2093
+ eventStore: httpConfig.eventStore,
2094
+ // In stateless mode, we don't track sessions
2095
+ onClose: async () => {
2096
+ // No session tracking in stateless mode
2097
+ },
2098
+ onConnect: async () => {
2099
+ // No persistent session tracking in stateless mode
2100
+ this.#logger.debug(
2101
+ `[FastMCP debug] Stateless HTTP Stream request handled`,
2102
+ );
2103
+ },
2104
+ onUnhandledRequest: async (req, res) => {
2105
+ await this.#handleUnhandledRequest(req, res, true);
2106
+ },
2107
+ port: httpConfig.port,
2108
+ stateless: true,
2109
+ streamEndpoint: httpConfig.endpoint,
2110
+ });
2111
+ } else {
2112
+ // Regular mode with session management
2113
+ this.#httpStreamServer = await startHTTPServer<FastMCPSession<T>>({
2114
+ createServer: async (request) => {
2115
+ let auth: T | undefined;
2116
2116
 
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);
2117
+ if (this.#authenticate) {
2118
+ auth = await this.#authenticate(request);
2146
2119
  }
2147
- }
2148
2120
 
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
- }
2121
+ return this.#createSession(auth);
2122
+ },
2123
+ enableJsonResponse: httpConfig.enableJsonResponse,
2124
+ eventStore: httpConfig.eventStore,
2125
+ onClose: async (session) => {
2126
+ this.emit("disconnect", {
2127
+ session: session as FastMCPSession<FastMCPSessionAuth>,
2128
+ });
2129
+ },
2130
+ onConnect: async (session) => {
2131
+ this.#sessions.push(session);
2168
2132
 
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
- }
2133
+ this.#logger.info(`[FastMCP info] HTTP Stream session established`);
2184
2134
 
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
- });
2135
+ this.emit("connect", {
2136
+ session: session as FastMCPSession<FastMCPSessionAuth>,
2137
+ });
2138
+ },
2191
2139
 
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
- );
2140
+ onUnhandledRequest: async (req, res) => {
2141
+ await this.#handleUnhandledRequest(req, res, false);
2142
+ },
2143
+ port: httpConfig.port,
2144
+ streamEndpoint: httpConfig.endpoint,
2145
+ });
2146
+
2147
+ this.#logger.info(
2148
+ `[FastMCP info] server is running on HTTP Stream at http://localhost:${httpConfig.port}${httpConfig.endpoint}`,
2149
+ );
2150
+ this.#logger.info(
2151
+ `[FastMCP info] Transport type: httpStream (Streamable HTTP, not SSE)`,
2152
+ );
2153
+ }
2198
2154
  } else {
2199
2155
  throw new Error("Invalid transport type");
2200
2156
  }
@@ -2209,12 +2165,154 @@ export class FastMCP<
2209
2165
  }
2210
2166
  }
2211
2167
 
2168
+ /**
2169
+ * Creates a new FastMCPSession instance with the current configuration.
2170
+ * Used both for regular sessions and stateless requests.
2171
+ */
2172
+ #createSession(auth?: T): FastMCPSession<T> {
2173
+ const allowedTools = auth
2174
+ ? this.#tools.filter((tool) =>
2175
+ tool.canAccess ? tool.canAccess(auth) : true,
2176
+ )
2177
+ : this.#tools;
2178
+ return new FastMCPSession<T>({
2179
+ auth,
2180
+ logger: this.#logger,
2181
+ name: this.#options.name,
2182
+ ping: this.#options.ping,
2183
+ prompts: this.#prompts,
2184
+ resources: this.#resources,
2185
+ resourcesTemplates: this.#resourcesTemplates,
2186
+ roots: this.#options.roots,
2187
+ tools: allowedTools,
2188
+ transportType: "httpStream",
2189
+ utils: this.#options.utils,
2190
+ version: this.#options.version,
2191
+ });
2192
+ }
2193
+
2194
+ /**
2195
+ * Handles unhandled HTTP requests with health, readiness, and OAuth endpoints
2196
+ */
2197
+ #handleUnhandledRequest = async (
2198
+ req: http.IncomingMessage,
2199
+ res: http.ServerResponse,
2200
+ isStateless = false,
2201
+ ) => {
2202
+ const healthConfig = this.#options.health ?? {};
2203
+
2204
+ const enabled =
2205
+ healthConfig.enabled === undefined ? true : healthConfig.enabled;
2206
+
2207
+ if (enabled) {
2208
+ const path = healthConfig.path ?? "/health";
2209
+ const url = new URL(req.url || "", "http://localhost");
2210
+
2211
+ try {
2212
+ if (req.method === "GET" && url.pathname === path) {
2213
+ res
2214
+ .writeHead(healthConfig.status ?? 200, {
2215
+ "Content-Type": "text/plain",
2216
+ })
2217
+ .end(healthConfig.message ?? "✓ Ok");
2218
+
2219
+ return;
2220
+ }
2221
+
2222
+ // Enhanced readiness check endpoint
2223
+ if (req.method === "GET" && url.pathname === "/ready") {
2224
+ if (isStateless) {
2225
+ // In stateless mode, we're always ready if the server is running
2226
+ const response = {
2227
+ mode: "stateless",
2228
+ ready: 1,
2229
+ status: "ready",
2230
+ total: 1,
2231
+ };
2232
+
2233
+ res
2234
+ .writeHead(200, {
2235
+ "Content-Type": "application/json",
2236
+ })
2237
+ .end(JSON.stringify(response));
2238
+ } else {
2239
+ const readySessions = this.#sessions.filter(
2240
+ (s) => s.isReady,
2241
+ ).length;
2242
+ const totalSessions = this.#sessions.length;
2243
+ const allReady =
2244
+ readySessions === totalSessions && totalSessions > 0;
2245
+
2246
+ const response = {
2247
+ ready: readySessions,
2248
+ status: allReady
2249
+ ? "ready"
2250
+ : totalSessions === 0
2251
+ ? "no_sessions"
2252
+ : "initializing",
2253
+ total: totalSessions,
2254
+ };
2255
+
2256
+ res
2257
+ .writeHead(allReady ? 200 : 503, {
2258
+ "Content-Type": "application/json",
2259
+ })
2260
+ .end(JSON.stringify(response));
2261
+ }
2262
+
2263
+ return;
2264
+ }
2265
+ } catch (error) {
2266
+ this.#logger.error("[FastMCP error] health endpoint error", error);
2267
+ }
2268
+ }
2269
+
2270
+ // Handle OAuth well-known endpoints
2271
+ const oauthConfig = this.#options.oauth;
2272
+ if (oauthConfig?.enabled && req.method === "GET") {
2273
+ const url = new URL(req.url || "", "http://localhost");
2274
+
2275
+ if (
2276
+ url.pathname === "/.well-known/oauth-authorization-server" &&
2277
+ oauthConfig.authorizationServer
2278
+ ) {
2279
+ const metadata = convertObjectToSnakeCase(
2280
+ oauthConfig.authorizationServer,
2281
+ );
2282
+ res
2283
+ .writeHead(200, {
2284
+ "Content-Type": "application/json",
2285
+ })
2286
+ .end(JSON.stringify(metadata));
2287
+ return;
2288
+ }
2289
+
2290
+ if (
2291
+ url.pathname === "/.well-known/oauth-protected-resource" &&
2292
+ oauthConfig.protectedResource
2293
+ ) {
2294
+ const metadata = convertObjectToSnakeCase(
2295
+ oauthConfig.protectedResource,
2296
+ );
2297
+ res
2298
+ .writeHead(200, {
2299
+ "Content-Type": "application/json",
2300
+ })
2301
+ .end(JSON.stringify(metadata));
2302
+ return;
2303
+ }
2304
+ }
2305
+
2306
+ // If the request was not handled above, return 404
2307
+ res.writeHead(404).end();
2308
+ };
2212
2309
  #parseRuntimeConfig(
2213
2310
  overrides?: Partial<{
2214
2311
  httpStream: {
2215
2312
  enableJsonResponse?: boolean;
2216
2313
  endpoint?: `/${string}`;
2217
2314
  port: number;
2315
+ stateless?: boolean;
2218
2316
  };
2219
2317
  transportType: "httpStream" | "stdio";
2220
2318
  }>,
@@ -2225,6 +2323,7 @@ export class FastMCP<
2225
2323
  endpoint: `/${string}`;
2226
2324
  eventStore?: EventStore;
2227
2325
  port: number;
2326
+ stateless?: boolean;
2228
2327
  };
2229
2328
  transportType: "httpStream";
2230
2329
  }
@@ -2241,10 +2340,12 @@ export class FastMCP<
2241
2340
  const transportArg = getArg("transport");
2242
2341
  const portArg = getArg("port");
2243
2342
  const endpointArg = getArg("endpoint");
2343
+ const statelessArg = getArg("stateless");
2244
2344
 
2245
2345
  const envTransport = process.env.FASTMCP_TRANSPORT;
2246
2346
  const envPort = process.env.FASTMCP_PORT;
2247
2347
  const envEndpoint = process.env.FASTMCP_ENDPOINT;
2348
+ const envStateless = process.env.FASTMCP_STATELESS;
2248
2349
 
2249
2350
  // Overrides > CLI > env > defaults
2250
2351
  const transportType =
@@ -2261,12 +2362,18 @@ export class FastMCP<
2261
2362
  overrides?.httpStream?.endpoint || endpointArg || envEndpoint || "/mcp";
2262
2363
  const enableJsonResponse =
2263
2364
  overrides?.httpStream?.enableJsonResponse || false;
2365
+ const stateless =
2366
+ overrides?.httpStream?.stateless ||
2367
+ statelessArg === "true" ||
2368
+ envStateless === "true" ||
2369
+ false;
2264
2370
 
2265
2371
  return {
2266
2372
  httpStream: {
2267
2373
  enableJsonResponse,
2268
2374
  endpoint: endpoint as `/${string}`,
2269
2375
  port,
2376
+ stateless,
2270
2377
  },
2271
2378
  transportType: "httpStream" as const,
2272
2379
  };