@prefecthq/fastmcp-ts 1.2.0 → 1.3.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 +21 -13
- package/dist/cli/index.cjs.map +1 -1
- package/dist/client.d.ts +6 -6
- package/dist/client.js +11 -10
- package/dist/client.js.map +1 -1
- package/dist/server.d.ts +312 -4
- package/dist/server.js +1622 -28
- package/dist/server.js.map +1 -1
- package/package.json +1 -1
package/dist/server.js
CHANGED
|
@@ -8,6 +8,9 @@ import { ProtocolError as ProtocolError3, ProtocolErrorCode as ProtocolErrorCode
|
|
|
8
8
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
9
9
|
|
|
10
10
|
// src/server/auth/types.ts
|
|
11
|
+
function isRequestVerifier(v) {
|
|
12
|
+
return "verifyRequest" in v;
|
|
13
|
+
}
|
|
11
14
|
var AuthorizationError = class extends Error {
|
|
12
15
|
constructor(message) {
|
|
13
16
|
super(message);
|
|
@@ -57,12 +60,94 @@ var BoundedEventStore = class {
|
|
|
57
60
|
|
|
58
61
|
// src/server/context.ts
|
|
59
62
|
import { AsyncLocalStorage } from "async_hooks";
|
|
63
|
+
|
|
64
|
+
// src/server/httpContext.ts
|
|
65
|
+
var DEFAULT_SENSITIVE_HEADERS = [
|
|
66
|
+
"authorization",
|
|
67
|
+
"cookie",
|
|
68
|
+
"proxy-authorization",
|
|
69
|
+
"mcp-session-id"
|
|
70
|
+
];
|
|
71
|
+
function resolveSensitiveHeaders(options) {
|
|
72
|
+
const set = new Set(DEFAULT_SENSITIVE_HEADERS);
|
|
73
|
+
for (const h of options?.redactHeaders ?? []) set.add(h.toLowerCase());
|
|
74
|
+
for (const h of options?.exposeHeaders ?? []) set.delete(h.toLowerCase());
|
|
75
|
+
return set;
|
|
76
|
+
}
|
|
77
|
+
function buildHttpRequestContext(req, sensitiveHeaders) {
|
|
78
|
+
const headers = new Headers();
|
|
79
|
+
const redacted = [];
|
|
80
|
+
req.headers.forEach((value, name) => {
|
|
81
|
+
if (sensitiveHeaders.has(name)) {
|
|
82
|
+
redacted.push(name);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
headers.append(name, value);
|
|
86
|
+
});
|
|
87
|
+
redacted.sort();
|
|
88
|
+
let url;
|
|
89
|
+
try {
|
|
90
|
+
const u = new URL(req.url);
|
|
91
|
+
url = u.pathname + u.search;
|
|
92
|
+
} catch {
|
|
93
|
+
url = req.url;
|
|
94
|
+
}
|
|
95
|
+
return { headers, redactedHeaderNames: redacted, method: req.method, url };
|
|
96
|
+
}
|
|
97
|
+
function nodeRequestToHttpContext(req) {
|
|
98
|
+
const headers = new Headers();
|
|
99
|
+
for (const [name, value] of Object.entries(req.headers)) {
|
|
100
|
+
if (value === void 0) continue;
|
|
101
|
+
if (Array.isArray(value)) {
|
|
102
|
+
for (const v of value) headers.append(name, v);
|
|
103
|
+
} else {
|
|
104
|
+
headers.append(name, value);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return { headers, redactedHeaderNames: [], method: req.method ?? "GET", url: req.url ?? "/" };
|
|
108
|
+
}
|
|
109
|
+
var UNFORWARDABLE = /* @__PURE__ */ new Set([
|
|
110
|
+
// credentials (see also DEFAULT_SENSITIVE_HEADERS; kept here too so the
|
|
111
|
+
// helper is safe on Headers that did not come from a redacted ctx.http)
|
|
112
|
+
"authorization",
|
|
113
|
+
"cookie",
|
|
114
|
+
"proxy-authorization",
|
|
115
|
+
"proxy-authenticate",
|
|
116
|
+
// hop-by-hop (RFC 9110 §7.6.1) and connection management
|
|
117
|
+
"connection",
|
|
118
|
+
"keep-alive",
|
|
119
|
+
"te",
|
|
120
|
+
"trailer",
|
|
121
|
+
"transfer-encoding",
|
|
122
|
+
"upgrade",
|
|
123
|
+
"proxy-connection",
|
|
124
|
+
// message framing / target-specific
|
|
125
|
+
"host",
|
|
126
|
+
"content-length",
|
|
127
|
+
"content-type",
|
|
128
|
+
"expect"
|
|
129
|
+
]);
|
|
130
|
+
function forwardableHeaders(headers, options) {
|
|
131
|
+
const include = new Set((options?.include ?? []).map((h) => h.toLowerCase()));
|
|
132
|
+
const out = new Headers();
|
|
133
|
+
headers.forEach((value, name) => {
|
|
134
|
+
if (include.has(name)) {
|
|
135
|
+
out.append(name, value);
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
if (UNFORWARDABLE.has(name) || name.startsWith("mcp-")) return;
|
|
139
|
+
out.append(name, value);
|
|
140
|
+
});
|
|
141
|
+
return out;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// src/server/context.ts
|
|
60
145
|
var contextStore = new AsyncLocalStorage();
|
|
61
146
|
var SESSION_CLOSE_CALLBACKS_KEY = "__fastmcp_session_close_callbacks";
|
|
62
147
|
var SESSION_STATE_MODERN_HTTP_ERROR = "[fastmcp] Session state is not available on modern HTTP requests (protocol revision 2026-07-28). Each request runs statelessly with no shared session store. Use ctx.requestState() to read per-request state. Use ctx.mintRequestState() to carry state across a multi-round-trip flow.";
|
|
63
148
|
var SESSION_STATE_STATELESS_HTTP_ERROR = "[fastmcp] Session state is not available on a stateless HTTP server (RunOptions.stateless or FASTMCP_STATELESS_HTTP). Each request runs against a fresh store that is discarded when the request ends. Use ctx.requestState() to read per-request state. Use ctx.mintRequestState() to carry state across a multi-round-trip flow. Turn stateless off to use session state, or keep state in your own external store.";
|
|
64
149
|
var SERVER_INITIATED_STATELESS_HTTP_ERROR = "[fastmcp] Server-initiated requests (ctx.elicit, ctx.sample, ctx.listRoots) are not available on a stateless HTTP server (RunOptions.stateless or FASTMCP_STATELESS_HTTP). They need a session: client capabilities are negotiated once at initialize, and the client replies on a separate request. Return inputRequired({ requestState }) to carry a multi-round-trip flow without a session; the embedded-request form of inputRequired needs a session too. Turn stateless off to use server-initiated requests.";
|
|
65
|
-
function createContext(server, sdkCtx, auth, sessionState, requestStateCodec, stateless) {
|
|
150
|
+
function createContext(server, sdkCtx, auth, sessionState, requestStateCodec, stateless, sensitiveHeaders) {
|
|
66
151
|
const requestId = String(sdkCtx.mcpReq.id);
|
|
67
152
|
const progressToken = sdkCtx.mcpReq._meta?.progressToken;
|
|
68
153
|
const isModernEra = sdkCtx.mcpReq.envelope !== void 0;
|
|
@@ -75,9 +160,12 @@ function createContext(server, sdkCtx, auth, sessionState, requestStateCodec, st
|
|
|
75
160
|
async function log(level, message, loggerName) {
|
|
76
161
|
await sdkCtx.mcpReq.log(level, message, loggerName);
|
|
77
162
|
}
|
|
163
|
+
const httpReq = sdkCtx.http?.req;
|
|
164
|
+
const sensitive = sensitiveHeaders ?? resolveSensitiveHeaders();
|
|
78
165
|
return {
|
|
79
166
|
auth,
|
|
80
167
|
requestId,
|
|
168
|
+
http: httpReq ? buildHttpRequestContext(httpReq, sensitive) : void 0,
|
|
81
169
|
log,
|
|
82
170
|
debug: (msg, logger) => log("debug", msg, logger),
|
|
83
171
|
info: (msg, logger) => log("info", msg, logger),
|
|
@@ -683,6 +771,12 @@ function convertResourceResult(value, uri, mimeType, stateless) {
|
|
|
683
771
|
}
|
|
684
772
|
return { contents: [{ uri, mimeType: mimeType ?? "text/plain", text: String(value) }] };
|
|
685
773
|
}
|
|
774
|
+
function attachResourceUiMeta(result, ui, uiCapable) {
|
|
775
|
+
if (!ui || !uiCapable || isInputRequiredResult(result)) return result;
|
|
776
|
+
return {
|
|
777
|
+
contents: result.contents.map((c) => ({ ...c, _meta: { ui, ...c._meta } }))
|
|
778
|
+
};
|
|
779
|
+
}
|
|
686
780
|
function isUriTemplate(uri) {
|
|
687
781
|
return uri.includes("{");
|
|
688
782
|
}
|
|
@@ -820,7 +914,7 @@ function toAccessToken(authInfo) {
|
|
|
820
914
|
}
|
|
821
915
|
var _cliEnvToken;
|
|
822
916
|
async function resolveCliEnvToken(verifier) {
|
|
823
|
-
if (!verifier) return void 0;
|
|
917
|
+
if (!verifier || !("verify" in verifier)) return void 0;
|
|
824
918
|
if (_cliEnvToken !== void 0) return _cliEnvToken ?? void 0;
|
|
825
919
|
const raw = process.env["FASTMCP_CLI_AUTH_TOKEN"];
|
|
826
920
|
if (!raw) {
|
|
@@ -851,6 +945,7 @@ var FastMCP = class {
|
|
|
851
945
|
version;
|
|
852
946
|
_auth;
|
|
853
947
|
_oauth;
|
|
948
|
+
_sensitiveHeaders;
|
|
854
949
|
_toolsPageSize;
|
|
855
950
|
_resourcesPageSize;
|
|
856
951
|
_tools = /* @__PURE__ */ new Map();
|
|
@@ -900,6 +995,7 @@ var FastMCP = class {
|
|
|
900
995
|
this.version = options.version ?? "0.0.1";
|
|
901
996
|
this._auth = options.auth;
|
|
902
997
|
this._oauth = options.oauth;
|
|
998
|
+
this._sensitiveHeaders = resolveSensitiveHeaders(options.http);
|
|
903
999
|
this._toolsPageSize = options.toolsPageSize ?? 50;
|
|
904
1000
|
this._resourcesPageSize = options.resourcesPageSize ?? 50;
|
|
905
1001
|
this._promptsPageSize = options.promptsPageSize ?? 50;
|
|
@@ -976,7 +1072,7 @@ var FastMCP = class {
|
|
|
976
1072
|
_setupHandlers(server, sessionState, opts) {
|
|
977
1073
|
server.setRequestHandler("tools/list", async (req, sdkCtx) => {
|
|
978
1074
|
const token = await this._resolveToken(sdkCtx.http?.authInfo);
|
|
979
|
-
const ctx = createContext(server, sdkCtx, token, sessionState, this._requestStateCodec, opts?.stateless);
|
|
1075
|
+
const ctx = createContext(server, sdkCtx, token, sessionState, this._requestStateCodec, opts?.stateless, this._sensitiveHeaders);
|
|
980
1076
|
return contextStore.run(
|
|
981
1077
|
ctx,
|
|
982
1078
|
() => runMiddlewareChain(this._middleware, "tools/list", req.params, ctx, async () => {
|
|
@@ -1055,7 +1151,7 @@ var FastMCP = class {
|
|
|
1055
1151
|
const synthTool = synthesizedList.find((s) => s.name === requestedName);
|
|
1056
1152
|
if (synthTool) {
|
|
1057
1153
|
if (synthTool.auth) await runAuthCheck(synthTool.auth, token);
|
|
1058
|
-
const ctx2 = createContext(server, sdkCtx, token, sessionState, this._requestStateCodec, opts?.stateless);
|
|
1154
|
+
const ctx2 = createContext(server, sdkCtx, token, sessionState, this._requestStateCodec, opts?.stateless, this._sensitiveHeaders);
|
|
1059
1155
|
try {
|
|
1060
1156
|
return await contextStore.run(
|
|
1061
1157
|
ctx2,
|
|
@@ -1108,7 +1204,7 @@ var FastMCP = class {
|
|
|
1108
1204
|
if (tool.config.auth) await runAuthCheck(tool.config.auth, token);
|
|
1109
1205
|
const resolvedTool = tool;
|
|
1110
1206
|
const rawArgs = req.params.arguments ?? {};
|
|
1111
|
-
const ctx = createContext(server, sdkCtx, token, sessionState, this._requestStateCodec, opts?.stateless);
|
|
1207
|
+
const ctx = createContext(server, sdkCtx, token, sessionState, this._requestStateCodec, opts?.stateless, this._sensitiveHeaders);
|
|
1112
1208
|
try {
|
|
1113
1209
|
return await contextStore.run(
|
|
1114
1210
|
ctx,
|
|
@@ -1150,7 +1246,7 @@ var FastMCP = class {
|
|
|
1150
1246
|
});
|
|
1151
1247
|
server.setRequestHandler("resources/list", async (req, sdkCtx) => {
|
|
1152
1248
|
const token = await this._resolveToken(sdkCtx.http?.authInfo);
|
|
1153
|
-
const ctx = createContext(server, sdkCtx, token, sessionState, this._requestStateCodec, opts?.stateless);
|
|
1249
|
+
const ctx = createContext(server, sdkCtx, token, sessionState, this._requestStateCodec, opts?.stateless, this._sensitiveHeaders);
|
|
1154
1250
|
return contextStore.run(
|
|
1155
1251
|
ctx,
|
|
1156
1252
|
() => runMiddlewareChain(this._middleware, "resources/list", req.params, ctx, async () => {
|
|
@@ -1197,7 +1293,7 @@ var FastMCP = class {
|
|
|
1197
1293
|
});
|
|
1198
1294
|
server.setRequestHandler("resources/templates/list", async (req, sdkCtx) => {
|
|
1199
1295
|
const token = await this._resolveToken(sdkCtx.http?.authInfo);
|
|
1200
|
-
const ctx = createContext(server, sdkCtx, token, sessionState, this._requestStateCodec, opts?.stateless);
|
|
1296
|
+
const ctx = createContext(server, sdkCtx, token, sessionState, this._requestStateCodec, opts?.stateless, this._sensitiveHeaders);
|
|
1201
1297
|
return contextStore.run(
|
|
1202
1298
|
ctx,
|
|
1203
1299
|
() => runMiddlewareChain(this._middleware, "resources/templates/list", req.params, ctx, async () => {
|
|
@@ -1249,7 +1345,7 @@ var FastMCP = class {
|
|
|
1249
1345
|
const resource = resolved.resource;
|
|
1250
1346
|
const templateParams = resolved.templateParams;
|
|
1251
1347
|
if (resource.config.auth) await runAuthCheck(resource.config.auth, token);
|
|
1252
|
-
const ctx = createContext(server, sdkCtx, token, sessionState, this._requestStateCodec, opts?.stateless);
|
|
1348
|
+
const ctx = createContext(server, sdkCtx, token, sessionState, this._requestStateCodec, opts?.stateless, this._sensitiveHeaders);
|
|
1253
1349
|
return contextStore.run(
|
|
1254
1350
|
ctx,
|
|
1255
1351
|
() => runMiddlewareChain(this._middleware, "resources/read", req.params, ctx, async () => {
|
|
@@ -1268,7 +1364,8 @@ var FastMCP = class {
|
|
|
1268
1364
|
);
|
|
1269
1365
|
}
|
|
1270
1366
|
const result = await executePromise;
|
|
1271
|
-
|
|
1367
|
+
const converted = convertResourceResult(result, requestedUri, resource.config.mimeType, opts?.stateless);
|
|
1368
|
+
return attachResourceUiMeta(converted, resource.config.ui, isUiCapable(server.getClientCapabilities()));
|
|
1272
1369
|
})
|
|
1273
1370
|
);
|
|
1274
1371
|
});
|
|
@@ -1282,7 +1379,7 @@ var FastMCP = class {
|
|
|
1282
1379
|
}
|
|
1283
1380
|
const resource = resolved.resource;
|
|
1284
1381
|
if (resource.config.auth) await runAuthCheck(resource.config.auth, token);
|
|
1285
|
-
const ctx = createContext(server, sdkCtx, token, sessionState, this._requestStateCodec, opts?.stateless);
|
|
1382
|
+
const ctx = createContext(server, sdkCtx, token, sessionState, this._requestStateCodec, opts?.stateless, this._sensitiveHeaders);
|
|
1286
1383
|
return contextStore.run(
|
|
1287
1384
|
ctx,
|
|
1288
1385
|
() => runMiddlewareChain(this._middleware, "resources/subscribe", req.params, ctx, async () => {
|
|
@@ -1300,7 +1397,7 @@ var FastMCP = class {
|
|
|
1300
1397
|
if (opts?.stateless) throw new ProtocolError3(ProtocolErrorCode3.MethodNotFound, STATELESS_SUBSCRIBE_ERROR);
|
|
1301
1398
|
const uri = req.params.uri;
|
|
1302
1399
|
const token = await this._resolveToken(sdkCtx.http?.authInfo);
|
|
1303
|
-
const ctx = createContext(server, sdkCtx, token, sessionState, this._requestStateCodec, opts?.stateless);
|
|
1400
|
+
const ctx = createContext(server, sdkCtx, token, sessionState, this._requestStateCodec, opts?.stateless, this._sensitiveHeaders);
|
|
1304
1401
|
return contextStore.run(
|
|
1305
1402
|
ctx,
|
|
1306
1403
|
() => runMiddlewareChain(this._middleware, "resources/unsubscribe", req.params, ctx, async () => {
|
|
@@ -1312,7 +1409,7 @@ var FastMCP = class {
|
|
|
1312
1409
|
});
|
|
1313
1410
|
server.setRequestHandler("prompts/list", async (req, sdkCtx) => {
|
|
1314
1411
|
const token = await this._resolveToken(sdkCtx.http?.authInfo);
|
|
1315
|
-
const ctx = createContext(server, sdkCtx, token, sessionState, this._requestStateCodec, opts?.stateless);
|
|
1412
|
+
const ctx = createContext(server, sdkCtx, token, sessionState, this._requestStateCodec, opts?.stateless, this._sensitiveHeaders);
|
|
1316
1413
|
return contextStore.run(
|
|
1317
1414
|
ctx,
|
|
1318
1415
|
() => runMiddlewareChain(this._middleware, "prompts/list", req.params, ctx, async () => {
|
|
@@ -1392,7 +1489,7 @@ var FastMCP = class {
|
|
|
1392
1489
|
);
|
|
1393
1490
|
}
|
|
1394
1491
|
}
|
|
1395
|
-
const ctx = createContext(server, sdkCtx, token, sessionState, this._requestStateCodec, opts?.stateless);
|
|
1492
|
+
const ctx = createContext(server, sdkCtx, token, sessionState, this._requestStateCodec, opts?.stateless, this._sensitiveHeaders);
|
|
1396
1493
|
return contextStore.run(
|
|
1397
1494
|
ctx,
|
|
1398
1495
|
() => runMiddlewareChain(this._middleware, "prompts/get", req.params, ctx, async () => {
|
|
@@ -1416,7 +1513,7 @@ var FastMCP = class {
|
|
|
1416
1513
|
});
|
|
1417
1514
|
server.setRequestHandler("completion/complete", async (req, sdkCtx) => {
|
|
1418
1515
|
const token = await this._resolveToken(sdkCtx.http?.authInfo);
|
|
1419
|
-
const ctx = createContext(server, sdkCtx, token, sessionState, this._requestStateCodec, opts?.stateless);
|
|
1516
|
+
const ctx = createContext(server, sdkCtx, token, sessionState, this._requestStateCodec, opts?.stateless, this._sensitiveHeaders);
|
|
1420
1517
|
return contextStore.run(
|
|
1421
1518
|
ctx,
|
|
1422
1519
|
() => runMiddlewareChain(this._middleware, "completion/complete", req.params, ctx, async () => {
|
|
@@ -2237,14 +2334,26 @@ var FastMCP = class {
|
|
|
2237
2334
|
}
|
|
2238
2335
|
for (const [k, v] of Object.entries(corsHeaders)) res.setHeader(k, v);
|
|
2239
2336
|
if (auth) {
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2337
|
+
if (isRequestVerifier(auth)) {
|
|
2338
|
+
let accessToken;
|
|
2339
|
+
try {
|
|
2340
|
+
accessToken = await auth.verifyRequest(nodeRequestToHttpContext(req));
|
|
2341
|
+
} catch (err) {
|
|
2342
|
+
if (err instanceof AuthorizationError) {
|
|
2343
|
+
res.writeHead(403, { "Content-Type": "application/json" }).end(JSON.stringify({ error: err.message }));
|
|
2344
|
+
} else {
|
|
2345
|
+
res.writeHead(401, { "Content-Type": "application/json" }).end(JSON.stringify({ error: err instanceof Error ? err.message : "Authentication failed" }));
|
|
2346
|
+
}
|
|
2347
|
+
return;
|
|
2348
|
+
}
|
|
2349
|
+
if (!accessToken.token) {
|
|
2350
|
+
console.error(
|
|
2351
|
+
"[fastmcp] RequestVerifier.verifyRequest must set AccessToken.token to a stable, non-empty per-identity value. It keys response-cache partitioning and downstream identity."
|
|
2352
|
+
);
|
|
2353
|
+
res.writeHead(500, { "Content-Type": "application/json" }).end(JSON.stringify({ error: "verifyRequest returned an AccessToken with an empty token" }));
|
|
2354
|
+
return;
|
|
2355
|
+
}
|
|
2356
|
+
;
|
|
2248
2357
|
req.auth = {
|
|
2249
2358
|
token: accessToken.token,
|
|
2250
2359
|
clientId: accessToken.clientId ?? "",
|
|
@@ -2252,13 +2361,30 @@ var FastMCP = class {
|
|
|
2252
2361
|
expiresAt: accessToken.expiresAt,
|
|
2253
2362
|
extra: accessToken.claims
|
|
2254
2363
|
};
|
|
2255
|
-
}
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
res.writeHead(401, { "Content-Type": "application/json", "WWW-Authenticate": 'Bearer realm="mcp"' }).end(JSON.stringify({ error:
|
|
2364
|
+
} else {
|
|
2365
|
+
const authHeader = req.headers.authorization;
|
|
2366
|
+
const bearer = authHeader?.startsWith("Bearer ") ? authHeader.slice(7) : null;
|
|
2367
|
+
if (!bearer) {
|
|
2368
|
+
res.writeHead(401, { "Content-Type": "application/json", "WWW-Authenticate": 'Bearer realm="mcp"' }).end(JSON.stringify({ error: "Missing bearer token" }));
|
|
2369
|
+
return;
|
|
2370
|
+
}
|
|
2371
|
+
try {
|
|
2372
|
+
const accessToken = await auth.verify(bearer);
|
|
2373
|
+
req.auth = {
|
|
2374
|
+
token: accessToken.token,
|
|
2375
|
+
clientId: accessToken.clientId ?? "",
|
|
2376
|
+
scopes: accessToken.scopes,
|
|
2377
|
+
expiresAt: accessToken.expiresAt,
|
|
2378
|
+
extra: accessToken.claims
|
|
2379
|
+
};
|
|
2380
|
+
} catch (err) {
|
|
2381
|
+
if (err instanceof AuthorizationError) {
|
|
2382
|
+
res.writeHead(403, { "Content-Type": "application/json" }).end(JSON.stringify({ error: err.message }));
|
|
2383
|
+
} else {
|
|
2384
|
+
res.writeHead(401, { "Content-Type": "application/json", "WWW-Authenticate": 'Bearer realm="mcp"' }).end(JSON.stringify({ error: err instanceof Error ? err.message : "Authentication failed" }));
|
|
2385
|
+
}
|
|
2386
|
+
return;
|
|
2260
2387
|
}
|
|
2261
|
-
return;
|
|
2262
2388
|
}
|
|
2263
2389
|
}
|
|
2264
2390
|
await this._dispatchHttp(req, res);
|
|
@@ -2816,6 +2942,1472 @@ async function createProxy(config, name) {
|
|
|
2816
2942
|
return buildProxyFromClient(client, { cacheTtl: config.cacheTtl, name });
|
|
2817
2943
|
}
|
|
2818
2944
|
|
|
2945
|
+
// src/server/openapi/internal.ts
|
|
2946
|
+
function truthy(value) {
|
|
2947
|
+
if (value === null || value === void 0 || value === false) return false;
|
|
2948
|
+
if (typeof value === "number") return value !== 0;
|
|
2949
|
+
if (typeof value === "string") return value.length > 0;
|
|
2950
|
+
if (Array.isArray(value)) return value.length > 0;
|
|
2951
|
+
if (typeof value === "object") return Object.keys(value).length > 0;
|
|
2952
|
+
return true;
|
|
2953
|
+
}
|
|
2954
|
+
function isPlainObject(value) {
|
|
2955
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2956
|
+
}
|
|
2957
|
+
function deepEqual(a, b) {
|
|
2958
|
+
if (a === b) return true;
|
|
2959
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
2960
|
+
return a.length === b.length && a.every((item, i) => deepEqual(item, b[i]));
|
|
2961
|
+
}
|
|
2962
|
+
if (isPlainObject(a) && isPlainObject(b)) {
|
|
2963
|
+
const aKeys = Object.keys(a);
|
|
2964
|
+
const bKeys = Object.keys(b);
|
|
2965
|
+
return aKeys.length === bKeys.length && aKeys.every((k) => Object.prototype.hasOwnProperty.call(b, k) && deepEqual(a[k], b[k]));
|
|
2966
|
+
}
|
|
2967
|
+
return false;
|
|
2968
|
+
}
|
|
2969
|
+
function warn(message) {
|
|
2970
|
+
console.warn(`[fastmcp] ${message}`);
|
|
2971
|
+
}
|
|
2972
|
+
|
|
2973
|
+
// src/server/openapi/parser.ts
|
|
2974
|
+
import { parse as parseYaml } from "yaml";
|
|
2975
|
+
|
|
2976
|
+
// src/server/openapi/jsonSchema.ts
|
|
2977
|
+
var OPENAPI_SPECIFIC_FIELDS = [
|
|
2978
|
+
"nullable",
|
|
2979
|
+
"discriminator",
|
|
2980
|
+
"readOnly",
|
|
2981
|
+
"writeOnly",
|
|
2982
|
+
"xml",
|
|
2983
|
+
"externalDocs",
|
|
2984
|
+
"deprecated"
|
|
2985
|
+
];
|
|
2986
|
+
var RECURSIVE_MAP_FIELDS = ["properties", "$defs", "$definitions"];
|
|
2987
|
+
var RECURSIVE_SINGLE_FIELDS = ["items", "additionalProperties", "not"];
|
|
2988
|
+
var RECURSIVE_LIST_FIELDS = ["allOf", "anyOf", "oneOf"];
|
|
2989
|
+
function convertOpenAPISchemaToJsonSchema(schema, openapiVersion) {
|
|
2990
|
+
if (!isPlainObject(schema)) return schema;
|
|
2991
|
+
let result = { ...schema };
|
|
2992
|
+
if (openapiVersion?.startsWith("3.0")) {
|
|
2993
|
+
result = convertNullableField(result);
|
|
2994
|
+
}
|
|
2995
|
+
if ("oneOf" in result) {
|
|
2996
|
+
result.anyOf = result.oneOf;
|
|
2997
|
+
delete result.oneOf;
|
|
2998
|
+
}
|
|
2999
|
+
result = requireDiscriminatorProperty(result);
|
|
3000
|
+
for (const field of OPENAPI_SPECIFIC_FIELDS) {
|
|
3001
|
+
delete result[field];
|
|
3002
|
+
}
|
|
3003
|
+
for (const field of RECURSIVE_MAP_FIELDS) {
|
|
3004
|
+
const map = result[field];
|
|
3005
|
+
if (isPlainObject(map)) {
|
|
3006
|
+
result[field] = Object.fromEntries(
|
|
3007
|
+
Object.entries(map).map(([name, sub]) => [
|
|
3008
|
+
name,
|
|
3009
|
+
isPlainObject(sub) ? convertOpenAPISchemaToJsonSchema(sub, openapiVersion) : sub
|
|
3010
|
+
])
|
|
3011
|
+
);
|
|
3012
|
+
}
|
|
3013
|
+
}
|
|
3014
|
+
for (const field of RECURSIVE_SINGLE_FIELDS) {
|
|
3015
|
+
const sub = result[field];
|
|
3016
|
+
if (isPlainObject(sub)) {
|
|
3017
|
+
result[field] = convertOpenAPISchemaToJsonSchema(sub, openapiVersion);
|
|
3018
|
+
}
|
|
3019
|
+
}
|
|
3020
|
+
for (const field of RECURSIVE_LIST_FIELDS) {
|
|
3021
|
+
const list = result[field];
|
|
3022
|
+
if (Array.isArray(list)) {
|
|
3023
|
+
result[field] = list.map(
|
|
3024
|
+
(item) => isPlainObject(item) ? convertOpenAPISchemaToJsonSchema(item, openapiVersion) : item
|
|
3025
|
+
);
|
|
3026
|
+
}
|
|
3027
|
+
}
|
|
3028
|
+
return result;
|
|
3029
|
+
}
|
|
3030
|
+
function convertNullableField(schema) {
|
|
3031
|
+
if (!("nullable" in schema)) return schema;
|
|
3032
|
+
const result = { ...schema };
|
|
3033
|
+
const nullableValue = result.nullable;
|
|
3034
|
+
delete result.nullable;
|
|
3035
|
+
if (!nullableValue) return result;
|
|
3036
|
+
const currentType = result.type;
|
|
3037
|
+
if ("type" in result) {
|
|
3038
|
+
if (typeof currentType === "string") {
|
|
3039
|
+
result.type = [currentType, "null"];
|
|
3040
|
+
} else if (Array.isArray(currentType) && !currentType.includes("null")) {
|
|
3041
|
+
result.type = [...currentType, "null"];
|
|
3042
|
+
}
|
|
3043
|
+
} else if ("oneOf" in result) {
|
|
3044
|
+
result.anyOf = [...result.oneOf, { type: "null" }];
|
|
3045
|
+
delete result.oneOf;
|
|
3046
|
+
} else if ("anyOf" in result) {
|
|
3047
|
+
const anyOf = result.anyOf;
|
|
3048
|
+
if (!anyOf.some((item) => isPlainObject(item) && item.type === "null")) {
|
|
3049
|
+
result.anyOf = [...anyOf, { type: "null" }];
|
|
3050
|
+
}
|
|
3051
|
+
} else if ("allOf" in result) {
|
|
3052
|
+
result.anyOf = [{ allOf: result.allOf }, { type: "null" }];
|
|
3053
|
+
delete result.allOf;
|
|
3054
|
+
}
|
|
3055
|
+
if (Array.isArray(result.enum) && !result.enum.includes(null)) {
|
|
3056
|
+
result.enum = [...result.enum, null];
|
|
3057
|
+
}
|
|
3058
|
+
return result;
|
|
3059
|
+
}
|
|
3060
|
+
function requireProperty(schema, propertyName) {
|
|
3061
|
+
const required = schema.required;
|
|
3062
|
+
if (required === void 0 || required === null) {
|
|
3063
|
+
return { ...schema, required: [propertyName] };
|
|
3064
|
+
}
|
|
3065
|
+
if (Array.isArray(required) && !required.includes(propertyName)) {
|
|
3066
|
+
return { ...schema, required: [...required, propertyName] };
|
|
3067
|
+
}
|
|
3068
|
+
return schema;
|
|
3069
|
+
}
|
|
3070
|
+
function requireDiscriminatorProperty(schema) {
|
|
3071
|
+
const discriminator = schema.discriminator;
|
|
3072
|
+
if (!isPlainObject(discriminator)) return schema;
|
|
3073
|
+
const propertyName = discriminator.propertyName;
|
|
3074
|
+
if (typeof propertyName !== "string") return schema;
|
|
3075
|
+
const result = { ...schema };
|
|
3076
|
+
for (const key of ["anyOf", "oneOf"]) {
|
|
3077
|
+
const variants = result[key];
|
|
3078
|
+
if (!Array.isArray(variants)) continue;
|
|
3079
|
+
result[key] = variants.map(
|
|
3080
|
+
(variant) => isPlainObject(variant) ? requireProperty(variant, propertyName) : variant
|
|
3081
|
+
);
|
|
3082
|
+
}
|
|
3083
|
+
return result;
|
|
3084
|
+
}
|
|
3085
|
+
|
|
3086
|
+
// src/server/openapi/schemas.ts
|
|
3087
|
+
var EXTERNAL_REF_MESSAGE = "External or non-local reference not supported";
|
|
3088
|
+
function isExternalRefError(error) {
|
|
3089
|
+
return error instanceof Error && error.message.includes(EXTERNAL_REF_MESSAGE);
|
|
3090
|
+
}
|
|
3091
|
+
function replaceRefWithDefs(info, description) {
|
|
3092
|
+
const schema = { ...info };
|
|
3093
|
+
const refPath = schema.$ref;
|
|
3094
|
+
if (truthy(refPath)) {
|
|
3095
|
+
if (typeof refPath === "string") {
|
|
3096
|
+
if (refPath.startsWith("#/components/schemas/")) {
|
|
3097
|
+
schema.$ref = `#/$defs/${refPath.split("/").pop()}`;
|
|
3098
|
+
} else if (!refPath.startsWith("#/")) {
|
|
3099
|
+
throw new Error(
|
|
3100
|
+
`${EXTERNAL_REF_MESSAGE}: ${refPath}. FastMCP only supports local schema references starting with '#/'. Please include all schema definitions within the OpenAPI document.`
|
|
3101
|
+
);
|
|
3102
|
+
}
|
|
3103
|
+
}
|
|
3104
|
+
} else if (truthy(schema.properties)) {
|
|
3105
|
+
const properties = schema.properties;
|
|
3106
|
+
if ("$ref" in properties) {
|
|
3107
|
+
schema.properties = replaceRefWithDefs(properties);
|
|
3108
|
+
} else {
|
|
3109
|
+
schema.properties = Object.fromEntries(
|
|
3110
|
+
Object.entries(properties).map(([name, prop]) => [
|
|
3111
|
+
name,
|
|
3112
|
+
isPlainObject(prop) ? replaceRefWithDefs(prop) : prop
|
|
3113
|
+
])
|
|
3114
|
+
);
|
|
3115
|
+
}
|
|
3116
|
+
} else if (truthy(schema.items)) {
|
|
3117
|
+
if (isPlainObject(schema.items)) {
|
|
3118
|
+
schema.items = replaceRefWithDefs(schema.items);
|
|
3119
|
+
}
|
|
3120
|
+
}
|
|
3121
|
+
for (const section of ["anyOf", "allOf", "oneOf"]) {
|
|
3122
|
+
const list = schema[section];
|
|
3123
|
+
if (Array.isArray(list)) {
|
|
3124
|
+
schema[section] = list.map((item) => isPlainObject(item) ? replaceRefWithDefs(item) : item);
|
|
3125
|
+
}
|
|
3126
|
+
}
|
|
3127
|
+
const additionalProperties = schema.additionalProperties;
|
|
3128
|
+
if (truthy(additionalProperties) && isPlainObject(additionalProperties)) {
|
|
3129
|
+
schema.additionalProperties = replaceRefWithDefs(additionalProperties);
|
|
3130
|
+
}
|
|
3131
|
+
const propertyNames = schema.propertyNames;
|
|
3132
|
+
if (truthy(propertyNames) && isPlainObject(propertyNames)) {
|
|
3133
|
+
schema.propertyNames = replaceRefWithDefs(propertyNames);
|
|
3134
|
+
}
|
|
3135
|
+
const patternProperties = schema.patternProperties;
|
|
3136
|
+
if (truthy(patternProperties) && isPlainObject(patternProperties)) {
|
|
3137
|
+
schema.patternProperties = Object.fromEntries(
|
|
3138
|
+
Object.entries(patternProperties).map(([pattern, sub]) => [
|
|
3139
|
+
pattern,
|
|
3140
|
+
isPlainObject(sub) ? replaceRefWithDefs(sub) : sub
|
|
3141
|
+
])
|
|
3142
|
+
);
|
|
3143
|
+
}
|
|
3144
|
+
if (info.description === void 0 && truthy(description) && !truthy(schema.description)) {
|
|
3145
|
+
schema.description = description;
|
|
3146
|
+
}
|
|
3147
|
+
return schema;
|
|
3148
|
+
}
|
|
3149
|
+
function allofMembers(schema, schemaDefs, resolving = /* @__PURE__ */ new Set()) {
|
|
3150
|
+
const ref = schema.$ref;
|
|
3151
|
+
if (typeof ref === "string") {
|
|
3152
|
+
for (const prefix of ["#/$defs/", "#/components/schemas/"]) {
|
|
3153
|
+
if (ref.startsWith(prefix)) {
|
|
3154
|
+
const name = ref.slice(prefix.length);
|
|
3155
|
+
const referenced = schemaDefs[name];
|
|
3156
|
+
if (isPlainObject(referenced) && !resolving.has(name)) {
|
|
3157
|
+
const siblings = Object.fromEntries(
|
|
3158
|
+
Object.entries(schema).filter(([key]) => key !== "$ref")
|
|
3159
|
+
);
|
|
3160
|
+
const members = allofMembers(referenced, schemaDefs, /* @__PURE__ */ new Set([...resolving, name]));
|
|
3161
|
+
return truthy(siblings) ? [...members, siblings] : members;
|
|
3162
|
+
}
|
|
3163
|
+
break;
|
|
3164
|
+
}
|
|
3165
|
+
}
|
|
3166
|
+
}
|
|
3167
|
+
const allOf = schema.allOf;
|
|
3168
|
+
if (Array.isArray(allOf)) {
|
|
3169
|
+
const members = [];
|
|
3170
|
+
for (const member of allOf) {
|
|
3171
|
+
if (isPlainObject(member)) {
|
|
3172
|
+
members.push(...allofMembers(member, schemaDefs, resolving));
|
|
3173
|
+
}
|
|
3174
|
+
}
|
|
3175
|
+
const siblings = Object.fromEntries(Object.entries(schema).filter(([key]) => key !== "allOf"));
|
|
3176
|
+
return truthy(siblings) ? [...members, siblings] : members;
|
|
3177
|
+
}
|
|
3178
|
+
return [schema];
|
|
3179
|
+
}
|
|
3180
|
+
function discriminatorTargetName(target) {
|
|
3181
|
+
for (const prefix of ["#/$defs/", "#/components/schemas/"]) {
|
|
3182
|
+
if (target.startsWith(prefix)) {
|
|
3183
|
+
return target.slice(prefix.length) || null;
|
|
3184
|
+
}
|
|
3185
|
+
}
|
|
3186
|
+
if (target.startsWith("#") || target.includes("/")) return null;
|
|
3187
|
+
return target || null;
|
|
3188
|
+
}
|
|
3189
|
+
function flattenDiscriminatorSubtypes(schema, schemaDefs) {
|
|
3190
|
+
const discriminator = schema.discriminator;
|
|
3191
|
+
if (!isPlainObject(discriminator)) return null;
|
|
3192
|
+
const propertyName = discriminator.propertyName;
|
|
3193
|
+
const mapping = discriminator.mapping;
|
|
3194
|
+
if (typeof propertyName !== "string" || !isPlainObject(mapping)) return null;
|
|
3195
|
+
const ownProps = isPlainObject(schema.properties) ? schema.properties : {};
|
|
3196
|
+
const alternatives = /* @__PURE__ */ new Map();
|
|
3197
|
+
const values = [];
|
|
3198
|
+
const variants = [];
|
|
3199
|
+
for (const [value, target] of Object.entries(mapping)) {
|
|
3200
|
+
if (typeof target !== "string") continue;
|
|
3201
|
+
const name = discriminatorTargetName(target);
|
|
3202
|
+
const subtype = name !== null ? schemaDefs[name] : void 0;
|
|
3203
|
+
if (!isPlainObject(subtype)) continue;
|
|
3204
|
+
const variantFields = [];
|
|
3205
|
+
for (const member of allofMembers(subtype, schemaDefs)) {
|
|
3206
|
+
const memberProps = isPlainObject(member.properties) ? member.properties : {};
|
|
3207
|
+
for (const [propName, propSchema] of Object.entries(memberProps)) {
|
|
3208
|
+
if (propName in ownProps) continue;
|
|
3209
|
+
if (!variantFields.includes(propName)) variantFields.push(propName);
|
|
3210
|
+
const seen = alternatives.get(propName) ?? [];
|
|
3211
|
+
if (!seen.some((existing2) => deepEqual(existing2, propSchema))) seen.push(propSchema);
|
|
3212
|
+
alternatives.set(propName, seen);
|
|
3213
|
+
}
|
|
3214
|
+
}
|
|
3215
|
+
values.push(`'${value}'`);
|
|
3216
|
+
if (variantFields.length > 0) {
|
|
3217
|
+
variants.push(`'${value}' uses ${variantFields.join(", ")}`);
|
|
3218
|
+
}
|
|
3219
|
+
}
|
|
3220
|
+
if (values.length === 0) return null;
|
|
3221
|
+
const subtypeProps = Object.fromEntries(
|
|
3222
|
+
[...alternatives.entries()].map(([propName, schemas]) => [
|
|
3223
|
+
propName,
|
|
3224
|
+
schemas.length === 1 ? schemas[0] : { anyOf: schemas }
|
|
3225
|
+
])
|
|
3226
|
+
);
|
|
3227
|
+
const properties = { ...ownProps, ...subtypeProps };
|
|
3228
|
+
let note = `Selects the variant. Accepted values: ${values.join(", ")}.`;
|
|
3229
|
+
if (variants.length > 0) {
|
|
3230
|
+
note += ` ${variants.join("; ")}. Send only the fields belonging to the selected variant.`;
|
|
3231
|
+
}
|
|
3232
|
+
const existing = properties[propertyName];
|
|
3233
|
+
const tagSchema = isPlainObject(existing) ? existing : { type: "string" };
|
|
3234
|
+
const existingDesc = tagSchema.description;
|
|
3235
|
+
properties[propertyName] = {
|
|
3236
|
+
...tagSchema,
|
|
3237
|
+
description: truthy(existingDesc) ? `${existingDesc} ${note}` : note
|
|
3238
|
+
};
|
|
3239
|
+
return properties;
|
|
3240
|
+
}
|
|
3241
|
+
function capitalize(text) {
|
|
3242
|
+
return text.charAt(0).toUpperCase() + text.slice(1);
|
|
3243
|
+
}
|
|
3244
|
+
function combineSchemasAndMapParams(route) {
|
|
3245
|
+
const properties = {};
|
|
3246
|
+
const required = [];
|
|
3247
|
+
const parameterMap = {};
|
|
3248
|
+
const paramNamesByLocation = {
|
|
3249
|
+
path: /* @__PURE__ */ new Set(),
|
|
3250
|
+
query: /* @__PURE__ */ new Set(),
|
|
3251
|
+
header: /* @__PURE__ */ new Set(),
|
|
3252
|
+
cookie: /* @__PURE__ */ new Set()
|
|
3253
|
+
};
|
|
3254
|
+
let bodySchema = {};
|
|
3255
|
+
let bodyProps = {};
|
|
3256
|
+
for (const param of route.parameters) {
|
|
3257
|
+
paramNamesByLocation[param.location].add(param.name);
|
|
3258
|
+
}
|
|
3259
|
+
const requestBody = route.requestBody;
|
|
3260
|
+
if (requestBody && truthy(requestBody.contentSchema)) {
|
|
3261
|
+
const contentType = Object.keys(requestBody.contentSchema)[0];
|
|
3262
|
+
bodySchema = requestBody.contentSchema[contentType];
|
|
3263
|
+
if (truthy(requestBody.description) && !truthy(bodySchema.description)) {
|
|
3264
|
+
bodySchema.description = requestBody.description;
|
|
3265
|
+
}
|
|
3266
|
+
if ("allOf" in bodySchema && Array.isArray(bodySchema.allOf)) {
|
|
3267
|
+
const mergedProps = {};
|
|
3268
|
+
const mergedRequired = [];
|
|
3269
|
+
for (const subSchema of allofMembers(bodySchema, route.requestSchemas)) {
|
|
3270
|
+
if ("properties" in subSchema && isPlainObject(subSchema.properties)) {
|
|
3271
|
+
Object.assign(mergedProps, subSchema.properties);
|
|
3272
|
+
}
|
|
3273
|
+
if ("required" in subSchema && Array.isArray(subSchema.required)) {
|
|
3274
|
+
mergedRequired.push(...subSchema.required);
|
|
3275
|
+
}
|
|
3276
|
+
}
|
|
3277
|
+
bodySchema.properties = mergedProps;
|
|
3278
|
+
if (mergedRequired.length > 0) {
|
|
3279
|
+
bodySchema.required = [...new Set(mergedRequired)];
|
|
3280
|
+
}
|
|
3281
|
+
delete bodySchema.allOf;
|
|
3282
|
+
}
|
|
3283
|
+
const flattenedProps = flattenDiscriminatorSubtypes(bodySchema, route.requestSchemas);
|
|
3284
|
+
if (flattenedProps !== null) {
|
|
3285
|
+
bodySchema.properties = flattenedProps;
|
|
3286
|
+
delete bodySchema.discriminator;
|
|
3287
|
+
}
|
|
3288
|
+
bodyProps = isPlainObject(bodySchema.properties) ? bodySchema.properties : {};
|
|
3289
|
+
}
|
|
3290
|
+
const allNonBodyParams = /* @__PURE__ */ new Set();
|
|
3291
|
+
for (const names of Object.values(paramNamesByLocation)) {
|
|
3292
|
+
for (const name of names) allNonBodyParams.add(name);
|
|
3293
|
+
}
|
|
3294
|
+
const bodyParamNames = new Set(Object.keys(bodyProps));
|
|
3295
|
+
const paramCounts = /* @__PURE__ */ new Map();
|
|
3296
|
+
for (const param of route.parameters) {
|
|
3297
|
+
paramCounts.set(param.name, (paramCounts.get(param.name) ?? 0) + 1);
|
|
3298
|
+
}
|
|
3299
|
+
const collidingParams = /* @__PURE__ */ new Set();
|
|
3300
|
+
for (const name of allNonBodyParams) {
|
|
3301
|
+
if (bodyParamNames.has(name)) collidingParams.add(name);
|
|
3302
|
+
}
|
|
3303
|
+
for (const [name, count] of paramCounts) {
|
|
3304
|
+
if (count > 1) collidingParams.add(name);
|
|
3305
|
+
}
|
|
3306
|
+
for (const param of route.parameters) {
|
|
3307
|
+
const paramSchema = { ...param.schema };
|
|
3308
|
+
if (truthy(param.description) && !truthy(paramSchema.description)) {
|
|
3309
|
+
paramSchema.description = param.description;
|
|
3310
|
+
}
|
|
3311
|
+
if (collidingParams.has(param.name)) {
|
|
3312
|
+
const suffixedName = `${param.name}__${param.location}`;
|
|
3313
|
+
if (param.required) required.push(suffixedName);
|
|
3314
|
+
parameterMap[suffixedName] = { location: param.location, openapiName: param.name };
|
|
3315
|
+
const originalDesc = truthy(paramSchema.description) ? paramSchema.description : "";
|
|
3316
|
+
const locationDesc = `(${capitalize(param.location)} parameter)`;
|
|
3317
|
+
paramSchema.description = originalDesc ? `${originalDesc} ${locationDesc}` : locationDesc;
|
|
3318
|
+
properties[suffixedName] = paramSchema;
|
|
3319
|
+
} else {
|
|
3320
|
+
if (param.required) required.push(param.name);
|
|
3321
|
+
parameterMap[param.name] = { location: param.location, openapiName: param.name };
|
|
3322
|
+
properties[param.name] = paramSchema;
|
|
3323
|
+
}
|
|
3324
|
+
}
|
|
3325
|
+
if (requestBody && truthy(requestBody.contentSchema)) {
|
|
3326
|
+
if ("$ref" in bodySchema && !truthy(bodyProps)) {
|
|
3327
|
+
properties.body = bodySchema;
|
|
3328
|
+
if (requestBody.required) required.push("body");
|
|
3329
|
+
parameterMap.body = { location: "body", openapiName: "body" };
|
|
3330
|
+
} else if (truthy(bodyProps)) {
|
|
3331
|
+
for (const [propName, propSchema] of Object.entries(bodyProps)) {
|
|
3332
|
+
properties[propName] = propSchema;
|
|
3333
|
+
parameterMap[propName] = { location: "body", openapiName: propName };
|
|
3334
|
+
}
|
|
3335
|
+
if (requestBody.required) {
|
|
3336
|
+
required.push(...bodySchema.required ?? []);
|
|
3337
|
+
}
|
|
3338
|
+
} else {
|
|
3339
|
+
let paramName = String(
|
|
3340
|
+
typeof bodySchema.title === "string" ? bodySchema.title : "body"
|
|
3341
|
+
).toLowerCase();
|
|
3342
|
+
paramName = paramName.replace(/[^a-zA-Z0-9_]/g, "_");
|
|
3343
|
+
if (!paramName || /^\d/.test(paramName)) paramName = "body_data";
|
|
3344
|
+
properties[paramName] = bodySchema;
|
|
3345
|
+
if (requestBody.required) required.push(paramName);
|
|
3346
|
+
parameterMap[paramName] = { location: "body", openapiName: paramName };
|
|
3347
|
+
}
|
|
3348
|
+
}
|
|
3349
|
+
let result = { type: "object", properties, required };
|
|
3350
|
+
if (truthy(route.requestSchemas)) {
|
|
3351
|
+
result.$defs = route.requestSchemas;
|
|
3352
|
+
}
|
|
3353
|
+
if (route.openapiVersion?.startsWith("3")) {
|
|
3354
|
+
result = convertOpenAPISchemaToJsonSchema(result, route.openapiVersion);
|
|
3355
|
+
}
|
|
3356
|
+
return [result, parameterMap];
|
|
3357
|
+
}
|
|
3358
|
+
var SUCCESS_CODE_PRIORITY = ["200", "201", "202", "204"];
|
|
3359
|
+
var JSON_COMPATIBLE_TYPES = [
|
|
3360
|
+
"application/json",
|
|
3361
|
+
"application/vnd.api+json",
|
|
3362
|
+
"application/hal+json",
|
|
3363
|
+
"application/ld+json",
|
|
3364
|
+
"text/json"
|
|
3365
|
+
];
|
|
3366
|
+
function extractOutputSchemaFromResponses(responses, schemaDefinitions, openapiVersion) {
|
|
3367
|
+
if (!truthy(responses)) return null;
|
|
3368
|
+
let responseInfo;
|
|
3369
|
+
for (const statusCode of SUCCESS_CODE_PRIORITY) {
|
|
3370
|
+
if (statusCode in responses) {
|
|
3371
|
+
responseInfo = responses[statusCode];
|
|
3372
|
+
break;
|
|
3373
|
+
}
|
|
3374
|
+
}
|
|
3375
|
+
if (responseInfo === void 0) {
|
|
3376
|
+
for (const [statusCode, respInfo] of Object.entries(responses)) {
|
|
3377
|
+
if (statusCode.startsWith("2")) {
|
|
3378
|
+
responseInfo = respInfo;
|
|
3379
|
+
break;
|
|
3380
|
+
}
|
|
3381
|
+
}
|
|
3382
|
+
}
|
|
3383
|
+
if (responseInfo === void 0 || !truthy(responseInfo.contentSchema)) return null;
|
|
3384
|
+
let schema;
|
|
3385
|
+
for (const contentType of JSON_COMPATIBLE_TYPES) {
|
|
3386
|
+
if (contentType in responseInfo.contentSchema) {
|
|
3387
|
+
schema = responseInfo.contentSchema[contentType];
|
|
3388
|
+
break;
|
|
3389
|
+
}
|
|
3390
|
+
}
|
|
3391
|
+
if (schema === void 0) {
|
|
3392
|
+
const firstContentType = Object.keys(responseInfo.contentSchema)[0];
|
|
3393
|
+
schema = responseInfo.contentSchema[firstContentType];
|
|
3394
|
+
}
|
|
3395
|
+
if (!truthy(schema) || !isPlainObject(schema)) return null;
|
|
3396
|
+
let outputSchema = replaceRefWithDefs(schema);
|
|
3397
|
+
if (typeof outputSchema.$ref === "string" && schemaDefinitions && truthy(schemaDefinitions)) {
|
|
3398
|
+
const refPath = outputSchema.$ref;
|
|
3399
|
+
if (refPath.startsWith("#/$defs/")) {
|
|
3400
|
+
const schemaName = refPath.split("/").pop();
|
|
3401
|
+
if (schemaName in schemaDefinitions) {
|
|
3402
|
+
outputSchema = replaceRefWithDefs(schemaDefinitions[schemaName]);
|
|
3403
|
+
}
|
|
3404
|
+
}
|
|
3405
|
+
}
|
|
3406
|
+
if (openapiVersion?.startsWith("3")) {
|
|
3407
|
+
outputSchema = convertOpenAPISchemaToJsonSchema(outputSchema, openapiVersion);
|
|
3408
|
+
}
|
|
3409
|
+
if (outputSchema.type !== "object") {
|
|
3410
|
+
outputSchema = {
|
|
3411
|
+
type: "object",
|
|
3412
|
+
properties: { result: outputSchema },
|
|
3413
|
+
required: ["result"],
|
|
3414
|
+
"x-fastmcp-wrap-result": true
|
|
3415
|
+
};
|
|
3416
|
+
}
|
|
3417
|
+
if (schemaDefinitions && truthy(schemaDefinitions)) {
|
|
3418
|
+
const processedDefs = { ...schemaDefinitions };
|
|
3419
|
+
for (const [name, defSchema] of Object.entries(processedDefs)) {
|
|
3420
|
+
if (isPlainObject(defSchema)) {
|
|
3421
|
+
processedDefs[name] = replaceRefWithDefs(defSchema);
|
|
3422
|
+
}
|
|
3423
|
+
}
|
|
3424
|
+
if (openapiVersion?.startsWith("3")) {
|
|
3425
|
+
for (const name of Object.keys(processedDefs)) {
|
|
3426
|
+
processedDefs[name] = convertOpenAPISchemaToJsonSchema(processedDefs[name], openapiVersion);
|
|
3427
|
+
}
|
|
3428
|
+
}
|
|
3429
|
+
outputSchema.$defs = processedDefs;
|
|
3430
|
+
}
|
|
3431
|
+
return outputSchema;
|
|
3432
|
+
}
|
|
3433
|
+
|
|
3434
|
+
// src/server/openapi/parser.ts
|
|
3435
|
+
function loadSpec(spec) {
|
|
3436
|
+
if (typeof spec !== "string") return spec;
|
|
3437
|
+
const parsed = parseYaml(spec);
|
|
3438
|
+
if (!isPlainObject(parsed)) {
|
|
3439
|
+
throw new Error("OpenAPI spec text must parse to an object");
|
|
3440
|
+
}
|
|
3441
|
+
return parsed;
|
|
3442
|
+
}
|
|
3443
|
+
var HTTP_METHODS_LOWER = [
|
|
3444
|
+
"get",
|
|
3445
|
+
"put",
|
|
3446
|
+
"post",
|
|
3447
|
+
"delete",
|
|
3448
|
+
"options",
|
|
3449
|
+
"head",
|
|
3450
|
+
"patch",
|
|
3451
|
+
"trace"
|
|
3452
|
+
];
|
|
3453
|
+
var PRIMARY_SUCCESS_PRIORITY = ["200", "201", "202", "204", "207"];
|
|
3454
|
+
var PARAMETER_LOCATIONS = ["path", "query", "header", "cookie"];
|
|
3455
|
+
function isSuccessStatusCode(statusCode) {
|
|
3456
|
+
if (/^\s*[+-]?\d+\s*$/.test(statusCode)) {
|
|
3457
|
+
const code = parseInt(statusCode, 10);
|
|
3458
|
+
return code >= 200 && code < 300;
|
|
3459
|
+
}
|
|
3460
|
+
return ["default", "2xx"].includes(statusCode.toLowerCase());
|
|
3461
|
+
}
|
|
3462
|
+
function parseOpenAPIToHttpRoutes(spec) {
|
|
3463
|
+
const openapiVersion = typeof spec.openapi === "string" ? spec.openapi : "";
|
|
3464
|
+
return new SpecParser(spec, openapiVersion).parse();
|
|
3465
|
+
}
|
|
3466
|
+
var SpecParser = class {
|
|
3467
|
+
constructor(spec, openapiVersion) {
|
|
3468
|
+
this.spec = spec;
|
|
3469
|
+
this.openapiVersion = openapiVersion;
|
|
3470
|
+
}
|
|
3471
|
+
spec;
|
|
3472
|
+
openapiVersion;
|
|
3473
|
+
toParameterLocation(paramIn) {
|
|
3474
|
+
if (PARAMETER_LOCATIONS.includes(paramIn)) {
|
|
3475
|
+
return paramIn;
|
|
3476
|
+
}
|
|
3477
|
+
warn(`Unknown parameter location: ${paramIn}, defaulting to 'query'`);
|
|
3478
|
+
return "query";
|
|
3479
|
+
}
|
|
3480
|
+
/** Resolve a `{$ref}` object to its target within the spec (local only). */
|
|
3481
|
+
resolveRef(item) {
|
|
3482
|
+
if (!isPlainObject(item) || typeof item.$ref !== "string") return item;
|
|
3483
|
+
const ref = item.$ref;
|
|
3484
|
+
try {
|
|
3485
|
+
if (!ref.startsWith("#/")) {
|
|
3486
|
+
throw new Error(`External or non-local reference not supported: ${ref}`);
|
|
3487
|
+
}
|
|
3488
|
+
const parts = ref.replace(/^[#/]+/, "").replace(/[#/]+$/, "").split("/");
|
|
3489
|
+
let target = this.spec;
|
|
3490
|
+
for (const part of parts) {
|
|
3491
|
+
if (/^\d+$/.test(part) && Array.isArray(target)) {
|
|
3492
|
+
target = target[Number(part)];
|
|
3493
|
+
} else if (isPlainObject(target)) {
|
|
3494
|
+
target = target[part];
|
|
3495
|
+
} else {
|
|
3496
|
+
throw new Error(`Cannot traverse part '${part}' in reference '${ref}'`);
|
|
3497
|
+
}
|
|
3498
|
+
if (target === null || target === void 0) {
|
|
3499
|
+
throw new Error(`Reference part '${part}' not found in path '${ref}'`);
|
|
3500
|
+
}
|
|
3501
|
+
}
|
|
3502
|
+
if (isPlainObject(target) && typeof target.$ref === "string") {
|
|
3503
|
+
return this.resolveRef(target);
|
|
3504
|
+
}
|
|
3505
|
+
return target;
|
|
3506
|
+
} catch (error) {
|
|
3507
|
+
throw new Error(`Failed to resolve reference '${ref}': ${error.message}`);
|
|
3508
|
+
}
|
|
3509
|
+
}
|
|
3510
|
+
/** Resolve a schema (or ref to one) and return it as a ref-converted copy. */
|
|
3511
|
+
extractSchemaAsDict(schemaObj) {
|
|
3512
|
+
try {
|
|
3513
|
+
const resolved = this.resolveRef(schemaObj);
|
|
3514
|
+
let result;
|
|
3515
|
+
if (isPlainObject(resolved)) {
|
|
3516
|
+
result = structuredClone(resolved);
|
|
3517
|
+
} else {
|
|
3518
|
+
warn(`Expected a schema object after resolving, got ${typeof resolved}. Using empty schema.`);
|
|
3519
|
+
result = {};
|
|
3520
|
+
}
|
|
3521
|
+
return replaceRefWithDefs(result);
|
|
3522
|
+
} catch (error) {
|
|
3523
|
+
if (isExternalRefError(error)) throw error;
|
|
3524
|
+
warn(`Failed to extract schema: ${error.message}`);
|
|
3525
|
+
return {};
|
|
3526
|
+
}
|
|
3527
|
+
}
|
|
3528
|
+
extractParameters(operationParams, pathItemParams) {
|
|
3529
|
+
const extracted = [];
|
|
3530
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3531
|
+
const all = [
|
|
3532
|
+
...Array.isArray(operationParams) ? operationParams : [],
|
|
3533
|
+
...Array.isArray(pathItemParams) ? pathItemParams : []
|
|
3534
|
+
];
|
|
3535
|
+
for (const paramOrRef of all) {
|
|
3536
|
+
try {
|
|
3537
|
+
const parameter = this.resolveRef(paramOrRef);
|
|
3538
|
+
if (!isPlainObject(parameter) || typeof parameter.name !== "string" || typeof parameter.in !== "string") {
|
|
3539
|
+
warn(`Expected a parameter object after resolving. Skipping.`);
|
|
3540
|
+
continue;
|
|
3541
|
+
}
|
|
3542
|
+
const location = this.toParameterLocation(parameter.in);
|
|
3543
|
+
const key = `${parameter.name}\0${parameter.in}`;
|
|
3544
|
+
if (seen.has(key)) continue;
|
|
3545
|
+
seen.add(key);
|
|
3546
|
+
let paramSchema = {};
|
|
3547
|
+
if (truthy(parameter.schema)) {
|
|
3548
|
+
paramSchema = this.extractSchemaAsDict(parameter.schema);
|
|
3549
|
+
} else if (isPlainObject(parameter.content) && truthy(parameter.content)) {
|
|
3550
|
+
const firstMediaType = Object.values(parameter.content)[0];
|
|
3551
|
+
if (isPlainObject(firstMediaType) && truthy(firstMediaType.schema)) {
|
|
3552
|
+
paramSchema = this.extractSchemaAsDict(firstMediaType.schema);
|
|
3553
|
+
}
|
|
3554
|
+
}
|
|
3555
|
+
extracted.push({
|
|
3556
|
+
name: parameter.name,
|
|
3557
|
+
location,
|
|
3558
|
+
required: Boolean(parameter.required),
|
|
3559
|
+
schema: paramSchema,
|
|
3560
|
+
...typeof parameter.description === "string" ? { description: parameter.description } : {},
|
|
3561
|
+
...typeof parameter.explode === "boolean" ? { explode: parameter.explode } : {},
|
|
3562
|
+
...typeof parameter.style === "string" ? { style: parameter.style } : {}
|
|
3563
|
+
});
|
|
3564
|
+
} catch (error) {
|
|
3565
|
+
warn(`Failed to extract parameter: ${error.message}`);
|
|
3566
|
+
}
|
|
3567
|
+
}
|
|
3568
|
+
return extracted;
|
|
3569
|
+
}
|
|
3570
|
+
extractRequestBody(requestBodyOrRef) {
|
|
3571
|
+
if (!truthy(requestBodyOrRef)) return void 0;
|
|
3572
|
+
try {
|
|
3573
|
+
const requestBody = this.resolveRef(requestBodyOrRef);
|
|
3574
|
+
if (!isPlainObject(requestBody)) {
|
|
3575
|
+
warn(`Expected a request body object after resolving. Skipping request body.`);
|
|
3576
|
+
return void 0;
|
|
3577
|
+
}
|
|
3578
|
+
const info = {
|
|
3579
|
+
required: Boolean(requestBody.required),
|
|
3580
|
+
contentSchema: {},
|
|
3581
|
+
...typeof requestBody.description === "string" ? { description: requestBody.description } : {}
|
|
3582
|
+
};
|
|
3583
|
+
if (isPlainObject(requestBody.content)) {
|
|
3584
|
+
for (const [mediaType, mediaTypeObj] of Object.entries(requestBody.content)) {
|
|
3585
|
+
if (isPlainObject(mediaTypeObj) && truthy(mediaTypeObj.schema)) {
|
|
3586
|
+
try {
|
|
3587
|
+
info.contentSchema[mediaType] = this.extractSchemaAsDict(mediaTypeObj.schema);
|
|
3588
|
+
} catch (error) {
|
|
3589
|
+
if (isExternalRefError(error)) throw error;
|
|
3590
|
+
warn(`Failed to extract schema for media type '${mediaType}': ${error.message}`);
|
|
3591
|
+
}
|
|
3592
|
+
}
|
|
3593
|
+
}
|
|
3594
|
+
}
|
|
3595
|
+
return info;
|
|
3596
|
+
} catch (error) {
|
|
3597
|
+
if (isExternalRefError(error)) throw error;
|
|
3598
|
+
warn(`Failed to extract request body: ${error.message}`);
|
|
3599
|
+
return void 0;
|
|
3600
|
+
}
|
|
3601
|
+
}
|
|
3602
|
+
getPrimarySuccessResponse(operationResponses) {
|
|
3603
|
+
for (const code of PRIMARY_SUCCESS_PRIORITY) {
|
|
3604
|
+
if (code in operationResponses) return [code, operationResponses[code]];
|
|
3605
|
+
}
|
|
3606
|
+
for (const [statusCode, respOrRef] of Object.entries(operationResponses)) {
|
|
3607
|
+
if (isSuccessStatusCode(statusCode)) return [statusCode, respOrRef];
|
|
3608
|
+
}
|
|
3609
|
+
return null;
|
|
3610
|
+
}
|
|
3611
|
+
/** Extract only the primary success response for the MCP component. */
|
|
3612
|
+
extractResponses(operationResponses) {
|
|
3613
|
+
const extracted = {};
|
|
3614
|
+
if (!isPlainObject(operationResponses) || !truthy(operationResponses)) return extracted;
|
|
3615
|
+
const primary = this.getPrimarySuccessResponse(operationResponses);
|
|
3616
|
+
if (primary === null) return extracted;
|
|
3617
|
+
const [statusCode, respOrRef] = primary;
|
|
3618
|
+
try {
|
|
3619
|
+
const response = this.resolveRef(respOrRef);
|
|
3620
|
+
if (!isPlainObject(response)) {
|
|
3621
|
+
warn(`Expected a response object after resolving for status code ${statusCode}. Skipping.`);
|
|
3622
|
+
return extracted;
|
|
3623
|
+
}
|
|
3624
|
+
const respInfo = {
|
|
3625
|
+
contentSchema: {},
|
|
3626
|
+
...typeof response.description === "string" ? { description: response.description } : {}
|
|
3627
|
+
};
|
|
3628
|
+
if (isPlainObject(response.content)) {
|
|
3629
|
+
for (const [mediaType, mediaTypeObj] of Object.entries(response.content)) {
|
|
3630
|
+
if (isPlainObject(mediaTypeObj) && truthy(mediaTypeObj.schema)) {
|
|
3631
|
+
try {
|
|
3632
|
+
let topLevelSchemaName = null;
|
|
3633
|
+
const mediaSchema = mediaTypeObj.schema;
|
|
3634
|
+
if (isPlainObject(mediaSchema) && typeof mediaSchema.$ref === "string") {
|
|
3635
|
+
if (mediaSchema.$ref.startsWith("#/components/schemas/")) {
|
|
3636
|
+
topLevelSchemaName = mediaSchema.$ref.split("/").pop();
|
|
3637
|
+
}
|
|
3638
|
+
}
|
|
3639
|
+
const schemaDict = this.extractSchemaAsDict(mediaSchema);
|
|
3640
|
+
if (topLevelSchemaName !== null) {
|
|
3641
|
+
schemaDict["x-fastmcp-top-level-schema"] = topLevelSchemaName;
|
|
3642
|
+
}
|
|
3643
|
+
respInfo.contentSchema[mediaType] = schemaDict;
|
|
3644
|
+
} catch (error) {
|
|
3645
|
+
if (isExternalRefError(error)) throw error;
|
|
3646
|
+
warn(
|
|
3647
|
+
`Failed to extract schema for media type '${mediaType}' in response ${statusCode}: ${error.message}`
|
|
3648
|
+
);
|
|
3649
|
+
}
|
|
3650
|
+
} else {
|
|
3651
|
+
respInfo.contentSchema[mediaType] ??= {};
|
|
3652
|
+
}
|
|
3653
|
+
}
|
|
3654
|
+
}
|
|
3655
|
+
extracted[statusCode] = respInfo;
|
|
3656
|
+
} catch (error) {
|
|
3657
|
+
if (isExternalRefError(error)) throw error;
|
|
3658
|
+
warn(`Failed to extract response for status code ${statusCode}: ${error.message}`);
|
|
3659
|
+
}
|
|
3660
|
+
return extracted;
|
|
3661
|
+
}
|
|
3662
|
+
/** Collect schema names referenced by `schema`, transitively. */
|
|
3663
|
+
extractSchemaDependencies(schema, allSchemas, collected = /* @__PURE__ */ new Set(), followDiscriminator = false) {
|
|
3664
|
+
const collect = (schemaName) => {
|
|
3665
|
+
if (!collected.has(schemaName) && schemaName in allSchemas) {
|
|
3666
|
+
collected.add(schemaName);
|
|
3667
|
+
findRefs(allSchemas[schemaName]);
|
|
3668
|
+
}
|
|
3669
|
+
};
|
|
3670
|
+
const findRefs = (obj) => {
|
|
3671
|
+
if (isPlainObject(obj)) {
|
|
3672
|
+
if (typeof obj.$ref === "string") {
|
|
3673
|
+
const ref = obj.$ref;
|
|
3674
|
+
if (ref.startsWith("#/$defs/") || ref.startsWith("#/components/schemas/")) {
|
|
3675
|
+
collect(ref.split("/").pop());
|
|
3676
|
+
}
|
|
3677
|
+
}
|
|
3678
|
+
if (followDiscriminator) {
|
|
3679
|
+
const discriminator = obj.discriminator;
|
|
3680
|
+
if (isPlainObject(discriminator) && isPlainObject(discriminator.mapping)) {
|
|
3681
|
+
for (const target of Object.values(discriminator.mapping)) {
|
|
3682
|
+
if (typeof target !== "string") continue;
|
|
3683
|
+
const name = discriminatorTargetName(target);
|
|
3684
|
+
if (name !== null) collect(name);
|
|
3685
|
+
}
|
|
3686
|
+
}
|
|
3687
|
+
}
|
|
3688
|
+
for (const value of Object.values(obj)) findRefs(value);
|
|
3689
|
+
} else if (Array.isArray(obj)) {
|
|
3690
|
+
for (const item of obj) findRefs(item);
|
|
3691
|
+
}
|
|
3692
|
+
};
|
|
3693
|
+
findRefs(schema);
|
|
3694
|
+
return collected;
|
|
3695
|
+
}
|
|
3696
|
+
extractInputSchemaDependencies(parameters, requestBody, allSchemas) {
|
|
3697
|
+
const needed = /* @__PURE__ */ new Set();
|
|
3698
|
+
for (const param of parameters) {
|
|
3699
|
+
if (truthy(param.schema)) {
|
|
3700
|
+
this.extractSchemaDependencies(param.schema, allSchemas, needed);
|
|
3701
|
+
}
|
|
3702
|
+
}
|
|
3703
|
+
if (requestBody && truthy(requestBody.contentSchema)) {
|
|
3704
|
+
for (const contentSchema of Object.values(requestBody.contentSchema)) {
|
|
3705
|
+
this.extractSchemaDependencies(contentSchema, allSchemas, needed, true);
|
|
3706
|
+
}
|
|
3707
|
+
}
|
|
3708
|
+
return Object.fromEntries([...needed].filter((n) => n in allSchemas).map((n) => [n, allSchemas[n]]));
|
|
3709
|
+
}
|
|
3710
|
+
extractOutputSchemaDependencies(responses, allSchemas) {
|
|
3711
|
+
if (!truthy(responses) || !truthy(allSchemas)) return {};
|
|
3712
|
+
const needed = /* @__PURE__ */ new Set();
|
|
3713
|
+
for (const response of Object.values(responses)) {
|
|
3714
|
+
if (!truthy(response.contentSchema)) continue;
|
|
3715
|
+
for (const contentSchema of Object.values(response.contentSchema)) {
|
|
3716
|
+
this.extractSchemaDependencies(contentSchema, allSchemas, needed);
|
|
3717
|
+
const schemaName = contentSchema["x-fastmcp-top-level-schema"];
|
|
3718
|
+
if (typeof schemaName === "string" && schemaName in allSchemas) {
|
|
3719
|
+
needed.add(schemaName);
|
|
3720
|
+
this.extractSchemaDependencies(allSchemas[schemaName], allSchemas, needed);
|
|
3721
|
+
}
|
|
3722
|
+
}
|
|
3723
|
+
}
|
|
3724
|
+
return Object.fromEntries([...needed].filter((n) => n in allSchemas).map((n) => [n, allSchemas[n]]));
|
|
3725
|
+
}
|
|
3726
|
+
parse() {
|
|
3727
|
+
const routes = [];
|
|
3728
|
+
const paths = this.spec.paths;
|
|
3729
|
+
if (!isPlainObject(paths) || !truthy(paths)) {
|
|
3730
|
+
warn("OpenAPI schema has no paths defined.");
|
|
3731
|
+
return [];
|
|
3732
|
+
}
|
|
3733
|
+
const schemaDefinitions = {};
|
|
3734
|
+
const components = this.spec.components;
|
|
3735
|
+
if (isPlainObject(components) && isPlainObject(components.schemas)) {
|
|
3736
|
+
for (const [name, schema] of Object.entries(components.schemas)) {
|
|
3737
|
+
try {
|
|
3738
|
+
schemaDefinitions[name] = this.extractSchemaAsDict(this.resolveRef(schema));
|
|
3739
|
+
} catch (error) {
|
|
3740
|
+
warn(`Failed to extract schema definition '${name}': ${error.message}`);
|
|
3741
|
+
}
|
|
3742
|
+
}
|
|
3743
|
+
}
|
|
3744
|
+
for (const [name, schema] of Object.entries(schemaDefinitions)) {
|
|
3745
|
+
if (isPlainObject(schema)) {
|
|
3746
|
+
schemaDefinitions[name] = replaceRefWithDefs(schema);
|
|
3747
|
+
}
|
|
3748
|
+
}
|
|
3749
|
+
for (const [pathStr, pathItemObj] of Object.entries(paths)) {
|
|
3750
|
+
if (!isPlainObject(pathItemObj)) {
|
|
3751
|
+
warn(`Skipping invalid path item for path '${pathStr}'`);
|
|
3752
|
+
continue;
|
|
3753
|
+
}
|
|
3754
|
+
const pathLevelParams = pathItemObj.parameters;
|
|
3755
|
+
for (const methodLower of HTTP_METHODS_LOWER) {
|
|
3756
|
+
const operation = pathItemObj[methodLower];
|
|
3757
|
+
if (!isPlainObject(operation)) continue;
|
|
3758
|
+
const method = methodLower.toUpperCase();
|
|
3759
|
+
try {
|
|
3760
|
+
const parameters = this.extractParameters(operation.parameters, pathLevelParams);
|
|
3761
|
+
const requestBody = this.extractRequestBody(operation.requestBody);
|
|
3762
|
+
const responses = this.extractResponses(operation.responses);
|
|
3763
|
+
const extensions = Object.fromEntries(
|
|
3764
|
+
Object.entries(operation).filter(([key]) => key.startsWith("x-"))
|
|
3765
|
+
);
|
|
3766
|
+
const requestSchemas = this.extractInputSchemaDependencies(
|
|
3767
|
+
parameters,
|
|
3768
|
+
requestBody,
|
|
3769
|
+
schemaDefinitions
|
|
3770
|
+
);
|
|
3771
|
+
const responseSchemas = this.extractOutputSchemaDependencies(
|
|
3772
|
+
responses,
|
|
3773
|
+
schemaDefinitions
|
|
3774
|
+
);
|
|
3775
|
+
const route = {
|
|
3776
|
+
path: pathStr,
|
|
3777
|
+
method,
|
|
3778
|
+
...typeof operation.operationId === "string" ? { operationId: operation.operationId } : {},
|
|
3779
|
+
...typeof operation.summary === "string" ? { summary: operation.summary } : {},
|
|
3780
|
+
...typeof operation.description === "string" ? { description: operation.description } : {},
|
|
3781
|
+
tags: Array.isArray(operation.tags) ? [...operation.tags] : [],
|
|
3782
|
+
parameters,
|
|
3783
|
+
...requestBody !== void 0 ? { requestBody } : {},
|
|
3784
|
+
responses,
|
|
3785
|
+
requestSchemas,
|
|
3786
|
+
responseSchemas,
|
|
3787
|
+
extensions,
|
|
3788
|
+
openapiVersion: this.openapiVersion,
|
|
3789
|
+
flatParamSchema: {},
|
|
3790
|
+
parameterMap: {}
|
|
3791
|
+
};
|
|
3792
|
+
try {
|
|
3793
|
+
const [flatSchema, parameterMap] = combineSchemasAndMapParams(route);
|
|
3794
|
+
route.flatParamSchema = flatSchema;
|
|
3795
|
+
route.parameterMap = parameterMap;
|
|
3796
|
+
} catch (error) {
|
|
3797
|
+
warn(
|
|
3798
|
+
`Failed to pre-calculate schema for route ${method} ${pathStr}: ${error.message}`
|
|
3799
|
+
);
|
|
3800
|
+
route.flatParamSchema = { type: "object", properties: {} };
|
|
3801
|
+
route.parameterMap = {};
|
|
3802
|
+
}
|
|
3803
|
+
routes.push(route);
|
|
3804
|
+
} catch (error) {
|
|
3805
|
+
if (isExternalRefError(error)) throw error;
|
|
3806
|
+
const opId = typeof operation.operationId === "string" ? operation.operationId : "unknown";
|
|
3807
|
+
warn(
|
|
3808
|
+
`Failed to process operation ${method} ${pathStr} (ID: ${opId}): ${error.message}`
|
|
3809
|
+
);
|
|
3810
|
+
}
|
|
3811
|
+
}
|
|
3812
|
+
}
|
|
3813
|
+
return routes;
|
|
3814
|
+
}
|
|
3815
|
+
};
|
|
3816
|
+
|
|
3817
|
+
// src/server/openapi/routing.ts
|
|
3818
|
+
var DEFAULT_ROUTE_MAPPINGS = [{ mcpType: "tool" }];
|
|
3819
|
+
function patternMatches(pattern, path) {
|
|
3820
|
+
if (pattern === void 0) return true;
|
|
3821
|
+
if (typeof pattern === "string") return new RegExp(pattern).test(path);
|
|
3822
|
+
return path.search(pattern) !== -1;
|
|
3823
|
+
}
|
|
3824
|
+
function determineRouteType(route, mappings) {
|
|
3825
|
+
for (const routeMap of mappings) {
|
|
3826
|
+
const methods = routeMap.methods ?? "*";
|
|
3827
|
+
if (methods !== "*" && !methods.includes(route.method)) continue;
|
|
3828
|
+
if (!patternMatches(routeMap.pattern, route.path)) continue;
|
|
3829
|
+
if (routeMap.tags && routeMap.tags.length > 0) {
|
|
3830
|
+
const routeTags = new Set(route.tags ?? []);
|
|
3831
|
+
if (!routeMap.tags.every((tag) => routeTags.has(tag))) continue;
|
|
3832
|
+
}
|
|
3833
|
+
return routeMap;
|
|
3834
|
+
}
|
|
3835
|
+
return { mcpType: "tool" };
|
|
3836
|
+
}
|
|
3837
|
+
|
|
3838
|
+
// src/server/openapi/naming.ts
|
|
3839
|
+
function slugify(text) {
|
|
3840
|
+
if (!text) return "";
|
|
3841
|
+
return text.replace(/[\s\-.]+/g, "_").replace(/[^a-zA-Z0-9_]/g, "").replace(/_+/g, "_").replace(/^_+|_+$/g, "");
|
|
3842
|
+
}
|
|
3843
|
+
function generateDefaultName(route, names) {
|
|
3844
|
+
let name;
|
|
3845
|
+
if (route.operationId) {
|
|
3846
|
+
name = names?.[route.operationId] ?? route.operationId.split("__")[0];
|
|
3847
|
+
} else {
|
|
3848
|
+
name = route.summary || `${route.method}_${route.path}`;
|
|
3849
|
+
}
|
|
3850
|
+
name = slugify(name);
|
|
3851
|
+
if (name.length > 56) name = name.slice(0, 56);
|
|
3852
|
+
return name;
|
|
3853
|
+
}
|
|
3854
|
+
var NameRegistry = class {
|
|
3855
|
+
counters = {
|
|
3856
|
+
tool: /* @__PURE__ */ new Map(),
|
|
3857
|
+
resource: /* @__PURE__ */ new Map(),
|
|
3858
|
+
resourceTemplate: /* @__PURE__ */ new Map(),
|
|
3859
|
+
prompt: /* @__PURE__ */ new Map()
|
|
3860
|
+
};
|
|
3861
|
+
getUniqueName(name, componentType) {
|
|
3862
|
+
const counter = this.counters[componentType];
|
|
3863
|
+
const count = (counter.get(name) ?? 0) + 1;
|
|
3864
|
+
counter.set(name, count);
|
|
3865
|
+
if (count === 1) return name;
|
|
3866
|
+
return `${name}_${count}`;
|
|
3867
|
+
}
|
|
3868
|
+
};
|
|
3869
|
+
|
|
3870
|
+
// src/server/openapi/director.ts
|
|
3871
|
+
function queryScalarToStr(value) {
|
|
3872
|
+
if (typeof value === "boolean") return value ? "true" : "false";
|
|
3873
|
+
return String(value);
|
|
3874
|
+
}
|
|
3875
|
+
var STYLE_DELIMITERS = {
|
|
3876
|
+
form: ",",
|
|
3877
|
+
spaceDelimited: " ",
|
|
3878
|
+
pipeDelimited: "|"
|
|
3879
|
+
};
|
|
3880
|
+
function unflattenArguments(route, flatArgs) {
|
|
3881
|
+
const pathParams = {};
|
|
3882
|
+
const queryParams = {};
|
|
3883
|
+
const headerParams = {};
|
|
3884
|
+
const cookieParams = {};
|
|
3885
|
+
const bodyProps = {};
|
|
3886
|
+
const buckets = {
|
|
3887
|
+
path: pathParams,
|
|
3888
|
+
query: queryParams,
|
|
3889
|
+
header: headerParams,
|
|
3890
|
+
cookie: cookieParams,
|
|
3891
|
+
body: bodyProps
|
|
3892
|
+
};
|
|
3893
|
+
if (Object.keys(route.parameterMap).length > 0) {
|
|
3894
|
+
for (const [argName, value] of Object.entries(flatArgs)) {
|
|
3895
|
+
if (value === null || value === void 0) continue;
|
|
3896
|
+
const mapping = route.parameterMap[argName];
|
|
3897
|
+
if (mapping === void 0) {
|
|
3898
|
+
warn(`Argument '${argName}' not found in parameter map for ${route.operationId ?? route.path}`);
|
|
3899
|
+
continue;
|
|
3900
|
+
}
|
|
3901
|
+
buckets[mapping.location][mapping.openapiName] = value;
|
|
3902
|
+
}
|
|
3903
|
+
} else {
|
|
3904
|
+
const paramLocations = new Map(route.parameters.map((p) => [p.name, p.location]));
|
|
3905
|
+
for (const [argName, value] of Object.entries(flatArgs)) {
|
|
3906
|
+
if (value === null || value === void 0) continue;
|
|
3907
|
+
const suffixIdx = argName.lastIndexOf("__");
|
|
3908
|
+
if (suffixIdx !== -1) {
|
|
3909
|
+
const baseName = argName.slice(0, suffixIdx);
|
|
3910
|
+
const location2 = argName.slice(suffixIdx + 2);
|
|
3911
|
+
if (location2 in buckets && location2 !== "body") {
|
|
3912
|
+
buckets[location2][baseName] = value;
|
|
3913
|
+
continue;
|
|
3914
|
+
}
|
|
3915
|
+
}
|
|
3916
|
+
const location = paramLocations.get(argName);
|
|
3917
|
+
if (location !== void 0) {
|
|
3918
|
+
buckets[location][argName] = value;
|
|
3919
|
+
} else {
|
|
3920
|
+
bodyProps[argName] = value;
|
|
3921
|
+
}
|
|
3922
|
+
}
|
|
3923
|
+
}
|
|
3924
|
+
let body;
|
|
3925
|
+
if (Object.keys(bodyProps).length > 0) {
|
|
3926
|
+
const contentSchema = route.requestBody?.contentSchema;
|
|
3927
|
+
if (contentSchema && Object.keys(contentSchema).length > 0) {
|
|
3928
|
+
const contentType = Object.keys(contentSchema)[0];
|
|
3929
|
+
const bodySchema = contentSchema[contentType];
|
|
3930
|
+
if (isPlainObject(bodySchema) && bodySchema.type === "object") {
|
|
3931
|
+
body = bodyProps;
|
|
3932
|
+
} else if (Object.keys(bodyProps).length === 1) {
|
|
3933
|
+
body = Object.values(bodyProps)[0];
|
|
3934
|
+
} else {
|
|
3935
|
+
body = bodyProps;
|
|
3936
|
+
}
|
|
3937
|
+
} else {
|
|
3938
|
+
body = bodyProps;
|
|
3939
|
+
}
|
|
3940
|
+
}
|
|
3941
|
+
return { pathParams, queryParams, headerParams, cookieParams, body };
|
|
3942
|
+
}
|
|
3943
|
+
function serializeQueryParams(route, queryParams) {
|
|
3944
|
+
if (Object.keys(queryParams).length === 0) return queryParams;
|
|
3945
|
+
const paramLookup = new Map(
|
|
3946
|
+
route.parameters.filter((p) => p.location === "query").map((p) => [p.name, p])
|
|
3947
|
+
);
|
|
3948
|
+
const serialized = {};
|
|
3949
|
+
for (const [key, value] of Object.entries(queryParams)) {
|
|
3950
|
+
const paramInfo = paramLookup.get(key);
|
|
3951
|
+
if (paramInfo !== void 0) {
|
|
3952
|
+
const explode = paramInfo.explode ?? true;
|
|
3953
|
+
if (isPlainObject(value)) {
|
|
3954
|
+
if (Object.keys(value).length === 0) continue;
|
|
3955
|
+
if (explode) {
|
|
3956
|
+
for (const [propKey, propValue] of Object.entries(value)) {
|
|
3957
|
+
const propertyName = paramInfo.style === "deepObject" ? `${key}[${queryScalarToStr(propKey)}]` : queryScalarToStr(propKey);
|
|
3958
|
+
serialized[propertyName] = queryScalarToStr(propValue);
|
|
3959
|
+
}
|
|
3960
|
+
} else {
|
|
3961
|
+
const delimiter = STYLE_DELIMITERS[paramInfo.style ?? "form"] ?? ",";
|
|
3962
|
+
const parts = [];
|
|
3963
|
+
for (const [propKey, propValue] of Object.entries(value)) {
|
|
3964
|
+
parts.push(queryScalarToStr(propKey), queryScalarToStr(propValue));
|
|
3965
|
+
}
|
|
3966
|
+
serialized[key] = parts.join(delimiter);
|
|
3967
|
+
}
|
|
3968
|
+
continue;
|
|
3969
|
+
}
|
|
3970
|
+
if (!explode && Array.isArray(value)) {
|
|
3971
|
+
if (value.length === 0) continue;
|
|
3972
|
+
const delimiter = STYLE_DELIMITERS[paramInfo.style ?? "form"] ?? ",";
|
|
3973
|
+
serialized[key] = value.map(queryScalarToStr).join(delimiter);
|
|
3974
|
+
continue;
|
|
3975
|
+
}
|
|
3976
|
+
}
|
|
3977
|
+
serialized[key] = value;
|
|
3978
|
+
}
|
|
3979
|
+
return serialized;
|
|
3980
|
+
}
|
|
3981
|
+
function quotePathValue(value) {
|
|
3982
|
+
return encodeURIComponent(value).replace(/[!'()*]/g, (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`).replace(/\./g, "%2E");
|
|
3983
|
+
}
|
|
3984
|
+
function buildUrl(pathTemplate, pathParams, baseUrl) {
|
|
3985
|
+
let urlPath = pathTemplate;
|
|
3986
|
+
for (const [paramName, paramValue] of Object.entries(pathParams)) {
|
|
3987
|
+
const placeholder = `{${paramName}}`;
|
|
3988
|
+
if (urlPath.includes(placeholder)) {
|
|
3989
|
+
urlPath = urlPath.split(placeholder).join(quotePathValue(String(paramValue)));
|
|
3990
|
+
}
|
|
3991
|
+
}
|
|
3992
|
+
const base = `${baseUrl.replace(/\/+$/, "")}/`;
|
|
3993
|
+
return new URL(urlPath.replace(/^\/+/, ""), base).toString();
|
|
3994
|
+
}
|
|
3995
|
+
function buildRequest(route, flatArgs, baseUrl) {
|
|
3996
|
+
const { pathParams, queryParams, headerParams, cookieParams, body } = unflattenArguments(
|
|
3997
|
+
route,
|
|
3998
|
+
flatArgs
|
|
3999
|
+
);
|
|
4000
|
+
const serializedQuery = serializeQueryParams(route, queryParams);
|
|
4001
|
+
const url = new URL(buildUrl(route.path, pathParams, baseUrl));
|
|
4002
|
+
for (const [key, value] of Object.entries(serializedQuery)) {
|
|
4003
|
+
if (Array.isArray(value)) {
|
|
4004
|
+
for (const item of value) url.searchParams.append(key, queryScalarToStr(item));
|
|
4005
|
+
} else {
|
|
4006
|
+
url.searchParams.append(key, queryScalarToStr(value));
|
|
4007
|
+
}
|
|
4008
|
+
}
|
|
4009
|
+
const headers = {};
|
|
4010
|
+
for (const [key, value] of Object.entries(headerParams)) {
|
|
4011
|
+
headers[key] = queryScalarToStr(value);
|
|
4012
|
+
}
|
|
4013
|
+
const cookiePairs = Object.entries(cookieParams).map(
|
|
4014
|
+
([key, value]) => `${key}=${queryScalarToStr(value)}`
|
|
4015
|
+
);
|
|
4016
|
+
if (cookiePairs.length > 0) {
|
|
4017
|
+
headers.cookie = cookiePairs.join("; ");
|
|
4018
|
+
}
|
|
4019
|
+
let rawContentType;
|
|
4020
|
+
let declaredContentType;
|
|
4021
|
+
const contentSchema = route.requestBody?.contentSchema;
|
|
4022
|
+
if (contentSchema && Object.keys(contentSchema).length > 0) {
|
|
4023
|
+
rawContentType = Object.keys(contentSchema)[0];
|
|
4024
|
+
declaredContentType = rawContentType.split(";")[0].trim().toLowerCase();
|
|
4025
|
+
}
|
|
4026
|
+
let requestBody;
|
|
4027
|
+
if (body !== null && body !== void 0) {
|
|
4028
|
+
if (declaredContentType === "multipart/form-data" && isPlainObject(body)) {
|
|
4029
|
+
const form = new FormData();
|
|
4030
|
+
for (const [key, value] of Object.entries(body)) {
|
|
4031
|
+
if (value instanceof Uint8Array) {
|
|
4032
|
+
form.append(key, new Blob([value]));
|
|
4033
|
+
} else {
|
|
4034
|
+
form.append(key, queryScalarToStr(value));
|
|
4035
|
+
}
|
|
4036
|
+
}
|
|
4037
|
+
requestBody = form;
|
|
4038
|
+
} else if (declaredContentType === "application/x-www-form-urlencoded" && isPlainObject(body)) {
|
|
4039
|
+
const params = new URLSearchParams();
|
|
4040
|
+
for (const [key, value] of Object.entries(body)) {
|
|
4041
|
+
params.append(key, queryScalarToStr(value));
|
|
4042
|
+
}
|
|
4043
|
+
requestBody = params;
|
|
4044
|
+
} else if (isPlainObject(body) || Array.isArray(body)) {
|
|
4045
|
+
requestBody = JSON.stringify(body);
|
|
4046
|
+
headers["content-type"] = declaredContentType !== void 0 && declaredContentType !== "application/json" && declaredContentType.includes("json") ? rawContentType : "application/json";
|
|
4047
|
+
} else {
|
|
4048
|
+
requestBody = String(body);
|
|
4049
|
+
if (rawContentType !== void 0) {
|
|
4050
|
+
headers["content-type"] = rawContentType;
|
|
4051
|
+
}
|
|
4052
|
+
}
|
|
4053
|
+
}
|
|
4054
|
+
return {
|
|
4055
|
+
method: route.method.toUpperCase(),
|
|
4056
|
+
url: url.toString(),
|
|
4057
|
+
headers,
|
|
4058
|
+
...requestBody !== void 0 ? { body: requestBody } : {}
|
|
4059
|
+
};
|
|
4060
|
+
}
|
|
4061
|
+
function pathArgumentName(route, parameterName) {
|
|
4062
|
+
for (const [argumentName, mapping] of Object.entries(route.parameterMap)) {
|
|
4063
|
+
if (mapping.location === "path" && mapping.openapiName === parameterName) {
|
|
4064
|
+
return argumentName;
|
|
4065
|
+
}
|
|
4066
|
+
}
|
|
4067
|
+
return parameterName;
|
|
4068
|
+
}
|
|
4069
|
+
|
|
4070
|
+
// src/server/openapi/execute.ts
|
|
4071
|
+
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
4072
|
+
function fillServerVariables(url, variables) {
|
|
4073
|
+
if (!isPlainObject(variables)) return url;
|
|
4074
|
+
let filled = url;
|
|
4075
|
+
for (const [name, variable] of Object.entries(variables)) {
|
|
4076
|
+
const fallback = isPlainObject(variable) ? variable.default : void 0;
|
|
4077
|
+
filled = filled.split(`{${name}}`).join(typeof fallback === "string" ? fallback : "");
|
|
4078
|
+
}
|
|
4079
|
+
return filled;
|
|
4080
|
+
}
|
|
4081
|
+
function resolveClient(options, spec) {
|
|
4082
|
+
let baseUrl = options.baseUrl;
|
|
4083
|
+
if (baseUrl === void 0) {
|
|
4084
|
+
const servers = spec.servers;
|
|
4085
|
+
const first = Array.isArray(servers) ? servers[0] : void 0;
|
|
4086
|
+
const serverUrl = isPlainObject(first) && typeof first.url === "string" ? first.url : void 0;
|
|
4087
|
+
if (serverUrl === void 0) {
|
|
4088
|
+
throw new Error(
|
|
4089
|
+
"No server URL found in OpenAPI spec. Either add a 'servers' entry to the spec or provide client.baseUrl explicitly."
|
|
4090
|
+
);
|
|
4091
|
+
}
|
|
4092
|
+
baseUrl = fillServerVariables(serverUrl, first?.variables);
|
|
4093
|
+
}
|
|
4094
|
+
try {
|
|
4095
|
+
new URL(baseUrl);
|
|
4096
|
+
} catch {
|
|
4097
|
+
throw new Error(
|
|
4098
|
+
`OpenAPI base URL must be absolute, got '${baseUrl}'. Provide client.baseUrl explicitly.`
|
|
4099
|
+
);
|
|
4100
|
+
}
|
|
4101
|
+
return {
|
|
4102
|
+
baseUrl,
|
|
4103
|
+
headers: options.headers ?? {},
|
|
4104
|
+
...options.auth !== void 0 ? { auth: options.auth } : {},
|
|
4105
|
+
fetch: options.fetch ?? globalThis.fetch,
|
|
4106
|
+
timeoutMs: options.timeoutMs ?? DEFAULT_TIMEOUT_MS
|
|
4107
|
+
};
|
|
4108
|
+
}
|
|
4109
|
+
function isTimeoutError(error) {
|
|
4110
|
+
return error instanceof Error && (error.name === "TimeoutError" || error.name === "AbortError");
|
|
4111
|
+
}
|
|
4112
|
+
async function sendRequest(client, route, args) {
|
|
4113
|
+
let url;
|
|
4114
|
+
let init;
|
|
4115
|
+
try {
|
|
4116
|
+
const directed = buildRequest(route, args, client.baseUrl);
|
|
4117
|
+
const authHeaders = client.auth ? await client.auth.getHeaders() : {};
|
|
4118
|
+
const headers = {};
|
|
4119
|
+
for (const source of [client.headers, authHeaders, directed.headers]) {
|
|
4120
|
+
for (const [name, value] of Object.entries(source)) {
|
|
4121
|
+
headers[name.toLowerCase()] = value;
|
|
4122
|
+
}
|
|
4123
|
+
}
|
|
4124
|
+
url = directed.url;
|
|
4125
|
+
init = {
|
|
4126
|
+
method: directed.method,
|
|
4127
|
+
headers,
|
|
4128
|
+
...directed.body !== void 0 ? { body: directed.body } : {},
|
|
4129
|
+
signal: AbortSignal.timeout(client.timeoutMs)
|
|
4130
|
+
};
|
|
4131
|
+
} catch (error) {
|
|
4132
|
+
const e = error;
|
|
4133
|
+
throw new Error(
|
|
4134
|
+
`Error building request for ${route.method.toUpperCase()} ${route.path}: ${e.name}: ${e.message}`
|
|
4135
|
+
);
|
|
4136
|
+
}
|
|
4137
|
+
let response;
|
|
4138
|
+
try {
|
|
4139
|
+
response = await client.fetch(url, init);
|
|
4140
|
+
} catch (error) {
|
|
4141
|
+
if (isTimeoutError(error)) {
|
|
4142
|
+
throw new Error(`HTTP request timed out (${error.name})`);
|
|
4143
|
+
}
|
|
4144
|
+
const e = error;
|
|
4145
|
+
throw new Error(`Request error (${e.name}): ${e.message}`);
|
|
4146
|
+
}
|
|
4147
|
+
if (!response.ok) {
|
|
4148
|
+
let message = `HTTP error ${response.status}: ${response.statusText}`;
|
|
4149
|
+
const text = await response.text().catch(() => "");
|
|
4150
|
+
if (text) message += ` - ${text}`;
|
|
4151
|
+
throw new Error(message);
|
|
4152
|
+
}
|
|
4153
|
+
return response;
|
|
4154
|
+
}
|
|
4155
|
+
async function runToolRequest(client, route, args, outputSchema) {
|
|
4156
|
+
const response = await sendRequest(client, route, args);
|
|
4157
|
+
const text = await response.text();
|
|
4158
|
+
let result;
|
|
4159
|
+
try {
|
|
4160
|
+
result = JSON.parse(text);
|
|
4161
|
+
} catch {
|
|
4162
|
+
return new ToolResult({ content: [{ type: "text", text }] });
|
|
4163
|
+
}
|
|
4164
|
+
let structured;
|
|
4165
|
+
if (outputSchema !== null && outputSchema !== void 0) {
|
|
4166
|
+
structured = truthy(outputSchema["x-fastmcp-wrap-result"]) ? { result } : result;
|
|
4167
|
+
} else if (!isPlainObject(result)) {
|
|
4168
|
+
structured = { result };
|
|
4169
|
+
} else {
|
|
4170
|
+
structured = result;
|
|
4171
|
+
}
|
|
4172
|
+
if (!isPlainObject(structured)) {
|
|
4173
|
+
structured = { result: structured };
|
|
4174
|
+
}
|
|
4175
|
+
return new ToolResult({
|
|
4176
|
+
content: [{ type: "text", text: JSON.stringify(structured) }],
|
|
4177
|
+
structuredContent: structured
|
|
4178
|
+
});
|
|
4179
|
+
}
|
|
4180
|
+
async function readResourceRequest(client, route, args, uri, mimeType) {
|
|
4181
|
+
const response = await sendRequest(client, route, args);
|
|
4182
|
+
const contentType = (response.headers.get("content-type") ?? "").toLowerCase();
|
|
4183
|
+
if (contentType.includes("application/json")) {
|
|
4184
|
+
const result = JSON.parse(await response.text());
|
|
4185
|
+
return new ResourceResult([
|
|
4186
|
+
{ uri, mimeType: "application/json", text: JSON.stringify(result) }
|
|
4187
|
+
]);
|
|
4188
|
+
}
|
|
4189
|
+
if (contentType.includes("text/") || contentType.includes("application/xml")) {
|
|
4190
|
+
return new ResourceResult([{ uri, mimeType, text: await response.text() }]);
|
|
4191
|
+
}
|
|
4192
|
+
const bytes = Buffer.from(await response.arrayBuffer());
|
|
4193
|
+
return new ResourceResult([{ uri, mimeType, blob: bytes.toString("base64") }]);
|
|
4194
|
+
}
|
|
4195
|
+
|
|
4196
|
+
// src/server/openapi/index.ts
|
|
4197
|
+
var DEFAULT_MIME_TYPE = "application/json";
|
|
4198
|
+
var JSON_COMPATIBLE_TYPES2 = [
|
|
4199
|
+
"application/json",
|
|
4200
|
+
"application/vnd.api+json",
|
|
4201
|
+
"application/hal+json",
|
|
4202
|
+
"application/ld+json",
|
|
4203
|
+
"text/json"
|
|
4204
|
+
];
|
|
4205
|
+
function extractMimeTypeFromRoute(route) {
|
|
4206
|
+
if (!truthy(route.responses)) return DEFAULT_MIME_TYPE;
|
|
4207
|
+
let responseInfo = void 0;
|
|
4208
|
+
for (const statusCode of ["200", "201", "202", "204"]) {
|
|
4209
|
+
if (statusCode in route.responses) {
|
|
4210
|
+
responseInfo = route.responses[statusCode];
|
|
4211
|
+
break;
|
|
4212
|
+
}
|
|
4213
|
+
}
|
|
4214
|
+
if (responseInfo === void 0) {
|
|
4215
|
+
for (const [statusCode, respInfo] of Object.entries(route.responses)) {
|
|
4216
|
+
if (statusCode.startsWith("2")) {
|
|
4217
|
+
responseInfo = respInfo;
|
|
4218
|
+
break;
|
|
4219
|
+
}
|
|
4220
|
+
}
|
|
4221
|
+
}
|
|
4222
|
+
if (responseInfo === void 0 || !truthy(responseInfo.contentSchema)) return DEFAULT_MIME_TYPE;
|
|
4223
|
+
const contentTypes = Object.keys(responseInfo.contentSchema);
|
|
4224
|
+
if (contentTypes.length === 1) return contentTypes[0];
|
|
4225
|
+
for (const contentType of JSON_COMPATIBLE_TYPES2) {
|
|
4226
|
+
if (contentType in responseInfo.contentSchema) return contentType;
|
|
4227
|
+
}
|
|
4228
|
+
return contentTypes[0];
|
|
4229
|
+
}
|
|
4230
|
+
function expandTemplateUri(uriTemplate, params) {
|
|
4231
|
+
let uri = uriTemplate;
|
|
4232
|
+
for (const [name, value] of Object.entries(params)) {
|
|
4233
|
+
uri = uri.split(`{${name}}`).join(encodeURIComponent(value));
|
|
4234
|
+
}
|
|
4235
|
+
return uri;
|
|
4236
|
+
}
|
|
4237
|
+
function templateArguments(route, params) {
|
|
4238
|
+
const args = {};
|
|
4239
|
+
for (const parameter of route.parameters) {
|
|
4240
|
+
if (parameter.location !== "path") continue;
|
|
4241
|
+
const argumentName = pathArgumentName(route, parameter.name);
|
|
4242
|
+
if (parameter.name in params) {
|
|
4243
|
+
args[argumentName] = params[parameter.name];
|
|
4244
|
+
continue;
|
|
4245
|
+
}
|
|
4246
|
+
const normalizedName = parameter.name.replace(/-/g, "_");
|
|
4247
|
+
if (normalizedName in params) {
|
|
4248
|
+
args[argumentName] = params[normalizedName];
|
|
4249
|
+
}
|
|
4250
|
+
}
|
|
4251
|
+
return args;
|
|
4252
|
+
}
|
|
4253
|
+
function createOpenAPIServer(options) {
|
|
4254
|
+
const spec = loadSpec(options.spec);
|
|
4255
|
+
const client = resolveClient(options.client ?? {}, spec);
|
|
4256
|
+
const routes = parseOpenAPIToHttpRoutes(spec);
|
|
4257
|
+
const server = new FastMCP({
|
|
4258
|
+
name: options.name ?? "OpenAPI Server",
|
|
4259
|
+
...options.version !== void 0 ? { version: options.version } : {},
|
|
4260
|
+
...options.serverOptions
|
|
4261
|
+
});
|
|
4262
|
+
const routeMaps = [...options.routeMaps ?? [], ...DEFAULT_ROUTE_MAPPINGS];
|
|
4263
|
+
const registry = new NameRegistry();
|
|
4264
|
+
const validateOutput = options.validateOutput ?? true;
|
|
4265
|
+
for (const route of routes) {
|
|
4266
|
+
const routeMap = determineRouteType(route, routeMaps);
|
|
4267
|
+
let routeType = routeMap.mcpType;
|
|
4268
|
+
if (options.routeMapFn !== void 0) {
|
|
4269
|
+
try {
|
|
4270
|
+
const result = options.routeMapFn(route, routeType);
|
|
4271
|
+
if (result !== null && result !== void 0) {
|
|
4272
|
+
routeType = result;
|
|
4273
|
+
}
|
|
4274
|
+
} catch (error) {
|
|
4275
|
+
warn(
|
|
4276
|
+
`Error in routeMapFn for ${route.method} ${route.path}: ${error.message}. Using mapped type.`
|
|
4277
|
+
);
|
|
4278
|
+
}
|
|
4279
|
+
}
|
|
4280
|
+
const componentName = generateDefaultName(route, options.names);
|
|
4281
|
+
const componentTags = [
|
|
4282
|
+
.../* @__PURE__ */ new Set([...route.tags, ...routeMap.mcpTags ?? [], ...options.tags ?? []])
|
|
4283
|
+
].sort();
|
|
4284
|
+
if (routeType === "tool") {
|
|
4285
|
+
createTool(server, client, route, componentName, componentTags, registry, validateOutput, options.componentFn);
|
|
4286
|
+
} else if (routeType === "resource") {
|
|
4287
|
+
createResource(server, client, route, componentName, componentTags, registry, options.componentFn);
|
|
4288
|
+
} else if (routeType === "resourceTemplate") {
|
|
4289
|
+
createTemplate(server, client, route, componentName, componentTags, registry, options.componentFn);
|
|
4290
|
+
}
|
|
4291
|
+
}
|
|
4292
|
+
return server;
|
|
4293
|
+
}
|
|
4294
|
+
function applyComponentFn(componentFn, route, component) {
|
|
4295
|
+
if (componentFn === void 0) return;
|
|
4296
|
+
try {
|
|
4297
|
+
componentFn(route, component);
|
|
4298
|
+
} catch (error) {
|
|
4299
|
+
warn(
|
|
4300
|
+
`Error in componentFn for ${component.kind} '${component.name}': ${error.message}`
|
|
4301
|
+
);
|
|
4302
|
+
}
|
|
4303
|
+
}
|
|
4304
|
+
function createTool(server, client, route, name, tags, registry, validateOutput, componentFn) {
|
|
4305
|
+
let outputSchema = extractOutputSchemaFromResponses(
|
|
4306
|
+
route.responses,
|
|
4307
|
+
route.responseSchemas,
|
|
4308
|
+
route.openapiVersion
|
|
4309
|
+
);
|
|
4310
|
+
if (!validateOutput && outputSchema !== null) {
|
|
4311
|
+
const permissive = { type: "object", additionalProperties: true };
|
|
4312
|
+
if (truthy(outputSchema["x-fastmcp-wrap-result"])) {
|
|
4313
|
+
permissive["x-fastmcp-wrap-result"] = true;
|
|
4314
|
+
}
|
|
4315
|
+
outputSchema = permissive;
|
|
4316
|
+
}
|
|
4317
|
+
const component = {
|
|
4318
|
+
kind: "tool",
|
|
4319
|
+
name: registry.getUniqueName(name, "tool"),
|
|
4320
|
+
description: route.description || route.summary || `Executes ${route.method} ${route.path}`,
|
|
4321
|
+
inputSchema: route.flatParamSchema,
|
|
4322
|
+
...outputSchema !== null ? { outputSchema } : {},
|
|
4323
|
+
tags
|
|
4324
|
+
};
|
|
4325
|
+
applyComponentFn(componentFn, route, component);
|
|
4326
|
+
server.tool(
|
|
4327
|
+
{
|
|
4328
|
+
name: component.name,
|
|
4329
|
+
description: component.description,
|
|
4330
|
+
inputSchema: component.inputSchema,
|
|
4331
|
+
...component.outputSchema !== void 0 ? { outputSchema: component.outputSchema } : {},
|
|
4332
|
+
tags: component.tags
|
|
4333
|
+
},
|
|
4334
|
+
async (args) => runToolRequest(client, route, isPlainObject(args) ? args : {}, component.outputSchema)
|
|
4335
|
+
);
|
|
4336
|
+
}
|
|
4337
|
+
function createResource(server, client, route, name, tags, registry, componentFn) {
|
|
4338
|
+
const uniqueName = registry.getUniqueName(name, "resource");
|
|
4339
|
+
const component = {
|
|
4340
|
+
kind: "resource",
|
|
4341
|
+
uri: `resource://${uniqueName}`,
|
|
4342
|
+
name: uniqueName,
|
|
4343
|
+
description: route.description || route.summary || `Represents ${route.path}`,
|
|
4344
|
+
mimeType: extractMimeTypeFromRoute(route),
|
|
4345
|
+
tags
|
|
4346
|
+
};
|
|
4347
|
+
applyComponentFn(componentFn, route, component);
|
|
4348
|
+
server.resource(
|
|
4349
|
+
{
|
|
4350
|
+
uri: component.uri,
|
|
4351
|
+
name: component.name,
|
|
4352
|
+
description: component.description,
|
|
4353
|
+
mimeType: component.mimeType,
|
|
4354
|
+
tags: component.tags
|
|
4355
|
+
},
|
|
4356
|
+
async () => readResourceRequest(client, route, {}, component.uri, component.mimeType)
|
|
4357
|
+
);
|
|
4358
|
+
}
|
|
4359
|
+
function createTemplate(server, client, route, name, tags, registry, componentFn) {
|
|
4360
|
+
const uniqueName = registry.getUniqueName(name, "resourceTemplate");
|
|
4361
|
+
const pathParams = route.parameters.filter((p) => p.location === "path").map((p) => p.name).sort();
|
|
4362
|
+
let uriTemplate = `resource://${uniqueName}`;
|
|
4363
|
+
if (pathParams.length > 0) {
|
|
4364
|
+
uriTemplate += "/" + pathParams.map((p) => `{${p}}`).join("/");
|
|
4365
|
+
}
|
|
4366
|
+
const parametersSchema = {
|
|
4367
|
+
type: "object",
|
|
4368
|
+
properties: Object.fromEntries(
|
|
4369
|
+
route.parameters.filter((p) => p.location === "path").map((p) => [
|
|
4370
|
+
p.name,
|
|
4371
|
+
{
|
|
4372
|
+
...isPlainObject(p.schema) ? p.schema : {},
|
|
4373
|
+
...truthy(p.description) && !(isPlainObject(p.schema) && "description" in p.schema) ? { description: p.description } : {}
|
|
4374
|
+
}
|
|
4375
|
+
])
|
|
4376
|
+
),
|
|
4377
|
+
required: route.parameters.filter((p) => p.location === "path" && p.required).map((p) => p.name)
|
|
4378
|
+
};
|
|
4379
|
+
const component = {
|
|
4380
|
+
kind: "resourceTemplate",
|
|
4381
|
+
uriTemplate,
|
|
4382
|
+
name: uniqueName,
|
|
4383
|
+
description: route.description || route.summary || `Template for ${route.path}`,
|
|
4384
|
+
mimeType: extractMimeTypeFromRoute(route),
|
|
4385
|
+
parameters: parametersSchema,
|
|
4386
|
+
tags
|
|
4387
|
+
};
|
|
4388
|
+
applyComponentFn(componentFn, route, component);
|
|
4389
|
+
server.resource(
|
|
4390
|
+
{
|
|
4391
|
+
uri: component.uriTemplate,
|
|
4392
|
+
name: component.name,
|
|
4393
|
+
description: component.description,
|
|
4394
|
+
mimeType: component.mimeType,
|
|
4395
|
+
tags: component.tags
|
|
4396
|
+
},
|
|
4397
|
+
async (params) => {
|
|
4398
|
+
const matched = params ?? {};
|
|
4399
|
+
const concreteUri = expandTemplateUri(component.uriTemplate, matched);
|
|
4400
|
+
return readResourceRequest(
|
|
4401
|
+
client,
|
|
4402
|
+
route,
|
|
4403
|
+
templateArguments(route, matched),
|
|
4404
|
+
concreteUri,
|
|
4405
|
+
component.mimeType
|
|
4406
|
+
);
|
|
4407
|
+
}
|
|
4408
|
+
);
|
|
4409
|
+
}
|
|
4410
|
+
|
|
2819
4411
|
// src/server/apps/FastMCPApp.ts
|
|
2820
4412
|
var FastMCPApp = class {
|
|
2821
4413
|
server;
|
|
@@ -3353,8 +4945,10 @@ export {
|
|
|
3353
4945
|
VersionFilter,
|
|
3354
4946
|
acceptedContent,
|
|
3355
4947
|
actionRef,
|
|
4948
|
+
createOpenAPIServer,
|
|
3356
4949
|
createProxy,
|
|
3357
4950
|
debugTokenVerifier,
|
|
4951
|
+
forwardableHeaders,
|
|
3358
4952
|
inputRequired,
|
|
3359
4953
|
inputResponse,
|
|
3360
4954
|
introspectionVerifier,
|