@ttoss/http-server-mcp-openapi 0.2.15 → 0.4.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 +70 -19
- package/dist/index.cjs +387 -104
- package/dist/index.d.cts +175 -66
- package/dist/index.d.mts +175 -66
- package/dist/index.mjs +388 -106
- package/package.json +2 -2
package/dist/index.mjs
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
/** Powered by @ttoss/config. https://ttoss.dev/docs/modules/packages/config/ */
|
|
2
|
-
import { registerToolFromSchema } from "@ttoss/http-server-mcp";
|
|
2
|
+
import { getApiHeaders, registerToolFromSchema } from "@ttoss/http-server-mcp";
|
|
3
3
|
|
|
4
4
|
//#region src/schema.ts
|
|
5
|
+
var isRecord = value => {
|
|
6
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
7
|
+
};
|
|
5
8
|
var getAlternativeSchemas = schema => {
|
|
6
9
|
if (Array.isArray(schema.oneOf) && schema.oneOf.length > 0) return schema.oneOf;
|
|
7
10
|
if (Array.isArray(schema.anyOf) && schema.anyOf.length > 0) return schema.anyOf;
|
|
@@ -23,36 +26,96 @@ var mergeResolvedSchemas = resolvedAlternatives => {
|
|
|
23
26
|
required: requiredIntersection && requiredIntersection.length > 0 ? requiredIntersection : void 0
|
|
24
27
|
};
|
|
25
28
|
};
|
|
29
|
+
var stripDotSlash = file => {
|
|
30
|
+
return file.replace(/^\.\//, "");
|
|
31
|
+
};
|
|
32
|
+
var findDocument = args => {
|
|
33
|
+
if (!args.documents) return void 0;
|
|
34
|
+
const wanted = stripDotSlash(args.file);
|
|
35
|
+
for (const [key, document] of Object.entries(args.documents)) if (stripDotSlash(key) === wanted) return document;
|
|
36
|
+
};
|
|
37
|
+
/** Follows an RFC 6901 JSON pointer (`/components/schemas/Tag`) into a value. */
|
|
38
|
+
var followPointer = args => {
|
|
39
|
+
const tokens = args.pointer.split("/").slice(1);
|
|
40
|
+
let current = args.root;
|
|
41
|
+
for (const rawToken of tokens) {
|
|
42
|
+
if (!isRecord(current) && !Array.isArray(current)) return void 0;
|
|
43
|
+
const token = decodeURIComponent(rawToken).replace(/~1/g, "/").replace(/~0/g, "~");
|
|
44
|
+
current = current[token];
|
|
45
|
+
}
|
|
46
|
+
return current;
|
|
47
|
+
};
|
|
48
|
+
/** Stable identity for each document, so cycle keys never collide across files. */
|
|
49
|
+
var documentIds = /* @__PURE__ */new WeakMap();
|
|
50
|
+
var nextDocumentId = 0;
|
|
51
|
+
var documentId = spec => {
|
|
52
|
+
const known = documentIds.get(spec);
|
|
53
|
+
if (known !== void 0) return known;
|
|
54
|
+
nextDocumentId += 1;
|
|
55
|
+
documentIds.set(spec, nextDocumentId);
|
|
56
|
+
return nextDocumentId;
|
|
57
|
+
};
|
|
58
|
+
/**
|
|
59
|
+
* Resolves a `$ref` to its target and the document the target lives in (the
|
|
60
|
+
* scope its own nested refs resolve against). A ref without a file part
|
|
61
|
+
* (`#/components/schemas/X`) points into the current document; one with a
|
|
62
|
+
* file part (`./tags.yaml#/components/schemas/Tag`) points into the matching
|
|
63
|
+
* entry of `documents`. Returns `undefined` when the target does not exist.
|
|
64
|
+
*/
|
|
65
|
+
var resolveRef = args => {
|
|
66
|
+
const hashIndex = args.ref.indexOf("#");
|
|
67
|
+
const file = hashIndex === -1 ? args.ref : args.ref.slice(0, hashIndex);
|
|
68
|
+
const pointer = hashIndex === -1 ? "" : args.ref.slice(hashIndex + 1);
|
|
69
|
+
const spec = file === "" ? args.scope.spec : findDocument({
|
|
70
|
+
file,
|
|
71
|
+
documents: args.scope.documents
|
|
72
|
+
});
|
|
73
|
+
if (!spec) return void 0;
|
|
74
|
+
const value = followPointer({
|
|
75
|
+
root: spec,
|
|
76
|
+
pointer
|
|
77
|
+
});
|
|
78
|
+
if (value === void 0) return void 0;
|
|
79
|
+
return {
|
|
80
|
+
value,
|
|
81
|
+
scope: {
|
|
82
|
+
spec,
|
|
83
|
+
documents: args.scope.documents
|
|
84
|
+
},
|
|
85
|
+
key: `${documentId(spec)}#${pointer}`
|
|
86
|
+
};
|
|
87
|
+
};
|
|
26
88
|
var dereferenceValue = args => {
|
|
27
89
|
const {
|
|
28
90
|
value,
|
|
29
|
-
|
|
91
|
+
scope,
|
|
30
92
|
seenRefs
|
|
31
93
|
} = args;
|
|
32
94
|
if (Array.isArray(value)) return value.map(item => {
|
|
33
95
|
return dereferenceValue({
|
|
34
96
|
value: item,
|
|
35
|
-
|
|
97
|
+
scope,
|
|
36
98
|
seenRefs
|
|
37
99
|
});
|
|
38
100
|
});
|
|
39
|
-
if (value
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
if (
|
|
101
|
+
if (isRecord(value)) {
|
|
102
|
+
if (typeof value.$ref === "string") {
|
|
103
|
+
const target = resolveRef({
|
|
104
|
+
ref: value.$ref,
|
|
105
|
+
scope
|
|
106
|
+
});
|
|
107
|
+
if (!target) return {};
|
|
108
|
+
if (seenRefs.has(target.key)) return {};
|
|
46
109
|
return dereferenceValue({
|
|
47
|
-
value:
|
|
48
|
-
|
|
49
|
-
seenRefs: new Set(seenRefs).add(
|
|
110
|
+
value: target.value,
|
|
111
|
+
scope: target.scope,
|
|
112
|
+
seenRefs: new Set(seenRefs).add(target.key)
|
|
50
113
|
});
|
|
51
114
|
}
|
|
52
115
|
const result = {};
|
|
53
|
-
for (const [key, entryValue] of Object.entries(
|
|
116
|
+
for (const [key, entryValue] of Object.entries(value)) result[key] = dereferenceValue({
|
|
54
117
|
value: entryValue,
|
|
55
|
-
|
|
118
|
+
scope,
|
|
56
119
|
seenRefs
|
|
57
120
|
});
|
|
58
121
|
return result;
|
|
@@ -64,12 +127,17 @@ var dereferenceValue = args => {
|
|
|
64
127
|
* `properties`, `items`, `oneOf`, `anyOf`, etc.), producing a self-contained
|
|
65
128
|
* schema safe to hand to an MCP client or LLM provider as a tool definition —
|
|
66
129
|
* provider tool schemas have no `components` section to resolve refs against.
|
|
130
|
+
*
|
|
131
|
+
* Refs into other files resolve against `documents`; see {@link OpenApiDocuments}.
|
|
67
132
|
*/
|
|
68
|
-
var dereferenceSchema = (schema, spec) => {
|
|
133
|
+
var dereferenceSchema = (schema, spec, documents) => {
|
|
69
134
|
if (!schema) return schema;
|
|
70
135
|
return dereferenceValue({
|
|
71
136
|
value: schema,
|
|
72
|
-
|
|
137
|
+
scope: {
|
|
138
|
+
spec,
|
|
139
|
+
documents
|
|
140
|
+
},
|
|
73
141
|
seenRefs: /* @__PURE__ */new Set()
|
|
74
142
|
});
|
|
75
143
|
};
|
|
@@ -78,25 +146,39 @@ var dereferenceSchema = (schema, spec) => {
|
|
|
78
146
|
* and merges `oneOf` / `anyOf` alternatives (union of properties, intersection
|
|
79
147
|
* of `required`) so the caller sees one flat property set.
|
|
80
148
|
*/
|
|
81
|
-
var resolveSchema = (schema, spec) => {
|
|
149
|
+
var resolveSchema = (schema, spec, documents) => {
|
|
82
150
|
if (!schema) return {};
|
|
83
151
|
if (typeof schema.$ref === "string") {
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
|
|
152
|
+
const target = resolveRef({
|
|
153
|
+
ref: schema.$ref,
|
|
154
|
+
scope: {
|
|
155
|
+
spec,
|
|
156
|
+
documents
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
return resolveSchema(target?.value, target?.scope.spec ?? spec, documents);
|
|
87
160
|
}
|
|
88
161
|
const alternatives = getAlternativeSchemas(schema);
|
|
89
162
|
if (alternatives) return mergeResolvedSchemas(alternatives.map(candidate => {
|
|
90
|
-
return resolveSchema(candidate, spec);
|
|
163
|
+
return resolveSchema(candidate, spec, documents);
|
|
91
164
|
}));
|
|
92
165
|
return schema;
|
|
93
166
|
};
|
|
94
|
-
/**
|
|
95
|
-
|
|
167
|
+
/**
|
|
168
|
+
* Follows a parameter `$ref` — into `components.parameters`, or into another
|
|
169
|
+
* file through `documents` — if present.
|
|
170
|
+
*/
|
|
171
|
+
var resolveParameter = (param, spec, documents) => {
|
|
96
172
|
if (!param) return {};
|
|
97
173
|
if (typeof param.$ref === "string") {
|
|
98
|
-
const
|
|
99
|
-
|
|
174
|
+
const target = resolveRef({
|
|
175
|
+
ref: param.$ref,
|
|
176
|
+
scope: {
|
|
177
|
+
spec,
|
|
178
|
+
documents
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
return isRecord(target?.value) ? target.value : {};
|
|
100
182
|
}
|
|
101
183
|
return param;
|
|
102
184
|
};
|
|
@@ -106,9 +188,9 @@ var buildPathFn = (pathTemplate, pathParams) => {
|
|
|
106
188
|
let result = pathTemplate;
|
|
107
189
|
for (const {
|
|
108
190
|
name,
|
|
109
|
-
|
|
191
|
+
argName
|
|
110
192
|
} of pathParams) {
|
|
111
|
-
const value = args[
|
|
193
|
+
const value = args[argName];
|
|
112
194
|
if (value !== void 0) result = result.replace(`{${name}}`, encodeURIComponent(String(value)));
|
|
113
195
|
}
|
|
114
196
|
return result;
|
|
@@ -119,9 +201,6 @@ var NON_EXPLODED_DELIMITERS = {
|
|
|
119
201
|
spaceDelimited: " ",
|
|
120
202
|
pipeDelimited: "|"
|
|
121
203
|
};
|
|
122
|
-
var isRecord = value => {
|
|
123
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
124
|
-
};
|
|
125
204
|
/**
|
|
126
205
|
* Appends a `deepObject` value as bracketed keys — `filters[documentId][$eq]`.
|
|
127
206
|
* OpenAPI only defines one level, but the APIs that ask for `deepObject`
|
|
@@ -209,7 +288,7 @@ var buildQueryFn = queryParams => {
|
|
|
209
288
|
return args => {
|
|
210
289
|
const search = new URLSearchParams();
|
|
211
290
|
for (const param of queryParams) {
|
|
212
|
-
const value = args[param.
|
|
291
|
+
const value = args[param.argName];
|
|
213
292
|
if (value === void 0 || value === null) continue;
|
|
214
293
|
appendQueryValue({
|
|
215
294
|
search,
|
|
@@ -222,8 +301,8 @@ var buildQueryFn = queryParams => {
|
|
|
222
301
|
};
|
|
223
302
|
};
|
|
224
303
|
/**
|
|
225
|
-
* Builds a function that maps
|
|
226
|
-
*
|
|
304
|
+
* Builds a function that maps tool args back to a request body keyed by the
|
|
305
|
+
* spec's property names, skipping `undefined` args. Returns `undefined` when the op has no body.
|
|
227
306
|
*/
|
|
228
307
|
var buildBodyFn = bodyProps => {
|
|
229
308
|
if (bodyProps.length === 0) return void 0;
|
|
@@ -231,29 +310,200 @@ var buildBodyFn = bodyProps => {
|
|
|
231
310
|
const body = {};
|
|
232
311
|
for (const {
|
|
233
312
|
snakeName,
|
|
234
|
-
|
|
235
|
-
} of bodyProps) if (args[
|
|
313
|
+
argName
|
|
314
|
+
} of bodyProps) if (args[argName] !== void 0) body[snakeName] = args[argName];
|
|
236
315
|
return body;
|
|
237
316
|
};
|
|
238
317
|
};
|
|
239
318
|
|
|
319
|
+
//#endregion
|
|
320
|
+
//#region src/serverManaged.ts
|
|
321
|
+
/**
|
|
322
|
+
* Whether a parameter or body property carries one of the server-managed
|
|
323
|
+
* extensions, and the value it pins when the extension is a string.
|
|
324
|
+
*
|
|
325
|
+
* A string pins the value (`x-mcp-server-managed: 'true'` always sends
|
|
326
|
+
* `true`); any other truthy value only hides it. The first matching name wins.
|
|
327
|
+
*/
|
|
328
|
+
var readServerManaged = args => {
|
|
329
|
+
const names = Array.isArray(args.extension) ? args.extension : [args.extension];
|
|
330
|
+
for (const name of names) {
|
|
331
|
+
const flag = args.node[name];
|
|
332
|
+
if (typeof flag === "string") return {
|
|
333
|
+
managed: true,
|
|
334
|
+
value: flag
|
|
335
|
+
};
|
|
336
|
+
if (flag) return {
|
|
337
|
+
managed: true
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
return {
|
|
341
|
+
managed: false
|
|
342
|
+
};
|
|
343
|
+
};
|
|
344
|
+
/** The args each pinned parameter is always sent with, keyed by `argName`. */
|
|
345
|
+
var pinnedArgs = parameters => {
|
|
346
|
+
const pinned = {};
|
|
347
|
+
for (const parameter of parameters) if (parameter.value !== void 0) pinned[parameter.argName] = parameter.value;
|
|
348
|
+
return pinned;
|
|
349
|
+
};
|
|
350
|
+
/**
|
|
351
|
+
* Wraps a `path` / `query` builder so pinned values replace whatever the args
|
|
352
|
+
* carry. Applied in the builder itself, so every consumer of the tool
|
|
353
|
+
* definition sends them, not only `registerOpenApiTools`.
|
|
354
|
+
*/
|
|
355
|
+
var withPinned = args => {
|
|
356
|
+
const {
|
|
357
|
+
build,
|
|
358
|
+
pinned
|
|
359
|
+
} = args;
|
|
360
|
+
if (!build || Object.keys(pinned).length === 0) return build;
|
|
361
|
+
return values => {
|
|
362
|
+
return build({
|
|
363
|
+
...values,
|
|
364
|
+
...pinned
|
|
365
|
+
});
|
|
366
|
+
};
|
|
367
|
+
};
|
|
368
|
+
var isPlainObject$1 = value => {
|
|
369
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
370
|
+
};
|
|
371
|
+
var mergeNullIntoType = schema => {
|
|
372
|
+
const {
|
|
373
|
+
type
|
|
374
|
+
} = schema;
|
|
375
|
+
if (typeof type === "string") schema.type = [type, "null"];else if (Array.isArray(type) && !type.includes("null")) schema.type = [...type, "null"];
|
|
376
|
+
};
|
|
377
|
+
/**
|
|
378
|
+
* Turns OpenAPI's `nullable` into JSON Schema at every depth: `nullable: true`
|
|
379
|
+
* merges `'null'` into a sibling `type` and is dropped where there is none.
|
|
380
|
+
* JSON Schema has no `nullable`, so a validator or model reading it would
|
|
381
|
+
* refuse `null` where the API accepts it.
|
|
382
|
+
*
|
|
383
|
+
* Only a boolean `nullable` is the keyword; a property *named* `nullable`
|
|
384
|
+
* inside `properties` is a schema and is kept.
|
|
385
|
+
*/
|
|
386
|
+
var normalizeNullable = value => {
|
|
387
|
+
if (Array.isArray(value)) return value.map(normalizeNullable);
|
|
388
|
+
if (!isPlainObject$1(value)) return value;
|
|
389
|
+
const result = {};
|
|
390
|
+
for (const [key, nested] of Object.entries(value)) {
|
|
391
|
+
if (key === "nullable" && typeof nested === "boolean") continue;
|
|
392
|
+
result[key] = normalizeNullable(nested);
|
|
393
|
+
}
|
|
394
|
+
if (value.nullable === true) mergeNullIntoType(result);
|
|
395
|
+
return result;
|
|
396
|
+
};
|
|
397
|
+
|
|
240
398
|
//#endregion
|
|
241
399
|
//#region src/types.ts
|
|
242
400
|
var DEFAULT_EXCLUDE_EXTENSION = "x-mcp-exclude";
|
|
243
401
|
var DEFAULT_SERVER_MANAGED_EXTENSION = "x-mcp-server-managed";
|
|
244
402
|
|
|
245
403
|
//#endregion
|
|
246
|
-
//#region src/
|
|
404
|
+
//#region src/parameters.ts
|
|
247
405
|
/**
|
|
248
406
|
* Folds `_` and `-` separators into camelCase. OpenAPI operation and parameter
|
|
249
|
-
* names may be snake_case (`agent_id`) or kebab-case (`list-tools`)
|
|
250
|
-
*
|
|
407
|
+
* names may be snake_case (`agent_id`) or kebab-case (`list-tools`); with the
|
|
408
|
+
* default `argumentNames: 'camelCase'` both are folded into tool arguments.
|
|
251
409
|
*/
|
|
252
410
|
var snakeToCamel = str => {
|
|
253
411
|
return str.replace(/[_-]([a-z])/g, (_, letter) => {
|
|
254
412
|
return letter.toUpperCase();
|
|
255
413
|
});
|
|
256
414
|
};
|
|
415
|
+
var verbatim = name => {
|
|
416
|
+
return name;
|
|
417
|
+
};
|
|
418
|
+
var argNameMapper = argumentNames => {
|
|
419
|
+
return argumentNames === "verbatim" ? verbatim : snakeToCamel;
|
|
420
|
+
};
|
|
421
|
+
/**
|
|
422
|
+
* Deduplicates parameter entries by `name`, keeping the last occurrence. When
|
|
423
|
+
* path-item-level and operation-level parameters are concatenated (operation
|
|
424
|
+
* last), this makes the operation-level entry win — as the OpenAPI spec requires.
|
|
425
|
+
*/
|
|
426
|
+
var dedupeByName = items => {
|
|
427
|
+
const byName = /* @__PURE__ */new Map();
|
|
428
|
+
for (const item of items) byName.set(item.name, item);
|
|
429
|
+
return [...byName.values()];
|
|
430
|
+
};
|
|
431
|
+
var managedFields = flag => {
|
|
432
|
+
return flag.value === void 0 ? {
|
|
433
|
+
serverManaged: flag.managed
|
|
434
|
+
} : {
|
|
435
|
+
serverManaged: true,
|
|
436
|
+
pinnedValue: flag.value
|
|
437
|
+
};
|
|
438
|
+
};
|
|
439
|
+
var extractPathParams = args => {
|
|
440
|
+
const toArgName = args.toArgName ?? snakeToCamel;
|
|
441
|
+
const flag = args.serverManagedExtension ?? "x-mcp-server-managed";
|
|
442
|
+
return dedupeByName((args.parameters || []).map(p => {
|
|
443
|
+
return resolveParameter(p, args.spec, args.documents);
|
|
444
|
+
}).filter(p => {
|
|
445
|
+
return p.in === "path";
|
|
446
|
+
}).map(p => {
|
|
447
|
+
return {
|
|
448
|
+
name: p.name || "",
|
|
449
|
+
argName: toArgName(p.name || ""),
|
|
450
|
+
...managedFields(readServerManaged({
|
|
451
|
+
node: p,
|
|
452
|
+
extension: flag
|
|
453
|
+
}))
|
|
454
|
+
};
|
|
455
|
+
}));
|
|
456
|
+
};
|
|
457
|
+
var extractQueryParams = args => {
|
|
458
|
+
const toArgName = args.toArgName ?? snakeToCamel;
|
|
459
|
+
const flag = args.serverManagedExtension ?? "x-mcp-server-managed";
|
|
460
|
+
return dedupeByName((args.parameters || []).map(p => {
|
|
461
|
+
return resolveParameter(p, args.spec, args.documents);
|
|
462
|
+
}).filter(p => {
|
|
463
|
+
return p.in === "query";
|
|
464
|
+
}).map(p => {
|
|
465
|
+
return {
|
|
466
|
+
name: p.name || "",
|
|
467
|
+
argName: toArgName(p.name || ""),
|
|
468
|
+
description: p.description || "",
|
|
469
|
+
required: p.required || false,
|
|
470
|
+
type: p.schema?.type || "string",
|
|
471
|
+
style: p.style,
|
|
472
|
+
explode: p.explode,
|
|
473
|
+
...managedFields(readServerManaged({
|
|
474
|
+
node: p,
|
|
475
|
+
extension: flag
|
|
476
|
+
}))
|
|
477
|
+
};
|
|
478
|
+
}));
|
|
479
|
+
};
|
|
480
|
+
var collectServerManagedParameters = args => {
|
|
481
|
+
return [...args.pathParams.map(p => {
|
|
482
|
+
return {
|
|
483
|
+
...p,
|
|
484
|
+
in: "path"
|
|
485
|
+
};
|
|
486
|
+
}), ...args.queryParams.map(p => {
|
|
487
|
+
return {
|
|
488
|
+
...p,
|
|
489
|
+
in: "query"
|
|
490
|
+
};
|
|
491
|
+
})].filter(p => {
|
|
492
|
+
return p.serverManaged;
|
|
493
|
+
}).map(p => {
|
|
494
|
+
return {
|
|
495
|
+
name: p.name,
|
|
496
|
+
in: p.in,
|
|
497
|
+
argName: p.argName,
|
|
498
|
+
...(p.pinnedValue === void 0 ? {} : {
|
|
499
|
+
value: p.pinnedValue
|
|
500
|
+
})
|
|
501
|
+
};
|
|
502
|
+
});
|
|
503
|
+
};
|
|
504
|
+
|
|
505
|
+
//#endregion
|
|
506
|
+
//#region src/toolDefinitions.ts
|
|
257
507
|
/** Converts a camelCase `operationId` to a kebab-case tool name. */
|
|
258
508
|
var operationIdToToolName = operationId => {
|
|
259
509
|
return operationId.replace(/([A-Z])/g, "-$1").toLowerCase().replace(/^-/, "");
|
|
@@ -318,23 +568,29 @@ var buildTypedProperty = param => {
|
|
|
318
568
|
};
|
|
319
569
|
};
|
|
320
570
|
var buildInputSchema = (pathParams, queryParams, bodyProps) => {
|
|
321
|
-
const
|
|
571
|
+
const modelPathParams = pathParams.filter(p => {
|
|
572
|
+
return !p.serverManaged;
|
|
573
|
+
});
|
|
574
|
+
const modelQueryParams = queryParams.filter(p => {
|
|
575
|
+
return !p.serverManaged;
|
|
576
|
+
});
|
|
577
|
+
const allParams = [...modelPathParams, ...modelQueryParams, ...bodyProps];
|
|
322
578
|
if (allParams.length === 0) return {
|
|
323
579
|
type: "object"
|
|
324
580
|
};
|
|
325
|
-
const requiredFields = [...
|
|
326
|
-
return p.
|
|
327
|
-
}), ...
|
|
581
|
+
const requiredFields = [...modelPathParams.map(p => {
|
|
582
|
+
return p.argName;
|
|
583
|
+
}), ...modelQueryParams.filter(p => {
|
|
328
584
|
return p.required;
|
|
329
585
|
}).map(p => {
|
|
330
|
-
return p.
|
|
586
|
+
return p.argName;
|
|
331
587
|
}), ...bodyProps.filter(p => {
|
|
332
588
|
return p.required;
|
|
333
589
|
}).map(p => {
|
|
334
|
-
return p.
|
|
590
|
+
return p.argName;
|
|
335
591
|
})];
|
|
336
592
|
const properties = {};
|
|
337
|
-
for (const param of allParams) properties[param.
|
|
593
|
+
for (const param of allParams) properties[param.argName] = "description" in param ? normalizeNullable(buildTypedProperty(param)) : {
|
|
338
594
|
type: "string",
|
|
339
595
|
description: ""
|
|
340
596
|
};
|
|
@@ -346,48 +602,9 @@ var buildInputSchema = (pathParams, queryParams, bodyProps) => {
|
|
|
346
602
|
} : {})
|
|
347
603
|
};
|
|
348
604
|
};
|
|
349
|
-
/**
|
|
350
|
-
* Deduplicates parameter entries by `name`, keeping the last occurrence. When
|
|
351
|
-
* path-item-level and operation-level parameters are concatenated (operation
|
|
352
|
-
* last), this makes the operation-level entry win — as the OpenAPI spec requires.
|
|
353
|
-
*/
|
|
354
|
-
var dedupeByName = items => {
|
|
355
|
-
const byName = /* @__PURE__ */new Map();
|
|
356
|
-
for (const item of items) byName.set(item.name, item);
|
|
357
|
-
return [...byName.values()];
|
|
358
|
-
};
|
|
359
|
-
var extractPathParams = args => {
|
|
360
|
-
return dedupeByName((args.parameters || []).map(p => {
|
|
361
|
-
return resolveParameter(p, args.spec);
|
|
362
|
-
}).filter(p => {
|
|
363
|
-
return p.in === "path";
|
|
364
|
-
}).map(p => {
|
|
365
|
-
return {
|
|
366
|
-
name: p.name || "",
|
|
367
|
-
camelName: snakeToCamel(p.name || "")
|
|
368
|
-
};
|
|
369
|
-
}));
|
|
370
|
-
};
|
|
371
|
-
var extractQueryParams = args => {
|
|
372
|
-
return dedupeByName((args.parameters || []).map(p => {
|
|
373
|
-
return resolveParameter(p, args.spec);
|
|
374
|
-
}).filter(p => {
|
|
375
|
-
return p.in === "query";
|
|
376
|
-
}).map(p => {
|
|
377
|
-
return {
|
|
378
|
-
name: p.name || "",
|
|
379
|
-
camelName: snakeToCamel(p.name || ""),
|
|
380
|
-
description: p.description || "",
|
|
381
|
-
required: p.required || false,
|
|
382
|
-
type: p.schema?.type || "string",
|
|
383
|
-
style: p.style,
|
|
384
|
-
explode: p.explode
|
|
385
|
-
};
|
|
386
|
-
}));
|
|
387
|
-
};
|
|
388
605
|
var resolveBodySchema = args => {
|
|
389
606
|
const rawBodySchema = args.requestBody?.content?.["application/json"]?.schema;
|
|
390
|
-
return resolveSchema(dereferenceSchema(rawBodySchema, args.spec), args.spec);
|
|
607
|
+
return resolveSchema(dereferenceSchema(rawBodySchema, args.spec, args.documents), args.spec, args.documents);
|
|
391
608
|
};
|
|
392
609
|
/**
|
|
393
610
|
* snake_case names of every top-level property an operation's request schema
|
|
@@ -421,15 +638,19 @@ var flattenSingleAllOf = schema => {
|
|
|
421
638
|
};
|
|
422
639
|
};
|
|
423
640
|
var extractBodyProps = args => {
|
|
641
|
+
const toArgName = args.toArgName ?? snakeToCamel;
|
|
424
642
|
const bodySchema = resolveBodySchema(args);
|
|
425
643
|
if (!bodySchema?.properties) return [];
|
|
426
644
|
return Object.entries(bodySchema.properties).filter(([, value]) => {
|
|
427
|
-
return !
|
|
645
|
+
return !readServerManaged({
|
|
646
|
+
node: value,
|
|
647
|
+
extension: args.serverManagedExtension
|
|
648
|
+
}).managed;
|
|
428
649
|
}).map(([key, value]) => {
|
|
429
650
|
const val = flattenSingleAllOf(value);
|
|
430
651
|
return {
|
|
431
652
|
snakeName: key,
|
|
432
|
-
|
|
653
|
+
argName: toArgName(key),
|
|
433
654
|
description: typeof val.description === "string" ? val.description : "",
|
|
434
655
|
required: (bodySchema.required || []).includes(key),
|
|
435
656
|
type: typeof val.type === "string" ? val.type : void 0,
|
|
@@ -454,23 +675,42 @@ var processOperation = args => {
|
|
|
454
675
|
if (args.operation[args.options.excludeExtension]) return null;
|
|
455
676
|
const toolName = operationIdToToolName(args.operation.operationId);
|
|
456
677
|
const parameters = [...(args.pathItemParameters ?? []), ...(args.operation.parameters ?? [])];
|
|
678
|
+
const {
|
|
679
|
+
documents,
|
|
680
|
+
serverManagedExtension
|
|
681
|
+
} = args.options;
|
|
682
|
+
const toArgName = argNameMapper(args.options.argumentNames);
|
|
457
683
|
const pathParams = extractPathParams({
|
|
458
684
|
parameters,
|
|
459
|
-
spec: args.spec
|
|
685
|
+
spec: args.spec,
|
|
686
|
+
documents,
|
|
687
|
+
toArgName,
|
|
688
|
+
serverManagedExtension
|
|
460
689
|
});
|
|
461
690
|
const queryParams = extractQueryParams({
|
|
462
691
|
parameters,
|
|
463
|
-
spec: args.spec
|
|
692
|
+
spec: args.spec,
|
|
693
|
+
documents,
|
|
694
|
+
toArgName,
|
|
695
|
+
serverManagedExtension
|
|
464
696
|
});
|
|
465
697
|
const bodyProps = extractBodyProps({
|
|
466
698
|
requestBody: args.operation.requestBody,
|
|
467
699
|
spec: args.spec,
|
|
468
|
-
serverManagedExtension
|
|
700
|
+
serverManagedExtension,
|
|
701
|
+
documents,
|
|
702
|
+
toArgName
|
|
469
703
|
});
|
|
470
704
|
const inputSchema = buildInputSchema(pathParams, queryParams, bodyProps);
|
|
705
|
+
const serverManagedParameters = collectServerManagedParameters({
|
|
706
|
+
pathParams,
|
|
707
|
+
queryParams
|
|
708
|
+
});
|
|
709
|
+
const pinned = pinnedArgs(serverManagedParameters);
|
|
471
710
|
const acceptedBodyFields = extractAcceptedBodyFields({
|
|
472
711
|
requestBody: args.operation.requestBody,
|
|
473
|
-
spec: args.spec
|
|
712
|
+
spec: args.spec,
|
|
713
|
+
documents
|
|
474
714
|
});
|
|
475
715
|
return {
|
|
476
716
|
name: toolName,
|
|
@@ -479,11 +719,18 @@ var processOperation = args => {
|
|
|
479
719
|
method: httpMethod,
|
|
480
720
|
pathTemplate: args.pathTemplate,
|
|
481
721
|
operationId: args.operation.operationId,
|
|
482
|
-
path:
|
|
483
|
-
|
|
722
|
+
path: withPinned({
|
|
723
|
+
build: buildPathFn(args.pathTemplate, pathParams),
|
|
724
|
+
pinned
|
|
725
|
+
}),
|
|
726
|
+
query: withPinned({
|
|
727
|
+
build: buildQueryFn(queryParams),
|
|
728
|
+
pinned
|
|
729
|
+
}),
|
|
484
730
|
body: buildBodyFn(bodyProps),
|
|
485
731
|
acceptedBodyFields,
|
|
486
|
-
extensions: extractExtensions(args.operation)
|
|
732
|
+
extensions: extractExtensions(args.operation),
|
|
733
|
+
serverManagedParameters
|
|
487
734
|
};
|
|
488
735
|
};
|
|
489
736
|
var processPath = args => {
|
|
@@ -504,6 +751,15 @@ var processPath = args => {
|
|
|
504
751
|
}
|
|
505
752
|
return tools;
|
|
506
753
|
};
|
|
754
|
+
/** Applies the defaults to {@link OpenApiToToolsOptions}. */
|
|
755
|
+
var resolveOptions = (options = {}) => {
|
|
756
|
+
return {
|
|
757
|
+
excludeExtension: options.excludeExtension ?? "x-mcp-exclude",
|
|
758
|
+
serverManagedExtension: options.serverManagedExtension ?? "x-mcp-server-managed",
|
|
759
|
+
argumentNames: options.argumentNames ?? "camelCase",
|
|
760
|
+
documents: options.documents
|
|
761
|
+
};
|
|
762
|
+
};
|
|
507
763
|
/**
|
|
508
764
|
* Translates one or more OpenAPI documents into REST-backed MCP tool
|
|
509
765
|
* definitions. Each translatable operation (has an `operationId`, a supported
|
|
@@ -517,10 +773,7 @@ var processPath = args => {
|
|
|
517
773
|
* ```
|
|
518
774
|
*/
|
|
519
775
|
var openApiToToolDefinitions = args => {
|
|
520
|
-
const options =
|
|
521
|
-
excludeExtension: args.options?.excludeExtension ?? "x-mcp-exclude",
|
|
522
|
-
serverManagedExtension: args.options?.serverManagedExtension ?? "x-mcp-server-managed"
|
|
523
|
-
};
|
|
776
|
+
const options = resolveOptions(args.options);
|
|
524
777
|
const specs = Array.isArray(args.spec) ? args.spec : [args.spec];
|
|
525
778
|
const tools = [];
|
|
526
779
|
for (const spec of specs) {
|
|
@@ -537,13 +790,34 @@ var openApiToToolDefinitions = args => {
|
|
|
537
790
|
|
|
538
791
|
//#endregion
|
|
539
792
|
//#region src/registerOpenApiTools.ts
|
|
793
|
+
/** The text the default `toText` answers when the API returned no body. */
|
|
794
|
+
var NO_CONTENT_TEXT = "Succeeded. The operation returned no content.";
|
|
540
795
|
var defaultToText = data => {
|
|
796
|
+
if (data === void 0 || data === "") return NO_CONTENT_TEXT;
|
|
541
797
|
return typeof data === "string" ? data : JSON.stringify(data, null, 2);
|
|
542
798
|
};
|
|
543
799
|
/**
|
|
800
|
+
* Replaces whatever the model sent for server-managed parameters with the
|
|
801
|
+
* values `serverParameters` supplies, so the model can never set them.
|
|
802
|
+
*/
|
|
803
|
+
var applyServerParameters = async args => {
|
|
804
|
+
const managed = args.tool.serverManagedParameters;
|
|
805
|
+
if (managed.length === 0) return args.handlerArgs;
|
|
806
|
+
const result = {
|
|
807
|
+
...args.handlerArgs
|
|
808
|
+
};
|
|
809
|
+
for (const param of managed) delete result[param.argName];
|
|
810
|
+
const values = args.serverParameters ? await args.serverParameters({
|
|
811
|
+
tool: args.tool,
|
|
812
|
+
headers: args.headers
|
|
813
|
+
}) : {};
|
|
814
|
+
for (const param of managed) if (values[param.name] !== void 0) result[param.argName] = values[param.name];
|
|
815
|
+
return result;
|
|
816
|
+
};
|
|
817
|
+
/**
|
|
544
818
|
* Derives MCP tools from OpenAPI document(s) and registers each on the given
|
|
545
|
-
* MCP server. Every tool's handler resolves the incoming
|
|
546
|
-
*
|
|
819
|
+
* MCP server. Every tool's handler resolves the incoming args into a concrete
|
|
820
|
+
* HTTP request and delegates execution to `callApi`.
|
|
547
821
|
*
|
|
548
822
|
* @returns The list of {@link ToolDefinition} that were registered.
|
|
549
823
|
*
|
|
@@ -557,10 +831,10 @@ var defaultToText = data => {
|
|
|
557
831
|
* registerOpenApiTools({
|
|
558
832
|
* server,
|
|
559
833
|
* spec: myOpenApiDocument,
|
|
560
|
-
* callApi: async ({ method, url, body }) => {
|
|
834
|
+
* callApi: async ({ method, url, body, headers }) => {
|
|
561
835
|
* const res = await fetch(`https://api.example.com${url}`, {
|
|
562
836
|
* method,
|
|
563
|
-
* headers: { 'Content-Type': 'application/json' },
|
|
837
|
+
* headers: { ...headers, 'Content-Type': 'application/json' },
|
|
564
838
|
* body: body ? JSON.stringify(body) : undefined,
|
|
565
839
|
* });
|
|
566
840
|
* return res.json();
|
|
@@ -578,7 +852,14 @@ var registerOpenApiTools = args => {
|
|
|
578
852
|
name: tool.name,
|
|
579
853
|
description: tool.description,
|
|
580
854
|
inputSchema: tool.inputSchema,
|
|
581
|
-
handler: async
|
|
855
|
+
handler: async rawArgs => {
|
|
856
|
+
const headers = getApiHeaders();
|
|
857
|
+
const handlerArgs = await applyServerParameters({
|
|
858
|
+
tool,
|
|
859
|
+
handlerArgs: rawArgs,
|
|
860
|
+
headers,
|
|
861
|
+
serverParameters: args.serverParameters
|
|
862
|
+
});
|
|
582
863
|
const url = tool.path(handlerArgs) + (tool.query ? tool.query(handlerArgs) : "");
|
|
583
864
|
return {
|
|
584
865
|
content: [{
|
|
@@ -587,7 +868,8 @@ var registerOpenApiTools = args => {
|
|
|
587
868
|
method: tool.method,
|
|
588
869
|
url,
|
|
589
870
|
body: tool.body ? tool.body(handlerArgs) : void 0,
|
|
590
|
-
tool
|
|
871
|
+
tool,
|
|
872
|
+
headers
|
|
591
873
|
}))
|
|
592
874
|
}]
|
|
593
875
|
};
|
|
@@ -597,4 +879,4 @@ var registerOpenApiTools = args => {
|
|
|
597
879
|
};
|
|
598
880
|
|
|
599
881
|
//#endregion
|
|
600
|
-
export { DEFAULT_EXCLUDE_EXTENSION, DEFAULT_SERVER_MANAGED_EXTENSION, buildBodyFn, buildInputSchema, buildPathFn, buildQueryFn, dereferenceSchema, extractAcceptedBodyFields, extractBodyProps, extractPathParams, extractQueryParams, getJsonSchemaType, openApiToToolDefinitions, operationIdToToolName, processOperation, processPath, registerOpenApiTools, resolveParameter, resolveSchema, snakeToCamel };
|
|
882
|
+
export { DEFAULT_EXCLUDE_EXTENSION, DEFAULT_SERVER_MANAGED_EXTENSION, NO_CONTENT_TEXT, buildBodyFn, buildInputSchema, buildPathFn, buildQueryFn, dereferenceSchema, extractAcceptedBodyFields, extractBodyProps, extractPathParams, extractQueryParams, getJsonSchemaType, openApiToToolDefinitions, operationIdToToolName, processOperation, processPath, registerOpenApiTools, resolveParameter, resolveSchema, snakeToCamel };
|