@prefecthq/fastmcp-ts 1.4.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 +72 -1
- package/dist/server.js +187 -11
- 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
|
@@ -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;
|
|
@@ -921,6 +950,18 @@ interface RunOptions {
|
|
|
921
950
|
* is already stateless and is unaffected.
|
|
922
951
|
*/
|
|
923
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;
|
|
924
965
|
/** Custom stdin stream for the stdio transport. Defaults to process.stdin. */
|
|
925
966
|
stdin?: Readable;
|
|
926
967
|
/** Custom stdout stream for the stdio transport. Defaults to process.stdout. */
|
|
@@ -931,11 +972,31 @@ interface ServerAddress {
|
|
|
931
972
|
port: number;
|
|
932
973
|
path: string;
|
|
933
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
|
+
}
|
|
934
993
|
interface ToolConfig {
|
|
935
994
|
name: string;
|
|
936
995
|
/** Human-readable display name shown in UIs. Takes precedence over `name` for display purposes. */
|
|
937
996
|
title?: string;
|
|
938
997
|
description: string;
|
|
998
|
+
/** Behavioral hints for clients (MCP 2025-03-26). Forwarded verbatim in tools/list. */
|
|
999
|
+
annotations?: ToolAnnotations;
|
|
939
1000
|
/** Standard Schema validator for the tool's input arguments. Used for runtime validation. */
|
|
940
1001
|
input?: StandardSchemaV1;
|
|
941
1002
|
/**
|
|
@@ -973,6 +1034,7 @@ declare class FastMCP {
|
|
|
973
1034
|
private _staticResources;
|
|
974
1035
|
private _templateResources;
|
|
975
1036
|
private _prompts;
|
|
1037
|
+
private _customRoutes;
|
|
976
1038
|
private _promptsPageSize;
|
|
977
1039
|
private _middleware;
|
|
978
1040
|
private _transforms;
|
|
@@ -1085,6 +1147,15 @@ declare class FastMCP {
|
|
|
1085
1147
|
tool(config: ToolConfig, handler: (args: Record<string, unknown>) => unknown): void;
|
|
1086
1148
|
prompt(config: PromptConfig, handler: (args?: Record<string, string>) => unknown): void;
|
|
1087
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;
|
|
1088
1159
|
_removeTool(name: string): boolean;
|
|
1089
1160
|
_removeResource(uri: string): boolean;
|
|
1090
1161
|
_removePrompt(name: string): boolean;
|
|
@@ -1770,4 +1841,4 @@ declare class FormInput {
|
|
|
1770
1841
|
constructor(options: FormInputOptions);
|
|
1771
1842
|
}
|
|
1772
1843
|
|
|
1773
|
-
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);
|
|
@@ -2159,6 +2272,7 @@ var FastMCP = class {
|
|
|
2159
2272
|
const host = options?.host ?? process.env.MCP_HOST ?? "127.0.0.1";
|
|
2160
2273
|
const path = options?.path ?? process.env.MCP_PATH ?? "/mcp";
|
|
2161
2274
|
this._stateless = options?.stateless ?? envBool("FASTMCP_STATELESS_HTTP") ?? false;
|
|
2275
|
+
const resolvedHealth = resolveHealth(options?.health);
|
|
2162
2276
|
if (transport === "stdio") {
|
|
2163
2277
|
const { StdioServerTransport, serveStdio } = await import("@modelcontextprotocol/server/stdio");
|
|
2164
2278
|
const stdioTransport = new StdioServerTransport(options?.stdin, options?.stdout);
|
|
@@ -2171,10 +2285,28 @@ var FastMCP = class {
|
|
|
2171
2285
|
},
|
|
2172
2286
|
{ transport: stdioTransport }
|
|
2173
2287
|
);
|
|
2174
|
-
} else if (this._oauth) {
|
|
2175
|
-
await this._runHttpOAuth(port, host, path);
|
|
2176
2288
|
} else {
|
|
2177
|
-
|
|
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
|
+
}
|
|
2178
2310
|
}
|
|
2179
2311
|
}
|
|
2180
2312
|
/** Lazily builds the modern (2026-07-28) HTTP handler. One per FastMCP instance;
|
|
@@ -2309,6 +2441,12 @@ var FastMCP = class {
|
|
|
2309
2441
|
const { requireBearerAuth } = await import("@modelcontextprotocol/server-legacy/auth");
|
|
2310
2442
|
const oauth = this._oauth;
|
|
2311
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
|
+
});
|
|
2312
2450
|
const httpServer = await new Promise((resolve, reject) => {
|
|
2313
2451
|
const srv = app.listen(port, host, () => resolve(srv));
|
|
2314
2452
|
srv.on("error", reject);
|
|
@@ -2352,7 +2490,14 @@ var FastMCP = class {
|
|
|
2352
2490
|
res.writeHead(204, corsHeaders).end();
|
|
2353
2491
|
return;
|
|
2354
2492
|
}
|
|
2355
|
-
|
|
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) {
|
|
2356
2501
|
res.writeHead(404).end();
|
|
2357
2502
|
return;
|
|
2358
2503
|
}
|
|
@@ -3010,7 +3155,7 @@ var OPENAPI_SPECIFIC_FIELDS = [
|
|
|
3010
3155
|
var RECURSIVE_MAP_FIELDS = ["properties", "$defs", "$definitions"];
|
|
3011
3156
|
var RECURSIVE_SINGLE_FIELDS = ["items", "additionalProperties", "not"];
|
|
3012
3157
|
var RECURSIVE_LIST_FIELDS = ["allOf", "anyOf", "oneOf"];
|
|
3013
|
-
function convertOpenAPISchemaToJsonSchema(schema, openapiVersion) {
|
|
3158
|
+
function convertOpenAPISchemaToJsonSchema(schema, openapiVersion, options = {}) {
|
|
3014
3159
|
if (!isPlainObject(schema)) return schema;
|
|
3015
3160
|
let result = { ...schema };
|
|
3016
3161
|
if (openapiVersion?.startsWith("3.0")) {
|
|
@@ -3024,13 +3169,16 @@ function convertOpenAPISchemaToJsonSchema(schema, openapiVersion) {
|
|
|
3024
3169
|
for (const field of OPENAPI_SPECIFIC_FIELDS) {
|
|
3025
3170
|
delete result[field];
|
|
3026
3171
|
}
|
|
3172
|
+
if (options.removeReadOnly === true || options.removeWriteOnly === true) {
|
|
3173
|
+
result = filterPropertiesByAccess(result, options);
|
|
3174
|
+
}
|
|
3027
3175
|
for (const field of RECURSIVE_MAP_FIELDS) {
|
|
3028
3176
|
const map = result[field];
|
|
3029
3177
|
if (isPlainObject(map)) {
|
|
3030
3178
|
result[field] = Object.fromEntries(
|
|
3031
3179
|
Object.entries(map).map(([name, sub]) => [
|
|
3032
3180
|
name,
|
|
3033
|
-
isPlainObject(sub) ? convertOpenAPISchemaToJsonSchema(sub, openapiVersion) : sub
|
|
3181
|
+
isPlainObject(sub) ? convertOpenAPISchemaToJsonSchema(sub, openapiVersion, options) : sub
|
|
3034
3182
|
])
|
|
3035
3183
|
);
|
|
3036
3184
|
}
|
|
@@ -3038,14 +3186,14 @@ function convertOpenAPISchemaToJsonSchema(schema, openapiVersion) {
|
|
|
3038
3186
|
for (const field of RECURSIVE_SINGLE_FIELDS) {
|
|
3039
3187
|
const sub = result[field];
|
|
3040
3188
|
if (isPlainObject(sub)) {
|
|
3041
|
-
result[field] = convertOpenAPISchemaToJsonSchema(sub, openapiVersion);
|
|
3189
|
+
result[field] = convertOpenAPISchemaToJsonSchema(sub, openapiVersion, options);
|
|
3042
3190
|
}
|
|
3043
3191
|
}
|
|
3044
3192
|
for (const field of RECURSIVE_LIST_FIELDS) {
|
|
3045
3193
|
const list = result[field];
|
|
3046
3194
|
if (Array.isArray(list)) {
|
|
3047
3195
|
result[field] = list.map(
|
|
3048
|
-
(item) => isPlainObject(item) ? convertOpenAPISchemaToJsonSchema(item, openapiVersion) : item
|
|
3196
|
+
(item) => isPlainObject(item) ? convertOpenAPISchemaToJsonSchema(item, openapiVersion, options) : item
|
|
3049
3197
|
);
|
|
3050
3198
|
}
|
|
3051
3199
|
}
|
|
@@ -3106,6 +3254,28 @@ function requireDiscriminatorProperty(schema) {
|
|
|
3106
3254
|
}
|
|
3107
3255
|
return result;
|
|
3108
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
|
+
}
|
|
3109
3279
|
|
|
3110
3280
|
// src/server/openapi/schemas.ts
|
|
3111
3281
|
var EXTERNAL_REF_MESSAGE = "External or non-local reference not supported";
|
|
@@ -3375,7 +3545,9 @@ function combineSchemasAndMapParams(route) {
|
|
|
3375
3545
|
result.$defs = route.requestSchemas;
|
|
3376
3546
|
}
|
|
3377
3547
|
if (route.openapiVersion?.startsWith("3")) {
|
|
3378
|
-
result = convertOpenAPISchemaToJsonSchema(result, route.openapiVersion
|
|
3548
|
+
result = convertOpenAPISchemaToJsonSchema(result, route.openapiVersion, {
|
|
3549
|
+
removeReadOnly: true
|
|
3550
|
+
});
|
|
3379
3551
|
}
|
|
3380
3552
|
return [result, parameterMap];
|
|
3381
3553
|
}
|
|
@@ -3428,7 +3600,9 @@ function extractOutputSchemaFromResponses(responses, schemaDefinitions, openapiV
|
|
|
3428
3600
|
}
|
|
3429
3601
|
}
|
|
3430
3602
|
if (openapiVersion?.startsWith("3")) {
|
|
3431
|
-
outputSchema = convertOpenAPISchemaToJsonSchema(outputSchema, openapiVersion
|
|
3603
|
+
outputSchema = convertOpenAPISchemaToJsonSchema(outputSchema, openapiVersion, {
|
|
3604
|
+
removeWriteOnly: true
|
|
3605
|
+
});
|
|
3432
3606
|
}
|
|
3433
3607
|
if (outputSchema.type !== "object") {
|
|
3434
3608
|
outputSchema = {
|
|
@@ -3447,7 +3621,9 @@ function extractOutputSchemaFromResponses(responses, schemaDefinitions, openapiV
|
|
|
3447
3621
|
}
|
|
3448
3622
|
if (openapiVersion?.startsWith("3")) {
|
|
3449
3623
|
for (const name of Object.keys(processedDefs)) {
|
|
3450
|
-
processedDefs[name] = convertOpenAPISchemaToJsonSchema(processedDefs[name], openapiVersion
|
|
3624
|
+
processedDefs[name] = convertOpenAPISchemaToJsonSchema(processedDefs[name], openapiVersion, {
|
|
3625
|
+
removeWriteOnly: true
|
|
3626
|
+
});
|
|
3451
3627
|
}
|
|
3452
3628
|
}
|
|
3453
3629
|
outputSchema.$defs = processedDefs;
|