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 +47 -0
- package/dist/FastMCP.d.ts +1 -0
- package/dist/FastMCP.js +162 -102
- package/dist/FastMCP.js.map +1 -1
- package/jsr.json +1 -1
- package/package.json +2 -2
- package/src/FastMCP.test.ts +112 -0
- package/src/FastMCP.ts +222 -136
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
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
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
|
-
|
|
2100
|
-
|
|
2060
|
+
this.#httpStreamServer = await startHTTPServer<FastMCPSession<T>>({
|
|
2061
|
+
createServer: async (request) => {
|
|
2062
|
+
let auth: T | undefined;
|
|
2101
2063
|
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2064
|
+
if (this.#authenticate) {
|
|
2065
|
+
auth = await this.#authenticate(request);
|
|
2066
|
+
}
|
|
2105
2067
|
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
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
|
-
|
|
2118
|
-
|
|
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
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
streamEndpoint: httpConfig.endpoint,
|
|
2190
|
-
});
|
|
2115
|
+
this.emit("connect", {
|
|
2116
|
+
session: session as FastMCPSession<FastMCPSessionAuth>,
|
|
2117
|
+
});
|
|
2118
|
+
},
|
|
2191
2119
|
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
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
|
};
|