@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.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
- }
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;
340
338
  }
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(", ")} })`;
348
- }
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
+ * A user-provided `group.name` always wins over the default namer, so callers stay in
429
+ * control of their output folders. Returns `null` when grouping is disabled, matching the
430
+ * per-plugin convention.
431
+ *
432
+ * @param group - The user-supplied group option, or `undefined` to disable grouping.
433
+ * @param options.suffix - Appended to non-`path` group names, e.g. `'Controller'` or `'Requests'`.
434
+ *
435
+ * @example
436
+ * ```ts
437
+ * createGroupConfig(group, { suffix: 'Controller' }) // plugin-ts, plugin-client, …
438
+ * createGroupConfig(group, { suffix: 'Requests' }) // 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: 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,62 +568,80 @@ 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 }) {
514
- return /* @__PURE__ */ (0, _kubb_renderer_jsx_jsx_runtime.jsxs)(_kubb_renderer_jsx.File.Source, {
515
- name,
516
- isExportable: true,
517
- isIndexable: true,
518
- children: [
519
- /* @__PURE__ */ (0, _kubb_renderer_jsx_jsx_runtime.jsx)(_kubb_renderer_jsx.Const, {
520
- name: "server",
521
- export: true,
522
- children: `
523
- new McpServer({
524
- name: '${serverName}',
525
- version: '${serverVersion}',
526
- })
527
- `
528
- }),
529
- operations.map(({ tool, mcp, zod, node }) => {
530
- const pathParams = _kubb_core.ast.caseParams(node.parameters, paramsCasing).filter((p) => p.in === "path");
531
- const pathEntries = [];
532
- const otherEntries = [];
533
- for (const p of pathParams) {
534
- const zodParam = zod.pathParams.find((zp) => zp.name === p.name);
535
- pathEntries.push({
536
- key: p.name,
537
- value: zodParam ? zodParam.schemaName : zodExprFromSchemaNode(p.schema)
538
- });
539
- }
540
- if (zod.requestName) otherEntries.push({
541
- key: "data",
542
- value: zod.requestName
543
- });
544
- if (zod.queryParams) otherEntries.push({
545
- key: "params",
546
- value: zodGroupExpr(zod.queryParams)
547
- });
548
- if (zod.headerParams) otherEntries.push({
549
- key: "headers",
550
- value: zodGroupExpr(zod.headerParams)
551
- });
552
- otherEntries.sort((a, b) => a.key.localeCompare(b.key));
553
- const entries = [...pathEntries, ...otherEntries];
554
- const paramsNode = entries.length ? _kubb_core.ast.createFunctionParameters({ params: [_kubb_core.ast.createParameterGroup({ properties: entries.map((e) => _kubb_core.ast.createFunctionParameter({
555
- name: e.key,
556
- optional: false
557
- })) })] }) : void 0;
558
- const destructured = paramsNode ? keysPrinter.print(paramsNode) ?? "" : "";
559
- const inputSchema = entries.length ? `{ ${entries.map((e) => `${e.key}: ${e.value}`).join(", ")} }` : void 0;
560
- const outputSchema = zod.responseName;
561
- const config = [
562
- tool.title ? `title: ${JSON.stringify(tool.title)}` : null,
563
- `description: ${JSON.stringify(tool.description)}`,
564
- outputSchema ? `outputSchema: { data: ${outputSchema} }` : null
565
- ].filter(Boolean).join(",\n ");
566
- if (inputSchema) return `
607
+ const registrations = operations.map(({ tool, mcp, zod, node }) => {
608
+ const { path: pathParams } = getOperationParameters(node, { paramsCasing });
609
+ const pathEntries = [];
610
+ const otherEntries = [];
611
+ for (const p of pathParams) {
612
+ const zodParam = zod.pathParams.find((zp) => zp.name === p.name);
613
+ pathEntries.push({
614
+ key: p.name,
615
+ value: zodParam ? zodParam.schemaName : zodExprFromSchemaNode(p.schema)
616
+ });
617
+ }
618
+ if (zod.requestName) otherEntries.push({
619
+ key: "data",
620
+ value: zod.requestName
621
+ });
622
+ if (zod.queryParams) otherEntries.push({
623
+ key: "params",
624
+ value: zodGroupExpr(zod.queryParams)
625
+ });
626
+ if (zod.headerParams) otherEntries.push({
627
+ key: "headers",
628
+ value: zodGroupExpr(zod.headerParams)
629
+ });
630
+ otherEntries.sort((a, b) => a.key.localeCompare(b.key));
631
+ const entries = [...pathEntries, ...otherEntries];
632
+ const paramsNode = entries.length ? _kubb_core.ast.createFunctionParameters({ params: [_kubb_core.ast.createParameterGroup({ properties: entries.map((e) => _kubb_core.ast.createFunctionParameter({
633
+ name: e.key,
634
+ optional: false
635
+ })) })] }) : null;
636
+ const destructured = paramsNode ? keysPrinter.print(paramsNode) ?? "" : "";
637
+ const inputSchema = entries.length ? `{ ${entries.map((e) => `${e.key}: ${e.value}`).join(", ")} }` : null;
638
+ const outputSchema = zod.responseName;
639
+ const config = [
640
+ tool.title ? `title: ${JSON.stringify(tool.title)}` : null,
641
+ `description: ${JSON.stringify(tool.description)}`,
642
+ outputSchema ? `outputSchema: { data: ${outputSchema} }` : null
643
+ ].filter(Boolean).join(",\n ");
644
+ if (inputSchema) return `
567
645
  server.registerTool(${JSON.stringify(tool.name)}, {
568
646
  ${config},
569
647
  inputSchema: ${inputSchema},
@@ -571,14 +649,34 @@ server.registerTool(${JSON.stringify(tool.name)}, {
571
649
  return ${mcp.name}(${destructured}, request)
572
650
  })
573
651
  `;
574
- return `
652
+ return `
575
653
  server.registerTool(${JSON.stringify(tool.name)}, {
576
654
  ${config},
577
655
  }, async (request) => {
578
656
  return ${mcp.name}(request)
579
657
  })
580
658
  `;
581
- }).filter(Boolean),
659
+ }).filter(Boolean).join("\n");
660
+ return /* @__PURE__ */ (0, _kubb_renderer_jsx_jsx_runtime.jsxs)(_kubb_renderer_jsx.File.Source, {
661
+ name,
662
+ isExportable: true,
663
+ isIndexable: true,
664
+ children: [
665
+ /* @__PURE__ */ (0, _kubb_renderer_jsx_jsx_runtime.jsx)(_kubb_renderer_jsx.Function, {
666
+ name: "getServer",
667
+ export: true,
668
+ children: `const server = new McpServer({
669
+ name: '${serverName}',
670
+ version: '${serverVersion}',
671
+ })
672
+ ${registrations}
673
+ return server`
674
+ }),
675
+ /* @__PURE__ */ (0, _kubb_renderer_jsx_jsx_runtime.jsx)(_kubb_renderer_jsx.Const, {
676
+ name: "server",
677
+ export: true,
678
+ children: "getServer()"
679
+ }),
582
680
  /* @__PURE__ */ (0, _kubb_renderer_jsx_jsx_runtime.jsx)(_kubb_renderer_jsx.Function, {
583
681
  name: "startServer",
584
682
  async: true,
@@ -597,29 +695,28 @@ server.registerTool(${JSON.stringify(tool.name)}, {
597
695
  }
598
696
  //#endregion
599
697
  //#region src/generators/mcpGenerator.tsx
698
+ /**
699
+ * Built-in operation generator for `@kubb/plugin-mcp`. Emits one MCP tool
700
+ * handler per OpenAPI operation, wiring the input Zod schema, the HTTP call,
701
+ * and the response shape into a single function that an MCP server can
702
+ * register as a callable tool.
703
+ */
600
704
  const mcpGenerator = (0, _kubb_core.defineGenerator)({
601
705
  name: "mcp",
602
- renderer: _kubb_renderer_jsx.jsxRenderer,
706
+ renderer: _kubb_renderer_jsx.jsxRendererSync,
603
707
  operation(node, ctx) {
708
+ if (!_kubb_core.ast.isHttpOperationNode(node)) return null;
604
709
  const { resolver, driver, root } = ctx;
605
710
  const { output, client, paramsCasing, group } = ctx.options;
606
711
  const pluginTs = driver.getPlugin(_kubb_plugin_ts.pluginTsName);
607
712
  if (!pluginTs) return null;
608
713
  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);
714
+ const importedTypeNames = resolveOperationTypeNames(node, tsResolver, {
715
+ paramsCasing,
716
+ responseStatusNames: "error"
717
+ });
621
718
  const meta = {
622
- name: resolver.resolveName(node.operationId),
719
+ name: resolver.resolveHandlerName(node),
623
720
  file: resolver.resolveFile({
624
721
  name: node.operationId,
625
722
  extname: ".ts",
@@ -628,7 +725,7 @@ const mcpGenerator = (0, _kubb_core.defineGenerator)({
628
725
  }, {
629
726
  root,
630
727
  output,
631
- group
728
+ group: group ?? void 0
632
729
  }),
633
730
  fileTs: tsResolver.resolveFile({
634
731
  name: node.operationId,
@@ -638,7 +735,7 @@ const mcpGenerator = (0, _kubb_core.defineGenerator)({
638
735
  }, {
639
736
  root,
640
737
  output: pluginTs.options?.output ?? output,
641
- group: pluginTs.options?.group
738
+ group: pluginTs.options?.group ?? void 0
642
739
  })
643
740
  };
644
741
  return /* @__PURE__ */ (0, _kubb_renderer_jsx_jsx_runtime.jsxs)(_kubb_renderer_jsx.File, {
@@ -682,7 +779,7 @@ const mcpGenerator = (0, _kubb_core.defineGenerator)({
682
779
  isTypeOnly: true
683
780
  }),
684
781
  /* @__PURE__ */ (0, _kubb_renderer_jsx_jsx_runtime.jsx)(_kubb_renderer_jsx.File.Import, {
685
- name: "fetch",
782
+ name: "client",
686
783
  path: client.importPath
687
784
  }),
688
785
  client.dataReturnType === "full" && /* @__PURE__ */ (0, _kubb_renderer_jsx_jsx_runtime.jsx)(_kubb_renderer_jsx.File.Import, {
@@ -698,18 +795,18 @@ const mcpGenerator = (0, _kubb_core.defineGenerator)({
698
795
  "ResponseErrorConfig"
699
796
  ],
700
797
  root: meta.file.path,
701
- path: node_path.default.resolve(root, ".kubb/fetch.ts"),
798
+ path: node_path.default.resolve(root, ".kubb/client.ts"),
702
799
  isTypeOnly: true
703
800
  }),
704
801
  /* @__PURE__ */ (0, _kubb_renderer_jsx_jsx_runtime.jsx)(_kubb_renderer_jsx.File.Import, {
705
- name: ["fetch"],
802
+ name: ["client"],
706
803
  root: meta.file.path,
707
- path: node_path.default.resolve(root, ".kubb/fetch.ts")
804
+ path: node_path.default.resolve(root, ".kubb/client.ts")
708
805
  }),
709
806
  client.dataReturnType === "full" && /* @__PURE__ */ (0, _kubb_renderer_jsx_jsx_runtime.jsx)(_kubb_renderer_jsx.File.Import, {
710
807
  name: ["ResponseConfig"],
711
808
  root: meta.file.path,
712
- path: node_path.default.resolve(root, ".kubb/fetch.ts"),
809
+ path: node_path.default.resolve(root, ".kubb/client.ts"),
713
810
  isTypeOnly: true
714
811
  })
715
812
  ] }),
@@ -736,9 +833,9 @@ const mcpGenerator = (0, _kubb_core.defineGenerator)({
736
833
  */
737
834
  const serverGenerator = (0, _kubb_core.defineGenerator)({
738
835
  name: "operations",
739
- renderer: _kubb_renderer_jsx.jsxRenderer,
836
+ renderer: _kubb_renderer_jsx.jsxRendererSync,
740
837
  operations(nodes, ctx) {
741
- const { adapter, config, resolver, plugin, driver, root } = ctx;
838
+ const { config, resolver, plugin, driver, root } = ctx;
742
839
  const { output, paramsCasing, group } = ctx.options;
743
840
  const pluginZod = driver.getPlugin(_kubb_plugin_zod.pluginZodName);
744
841
  if (!pluginZod) return;
@@ -754,11 +851,8 @@ const serverGenerator = (0, _kubb_core.defineGenerator)({
754
851
  path: node_path.default.resolve(root, output.path, ".mcp.json"),
755
852
  meta: { pluginName: plugin.name }
756
853
  };
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");
854
+ const operationsMapped = nodes.filter(_kubb_core.ast.isHttpOperationNode).map((node) => {
855
+ const { path: pathParams, query: queryParams, header: headerParams } = getOperationParameters(node, { paramsCasing });
762
856
  const mcpFile = resolver.resolveFile({
763
857
  name: node.operationId,
764
858
  extname: ".ts",
@@ -767,7 +861,7 @@ const serverGenerator = (0, _kubb_core.defineGenerator)({
767
861
  }, {
768
862
  root,
769
863
  output,
770
- group
864
+ group: group ?? void 0
771
865
  });
772
866
  const zodFile = zodResolver.resolveFile({
773
867
  name: node.operationId,
@@ -777,11 +871,11 @@ const serverGenerator = (0, _kubb_core.defineGenerator)({
777
871
  }, {
778
872
  root,
779
873
  output: pluginZod.options?.output ?? output,
780
- group: pluginZod.options?.group
874
+ group: pluginZod.options?.group ?? void 0
781
875
  });
782
- const requestName = node.requestBody?.content?.[0]?.schema ? zodResolver.resolveDataName(node) : void 0;
876
+ const requestName = node.requestBody?.content?.[0]?.schema ? zodResolver.resolveDataName(node) : null;
783
877
  const successStatus = findSuccessStatusCode(node.responses);
784
- const responseName = successStatus ? zodResolver.resolveResponseStatusName(node, successStatus) : void 0;
878
+ const responseName = successStatus ? zodResolver.resolveResponseStatusName(node, successStatus) : null;
785
879
  const resolveParams = (params) => params.map((p) => ({
786
880
  name: p.name,
787
881
  schemaName: zodResolver.resolveParamName(node, p)
@@ -793,13 +887,13 @@ const serverGenerator = (0, _kubb_core.defineGenerator)({
793
887
  description: node.description || `Make a ${node.method.toUpperCase()} request to ${node.path}`
794
888
  },
795
889
  mcp: {
796
- name: resolver.resolveName(node.operationId),
890
+ name: resolver.resolveHandlerName(node),
797
891
  file: mcpFile
798
892
  },
799
893
  zod: {
800
894
  pathParams: resolveParams(pathParams),
801
- queryParams: queryParams.length ? resolveParams(queryParams) : void 0,
802
- headerParams: headerParams.length ? resolveParams(headerParams) : void 0,
895
+ queryParams: queryParams.length ? resolveParams(queryParams) : null,
896
+ headerParams: headerParams.length ? resolveParams(headerParams) : null,
803
897
  requestName,
804
898
  responseName,
805
899
  file: zodFile
@@ -814,7 +908,7 @@ const serverGenerator = (0, _kubb_core.defineGenerator)({
814
908
  ...(zod.headerParams ?? []).map((p) => p.schemaName),
815
909
  zod.requestName,
816
910
  zod.responseName
817
- ].filter(Boolean);
911
+ ].filter((name) => Boolean(name));
818
912
  const uniqueNames = [...new Set(zodNames)].sort();
819
913
  return [/* @__PURE__ */ (0, _kubb_renderer_jsx_jsx_runtime.jsx)(_kubb_renderer_jsx.File.Import, {
820
914
  name: [mcp.name],
@@ -830,13 +924,21 @@ const serverGenerator = (0, _kubb_core.defineGenerator)({
830
924
  baseName: serverFile.baseName,
831
925
  path: serverFile.path,
832
926
  meta: serverFile.meta,
833
- banner: resolver.resolveBanner(adapter.inputNode, {
927
+ banner: resolver.resolveBanner(ctx.meta, {
834
928
  output,
835
- config
929
+ config,
930
+ file: {
931
+ path: serverFile.path,
932
+ baseName: serverFile.baseName
933
+ }
836
934
  }),
837
- footer: resolver.resolveFooter(adapter.inputNode, {
935
+ footer: resolver.resolveFooter(ctx.meta, {
838
936
  output,
839
- config
937
+ config,
938
+ file: {
939
+ path: serverFile.path,
940
+ baseName: serverFile.baseName
941
+ }
840
942
  }),
841
943
  children: [
842
944
  /* @__PURE__ */ (0, _kubb_renderer_jsx_jsx_runtime.jsx)(_kubb_renderer_jsx.File.Import, {
@@ -854,8 +956,8 @@ const serverGenerator = (0, _kubb_core.defineGenerator)({
854
956
  imports,
855
957
  /* @__PURE__ */ (0, _kubb_renderer_jsx_jsx_runtime.jsx)(Server, {
856
958
  name,
857
- serverName: adapter.inputNode?.meta?.title ?? "server",
858
- serverVersion: adapter.inputNode?.meta?.version ?? "0.0.0",
959
+ serverName: ctx.meta.title ?? "server",
960
+ serverVersion: ctx.meta.version ?? "0.0.0",
859
961
  paramsCasing,
860
962
  operations: operationsMapped
861
963
  })
@@ -869,7 +971,7 @@ const serverGenerator = (0, _kubb_core.defineGenerator)({
869
971
  children: `
870
972
  {
871
973
  "mcpServers": {
872
- "${adapter.inputNode?.meta?.title || "server"}": {
974
+ "${ctx.meta.title || "server"}": {
873
975
  "type": "stdio",
874
976
  "command": "npx",
875
977
  "args": ["tsx", "${node_path.default.relative(node_path.default.dirname(jsonFile.path), serverFile.path)}"]
@@ -884,14 +986,18 @@ const serverGenerator = (0, _kubb_core.defineGenerator)({
884
986
  //#endregion
885
987
  //#region src/resolvers/resolverMcp.ts
886
988
  /**
887
- * Naming convention resolver for MCP plugin.
989
+ * Default resolver used by `@kubb/plugin-mcp`. Decides the names and file
990
+ * paths for every generated MCP tool handler. Function names get a `Handler`
991
+ * suffix so an operation `addPet` becomes `addPetHandler`.
888
992
  *
889
- * Provides default naming helpers using camelCase with a `handler` suffix for functions.
993
+ * @example Resolve a handler name
994
+ * ```ts
995
+ * import { resolverMcp } from '@kubb/plugin-mcp'
890
996
  *
891
- * @example
892
- * `resolverMcp.default('addPet', 'function') // → 'addPetHandler'`
997
+ * resolverMcp.default('addPet', 'function') // 'addPetHandler'
998
+ * ```
893
999
  */
894
- const resolverMcp = (0, _kubb_core.defineResolver)((ctx) => ({
1000
+ const resolverMcp = (0, _kubb_core.defineResolver)(() => ({
895
1001
  name: "default",
896
1002
  pluginName: "plugin-mcp",
897
1003
  default(name, type) {
@@ -899,12 +1005,50 @@ const resolverMcp = (0, _kubb_core.defineResolver)((ctx) => ({
899
1005
  return camelCase(name, { suffix: "handler" });
900
1006
  },
901
1007
  resolveName(name) {
902
- return ctx.default(name, "function");
1008
+ return this.default(name, "function");
1009
+ },
1010
+ resolvePathName(name, type) {
1011
+ return this.default(name, type);
1012
+ },
1013
+ resolveHandlerName(node) {
1014
+ return this.resolveName(node.operationId);
903
1015
  }
904
1016
  }));
905
1017
  //#endregion
906
1018
  //#region src/plugin.ts
1019
+ /**
1020
+ * Canonical plugin name for `@kubb/plugin-mcp`. Used for driver lookups and
1021
+ * cross-plugin dependency references.
1022
+ */
907
1023
  const pluginMcpName = "plugin-mcp";
1024
+ /**
1025
+ * Generates a Model Context Protocol (MCP) server from an OpenAPI spec. Every
1026
+ * operation becomes a typed MCP tool that AI assistants (Claude Desktop, Claude
1027
+ * Code, MCP-compatible clients) can call directly.
1028
+ *
1029
+ * @example
1030
+ * ```ts
1031
+ * import { defineConfig } from 'kubb'
1032
+ * import { pluginTs } from '@kubb/plugin-ts'
1033
+ * import { pluginClient } from '@kubb/plugin-client'
1034
+ * import { pluginZod } from '@kubb/plugin-zod'
1035
+ * import { pluginMcp } from '@kubb/plugin-mcp'
1036
+ *
1037
+ * export default defineConfig({
1038
+ * input: { path: './petStore.yaml' },
1039
+ * output: { path: './src/gen' },
1040
+ * plugins: [
1041
+ * pluginTs(),
1042
+ * pluginClient(),
1043
+ * pluginZod(),
1044
+ * pluginMcp({
1045
+ * output: { path: './mcp' },
1046
+ * client: { baseURL: 'https://petstore.swagger.io/v2' },
1047
+ * }),
1048
+ * ],
1049
+ * })
1050
+ * ```
1051
+ */
908
1052
  const pluginMcp = (0, _kubb_core.definePlugin)((options) => {
909
1053
  const { output = {
910
1054
  path: "mcp",
@@ -912,13 +1056,7 @@ const pluginMcp = (0, _kubb_core.definePlugin)((options) => {
912
1056
  }, group, exclude = [], include, override = [], paramsCasing, client, resolver: userResolver, transformer: userTransformer, generators: userGenerators = [] } = options;
913
1057
  const clientName = client?.client ?? "axios";
914
1058
  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;
1059
+ const groupConfig = createGroupConfig(group, { suffix: "Requests" });
922
1060
  return {
923
1061
  name: pluginMcpName,
924
1062
  options,
@@ -954,10 +1092,10 @@ const pluginMcp = (0, _kubb_core.definePlugin)((options) => {
954
1092
  const root = node_path$1.default.resolve(ctx.config.root, ctx.config.output.path);
955
1093
  const hasClientPlugin = ctx.config.plugins?.some((p) => p.name === _kubb_plugin_client.pluginClientName);
956
1094
  if (client?.bundle && !hasClientPlugin && !clientImportPath) ctx.injectFile({
957
- baseName: "fetch.ts",
958
- path: node_path$1.default.resolve(root, ".kubb/fetch.ts"),
1095
+ baseName: "client.ts",
1096
+ path: node_path$1.default.resolve(root, ".kubb/client.ts"),
959
1097
  sources: [_kubb_core.ast.createSource({
960
- name: "fetch",
1098
+ name: "client",
961
1099
  nodes: [_kubb_core.ast.createText(clientName === "fetch" ? _kubb_plugin_client_templates_clients_fetch_source.source : _kubb_plugin_client_templates_clients_axios_source.source)],
962
1100
  isExportable: true,
963
1101
  isIndexable: true