@prefecthq/fastmcp-ts 1.0.0-rc.0 → 1.0.0-rc.1
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/dist/cli/index.cjs +2 -2
- package/dist/server.d.ts +31 -2
- package/dist/server.js +94 -27
- package/dist/server.js.map +1 -1
- package/package.json +1 -1
package/dist/cli/index.cjs
CHANGED
|
@@ -1172,7 +1172,7 @@ var init_version = __esm({
|
|
|
1172
1172
|
async run({ args }) {
|
|
1173
1173
|
if (args.json) setJsonMode(true);
|
|
1174
1174
|
const data = {
|
|
1175
|
-
fastmcp: "1.0.0-rc.
|
|
1175
|
+
fastmcp: "1.0.0-rc.1",
|
|
1176
1176
|
"mcp-sdk": "2.0.0-beta.5",
|
|
1177
1177
|
node: process.version,
|
|
1178
1178
|
platform: `${process.platform} ${process.arch}`
|
|
@@ -46213,7 +46213,7 @@ init_format();
|
|
|
46213
46213
|
var main = defineCommand({
|
|
46214
46214
|
meta: {
|
|
46215
46215
|
name: "fastmcp",
|
|
46216
|
-
version: "1.0.0-rc.
|
|
46216
|
+
version: "1.0.0-rc.1",
|
|
46217
46217
|
description: "FastMCP CLI \u2014 build, run, and manage MCP servers"
|
|
46218
46218
|
},
|
|
46219
46219
|
args: {
|
package/dist/server.d.ts
CHANGED
|
@@ -796,6 +796,18 @@ interface RunOptions {
|
|
|
796
796
|
port?: number;
|
|
797
797
|
host?: string;
|
|
798
798
|
path?: string;
|
|
799
|
+
/**
|
|
800
|
+
* Serve the legacy (2025-era) HTTP transport statelessly: a fresh server and
|
|
801
|
+
* transport per request, no session registry, incoming `mcp-session-id`
|
|
802
|
+
* ignored, and no session id issued. Use this behind a load balancer or on
|
|
803
|
+
* serverless compute where consecutive requests reach different instances.
|
|
804
|
+
*
|
|
805
|
+
* Falls back to the `FASTMCP_STATELESS_HTTP` environment variable, then to
|
|
806
|
+
* `false`. A valid value is ignored for the stdio transport, but a malformed
|
|
807
|
+
* one still aborts stdio startup, deliberately. The modern (2026-07-28) era
|
|
808
|
+
* is already stateless and is unaffected.
|
|
809
|
+
*/
|
|
810
|
+
stateless?: boolean;
|
|
799
811
|
/** Custom stdin stream for the stdio transport. Defaults to process.stdin. */
|
|
800
812
|
stdin?: Readable;
|
|
801
813
|
/** Custom stdout stream for the stdio transport. Defaults to process.stdout. */
|
|
@@ -862,11 +874,13 @@ declare class FastMCP {
|
|
|
862
874
|
private _address;
|
|
863
875
|
private _isRunning;
|
|
864
876
|
private _sessions;
|
|
877
|
+
private _stateless;
|
|
865
878
|
private _primaryServer;
|
|
866
879
|
private _stdioServer;
|
|
867
880
|
private _stdioState;
|
|
868
881
|
private _stdioHandle;
|
|
869
882
|
private _modernHandler;
|
|
883
|
+
private _statelessLegacyHandler;
|
|
870
884
|
private _toolRegisteredCallbacks;
|
|
871
885
|
private _resourceRegisteredCallbacks;
|
|
872
886
|
private _promptRegisteredCallbacks;
|
|
@@ -898,6 +912,11 @@ declare class FastMCP {
|
|
|
898
912
|
* client here, so nothing needs the era fork the way `subscribe` does. The
|
|
899
913
|
* SDK also requires this capability to be present to register the
|
|
900
914
|
* `completion/complete` handler (`assertRequestHandlerCapability`).
|
|
915
|
+
*
|
|
916
|
+
* `stateless` withdraws the same capability for a different reason. A
|
|
917
|
+
* stateless server has no session for a subscription to live in and no
|
|
918
|
+
* channel to deliver `notifications/resources/updated` on, so it must not
|
|
919
|
+
* claim the capability.
|
|
901
920
|
*/
|
|
902
921
|
private _makeServer;
|
|
903
922
|
private _resolveToken;
|
|
@@ -961,7 +980,7 @@ declare class FastMCP {
|
|
|
961
980
|
* Dispatch a tool call through this server's middleware chain using an inherited context.
|
|
962
981
|
* Used by parent servers to honour child-level middleware when routing mounted tool calls.
|
|
963
982
|
*/
|
|
964
|
-
_dispatchTool(name: string, rawArgs: unknown, ctx: McpContext): Promise<{
|
|
983
|
+
_dispatchTool(name: string, rawArgs: unknown, ctx: McpContext): Promise<_modelcontextprotocol_server.InputRequiredResult | {
|
|
965
984
|
[x: string]: unknown;
|
|
966
985
|
content: ({
|
|
967
986
|
type: "text";
|
|
@@ -1064,7 +1083,7 @@ declare class FastMCP {
|
|
|
1064
1083
|
} | undefined;
|
|
1065
1084
|
structuredContent?: unknown;
|
|
1066
1085
|
isError?: boolean | undefined;
|
|
1067
|
-
}
|
|
1086
|
+
}>;
|
|
1068
1087
|
/**
|
|
1069
1088
|
* Run a resource read through this server's middleware chain using an inherited context.
|
|
1070
1089
|
* Returns the raw handler result so the parent can apply convertResourceResult with the
|
|
@@ -1139,6 +1158,16 @@ declare class FastMCP {
|
|
|
1139
1158
|
* of createMcpHandler's own stateless legacy fallback, so session state and the
|
|
1140
1159
|
* legacy server-initiated-request shim keep working for 2025-era clients. */
|
|
1141
1160
|
private _getModernHandler;
|
|
1161
|
+
/** Lazily builds the stateless legacy (2025-era) HTTP handler. One per FastMCP
|
|
1162
|
+
* instance, mirroring _getModernHandler's per-instance, per-request-factory model.
|
|
1163
|
+
*
|
|
1164
|
+
* The SDK serves each POST from a fresh instance of the factory over a transport
|
|
1165
|
+
* built with `sessionIdGenerator: undefined`, and answers GET and DELETE with 405
|
|
1166
|
+
* because both are session operations that mean nothing per request.
|
|
1167
|
+
*
|
|
1168
|
+
* Only reachable when `_stateless` is on. The sessionful transport in
|
|
1169
|
+
* _dispatchLegacyHttp is untouched and still serves every other deployment. */
|
|
1170
|
+
private _getStatelessLegacyHandler;
|
|
1142
1171
|
/**
|
|
1143
1172
|
* Dual-era HTTP dispatch shared by the OAuth and non-OAuth serve paths. Assumes CORS
|
|
1144
1173
|
* and auth have already been handled by the caller (req.auth already set, if any —
|
package/dist/server.js
CHANGED
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
|
|
5
5
|
// src/server/FastMCP.ts
|
|
6
6
|
import { hostHeaderValidation, originValidation } from "@modelcontextprotocol/node";
|
|
7
|
-
import { ProtocolError as ProtocolError3, ProtocolErrorCode as ProtocolErrorCode3, ResourceNotFoundError, Server as Server2, createMcpHandler, isLegacyRequest, isJsonContentType, createRequestStateCodec, localhostAllowedHostnames, localhostAllowedOrigins, assertCompleteRequestPrompt, assertCompleteRequestResourceTemplate } from "@modelcontextprotocol/server";
|
|
7
|
+
import { ProtocolError as ProtocolError3, ProtocolErrorCode as ProtocolErrorCode3, ResourceNotFoundError, Server as Server2, createMcpHandler, legacyStatelessFallback, isLegacyRequest, isJsonContentType, createRequestStateCodec, localhostAllowedHostnames, localhostAllowedOrigins, assertCompleteRequestPrompt, assertCompleteRequestResourceTemplate } from "@modelcontextprotocol/server";
|
|
8
8
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
9
9
|
|
|
10
10
|
// src/server/auth/types.ts
|
|
@@ -60,11 +60,17 @@ import { AsyncLocalStorage } from "async_hooks";
|
|
|
60
60
|
var contextStore = new AsyncLocalStorage();
|
|
61
61
|
var SESSION_CLOSE_CALLBACKS_KEY = "__fastmcp_session_close_callbacks";
|
|
62
62
|
var SESSION_STATE_MODERN_HTTP_ERROR = "[fastmcp] Session state is not available on modern HTTP requests (protocol revision 2026-07-28). Each request runs statelessly with no shared session store. Use ctx.requestState() to read per-request state. Use ctx.mintRequestState() to carry state across a multi-round-trip flow.";
|
|
63
|
-
|
|
63
|
+
var SESSION_STATE_STATELESS_HTTP_ERROR = "[fastmcp] Session state is not available on a stateless HTTP server (RunOptions.stateless or FASTMCP_STATELESS_HTTP). Each request runs against a fresh store that is discarded when the request ends. Use ctx.requestState() to read per-request state. Use ctx.mintRequestState() to carry state across a multi-round-trip flow. Turn stateless off to use session state, or keep state in your own external store.";
|
|
64
|
+
var SERVER_INITIATED_STATELESS_HTTP_ERROR = "[fastmcp] Server-initiated requests (ctx.elicit, ctx.sample, ctx.listRoots) are not available on a stateless HTTP server (RunOptions.stateless or FASTMCP_STATELESS_HTTP). They need a session: client capabilities are negotiated once at initialize, and the client replies on a separate request. Return inputRequired({ requestState }) to carry a multi-round-trip flow without a session; the embedded-request form of inputRequired needs a session too. Turn stateless off to use server-initiated requests.";
|
|
65
|
+
function createContext(server, sdkCtx, auth, sessionState, requestStateCodec, stateless) {
|
|
64
66
|
const requestId = String(sdkCtx.mcpReq.id);
|
|
65
67
|
const progressToken = sdkCtx.mcpReq._meta?.progressToken;
|
|
66
68
|
const isModernEra = sdkCtx.mcpReq.envelope !== void 0;
|
|
67
69
|
const isModernHttpRequest = isModernEra && sdkCtx.http !== void 0;
|
|
70
|
+
const sessionlessReason = isModernHttpRequest ? "modern-http" : stateless && sdkCtx.http !== void 0 ? "stateless-http" : null;
|
|
71
|
+
const sessionStateError = () => new Error(
|
|
72
|
+
sessionlessReason === "modern-http" ? SESSION_STATE_MODERN_HTTP_ERROR : SESSION_STATE_STATELESS_HTTP_ERROR
|
|
73
|
+
);
|
|
68
74
|
const serverRequestOptions = { relatedRequestId: sdkCtx.mcpReq.id };
|
|
69
75
|
async function log(level, message, loggerName) {
|
|
70
76
|
await sdkCtx.mcpReq.log(level, message, loggerName);
|
|
@@ -94,6 +100,7 @@ function createContext(server, sdkCtx, auth, sessionState, requestStateCodec) {
|
|
|
94
100
|
});
|
|
95
101
|
},
|
|
96
102
|
async sample(params) {
|
|
103
|
+
if (sessionlessReason === "stateless-http") throw new Error(SERVER_INITIATED_STATELESS_HTTP_ERROR);
|
|
97
104
|
if (!isModernEra) {
|
|
98
105
|
const caps = server.getClientCapabilities();
|
|
99
106
|
if (!caps?.sampling) {
|
|
@@ -121,6 +128,7 @@ function createContext(server, sdkCtx, auth, sessionState, requestStateCodec) {
|
|
|
121
128
|
};
|
|
122
129
|
},
|
|
123
130
|
async elicit(message, schema) {
|
|
131
|
+
if (sessionlessReason === "stateless-http") throw new Error(SERVER_INITIATED_STATELESS_HTTP_ERROR);
|
|
124
132
|
if (!isModernEra) {
|
|
125
133
|
const caps = server.getClientCapabilities();
|
|
126
134
|
if (!caps?.elicitation) {
|
|
@@ -144,6 +152,7 @@ function createContext(server, sdkCtx, auth, sessionState, requestStateCodec) {
|
|
|
144
152
|
};
|
|
145
153
|
},
|
|
146
154
|
async listRoots() {
|
|
155
|
+
if (sessionlessReason === "stateless-http") throw new Error(SERVER_INITIATED_STATELESS_HTTP_ERROR);
|
|
147
156
|
if (!isModernEra) {
|
|
148
157
|
const caps = server.getClientCapabilities();
|
|
149
158
|
if (!caps?.roots) {
|
|
@@ -165,15 +174,15 @@ function createContext(server, sdkCtx, auth, sessionState, requestStateCodec) {
|
|
|
165
174
|
return JSON.stringify(payload);
|
|
166
175
|
},
|
|
167
176
|
getState: (key) => {
|
|
168
|
-
if (
|
|
177
|
+
if (sessionlessReason) throw sessionStateError();
|
|
169
178
|
return sessionState.get(key);
|
|
170
179
|
},
|
|
171
180
|
setState: (key, value) => {
|
|
172
|
-
if (
|
|
181
|
+
if (sessionlessReason) throw sessionStateError();
|
|
173
182
|
sessionState.set(key, value);
|
|
174
183
|
},
|
|
175
184
|
deleteState: (key) => {
|
|
176
|
-
if (
|
|
185
|
+
if (sessionlessReason) throw sessionStateError();
|
|
177
186
|
sessionState.delete(key);
|
|
178
187
|
},
|
|
179
188
|
resolveToolName: (name) => name,
|
|
@@ -184,6 +193,20 @@ function createContext(server, sdkCtx, auth, sessionState, requestStateCodec) {
|
|
|
184
193
|
};
|
|
185
194
|
}
|
|
186
195
|
|
|
196
|
+
// src/server/env.ts
|
|
197
|
+
var TRUE_VALUES = /* @__PURE__ */ new Set(["1", "true", "yes", "on"]);
|
|
198
|
+
var FALSE_VALUES = /* @__PURE__ */ new Set(["0", "false", "no", "off"]);
|
|
199
|
+
function envBool(name) {
|
|
200
|
+
const raw = process.env[name];
|
|
201
|
+
if (raw === void 0 || raw.trim() === "") return void 0;
|
|
202
|
+
const normalized = raw.trim().toLowerCase();
|
|
203
|
+
if (TRUE_VALUES.has(normalized)) return true;
|
|
204
|
+
if (FALSE_VALUES.has(normalized)) return false;
|
|
205
|
+
throw new Error(
|
|
206
|
+
`[fastmcp] ${name} must be one of 1/true/yes/on or 0/false/no/off (case-insensitive). Received: ${JSON.stringify(raw)}`
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
|
|
187
210
|
// src/server/middleware.ts
|
|
188
211
|
import { ProtocolError, ProtocolErrorCode } from "@modelcontextprotocol/server";
|
|
189
212
|
import { createHash } from "crypto";
|
|
@@ -500,6 +523,13 @@ import { ProtocolError as ProtocolError2, ProtocolErrorCode as ProtocolErrorCode
|
|
|
500
523
|
|
|
501
524
|
// src/server/mrtr.ts
|
|
502
525
|
import { inputRequired, acceptedContent, inputResponse, isInputRequiredResult } from "@modelcontextprotocol/server";
|
|
526
|
+
var INPUT_REQUESTS_STATELESS_HTTP_ERROR = "[fastmcp] inputRequired({ inputRequests }) is not available on a stateless HTTP server (RunOptions.stateless or FASTMCP_STATELESS_HTTP). Fulfilling inputRequests needs a session: the client's reply to each embedded request arrives on a separate request, and a stateless server discards its per-request Server before that reply could arrive \u2014 the same reason ctx.elicit/ctx.sample/ctx.listRoots are unavailable here. inputRequired({ requestState }), with no inputRequests, remains available \u2014 it carries state across rounds without needing a session. Turn stateless off to use inputRequests.";
|
|
527
|
+
function assertInputRequestsAllowedStateless(value, stateless) {
|
|
528
|
+
const hasInputRequests = value.inputRequests !== void 0 && Object.keys(value.inputRequests).length > 0;
|
|
529
|
+
if (stateless && hasInputRequests) {
|
|
530
|
+
throw new Error(INPUT_REQUESTS_STATELESS_HTTP_ERROR);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
503
533
|
|
|
504
534
|
// src/server/tool.ts
|
|
505
535
|
var Image = class {
|
|
@@ -526,8 +556,9 @@ var ToolResult = class {
|
|
|
526
556
|
}
|
|
527
557
|
result;
|
|
528
558
|
};
|
|
529
|
-
function convertResult(value) {
|
|
559
|
+
function convertResult(value, stateless) {
|
|
530
560
|
if (isInputRequiredResult(value)) {
|
|
561
|
+
assertInputRequestsAllowedStateless(value, stateless);
|
|
531
562
|
return value;
|
|
532
563
|
}
|
|
533
564
|
if (value instanceof ToolResult) {
|
|
@@ -623,8 +654,9 @@ var ResourceResult = class {
|
|
|
623
654
|
}
|
|
624
655
|
contents;
|
|
625
656
|
};
|
|
626
|
-
function convertResourceResult(value, uri, mimeType) {
|
|
657
|
+
function convertResourceResult(value, uri, mimeType, stateless) {
|
|
627
658
|
if (isInputRequiredResult(value)) {
|
|
659
|
+
assertInputRequestsAllowedStateless(value, stateless);
|
|
628
660
|
return value;
|
|
629
661
|
}
|
|
630
662
|
if (value instanceof ResourceResult) {
|
|
@@ -721,8 +753,9 @@ function isPromptMessage(value) {
|
|
|
721
753
|
if (!v.content || typeof v.content !== "object") return false;
|
|
722
754
|
return typeof v.content.type === "string";
|
|
723
755
|
}
|
|
724
|
-
function convertPromptResult(value) {
|
|
756
|
+
function convertPromptResult(value, stateless) {
|
|
725
757
|
if (isInputRequiredResult(value)) {
|
|
758
|
+
assertInputRequestsAllowedStateless(value, stateless);
|
|
726
759
|
return value;
|
|
727
760
|
}
|
|
728
761
|
if (value instanceof PromptResult) {
|
|
@@ -766,6 +799,7 @@ function prefixResourceUri(uri, prefix) {
|
|
|
766
799
|
return `${uri.slice(0, idx + 3)}${prefix}/${uri.slice(idx + 3)}`;
|
|
767
800
|
}
|
|
768
801
|
var RESOURCE_SUBSCRIPTIONS_KEY = "__fastmcp_resource_subscriptions";
|
|
802
|
+
var STATELESS_SUBSCRIBE_ERROR = "[fastmcp] resources/subscribe and resources/unsubscribe are unavailable on a stateless HTTP server. A stateless server keeps no session, so a subscription has nowhere to live and no channel to deliver on. The server does not advertise the resources.subscribe capability; a compliant client will not call this.";
|
|
769
803
|
function inferDescription(name) {
|
|
770
804
|
return name.replace(/([A-Z])/g, " $1").replace(/[-_]+/g, " ").trim().toLowerCase();
|
|
771
805
|
}
|
|
@@ -841,6 +875,7 @@ var FastMCP = class {
|
|
|
841
875
|
_address = null;
|
|
842
876
|
_isRunning = false;
|
|
843
877
|
_sessions = /* @__PURE__ */ new Map();
|
|
878
|
+
_stateless = false;
|
|
844
879
|
// Primary server used by connect() (in-process transports — always 2025-era)
|
|
845
880
|
_primaryServer;
|
|
846
881
|
// The pinned Server instance for a run({transport:'stdio'}) connection (2025- or
|
|
@@ -854,6 +889,7 @@ var FastMCP = class {
|
|
|
854
889
|
// Modern (2026-07-28) HTTP handler — one per FastMCP instance, lazily created.
|
|
855
890
|
// Builds a fresh Server (via _makeServer) per request; see createMcpHandler.
|
|
856
891
|
_modernHandler = null;
|
|
892
|
+
_statelessLegacyHandler = null;
|
|
857
893
|
_toolRegisteredCallbacks = [];
|
|
858
894
|
_resourceRegisteredCallbacks = [];
|
|
859
895
|
_promptRegisteredCallbacks = [];
|
|
@@ -912,6 +948,11 @@ var FastMCP = class {
|
|
|
912
948
|
* client here, so nothing needs the era fork the way `subscribe` does. The
|
|
913
949
|
* SDK also requires this capability to be present to register the
|
|
914
950
|
* `completion/complete` handler (`assertRequestHandlerCapability`).
|
|
951
|
+
*
|
|
952
|
+
* `stateless` withdraws the same capability for a different reason. A
|
|
953
|
+
* stateless server has no session for a subscription to live in and no
|
|
954
|
+
* channel to deliver `notifications/resources/updated` on, so it must not
|
|
955
|
+
* claim the capability.
|
|
915
956
|
*/
|
|
916
957
|
_makeServer(sessionState, opts) {
|
|
917
958
|
const state = sessionState ?? this._primaryState;
|
|
@@ -919,23 +960,23 @@ var FastMCP = class {
|
|
|
919
960
|
const server = new Server2(
|
|
920
961
|
{ name: this.name, version: this.version },
|
|
921
962
|
{
|
|
922
|
-
capabilities: { tools: { listChanged: true }, resources: { listChanged: true, ...opts?.modern ? {} : { subscribe: true } }, prompts: { listChanged: true }, logging: {}, completions: {}, ...extensions ? { extensions } : {} },
|
|
963
|
+
capabilities: { tools: { listChanged: true }, resources: { listChanged: true, ...opts?.modern || opts?.stateless ? {} : { subscribe: true } }, prompts: { listChanged: true }, logging: {}, completions: {}, ...extensions ? { extensions } : {} },
|
|
923
964
|
...this._cacheHints ? { cacheHints: this._cacheHints } : {},
|
|
924
965
|
...this._requestStateCodec ? { requestState: { verify: this._requestStateCodec.verify.bind(this._requestStateCodec) } } : {},
|
|
925
966
|
...this._inputRequiredOptions ? { inputRequired: this._inputRequiredOptions } : {}
|
|
926
967
|
}
|
|
927
968
|
);
|
|
928
969
|
for (const mw of this._middleware) mw.setup?.(server);
|
|
929
|
-
this._setupHandlers(server, state);
|
|
970
|
+
this._setupHandlers(server, state, opts);
|
|
930
971
|
return server;
|
|
931
972
|
}
|
|
932
973
|
async _resolveToken(authInfo) {
|
|
933
974
|
return toAccessToken(authInfo) ?? await resolveCliEnvToken(this._auth);
|
|
934
975
|
}
|
|
935
|
-
_setupHandlers(server, sessionState) {
|
|
976
|
+
_setupHandlers(server, sessionState, opts) {
|
|
936
977
|
server.setRequestHandler("tools/list", async (req, sdkCtx) => {
|
|
937
978
|
const token = await this._resolveToken(sdkCtx.http?.authInfo);
|
|
938
|
-
const ctx = createContext(server, sdkCtx, token, sessionState, this._requestStateCodec);
|
|
979
|
+
const ctx = createContext(server, sdkCtx, token, sessionState, this._requestStateCodec, opts?.stateless);
|
|
939
980
|
return contextStore.run(
|
|
940
981
|
ctx,
|
|
941
982
|
() => runMiddlewareChain(this._middleware, "tools/list", req.params, ctx, async () => {
|
|
@@ -1014,7 +1055,7 @@ var FastMCP = class {
|
|
|
1014
1055
|
const synthTool = synthesizedList.find((s) => s.name === requestedName);
|
|
1015
1056
|
if (synthTool) {
|
|
1016
1057
|
if (synthTool.auth) await runAuthCheck(synthTool.auth, token);
|
|
1017
|
-
const ctx2 = createContext(server, sdkCtx, token, sessionState, this._requestStateCodec);
|
|
1058
|
+
const ctx2 = createContext(server, sdkCtx, token, sessionState, this._requestStateCodec, opts?.stateless);
|
|
1018
1059
|
try {
|
|
1019
1060
|
return await contextStore.run(
|
|
1020
1061
|
ctx2,
|
|
@@ -1035,7 +1076,7 @@ var FastMCP = class {
|
|
|
1035
1076
|
() => clearTimeout(timer)
|
|
1036
1077
|
);
|
|
1037
1078
|
}
|
|
1038
|
-
return convertResult(await executePromise);
|
|
1079
|
+
return convertResult(await executePromise, opts?.stateless);
|
|
1039
1080
|
})
|
|
1040
1081
|
);
|
|
1041
1082
|
} catch (err) {
|
|
@@ -1067,7 +1108,7 @@ var FastMCP = class {
|
|
|
1067
1108
|
if (tool.config.auth) await runAuthCheck(tool.config.auth, token);
|
|
1068
1109
|
const resolvedTool = tool;
|
|
1069
1110
|
const rawArgs = req.params.arguments ?? {};
|
|
1070
|
-
const ctx = createContext(server, sdkCtx, token, sessionState, this._requestStateCodec);
|
|
1111
|
+
const ctx = createContext(server, sdkCtx, token, sessionState, this._requestStateCodec, opts?.stateless);
|
|
1071
1112
|
try {
|
|
1072
1113
|
return await contextStore.run(
|
|
1073
1114
|
ctx,
|
|
@@ -1089,7 +1130,7 @@ var FastMCP = class {
|
|
|
1089
1130
|
}
|
|
1090
1131
|
let resultValue = await executePromise;
|
|
1091
1132
|
if (resolvedTool.config.output) resultValue = await validateInput(resolvedTool.config.output, resultValue);
|
|
1092
|
-
const callResult = convertResult(resultValue);
|
|
1133
|
+
const callResult = convertResult(resultValue, opts?.stateless);
|
|
1093
1134
|
if (resolvedTool.config.ui) {
|
|
1094
1135
|
const clientIsUi = isUiCapable(server.getClientCapabilities());
|
|
1095
1136
|
if (!clientIsUi && callResult.structuredContent !== void 0) {
|
|
@@ -1109,7 +1150,7 @@ var FastMCP = class {
|
|
|
1109
1150
|
});
|
|
1110
1151
|
server.setRequestHandler("resources/list", async (req, sdkCtx) => {
|
|
1111
1152
|
const token = await this._resolveToken(sdkCtx.http?.authInfo);
|
|
1112
|
-
const ctx = createContext(server, sdkCtx, token, sessionState, this._requestStateCodec);
|
|
1153
|
+
const ctx = createContext(server, sdkCtx, token, sessionState, this._requestStateCodec, opts?.stateless);
|
|
1113
1154
|
return contextStore.run(
|
|
1114
1155
|
ctx,
|
|
1115
1156
|
() => runMiddlewareChain(this._middleware, "resources/list", req.params, ctx, async () => {
|
|
@@ -1156,7 +1197,7 @@ var FastMCP = class {
|
|
|
1156
1197
|
});
|
|
1157
1198
|
server.setRequestHandler("resources/templates/list", async (req, sdkCtx) => {
|
|
1158
1199
|
const token = await this._resolveToken(sdkCtx.http?.authInfo);
|
|
1159
|
-
const ctx = createContext(server, sdkCtx, token, sessionState, this._requestStateCodec);
|
|
1200
|
+
const ctx = createContext(server, sdkCtx, token, sessionState, this._requestStateCodec, opts?.stateless);
|
|
1160
1201
|
return contextStore.run(
|
|
1161
1202
|
ctx,
|
|
1162
1203
|
() => runMiddlewareChain(this._middleware, "resources/templates/list", req.params, ctx, async () => {
|
|
@@ -1208,7 +1249,7 @@ var FastMCP = class {
|
|
|
1208
1249
|
const resource = resolved.resource;
|
|
1209
1250
|
const templateParams = resolved.templateParams;
|
|
1210
1251
|
if (resource.config.auth) await runAuthCheck(resource.config.auth, token);
|
|
1211
|
-
const ctx = createContext(server, sdkCtx, token, sessionState, this._requestStateCodec);
|
|
1252
|
+
const ctx = createContext(server, sdkCtx, token, sessionState, this._requestStateCodec, opts?.stateless);
|
|
1212
1253
|
return contextStore.run(
|
|
1213
1254
|
ctx,
|
|
1214
1255
|
() => runMiddlewareChain(this._middleware, "resources/read", req.params, ctx, async () => {
|
|
@@ -1227,11 +1268,12 @@ var FastMCP = class {
|
|
|
1227
1268
|
);
|
|
1228
1269
|
}
|
|
1229
1270
|
const result = await executePromise;
|
|
1230
|
-
return convertResourceResult(result, requestedUri, resource.config.mimeType);
|
|
1271
|
+
return convertResourceResult(result, requestedUri, resource.config.mimeType, opts?.stateless);
|
|
1231
1272
|
})
|
|
1232
1273
|
);
|
|
1233
1274
|
});
|
|
1234
1275
|
server.setRequestHandler("resources/subscribe", async (req, sdkCtx) => {
|
|
1276
|
+
if (opts?.stateless) throw new ProtocolError3(ProtocolErrorCode3.MethodNotFound, STATELESS_SUBSCRIBE_ERROR);
|
|
1235
1277
|
const uri = req.params.uri;
|
|
1236
1278
|
const token = await this._resolveToken(sdkCtx.http?.authInfo);
|
|
1237
1279
|
const resolved = this._resolveResource(uri);
|
|
@@ -1240,7 +1282,7 @@ var FastMCP = class {
|
|
|
1240
1282
|
}
|
|
1241
1283
|
const resource = resolved.resource;
|
|
1242
1284
|
if (resource.config.auth) await runAuthCheck(resource.config.auth, token);
|
|
1243
|
-
const ctx = createContext(server, sdkCtx, token, sessionState, this._requestStateCodec);
|
|
1285
|
+
const ctx = createContext(server, sdkCtx, token, sessionState, this._requestStateCodec, opts?.stateless);
|
|
1244
1286
|
return contextStore.run(
|
|
1245
1287
|
ctx,
|
|
1246
1288
|
() => runMiddlewareChain(this._middleware, "resources/subscribe", req.params, ctx, async () => {
|
|
@@ -1255,9 +1297,10 @@ var FastMCP = class {
|
|
|
1255
1297
|
);
|
|
1256
1298
|
});
|
|
1257
1299
|
server.setRequestHandler("resources/unsubscribe", async (req, sdkCtx) => {
|
|
1300
|
+
if (opts?.stateless) throw new ProtocolError3(ProtocolErrorCode3.MethodNotFound, STATELESS_SUBSCRIBE_ERROR);
|
|
1258
1301
|
const uri = req.params.uri;
|
|
1259
1302
|
const token = await this._resolveToken(sdkCtx.http?.authInfo);
|
|
1260
|
-
const ctx = createContext(server, sdkCtx, token, sessionState, this._requestStateCodec);
|
|
1303
|
+
const ctx = createContext(server, sdkCtx, token, sessionState, this._requestStateCodec, opts?.stateless);
|
|
1261
1304
|
return contextStore.run(
|
|
1262
1305
|
ctx,
|
|
1263
1306
|
() => runMiddlewareChain(this._middleware, "resources/unsubscribe", req.params, ctx, async () => {
|
|
@@ -1269,7 +1312,7 @@ var FastMCP = class {
|
|
|
1269
1312
|
});
|
|
1270
1313
|
server.setRequestHandler("prompts/list", async (req, sdkCtx) => {
|
|
1271
1314
|
const token = await this._resolveToken(sdkCtx.http?.authInfo);
|
|
1272
|
-
const ctx = createContext(server, sdkCtx, token, sessionState, this._requestStateCodec);
|
|
1315
|
+
const ctx = createContext(server, sdkCtx, token, sessionState, this._requestStateCodec, opts?.stateless);
|
|
1273
1316
|
return contextStore.run(
|
|
1274
1317
|
ctx,
|
|
1275
1318
|
() => runMiddlewareChain(this._middleware, "prompts/list", req.params, ctx, async () => {
|
|
@@ -1349,7 +1392,7 @@ var FastMCP = class {
|
|
|
1349
1392
|
);
|
|
1350
1393
|
}
|
|
1351
1394
|
}
|
|
1352
|
-
const ctx = createContext(server, sdkCtx, token, sessionState, this._requestStateCodec);
|
|
1395
|
+
const ctx = createContext(server, sdkCtx, token, sessionState, this._requestStateCodec, opts?.stateless);
|
|
1353
1396
|
return contextStore.run(
|
|
1354
1397
|
ctx,
|
|
1355
1398
|
() => runMiddlewareChain(this._middleware, "prompts/get", req.params, ctx, async () => {
|
|
@@ -1367,13 +1410,13 @@ var FastMCP = class {
|
|
|
1367
1410
|
() => clearTimeout(timer)
|
|
1368
1411
|
);
|
|
1369
1412
|
}
|
|
1370
|
-
return convertPromptResult(await executePromise);
|
|
1413
|
+
return convertPromptResult(await executePromise, opts?.stateless);
|
|
1371
1414
|
})
|
|
1372
1415
|
);
|
|
1373
1416
|
});
|
|
1374
1417
|
server.setRequestHandler("completion/complete", async (req, sdkCtx) => {
|
|
1375
1418
|
const token = await this._resolveToken(sdkCtx.http?.authInfo);
|
|
1376
|
-
const ctx = createContext(server, sdkCtx, token, sessionState, this._requestStateCodec);
|
|
1419
|
+
const ctx = createContext(server, sdkCtx, token, sessionState, this._requestStateCodec, opts?.stateless);
|
|
1377
1420
|
return contextStore.run(
|
|
1378
1421
|
ctx,
|
|
1379
1422
|
() => runMiddlewareChain(this._middleware, "completion/complete", req.params, ctx, async () => {
|
|
@@ -1995,6 +2038,7 @@ var FastMCP = class {
|
|
|
1995
2038
|
const port = options?.port ?? parseInt(process.env.MCP_PORT ?? process.env.PORT ?? "3000", 10);
|
|
1996
2039
|
const host = options?.host ?? process.env.MCP_HOST ?? "127.0.0.1";
|
|
1997
2040
|
const path = options?.path ?? process.env.MCP_PATH ?? "/mcp";
|
|
2041
|
+
this._stateless = options?.stateless ?? envBool("FASTMCP_STATELESS_HTTP") ?? false;
|
|
1998
2042
|
if (transport === "stdio") {
|
|
1999
2043
|
const { StdioServerTransport, serveStdio } = await import("@modelcontextprotocol/server/stdio");
|
|
2000
2044
|
const stdioTransport = new StdioServerTransport(options?.stdin, options?.stdout);
|
|
@@ -2028,6 +2072,24 @@ var FastMCP = class {
|
|
|
2028
2072
|
}
|
|
2029
2073
|
return this._modernHandler;
|
|
2030
2074
|
}
|
|
2075
|
+
/** Lazily builds the stateless legacy (2025-era) HTTP handler. One per FastMCP
|
|
2076
|
+
* instance, mirroring _getModernHandler's per-instance, per-request-factory model.
|
|
2077
|
+
*
|
|
2078
|
+
* The SDK serves each POST from a fresh instance of the factory over a transport
|
|
2079
|
+
* built with `sessionIdGenerator: undefined`, and answers GET and DELETE with 405
|
|
2080
|
+
* because both are session operations that mean nothing per request.
|
|
2081
|
+
*
|
|
2082
|
+
* Only reachable when `_stateless` is on. The sessionful transport in
|
|
2083
|
+
* _dispatchLegacyHttp is untouched and still serves every other deployment. */
|
|
2084
|
+
_getStatelessLegacyHandler() {
|
|
2085
|
+
if (!this._statelessLegacyHandler) {
|
|
2086
|
+
this._statelessLegacyHandler = legacyStatelessFallback(
|
|
2087
|
+
() => this._makeServer(/* @__PURE__ */ new Map(), { stateless: true }),
|
|
2088
|
+
(error) => console.error("[fastmcp] stateless legacy serving failed:", error)
|
|
2089
|
+
);
|
|
2090
|
+
}
|
|
2091
|
+
return this._statelessLegacyHandler;
|
|
2092
|
+
}
|
|
2031
2093
|
/**
|
|
2032
2094
|
* Dual-era HTTP dispatch shared by the OAuth and non-OAuth serve paths. Assumes CORS
|
|
2033
2095
|
* and auth have already been handled by the caller (req.auth already set, if any —
|
|
@@ -2045,7 +2107,11 @@ var FastMCP = class {
|
|
|
2045
2107
|
const legacy = await isLegacyRequest(request);
|
|
2046
2108
|
const parsedBody = req.method === "POST" ? await request.json().catch(() => void 0) : void 0;
|
|
2047
2109
|
if (legacy) {
|
|
2048
|
-
|
|
2110
|
+
if (this._stateless) {
|
|
2111
|
+
await toNodeHandler({ fetch: this._getStatelessLegacyHandler() })(req, res, parsedBody);
|
|
2112
|
+
} else {
|
|
2113
|
+
await this._dispatchLegacyHttp(req, res, parsedBody);
|
|
2114
|
+
}
|
|
2049
2115
|
} else {
|
|
2050
2116
|
await toNodeHandler(this._getModernHandler())(req, res, parsedBody);
|
|
2051
2117
|
}
|
|
@@ -2218,6 +2284,7 @@ var FastMCP = class {
|
|
|
2218
2284
|
await this._modernHandler.close();
|
|
2219
2285
|
this._modernHandler = null;
|
|
2220
2286
|
}
|
|
2287
|
+
this._statelessLegacyHandler = null;
|
|
2221
2288
|
if (this._stdioHandle) {
|
|
2222
2289
|
await this._stdioHandle.close();
|
|
2223
2290
|
this._stdioHandle = null;
|