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