@ttoss/http-server-mcp-openapi 0.3.0 → 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 +13 -3
- package/dist/index.cjs +118 -12
- package/dist/index.d.cts +18 -4
- package/dist/index.d.mts +18 -4
- package/dist/index.mjs +118 -12
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -69,6 +69,10 @@ body property declared as a single-entry `allOf` (usually `allOf: [{ $ref }]`
|
|
|
69
69
|
beside its own `description`) takes `type`, `nullable` and `items` from the
|
|
70
70
|
referenced schema; a multi-entry `allOf` is forwarded verbatim, and a property
|
|
71
71
|
with no declared type is advertised untyped so it accepts any value.
|
|
72
|
+
OpenAPI's `nullable` becomes JSON Schema at every depth — inside `items`,
|
|
73
|
+
`properties`, `additionalProperties` and `oneOf` / `anyOf` / `allOf`
|
|
74
|
+
alternatives: `nullable: true` adds `'null'` to the `type`, and the keyword is
|
|
75
|
+
dropped.
|
|
72
76
|
Parameters declared at the **path-item level** (shared by every operation on a
|
|
73
77
|
path) are merged into each operation; an operation-level parameter overrides a
|
|
74
78
|
path-item one with the same `name`+`in`.
|
|
@@ -130,7 +134,7 @@ registerOpenApiTools({
|
|
|
130
134
|
callApi,
|
|
131
135
|
options: {
|
|
132
136
|
excludeExtension: 'x-mcp-exclude', // operations flagged truthy are skipped
|
|
133
|
-
serverManagedExtension: 'x-mcp-server-managed', //
|
|
137
|
+
serverManagedExtension: 'x-mcp-server-managed', // or several: ['x-a', 'x-b']
|
|
134
138
|
argumentNames: 'camelCase', // or 'verbatim'
|
|
135
139
|
documents: { './tags.yaml': tagsDocument }, // targets of cross-file $refs
|
|
136
140
|
},
|
|
@@ -139,8 +143,8 @@ registerOpenApiTools({
|
|
|
139
143
|
|
|
140
144
|
- **`excludeExtension`** (default `x-mcp-exclude`) — an operation with this
|
|
141
145
|
extension set truthy is omitted from the tool surface.
|
|
142
|
-
- **`serverManagedExtension`** (default `x-mcp-server-managed`) —
|
|
143
|
-
[Server-managed values](#server-managed-values).
|
|
146
|
+
- **`serverManagedExtension`** (default `x-mcp-server-managed`) — one name or
|
|
147
|
+
an array of names; see [Server-managed values](#server-managed-values).
|
|
144
148
|
- **`argumentNames`** (default `camelCase`) — `verbatim` keeps the spec's
|
|
145
149
|
parameter and property names as tool argument names.
|
|
146
150
|
- **`documents`** — sibling documents for `$ref`s with a file part, keyed by
|
|
@@ -173,6 +177,12 @@ registerOpenApiTools({
|
|
|
173
177
|
With `openApiToToolDefinitions`, set each entry's `argName` in the args before
|
|
174
178
|
calling `tool.path` / `tool.query`.
|
|
175
179
|
|
|
180
|
+
A **string** extension value pins the parameter: `wait` declared with
|
|
181
|
+
`x-mcp-server-managed: 'true'` is always sent as `wait=true`. `tool.path` and
|
|
182
|
+
`tool.query` apply pinned values themselves, over anything in the args or
|
|
183
|
+
`serverParameters`, and the entry in `serverManagedParameters` carries it as
|
|
184
|
+
`value`.
|
|
185
|
+
|
|
176
186
|
### Reading custom extensions
|
|
177
187
|
|
|
178
188
|
Every `x-` prefixed extension on an operation is forwarded verbatim on
|
package/dist/index.cjs
CHANGED
|
@@ -319,6 +319,85 @@ var buildBodyFn = bodyProps => {
|
|
|
319
319
|
};
|
|
320
320
|
};
|
|
321
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
|
+
|
|
322
401
|
//#endregion
|
|
323
402
|
//#region src/types.ts
|
|
324
403
|
var DEFAULT_EXCLUDE_EXTENSION = "x-mcp-exclude";
|
|
@@ -352,6 +431,14 @@ var dedupeByName = items => {
|
|
|
352
431
|
for (const item of items) byName.set(item.name, item);
|
|
353
432
|
return [...byName.values()];
|
|
354
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
|
+
};
|
|
355
442
|
var extractPathParams = args => {
|
|
356
443
|
const toArgName = args.toArgName ?? snakeToCamel;
|
|
357
444
|
const flag = args.serverManagedExtension ?? "x-mcp-server-managed";
|
|
@@ -363,7 +450,10 @@ var extractPathParams = args => {
|
|
|
363
450
|
return {
|
|
364
451
|
name: p.name || "",
|
|
365
452
|
argName: toArgName(p.name || ""),
|
|
366
|
-
|
|
453
|
+
...managedFields(readServerManaged({
|
|
454
|
+
node: p,
|
|
455
|
+
extension: flag
|
|
456
|
+
}))
|
|
367
457
|
};
|
|
368
458
|
}));
|
|
369
459
|
};
|
|
@@ -383,11 +473,13 @@ var extractQueryParams = args => {
|
|
|
383
473
|
type: p.schema?.type || "string",
|
|
384
474
|
style: p.style,
|
|
385
475
|
explode: p.explode,
|
|
386
|
-
|
|
476
|
+
...managedFields(readServerManaged({
|
|
477
|
+
node: p,
|
|
478
|
+
extension: flag
|
|
479
|
+
}))
|
|
387
480
|
};
|
|
388
481
|
}));
|
|
389
482
|
};
|
|
390
|
-
/** Lists the path and query params flagged as server-managed. */
|
|
391
483
|
var collectServerManagedParameters = args => {
|
|
392
484
|
return [...args.pathParams.map(p => {
|
|
393
485
|
return {
|
|
@@ -405,7 +497,10 @@ var collectServerManagedParameters = args => {
|
|
|
405
497
|
return {
|
|
406
498
|
name: p.name,
|
|
407
499
|
in: p.in,
|
|
408
|
-
argName: p.argName
|
|
500
|
+
argName: p.argName,
|
|
501
|
+
...(p.pinnedValue === void 0 ? {} : {
|
|
502
|
+
value: p.pinnedValue
|
|
503
|
+
})
|
|
409
504
|
};
|
|
410
505
|
});
|
|
411
506
|
};
|
|
@@ -498,7 +593,7 @@ var buildInputSchema = (pathParams, queryParams, bodyProps) => {
|
|
|
498
593
|
return p.argName;
|
|
499
594
|
})];
|
|
500
595
|
const properties = {};
|
|
501
|
-
for (const param of allParams) properties[param.argName] = "description" in param ? buildTypedProperty(param) : {
|
|
596
|
+
for (const param of allParams) properties[param.argName] = "description" in param ? normalizeNullable(buildTypedProperty(param)) : {
|
|
502
597
|
type: "string",
|
|
503
598
|
description: ""
|
|
504
599
|
};
|
|
@@ -550,7 +645,10 @@ var extractBodyProps = args => {
|
|
|
550
645
|
const bodySchema = resolveBodySchema(args);
|
|
551
646
|
if (!bodySchema?.properties) return [];
|
|
552
647
|
return Object.entries(bodySchema.properties).filter(([, value]) => {
|
|
553
|
-
return !
|
|
648
|
+
return !readServerManaged({
|
|
649
|
+
node: value,
|
|
650
|
+
extension: args.serverManagedExtension
|
|
651
|
+
}).managed;
|
|
554
652
|
}).map(([key, value]) => {
|
|
555
653
|
const val = flattenSingleAllOf(value);
|
|
556
654
|
return {
|
|
@@ -607,6 +705,11 @@ var processOperation = args => {
|
|
|
607
705
|
toArgName
|
|
608
706
|
});
|
|
609
707
|
const inputSchema = buildInputSchema(pathParams, queryParams, bodyProps);
|
|
708
|
+
const serverManagedParameters = collectServerManagedParameters({
|
|
709
|
+
pathParams,
|
|
710
|
+
queryParams
|
|
711
|
+
});
|
|
712
|
+
const pinned = pinnedArgs(serverManagedParameters);
|
|
610
713
|
const acceptedBodyFields = extractAcceptedBodyFields({
|
|
611
714
|
requestBody: args.operation.requestBody,
|
|
612
715
|
spec: args.spec,
|
|
@@ -619,15 +722,18 @@ var processOperation = args => {
|
|
|
619
722
|
method: httpMethod,
|
|
620
723
|
pathTemplate: args.pathTemplate,
|
|
621
724
|
operationId: args.operation.operationId,
|
|
622
|
-
path:
|
|
623
|
-
|
|
725
|
+
path: withPinned({
|
|
726
|
+
build: buildPathFn(args.pathTemplate, pathParams),
|
|
727
|
+
pinned
|
|
728
|
+
}),
|
|
729
|
+
query: withPinned({
|
|
730
|
+
build: buildQueryFn(queryParams),
|
|
731
|
+
pinned
|
|
732
|
+
}),
|
|
624
733
|
body: buildBodyFn(bodyProps),
|
|
625
734
|
acceptedBodyFields,
|
|
626
735
|
extensions: extractExtensions(args.operation),
|
|
627
|
-
serverManagedParameters
|
|
628
|
-
pathParams,
|
|
629
|
-
queryParams
|
|
630
|
-
})
|
|
736
|
+
serverManagedParameters
|
|
631
737
|
};
|
|
632
738
|
};
|
|
633
739
|
var processPath = args => {
|
package/dist/index.d.cts
CHANGED
|
@@ -84,6 +84,11 @@ interface ServerManagedParameter {
|
|
|
84
84
|
in: 'path' | 'query';
|
|
85
85
|
/** The args key `path` / `query` read the value from. */
|
|
86
86
|
argName: string;
|
|
87
|
+
/**
|
|
88
|
+
* The value the spec pins, when the extension is a string. `path` / `query`
|
|
89
|
+
* always send it, whatever the args or `serverParameters` carry.
|
|
90
|
+
*/
|
|
91
|
+
value?: string;
|
|
87
92
|
}
|
|
88
93
|
/** Minimal shape of an OpenAPI document consumed by the generator. */
|
|
89
94
|
interface OpenApiSpec {
|
|
@@ -150,11 +155,14 @@ interface OpenApiToToolsOptions {
|
|
|
150
155
|
* - On a **request-body property**, the property stays in
|
|
151
156
|
* `acceptedBodyFields` and is never sent (the API sets it itself).
|
|
152
157
|
* - On a **path or query parameter**, the parameter is listed in
|
|
153
|
-
* `serverManagedParameters` and the consumer supplies its value.
|
|
158
|
+
* `serverManagedParameters` and the consumer supplies its value. A string
|
|
159
|
+
* extension value (`'true'`) pins it: the builders always send that value.
|
|
160
|
+
*
|
|
161
|
+
* Several names may be given; a node flagged by any of them is managed.
|
|
154
162
|
*
|
|
155
163
|
* @default 'x-mcp-server-managed'
|
|
156
164
|
*/
|
|
157
|
-
serverManagedExtension?: string;
|
|
165
|
+
serverManagedExtension?: string | string[];
|
|
158
166
|
/**
|
|
159
167
|
* How tool argument names are derived from the spec's parameter and
|
|
160
168
|
* body-property names.
|
|
@@ -181,6 +189,10 @@ type ResolvedToolOptions = Required<Omit<OpenApiToToolsOptions, 'documents'>> &
|
|
|
181
189
|
declare const DEFAULT_EXCLUDE_EXTENSION = "x-mcp-exclude";
|
|
182
190
|
declare const DEFAULT_SERVER_MANAGED_EXTENSION = "x-mcp-server-managed";
|
|
183
191
|
//#endregion
|
|
192
|
+
//#region src/serverManaged.d.ts
|
|
193
|
+
/** One extension name, or several that all mean "the server sets this". */
|
|
194
|
+
type ServerManagedExtension = string | string[];
|
|
195
|
+
//#endregion
|
|
184
196
|
//#region src/parameters.d.ts
|
|
185
197
|
/**
|
|
186
198
|
* Folds `_` and `-` separators into camelCase. OpenAPI operation and parameter
|
|
@@ -200,12 +212,13 @@ type ExtractParamsArgs = {
|
|
|
200
212
|
spec: OpenApiSpec; /** Sibling documents cross-file `$ref`s resolve against. */
|
|
201
213
|
documents?: OpenApiDocuments; /** Maps a spec name to its tool argument name. @default snakeToCamel */
|
|
202
214
|
toArgName?: ToArgName; /** @default DEFAULT_SERVER_MANAGED_EXTENSION */
|
|
203
|
-
serverManagedExtension?:
|
|
215
|
+
serverManagedExtension?: ServerManagedExtension;
|
|
204
216
|
};
|
|
205
217
|
declare const extractPathParams: (args: ExtractParamsArgs) => Array<{
|
|
206
218
|
name: string;
|
|
207
219
|
argName: string;
|
|
208
220
|
serverManaged: boolean;
|
|
221
|
+
pinnedValue?: string;
|
|
209
222
|
}>;
|
|
210
223
|
declare const extractQueryParams: (args: ExtractParamsArgs) => Array<{
|
|
211
224
|
name: string;
|
|
@@ -216,6 +229,7 @@ declare const extractQueryParams: (args: ExtractParamsArgs) => Array<{
|
|
|
216
229
|
style?: string;
|
|
217
230
|
explode?: boolean;
|
|
218
231
|
serverManaged: boolean;
|
|
232
|
+
pinnedValue?: string;
|
|
219
233
|
}>;
|
|
220
234
|
//#endregion
|
|
221
235
|
//#region src/registerOpenApiTools.d.ts
|
|
@@ -415,7 +429,7 @@ declare const extractAcceptedBodyFields: (args: {
|
|
|
415
429
|
declare const extractBodyProps: (args: {
|
|
416
430
|
requestBody?: RequestBodySpec;
|
|
417
431
|
spec: OpenApiSpec;
|
|
418
|
-
serverManagedExtension:
|
|
432
|
+
serverManagedExtension: ServerManagedExtension;
|
|
419
433
|
documents?: OpenApiDocuments; /** Maps a spec name to its tool argument name. @default snakeToCamel */
|
|
420
434
|
toArgName?: ToArgName;
|
|
421
435
|
}) => Array<{
|
package/dist/index.d.mts
CHANGED
|
@@ -84,6 +84,11 @@ interface ServerManagedParameter {
|
|
|
84
84
|
in: 'path' | 'query';
|
|
85
85
|
/** The args key `path` / `query` read the value from. */
|
|
86
86
|
argName: string;
|
|
87
|
+
/**
|
|
88
|
+
* The value the spec pins, when the extension is a string. `path` / `query`
|
|
89
|
+
* always send it, whatever the args or `serverParameters` carry.
|
|
90
|
+
*/
|
|
91
|
+
value?: string;
|
|
87
92
|
}
|
|
88
93
|
/** Minimal shape of an OpenAPI document consumed by the generator. */
|
|
89
94
|
interface OpenApiSpec {
|
|
@@ -150,11 +155,14 @@ interface OpenApiToToolsOptions {
|
|
|
150
155
|
* - On a **request-body property**, the property stays in
|
|
151
156
|
* `acceptedBodyFields` and is never sent (the API sets it itself).
|
|
152
157
|
* - On a **path or query parameter**, the parameter is listed in
|
|
153
|
-
* `serverManagedParameters` and the consumer supplies its value.
|
|
158
|
+
* `serverManagedParameters` and the consumer supplies its value. A string
|
|
159
|
+
* extension value (`'true'`) pins it: the builders always send that value.
|
|
160
|
+
*
|
|
161
|
+
* Several names may be given; a node flagged by any of them is managed.
|
|
154
162
|
*
|
|
155
163
|
* @default 'x-mcp-server-managed'
|
|
156
164
|
*/
|
|
157
|
-
serverManagedExtension?: string;
|
|
165
|
+
serverManagedExtension?: string | string[];
|
|
158
166
|
/**
|
|
159
167
|
* How tool argument names are derived from the spec's parameter and
|
|
160
168
|
* body-property names.
|
|
@@ -181,6 +189,10 @@ type ResolvedToolOptions = Required<Omit<OpenApiToToolsOptions, 'documents'>> &
|
|
|
181
189
|
declare const DEFAULT_EXCLUDE_EXTENSION = "x-mcp-exclude";
|
|
182
190
|
declare const DEFAULT_SERVER_MANAGED_EXTENSION = "x-mcp-server-managed";
|
|
183
191
|
//#endregion
|
|
192
|
+
//#region src/serverManaged.d.ts
|
|
193
|
+
/** One extension name, or several that all mean "the server sets this". */
|
|
194
|
+
type ServerManagedExtension = string | string[];
|
|
195
|
+
//#endregion
|
|
184
196
|
//#region src/parameters.d.ts
|
|
185
197
|
/**
|
|
186
198
|
* Folds `_` and `-` separators into camelCase. OpenAPI operation and parameter
|
|
@@ -200,12 +212,13 @@ type ExtractParamsArgs = {
|
|
|
200
212
|
spec: OpenApiSpec; /** Sibling documents cross-file `$ref`s resolve against. */
|
|
201
213
|
documents?: OpenApiDocuments; /** Maps a spec name to its tool argument name. @default snakeToCamel */
|
|
202
214
|
toArgName?: ToArgName; /** @default DEFAULT_SERVER_MANAGED_EXTENSION */
|
|
203
|
-
serverManagedExtension?:
|
|
215
|
+
serverManagedExtension?: ServerManagedExtension;
|
|
204
216
|
};
|
|
205
217
|
declare const extractPathParams: (args: ExtractParamsArgs) => Array<{
|
|
206
218
|
name: string;
|
|
207
219
|
argName: string;
|
|
208
220
|
serverManaged: boolean;
|
|
221
|
+
pinnedValue?: string;
|
|
209
222
|
}>;
|
|
210
223
|
declare const extractQueryParams: (args: ExtractParamsArgs) => Array<{
|
|
211
224
|
name: string;
|
|
@@ -216,6 +229,7 @@ declare const extractQueryParams: (args: ExtractParamsArgs) => Array<{
|
|
|
216
229
|
style?: string;
|
|
217
230
|
explode?: boolean;
|
|
218
231
|
serverManaged: boolean;
|
|
232
|
+
pinnedValue?: string;
|
|
219
233
|
}>;
|
|
220
234
|
//#endregion
|
|
221
235
|
//#region src/registerOpenApiTools.d.ts
|
|
@@ -415,7 +429,7 @@ declare const extractAcceptedBodyFields: (args: {
|
|
|
415
429
|
declare const extractBodyProps: (args: {
|
|
416
430
|
requestBody?: RequestBodySpec;
|
|
417
431
|
spec: OpenApiSpec;
|
|
418
|
-
serverManagedExtension:
|
|
432
|
+
serverManagedExtension: ServerManagedExtension;
|
|
419
433
|
documents?: OpenApiDocuments; /** Maps a spec name to its tool argument name. @default snakeToCamel */
|
|
420
434
|
toArgName?: ToArgName;
|
|
421
435
|
}) => Array<{
|
package/dist/index.mjs
CHANGED
|
@@ -316,6 +316,85 @@ var buildBodyFn = bodyProps => {
|
|
|
316
316
|
};
|
|
317
317
|
};
|
|
318
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
|
+
|
|
319
398
|
//#endregion
|
|
320
399
|
//#region src/types.ts
|
|
321
400
|
var DEFAULT_EXCLUDE_EXTENSION = "x-mcp-exclude";
|
|
@@ -349,6 +428,14 @@ var dedupeByName = items => {
|
|
|
349
428
|
for (const item of items) byName.set(item.name, item);
|
|
350
429
|
return [...byName.values()];
|
|
351
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
|
+
};
|
|
352
439
|
var extractPathParams = args => {
|
|
353
440
|
const toArgName = args.toArgName ?? snakeToCamel;
|
|
354
441
|
const flag = args.serverManagedExtension ?? "x-mcp-server-managed";
|
|
@@ -360,7 +447,10 @@ var extractPathParams = args => {
|
|
|
360
447
|
return {
|
|
361
448
|
name: p.name || "",
|
|
362
449
|
argName: toArgName(p.name || ""),
|
|
363
|
-
|
|
450
|
+
...managedFields(readServerManaged({
|
|
451
|
+
node: p,
|
|
452
|
+
extension: flag
|
|
453
|
+
}))
|
|
364
454
|
};
|
|
365
455
|
}));
|
|
366
456
|
};
|
|
@@ -380,11 +470,13 @@ var extractQueryParams = args => {
|
|
|
380
470
|
type: p.schema?.type || "string",
|
|
381
471
|
style: p.style,
|
|
382
472
|
explode: p.explode,
|
|
383
|
-
|
|
473
|
+
...managedFields(readServerManaged({
|
|
474
|
+
node: p,
|
|
475
|
+
extension: flag
|
|
476
|
+
}))
|
|
384
477
|
};
|
|
385
478
|
}));
|
|
386
479
|
};
|
|
387
|
-
/** Lists the path and query params flagged as server-managed. */
|
|
388
480
|
var collectServerManagedParameters = args => {
|
|
389
481
|
return [...args.pathParams.map(p => {
|
|
390
482
|
return {
|
|
@@ -402,7 +494,10 @@ var collectServerManagedParameters = args => {
|
|
|
402
494
|
return {
|
|
403
495
|
name: p.name,
|
|
404
496
|
in: p.in,
|
|
405
|
-
argName: p.argName
|
|
497
|
+
argName: p.argName,
|
|
498
|
+
...(p.pinnedValue === void 0 ? {} : {
|
|
499
|
+
value: p.pinnedValue
|
|
500
|
+
})
|
|
406
501
|
};
|
|
407
502
|
});
|
|
408
503
|
};
|
|
@@ -495,7 +590,7 @@ var buildInputSchema = (pathParams, queryParams, bodyProps) => {
|
|
|
495
590
|
return p.argName;
|
|
496
591
|
})];
|
|
497
592
|
const properties = {};
|
|
498
|
-
for (const param of allParams) properties[param.argName] = "description" in param ? buildTypedProperty(param) : {
|
|
593
|
+
for (const param of allParams) properties[param.argName] = "description" in param ? normalizeNullable(buildTypedProperty(param)) : {
|
|
499
594
|
type: "string",
|
|
500
595
|
description: ""
|
|
501
596
|
};
|
|
@@ -547,7 +642,10 @@ var extractBodyProps = args => {
|
|
|
547
642
|
const bodySchema = resolveBodySchema(args);
|
|
548
643
|
if (!bodySchema?.properties) return [];
|
|
549
644
|
return Object.entries(bodySchema.properties).filter(([, value]) => {
|
|
550
|
-
return !
|
|
645
|
+
return !readServerManaged({
|
|
646
|
+
node: value,
|
|
647
|
+
extension: args.serverManagedExtension
|
|
648
|
+
}).managed;
|
|
551
649
|
}).map(([key, value]) => {
|
|
552
650
|
const val = flattenSingleAllOf(value);
|
|
553
651
|
return {
|
|
@@ -604,6 +702,11 @@ var processOperation = args => {
|
|
|
604
702
|
toArgName
|
|
605
703
|
});
|
|
606
704
|
const inputSchema = buildInputSchema(pathParams, queryParams, bodyProps);
|
|
705
|
+
const serverManagedParameters = collectServerManagedParameters({
|
|
706
|
+
pathParams,
|
|
707
|
+
queryParams
|
|
708
|
+
});
|
|
709
|
+
const pinned = pinnedArgs(serverManagedParameters);
|
|
607
710
|
const acceptedBodyFields = extractAcceptedBodyFields({
|
|
608
711
|
requestBody: args.operation.requestBody,
|
|
609
712
|
spec: args.spec,
|
|
@@ -616,15 +719,18 @@ var processOperation = args => {
|
|
|
616
719
|
method: httpMethod,
|
|
617
720
|
pathTemplate: args.pathTemplate,
|
|
618
721
|
operationId: args.operation.operationId,
|
|
619
|
-
path:
|
|
620
|
-
|
|
722
|
+
path: withPinned({
|
|
723
|
+
build: buildPathFn(args.pathTemplate, pathParams),
|
|
724
|
+
pinned
|
|
725
|
+
}),
|
|
726
|
+
query: withPinned({
|
|
727
|
+
build: buildQueryFn(queryParams),
|
|
728
|
+
pinned
|
|
729
|
+
}),
|
|
621
730
|
body: buildBodyFn(bodyProps),
|
|
622
731
|
acceptedBodyFields,
|
|
623
732
|
extensions: extractExtensions(args.operation),
|
|
624
|
-
serverManagedParameters
|
|
625
|
-
pathParams,
|
|
626
|
-
queryParams
|
|
627
|
-
})
|
|
733
|
+
serverManagedParameters
|
|
628
734
|
};
|
|
629
735
|
};
|
|
630
736
|
var processPath = args => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ttoss/http-server-mcp-openapi",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Generate Model Context Protocol (MCP) tools from an OpenAPI specification for @ttoss/http-server-mcp",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai",
|
|
@@ -42,8 +42,8 @@
|
|
|
42
42
|
"jest": "^30.4.2",
|
|
43
43
|
"supertest": "^7.2.2",
|
|
44
44
|
"tsdown": "^0.22.2",
|
|
45
|
-
"@ttoss/
|
|
46
|
-
"@ttoss/
|
|
45
|
+
"@ttoss/config": "^1.38.0",
|
|
46
|
+
"@ttoss/http-server": "^0.8.1"
|
|
47
47
|
},
|
|
48
48
|
"publishConfig": {
|
|
49
49
|
"access": "public",
|