@ttoss/http-server-mcp-openapi 0.2.14 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +65 -20
- package/dist/index.cjs +330 -108
- package/dist/index.d.cts +167 -67
- package/dist/index.d.mts +167 -67
- package/dist/index.mjs +331 -110
- package/package.json +4 -4
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,8 +310,8 @@ 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
|
};
|
|
@@ -243,17 +322,93 @@ var DEFAULT_EXCLUDE_EXTENSION = "x-mcp-exclude";
|
|
|
243
322
|
var DEFAULT_SERVER_MANAGED_EXTENSION = "x-mcp-server-managed";
|
|
244
323
|
|
|
245
324
|
//#endregion
|
|
246
|
-
//#region src/
|
|
325
|
+
//#region src/parameters.ts
|
|
247
326
|
/**
|
|
248
327
|
* Folds `_` and `-` separators into camelCase. OpenAPI operation and parameter
|
|
249
|
-
* names may be snake_case (`agent_id`) or kebab-case (`list-tools`)
|
|
250
|
-
*
|
|
328
|
+
* names may be snake_case (`agent_id`) or kebab-case (`list-tools`); with the
|
|
329
|
+
* default `argumentNames: 'camelCase'` both are folded into tool arguments.
|
|
251
330
|
*/
|
|
252
331
|
var snakeToCamel = str => {
|
|
253
332
|
return str.replace(/[_-]([a-z])/g, (_, letter) => {
|
|
254
333
|
return letter.toUpperCase();
|
|
255
334
|
});
|
|
256
335
|
};
|
|
336
|
+
var verbatim = name => {
|
|
337
|
+
return name;
|
|
338
|
+
};
|
|
339
|
+
var argNameMapper = argumentNames => {
|
|
340
|
+
return argumentNames === "verbatim" ? verbatim : snakeToCamel;
|
|
341
|
+
};
|
|
342
|
+
/**
|
|
343
|
+
* Deduplicates parameter entries by `name`, keeping the last occurrence. When
|
|
344
|
+
* path-item-level and operation-level parameters are concatenated (operation
|
|
345
|
+
* last), this makes the operation-level entry win — as the OpenAPI spec requires.
|
|
346
|
+
*/
|
|
347
|
+
var dedupeByName = items => {
|
|
348
|
+
const byName = /* @__PURE__ */new Map();
|
|
349
|
+
for (const item of items) byName.set(item.name, item);
|
|
350
|
+
return [...byName.values()];
|
|
351
|
+
};
|
|
352
|
+
var extractPathParams = args => {
|
|
353
|
+
const toArgName = args.toArgName ?? snakeToCamel;
|
|
354
|
+
const flag = args.serverManagedExtension ?? "x-mcp-server-managed";
|
|
355
|
+
return dedupeByName((args.parameters || []).map(p => {
|
|
356
|
+
return resolveParameter(p, args.spec, args.documents);
|
|
357
|
+
}).filter(p => {
|
|
358
|
+
return p.in === "path";
|
|
359
|
+
}).map(p => {
|
|
360
|
+
return {
|
|
361
|
+
name: p.name || "",
|
|
362
|
+
argName: toArgName(p.name || ""),
|
|
363
|
+
serverManaged: Boolean(p[flag])
|
|
364
|
+
};
|
|
365
|
+
}));
|
|
366
|
+
};
|
|
367
|
+
var extractQueryParams = args => {
|
|
368
|
+
const toArgName = args.toArgName ?? snakeToCamel;
|
|
369
|
+
const flag = args.serverManagedExtension ?? "x-mcp-server-managed";
|
|
370
|
+
return dedupeByName((args.parameters || []).map(p => {
|
|
371
|
+
return resolveParameter(p, args.spec, args.documents);
|
|
372
|
+
}).filter(p => {
|
|
373
|
+
return p.in === "query";
|
|
374
|
+
}).map(p => {
|
|
375
|
+
return {
|
|
376
|
+
name: p.name || "",
|
|
377
|
+
argName: toArgName(p.name || ""),
|
|
378
|
+
description: p.description || "",
|
|
379
|
+
required: p.required || false,
|
|
380
|
+
type: p.schema?.type || "string",
|
|
381
|
+
style: p.style,
|
|
382
|
+
explode: p.explode,
|
|
383
|
+
serverManaged: Boolean(p[flag])
|
|
384
|
+
};
|
|
385
|
+
}));
|
|
386
|
+
};
|
|
387
|
+
/** Lists the path and query params flagged as server-managed. */
|
|
388
|
+
var collectServerManagedParameters = args => {
|
|
389
|
+
return [...args.pathParams.map(p => {
|
|
390
|
+
return {
|
|
391
|
+
...p,
|
|
392
|
+
in: "path"
|
|
393
|
+
};
|
|
394
|
+
}), ...args.queryParams.map(p => {
|
|
395
|
+
return {
|
|
396
|
+
...p,
|
|
397
|
+
in: "query"
|
|
398
|
+
};
|
|
399
|
+
})].filter(p => {
|
|
400
|
+
return p.serverManaged;
|
|
401
|
+
}).map(p => {
|
|
402
|
+
return {
|
|
403
|
+
name: p.name,
|
|
404
|
+
in: p.in,
|
|
405
|
+
argName: p.argName
|
|
406
|
+
};
|
|
407
|
+
});
|
|
408
|
+
};
|
|
409
|
+
|
|
410
|
+
//#endregion
|
|
411
|
+
//#region src/toolDefinitions.ts
|
|
257
412
|
/** Converts a camelCase `operationId` to a kebab-case tool name. */
|
|
258
413
|
var operationIdToToolName = operationId => {
|
|
259
414
|
return operationId.replace(/([A-Z])/g, "-$1").toLowerCase().replace(/^-/, "");
|
|
@@ -269,11 +424,13 @@ var sanitizeDescription = description => {
|
|
|
269
424
|
return (description || "").replace(/'/g, "\\'").replace(/\n/g, " ").trim();
|
|
270
425
|
};
|
|
271
426
|
/**
|
|
272
|
-
*
|
|
273
|
-
*
|
|
427
|
+
* Forwards a property's `oneOf` / `anyOf` / multi-entry `allOf` verbatim, or
|
|
428
|
+
* returns `undefined` when it declares none.
|
|
274
429
|
*/
|
|
275
|
-
var
|
|
276
|
-
const
|
|
430
|
+
var buildComposedProperty = param => {
|
|
431
|
+
const {
|
|
432
|
+
description
|
|
433
|
+
} = param;
|
|
277
434
|
if (param.oneOf && param.oneOf.length > 0) return {
|
|
278
435
|
oneOf: param.oneOf,
|
|
279
436
|
description
|
|
@@ -282,6 +439,25 @@ var buildTypedProperty = param => {
|
|
|
282
439
|
anyOf: param.anyOf,
|
|
283
440
|
description
|
|
284
441
|
};
|
|
442
|
+
if (param.allOf && param.allOf.length > 0) return {
|
|
443
|
+
allOf: param.allOf,
|
|
444
|
+
description
|
|
445
|
+
};
|
|
446
|
+
};
|
|
447
|
+
/**
|
|
448
|
+
* Builds a single body/query property's `JsonSchemaProperty`. Split out of
|
|
449
|
+
* {@link buildInputSchema} to keep that function's size and branching down.
|
|
450
|
+
*/
|
|
451
|
+
var buildTypedProperty = param => {
|
|
452
|
+
const description = sanitizeDescription(param.description);
|
|
453
|
+
const composed = buildComposedProperty({
|
|
454
|
+
...param,
|
|
455
|
+
description
|
|
456
|
+
});
|
|
457
|
+
if (composed) return composed;
|
|
458
|
+
if (param.type === void 0) return {
|
|
459
|
+
description
|
|
460
|
+
};
|
|
285
461
|
const jsonType = getJsonSchemaType(param.type);
|
|
286
462
|
const finalType = param.nullable === true ? [jsonType, "null"] : jsonType;
|
|
287
463
|
if (param.type === "array") return {
|
|
@@ -297,23 +473,29 @@ var buildTypedProperty = param => {
|
|
|
297
473
|
};
|
|
298
474
|
};
|
|
299
475
|
var buildInputSchema = (pathParams, queryParams, bodyProps) => {
|
|
300
|
-
const
|
|
476
|
+
const modelPathParams = pathParams.filter(p => {
|
|
477
|
+
return !p.serverManaged;
|
|
478
|
+
});
|
|
479
|
+
const modelQueryParams = queryParams.filter(p => {
|
|
480
|
+
return !p.serverManaged;
|
|
481
|
+
});
|
|
482
|
+
const allParams = [...modelPathParams, ...modelQueryParams, ...bodyProps];
|
|
301
483
|
if (allParams.length === 0) return {
|
|
302
484
|
type: "object"
|
|
303
485
|
};
|
|
304
|
-
const requiredFields = [...
|
|
305
|
-
return p.
|
|
306
|
-
}), ...
|
|
486
|
+
const requiredFields = [...modelPathParams.map(p => {
|
|
487
|
+
return p.argName;
|
|
488
|
+
}), ...modelQueryParams.filter(p => {
|
|
307
489
|
return p.required;
|
|
308
490
|
}).map(p => {
|
|
309
|
-
return p.
|
|
491
|
+
return p.argName;
|
|
310
492
|
}), ...bodyProps.filter(p => {
|
|
311
493
|
return p.required;
|
|
312
494
|
}).map(p => {
|
|
313
|
-
return p.
|
|
495
|
+
return p.argName;
|
|
314
496
|
})];
|
|
315
497
|
const properties = {};
|
|
316
|
-
for (const param of allParams) properties[param.
|
|
498
|
+
for (const param of allParams) properties[param.argName] = "description" in param ? buildTypedProperty(param) : {
|
|
317
499
|
type: "string",
|
|
318
500
|
description: ""
|
|
319
501
|
};
|
|
@@ -325,48 +507,9 @@ var buildInputSchema = (pathParams, queryParams, bodyProps) => {
|
|
|
325
507
|
} : {})
|
|
326
508
|
};
|
|
327
509
|
};
|
|
328
|
-
/**
|
|
329
|
-
* Deduplicates parameter entries by `name`, keeping the last occurrence. When
|
|
330
|
-
* path-item-level and operation-level parameters are concatenated (operation
|
|
331
|
-
* last), this makes the operation-level entry win — as the OpenAPI spec requires.
|
|
332
|
-
*/
|
|
333
|
-
var dedupeByName = items => {
|
|
334
|
-
const byName = /* @__PURE__ */new Map();
|
|
335
|
-
for (const item of items) byName.set(item.name, item);
|
|
336
|
-
return [...byName.values()];
|
|
337
|
-
};
|
|
338
|
-
var extractPathParams = args => {
|
|
339
|
-
return dedupeByName((args.parameters || []).map(p => {
|
|
340
|
-
return resolveParameter(p, args.spec);
|
|
341
|
-
}).filter(p => {
|
|
342
|
-
return p.in === "path";
|
|
343
|
-
}).map(p => {
|
|
344
|
-
return {
|
|
345
|
-
name: p.name || "",
|
|
346
|
-
camelName: snakeToCamel(p.name || "")
|
|
347
|
-
};
|
|
348
|
-
}));
|
|
349
|
-
};
|
|
350
|
-
var extractQueryParams = args => {
|
|
351
|
-
return dedupeByName((args.parameters || []).map(p => {
|
|
352
|
-
return resolveParameter(p, args.spec);
|
|
353
|
-
}).filter(p => {
|
|
354
|
-
return p.in === "query";
|
|
355
|
-
}).map(p => {
|
|
356
|
-
return {
|
|
357
|
-
name: p.name || "",
|
|
358
|
-
camelName: snakeToCamel(p.name || ""),
|
|
359
|
-
description: p.description || "",
|
|
360
|
-
required: p.required || false,
|
|
361
|
-
type: p.schema?.type || "string",
|
|
362
|
-
style: p.style,
|
|
363
|
-
explode: p.explode
|
|
364
|
-
};
|
|
365
|
-
}));
|
|
366
|
-
};
|
|
367
510
|
var resolveBodySchema = args => {
|
|
368
511
|
const rawBodySchema = args.requestBody?.content?.["application/json"]?.schema;
|
|
369
|
-
return resolveSchema(dereferenceSchema(rawBodySchema, args.spec), args.spec);
|
|
512
|
+
return resolveSchema(dereferenceSchema(rawBodySchema, args.spec, args.documents), args.spec, args.documents);
|
|
370
513
|
};
|
|
371
514
|
/**
|
|
372
515
|
* snake_case names of every top-level property an operation's request schema
|
|
@@ -376,23 +519,48 @@ var extractAcceptedBodyFields = args => {
|
|
|
376
519
|
const bodySchema = resolveBodySchema(args);
|
|
377
520
|
return Object.keys(bodySchema?.properties ?? {});
|
|
378
521
|
};
|
|
522
|
+
var isPlainObject = value => {
|
|
523
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
524
|
+
};
|
|
525
|
+
/**
|
|
526
|
+
* Folds a single-entry `allOf` into the property that wraps it. OpenAPI
|
|
527
|
+
* declares `allOf: [{ $ref }]` so a property can carry its own `description`
|
|
528
|
+
* next to a referenced schema; without folding, the referenced `type`,
|
|
529
|
+
* `nullable` and `items` would be lost. Keys on the wrapper win over the
|
|
530
|
+
* referenced schema's. Multi-entry `allOf` is left intact.
|
|
531
|
+
*/
|
|
532
|
+
var flattenSingleAllOf = schema => {
|
|
533
|
+
const {
|
|
534
|
+
allOf,
|
|
535
|
+
...rest
|
|
536
|
+
} = schema;
|
|
537
|
+
if (!Array.isArray(allOf) || allOf.length !== 1) return schema;
|
|
538
|
+
const [entry] = allOf;
|
|
539
|
+
if (!isPlainObject(entry)) return rest;
|
|
540
|
+
return {
|
|
541
|
+
...flattenSingleAllOf(entry),
|
|
542
|
+
...rest
|
|
543
|
+
};
|
|
544
|
+
};
|
|
379
545
|
var extractBodyProps = args => {
|
|
546
|
+
const toArgName = args.toArgName ?? snakeToCamel;
|
|
380
547
|
const bodySchema = resolveBodySchema(args);
|
|
381
548
|
if (!bodySchema?.properties) return [];
|
|
382
549
|
return Object.entries(bodySchema.properties).filter(([, value]) => {
|
|
383
550
|
return !value[args.serverManagedExtension];
|
|
384
551
|
}).map(([key, value]) => {
|
|
385
|
-
const val = value;
|
|
552
|
+
const val = flattenSingleAllOf(value);
|
|
386
553
|
return {
|
|
387
554
|
snakeName: key,
|
|
388
|
-
|
|
555
|
+
argName: toArgName(key),
|
|
389
556
|
description: typeof val.description === "string" ? val.description : "",
|
|
390
557
|
required: (bodySchema.required || []).includes(key),
|
|
391
|
-
type: typeof val.type === "string" ? val.type :
|
|
558
|
+
type: typeof val.type === "string" ? val.type : void 0,
|
|
392
559
|
items: val.items,
|
|
393
560
|
nullable: val.nullable === true,
|
|
394
561
|
oneOf: Array.isArray(val.oneOf) ? val.oneOf : void 0,
|
|
395
|
-
anyOf: Array.isArray(val.anyOf) ? val.anyOf : void 0
|
|
562
|
+
anyOf: Array.isArray(val.anyOf) ? val.anyOf : void 0,
|
|
563
|
+
allOf: Array.isArray(val.allOf) ? val.allOf : void 0
|
|
396
564
|
};
|
|
397
565
|
});
|
|
398
566
|
};
|
|
@@ -409,23 +577,37 @@ var processOperation = args => {
|
|
|
409
577
|
if (args.operation[args.options.excludeExtension]) return null;
|
|
410
578
|
const toolName = operationIdToToolName(args.operation.operationId);
|
|
411
579
|
const parameters = [...(args.pathItemParameters ?? []), ...(args.operation.parameters ?? [])];
|
|
580
|
+
const {
|
|
581
|
+
documents,
|
|
582
|
+
serverManagedExtension
|
|
583
|
+
} = args.options;
|
|
584
|
+
const toArgName = argNameMapper(args.options.argumentNames);
|
|
412
585
|
const pathParams = extractPathParams({
|
|
413
586
|
parameters,
|
|
414
|
-
spec: args.spec
|
|
587
|
+
spec: args.spec,
|
|
588
|
+
documents,
|
|
589
|
+
toArgName,
|
|
590
|
+
serverManagedExtension
|
|
415
591
|
});
|
|
416
592
|
const queryParams = extractQueryParams({
|
|
417
593
|
parameters,
|
|
418
|
-
spec: args.spec
|
|
594
|
+
spec: args.spec,
|
|
595
|
+
documents,
|
|
596
|
+
toArgName,
|
|
597
|
+
serverManagedExtension
|
|
419
598
|
});
|
|
420
599
|
const bodyProps = extractBodyProps({
|
|
421
600
|
requestBody: args.operation.requestBody,
|
|
422
601
|
spec: args.spec,
|
|
423
|
-
serverManagedExtension
|
|
602
|
+
serverManagedExtension,
|
|
603
|
+
documents,
|
|
604
|
+
toArgName
|
|
424
605
|
});
|
|
425
606
|
const inputSchema = buildInputSchema(pathParams, queryParams, bodyProps);
|
|
426
607
|
const acceptedBodyFields = extractAcceptedBodyFields({
|
|
427
608
|
requestBody: args.operation.requestBody,
|
|
428
|
-
spec: args.spec
|
|
609
|
+
spec: args.spec,
|
|
610
|
+
documents
|
|
429
611
|
});
|
|
430
612
|
return {
|
|
431
613
|
name: toolName,
|
|
@@ -438,7 +620,11 @@ var processOperation = args => {
|
|
|
438
620
|
query: buildQueryFn(queryParams),
|
|
439
621
|
body: buildBodyFn(bodyProps),
|
|
440
622
|
acceptedBodyFields,
|
|
441
|
-
extensions: extractExtensions(args.operation)
|
|
623
|
+
extensions: extractExtensions(args.operation),
|
|
624
|
+
serverManagedParameters: collectServerManagedParameters({
|
|
625
|
+
pathParams,
|
|
626
|
+
queryParams
|
|
627
|
+
})
|
|
442
628
|
};
|
|
443
629
|
};
|
|
444
630
|
var processPath = args => {
|
|
@@ -459,6 +645,15 @@ var processPath = args => {
|
|
|
459
645
|
}
|
|
460
646
|
return tools;
|
|
461
647
|
};
|
|
648
|
+
/** Applies the defaults to {@link OpenApiToToolsOptions}. */
|
|
649
|
+
var resolveOptions = (options = {}) => {
|
|
650
|
+
return {
|
|
651
|
+
excludeExtension: options.excludeExtension ?? "x-mcp-exclude",
|
|
652
|
+
serverManagedExtension: options.serverManagedExtension ?? "x-mcp-server-managed",
|
|
653
|
+
argumentNames: options.argumentNames ?? "camelCase",
|
|
654
|
+
documents: options.documents
|
|
655
|
+
};
|
|
656
|
+
};
|
|
462
657
|
/**
|
|
463
658
|
* Translates one or more OpenAPI documents into REST-backed MCP tool
|
|
464
659
|
* definitions. Each translatable operation (has an `operationId`, a supported
|
|
@@ -472,10 +667,7 @@ var processPath = args => {
|
|
|
472
667
|
* ```
|
|
473
668
|
*/
|
|
474
669
|
var openApiToToolDefinitions = args => {
|
|
475
|
-
const options =
|
|
476
|
-
excludeExtension: args.options?.excludeExtension ?? "x-mcp-exclude",
|
|
477
|
-
serverManagedExtension: args.options?.serverManagedExtension ?? "x-mcp-server-managed"
|
|
478
|
-
};
|
|
670
|
+
const options = resolveOptions(args.options);
|
|
479
671
|
const specs = Array.isArray(args.spec) ? args.spec : [args.spec];
|
|
480
672
|
const tools = [];
|
|
481
673
|
for (const spec of specs) {
|
|
@@ -492,13 +684,34 @@ var openApiToToolDefinitions = args => {
|
|
|
492
684
|
|
|
493
685
|
//#endregion
|
|
494
686
|
//#region src/registerOpenApiTools.ts
|
|
687
|
+
/** The text the default `toText` answers when the API returned no body. */
|
|
688
|
+
var NO_CONTENT_TEXT = "Succeeded. The operation returned no content.";
|
|
495
689
|
var defaultToText = data => {
|
|
690
|
+
if (data === void 0 || data === "") return NO_CONTENT_TEXT;
|
|
496
691
|
return typeof data === "string" ? data : JSON.stringify(data, null, 2);
|
|
497
692
|
};
|
|
498
693
|
/**
|
|
694
|
+
* Replaces whatever the model sent for server-managed parameters with the
|
|
695
|
+
* values `serverParameters` supplies, so the model can never set them.
|
|
696
|
+
*/
|
|
697
|
+
var applyServerParameters = async args => {
|
|
698
|
+
const managed = args.tool.serverManagedParameters;
|
|
699
|
+
if (managed.length === 0) return args.handlerArgs;
|
|
700
|
+
const result = {
|
|
701
|
+
...args.handlerArgs
|
|
702
|
+
};
|
|
703
|
+
for (const param of managed) delete result[param.argName];
|
|
704
|
+
const values = args.serverParameters ? await args.serverParameters({
|
|
705
|
+
tool: args.tool,
|
|
706
|
+
headers: args.headers
|
|
707
|
+
}) : {};
|
|
708
|
+
for (const param of managed) if (values[param.name] !== void 0) result[param.argName] = values[param.name];
|
|
709
|
+
return result;
|
|
710
|
+
};
|
|
711
|
+
/**
|
|
499
712
|
* Derives MCP tools from OpenAPI document(s) and registers each on the given
|
|
500
|
-
* MCP server. Every tool's handler resolves the incoming
|
|
501
|
-
*
|
|
713
|
+
* MCP server. Every tool's handler resolves the incoming args into a concrete
|
|
714
|
+
* HTTP request and delegates execution to `callApi`.
|
|
502
715
|
*
|
|
503
716
|
* @returns The list of {@link ToolDefinition} that were registered.
|
|
504
717
|
*
|
|
@@ -512,10 +725,10 @@ var defaultToText = data => {
|
|
|
512
725
|
* registerOpenApiTools({
|
|
513
726
|
* server,
|
|
514
727
|
* spec: myOpenApiDocument,
|
|
515
|
-
* callApi: async ({ method, url, body }) => {
|
|
728
|
+
* callApi: async ({ method, url, body, headers }) => {
|
|
516
729
|
* const res = await fetch(`https://api.example.com${url}`, {
|
|
517
730
|
* method,
|
|
518
|
-
* headers: { 'Content-Type': 'application/json' },
|
|
731
|
+
* headers: { ...headers, 'Content-Type': 'application/json' },
|
|
519
732
|
* body: body ? JSON.stringify(body) : undefined,
|
|
520
733
|
* });
|
|
521
734
|
* return res.json();
|
|
@@ -533,7 +746,14 @@ var registerOpenApiTools = args => {
|
|
|
533
746
|
name: tool.name,
|
|
534
747
|
description: tool.description,
|
|
535
748
|
inputSchema: tool.inputSchema,
|
|
536
|
-
handler: async
|
|
749
|
+
handler: async rawArgs => {
|
|
750
|
+
const headers = getApiHeaders();
|
|
751
|
+
const handlerArgs = await applyServerParameters({
|
|
752
|
+
tool,
|
|
753
|
+
handlerArgs: rawArgs,
|
|
754
|
+
headers,
|
|
755
|
+
serverParameters: args.serverParameters
|
|
756
|
+
});
|
|
537
757
|
const url = tool.path(handlerArgs) + (tool.query ? tool.query(handlerArgs) : "");
|
|
538
758
|
return {
|
|
539
759
|
content: [{
|
|
@@ -542,7 +762,8 @@ var registerOpenApiTools = args => {
|
|
|
542
762
|
method: tool.method,
|
|
543
763
|
url,
|
|
544
764
|
body: tool.body ? tool.body(handlerArgs) : void 0,
|
|
545
|
-
tool
|
|
765
|
+
tool,
|
|
766
|
+
headers
|
|
546
767
|
}))
|
|
547
768
|
}]
|
|
548
769
|
};
|
|
@@ -552,4 +773,4 @@ var registerOpenApiTools = args => {
|
|
|
552
773
|
};
|
|
553
774
|
|
|
554
775
|
//#endregion
|
|
555
|
-
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 };
|
|
776
|
+
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 };
|