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/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
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
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
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
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
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
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
|
};
|