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