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/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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1107
|
+
this.#logger.debug(
|
|
1091
1108
|
"[FastMCP debug] listRoots method not supported by client",
|
|
1092
1109
|
);
|
|
1093
1110
|
} else {
|
|
1094
|
-
|
|
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
|
-
|
|
1134
|
+
this.#logger.debug("[FastMCP debug] server ping failed");
|
|
1118
1135
|
} else if (logLevel === "warning") {
|
|
1119
|
-
|
|
1136
|
+
this.#logger.warn(
|
|
1120
1137
|
"[FastMCP warning] server is not responding to ping",
|
|
1121
1138
|
);
|
|
1122
1139
|
} else if (logLevel === "error") {
|
|
1123
|
-
|
|
1140
|
+
this.#logger.error(
|
|
1124
1141
|
"[FastMCP error] server is not responding to ping",
|
|
1125
1142
|
);
|
|
1126
1143
|
} else {
|
|
1127
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1609
|
+
this.#logger.debug(
|
|
1593
1610
|
"[FastMCP debug] listRoots method not supported by client",
|
|
1594
1611
|
);
|
|
1595
1612
|
} else {
|
|
1596
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 ?? {};
|
|
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
|
-
|
|
2100
|
-
|
|
2080
|
+
this.#httpStreamServer = await startHTTPServer<FastMCPSession<T>>({
|
|
2081
|
+
createServer: async (request) => {
|
|
2082
|
+
let auth: T | undefined;
|
|
2101
2083
|
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2084
|
+
if (this.#authenticate) {
|
|
2085
|
+
auth = await this.#authenticate(request);
|
|
2086
|
+
}
|
|
2105
2087
|
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
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
|
-
|
|
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);
|
|
2117
|
+
if (this.#authenticate) {
|
|
2118
|
+
auth = await this.#authenticate(request);
|
|
2146
2119
|
}
|
|
2147
|
-
}
|
|
2148
2120
|
|
|
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
|
-
}
|
|
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
|
-
|
|
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
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
streamEndpoint: httpConfig.endpoint,
|
|
2190
|
-
});
|
|
2135
|
+
this.emit("connect", {
|
|
2136
|
+
session: session as FastMCPSession<FastMCPSessionAuth>,
|
|
2137
|
+
});
|
|
2138
|
+
},
|
|
2191
2139
|
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
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
|
};
|