@prefecthq/fastmcp-ts 1.3.0 → 1.5.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/dist/cli/index.cjs +3 -3
- package/dist/server.d.ts +100 -12
- package/dist/server.js +216 -16
- 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.
|
|
1175
|
+
fastmcp: "1.5.0",
|
|
1176
1176
|
"mcp-sdk": "2.0.0",
|
|
1177
1177
|
node: process.version,
|
|
1178
1178
|
platform: `${process.platform} ${process.arch}`
|
|
@@ -33404,7 +33404,7 @@ var init_inspect = __esm({
|
|
|
33404
33404
|
// only case Python's inspect supports); over --url/--command the
|
|
33405
33405
|
// generation is unknowable from the wire.
|
|
33406
33406
|
generation: mode.kind === "inprocess" ? 2 : null,
|
|
33407
|
-
fastmcpVersion: "1.
|
|
33407
|
+
fastmcpVersion: "1.5.0",
|
|
33408
33408
|
mcpVersion: "2.0.0",
|
|
33409
33409
|
tools: tools2,
|
|
33410
33410
|
prompts: prompts2,
|
|
@@ -46908,7 +46908,7 @@ init_format();
|
|
|
46908
46908
|
var main = defineCommand({
|
|
46909
46909
|
meta: {
|
|
46910
46910
|
name: "fastmcp",
|
|
46911
|
-
version: "1.
|
|
46911
|
+
version: "1.5.0",
|
|
46912
46912
|
description: "FastMCP CLI \u2014 build, run, and manage MCP servers"
|
|
46913
46913
|
},
|
|
46914
46914
|
args: {
|
package/dist/server.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as _modelcontextprotocol_server from '@modelcontextprotocol/server';
|
|
2
2
|
import { InputResponses, Server, CacheHint, ServerContext, ServerEventBus, Transport, McpHandlerRequestOptions, CallToolResult, OAuthClientInformationFull, AuthInfo } from '@modelcontextprotocol/server';
|
|
3
|
-
export { InputRequest, InputRequests, InputRequiredResult, InputRequiredSpec, InputResponse, InputResponseView, InputResponses, ServerEventBus, acceptedContent, inputRequired, inputResponse, isInputRequiredResult } from '@modelcontextprotocol/server';
|
|
3
|
+
export { AuthInfo, InputRequest, InputRequests, InputRequiredResult, InputRequiredSpec, InputResponse, InputResponseView, InputResponses, McpHandlerRequestOptions, ServerEventBus, acceptedContent, inputRequired, inputResponse, isInputRequiredResult } from '@modelcontextprotocol/server';
|
|
4
4
|
import { OAuthServerProvider, AuthorizationParams } from '@modelcontextprotocol/server-legacy/auth';
|
|
5
5
|
import { StandardSchemaV1 } from '@standard-schema/spec';
|
|
6
6
|
import { Readable, Writable } from 'node:stream';
|
|
@@ -407,6 +407,35 @@ interface McpContext {
|
|
|
407
407
|
onClose(callback: () => void): void;
|
|
408
408
|
}
|
|
409
409
|
|
|
410
|
+
/**
|
|
411
|
+
* Handler for a custom HTTP route served by the listener `run()` starts.
|
|
412
|
+
* Owns the whole exchange: no MCP auth, no DNS-rebinding guards, and no CORS
|
|
413
|
+
* headers run in front of it, and its Response is written back verbatim.
|
|
414
|
+
*/
|
|
415
|
+
type CustomRouteHandler = (request: Request) => Response | Promise<Response>;
|
|
416
|
+
interface CustomRouteConfig {
|
|
417
|
+
/** Exact request path to serve; the query string is ignored for matching.
|
|
418
|
+
* Must start with `/`. No patterns, no path parameters. */
|
|
419
|
+
path: string;
|
|
420
|
+
/**
|
|
421
|
+
* HTTP methods served, compared case-insensitively. Default: `['GET']`.
|
|
422
|
+
* `OPTIONS` is rejected at registration: the global CORS preflight answers
|
|
423
|
+
* `OPTIONS` before route dispatch, so a route on it would be unreachable.
|
|
424
|
+
*/
|
|
425
|
+
methods?: string[];
|
|
426
|
+
}
|
|
427
|
+
/** `RunOptions.health` object form. Presence implies enabled. */
|
|
428
|
+
interface HealthOptions {
|
|
429
|
+
/** Force off with `false`; the object's presence already implies `true`. */
|
|
430
|
+
enabled?: boolean;
|
|
431
|
+
/** Request path of the endpoint. Default: `/healthz`. */
|
|
432
|
+
path?: string;
|
|
433
|
+
/** Response status code. Default: `200`. */
|
|
434
|
+
status?: number;
|
|
435
|
+
/** Response body, served as `text/plain`. Default: `'ok'`. */
|
|
436
|
+
body?: string;
|
|
437
|
+
}
|
|
438
|
+
|
|
410
439
|
interface MiddlewareContext<T = unknown> {
|
|
411
440
|
readonly method: string;
|
|
412
441
|
readonly request: T;
|
|
@@ -871,11 +900,14 @@ interface FastMCPOptions {
|
|
|
871
900
|
*/
|
|
872
901
|
eventBus?: ServerEventBus;
|
|
873
902
|
/**
|
|
874
|
-
* DNS-rebinding protection for the HTTP
|
|
875
|
-
* `Origin` request headers (port-agnostic, by hostname) and rejects
|
|
876
|
-
* with `403`. Defends localhost servers against a malicious web page
|
|
877
|
-
* rebinds to `127.0.0.1` (MCP transport security best practice).
|
|
878
|
-
*
|
|
903
|
+
* DNS-rebinding protection for the HTTP listener started by `run()`: validates
|
|
904
|
+
* the `Host` and `Origin` request headers (port-agnostic, by hostname) and rejects
|
|
905
|
+
* mismatches with `403`. Defends localhost servers against a malicious web page
|
|
906
|
+
* whose DNS rebinds to `127.0.0.1` (MCP transport security best practice).
|
|
907
|
+
*
|
|
908
|
+
* This option does not apply to `fetch()`, which has no bind address from which
|
|
909
|
+
* to infer a trusted host. A framework embedding `fetch()` owns Host/Origin
|
|
910
|
+
* validation at its HTTP boundary. stdio is unaffected.
|
|
879
911
|
*
|
|
880
912
|
* Default posture (option omitted): protection auto-enables when, and only when,
|
|
881
913
|
* `run()` binds the HTTP server to a loopback host (`127.0.0.1`, `::1`,
|
|
@@ -918,6 +950,18 @@ interface RunOptions {
|
|
|
918
950
|
* is already stateless and is unaffected.
|
|
919
951
|
*/
|
|
920
952
|
stateless?: boolean;
|
|
953
|
+
/**
|
|
954
|
+
* Health endpoint on the HTTP listener `run()` starts — for Kubernetes
|
|
955
|
+
* liveness/readiness probes and load-balancer checks. Off unless supplied:
|
|
956
|
+
* `true` (or any object) enables it with defaults `path: '/healthz'`,
|
|
957
|
+
* `status: 200`, `body: 'ok'` (`text/plain`); `enabled: false` (or `false`)
|
|
958
|
+
* forces it off. Served before MCP auth, CORS, and the DNS-rebinding
|
|
959
|
+
* guards, so probes need no credentials and may send a pod-IP Host header.
|
|
960
|
+
* Sugar over `customRoute()` — registering your own route on the same path
|
|
961
|
+
* is a startup error. Ignored on stdio (a malformed value still aborts
|
|
962
|
+
* startup, like `stateless`) and never served through `fetch()`.
|
|
963
|
+
*/
|
|
964
|
+
health?: boolean | HealthOptions;
|
|
921
965
|
/** Custom stdin stream for the stdio transport. Defaults to process.stdin. */
|
|
922
966
|
stdin?: Readable;
|
|
923
967
|
/** Custom stdout stream for the stdio transport. Defaults to process.stdout. */
|
|
@@ -928,11 +972,31 @@ interface ServerAddress {
|
|
|
928
972
|
port: number;
|
|
929
973
|
path: string;
|
|
930
974
|
}
|
|
975
|
+
/**
|
|
976
|
+
* Behavioral hints for a tool (MCP 2025-03-26). Advisory only — clients may use
|
|
977
|
+
* them for UI/UX and safety decisions (e.g. auto-approving a read-only call); the
|
|
978
|
+
* server does not enforce them. Per the MCP spec, annotations from an untrusted
|
|
979
|
+
* server should not be relied upon.
|
|
980
|
+
*/
|
|
981
|
+
interface ToolAnnotations {
|
|
982
|
+
/** Human-readable title for display. */
|
|
983
|
+
title?: string;
|
|
984
|
+
/** The tool does not modify its environment. Default: false. */
|
|
985
|
+
readOnlyHint?: boolean;
|
|
986
|
+
/** The tool may perform destructive updates. Only meaningful when readOnlyHint is false. Default: true. */
|
|
987
|
+
destructiveHint?: boolean;
|
|
988
|
+
/** Repeated calls with the same arguments have no additional effect. Only meaningful when readOnlyHint is false. Default: false. */
|
|
989
|
+
idempotentHint?: boolean;
|
|
990
|
+
/** The tool may interact with an open world of external entities. Default: true. */
|
|
991
|
+
openWorldHint?: boolean;
|
|
992
|
+
}
|
|
931
993
|
interface ToolConfig {
|
|
932
994
|
name: string;
|
|
933
995
|
/** Human-readable display name shown in UIs. Takes precedence over `name` for display purposes. */
|
|
934
996
|
title?: string;
|
|
935
997
|
description: string;
|
|
998
|
+
/** Behavioral hints for clients (MCP 2025-03-26). Forwarded verbatim in tools/list. */
|
|
999
|
+
annotations?: ToolAnnotations;
|
|
936
1000
|
/** Standard Schema validator for the tool's input arguments. Used for runtime validation. */
|
|
937
1001
|
input?: StandardSchemaV1;
|
|
938
1002
|
/**
|
|
@@ -970,6 +1034,7 @@ declare class FastMCP {
|
|
|
970
1034
|
private _staticResources;
|
|
971
1035
|
private _templateResources;
|
|
972
1036
|
private _prompts;
|
|
1037
|
+
private _customRoutes;
|
|
973
1038
|
private _promptsPageSize;
|
|
974
1039
|
private _middleware;
|
|
975
1040
|
private _transforms;
|
|
@@ -1082,6 +1147,15 @@ declare class FastMCP {
|
|
|
1082
1147
|
tool(config: ToolConfig, handler: (args: Record<string, unknown>) => unknown): void;
|
|
1083
1148
|
prompt(config: PromptConfig, handler: (args?: Record<string, string>) => unknown): void;
|
|
1084
1149
|
resource(config: ResourceConfig, handler: (params?: Record<string, string>) => unknown): void;
|
|
1150
|
+
/**
|
|
1151
|
+
* Register a custom HTTP route on the listener `run()` starts — e.g. a
|
|
1152
|
+
* Kubernetes liveness/readiness probe. The handler owns the whole exchange:
|
|
1153
|
+
* no MCP auth, no DNS-rebinding guards, and no CORS headers run in front of
|
|
1154
|
+
* it. Matching is exact-path (query string ignored); methods default to GET.
|
|
1155
|
+
* Routes never serve on stdio or through `fetch()` — a framework embedding
|
|
1156
|
+
* `fetch()` owns its own routing (same posture as `dnsRebinding`).
|
|
1157
|
+
*/
|
|
1158
|
+
customRoute(config: CustomRouteConfig, handler: CustomRouteHandler): void;
|
|
1085
1159
|
_removeTool(name: string): boolean;
|
|
1086
1160
|
_removeResource(uri: string): boolean;
|
|
1087
1161
|
_removePrompt(name: string): boolean;
|
|
@@ -1249,6 +1323,19 @@ declare class FastMCP {
|
|
|
1249
1323
|
*/
|
|
1250
1324
|
get isRunning(): boolean;
|
|
1251
1325
|
connect(transport: Transport): Promise<void>;
|
|
1326
|
+
/**
|
|
1327
|
+
* Serve an MCP request through a web-standard, fetch-native HTTP entrypoint.
|
|
1328
|
+
*
|
|
1329
|
+
* This entrypoint is always stateless and does not require `run()`: legacy
|
|
1330
|
+
* (2025-era) requests use a fresh server instance per request, while modern
|
|
1331
|
+
* (2026-07-28) requests share the instance's modern handler and event bus.
|
|
1332
|
+
* Authentication is deliberately external — `options.authInfo` is trusted and
|
|
1333
|
+
* passed unchanged into the protocol handler, where it is exposed as `ctx.auth`.
|
|
1334
|
+
* The embedding framework also owns Host/Origin validation and should abort its
|
|
1335
|
+
* carrying requests during shutdown; stateless legacy response streams follow
|
|
1336
|
+
* that request lifecycle, while `close()` terminates modern exchanges and streams.
|
|
1337
|
+
*/
|
|
1338
|
+
fetch(request: Request, options?: McpHandlerRequestOptions): Promise<Response>;
|
|
1252
1339
|
/**
|
|
1253
1340
|
* Modern-era (2026-07-28) in-process fetch entrypoint — the `McpServerLike`
|
|
1254
1341
|
* duck-type hook `fastmcp-ts/client`'s `Client` looks for when a caller pins
|
|
@@ -1265,9 +1352,9 @@ declare class FastMCP {
|
|
|
1265
1352
|
/** Lazily builds the modern (2026-07-28) HTTP handler. One per FastMCP instance;
|
|
1266
1353
|
* builds a fresh Server (via _makeServer) per request, matching createMcpHandler's
|
|
1267
1354
|
* per-request-factory model. Modern-only (legacy: 'reject') — legacy (2025-era)
|
|
1268
|
-
* traffic is routed to the existing sessionful transport by
|
|
1269
|
-
*
|
|
1270
|
-
*
|
|
1355
|
+
* traffic is routed separately: to the existing sessionful transport by
|
|
1356
|
+
* _dispatchHttp, or to the stateless fallback by fetch(). This preserves the
|
|
1357
|
+
* session state and server-initiated-request shim for run()'s default HTTP path. */
|
|
1271
1358
|
private _getModernHandler;
|
|
1272
1359
|
/** Lazily builds the stateless legacy (2025-era) HTTP handler. One per FastMCP
|
|
1273
1360
|
* instance, mirroring _getModernHandler's per-instance, per-request-factory model.
|
|
@@ -1276,8 +1363,9 @@ declare class FastMCP {
|
|
|
1276
1363
|
* built with `sessionIdGenerator: undefined`, and answers GET and DELETE with 405
|
|
1277
1364
|
* because both are session operations that mean nothing per request.
|
|
1278
1365
|
*
|
|
1279
|
-
*
|
|
1280
|
-
* _dispatchLegacyHttp is untouched and
|
|
1366
|
+
* Used unconditionally by fetch(), and by run() only when `_stateless` is on.
|
|
1367
|
+
* The sessionful transport in _dispatchLegacyHttp is untouched and remains the
|
|
1368
|
+
* default for listener-based HTTP deployments. */
|
|
1281
1369
|
private _getStatelessLegacyHandler;
|
|
1282
1370
|
/**
|
|
1283
1371
|
* Dual-era HTTP dispatch shared by the OAuth and non-OAuth serve paths. Assumes CORS
|
|
@@ -1753,4 +1841,4 @@ declare class FormInput {
|
|
|
1753
1841
|
constructor(options: FormInputOptions);
|
|
1754
1842
|
}
|
|
1755
1843
|
|
|
1756
|
-
export { type AccessToken, Approval, Area, type AuthCheck, AuthorizationError, type BackendToolConfig, Badge, Bar, type BrowserPermissions, Button, type CacheKeyFn, CachingMiddleware, CancellationMiddleware, type CatalogEntry, Choice, Column, type CompleteCallback, type CompletionContext, type CompletionResult, type Component, type ComponentFn, type CspPolicy, type ElicitationResult, type ElicitationSchema, type EntrypointConfig, ErrorNormalizationMiddleware, FastMCP, FastMCPApp, type FastMCPAppOptions, type FastMCPOptions, File, type FileHandle, type FileStorageAdapter, FileUpload, type FileUploadOptions, FilterTransform, ForEach, FormInput, type FormInputOptions, GenerativeUI, Grid, type HTTPRoute, type HttpMethod, type HttpRequestContext, If, type IfNode, Image, Input, Line, type LogLevel, LoggingMiddleware, type MCPType, type McpContext, type Middleware, type MiddlewareContext, NamespaceTransform, type Next, type OAuthConfig, type OAuthProviderOptions, type OAuthProxyOptions, type OpenAPIClientOptions, type OpenAPIComponent, type OpenAPIResourceComponent, type OpenAPIResourceTemplateComponent, type OpenAPIServerOptions, type OpenAPIToolComponent, type ParameterInfo, Pie, type PromptArgument, type PromptConfig, type PromptContent, type PromptMessage, PromptResult, type PromptView, PromptsAsTools, type ProxyTransport, RateLimitingMiddleware, type RequestBodyInfo, type RequestVerifier, type ResourceAnnotations, type ResourceConfig, ResourceResult, type ResourceUiMeta, type ResourceView, ResourcesAsTools, type ResponseInfo, type Root, type RouteMap, type RouteMapFn, Row, type RunOptions, Rx, type SamplingMessage, type SamplingParams, type SamplingResult, Select, type ServerAddress, SizeLimitingMiddleware, type SynthesizedTool, Table, Text, type TokenVerifier, type ToolConfig, ToolResult, type ToolView, type Transform, type UiToolMeta, VersionFilter, type Visibility, actionRef, createOpenAPIServer, createProxy, debugTokenVerifier, forwardableHeaders, introspectionVerifier, jwtVerifier, multiAuth, oauthProvider, oauthProxy, redescribeTool, renameTool, requireScopes, staticTokenVerifier };
|
|
1844
|
+
export { type AccessToken, Approval, Area, type AuthCheck, AuthorizationError, type BackendToolConfig, Badge, Bar, type BrowserPermissions, Button, type CacheKeyFn, CachingMiddleware, CancellationMiddleware, type CatalogEntry, Choice, Column, type CompleteCallback, type CompletionContext, type CompletionResult, type Component, type ComponentFn, type CspPolicy, type CustomRouteConfig, type CustomRouteHandler, type ElicitationResult, type ElicitationSchema, type EntrypointConfig, ErrorNormalizationMiddleware, FastMCP, FastMCPApp, type FastMCPAppOptions, type FastMCPOptions, File, type FileHandle, type FileStorageAdapter, FileUpload, type FileUploadOptions, FilterTransform, ForEach, FormInput, type FormInputOptions, GenerativeUI, Grid, type HTTPRoute, type HealthOptions, type HttpMethod, type HttpRequestContext, If, type IfNode, Image, Input, Line, type LogLevel, LoggingMiddleware, type MCPType, type McpContext, type Middleware, type MiddlewareContext, NamespaceTransform, type Next, type OAuthConfig, type OAuthProviderOptions, type OAuthProxyOptions, type OpenAPIClientOptions, type OpenAPIComponent, type OpenAPIResourceComponent, type OpenAPIResourceTemplateComponent, type OpenAPIServerOptions, type OpenAPIToolComponent, type ParameterInfo, Pie, type PromptArgument, type PromptConfig, type PromptContent, type PromptMessage, PromptResult, type PromptView, PromptsAsTools, type ProxyTransport, RateLimitingMiddleware, type RequestBodyInfo, type RequestVerifier, type ResourceAnnotations, type ResourceConfig, ResourceResult, type ResourceUiMeta, type ResourceView, ResourcesAsTools, type ResponseInfo, type Root, type RouteMap, type RouteMapFn, Row, type RunOptions, Rx, type SamplingMessage, type SamplingParams, type SamplingResult, Select, type ServerAddress, SizeLimitingMiddleware, type SynthesizedTool, Table, Text, type TokenVerifier, type ToolAnnotations, type ToolConfig, ToolResult, type ToolView, type Transform, type UiToolMeta, VersionFilter, type Visibility, actionRef, createOpenAPIServer, createProxy, debugTokenVerifier, forwardableHeaders, introspectionVerifier, jwtVerifier, multiAuth, oauthProvider, oauthProxy, redescribeTool, renameTool, requireScopes, staticTokenVerifier };
|
package/dist/server.js
CHANGED
|
@@ -281,6 +281,106 @@ function createContext(server, sdkCtx, auth, sessionState, requestStateCodec, st
|
|
|
281
281
|
};
|
|
282
282
|
}
|
|
283
283
|
|
|
284
|
+
// src/server/customRoutes.ts
|
|
285
|
+
import { Readable } from "stream";
|
|
286
|
+
var CustomRouteRegistry = class {
|
|
287
|
+
_routes = /* @__PURE__ */ new Map();
|
|
288
|
+
register(config, handler) {
|
|
289
|
+
const path = config?.path;
|
|
290
|
+
if (typeof path !== "string" || !path.startsWith("/")) {
|
|
291
|
+
throw new Error(`customRoute path must be a string starting with "/", got: ${JSON.stringify(path)}`);
|
|
292
|
+
}
|
|
293
|
+
const methods = (config.methods ?? ["GET"]).map((m) => m.toUpperCase());
|
|
294
|
+
if (methods.length === 0) {
|
|
295
|
+
throw new Error(`customRoute ${path}: methods must not be empty (omit it for the GET default)`);
|
|
296
|
+
}
|
|
297
|
+
if (methods.includes("OPTIONS")) {
|
|
298
|
+
throw new Error(
|
|
299
|
+
`customRoute ${path}: OPTIONS cannot be routed \u2014 the CORS preflight handler answers OPTIONS before route dispatch`
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
if (this._routes.has(path)) {
|
|
303
|
+
throw new Error(`customRoute already registered for path ${path}`);
|
|
304
|
+
}
|
|
305
|
+
this._routes.set(path, { methods: new Set(methods), handler });
|
|
306
|
+
}
|
|
307
|
+
/** Throws when a registered route sits on the MCP endpoint path. Called at HTTP startup. */
|
|
308
|
+
assertNoMcpCollision(mcpPath) {
|
|
309
|
+
if (this._routes.has(mcpPath)) {
|
|
310
|
+
throw new Error(
|
|
311
|
+
`customRoute path ${mcpPath} collides with the MCP endpoint path; move the route or the MCP \`path\``
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
/** Match a request path + method. OPTIONS never matches (preflight owns it). */
|
|
316
|
+
match(pathname, method) {
|
|
317
|
+
const route = this._routes.get(pathname);
|
|
318
|
+
if (!route) return null;
|
|
319
|
+
const upper = method.toUpperCase();
|
|
320
|
+
if (upper === "OPTIONS") return null;
|
|
321
|
+
if (route.methods.has(upper)) return { kind: "handler", handler: route.handler };
|
|
322
|
+
return { kind: "method-mismatch", allow: [...route.methods].join(", ") };
|
|
323
|
+
}
|
|
324
|
+
paths() {
|
|
325
|
+
return [...this._routes.keys()];
|
|
326
|
+
}
|
|
327
|
+
get size() {
|
|
328
|
+
return this._routes.size;
|
|
329
|
+
}
|
|
330
|
+
};
|
|
331
|
+
var NULL_BODY_STATUSES = /* @__PURE__ */ new Set([101, 204, 205, 304]);
|
|
332
|
+
function resolveHealth(health) {
|
|
333
|
+
if (health === void 0 || health === false) return null;
|
|
334
|
+
if (health === true) return { path: "/healthz", status: 200, body: "ok" };
|
|
335
|
+
if (typeof health !== "object" || health === null) {
|
|
336
|
+
throw new Error(`Invalid health option: expected boolean or object, got ${typeof health}`);
|
|
337
|
+
}
|
|
338
|
+
if (health.enabled === false) return null;
|
|
339
|
+
const path = health.path ?? "/healthz";
|
|
340
|
+
if (typeof path !== "string" || !path.startsWith("/")) {
|
|
341
|
+
throw new Error(`Invalid health.path: must be a string starting with "/", got: ${JSON.stringify(health.path)}`);
|
|
342
|
+
}
|
|
343
|
+
const status = health.status ?? 200;
|
|
344
|
+
if (!Number.isInteger(status) || status < 100 || status > 599) {
|
|
345
|
+
throw new Error(`Invalid health.status: must be an integer HTTP status code, got: ${JSON.stringify(health.status)}`);
|
|
346
|
+
}
|
|
347
|
+
const body = health.body ?? "ok";
|
|
348
|
+
if (typeof body !== "string") {
|
|
349
|
+
throw new Error(`Invalid health.body: must be a string, got: ${typeof health.body}`);
|
|
350
|
+
}
|
|
351
|
+
if (body !== "" && NULL_BODY_STATUSES.has(status)) {
|
|
352
|
+
throw new Error(
|
|
353
|
+
`Invalid health config: status ${status} cannot carry a response body (the fetch spec forbids one on 101/204/205/304); set body: '' or choose a different status`
|
|
354
|
+
);
|
|
355
|
+
}
|
|
356
|
+
return { path, status, body };
|
|
357
|
+
}
|
|
358
|
+
async function serveCustomRouteNode(handler, req, res) {
|
|
359
|
+
const { toWebRequest } = await import("@modelcontextprotocol/node");
|
|
360
|
+
let response;
|
|
361
|
+
try {
|
|
362
|
+
response = await handler(await toWebRequest(req));
|
|
363
|
+
} catch (error) {
|
|
364
|
+
console.error("[fastmcp] custom route handler failed:", error);
|
|
365
|
+
res.writeHead(500, { "Content-Type": "application/json" }).end(JSON.stringify({ error: "Internal Server Error" }));
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
res.writeHead(response.status, Object.fromEntries(response.headers));
|
|
369
|
+
if (response.body) {
|
|
370
|
+
const stream = Readable.fromWeb(response.body);
|
|
371
|
+
stream.on("error", (error) => {
|
|
372
|
+
console.error("[fastmcp] custom route response stream failed:", error);
|
|
373
|
+
res.destroy();
|
|
374
|
+
});
|
|
375
|
+
stream.pipe(res);
|
|
376
|
+
} else {
|
|
377
|
+
res.end();
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
function writeMethodNotAllowed(res, allow) {
|
|
381
|
+
res.writeHead(405, { Allow: allow, "Content-Type": "application/json" }).end(JSON.stringify({ error: "Method Not Allowed" }));
|
|
382
|
+
}
|
|
383
|
+
|
|
284
384
|
// src/server/env.ts
|
|
285
385
|
var TRUE_VALUES = /* @__PURE__ */ new Set(["1", "true", "yes", "on"]);
|
|
286
386
|
var FALSE_VALUES = /* @__PURE__ */ new Set(["0", "false", "no", "off"]);
|
|
@@ -952,6 +1052,7 @@ var FastMCP = class {
|
|
|
952
1052
|
_staticResources = /* @__PURE__ */ new Map();
|
|
953
1053
|
_templateResources = /* @__PURE__ */ new Map();
|
|
954
1054
|
_prompts = /* @__PURE__ */ new Map();
|
|
1055
|
+
_customRoutes = new CustomRouteRegistry();
|
|
955
1056
|
_promptsPageSize;
|
|
956
1057
|
_middleware;
|
|
957
1058
|
_transforms;
|
|
@@ -1135,6 +1236,7 @@ var FastMCP = class {
|
|
|
1135
1236
|
description: entry.description,
|
|
1136
1237
|
inputSchema,
|
|
1137
1238
|
...outputSchema ? { outputSchema } : {},
|
|
1239
|
+
...t.annotations ? { annotations: t.annotations } : {},
|
|
1138
1240
|
...uiMeta ? { _meta: { ui: uiMeta } } : {}
|
|
1139
1241
|
};
|
|
1140
1242
|
})
|
|
@@ -1748,6 +1850,17 @@ var FastMCP = class {
|
|
|
1748
1850
|
this._notifyResourceListChanged();
|
|
1749
1851
|
for (const cb of this._resourceRegisteredCallbacks) cb(registered);
|
|
1750
1852
|
}
|
|
1853
|
+
/**
|
|
1854
|
+
* Register a custom HTTP route on the listener `run()` starts — e.g. a
|
|
1855
|
+
* Kubernetes liveness/readiness probe. The handler owns the whole exchange:
|
|
1856
|
+
* no MCP auth, no DNS-rebinding guards, and no CORS headers run in front of
|
|
1857
|
+
* it. Matching is exact-path (query string ignored); methods default to GET.
|
|
1858
|
+
* Routes never serve on stdio or through `fetch()` — a framework embedding
|
|
1859
|
+
* `fetch()` owns its own routing (same posture as `dnsRebinding`).
|
|
1860
|
+
*/
|
|
1861
|
+
customRoute(config, handler) {
|
|
1862
|
+
this._customRoutes.register(config, handler);
|
|
1863
|
+
}
|
|
1751
1864
|
_removeTool(name) {
|
|
1752
1865
|
if (!this._tools.has(name)) return false;
|
|
1753
1866
|
this._tools.delete(name);
|
|
@@ -2111,6 +2224,29 @@ var FastMCP = class {
|
|
|
2111
2224
|
this._primaryServer = this._makeServer();
|
|
2112
2225
|
await this._primaryServer.connect(transport);
|
|
2113
2226
|
}
|
|
2227
|
+
/**
|
|
2228
|
+
* Serve an MCP request through a web-standard, fetch-native HTTP entrypoint.
|
|
2229
|
+
*
|
|
2230
|
+
* This entrypoint is always stateless and does not require `run()`: legacy
|
|
2231
|
+
* (2025-era) requests use a fresh server instance per request, while modern
|
|
2232
|
+
* (2026-07-28) requests share the instance's modern handler and event bus.
|
|
2233
|
+
* Authentication is deliberately external — `options.authInfo` is trusted and
|
|
2234
|
+
* passed unchanged into the protocol handler, where it is exposed as `ctx.auth`.
|
|
2235
|
+
* The embedding framework also owns Host/Origin validation and should abort its
|
|
2236
|
+
* carrying requests during shutdown; stateless legacy response streams follow
|
|
2237
|
+
* that request lifecycle, while `close()` terminates modern exchanges and streams.
|
|
2238
|
+
*/
|
|
2239
|
+
async fetch(request, options) {
|
|
2240
|
+
if (request.method.toUpperCase() === "POST" && !isJsonContentType(request.headers.get("content-type"))) {
|
|
2241
|
+
return new Response(
|
|
2242
|
+
JSON.stringify({ error: "Unsupported Media Type: expected application/json" }),
|
|
2243
|
+
{ status: 415, headers: { "Content-Type": "application/json" } }
|
|
2244
|
+
);
|
|
2245
|
+
}
|
|
2246
|
+
const legacy = await isLegacyRequest(request, options?.parsedBody);
|
|
2247
|
+
if (legacy) return this._getStatelessLegacyHandler()(request, options);
|
|
2248
|
+
return this._getModernHandler().fetch(request, options);
|
|
2249
|
+
}
|
|
2114
2250
|
/**
|
|
2115
2251
|
* Modern-era (2026-07-28) in-process fetch entrypoint — the `McpServerLike`
|
|
2116
2252
|
* duck-type hook `fastmcp-ts/client`'s `Client` looks for when a caller pins
|
|
@@ -2136,6 +2272,7 @@ var FastMCP = class {
|
|
|
2136
2272
|
const host = options?.host ?? process.env.MCP_HOST ?? "127.0.0.1";
|
|
2137
2273
|
const path = options?.path ?? process.env.MCP_PATH ?? "/mcp";
|
|
2138
2274
|
this._stateless = options?.stateless ?? envBool("FASTMCP_STATELESS_HTTP") ?? false;
|
|
2275
|
+
const resolvedHealth = resolveHealth(options?.health);
|
|
2139
2276
|
if (transport === "stdio") {
|
|
2140
2277
|
const { StdioServerTransport, serveStdio } = await import("@modelcontextprotocol/server/stdio");
|
|
2141
2278
|
const stdioTransport = new StdioServerTransport(options?.stdin, options?.stdout);
|
|
@@ -2148,18 +2285,36 @@ var FastMCP = class {
|
|
|
2148
2285
|
},
|
|
2149
2286
|
{ transport: stdioTransport }
|
|
2150
2287
|
);
|
|
2151
|
-
} else if (this._oauth) {
|
|
2152
|
-
await this._runHttpOAuth(port, host, path);
|
|
2153
2288
|
} else {
|
|
2154
|
-
|
|
2289
|
+
if (resolvedHealth) {
|
|
2290
|
+
this._customRoutes.register(
|
|
2291
|
+
{ path: resolvedHealth.path },
|
|
2292
|
+
() => (
|
|
2293
|
+
// An empty body maps to a null Response body (no Content-Type either): the
|
|
2294
|
+
// fetch spec forbids any body, even '', on 101/204/205/304, and resolveHealth
|
|
2295
|
+
// already rejected a non-empty body on those statuses — so branching on the
|
|
2296
|
+
// body alone is enough to keep this call from throwing.
|
|
2297
|
+
resolvedHealth.body === "" ? new Response(null, { status: resolvedHealth.status }) : new Response(resolvedHealth.body, {
|
|
2298
|
+
status: resolvedHealth.status,
|
|
2299
|
+
headers: { "Content-Type": "text/plain" }
|
|
2300
|
+
})
|
|
2301
|
+
)
|
|
2302
|
+
);
|
|
2303
|
+
}
|
|
2304
|
+
this._customRoutes.assertNoMcpCollision(path);
|
|
2305
|
+
if (this._oauth) {
|
|
2306
|
+
await this._runHttpOAuth(port, host, path);
|
|
2307
|
+
} else {
|
|
2308
|
+
await this._runHttpSimple(port, host, path);
|
|
2309
|
+
}
|
|
2155
2310
|
}
|
|
2156
2311
|
}
|
|
2157
2312
|
/** Lazily builds the modern (2026-07-28) HTTP handler. One per FastMCP instance;
|
|
2158
2313
|
* builds a fresh Server (via _makeServer) per request, matching createMcpHandler's
|
|
2159
2314
|
* per-request-factory model. Modern-only (legacy: 'reject') — legacy (2025-era)
|
|
2160
|
-
* traffic is routed to the existing sessionful transport by
|
|
2161
|
-
*
|
|
2162
|
-
*
|
|
2315
|
+
* traffic is routed separately: to the existing sessionful transport by
|
|
2316
|
+
* _dispatchHttp, or to the stateless fallback by fetch(). This preserves the
|
|
2317
|
+
* session state and server-initiated-request shim for run()'s default HTTP path. */
|
|
2163
2318
|
_getModernHandler() {
|
|
2164
2319
|
if (!this._modernHandler) {
|
|
2165
2320
|
this._modernHandler = createMcpHandler(() => this._makeServer(/* @__PURE__ */ new Map(), { modern: true }), {
|
|
@@ -2176,8 +2331,9 @@ var FastMCP = class {
|
|
|
2176
2331
|
* built with `sessionIdGenerator: undefined`, and answers GET and DELETE with 405
|
|
2177
2332
|
* because both are session operations that mean nothing per request.
|
|
2178
2333
|
*
|
|
2179
|
-
*
|
|
2180
|
-
* _dispatchLegacyHttp is untouched and
|
|
2334
|
+
* Used unconditionally by fetch(), and by run() only when `_stateless` is on.
|
|
2335
|
+
* The sessionful transport in _dispatchLegacyHttp is untouched and remains the
|
|
2336
|
+
* default for listener-based HTTP deployments. */
|
|
2181
2337
|
_getStatelessLegacyHandler() {
|
|
2182
2338
|
if (!this._statelessLegacyHandler) {
|
|
2183
2339
|
this._statelessLegacyHandler = legacyStatelessFallback(
|
|
@@ -2285,6 +2441,12 @@ var FastMCP = class {
|
|
|
2285
2441
|
const { requireBearerAuth } = await import("@modelcontextprotocol/server-legacy/auth");
|
|
2286
2442
|
const oauth = this._oauth;
|
|
2287
2443
|
const app = express();
|
|
2444
|
+
app.use((req, res, next) => {
|
|
2445
|
+
const match = this._customRoutes.match(req.path, req.method ?? "");
|
|
2446
|
+
if (!match) return next();
|
|
2447
|
+
if (match.kind === "method-mismatch") return writeMethodNotAllowed(res, match.allow);
|
|
2448
|
+
void serveCustomRouteNode(match.handler, req, res);
|
|
2449
|
+
});
|
|
2288
2450
|
const httpServer = await new Promise((resolve, reject) => {
|
|
2289
2451
|
const srv = app.listen(port, host, () => resolve(srv));
|
|
2290
2452
|
srv.on("error", reject);
|
|
@@ -2328,7 +2490,14 @@ var FastMCP = class {
|
|
|
2328
2490
|
res.writeHead(204, corsHeaders).end();
|
|
2329
2491
|
return;
|
|
2330
2492
|
}
|
|
2331
|
-
|
|
2493
|
+
const pathname = req.url?.split("?")[0] ?? "";
|
|
2494
|
+
const routeMatch = this._customRoutes.match(pathname, req.method ?? "");
|
|
2495
|
+
if (routeMatch) {
|
|
2496
|
+
if (routeMatch.kind === "method-mismatch") writeMethodNotAllowed(res, routeMatch.allow);
|
|
2497
|
+
else await serveCustomRouteNode(routeMatch.handler, req, res);
|
|
2498
|
+
return;
|
|
2499
|
+
}
|
|
2500
|
+
if (pathname !== path) {
|
|
2332
2501
|
res.writeHead(404).end();
|
|
2333
2502
|
return;
|
|
2334
2503
|
}
|
|
@@ -2986,7 +3155,7 @@ var OPENAPI_SPECIFIC_FIELDS = [
|
|
|
2986
3155
|
var RECURSIVE_MAP_FIELDS = ["properties", "$defs", "$definitions"];
|
|
2987
3156
|
var RECURSIVE_SINGLE_FIELDS = ["items", "additionalProperties", "not"];
|
|
2988
3157
|
var RECURSIVE_LIST_FIELDS = ["allOf", "anyOf", "oneOf"];
|
|
2989
|
-
function convertOpenAPISchemaToJsonSchema(schema, openapiVersion) {
|
|
3158
|
+
function convertOpenAPISchemaToJsonSchema(schema, openapiVersion, options = {}) {
|
|
2990
3159
|
if (!isPlainObject(schema)) return schema;
|
|
2991
3160
|
let result = { ...schema };
|
|
2992
3161
|
if (openapiVersion?.startsWith("3.0")) {
|
|
@@ -3000,13 +3169,16 @@ function convertOpenAPISchemaToJsonSchema(schema, openapiVersion) {
|
|
|
3000
3169
|
for (const field of OPENAPI_SPECIFIC_FIELDS) {
|
|
3001
3170
|
delete result[field];
|
|
3002
3171
|
}
|
|
3172
|
+
if (options.removeReadOnly === true || options.removeWriteOnly === true) {
|
|
3173
|
+
result = filterPropertiesByAccess(result, options);
|
|
3174
|
+
}
|
|
3003
3175
|
for (const field of RECURSIVE_MAP_FIELDS) {
|
|
3004
3176
|
const map = result[field];
|
|
3005
3177
|
if (isPlainObject(map)) {
|
|
3006
3178
|
result[field] = Object.fromEntries(
|
|
3007
3179
|
Object.entries(map).map(([name, sub]) => [
|
|
3008
3180
|
name,
|
|
3009
|
-
isPlainObject(sub) ? convertOpenAPISchemaToJsonSchema(sub, openapiVersion) : sub
|
|
3181
|
+
isPlainObject(sub) ? convertOpenAPISchemaToJsonSchema(sub, openapiVersion, options) : sub
|
|
3010
3182
|
])
|
|
3011
3183
|
);
|
|
3012
3184
|
}
|
|
@@ -3014,14 +3186,14 @@ function convertOpenAPISchemaToJsonSchema(schema, openapiVersion) {
|
|
|
3014
3186
|
for (const field of RECURSIVE_SINGLE_FIELDS) {
|
|
3015
3187
|
const sub = result[field];
|
|
3016
3188
|
if (isPlainObject(sub)) {
|
|
3017
|
-
result[field] = convertOpenAPISchemaToJsonSchema(sub, openapiVersion);
|
|
3189
|
+
result[field] = convertOpenAPISchemaToJsonSchema(sub, openapiVersion, options);
|
|
3018
3190
|
}
|
|
3019
3191
|
}
|
|
3020
3192
|
for (const field of RECURSIVE_LIST_FIELDS) {
|
|
3021
3193
|
const list = result[field];
|
|
3022
3194
|
if (Array.isArray(list)) {
|
|
3023
3195
|
result[field] = list.map(
|
|
3024
|
-
(item) => isPlainObject(item) ? convertOpenAPISchemaToJsonSchema(item, openapiVersion) : item
|
|
3196
|
+
(item) => isPlainObject(item) ? convertOpenAPISchemaToJsonSchema(item, openapiVersion, options) : item
|
|
3025
3197
|
);
|
|
3026
3198
|
}
|
|
3027
3199
|
}
|
|
@@ -3082,6 +3254,28 @@ function requireDiscriminatorProperty(schema) {
|
|
|
3082
3254
|
}
|
|
3083
3255
|
return result;
|
|
3084
3256
|
}
|
|
3257
|
+
function filterPropertiesByAccess(schema, options) {
|
|
3258
|
+
const properties = schema.properties;
|
|
3259
|
+
if (!isPlainObject(properties)) return schema;
|
|
3260
|
+
const removed = /* @__PURE__ */ new Set();
|
|
3261
|
+
for (const [name, prop] of Object.entries(properties)) {
|
|
3262
|
+
if (!isPlainObject(prop)) continue;
|
|
3263
|
+
if (options.removeReadOnly === true && truthy(prop.readOnly) || options.removeWriteOnly === true && truthy(prop.writeOnly)) {
|
|
3264
|
+
removed.add(name);
|
|
3265
|
+
}
|
|
3266
|
+
}
|
|
3267
|
+
if (removed.size === 0) return schema;
|
|
3268
|
+
const result = { ...schema };
|
|
3269
|
+
result.properties = Object.fromEntries(
|
|
3270
|
+
Object.entries(properties).filter(([name]) => !removed.has(name))
|
|
3271
|
+
);
|
|
3272
|
+
if (Array.isArray(result.required)) {
|
|
3273
|
+
result.required = result.required.filter(
|
|
3274
|
+
(name) => typeof name !== "string" || !removed.has(name)
|
|
3275
|
+
);
|
|
3276
|
+
}
|
|
3277
|
+
return result;
|
|
3278
|
+
}
|
|
3085
3279
|
|
|
3086
3280
|
// src/server/openapi/schemas.ts
|
|
3087
3281
|
var EXTERNAL_REF_MESSAGE = "External or non-local reference not supported";
|
|
@@ -3351,7 +3545,9 @@ function combineSchemasAndMapParams(route) {
|
|
|
3351
3545
|
result.$defs = route.requestSchemas;
|
|
3352
3546
|
}
|
|
3353
3547
|
if (route.openapiVersion?.startsWith("3")) {
|
|
3354
|
-
result = convertOpenAPISchemaToJsonSchema(result, route.openapiVersion
|
|
3548
|
+
result = convertOpenAPISchemaToJsonSchema(result, route.openapiVersion, {
|
|
3549
|
+
removeReadOnly: true
|
|
3550
|
+
});
|
|
3355
3551
|
}
|
|
3356
3552
|
return [result, parameterMap];
|
|
3357
3553
|
}
|
|
@@ -3404,7 +3600,9 @@ function extractOutputSchemaFromResponses(responses, schemaDefinitions, openapiV
|
|
|
3404
3600
|
}
|
|
3405
3601
|
}
|
|
3406
3602
|
if (openapiVersion?.startsWith("3")) {
|
|
3407
|
-
outputSchema = convertOpenAPISchemaToJsonSchema(outputSchema, openapiVersion
|
|
3603
|
+
outputSchema = convertOpenAPISchemaToJsonSchema(outputSchema, openapiVersion, {
|
|
3604
|
+
removeWriteOnly: true
|
|
3605
|
+
});
|
|
3408
3606
|
}
|
|
3409
3607
|
if (outputSchema.type !== "object") {
|
|
3410
3608
|
outputSchema = {
|
|
@@ -3423,7 +3621,9 @@ function extractOutputSchemaFromResponses(responses, schemaDefinitions, openapiV
|
|
|
3423
3621
|
}
|
|
3424
3622
|
if (openapiVersion?.startsWith("3")) {
|
|
3425
3623
|
for (const name of Object.keys(processedDefs)) {
|
|
3426
|
-
processedDefs[name] = convertOpenAPISchemaToJsonSchema(processedDefs[name], openapiVersion
|
|
3624
|
+
processedDefs[name] = convertOpenAPISchemaToJsonSchema(processedDefs[name], openapiVersion, {
|
|
3625
|
+
removeWriteOnly: true
|
|
3626
|
+
});
|
|
3427
3627
|
}
|
|
3428
3628
|
}
|
|
3429
3629
|
outputSchema.$defs = processedDefs;
|