@kubb/plugin-mcp 5.0.0-beta.4 → 5.0.0-beta.42

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
@@ -1,8 +1,8 @@
1
- import "./chunk--u3MIqq1.js";
1
+ import "./chunk-C0LytTxp.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
- }
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;
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;
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
+ * A user-provided `group.name` always wins over the default namer, so callers stay in
402
+ * control of their output folders. Returns `null` when grouping is disabled, matching the
403
+ * per-plugin convention.
404
+ *
405
+ * @param group - The user-supplied group option, or `undefined` to disable grouping.
406
+ * @param options.suffix - Appended to non-`path` group names, e.g. `'Controller'` or `'Requests'`.
407
+ *
408
+ * @example
409
+ * ```ts
410
+ * createGroupConfig(group, { suffix: 'Controller' }) // plugin-ts, plugin-client, …
411
+ * createGroupConfig(group, { suffix: 'Requests' }) // 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: 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,62 +541,80 @@ 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 }) {
487
- return /* @__PURE__ */ jsxs(File.Source, {
488
- name,
489
- isExportable: true,
490
- isIndexable: true,
491
- children: [
492
- /* @__PURE__ */ jsx(Const, {
493
- name: "server",
494
- export: true,
495
- children: `
496
- new McpServer({
497
- name: '${serverName}',
498
- version: '${serverVersion}',
499
- })
500
- `
501
- }),
502
- operations.map(({ tool, mcp, zod, node }) => {
503
- const pathParams = ast.caseParams(node.parameters, paramsCasing).filter((p) => p.in === "path");
504
- const pathEntries = [];
505
- const otherEntries = [];
506
- for (const p of pathParams) {
507
- const zodParam = zod.pathParams.find((zp) => zp.name === p.name);
508
- pathEntries.push({
509
- key: p.name,
510
- value: zodParam ? zodParam.schemaName : zodExprFromSchemaNode(p.schema)
511
- });
512
- }
513
- if (zod.requestName) otherEntries.push({
514
- key: "data",
515
- value: zod.requestName
516
- });
517
- if (zod.queryParams) otherEntries.push({
518
- key: "params",
519
- value: zodGroupExpr(zod.queryParams)
520
- });
521
- if (zod.headerParams) otherEntries.push({
522
- key: "headers",
523
- value: zodGroupExpr(zod.headerParams)
524
- });
525
- otherEntries.sort((a, b) => a.key.localeCompare(b.key));
526
- const entries = [...pathEntries, ...otherEntries];
527
- const paramsNode = entries.length ? ast.createFunctionParameters({ params: [ast.createParameterGroup({ properties: entries.map((e) => ast.createFunctionParameter({
528
- name: e.key,
529
- optional: false
530
- })) })] }) : void 0;
531
- const destructured = paramsNode ? keysPrinter.print(paramsNode) ?? "" : "";
532
- const inputSchema = entries.length ? `{ ${entries.map((e) => `${e.key}: ${e.value}`).join(", ")} }` : void 0;
533
- const outputSchema = zod.responseName;
534
- const config = [
535
- tool.title ? `title: ${JSON.stringify(tool.title)}` : null,
536
- `description: ${JSON.stringify(tool.description)}`,
537
- outputSchema ? `outputSchema: { data: ${outputSchema} }` : null
538
- ].filter(Boolean).join(",\n ");
539
- if (inputSchema) return `
580
+ const registrations = operations.map(({ tool, mcp, zod, node }) => {
581
+ const { path: pathParams } = getOperationParameters(node, { paramsCasing });
582
+ const pathEntries = [];
583
+ const otherEntries = [];
584
+ for (const p of pathParams) {
585
+ const zodParam = zod.pathParams.find((zp) => zp.name === p.name);
586
+ pathEntries.push({
587
+ key: p.name,
588
+ value: zodParam ? zodParam.schemaName : zodExprFromSchemaNode(p.schema)
589
+ });
590
+ }
591
+ if (zod.requestName) otherEntries.push({
592
+ key: "data",
593
+ value: zod.requestName
594
+ });
595
+ if (zod.queryParams) otherEntries.push({
596
+ key: "params",
597
+ value: zodGroupExpr(zod.queryParams)
598
+ });
599
+ if (zod.headerParams) otherEntries.push({
600
+ key: "headers",
601
+ value: zodGroupExpr(zod.headerParams)
602
+ });
603
+ otherEntries.sort((a, b) => a.key.localeCompare(b.key));
604
+ const entries = [...pathEntries, ...otherEntries];
605
+ const paramsNode = entries.length ? ast.createFunctionParameters({ params: [ast.createParameterGroup({ properties: entries.map((e) => ast.createFunctionParameter({
606
+ name: e.key,
607
+ optional: false
608
+ })) })] }) : null;
609
+ const destructured = paramsNode ? keysPrinter.print(paramsNode) ?? "" : "";
610
+ const inputSchema = entries.length ? `{ ${entries.map((e) => `${e.key}: ${e.value}`).join(", ")} }` : null;
611
+ const outputSchema = zod.responseName;
612
+ const config = [
613
+ tool.title ? `title: ${JSON.stringify(tool.title)}` : null,
614
+ `description: ${JSON.stringify(tool.description)}`,
615
+ outputSchema ? `outputSchema: { data: ${outputSchema} }` : null
616
+ ].filter(Boolean).join(",\n ");
617
+ if (inputSchema) return `
540
618
  server.registerTool(${JSON.stringify(tool.name)}, {
541
619
  ${config},
542
620
  inputSchema: ${inputSchema},
@@ -544,14 +622,34 @@ server.registerTool(${JSON.stringify(tool.name)}, {
544
622
  return ${mcp.name}(${destructured}, request)
545
623
  })
546
624
  `;
547
- return `
625
+ return `
548
626
  server.registerTool(${JSON.stringify(tool.name)}, {
549
627
  ${config},
550
628
  }, async (request) => {
551
629
  return ${mcp.name}(request)
552
630
  })
553
631
  `;
554
- }).filter(Boolean),
632
+ }).filter(Boolean).join("\n");
633
+ return /* @__PURE__ */ jsxs(File.Source, {
634
+ name,
635
+ isExportable: true,
636
+ isIndexable: true,
637
+ children: [
638
+ /* @__PURE__ */ jsx(Function, {
639
+ name: "getServer",
640
+ export: true,
641
+ children: `const server = new McpServer({
642
+ name: '${serverName}',
643
+ version: '${serverVersion}',
644
+ })
645
+ ${registrations}
646
+ return server`
647
+ }),
648
+ /* @__PURE__ */ jsx(Const, {
649
+ name: "server",
650
+ export: true,
651
+ children: "getServer()"
652
+ }),
555
653
  /* @__PURE__ */ jsx(Function, {
556
654
  name: "startServer",
557
655
  async: true,
@@ -570,29 +668,28 @@ server.registerTool(${JSON.stringify(tool.name)}, {
570
668
  }
571
669
  //#endregion
572
670
  //#region src/generators/mcpGenerator.tsx
671
+ /**
672
+ * Built-in operation generator for `@kubb/plugin-mcp`. Emits one MCP tool
673
+ * handler per OpenAPI operation, wiring the input Zod schema, the HTTP call,
674
+ * and the response shape into a single function that an MCP server can
675
+ * register as a callable tool.
676
+ */
573
677
  const mcpGenerator = defineGenerator({
574
678
  name: "mcp",
575
- renderer: jsxRenderer,
679
+ renderer: jsxRendererSync,
576
680
  operation(node, ctx) {
681
+ if (!ast.isHttpOperationNode(node)) return null;
577
682
  const { resolver, driver, root } = ctx;
578
683
  const { output, client, paramsCasing, group } = ctx.options;
579
684
  const pluginTs = driver.getPlugin(pluginTsName);
580
685
  if (!pluginTs) return null;
581
686
  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);
687
+ const importedTypeNames = resolveOperationTypeNames(node, tsResolver, {
688
+ paramsCasing,
689
+ responseStatusNames: "error"
690
+ });
594
691
  const meta = {
595
- name: resolver.resolveName(node.operationId),
692
+ name: resolver.resolveHandlerName(node),
596
693
  file: resolver.resolveFile({
597
694
  name: node.operationId,
598
695
  extname: ".ts",
@@ -601,7 +698,7 @@ const mcpGenerator = defineGenerator({
601
698
  }, {
602
699
  root,
603
700
  output,
604
- group
701
+ group: group ?? void 0
605
702
  }),
606
703
  fileTs: tsResolver.resolveFile({
607
704
  name: node.operationId,
@@ -611,7 +708,7 @@ const mcpGenerator = defineGenerator({
611
708
  }, {
612
709
  root,
613
710
  output: pluginTs.options?.output ?? output,
614
- group: pluginTs.options?.group
711
+ group: pluginTs.options?.group ?? void 0
615
712
  })
616
713
  };
617
714
  return /* @__PURE__ */ jsxs(File, {
@@ -655,7 +752,7 @@ const mcpGenerator = defineGenerator({
655
752
  isTypeOnly: true
656
753
  }),
657
754
  /* @__PURE__ */ jsx(File.Import, {
658
- name: "fetch",
755
+ name: "client",
659
756
  path: client.importPath
660
757
  }),
661
758
  client.dataReturnType === "full" && /* @__PURE__ */ jsx(File.Import, {
@@ -671,18 +768,18 @@ const mcpGenerator = defineGenerator({
671
768
  "ResponseErrorConfig"
672
769
  ],
673
770
  root: meta.file.path,
674
- path: path.resolve(root, ".kubb/fetch.ts"),
771
+ path: path.resolve(root, ".kubb/client.ts"),
675
772
  isTypeOnly: true
676
773
  }),
677
774
  /* @__PURE__ */ jsx(File.Import, {
678
- name: ["fetch"],
775
+ name: ["client"],
679
776
  root: meta.file.path,
680
- path: path.resolve(root, ".kubb/fetch.ts")
777
+ path: path.resolve(root, ".kubb/client.ts")
681
778
  }),
682
779
  client.dataReturnType === "full" && /* @__PURE__ */ jsx(File.Import, {
683
780
  name: ["ResponseConfig"],
684
781
  root: meta.file.path,
685
- path: path.resolve(root, ".kubb/fetch.ts"),
782
+ path: path.resolve(root, ".kubb/client.ts"),
686
783
  isTypeOnly: true
687
784
  })
688
785
  ] }),
@@ -709,9 +806,9 @@ const mcpGenerator = defineGenerator({
709
806
  */
710
807
  const serverGenerator = defineGenerator({
711
808
  name: "operations",
712
- renderer: jsxRenderer,
809
+ renderer: jsxRendererSync,
713
810
  operations(nodes, ctx) {
714
- const { adapter, config, resolver, plugin, driver, root } = ctx;
811
+ const { config, resolver, plugin, driver, root } = ctx;
715
812
  const { output, paramsCasing, group } = ctx.options;
716
813
  const pluginZod = driver.getPlugin(pluginZodName);
717
814
  if (!pluginZod) return;
@@ -727,11 +824,8 @@ const serverGenerator = defineGenerator({
727
824
  path: path.resolve(root, output.path, ".mcp.json"),
728
825
  meta: { pluginName: plugin.name }
729
826
  };
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");
827
+ const operationsMapped = nodes.filter(ast.isHttpOperationNode).map((node) => {
828
+ const { path: pathParams, query: queryParams, header: headerParams } = getOperationParameters(node, { paramsCasing });
735
829
  const mcpFile = resolver.resolveFile({
736
830
  name: node.operationId,
737
831
  extname: ".ts",
@@ -740,7 +834,7 @@ const serverGenerator = defineGenerator({
740
834
  }, {
741
835
  root,
742
836
  output,
743
- group
837
+ group: group ?? void 0
744
838
  });
745
839
  const zodFile = zodResolver.resolveFile({
746
840
  name: node.operationId,
@@ -750,11 +844,11 @@ const serverGenerator = defineGenerator({
750
844
  }, {
751
845
  root,
752
846
  output: pluginZod.options?.output ?? output,
753
- group: pluginZod.options?.group
847
+ group: pluginZod.options?.group ?? void 0
754
848
  });
755
- const requestName = node.requestBody?.content?.[0]?.schema ? zodResolver.resolveDataName(node) : void 0;
849
+ const requestName = node.requestBody?.content?.[0]?.schema ? zodResolver.resolveDataName(node) : null;
756
850
  const successStatus = findSuccessStatusCode(node.responses);
757
- const responseName = successStatus ? zodResolver.resolveResponseStatusName(node, successStatus) : void 0;
851
+ const responseName = successStatus ? zodResolver.resolveResponseStatusName(node, successStatus) : null;
758
852
  const resolveParams = (params) => params.map((p) => ({
759
853
  name: p.name,
760
854
  schemaName: zodResolver.resolveParamName(node, p)
@@ -766,13 +860,13 @@ const serverGenerator = defineGenerator({
766
860
  description: node.description || `Make a ${node.method.toUpperCase()} request to ${node.path}`
767
861
  },
768
862
  mcp: {
769
- name: resolver.resolveName(node.operationId),
863
+ name: resolver.resolveHandlerName(node),
770
864
  file: mcpFile
771
865
  },
772
866
  zod: {
773
867
  pathParams: resolveParams(pathParams),
774
- queryParams: queryParams.length ? resolveParams(queryParams) : void 0,
775
- headerParams: headerParams.length ? resolveParams(headerParams) : void 0,
868
+ queryParams: queryParams.length ? resolveParams(queryParams) : null,
869
+ headerParams: headerParams.length ? resolveParams(headerParams) : null,
776
870
  requestName,
777
871
  responseName,
778
872
  file: zodFile
@@ -787,7 +881,7 @@ const serverGenerator = defineGenerator({
787
881
  ...(zod.headerParams ?? []).map((p) => p.schemaName),
788
882
  zod.requestName,
789
883
  zod.responseName
790
- ].filter(Boolean);
884
+ ].filter((name) => Boolean(name));
791
885
  const uniqueNames = [...new Set(zodNames)].sort();
792
886
  return [/* @__PURE__ */ jsx(File.Import, {
793
887
  name: [mcp.name],
@@ -803,13 +897,21 @@ const serverGenerator = defineGenerator({
803
897
  baseName: serverFile.baseName,
804
898
  path: serverFile.path,
805
899
  meta: serverFile.meta,
806
- banner: resolver.resolveBanner(adapter.inputNode, {
900
+ banner: resolver.resolveBanner(ctx.meta, {
807
901
  output,
808
- config
902
+ config,
903
+ file: {
904
+ path: serverFile.path,
905
+ baseName: serverFile.baseName
906
+ }
809
907
  }),
810
- footer: resolver.resolveFooter(adapter.inputNode, {
908
+ footer: resolver.resolveFooter(ctx.meta, {
811
909
  output,
812
- config
910
+ config,
911
+ file: {
912
+ path: serverFile.path,
913
+ baseName: serverFile.baseName
914
+ }
813
915
  }),
814
916
  children: [
815
917
  /* @__PURE__ */ jsx(File.Import, {
@@ -827,8 +929,8 @@ const serverGenerator = defineGenerator({
827
929
  imports,
828
930
  /* @__PURE__ */ jsx(Server, {
829
931
  name,
830
- serverName: adapter.inputNode?.meta?.title ?? "server",
831
- serverVersion: adapter.inputNode?.meta?.version ?? "0.0.0",
932
+ serverName: ctx.meta.title ?? "server",
933
+ serverVersion: ctx.meta.version ?? "0.0.0",
832
934
  paramsCasing,
833
935
  operations: operationsMapped
834
936
  })
@@ -842,7 +944,7 @@ const serverGenerator = defineGenerator({
842
944
  children: `
843
945
  {
844
946
  "mcpServers": {
845
- "${adapter.inputNode?.meta?.title || "server"}": {
947
+ "${ctx.meta.title || "server"}": {
846
948
  "type": "stdio",
847
949
  "command": "npx",
848
950
  "args": ["tsx", "${path.relative(path.dirname(jsonFile.path), serverFile.path)}"]
@@ -857,14 +959,18 @@ const serverGenerator = defineGenerator({
857
959
  //#endregion
858
960
  //#region src/resolvers/resolverMcp.ts
859
961
  /**
860
- * Naming convention resolver for MCP plugin.
962
+ * Default resolver used by `@kubb/plugin-mcp`. Decides the names and file
963
+ * paths for every generated MCP tool handler. Function names get a `Handler`
964
+ * suffix so an operation `addPet` becomes `addPetHandler`.
861
965
  *
862
- * Provides default naming helpers using camelCase with a `handler` suffix for functions.
966
+ * @example Resolve a handler name
967
+ * ```ts
968
+ * import { resolverMcp } from '@kubb/plugin-mcp'
863
969
  *
864
- * @example
865
- * `resolverMcp.default('addPet', 'function') // → 'addPetHandler'`
970
+ * resolverMcp.default('addPet', 'function') // 'addPetHandler'
971
+ * ```
866
972
  */
867
- const resolverMcp = defineResolver((ctx) => ({
973
+ const resolverMcp = defineResolver(() => ({
868
974
  name: "default",
869
975
  pluginName: "plugin-mcp",
870
976
  default(name, type) {
@@ -872,12 +978,50 @@ const resolverMcp = defineResolver((ctx) => ({
872
978
  return camelCase(name, { suffix: "handler" });
873
979
  },
874
980
  resolveName(name) {
875
- return ctx.default(name, "function");
981
+ return this.default(name, "function");
982
+ },
983
+ resolvePathName(name, type) {
984
+ return this.default(name, type);
985
+ },
986
+ resolveHandlerName(node) {
987
+ return this.resolveName(node.operationId);
876
988
  }
877
989
  }));
878
990
  //#endregion
879
991
  //#region src/plugin.ts
992
+ /**
993
+ * Canonical plugin name for `@kubb/plugin-mcp`. Used for driver lookups and
994
+ * cross-plugin dependency references.
995
+ */
880
996
  const pluginMcpName = "plugin-mcp";
997
+ /**
998
+ * Generates a Model Context Protocol (MCP) server from an OpenAPI spec. Every
999
+ * operation becomes a typed MCP tool that AI assistants (Claude Desktop, Claude
1000
+ * Code, MCP-compatible clients) can call directly.
1001
+ *
1002
+ * @example
1003
+ * ```ts
1004
+ * import { defineConfig } from 'kubb'
1005
+ * import { pluginTs } from '@kubb/plugin-ts'
1006
+ * import { pluginClient } from '@kubb/plugin-client'
1007
+ * import { pluginZod } from '@kubb/plugin-zod'
1008
+ * import { pluginMcp } from '@kubb/plugin-mcp'
1009
+ *
1010
+ * export default defineConfig({
1011
+ * input: { path: './petStore.yaml' },
1012
+ * output: { path: './src/gen' },
1013
+ * plugins: [
1014
+ * pluginTs(),
1015
+ * pluginClient(),
1016
+ * pluginZod(),
1017
+ * pluginMcp({
1018
+ * output: { path: './mcp' },
1019
+ * client: { baseURL: 'https://petstore.swagger.io/v2' },
1020
+ * }),
1021
+ * ],
1022
+ * })
1023
+ * ```
1024
+ */
881
1025
  const pluginMcp = definePlugin((options) => {
882
1026
  const { output = {
883
1027
  path: "mcp",
@@ -885,13 +1029,7 @@ const pluginMcp = definePlugin((options) => {
885
1029
  }, group, exclude = [], include, override = [], paramsCasing, client, resolver: userResolver, transformer: userTransformer, generators: userGenerators = [] } = options;
886
1030
  const clientName = client?.client ?? "axios";
887
1031
  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;
1032
+ const groupConfig = createGroupConfig(group, { suffix: "Requests" });
895
1033
  return {
896
1034
  name: pluginMcpName,
897
1035
  options,
@@ -927,10 +1065,10 @@ const pluginMcp = definePlugin((options) => {
927
1065
  const root = path.resolve(ctx.config.root, ctx.config.output.path);
928
1066
  const hasClientPlugin = ctx.config.plugins?.some((p) => p.name === pluginClientName);
929
1067
  if (client?.bundle && !hasClientPlugin && !clientImportPath) ctx.injectFile({
930
- baseName: "fetch.ts",
931
- path: path.resolve(root, ".kubb/fetch.ts"),
1068
+ baseName: "client.ts",
1069
+ path: path.resolve(root, ".kubb/client.ts"),
932
1070
  sources: [ast.createSource({
933
- name: "fetch",
1071
+ name: "client",
934
1072
  nodes: [ast.createText(clientName === "fetch" ? source$1 : source)],
935
1073
  isExportable: true,
936
1074
  isIndexable: true