@kubb/plugin-mcp 5.0.0-beta.64 → 5.0.0-beta.73

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
@@ -25,18 +25,12 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
25
25
  }) : target, mod));
26
26
  //#endregion
27
27
  let node_path = require("node:path");
28
- let node_path$1 = __toESM(node_path, 1);
29
- node_path = __toESM(node_path);
28
+ node_path = __toESM(node_path, 1);
30
29
  let _kubb_core = require("@kubb/core");
31
- let _kubb_ast_utils = require("@kubb/ast/utils");
32
- let _kubb_plugin_ts = require("@kubb/plugin-ts");
33
30
  let _kubb_renderer_jsx = require("@kubb/renderer-jsx");
34
31
  let _kubb_renderer_jsx_jsx_runtime = require("@kubb/renderer-jsx/jsx-runtime");
32
+ let _kubb_plugin_ts = require("@kubb/plugin-ts");
35
33
  let _kubb_plugin_zod = require("@kubb/plugin-zod");
36
- let _kubb_plugin_client = require("@kubb/plugin-client");
37
- let _kubb_plugin_client_templates_clients_axios_source = require("@kubb/plugin-client/templates/clients/axios.source");
38
- let _kubb_plugin_client_templates_clients_fetch_source = require("@kubb/plugin-client/templates/clients/fetch.source");
39
- let _kubb_plugin_client_templates_config_source = require("@kubb/plugin-client/templates/config.source");
40
34
  //#region ../../internals/utils/src/casing.ts
41
35
  /**
42
36
  * Shared implementation for camelCase and PascalCase conversion.
@@ -249,6 +243,22 @@ var Url = class Url {
249
243
  return `\`${prefix ?? ""}${result}\``;
250
244
  }
251
245
  /**
246
+ * Converts an OpenAPI/Swagger path to a template literal that reads each parameter off the
247
+ * grouped `path` request option, e.g. `/pet/{petId}` becomes `` `/pet/${path.petId}` ``. Parameter
248
+ * names are camelCased to match the generated `path` type, and `prefix` is prepended inside the
249
+ * literal. Shared by the client and cypress generators that pass a grouped `path` object.
250
+ *
251
+ * @example
252
+ * Url.toGroupedTemplateString('/pet/{petId}') // '`/pet/${path.petId}`'
253
+ */
254
+ static toGroupedTemplateString(path, { prefix } = {}) {
255
+ return Url.toTemplateString(path, {
256
+ prefix,
257
+ casing: "camelcase",
258
+ replacer: (name) => `path.${name}`
259
+ });
260
+ }
261
+ /**
252
262
  * Returns the path and its extracted params as a structured `URLObject`, or as a stringified
253
263
  * expression when `stringify` is set.
254
264
  *
@@ -276,13 +286,121 @@ var Url = class Url {
276
286
  }
277
287
  };
278
288
  //#endregion
289
+ //#region ../../internals/shared/src/params.ts
290
+ const caseParamsCache = /* @__PURE__ */ new WeakMap();
291
+ /**
292
+ * Applies camelCase to parameter names and returns a new array without mutating the input.
293
+ *
294
+ * Run it before handing parameters to schema builders so output property keys get the right casing
295
+ * while `OperationNode.parameters` stays intact for other consumers. When `casing` is unset, the
296
+ * original array is returned unchanged. Results are cached per input array.
297
+ */
298
+ function caseParams(params, casing) {
299
+ if (!casing) return params;
300
+ const cached = caseParamsCache.get(params);
301
+ if (cached) return cached;
302
+ const result = params.map((param) => ({
303
+ ...param,
304
+ name: camelCase(param.name)
305
+ }));
306
+ caseParamsCache.set(params, result);
307
+ return result;
308
+ }
309
+ //#endregion
279
310
  //#region ../../internals/shared/src/operation.ts
311
+ /**
312
+ * Builds the `ResolverFileParams` every operation generator passes to
313
+ * `resolver.resolveFile`: a file named `name`, tagged by the operation's first
314
+ * tag (or `'default'`), at the operation's path. Centralizes the entry object
315
+ * that was repeated at dozens of call sites across the client and query plugins.
316
+ *
317
+ * @example
318
+ * ```ts
319
+ * resolver.resolveFile(operationFileEntry(node, node.operationId), { root, output, group })
320
+ * ```
321
+ */
322
+ function operationFileEntry(node, name, extname = ".ts") {
323
+ return {
324
+ name,
325
+ extname,
326
+ tag: node.tags[0] ?? "default",
327
+ path: node.path
328
+ };
329
+ }
280
330
  function getOperationLink(node, link) {
281
331
  if (!link) return null;
282
332
  if (typeof link === "function") return link(node) ?? null;
283
333
  if (link === "urlPath") return node.path ? `{@link ${Url.toPath(node.path)}}` : null;
284
334
  return node.path ? `{@link ${node.path.replaceAll("{", ":").replaceAll("}", "")}}` : null;
285
335
  }
