@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.
@@ -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.4.0",
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.4.0",
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.4.0",
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
- await this._runHttpSimple(port, host, path);
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
- if (req.url?.split("?")[0] !== path) {
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;