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/README.md +86 -0
- package/dist/FastMCP.d.ts +16 -2
- package/dist/FastMCP.js +184 -117
- 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 +258 -151
- package/src/examples/custom-logger.ts +201 -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
|
|
@@ -649,6 +696,44 @@ server.addTool({
|
|
|
649
696
|
});
|
|
650
697
|
```
|
|
651
698
|
|
|
699
|
+
#### Custom Logger
|
|
700
|
+
|
|
701
|
+
FastMCP allows you to provide a custom logger implementation to control how the server logs messages. This is useful for integrating with existing logging infrastructure or customizing log formatting.
|
|
702
|
+
|
|
703
|
+
```ts
|
|
704
|
+
import { FastMCP, Logger } from "fastmcp";
|
|
705
|
+
|
|
706
|
+
class CustomLogger implements Logger {
|
|
707
|
+
debug(...args: unknown[]): void {
|
|
708
|
+
console.log("[DEBUG]", new Date().toISOString(), ...args);
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
error(...args: unknown[]): void {
|
|
712
|
+
console.error("[ERROR]", new Date().toISOString(), ...args);
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
info(...args: unknown[]): void {
|
|
716
|
+
console.info("[INFO]", new Date().toISOString(), ...args);
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
log(...args: unknown[]): void {
|
|
720
|
+
console.log("[LOG]", new Date().toISOString(), ...args);
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
warn(...args: unknown[]): void {
|
|
724
|
+
console.warn("[WARN]", new Date().toISOString(), ...args);
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
const server = new FastMCP({
|
|
729
|
+
name: "My Server",
|
|
730
|
+
version: "1.0.0",
|
|
731
|
+
logger: new CustomLogger(),
|
|
732
|
+
});
|
|
733
|
+
```
|
|
734
|
+
|
|
735
|
+
See `src/examples/custom-logger.ts` for examples with Winston, Pino, and file-based logging.
|
|
736
|
+
|
|
652
737
|
#### Logging
|
|
653
738
|
|
|
654
739
|
Tools can log messages to the client using the `log` object in the context object:
|
|
@@ -1666,6 +1751,7 @@ Refer to this [issue](https://github.com/punkpeye/fastmcp/issues/25#issuecomment
|
|
|
1666
1751
|
- [cswkim/discogs-mcp-server](https://github.com/cswkim/discogs-mcp-server) - connects to the Discogs API for interacting with your music collection
|
|
1667
1752
|
- [Panzer-Jack/feuse-mcp](https://github.com/Panzer-Jack/feuse-mcp) - Frontend Useful MCP Tools - Essential utilities for web developers to automate API integration and code generation
|
|
1668
1753
|
- [sunra-ai/sunra-clients](https://github.com/sunra-ai/sunra-clients/tree/main/mcp-server) - Sunra.ai is a generative media platform built for developers, providing high-performance AI model inference capabilities.
|
|
1754
|
+
- [foxtrottwist/shortcuts-mcp](https://github.com/foxtrottwist/shortcuts-mcp) - connects Claude to macOS Shortcuts for system automation, app integration, and interactive workflows
|
|
1669
1755
|
|
|
1670
1756
|
## Acknowledgements
|
|
1671
1757
|
|
package/dist/FastMCP.d.ts
CHANGED
|
@@ -10,6 +10,13 @@ import http from 'http';
|
|
|
10
10
|
import { StrictEventEmitter } from 'strict-event-emitter-types';
|
|
11
11
|
import { z } from 'zod';
|
|
12
12
|
|
|
13
|
+
interface Logger {
|
|
14
|
+
debug(...args: unknown[]): void;
|
|
15
|
+
error(...args: unknown[]): void;
|
|
16
|
+
info(...args: unknown[]): void;
|
|
17
|
+
log(...args: unknown[]): void;
|
|
18
|
+
warn(...args: unknown[]): void;
|
|
19
|
+
}
|
|
13
20
|
type SSEServer = {
|
|
14
21
|
close: () => Promise<void>;
|
|
15
22
|
};
|
|
@@ -247,6 +254,11 @@ type ServerOptions<T extends FastMCPSessionAuth> = {
|
|
|
247
254
|
status?: number;
|
|
248
255
|
};
|
|
249
256
|
instructions?: string;
|
|
257
|
+
/**
|
|
258
|
+
* Custom logger instance. If not provided, defaults to console.
|
|
259
|
+
* Use this to integrate with your own logging system.
|
|
260
|
+
*/
|
|
261
|
+
logger?: Logger;
|
|
250
262
|
name: string;
|
|
251
263
|
/**
|
|
252
264
|
* Configuration for OAuth well-known discovery endpoints that can be exposed
|
|
@@ -534,9 +546,10 @@ declare class FastMCPSession<T extends FastMCPSessionAuth = FastMCPSessionAuth>
|
|
|
534
546
|
get loggingLevel(): LoggingLevel;
|
|
535
547
|
get roots(): Root[];
|
|
536
548
|
get server(): Server;
|
|
537
|
-
constructor({ auth, instructions, name, ping, prompts, resources, resourcesTemplates, roots, tools, transportType, utils, version, }: {
|
|
549
|
+
constructor({ auth, instructions, logger, name, ping, prompts, resources, resourcesTemplates, roots, tools, transportType, utils, version, }: {
|
|
538
550
|
auth?: T;
|
|
539
551
|
instructions?: string;
|
|
552
|
+
logger: Logger;
|
|
540
553
|
name: string;
|
|
541
554
|
ping?: ServerOptions<T>["ping"];
|
|
542
555
|
prompts: Prompt<T>[];
|
|
@@ -606,6 +619,7 @@ declare class FastMCP<T extends FastMCPSessionAuth = FastMCPSessionAuth> extends
|
|
|
606
619
|
endpoint?: `/${string}`;
|
|
607
620
|
eventStore?: EventStore;
|
|
608
621
|
port: number;
|
|
622
|
+
stateless?: boolean;
|
|
609
623
|
};
|
|
610
624
|
transportType: "httpStream" | "stdio";
|
|
611
625
|
}>): Promise<void>;
|
|
@@ -615,4 +629,4 @@ declare class FastMCP<T extends FastMCPSessionAuth = FastMCPSessionAuth> extends
|
|
|
615
629
|
stop(): Promise<void>;
|
|
616
630
|
}
|
|
617
631
|
|
|
618
|
-
export { type AudioContent, type Content, type ContentResult, type Context, FastMCP, type FastMCPEvents, FastMCPSession, type FastMCPSessionEvents, type ImageContent, type InputPrompt, type InputPromptArgument, type LoggingLevel, type Progress, type Prompt, type PromptArgument, type Resource, type ResourceContent, type ResourceResult, type ResourceTemplate, type ResourceTemplateArgument, type SSEServer, type SerializableValue, type ServerOptions, type TextContent, type Tool, type ToolParameters, UnexpectedStateError, UserError, audioContent, imageContent };
|
|
632
|
+
export { type AudioContent, type Content, type ContentResult, type Context, FastMCP, type FastMCPEvents, FastMCPSession, type FastMCPSessionEvents, type ImageContent, type InputPrompt, type InputPromptArgument, type Logger, type LoggingLevel, type Progress, type Prompt, type PromptArgument, type Resource, type ResourceContent, type ResourceResult, type ResourceTemplate, type ResourceTemplateArgument, type SSEServer, type SerializableValue, type ServerOptions, type TextContent, type Tool, type ToolParameters, UnexpectedStateError, UserError, audioContent, imageContent };
|
package/dist/FastMCP.js
CHANGED
|
@@ -237,6 +237,7 @@ var FastMCPSession = class extends FastMCPSessionEventEmitter {
|
|
|
237
237
|
#capabilities = {};
|
|
238
238
|
#clientCapabilities;
|
|
239
239
|
#connectionState = "connecting";
|
|
240
|
+
#logger;
|
|
240
241
|
#loggingLevel = "info";
|
|
241
242
|
#needsEventLoopFlush = false;
|
|
242
243
|
#pingConfig;
|
|
@@ -251,6 +252,7 @@ var FastMCPSession = class extends FastMCPSessionEventEmitter {
|
|
|
251
252
|
constructor({
|
|
252
253
|
auth,
|
|
253
254
|
instructions,
|
|
255
|
+
logger,
|
|
254
256
|
name,
|
|
255
257
|
ping,
|
|
256
258
|
prompts,
|
|
@@ -264,6 +266,7 @@ var FastMCPSession = class extends FastMCPSessionEventEmitter {
|
|
|
264
266
|
}) {
|
|
265
267
|
super();
|
|
266
268
|
this.#auth = auth;
|
|
269
|
+
this.#logger = logger;
|
|
267
270
|
this.#pingConfig = ping;
|
|
268
271
|
this.#rootsConfig = roots;
|
|
269
272
|
this.#needsEventLoopFlush = transportType === "httpStream";
|
|
@@ -316,7 +319,7 @@ var FastMCPSession = class extends FastMCPSessionEventEmitter {
|
|
|
316
319
|
try {
|
|
317
320
|
await this.#server.close();
|
|
318
321
|
} catch (error) {
|
|
319
|
-
|
|
322
|
+
this.#logger.error("[FastMCP error]", "could not close server", error);
|
|
320
323
|
}
|
|
321
324
|
}
|
|
322
325
|
async connect(transport) {
|
|
@@ -338,7 +341,7 @@ var FastMCPSession = class extends FastMCPSessionEventEmitter {
|
|
|
338
341
|
await delay(retryDelay);
|
|
339
342
|
}
|
|
340
343
|
if (!this.#clientCapabilities) {
|
|
341
|
-
|
|
344
|
+
this.#logger.warn(
|
|
342
345
|
`[FastMCP warning] could not infer client capabilities after ${maxAttempts} attempts. Connection may be unstable.`
|
|
343
346
|
);
|
|
344
347
|
}
|
|
@@ -348,11 +351,11 @@ var FastMCPSession = class extends FastMCPSessionEventEmitter {
|
|
|
348
351
|
this.#roots = roots?.roots || [];
|
|
349
352
|
} catch (e) {
|
|
350
353
|
if (e instanceof McpError && e.code === ErrorCode.MethodNotFound) {
|
|
351
|
-
|
|
354
|
+
this.#logger.debug(
|
|
352
355
|
"[FastMCP debug] listRoots method not supported by client"
|
|
353
356
|
);
|
|
354
357
|
} else {
|
|
355
|
-
|
|
358
|
+
this.#logger.error(
|
|
356
359
|
`[FastMCP error] received error listing roots.
|
|
357
360
|
|
|
358
361
|
${e instanceof Error ? e.stack : JSON.stringify(e)}`
|
|
@@ -369,17 +372,17 @@ ${e instanceof Error ? e.stack : JSON.stringify(e)}`
|
|
|
369
372
|
} catch {
|
|
370
373
|
const logLevel = pingConfig.logLevel;
|
|
371
374
|
if (logLevel === "debug") {
|
|
372
|
-
|
|
375
|
+
this.#logger.debug("[FastMCP debug] server ping failed");
|
|
373
376
|
} else if (logLevel === "warning") {
|
|
374
|
-
|
|
377
|
+
this.#logger.warn(
|
|
375
378
|
"[FastMCP warning] server is not responding to ping"
|
|
376
379
|
);
|
|
377
380
|
} else if (logLevel === "error") {
|
|
378
|
-
|
|
381
|
+
this.#logger.error(
|
|
379
382
|
"[FastMCP error] server is not responding to ping"
|
|
380
383
|
);
|
|
381
384
|
} else {
|
|
382
|
-
|
|
385
|
+
this.#logger.info("[FastMCP info] server ping failed");
|
|
383
386
|
}
|
|
384
387
|
}
|
|
385
388
|
}, pingConfig.intervalMs);
|
|
@@ -565,7 +568,7 @@ ${e instanceof Error ? e.stack : JSON.stringify(e)}`
|
|
|
565
568
|
}
|
|
566
569
|
setupErrorHandling() {
|
|
567
570
|
this.#server.onerror = (error) => {
|
|
568
|
-
|
|
571
|
+
this.#logger.error("[FastMCP error]", error);
|
|
569
572
|
};
|
|
570
573
|
}
|
|
571
574
|
setupLoggingHandlers() {
|
|
@@ -731,7 +734,7 @@ ${e instanceof Error ? e.stack : JSON.stringify(e)}`
|
|
|
731
734
|
}
|
|
732
735
|
setupRootsHandlers() {
|
|
733
736
|
if (this.#rootsConfig?.enabled === false) {
|
|
734
|
-
|
|
737
|
+
this.#logger.debug(
|
|
735
738
|
"[FastMCP debug] roots capability explicitly disabled via config"
|
|
736
739
|
);
|
|
737
740
|
return;
|
|
@@ -747,11 +750,11 @@ ${e instanceof Error ? e.stack : JSON.stringify(e)}`
|
|
|
747
750
|
});
|
|
748
751
|
}).catch((error) => {
|
|
749
752
|
if (error instanceof McpError && error.code === ErrorCode.MethodNotFound) {
|
|
750
|
-
|
|
753
|
+
this.#logger.debug(
|
|
751
754
|
"[FastMCP debug] listRoots method not supported by client"
|
|
752
755
|
);
|
|
753
756
|
} else {
|
|
754
|
-
|
|
757
|
+
this.#logger.error(
|
|
755
758
|
`[FastMCP error] received error listing roots.
|
|
756
759
|
|
|
757
760
|
${error instanceof Error ? error.stack : JSON.stringify(error)}`
|
|
@@ -761,7 +764,7 @@ ${error instanceof Error ? error.stack : JSON.stringify(error)}`
|
|
|
761
764
|
}
|
|
762
765
|
);
|
|
763
766
|
} else {
|
|
764
|
-
|
|
767
|
+
this.#logger.debug(
|
|
765
768
|
"[FastMCP debug] roots capability not available, not setting up notification handler"
|
|
766
769
|
);
|
|
767
770
|
}
|
|
@@ -827,7 +830,7 @@ ${error instanceof Error ? error.stack : JSON.stringify(error)}`
|
|
|
827
830
|
await new Promise((resolve) => setImmediate(resolve));
|
|
828
831
|
}
|
|
829
832
|
} catch (progressError) {
|
|
830
|
-
|
|
833
|
+
this.#logger.warn(
|
|
831
834
|
`[FastMCP warning] Failed to report progress for tool '${request.params.name}':`,
|
|
832
835
|
progressError instanceof Error ? progressError.message : String(progressError)
|
|
833
836
|
);
|
|
@@ -885,7 +888,7 @@ ${error instanceof Error ? error.stack : JSON.stringify(error)}`
|
|
|
885
888
|
await new Promise((resolve) => setImmediate(resolve));
|
|
886
889
|
}
|
|
887
890
|
} catch (streamError) {
|
|
888
|
-
|
|
891
|
+
this.#logger.warn(
|
|
889
892
|
`[FastMCP warning] Failed to stream content for tool '${request.params.name}':`,
|
|
890
893
|
streamError instanceof Error ? streamError.message : String(streamError)
|
|
891
894
|
);
|
|
@@ -968,12 +971,14 @@ var FastMCP = class extends FastMCPEventEmitter {
|
|
|
968
971
|
this.options = options;
|
|
969
972
|
this.#options = options;
|
|
970
973
|
this.#authenticate = options.authenticate;
|
|
974
|
+
this.#logger = options.logger || console;
|
|
971
975
|
}
|
|
972
976
|
get sessions() {
|
|
973
977
|
return this.#sessions;
|
|
974
978
|
}
|
|
975
979
|
#authenticate;
|
|
976
980
|
#httpStreamServer = null;
|
|
981
|
+
#logger;
|
|
977
982
|
#options;
|
|
978
983
|
#prompts = [];
|
|
979
984
|
#resources = [];
|
|
@@ -1073,6 +1078,7 @@ var FastMCP = class extends FastMCPEventEmitter {
|
|
|
1073
1078
|
const transport = new StdioServerTransport();
|
|
1074
1079
|
const session = new FastMCPSession({
|
|
1075
1080
|
instructions: this.#options.instructions,
|
|
1081
|
+
logger: this.#logger,
|
|
1076
1082
|
name: this.#options.name,
|
|
1077
1083
|
ping: this.#options.ping,
|
|
1078
1084
|
prompts: this.#prompts,
|
|
@@ -1091,109 +1097,71 @@ var FastMCP = class extends FastMCPEventEmitter {
|
|
|
1091
1097
|
});
|
|
1092
1098
|
} else if (config.transportType === "httpStream") {
|
|
1093
1099
|
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;
|
|
1100
|
+
if (httpConfig.stateless) {
|
|
1101
|
+
this.#logger.info(
|
|
1102
|
+
`[FastMCP info] Starting server in stateless mode on HTTP Stream at http://localhost:${httpConfig.port}${httpConfig.endpoint}`
|
|
1103
|
+
);
|
|
1104
|
+
this.#httpStreamServer = await startHTTPServer({
|
|
1105
|
+
createServer: async (request) => {
|
|
1106
|
+
let auth;
|
|
1107
|
+
if (this.#authenticate) {
|
|
1108
|
+
auth = await this.#authenticate(request);
|
|
1175
1109
|
}
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1110
|
+
return this.#createSession(auth);
|
|
1111
|
+
},
|
|
1112
|
+
enableJsonResponse: httpConfig.enableJsonResponse,
|
|
1113
|
+
eventStore: httpConfig.eventStore,
|
|
1114
|
+
// In stateless mode, we don't track sessions
|
|
1115
|
+
onClose: async () => {
|
|
1116
|
+
},
|
|
1117
|
+
onConnect: async () => {
|
|
1118
|
+
this.#logger.debug(
|
|
1119
|
+
`[FastMCP debug] Stateless HTTP Stream request handled`
|
|
1120
|
+
);
|
|
1121
|
+
},
|
|
1122
|
+
onUnhandledRequest: async (req, res) => {
|
|
1123
|
+
await this.#handleUnhandledRequest(req, res, true);
|
|
1124
|
+
},
|
|
1125
|
+
port: httpConfig.port,
|
|
1126
|
+
stateless: true,
|
|
1127
|
+
streamEndpoint: httpConfig.endpoint
|
|
1128
|
+
});
|
|
1129
|
+
} else {
|
|
1130
|
+
this.#httpStreamServer = await startHTTPServer({
|
|
1131
|
+
createServer: async (request) => {
|
|
1132
|
+
let auth;
|
|
1133
|
+
if (this.#authenticate) {
|
|
1134
|
+
auth = await this.#authenticate(request);
|
|
1184
1135
|
}
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1136
|
+
return this.#createSession(auth);
|
|
1137
|
+
},
|
|
1138
|
+
enableJsonResponse: httpConfig.enableJsonResponse,
|
|
1139
|
+
eventStore: httpConfig.eventStore,
|
|
1140
|
+
onClose: async (session) => {
|
|
1141
|
+
this.emit("disconnect", {
|
|
1142
|
+
session
|
|
1143
|
+
});
|
|
1144
|
+
},
|
|
1145
|
+
onConnect: async (session) => {
|
|
1146
|
+
this.#sessions.push(session);
|
|
1147
|
+
this.#logger.info(`[FastMCP info] HTTP Stream session established`);
|
|
1148
|
+
this.emit("connect", {
|
|
1149
|
+
session
|
|
1150
|
+
});
|
|
1151
|
+
},
|
|
1152
|
+
onUnhandledRequest: async (req, res) => {
|
|
1153
|
+
await this.#handleUnhandledRequest(req, res, false);
|
|
1154
|
+
},
|
|
1155
|
+
port: httpConfig.port,
|
|
1156
|
+
streamEndpoint: httpConfig.endpoint
|
|
1157
|
+
});
|
|
1158
|
+
this.#logger.info(
|
|
1159
|
+
`[FastMCP info] server is running on HTTP Stream at http://localhost:${httpConfig.port}${httpConfig.endpoint}`
|
|
1160
|
+
);
|
|
1161
|
+
this.#logger.info(
|
|
1162
|
+
`[FastMCP info] Transport type: httpStream (Streamable HTTP, not SSE)`
|
|
1163
|
+
);
|
|
1164
|
+
}
|
|
1197
1165
|
} else {
|
|
1198
1166
|
throw new Error("Invalid transport type");
|
|
1199
1167
|
}
|
|
@@ -1206,6 +1174,101 @@ var FastMCP = class extends FastMCPEventEmitter {
|
|
|
1206
1174
|
await this.#httpStreamServer.close();
|
|
1207
1175
|
}
|
|
1208
1176
|
}
|
|
1177
|
+
/**
|
|
1178
|
+
* Creates a new FastMCPSession instance with the current configuration.
|
|
1179
|
+
* Used both for regular sessions and stateless requests.
|
|
1180
|
+
*/
|
|
1181
|
+
#createSession(auth) {
|
|
1182
|
+
const allowedTools = auth ? this.#tools.filter(
|
|
1183
|
+
(tool) => tool.canAccess ? tool.canAccess(auth) : true
|
|
1184
|
+
) : this.#tools;
|
|
1185
|
+
return new FastMCPSession({
|
|
1186
|
+
auth,
|
|
1187
|
+
logger: this.#logger,
|
|
1188
|
+
name: this.#options.name,
|
|
1189
|
+
ping: this.#options.ping,
|
|
1190
|
+
prompts: this.#prompts,
|
|
1191
|
+
resources: this.#resources,
|
|
1192
|
+
resourcesTemplates: this.#resourcesTemplates,
|
|
1193
|
+
roots: this.#options.roots,
|
|
1194
|
+
tools: allowedTools,
|
|
1195
|
+
transportType: "httpStream",
|
|
1196
|
+
utils: this.#options.utils,
|
|
1197
|
+
version: this.#options.version
|
|
1198
|
+
});
|
|
1199
|
+
}
|
|
1200
|
+
/**
|
|
1201
|
+
* Handles unhandled HTTP requests with health, readiness, and OAuth endpoints
|
|
1202
|
+
*/
|
|
1203
|
+
#handleUnhandledRequest = async (req, res, isStateless = false) => {
|
|
1204
|
+
const healthConfig = this.#options.health ?? {};
|
|
1205
|
+
const enabled = healthConfig.enabled === void 0 ? true : healthConfig.enabled;
|
|
1206
|
+
if (enabled) {
|
|
1207
|
+
const path = healthConfig.path ?? "/health";
|
|
1208
|
+
const url = new URL(req.url || "", "http://localhost");
|
|
1209
|
+
try {
|
|
1210
|
+
if (req.method === "GET" && url.pathname === path) {
|
|
1211
|
+
res.writeHead(healthConfig.status ?? 200, {
|
|
1212
|
+
"Content-Type": "text/plain"
|
|
1213
|
+
}).end(healthConfig.message ?? "\u2713 Ok");
|
|
1214
|
+
return;
|
|
1215
|
+
}
|
|
1216
|
+
if (req.method === "GET" && url.pathname === "/ready") {
|
|
1217
|
+
if (isStateless) {
|
|
1218
|
+
const response = {
|
|
1219
|
+
mode: "stateless",
|
|
1220
|
+
ready: 1,
|
|
1221
|
+
status: "ready",
|
|
1222
|
+
total: 1
|
|
1223
|
+
};
|
|
1224
|
+
res.writeHead(200, {
|
|
1225
|
+
"Content-Type": "application/json"
|
|
1226
|
+
}).end(JSON.stringify(response));
|
|
1227
|
+
} else {
|
|
1228
|
+
const readySessions = this.#sessions.filter(
|
|
1229
|
+
(s) => s.isReady
|
|
1230
|
+
).length;
|
|
1231
|
+
const totalSessions = this.#sessions.length;
|
|
1232
|
+
const allReady = readySessions === totalSessions && totalSessions > 0;
|
|
1233
|
+
const response = {
|
|
1234
|
+
ready: readySessions,
|
|
1235
|
+
status: allReady ? "ready" : totalSessions === 0 ? "no_sessions" : "initializing",
|
|
1236
|
+
total: totalSessions
|
|
1237
|
+
};
|
|
1238
|
+
res.writeHead(allReady ? 200 : 503, {
|
|
1239
|
+
"Content-Type": "application/json"
|
|
1240
|
+
}).end(JSON.stringify(response));
|
|
1241
|
+
}
|
|
1242
|
+
return;
|
|
1243
|
+
}
|
|
1244
|
+
} catch (error) {
|
|
1245
|
+
this.#logger.error("[FastMCP error] health endpoint error", error);
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
const oauthConfig = this.#options.oauth;
|
|
1249
|
+
if (oauthConfig?.enabled && req.method === "GET") {
|
|
1250
|
+
const url = new URL(req.url || "", "http://localhost");
|
|
1251
|
+
if (url.pathname === "/.well-known/oauth-authorization-server" && oauthConfig.authorizationServer) {
|
|
1252
|
+
const metadata = convertObjectToSnakeCase(
|
|
1253
|
+
oauthConfig.authorizationServer
|
|
1254
|
+
);
|
|
1255
|
+
res.writeHead(200, {
|
|
1256
|
+
"Content-Type": "application/json"
|
|
1257
|
+
}).end(JSON.stringify(metadata));
|
|
1258
|
+
return;
|
|
1259
|
+
}
|
|
1260
|
+
if (url.pathname === "/.well-known/oauth-protected-resource" && oauthConfig.protectedResource) {
|
|
1261
|
+
const metadata = convertObjectToSnakeCase(
|
|
1262
|
+
oauthConfig.protectedResource
|
|
1263
|
+
);
|
|
1264
|
+
res.writeHead(200, {
|
|
1265
|
+
"Content-Type": "application/json"
|
|
1266
|
+
}).end(JSON.stringify(metadata));
|
|
1267
|
+
return;
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
res.writeHead(404).end();
|
|
1271
|
+
};
|
|
1209
1272
|
#parseRuntimeConfig(overrides) {
|
|
1210
1273
|
const args = process.argv.slice(2);
|
|
1211
1274
|
const getArg = (name) => {
|
|
@@ -1215,9 +1278,11 @@ var FastMCP = class extends FastMCPEventEmitter {
|
|
|
1215
1278
|
const transportArg = getArg("transport");
|
|
1216
1279
|
const portArg = getArg("port");
|
|
1217
1280
|
const endpointArg = getArg("endpoint");
|
|
1281
|
+
const statelessArg = getArg("stateless");
|
|
1218
1282
|
const envTransport = process.env.FASTMCP_TRANSPORT;
|
|
1219
1283
|
const envPort = process.env.FASTMCP_PORT;
|
|
1220
1284
|
const envEndpoint = process.env.FASTMCP_ENDPOINT;
|
|
1285
|
+
const envStateless = process.env.FASTMCP_STATELESS;
|
|
1221
1286
|
const transportType = overrides?.transportType || (transportArg === "http-stream" ? "httpStream" : transportArg) || envTransport || "stdio";
|
|
1222
1287
|
if (transportType === "httpStream") {
|
|
1223
1288
|
const port = parseInt(
|
|
@@ -1225,11 +1290,13 @@ var FastMCP = class extends FastMCPEventEmitter {
|
|
|
1225
1290
|
);
|
|
1226
1291
|
const endpoint = overrides?.httpStream?.endpoint || endpointArg || envEndpoint || "/mcp";
|
|
1227
1292
|
const enableJsonResponse = overrides?.httpStream?.enableJsonResponse || false;
|
|
1293
|
+
const stateless = overrides?.httpStream?.stateless || statelessArg === "true" || envStateless === "true" || false;
|
|
1228
1294
|
return {
|
|
1229
1295
|
httpStream: {
|
|
1230
1296
|
enableJsonResponse,
|
|
1231
1297
|
endpoint,
|
|
1232
|
-
port
|
|
1298
|
+
port,
|
|
1299
|
+
stateless
|
|
1233
1300
|
},
|
|
1234
1301
|
transportType: "httpStream"
|
|
1235
1302
|
};
|