@powerduck/openapi-mcp-server 1.0.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/README.md +116 -0
- package/dist/admin-server-B4fQWUnc.d.cts +313 -0
- package/dist/admin-server-B4fQWUnc.d.ts +313 -0
- package/dist/cli.cjs +3703 -0
- package/dist/cli.cjs.map +1 -0
- package/dist/cli.d.cts +2 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.mjs +3690 -0
- package/dist/cli.mjs.map +1 -0
- package/dist/index.cjs +3697 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +374 -0
- package/dist/index.d.ts +374 -0
- package/dist/index.mjs +3658 -0
- package/dist/index.mjs.map +1 -0
- package/dist/server/admin-server.cjs +3570 -0
- package/dist/server/admin-server.cjs.map +1 -0
- package/dist/server/admin-server.d.cts +5 -0
- package/dist/server/admin-server.d.ts +5 -0
- package/dist/server/admin-server.mjs +3557 -0
- package/dist/server/admin-server.mjs.map +1 -0
- package/dist/webui/index.html +2724 -0
- package/package.json +62 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,3697 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
var fs2 = require('fs');
|
|
5
|
+
var jsYaml = require('js-yaml');
|
|
6
|
+
var openapiParser = require('@scalar/openapi-parser');
|
|
7
|
+
var types_js = require('@modelcontextprotocol/sdk/types.js');
|
|
8
|
+
var axios = require('axios');
|
|
9
|
+
var crypto = require('crypto');
|
|
10
|
+
var index_js = require('@modelcontextprotocol/sdk/server/index.js');
|
|
11
|
+
var stdio_js = require('@modelcontextprotocol/sdk/server/stdio.js');
|
|
12
|
+
var express2 = require('express');
|
|
13
|
+
var sse_js = require('@modelcontextprotocol/sdk/server/sse.js');
|
|
14
|
+
var streamableHttp_js = require('@modelcontextprotocol/sdk/server/streamableHttp.js');
|
|
15
|
+
var cors = require('cors');
|
|
16
|
+
var multer = require('multer');
|
|
17
|
+
var path = require('path');
|
|
18
|
+
var http = require('http');
|
|
19
|
+
var url = require('url');
|
|
20
|
+
var os = require('os');
|
|
21
|
+
|
|
22
|
+
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
23
|
+
|
|
24
|
+
var fs2__default = /*#__PURE__*/_interopDefault(fs2);
|
|
25
|
+
var axios__default = /*#__PURE__*/_interopDefault(axios);
|
|
26
|
+
var express2__default = /*#__PURE__*/_interopDefault(express2);
|
|
27
|
+
var cors__default = /*#__PURE__*/_interopDefault(cors);
|
|
28
|
+
var multer__default = /*#__PURE__*/_interopDefault(multer);
|
|
29
|
+
var path__default = /*#__PURE__*/_interopDefault(path);
|
|
30
|
+
var os__default = /*#__PURE__*/_interopDefault(os);
|
|
31
|
+
|
|
32
|
+
// node_modules/tsup/assets/cjs_shims.js
|
|
33
|
+
var getImportMetaUrl = () => typeof document === "undefined" ? new URL(`file:${__filename}`).href : document.currentScript && document.currentScript.tagName.toUpperCase() === "SCRIPT" ? document.currentScript.src : new URL("main.js", document.baseURI).href;
|
|
34
|
+
var importMetaUrl = /* @__PURE__ */ getImportMetaUrl();
|
|
35
|
+
|
|
36
|
+
// src/core/spec-utils.ts
|
|
37
|
+
var HTTP_METHODS = [
|
|
38
|
+
"get",
|
|
39
|
+
"put",
|
|
40
|
+
"post",
|
|
41
|
+
"delete",
|
|
42
|
+
"options",
|
|
43
|
+
"head",
|
|
44
|
+
"patch",
|
|
45
|
+
"trace",
|
|
46
|
+
"query"
|
|
47
|
+
];
|
|
48
|
+
var HTTP_METHOD_SET = new Set(HTTP_METHODS);
|
|
49
|
+
var PATH_ITEM_METADATA_KEYS = /* @__PURE__ */ new Set([
|
|
50
|
+
"$ref",
|
|
51
|
+
"summary",
|
|
52
|
+
"description",
|
|
53
|
+
"servers",
|
|
54
|
+
"parameters",
|
|
55
|
+
"additionalOperations"
|
|
56
|
+
]);
|
|
57
|
+
var PARAMETER_LOCATIONS = [
|
|
58
|
+
"path",
|
|
59
|
+
"query",
|
|
60
|
+
"header",
|
|
61
|
+
"cookie"
|
|
62
|
+
];
|
|
63
|
+
var PARAMETER_LOCATION_SET = new Set(
|
|
64
|
+
PARAMETER_LOCATIONS
|
|
65
|
+
);
|
|
66
|
+
var KNOWN_UNSUPPORTED_LOCATIONS = /* @__PURE__ */ new Set([
|
|
67
|
+
"querystring"
|
|
68
|
+
]);
|
|
69
|
+
var RESERVED_HEADER_NAMES = /* @__PURE__ */ new Set([
|
|
70
|
+
"accept",
|
|
71
|
+
"content-type",
|
|
72
|
+
"authorization",
|
|
73
|
+
"host",
|
|
74
|
+
"content-length",
|
|
75
|
+
"connection",
|
|
76
|
+
"transfer-encoding"
|
|
77
|
+
]);
|
|
78
|
+
var MAX_GENERATED_NAME_LENGTH = 64;
|
|
79
|
+
function report(sink, issue) {
|
|
80
|
+
sink?.push(issue);
|
|
81
|
+
}
|
|
82
|
+
function isPlainObject(value) {
|
|
83
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
84
|
+
}
|
|
85
|
+
function isReference(value) {
|
|
86
|
+
return isPlainObject(value) && typeof value.$ref === "string";
|
|
87
|
+
}
|
|
88
|
+
function stableDigest(input) {
|
|
89
|
+
let hash = 2166136261;
|
|
90
|
+
for (let index = 0; index < input.length; index += 1) {
|
|
91
|
+
hash ^= input.charCodeAt(index);
|
|
92
|
+
hash = Math.imul(hash, 16777619) >>> 0;
|
|
93
|
+
}
|
|
94
|
+
return hash.toString(36).padStart(7, "0").slice(0, 7);
|
|
95
|
+
}
|
|
96
|
+
function truncateGeneratedName(name, limit = MAX_GENERATED_NAME_LENGTH) {
|
|
97
|
+
if (name.length <= limit) return name;
|
|
98
|
+
const digest = stableDigest(name);
|
|
99
|
+
const keep = Math.max(1, limit - digest.length - 1);
|
|
100
|
+
return `${name.slice(0, keep).replace(/_+$/, "")}_${digest}`;
|
|
101
|
+
}
|
|
102
|
+
function extractPathTemplateVariables(pathTemplate) {
|
|
103
|
+
const found = /* @__PURE__ */ new Set();
|
|
104
|
+
const pattern = /\{([^{}/]+)\}/g;
|
|
105
|
+
let match;
|
|
106
|
+
while ((match = pattern.exec(pathTemplate)) !== null) {
|
|
107
|
+
const name = match[1]?.trim();
|
|
108
|
+
if (name) found.add(name);
|
|
109
|
+
}
|
|
110
|
+
return [...found];
|
|
111
|
+
}
|
|
112
|
+
function selectContentMediaType(content) {
|
|
113
|
+
const keys = Object.keys(content);
|
|
114
|
+
if (keys.length === 0) return void 0;
|
|
115
|
+
const json = keys.find((key) => /\bjson\b/i.test(key));
|
|
116
|
+
return json ?? keys[0];
|
|
117
|
+
}
|
|
118
|
+
function normalizeParameter(candidate) {
|
|
119
|
+
if (!isPlainObject(candidate)) return null;
|
|
120
|
+
if (isReference(candidate)) return null;
|
|
121
|
+
const name = candidate.name;
|
|
122
|
+
const location = candidate.in;
|
|
123
|
+
if (typeof name !== "string" || name.trim().length === 0) return null;
|
|
124
|
+
if (typeof location !== "string") return null;
|
|
125
|
+
if (!PARAMETER_LOCATION_SET.has(location)) return null;
|
|
126
|
+
const hasSchema = isPlainObject(candidate.schema);
|
|
127
|
+
const hasContent = isPlainObject(candidate.content) && Object.keys(candidate.content).length > 0;
|
|
128
|
+
if (hasSchema && hasContent) return null;
|
|
129
|
+
const trimmedName = name.trim();
|
|
130
|
+
const required = location === "path" ? true : candidate.required === true;
|
|
131
|
+
const normalized = {
|
|
132
|
+
name: trimmedName,
|
|
133
|
+
in: location,
|
|
134
|
+
required,
|
|
135
|
+
raw: candidate
|
|
136
|
+
};
|
|
137
|
+
if (location === "header" && RESERVED_HEADER_NAMES.has(trimmedName.toLowerCase())) {
|
|
138
|
+
normalized.reserved = true;
|
|
139
|
+
}
|
|
140
|
+
if (typeof candidate.description === "string") {
|
|
141
|
+
normalized.description = candidate.description;
|
|
142
|
+
}
|
|
143
|
+
if (typeof candidate.deprecated === "boolean") {
|
|
144
|
+
normalized.deprecated = candidate.deprecated;
|
|
145
|
+
}
|
|
146
|
+
if (typeof candidate.style === "string") normalized.style = candidate.style;
|
|
147
|
+
if (typeof candidate.explode === "boolean") {
|
|
148
|
+
normalized.explode = candidate.explode;
|
|
149
|
+
}
|
|
150
|
+
if (typeof candidate.allowReserved === "boolean") {
|
|
151
|
+
normalized.allowReserved = candidate.allowReserved;
|
|
152
|
+
}
|
|
153
|
+
if (typeof candidate.allowEmptyValue === "boolean") {
|
|
154
|
+
normalized.allowEmptyValue = candidate.allowEmptyValue;
|
|
155
|
+
}
|
|
156
|
+
if (hasSchema) {
|
|
157
|
+
normalized.schema = candidate.schema;
|
|
158
|
+
}
|
|
159
|
+
if (hasContent) {
|
|
160
|
+
const content = candidate.content;
|
|
161
|
+
normalized.content = content;
|
|
162
|
+
const mediaType = selectContentMediaType(content);
|
|
163
|
+
if (mediaType) normalized.contentMediaType = mediaType;
|
|
164
|
+
}
|
|
165
|
+
return normalized;
|
|
166
|
+
}
|
|
167
|
+
function effectiveStyle(parameter) {
|
|
168
|
+
if (parameter.style) return parameter.style;
|
|
169
|
+
switch (parameter.in) {
|
|
170
|
+
case "query":
|
|
171
|
+
case "cookie":
|
|
172
|
+
return "form";
|
|
173
|
+
case "path":
|
|
174
|
+
case "header":
|
|
175
|
+
default:
|
|
176
|
+
return "simple";
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
function effectiveExplode(parameter) {
|
|
180
|
+
if (typeof parameter.explode === "boolean") return parameter.explode;
|
|
181
|
+
return effectiveStyle(parameter) === "form";
|
|
182
|
+
}
|
|
183
|
+
function collectOperationParameters(pathItem, operation, issues, context) {
|
|
184
|
+
const merged = /* @__PURE__ */ new Map();
|
|
185
|
+
const contextPath = context?.path ?? "";
|
|
186
|
+
const contextMethod = context?.method;
|
|
187
|
+
const absorb = (list, origin) => {
|
|
188
|
+
if (list === void 0 || list === null) return;
|
|
189
|
+
if (!Array.isArray(list)) {
|
|
190
|
+
report(issues, {
|
|
191
|
+
path: contextPath,
|
|
192
|
+
method: contextMethod,
|
|
193
|
+
code: "invalid-parameter",
|
|
194
|
+
message: `The ${origin} "parameters" field is not an array and was ignored.`
|
|
195
|
+
});
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
for (const entry of list) {
|
|
199
|
+
const normalized = normalizeParameter(entry);
|
|
200
|
+
if (!normalized) {
|
|
201
|
+
const location = isPlainObject(entry) ? entry.in : void 0;
|
|
202
|
+
if (typeof location === "string" && KNOWN_UNSUPPORTED_LOCATIONS.has(location)) {
|
|
203
|
+
report(issues, {
|
|
204
|
+
path: contextPath,
|
|
205
|
+
method: contextMethod,
|
|
206
|
+
code: "unsupported-parameter-location",
|
|
207
|
+
message: `Parameter location "${location}" is not supported and was skipped.`
|
|
208
|
+
});
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
report(issues, {
|
|
212
|
+
path: contextPath,
|
|
213
|
+
method: contextMethod,
|
|
214
|
+
code: isReference(entry) ? "unresolved-ref" : "invalid-parameter",
|
|
215
|
+
message: `Skipped an unusable ${origin} parameter entry.`
|
|
216
|
+
});
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
if (normalized.reserved) {
|
|
220
|
+
report(issues, {
|
|
221
|
+
path: contextPath,
|
|
222
|
+
method: contextMethod,
|
|
223
|
+
code: "reserved-parameter",
|
|
224
|
+
message: `Header parameter "${normalized.name}" is managed by the transport and will not be exposed as an argument.`
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
merged.set(`${normalized.in}:${normalized.name}`, normalized);
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
absorb(pathItem?.parameters, "path-item");
|
|
231
|
+
absorb(operation?.parameters, "operation");
|
|
232
|
+
return [...merged.values()];
|
|
233
|
+
}
|
|
234
|
+
function synthesizeOperationId(method, pathTemplate) {
|
|
235
|
+
const segments = pathTemplate.split("/").map((segment) => segment.trim()).filter(Boolean).map((segment) => {
|
|
236
|
+
const templated = segment.match(/^\{(.+)\}$/);
|
|
237
|
+
if (templated) return `by_${templated[1]}`;
|
|
238
|
+
return segment;
|
|
239
|
+
}).map((segment) => segment.replace(/[^A-Za-z0-9]+/g, "_")).map((segment) => segment.replace(/^_+|_+$/g, "")).filter(Boolean);
|
|
240
|
+
const safeMethod = method.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "") || "call";
|
|
241
|
+
const base = [safeMethod, ...segments].join("_");
|
|
242
|
+
const cleaned = base.replace(/_{2,}/g, "_").replace(/^_+|_+$/g, "");
|
|
243
|
+
return cleaned || `${safeMethod}_root`;
|
|
244
|
+
}
|
|
245
|
+
function ensureUniqueName(candidate, taken) {
|
|
246
|
+
if (!taken.has(candidate)) {
|
|
247
|
+
taken.add(candidate);
|
|
248
|
+
return candidate;
|
|
249
|
+
}
|
|
250
|
+
let counter = 2;
|
|
251
|
+
let next = `${candidate}_${counter}`;
|
|
252
|
+
while (taken.has(next)) {
|
|
253
|
+
counter += 1;
|
|
254
|
+
next = `${candidate}_${counter}`;
|
|
255
|
+
}
|
|
256
|
+
taken.add(next);
|
|
257
|
+
return next;
|
|
258
|
+
}
|
|
259
|
+
function readAdditionalOperations(pathItem) {
|
|
260
|
+
const extra = pathItem.additionalOperations;
|
|
261
|
+
if (!isPlainObject(extra)) return [];
|
|
262
|
+
return Object.entries(extra);
|
|
263
|
+
}
|
|
264
|
+
function* iterateOperations(spec, issues) {
|
|
265
|
+
if (!spec) return;
|
|
266
|
+
if (spec.paths === void 0 || spec.paths === null) return;
|
|
267
|
+
if (!isPlainObject(spec.paths)) {
|
|
268
|
+
report(issues, {
|
|
269
|
+
path: "",
|
|
270
|
+
code: "invalid-paths-object",
|
|
271
|
+
message: `The document's "paths" field is not an object.`
|
|
272
|
+
});
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
const usedIds = /* @__PURE__ */ new Set();
|
|
276
|
+
for (const [pathTemplate, rawPathItem] of Object.entries(spec.paths)) {
|
|
277
|
+
if (pathTemplate.startsWith("x-")) continue;
|
|
278
|
+
if (!pathTemplate.startsWith("/")) {
|
|
279
|
+
report(issues, {
|
|
280
|
+
path: pathTemplate,
|
|
281
|
+
code: "invalid-path-template",
|
|
282
|
+
message: 'Path templates must start with "/"; entry was skipped.'
|
|
283
|
+
});
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
if (!isPlainObject(rawPathItem)) {
|
|
287
|
+
report(issues, {
|
|
288
|
+
path: pathTemplate,
|
|
289
|
+
code: "invalid-path-item",
|
|
290
|
+
message: "Path item is not an object and was skipped."
|
|
291
|
+
});
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
if (isReference(rawPathItem)) {
|
|
295
|
+
report(issues, {
|
|
296
|
+
path: pathTemplate,
|
|
297
|
+
code: "unresolved-ref",
|
|
298
|
+
message: "Path item still contains an unresolved $ref and was skipped."
|
|
299
|
+
});
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
const pathItem = rawPathItem;
|
|
303
|
+
const candidates = [];
|
|
304
|
+
for (const [key, value] of Object.entries(rawPathItem)) {
|
|
305
|
+
if (key.startsWith("x-")) continue;
|
|
306
|
+
if (PATH_ITEM_METADATA_KEYS.has(key)) continue;
|
|
307
|
+
if (!HTTP_METHOD_SET.has(key.toLowerCase())) continue;
|
|
308
|
+
candidates.push({ key: key.toLowerCase(), value, standard: true });
|
|
309
|
+
}
|
|
310
|
+
for (const [key, value] of readAdditionalOperations(rawPathItem)) {
|
|
311
|
+
const method = key.trim().toLowerCase();
|
|
312
|
+
if (!method) continue;
|
|
313
|
+
if (candidates.some((candidate) => candidate.key === method)) continue;
|
|
314
|
+
candidates.push({ key: method, value, standard: false });
|
|
315
|
+
}
|
|
316
|
+
for (const { key: method, value: rawOperation, standard } of candidates) {
|
|
317
|
+
if (!isPlainObject(rawOperation)) {
|
|
318
|
+
report(issues, {
|
|
319
|
+
path: pathTemplate,
|
|
320
|
+
method,
|
|
321
|
+
code: "invalid-operation",
|
|
322
|
+
message: "Operation is not an object and was skipped."
|
|
323
|
+
});
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
if (isReference(rawOperation)) {
|
|
327
|
+
report(issues, {
|
|
328
|
+
path: pathTemplate,
|
|
329
|
+
method,
|
|
330
|
+
code: "unresolved-ref",
|
|
331
|
+
message: "Operation still contains an unresolved $ref and was skipped."
|
|
332
|
+
});
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
const operation = rawOperation;
|
|
336
|
+
const declaredId = typeof operation.operationId === "string" ? operation.operationId.trim() : "";
|
|
337
|
+
let generated = false;
|
|
338
|
+
let candidate = declaredId;
|
|
339
|
+
if (!candidate) {
|
|
340
|
+
candidate = synthesizeOperationId(method, pathTemplate);
|
|
341
|
+
generated = true;
|
|
342
|
+
report(issues, {
|
|
343
|
+
path: pathTemplate,
|
|
344
|
+
method,
|
|
345
|
+
code: "generated-operation-id",
|
|
346
|
+
message: `Missing operationId; generated "${candidate}".`
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
const capped = truncateGeneratedName(candidate);
|
|
350
|
+
if (capped !== candidate) {
|
|
351
|
+
report(issues, {
|
|
352
|
+
path: pathTemplate,
|
|
353
|
+
method,
|
|
354
|
+
code: "truncated-operation-id",
|
|
355
|
+
message: `operationId "${candidate}" exceeded ${MAX_GENERATED_NAME_LENGTH} characters and was shortened to "${capped}".`
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
const operationId = ensureUniqueName(capped, usedIds);
|
|
359
|
+
if (operationId !== capped) {
|
|
360
|
+
report(issues, {
|
|
361
|
+
path: pathTemplate,
|
|
362
|
+
method,
|
|
363
|
+
code: "duplicate-operation-id",
|
|
364
|
+
message: `Duplicate operationId "${capped}"; renamed to "${operationId}".`
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
yield {
|
|
368
|
+
path: pathTemplate,
|
|
369
|
+
method,
|
|
370
|
+
operation,
|
|
371
|
+
pathItem,
|
|
372
|
+
operationId,
|
|
373
|
+
operationIdGenerated: generated,
|
|
374
|
+
isStandardMethod: standard
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
function listOperations(spec, issues) {
|
|
380
|
+
return [...iterateOperations(spec, issues)];
|
|
381
|
+
}
|
|
382
|
+
function declaredOperationId(entry) {
|
|
383
|
+
const value = entry.operation.operationId;
|
|
384
|
+
return typeof value === "string" ? value.trim() : "";
|
|
385
|
+
}
|
|
386
|
+
var indexCache = /* @__PURE__ */ new WeakMap();
|
|
387
|
+
function buildIndex(spec) {
|
|
388
|
+
const operations = listOperations(spec);
|
|
389
|
+
const byOperationId = /* @__PURE__ */ new Map();
|
|
390
|
+
for (const entry of operations) {
|
|
391
|
+
byOperationId.set(entry.operationId, entry);
|
|
392
|
+
}
|
|
393
|
+
const declaredCounts = /* @__PURE__ */ new Map();
|
|
394
|
+
for (const entry of operations) {
|
|
395
|
+
const declared = declaredOperationId(entry);
|
|
396
|
+
if (!declared) continue;
|
|
397
|
+
declaredCounts.set(declared, (declaredCounts.get(declared) ?? 0) + 1);
|
|
398
|
+
}
|
|
399
|
+
const byDeclaredId = /* @__PURE__ */ new Map();
|
|
400
|
+
for (const entry of operations) {
|
|
401
|
+
const declared = declaredOperationId(entry);
|
|
402
|
+
if (!declared) continue;
|
|
403
|
+
if (declaredCounts.get(declared) !== 1) continue;
|
|
404
|
+
if (byOperationId.has(declared)) continue;
|
|
405
|
+
byDeclaredId.set(declared, entry);
|
|
406
|
+
}
|
|
407
|
+
return { operations, byOperationId, byDeclaredId };
|
|
408
|
+
}
|
|
409
|
+
function getOperationIndex(spec) {
|
|
410
|
+
if (!spec) {
|
|
411
|
+
return {
|
|
412
|
+
operations: [],
|
|
413
|
+
byOperationId: /* @__PURE__ */ new Map(),
|
|
414
|
+
byDeclaredId: /* @__PURE__ */ new Map()
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
const cached = indexCache.get(spec);
|
|
418
|
+
if (cached) return cached;
|
|
419
|
+
const built = buildIndex(spec);
|
|
420
|
+
indexCache.set(spec, built);
|
|
421
|
+
return built;
|
|
422
|
+
}
|
|
423
|
+
function findOperationById(spec, operationId) {
|
|
424
|
+
if (typeof operationId !== "string") return null;
|
|
425
|
+
const target = operationId.trim();
|
|
426
|
+
if (!target) return null;
|
|
427
|
+
const index = getOperationIndex(spec);
|
|
428
|
+
return index.byOperationId.get(target) ?? index.byDeclaredId.get(target) ?? null;
|
|
429
|
+
}
|
|
430
|
+
function findDuplicateOperationIds(spec) {
|
|
431
|
+
const seen = /* @__PURE__ */ new Map();
|
|
432
|
+
for (const entry of getOperationIndex(spec).operations) {
|
|
433
|
+
const declared = declaredOperationId(entry);
|
|
434
|
+
if (!declared) continue;
|
|
435
|
+
const bucket = seen.get(declared);
|
|
436
|
+
if (bucket) {
|
|
437
|
+
bucket.push({ method: entry.method, path: entry.path });
|
|
438
|
+
} else {
|
|
439
|
+
seen.set(declared, [{ method: entry.method, path: entry.path }]);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
const duplicates = [];
|
|
443
|
+
for (const [operationId, occurrences] of seen) {
|
|
444
|
+
if (occurrences.length > 1) duplicates.push({ operationId, occurrences });
|
|
445
|
+
}
|
|
446
|
+
return duplicates;
|
|
447
|
+
}
|
|
448
|
+
function assertUniqueOperationIds(spec) {
|
|
449
|
+
const duplicates = findDuplicateOperationIds(spec);
|
|
450
|
+
if (duplicates.length === 0) return;
|
|
451
|
+
const detail = duplicates.map((duplicate) => {
|
|
452
|
+
const where = duplicate.occurrences.map((item) => `${item.method.toUpperCase()} ${item.path}`).join(", ");
|
|
453
|
+
return `"${duplicate.operationId}" (${where})`;
|
|
454
|
+
}).join("; ");
|
|
455
|
+
throw new Error(`Duplicate operationId values in specification: ${detail}`);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// src/core/openapi-loader.ts
|
|
459
|
+
function isPlainObject2(value) {
|
|
460
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
461
|
+
}
|
|
462
|
+
function parseRaw(rawText, isYaml) {
|
|
463
|
+
if (isYaml) {
|
|
464
|
+
try {
|
|
465
|
+
return jsYaml.load(rawText);
|
|
466
|
+
} catch (error) {
|
|
467
|
+
throw new Error(
|
|
468
|
+
`Failed to parse YAML: ${error instanceof Error ? error.message : String(error)}`
|
|
469
|
+
);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
try {
|
|
473
|
+
return JSON.parse(rawText);
|
|
474
|
+
} catch (error) {
|
|
475
|
+
throw new Error(
|
|
476
|
+
`Failed to parse JSON: ${error instanceof Error ? error.message : String(error)}`
|
|
477
|
+
);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
async function validateAndDereference(doc) {
|
|
481
|
+
if (!isPlainObject2(doc)) {
|
|
482
|
+
throw new Error("The OpenAPI document must be a JSON or YAML object.");
|
|
483
|
+
}
|
|
484
|
+
if (typeof doc.openapi !== "string" || !doc.openapi.trim()) {
|
|
485
|
+
throw new Error(
|
|
486
|
+
'The OpenAPI document must include a non-empty "openapi" field.'
|
|
487
|
+
);
|
|
488
|
+
}
|
|
489
|
+
if (!isPlainObject2(doc.info)) {
|
|
490
|
+
throw new Error('The OpenAPI document must include a valid "info" object.');
|
|
491
|
+
}
|
|
492
|
+
if (typeof doc.info.title !== "string" || !doc.info.title.trim()) {
|
|
493
|
+
throw new Error(
|
|
494
|
+
'The OpenAPI document must include a non-empty "info.title" value.'
|
|
495
|
+
);
|
|
496
|
+
}
|
|
497
|
+
if (typeof doc.info.version !== "string" || !doc.info.version.trim()) {
|
|
498
|
+
throw new Error(
|
|
499
|
+
'The OpenAPI document must include a non-empty "info.version" value.'
|
|
500
|
+
);
|
|
501
|
+
}
|
|
502
|
+
const validationResult = await openapiParser.validate(doc);
|
|
503
|
+
if (!validationResult.valid) {
|
|
504
|
+
const errors = (validationResult.errors ?? []).map((item) => typeof item === "string" ? item : JSON.stringify(item)).join("; ");
|
|
505
|
+
throw new Error(
|
|
506
|
+
`The OpenAPI document is invalid: ${errors || "Unknown validation error."}`
|
|
507
|
+
);
|
|
508
|
+
}
|
|
509
|
+
const dereferenced = await openapiParser.dereference(doc);
|
|
510
|
+
if (dereferenced.errors?.length) {
|
|
511
|
+
const errors = dereferenced.errors.map((item) => typeof item === "string" ? item : JSON.stringify(item)).join("; ");
|
|
512
|
+
throw new Error(`Failed to dereference the OpenAPI document: ${errors}`);
|
|
513
|
+
}
|
|
514
|
+
const schema = dereferenced.schema;
|
|
515
|
+
if (!schema.paths || Object.keys(schema.paths).length === 0) {
|
|
516
|
+
throw new Error("The OpenAPI document does not define any paths.");
|
|
517
|
+
}
|
|
518
|
+
assertUniqueOperationIds(schema);
|
|
519
|
+
return schema;
|
|
520
|
+
}
|
|
521
|
+
async function loadOpenApiSpec(filePath) {
|
|
522
|
+
const raw = fs2__default.default.readFileSync(filePath, "utf8");
|
|
523
|
+
return validateAndDereference(parseRaw(raw, /\.ya?ml$/i.test(filePath)));
|
|
524
|
+
}
|
|
525
|
+
async function parseSpecContent(rawText, isYaml) {
|
|
526
|
+
if (!rawText.trim()) {
|
|
527
|
+
throw new Error("The OpenAPI content is empty.");
|
|
528
|
+
}
|
|
529
|
+
return validateAndDereference(parseRaw(rawText, isYaml));
|
|
530
|
+
}
|
|
531
|
+
var BODY_PREFERENCE = [
|
|
532
|
+
{ test: (m) => m === "application/json", encoding: "json" },
|
|
533
|
+
{ test: (m) => /^application\/[\w.+-]+\+json$/.test(m), encoding: "json" },
|
|
534
|
+
{
|
|
535
|
+
test: (m) => m === "application/x-www-form-urlencoded",
|
|
536
|
+
encoding: "form-urlencoded"
|
|
537
|
+
},
|
|
538
|
+
{ test: (m) => m === "multipart/form-data", encoding: "multipart" },
|
|
539
|
+
{ test: (m) => m.startsWith("text/"), encoding: "text" },
|
|
540
|
+
{ test: (m) => m === "application/octet-stream", encoding: "binary" }
|
|
541
|
+
];
|
|
542
|
+
var BODY_KEY = "body";
|
|
543
|
+
var FALLBACK_BODY_KEY = "requestBody";
|
|
544
|
+
var BODYLESS_METHODS = /* @__PURE__ */ new Set(["get", "head", "trace"]);
|
|
545
|
+
var READ_ONLY_METHODS = /* @__PURE__ */ new Set([
|
|
546
|
+
"get",
|
|
547
|
+
"head",
|
|
548
|
+
"options",
|
|
549
|
+
"trace",
|
|
550
|
+
"query"
|
|
551
|
+
]);
|
|
552
|
+
var IDEMPOTENT_METHODS = /* @__PURE__ */ new Set([
|
|
553
|
+
"get",
|
|
554
|
+
"head",
|
|
555
|
+
"options",
|
|
556
|
+
"trace",
|
|
557
|
+
"put",
|
|
558
|
+
"delete",
|
|
559
|
+
"query"
|
|
560
|
+
]);
|
|
561
|
+
var MAX_DESCRIPTION_LENGTH = 1024;
|
|
562
|
+
function truncateText(text, limit) {
|
|
563
|
+
if (text.length <= limit) return text;
|
|
564
|
+
return `${text.slice(0, Math.max(0, limit - 1)).trimEnd()}\u2026`;
|
|
565
|
+
}
|
|
566
|
+
function stripMediaTypeParameters(mediaType) {
|
|
567
|
+
const semicolon = mediaType.indexOf(";");
|
|
568
|
+
const base = semicolon === -1 ? mediaType : mediaType.slice(0, semicolon);
|
|
569
|
+
return base.trim().toLowerCase();
|
|
570
|
+
}
|
|
571
|
+
function extractParameterSchema(parameter) {
|
|
572
|
+
if (parameter.schema) return parameter.schema;
|
|
573
|
+
if (parameter.content) {
|
|
574
|
+
const entries = Object.entries(parameter.content).filter(
|
|
575
|
+
([, media2]) => isPlainObject(media2)
|
|
576
|
+
);
|
|
577
|
+
const jsonEntry = entries.find(
|
|
578
|
+
([mediaType]) => /json/.test(stripMediaTypeParameters(mediaType))
|
|
579
|
+
) ?? entries[0];
|
|
580
|
+
if (!jsonEntry) return null;
|
|
581
|
+
const media = jsonEntry[1];
|
|
582
|
+
if (isPlainObject(media.schema)) {
|
|
583
|
+
return media.schema;
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
return null;
|
|
587
|
+
}
|
|
588
|
+
function stripReadOnly(schema, memo = /* @__PURE__ */ new WeakMap()) {
|
|
589
|
+
if (Array.isArray(schema)) {
|
|
590
|
+
const cached2 = memo.get(schema);
|
|
591
|
+
if (cached2 !== void 0) return cached2;
|
|
592
|
+
const output2 = [];
|
|
593
|
+
memo.set(schema, output2);
|
|
594
|
+
for (const entry of schema) output2.push(stripReadOnly(entry, memo));
|
|
595
|
+
return output2;
|
|
596
|
+
}
|
|
597
|
+
if (!isPlainObject(schema)) return schema;
|
|
598
|
+
const cached = memo.get(schema);
|
|
599
|
+
if (cached !== void 0) return cached;
|
|
600
|
+
const output = {};
|
|
601
|
+
memo.set(schema, output);
|
|
602
|
+
for (const [key, value] of Object.entries(schema)) {
|
|
603
|
+
if (key === "properties" && isPlainObject(value)) {
|
|
604
|
+
const properties = {};
|
|
605
|
+
for (const [propertyName, propertySchema] of Object.entries(value)) {
|
|
606
|
+
if (isPlainObject(propertySchema) && propertySchema.readOnly === true) {
|
|
607
|
+
continue;
|
|
608
|
+
}
|
|
609
|
+
properties[propertyName] = stripReadOnly(propertySchema, memo);
|
|
610
|
+
}
|
|
611
|
+
output.properties = properties;
|
|
612
|
+
continue;
|
|
613
|
+
}
|
|
614
|
+
if (key === "items" || key === "additionalProperties" || key === "not" || key === "allOf" || key === "anyOf" || key === "oneOf" || key === "prefixItems" || key === "patternProperties" || key === "$defs") {
|
|
615
|
+
output[key] = stripReadOnly(value, memo);
|
|
616
|
+
continue;
|
|
617
|
+
}
|
|
618
|
+
output[key] = value;
|
|
619
|
+
}
|
|
620
|
+
if (Array.isArray(output.required) && isPlainObject(output.properties)) {
|
|
621
|
+
const properties = output.properties;
|
|
622
|
+
const filtered = output.required.filter(
|
|
623
|
+
(entry) => typeof entry === "string" && entry in properties
|
|
624
|
+
);
|
|
625
|
+
if (filtered.length > 0) output.required = filtered;
|
|
626
|
+
else delete output.required;
|
|
627
|
+
}
|
|
628
|
+
return output;
|
|
629
|
+
}
|
|
630
|
+
function buildParameterSchema(parameter) {
|
|
631
|
+
const resolved = extractParameterSchema(parameter);
|
|
632
|
+
const base = resolved ? { ...stripReadOnly(resolved) } : { type: "string" };
|
|
633
|
+
const notes = [];
|
|
634
|
+
if (parameter.description) notes.push(parameter.description);
|
|
635
|
+
notes.push(`Sent as a ${parameter.in} parameter named "${parameter.name}".`);
|
|
636
|
+
if (parameter.deprecated) notes.push("This parameter is deprecated.");
|
|
637
|
+
const existingDescription = typeof base.description === "string" ? base.description : void 0;
|
|
638
|
+
const description = existingDescription ? [existingDescription, ...notes.slice(parameter.description ? 1 : 0)].filter(Boolean).join(" ") : notes.join(" ");
|
|
639
|
+
base.description = truncateText(description, MAX_DESCRIPTION_LENGTH);
|
|
640
|
+
return base;
|
|
641
|
+
}
|
|
642
|
+
function selectBodyMedia(body) {
|
|
643
|
+
if (!isPlainObject(body.content)) return null;
|
|
644
|
+
const candidates = Object.entries(body.content).filter(([, media]) => isPlainObject(media)).map(([mediaType, media]) => ({
|
|
645
|
+
mediaType,
|
|
646
|
+
normalized: stripMediaTypeParameters(mediaType),
|
|
647
|
+
media
|
|
648
|
+
}));
|
|
649
|
+
for (const preference of BODY_PREFERENCE) {
|
|
650
|
+
const hit = candidates.find(
|
|
651
|
+
(candidate) => preference.test(candidate.normalized)
|
|
652
|
+
);
|
|
653
|
+
if (hit) {
|
|
654
|
+
return {
|
|
655
|
+
mediaType: hit.mediaType,
|
|
656
|
+
media: hit.media,
|
|
657
|
+
encoding: preference.encoding
|
|
658
|
+
};
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
return null;
|
|
662
|
+
}
|
|
663
|
+
function buildAnnotations(method, isStandardMethod, title) {
|
|
664
|
+
const annotations = { title };
|
|
665
|
+
if (!isStandardMethod) {
|
|
666
|
+
annotations.readOnlyHint = false;
|
|
667
|
+
annotations.destructiveHint = true;
|
|
668
|
+
annotations.idempotentHint = false;
|
|
669
|
+
annotations.openWorldHint = true;
|
|
670
|
+
return annotations;
|
|
671
|
+
}
|
|
672
|
+
const readOnly = READ_ONLY_METHODS.has(method);
|
|
673
|
+
annotations.readOnlyHint = readOnly;
|
|
674
|
+
annotations.destructiveHint = readOnly ? false : method === "delete";
|
|
675
|
+
annotations.idempotentHint = IDEMPOTENT_METHODS.has(method);
|
|
676
|
+
annotations.openWorldHint = true;
|
|
677
|
+
return annotations;
|
|
678
|
+
}
|
|
679
|
+
function buildToolForOperation(toolName, path3, pathItem, method, isStandardMethod, operation, parameters) {
|
|
680
|
+
const properties = {};
|
|
681
|
+
const required = [];
|
|
682
|
+
const argumentBindings = [];
|
|
683
|
+
const degradationReasons = [];
|
|
684
|
+
const usageNotes = [];
|
|
685
|
+
const omittedParameters = [];
|
|
686
|
+
const usedKeys = /* @__PURE__ */ new Set();
|
|
687
|
+
const exposedParameters = [];
|
|
688
|
+
for (const parameter of parameters) {
|
|
689
|
+
if (parameter.reserved) {
|
|
690
|
+
omittedParameters.push({
|
|
691
|
+
name: parameter.name,
|
|
692
|
+
in: parameter.in,
|
|
693
|
+
reason: "Managed by the transport layer; supplied from the server configuration."
|
|
694
|
+
});
|
|
695
|
+
continue;
|
|
696
|
+
}
|
|
697
|
+
exposedParameters.push(parameter);
|
|
698
|
+
}
|
|
699
|
+
const bodyObject = operation.requestBody && !isReference(operation.requestBody) ? operation.requestBody : null;
|
|
700
|
+
const bodyAllowed = !BODYLESS_METHODS.has(method);
|
|
701
|
+
const bodySelection = bodyObject && bodyAllowed ? selectBodyMedia(bodyObject) : null;
|
|
702
|
+
if (bodyObject && !bodyAllowed) {
|
|
703
|
+
degradationReasons.push(
|
|
704
|
+
`The document declares a request body for ${method.toUpperCase()}, which cannot carry a payload; it was ignored.`
|
|
705
|
+
);
|
|
706
|
+
}
|
|
707
|
+
let bodyKey = null;
|
|
708
|
+
if (bodySelection) {
|
|
709
|
+
const clashes = exposedParameters.some(
|
|
710
|
+
(parameter) => parameter.name === BODY_KEY
|
|
711
|
+
);
|
|
712
|
+
bodyKey = clashes ? FALLBACK_BODY_KEY : BODY_KEY;
|
|
713
|
+
usedKeys.add(bodyKey);
|
|
714
|
+
}
|
|
715
|
+
for (const parameter of exposedParameters) {
|
|
716
|
+
let key = parameter.name;
|
|
717
|
+
if (usedKeys.has(key)) {
|
|
718
|
+
key = `${parameter.in}_${parameter.name}`;
|
|
719
|
+
}
|
|
720
|
+
key = ensureUniqueName(key, usedKeys);
|
|
721
|
+
if (!parameter.schema && !parameter.content) {
|
|
722
|
+
degradationReasons.push(
|
|
723
|
+
`Parameter "${parameter.name}" (${parameter.in}) has no schema; treated as a string.`
|
|
724
|
+
);
|
|
725
|
+
}
|
|
726
|
+
properties[key] = buildParameterSchema(parameter);
|
|
727
|
+
if (parameter.required) required.push(key);
|
|
728
|
+
argumentBindings.push({
|
|
729
|
+
key,
|
|
730
|
+
name: parameter.name,
|
|
731
|
+
in: parameter.in,
|
|
732
|
+
parameter
|
|
733
|
+
});
|
|
734
|
+
}
|
|
735
|
+
const declaredPathParameters = new Set(
|
|
736
|
+
exposedParameters.filter((parameter) => parameter.in === "path").map((parameter) => parameter.name)
|
|
737
|
+
);
|
|
738
|
+
for (const variable of extractPathTemplateVariables(path3)) {
|
|
739
|
+
if (!declaredPathParameters.has(variable)) {
|
|
740
|
+
degradationReasons.push(
|
|
741
|
+
`Path template variable "{${variable}}" has no matching path parameter definition.`
|
|
742
|
+
);
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
if (bodySelection && bodyKey) {
|
|
746
|
+
const rawSchema = isPlainObject(bodySelection.media.schema) ? stripReadOnly(bodySelection.media.schema) : null;
|
|
747
|
+
let bodySchema;
|
|
748
|
+
if (rawSchema) {
|
|
749
|
+
bodySchema = { ...rawSchema };
|
|
750
|
+
} else if (bodySelection.encoding === "binary") {
|
|
751
|
+
bodySchema = { type: "string", contentEncoding: "base64" };
|
|
752
|
+
} else if (bodySelection.encoding === "text") {
|
|
753
|
+
bodySchema = { type: "string" };
|
|
754
|
+
} else {
|
|
755
|
+
bodySchema = { type: "object", additionalProperties: true };
|
|
756
|
+
degradationReasons.push(
|
|
757
|
+
`Request body for "${bodySelection.mediaType}" has no schema; accepting a free-form object.`
|
|
758
|
+
);
|
|
759
|
+
}
|
|
760
|
+
if (bodySelection.encoding === "multipart") {
|
|
761
|
+
usageNotes.push(
|
|
762
|
+
"Multipart bodies are sent as encoded fields; binary file parts must be supplied as base64 strings."
|
|
763
|
+
);
|
|
764
|
+
}
|
|
765
|
+
const bodyDescription = [
|
|
766
|
+
bodyObject?.description,
|
|
767
|
+
`Request body encoded as ${bodySelection.mediaType}.`
|
|
768
|
+
].filter(Boolean).join(" ");
|
|
769
|
+
bodySchema.description = truncateText(
|
|
770
|
+
bodyDescription,
|
|
771
|
+
MAX_DESCRIPTION_LENGTH
|
|
772
|
+
);
|
|
773
|
+
properties[bodyKey] = bodySchema;
|
|
774
|
+
if (bodyObject?.required === true) required.push(bodyKey);
|
|
775
|
+
} else if (bodyObject && bodyAllowed) {
|
|
776
|
+
const available = isPlainObject(bodyObject.content) ? Object.keys(bodyObject.content).join(", ") || "none" : "none";
|
|
777
|
+
degradationReasons.push(
|
|
778
|
+
`No supported request body media type (available: ${available}).`
|
|
779
|
+
);
|
|
780
|
+
} else if (isReference(operation.requestBody)) {
|
|
781
|
+
degradationReasons.push(
|
|
782
|
+
"Request body still contains an unresolved $ref and was ignored."
|
|
783
|
+
);
|
|
784
|
+
}
|
|
785
|
+
const fullySupported = degradationReasons.length === 0;
|
|
786
|
+
const inputSchema = {
|
|
787
|
+
type: "object",
|
|
788
|
+
properties,
|
|
789
|
+
...required.length > 0 ? { required } : {},
|
|
790
|
+
additionalProperties: !fullySupported
|
|
791
|
+
};
|
|
792
|
+
const label = `${method.toUpperCase()} ${path3}`;
|
|
793
|
+
const descriptionParts = [operation.summary, operation.description].filter(
|
|
794
|
+
(entry) => typeof entry === "string" && entry.trim().length > 0
|
|
795
|
+
);
|
|
796
|
+
if (descriptionParts.length === 0) descriptionParts.push(label);
|
|
797
|
+
if (operation.deprecated === true) {
|
|
798
|
+
descriptionParts.push("[DEPRECATED]");
|
|
799
|
+
}
|
|
800
|
+
if (!isStandardMethod) {
|
|
801
|
+
descriptionParts.push(`[CUSTOM METHOD ${method.toUpperCase()}]`);
|
|
802
|
+
}
|
|
803
|
+
if (omittedParameters.length > 0) {
|
|
804
|
+
descriptionParts.push(
|
|
805
|
+
`[SERVER-MANAGED: ${omittedParameters.map((entry) => entry.name).join(", ")}]`
|
|
806
|
+
);
|
|
807
|
+
}
|
|
808
|
+
if (usageNotes.length > 0) {
|
|
809
|
+
descriptionParts.push(`[NOTE] ${usageNotes.join(" ")}`);
|
|
810
|
+
}
|
|
811
|
+
if (!fullySupported) {
|
|
812
|
+
descriptionParts.push(`[PARTIAL SUPPORT] ${degradationReasons.join(" ")}`);
|
|
813
|
+
}
|
|
814
|
+
const tool = {
|
|
815
|
+
name: toolName,
|
|
816
|
+
description: truncateText(
|
|
817
|
+
descriptionParts.join(" \u2014 "),
|
|
818
|
+
MAX_DESCRIPTION_LENGTH
|
|
819
|
+
),
|
|
820
|
+
inputSchema,
|
|
821
|
+
annotations: buildAnnotations(
|
|
822
|
+
method,
|
|
823
|
+
isStandardMethod,
|
|
824
|
+
operation.summary?.trim() || label
|
|
825
|
+
)
|
|
826
|
+
};
|
|
827
|
+
const binding = {
|
|
828
|
+
toolName,
|
|
829
|
+
path: path3,
|
|
830
|
+
pathItem,
|
|
831
|
+
method,
|
|
832
|
+
isStandardMethod,
|
|
833
|
+
operation,
|
|
834
|
+
arguments: argumentBindings,
|
|
835
|
+
bodyKey,
|
|
836
|
+
bodyEncoding: bodySelection?.encoding ?? null,
|
|
837
|
+
bodyMediaType: bodySelection?.mediaType ?? null,
|
|
838
|
+
bodyRequired: bodyObject?.required === true && bodySelection !== null,
|
|
839
|
+
omittedParameters,
|
|
840
|
+
fullySupported,
|
|
841
|
+
degradationReasons,
|
|
842
|
+
usageNotes
|
|
843
|
+
};
|
|
844
|
+
return { tool, binding };
|
|
845
|
+
}
|
|
846
|
+
function generateToolsDetailed(spec) {
|
|
847
|
+
const issues = [];
|
|
848
|
+
const tools = [];
|
|
849
|
+
const bindings = [];
|
|
850
|
+
const usedToolNames = /* @__PURE__ */ new Set();
|
|
851
|
+
for (const resolved of iterateOperations(spec, issues)) {
|
|
852
|
+
const parameters = collectOperationParameters(
|
|
853
|
+
resolved.pathItem,
|
|
854
|
+
resolved.operation,
|
|
855
|
+
issues,
|
|
856
|
+
{ path: resolved.path, method: resolved.method }
|
|
857
|
+
);
|
|
858
|
+
const toolName = ensureUniqueName(
|
|
859
|
+
truncateGeneratedName(resolved.operationId),
|
|
860
|
+
usedToolNames
|
|
861
|
+
);
|
|
862
|
+
const generated = buildToolForOperation(
|
|
863
|
+
toolName,
|
|
864
|
+
resolved.path,
|
|
865
|
+
resolved.pathItem,
|
|
866
|
+
resolved.method,
|
|
867
|
+
resolved.isStandardMethod,
|
|
868
|
+
resolved.operation,
|
|
869
|
+
parameters
|
|
870
|
+
);
|
|
871
|
+
tools.push(generated.tool);
|
|
872
|
+
bindings.push(generated.binding);
|
|
873
|
+
}
|
|
874
|
+
return { tools, bindings, issues };
|
|
875
|
+
}
|
|
876
|
+
function generateTools(spec) {
|
|
877
|
+
return generateToolsDetailed(spec).tools;
|
|
878
|
+
}
|
|
879
|
+
var resultCache = /* @__PURE__ */ new WeakMap();
|
|
880
|
+
var bindingIndexCache = /* @__PURE__ */ new WeakMap();
|
|
881
|
+
var EMPTY_RESULT = Object.freeze({
|
|
882
|
+
tools: [],
|
|
883
|
+
bindings: [],
|
|
884
|
+
issues: []
|
|
885
|
+
});
|
|
886
|
+
function getGeneratedTools(spec) {
|
|
887
|
+
if (!spec) return EMPTY_RESULT;
|
|
888
|
+
const cached = resultCache.get(spec);
|
|
889
|
+
if (cached) return cached;
|
|
890
|
+
const generated = generateToolsDetailed(spec);
|
|
891
|
+
resultCache.set(spec, generated);
|
|
892
|
+
return generated;
|
|
893
|
+
}
|
|
894
|
+
function getBindingIndex(spec) {
|
|
895
|
+
if (!spec) return /* @__PURE__ */ new Map();
|
|
896
|
+
const cached = bindingIndexCache.get(spec);
|
|
897
|
+
if (cached) return cached;
|
|
898
|
+
const index = /* @__PURE__ */ new Map();
|
|
899
|
+
for (const binding of getGeneratedTools(spec).bindings) {
|
|
900
|
+
index.set(binding.toolName, binding);
|
|
901
|
+
}
|
|
902
|
+
bindingIndexCache.set(spec, index);
|
|
903
|
+
return index;
|
|
904
|
+
}
|
|
905
|
+
var buildBindingIndex = getBindingIndex;
|
|
906
|
+
|
|
907
|
+
// src/registry/prompt-registry.ts
|
|
908
|
+
var MAX_PROMPT_NAME_LENGTH = 96;
|
|
909
|
+
var MAX_DESCRIPTION_LENGTH2 = 400;
|
|
910
|
+
var MAX_ARGS_TEXT_LENGTH = 4e3;
|
|
911
|
+
var MAX_OPERATIONS = 2e3;
|
|
912
|
+
var indexCache2 = /* @__PURE__ */ new WeakMap();
|
|
913
|
+
function sanitizeSegment(raw) {
|
|
914
|
+
const normalized = raw.normalize("NFKD").replace(/[^A-Za-z0-9]+/g, "_").replace(/_+/g, "_").replace(/^_+|_+$/g, "");
|
|
915
|
+
return normalized.length > 0 ? normalized : "operation";
|
|
916
|
+
}
|
|
917
|
+
function truncate(text, limit) {
|
|
918
|
+
if (text.length <= limit) return text;
|
|
919
|
+
return `${text.slice(0, Math.max(0, limit - 1)).trimEnd()}\u2026`;
|
|
920
|
+
}
|
|
921
|
+
function describe(primary, fallback) {
|
|
922
|
+
const candidate = typeof primary === "string" ? primary.trim() : "";
|
|
923
|
+
return truncate(
|
|
924
|
+
candidate.length > 0 ? candidate : fallback,
|
|
925
|
+
MAX_DESCRIPTION_LENGTH2
|
|
926
|
+
);
|
|
927
|
+
}
|
|
928
|
+
function safeSerializeArgs(args) {
|
|
929
|
+
const keys = Object.keys(args);
|
|
930
|
+
if (keys.length === 0) return "(none)";
|
|
931
|
+
const seen = /* @__PURE__ */ new WeakSet();
|
|
932
|
+
let serialized;
|
|
933
|
+
try {
|
|
934
|
+
serialized = JSON.stringify(
|
|
935
|
+
args,
|
|
936
|
+
(_key, value) => {
|
|
937
|
+
if (typeof value === "bigint") return value.toString();
|
|
938
|
+
if (typeof value === "function" || typeof value === "symbol") {
|
|
939
|
+
return `[${typeof value}]`;
|
|
940
|
+
}
|
|
941
|
+
if (value && typeof value === "object") {
|
|
942
|
+
if (seen.has(value)) return "[Circular]";
|
|
943
|
+
seen.add(value);
|
|
944
|
+
}
|
|
945
|
+
return value;
|
|
946
|
+
},
|
|
947
|
+
2
|
|
948
|
+
);
|
|
949
|
+
} catch {
|
|
950
|
+
return `(unserializable arguments: ${keys.join(", ")})`;
|
|
951
|
+
}
|
|
952
|
+
if (typeof serialized !== "string") return "(none)";
|
|
953
|
+
return truncate(serialized, MAX_ARGS_TEXT_LENGTH);
|
|
954
|
+
}
|
|
955
|
+
function userMessage(text) {
|
|
956
|
+
return { role: "user", content: { type: "text", text } };
|
|
957
|
+
}
|
|
958
|
+
function makeResult(description, text) {
|
|
959
|
+
return { description, messages: [userMessage(text)] };
|
|
960
|
+
}
|
|
961
|
+
function toPromptArguments(parameters) {
|
|
962
|
+
const result = [];
|
|
963
|
+
const used = /* @__PURE__ */ new Set();
|
|
964
|
+
for (const parameter of parameters) {
|
|
965
|
+
const rawName = typeof parameter.name === "string" ? parameter.name.trim() : "";
|
|
966
|
+
if (!rawName) continue;
|
|
967
|
+
let name = rawName;
|
|
968
|
+
if (used.has(name)) {
|
|
969
|
+
const location = typeof parameter.in === "string" ? parameter.in : "arg";
|
|
970
|
+
name = `${rawName}_${location}`;
|
|
971
|
+
let suffix = 2;
|
|
972
|
+
while (used.has(name)) {
|
|
973
|
+
name = `${rawName}_${location}_${suffix}`;
|
|
974
|
+
suffix += 1;
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
used.add(name);
|
|
978
|
+
result.push({
|
|
979
|
+
name,
|
|
980
|
+
description: describe(
|
|
981
|
+
parameter.description,
|
|
982
|
+
`Input for the ${parameter.in ?? "unknown"} parameter "${rawName}".`
|
|
983
|
+
),
|
|
984
|
+
// Sample generation must never hard-fail on a missing hint.
|
|
985
|
+
required: false
|
|
986
|
+
});
|
|
987
|
+
}
|
|
988
|
+
return result;
|
|
989
|
+
}
|
|
990
|
+
function buildIndex2(spec) {
|
|
991
|
+
const ordered = [];
|
|
992
|
+
const byName = /* @__PURE__ */ new Map();
|
|
993
|
+
const title = spec.info?.title?.trim() || "Untitled API";
|
|
994
|
+
const version = spec.info?.version?.trim() || "unknown";
|
|
995
|
+
const register = (prompt, build) => {
|
|
996
|
+
const generated = prompt;
|
|
997
|
+
if (byName.has(generated.name)) return;
|
|
998
|
+
byName.set(generated.name, { prompt: generated, build });
|
|
999
|
+
ordered.push(generated);
|
|
1000
|
+
};
|
|
1001
|
+
const operations = [];
|
|
1002
|
+
let truncatedOperations = false;
|
|
1003
|
+
for (const entry of iterateOperations(spec)) {
|
|
1004
|
+
if (operations.length >= MAX_OPERATIONS) {
|
|
1005
|
+
truncatedOperations = true;
|
|
1006
|
+
break;
|
|
1007
|
+
}
|
|
1008
|
+
const method = String(entry.method ?? "get").toUpperCase();
|
|
1009
|
+
const parameters = collectOperationParameters(
|
|
1010
|
+
entry.pathItem,
|
|
1011
|
+
entry.operation
|
|
1012
|
+
);
|
|
1013
|
+
const args = toPromptArguments(parameters);
|
|
1014
|
+
operations.push({
|
|
1015
|
+
path: entry.path,
|
|
1016
|
+
method,
|
|
1017
|
+
operationId: entry.operationId,
|
|
1018
|
+
summary: entry.operation.summary?.trim() || `${method} ${entry.path}`,
|
|
1019
|
+
description: entry.operation.description,
|
|
1020
|
+
deprecated: entry.operation.deprecated === true,
|
|
1021
|
+
args,
|
|
1022
|
+
parameterHints: parameters.length > 0 ? parameters.map(
|
|
1023
|
+
(parameter) => `- ${parameter.name} (${parameter.in}${parameter.required ? ", required" : ""})`
|
|
1024
|
+
).join("\n") : "- (this operation declares no parameters)"
|
|
1025
|
+
});
|
|
1026
|
+
}
|
|
1027
|
+
register(
|
|
1028
|
+
{
|
|
1029
|
+
name: "spec_overview",
|
|
1030
|
+
description: `Summarize the scope, conventions and major capabilities of "${truncate(title, 60)}".`,
|
|
1031
|
+
arguments: []
|
|
1032
|
+
},
|
|
1033
|
+
() => makeResult(
|
|
1034
|
+
"High-level orientation for the loaded API specification.",
|
|
1035
|
+
[
|
|
1036
|
+
`Summarize the API "${title}" (version "${version}").`,
|
|
1037
|
+
`It exposes ${operations.length} callable operation(s)${truncatedOperations ? " (list truncated for size)" : ""}.`,
|
|
1038
|
+
"Cover: authentication requirements, the main resource groups, naming conventions,",
|
|
1039
|
+
"pagination style, and any caveats a client should know before integrating."
|
|
1040
|
+
].join("\n")
|
|
1041
|
+
)
|
|
1042
|
+
);
|
|
1043
|
+
register(
|
|
1044
|
+
{
|
|
1045
|
+
name: "integration_guardrails",
|
|
1046
|
+
description: "Describe how to call this API safely and incrementally.",
|
|
1047
|
+
arguments: []
|
|
1048
|
+
},
|
|
1049
|
+
() => makeResult(
|
|
1050
|
+
"Conservative integration guidance for the loaded API specification.",
|
|
1051
|
+
[
|
|
1052
|
+
`Provide conservative integration guidance for "${title}" (version "${version}").`,
|
|
1053
|
+
"Address: retry and backoff policy, idempotency of mutating calls, pagination expectations,",
|
|
1054
|
+
"rate-limit handling, error-response interpretation, and data-safety or PII considerations.",
|
|
1055
|
+
"Prefer read-only verification steps before recommending any destructive call."
|
|
1056
|
+
].join("\n")
|
|
1057
|
+
)
|
|
1058
|
+
);
|
|
1059
|
+
const usedOperationSlugs = /* @__PURE__ */ new Set();
|
|
1060
|
+
for (const operation of operations) {
|
|
1061
|
+
let slug = truncate(
|
|
1062
|
+
sanitizeSegment(operation.operationId),
|
|
1063
|
+
MAX_PROMPT_NAME_LENGTH - 12
|
|
1064
|
+
).replace(/…$/, "");
|
|
1065
|
+
if (usedOperationSlugs.has(slug)) {
|
|
1066
|
+
let suffix = 2;
|
|
1067
|
+
while (usedOperationSlugs.has(`${slug}_${suffix}`)) suffix += 1;
|
|
1068
|
+
slug = `${slug}_${suffix}`;
|
|
1069
|
+
}
|
|
1070
|
+
usedOperationSlugs.add(slug);
|
|
1071
|
+
const label = `${operation.method} ${operation.path}`;
|
|
1072
|
+
const deprecationNote = operation.deprecated ? "\nNote: this operation is marked deprecated in the specification; mention safer alternatives if any exist." : "";
|
|
1073
|
+
register(
|
|
1074
|
+
{
|
|
1075
|
+
name: `explain_${slug}`,
|
|
1076
|
+
description: describe(
|
|
1077
|
+
operation.description,
|
|
1078
|
+
`Explain how to use ${label}.`
|
|
1079
|
+
),
|
|
1080
|
+
arguments: []
|
|
1081
|
+
},
|
|
1082
|
+
() => makeResult(
|
|
1083
|
+
`Explanation of ${label}.`,
|
|
1084
|
+
[
|
|
1085
|
+
`Explain the operation "${operation.operationId}" (${label}).`,
|
|
1086
|
+
`Summary: ${operation.summary}`,
|
|
1087
|
+
"Describe required inputs, optional inputs, request body rules, authentication needs,",
|
|
1088
|
+
"and the likely success and error response shapes.",
|
|
1089
|
+
"Parameters:",
|
|
1090
|
+
operation.parameterHints
|
|
1091
|
+
].join("\n") + deprecationNote
|
|
1092
|
+
)
|
|
1093
|
+
);
|
|
1094
|
+
register(
|
|
1095
|
+
{
|
|
1096
|
+
name: `plan_${slug}`,
|
|
1097
|
+
description: `Create an execution plan for ${label}.`,
|
|
1098
|
+
arguments: []
|
|
1099
|
+
},
|
|
1100
|
+
() => makeResult(
|
|
1101
|
+
`Execution plan for ${label}.`,
|
|
1102
|
+
[
|
|
1103
|
+
`Create a step-by-step execution plan for "${operation.operationId}" (${label}).`,
|
|
1104
|
+
"List every required parameter, any preconditions or prerequisite calls,",
|
|
1105
|
+
"the exact call to make, and how to validate the response before proceeding.",
|
|
1106
|
+
"Parameters:",
|
|
1107
|
+
operation.parameterHints
|
|
1108
|
+
].join("\n") + deprecationNote
|
|
1109
|
+
)
|
|
1110
|
+
);
|
|
1111
|
+
register(
|
|
1112
|
+
{
|
|
1113
|
+
name: `sample_${slug}`,
|
|
1114
|
+
description: `Generate realistic sample arguments for ${label}.`,
|
|
1115
|
+
arguments: operation.args
|
|
1116
|
+
},
|
|
1117
|
+
(args) => makeResult(
|
|
1118
|
+
`Sample arguments for ${label}.`,
|
|
1119
|
+
[
|
|
1120
|
+
`Generate realistic sample arguments for "${operation.operationId}" (${label}).`,
|
|
1121
|
+
"Use production-like placeholders that satisfy the declared types and formats.",
|
|
1122
|
+
"Do not invent real credentials, tokens, or personal data.",
|
|
1123
|
+
"Parameters:",
|
|
1124
|
+
operation.parameterHints,
|
|
1125
|
+
"Values already provided by the caller:",
|
|
1126
|
+
safeSerializeArgs(args)
|
|
1127
|
+
].join("\n")
|
|
1128
|
+
)
|
|
1129
|
+
);
|
|
1130
|
+
}
|
|
1131
|
+
return { ordered, byName };
|
|
1132
|
+
}
|
|
1133
|
+
function getIndex(spec) {
|
|
1134
|
+
const cached = indexCache2.get(spec);
|
|
1135
|
+
if (cached) return cached;
|
|
1136
|
+
const built = buildIndex2(spec);
|
|
1137
|
+
indexCache2.set(spec, built);
|
|
1138
|
+
return built;
|
|
1139
|
+
}
|
|
1140
|
+
function generatePrompts(spec) {
|
|
1141
|
+
return getIndex(spec).ordered.slice();
|
|
1142
|
+
}
|
|
1143
|
+
function resolvePrompt(spec, name, args = {}) {
|
|
1144
|
+
if (typeof name !== "string" || name.length === 0) return null;
|
|
1145
|
+
const entry = getIndex(spec).byName.get(name);
|
|
1146
|
+
if (!entry) return null;
|
|
1147
|
+
const safeArgs = args && typeof args === "object" && !Array.isArray(args) ? args : {};
|
|
1148
|
+
try {
|
|
1149
|
+
return entry.build(safeArgs);
|
|
1150
|
+
} catch (error) {
|
|
1151
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1152
|
+
return makeResult(
|
|
1153
|
+
entry.prompt.description ?? name,
|
|
1154
|
+
`The prompt "${name}" could not be fully rendered from the specification (${message}). Explain what you can from the surrounding API context.`
|
|
1155
|
+
);
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
// src/registry/resource-registry.ts
|
|
1160
|
+
function generateResources(spec) {
|
|
1161
|
+
return [
|
|
1162
|
+
{
|
|
1163
|
+
uri: "openapi://spec/summary",
|
|
1164
|
+
name: "OpenAPI Spec Summary",
|
|
1165
|
+
description: "High-level API metadata and counts.",
|
|
1166
|
+
mimeType: "application/json"
|
|
1167
|
+
},
|
|
1168
|
+
{
|
|
1169
|
+
uri: "openapi://spec/raw-info",
|
|
1170
|
+
name: "OpenAPI Info Object",
|
|
1171
|
+
description: "The OpenAPI info metadata.",
|
|
1172
|
+
mimeType: "application/json"
|
|
1173
|
+
},
|
|
1174
|
+
{
|
|
1175
|
+
uri: "openapi://catalog/tools",
|
|
1176
|
+
name: "Tool Catalog",
|
|
1177
|
+
description: "The generated MCP tool catalog.",
|
|
1178
|
+
mimeType: "application/json"
|
|
1179
|
+
},
|
|
1180
|
+
{
|
|
1181
|
+
uri: "openapi://catalog/prompts",
|
|
1182
|
+
name: "Prompt Catalog",
|
|
1183
|
+
description: "The generated MCP prompt catalog.",
|
|
1184
|
+
mimeType: "application/json"
|
|
1185
|
+
},
|
|
1186
|
+
{
|
|
1187
|
+
uri: "openapi://catalog/resources",
|
|
1188
|
+
name: "Resource Catalog",
|
|
1189
|
+
description: "The generated MCP resource catalog.",
|
|
1190
|
+
mimeType: "application/json"
|
|
1191
|
+
},
|
|
1192
|
+
{
|
|
1193
|
+
uri: "openapi://catalog/operations",
|
|
1194
|
+
name: "Operation Catalog",
|
|
1195
|
+
description: "The full operation catalog.",
|
|
1196
|
+
mimeType: "application/json"
|
|
1197
|
+
},
|
|
1198
|
+
{
|
|
1199
|
+
uri: "openapi://spec/document",
|
|
1200
|
+
name: "OpenAPI Document",
|
|
1201
|
+
description: "The dereferenced OpenAPI document.",
|
|
1202
|
+
mimeType: "application/json"
|
|
1203
|
+
}
|
|
1204
|
+
];
|
|
1205
|
+
}
|
|
1206
|
+
function readResource(spec, uri) {
|
|
1207
|
+
if (uri === "openapi://spec/summary") {
|
|
1208
|
+
return {
|
|
1209
|
+
uri,
|
|
1210
|
+
mimeType: "application/json",
|
|
1211
|
+
text: JSON.stringify(
|
|
1212
|
+
{
|
|
1213
|
+
title: spec.info?.title ?? null,
|
|
1214
|
+
version: spec.info?.version ?? null,
|
|
1215
|
+
openapi: spec.openapi,
|
|
1216
|
+
servers: spec.servers ?? [],
|
|
1217
|
+
operationCount: Array.from(iterateOperations(spec)).length
|
|
1218
|
+
},
|
|
1219
|
+
null,
|
|
1220
|
+
2
|
|
1221
|
+
)
|
|
1222
|
+
};
|
|
1223
|
+
}
|
|
1224
|
+
if (uri === "openapi://spec/raw-info") {
|
|
1225
|
+
return {
|
|
1226
|
+
uri,
|
|
1227
|
+
mimeType: "application/json",
|
|
1228
|
+
text: JSON.stringify(spec.info ?? {}, null, 2)
|
|
1229
|
+
};
|
|
1230
|
+
}
|
|
1231
|
+
if (uri === "openapi://catalog/tools") {
|
|
1232
|
+
return {
|
|
1233
|
+
uri,
|
|
1234
|
+
mimeType: "application/json",
|
|
1235
|
+
text: JSON.stringify(generateToolsDetailed(spec), null, 2)
|
|
1236
|
+
};
|
|
1237
|
+
}
|
|
1238
|
+
if (uri === "openapi://catalog/prompts") {
|
|
1239
|
+
return {
|
|
1240
|
+
uri,
|
|
1241
|
+
mimeType: "application/json",
|
|
1242
|
+
text: JSON.stringify({ prompts: generatePrompts(spec) }, null, 2)
|
|
1243
|
+
};
|
|
1244
|
+
}
|
|
1245
|
+
if (uri === "openapi://catalog/resources") {
|
|
1246
|
+
return {
|
|
1247
|
+
uri,
|
|
1248
|
+
mimeType: "application/json",
|
|
1249
|
+
text: JSON.stringify({ resources: generateResources() }, null, 2)
|
|
1250
|
+
};
|
|
1251
|
+
}
|
|
1252
|
+
if (uri === "openapi://catalog/operations") {
|
|
1253
|
+
return {
|
|
1254
|
+
uri,
|
|
1255
|
+
mimeType: "application/json",
|
|
1256
|
+
text: JSON.stringify(
|
|
1257
|
+
Array.from(iterateOperations(spec)).map((item) => ({
|
|
1258
|
+
operationId: item.operationId,
|
|
1259
|
+
method: item.method,
|
|
1260
|
+
path: item.path,
|
|
1261
|
+
summary: item.operation.summary ?? null,
|
|
1262
|
+
description: item.operation.description ?? null
|
|
1263
|
+
})),
|
|
1264
|
+
null,
|
|
1265
|
+
2
|
|
1266
|
+
)
|
|
1267
|
+
};
|
|
1268
|
+
}
|
|
1269
|
+
if (uri === "openapi://spec/document") {
|
|
1270
|
+
return {
|
|
1271
|
+
uri,
|
|
1272
|
+
mimeType: "application/json",
|
|
1273
|
+
text: JSON.stringify(spec, null, 2)
|
|
1274
|
+
};
|
|
1275
|
+
}
|
|
1276
|
+
return null;
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
// src/core/request-builder.ts
|
|
1280
|
+
var RESERVED_HEADERS = /* @__PURE__ */ new Set([
|
|
1281
|
+
"host",
|
|
1282
|
+
"content-length",
|
|
1283
|
+
"content-type",
|
|
1284
|
+
"transfer-encoding",
|
|
1285
|
+
"connection",
|
|
1286
|
+
"keep-alive",
|
|
1287
|
+
"upgrade",
|
|
1288
|
+
"te",
|
|
1289
|
+
"trailer",
|
|
1290
|
+
"expect",
|
|
1291
|
+
"authorization",
|
|
1292
|
+
"proxy-authorization",
|
|
1293
|
+
"proxy-connection",
|
|
1294
|
+
"accept-encoding",
|
|
1295
|
+
"cookie"
|
|
1296
|
+
]);
|
|
1297
|
+
function isPlainObject3(value) {
|
|
1298
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1299
|
+
}
|
|
1300
|
+
function renderScalar(value) {
|
|
1301
|
+
if (value === null || value === void 0) return "";
|
|
1302
|
+
if (typeof value === "string") return value;
|
|
1303
|
+
if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") {
|
|
1304
|
+
return String(value);
|
|
1305
|
+
}
|
|
1306
|
+
if (value instanceof Date) return value.toISOString();
|
|
1307
|
+
return String(value);
|
|
1308
|
+
}
|
|
1309
|
+
function encodeComponent(value) {
|
|
1310
|
+
return encodeURIComponent(value);
|
|
1311
|
+
}
|
|
1312
|
+
function isEmptyValue(value) {
|
|
1313
|
+
return value === void 0 || value === null || value === "";
|
|
1314
|
+
}
|
|
1315
|
+
function serializePathParameter(name, value, style, explode) {
|
|
1316
|
+
const encodedName = encodeComponent(name);
|
|
1317
|
+
if (Array.isArray(value)) {
|
|
1318
|
+
const parts = value.filter((entry) => entry !== void 0 && entry !== null).map((entry) => encodeComponent(renderScalar(entry)));
|
|
1319
|
+
switch (style) {
|
|
1320
|
+
case "label":
|
|
1321
|
+
return explode ? `.${parts.join(".")}` : `.${parts.join(",")}`;
|
|
1322
|
+
case "matrix":
|
|
1323
|
+
return explode ? parts.map((part) => `;${encodedName}=${part}`).join("") : `;${encodedName}=${parts.join(",")}`;
|
|
1324
|
+
default:
|
|
1325
|
+
return parts.join(",");
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
if (isPlainObject3(value)) {
|
|
1329
|
+
const entries = Object.entries(value).filter(
|
|
1330
|
+
([, entryValue]) => entryValue !== void 0 && entryValue !== null
|
|
1331
|
+
);
|
|
1332
|
+
const flat = entries.flatMap(([key, entryValue]) => [
|
|
1333
|
+
encodeComponent(key),
|
|
1334
|
+
encodeComponent(renderScalar(entryValue))
|
|
1335
|
+
]);
|
|
1336
|
+
const pairs = entries.map(
|
|
1337
|
+
([key, entryValue]) => `${encodeComponent(key)}=${encodeComponent(renderScalar(entryValue))}`
|
|
1338
|
+
);
|
|
1339
|
+
switch (style) {
|
|
1340
|
+
case "label":
|
|
1341
|
+
return explode ? `.${pairs.join(".")}` : `.${flat.join(",")}`;
|
|
1342
|
+
case "matrix":
|
|
1343
|
+
return explode ? `;${pairs.join(";")}` : `;${encodedName}=${flat.join(",")}`;
|
|
1344
|
+
default:
|
|
1345
|
+
return explode ? pairs.join(",") : flat.join(",");
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
const scalar = encodeComponent(renderScalar(value));
|
|
1349
|
+
switch (style) {
|
|
1350
|
+
case "label":
|
|
1351
|
+
return `.${scalar}`;
|
|
1352
|
+
case "matrix":
|
|
1353
|
+
return `;${encodedName}=${scalar}`;
|
|
1354
|
+
default:
|
|
1355
|
+
return scalar;
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
function serializeQueryParameter(name, value, style, explode) {
|
|
1359
|
+
if (Array.isArray(value)) {
|
|
1360
|
+
const parts = value.filter((entry) => entry !== void 0 && entry !== null).map((entry) => renderScalar(entry));
|
|
1361
|
+
if (parts.length === 0) return explode ? [] : [[name, ""]];
|
|
1362
|
+
if (explode) return parts.map((part) => [name, part]);
|
|
1363
|
+
if (style === "spaceDelimited") return [[name, parts.join(" ")]];
|
|
1364
|
+
if (style === "pipeDelimited") return [[name, parts.join("|")]];
|
|
1365
|
+
return [[name, parts.join(",")]];
|
|
1366
|
+
}
|
|
1367
|
+
if (isPlainObject3(value)) {
|
|
1368
|
+
const entries = Object.entries(value).filter(
|
|
1369
|
+
([, entryValue]) => entryValue !== void 0 && entryValue !== null
|
|
1370
|
+
);
|
|
1371
|
+
if (style === "deepObject") {
|
|
1372
|
+
return entries.map(([key, entryValue]) => {
|
|
1373
|
+
return [`${name}[${key}]`, renderScalar(entryValue)];
|
|
1374
|
+
});
|
|
1375
|
+
}
|
|
1376
|
+
if (explode) {
|
|
1377
|
+
return entries.map(
|
|
1378
|
+
([key, entryValue]) => [key, renderScalar(entryValue)]
|
|
1379
|
+
);
|
|
1380
|
+
}
|
|
1381
|
+
const flat = entries.flatMap(([key, entryValue]) => [
|
|
1382
|
+
key,
|
|
1383
|
+
renderScalar(entryValue)
|
|
1384
|
+
]);
|
|
1385
|
+
return [[name, flat.join(",")]];
|
|
1386
|
+
}
|
|
1387
|
+
return [[name, renderScalar(value)]];
|
|
1388
|
+
}
|
|
1389
|
+
function serializeHeaderParameter(value, explode) {
|
|
1390
|
+
if (Array.isArray(value)) {
|
|
1391
|
+
return value.filter((entry) => entry !== void 0 && entry !== null).map((entry) => renderScalar(entry)).join(",");
|
|
1392
|
+
}
|
|
1393
|
+
if (isPlainObject3(value)) {
|
|
1394
|
+
const entries = Object.entries(value).filter(
|
|
1395
|
+
([, entryValue]) => entryValue !== void 0 && entryValue !== null
|
|
1396
|
+
);
|
|
1397
|
+
return explode ? entries.map(([key, v]) => `${key}=${renderScalar(v)}`).join(",") : entries.flatMap(([key, v]) => [key, renderScalar(v)]).join(",");
|
|
1398
|
+
}
|
|
1399
|
+
return renderScalar(value);
|
|
1400
|
+
}
|
|
1401
|
+
function assertSafeHeaderValue(name, value) {
|
|
1402
|
+
if (/[\r\n\u0000]/.test(value)) {
|
|
1403
|
+
throw new Error(
|
|
1404
|
+
`Header parameter "${name}" contains a control character and was rejected.`
|
|
1405
|
+
);
|
|
1406
|
+
}
|
|
1407
|
+
}
|
|
1408
|
+
function isValidHeaderName(name) {
|
|
1409
|
+
return /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/.test(name);
|
|
1410
|
+
}
|
|
1411
|
+
function buildRequest(baseUrl, templatePath, pathItem, operation, args, binding) {
|
|
1412
|
+
let parsedBaseUrl;
|
|
1413
|
+
try {
|
|
1414
|
+
parsedBaseUrl = new URL(baseUrl);
|
|
1415
|
+
} catch {
|
|
1416
|
+
throw new Error(`Invalid upstream base URL: "${baseUrl}".`);
|
|
1417
|
+
}
|
|
1418
|
+
if (parsedBaseUrl.protocol !== "http:" && parsedBaseUrl.protocol !== "https:") {
|
|
1419
|
+
throw new Error(
|
|
1420
|
+
`Unsupported upstream base URL protocol: "${parsedBaseUrl.protocol}". Only http and https are allowed.`
|
|
1421
|
+
);
|
|
1422
|
+
}
|
|
1423
|
+
if (/\{[^}]+\}/.test(parsedBaseUrl.pathname) || /\{/.test(parsedBaseUrl.host)) {
|
|
1424
|
+
throw new Error(
|
|
1425
|
+
`The upstream base URL "${baseUrl}" still contains unresolved server variables.`
|
|
1426
|
+
);
|
|
1427
|
+
}
|
|
1428
|
+
const query = new URLSearchParams();
|
|
1429
|
+
for (const [key, value] of parsedBaseUrl.searchParams.entries()) {
|
|
1430
|
+
query.append(key, value);
|
|
1431
|
+
}
|
|
1432
|
+
const parameters = binding ? [] : collectOperationParameters(pathItem, operation, []);
|
|
1433
|
+
const bindingMap = new Map(
|
|
1434
|
+
binding?.arguments.map((item) => [item.key, item]) ?? []
|
|
1435
|
+
);
|
|
1436
|
+
let path3 = templatePath;
|
|
1437
|
+
const headers = {};
|
|
1438
|
+
const cookies = [];
|
|
1439
|
+
const dropped = [];
|
|
1440
|
+
const bodyKey = binding ? binding.bodyKey : "body" in args ? "body" : null;
|
|
1441
|
+
for (const [key, value] of Object.entries(args)) {
|
|
1442
|
+
if (bodyKey !== null && key === bodyKey) continue;
|
|
1443
|
+
const bound = bindingMap.get(key);
|
|
1444
|
+
const parameter = bound?.parameter ?? parameters.find((item) => item.name === key);
|
|
1445
|
+
if (!bound && !parameter) {
|
|
1446
|
+
dropped.push({
|
|
1447
|
+
key,
|
|
1448
|
+
reason: "Not a declared parameter of this operation."
|
|
1449
|
+
});
|
|
1450
|
+
continue;
|
|
1451
|
+
}
|
|
1452
|
+
const location = bound?.in ?? parameter?.in ?? "query";
|
|
1453
|
+
const targetName = bound?.name ?? parameter?.name ?? key;
|
|
1454
|
+
const style = parameter ? effectiveStyle(parameter) : location === "path" || location === "header" ? "simple" : "form";
|
|
1455
|
+
const explode = parameter ? effectiveExplode(parameter) : style === "form";
|
|
1456
|
+
if (location === "path") {
|
|
1457
|
+
const placeholder = `{${targetName}}`;
|
|
1458
|
+
if (!path3.includes(placeholder)) {
|
|
1459
|
+
dropped.push({
|
|
1460
|
+
key,
|
|
1461
|
+
reason: `No "${placeholder}" placeholder exists in the path template.`
|
|
1462
|
+
});
|
|
1463
|
+
continue;
|
|
1464
|
+
}
|
|
1465
|
+
if (isEmptyValue(value) || Array.isArray(value) && value.length === 0) {
|
|
1466
|
+
throw new Error(
|
|
1467
|
+
`Path parameter "${targetName}" requires a non-empty value.`
|
|
1468
|
+
);
|
|
1469
|
+
}
|
|
1470
|
+
path3 = path3.split(placeholder).join(serializePathParameter(targetName, value, style, explode));
|
|
1471
|
+
continue;
|
|
1472
|
+
}
|
|
1473
|
+
if (value === void 0 || value === null) continue;
|
|
1474
|
+
if (value === "" && !(location === "query" && parameter?.allowEmptyValue === true)) {
|
|
1475
|
+
continue;
|
|
1476
|
+
}
|
|
1477
|
+
if (location === "header") {
|
|
1478
|
+
if (RESERVED_HEADERS.has(targetName.toLowerCase())) {
|
|
1479
|
+
dropped.push({
|
|
1480
|
+
key,
|
|
1481
|
+
reason: `Header "${targetName}" is managed by the transport layer.`
|
|
1482
|
+
});
|
|
1483
|
+
continue;
|
|
1484
|
+
}
|
|
1485
|
+
if (!isValidHeaderName(targetName)) {
|
|
1486
|
+
dropped.push({
|
|
1487
|
+
key,
|
|
1488
|
+
reason: `"${targetName}" is not a valid header name.`
|
|
1489
|
+
});
|
|
1490
|
+
continue;
|
|
1491
|
+
}
|
|
1492
|
+
const rendered = serializeHeaderParameter(value, explode);
|
|
1493
|
+
assertSafeHeaderValue(targetName, rendered);
|
|
1494
|
+
headers[targetName] = rendered;
|
|
1495
|
+
continue;
|
|
1496
|
+
}
|
|
1497
|
+
if (location === "cookie") {
|
|
1498
|
+
if (isPlainObject3(value) && explode) {
|
|
1499
|
+
for (const [cookieKey, cookieValue] of Object.entries(value)) {
|
|
1500
|
+
if (cookieValue === void 0 || cookieValue === null) continue;
|
|
1501
|
+
cookies.push(
|
|
1502
|
+
`${cookieKey}=${encodeComponent(renderScalar(cookieValue))}`
|
|
1503
|
+
);
|
|
1504
|
+
}
|
|
1505
|
+
continue;
|
|
1506
|
+
}
|
|
1507
|
+
if (Array.isArray(value) && explode) {
|
|
1508
|
+
for (const entry of value) {
|
|
1509
|
+
if (entry === void 0 || entry === null) continue;
|
|
1510
|
+
cookies.push(`${targetName}=${encodeComponent(renderScalar(entry))}`);
|
|
1511
|
+
}
|
|
1512
|
+
continue;
|
|
1513
|
+
}
|
|
1514
|
+
for (const [, cookieValue] of serializeQueryParameter(
|
|
1515
|
+
targetName,
|
|
1516
|
+
value,
|
|
1517
|
+
"form",
|
|
1518
|
+
false
|
|
1519
|
+
)) {
|
|
1520
|
+
cookies.push(`${targetName}=${encodeComponent(cookieValue)}`);
|
|
1521
|
+
}
|
|
1522
|
+
continue;
|
|
1523
|
+
}
|
|
1524
|
+
if (style === "deepObject" && !isPlainObject3(value)) {
|
|
1525
|
+
throw new Error(
|
|
1526
|
+
`Parameter "${targetName}" uses style "deepObject", which requires an object value.`
|
|
1527
|
+
);
|
|
1528
|
+
}
|
|
1529
|
+
for (const [pairName, pairValue] of serializeQueryParameter(
|
|
1530
|
+
targetName,
|
|
1531
|
+
value,
|
|
1532
|
+
style,
|
|
1533
|
+
explode
|
|
1534
|
+
)) {
|
|
1535
|
+
query.append(pairName, pairValue);
|
|
1536
|
+
}
|
|
1537
|
+
}
|
|
1538
|
+
const unresolved = path3.match(/\{[^}]+\}/g);
|
|
1539
|
+
if (unresolved?.length) {
|
|
1540
|
+
throw new Error(
|
|
1541
|
+
`Missing required path parameters: ${unresolved.join(", ")}.`
|
|
1542
|
+
);
|
|
1543
|
+
}
|
|
1544
|
+
if (cookies.length > 0) {
|
|
1545
|
+
const cookieHeader = cookies.join("; ");
|
|
1546
|
+
assertSafeHeaderValue("Cookie", cookieHeader);
|
|
1547
|
+
headers.Cookie = cookieHeader;
|
|
1548
|
+
}
|
|
1549
|
+
const body = bodyKey !== null ? args[bodyKey] : void 0;
|
|
1550
|
+
const basePath = parsedBaseUrl.pathname.replace(/\/+$/, "");
|
|
1551
|
+
const suffix = path3.startsWith("/") ? path3 : `/${path3}`;
|
|
1552
|
+
return {
|
|
1553
|
+
url: `${parsedBaseUrl.origin}${basePath}${suffix}`,
|
|
1554
|
+
query,
|
|
1555
|
+
headers,
|
|
1556
|
+
body,
|
|
1557
|
+
dropped
|
|
1558
|
+
};
|
|
1559
|
+
}
|
|
1560
|
+
function isAbortError(error) {
|
|
1561
|
+
if (!error || typeof error !== "object") return false;
|
|
1562
|
+
const name = error.name;
|
|
1563
|
+
return name === "AbortError" || name === "CanceledError";
|
|
1564
|
+
}
|
|
1565
|
+
function newId() {
|
|
1566
|
+
try {
|
|
1567
|
+
return crypto.randomUUID();
|
|
1568
|
+
} catch {
|
|
1569
|
+
return `id_${Date.now()}_${Math.random().toString(16).slice(2)}`;
|
|
1570
|
+
}
|
|
1571
|
+
}
|
|
1572
|
+
|
|
1573
|
+
// src/core/http-executor.ts
|
|
1574
|
+
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
1575
|
+
var MIN_TIMEOUT_MS = 1e3;
|
|
1576
|
+
var MAX_CONTENT_BYTES = 10 * 1024 * 1024;
|
|
1577
|
+
var MAX_RESULT_CHARS = 5e4;
|
|
1578
|
+
var MAX_LOGGED_BODY_CHARS = 8e3;
|
|
1579
|
+
var SENSITIVE_HEADERS = /* @__PURE__ */ new Set([
|
|
1580
|
+
"authorization",
|
|
1581
|
+
"proxy-authorization",
|
|
1582
|
+
"cookie",
|
|
1583
|
+
"set-cookie",
|
|
1584
|
+
"x-api-key",
|
|
1585
|
+
"api-key",
|
|
1586
|
+
"x-auth-token",
|
|
1587
|
+
"x-access-token",
|
|
1588
|
+
"x-csrf-token"
|
|
1589
|
+
]);
|
|
1590
|
+
function groupQuery(params) {
|
|
1591
|
+
const grouped = {};
|
|
1592
|
+
for (const [key, value] of params.entries()) {
|
|
1593
|
+
const existing = grouped[key];
|
|
1594
|
+
if (existing === void 0) {
|
|
1595
|
+
grouped[key] = value;
|
|
1596
|
+
} else if (Array.isArray(existing)) {
|
|
1597
|
+
existing.push(value);
|
|
1598
|
+
} else {
|
|
1599
|
+
grouped[key] = [existing, value];
|
|
1600
|
+
}
|
|
1601
|
+
}
|
|
1602
|
+
return grouped;
|
|
1603
|
+
}
|
|
1604
|
+
function classifyFailure(error, callerAborted = false) {
|
|
1605
|
+
const code = error.code ?? "";
|
|
1606
|
+
const name = error.name ?? "";
|
|
1607
|
+
const message = error.message ?? "";
|
|
1608
|
+
if (code === "ETIMEDOUT" || /timeout/i.test(message)) return "timeout";
|
|
1609
|
+
if (code === "ERR_CANCELED" || code === "ECONNABORTED" || name === "CanceledError" || name === "AbortError") {
|
|
1610
|
+
return callerAborted ? "canceled" : "timeout";
|
|
1611
|
+
}
|
|
1612
|
+
if (code === "ENOTFOUND" || code === "EAI_AGAIN") return "dns";
|
|
1613
|
+
if (code === "ECONNREFUSED" || code === "ECONNRESET" || code === "EHOSTUNREACH") {
|
|
1614
|
+
return "connection-refused";
|
|
1615
|
+
}
|
|
1616
|
+
if (code.startsWith("ERR_TLS") || code === "CERT_HAS_EXPIRED" || code === "DEPTH_ZERO_SELF_SIGNED_CERT") {
|
|
1617
|
+
return "tls";
|
|
1618
|
+
}
|
|
1619
|
+
if (code === "ERR_FR_MAX_BODY_LENGTH_EXCEEDED" || code === "ERR_BAD_RESPONSE" && /maxContentLength/i.test(message)) {
|
|
1620
|
+
return "payload-too-large";
|
|
1621
|
+
}
|
|
1622
|
+
return "unknown";
|
|
1623
|
+
}
|
|
1624
|
+
function findHeader(headers, name) {
|
|
1625
|
+
const target = name.toLowerCase();
|
|
1626
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
1627
|
+
if (key.toLowerCase() === target) return value;
|
|
1628
|
+
}
|
|
1629
|
+
return void 0;
|
|
1630
|
+
}
|
|
1631
|
+
function hasHeader(headers, name) {
|
|
1632
|
+
return findHeader(headers, name) !== void 0;
|
|
1633
|
+
}
|
|
1634
|
+
function redactHeaders(headers) {
|
|
1635
|
+
const output = {};
|
|
1636
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
1637
|
+
if (value === void 0) continue;
|
|
1638
|
+
output[key] = SENSITIVE_HEADERS.has(key.toLowerCase()) ? "[redacted]" : value;
|
|
1639
|
+
}
|
|
1640
|
+
return output;
|
|
1641
|
+
}
|
|
1642
|
+
function normalizeResponseHeaders(raw) {
|
|
1643
|
+
if (!raw || typeof raw !== "object") return {};
|
|
1644
|
+
const output = {};
|
|
1645
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
1646
|
+
output[key] = SENSITIVE_HEADERS.has(key.toLowerCase()) ? "[redacted]" : value;
|
|
1647
|
+
}
|
|
1648
|
+
return output;
|
|
1649
|
+
}
|
|
1650
|
+
function trimAtLimits(value, limits) {
|
|
1651
|
+
if (value === void 0 || value === null || typeof value === "number") {
|
|
1652
|
+
return limits.map(() => ({ value, truncated: false }));
|
|
1653
|
+
}
|
|
1654
|
+
let serialized = null;
|
|
1655
|
+
if (typeof value === "string") {
|
|
1656
|
+
serialized = value;
|
|
1657
|
+
} else if (typeof value === "object") {
|
|
1658
|
+
try {
|
|
1659
|
+
serialized = JSON.stringify(value);
|
|
1660
|
+
} catch {
|
|
1661
|
+
return limits.map(() => ({
|
|
1662
|
+
value: "[unserializable value]",
|
|
1663
|
+
truncated: true
|
|
1664
|
+
}));
|
|
1665
|
+
}
|
|
1666
|
+
}
|
|
1667
|
+
if (serialized === null) {
|
|
1668
|
+
return limits.map(() => ({ value, truncated: false }));
|
|
1669
|
+
}
|
|
1670
|
+
const text = serialized;
|
|
1671
|
+
return limits.map(
|
|
1672
|
+
(limit) => text.length <= limit ? { value, truncated: false } : { value: `${text.slice(0, limit)}
|
|
1673
|
+
...[truncated]`, truncated: true }
|
|
1674
|
+
);
|
|
1675
|
+
}
|
|
1676
|
+
function trimLargeValue(value, limit) {
|
|
1677
|
+
return trimAtLimits(value, [limit, limit])[0];
|
|
1678
|
+
}
|
|
1679
|
+
function emitLog(onLog, entry) {
|
|
1680
|
+
try {
|
|
1681
|
+
onLog?.(entry);
|
|
1682
|
+
} catch {
|
|
1683
|
+
}
|
|
1684
|
+
}
|
|
1685
|
+
function toSearchParams(query) {
|
|
1686
|
+
if (query instanceof URLSearchParams) return query;
|
|
1687
|
+
const params = new URLSearchParams();
|
|
1688
|
+
if (!query || typeof query !== "object") return params;
|
|
1689
|
+
if (Array.isArray(query)) {
|
|
1690
|
+
for (const entry of query) {
|
|
1691
|
+
if (Array.isArray(entry) && entry.length === 2) {
|
|
1692
|
+
params.append(String(entry[0]), String(entry[1] ?? ""));
|
|
1693
|
+
}
|
|
1694
|
+
}
|
|
1695
|
+
return params;
|
|
1696
|
+
}
|
|
1697
|
+
for (const [key, value] of Object.entries(query)) {
|
|
1698
|
+
if (value === void 0 || value === null) continue;
|
|
1699
|
+
if (Array.isArray(value)) {
|
|
1700
|
+
for (const item of value) {
|
|
1701
|
+
if (item === void 0 || item === null) continue;
|
|
1702
|
+
params.append(key, String(item));
|
|
1703
|
+
}
|
|
1704
|
+
continue;
|
|
1705
|
+
}
|
|
1706
|
+
params.append(key, String(value));
|
|
1707
|
+
}
|
|
1708
|
+
return params;
|
|
1709
|
+
}
|
|
1710
|
+
function serializeBody(body, encoding) {
|
|
1711
|
+
if (body === void 0) return { data: void 0 };
|
|
1712
|
+
switch (encoding) {
|
|
1713
|
+
case "form-urlencoded": {
|
|
1714
|
+
const text = body && typeof body === "object" ? toSearchParams(body).toString() : String(body);
|
|
1715
|
+
return {
|
|
1716
|
+
data: text,
|
|
1717
|
+
contentType: "application/x-www-form-urlencoded"
|
|
1718
|
+
};
|
|
1719
|
+
}
|
|
1720
|
+
case "multipart": {
|
|
1721
|
+
if (!body || typeof body !== "object") return { data: body };
|
|
1722
|
+
const form = new FormData();
|
|
1723
|
+
for (const [key, value] of Object.entries(
|
|
1724
|
+
body
|
|
1725
|
+
)) {
|
|
1726
|
+
if (value === void 0 || value === null) continue;
|
|
1727
|
+
if (Array.isArray(value)) {
|
|
1728
|
+
for (const item of value) {
|
|
1729
|
+
if (item === void 0 || item === null) continue;
|
|
1730
|
+
form.append(key, String(item));
|
|
1731
|
+
}
|
|
1732
|
+
continue;
|
|
1733
|
+
}
|
|
1734
|
+
if (typeof value === "object") {
|
|
1735
|
+
form.append(
|
|
1736
|
+
key,
|
|
1737
|
+
new Blob([JSON.stringify(value)], { type: "application/json" })
|
|
1738
|
+
);
|
|
1739
|
+
continue;
|
|
1740
|
+
}
|
|
1741
|
+
form.append(key, String(value));
|
|
1742
|
+
}
|
|
1743
|
+
return { data: form };
|
|
1744
|
+
}
|
|
1745
|
+
case "binary": {
|
|
1746
|
+
if (typeof body === "string") {
|
|
1747
|
+
try {
|
|
1748
|
+
return {
|
|
1749
|
+
data: Buffer.from(body, "base64"),
|
|
1750
|
+
contentType: "application/octet-stream"
|
|
1751
|
+
};
|
|
1752
|
+
} catch {
|
|
1753
|
+
return { data: body, contentType: "application/octet-stream" };
|
|
1754
|
+
}
|
|
1755
|
+
}
|
|
1756
|
+
return { data: body, contentType: "application/octet-stream" };
|
|
1757
|
+
}
|
|
1758
|
+
case "text":
|
|
1759
|
+
return {
|
|
1760
|
+
data: typeof body === "string" ? body : JSON.stringify(body),
|
|
1761
|
+
contentType: "text/plain"
|
|
1762
|
+
};
|
|
1763
|
+
case "json":
|
|
1764
|
+
default:
|
|
1765
|
+
return { data: body, contentType: "application/json" };
|
|
1766
|
+
}
|
|
1767
|
+
}
|
|
1768
|
+
function describeBodyForLog(data) {
|
|
1769
|
+
if (typeof FormData !== "undefined" && data instanceof FormData) {
|
|
1770
|
+
return "[multipart form data]";
|
|
1771
|
+
}
|
|
1772
|
+
if (Buffer.isBuffer(data)) return `[binary, ${data.byteLength} bytes]`;
|
|
1773
|
+
return data;
|
|
1774
|
+
}
|
|
1775
|
+
async function executeToolCall(spec, toolName, args, context = {}) {
|
|
1776
|
+
const trimmedName = toolName.trim();
|
|
1777
|
+
if (!trimmedName) throw new Error("A tool name is required.");
|
|
1778
|
+
const binding = getBindingIndex(spec).get(trimmedName);
|
|
1779
|
+
const resolved = binding ? {
|
|
1780
|
+
path: binding.path,
|
|
1781
|
+
method: binding.method,
|
|
1782
|
+
operation: binding.operation,
|
|
1783
|
+
pathItem: binding.pathItem
|
|
1784
|
+
} : findOperationById(spec, trimmedName);
|
|
1785
|
+
if (!resolved) {
|
|
1786
|
+
throw new Error(
|
|
1787
|
+
`Tool "${trimmedName}" was not found in the specification.`
|
|
1788
|
+
);
|
|
1789
|
+
}
|
|
1790
|
+
const baseUrl = context.baseUrlOverride ?? spec.servers?.[0]?.url ?? "";
|
|
1791
|
+
if (!baseUrl) {
|
|
1792
|
+
throw new Error(
|
|
1793
|
+
"No upstream base URL is available: the document declares no servers and no override was configured."
|
|
1794
|
+
);
|
|
1795
|
+
}
|
|
1796
|
+
const protocol = context.protocol;
|
|
1797
|
+
const correlationId = newId();
|
|
1798
|
+
const methodUpper = resolved.method.toUpperCase();
|
|
1799
|
+
const timeoutMs = Math.max(
|
|
1800
|
+
MIN_TIMEOUT_MS,
|
|
1801
|
+
context.requestTimeoutMs ?? DEFAULT_TIMEOUT_MS
|
|
1802
|
+
);
|
|
1803
|
+
if (context.signal?.aborted) {
|
|
1804
|
+
throw new DOMException(
|
|
1805
|
+
"The tool call was cancelled before it was dispatched.",
|
|
1806
|
+
"AbortError"
|
|
1807
|
+
);
|
|
1808
|
+
}
|
|
1809
|
+
const built = buildRequest(
|
|
1810
|
+
baseUrl,
|
|
1811
|
+
resolved.path,
|
|
1812
|
+
resolved.pathItem,
|
|
1813
|
+
resolved.operation,
|
|
1814
|
+
args ?? {},
|
|
1815
|
+
binding
|
|
1816
|
+
);
|
|
1817
|
+
const startedAt = Date.now();
|
|
1818
|
+
const searchParams = toSearchParams(built.query);
|
|
1819
|
+
const mergedHeaders = {};
|
|
1820
|
+
for (const [key, value] of Object.entries(context.upstreamHeaders ?? {})) {
|
|
1821
|
+
if (typeof value === "string") mergedHeaders[key] = value;
|
|
1822
|
+
}
|
|
1823
|
+
for (const [key, value] of Object.entries(built.headers ?? {})) {
|
|
1824
|
+
if (typeof value === "string") mergedHeaders[key] = value;
|
|
1825
|
+
}
|
|
1826
|
+
if (context.security?.bearerToken) {
|
|
1827
|
+
mergedHeaders.Authorization = `Bearer ${context.security.bearerToken}`;
|
|
1828
|
+
} else if (context.security?.basicAuth) {
|
|
1829
|
+
const raw = `${context.security.basicAuth.username}:${context.security.basicAuth.password}`;
|
|
1830
|
+
mergedHeaders.Authorization = `Basic ${Buffer.from(raw, "utf8").toString(
|
|
1831
|
+
"base64"
|
|
1832
|
+
)}`;
|
|
1833
|
+
}
|
|
1834
|
+
for (const [name, value] of Object.entries(context.security?.apiKeys ?? {})) {
|
|
1835
|
+
if (!hasHeader(mergedHeaders, name)) mergedHeaders[name] = value;
|
|
1836
|
+
}
|
|
1837
|
+
const serialized = serializeBody(built.body, binding?.bodyEncoding ?? null);
|
|
1838
|
+
if (serialized.data !== void 0 && !hasHeader(mergedHeaders, "content-type")) {
|
|
1839
|
+
const chosen = binding?.bodyEncoding === "multipart" ? void 0 : binding?.bodyMediaType ?? serialized.contentType;
|
|
1840
|
+
if (chosen) mergedHeaders["Content-Type"] = chosen;
|
|
1841
|
+
}
|
|
1842
|
+
if (!hasHeader(mergedHeaders, "accept")) {
|
|
1843
|
+
mergedHeaders.Accept = "application/json, text/plain;q=0.9, */*;q=0.8";
|
|
1844
|
+
}
|
|
1845
|
+
const loggedRequestBody = trimLargeValue(
|
|
1846
|
+
describeBodyForLog(serialized.data),
|
|
1847
|
+
MAX_LOGGED_BODY_CHARS
|
|
1848
|
+
);
|
|
1849
|
+
emitLog(context.onLog, {
|
|
1850
|
+
id: newId(),
|
|
1851
|
+
correlationId,
|
|
1852
|
+
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1853
|
+
direction: "request",
|
|
1854
|
+
protocol,
|
|
1855
|
+
toolName: trimmedName,
|
|
1856
|
+
operationId: trimmedName,
|
|
1857
|
+
method: methodUpper,
|
|
1858
|
+
path: resolved.path,
|
|
1859
|
+
url: built.url,
|
|
1860
|
+
requestHeaders: redactHeaders(mergedHeaders),
|
|
1861
|
+
requestQuery: groupQuery(searchParams),
|
|
1862
|
+
requestBody: loggedRequestBody.value,
|
|
1863
|
+
truncated: loggedRequestBody.truncated,
|
|
1864
|
+
meta: { stage: "before-upstream" }
|
|
1865
|
+
});
|
|
1866
|
+
try {
|
|
1867
|
+
const response = await axios__default.default({
|
|
1868
|
+
// A lowercase custom method (OpenAPI 3.2 additional operations) is passed
|
|
1869
|
+
// through unchanged; axios forwards it to the HTTP layer verbatim.
|
|
1870
|
+
method: resolved.method,
|
|
1871
|
+
url: built.url,
|
|
1872
|
+
params: searchParams,
|
|
1873
|
+
// The builder already applied the style and explode rules together with
|
|
1874
|
+
// percent-encoding. Re-serializing here would double-encode every value.
|
|
1875
|
+
paramsSerializer: {
|
|
1876
|
+
serialize: (params) => params instanceof URLSearchParams ? params.toString() : toSearchParams(params).toString()
|
|
1877
|
+
},
|
|
1878
|
+
...serialized.data !== void 0 ? { data: serialized.data } : {},
|
|
1879
|
+
headers: new axios.AxiosHeaders(mergedHeaders),
|
|
1880
|
+
timeout: timeoutMs,
|
|
1881
|
+
maxContentLength: MAX_CONTENT_BYTES,
|
|
1882
|
+
maxBodyLength: MAX_CONTENT_BYTES,
|
|
1883
|
+
maxRedirects: 5,
|
|
1884
|
+
decompress: true,
|
|
1885
|
+
// Read as text and parse manually: axios's own JSON parsing hides the raw
|
|
1886
|
+
// payload, which is exactly what an operator needs when a response is
|
|
1887
|
+
// malformed.
|
|
1888
|
+
responseType: "text",
|
|
1889
|
+
transformResponse: [(data2) => data2],
|
|
1890
|
+
// Any status is a valid answer. Letting axios throw on 4xx would turn an
|
|
1891
|
+
// upstream validation error into an opaque transport failure.
|
|
1892
|
+
validateStatus: () => true,
|
|
1893
|
+
...context.signal ? { signal: context.signal } : {}
|
|
1894
|
+
});
|
|
1895
|
+
const responseHeaders = normalizeResponseHeaders(response.headers);
|
|
1896
|
+
const rawHeaders = response.headers;
|
|
1897
|
+
const contentType = String(rawHeaders?.["content-type"] ?? "");
|
|
1898
|
+
let data = response.data;
|
|
1899
|
+
if (typeof data === "string" && data.length > 0 && /\bjson\b/i.test(contentType)) {
|
|
1900
|
+
try {
|
|
1901
|
+
data = JSON.parse(data);
|
|
1902
|
+
} catch {
|
|
1903
|
+
}
|
|
1904
|
+
}
|
|
1905
|
+
const [forModel, forLog] = trimAtLimits(data, [
|
|
1906
|
+
MAX_RESULT_CHARS,
|
|
1907
|
+
MAX_LOGGED_BODY_CHARS
|
|
1908
|
+
]);
|
|
1909
|
+
const durationMs = Date.now() - startedAt;
|
|
1910
|
+
emitLog(context.onLog, {
|
|
1911
|
+
id: newId(),
|
|
1912
|
+
correlationId,
|
|
1913
|
+
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1914
|
+
direction: "response",
|
|
1915
|
+
protocol,
|
|
1916
|
+
toolName: trimmedName,
|
|
1917
|
+
operationId: trimmedName,
|
|
1918
|
+
method: methodUpper,
|
|
1919
|
+
path: resolved.path,
|
|
1920
|
+
url: built.url,
|
|
1921
|
+
status: response.status,
|
|
1922
|
+
durationMs,
|
|
1923
|
+
success: response.status < 400,
|
|
1924
|
+
responseHeaders,
|
|
1925
|
+
responseBody: forLog.value,
|
|
1926
|
+
truncated: forLog.truncated
|
|
1927
|
+
});
|
|
1928
|
+
return {
|
|
1929
|
+
status: response.status,
|
|
1930
|
+
statusText: response.statusText ?? "",
|
|
1931
|
+
headers: responseHeaders,
|
|
1932
|
+
data: forModel.value,
|
|
1933
|
+
truncated: forModel.truncated,
|
|
1934
|
+
url: built.url,
|
|
1935
|
+
method: methodUpper,
|
|
1936
|
+
durationMs,
|
|
1937
|
+
correlationId
|
|
1938
|
+
};
|
|
1939
|
+
} catch (error) {
|
|
1940
|
+
const err = error;
|
|
1941
|
+
const durationMs = Date.now() - startedAt;
|
|
1942
|
+
const kind = classifyFailure(err, context.signal?.aborted === true);
|
|
1943
|
+
const message = (() => {
|
|
1944
|
+
switch (kind) {
|
|
1945
|
+
case "timeout":
|
|
1946
|
+
return `The upstream request timed out after ${timeoutMs}ms.`;
|
|
1947
|
+
case "canceled":
|
|
1948
|
+
return "The tool call was cancelled by the client.";
|
|
1949
|
+
case "dns":
|
|
1950
|
+
return `The upstream host could not be resolved: ${err.message}`;
|
|
1951
|
+
case "connection-refused":
|
|
1952
|
+
return `The upstream connection was refused or reset: ${err.message}`;
|
|
1953
|
+
case "tls":
|
|
1954
|
+
return `The upstream TLS handshake failed: ${err.message}`;
|
|
1955
|
+
case "payload-too-large":
|
|
1956
|
+
return `The request or response exceeded the ${MAX_CONTENT_BYTES}-byte limit.`;
|
|
1957
|
+
default:
|
|
1958
|
+
return `Upstream request failed: ${err.message}`;
|
|
1959
|
+
}
|
|
1960
|
+
})();
|
|
1961
|
+
emitLog(context.onLog, {
|
|
1962
|
+
id: newId(),
|
|
1963
|
+
correlationId,
|
|
1964
|
+
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1965
|
+
direction: "response",
|
|
1966
|
+
protocol,
|
|
1967
|
+
toolName: trimmedName,
|
|
1968
|
+
operationId: trimmedName,
|
|
1969
|
+
method: methodUpper,
|
|
1970
|
+
path: resolved.path,
|
|
1971
|
+
url: built.url,
|
|
1972
|
+
durationMs,
|
|
1973
|
+
success: false,
|
|
1974
|
+
error: message,
|
|
1975
|
+
meta: { failureKind: kind, code: err.code }
|
|
1976
|
+
});
|
|
1977
|
+
const wrapped = new Error(message, { cause: err });
|
|
1978
|
+
if (kind === "canceled") wrapped.name = "AbortError";
|
|
1979
|
+
wrapped.failureKind = kind;
|
|
1980
|
+
throw wrapped;
|
|
1981
|
+
}
|
|
1982
|
+
}
|
|
1983
|
+
var DEFAULT_PAGE_SIZE = 100;
|
|
1984
|
+
var MAX_PAGE_SIZE = 500;
|
|
1985
|
+
var NO_SERVICE_MESSAGE = "No MCP service is currently running: the specification was cleared or the service was stopped.";
|
|
1986
|
+
function requireSpec(specProvider) {
|
|
1987
|
+
const spec = specProvider();
|
|
1988
|
+
if (!spec) {
|
|
1989
|
+
throw new types_js.McpError(types_js.ErrorCode.InternalError, NO_SERVICE_MESSAGE);
|
|
1990
|
+
}
|
|
1991
|
+
return spec;
|
|
1992
|
+
}
|
|
1993
|
+
function paginate(items, cursor, pageSize) {
|
|
1994
|
+
let offset = 0;
|
|
1995
|
+
if (cursor !== void 0) {
|
|
1996
|
+
let decoded;
|
|
1997
|
+
try {
|
|
1998
|
+
decoded = Buffer.from(cursor, "base64url").toString("utf8");
|
|
1999
|
+
} catch {
|
|
2000
|
+
throw new types_js.McpError(types_js.ErrorCode.InvalidParams, "The cursor is malformed.");
|
|
2001
|
+
}
|
|
2002
|
+
const parsed = Number.parseInt(decoded, 10);
|
|
2003
|
+
if (!Number.isInteger(parsed) || parsed < 0 || parsed > items.length) {
|
|
2004
|
+
throw new types_js.McpError(
|
|
2005
|
+
types_js.ErrorCode.InvalidParams,
|
|
2006
|
+
"The cursor is no longer valid; restart the listing without a cursor."
|
|
2007
|
+
);
|
|
2008
|
+
}
|
|
2009
|
+
offset = parsed;
|
|
2010
|
+
}
|
|
2011
|
+
const page = items.slice(offset, offset + pageSize);
|
|
2012
|
+
const nextOffset = offset + page.length;
|
|
2013
|
+
if (nextOffset < items.length) {
|
|
2014
|
+
return {
|
|
2015
|
+
page,
|
|
2016
|
+
nextCursor: Buffer.from(String(nextOffset), "utf8").toString("base64url")
|
|
2017
|
+
};
|
|
2018
|
+
}
|
|
2019
|
+
return { page };
|
|
2020
|
+
}
|
|
2021
|
+
function toolErrorResult(message) {
|
|
2022
|
+
return {
|
|
2023
|
+
isError: true,
|
|
2024
|
+
content: [{ type: "text", text: message }]
|
|
2025
|
+
};
|
|
2026
|
+
}
|
|
2027
|
+
function buildMcpServer(specProvider, contextProvider = () => ({}), options = {}) {
|
|
2028
|
+
const pageSize = Math.min(
|
|
2029
|
+
MAX_PAGE_SIZE,
|
|
2030
|
+
Math.max(1, options.pageSize ?? DEFAULT_PAGE_SIZE)
|
|
2031
|
+
);
|
|
2032
|
+
const server = new index_js.Server(
|
|
2033
|
+
{
|
|
2034
|
+
name: options.name ?? "openapi-mcp",
|
|
2035
|
+
version: options.version ?? "1.1.0"
|
|
2036
|
+
},
|
|
2037
|
+
{
|
|
2038
|
+
// `listChanged` is intentionally not advertised: replacing the document
|
|
2039
|
+
// terminates existing sessions instead of mutating them in place, so this
|
|
2040
|
+
// server never emits list-changed notifications. Advertising a capability
|
|
2041
|
+
// it does not honour would make well-behaved clients wait for updates
|
|
2042
|
+
// that never arrive.
|
|
2043
|
+
capabilities: {
|
|
2044
|
+
tools: {},
|
|
2045
|
+
prompts: {},
|
|
2046
|
+
resources: {}
|
|
2047
|
+
},
|
|
2048
|
+
...options.instructions ? { instructions: options.instructions } : {
|
|
2049
|
+
instructions: "Tools are generated from an OpenAPI document. Call tools/list first; arguments marked with a location note map to path, query, header or cookie parameters. Tools tagged [PARTIAL SUPPORT] could not be mapped completely and may require additional free-form arguments."
|
|
2050
|
+
}
|
|
2051
|
+
}
|
|
2052
|
+
);
|
|
2053
|
+
server.setRequestHandler(types_js.ListToolsRequestSchema, async (request) => {
|
|
2054
|
+
const spec = specProvider();
|
|
2055
|
+
const all = spec ? getGeneratedTools(spec).tools : [];
|
|
2056
|
+
const { page, nextCursor } = paginate(
|
|
2057
|
+
all,
|
|
2058
|
+
request.params?.cursor,
|
|
2059
|
+
pageSize
|
|
2060
|
+
);
|
|
2061
|
+
return { tools: page, ...nextCursor ? { nextCursor } : {} };
|
|
2062
|
+
});
|
|
2063
|
+
server.setRequestHandler(types_js.CallToolRequestSchema, async (request, extra) => {
|
|
2064
|
+
const spec = requireSpec(specProvider);
|
|
2065
|
+
const name = request.params.name;
|
|
2066
|
+
const known = getGeneratedTools(spec).bindings.some(
|
|
2067
|
+
(binding) => binding.toolName === name
|
|
2068
|
+
);
|
|
2069
|
+
if (!known) {
|
|
2070
|
+
throw new types_js.McpError(types_js.ErrorCode.InvalidParams, `Unknown tool "${name}".`);
|
|
2071
|
+
}
|
|
2072
|
+
const rawArgs = request.params.arguments;
|
|
2073
|
+
if (rawArgs !== void 0 && (typeof rawArgs !== "object" || rawArgs === null || Array.isArray(rawArgs))) {
|
|
2074
|
+
throw new types_js.McpError(
|
|
2075
|
+
types_js.ErrorCode.InvalidParams,
|
|
2076
|
+
"Tool arguments must be an object."
|
|
2077
|
+
);
|
|
2078
|
+
}
|
|
2079
|
+
try {
|
|
2080
|
+
const result = await executeToolCall(
|
|
2081
|
+
spec,
|
|
2082
|
+
name,
|
|
2083
|
+
rawArgs ?? {},
|
|
2084
|
+
{
|
|
2085
|
+
...contextProvider(),
|
|
2086
|
+
// Propagating the abort signal is what makes client-side
|
|
2087
|
+
// cancellation actually release the upstream connection.
|
|
2088
|
+
signal: extra.signal
|
|
2089
|
+
}
|
|
2090
|
+
);
|
|
2091
|
+
const payload = {
|
|
2092
|
+
status: result.status,
|
|
2093
|
+
url: result.url,
|
|
2094
|
+
method: result.method,
|
|
2095
|
+
durationMs: result.durationMs,
|
|
2096
|
+
truncated: result.truncated,
|
|
2097
|
+
headers: result.headers,
|
|
2098
|
+
data: result.data
|
|
2099
|
+
};
|
|
2100
|
+
let text;
|
|
2101
|
+
try {
|
|
2102
|
+
text = JSON.stringify(payload, null, 2);
|
|
2103
|
+
} catch {
|
|
2104
|
+
text = JSON.stringify(
|
|
2105
|
+
{ ...payload, data: "[unserializable response body]" },
|
|
2106
|
+
null,
|
|
2107
|
+
2
|
|
2108
|
+
);
|
|
2109
|
+
}
|
|
2110
|
+
return {
|
|
2111
|
+
// An upstream 4xx/5xx is a tool execution failure, which MCP models as
|
|
2112
|
+
// `isError` on a successful protocol response — the model is expected to
|
|
2113
|
+
// read the status and decide what to do.
|
|
2114
|
+
isError: result.status >= 400,
|
|
2115
|
+
content: [{ type: "text", text }]
|
|
2116
|
+
};
|
|
2117
|
+
} catch (error) {
|
|
2118
|
+
if (error instanceof types_js.McpError) throw error;
|
|
2119
|
+
return toolErrorResult(
|
|
2120
|
+
error instanceof Error ? error.message : String(error)
|
|
2121
|
+
);
|
|
2122
|
+
}
|
|
2123
|
+
});
|
|
2124
|
+
server.setRequestHandler(types_js.ListPromptsRequestSchema, async (request) => {
|
|
2125
|
+
const spec = specProvider();
|
|
2126
|
+
const all = spec ? generatePrompts(spec) : [];
|
|
2127
|
+
const { page, nextCursor } = paginate(
|
|
2128
|
+
all,
|
|
2129
|
+
request.params?.cursor,
|
|
2130
|
+
pageSize
|
|
2131
|
+
);
|
|
2132
|
+
return { prompts: page, ...nextCursor ? { nextCursor } : {} };
|
|
2133
|
+
});
|
|
2134
|
+
server.setRequestHandler(types_js.GetPromptRequestSchema, async (request) => {
|
|
2135
|
+
const spec = requireSpec(specProvider);
|
|
2136
|
+
const name = request.params.name;
|
|
2137
|
+
const rawArgs = request.params.arguments;
|
|
2138
|
+
const args = rawArgs && typeof rawArgs === "object" && !Array.isArray(rawArgs) ? rawArgs : {};
|
|
2139
|
+
const result = resolvePrompt(spec, name, args);
|
|
2140
|
+
if (!result) {
|
|
2141
|
+
throw new types_js.McpError(types_js.ErrorCode.InvalidParams, `Unknown prompt "${name}".`);
|
|
2142
|
+
}
|
|
2143
|
+
return result;
|
|
2144
|
+
});
|
|
2145
|
+
server.setRequestHandler(types_js.ListResourcesRequestSchema, async (request) => {
|
|
2146
|
+
const spec = specProvider();
|
|
2147
|
+
const all = spec ? generateResources() : [];
|
|
2148
|
+
const { page, nextCursor } = paginate(
|
|
2149
|
+
all,
|
|
2150
|
+
request.params?.cursor,
|
|
2151
|
+
pageSize
|
|
2152
|
+
);
|
|
2153
|
+
return { resources: page, ...nextCursor ? { nextCursor } : {} };
|
|
2154
|
+
});
|
|
2155
|
+
server.setRequestHandler(types_js.ListResourceTemplatesRequestSchema, async () => ({
|
|
2156
|
+
resourceTemplates: []
|
|
2157
|
+
}));
|
|
2158
|
+
server.setRequestHandler(types_js.ReadResourceRequestSchema, async (request) => {
|
|
2159
|
+
const spec = requireSpec(specProvider);
|
|
2160
|
+
const uri = request.params.uri;
|
|
2161
|
+
const resource = readResource(spec, uri);
|
|
2162
|
+
if (!resource) {
|
|
2163
|
+
throw new types_js.McpError(
|
|
2164
|
+
types_js.ErrorCode.InvalidParams,
|
|
2165
|
+
`Unknown resource URI: ${uri}`
|
|
2166
|
+
);
|
|
2167
|
+
}
|
|
2168
|
+
return { contents: [resource] };
|
|
2169
|
+
});
|
|
2170
|
+
return server;
|
|
2171
|
+
}
|
|
2172
|
+
function divertConsoleToStderr() {
|
|
2173
|
+
const original = {
|
|
2174
|
+
log: console.log,
|
|
2175
|
+
info: console.info,
|
|
2176
|
+
debug: console.debug,
|
|
2177
|
+
dir: console.dir
|
|
2178
|
+
};
|
|
2179
|
+
const toStderr = (...args) => {
|
|
2180
|
+
console.error(...args);
|
|
2181
|
+
};
|
|
2182
|
+
console.log = toStderr;
|
|
2183
|
+
console.info = toStderr;
|
|
2184
|
+
console.debug = toStderr;
|
|
2185
|
+
console.dir = toStderr;
|
|
2186
|
+
return () => {
|
|
2187
|
+
console.log = original.log;
|
|
2188
|
+
console.info = original.info;
|
|
2189
|
+
console.debug = original.debug;
|
|
2190
|
+
console.dir = original.dir;
|
|
2191
|
+
};
|
|
2192
|
+
}
|
|
2193
|
+
async function startStdioServer(spec, context = {}, options = {}) {
|
|
2194
|
+
const restoreConsole = divertConsoleToStderr();
|
|
2195
|
+
const upstreamLog = context.onLog;
|
|
2196
|
+
const onLog = upstreamLog ? (entry) => {
|
|
2197
|
+
try {
|
|
2198
|
+
upstreamLog(entry);
|
|
2199
|
+
} catch {
|
|
2200
|
+
}
|
|
2201
|
+
} : void 0;
|
|
2202
|
+
const stdioContext = {
|
|
2203
|
+
...context,
|
|
2204
|
+
protocol: "stdio",
|
|
2205
|
+
...onLog ? { onLog } : {}
|
|
2206
|
+
};
|
|
2207
|
+
const server = buildMcpServer(
|
|
2208
|
+
() => spec,
|
|
2209
|
+
() => stdioContext,
|
|
2210
|
+
{ }
|
|
2211
|
+
);
|
|
2212
|
+
const transport = new stdio_js.StdioServerTransport();
|
|
2213
|
+
let settleClosed = () => {
|
|
2214
|
+
};
|
|
2215
|
+
const closed = new Promise((resolve) => {
|
|
2216
|
+
settleClosed = resolve;
|
|
2217
|
+
});
|
|
2218
|
+
let shuttingDown = false;
|
|
2219
|
+
const detachSignals = [];
|
|
2220
|
+
const finish = () => {
|
|
2221
|
+
if (shuttingDown) return;
|
|
2222
|
+
shuttingDown = true;
|
|
2223
|
+
for (const detach of detachSignals) detach();
|
|
2224
|
+
restoreConsole();
|
|
2225
|
+
settleClosed();
|
|
2226
|
+
};
|
|
2227
|
+
transport.onerror = (error) => {
|
|
2228
|
+
console.error("[mcp:stdio] transport error:", error);
|
|
2229
|
+
};
|
|
2230
|
+
transport.onclose = () => {
|
|
2231
|
+
finish();
|
|
2232
|
+
};
|
|
2233
|
+
await server.connect(transport);
|
|
2234
|
+
if (options.handleSignals !== false) {
|
|
2235
|
+
for (const signal of ["SIGINT", "SIGTERM"]) {
|
|
2236
|
+
const handler = () => {
|
|
2237
|
+
void server.close().catch(() => {
|
|
2238
|
+
});
|
|
2239
|
+
};
|
|
2240
|
+
process.once(signal, handler);
|
|
2241
|
+
detachSignals.push(() => process.removeListener(signal, handler));
|
|
2242
|
+
}
|
|
2243
|
+
}
|
|
2244
|
+
return {
|
|
2245
|
+
closed,
|
|
2246
|
+
close: async () => {
|
|
2247
|
+
try {
|
|
2248
|
+
await server.close();
|
|
2249
|
+
} finally {
|
|
2250
|
+
finish();
|
|
2251
|
+
}
|
|
2252
|
+
}
|
|
2253
|
+
};
|
|
2254
|
+
}
|
|
2255
|
+
var MCP_ROUTES = {
|
|
2256
|
+
/** Streamable HTTP endpoint (POST/GET/DELETE). Preferred by modern clients. */
|
|
2257
|
+
streamableHttp: "/mcp",
|
|
2258
|
+
/** Legacy HTTP+SSE stream endpoint. */
|
|
2259
|
+
sse: "/sse",
|
|
2260
|
+
/** Legacy HTTP+SSE message endpoint. */
|
|
2261
|
+
messages: "/mcp/messages"
|
|
2262
|
+
};
|
|
2263
|
+
var DEFAULT_MAX_SESSIONS = 64;
|
|
2264
|
+
var DEFAULT_SESSION_IDLE_MS = 10 * 60 * 1e3;
|
|
2265
|
+
var KEEPALIVE_INTERVAL_MS = 25 * 1e3;
|
|
2266
|
+
var SWEEP_INTERVAL_MS = 30 * 1e3;
|
|
2267
|
+
var DRAIN_TIMEOUT_MS = 5 * 1e3;
|
|
2268
|
+
function rpcError(code, message) {
|
|
2269
|
+
return { jsonrpc: "2.0", id: null, error: { code, message } };
|
|
2270
|
+
}
|
|
2271
|
+
function readHeader(value) {
|
|
2272
|
+
if (typeof value === "string" && value.trim()) return value.trim();
|
|
2273
|
+
if (Array.isArray(value)) {
|
|
2274
|
+
const first = value.find(
|
|
2275
|
+
(entry) => typeof entry === "string" && entry.trim() !== ""
|
|
2276
|
+
);
|
|
2277
|
+
return first?.trim();
|
|
2278
|
+
}
|
|
2279
|
+
return void 0;
|
|
2280
|
+
}
|
|
2281
|
+
function readQueryValue(value) {
|
|
2282
|
+
if (typeof value === "string") return value.trim();
|
|
2283
|
+
if (Array.isArray(value)) {
|
|
2284
|
+
const first = value.find(
|
|
2285
|
+
(entry) => typeof entry === "string" && entry.trim() !== ""
|
|
2286
|
+
);
|
|
2287
|
+
return first?.trim() ?? "";
|
|
2288
|
+
}
|
|
2289
|
+
return "";
|
|
2290
|
+
}
|
|
2291
|
+
function describe2(error) {
|
|
2292
|
+
return error instanceof Error ? error.message : String(error);
|
|
2293
|
+
}
|
|
2294
|
+
function asTransport(value) {
|
|
2295
|
+
return value;
|
|
2296
|
+
}
|
|
2297
|
+
function asStreamableTransport(value) {
|
|
2298
|
+
return value;
|
|
2299
|
+
}
|
|
2300
|
+
function asSseTransport(value) {
|
|
2301
|
+
return value;
|
|
2302
|
+
}
|
|
2303
|
+
function chainOnClose(transport, handler) {
|
|
2304
|
+
const previous = transport.onclose;
|
|
2305
|
+
transport.onclose = () => {
|
|
2306
|
+
try {
|
|
2307
|
+
previous?.();
|
|
2308
|
+
} finally {
|
|
2309
|
+
handler();
|
|
2310
|
+
}
|
|
2311
|
+
};
|
|
2312
|
+
}
|
|
2313
|
+
function chainOnError(transport, handler) {
|
|
2314
|
+
const previous = transport.onerror;
|
|
2315
|
+
transport.onerror = (error) => {
|
|
2316
|
+
try {
|
|
2317
|
+
previous?.(error);
|
|
2318
|
+
} finally {
|
|
2319
|
+
handler(error);
|
|
2320
|
+
}
|
|
2321
|
+
};
|
|
2322
|
+
}
|
|
2323
|
+
function attachMcpRoutes(app, options) {
|
|
2324
|
+
const { specProvider, contextProvider } = options;
|
|
2325
|
+
const sessions = /* @__PURE__ */ new Map();
|
|
2326
|
+
const closures = /* @__PURE__ */ new Map();
|
|
2327
|
+
let pending = 0;
|
|
2328
|
+
const guard = options.routeGuard ? [options.routeGuard] : [];
|
|
2329
|
+
const maxSessions = Math.max(1, options.maxSessions ?? DEFAULT_MAX_SESSIONS);
|
|
2330
|
+
const idleMs = Math.max(0, options.sessionIdleMs ?? DEFAULT_SESSION_IDLE_MS);
|
|
2331
|
+
const legacyEnabled = options.enableLegacySse !== false;
|
|
2332
|
+
const allowedOrigins = options.allowedOrigins?.filter(
|
|
2333
|
+
(entry) => Boolean(entry) && entry !== "*"
|
|
2334
|
+
) ?? [];
|
|
2335
|
+
const configuredHosts = options.allowedHosts?.filter(Boolean) ?? [];
|
|
2336
|
+
const dnsProtection = allowedOrigins.length > 0 || configuredHosts.length > 0;
|
|
2337
|
+
const allowedHosts = configuredHosts.length > 0 ? configuredHosts : allowedOrigins.map((origin) => {
|
|
2338
|
+
try {
|
|
2339
|
+
return new URL(origin).host;
|
|
2340
|
+
} catch {
|
|
2341
|
+
return "";
|
|
2342
|
+
}
|
|
2343
|
+
}).filter(Boolean);
|
|
2344
|
+
const emit = (event) => {
|
|
2345
|
+
try {
|
|
2346
|
+
options.onEvent?.(event);
|
|
2347
|
+
} catch {
|
|
2348
|
+
}
|
|
2349
|
+
};
|
|
2350
|
+
function touch(sessionId) {
|
|
2351
|
+
const record = sessions.get(sessionId);
|
|
2352
|
+
if (record) record.lastActivityAt = Date.now();
|
|
2353
|
+
}
|
|
2354
|
+
async function destroySession(sessionId, reason) {
|
|
2355
|
+
const record = sessions.get(sessionId);
|
|
2356
|
+
if (!record || record.closing) return false;
|
|
2357
|
+
record.closing = true;
|
|
2358
|
+
sessions.delete(sessionId);
|
|
2359
|
+
if (record.keepAlive) clearInterval(record.keepAlive);
|
|
2360
|
+
const closure = (async () => {
|
|
2361
|
+
try {
|
|
2362
|
+
await record.transport.close();
|
|
2363
|
+
} catch {
|
|
2364
|
+
}
|
|
2365
|
+
try {
|
|
2366
|
+
await record.server.close();
|
|
2367
|
+
} catch {
|
|
2368
|
+
}
|
|
2369
|
+
})();
|
|
2370
|
+
closures.set(sessionId, closure);
|
|
2371
|
+
try {
|
|
2372
|
+
await closure;
|
|
2373
|
+
} finally {
|
|
2374
|
+
closures.delete(sessionId);
|
|
2375
|
+
}
|
|
2376
|
+
emit({ type: "session-closed", kind: record.kind, sessionId, reason });
|
|
2377
|
+
return true;
|
|
2378
|
+
}
|
|
2379
|
+
function admit(kind, response) {
|
|
2380
|
+
if (!specProvider()) {
|
|
2381
|
+
emit({ type: "session-rejected", kind, reason: "no-active-spec" });
|
|
2382
|
+
response.status(503).json(
|
|
2383
|
+
rpcError(
|
|
2384
|
+
-32e3,
|
|
2385
|
+
"No MCP service is currently running. Load a specification and start the service first."
|
|
2386
|
+
)
|
|
2387
|
+
);
|
|
2388
|
+
return false;
|
|
2389
|
+
}
|
|
2390
|
+
if (sessions.size + pending >= maxSessions) {
|
|
2391
|
+
emit({ type: "session-rejected", kind, reason: "capacity" });
|
|
2392
|
+
response.status(429).json(
|
|
2393
|
+
rpcError(
|
|
2394
|
+
-32e3,
|
|
2395
|
+
`Too many concurrent MCP sessions (limit ${maxSessions}).`
|
|
2396
|
+
)
|
|
2397
|
+
);
|
|
2398
|
+
return false;
|
|
2399
|
+
}
|
|
2400
|
+
return true;
|
|
2401
|
+
}
|
|
2402
|
+
async function openStreamableSession(request, response, body) {
|
|
2403
|
+
const server = buildMcpServer(specProvider, contextProvider, {
|
|
2404
|
+
});
|
|
2405
|
+
const transport = new streamableHttp_js.StreamableHTTPServerTransport({
|
|
2406
|
+
sessionIdGenerator: () => crypto.randomUUID(),
|
|
2407
|
+
enableDnsRebindingProtection: dnsProtection,
|
|
2408
|
+
...allowedHosts.length > 0 ? { allowedHosts } : {},
|
|
2409
|
+
...allowedOrigins.length > 0 ? { allowedOrigins } : {},
|
|
2410
|
+
onsessioninitialized: (sessionId) => {
|
|
2411
|
+
const now = Date.now();
|
|
2412
|
+
sessions.set(sessionId, {
|
|
2413
|
+
sessionId,
|
|
2414
|
+
kind: "streamable-http",
|
|
2415
|
+
transport: asTransport(transport),
|
|
2416
|
+
server,
|
|
2417
|
+
createdAt: now,
|
|
2418
|
+
lastActivityAt: now,
|
|
2419
|
+
closing: false,
|
|
2420
|
+
remoteAddress: request.ip,
|
|
2421
|
+
userAgent: readHeader(request.headers["user-agent"])
|
|
2422
|
+
});
|
|
2423
|
+
emit({ type: "session-opened", kind: "streamable-http", sessionId });
|
|
2424
|
+
},
|
|
2425
|
+
onsessionclosed: (sessionId) => {
|
|
2426
|
+
void destroySession(sessionId, "client-terminated");
|
|
2427
|
+
}
|
|
2428
|
+
});
|
|
2429
|
+
const rollback = async () => {
|
|
2430
|
+
await transport.close().catch(() => void 0);
|
|
2431
|
+
await server.close().catch(() => void 0);
|
|
2432
|
+
};
|
|
2433
|
+
pending += 1;
|
|
2434
|
+
try {
|
|
2435
|
+
try {
|
|
2436
|
+
await server.connect(asTransport(transport));
|
|
2437
|
+
} catch (error) {
|
|
2438
|
+
await rollback();
|
|
2439
|
+
emit({
|
|
2440
|
+
type: "session-error",
|
|
2441
|
+
kind: "streamable-http",
|
|
2442
|
+
reason: describe2(error)
|
|
2443
|
+
});
|
|
2444
|
+
if (!response.headersSent) {
|
|
2445
|
+
response.status(500).json(rpcError(-32603, "Failed to initialize the MCP session."));
|
|
2446
|
+
}
|
|
2447
|
+
return false;
|
|
2448
|
+
}
|
|
2449
|
+
chainOnClose(transport, () => {
|
|
2450
|
+
const sessionId = transport.sessionId;
|
|
2451
|
+
if (sessionId) void destroySession(sessionId, "transport-closed");
|
|
2452
|
+
});
|
|
2453
|
+
chainOnError(transport, (error) => {
|
|
2454
|
+
emit({
|
|
2455
|
+
type: "session-error",
|
|
2456
|
+
kind: "streamable-http",
|
|
2457
|
+
sessionId: transport.sessionId,
|
|
2458
|
+
reason: error.message
|
|
2459
|
+
});
|
|
2460
|
+
});
|
|
2461
|
+
try {
|
|
2462
|
+
await transport.handleRequest(request, response, body);
|
|
2463
|
+
} catch (error) {
|
|
2464
|
+
const sessionId = transport.sessionId;
|
|
2465
|
+
if (sessionId && sessions.has(sessionId)) {
|
|
2466
|
+
await destroySession(sessionId, "initialize-failed");
|
|
2467
|
+
} else {
|
|
2468
|
+
await rollback();
|
|
2469
|
+
}
|
|
2470
|
+
emit({
|
|
2471
|
+
type: "session-error",
|
|
2472
|
+
kind: "streamable-http",
|
|
2473
|
+
sessionId,
|
|
2474
|
+
reason: describe2(error)
|
|
2475
|
+
});
|
|
2476
|
+
if (!response.headersSent) {
|
|
2477
|
+
response.status(500).json(rpcError(-32603, "Failed to handle the initialize request."));
|
|
2478
|
+
}
|
|
2479
|
+
return false;
|
|
2480
|
+
}
|
|
2481
|
+
if (!transport.sessionId) {
|
|
2482
|
+
await rollback();
|
|
2483
|
+
return false;
|
|
2484
|
+
}
|
|
2485
|
+
return true;
|
|
2486
|
+
} finally {
|
|
2487
|
+
pending -= 1;
|
|
2488
|
+
}
|
|
2489
|
+
}
|
|
2490
|
+
const handleStreamable = async (request, response) => {
|
|
2491
|
+
const sessionId = readHeader(request.headers["mcp-session-id"]);
|
|
2492
|
+
try {
|
|
2493
|
+
if (sessionId) {
|
|
2494
|
+
const record = sessions.get(sessionId);
|
|
2495
|
+
if (!record || record.kind !== "streamable-http" || record.closing) {
|
|
2496
|
+
response.status(404).json(rpcError(-32001, "Unknown or expired MCP session."));
|
|
2497
|
+
return;
|
|
2498
|
+
}
|
|
2499
|
+
touch(sessionId);
|
|
2500
|
+
await asStreamableTransport(record.transport).handleRequest(
|
|
2501
|
+
request,
|
|
2502
|
+
response,
|
|
2503
|
+
request.body
|
|
2504
|
+
);
|
|
2505
|
+
touch(sessionId);
|
|
2506
|
+
return;
|
|
2507
|
+
}
|
|
2508
|
+
if (request.method !== "POST") {
|
|
2509
|
+
response.status(400).json(
|
|
2510
|
+
rpcError(
|
|
2511
|
+
-32e3,
|
|
2512
|
+
"Missing mcp-session-id header. Send an initialize request over POST first, or use the legacy /sse endpoint."
|
|
2513
|
+
)
|
|
2514
|
+
);
|
|
2515
|
+
return;
|
|
2516
|
+
}
|
|
2517
|
+
if (!types_js.isInitializeRequest(request.body)) {
|
|
2518
|
+
response.status(400).json(
|
|
2519
|
+
rpcError(
|
|
2520
|
+
-32e3,
|
|
2521
|
+
"Missing mcp-session-id header and the payload is not an initialize request. The session may have expired; re-initialize before retrying."
|
|
2522
|
+
)
|
|
2523
|
+
);
|
|
2524
|
+
return;
|
|
2525
|
+
}
|
|
2526
|
+
if (!admit("streamable-http", response)) return;
|
|
2527
|
+
await openStreamableSession(request, response, request.body);
|
|
2528
|
+
} catch (error) {
|
|
2529
|
+
emit({
|
|
2530
|
+
type: "session-error",
|
|
2531
|
+
kind: "streamable-http",
|
|
2532
|
+
sessionId,
|
|
2533
|
+
reason: describe2(error)
|
|
2534
|
+
});
|
|
2535
|
+
if (!response.headersSent) {
|
|
2536
|
+
response.status(500).json(
|
|
2537
|
+
rpcError(-32603, "Internal error while handling the MCP request.")
|
|
2538
|
+
);
|
|
2539
|
+
} else {
|
|
2540
|
+
response.end();
|
|
2541
|
+
}
|
|
2542
|
+
}
|
|
2543
|
+
};
|
|
2544
|
+
app.post(MCP_ROUTES.streamableHttp, ...guard, handleStreamable);
|
|
2545
|
+
app.get(MCP_ROUTES.streamableHttp, ...guard, handleStreamable);
|
|
2546
|
+
app.delete(MCP_ROUTES.streamableHttp, ...guard, handleStreamable);
|
|
2547
|
+
if (legacyEnabled) {
|
|
2548
|
+
app.get(MCP_ROUTES.sse, ...guard, async (request, response) => {
|
|
2549
|
+
if (allowedOrigins.length > 0) {
|
|
2550
|
+
const origin = readHeader(request.headers.origin);
|
|
2551
|
+
if (origin && !allowedOrigins.includes(origin)) {
|
|
2552
|
+
emit({ type: "session-rejected", kind: "sse", reason: "origin" });
|
|
2553
|
+
response.status(403).json(rpcError(-32e3, "Origin is not allowed."));
|
|
2554
|
+
return;
|
|
2555
|
+
}
|
|
2556
|
+
}
|
|
2557
|
+
if (!admit("sse", response)) return;
|
|
2558
|
+
const server = buildMcpServer(specProvider, contextProvider, {
|
|
2559
|
+
});
|
|
2560
|
+
const transport = new sse_js.SSEServerTransport(MCP_ROUTES.messages, response);
|
|
2561
|
+
const sessionId = transport.sessionId;
|
|
2562
|
+
const now = Date.now();
|
|
2563
|
+
const record = {
|
|
2564
|
+
sessionId,
|
|
2565
|
+
kind: "sse",
|
|
2566
|
+
transport: asTransport(transport),
|
|
2567
|
+
server,
|
|
2568
|
+
createdAt: now,
|
|
2569
|
+
lastActivityAt: now,
|
|
2570
|
+
closing: false,
|
|
2571
|
+
remoteAddress: request.ip,
|
|
2572
|
+
userAgent: readHeader(request.headers["user-agent"])
|
|
2573
|
+
};
|
|
2574
|
+
sessions.set(sessionId, record);
|
|
2575
|
+
response.on(
|
|
2576
|
+
"close",
|
|
2577
|
+
() => void destroySession(sessionId, "client-closed")
|
|
2578
|
+
);
|
|
2579
|
+
response.on(
|
|
2580
|
+
"error",
|
|
2581
|
+
() => void destroySession(sessionId, "stream-error")
|
|
2582
|
+
);
|
|
2583
|
+
try {
|
|
2584
|
+
await server.connect(asTransport(transport));
|
|
2585
|
+
} catch (error) {
|
|
2586
|
+
emit({
|
|
2587
|
+
type: "session-error",
|
|
2588
|
+
kind: "sse",
|
|
2589
|
+
sessionId,
|
|
2590
|
+
reason: describe2(error)
|
|
2591
|
+
});
|
|
2592
|
+
await destroySession(sessionId, "connect-failed");
|
|
2593
|
+
if (!response.headersSent) {
|
|
2594
|
+
response.status(500).json(rpcError(-32603, "Failed to initialize the MCP session."));
|
|
2595
|
+
}
|
|
2596
|
+
return;
|
|
2597
|
+
}
|
|
2598
|
+
chainOnClose(transport, () => {
|
|
2599
|
+
void destroySession(sessionId, "transport-closed");
|
|
2600
|
+
});
|
|
2601
|
+
chainOnError(transport, (error) => {
|
|
2602
|
+
emit({
|
|
2603
|
+
type: "session-error",
|
|
2604
|
+
kind: "sse",
|
|
2605
|
+
sessionId,
|
|
2606
|
+
reason: error.message
|
|
2607
|
+
});
|
|
2608
|
+
});
|
|
2609
|
+
record.keepAlive = setInterval(() => {
|
|
2610
|
+
if (response.writableEnded || response.destroyed) return;
|
|
2611
|
+
response.write(": keepalive\n\n", (error) => {
|
|
2612
|
+
if (error) void destroySession(sessionId, "keepalive-write-failed");
|
|
2613
|
+
});
|
|
2614
|
+
}, KEEPALIVE_INTERVAL_MS);
|
|
2615
|
+
record.keepAlive.unref?.();
|
|
2616
|
+
emit({ type: "session-opened", kind: "sse", sessionId });
|
|
2617
|
+
});
|
|
2618
|
+
app.post(MCP_ROUTES.messages, ...guard, async (request, response) => {
|
|
2619
|
+
const sessionId = readQueryValue(request.query.sessionId) || readHeader(request.headers["mcp-session-id"]) || "";
|
|
2620
|
+
if (!sessionId) {
|
|
2621
|
+
response.status(400).json(rpcError(-32e3, "A sessionId query parameter is required."));
|
|
2622
|
+
return;
|
|
2623
|
+
}
|
|
2624
|
+
const record = sessions.get(sessionId);
|
|
2625
|
+
if (!record || record.kind !== "sse" || record.closing) {
|
|
2626
|
+
response.status(404).json(rpcError(-32001, "Unknown or expired sessionId."));
|
|
2627
|
+
return;
|
|
2628
|
+
}
|
|
2629
|
+
touch(sessionId);
|
|
2630
|
+
try {
|
|
2631
|
+
await asSseTransport(record.transport).handlePostMessage(
|
|
2632
|
+
request,
|
|
2633
|
+
response,
|
|
2634
|
+
request.body
|
|
2635
|
+
);
|
|
2636
|
+
} catch (error) {
|
|
2637
|
+
emit({
|
|
2638
|
+
type: "session-error",
|
|
2639
|
+
kind: "sse",
|
|
2640
|
+
sessionId,
|
|
2641
|
+
reason: describe2(error)
|
|
2642
|
+
});
|
|
2643
|
+
if (!response.headersSent) {
|
|
2644
|
+
response.status(500).json(rpcError(-32603, "Failed to dispatch the MCP message."));
|
|
2645
|
+
} else {
|
|
2646
|
+
response.end();
|
|
2647
|
+
}
|
|
2648
|
+
}
|
|
2649
|
+
});
|
|
2650
|
+
}
|
|
2651
|
+
let sweeper;
|
|
2652
|
+
if (idleMs > 0) {
|
|
2653
|
+
sweeper = setInterval(() => {
|
|
2654
|
+
const cutoff = Date.now() - idleMs;
|
|
2655
|
+
for (const record of [...sessions.values()]) {
|
|
2656
|
+
if (record.lastActivityAt < cutoff) {
|
|
2657
|
+
void destroySession(record.sessionId, "idle-timeout");
|
|
2658
|
+
}
|
|
2659
|
+
}
|
|
2660
|
+
}, SWEEP_INTERVAL_MS);
|
|
2661
|
+
sweeper.unref?.();
|
|
2662
|
+
}
|
|
2663
|
+
async function closeAll() {
|
|
2664
|
+
await Promise.all(
|
|
2665
|
+
[...sessions.keys()].map(
|
|
2666
|
+
(sessionId) => destroySession(sessionId, "shutdown")
|
|
2667
|
+
)
|
|
2668
|
+
);
|
|
2669
|
+
const deadline = Date.now() + DRAIN_TIMEOUT_MS;
|
|
2670
|
+
while (closures.size > 0 && Date.now() < deadline) {
|
|
2671
|
+
await Promise.race([
|
|
2672
|
+
Promise.all([...closures.values()]),
|
|
2673
|
+
new Promise((resolve) => setTimeout(resolve, 100).unref?.())
|
|
2674
|
+
]);
|
|
2675
|
+
}
|
|
2676
|
+
}
|
|
2677
|
+
return {
|
|
2678
|
+
activeSessionCount: () => sessions.size,
|
|
2679
|
+
pendingSessionCount: () => pending,
|
|
2680
|
+
listSessions: () => [...sessions.values()].map((record) => ({
|
|
2681
|
+
sessionId: record.sessionId,
|
|
2682
|
+
kind: record.kind,
|
|
2683
|
+
createdAt: new Date(record.createdAt).toISOString(),
|
|
2684
|
+
lastActivityAt: new Date(record.lastActivityAt).toISOString(),
|
|
2685
|
+
remoteAddress: record.remoteAddress,
|
|
2686
|
+
userAgent: record.userAgent
|
|
2687
|
+
})),
|
|
2688
|
+
closeSession: (sessionId, reason = "closed-by-operator") => destroySession(sessionId, reason),
|
|
2689
|
+
closeAll,
|
|
2690
|
+
dispose: async () => {
|
|
2691
|
+
if (sweeper) clearInterval(sweeper);
|
|
2692
|
+
sweeper = void 0;
|
|
2693
|
+
await closeAll();
|
|
2694
|
+
}
|
|
2695
|
+
};
|
|
2696
|
+
}
|
|
2697
|
+
function attachSseRoutes(app, specProvider, contextProvider, routeGuard) {
|
|
2698
|
+
const handle = attachMcpRoutes(app, {
|
|
2699
|
+
specProvider,
|
|
2700
|
+
contextProvider,
|
|
2701
|
+
...routeGuard ? { routeGuard } : {}
|
|
2702
|
+
});
|
|
2703
|
+
return {
|
|
2704
|
+
activeSessionCount: handle.activeSessionCount,
|
|
2705
|
+
closeAll: handle.closeAll
|
|
2706
|
+
};
|
|
2707
|
+
}
|
|
2708
|
+
function safeCompare(left, right) {
|
|
2709
|
+
const digest = (value) => crypto.createHash("sha256").update(value, "utf8").digest();
|
|
2710
|
+
return crypto.timingSafeEqual(digest(left), digest(right));
|
|
2711
|
+
}
|
|
2712
|
+
function readBearerToken(header) {
|
|
2713
|
+
if (!header) return void 0;
|
|
2714
|
+
const match = /^bearer\s+(.+)$/i.exec(header.trim());
|
|
2715
|
+
return match?.[1]?.trim() || void 0;
|
|
2716
|
+
}
|
|
2717
|
+
function firstHeaderValue(value) {
|
|
2718
|
+
if (Array.isArray(value)) return value[0];
|
|
2719
|
+
return value;
|
|
2720
|
+
}
|
|
2721
|
+
function createAuthMiddleware(apiKey) {
|
|
2722
|
+
if (apiKey !== void 0 && apiKey.trim().length === 0) {
|
|
2723
|
+
throw new Error(
|
|
2724
|
+
"The configured API key is empty. Omit it entirely to run without authentication, or supply a non-empty value."
|
|
2725
|
+
);
|
|
2726
|
+
}
|
|
2727
|
+
const expected = apiKey;
|
|
2728
|
+
return (request, response, next) => {
|
|
2729
|
+
if (!expected) return next();
|
|
2730
|
+
if (request.method === "OPTIONS") return next();
|
|
2731
|
+
const presented = firstHeaderValue(request.headers["x-api-key"]) ?? readBearerToken(firstHeaderValue(request.headers.authorization));
|
|
2732
|
+
if (!presented || !safeCompare(presented, expected)) {
|
|
2733
|
+
response.status(401).set("WWW-Authenticate", 'Bearer realm="openapi-mcp"').set("Cache-Control", "no-store").json({ error: "Unauthorized" });
|
|
2734
|
+
return;
|
|
2735
|
+
}
|
|
2736
|
+
next();
|
|
2737
|
+
};
|
|
2738
|
+
}
|
|
2739
|
+
var STATE_FILE_MODE = 384;
|
|
2740
|
+
var STATE_DIR_MODE = 448;
|
|
2741
|
+
var STATE_FILE_NAME = "state.json";
|
|
2742
|
+
var APP_DIR_NAME = "openapi-mcp";
|
|
2743
|
+
function resolveStateFilePath() {
|
|
2744
|
+
const explicit = process.env.OPENAPI_MCP_STATE_FILE?.trim();
|
|
2745
|
+
if (explicit) return path__default.default.resolve(explicit);
|
|
2746
|
+
const xdg = process.env.XDG_CONFIG_HOME?.trim();
|
|
2747
|
+
const base = xdg ? path__default.default.resolve(xdg) : process.platform === "win32" ? path__default.default.join(process.env.APPDATA ?? os__default.default.homedir(), "") : path__default.default.join(os__default.default.homedir(), ".config");
|
|
2748
|
+
return path__default.default.join(base, APP_DIR_NAME, STATE_FILE_NAME);
|
|
2749
|
+
}
|
|
2750
|
+
function describe3(error) {
|
|
2751
|
+
return error instanceof Error ? error.message : String(error);
|
|
2752
|
+
}
|
|
2753
|
+
function toError(error) {
|
|
2754
|
+
return error instanceof Error ? error : new Error(String(error));
|
|
2755
|
+
}
|
|
2756
|
+
function safeReviver(key, value) {
|
|
2757
|
+
if (key === "__proto__" || key === "constructor" || key === "prototype") {
|
|
2758
|
+
return void 0;
|
|
2759
|
+
}
|
|
2760
|
+
return value;
|
|
2761
|
+
}
|
|
2762
|
+
function saveState(obj, stateFilePath = resolveStateFilePath()) {
|
|
2763
|
+
const dir = path__default.default.dirname(stateFilePath);
|
|
2764
|
+
let tempFile;
|
|
2765
|
+
try {
|
|
2766
|
+
fs2__default.default.mkdirSync(dir, { recursive: true, mode: STATE_DIR_MODE });
|
|
2767
|
+
const payload = JSON.stringify(obj, null, 2);
|
|
2768
|
+
tempFile = path__default.default.join(
|
|
2769
|
+
dir,
|
|
2770
|
+
`.${path__default.default.basename(stateFilePath)}.${process.pid}.${Date.now()}.tmp`
|
|
2771
|
+
);
|
|
2772
|
+
const handle = fs2__default.default.openSync(tempFile, "wx", STATE_FILE_MODE);
|
|
2773
|
+
try {
|
|
2774
|
+
fs2__default.default.writeFileSync(handle, payload, "utf8");
|
|
2775
|
+
fs2__default.default.fsyncSync(handle);
|
|
2776
|
+
} finally {
|
|
2777
|
+
fs2__default.default.closeSync(handle);
|
|
2778
|
+
}
|
|
2779
|
+
fs2__default.default.renameSync(tempFile, stateFilePath);
|
|
2780
|
+
tempFile = void 0;
|
|
2781
|
+
try {
|
|
2782
|
+
fs2__default.default.chmodSync(stateFilePath, STATE_FILE_MODE);
|
|
2783
|
+
} catch {
|
|
2784
|
+
}
|
|
2785
|
+
return { ok: true, path: stateFilePath };
|
|
2786
|
+
} catch (error) {
|
|
2787
|
+
console.warn(
|
|
2788
|
+
`[config-store] Failed to persist state to ${stateFilePath}: ${describe3(error)}`
|
|
2789
|
+
);
|
|
2790
|
+
return { ok: false, path: stateFilePath, error: toError(error) };
|
|
2791
|
+
} finally {
|
|
2792
|
+
if (tempFile) {
|
|
2793
|
+
try {
|
|
2794
|
+
fs2__default.default.unlinkSync(tempFile);
|
|
2795
|
+
} catch {
|
|
2796
|
+
}
|
|
2797
|
+
}
|
|
2798
|
+
}
|
|
2799
|
+
}
|
|
2800
|
+
function quarantine(stateFilePath) {
|
|
2801
|
+
const backup = `${stateFilePath}.corrupt-${Date.now()}`;
|
|
2802
|
+
try {
|
|
2803
|
+
fs2__default.default.renameSync(stateFilePath, backup);
|
|
2804
|
+
console.warn(`[config-store] Moved unreadable state file to ${backup}`);
|
|
2805
|
+
} catch (error) {
|
|
2806
|
+
console.warn(
|
|
2807
|
+
`[config-store] Could not quarantine ${stateFilePath}: ${describe3(error)}`
|
|
2808
|
+
);
|
|
2809
|
+
}
|
|
2810
|
+
}
|
|
2811
|
+
function loadState(stateFilePath = resolveStateFilePath()) {
|
|
2812
|
+
let raw;
|
|
2813
|
+
try {
|
|
2814
|
+
raw = fs2__default.default.readFileSync(stateFilePath, "utf8");
|
|
2815
|
+
} catch (error) {
|
|
2816
|
+
const code = error.code;
|
|
2817
|
+
if (code !== "ENOENT") {
|
|
2818
|
+
console.warn(
|
|
2819
|
+
`[config-store] Failed to read state from ${stateFilePath}: ${describe3(error)}`
|
|
2820
|
+
);
|
|
2821
|
+
}
|
|
2822
|
+
return {};
|
|
2823
|
+
}
|
|
2824
|
+
if (raw.trim() === "") return {};
|
|
2825
|
+
let parsed;
|
|
2826
|
+
try {
|
|
2827
|
+
parsed = JSON.parse(raw, safeReviver);
|
|
2828
|
+
} catch (error) {
|
|
2829
|
+
console.warn(
|
|
2830
|
+
`[config-store] State file is not valid JSON: ${describe3(error)}`
|
|
2831
|
+
);
|
|
2832
|
+
quarantine(stateFilePath);
|
|
2833
|
+
return {};
|
|
2834
|
+
}
|
|
2835
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
2836
|
+
console.warn("[config-store] State file does not contain a JSON object.");
|
|
2837
|
+
quarantine(stateFilePath);
|
|
2838
|
+
return {};
|
|
2839
|
+
}
|
|
2840
|
+
return parsed;
|
|
2841
|
+
}
|
|
2842
|
+
function clearState(stateFilePath = resolveStateFilePath()) {
|
|
2843
|
+
try {
|
|
2844
|
+
fs2__default.default.rmSync(stateFilePath, { force: true });
|
|
2845
|
+
return { ok: true, path: stateFilePath };
|
|
2846
|
+
} catch (error) {
|
|
2847
|
+
console.warn(
|
|
2848
|
+
`[config-store] Failed to clear state at ${stateFilePath}: ${describe3(error)}`
|
|
2849
|
+
);
|
|
2850
|
+
return { ok: false, path: stateFilePath, error: toError(error) };
|
|
2851
|
+
}
|
|
2852
|
+
}
|
|
2853
|
+
|
|
2854
|
+
// src/runtime/service-registry.ts
|
|
2855
|
+
var ServiceRegistry = class {
|
|
2856
|
+
records = /* @__PURE__ */ new Map();
|
|
2857
|
+
upsert(record) {
|
|
2858
|
+
this.records.set(record.id, record);
|
|
2859
|
+
}
|
|
2860
|
+
list() {
|
|
2861
|
+
return Array.from(this.records.values()).sort(
|
|
2862
|
+
(left, right) => left.createdAt.localeCompare(right.createdAt)
|
|
2863
|
+
);
|
|
2864
|
+
}
|
|
2865
|
+
get(id) {
|
|
2866
|
+
return this.records.get(id);
|
|
2867
|
+
}
|
|
2868
|
+
remove(id) {
|
|
2869
|
+
return this.records.delete(id);
|
|
2870
|
+
}
|
|
2871
|
+
clear() {
|
|
2872
|
+
this.records.clear();
|
|
2873
|
+
}
|
|
2874
|
+
start(id) {
|
|
2875
|
+
const current = this.records.get(id);
|
|
2876
|
+
if (!current) return void 0;
|
|
2877
|
+
const next = {
|
|
2878
|
+
...current,
|
|
2879
|
+
status: "running",
|
|
2880
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2881
|
+
lastError: void 0
|
|
2882
|
+
};
|
|
2883
|
+
this.records.set(id, next);
|
|
2884
|
+
return next;
|
|
2885
|
+
}
|
|
2886
|
+
stop(id) {
|
|
2887
|
+
const current = this.records.get(id);
|
|
2888
|
+
if (!current) return void 0;
|
|
2889
|
+
const next = {
|
|
2890
|
+
...current,
|
|
2891
|
+
status: "stopped",
|
|
2892
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2893
|
+
};
|
|
2894
|
+
this.records.set(id, next);
|
|
2895
|
+
return next;
|
|
2896
|
+
}
|
|
2897
|
+
fail(id, message) {
|
|
2898
|
+
const current = this.records.get(id);
|
|
2899
|
+
if (!current) return void 0;
|
|
2900
|
+
const next = {
|
|
2901
|
+
...current,
|
|
2902
|
+
status: "error",
|
|
2903
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2904
|
+
lastError: {
|
|
2905
|
+
message,
|
|
2906
|
+
at: (/* @__PURE__ */ new Date()).toISOString()
|
|
2907
|
+
}
|
|
2908
|
+
};
|
|
2909
|
+
this.records.set(id, next);
|
|
2910
|
+
return next;
|
|
2911
|
+
}
|
|
2912
|
+
};
|
|
2913
|
+
|
|
2914
|
+
// src/server/admin-server.ts
|
|
2915
|
+
var MODULE_DIR = path__default.default.dirname(url.fileURLToPath(importMetaUrl));
|
|
2916
|
+
var STATIC_DIR = [
|
|
2917
|
+
path__default.default.resolve(MODULE_DIR, "webui"),
|
|
2918
|
+
path__default.default.resolve(MODULE_DIR, "../webui"),
|
|
2919
|
+
path__default.default.resolve(MODULE_DIR, "../../webui")
|
|
2920
|
+
].find((candidate) => fs2__default.default.existsSync(path__default.default.join(candidate, "index.html"))) ?? path__default.default.resolve(MODULE_DIR, "webui");
|
|
2921
|
+
var INDEX_HTML = path__default.default.join(STATIC_DIR, "index.html");
|
|
2922
|
+
var MAX_UPLOAD_BYTES = 20 * 1024 * 1024;
|
|
2923
|
+
var JSON_BODY_LIMIT = "20mb";
|
|
2924
|
+
var DEFAULT_MAX_LOG_ENTRIES = 200;
|
|
2925
|
+
var PERSIST_DEBOUNCE_MS = 250;
|
|
2926
|
+
var REQUEST_TIMEOUT_MS = 3e5;
|
|
2927
|
+
var ENDPOINTS = {
|
|
2928
|
+
streamableHttp: MCP_ROUTES.streamableHttp,
|
|
2929
|
+
sse: MCP_ROUTES.sse,
|
|
2930
|
+
messages: MCP_ROUTES.messages,
|
|
2931
|
+
stdioSupported: true
|
|
2932
|
+
};
|
|
2933
|
+
function readParam(value) {
|
|
2934
|
+
if (typeof value === "string") {
|
|
2935
|
+
const trimmed = value.trim();
|
|
2936
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
2937
|
+
}
|
|
2938
|
+
if (Array.isArray(value)) {
|
|
2939
|
+
for (const candidate of value) {
|
|
2940
|
+
if (typeof candidate === "string" && candidate.trim().length > 0) {
|
|
2941
|
+
return candidate.trim();
|
|
2942
|
+
}
|
|
2943
|
+
}
|
|
2944
|
+
}
|
|
2945
|
+
return null;
|
|
2946
|
+
}
|
|
2947
|
+
function newLogId() {
|
|
2948
|
+
try {
|
|
2949
|
+
return crypto.randomUUID();
|
|
2950
|
+
} catch {
|
|
2951
|
+
return `log_${Date.now()}_${Math.random().toString(16).slice(2)}`;
|
|
2952
|
+
}
|
|
2953
|
+
}
|
|
2954
|
+
function stripBom(text) {
|
|
2955
|
+
return text.charCodeAt(0) === 65279 ? text.slice(1) : text;
|
|
2956
|
+
}
|
|
2957
|
+
function shouldParseAsYaml(raw) {
|
|
2958
|
+
const trimmed = stripBom(raw).trimStart();
|
|
2959
|
+
return !(trimmed.startsWith("{") || trimmed.startsWith("["));
|
|
2960
|
+
}
|
|
2961
|
+
async function parseWithFallback(raw, preferYaml) {
|
|
2962
|
+
try {
|
|
2963
|
+
return {
|
|
2964
|
+
spec: await parseSpecContent(raw, preferYaml),
|
|
2965
|
+
isYaml: preferYaml
|
|
2966
|
+
};
|
|
2967
|
+
} catch (firstError) {
|
|
2968
|
+
try {
|
|
2969
|
+
return {
|
|
2970
|
+
spec: await parseSpecContent(raw, !preferYaml),
|
|
2971
|
+
isYaml: !preferYaml
|
|
2972
|
+
};
|
|
2973
|
+
} catch {
|
|
2974
|
+
throw firstError;
|
|
2975
|
+
}
|
|
2976
|
+
}
|
|
2977
|
+
}
|
|
2978
|
+
function computeServiceId(title, version, rawText) {
|
|
2979
|
+
const digest = crypto.createHash("sha256").update(`${title}\0${version}\0${rawText}`).digest("hex").slice(0, 12);
|
|
2980
|
+
return `svc_${digest}`;
|
|
2981
|
+
}
|
|
2982
|
+
function buildCorsOptions(config) {
|
|
2983
|
+
const allowed = config.allowedOrigins;
|
|
2984
|
+
if (!allowed || allowed.length === 0) return { origin: false };
|
|
2985
|
+
if (allowed.includes("*")) return { origin: true, credentials: false };
|
|
2986
|
+
const allowSet = new Set(allowed);
|
|
2987
|
+
return {
|
|
2988
|
+
origin(origin, callback) {
|
|
2989
|
+
if (!origin || allowSet.has(origin)) {
|
|
2990
|
+
callback(null, true);
|
|
2991
|
+
return;
|
|
2992
|
+
}
|
|
2993
|
+
callback(null, false);
|
|
2994
|
+
},
|
|
2995
|
+
credentials: true
|
|
2996
|
+
};
|
|
2997
|
+
}
|
|
2998
|
+
function toMessage(error) {
|
|
2999
|
+
return error instanceof Error ? error.message : String(error);
|
|
3000
|
+
}
|
|
3001
|
+
function formatIssue(issue) {
|
|
3002
|
+
const method = issue.method ? `${issue.method.toUpperCase()} ` : "";
|
|
3003
|
+
return `${method}${issue.path}: ${issue.message}`;
|
|
3004
|
+
}
|
|
3005
|
+
var promptCache = /* @__PURE__ */ new WeakMap();
|
|
3006
|
+
var resourceCache = /* @__PURE__ */ new WeakMap();
|
|
3007
|
+
function cachedPrompts(spec) {
|
|
3008
|
+
const key = spec;
|
|
3009
|
+
const hit = promptCache.get(key);
|
|
3010
|
+
if (hit) return hit;
|
|
3011
|
+
const value = generatePrompts(spec);
|
|
3012
|
+
promptCache.set(key, value);
|
|
3013
|
+
return value;
|
|
3014
|
+
}
|
|
3015
|
+
function cachedResources(spec) {
|
|
3016
|
+
const key = spec;
|
|
3017
|
+
const hit = resourceCache.get(key);
|
|
3018
|
+
if (hit) return hit;
|
|
3019
|
+
const value = generateResources();
|
|
3020
|
+
resourceCache.set(key, value);
|
|
3021
|
+
return value;
|
|
3022
|
+
}
|
|
3023
|
+
async function startAdminServer(config) {
|
|
3024
|
+
const app = express2__default.default();
|
|
3025
|
+
const auth = createAuthMiddleware(config.apiKey);
|
|
3026
|
+
const registry = new ServiceRegistry();
|
|
3027
|
+
const persistEnabled = config.persistState !== false;
|
|
3028
|
+
const maxLogEntries = Math.max(
|
|
3029
|
+
20,
|
|
3030
|
+
config.maxLogEntries ?? DEFAULT_MAX_LOG_ENTRIES
|
|
3031
|
+
);
|
|
3032
|
+
const state = {
|
|
3033
|
+
spec: null,
|
|
3034
|
+
baseUrlOverride: config.baseUrlOverride,
|
|
3035
|
+
specSource: config.specPath ? "startup-file" : "runtime"
|
|
3036
|
+
};
|
|
3037
|
+
const logEntries = [];
|
|
3038
|
+
let activeServiceId = null;
|
|
3039
|
+
let activeSpecRawText = "";
|
|
3040
|
+
let activeSpecIsYaml = false;
|
|
3041
|
+
let closed = false;
|
|
3042
|
+
app.disable("x-powered-by");
|
|
3043
|
+
app.use(cors__default.default(buildCorsOptions(config)));
|
|
3044
|
+
app.use(express2__default.default.json({ limit: JSON_BODY_LIMIT }));
|
|
3045
|
+
function addLog(entry) {
|
|
3046
|
+
logEntries.unshift({
|
|
3047
|
+
...entry,
|
|
3048
|
+
serviceId: entry.serviceId ?? activeServiceId ?? void 0
|
|
3049
|
+
});
|
|
3050
|
+
if (logEntries.length > maxLogEntries) {
|
|
3051
|
+
logEntries.length = maxLogEntries;
|
|
3052
|
+
}
|
|
3053
|
+
}
|
|
3054
|
+
function addInternalLog(event, success, serviceId, extra, error) {
|
|
3055
|
+
addLog({
|
|
3056
|
+
id: newLogId(),
|
|
3057
|
+
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3058
|
+
direction: "internal",
|
|
3059
|
+
protocol: void 0,
|
|
3060
|
+
success,
|
|
3061
|
+
serviceId,
|
|
3062
|
+
error,
|
|
3063
|
+
meta: { event, ...extra }
|
|
3064
|
+
});
|
|
3065
|
+
}
|
|
3066
|
+
function isActive() {
|
|
3067
|
+
if (!state.spec || !activeServiceId) return false;
|
|
3068
|
+
return registry.get(activeServiceId)?.status === "running";
|
|
3069
|
+
}
|
|
3070
|
+
const contextProvider = () => ({
|
|
3071
|
+
baseUrlOverride: state.baseUrlOverride,
|
|
3072
|
+
upstreamHeaders: config.upstreamHeaders,
|
|
3073
|
+
requestTimeoutMs: config.requestTimeoutMs,
|
|
3074
|
+
onLog: addLog
|
|
3075
|
+
});
|
|
3076
|
+
const mcp = attachMcpRoutes(app, {
|
|
3077
|
+
specProvider: () => isActive() ? state.spec : null,
|
|
3078
|
+
contextProvider,
|
|
3079
|
+
...config.apiKey ? { routeGuard: auth } : {},
|
|
3080
|
+
...config.allowedOrigins?.length ? { allowedOrigins: config.allowedOrigins } : {},
|
|
3081
|
+
...config.allowedHosts?.length ? { allowedHosts: config.allowedHosts } : {},
|
|
3082
|
+
onEvent(event) {
|
|
3083
|
+
addInternalLog(
|
|
3084
|
+
event.type,
|
|
3085
|
+
event.type !== "session-error",
|
|
3086
|
+
void 0,
|
|
3087
|
+
// Spread conditionally: several event types genuinely carry no session
|
|
3088
|
+
// id or reason, and writing explicit undefined into the log record made
|
|
3089
|
+
// the viewer render empty fields for every rejection.
|
|
3090
|
+
{
|
|
3091
|
+
transport: event.kind,
|
|
3092
|
+
...event.sessionId ? { sessionId: event.sessionId } : {},
|
|
3093
|
+
...event.reason ? { reason: event.reason } : {}
|
|
3094
|
+
}
|
|
3095
|
+
);
|
|
3096
|
+
}
|
|
3097
|
+
});
|
|
3098
|
+
async function closeSessions(reason) {
|
|
3099
|
+
const before = mcp.activeSessionCount();
|
|
3100
|
+
if (before === 0) return;
|
|
3101
|
+
await mcp.closeAll();
|
|
3102
|
+
addInternalLog("sessions-closed", true, void 0, {
|
|
3103
|
+
reason,
|
|
3104
|
+
closed: before
|
|
3105
|
+
});
|
|
3106
|
+
}
|
|
3107
|
+
let persistTimer;
|
|
3108
|
+
let persistPending = null;
|
|
3109
|
+
function flushState() {
|
|
3110
|
+
if (persistTimer) {
|
|
3111
|
+
clearTimeout(persistTimer);
|
|
3112
|
+
persistTimer = void 0;
|
|
3113
|
+
}
|
|
3114
|
+
const payload = persistPending;
|
|
3115
|
+
persistPending = null;
|
|
3116
|
+
if (!payload) return;
|
|
3117
|
+
let result;
|
|
3118
|
+
try {
|
|
3119
|
+
result = saveState(
|
|
3120
|
+
{
|
|
3121
|
+
// Explicit null rather than undefined: JSON.stringify drops undefined
|
|
3122
|
+
// keys, making "no specification" indistinguishable from a truncated
|
|
3123
|
+
// state file on the next read.
|
|
3124
|
+
specRaw: payload.specRaw,
|
|
3125
|
+
specIsYaml: payload.specIsYaml,
|
|
3126
|
+
baseUrlOverride: state.baseUrlOverride ?? null
|
|
3127
|
+
},
|
|
3128
|
+
config.stateFilePath
|
|
3129
|
+
);
|
|
3130
|
+
} catch (error) {
|
|
3131
|
+
addInternalLog(
|
|
3132
|
+
"state-persist-failed",
|
|
3133
|
+
false,
|
|
3134
|
+
void 0,
|
|
3135
|
+
void 0,
|
|
3136
|
+
toMessage(error)
|
|
3137
|
+
);
|
|
3138
|
+
return;
|
|
3139
|
+
}
|
|
3140
|
+
if (!result.ok) {
|
|
3141
|
+
addInternalLog(
|
|
3142
|
+
"state-persist-failed",
|
|
3143
|
+
false,
|
|
3144
|
+
void 0,
|
|
3145
|
+
void 0,
|
|
3146
|
+
`${result.path}: ${result.error?.message ?? "unknown error"}`
|
|
3147
|
+
);
|
|
3148
|
+
return;
|
|
3149
|
+
}
|
|
3150
|
+
addInternalLog("state-persisted", true, void 0, { path: result.path });
|
|
3151
|
+
}
|
|
3152
|
+
function queueState(specRaw, specIsYaml) {
|
|
3153
|
+
if (!persistEnabled) return;
|
|
3154
|
+
persistPending = { specRaw, specIsYaml };
|
|
3155
|
+
if (persistTimer) return;
|
|
3156
|
+
persistTimer = setTimeout(() => {
|
|
3157
|
+
persistTimer = void 0;
|
|
3158
|
+
flushState();
|
|
3159
|
+
}, PERSIST_DEBOUNCE_MS);
|
|
3160
|
+
persistTimer.unref?.();
|
|
3161
|
+
}
|
|
3162
|
+
function persistSpec(rawText, isYaml) {
|
|
3163
|
+
queueState(rawText, isYaml);
|
|
3164
|
+
}
|
|
3165
|
+
function persistConfig() {
|
|
3166
|
+
queueState(activeSpecRawText || null, activeSpecIsYaml);
|
|
3167
|
+
}
|
|
3168
|
+
function applySpec(spec, rawText, isYaml, source, writeThrough = true) {
|
|
3169
|
+
const generated = getGeneratedTools(spec);
|
|
3170
|
+
const prompts = cachedPrompts(spec);
|
|
3171
|
+
const resources = cachedResources(spec);
|
|
3172
|
+
const previousServiceId = activeServiceId;
|
|
3173
|
+
state.spec = spec;
|
|
3174
|
+
state.specSource = source;
|
|
3175
|
+
activeSpecRawText = rawText;
|
|
3176
|
+
activeSpecIsYaml = isYaml;
|
|
3177
|
+
const title = spec.info?.title ?? "Untitled API";
|
|
3178
|
+
const version = spec.info?.version ?? "0.0.0";
|
|
3179
|
+
const serviceId = computeServiceId(title, version, rawText);
|
|
3180
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
3181
|
+
const existing = registry.get(serviceId);
|
|
3182
|
+
registry.upsert({
|
|
3183
|
+
id: serviceId,
|
|
3184
|
+
createdAt: existing?.createdAt ?? now,
|
|
3185
|
+
updatedAt: now,
|
|
3186
|
+
// Reloading an identical document must not silently resurrect a service
|
|
3187
|
+
// the operator deliberately stopped.
|
|
3188
|
+
status: existing?.status ?? "running",
|
|
3189
|
+
title,
|
|
3190
|
+
version,
|
|
3191
|
+
source,
|
|
3192
|
+
toolCount: generated.tools.length,
|
|
3193
|
+
promptCount: prompts.length,
|
|
3194
|
+
resourceCount: resources.length,
|
|
3195
|
+
endpoint: { ...ENDPOINTS }
|
|
3196
|
+
});
|
|
3197
|
+
for (const id of registry.list().map((service) => service.id)) {
|
|
3198
|
+
if (id !== serviceId) registry.remove(id);
|
|
3199
|
+
}
|
|
3200
|
+
activeServiceId = serviceId;
|
|
3201
|
+
if (writeThrough) persistSpec(rawText, isYaml);
|
|
3202
|
+
if (previousServiceId && previousServiceId !== serviceId) {
|
|
3203
|
+
void closeSessions("specification-replaced").catch((error) => {
|
|
3204
|
+
addInternalLog(
|
|
3205
|
+
"sessions-close-failed",
|
|
3206
|
+
false,
|
|
3207
|
+
serviceId,
|
|
3208
|
+
void 0,
|
|
3209
|
+
toMessage(error)
|
|
3210
|
+
);
|
|
3211
|
+
});
|
|
3212
|
+
}
|
|
3213
|
+
addInternalLog("spec-applied", true, serviceId, {
|
|
3214
|
+
source,
|
|
3215
|
+
toolCount: generated.tools.length,
|
|
3216
|
+
promptCount: prompts.length,
|
|
3217
|
+
resourceCount: resources.length,
|
|
3218
|
+
degradedToolCount: generated.bindings.filter((b) => !b.fullySupported).length
|
|
3219
|
+
});
|
|
3220
|
+
return {
|
|
3221
|
+
serviceId,
|
|
3222
|
+
toolCount: generated.tools.length,
|
|
3223
|
+
promptCount: prompts.length,
|
|
3224
|
+
resourceCount: resources.length,
|
|
3225
|
+
degradedTools: generated.bindings.filter((binding) => !binding.fullySupported).map((binding) => binding.toolName),
|
|
3226
|
+
issues: generated.issues.map(formatIssue)
|
|
3227
|
+
};
|
|
3228
|
+
}
|
|
3229
|
+
async function clearSpec() {
|
|
3230
|
+
state.spec = null;
|
|
3231
|
+
state.specSource = "runtime";
|
|
3232
|
+
activeSpecRawText = "";
|
|
3233
|
+
activeSpecIsYaml = false;
|
|
3234
|
+
activeServiceId = null;
|
|
3235
|
+
registry.clear();
|
|
3236
|
+
if (persistEnabled) {
|
|
3237
|
+
persistPending = null;
|
|
3238
|
+
if (persistTimer) {
|
|
3239
|
+
clearTimeout(persistTimer);
|
|
3240
|
+
persistTimer = void 0;
|
|
3241
|
+
}
|
|
3242
|
+
const result = clearState(config.stateFilePath);
|
|
3243
|
+
if (!result.ok) {
|
|
3244
|
+
addInternalLog(
|
|
3245
|
+
"state-clear-failed",
|
|
3246
|
+
false,
|
|
3247
|
+
void 0,
|
|
3248
|
+
void 0,
|
|
3249
|
+
`${result.path}: ${result.error?.message ?? "unknown error"}`
|
|
3250
|
+
);
|
|
3251
|
+
}
|
|
3252
|
+
}
|
|
3253
|
+
await closeSessions("specification-cleared");
|
|
3254
|
+
addInternalLog("spec-cleared", true);
|
|
3255
|
+
}
|
|
3256
|
+
async function bootstrap() {
|
|
3257
|
+
if (config.specPath) {
|
|
3258
|
+
const raw = stripBom(await fs2__default.default.promises.readFile(config.specPath, "utf8"));
|
|
3259
|
+
const parsed = await parseWithFallback(raw, shouldParseAsYaml(raw));
|
|
3260
|
+
applySpec(parsed.spec, raw, parsed.isYaml, "startup-file", false);
|
|
3261
|
+
return;
|
|
3262
|
+
}
|
|
3263
|
+
if (!persistEnabled) return;
|
|
3264
|
+
const saved = loadState(config.stateFilePath);
|
|
3265
|
+
if (typeof saved.baseUrlOverride === "string" && !state.baseUrlOverride) {
|
|
3266
|
+
state.baseUrlOverride = saved.baseUrlOverride;
|
|
3267
|
+
}
|
|
3268
|
+
if (typeof saved.specRaw === "string" && saved.specRaw.trim()) {
|
|
3269
|
+
const raw = stripBom(saved.specRaw);
|
|
3270
|
+
try {
|
|
3271
|
+
const parsed = await parseWithFallback(
|
|
3272
|
+
raw,
|
|
3273
|
+
typeof saved.specIsYaml === "boolean" ? saved.specIsYaml : shouldParseAsYaml(raw)
|
|
3274
|
+
);
|
|
3275
|
+
applySpec(parsed.spec, raw, parsed.isYaml, "runtime", false);
|
|
3276
|
+
} catch (error) {
|
|
3277
|
+
addInternalLog(
|
|
3278
|
+
"spec-restore-failed",
|
|
3279
|
+
false,
|
|
3280
|
+
void 0,
|
|
3281
|
+
void 0,
|
|
3282
|
+
`Persisted specification could not be restored: ${toMessage(error)}`
|
|
3283
|
+
);
|
|
3284
|
+
}
|
|
3285
|
+
}
|
|
3286
|
+
}
|
|
3287
|
+
await bootstrap();
|
|
3288
|
+
function buildStatus() {
|
|
3289
|
+
const generated = state.spec ? getGeneratedTools(state.spec) : { tools: [], bindings: [], issues: [] };
|
|
3290
|
+
const prompts = state.spec ? cachedPrompts(state.spec) : [];
|
|
3291
|
+
const resources = state.spec ? cachedResources(state.spec) : [];
|
|
3292
|
+
return {
|
|
3293
|
+
specLoaded: Boolean(state.spec),
|
|
3294
|
+
specSource: state.specSource ?? null,
|
|
3295
|
+
serviceId: activeServiceId,
|
|
3296
|
+
active: isActive(),
|
|
3297
|
+
title: state.spec?.info?.title ?? null,
|
|
3298
|
+
version: state.spec?.info?.version ?? null,
|
|
3299
|
+
baseUrl: state.baseUrlOverride ?? state.spec?.servers?.[0]?.url ?? null,
|
|
3300
|
+
toolCount: generated.tools.length,
|
|
3301
|
+
degradedToolCount: generated.bindings.filter(
|
|
3302
|
+
(binding) => !binding.fullySupported
|
|
3303
|
+
).length,
|
|
3304
|
+
promptCount: prompts.length,
|
|
3305
|
+
resourceCount: resources.length,
|
|
3306
|
+
activeSessions: mcp.activeSessionCount(),
|
|
3307
|
+
pendingSessions: mcp.pendingSessionCount(),
|
|
3308
|
+
//Object literal may only specify known properties, and 'pendingSessions' does not exist in type 'AdminStatus'.ts(2353)
|
|
3309
|
+
// Surfaced so the web UI can warn that changes will not survive a
|
|
3310
|
+
// restart; previously this was silently invisible to the operator.
|
|
3311
|
+
persistEnabled,
|
|
3312
|
+
authRequired: Boolean(config.apiKey),
|
|
3313
|
+
services: registry.list(),
|
|
3314
|
+
issues: generated.issues.map(formatIssue),
|
|
3315
|
+
lastUpdatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3316
|
+
};
|
|
3317
|
+
}
|
|
3318
|
+
const upload = multer__default.default({
|
|
3319
|
+
storage: multer__default.default.memoryStorage(),
|
|
3320
|
+
limits: { fileSize: MAX_UPLOAD_BYTES, files: 1 }
|
|
3321
|
+
});
|
|
3322
|
+
app.post(
|
|
3323
|
+
"/api/spec/upload",
|
|
3324
|
+
auth,
|
|
3325
|
+
upload.single("file"),
|
|
3326
|
+
async (request, response) => {
|
|
3327
|
+
try {
|
|
3328
|
+
if (!request.file) {
|
|
3329
|
+
response.status(400).json({ error: "No file was uploaded." });
|
|
3330
|
+
return;
|
|
3331
|
+
}
|
|
3332
|
+
const rawText = stripBom(request.file.buffer.toString("utf8"));
|
|
3333
|
+
if (!rawText.trim()) {
|
|
3334
|
+
response.status(400).json({ error: "The uploaded file is empty." });
|
|
3335
|
+
return;
|
|
3336
|
+
}
|
|
3337
|
+
const parsed = await parseWithFallback(
|
|
3338
|
+
rawText,
|
|
3339
|
+
shouldParseAsYaml(rawText)
|
|
3340
|
+
);
|
|
3341
|
+
const result = applySpec(parsed.spec, rawText, parsed.isYaml, "upload");
|
|
3342
|
+
response.json({ ok: true, result, ...buildStatus() });
|
|
3343
|
+
} catch (error) {
|
|
3344
|
+
response.status(400).json({ error: toMessage(error) });
|
|
3345
|
+
}
|
|
3346
|
+
}
|
|
3347
|
+
);
|
|
3348
|
+
app.post("/api/spec/paste", auth, async (request, response) => {
|
|
3349
|
+
try {
|
|
3350
|
+
const content = request.body?.content;
|
|
3351
|
+
if (typeof content !== "string" || !content.trim()) {
|
|
3352
|
+
response.status(400).json({ error: "content must be a non-empty string." });
|
|
3353
|
+
return;
|
|
3354
|
+
}
|
|
3355
|
+
const raw = stripBom(content);
|
|
3356
|
+
const isYamlHint = request.body.isYaml;
|
|
3357
|
+
const preferYaml = typeof isYamlHint === "boolean" ? isYamlHint : shouldParseAsYaml(raw);
|
|
3358
|
+
const parsed = await parseWithFallback(raw, preferYaml);
|
|
3359
|
+
const result = applySpec(parsed.spec, raw, parsed.isYaml, "paste");
|
|
3360
|
+
response.json({ ok: true, result, ...buildStatus() });
|
|
3361
|
+
} catch (error) {
|
|
3362
|
+
response.status(400).json({ error: toMessage(error) });
|
|
3363
|
+
}
|
|
3364
|
+
});
|
|
3365
|
+
app.post("/api/spec/clear", auth, async (_request, response) => {
|
|
3366
|
+
await clearSpec();
|
|
3367
|
+
response.json({ ok: true, ...buildStatus() });
|
|
3368
|
+
});
|
|
3369
|
+
app.get("/api/spec/raw", auth, (_request, response) => {
|
|
3370
|
+
response.json({
|
|
3371
|
+
content: activeSpecRawText,
|
|
3372
|
+
isYaml: activeSpecIsYaml,
|
|
3373
|
+
source: state.specSource ?? null
|
|
3374
|
+
});
|
|
3375
|
+
});
|
|
3376
|
+
app.post("/api/config/base-url", auth, (request, response) => {
|
|
3377
|
+
const value = request.body?.baseUrl;
|
|
3378
|
+
if (value === null || value === void 0 || value === "") {
|
|
3379
|
+
state.baseUrlOverride = void 0;
|
|
3380
|
+
persistConfig();
|
|
3381
|
+
addInternalLog("base-url-cleared", true);
|
|
3382
|
+
response.json({ ok: true, ...buildStatus() });
|
|
3383
|
+
return;
|
|
3384
|
+
}
|
|
3385
|
+
if (typeof value !== "string") {
|
|
3386
|
+
response.status(400).json({ error: "baseUrl must be a string or null." });
|
|
3387
|
+
return;
|
|
3388
|
+
}
|
|
3389
|
+
let parsedUrl;
|
|
3390
|
+
try {
|
|
3391
|
+
parsedUrl = new URL(value);
|
|
3392
|
+
} catch {
|
|
3393
|
+
response.status(400).json({ error: "baseUrl is not a valid URL." });
|
|
3394
|
+
return;
|
|
3395
|
+
}
|
|
3396
|
+
if (parsedUrl.protocol !== "http:" && parsedUrl.protocol !== "https:") {
|
|
3397
|
+
response.status(400).json({ error: "baseUrl must use the http or https scheme." });
|
|
3398
|
+
return;
|
|
3399
|
+
}
|
|
3400
|
+
state.baseUrlOverride = value;
|
|
3401
|
+
persistConfig();
|
|
3402
|
+
addInternalLog("base-url-updated", true, void 0, { baseUrl: value });
|
|
3403
|
+
response.json({ ok: true, ...buildStatus() });
|
|
3404
|
+
});
|
|
3405
|
+
app.get("/api/status", auth, (_request, response) => {
|
|
3406
|
+
response.json(buildStatus());
|
|
3407
|
+
});
|
|
3408
|
+
app.get("/api/tools", auth, (_request, response) => {
|
|
3409
|
+
if (!state.spec) {
|
|
3410
|
+
response.json({ tools: [], bindings: [], issues: [] });
|
|
3411
|
+
return;
|
|
3412
|
+
}
|
|
3413
|
+
const generated = getGeneratedTools(state.spec);
|
|
3414
|
+
response.json({
|
|
3415
|
+
tools: generated.tools,
|
|
3416
|
+
bindings: generated.bindings.map((binding) => ({
|
|
3417
|
+
toolName: binding.toolName,
|
|
3418
|
+
method: binding.method,
|
|
3419
|
+
isStandardMethod: binding.isStandardMethod,
|
|
3420
|
+
path: binding.path,
|
|
3421
|
+
bodyKey: binding.bodyKey,
|
|
3422
|
+
bodyMediaType: binding.bodyMediaType,
|
|
3423
|
+
bodyRequired: binding.bodyRequired,
|
|
3424
|
+
fullySupported: binding.fullySupported,
|
|
3425
|
+
degradationReasons: binding.degradationReasons,
|
|
3426
|
+
omittedParameters: binding.omittedParameters,
|
|
3427
|
+
// Projected because the web UI renders it; omitting it silently blanked
|
|
3428
|
+
// the multipart and serialisation notes.
|
|
3429
|
+
usageNotes: binding.usageNotes,
|
|
3430
|
+
arguments: binding.arguments.map((argument) => ({
|
|
3431
|
+
key: argument.key,
|
|
3432
|
+
name: argument.name,
|
|
3433
|
+
in: argument.in,
|
|
3434
|
+
required: argument.parameter.required === true
|
|
3435
|
+
}))
|
|
3436
|
+
})),
|
|
3437
|
+
issues: generated.issues.map(formatIssue)
|
|
3438
|
+
});
|
|
3439
|
+
});
|
|
3440
|
+
app.get("/api/prompts", auth, (_request, response) => {
|
|
3441
|
+
response.json({ prompts: state.spec ? cachedPrompts(state.spec) : [] });
|
|
3442
|
+
});
|
|
3443
|
+
app.get("/api/resources", auth, (_request, response) => {
|
|
3444
|
+
response.json({
|
|
3445
|
+
resources: state.spec ? cachedResources(state.spec) : []
|
|
3446
|
+
});
|
|
3447
|
+
});
|
|
3448
|
+
app.get("/api/servers", auth, (_request, response) => {
|
|
3449
|
+
response.json({
|
|
3450
|
+
services: registry.list(),
|
|
3451
|
+
active: isActive(),
|
|
3452
|
+
endpoints: { ...ENDPOINTS }
|
|
3453
|
+
});
|
|
3454
|
+
});
|
|
3455
|
+
app.get("/api/sessions", auth, (_request, response) => {
|
|
3456
|
+
response.json({
|
|
3457
|
+
sessions: mcp.listSessions(),
|
|
3458
|
+
total: mcp.activeSessionCount(),
|
|
3459
|
+
pending: mcp.pendingSessionCount()
|
|
3460
|
+
});
|
|
3461
|
+
});
|
|
3462
|
+
app.delete("/api/sessions/:id", auth, async (request, response) => {
|
|
3463
|
+
const id = readParam(request.params.id);
|
|
3464
|
+
if (!id) {
|
|
3465
|
+
response.status(400).json({ error: "A session id is required." });
|
|
3466
|
+
return;
|
|
3467
|
+
}
|
|
3468
|
+
const removed = await mcp.closeSession(id, "closed-by-operator");
|
|
3469
|
+
if (!removed) {
|
|
3470
|
+
response.status(404).json({ error: "Session not found." });
|
|
3471
|
+
return;
|
|
3472
|
+
}
|
|
3473
|
+
response.json({ ok: true, sessions: mcp.listSessions() });
|
|
3474
|
+
});
|
|
3475
|
+
app.post("/api/servers/:id/start", auth, (request, response) => {
|
|
3476
|
+
const id = readParam(request.params.id);
|
|
3477
|
+
if (!id) {
|
|
3478
|
+
response.status(400).json({ error: "A service id is required." });
|
|
3479
|
+
return;
|
|
3480
|
+
}
|
|
3481
|
+
if (!registry.get(id)) {
|
|
3482
|
+
response.status(404).json({ error: "Service not found." });
|
|
3483
|
+
return;
|
|
3484
|
+
}
|
|
3485
|
+
if (!state.spec) {
|
|
3486
|
+
response.status(409).json({ error: "No specification is loaded; nothing to start." });
|
|
3487
|
+
return;
|
|
3488
|
+
}
|
|
3489
|
+
registry.start(id);
|
|
3490
|
+
activeServiceId = id;
|
|
3491
|
+
addInternalLog("service-started", true, id);
|
|
3492
|
+
response.json({ ok: true, ...buildStatus() });
|
|
3493
|
+
});
|
|
3494
|
+
app.post("/api/servers/:id/stop", auth, async (request, response) => {
|
|
3495
|
+
const id = readParam(request.params.id);
|
|
3496
|
+
if (!id) {
|
|
3497
|
+
response.status(400).json({ error: "A service id is required." });
|
|
3498
|
+
return;
|
|
3499
|
+
}
|
|
3500
|
+
if (!registry.get(id)) {
|
|
3501
|
+
response.status(404).json({ error: "Service not found." });
|
|
3502
|
+
return;
|
|
3503
|
+
}
|
|
3504
|
+
registry.stop(id);
|
|
3505
|
+
await closeSessions("service-stopped");
|
|
3506
|
+
addInternalLog("service-stopped", true, id);
|
|
3507
|
+
response.json({ ok: true, ...buildStatus() });
|
|
3508
|
+
});
|
|
3509
|
+
app.get("/api/logs", auth, (request, response) => {
|
|
3510
|
+
const rawLimit = Number(request.query.limit);
|
|
3511
|
+
const limit = Number.isFinite(rawLimit) ? Math.min(Math.max(Math.trunc(rawLimit), 1), maxLogEntries) : maxLogEntries;
|
|
3512
|
+
const direction = readParam(request.query.direction);
|
|
3513
|
+
const filtered = direction ? logEntries.filter((entry) => entry.direction === direction) : logEntries;
|
|
3514
|
+
response.json({
|
|
3515
|
+
logs: filtered.slice(0, limit),
|
|
3516
|
+
total: filtered.length,
|
|
3517
|
+
capacity: maxLogEntries
|
|
3518
|
+
});
|
|
3519
|
+
});
|
|
3520
|
+
app.post("/api/logs/clear", auth, (_request, response) => {
|
|
3521
|
+
logEntries.length = 0;
|
|
3522
|
+
response.json({ ok: true, logs: [] });
|
|
3523
|
+
});
|
|
3524
|
+
app.post("/api/debug/call-tool", auth, async (request, response) => {
|
|
3525
|
+
if (!state.spec) {
|
|
3526
|
+
response.status(409).json({ error: "No specification is loaded." });
|
|
3527
|
+
return;
|
|
3528
|
+
}
|
|
3529
|
+
const body = request.body ?? {};
|
|
3530
|
+
const toolName = body.toolName;
|
|
3531
|
+
const args = body.arguments;
|
|
3532
|
+
if (typeof toolName !== "string" || !toolName.trim()) {
|
|
3533
|
+
response.status(400).json({ error: "toolName must be a non-empty string." });
|
|
3534
|
+
return;
|
|
3535
|
+
}
|
|
3536
|
+
if (args !== void 0 && (typeof args !== "object" || args === null || Array.isArray(args))) {
|
|
3537
|
+
response.status(400).json({ error: "arguments must be an object when provided." });
|
|
3538
|
+
return;
|
|
3539
|
+
}
|
|
3540
|
+
const controller = new AbortController();
|
|
3541
|
+
const raw = response;
|
|
3542
|
+
const onClientGone = () => {
|
|
3543
|
+
if (!raw.writableEnded) {
|
|
3544
|
+
controller.abort(
|
|
3545
|
+
new DOMException("Client disconnected.", "AbortError")
|
|
3546
|
+
);
|
|
3547
|
+
}
|
|
3548
|
+
};
|
|
3549
|
+
raw.on("close", onClientGone);
|
|
3550
|
+
try {
|
|
3551
|
+
const result = await executeToolCall(
|
|
3552
|
+
state.spec,
|
|
3553
|
+
toolName.trim(),
|
|
3554
|
+
args ?? {},
|
|
3555
|
+
{
|
|
3556
|
+
...contextProvider(),
|
|
3557
|
+
signal: controller.signal,
|
|
3558
|
+
onLog(entry) {
|
|
3559
|
+
addLog({
|
|
3560
|
+
...entry,
|
|
3561
|
+
protocol: void 0,
|
|
3562
|
+
meta: { ...entry.meta, debug: true }
|
|
3563
|
+
});
|
|
3564
|
+
}
|
|
3565
|
+
}
|
|
3566
|
+
);
|
|
3567
|
+
response.json({
|
|
3568
|
+
ok: true,
|
|
3569
|
+
// Surfaced as data rather than enforced as a gate, so the operator knows
|
|
3570
|
+
// the result came from a direct upstream call while the service was down.
|
|
3571
|
+
serviceRunning: isActive(),
|
|
3572
|
+
result
|
|
3573
|
+
});
|
|
3574
|
+
} catch (error) {
|
|
3575
|
+
const aborted = isAbortError(error);
|
|
3576
|
+
if (aborted && raw.destroyed) {
|
|
3577
|
+
addLog({
|
|
3578
|
+
id: newId(),
|
|
3579
|
+
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3580
|
+
direction: "response",
|
|
3581
|
+
toolName: toolName.trim(),
|
|
3582
|
+
success: false,
|
|
3583
|
+
error: "The client disconnected before the upstream call completed.",
|
|
3584
|
+
meta: { debug: true, cancelled: true }
|
|
3585
|
+
});
|
|
3586
|
+
return;
|
|
3587
|
+
}
|
|
3588
|
+
if (aborted) {
|
|
3589
|
+
if (!response.headersSent) {
|
|
3590
|
+
response.status(504).json({
|
|
3591
|
+
error: "The upstream call was aborted before it completed."
|
|
3592
|
+
});
|
|
3593
|
+
}
|
|
3594
|
+
return;
|
|
3595
|
+
}
|
|
3596
|
+
if (!response.headersSent) {
|
|
3597
|
+
response.status(400).json({ error: toMessage(error) });
|
|
3598
|
+
}
|
|
3599
|
+
} finally {
|
|
3600
|
+
raw.off("close", onClientGone);
|
|
3601
|
+
}
|
|
3602
|
+
});
|
|
3603
|
+
app.use(
|
|
3604
|
+
express2__default.default.static(STATIC_DIR, {
|
|
3605
|
+
index: false,
|
|
3606
|
+
dotfiles: "ignore",
|
|
3607
|
+
fallthrough: true
|
|
3608
|
+
})
|
|
3609
|
+
);
|
|
3610
|
+
app.get(/^\/(?!api\/|mcp|sse).*/, (_request, response, next) => {
|
|
3611
|
+
response.sendFile(INDEX_HTML, (error) => {
|
|
3612
|
+
if (error) next(error);
|
|
3613
|
+
});
|
|
3614
|
+
});
|
|
3615
|
+
app.use((_request, response) => {
|
|
3616
|
+
response.status(404).json({ error: "Not found." });
|
|
3617
|
+
});
|
|
3618
|
+
app.use(
|
|
3619
|
+
(error, _request, response, _next) => {
|
|
3620
|
+
const code = error?.code;
|
|
3621
|
+
if (code === "LIMIT_FILE_SIZE") {
|
|
3622
|
+
response.status(413).json({ error: "The uploaded file exceeds the size limit." });
|
|
3623
|
+
return;
|
|
3624
|
+
}
|
|
3625
|
+
if (code === "LIMIT_FILE_COUNT" || code === "LIMIT_UNEXPECTED_FILE") {
|
|
3626
|
+
response.status(400).json({ error: 'Exactly one file field named "file" is expected.' });
|
|
3627
|
+
return;
|
|
3628
|
+
}
|
|
3629
|
+
console.error("[admin] Unhandled error:", error);
|
|
3630
|
+
if (response.headersSent) {
|
|
3631
|
+
response.end();
|
|
3632
|
+
return;
|
|
3633
|
+
}
|
|
3634
|
+
response.status(500).json({ error: "Internal server error." });
|
|
3635
|
+
}
|
|
3636
|
+
);
|
|
3637
|
+
const server = http.createServer(app);
|
|
3638
|
+
server.keepAliveTimeout = 76e3;
|
|
3639
|
+
server.headersTimeout = 8e4;
|
|
3640
|
+
server.requestTimeout = REQUEST_TIMEOUT_MS;
|
|
3641
|
+
await new Promise((resolve, reject) => {
|
|
3642
|
+
const onError = (error) => {
|
|
3643
|
+
server.off("listening", onListening);
|
|
3644
|
+
reject(error);
|
|
3645
|
+
};
|
|
3646
|
+
const onListening = () => {
|
|
3647
|
+
server.off("error", onError);
|
|
3648
|
+
resolve();
|
|
3649
|
+
};
|
|
3650
|
+
server.once("listening", onListening);
|
|
3651
|
+
server.once("error", onError);
|
|
3652
|
+
server.listen(config.port, config.host ?? "127.0.0.1");
|
|
3653
|
+
});
|
|
3654
|
+
const address = server.address();
|
|
3655
|
+
const boundPort = address && typeof address === "object" ? address.port : config.port;
|
|
3656
|
+
const close = async () => {
|
|
3657
|
+
if (closed) return;
|
|
3658
|
+
closed = true;
|
|
3659
|
+
flushState();
|
|
3660
|
+
await mcp.dispose();
|
|
3661
|
+
await new Promise((resolve) => {
|
|
3662
|
+
server.close(() => resolve());
|
|
3663
|
+
server.closeIdleConnections?.();
|
|
3664
|
+
});
|
|
3665
|
+
};
|
|
3666
|
+
return { app, server, port: boundPort, close };
|
|
3667
|
+
}
|
|
3668
|
+
|
|
3669
|
+
exports.assertUniqueOperationIds = assertUniqueOperationIds;
|
|
3670
|
+
exports.attachSseRoutes = attachSseRoutes;
|
|
3671
|
+
exports.buildBindingIndex = buildBindingIndex;
|
|
3672
|
+
exports.buildMcpServer = buildMcpServer;
|
|
3673
|
+
exports.buildRequest = buildRequest;
|
|
3674
|
+
exports.collectOperationParameters = collectOperationParameters;
|
|
3675
|
+
exports.createAuthMiddleware = createAuthMiddleware;
|
|
3676
|
+
exports.effectiveExplode = effectiveExplode;
|
|
3677
|
+
exports.effectiveStyle = effectiveStyle;
|
|
3678
|
+
exports.ensureUniqueName = ensureUniqueName;
|
|
3679
|
+
exports.executeToolCall = executeToolCall;
|
|
3680
|
+
exports.extractPathTemplateVariables = extractPathTemplateVariables;
|
|
3681
|
+
exports.findDuplicateOperationIds = findDuplicateOperationIds;
|
|
3682
|
+
exports.findOperationById = findOperationById;
|
|
3683
|
+
exports.generatePrompts = generatePrompts;
|
|
3684
|
+
exports.generateResources = generateResources;
|
|
3685
|
+
exports.generateTools = generateTools;
|
|
3686
|
+
exports.generateToolsDetailed = generateToolsDetailed;
|
|
3687
|
+
exports.iterateOperations = iterateOperations;
|
|
3688
|
+
exports.loadOpenApiSpec = loadOpenApiSpec;
|
|
3689
|
+
exports.normalizeParameter = normalizeParameter;
|
|
3690
|
+
exports.parseSpecContent = parseSpecContent;
|
|
3691
|
+
exports.readResource = readResource;
|
|
3692
|
+
exports.resolvePrompt = resolvePrompt;
|
|
3693
|
+
exports.startAdminServer = startAdminServer;
|
|
3694
|
+
exports.startStdioServer = startStdioServer;
|
|
3695
|
+
exports.synthesizeOperationId = synthesizeOperationId;
|
|
3696
|
+
//# sourceMappingURL=index.cjs.map
|
|
3697
|
+
//# sourceMappingURL=index.cjs.map
|