@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/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, jsxRenderer } from "@kubb/renderer-jsx";
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 `undefined` when the path has no parameters.
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 // undefined
228
+ * new URLPath('/pet').params // null
229
229
  * ```
230
230
  */
231
231
  get params() {
232
- return this.getParams();
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.getParams()
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 = "", replacer } = {}) {
267
- return `\`${prefix}${this.path.split(/\{([^}]+)\}/).map((part, i) => {
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}').getParams()
281
+ * new URLPath('/pet/{petId}/tag/{tagId}').toParamsObject()
281
282
  * // { petId: 'petId', tagId: 'tagId' }
282
283
  * ```
283
284
  */
284
- getParams(replacer) {
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 : void 0;
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/utils.ts
305
- /**
306
- * Find the first 2xx response status code from an operation's responses.
307
- */
308
- function findSuccessStatusCode(responses) {
309
- for (const res of responses) {
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
- * 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
- `{@link ${node.path.replaceAll("{", ":").replaceAll("}", "")}}`
331
- ].filter((x) => Boolean(x));
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
- * Build a mapping of original param names → camelCase names.
335
- * Returns `undefined` when no names actually change (no remapping needed).
336
- */
337
- function getParamsMapping(params) {
338
- if (!params.length) return;
339
- const mapping = {};
340
- let hasDifference = false;
341
- for (const p of params) {
342
- const camelName = camelCase(p.name);
343
- mapping[p.name] = camelName;
344
- if (p.name !== camelName) hasDifference = true;
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
- return hasDifference ? mapping : void 0;
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
- * Convert a SchemaNode type to an inline Zod expression string.
350
- * Used as fallback when no named zod schema is available for a path parameter.
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 zodExprFromSchemaNode(schema) {
353
- let expr;
354
- switch (schema.type) {
355
- case "enum": {
356
- const rawValues = schema.namedEnumValues?.length ? schema.namedEnumValues.map((v) => v.value) : (schema.enumValues ?? []).filter((v) => v !== null);
357
- if (rawValues.length > 0 && rawValues.every((v) => typeof v === "string")) expr = `z.enum([${rawValues.map((v) => JSON.stringify(v)).join(", ")}])`;
358
- else if (rawValues.length > 0) {
359
- const literals = rawValues.map((v) => `z.literal(${JSON.stringify(v)})`);
360
- expr = literals.length === 1 ? literals[0] : `z.union([${literals.join(", ")}])`;
361
- } else expr = "z.string()";
362
- break;
363
- }
364
- case "integer":
365
- expr = "z.coerce.number()";
366
- break;
367
- case "number":
368
- expr = "z.number()";
369
- break;
370
- case "boolean":
371
- expr = "z.boolean()";
372
- break;
373
- case "array":
374
- expr = "z.array(z.unknown())";
375
- break;
376
- default: expr = "z.string()";
377
- }
378
- if (schema.nullable) expr = `${expr}.nullable()`;
379
- return expr;
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 casedParams = ast.caseParams(node.parameters, paramsCasing);
395
- const queryParams = casedParams.filter((p) => p.in === "query");
396
- const headerParams = casedParams.filter((p) => p.in === "header");
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 ? getParamsMapping(originalPathParams) : void 0;
417
- const queryParamsMapping = paramsCasing ? getParamsMapping(originalQueryParams) : void 0;
418
- const headerParamsMapping = paramsCasing ? getParamsMapping(originalHeaderParams) : void 0;
419
- const contentTypeHeader = contentType && contentType !== "application/json" && contentType !== "multipart/form-data" ? `'Content-Type': '${contentType}'` : void 0;
420
- const headers = [headerParams.length ? headerParamsMapping ? "...mappedHeaders" : "...headers" : void 0, contentTypeHeader].filter(Boolean);
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: getComments(node) },
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 fetch<${generics.join(", ")}>({ ${fetchConfig.join(", ")} }, request)`,
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 = ast.caseParams(node.parameters, paramsCasing).filter((p) => p.in === "path");
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
- })) })] }) : void 0;
623
+ })) })] }) : null;
531
624
  const destructured = paramsNode ? keysPrinter.print(paramsNode) ?? "" : "";
532
- const inputSchema = entries.length ? `{ ${entries.map((e) => `${e.key}: ${e.value}`).join(", ")} }` : void 0;
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: jsxRenderer,
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 casedParams = ast.caseParams(node.parameters, paramsCasing);
583
- const pathParams = casedParams.filter((p) => p.in === "path");
584
- const queryParams = casedParams.filter((p) => p.in === "query");
585
- const headerParams = casedParams.filter((p) => p.in === "header");
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.resolveName(node.operationId),
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: "fetch",
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/fetch.ts"),
766
+ path: path.resolve(root, ".kubb/client.ts"),
675
767
  isTypeOnly: true
676
768
  }),
677
769
  /* @__PURE__ */ jsx(File.Import, {
678
- name: ["fetch"],
770
+ name: ["client"],
679
771
  root: meta.file.path,
680
- path: path.resolve(root, ".kubb/fetch.ts")
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/fetch.ts"),
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: jsxRenderer,
804
+ renderer: jsxRendererSync,
713
805
  operations(nodes, ctx) {
714
- const { adapter, config, resolver, plugin, driver, root } = ctx;
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 casedParams = ast.caseParams(node.parameters, paramsCasing);
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) : void 0;
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) : void 0;
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.resolveName(node.operationId),
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) : void 0,
775
- headerParams: headerParams.length ? resolveParams(headerParams) : void 0,
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(adapter.inputNode, {
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(adapter.inputNode, {
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: adapter.inputNode?.meta?.title ?? "server",
831
- serverVersion: adapter.inputNode?.meta?.version ?? "0.0.0",
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
- "${adapter.inputNode?.meta?.title || "server"}": {
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
- * Naming convention resolver for MCP plugin.
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
- * Provides default naming helpers using camelCase with a `handler` suffix for functions.
961
+ * @example Resolve a handler name
962
+ * ```ts
963
+ * import { resolverMcp } from '@kubb/plugin-mcp'
863
964
  *
864
- * @example
865
- * `resolverMcp.default('addPet', 'function') // → 'addPetHandler'`
965
+ * resolverMcp.default('addPet', 'function') // 'addPetHandler'
966
+ * ```
866
967
  */
867
- const resolverMcp = defineResolver((ctx) => ({
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 ctx.default(name, "function");
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
- ...group,
890
- name: group.name ? group.name : (ctx) => {
891
- if (group.type === "path") return `${ctx.group.split("/")[1]}`;
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: "fetch.ts",
931
- path: path.resolve(root, ".kubb/fetch.ts"),
1066
+ baseName: "client.ts",
1067
+ path: path.resolve(root, ".kubb/client.ts"),
932
1068
  sources: [ast.createSource({
933
- name: "fetch",
1069
+ name: "client",
934
1070
  nodes: [ast.createText(clientName === "fetch" ? source$1 : source)],
935
1071
  isExportable: true,
936
1072
  isIndexable: true