@kubb/plugin-mcp 5.0.0-beta.3 → 5.0.0-beta.31
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 +25 -5
- package/dist/index.cjs +289 -153
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +78 -22
- package/dist/index.js +290 -154
- package/dist/index.js.map +1 -1
- package/extension.yaml +926 -0
- package/package.json +11 -12
- package/src/components/McpHandler.tsx +15 -20
- package/src/components/Server.tsx +8 -8
- package/src/generators/mcpGenerator.tsx +21 -23
- package/src/generators/serverGenerator.tsx +22 -22
- package/src/plugin.ts +38 -18
- package/src/resolvers/resolverMcp.ts +16 -6
- package/src/types.ts +27 -13
- package/src/utils.ts +15 -80
package/dist/index.cjs
CHANGED
|
@@ -247,16 +247,16 @@ var URLPath = class {
|
|
|
247
247
|
get object() {
|
|
248
248
|
return this.toObject();
|
|
249
249
|
}
|
|
250
|
-
/** Returns a map of path parameter names, or `
|
|
250
|
+
/** Returns a map of path parameter names, or `null` when the path has no parameters.
|
|
251
251
|
*
|
|
252
252
|
* @example
|
|
253
253
|
* ```ts
|
|
254
254
|
* new URLPath('/pet/{petId}').params // { petId: 'petId' }
|
|
255
|
-
* new URLPath('/pet').params //
|
|
255
|
+
* new URLPath('/pet').params // null
|
|
256
256
|
* ```
|
|
257
257
|
*/
|
|
258
258
|
get params() {
|
|
259
|
-
return this.
|
|
259
|
+
return this.toParamsObject();
|
|
260
260
|
}
|
|
261
261
|
#transformParam(raw) {
|
|
262
262
|
const param = isValidVarName(raw) ? raw : camelCase(raw);
|
|
@@ -274,7 +274,7 @@ var URLPath = class {
|
|
|
274
274
|
toObject({ type = "path", replacer, stringify } = {}) {
|
|
275
275
|
const object = {
|
|
276
276
|
url: type === "path" ? this.toURLPath() : this.toTemplateString({ replacer }),
|
|
277
|
-
params: this.
|
|
277
|
+
params: this.toParamsObject()
|
|
278
278
|
};
|
|
279
279
|
if (stringify) {
|
|
280
280
|
if (type === "template") return JSON.stringify(object).replaceAll("'", "").replaceAll(`"`, "");
|
|
@@ -290,12 +290,13 @@ var URLPath = class {
|
|
|
290
290
|
* @example
|
|
291
291
|
* new URLPath('/pet/{petId}').toTemplateString() // '`/pet/${petId}`'
|
|
292
292
|
*/
|
|
293
|
-
toTemplateString({ prefix
|
|
294
|
-
|
|
293
|
+
toTemplateString({ prefix, replacer } = {}) {
|
|
294
|
+
const result = this.path.split(/\{([^}]+)\}/).map((part, i) => {
|
|
295
295
|
if (i % 2 === 0) return part;
|
|
296
296
|
const param = this.#transformParam(part);
|
|
297
297
|
return `\${${replacer ? replacer(param) : param}}`;
|
|
298
|
-
}).join("")
|
|
298
|
+
}).join("");
|
|
299
|
+
return `\`${prefix ?? ""}${result}\``;
|
|
299
300
|
}
|
|
300
301
|
/**
|
|
301
302
|
* Extracts all `{param}` segments from the path and returns them as a key-value map.
|
|
@@ -304,17 +305,17 @@ var URLPath = class {
|
|
|
304
305
|
*
|
|
305
306
|
* @example
|
|
306
307
|
* ```ts
|
|
307
|
-
* new URLPath('/pet/{petId}/tag/{tagId}').
|
|
308
|
+
* new URLPath('/pet/{petId}/tag/{tagId}').toParamsObject()
|
|
308
309
|
* // { petId: 'petId', tagId: 'tagId' }
|
|
309
310
|
* ```
|
|
310
311
|
*/
|
|
311
|
-
|
|
312
|
+
toParamsObject(replacer) {
|
|
312
313
|
const params = {};
|
|
313
314
|
this.#eachParam((_raw, param) => {
|
|
314
315
|
const key = replacer ? replacer(param) : param;
|
|
315
316
|
params[key] = key;
|
|
316
317
|
});
|
|
317
|
-
return Object.keys(params).length > 0 ? params :
|
|
318
|
+
return Object.keys(params).length > 0 ? params : null;
|
|
318
319
|
}
|
|
319
320
|
/** Converts the OpenAPI path to Express-style colon syntax.
|
|
320
321
|
*
|
|
@@ -328,82 +329,144 @@ var URLPath = class {
|
|
|
328
329
|
}
|
|
329
330
|
};
|
|
330
331
|
//#endregion
|
|
331
|
-
//#region src/
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
const code = Number(res.statusCode);
|
|
338
|
-
if (code >= 200 && code < 300) return res.statusCode;
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
/**
|
|
342
|
-
* Render a group param value — compose individual schemas into `z.object({ ... })`,
|
|
343
|
-
* or use a schema name string directly.
|
|
344
|
-
*/
|
|
345
|
-
function zodGroupExpr(entry) {
|
|
346
|
-
if (typeof entry === "string") return entry;
|
|
347
|
-
return `z.object({ ${entry.map((p) => `${JSON.stringify(p.name)}: ${p.schemaName}`).join(", ")} })`;
|
|
332
|
+
//#region ../../internals/shared/src/operation.ts
|
|
333
|
+
function getOperationLink(node, link) {
|
|
334
|
+
if (!link) return null;
|
|
335
|
+
if (typeof link === "function") return link(node) ?? null;
|
|
336
|
+
if (link === "urlPath") return node.path ? `{@link ${new URLPath(node.path).URL}}` : null;
|
|
337
|
+
return node.path ? `{@link ${node.path.replaceAll("{", ":").replaceAll("}", "")}}` : null;
|
|
348
338
|
}
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
339
|
+
function buildOperationComments(node, options = {}) {
|
|
340
|
+
const { link = "pathTemplate", linkPosition = "afterDeprecated", splitLines = false } = options;
|
|
341
|
+
const linkComment = getOperationLink(node, link);
|
|
342
|
+
const filteredComments = (linkPosition === "beforeDeprecated" ? [
|
|
343
|
+
node.description && `@description ${node.description}`,
|
|
344
|
+
node.summary && `@summary ${node.summary}`,
|
|
345
|
+
linkComment,
|
|
346
|
+
node.deprecated && "@deprecated"
|
|
347
|
+
] : [
|
|
354
348
|
node.description && `@description ${node.description}`,
|
|
355
349
|
node.summary && `@summary ${node.summary}`,
|
|
356
350
|
node.deprecated && "@deprecated",
|
|
357
|
-
|
|
358
|
-
].filter((
|
|
351
|
+
linkComment
|
|
352
|
+
]).filter((comment) => Boolean(comment));
|
|
353
|
+
if (!splitLines) return filteredComments;
|
|
354
|
+
return filteredComments.flatMap((text) => text.split(/\r?\n/).map((line) => line.trim())).filter((comment) => Boolean(comment));
|
|
359
355
|
}
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
356
|
+
function getOperationParameters(node, options = {}) {
|
|
357
|
+
const params = _kubb_core.ast.caseParams(node.parameters, options.paramsCasing);
|
|
358
|
+
return {
|
|
359
|
+
path: params.filter((param) => param.in === "path"),
|
|
360
|
+
query: params.filter((param) => param.in === "query"),
|
|
361
|
+
header: params.filter((param) => param.in === "header"),
|
|
362
|
+
cookie: params.filter((param) => param.in === "cookie")
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
function getStatusCodeNumber(statusCode) {
|
|
366
|
+
const code = Number(statusCode);
|
|
367
|
+
return Number.isNaN(code) ? null : code;
|
|
368
|
+
}
|
|
369
|
+
function isSuccessStatusCode(statusCode) {
|
|
370
|
+
const code = getStatusCodeNumber(statusCode);
|
|
371
|
+
return code !== null && code >= 200 && code < 300;
|
|
372
|
+
}
|
|
373
|
+
function isErrorStatusCode(statusCode) {
|
|
374
|
+
const code = getStatusCodeNumber(statusCode);
|
|
375
|
+
return code !== null && code >= 400;
|
|
376
|
+
}
|
|
377
|
+
function resolveErrorNames(node, resolver) {
|
|
378
|
+
return node.responses.filter((response) => isErrorStatusCode(response.statusCode)).map((response) => resolver.resolveResponseStatusName(node, response.statusCode));
|
|
379
|
+
}
|
|
380
|
+
function resolveStatusCodeNames(node, resolver) {
|
|
381
|
+
return node.responses.map((response) => resolver.resolveResponseStatusName(node, response.statusCode));
|
|
382
|
+
}
|
|
383
|
+
const typeNamesByResolver = /* @__PURE__ */ new WeakMap();
|
|
384
|
+
function resolveOperationTypeNames(node, resolver, options = {}) {
|
|
385
|
+
const cacheKey = `${node.operationId}\0${options.paramsCasing ?? ""}\0${options.order ?? ""}\0${options.responseStatusNames ?? ""}\0${(options.exclude ?? []).join(",")}`;
|
|
386
|
+
let byResolver = typeNamesByResolver.get(resolver);
|
|
387
|
+
if (byResolver) {
|
|
388
|
+
const cached = byResolver.get(cacheKey);
|
|
389
|
+
if (cached) return cached;
|
|
390
|
+
} else {
|
|
391
|
+
byResolver = /* @__PURE__ */ new Map();
|
|
392
|
+
typeNamesByResolver.set(resolver, byResolver);
|
|
372
393
|
}
|
|
373
|
-
|
|
394
|
+
const { path, query, header } = getOperationParameters(node, { paramsCasing: options.paramsCasing });
|
|
395
|
+
const responseStatusNames = options.responseStatusNames === "error" ? resolveErrorNames(node, resolver) : options.responseStatusNames === false ? [] : resolveStatusCodeNames(node, resolver);
|
|
396
|
+
const exclude = new Set(options.exclude ?? []);
|
|
397
|
+
const paramNames = [
|
|
398
|
+
...path.map((param) => resolver.resolvePathParamsName(node, param)),
|
|
399
|
+
...query.map((param) => resolver.resolveQueryParamsName(node, param)),
|
|
400
|
+
...header.map((param) => resolver.resolveHeaderParamsName(node, param))
|
|
401
|
+
];
|
|
402
|
+
const bodyAndResponseNames = [node.requestBody?.content?.[0]?.schema ? resolver.resolveDataName(node) : null, resolver.resolveResponseName(node)];
|
|
403
|
+
const result = (options.order === "body-response-first" ? [
|
|
404
|
+
...bodyAndResponseNames,
|
|
405
|
+
...paramNames,
|
|
406
|
+
...responseStatusNames
|
|
407
|
+
] : [
|
|
408
|
+
...paramNames,
|
|
409
|
+
...bodyAndResponseNames,
|
|
410
|
+
...responseStatusNames
|
|
411
|
+
]).filter((name) => Boolean(name) && !exclude.has(name));
|
|
412
|
+
byResolver.set(cacheKey, result);
|
|
413
|
+
return result;
|
|
414
|
+
}
|
|
415
|
+
function findSuccessStatusCode(responses) {
|
|
416
|
+
for (const response of responses) if (isSuccessStatusCode(response.statusCode)) return response.statusCode;
|
|
417
|
+
return null;
|
|
374
418
|
}
|
|
419
|
+
//#endregion
|
|
420
|
+
//#region ../../internals/shared/src/group.ts
|
|
375
421
|
/**
|
|
376
|
-
*
|
|
377
|
-
*
|
|
422
|
+
* Builds the `group` config a Kubb plugin passes to `ctx.setOptions`, applying the
|
|
423
|
+
* shared default naming so every plugin groups output consistently:
|
|
424
|
+
*
|
|
425
|
+
* - `path` groups use the second path segment (`/pet/findByStatus` → `pet`).
|
|
426
|
+
* - other groups use `${camelCase(group)}${suffix}` (e.g. `petController`).
|
|
427
|
+
*
|
|
428
|
+
* Returns `null` when grouping is disabled, matching the per-plugin convention.
|
|
429
|
+
*
|
|
430
|
+
* @param group - The user-supplied group option, or `undefined` to disable grouping.
|
|
431
|
+
* @param options.suffix - Appended to non-`path` group names, e.g. `'Controller'` or `'Requests'`.
|
|
432
|
+
* @param options.honorName - When `true`, a user-provided `group.name` overrides the default namer.
|
|
433
|
+
*
|
|
434
|
+
* @example
|
|
435
|
+
* ```ts
|
|
436
|
+
* createGroupConfig(group, { suffix: 'Controller' }) // plugin-ts, plugin-zod
|
|
437
|
+
* createGroupConfig(group, { suffix: 'Controller', honorName: true }) // plugin-faker, plugin-client, …
|
|
438
|
+
* createGroupConfig(group, { suffix: 'Requests', honorName: true }) // plugin-cypress, plugin-mcp
|
|
439
|
+
* ```
|
|
378
440
|
*/
|
|
379
|
-
function
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
441
|
+
function createGroupConfig(group, options) {
|
|
442
|
+
if (!group) return null;
|
|
443
|
+
const defaultName = (ctx) => {
|
|
444
|
+
if (group.type === "path") return `${ctx.group.split("/")[1]}`;
|
|
445
|
+
return `${camelCase(ctx.group)}${options.suffix}`;
|
|
446
|
+
};
|
|
447
|
+
return {
|
|
448
|
+
...group,
|
|
449
|
+
name: options.honorName && group.name ? group.name : defaultName
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
//#endregion
|
|
453
|
+
//#region ../../internals/shared/src/params.ts
|
|
454
|
+
function buildParamsMapping(originalParams, mappedParams) {
|
|
455
|
+
const mapping = {};
|
|
456
|
+
let hasChanged = false;
|
|
457
|
+
originalParams.forEach((param, i) => {
|
|
458
|
+
const mappedName = mappedParams[i]?.name ?? param.name;
|
|
459
|
+
mapping[param.name] = mappedName;
|
|
460
|
+
if (param.name !== mappedName) hasChanged = true;
|
|
461
|
+
});
|
|
462
|
+
return hasChanged ? mapping : null;
|
|
463
|
+
}
|
|
464
|
+
function buildTransformedParamsMapping(params, transformName) {
|
|
465
|
+
if (!params.length) return null;
|
|
466
|
+
return buildParamsMapping(params, params.map((param) => ({
|
|
467
|
+
...param,
|
|
468
|
+
name: transformName(param.name)
|
|
469
|
+
})));
|
|
407
470
|
}
|
|
408
471
|
//#endregion
|
|
409
472
|
//#region src/components/McpHandler.tsx
|
|
@@ -415,16 +478,13 @@ function buildRemappingCode(mapping, varName, sourceName) {
|
|
|
415
478
|
}
|
|
416
479
|
const declarationPrinter = (0, _kubb_plugin_ts.functionPrinter)({ mode: "declaration" });
|
|
417
480
|
function McpHandler({ name, node, resolver, baseURL, dataReturnType, paramsCasing }) {
|
|
481
|
+
if (!_kubb_core.ast.isHttpOperationNode(node)) return null;
|
|
418
482
|
const urlPath = new URLPath(node.path);
|
|
419
483
|
const contentType = node.requestBody?.content?.[0]?.contentType;
|
|
420
484
|
const isFormData = contentType === "multipart/form-data";
|
|
421
|
-
const
|
|
422
|
-
const
|
|
423
|
-
const
|
|
424
|
-
const originalPathParams = node.parameters.filter((p) => p.in === "path");
|
|
425
|
-
const originalQueryParams = node.parameters.filter((p) => p.in === "query");
|
|
426
|
-
const originalHeaderParams = node.parameters.filter((p) => p.in === "header");
|
|
427
|
-
const requestName = node.requestBody?.content?.[0]?.schema ? resolver.resolveDataName(node) : void 0;
|
|
485
|
+
const { query: queryParams, header: headerParams } = getOperationParameters(node, { paramsCasing });
|
|
486
|
+
const { path: originalPathParams, query: originalQueryParams, header: originalHeaderParams } = getOperationParameters(node);
|
|
487
|
+
const requestName = node.requestBody?.content?.[0]?.schema ? resolver.resolveDataName(node) : null;
|
|
428
488
|
const responseName = resolver.resolveResponseName(node);
|
|
429
489
|
const errorResponses = node.responses.filter((r) => Number(r.statusCode) >= 400).map((r) => resolver.resolveResponseStatusName(node, r.statusCode));
|
|
430
490
|
const generics = [
|
|
@@ -440,11 +500,11 @@ function McpHandler({ name, node, resolver, baseURL, dataReturnType, paramsCasin
|
|
|
440
500
|
});
|
|
441
501
|
const baseParamsSignature = declarationPrinter.print(paramsNode) ?? "";
|
|
442
502
|
const paramsSignature = baseParamsSignature ? `${baseParamsSignature}, request: RequestHandlerExtra<ServerRequest, ServerNotification>` : "request: RequestHandlerExtra<ServerRequest, ServerNotification>";
|
|
443
|
-
const pathParamsMapping = paramsCasing ?
|
|
444
|
-
const queryParamsMapping = paramsCasing ?
|
|
445
|
-
const headerParamsMapping = paramsCasing ?
|
|
446
|
-
const contentTypeHeader = contentType && contentType !== "application/json" && contentType !== "multipart/form-data" ? `'Content-Type': '${contentType}'` :
|
|
447
|
-
const headers = [headerParams.length ? headerParamsMapping ? "...mappedHeaders" : "...headers" :
|
|
503
|
+
const pathParamsMapping = paramsCasing ? buildTransformedParamsMapping(originalPathParams, camelCase) : null;
|
|
504
|
+
const queryParamsMapping = paramsCasing ? buildTransformedParamsMapping(originalQueryParams, camelCase) : null;
|
|
505
|
+
const headerParamsMapping = paramsCasing ? buildTransformedParamsMapping(originalHeaderParams, camelCase) : null;
|
|
506
|
+
const contentTypeHeader = contentType && contentType !== "application/json" && contentType !== "multipart/form-data" ? `'Content-Type': '${contentType}'` : null;
|
|
507
|
+
const headers = [headerParams.length ? headerParamsMapping ? "...mappedHeaders" : "...headers" : null, contentTypeHeader].filter(Boolean);
|
|
448
508
|
const fetchConfig = [];
|
|
449
509
|
fetchConfig.push(`method: ${JSON.stringify(node.method.toUpperCase())}`);
|
|
450
510
|
fetchConfig.push(`url: ${urlPath.template}`);
|
|
@@ -478,7 +538,7 @@ function McpHandler({ name, node, resolver, baseURL, dataReturnType, paramsCasin
|
|
|
478
538
|
async: true,
|
|
479
539
|
export: true,
|
|
480
540
|
params: paramsSignature,
|
|
481
|
-
JSDoc: { comments:
|
|
541
|
+
JSDoc: { comments: buildOperationComments(node) },
|
|
482
542
|
returnType: "Promise<CallToolResult>",
|
|
483
543
|
children: [
|
|
484
544
|
"",
|
|
@@ -500,7 +560,7 @@ function McpHandler({ name, node, resolver, baseURL, dataReturnType, paramsCasin
|
|
|
500
560
|
/* @__PURE__ */ (0, _kubb_renderer_jsx_jsx_runtime.jsx)("br", {}),
|
|
501
561
|
isFormData && requestName && "const formData = buildFormData(requestData)",
|
|
502
562
|
/* @__PURE__ */ (0, _kubb_renderer_jsx_jsx_runtime.jsx)("br", {}),
|
|
503
|
-
`const res = await
|
|
563
|
+
`const res = await client<${generics.join(", ")}>({ ${fetchConfig.join(", ")} }, request)`,
|
|
504
564
|
/* @__PURE__ */ (0, _kubb_renderer_jsx_jsx_runtime.jsx)("br", {}),
|
|
505
565
|
callToolResult
|
|
506
566
|
]
|
|
@@ -508,6 +568,39 @@ function McpHandler({ name, node, resolver, baseURL, dataReturnType, paramsCasin
|
|
|
508
568
|
});
|
|
509
569
|
}
|
|
510
570
|
//#endregion
|
|
571
|
+
//#region src/utils.ts
|
|
572
|
+
/**
|
|
573
|
+
* Render a group param value — compose individual schemas into `z.object({ ... })`,
|
|
574
|
+
* or use a schema name string directly.
|
|
575
|
+
*/
|
|
576
|
+
function zodGroupExpr(entry) {
|
|
577
|
+
if (typeof entry === "string") return entry;
|
|
578
|
+
return `z.object({ ${entry.map((p) => `${JSON.stringify(p.name)}: ${p.schemaName}`).join(", ")} })`;
|
|
579
|
+
}
|
|
580
|
+
/**
|
|
581
|
+
* Convert a SchemaNode type to an inline Zod expression string.
|
|
582
|
+
* Used as fallback when no named zod schema is available for a path parameter.
|
|
583
|
+
*/
|
|
584
|
+
function zodExprFromSchemaNode(schema) {
|
|
585
|
+
const baseExpr = (() => {
|
|
586
|
+
if (schema.type === "enum") {
|
|
587
|
+
const rawValues = schema.namedEnumValues?.length ? schema.namedEnumValues.map((v) => v.value) : (schema.enumValues ?? []).filter((v) => v !== null);
|
|
588
|
+
if (rawValues.length > 0 && rawValues.every((v) => typeof v === "string")) return `z.enum([${rawValues.map((v) => JSON.stringify(v)).join(", ")}])`;
|
|
589
|
+
if (rawValues.length > 0) {
|
|
590
|
+
const literals = rawValues.map((v) => `z.literal(${JSON.stringify(v)})`);
|
|
591
|
+
return literals.length === 1 ? literals[0] : `z.union([${literals.join(", ")}])`;
|
|
592
|
+
}
|
|
593
|
+
return "z.string()";
|
|
594
|
+
}
|
|
595
|
+
if (schema.type === "integer") return "z.coerce.number()";
|
|
596
|
+
if (schema.type === "number") return "z.number()";
|
|
597
|
+
if (schema.type === "boolean") return "z.boolean()";
|
|
598
|
+
if (schema.type === "array") return "z.array(z.unknown())";
|
|
599
|
+
return "z.string()";
|
|
600
|
+
})();
|
|
601
|
+
return schema.nullable ? `${baseExpr}.nullable()` : baseExpr;
|
|
602
|
+
}
|
|
603
|
+
//#endregion
|
|
511
604
|
//#region src/components/Server.tsx
|
|
512
605
|
const keysPrinter = (0, _kubb_plugin_ts.functionPrinter)({ mode: "keys" });
|
|
513
606
|
function Server({ name, serverName, serverVersion, paramsCasing, operations }) {
|
|
@@ -527,7 +620,7 @@ function Server({ name, serverName, serverVersion, paramsCasing, operations }) {
|
|
|
527
620
|
`
|
|
528
621
|
}),
|
|
529
622
|
operations.map(({ tool, mcp, zod, node }) => {
|
|
530
|
-
const pathParams =
|
|
623
|
+
const { path: pathParams } = getOperationParameters(node, { paramsCasing });
|
|
531
624
|
const pathEntries = [];
|
|
532
625
|
const otherEntries = [];
|
|
533
626
|
for (const p of pathParams) {
|
|
@@ -554,9 +647,9 @@ function Server({ name, serverName, serverVersion, paramsCasing, operations }) {
|
|
|
554
647
|
const paramsNode = entries.length ? _kubb_core.ast.createFunctionParameters({ params: [_kubb_core.ast.createParameterGroup({ properties: entries.map((e) => _kubb_core.ast.createFunctionParameter({
|
|
555
648
|
name: e.key,
|
|
556
649
|
optional: false
|
|
557
|
-
})) })] }) :
|
|
650
|
+
})) })] }) : null;
|
|
558
651
|
const destructured = paramsNode ? keysPrinter.print(paramsNode) ?? "" : "";
|
|
559
|
-
const inputSchema = entries.length ? `{ ${entries.map((e) => `${e.key}: ${e.value}`).join(", ")} }` :
|
|
652
|
+
const inputSchema = entries.length ? `{ ${entries.map((e) => `${e.key}: ${e.value}`).join(", ")} }` : null;
|
|
560
653
|
const outputSchema = zod.responseName;
|
|
561
654
|
const config = [
|
|
562
655
|
tool.title ? `title: ${JSON.stringify(tool.title)}` : null,
|
|
@@ -597,29 +690,28 @@ server.registerTool(${JSON.stringify(tool.name)}, {
|
|
|
597
690
|
}
|
|
598
691
|
//#endregion
|
|
599
692
|
//#region src/generators/mcpGenerator.tsx
|
|
693
|
+
/**
|
|
694
|
+
* Built-in operation generator for `@kubb/plugin-mcp`. Emits one MCP tool
|
|
695
|
+
* handler per OpenAPI operation, wiring the input Zod schema, the HTTP call,
|
|
696
|
+
* and the response shape into a single function that an MCP server can
|
|
697
|
+
* register as a callable tool.
|
|
698
|
+
*/
|
|
600
699
|
const mcpGenerator = (0, _kubb_core.defineGenerator)({
|
|
601
700
|
name: "mcp",
|
|
602
|
-
renderer: _kubb_renderer_jsx.
|
|
701
|
+
renderer: _kubb_renderer_jsx.jsxRendererSync,
|
|
603
702
|
operation(node, ctx) {
|
|
703
|
+
if (!_kubb_core.ast.isHttpOperationNode(node)) return null;
|
|
604
704
|
const { resolver, driver, root } = ctx;
|
|
605
705
|
const { output, client, paramsCasing, group } = ctx.options;
|
|
606
706
|
const pluginTs = driver.getPlugin(_kubb_plugin_ts.pluginTsName);
|
|
607
707
|
if (!pluginTs) return null;
|
|
608
708
|
const tsResolver = driver.getResolver(_kubb_plugin_ts.pluginTsName);
|
|
609
|
-
const
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
const importedTypeNames = [
|
|
614
|
-
...pathParams.map((p) => tsResolver.resolvePathParamsName(node, p)),
|
|
615
|
-
...queryParams.map((p) => tsResolver.resolveQueryParamsName(node, p)),
|
|
616
|
-
...headerParams.map((p) => tsResolver.resolveHeaderParamsName(node, p)),
|
|
617
|
-
node.requestBody?.content?.[0]?.schema ? tsResolver.resolveDataName(node) : void 0,
|
|
618
|
-
tsResolver.resolveResponseName(node),
|
|
619
|
-
...node.responses.filter((r) => Number(r.statusCode) >= 400).map((r) => tsResolver.resolveResponseStatusName(node, r.statusCode))
|
|
620
|
-
].filter(Boolean);
|
|
709
|
+
const importedTypeNames = resolveOperationTypeNames(node, tsResolver, {
|
|
710
|
+
paramsCasing,
|
|
711
|
+
responseStatusNames: "error"
|
|
712
|
+
});
|
|
621
713
|
const meta = {
|
|
622
|
-
name: resolver.
|
|
714
|
+
name: resolver.resolveHandlerName(node),
|
|
623
715
|
file: resolver.resolveFile({
|
|
624
716
|
name: node.operationId,
|
|
625
717
|
extname: ".ts",
|
|
@@ -628,7 +720,7 @@ const mcpGenerator = (0, _kubb_core.defineGenerator)({
|
|
|
628
720
|
}, {
|
|
629
721
|
root,
|
|
630
722
|
output,
|
|
631
|
-
group
|
|
723
|
+
group: group ?? void 0
|
|
632
724
|
}),
|
|
633
725
|
fileTs: tsResolver.resolveFile({
|
|
634
726
|
name: node.operationId,
|
|
@@ -638,7 +730,7 @@ const mcpGenerator = (0, _kubb_core.defineGenerator)({
|
|
|
638
730
|
}, {
|
|
639
731
|
root,
|
|
640
732
|
output: pluginTs.options?.output ?? output,
|
|
641
|
-
group: pluginTs.options?.group
|
|
733
|
+
group: pluginTs.options?.group ?? void 0
|
|
642
734
|
})
|
|
643
735
|
};
|
|
644
736
|
return /* @__PURE__ */ (0, _kubb_renderer_jsx_jsx_runtime.jsxs)(_kubb_renderer_jsx.File, {
|
|
@@ -682,7 +774,7 @@ const mcpGenerator = (0, _kubb_core.defineGenerator)({
|
|
|
682
774
|
isTypeOnly: true
|
|
683
775
|
}),
|
|
684
776
|
/* @__PURE__ */ (0, _kubb_renderer_jsx_jsx_runtime.jsx)(_kubb_renderer_jsx.File.Import, {
|
|
685
|
-
name: "
|
|
777
|
+
name: "client",
|
|
686
778
|
path: client.importPath
|
|
687
779
|
}),
|
|
688
780
|
client.dataReturnType === "full" && /* @__PURE__ */ (0, _kubb_renderer_jsx_jsx_runtime.jsx)(_kubb_renderer_jsx.File.Import, {
|
|
@@ -698,18 +790,18 @@ const mcpGenerator = (0, _kubb_core.defineGenerator)({
|
|
|
698
790
|
"ResponseErrorConfig"
|
|
699
791
|
],
|
|
700
792
|
root: meta.file.path,
|
|
701
|
-
path: node_path.default.resolve(root, ".kubb/
|
|
793
|
+
path: node_path.default.resolve(root, ".kubb/client.ts"),
|
|
702
794
|
isTypeOnly: true
|
|
703
795
|
}),
|
|
704
796
|
/* @__PURE__ */ (0, _kubb_renderer_jsx_jsx_runtime.jsx)(_kubb_renderer_jsx.File.Import, {
|
|
705
|
-
name: ["
|
|
797
|
+
name: ["client"],
|
|
706
798
|
root: meta.file.path,
|
|
707
|
-
path: node_path.default.resolve(root, ".kubb/
|
|
799
|
+
path: node_path.default.resolve(root, ".kubb/client.ts")
|
|
708
800
|
}),
|
|
709
801
|
client.dataReturnType === "full" && /* @__PURE__ */ (0, _kubb_renderer_jsx_jsx_runtime.jsx)(_kubb_renderer_jsx.File.Import, {
|
|
710
802
|
name: ["ResponseConfig"],
|
|
711
803
|
root: meta.file.path,
|
|
712
|
-
path: node_path.default.resolve(root, ".kubb/
|
|
804
|
+
path: node_path.default.resolve(root, ".kubb/client.ts"),
|
|
713
805
|
isTypeOnly: true
|
|
714
806
|
})
|
|
715
807
|
] }),
|
|
@@ -736,9 +828,9 @@ const mcpGenerator = (0, _kubb_core.defineGenerator)({
|
|
|
736
828
|
*/
|
|
737
829
|
const serverGenerator = (0, _kubb_core.defineGenerator)({
|
|
738
830
|
name: "operations",
|
|
739
|
-
renderer: _kubb_renderer_jsx.
|
|
831
|
+
renderer: _kubb_renderer_jsx.jsxRendererSync,
|
|
740
832
|
operations(nodes, ctx) {
|
|
741
|
-
const {
|
|
833
|
+
const { config, resolver, plugin, driver, root } = ctx;
|
|
742
834
|
const { output, paramsCasing, group } = ctx.options;
|
|
743
835
|
const pluginZod = driver.getPlugin(_kubb_plugin_zod.pluginZodName);
|
|
744
836
|
if (!pluginZod) return;
|
|
@@ -754,11 +846,8 @@ const serverGenerator = (0, _kubb_core.defineGenerator)({
|
|
|
754
846
|
path: node_path.default.resolve(root, output.path, ".mcp.json"),
|
|
755
847
|
meta: { pluginName: plugin.name }
|
|
756
848
|
};
|
|
757
|
-
const operationsMapped = nodes.map((node) => {
|
|
758
|
-
const
|
|
759
|
-
const pathParams = casedParams.filter((p) => p.in === "path");
|
|
760
|
-
const queryParams = casedParams.filter((p) => p.in === "query");
|
|
761
|
-
const headerParams = casedParams.filter((p) => p.in === "header");
|
|
849
|
+
const operationsMapped = nodes.filter(_kubb_core.ast.isHttpOperationNode).map((node) => {
|
|
850
|
+
const { path: pathParams, query: queryParams, header: headerParams } = getOperationParameters(node, { paramsCasing });
|
|
762
851
|
const mcpFile = resolver.resolveFile({
|
|
763
852
|
name: node.operationId,
|
|
764
853
|
extname: ".ts",
|
|
@@ -767,7 +856,7 @@ const serverGenerator = (0, _kubb_core.defineGenerator)({
|
|
|
767
856
|
}, {
|
|
768
857
|
root,
|
|
769
858
|
output,
|
|
770
|
-
group
|
|
859
|
+
group: group ?? void 0
|
|
771
860
|
});
|
|
772
861
|
const zodFile = zodResolver.resolveFile({
|
|
773
862
|
name: node.operationId,
|
|
@@ -777,11 +866,11 @@ const serverGenerator = (0, _kubb_core.defineGenerator)({
|
|
|
777
866
|
}, {
|
|
778
867
|
root,
|
|
779
868
|
output: pluginZod.options?.output ?? output,
|
|
780
|
-
group: pluginZod.options?.group
|
|
869
|
+
group: pluginZod.options?.group ?? void 0
|
|
781
870
|
});
|
|
782
|
-
const requestName = node.requestBody?.content?.[0]?.schema ? zodResolver.resolveDataName(node) :
|
|
871
|
+
const requestName = node.requestBody?.content?.[0]?.schema ? zodResolver.resolveDataName(node) : null;
|
|
783
872
|
const successStatus = findSuccessStatusCode(node.responses);
|
|
784
|
-
const responseName = successStatus ? zodResolver.resolveResponseStatusName(node, successStatus) :
|
|
873
|
+
const responseName = successStatus ? zodResolver.resolveResponseStatusName(node, successStatus) : null;
|
|
785
874
|
const resolveParams = (params) => params.map((p) => ({
|
|
786
875
|
name: p.name,
|
|
787
876
|
schemaName: zodResolver.resolveParamName(node, p)
|
|
@@ -793,13 +882,13 @@ const serverGenerator = (0, _kubb_core.defineGenerator)({
|
|
|
793
882
|
description: node.description || `Make a ${node.method.toUpperCase()} request to ${node.path}`
|
|
794
883
|
},
|
|
795
884
|
mcp: {
|
|
796
|
-
name: resolver.
|
|
885
|
+
name: resolver.resolveHandlerName(node),
|
|
797
886
|
file: mcpFile
|
|
798
887
|
},
|
|
799
888
|
zod: {
|
|
800
889
|
pathParams: resolveParams(pathParams),
|
|
801
|
-
queryParams: queryParams.length ? resolveParams(queryParams) :
|
|
802
|
-
headerParams: headerParams.length ? resolveParams(headerParams) :
|
|
890
|
+
queryParams: queryParams.length ? resolveParams(queryParams) : null,
|
|
891
|
+
headerParams: headerParams.length ? resolveParams(headerParams) : null,
|
|
803
892
|
requestName,
|
|
804
893
|
responseName,
|
|
805
894
|
file: zodFile
|
|
@@ -814,7 +903,7 @@ const serverGenerator = (0, _kubb_core.defineGenerator)({
|
|
|
814
903
|
...(zod.headerParams ?? []).map((p) => p.schemaName),
|
|
815
904
|
zod.requestName,
|
|
816
905
|
zod.responseName
|
|
817
|
-
].filter(Boolean);
|
|
906
|
+
].filter((name) => Boolean(name));
|
|
818
907
|
const uniqueNames = [...new Set(zodNames)].sort();
|
|
819
908
|
return [/* @__PURE__ */ (0, _kubb_renderer_jsx_jsx_runtime.jsx)(_kubb_renderer_jsx.File.Import, {
|
|
820
909
|
name: [mcp.name],
|
|
@@ -830,13 +919,21 @@ const serverGenerator = (0, _kubb_core.defineGenerator)({
|
|
|
830
919
|
baseName: serverFile.baseName,
|
|
831
920
|
path: serverFile.path,
|
|
832
921
|
meta: serverFile.meta,
|
|
833
|
-
banner: resolver.resolveBanner(
|
|
922
|
+
banner: resolver.resolveBanner(ctx.meta, {
|
|
834
923
|
output,
|
|
835
|
-
config
|
|
924
|
+
config,
|
|
925
|
+
file: {
|
|
926
|
+
path: serverFile.path,
|
|
927
|
+
baseName: serverFile.baseName
|
|
928
|
+
}
|
|
836
929
|
}),
|
|
837
|
-
footer: resolver.resolveFooter(
|
|
930
|
+
footer: resolver.resolveFooter(ctx.meta, {
|
|
838
931
|
output,
|
|
839
|
-
config
|
|
932
|
+
config,
|
|
933
|
+
file: {
|
|
934
|
+
path: serverFile.path,
|
|
935
|
+
baseName: serverFile.baseName
|
|
936
|
+
}
|
|
840
937
|
}),
|
|
841
938
|
children: [
|
|
842
939
|
/* @__PURE__ */ (0, _kubb_renderer_jsx_jsx_runtime.jsx)(_kubb_renderer_jsx.File.Import, {
|
|
@@ -854,8 +951,8 @@ const serverGenerator = (0, _kubb_core.defineGenerator)({
|
|
|
854
951
|
imports,
|
|
855
952
|
/* @__PURE__ */ (0, _kubb_renderer_jsx_jsx_runtime.jsx)(Server, {
|
|
856
953
|
name,
|
|
857
|
-
serverName:
|
|
858
|
-
serverVersion:
|
|
954
|
+
serverName: ctx.meta.title ?? "server",
|
|
955
|
+
serverVersion: ctx.meta.version ?? "0.0.0",
|
|
859
956
|
paramsCasing,
|
|
860
957
|
operations: operationsMapped
|
|
861
958
|
})
|
|
@@ -869,7 +966,7 @@ const serverGenerator = (0, _kubb_core.defineGenerator)({
|
|
|
869
966
|
children: `
|
|
870
967
|
{
|
|
871
968
|
"mcpServers": {
|
|
872
|
-
"${
|
|
969
|
+
"${ctx.meta.title || "server"}": {
|
|
873
970
|
"type": "stdio",
|
|
874
971
|
"command": "npx",
|
|
875
972
|
"args": ["tsx", "${node_path.default.relative(node_path.default.dirname(jsonFile.path), serverFile.path)}"]
|
|
@@ -884,14 +981,18 @@ const serverGenerator = (0, _kubb_core.defineGenerator)({
|
|
|
884
981
|
//#endregion
|
|
885
982
|
//#region src/resolvers/resolverMcp.ts
|
|
886
983
|
/**
|
|
887
|
-
*
|
|
984
|
+
* Default resolver used by `@kubb/plugin-mcp`. Decides the names and file
|
|
985
|
+
* paths for every generated MCP tool handler. Function names get a `Handler`
|
|
986
|
+
* suffix so an operation `addPet` becomes `addPetHandler`.
|
|
888
987
|
*
|
|
889
|
-
*
|
|
988
|
+
* @example Resolve a handler name
|
|
989
|
+
* ```ts
|
|
990
|
+
* import { resolverMcp } from '@kubb/plugin-mcp'
|
|
890
991
|
*
|
|
891
|
-
*
|
|
892
|
-
*
|
|
992
|
+
* resolverMcp.default('addPet', 'function') // 'addPetHandler'
|
|
993
|
+
* ```
|
|
893
994
|
*/
|
|
894
|
-
const resolverMcp = (0, _kubb_core.defineResolver)((
|
|
995
|
+
const resolverMcp = (0, _kubb_core.defineResolver)(() => ({
|
|
895
996
|
name: "default",
|
|
896
997
|
pluginName: "plugin-mcp",
|
|
897
998
|
default(name, type) {
|
|
@@ -899,12 +1000,50 @@ const resolverMcp = (0, _kubb_core.defineResolver)((ctx) => ({
|
|
|
899
1000
|
return camelCase(name, { suffix: "handler" });
|
|
900
1001
|
},
|
|
901
1002
|
resolveName(name) {
|
|
902
|
-
return
|
|
1003
|
+
return this.default(name, "function");
|
|
1004
|
+
},
|
|
1005
|
+
resolvePathName(name, type) {
|
|
1006
|
+
return this.default(name, type);
|
|
1007
|
+
},
|
|
1008
|
+
resolveHandlerName(node) {
|
|
1009
|
+
return this.resolveName(node.operationId);
|
|
903
1010
|
}
|
|
904
1011
|
}));
|
|
905
1012
|
//#endregion
|
|
906
1013
|
//#region src/plugin.ts
|
|
1014
|
+
/**
|
|
1015
|
+
* Canonical plugin name for `@kubb/plugin-mcp`. Used for driver lookups and
|
|
1016
|
+
* cross-plugin dependency references.
|
|
1017
|
+
*/
|
|
907
1018
|
const pluginMcpName = "plugin-mcp";
|
|
1019
|
+
/**
|
|
1020
|
+
* Generates a Model Context Protocol (MCP) server from an OpenAPI spec. Every
|
|
1021
|
+
* operation becomes a typed MCP tool that AI assistants (Claude Desktop, Claude
|
|
1022
|
+
* Code, MCP-compatible clients) can call directly.
|
|
1023
|
+
*
|
|
1024
|
+
* @example
|
|
1025
|
+
* ```ts
|
|
1026
|
+
* import { defineConfig } from 'kubb'
|
|
1027
|
+
* import { pluginTs } from '@kubb/plugin-ts'
|
|
1028
|
+
* import { pluginClient } from '@kubb/plugin-client'
|
|
1029
|
+
* import { pluginZod } from '@kubb/plugin-zod'
|
|
1030
|
+
* import { pluginMcp } from '@kubb/plugin-mcp'
|
|
1031
|
+
*
|
|
1032
|
+
* export default defineConfig({
|
|
1033
|
+
* input: { path: './petStore.yaml' },
|
|
1034
|
+
* output: { path: './src/gen' },
|
|
1035
|
+
* plugins: [
|
|
1036
|
+
* pluginTs(),
|
|
1037
|
+
* pluginClient(),
|
|
1038
|
+
* pluginZod(),
|
|
1039
|
+
* pluginMcp({
|
|
1040
|
+
* output: { path: './mcp' },
|
|
1041
|
+
* client: { baseURL: 'https://petstore.swagger.io/v2' },
|
|
1042
|
+
* }),
|
|
1043
|
+
* ],
|
|
1044
|
+
* })
|
|
1045
|
+
* ```
|
|
1046
|
+
*/
|
|
908
1047
|
const pluginMcp = (0, _kubb_core.definePlugin)((options) => {
|
|
909
1048
|
const { output = {
|
|
910
1049
|
path: "mcp",
|
|
@@ -912,13 +1051,10 @@ const pluginMcp = (0, _kubb_core.definePlugin)((options) => {
|
|
|
912
1051
|
}, group, exclude = [], include, override = [], paramsCasing, client, resolver: userResolver, transformer: userTransformer, generators: userGenerators = [] } = options;
|
|
913
1052
|
const clientName = client?.client ?? "axios";
|
|
914
1053
|
const clientImportPath = client?.importPath ?? (!client?.bundle ? `@kubb/plugin-client/clients/${clientName}` : void 0);
|
|
915
|
-
const groupConfig = group
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
return `${camelCase(ctx.group)}Requests`;
|
|
920
|
-
}
|
|
921
|
-
} : void 0;
|
|
1054
|
+
const groupConfig = createGroupConfig(group, {
|
|
1055
|
+
suffix: "Requests",
|
|
1056
|
+
honorName: true
|
|
1057
|
+
});
|
|
922
1058
|
return {
|
|
923
1059
|
name: pluginMcpName,
|
|
924
1060
|
options,
|
|
@@ -954,10 +1090,10 @@ const pluginMcp = (0, _kubb_core.definePlugin)((options) => {
|
|
|
954
1090
|
const root = node_path$1.default.resolve(ctx.config.root, ctx.config.output.path);
|
|
955
1091
|
const hasClientPlugin = ctx.config.plugins?.some((p) => p.name === _kubb_plugin_client.pluginClientName);
|
|
956
1092
|
if (client?.bundle && !hasClientPlugin && !clientImportPath) ctx.injectFile({
|
|
957
|
-
baseName: "
|
|
958
|
-
path: node_path$1.default.resolve(root, ".kubb/
|
|
1093
|
+
baseName: "client.ts",
|
|
1094
|
+
path: node_path$1.default.resolve(root, ".kubb/client.ts"),
|
|
959
1095
|
sources: [_kubb_core.ast.createSource({
|
|
960
|
-
name: "
|
|
1096
|
+
name: "client",
|
|
961
1097
|
nodes: [_kubb_core.ast.createText(clientName === "fetch" ? _kubb_plugin_client_templates_clients_fetch_source.source : _kubb_plugin_client_templates_clients_axios_source.source)],
|
|
962
1098
|
isExportable: true,
|
|
963
1099
|
isIndexable: true
|