fastmcp 3.11.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 +132 -4
- package/dist/FastMCP.js +167 -103
- package/dist/FastMCP.js.map +1 -1
- package/jsr.json +1 -1
- package/package.json +2 -2
- package/src/FastMCP.oauth.test.ts +48 -0
- package/src/FastMCP.test.ts +112 -0
- package/src/FastMCP.ts +373 -141
- package/src/examples/oauth-server.ts +12 -0
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
|
@@ -293,20 +293,147 @@ type ServerOptions<T extends FastMCPSessionAuth> = {
|
|
|
293
293
|
*/
|
|
294
294
|
enabled: boolean;
|
|
295
295
|
/**
|
|
296
|
-
* OAuth Protected Resource metadata for
|
|
296
|
+
* OAuth Protected Resource metadata for `/.well-known/oauth-protected-resource`
|
|
297
297
|
*
|
|
298
|
-
* This endpoint follows
|
|
299
|
-
* and provides metadata
|
|
298
|
+
* This endpoint follows {@link https://www.rfc-editor.org/rfc/rfc9728.html | RFC 9728}
|
|
299
|
+
* and provides metadata describing how an OAuth 2.0 protected resource (in this case,
|
|
300
|
+
* an MCP server) expects to be accessed.
|
|
300
301
|
*
|
|
301
|
-
*
|
|
302
|
+
* When configured, FastMCP will automatically serve this metadata at the
|
|
303
|
+
* `/.well-known/oauth-protected-resource` endpoint. The `authorizationServers` and `resource`
|
|
304
|
+
* fields are required. All others are optional and will be omitted from the published
|
|
305
|
+
* metadata if not specified.
|
|
306
|
+
*
|
|
307
|
+
* This satisfies the requirements of the MCP Authorization specification's
|
|
308
|
+
* {@link https://modelcontextprotocol.io/specification/2025-06-18/basic/authorization#authorization-server-location | Authorization Server Location section}.
|
|
309
|
+
*
|
|
310
|
+
* Clients consuming this metadata MUST validate that any presented values comply with
|
|
311
|
+
* RFC 9728, including strict validation of the `resource` identifier and intended audience
|
|
312
|
+
* when access tokens are issued and presented (per RFC 8707 §2).
|
|
313
|
+
*
|
|
314
|
+
* @remarks Required by MCP Specification version 2025-06-18
|
|
302
315
|
*/
|
|
303
316
|
protectedResource?: {
|
|
317
|
+
/**
|
|
318
|
+
* Allows for additional metadata fields beyond those defined in RFC 9728.
|
|
319
|
+
*
|
|
320
|
+
* @remarks This supports vendor-specific or experimental extensions.
|
|
321
|
+
* @see {@link https://www.rfc-editor.org/rfc/rfc9728.html#section-2.3 | RFC 9728 §2.3}
|
|
322
|
+
*/
|
|
323
|
+
[key: string]: unknown;
|
|
324
|
+
/**
|
|
325
|
+
* Supported values for the `authorization_details` parameter (RFC 9396).
|
|
326
|
+
*
|
|
327
|
+
* @remarks Used when fine-grained access control is in play.
|
|
328
|
+
* @see {@link https://www.rfc-editor.org/rfc/rfc9728.html#section-2-2.23 | RFC 9728 §2.2.23}
|
|
329
|
+
*/
|
|
330
|
+
authorizationDetailsTypesSupported?: string[];
|
|
331
|
+
/**
|
|
332
|
+
* List of OAuth 2.0 authorization server issuer identifiers.
|
|
333
|
+
*
|
|
334
|
+
* These correspond to ASes that can issue access tokens for this protected resource.
|
|
335
|
+
* MCP clients use these values to locate the relevant `/.well-known/oauth-authorization-server`
|
|
336
|
+
* metadata for initiating the OAuth flow.
|
|
337
|
+
*
|
|
338
|
+
* @remarks Required by the MCP spec. MCP servers MUST provide at least one issuer.
|
|
339
|
+
* Clients are responsible for choosing among them (see RFC 9728 §7.6).
|
|
340
|
+
* @see {@link https://www.rfc-editor.org/rfc/rfc9728.html#section-2-2.3 | RFC 9728 §2.2.3}
|
|
341
|
+
*/
|
|
304
342
|
authorizationServers: string[];
|
|
343
|
+
/**
|
|
344
|
+
* List of supported methods for presenting OAuth 2.0 bearer tokens.
|
|
345
|
+
*
|
|
346
|
+
* @remarks Valid values are `header`, `body`, and `query`.
|
|
347
|
+
* If omitted, clients MAY assume only `header` is supported, per RFC 6750.
|
|
348
|
+
* This is a client-side interpretation and not a serialization default.
|
|
349
|
+
* @see {@link https://www.rfc-editor.org/rfc/rfc9728.html#section-2-2.9 | RFC 9728 §2.2.9}
|
|
350
|
+
*/
|
|
305
351
|
bearerMethodsSupported?: string[];
|
|
352
|
+
/**
|
|
353
|
+
* Whether this resource requires all access tokens to be DPoP-bound.
|
|
354
|
+
*
|
|
355
|
+
* @remarks If omitted, clients SHOULD assume this is `false`.
|
|
356
|
+
* @see {@link https://www.rfc-editor.org/rfc/rfc9728.html#section-2-2.27 | RFC 9728 §2.2.27}
|
|
357
|
+
*/
|
|
358
|
+
dpopBoundAccessTokensRequired?: boolean;
|
|
359
|
+
/**
|
|
360
|
+
* Supported algorithms for verifying DPoP proofs (RFC 9449).
|
|
361
|
+
*
|
|
362
|
+
* @see {@link https://www.rfc-editor.org/rfc/rfc9728.html#section-2-2.25 | RFC 9728 §2.2.25}
|
|
363
|
+
*/
|
|
364
|
+
dpopSigningAlgValuesSupported?: string[];
|
|
365
|
+
/**
|
|
366
|
+
* JWKS URI of this resource. Used to validate access tokens or sign responses.
|
|
367
|
+
*
|
|
368
|
+
* @remarks When present, this MUST be an `https:` URI pointing to a valid JWK Set (RFC 7517).
|
|
369
|
+
* @see {@link https://www.rfc-editor.org/rfc/rfc9728.html#section-2-2.5 | RFC 9728 §2.2.5}
|
|
370
|
+
*/
|
|
306
371
|
jwksUri?: string;
|
|
372
|
+
/**
|
|
373
|
+
* Canonical OAuth resource identifier for this protected resource (the MCP server).
|
|
374
|
+
*
|
|
375
|
+
* @remarks Typically the base URL of the MCP server. Clients MUST use this as the
|
|
376
|
+
* `resource` parameter in authorization and token requests (per RFC 8707).
|
|
377
|
+
* @see {@link https://www.rfc-editor.org/rfc/rfc9728.html#section-2-2.1 | RFC 9728 §2.2.1}
|
|
378
|
+
*/
|
|
307
379
|
resource: string;
|
|
380
|
+
/**
|
|
381
|
+
* URL to developer-accessible documentation for this resource.
|
|
382
|
+
*
|
|
383
|
+
* @remarks This field MAY be localized.
|
|
384
|
+
* @see {@link https://www.rfc-editor.org/rfc/rfc9728.html#section-2-2.15 | RFC 9728 §2.2.15}
|
|
385
|
+
*/
|
|
308
386
|
resourceDocumentation?: string;
|
|
387
|
+
/**
|
|
388
|
+
* Human-readable name for display purposes (e.g., in UIs).
|
|
389
|
+
*
|
|
390
|
+
* @remarks This field MAY be localized using language tags (`resource_name#en`, etc.).
|
|
391
|
+
* @see {@link https://www.rfc-editor.org/rfc/rfc9728.html#section-2-2.13 | RFC 9728 §2.2.13}
|
|
392
|
+
*/
|
|
393
|
+
resourceName?: string;
|
|
394
|
+
/**
|
|
395
|
+
* URL to a human-readable policy page describing acceptable use.
|
|
396
|
+
*
|
|
397
|
+
* @remarks This field MAY be localized.
|
|
398
|
+
* @see {@link https://www.rfc-editor.org/rfc/rfc9728.html#section-2-2.17 | RFC 9728 §2.2.17}
|
|
399
|
+
*/
|
|
309
400
|
resourcePolicyUri?: string;
|
|
401
|
+
/**
|
|
402
|
+
* Supported JWS algorithms for signed responses from this resource (e.g., response signing).
|
|
403
|
+
*
|
|
404
|
+
* @remarks MUST NOT include `none`.
|
|
405
|
+
* @see {@link https://www.rfc-editor.org/rfc/rfc9728.html#section-2-2.11 | RFC 9728 §2.2.11}
|
|
406
|
+
*/
|
|
407
|
+
resourceSigningAlgValuesSupported?: string[];
|
|
408
|
+
/**
|
|
409
|
+
* URL to the protected resource’s Terms of Service.
|
|
410
|
+
*
|
|
411
|
+
* @remarks This field MAY be localized.
|
|
412
|
+
* @see {@link https://www.rfc-editor.org/rfc/rfc9728.html#section-2-2.19 | RFC 9728 §2.2.19}
|
|
413
|
+
*/
|
|
414
|
+
resourceTosUri?: string;
|
|
415
|
+
/**
|
|
416
|
+
* Supported OAuth scopes for requesting access to this resource.
|
|
417
|
+
*
|
|
418
|
+
* @remarks Useful for discovery, but clients SHOULD still request the minimal scope required.
|
|
419
|
+
* @see {@link https://www.rfc-editor.org/rfc/rfc9728.html#section-2-2.7 | RFC 9728 §2.2.7}
|
|
420
|
+
*/
|
|
421
|
+
scopesSupported?: string[];
|
|
422
|
+
/**
|
|
423
|
+
* Developer-accessible documentation for how to use the service (not end-user docs).
|
|
424
|
+
*
|
|
425
|
+
* @remarks Semantically equivalent to `resourceDocumentation`, but included under its
|
|
426
|
+
* alternate name for compatibility with tools or schemas expecting either.
|
|
427
|
+
* @see {@link https://www.rfc-editor.org/rfc/rfc9728.html#section-2-2.15 | RFC 9728 §2.2.15}
|
|
428
|
+
*/
|
|
429
|
+
serviceDocumentation?: string;
|
|
430
|
+
/**
|
|
431
|
+
* Whether mutual-TLS-bound access tokens are required.
|
|
432
|
+
*
|
|
433
|
+
* @remarks If omitted, clients SHOULD assume this is `false` (client-side behavior).
|
|
434
|
+
* @see {@link https://www.rfc-editor.org/rfc/rfc9728.html#section-2-2.21 | RFC 9728 §2.2.21}
|
|
435
|
+
*/
|
|
436
|
+
tlsClientCertificateBoundAccessTokens?: boolean;
|
|
310
437
|
};
|
|
311
438
|
};
|
|
312
439
|
ping?: {
|
|
@@ -479,6 +606,7 @@ declare class FastMCP<T extends FastMCPSessionAuth = FastMCPSessionAuth> extends
|
|
|
479
606
|
endpoint?: `/${string}`;
|
|
480
607
|
eventStore?: EventStore;
|
|
481
608
|
port: number;
|
|
609
|
+
stateless?: boolean;
|
|
482
610
|
};
|
|
483
611
|
transportType: "httpStream" | "stdio";
|
|
484
612
|
}>): Promise<void>;
|
package/dist/FastMCP.js
CHANGED
|
@@ -751,7 +751,11 @@ ${e instanceof Error ? e.stack : JSON.stringify(e)}`
|
|
|
751
751
|
"[FastMCP debug] listRoots method not supported by client"
|
|
752
752
|
);
|
|
753
753
|
} else {
|
|
754
|
-
console.error(
|
|
754
|
+
console.error(
|
|
755
|
+
`[FastMCP error] received error listing roots.
|
|
756
|
+
|
|
757
|
+
${error instanceof Error ? error.stack : JSON.stringify(error)}`
|
|
758
|
+
);
|
|
755
759
|
}
|
|
756
760
|
});
|
|
757
761
|
}
|
|
@@ -1087,109 +1091,71 @@ var FastMCP = class extends FastMCPEventEmitter {
|
|
|
1087
1091
|
});
|
|
1088
1092
|
} else if (config.transportType === "httpStream") {
|
|
1089
1093
|
const httpConfig = config.httpStream;
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
return new FastMCPSession({
|
|
1100
|
-
auth,
|
|
1101
|
-
name: this.#options.name,
|
|
1102
|
-
ping: this.#options.ping,
|
|
1103
|
-
prompts: this.#prompts,
|
|
1104
|
-
resources: this.#resources,
|
|
1105
|
-
resourcesTemplates: this.#resourcesTemplates,
|
|
1106
|
-
roots: this.#options.roots,
|
|
1107
|
-
tools: allowedTools,
|
|
1108
|
-
transportType: "httpStream",
|
|
1109
|
-
utils: this.#options.utils,
|
|
1110
|
-
version: this.#options.version
|
|
1111
|
-
});
|
|
1112
|
-
},
|
|
1113
|
-
enableJsonResponse: httpConfig.enableJsonResponse,
|
|
1114
|
-
eventStore: httpConfig.eventStore,
|
|
1115
|
-
onClose: async (session) => {
|
|
1116
|
-
this.emit("disconnect", {
|
|
1117
|
-
session
|
|
1118
|
-
});
|
|
1119
|
-
},
|
|
1120
|
-
onConnect: async (session) => {
|
|
1121
|
-
this.#sessions.push(session);
|
|
1122
|
-
console.info(`[FastMCP info] HTTP Stream session established`);
|
|
1123
|
-
this.emit("connect", {
|
|
1124
|
-
session
|
|
1125
|
-
});
|
|
1126
|
-
},
|
|
1127
|
-
onUnhandledRequest: async (req, res) => {
|
|
1128
|
-
const healthConfig = this.#options.health ?? {};
|
|
1129
|
-
const enabled = healthConfig.enabled === void 0 ? true : healthConfig.enabled;
|
|
1130
|
-
if (enabled) {
|
|
1131
|
-
const path = healthConfig.path ?? "/health";
|
|
1132
|
-
const url = new URL(req.url || "", "http://localhost");
|
|
1133
|
-
try {
|
|
1134
|
-
if (req.method === "GET" && url.pathname === path) {
|
|
1135
|
-
res.writeHead(healthConfig.status ?? 200, {
|
|
1136
|
-
"Content-Type": "text/plain"
|
|
1137
|
-
}).end(healthConfig.message ?? "\u2713 Ok");
|
|
1138
|
-
return;
|
|
1139
|
-
}
|
|
1140
|
-
if (req.method === "GET" && url.pathname === "/ready") {
|
|
1141
|
-
const readySessions = this.#sessions.filter(
|
|
1142
|
-
(s) => s.isReady
|
|
1143
|
-
).length;
|
|
1144
|
-
const totalSessions = this.#sessions.length;
|
|
1145
|
-
const allReady = readySessions === totalSessions && totalSessions > 0;
|
|
1146
|
-
const response = {
|
|
1147
|
-
ready: readySessions,
|
|
1148
|
-
status: allReady ? "ready" : totalSessions === 0 ? "no_sessions" : "initializing",
|
|
1149
|
-
total: totalSessions
|
|
1150
|
-
};
|
|
1151
|
-
res.writeHead(allReady ? 200 : 503, {
|
|
1152
|
-
"Content-Type": "application/json"
|
|
1153
|
-
}).end(JSON.stringify(response));
|
|
1154
|
-
return;
|
|
1155
|
-
}
|
|
1156
|
-
} catch (error) {
|
|
1157
|
-
console.error("[FastMCP error] health endpoint error", error);
|
|
1158
|
-
}
|
|
1159
|
-
}
|
|
1160
|
-
const oauthConfig = this.#options.oauth;
|
|
1161
|
-
if (oauthConfig?.enabled && req.method === "GET") {
|
|
1162
|
-
const url = new URL(req.url || "", "http://localhost");
|
|
1163
|
-
if (url.pathname === "/.well-known/oauth-authorization-server" && oauthConfig.authorizationServer) {
|
|
1164
|
-
const metadata = convertObjectToSnakeCase(
|
|
1165
|
-
oauthConfig.authorizationServer
|
|
1166
|
-
);
|
|
1167
|
-
res.writeHead(200, {
|
|
1168
|
-
"Content-Type": "application/json"
|
|
1169
|
-
}).end(JSON.stringify(metadata));
|
|
1170
|
-
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);
|
|
1171
1103
|
}
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
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);
|
|
1180
1129
|
}
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
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
|
+
}
|
|
1193
1159
|
} else {
|
|
1194
1160
|
throw new Error("Invalid transport type");
|
|
1195
1161
|
}
|
|
@@ -1202,6 +1168,100 @@ var FastMCP = class extends FastMCPEventEmitter {
|
|
|
1202
1168
|
await this.#httpStreamServer.close();
|
|
1203
1169
|
}
|
|
1204
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
|
+
};
|
|
1205
1265
|
#parseRuntimeConfig(overrides) {
|
|
1206
1266
|
const args = process.argv.slice(2);
|
|
1207
1267
|
const getArg = (name) => {
|
|
@@ -1211,9 +1271,11 @@ var FastMCP = class extends FastMCPEventEmitter {
|
|
|
1211
1271
|
const transportArg = getArg("transport");
|
|
1212
1272
|
const portArg = getArg("port");
|
|
1213
1273
|
const endpointArg = getArg("endpoint");
|
|
1274
|
+
const statelessArg = getArg("stateless");
|
|
1214
1275
|
const envTransport = process.env.FASTMCP_TRANSPORT;
|
|
1215
1276
|
const envPort = process.env.FASTMCP_PORT;
|
|
1216
1277
|
const envEndpoint = process.env.FASTMCP_ENDPOINT;
|
|
1278
|
+
const envStateless = process.env.FASTMCP_STATELESS;
|
|
1217
1279
|
const transportType = overrides?.transportType || (transportArg === "http-stream" ? "httpStream" : transportArg) || envTransport || "stdio";
|
|
1218
1280
|
if (transportType === "httpStream") {
|
|
1219
1281
|
const port = parseInt(
|
|
@@ -1221,11 +1283,13 @@ var FastMCP = class extends FastMCPEventEmitter {
|
|
|
1221
1283
|
);
|
|
1222
1284
|
const endpoint = overrides?.httpStream?.endpoint || endpointArg || envEndpoint || "/mcp";
|
|
1223
1285
|
const enableJsonResponse = overrides?.httpStream?.enableJsonResponse || false;
|
|
1286
|
+
const stateless = overrides?.httpStream?.stateless || statelessArg === "true" || envStateless === "true" || false;
|
|
1224
1287
|
return {
|
|
1225
1288
|
httpStream: {
|
|
1226
1289
|
enableJsonResponse,
|
|
1227
1290
|
endpoint,
|
|
1228
|
-
port
|
|
1291
|
+
port,
|
|
1292
|
+
stateless
|
|
1229
1293
|
},
|
|
1230
1294
|
transportType: "httpStream"
|
|
1231
1295
|
};
|