@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.js
CHANGED
|
@@ -2,7 +2,7 @@ import "./chunk--u3MIqq1.js";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { ast, defineGenerator, definePlugin, defineResolver } from "@kubb/core";
|
|
4
4
|
import { functionPrinter, pluginTsName } from "@kubb/plugin-ts";
|
|
5
|
-
import { Const, File, Function,
|
|
5
|
+
import { Const, File, Function, jsxRendererSync } from "@kubb/renderer-jsx";
|
|
6
6
|
import { Fragment, jsx, jsxs } from "@kubb/renderer-jsx/jsx-runtime";
|
|
7
7
|
import { pluginZodName } from "@kubb/plugin-zod";
|
|
8
8
|
import { pluginClientName } from "@kubb/plugin-client";
|
|
@@ -220,16 +220,16 @@ var URLPath = class {
|
|
|
220
220
|
get object() {
|
|
221
221
|
return this.toObject();
|
|
222
222
|
}
|
|
223
|
-
/** Returns a map of path parameter names, or `
|
|
223
|
+
/** Returns a map of path parameter names, or `null` when the path has no parameters.
|
|
224
224
|
*
|
|
225
225
|
* @example
|
|
226
226
|
* ```ts
|
|
227
227
|
* new URLPath('/pet/{petId}').params // { petId: 'petId' }
|
|
228
|
-
* new URLPath('/pet').params //
|
|
228
|
+
* new URLPath('/pet').params // null
|
|
229
229
|
* ```
|
|
230
230
|
*/
|
|
231
231
|
get params() {
|
|
232
|
-
return this.
|
|
232
|
+
return this.toParamsObject();
|
|
233
233
|
}
|
|
234
234
|
#transformParam(raw) {
|
|
235
235
|
const param = isValidVarName(raw) ? raw : camelCase(raw);
|
|
@@ -247,7 +247,7 @@ var URLPath = class {
|
|
|
247
247
|
toObject({ type = "path", replacer, stringify } = {}) {
|
|
248
248
|
const object = {
|
|
249
249
|
url: type === "path" ? this.toURLPath() : this.toTemplateString({ replacer }),
|
|
250
|
-
params: this.
|
|
250
|
+
params: this.toParamsObject()
|
|
251
251
|
};
|
|
252
252
|
if (stringify) {
|
|
253
253
|
if (type === "template") return JSON.stringify(object).replaceAll("'", "").replaceAll(`"`, "");
|
|
@@ -263,12 +263,13 @@ var URLPath = class {
|
|
|
263
263
|
* @example
|
|
264
264
|
* new URLPath('/pet/{petId}').toTemplateString() // '`/pet/${petId}`'
|
|
265
265
|
*/
|
|
266
|
-
toTemplateString({ prefix
|
|
267
|
-
|
|
266
|
+
toTemplateString({ prefix, replacer } = {}) {
|
|
267
|
+
const result = this.path.split(/\{([^}]+)\}/).map((part, i) => {
|
|
268
268
|
if (i % 2 === 0) return part;
|
|
269
269
|
const param = this.#transformParam(part);
|
|
270
270
|
return `\${${replacer ? replacer(param) : param}}`;
|
|
271
|
-
}).join("")
|
|
271
|
+
}).join("");
|
|
272
|
+
return `\`${prefix ?? ""}${result}\``;
|
|
272
273
|
}
|
|
273
274
|
/**
|
|
274
275
|
* Extracts all `{param}` segments from the path and returns them as a key-value map.
|
|
@@ -277,17 +278,17 @@ var URLPath = class {
|
|
|
277
278
|
*
|
|
278
279
|
* @example
|
|
279
280
|
* ```ts
|
|
280
|
-
* new URLPath('/pet/{petId}/tag/{tagId}').
|
|
281
|
+
* new URLPath('/pet/{petId}/tag/{tagId}').toParamsObject()
|
|
281
282
|
* // { petId: 'petId', tagId: 'tagId' }
|
|
282
283
|
* ```
|
|
283
284
|
*/
|
|
284
|
-
|
|
285
|
+
toParamsObject(replacer) {
|
|
285
286
|
const params = {};
|
|
286
287
|
this.#eachParam((_raw, param) => {
|
|
287
288
|
const key = replacer ? replacer(param) : param;
|
|
288
289
|
params[key] = key;
|
|
289
290
|
});
|
|
290
|
-
return Object.keys(params).length > 0 ? params :
|
|
291
|
+
return Object.keys(params).length > 0 ? params : null;
|
|
291
292
|
}
|
|
292
293
|
/** Converts the OpenAPI path to Express-style colon syntax.
|
|
293
294
|
*
|
|
@@ -301,82 +302,144 @@ var URLPath = class {
|
|
|
301
302
|
}
|
|
302
303
|
};
|
|
303
304
|
//#endregion
|
|
304
|
-
//#region src/
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
const code = Number(res.statusCode);
|
|
311
|
-
if (code >= 200 && code < 300) return res.statusCode;
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
/**
|
|
315
|
-
* Render a group param value — compose individual schemas into `z.object({ ... })`,
|
|
316
|
-
* or use a schema name string directly.
|
|
317
|
-
*/
|
|
318
|
-
function zodGroupExpr(entry) {
|
|
319
|
-
if (typeof entry === "string") return entry;
|
|
320
|
-
return `z.object({ ${entry.map((p) => `${JSON.stringify(p.name)}: ${p.schemaName}`).join(", ")} })`;
|
|
305
|
+
//#region ../../internals/shared/src/operation.ts
|
|
306
|
+
function getOperationLink(node, link) {
|
|
307
|
+
if (!link) return null;
|
|
308
|
+
if (typeof link === "function") return link(node) ?? null;
|
|
309
|
+
if (link === "urlPath") return node.path ? `{@link ${new URLPath(node.path).URL}}` : null;
|
|
310
|
+
return node.path ? `{@link ${node.path.replaceAll("{", ":").replaceAll("}", "")}}` : null;
|
|
321
311
|
}
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
312
|
+
function buildOperationComments(node, options = {}) {
|
|
313
|
+
const { link = "pathTemplate", linkPosition = "afterDeprecated", splitLines = false } = options;
|
|
314
|
+
const linkComment = getOperationLink(node, link);
|
|
315
|
+
const filteredComments = (linkPosition === "beforeDeprecated" ? [
|
|
316
|
+
node.description && `@description ${node.description}`,
|
|
317
|
+
node.summary && `@summary ${node.summary}`,
|
|
318
|
+
linkComment,
|
|
319
|
+
node.deprecated && "@deprecated"
|
|
320
|
+
] : [
|
|
327
321
|
node.description && `@description ${node.description}`,
|
|
328
322
|
node.summary && `@summary ${node.summary}`,
|
|
329
323
|
node.deprecated && "@deprecated",
|
|
330
|
-
|
|
331
|
-
].filter((
|
|
324
|
+
linkComment
|
|
325
|
+
]).filter((comment) => Boolean(comment));
|
|
326
|
+
if (!splitLines) return filteredComments;
|
|
327
|
+
return filteredComments.flatMap((text) => text.split(/\r?\n/).map((line) => line.trim())).filter((comment) => Boolean(comment));
|
|
332
328
|
}
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
329
|
+
function getOperationParameters(node, options = {}) {
|
|
330
|
+
const params = ast.caseParams(node.parameters, options.paramsCasing);
|
|
331
|
+
return {
|
|
332
|
+
path: params.filter((param) => param.in === "path"),
|
|
333
|
+
query: params.filter((param) => param.in === "query"),
|
|
334
|
+
header: params.filter((param) => param.in === "header"),
|
|
335
|
+
cookie: params.filter((param) => param.in === "cookie")
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
function getStatusCodeNumber(statusCode) {
|
|
339
|
+
const code = Number(statusCode);
|
|
340
|
+
return Number.isNaN(code) ? null : code;
|
|
341
|
+
}
|
|
342
|
+
function isSuccessStatusCode(statusCode) {
|
|
343
|
+
const code = getStatusCodeNumber(statusCode);
|
|
344
|
+
return code !== null && code >= 200 && code < 300;
|
|
345
|
+
}
|
|
346
|
+
function isErrorStatusCode(statusCode) {
|
|
347
|
+
const code = getStatusCodeNumber(statusCode);
|
|
348
|
+
return code !== null && code >= 400;
|
|
349
|
+
}
|
|
350
|
+
function resolveErrorNames(node, resolver) {
|
|
351
|
+
return node.responses.filter((response) => isErrorStatusCode(response.statusCode)).map((response) => resolver.resolveResponseStatusName(node, response.statusCode));
|
|
352
|
+
}
|
|
353
|
+
function resolveStatusCodeNames(node, resolver) {
|
|
354
|
+
return node.responses.map((response) => resolver.resolveResponseStatusName(node, response.statusCode));
|
|
355
|
+
}
|
|
356
|
+
const typeNamesByResolver = /* @__PURE__ */ new WeakMap();
|
|
357
|
+
function resolveOperationTypeNames(node, resolver, options = {}) {
|
|
358
|
+
const cacheKey = `${node.operationId}\0${options.paramsCasing ?? ""}\0${options.order ?? ""}\0${options.responseStatusNames ?? ""}\0${(options.exclude ?? []).join(",")}`;
|
|
359
|
+
let byResolver = typeNamesByResolver.get(resolver);
|
|
360
|
+
if (byResolver) {
|
|
361
|
+
const cached = byResolver.get(cacheKey);
|
|
362
|
+
if (cached) return cached;
|
|
363
|
+
} else {
|
|
364
|
+
byResolver = /* @__PURE__ */ new Map();
|
|
365
|
+
typeNamesByResolver.set(resolver, byResolver);
|
|
345
366
|
}
|
|
346
|
-
|
|
367
|
+
const { path, query, header } = getOperationParameters(node, { paramsCasing: options.paramsCasing });
|
|
368
|
+
const responseStatusNames = options.responseStatusNames === "error" ? resolveErrorNames(node, resolver) : options.responseStatusNames === false ? [] : resolveStatusCodeNames(node, resolver);
|
|
369
|
+
const exclude = new Set(options.exclude ?? []);
|
|
370
|
+
const paramNames = [
|
|
371
|
+
...path.map((param) => resolver.resolvePathParamsName(node, param)),
|
|
372
|
+
...query.map((param) => resolver.resolveQueryParamsName(node, param)),
|
|
373
|
+
...header.map((param) => resolver.resolveHeaderParamsName(node, param))
|
|
374
|
+
];
|
|
375
|
+
const bodyAndResponseNames = [node.requestBody?.content?.[0]?.schema ? resolver.resolveDataName(node) : null, resolver.resolveResponseName(node)];
|
|
376
|
+
const result = (options.order === "body-response-first" ? [
|
|
377
|
+
...bodyAndResponseNames,
|
|
378
|
+
...paramNames,
|
|
379
|
+
...responseStatusNames
|
|
380
|
+
] : [
|
|
381
|
+
...paramNames,
|
|
382
|
+
...bodyAndResponseNames,
|
|
383
|
+
...responseStatusNames
|
|
384
|
+
]).filter((name) => Boolean(name) && !exclude.has(name));
|
|
385
|
+
byResolver.set(cacheKey, result);
|
|
386
|
+
return result;
|
|
387
|
+
}
|
|
388
|
+
function findSuccessStatusCode(responses) {
|
|
389
|
+
for (const response of responses) if (isSuccessStatusCode(response.statusCode)) return response.statusCode;
|
|
390
|
+
return null;
|
|
347
391
|
}
|
|
392
|
+
//#endregion
|
|
393
|
+
//#region ../../internals/shared/src/group.ts
|
|
348
394
|
/**
|
|
349
|
-
*
|
|
350
|
-
*
|
|
395
|
+
* Builds the `group` config a Kubb plugin passes to `ctx.setOptions`, applying the
|
|
396
|
+
* shared default naming so every plugin groups output consistently:
|
|
397
|
+
*
|
|
398
|
+
* - `path` groups use the second path segment (`/pet/findByStatus` → `pet`).
|
|
399
|
+
* - other groups use `${camelCase(group)}${suffix}` (e.g. `petController`).
|
|
400
|
+
*
|
|
401
|
+
* Returns `null` when grouping is disabled, matching the per-plugin convention.
|
|
402
|
+
*
|
|
403
|
+
* @param group - The user-supplied group option, or `undefined` to disable grouping.
|
|
404
|
+
* @param options.suffix - Appended to non-`path` group names, e.g. `'Controller'` or `'Requests'`.
|
|
405
|
+
* @param options.honorName - When `true`, a user-provided `group.name` overrides the default namer.
|
|
406
|
+
*
|
|
407
|
+
* @example
|
|
408
|
+
* ```ts
|
|
409
|
+
* createGroupConfig(group, { suffix: 'Controller' }) // plugin-ts, plugin-zod
|
|
410
|
+
* createGroupConfig(group, { suffix: 'Controller', honorName: true }) // plugin-faker, plugin-client, …
|
|
411
|
+
* createGroupConfig(group, { suffix: 'Requests', honorName: true }) // plugin-cypress, plugin-mcp
|
|
412
|
+
* ```
|
|
351
413
|
*/
|
|
352
|
-
function
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
414
|
+
function createGroupConfig(group, options) {
|
|
415
|
+
if (!group) return null;
|
|
416
|
+
const defaultName = (ctx) => {
|
|
417
|
+
if (group.type === "path") return `${ctx.group.split("/")[1]}`;
|
|
418
|
+
return `${camelCase(ctx.group)}${options.suffix}`;
|
|
419
|
+
};
|
|
420
|
+
return {
|
|
421
|
+
...group,
|
|
422
|
+
name: options.honorName && group.name ? group.name : defaultName
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
//#endregion
|
|
426
|
+
//#region ../../internals/shared/src/params.ts
|
|
427
|
+
function buildParamsMapping(originalParams, mappedParams) {
|
|
428
|
+
const mapping = {};
|
|
429
|
+
let hasChanged = false;
|
|
430
|
+
originalParams.forEach((param, i) => {
|
|
431
|
+
const mappedName = mappedParams[i]?.name ?? param.name;
|
|
432
|
+
mapping[param.name] = mappedName;
|
|
433
|
+
if (param.name !== mappedName) hasChanged = true;
|
|
434
|
+
});
|
|
435
|
+
return hasChanged ? mapping : null;
|
|
436
|
+
}
|
|
437
|
+
function buildTransformedParamsMapping(params, transformName) {
|
|
438
|
+
if (!params.length) return null;
|
|
439
|
+
return buildParamsMapping(params, params.map((param) => ({
|
|
440
|
+
...param,
|
|
441
|
+
name: transformName(param.name)
|
|
442
|
+
})));
|
|
380
443
|
}
|
|
381
444
|
//#endregion
|
|
382
445
|
//#region src/components/McpHandler.tsx
|
|
@@ -388,16 +451,13 @@ function buildRemappingCode(mapping, varName, sourceName) {
|
|
|
388
451
|
}
|
|
389
452
|
const declarationPrinter = functionPrinter({ mode: "declaration" });
|
|
390
453
|
function McpHandler({ name, node, resolver, baseURL, dataReturnType, paramsCasing }) {
|
|
454
|
+
if (!ast.isHttpOperationNode(node)) return null;
|
|
391
455
|
const urlPath = new URLPath(node.path);
|
|
392
456
|
const contentType = node.requestBody?.content?.[0]?.contentType;
|
|
393
457
|
const isFormData = contentType === "multipart/form-data";
|
|
394
|
-
const
|
|
395
|
-
const
|
|
396
|
-
const
|
|
397
|
-
const originalPathParams = node.parameters.filter((p) => p.in === "path");
|
|
398
|
-
const originalQueryParams = node.parameters.filter((p) => p.in === "query");
|
|
399
|
-
const originalHeaderParams = node.parameters.filter((p) => p.in === "header");
|
|
400
|
-
const requestName = node.requestBody?.content?.[0]?.schema ? resolver.resolveDataName(node) : void 0;
|
|
458
|
+
const { query: queryParams, header: headerParams } = getOperationParameters(node, { paramsCasing });
|
|
459
|
+
const { path: originalPathParams, query: originalQueryParams, header: originalHeaderParams } = getOperationParameters(node);
|
|
460
|
+
const requestName = node.requestBody?.content?.[0]?.schema ? resolver.resolveDataName(node) : null;
|
|
401
461
|
const responseName = resolver.resolveResponseName(node);
|
|
402
462
|
const errorResponses = node.responses.filter((r) => Number(r.statusCode) >= 400).map((r) => resolver.resolveResponseStatusName(node, r.statusCode));
|
|
403
463
|
const generics = [
|
|
@@ -413,11 +473,11 @@ function McpHandler({ name, node, resolver, baseURL, dataReturnType, paramsCasin
|
|
|
413
473
|
});
|
|
414
474
|
const baseParamsSignature = declarationPrinter.print(paramsNode) ?? "";
|
|
415
475
|
const paramsSignature = baseParamsSignature ? `${baseParamsSignature}, request: RequestHandlerExtra<ServerRequest, ServerNotification>` : "request: RequestHandlerExtra<ServerRequest, ServerNotification>";
|
|
416
|
-
const pathParamsMapping = paramsCasing ?
|
|
417
|
-
const queryParamsMapping = paramsCasing ?
|
|
418
|
-
const headerParamsMapping = paramsCasing ?
|
|
419
|
-
const contentTypeHeader = contentType && contentType !== "application/json" && contentType !== "multipart/form-data" ? `'Content-Type': '${contentType}'` :
|
|
420
|
-
const headers = [headerParams.length ? headerParamsMapping ? "...mappedHeaders" : "...headers" :
|
|
476
|
+
const pathParamsMapping = paramsCasing ? buildTransformedParamsMapping(originalPathParams, camelCase) : null;
|
|
477
|
+
const queryParamsMapping = paramsCasing ? buildTransformedParamsMapping(originalQueryParams, camelCase) : null;
|
|
478
|
+
const headerParamsMapping = paramsCasing ? buildTransformedParamsMapping(originalHeaderParams, camelCase) : null;
|
|
479
|
+
const contentTypeHeader = contentType && contentType !== "application/json" && contentType !== "multipart/form-data" ? `'Content-Type': '${contentType}'` : null;
|
|
480
|
+
const headers = [headerParams.length ? headerParamsMapping ? "...mappedHeaders" : "...headers" : null, contentTypeHeader].filter(Boolean);
|
|
421
481
|
const fetchConfig = [];
|
|
422
482
|
fetchConfig.push(`method: ${JSON.stringify(node.method.toUpperCase())}`);
|
|
423
483
|
fetchConfig.push(`url: ${urlPath.template}`);
|
|
@@ -451,7 +511,7 @@ function McpHandler({ name, node, resolver, baseURL, dataReturnType, paramsCasin
|
|
|
451
511
|
async: true,
|
|
452
512
|
export: true,
|
|
453
513
|
params: paramsSignature,
|
|
454
|
-
JSDoc: { comments:
|
|
514
|
+
JSDoc: { comments: buildOperationComments(node) },
|
|
455
515
|
returnType: "Promise<CallToolResult>",
|
|
456
516
|
children: [
|
|
457
517
|
"",
|
|
@@ -473,7 +533,7 @@ function McpHandler({ name, node, resolver, baseURL, dataReturnType, paramsCasin
|
|
|
473
533
|
/* @__PURE__ */ jsx("br", {}),
|
|
474
534
|
isFormData && requestName && "const formData = buildFormData(requestData)",
|
|
475
535
|
/* @__PURE__ */ jsx("br", {}),
|
|
476
|
-
`const res = await
|
|
536
|
+
`const res = await client<${generics.join(", ")}>({ ${fetchConfig.join(", ")} }, request)`,
|
|
477
537
|
/* @__PURE__ */ jsx("br", {}),
|
|
478
538
|
callToolResult
|
|
479
539
|
]
|
|
@@ -481,6 +541,39 @@ function McpHandler({ name, node, resolver, baseURL, dataReturnType, paramsCasin
|
|
|
481
541
|
});
|
|
482
542
|
}
|
|
483
543
|
//#endregion
|
|
544
|
+
//#region src/utils.ts
|
|
545
|
+
/**
|
|
546
|
+
* Render a group param value — compose individual schemas into `z.object({ ... })`,
|
|
547
|
+
* or use a schema name string directly.
|
|
548
|
+
*/
|
|
549
|
+
function zodGroupExpr(entry) {
|
|
550
|
+
if (typeof entry === "string") return entry;
|
|
551
|
+
return `z.object({ ${entry.map((p) => `${JSON.stringify(p.name)}: ${p.schemaName}`).join(", ")} })`;
|
|
552
|
+
}
|
|
553
|
+
/**
|
|
554
|
+
* Convert a SchemaNode type to an inline Zod expression string.
|
|
555
|
+
* Used as fallback when no named zod schema is available for a path parameter.
|
|
556
|
+
*/
|
|
557
|
+
function zodExprFromSchemaNode(schema) {
|
|
558
|
+
const baseExpr = (() => {
|
|
559
|
+
if (schema.type === "enum") {
|
|
560
|
+
const rawValues = schema.namedEnumValues?.length ? schema.namedEnumValues.map((v) => v.value) : (schema.enumValues ?? []).filter((v) => v !== null);
|
|
561
|
+
if (rawValues.length > 0 && rawValues.every((v) => typeof v === "string")) return `z.enum([${rawValues.map((v) => JSON.stringify(v)).join(", ")}])`;
|
|
562
|
+
if (rawValues.length > 0) {
|
|
563
|
+
const literals = rawValues.map((v) => `z.literal(${JSON.stringify(v)})`);
|
|
564
|
+
return literals.length === 1 ? literals[0] : `z.union([${literals.join(", ")}])`;
|
|
565
|
+
}
|
|
566
|
+
return "z.string()";
|
|
567
|
+
}
|
|
568
|
+
if (schema.type === "integer") return "z.coerce.number()";
|
|
569
|
+
if (schema.type === "number") return "z.number()";
|
|
570
|
+
if (schema.type === "boolean") return "z.boolean()";
|
|
571
|
+
if (schema.type === "array") return "z.array(z.unknown())";
|
|
572
|
+
return "z.string()";
|
|
573
|
+
})();
|
|
574
|
+
return schema.nullable ? `${baseExpr}.nullable()` : baseExpr;
|
|
575
|
+
}
|
|
576
|
+
//#endregion
|
|
484
577
|
//#region src/components/Server.tsx
|
|
485
578
|
const keysPrinter = functionPrinter({ mode: "keys" });
|
|
486
579
|
function Server({ name, serverName, serverVersion, paramsCasing, operations }) {
|
|
@@ -500,7 +593,7 @@ function Server({ name, serverName, serverVersion, paramsCasing, operations }) {
|
|
|
500
593
|
`
|
|
501
594
|
}),
|
|
502
595
|
operations.map(({ tool, mcp, zod, node }) => {
|
|
503
|
-
const pathParams =
|
|
596
|
+
const { path: pathParams } = getOperationParameters(node, { paramsCasing });
|
|
504
597
|
const pathEntries = [];
|
|
505
598
|
const otherEntries = [];
|
|
506
599
|
for (const p of pathParams) {
|
|
@@ -527,9 +620,9 @@ function Server({ name, serverName, serverVersion, paramsCasing, operations }) {
|
|
|
527
620
|
const paramsNode = entries.length ? ast.createFunctionParameters({ params: [ast.createParameterGroup({ properties: entries.map((e) => ast.createFunctionParameter({
|
|
528
621
|
name: e.key,
|
|
529
622
|
optional: false
|
|
530
|
-
})) })] }) :
|
|
623
|
+
})) })] }) : null;
|
|
531
624
|
const destructured = paramsNode ? keysPrinter.print(paramsNode) ?? "" : "";
|
|
532
|
-
const inputSchema = entries.length ? `{ ${entries.map((e) => `${e.key}: ${e.value}`).join(", ")} }` :
|
|
625
|
+
const inputSchema = entries.length ? `{ ${entries.map((e) => `${e.key}: ${e.value}`).join(", ")} }` : null;
|
|
533
626
|
const outputSchema = zod.responseName;
|
|
534
627
|
const config = [
|
|
535
628
|
tool.title ? `title: ${JSON.stringify(tool.title)}` : null,
|
|
@@ -570,29 +663,28 @@ server.registerTool(${JSON.stringify(tool.name)}, {
|
|
|
570
663
|
}
|
|
571
664
|
//#endregion
|
|
572
665
|
//#region src/generators/mcpGenerator.tsx
|
|
666
|
+
/**
|
|
667
|
+
* Built-in operation generator for `@kubb/plugin-mcp`. Emits one MCP tool
|
|
668
|
+
* handler per OpenAPI operation, wiring the input Zod schema, the HTTP call,
|
|
669
|
+
* and the response shape into a single function that an MCP server can
|
|
670
|
+
* register as a callable tool.
|
|
671
|
+
*/
|
|
573
672
|
const mcpGenerator = defineGenerator({
|
|
574
673
|
name: "mcp",
|
|
575
|
-
renderer:
|
|
674
|
+
renderer: jsxRendererSync,
|
|
576
675
|
operation(node, ctx) {
|
|
676
|
+
if (!ast.isHttpOperationNode(node)) return null;
|
|
577
677
|
const { resolver, driver, root } = ctx;
|
|
578
678
|
const { output, client, paramsCasing, group } = ctx.options;
|
|
579
679
|
const pluginTs = driver.getPlugin(pluginTsName);
|
|
580
680
|
if (!pluginTs) return null;
|
|
581
681
|
const tsResolver = driver.getResolver(pluginTsName);
|
|
582
|
-
const
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
const importedTypeNames = [
|
|
587
|
-
...pathParams.map((p) => tsResolver.resolvePathParamsName(node, p)),
|
|
588
|
-
...queryParams.map((p) => tsResolver.resolveQueryParamsName(node, p)),
|
|
589
|
-
...headerParams.map((p) => tsResolver.resolveHeaderParamsName(node, p)),
|
|
590
|
-
node.requestBody?.content?.[0]?.schema ? tsResolver.resolveDataName(node) : void 0,
|
|
591
|
-
tsResolver.resolveResponseName(node),
|
|
592
|
-
...node.responses.filter((r) => Number(r.statusCode) >= 400).map((r) => tsResolver.resolveResponseStatusName(node, r.statusCode))
|
|
593
|
-
].filter(Boolean);
|
|
682
|
+
const importedTypeNames = resolveOperationTypeNames(node, tsResolver, {
|
|
683
|
+
paramsCasing,
|
|
684
|
+
responseStatusNames: "error"
|
|
685
|
+
});
|
|
594
686
|
const meta = {
|
|
595
|
-
name: resolver.
|
|
687
|
+
name: resolver.resolveHandlerName(node),
|
|
596
688
|
file: resolver.resolveFile({
|
|
597
689
|
name: node.operationId,
|
|
598
690
|
extname: ".ts",
|
|
@@ -601,7 +693,7 @@ const mcpGenerator = defineGenerator({
|
|
|
601
693
|
}, {
|
|
602
694
|
root,
|
|
603
695
|
output,
|
|
604
|
-
group
|
|
696
|
+
group: group ?? void 0
|
|
605
697
|
}),
|
|
606
698
|
fileTs: tsResolver.resolveFile({
|
|
607
699
|
name: node.operationId,
|
|
@@ -611,7 +703,7 @@ const mcpGenerator = defineGenerator({
|
|
|
611
703
|
}, {
|
|
612
704
|
root,
|
|
613
705
|
output: pluginTs.options?.output ?? output,
|
|
614
|
-
group: pluginTs.options?.group
|
|
706
|
+
group: pluginTs.options?.group ?? void 0
|
|
615
707
|
})
|
|
616
708
|
};
|
|
617
709
|
return /* @__PURE__ */ jsxs(File, {
|
|
@@ -655,7 +747,7 @@ const mcpGenerator = defineGenerator({
|
|
|
655
747
|
isTypeOnly: true
|
|
656
748
|
}),
|
|
657
749
|
/* @__PURE__ */ jsx(File.Import, {
|
|
658
|
-
name: "
|
|
750
|
+
name: "client",
|
|
659
751
|
path: client.importPath
|
|
660
752
|
}),
|
|
661
753
|
client.dataReturnType === "full" && /* @__PURE__ */ jsx(File.Import, {
|
|
@@ -671,18 +763,18 @@ const mcpGenerator = defineGenerator({
|
|
|
671
763
|
"ResponseErrorConfig"
|
|
672
764
|
],
|
|
673
765
|
root: meta.file.path,
|
|
674
|
-
path: path.resolve(root, ".kubb/
|
|
766
|
+
path: path.resolve(root, ".kubb/client.ts"),
|
|
675
767
|
isTypeOnly: true
|
|
676
768
|
}),
|
|
677
769
|
/* @__PURE__ */ jsx(File.Import, {
|
|
678
|
-
name: ["
|
|
770
|
+
name: ["client"],
|
|
679
771
|
root: meta.file.path,
|
|
680
|
-
path: path.resolve(root, ".kubb/
|
|
772
|
+
path: path.resolve(root, ".kubb/client.ts")
|
|
681
773
|
}),
|
|
682
774
|
client.dataReturnType === "full" && /* @__PURE__ */ jsx(File.Import, {
|
|
683
775
|
name: ["ResponseConfig"],
|
|
684
776
|
root: meta.file.path,
|
|
685
|
-
path: path.resolve(root, ".kubb/
|
|
777
|
+
path: path.resolve(root, ".kubb/client.ts"),
|
|
686
778
|
isTypeOnly: true
|
|
687
779
|
})
|
|
688
780
|
] }),
|
|
@@ -709,9 +801,9 @@ const mcpGenerator = defineGenerator({
|
|
|
709
801
|
*/
|
|
710
802
|
const serverGenerator = defineGenerator({
|
|
711
803
|
name: "operations",
|
|
712
|
-
renderer:
|
|
804
|
+
renderer: jsxRendererSync,
|
|
713
805
|
operations(nodes, ctx) {
|
|
714
|
-
const {
|
|
806
|
+
const { config, resolver, plugin, driver, root } = ctx;
|
|
715
807
|
const { output, paramsCasing, group } = ctx.options;
|
|
716
808
|
const pluginZod = driver.getPlugin(pluginZodName);
|
|
717
809
|
if (!pluginZod) return;
|
|
@@ -727,11 +819,8 @@ const serverGenerator = defineGenerator({
|
|
|
727
819
|
path: path.resolve(root, output.path, ".mcp.json"),
|
|
728
820
|
meta: { pluginName: plugin.name }
|
|
729
821
|
};
|
|
730
|
-
const operationsMapped = nodes.map((node) => {
|
|
731
|
-
const
|
|
732
|
-
const pathParams = casedParams.filter((p) => p.in === "path");
|
|
733
|
-
const queryParams = casedParams.filter((p) => p.in === "query");
|
|
734
|
-
const headerParams = casedParams.filter((p) => p.in === "header");
|
|
822
|
+
const operationsMapped = nodes.filter(ast.isHttpOperationNode).map((node) => {
|
|
823
|
+
const { path: pathParams, query: queryParams, header: headerParams } = getOperationParameters(node, { paramsCasing });
|
|
735
824
|
const mcpFile = resolver.resolveFile({
|
|
736
825
|
name: node.operationId,
|
|
737
826
|
extname: ".ts",
|
|
@@ -740,7 +829,7 @@ const serverGenerator = defineGenerator({
|
|
|
740
829
|
}, {
|
|
741
830
|
root,
|
|
742
831
|
output,
|
|
743
|
-
group
|
|
832
|
+
group: group ?? void 0
|
|
744
833
|
});
|
|
745
834
|
const zodFile = zodResolver.resolveFile({
|
|
746
835
|
name: node.operationId,
|
|
@@ -750,11 +839,11 @@ const serverGenerator = defineGenerator({
|
|
|
750
839
|
}, {
|
|
751
840
|
root,
|
|
752
841
|
output: pluginZod.options?.output ?? output,
|
|
753
|
-
group: pluginZod.options?.group
|
|
842
|
+
group: pluginZod.options?.group ?? void 0
|
|
754
843
|
});
|
|
755
|
-
const requestName = node.requestBody?.content?.[0]?.schema ? zodResolver.resolveDataName(node) :
|
|
844
|
+
const requestName = node.requestBody?.content?.[0]?.schema ? zodResolver.resolveDataName(node) : null;
|
|
756
845
|
const successStatus = findSuccessStatusCode(node.responses);
|
|
757
|
-
const responseName = successStatus ? zodResolver.resolveResponseStatusName(node, successStatus) :
|
|
846
|
+
const responseName = successStatus ? zodResolver.resolveResponseStatusName(node, successStatus) : null;
|
|
758
847
|
const resolveParams = (params) => params.map((p) => ({
|
|
759
848
|
name: p.name,
|
|
760
849
|
schemaName: zodResolver.resolveParamName(node, p)
|
|
@@ -766,13 +855,13 @@ const serverGenerator = defineGenerator({
|
|
|
766
855
|
description: node.description || `Make a ${node.method.toUpperCase()} request to ${node.path}`
|
|
767
856
|
},
|
|
768
857
|
mcp: {
|
|
769
|
-
name: resolver.
|
|
858
|
+
name: resolver.resolveHandlerName(node),
|
|
770
859
|
file: mcpFile
|
|
771
860
|
},
|
|
772
861
|
zod: {
|
|
773
862
|
pathParams: resolveParams(pathParams),
|
|
774
|
-
queryParams: queryParams.length ? resolveParams(queryParams) :
|
|
775
|
-
headerParams: headerParams.length ? resolveParams(headerParams) :
|
|
863
|
+
queryParams: queryParams.length ? resolveParams(queryParams) : null,
|
|
864
|
+
headerParams: headerParams.length ? resolveParams(headerParams) : null,
|
|
776
865
|
requestName,
|
|
777
866
|
responseName,
|
|
778
867
|
file: zodFile
|
|
@@ -787,7 +876,7 @@ const serverGenerator = defineGenerator({
|
|
|
787
876
|
...(zod.headerParams ?? []).map((p) => p.schemaName),
|
|
788
877
|
zod.requestName,
|
|
789
878
|
zod.responseName
|
|
790
|
-
].filter(Boolean);
|
|
879
|
+
].filter((name) => Boolean(name));
|
|
791
880
|
const uniqueNames = [...new Set(zodNames)].sort();
|
|
792
881
|
return [/* @__PURE__ */ jsx(File.Import, {
|
|
793
882
|
name: [mcp.name],
|
|
@@ -803,13 +892,21 @@ const serverGenerator = defineGenerator({
|
|
|
803
892
|
baseName: serverFile.baseName,
|
|
804
893
|
path: serverFile.path,
|
|
805
894
|
meta: serverFile.meta,
|
|
806
|
-
banner: resolver.resolveBanner(
|
|
895
|
+
banner: resolver.resolveBanner(ctx.meta, {
|
|
807
896
|
output,
|
|
808
|
-
config
|
|
897
|
+
config,
|
|
898
|
+
file: {
|
|
899
|
+
path: serverFile.path,
|
|
900
|
+
baseName: serverFile.baseName
|
|
901
|
+
}
|
|
809
902
|
}),
|
|
810
|
-
footer: resolver.resolveFooter(
|
|
903
|
+
footer: resolver.resolveFooter(ctx.meta, {
|
|
811
904
|
output,
|
|
812
|
-
config
|
|
905
|
+
config,
|
|
906
|
+
file: {
|
|
907
|
+
path: serverFile.path,
|
|
908
|
+
baseName: serverFile.baseName
|
|
909
|
+
}
|
|
813
910
|
}),
|
|
814
911
|
children: [
|
|
815
912
|
/* @__PURE__ */ jsx(File.Import, {
|
|
@@ -827,8 +924,8 @@ const serverGenerator = defineGenerator({
|
|
|
827
924
|
imports,
|
|
828
925
|
/* @__PURE__ */ jsx(Server, {
|
|
829
926
|
name,
|
|
830
|
-
serverName:
|
|
831
|
-
serverVersion:
|
|
927
|
+
serverName: ctx.meta.title ?? "server",
|
|
928
|
+
serverVersion: ctx.meta.version ?? "0.0.0",
|
|
832
929
|
paramsCasing,
|
|
833
930
|
operations: operationsMapped
|
|
834
931
|
})
|
|
@@ -842,7 +939,7 @@ const serverGenerator = defineGenerator({
|
|
|
842
939
|
children: `
|
|
843
940
|
{
|
|
844
941
|
"mcpServers": {
|
|
845
|
-
"${
|
|
942
|
+
"${ctx.meta.title || "server"}": {
|
|
846
943
|
"type": "stdio",
|
|
847
944
|
"command": "npx",
|
|
848
945
|
"args": ["tsx", "${path.relative(path.dirname(jsonFile.path), serverFile.path)}"]
|
|
@@ -857,14 +954,18 @@ const serverGenerator = defineGenerator({
|
|
|
857
954
|
//#endregion
|
|
858
955
|
//#region src/resolvers/resolverMcp.ts
|
|
859
956
|
/**
|
|
860
|
-
*
|
|
957
|
+
* Default resolver used by `@kubb/plugin-mcp`. Decides the names and file
|
|
958
|
+
* paths for every generated MCP tool handler. Function names get a `Handler`
|
|
959
|
+
* suffix so an operation `addPet` becomes `addPetHandler`.
|
|
861
960
|
*
|
|
862
|
-
*
|
|
961
|
+
* @example Resolve a handler name
|
|
962
|
+
* ```ts
|
|
963
|
+
* import { resolverMcp } from '@kubb/plugin-mcp'
|
|
863
964
|
*
|
|
864
|
-
*
|
|
865
|
-
*
|
|
965
|
+
* resolverMcp.default('addPet', 'function') // 'addPetHandler'
|
|
966
|
+
* ```
|
|
866
967
|
*/
|
|
867
|
-
const resolverMcp = defineResolver((
|
|
968
|
+
const resolverMcp = defineResolver(() => ({
|
|
868
969
|
name: "default",
|
|
869
970
|
pluginName: "plugin-mcp",
|
|
870
971
|
default(name, type) {
|
|
@@ -872,12 +973,50 @@ const resolverMcp = defineResolver((ctx) => ({
|
|
|
872
973
|
return camelCase(name, { suffix: "handler" });
|
|
873
974
|
},
|
|
874
975
|
resolveName(name) {
|
|
875
|
-
return
|
|
976
|
+
return this.default(name, "function");
|
|
977
|
+
},
|
|
978
|
+
resolvePathName(name, type) {
|
|
979
|
+
return this.default(name, type);
|
|
980
|
+
},
|
|
981
|
+
resolveHandlerName(node) {
|
|
982
|
+
return this.resolveName(node.operationId);
|
|
876
983
|
}
|
|
877
984
|
}));
|
|
878
985
|
//#endregion
|
|
879
986
|
//#region src/plugin.ts
|
|
987
|
+
/**
|
|
988
|
+
* Canonical plugin name for `@kubb/plugin-mcp`. Used for driver lookups and
|
|
989
|
+
* cross-plugin dependency references.
|
|
990
|
+
*/
|
|
880
991
|
const pluginMcpName = "plugin-mcp";
|
|
992
|
+
/**
|
|
993
|
+
* Generates a Model Context Protocol (MCP) server from an OpenAPI spec. Every
|
|
994
|
+
* operation becomes a typed MCP tool that AI assistants (Claude Desktop, Claude
|
|
995
|
+
* Code, MCP-compatible clients) can call directly.
|
|
996
|
+
*
|
|
997
|
+
* @example
|
|
998
|
+
* ```ts
|
|
999
|
+
* import { defineConfig } from 'kubb'
|
|
1000
|
+
* import { pluginTs } from '@kubb/plugin-ts'
|
|
1001
|
+
* import { pluginClient } from '@kubb/plugin-client'
|
|
1002
|
+
* import { pluginZod } from '@kubb/plugin-zod'
|
|
1003
|
+
* import { pluginMcp } from '@kubb/plugin-mcp'
|
|
1004
|
+
*
|
|
1005
|
+
* export default defineConfig({
|
|
1006
|
+
* input: { path: './petStore.yaml' },
|
|
1007
|
+
* output: { path: './src/gen' },
|
|
1008
|
+
* plugins: [
|
|
1009
|
+
* pluginTs(),
|
|
1010
|
+
* pluginClient(),
|
|
1011
|
+
* pluginZod(),
|
|
1012
|
+
* pluginMcp({
|
|
1013
|
+
* output: { path: './mcp' },
|
|
1014
|
+
* client: { baseURL: 'https://petstore.swagger.io/v2' },
|
|
1015
|
+
* }),
|
|
1016
|
+
* ],
|
|
1017
|
+
* })
|
|
1018
|
+
* ```
|
|
1019
|
+
*/
|
|
881
1020
|
const pluginMcp = definePlugin((options) => {
|
|
882
1021
|
const { output = {
|
|
883
1022
|
path: "mcp",
|
|
@@ -885,13 +1024,10 @@ const pluginMcp = definePlugin((options) => {
|
|
|
885
1024
|
}, group, exclude = [], include, override = [], paramsCasing, client, resolver: userResolver, transformer: userTransformer, generators: userGenerators = [] } = options;
|
|
886
1025
|
const clientName = client?.client ?? "axios";
|
|
887
1026
|
const clientImportPath = client?.importPath ?? (!client?.bundle ? `@kubb/plugin-client/clients/${clientName}` : void 0);
|
|
888
|
-
const groupConfig = group
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
return `${camelCase(ctx.group)}Requests`;
|
|
893
|
-
}
|
|
894
|
-
} : void 0;
|
|
1027
|
+
const groupConfig = createGroupConfig(group, {
|
|
1028
|
+
suffix: "Requests",
|
|
1029
|
+
honorName: true
|
|
1030
|
+
});
|
|
895
1031
|
return {
|
|
896
1032
|
name: pluginMcpName,
|
|
897
1033
|
options,
|
|
@@ -927,10 +1063,10 @@ const pluginMcp = definePlugin((options) => {
|
|
|
927
1063
|
const root = path.resolve(ctx.config.root, ctx.config.output.path);
|
|
928
1064
|
const hasClientPlugin = ctx.config.plugins?.some((p) => p.name === pluginClientName);
|
|
929
1065
|
if (client?.bundle && !hasClientPlugin && !clientImportPath) ctx.injectFile({
|
|
930
|
-
baseName: "
|
|
931
|
-
path: path.resolve(root, ".kubb/
|
|
1066
|
+
baseName: "client.ts",
|
|
1067
|
+
path: path.resolve(root, ".kubb/client.ts"),
|
|
932
1068
|
sources: [ast.createSource({
|
|
933
|
-
name: "
|
|
1069
|
+
name: "client",
|
|
934
1070
|
nodes: [ast.createText(clientName === "fetch" ? source$1 : source)],
|
|
935
1071
|
isExportable: true,
|
|
936
1072
|
isIndexable: true
|