@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/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,111 @@ 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
- }
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
- * 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(", ")} })`;
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
- `{@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;
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
- * 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.
351
- */
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()";
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
- if (schema.nullable) expr = `${expr}.nullable()`;
379
- return expr;
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 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;
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 ? 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);
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: getComments(node) },
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 fetch<${generics.join(", ")}>({ ${fetchConfig.join(", ")} }, request)`,
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 = ast.caseParams(node.parameters, paramsCasing).filter((p) => p.in === "path");
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
- })) })] }) : void 0;
589
+ })) })] }) : null;
531
590
  const destructured = paramsNode ? keysPrinter.print(paramsNode) ?? "" : "";
532
- const inputSchema = entries.length ? `{ ${entries.map((e) => `${e.key}: ${e.value}`).join(", ")} }` : void 0;
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: jsxRenderer,
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 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);
647
+ const importedTypeNames = resolveOperationTypeNames(node, tsResolver, {
648
+ paramsCasing,
649
+ responseStatusNames: "error"
650
+ });
594
651
  const meta = {
595
- name: resolver.resolveName(node.operationId),
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: "fetch",
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/fetch.ts"),
731
+ path: path.resolve(root, ".kubb/client.ts"),
675
732
  isTypeOnly: true
676
733
  }),
677
734
  /* @__PURE__ */ jsx(File.Import, {
678
- name: ["fetch"],
735
+ name: ["client"],
679
736
  root: meta.file.path,
680
- path: path.resolve(root, ".kubb/fetch.ts")
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/fetch.ts"),
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: jsxRenderer,
769
+ renderer: jsxRendererSync,
713
770
  operations(nodes, ctx) {
714
- const { adapter, config, resolver, plugin, driver, root } = ctx;
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 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");
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) : void 0;
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) : void 0;
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.resolveName(node.operationId),
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) : void 0,
775
- headerParams: headerParams.length ? resolveParams(headerParams) : void 0,
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(adapter.inputNode, {
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(adapter.inputNode, {
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: adapter.inputNode?.meta?.title ?? "server",
831
- serverVersion: adapter.inputNode?.meta?.version ?? "0.0.0",
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
- "${adapter.inputNode?.meta?.title || "server"}": {
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
- * Naming convention resolver for MCP plugin.
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
- * Provides default naming helpers using camelCase with a `handler` suffix for functions.
926
+ * @example Resolve a handler name
927
+ * ```ts
928
+ * import { resolverMcp } from '@kubb/plugin-mcp'
863
929
  *
864
- * @example
865
- * `resolverMcp.default('addPet', 'function') // → 'addPetHandler'`
930
+ * resolverMcp.default('addPet', 'function') // 'addPetHandler'
931
+ * ```
866
932
  */
867
- const resolverMcp = defineResolver((ctx) => ({
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 ctx.default(name, "function");
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
- } : void 0;
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: "fetch.ts",
931
- path: path.resolve(root, ".kubb/fetch.ts"),
1034
+ baseName: "client.ts",
1035
+ path: path.resolve(root, ".kubb/client.ts"),
932
1036
  sources: [ast.createSource({
933
- name: "fetch",
1037
+ name: "client",
934
1038
  nodes: [ast.createText(clientName === "fetch" ? source$1 : source)],
935
1039
  isExportable: true,
936
1040
  isIndexable: true