336
+ function getContentTypeInfo(node) {
337
+ const contentTypes = node.requestBody?.content?.map((e) => e.contentType) ?? [];
338
+ const isMultipleContentTypes = contentTypes.length > 1;
339
+ return {
340
+ contentTypes,
341
+ isMultipleContentTypes,
342
+ contentTypeUnion: isMultipleContentTypes ? contentTypes.map((ct) => JSON.stringify(ct)).join(" | ") : "",
343
+ defaultContentType: contentTypes[0] ?? "application/json",
344
+ hasFormData: contentTypes.some((ct) => ct === "multipart/form-data")
345
+ };
346
+ }
347
+ function buildRequestConfigType(node) {
348
+ const { isMultipleContentTypes, contentTypeUnion } = getContentTypeInfo(node);
349
+ const configType = `Partial<Omit<RequestConfig, 'path' | 'query' | 'body' | 'headers' | 'url'>>`;
350
+ const contentTypeProp = isMultipleContentTypes ? `contentType?: ${contentTypeUnion}` : null;
351
+ return contentTypeProp ? `${configType} & { ${contentTypeProp} }` : configType;
352
+ }
353
+ /**
354
+ * Which of the grouped request options an operation carries.
355
+ */
356
+ function getRequestGroups(node) {
357
+ const { path, query, header } = getOperationParameters(node);
358
+ return {
359
+ path: path.length > 0,
360
+ query: query.length > 0,
361
+ body: Boolean(node.requestBody?.content?.[0]?.schema),
362
+ headers: header.length > 0
363
+ };
364
+ }
365
+ /**
366
+ * Resolves which grouped request options an operation carries together with whether each group
367
+ * holds a required member. The grouped parameter stays optional only when nothing inside it is
368
+ * required, matching the generated `RequestConfig` type.
369
+ */
370
+ function getRequestGroupOptionality(node) {
371
+ const groups = getRequestGroups(node);
372
+ const { path, query, header } = getOperationParameters(node);
373
+ const hasRequiredPath = path.some((param) => param.required);
374
+ const hasRequiredQuery = query.some((param) => param.required);
375
+ const hasRequiredHeader = header.some((param) => param.required);
376
+ return {
377
+ groups,
378
+ hasRequiredPath,
379
+ hasRequiredQuery,
380
+ hasRequiredHeader,
381
+ isOptional: !hasRequiredPath && !hasRequiredQuery && !hasRequiredHeader && !groups.body
382
+ };
383
+ }
384
+ /**
385
+ * Builds the grouped `{ path, query, body, headers }` parameter for a generated client
386
+ * function, typed from the operation's `RequestConfig` (minus `url`). Only the groups the
387
+ * operation actually has are destructured. The trailing `config` parameter carries the
388
+ * runtime `RequestConfig` overrides plus `client`.
389
+ */
390
+ function buildRequestParamsSignature(node, resolver, options = {}) {
391
+ const { isConfigurable = true } = options;
392
+ const { groups, isOptional } = getRequestGroupOptionality(node);
393
+ const names = [
394
+ "path",
395
+ "query",
396
+ "body",
397
+ "headers"
398
+ ].filter((key) => groups[key]);
399
+ return {
400
+ signature: [names.length > 0 ? `{ ${names.join(", ")} }: ${resolver.resolveRequestConfigName(node)}${isOptional ? " = {}" : ""}` : null, isConfigurable ? `config: ${buildRequestConfigType(node)} = {}` : null].filter(Boolean).join(", "),
401
+ groups
402
+ };
403
+ }
286
404
  function buildOperationComments(node, options = {}) {
287
405
  const { link = "pathTemplate", linkPosition = "afterDeprecated", splitLines = false } = options;
288
406
  const linkComment = getOperationLink(node, link);
@@ -301,7 +419,7 @@ function buildOperationComments(node, options = {}) {
301
419
  return filteredComments.flatMap((text) => text.split(/\r?\n/).map((line) => line.trim())).filter((comment) => Boolean(comment));
302
420
  }
303
421
  function getOperationParameters(node, options = {}) {
304
- const params = (0, _kubb_ast_utils.caseParams)(node.parameters, options.paramsCasing);
422
+ const params = caseParams(node.parameters, options.paramsCasing === "original" ? void 0 : "camelcase");
305
423
  return {
306
424
  path: params.filter((param) => param.in === "path"),
307
425
  query: params.filter((param) => param.in === "query"),
@@ -317,48 +435,6 @@ function isSuccessStatusCode(statusCode) {
317
435
  const code = getStatusCodeNumber(statusCode);
318
436
  return code !== null && code >= 200 && code < 300;
319
437
  }
320
- function isErrorStatusCode(statusCode) {
321
- const code = getStatusCodeNumber(statusCode);
322
- return code !== null && code >= 400;
323
- }
324
- function resolveErrorNames(node, resolver) {
325
- return node.responses.filter((response) => isErrorStatusCode(response.statusCode)).map((response) => resolver.resolveResponseStatusName(node, response.statusCode));
326
- }
327
- function resolveStatusCodeNames(node, resolver) {
328
- return node.responses.map((response) => resolver.resolveResponseStatusName(node, response.statusCode));
329
- }
330
- const typeNamesByResolver = /* @__PURE__ */ new WeakMap();
331
- function resolveOperationTypeNames(node, resolver, options = {}) {
332
- const cacheKey = `${node.operationId}\0${options.paramsCasing ?? ""}\0${options.order ?? ""}\0${options.responseStatusNames ?? ""}\0${(options.exclude ?? []).join(",")}`;
333
- let byResolver = typeNamesByResolver.get(resolver);
334
- if (byResolver) {
335
- const cached = byResolver.get(cacheKey);
336
- if (cached) return cached;
337
- } else {
338
- byResolver = /* @__PURE__ */ new Map();
339
- typeNamesByResolver.set(resolver, byResolver);
340
- }
341
- const { path, query, header } = getOperationParameters(node, { paramsCasing: options.paramsCasing });
342
- const responseStatusNames = options.responseStatusNames === "error" ? resolveErrorNames(node, resolver) : options.responseStatusNames === false ? [] : resolveStatusCodeNames(node, resolver);
343
- const exclude = new Set(options.exclude ?? []);
344
- const paramNames = [
345
- ...path.map((param) => resolver.resolvePathParamsName(node, param)),
346
- ...query.map((param) => resolver.resolveQueryParamsName(node, param)),
347
- ...header.map((param) => resolver.resolveHeaderParamsName(node, param))
348
- ];
349
- const bodyAndResponseNames = [node.requestBody?.content?.[0]?.schema ? resolver.resolveDataName(node) : null, resolver.resolveResponseName(node)];
350
- const result = (options.order === "body-response-first" ? [
351
- ...bodyAndResponseNames,
352
- ...paramNames,
353
- ...responseStatusNames
354
- ] : [
355
- ...paramNames,
356
- ...bodyAndResponseNames,
357
- ...responseStatusNames
358
- ]).filter((name) => Boolean(name) && !exclude.has(name));
359
- byResolver.set(cacheKey, result);
360
- return result;
361
- }
362
438
  function findSuccessStatusCode(responses) {
363
439
  for (const response of responses) if (isSuccessStatusCode(response.statusCode)) return response.statusCode;
364
440
  return null;
@@ -395,76 +471,19 @@ function createGroupConfig(group) {
395
471
  };
396
472
  }
397
473
  //#endregion
398
- //#region ../../internals/shared/src/params.ts
399
- function buildParamsMapping(originalParams, mappedParams) {
400
- const mapping = {};
401
- let hasChanged = false;
402
- originalParams.forEach((param, i) => {
403
- const mappedName = mappedParams[i]?.name ?? param.name;
404
- mapping[param.name] = mappedName;
405
- if (param.name !== mappedName) hasChanged = true;
406
- });
407
- return hasChanged ? mapping : null;
408
- }
409
- function buildTransformedParamsMapping(params, transformName) {
410
- if (!params.length) return null;
411
- return buildParamsMapping(params, params.map((param) => ({
412
- ...param,
413
- name: transformName(param.name)
414
- })));
415
- }
416
- //#endregion
417
474
  //#region src/components/McpHandler.tsx
418
- /**
419
- * Generate a remapping statement: `const mappedX = x ? { "orig": x.camel, ... } : undefined`
420
- */
421
- function buildRemappingCode(mapping, varName, sourceName) {
422
- return `const ${varName} = ${sourceName} ? { ${Object.entries(mapping).map(([orig, camel]) => `"${orig}": ${sourceName}.${camel}`).join(", ")} } : undefined`;
423
- }
424
- const declarationPrinter = (0, _kubb_plugin_ts.functionPrinter)({ mode: "declaration" });
425
- function McpHandler({ name, node, resolver, baseURL, dataReturnType, paramsCasing }) {
475
+ const requestGroupOrder = [
476
+ "path",
477
+ "query",
478
+ "headers",
479
+ "body"
480
+ ];
481
+ function McpHandler({ name, clientName, node, resolver }) {
426
482
  if (!_kubb_core.ast.isHttpOperationNode(node)) return null;
427
- const contentType = node.requestBody?.content?.[0]?.contentType;
428
- const isFormData = contentType === "multipart/form-data";
429
- const { query: queryParams, header: headerParams } = getOperationParameters(node, { paramsCasing });
430
- const { path: originalPathParams, query: originalQueryParams, header: originalHeaderParams } = getOperationParameters(node);
431
- const requestName = node.requestBody?.content?.[0]?.schema ? resolver.resolveDataName(node) : null;
432
- const responseName = resolver.resolveResponseName(node);
433
- const errorResponses = node.responses.filter((r) => Number(r.statusCode) >= 400).map((r) => resolver.resolveResponseStatusName(node, r.statusCode));
434
- const generics = [
435
- responseName,
436
- `ResponseErrorConfig<${errorResponses.length > 0 ? errorResponses.join(" | ") : "Error"}>`,
437
- requestName || "unknown"
438
- ].filter(Boolean);
439
- const paramsNode = (0, _kubb_ast_utils.createOperationParams)(node, {
440
- paramsType: "object",
441
- pathParamsType: "inline",
442
- resolver,
443
- paramsCasing
444
- });
445
- const baseParamsSignature = declarationPrinter.print(paramsNode) ?? "";
446
- const paramsSignature = baseParamsSignature ? `${baseParamsSignature}, request: RequestHandlerExtra<ServerRequest, ServerNotification>` : "request: RequestHandlerExtra<ServerRequest, ServerNotification>";
447
- const pathParamsMapping = paramsCasing ? buildTransformedParamsMapping(originalPathParams, camelCase) : null;
448
- const queryParamsMapping = paramsCasing ? buildTransformedParamsMapping(originalQueryParams, camelCase) : null;
449
- const headerParamsMapping = paramsCasing ? buildTransformedParamsMapping(originalHeaderParams, camelCase) : null;
450
- const contentTypeHeader = contentType && contentType !== "application/json" && contentType !== "multipart/form-data" ? `'Content-Type': '${contentType}'` : null;
451
- const headers = [headerParams.length ? headerParamsMapping ? "...mappedHeaders" : "...headers" : null, contentTypeHeader].filter(Boolean);
452
- const fetchConfig = [];
453
- fetchConfig.push(`method: ${JSON.stringify(node.method.toUpperCase())}`);
454
- fetchConfig.push(`url: ${Url.toTemplateString(node.path)}`);
455
- if (baseURL) fetchConfig.push(`baseURL: \`${baseURL}\``);
456
- if (queryParams.length) fetchConfig.push(queryParamsMapping ? "params: mappedParams" : "params");
457
- if (requestName) fetchConfig.push(`data: ${isFormData ? "formData as FormData" : "requestData"}`);
458
- if (headers.length) fetchConfig.push(`headers: { ${headers.join(", ")} }`);
459
- const callToolResult = `return {
460
- content: [
461
- {
462
- type: 'text',
463
- text: JSON.stringify(${dataReturnType === "data" ? "res.data" : "res"})
464
- }
465
- ],
466
- structuredContent: { data: res.data }
467
- }`;
483
+ const { signature, groups } = buildRequestParamsSignature(node, resolver, { isConfigurable: false });
484
+ const paramsSignature = [signature, "request: RequestHandlerExtra<ServerRequest, ServerNotification>"].filter(Boolean).join(", ");
485
+ const callArgs = requestGroupOrder.filter((key) => groups[key]);
486
+ const callConfig = callArgs.length ? `{ ${callArgs.join(", ")} }` : "{}";
468
487
  return /* @__PURE__ */ (0, _kubb_renderer_jsx_jsx_runtime.jsx)(_kubb_renderer_jsx.File.Source, {
469
488
  name,
470
489
  isExportable: true,
@@ -477,28 +496,17 @@ function McpHandler({ name, node, resolver, baseURL, dataReturnType, paramsCasin
477
496
  JSDoc: { comments: buildOperationComments(node) },
478
497
  returnType: "Promise<CallToolResult>",
479
498
  children: [
480
- "",
481
- /* @__PURE__ */ (0, _kubb_renderer_jsx_jsx_runtime.jsx)("br", {}),
482
- /* @__PURE__ */ (0, _kubb_renderer_jsx_jsx_runtime.jsx)("br", {}),
483
- pathParamsMapping && Object.entries(pathParamsMapping).filter(([originalName, camelCaseName]) => originalName !== camelCaseName && isValidVarName(originalName)).map(([originalName, camelCaseName]) => `const ${originalName} = ${camelCaseName}`).join("\n"),
484
- pathParamsMapping && /* @__PURE__ */ (0, _kubb_renderer_jsx_jsx_runtime.jsxs)(_kubb_renderer_jsx_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, _kubb_renderer_jsx_jsx_runtime.jsx)("br", {}), /* @__PURE__ */ (0, _kubb_renderer_jsx_jsx_runtime.jsx)("br", {})] }),
485
- queryParamsMapping && queryParams.length > 0 && /* @__PURE__ */ (0, _kubb_renderer_jsx_jsx_runtime.jsxs)(_kubb_renderer_jsx_jsx_runtime.Fragment, { children: [
486
- buildRemappingCode(queryParamsMapping, "mappedParams", "params"),
487
- /* @__PURE__ */ (0, _kubb_renderer_jsx_jsx_runtime.jsx)("br", {}),
488
- /* @__PURE__ */ (0, _kubb_renderer_jsx_jsx_runtime.jsx)("br", {})
489
- ] }),
490
- headerParamsMapping && headerParams.length > 0 && /* @__PURE__ */ (0, _kubb_renderer_jsx_jsx_runtime.jsxs)(_kubb_renderer_jsx_jsx_runtime.Fragment, { children: [
491
- buildRemappingCode(headerParamsMapping, "mappedHeaders", "headers"),
492
- /* @__PURE__ */ (0, _kubb_renderer_jsx_jsx_runtime.jsx)("br", {}),
493
- /* @__PURE__ */ (0, _kubb_renderer_jsx_jsx_runtime.jsx)("br", {})
494
- ] }),
495
- requestName && "const requestData = data",
496
- /* @__PURE__ */ (0, _kubb_renderer_jsx_jsx_runtime.jsx)("br", {}),
497
- isFormData && requestName && "const formData = buildFormData(requestData)",
499
+ `const res = await ${clientName}(${callConfig})`,
498
500
  /* @__PURE__ */ (0, _kubb_renderer_jsx_jsx_runtime.jsx)("br", {}),
499
- `const res = await client<${generics.join(", ")}>({ ${fetchConfig.join(", ")} }, request)`,
500
- /* @__PURE__ */ (0, _kubb_renderer_jsx_jsx_runtime.jsx)("br", {}),
501
- callToolResult
501
+ `return {
502
+ content: [
503
+ {
504
+ type: 'text',
505
+ text: JSON.stringify(res.data)
506
+ }
507
+ ],
508
+ structuredContent: { data: res.data }
509
+ }`
502
510
  ]
503
511
  })
504
512
  });
@@ -539,37 +547,36 @@ function zodExprFromSchemaNode(schema) {
539
547
  //#endregion
540
548
  //#region src/components/Server.tsx
541
549
  const keysPrinter = (0, _kubb_plugin_ts.functionPrinter)({ mode: "call" });
542
- function Server({ name, serverName, serverVersion, paramsCasing, operations }) {
550
+ function Server({ name, serverName, serverVersion, operations }) {
543
551
  const registrations = operations.map(({ tool, mcp, zod, node }) => {
544
- const { path: pathParams } = getOperationParameters(node, { paramsCasing });
545
- const pathEntries = [];
546
- const otherEntries = [];
547
- for (const p of pathParams) {
548
- const zodParam = zod.pathParams.find((zp) => zp.name === p.name);
549
- pathEntries.push({
550
- key: p.name,
551
- value: zodParam ? zodParam.schemaName : zodExprFromSchemaNode(p.schema)
552
+ const { path: pathParams } = getOperationParameters(node);
553
+ const entries = [];
554
+ if (pathParams.length) {
555
+ const pathFields = pathParams.map((p) => {
556
+ const zodParam = zod.pathParams.find((zp) => zp.name === p.name);
557
+ return `${JSON.stringify(p.name)}: ${zodParam ? zodParam.schemaName : zodExprFromSchemaNode(p.schema)}`;
558
+ }).join(", ");
559
+ entries.push({
560
+ key: "path",
561
+ value: `z.object({ ${pathFields} })`
552
562
  });
553
563
  }
554
- if (zod.requestName) otherEntries.push({
555
- key: "data",
556
- value: zod.requestName
557
- });
558
- if (zod.queryParams) otherEntries.push({
559
- key: "params",
564
+ if (zod.queryParams) entries.push({
565
+ key: "query",
560
566
  value: zodGroupExpr(zod.queryParams)
561
567
  });
562
- if (zod.headerParams) otherEntries.push({
568
+ if (zod.headerParams) entries.push({
563
569
  key: "headers",
564
570
  value: zodGroupExpr(zod.headerParams)
565
571
  });
566
- otherEntries.sort((a, b) => a.key.localeCompare(b.key));
567
- const entries = [...pathEntries, ...otherEntries];
568
- const paramsNode = entries.length ? _kubb_core.ast.factory.createFunctionParameters({ params: [{
569
- kind: "FunctionParameter",
570
- name: _kubb_core.ast.factory.createObjectBindingPattern({ elements: entries.map((e) => ({ name: e.key })) }),
572
+ if (zod.requestName) entries.push({
573
+ key: "body",
574
+ value: zod.requestName
575
+ });
576
+ const paramsNode = entries.length ? (0, _kubb_plugin_ts.createFunctionParameters)({ params: [(0, _kubb_plugin_ts.createFunctionParameter)({
577
+ name: (0, _kubb_plugin_ts.createObjectBindingPattern)({ elements: entries.map((e) => ({ name: e.key })) }),
571
578
  optional: false
572
- }] }) : null;
579
+ })] }) : null;
573
580
  const destructured = paramsNode ? keysPrinter.print(paramsNode) ?? "" : "";
574
581
  const inputSchema = entries.length ? `{ ${entries.map((e) => `${e.key}: ${e.value}`).join(", ")} }` : null;
575
582
  const outputSchema = zod.responseName;
@@ -631,12 +638,105 @@ return server`
631
638
  });
632
639
  }
633
640
  //#endregion
641
+ //#region ../../internals/client/src/resolveClient.ts
642
+ /**
643
+ * Resolves which client plugin a consumer (react-query, vue-query, swr, mcp) should call for an
644
+ * operation, shared so the selection rules and diagnostics stay in one place.
645
+ *
646
+ * Every client runtime lives in a dedicated client plugin, so a consumer always calls a registered
647
+ * contract client plugin (plugin-fetch or plugin-axios) and never emits its own inline client:
648
+ *
649
+ * - `contract` — a registered contract client plugin owns the `<op>` functions and the consumer
650
+ * imports and calls them.
651
+ * - `error` — no client plugin is registered, several are registered without a selector, or the
652
+ * requested one is missing.
653
+ *
654
+ * The `client` string selects explicitly (`'fetch'` / `'axios'`); when it is unset a lone registered
655
+ * contract client plugin is picked up automatically.
656
+ */
657
+ const pluginFetchName = "plugin-fetch";
658
+ const pluginAxiosName = "plugin-axios";
659
+ const selectorToPlugin = {
660
+ fetch: pluginFetchName,
661
+ axios: pluginAxiosName
662
+ };
663
+ const contractPlugins = [pluginFetchName, pluginAxiosName];
664
+ /**
665
+ * Applies the `client` resolution rules. See the module comment for the strategy shape.
666
+ *
667
+ * @example
668
+ * ```ts
669
+ * resolveClient({ client: 'fetch', pluginNames: ['plugin-ts', 'plugin-fetch'] })
670
+ * // { kind: 'contract', pluginName: 'plugin-fetch' }
671
+ * ```
672
+ *
673
+ * @example
674
+ * ```ts
675
+ * resolveClient({ client: undefined, pluginNames: ['plugin-ts'] })
676
+ * // { kind: 'error', message: 'No client plugin is registered…' }
677
+ * ```
678
+ */
679
+ function resolveClient(options) {
680
+ const { client, pluginNames } = options;
681
+ const has = (name) => pluginNames.includes(name);
682
+ if (client === "fetch" || client === "axios") {
683
+ const pluginName = selectorToPlugin[client];
684
+ if (!has(pluginName)) return {
685
+ kind: "error",
686
+ message: `\`client: '${client}'\` is set but \`@kubb/plugin-${client}\` is not registered in \`plugins\`. Add it, or drop \`client\` to use a different client plugin.`
687
+ };
688
+ return {
689
+ kind: "contract",
690
+ pluginName
691
+ };
692
+ }
693
+ const registered = contractPlugins.filter(has);
694
+ if (registered.length === 1) return {
695
+ kind: "contract",
696
+ pluginName: registered[0]
697
+ };
698
+ if (registered.length > 1) return {
699
+ kind: "error",
700
+ message: `Multiple client plugins are registered (${registered.map((name) => `\`@kubb/${name}\``).join(", ")}). Set \`client: 'fetch' | 'axios'\` to choose which client the hooks call, or register a single client plugin.`
701
+ };
702
+ return {
703
+ kind: "error",
704
+ message: "No client plugin is registered. Add `@kubb/plugin-axios` or `@kubb/plugin-fetch` to `plugins` so the generated code has an HTTP client to call."
705
+ };
706
+ }
707
+ //#endregion
708
+ //#region ../../internals/client/src/resolveClientOperation.ts
709
+ /**
710
+ * Resolves the contract client `<op>` a consumer (query hook, MCP handler) imports, by looking up
711
+ * the registered contract client plugin's resolver and output. Works for any contract client plugin
712
+ * (plugin-fetch or plugin-axios). Returns `null` when no contract plugin is in play (the inline
713
+ * path). The plugin injects `.kubb/client.ts` at the global output root, the same path consumers
714
+ * read `RequestConfig` / `ResponseErrorConfig` from.
715
+ */
716
+ function resolveClientOperation(options) {
717
+ const { clientPlugin, driver, node, root, output } = options;
718
+ if (!clientPlugin) return null;
719
+ const resolver = driver.getResolver(clientPlugin.pluginName);
720
+ if (!resolver) return null;
721
+ const plugin = driver.getPlugin(clientPlugin.pluginName);
722
+ const file = resolver.resolveFile(operationFileEntry(node, node.operationId), {
723
+ root,
724
+ output: plugin?.options?.output ?? output,
725
+ group: plugin?.options?.group ?? void 0
726
+ });
727
+ return {
728
+ name: resolver.resolveName(node.operationId),
729
+ path: file.path,
730
+ clientPath: node_path.default.resolve(root, ".kubb/client.ts")
731
+ };
732
+ }
733
+ //#endregion
634
734
  //#region src/generators/mcpGenerator.tsx
635
735
  /**
636
736
  * Built-in operation generator for `@kubb/plugin-mcp`. Emits one MCP tool
637
- * handler per OpenAPI operation, wiring the input Zod schema, the HTTP call,
638
- * and the response shape into a single function that an MCP server can
639
- * register as a callable tool.
737
+ * handler per OpenAPI operation. The handler takes the grouped `{ path, query,
738
+ * headers, body }` config and forwards it to the registered client plugin's
739
+ * (`@kubb/plugin-axios` / `@kubb/plugin-fetch`) `<op>` function.
640
740
  */
641
741
  const mcpGenerator = (0, _kubb_core.defineGenerator)({
642
742
  name: "mcp",
@@ -644,14 +744,19 @@ const mcpGenerator = (0, _kubb_core.defineGenerator)({
644
744
  operation(node, ctx) {
645
745
  if (!_kubb_core.ast.isHttpOperationNode(node)) return null;
646
746
  const { resolver, driver, root } = ctx;
647
- const { output, client, paramsCasing, group } = ctx.options;
747
+ const { output, client, group } = ctx.options;
648
748
  const pluginTs = driver.getPlugin(_kubb_plugin_ts.pluginTsName);
649
749
  if (!pluginTs) return null;
650
750
  const tsResolver = driver.getResolver(_kubb_plugin_ts.pluginTsName);
651
- const importedTypeNames = resolveOperationTypeNames(node, tsResolver, {
652
- paramsCasing,
653
- responseStatusNames: "error"
751
+ const contractOp = resolveClientOperation({
752
+ clientPlugin: { pluginName: client.pluginName },
753
+ driver,
754
+ node,
755
+ root,
756
+ output
654
757
  });
758
+ if (!contractOp) return null;
759
+ const requestConfigName = tsResolver.resolveRequestConfigName(node);
655
760
  const meta = {
656
761
  name: resolver.resolveHandlerName(node),
657
762
  file: resolver.resolveFile({
@@ -680,8 +785,8 @@ const mcpGenerator = (0, _kubb_core.defineGenerator)({
680
785
  path: meta.file.path,
681
786
  meta: meta.file.meta,
682
787
  children: [
683
- meta.fileTs && importedTypeNames.length > 0 && /* @__PURE__ */ (0, _kubb_renderer_jsx_jsx_runtime.jsx)(_kubb_renderer_jsx.File.Import, {
684
- name: Array.from(new Set(importedTypeNames)).sort(),
788
+ meta.fileTs && requestConfigName && /* @__PURE__ */ (0, _kubb_renderer_jsx_jsx_runtime.jsx)(_kubb_renderer_jsx.File.Import, {
789
+ name: [requestConfigName],
685
790
  root: meta.file.path,
686
791
  path: meta.fileTs.path,
687
792
  isTypeOnly: true
@@ -701,59 +806,15 @@ const mcpGenerator = (0, _kubb_core.defineGenerator)({
701
806
  isTypeOnly: true
702
807
  }),
703
808
  /* @__PURE__ */ (0, _kubb_renderer_jsx_jsx_runtime.jsx)(_kubb_renderer_jsx.File.Import, {
704
- name: ["buildFormData"],
809
+ name: [contractOp.name],
705
810
  root: meta.file.path,
706
- path: node_path.default.resolve(root, ".kubb/config.ts")
811
+ path: contractOp.path
707
812
  }),
708
- client.importPath ? /* @__PURE__ */ (0, _kubb_renderer_jsx_jsx_runtime.jsxs)(_kubb_renderer_jsx_jsx_runtime.Fragment, { children: [
709
- /* @__PURE__ */ (0, _kubb_renderer_jsx_jsx_runtime.jsx)(_kubb_renderer_jsx.File.Import, {
710
- name: [
711
- "Client",
712
- "RequestConfig",
713
- "ResponseErrorConfig"
714
- ],
715
- path: client.importPath,
716
- isTypeOnly: true
717
- }),
718
- /* @__PURE__ */ (0, _kubb_renderer_jsx_jsx_runtime.jsx)(_kubb_renderer_jsx.File.Import, {
719
- name: "client",
720
- path: client.importPath
721
- }),
722
- client.dataReturnType === "full" && /* @__PURE__ */ (0, _kubb_renderer_jsx_jsx_runtime.jsx)(_kubb_renderer_jsx.File.Import, {
723
- name: ["ResponseConfig"],
724
- path: client.importPath,
725
- isTypeOnly: true
726
- })
727
- ] }) : /* @__PURE__ */ (0, _kubb_renderer_jsx_jsx_runtime.jsxs)(_kubb_renderer_jsx_jsx_runtime.Fragment, { children: [
728
- /* @__PURE__ */ (0, _kubb_renderer_jsx_jsx_runtime.jsx)(_kubb_renderer_jsx.File.Import, {
729
- name: [
730
- "Client",
731
- "RequestConfig",
732
- "ResponseErrorConfig"
733
- ],
734
- root: meta.file.path,
735
- path: node_path.default.resolve(root, ".kubb/client.ts"),
736
- isTypeOnly: true
737
- }),
738
- /* @__PURE__ */ (0, _kubb_renderer_jsx_jsx_runtime.jsx)(_kubb_renderer_jsx.File.Import, {
739
- name: ["client"],
740
- root: meta.file.path,
741
- path: node_path.default.resolve(root, ".kubb/client.ts")
742
- }),
743
- client.dataReturnType === "full" && /* @__PURE__ */ (0, _kubb_renderer_jsx_jsx_runtime.jsx)(_kubb_renderer_jsx.File.Import, {
744
- name: ["ResponseConfig"],
745
- root: meta.file.path,
746
- path: node_path.default.resolve(root, ".kubb/client.ts"),
747
- isTypeOnly: true
748
- })
749
- ] }),
750
813
  /* @__PURE__ */ (0, _kubb_renderer_jsx_jsx_runtime.jsx)(McpHandler, {
751
814
  name: meta.name,
815
+ clientName: contractOp.name,
752
816
  node,
753
- resolver: tsResolver,
754
- baseURL: client.baseURL,
755
- dataReturnType: client.dataReturnType || "data",
756
- paramsCasing
817
+ resolver: tsResolver
757
818
  })
758
819
  ]
759
820
  });
@@ -773,7 +834,7 @@ const serverGenerator = (0, _kubb_core.defineGenerator)({
773
834
  renderer: _kubb_renderer_jsx.jsxRenderer,
774
835
  operations(nodes, ctx) {
775
836
  const { config, resolver, plugin, driver, root } = ctx;
776
- const { output, paramsCasing, group } = ctx.options;
837
+ const { output, group } = ctx.options;
777
838
  const pluginZod = driver.getPlugin(_kubb_plugin_zod.pluginZodName);
778
839
  if (!pluginZod) return;
779
840
  const zodResolver = driver.getResolver(_kubb_plugin_zod.pluginZodName);
@@ -789,7 +850,7 @@ const serverGenerator = (0, _kubb_core.defineGenerator)({
789
850
  meta: { pluginName: plugin.name }
790
851
  };
791
852
  const operationsMapped = nodes.filter(_kubb_core.ast.isHttpOperationNode).map((node) => {
792
- const { path: pathParams, query: queryParams, header: headerParams } = getOperationParameters(node, { paramsCasing });
853
+ const { path: pathParams, query: queryParams, header: headerParams } = getOperationParameters(node);
793
854
  const mcpFile = resolver.resolveFile({
794
855
  name: node.operationId,
795
856
  extname: ".ts",
@@ -895,7 +956,6 @@ const serverGenerator = (0, _kubb_core.defineGenerator)({
895
956
  name,
896
957
  serverName: ctx.meta.title ?? "server",
897
958
  serverVersion: ctx.meta.version ?? "0.0.0",
898
- paramsCasing,
899
959
  operations: operationsMapped
900
960
  })
901
961
  ]
@@ -961,13 +1021,13 @@ const pluginMcpName = "plugin-mcp";
961
1021
  /**
962
1022
  * Generates a Model Context Protocol (MCP) server from an OpenAPI spec. Every
963
1023
  * operation becomes a typed MCP tool that AI assistants (Claude Desktop, Claude
964
- * Code, MCP-compatible clients) can call directly.
1024
+ * Code, MCP-compatible clients) can call directly. Each handler calls a contract
1025
+ * client `<op>` from a registered `@kubb/plugin-axios` or `@kubb/plugin-fetch`.
965
1026
  *
966
1027
  * @example
967
1028
  * ```ts
968
1029
  * import { defineConfig } from 'kubb'
969
1030
  * import { pluginTs } from '@kubb/plugin-ts'
970
- * import { pluginClient } from '@kubb/plugin-client'
971
1031
  * import { pluginZod } from '@kubb/plugin-zod'
972
1032
  * import { pluginMcp } from '@kubb/plugin-mcp'
973
1033
  *
@@ -976,11 +1036,10 @@ const pluginMcpName = "plugin-mcp";
976
1036
  * output: { path: './src/gen' },
977
1037
  * plugins: [
978
1038
  * pluginTs(),
979
- * pluginClient(),
980
1039
  * pluginZod(),
981
1040
  * pluginMcp({
982
1041
  * output: { path: './mcp' },
983
- * client: { baseURL: 'https://petstore.swagger.io/v2' },
1042
+ * baseURL: 'https://petstore.swagger.io/v2',
984
1043
  * }),
985
1044
  * ],
986
1045
  * })
@@ -990,9 +1049,7 @@ const pluginMcp = (0, _kubb_core.definePlugin)((options) => {
990
1049
  const { output = {
991
1050
  path: "mcp",
992
1051
  barrel: { type: "named" }
993
- }, group, exclude = [], include, override = [], paramsCasing, client, resolver: userResolver, macros: userMacros, generators: userGenerators = [] } = options;
994
- const clientName = client?.client ?? "axios";
995
- const clientImportPath = client?.importPath ?? (!client?.bundle ? `@kubb/plugin-client/clients/${clientName}` : void 0);
1052
+ }, group, exclude = [], include, override = [], client, resolver: userResolver, macros: userMacros } = options;
996
1053
  const groupConfig = createGroupConfig(group);
997
1054
  return {
998
1055
  name: pluginMcpName,
@@ -1003,51 +1060,28 @@ const pluginMcp = (0, _kubb_core.definePlugin)((options) => {
1003
1060
  ...resolverMcp,
1004
1061
  ...userResolver
1005
1062
  } : resolverMcp;
1063
+ const resolvedClient = resolveClient({
1064
+ client,
1065
+ pluginNames: (ctx.config.plugins ?? []).map((p) => p.name).filter((name) => Boolean(name))
1066
+ });
1067
+ if (resolvedClient.kind === "error") throw new Error(resolvedClient.message);
1068
+ const resolvedClientDescriptor = {
1069
+ kind: "contract",
1070
+ pluginName: resolvedClient.pluginName
1071
+ };
1006
1072
  ctx.setOptions({
1007
1073
  output,
1008
1074
  exclude,
1009
1075
  include,
1010
1076
  override,
1011
1077
  group: groupConfig,
1012
- paramsCasing,
1013
- client: {
1014
- client: clientName,
1015
- clientType: client?.clientType ?? "function",
1016
- importPath: clientImportPath,
1017
- dataReturnType: client?.dataReturnType ?? "data",
1018
- bundle: client?.bundle,
1019
- baseURL: client?.baseURL,
1020
- paramsCasing: client?.paramsCasing
1021
- },
1078
+ client: resolvedClientDescriptor,
1022
1079
  resolver
1023
1080
  });
1024
1081
  ctx.setResolver(resolver);
1025
1082
  if (userMacros?.length) ctx.setMacros(userMacros);
1026
1083
  ctx.addGenerator(mcpGenerator);
1027
1084
  ctx.addGenerator(serverGenerator);
1028
- for (const gen of userGenerators) ctx.addGenerator(gen);
1029
- const root = node_path$1.default.resolve(ctx.config.root, ctx.config.output.path);
1030
- const hasClientPlugin = ctx.config.plugins?.some((p) => p.name === _kubb_plugin_client.pluginClientName);
1031
- if (client?.bundle && !hasClientPlugin && !clientImportPath) ctx.injectFile({
1032
- baseName: "client.ts",
1033
- path: node_path$1.default.resolve(root, ".kubb/client.ts"),
1034
- sources: [_kubb_core.ast.factory.createSource({
1035
- name: "client",
1036
- nodes: [_kubb_core.ast.factory.createText(clientName === "fetch" ? _kubb_plugin_client_templates_clients_fetch_source.source : _kubb_plugin_client_templates_clients_axios_source.source)],
1037
- isExportable: true,
1038
- isIndexable: true
1039
- })]
1040
- });
1041
- if (!hasClientPlugin) ctx.injectFile({
1042
- baseName: "config.ts",
1043
- path: node_path$1.default.resolve(root, ".kubb/config.ts"),
1044
- sources: [_kubb_core.ast.factory.createSource({
1045
- name: "config",
1046
- nodes: [_kubb_core.ast.factory.createText(_kubb_plugin_client_templates_config_source.source)],
1047
- isExportable: false,
1048
- isIndexable: false
1049
- })]
1050
- });
1051
1085
  } }
1052
1086
  };
1053
1087
  });