@kubb/plugin-mcp 5.0.0-beta.3 → 5.0.0-beta.30
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 +252 -148
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +78 -22
- package/dist/index.js +253 -149
- package/dist/index.js.map +1 -1
- package/extension.yaml +926 -0
- package/package.json +11 -12
- package/src/components/McpHandler.tsx +14 -20
- package/src/components/Server.tsx +8 -8
- package/src/generators/mcpGenerator.tsx +21 -24
- package/src/generators/serverGenerator.tsx +22 -22
- package/src/plugin.ts +36 -4
- 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,111 @@ 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
|
-
}
|
|
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 `{@link ${node.path.replaceAll("{", ":").replaceAll("}", "")}}`;
|
|
313
311
|
}
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
* Build JSDoc comment lines from an OperationNode.
|
|
324
|
-
*/
|
|
325
|
-
function getComments(node) {
|
|
326
|
-
return [
|
|
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
|
-
for (const p of params) {
|
|
342
|
-
const camelName = camelCase(p.name);
|
|
343
|
-
mapping[p.name] = camelName;
|
|
344
|
-
if (p.name !== camelName) hasDifference = true;
|
|
345
|
-
}
|
|
346
|
-
return hasDifference ? mapping : void 0;
|
|
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
|
+
};
|
|
347
337
|
}
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
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
|
-
default: expr = "z.string()";
|
|
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);
|
|
377
366
|
}
|
|
378
|
-
|
|
379
|
-
|
|
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;
|
|
391
|
+
}
|
|
392
|
+
//#endregion
|
|
393
|
+
//#region ../../internals/shared/src/params.ts
|
|
394
|
+
function buildParamsMapping(originalParams, mappedParams) {
|
|
395
|
+
const mapping = {};
|
|
396
|
+
let hasChanged = false;
|
|
397
|
+
originalParams.forEach((param, i) => {
|
|
398
|
+
const mappedName = mappedParams[i]?.name ?? param.name;
|
|
399
|
+
mapping[param.name] = mappedName;
|
|
400
|
+
if (param.name !== mappedName) hasChanged = true;
|
|
401
|
+
});
|
|
402
|
+
return hasChanged ? mapping : null;
|
|
403
|
+
}
|
|
404
|
+
function buildTransformedParamsMapping(params, transformName) {
|
|
405
|
+
if (!params.length) return null;
|
|
406
|
+
return buildParamsMapping(params, params.map((param) => ({
|
|
407
|
+
...param,
|
|
408
|
+
name: transformName(param.name)
|
|
409
|
+
})));
|
|
380
410
|
}
|
|
381
411
|
//#endregion
|
|
382
412
|
//#region src/components/McpHandler.tsx
|
|
@@ -391,13 +421,9 @@ function McpHandler({ name, node, resolver, baseURL, dataReturnType, paramsCasin
|
|
|
391
421
|
const urlPath = new URLPath(node.path);
|
|
392
422
|
const contentType = node.requestBody?.content?.[0]?.contentType;
|
|
393
423
|
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;
|
|
424
|
+
const { query: queryParams, header: headerParams } = getOperationParameters(node, { paramsCasing });
|
|
425
|
+
const { path: originalPathParams, query: originalQueryParams, header: originalHeaderParams } = getOperationParameters(node);
|
|
426
|
+
const requestName = node.requestBody?.content?.[0]?.schema ? resolver.resolveDataName(node) : null;
|
|
401
427
|
const responseName = resolver.resolveResponseName(node);
|
|
402
428
|
const errorResponses = node.responses.filter((r) => Number(r.statusCode) >= 400).map((r) => resolver.resolveResponseStatusName(node, r.statusCode));
|
|
403
429
|
const generics = [
|
|
@@ -413,11 +439,11 @@ function McpHandler({ name, node, resolver, baseURL, dataReturnType, paramsCasin
|
|
|
413
439
|
});
|
|
414
440
|
const baseParamsSignature = declarationPrinter.print(paramsNode) ?? "";
|
|
415
441
|
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" :
|
|
442
|
+
const pathParamsMapping = paramsCasing ? buildTransformedParamsMapping(originalPathParams, camelCase) : null;
|
|
443
|
+
const queryParamsMapping = paramsCasing ? buildTransformedParamsMapping(originalQueryParams, camelCase) : null;
|
|
444
|
+
const headerParamsMapping = paramsCasing ? buildTransformedParamsMapping(originalHeaderParams, camelCase) : null;
|
|
445
|
+
const contentTypeHeader = contentType && contentType !== "application/json" && contentType !== "multipart/form-data" ? `'Content-Type': '${contentType}'` : null;
|
|
446
|
+
const headers = [headerParams.length ? headerParamsMapping ? "...mappedHeaders" : "...headers" : null, contentTypeHeader].filter(Boolean);
|
|
421
447
|
const fetchConfig = [];
|
|
422
448
|
fetchConfig.push(`method: ${JSON.stringify(node.method.toUpperCase())}`);
|
|
423
449
|
fetchConfig.push(`url: ${urlPath.template}`);
|
|
@@ -451,7 +477,7 @@ function McpHandler({ name, node, resolver, baseURL, dataReturnType, paramsCasin
|
|
|
451
477
|
async: true,
|
|
452
478
|
export: true,
|
|
453
479
|
params: paramsSignature,
|
|
454
|
-
JSDoc: { comments:
|
|
480
|
+
JSDoc: { comments: buildOperationComments(node) },
|
|
455
481
|
returnType: "Promise<CallToolResult>",
|
|
456
482
|
children: [
|
|
457
483
|
"",
|
|
@@ -473,7 +499,7 @@ function McpHandler({ name, node, resolver, baseURL, dataReturnType, paramsCasin
|
|
|
473
499
|
/* @__PURE__ */ jsx("br", {}),
|
|
474
500
|
isFormData && requestName && "const formData = buildFormData(requestData)",
|
|
475
501
|
/* @__PURE__ */ jsx("br", {}),
|
|
476
|
-
`const res = await
|
|
502
|
+
`const res = await client<${generics.join(", ")}>({ ${fetchConfig.join(", ")} }, request)`,
|
|
477
503
|
/* @__PURE__ */ jsx("br", {}),
|
|
478
504
|
callToolResult
|
|
479
505
|
]
|
|
@@ -481,6 +507,39 @@ function McpHandler({ name, node, resolver, baseURL, dataReturnType, paramsCasin
|
|
|
481
507
|
});
|
|
482
508
|
}
|
|
483
509
|
//#endregion
|
|
510
|
+
//#region src/utils.ts
|
|
511
|
+
/**
|
|
512
|
+
* Render a group param value — compose individual schemas into `z.object({ ... })`,
|
|
513
|
+
* or use a schema name string directly.
|
|
514
|
+
*/
|
|
515
|
+
function zodGroupExpr(entry) {
|
|
516
|
+
if (typeof entry === "string") return entry;
|
|
517
|
+
return `z.object({ ${entry.map((p) => `${JSON.stringify(p.name)}: ${p.schemaName}`).join(", ")} })`;
|
|
518
|
+
}
|
|
519
|
+
/**
|
|
520
|
+
* Convert a SchemaNode type to an inline Zod expression string.
|
|
521
|
+
* Used as fallback when no named zod schema is available for a path parameter.
|
|
522
|
+
*/
|
|
523
|
+
function zodExprFromSchemaNode(schema) {
|
|
524
|
+
const baseExpr = (() => {
|
|
525
|
+
if (schema.type === "enum") {
|
|
526
|
+
const rawValues = schema.namedEnumValues?.length ? schema.namedEnumValues.map((v) => v.value) : (schema.enumValues ?? []).filter((v) => v !== null);
|
|
527
|
+
if (rawValues.length > 0 && rawValues.every((v) => typeof v === "string")) return `z.enum([${rawValues.map((v) => JSON.stringify(v)).join(", ")}])`;
|
|
528
|
+
if (rawValues.length > 0) {
|
|
529
|
+
const literals = rawValues.map((v) => `z.literal(${JSON.stringify(v)})`);
|
|
530
|
+
return literals.length === 1 ? literals[0] : `z.union([${literals.join(", ")}])`;
|
|
531
|
+
}
|
|
532
|
+
return "z.string()";
|
|
533
|
+
}
|
|
534
|
+
if (schema.type === "integer") return "z.coerce.number()";
|
|
535
|
+
if (schema.type === "number") return "z.number()";
|
|
536
|
+
if (schema.type === "boolean") return "z.boolean()";
|
|
537
|
+
if (schema.type === "array") return "z.array(z.unknown())";
|
|
538
|
+
return "z.string()";
|
|
539
|
+
})();
|
|
540
|
+
return schema.nullable ? `${baseExpr}.nullable()` : baseExpr;
|
|
541
|
+
}
|
|
542
|
+
//#endregion
|
|
484
543
|
//#region src/components/Server.tsx
|
|
485
544
|
const keysPrinter = functionPrinter({ mode: "keys" });
|
|
486
545
|
function Server({ name, serverName, serverVersion, paramsCasing, operations }) {
|
|
@@ -500,7 +559,7 @@ function Server({ name, serverName, serverVersion, paramsCasing, operations }) {
|
|
|
500
559
|
`
|
|
501
560
|
}),
|
|
502
561
|
operations.map(({ tool, mcp, zod, node }) => {
|
|
503
|
-
const pathParams =
|
|
562
|
+
const { path: pathParams } = getOperationParameters(node, { paramsCasing });
|
|
504
563
|
const pathEntries = [];
|
|
505
564
|
const otherEntries = [];
|
|
506
565
|
for (const p of pathParams) {
|
|
@@ -527,9 +586,9 @@ function Server({ name, serverName, serverVersion, paramsCasing, operations }) {
|
|
|
527
586
|
const paramsNode = entries.length ? ast.createFunctionParameters({ params: [ast.createParameterGroup({ properties: entries.map((e) => ast.createFunctionParameter({
|
|
528
587
|
name: e.key,
|
|
529
588
|
optional: false
|
|
530
|
-
})) })] }) :
|
|
589
|
+
})) })] }) : null;
|
|
531
590
|
const destructured = paramsNode ? keysPrinter.print(paramsNode) ?? "" : "";
|
|
532
|
-
const inputSchema = entries.length ? `{ ${entries.map((e) => `${e.key}: ${e.value}`).join(", ")} }` :
|
|
591
|
+
const inputSchema = entries.length ? `{ ${entries.map((e) => `${e.key}: ${e.value}`).join(", ")} }` : null;
|
|
533
592
|
const outputSchema = zod.responseName;
|
|
534
593
|
const config = [
|
|
535
594
|
tool.title ? `title: ${JSON.stringify(tool.title)}` : null,
|
|
@@ -570,29 +629,27 @@ server.registerTool(${JSON.stringify(tool.name)}, {
|
|
|
570
629
|
}
|
|
571
630
|
//#endregion
|
|
572
631
|
//#region src/generators/mcpGenerator.tsx
|
|
632
|
+
/**
|
|
633
|
+
* Built-in operation generator for `@kubb/plugin-mcp`. Emits one MCP tool
|
|
634
|
+
* handler per OpenAPI operation, wiring the input Zod schema, the HTTP call,
|
|
635
|
+
* and the response shape into a single function that an MCP server can
|
|
636
|
+
* register as a callable tool.
|
|
637
|
+
*/
|
|
573
638
|
const mcpGenerator = defineGenerator({
|
|
574
639
|
name: "mcp",
|
|
575
|
-
renderer:
|
|
640
|
+
renderer: jsxRendererSync,
|
|
576
641
|
operation(node, ctx) {
|
|
577
642
|
const { resolver, driver, root } = ctx;
|
|
578
643
|
const { output, client, paramsCasing, group } = ctx.options;
|
|
579
644
|
const pluginTs = driver.getPlugin(pluginTsName);
|
|
580
645
|
if (!pluginTs) return null;
|
|
581
646
|
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);
|
|
647
|
+
const importedTypeNames = resolveOperationTypeNames(node, tsResolver, {
|
|
648
|
+
paramsCasing,
|
|
649
|
+
responseStatusNames: "error"
|
|
650
|
+
});
|
|
594
651
|
const meta = {
|
|
595
|
-
name: resolver.
|
|
652
|
+
name: resolver.resolveHandlerName(node),
|
|
596
653
|
file: resolver.resolveFile({
|
|
597
654
|
name: node.operationId,
|
|
598
655
|
extname: ".ts",
|
|
@@ -601,7 +658,7 @@ const mcpGenerator = defineGenerator({
|
|
|
601
658
|
}, {
|
|
602
659
|
root,
|
|
603
660
|
output,
|
|
604
|
-
group
|
|
661
|
+
group: group ?? void 0
|
|
605
662
|
}),
|
|
606
663
|
fileTs: tsResolver.resolveFile({
|
|
607
664
|
name: node.operationId,
|
|
@@ -611,7 +668,7 @@ const mcpGenerator = defineGenerator({
|
|
|
611
668
|
}, {
|
|
612
669
|
root,
|
|
613
670
|
output: pluginTs.options?.output ?? output,
|
|
614
|
-
group: pluginTs.options?.group
|
|
671
|
+
group: pluginTs.options?.group ?? void 0
|
|
615
672
|
})
|
|
616
673
|
};
|
|
617
674
|
return /* @__PURE__ */ jsxs(File, {
|
|
@@ -655,7 +712,7 @@ const mcpGenerator = defineGenerator({
|
|
|
655
712
|
isTypeOnly: true
|
|
656
713
|
}),
|
|
657
714
|
/* @__PURE__ */ jsx(File.Import, {
|
|
658
|
-
name: "
|
|
715
|
+
name: "client",
|
|
659
716
|
path: client.importPath
|
|
660
717
|
}),
|
|
661
718
|
client.dataReturnType === "full" && /* @__PURE__ */ jsx(File.Import, {
|
|
@@ -671,18 +728,18 @@ const mcpGenerator = defineGenerator({
|
|
|
671
728
|
"ResponseErrorConfig"
|
|
672
729
|
],
|
|
673
730
|
root: meta.file.path,
|
|
674
|
-
path: path.resolve(root, ".kubb/
|
|
731
|
+
path: path.resolve(root, ".kubb/client.ts"),
|
|
675
732
|
isTypeOnly: true
|
|
676
733
|
}),
|
|
677
734
|
/* @__PURE__ */ jsx(File.Import, {
|
|
678
|
-
name: ["
|
|
735
|
+
name: ["client"],
|
|
679
736
|
root: meta.file.path,
|
|
680
|
-
path: path.resolve(root, ".kubb/
|
|
737
|
+
path: path.resolve(root, ".kubb/client.ts")
|
|
681
738
|
}),
|
|
682
739
|
client.dataReturnType === "full" && /* @__PURE__ */ jsx(File.Import, {
|
|
683
740
|
name: ["ResponseConfig"],
|
|
684
741
|
root: meta.file.path,
|
|
685
|
-
path: path.resolve(root, ".kubb/
|
|
742
|
+
path: path.resolve(root, ".kubb/client.ts"),
|
|
686
743
|
isTypeOnly: true
|
|
687
744
|
})
|
|
688
745
|
] }),
|
|
@@ -709,9 +766,9 @@ const mcpGenerator = defineGenerator({
|
|
|
709
766
|
*/
|
|
710
767
|
const serverGenerator = defineGenerator({
|
|
711
768
|
name: "operations",
|
|
712
|
-
renderer:
|
|
769
|
+
renderer: jsxRendererSync,
|
|
713
770
|
operations(nodes, ctx) {
|
|
714
|
-
const {
|
|
771
|
+
const { config, resolver, plugin, driver, root } = ctx;
|
|
715
772
|
const { output, paramsCasing, group } = ctx.options;
|
|
716
773
|
const pluginZod = driver.getPlugin(pluginZodName);
|
|
717
774
|
if (!pluginZod) return;
|
|
@@ -728,10 +785,7 @@ const serverGenerator = defineGenerator({
|
|
|
728
785
|
meta: { pluginName: plugin.name }
|
|
729
786
|
};
|
|
730
787
|
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");
|
|
788
|
+
const { path: pathParams, query: queryParams, header: headerParams } = getOperationParameters(node, { paramsCasing });
|
|
735
789
|
const mcpFile = resolver.resolveFile({
|
|
736
790
|
name: node.operationId,
|
|
737
791
|
extname: ".ts",
|
|
@@ -740,7 +794,7 @@ const serverGenerator = defineGenerator({
|
|
|
740
794
|
}, {
|
|
741
795
|
root,
|
|
742
796
|
output,
|
|
743
|
-
group
|
|
797
|
+
group: group ?? void 0
|
|
744
798
|
});
|
|
745
799
|
const zodFile = zodResolver.resolveFile({
|
|
746
800
|
name: node.operationId,
|
|
@@ -750,11 +804,11 @@ const serverGenerator = defineGenerator({
|
|
|
750
804
|
}, {
|
|
751
805
|
root,
|
|
752
806
|
output: pluginZod.options?.output ?? output,
|
|
753
|
-
group: pluginZod.options?.group
|
|
807
|
+
group: pluginZod.options?.group ?? void 0
|
|
754
808
|
});
|
|
755
|
-
const requestName = node.requestBody?.content?.[0]?.schema ? zodResolver.resolveDataName(node) :
|
|
809
|
+
const requestName = node.requestBody?.content?.[0]?.schema ? zodResolver.resolveDataName(node) : null;
|
|
756
810
|
const successStatus = findSuccessStatusCode(node.responses);
|
|
757
|
-
const responseName = successStatus ? zodResolver.resolveResponseStatusName(node, successStatus) :
|
|
811
|
+
const responseName = successStatus ? zodResolver.resolveResponseStatusName(node, successStatus) : null;
|
|
758
812
|
const resolveParams = (params) => params.map((p) => ({
|
|
759
813
|
name: p.name,
|
|
760
814
|
schemaName: zodResolver.resolveParamName(node, p)
|
|
@@ -766,13 +820,13 @@ const serverGenerator = defineGenerator({
|
|
|
766
820
|
description: node.description || `Make a ${node.method.toUpperCase()} request to ${node.path}`
|
|
767
821
|
},
|
|
768
822
|
mcp: {
|
|
769
|
-
name: resolver.
|
|
823
|
+
name: resolver.resolveHandlerName(node),
|
|
770
824
|
file: mcpFile
|
|
771
825
|
},
|
|
772
826
|
zod: {
|
|
773
827
|
pathParams: resolveParams(pathParams),
|
|
774
|
-
queryParams: queryParams.length ? resolveParams(queryParams) :
|
|
775
|
-
headerParams: headerParams.length ? resolveParams(headerParams) :
|
|
828
|
+
queryParams: queryParams.length ? resolveParams(queryParams) : null,
|
|
829
|
+
headerParams: headerParams.length ? resolveParams(headerParams) : null,
|
|
776
830
|
requestName,
|
|
777
831
|
responseName,
|
|
778
832
|
file: zodFile
|
|
@@ -787,7 +841,7 @@ const serverGenerator = defineGenerator({
|
|
|
787
841
|
...(zod.headerParams ?? []).map((p) => p.schemaName),
|
|
788
842
|
zod.requestName,
|
|
789
843
|
zod.responseName
|
|
790
|
-
].filter(Boolean);
|
|
844
|
+
].filter((name) => Boolean(name));
|
|
791
845
|
const uniqueNames = [...new Set(zodNames)].sort();
|
|
792
846
|
return [/* @__PURE__ */ jsx(File.Import, {
|
|
793
847
|
name: [mcp.name],
|
|
@@ -803,13 +857,21 @@ const serverGenerator = defineGenerator({
|
|
|
803
857
|
baseName: serverFile.baseName,
|
|
804
858
|
path: serverFile.path,
|
|
805
859
|
meta: serverFile.meta,
|
|
806
|
-
banner: resolver.resolveBanner(
|
|
860
|
+
banner: resolver.resolveBanner(ctx.meta, {
|
|
807
861
|
output,
|
|
808
|
-
config
|
|
862
|
+
config,
|
|
863
|
+
file: {
|
|
864
|
+
path: serverFile.path,
|
|
865
|
+
baseName: serverFile.baseName
|
|
866
|
+
}
|
|
809
867
|
}),
|
|
810
|
-
footer: resolver.resolveFooter(
|
|
868
|
+
footer: resolver.resolveFooter(ctx.meta, {
|
|
811
869
|
output,
|
|
812
|
-
config
|
|
870
|
+
config,
|
|
871
|
+
file: {
|
|
872
|
+
path: serverFile.path,
|
|
873
|
+
baseName: serverFile.baseName
|
|
874
|
+
}
|
|
813
875
|
}),
|
|
814
876
|
children: [
|
|
815
877
|
/* @__PURE__ */ jsx(File.Import, {
|
|
@@ -827,8 +889,8 @@ const serverGenerator = defineGenerator({
|
|
|
827
889
|
imports,
|
|
828
890
|
/* @__PURE__ */ jsx(Server, {
|
|
829
891
|
name,
|
|
830
|
-
serverName:
|
|
831
|
-
serverVersion:
|
|
892
|
+
serverName: ctx.meta.title ?? "server",
|
|
893
|
+
serverVersion: ctx.meta.version ?? "0.0.0",
|
|
832
894
|
paramsCasing,
|
|
833
895
|
operations: operationsMapped
|
|
834
896
|
})
|
|
@@ -842,7 +904,7 @@ const serverGenerator = defineGenerator({
|
|
|
842
904
|
children: `
|
|
843
905
|
{
|
|
844
906
|
"mcpServers": {
|
|
845
|
-
"${
|
|
907
|
+
"${ctx.meta.title || "server"}": {
|
|
846
908
|
"type": "stdio",
|
|
847
909
|
"command": "npx",
|
|
848
910
|
"args": ["tsx", "${path.relative(path.dirname(jsonFile.path), serverFile.path)}"]
|
|
@@ -857,14 +919,18 @@ const serverGenerator = defineGenerator({
|
|
|
857
919
|
//#endregion
|
|
858
920
|
//#region src/resolvers/resolverMcp.ts
|
|
859
921
|
/**
|
|
860
|
-
*
|
|
922
|
+
* Default resolver used by `@kubb/plugin-mcp`. Decides the names and file
|
|
923
|
+
* paths for every generated MCP tool handler. Function names get a `Handler`
|
|
924
|
+
* suffix so an operation `addPet` becomes `addPetHandler`.
|
|
861
925
|
*
|
|
862
|
-
*
|
|
926
|
+
* @example Resolve a handler name
|
|
927
|
+
* ```ts
|
|
928
|
+
* import { resolverMcp } from '@kubb/plugin-mcp'
|
|
863
929
|
*
|
|
864
|
-
*
|
|
865
|
-
*
|
|
930
|
+
* resolverMcp.default('addPet', 'function') // 'addPetHandler'
|
|
931
|
+
* ```
|
|
866
932
|
*/
|
|
867
|
-
const resolverMcp = defineResolver((
|
|
933
|
+
const resolverMcp = defineResolver(() => ({
|
|
868
934
|
name: "default",
|
|
869
935
|
pluginName: "plugin-mcp",
|
|
870
936
|
default(name, type) {
|
|
@@ -872,12 +938,50 @@ const resolverMcp = defineResolver((ctx) => ({
|
|
|
872
938
|
return camelCase(name, { suffix: "handler" });
|
|
873
939
|
},
|
|
874
940
|
resolveName(name) {
|
|
875
|
-
return
|
|
941
|
+
return this.default(name, "function");
|
|
942
|
+
},
|
|
943
|
+
resolvePathName(name, type) {
|
|
944
|
+
return this.default(name, type);
|
|
945
|
+
},
|
|
946
|
+
resolveHandlerName(node) {
|
|
947
|
+
return this.resolveName(node.operationId);
|
|
876
948
|
}
|
|
877
949
|
}));
|
|
878
950
|
//#endregion
|
|
879
951
|
//#region src/plugin.ts
|
|
952
|
+
/**
|
|
953
|
+
* Canonical plugin name for `@kubb/plugin-mcp`. Used for driver lookups and
|
|
954
|
+
* cross-plugin dependency references.
|
|
955
|
+
*/
|
|
880
956
|
const pluginMcpName = "plugin-mcp";
|
|
957
|
+
/**
|
|
958
|
+
* Generates a Model Context Protocol (MCP) server from an OpenAPI spec. Every
|
|
959
|
+
* operation becomes a typed MCP tool that AI assistants (Claude Desktop, Claude
|
|
960
|
+
* Code, MCP-compatible clients) can call directly.
|
|
961
|
+
*
|
|
962
|
+
* @example
|
|
963
|
+
* ```ts
|
|
964
|
+
* import { defineConfig } from 'kubb'
|
|
965
|
+
* import { pluginTs } from '@kubb/plugin-ts'
|
|
966
|
+
* import { pluginClient } from '@kubb/plugin-client'
|
|
967
|
+
* import { pluginZod } from '@kubb/plugin-zod'
|
|
968
|
+
* import { pluginMcp } from '@kubb/plugin-mcp'
|
|
969
|
+
*
|
|
970
|
+
* export default defineConfig({
|
|
971
|
+
* input: { path: './petStore.yaml' },
|
|
972
|
+
* output: { path: './src/gen' },
|
|
973
|
+
* plugins: [
|
|
974
|
+
* pluginTs(),
|
|
975
|
+
* pluginClient(),
|
|
976
|
+
* pluginZod(),
|
|
977
|
+
* pluginMcp({
|
|
978
|
+
* output: { path: './mcp' },
|
|
979
|
+
* client: { baseURL: 'https://petstore.swagger.io/v2' },
|
|
980
|
+
* }),
|
|
981
|
+
* ],
|
|
982
|
+
* })
|
|
983
|
+
* ```
|
|
984
|
+
*/
|
|
881
985
|
const pluginMcp = definePlugin((options) => {
|
|
882
986
|
const { output = {
|
|
883
987
|
path: "mcp",
|
|
@@ -891,7 +995,7 @@ const pluginMcp = definePlugin((options) => {
|
|
|
891
995
|
if (group.type === "path") return `${ctx.group.split("/")[1]}`;
|
|
892
996
|
return `${camelCase(ctx.group)}Requests`;
|
|
893
997
|
}
|
|
894
|
-
} :
|
|
998
|
+
} : null;
|
|
895
999
|
return {
|
|
896
1000
|
name: pluginMcpName,
|
|
897
1001
|
options,
|
|
@@ -927,10 +1031,10 @@ const pluginMcp = definePlugin((options) => {
|
|
|
927
1031
|
const root = path.resolve(ctx.config.root, ctx.config.output.path);
|
|
928
1032
|
const hasClientPlugin = ctx.config.plugins?.some((p) => p.name === pluginClientName);
|
|
929
1033
|
if (client?.bundle && !hasClientPlugin && !clientImportPath) ctx.injectFile({
|
|
930
|
-
baseName: "
|
|
931
|
-
path: path.resolve(root, ".kubb/
|
|
1034
|
+
baseName: "client.ts",
|
|
1035
|
+
path: path.resolve(root, ".kubb/client.ts"),
|
|
932
1036
|
sources: [ast.createSource({
|
|
933
|
-
name: "
|
|
1037
|
+
name: "client",
|
|
934
1038
|
nodes: [ast.createText(clientName === "fetch" ? source$1 : source)],
|
|
935
1039
|
isExportable: true,
|
|
936
1040
|
isIndexable: true
|