@kubb/plugin-mcp 5.0.0-beta.56 → 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.js CHANGED
@@ -1,14 +1,10 @@
1
- import "./chunk-C0LytTxp.js";
1
+ import "./rolldown-runtime-C0LytTxp.js";
2
2
  import path from "node:path";
3
3
  import { ast, defineGenerator, definePlugin, defineResolver } from "@kubb/core";
4
- import { functionPrinter, pluginTsName } from "@kubb/plugin-ts";
5
4
  import { Const, File, Function, jsxRenderer } from "@kubb/renderer-jsx";
6
5
  import { Fragment, jsx, jsxs } from "@kubb/renderer-jsx/jsx-runtime";
6
+ import { createFunctionParameter, createFunctionParameters, createObjectBindingPattern, functionPrinter, pluginTsName } from "@kubb/plugin-ts";
7
7
  import { pluginZodName } from "@kubb/plugin-zod";
8
- import { pluginClientName } from "@kubb/plugin-client";
9
- import { source } from "@kubb/plugin-client/templates/clients/axios.source";
10
- import { source as source$1 } from "@kubb/plugin-client/templates/clients/fetch.source";
11
- import { source as source$2 } from "@kubb/plugin-client/templates/config.source";
12
8
  //#region ../../internals/utils/src/casing.ts
13
9
  /**
14
10
  * Shared implementation for camelCase and PascalCase conversion.
@@ -221,6 +217,22 @@ var Url = class Url {
221
217
  return `\`${prefix ?? ""}${result}\``;
222
218
  }
223
219
  /**
220
+ * Converts an OpenAPI/Swagger path to a template literal that reads each parameter off the
221
+ * grouped `path` request option, e.g. `/pet/{petId}` becomes `` `/pet/${path.petId}` ``. Parameter
222
+ * names are camelCased to match the generated `path` type, and `prefix` is prepended inside the
223
+ * literal. Shared by the client and cypress generators that pass a grouped `path` object.
224
+ *
225
+ * @example
226
+ * Url.toGroupedTemplateString('/pet/{petId}') // '`/pet/${path.petId}`'
227
+ */
228
+ static toGroupedTemplateString(path, { prefix } = {}) {
229
+ return Url.toTemplateString(path, {
230
+ prefix,
231
+ casing: "camelcase",
232
+ replacer: (name) => `path.${name}`
233
+ });
234
+ }
235
+ /**
224
236
  * Returns the path and its extracted params as a structured `URLObject`, or as a stringified
225
237
  * expression when `stringify` is set.
226
238
  *
@@ -248,13 +260,121 @@ var Url = class Url {
248
260
  }
249
261
  };
250
262
  //#endregion
263
+ //#region ../../internals/shared/src/params.ts
264
+ const caseParamsCache = /* @__PURE__ */ new WeakMap();
265
+ /**
266
+ * Applies camelCase to parameter names and returns a new array without mutating the input.
267
+ *
268
+ * Run it before handing parameters to schema builders so output property keys get the right casing
269
+ * while `OperationNode.parameters` stays intact for other consumers. When `casing` is unset, the
270
+ * original array is returned unchanged. Results are cached per input array.
271
+ */
272
+ function caseParams(params, casing) {
273
+ if (!casing) return params;
274
+ const cached = caseParamsCache.get(params);
275
+ if (cached) return cached;
276
+ const result = params.map((param) => ({
277
+ ...param,
278
+ name: camelCase(param.name)
279
+ }));
280
+ caseParamsCache.set(params, result);
281
+ return result;
282
+ }
283
+ //#endregion
251
284
  //#region ../../internals/shared/src/operation.ts
285
+ /**
286
+ * Builds the `ResolverFileParams` every operation generator passes to
287
+ * `resolver.resolveFile`: a file named `name`, tagged by the operation's first
288
+ * tag (or `'default'`), at the operation's path. Centralizes the entry object
289
+ * that was repeated at dozens of call sites across the client and query plugins.
290
+ *
291
+ * @example
292
+ * ```ts
293
+ * resolver.resolveFile(operationFileEntry(node, node.operationId), { root, output, group })
294
+ * ```
295
+ */
296
+ function operationFileEntry(node, name, extname = ".ts") {
297
+ return {
298
+ name,
299
+ extname,
300
+ tag: node.tags[0] ?? "default",
301
+ path: node.path
302
+ };
303
+ }
252
304
  function getOperationLink(node, link) {
253
305
  if (!link) return null;
254
306
  if (typeof link === "function") return link(node) ?? null;
255
307
  if (link === "urlPath") return node.path ? `{@link ${Url.toPath(node.path)}}` : null;
256
308
  return node.path ? `{@link ${node.path.replaceAll("{", ":").replaceAll("}", "")}}` : null;
257
309
  }
310
+ function getContentTypeInfo(node) {
311
+ const contentTypes = node.requestBody?.content?.map((e) => e.contentType) ?? [];
312
+ const isMultipleContentTypes = contentTypes.length > 1;
313
+ return {
314
+ contentTypes,
315
+ isMultipleContentTypes,
316
+ contentTypeUnion: isMultipleContentTypes ? contentTypes.map((ct) => JSON.stringify(ct)).join(" | ") : "",
317
+ defaultContentType: contentTypes[0] ?? "application/json",
318
+ hasFormData: contentTypes.some((ct) => ct === "multipart/form-data")
319
+ };
320
+ }
321
+ function buildRequestConfigType(node) {
322
+ const { isMultipleContentTypes, contentTypeUnion } = getContentTypeInfo(node);
323
+ const configType = `Partial<Omit<RequestConfig, 'path' | 'query' | 'body' | 'headers' | 'url'>>`;
324
+ const contentTypeProp = isMultipleContentTypes ? `contentType?: ${contentTypeUnion}` : null;
325
+ return contentTypeProp ? `${configType} & { ${contentTypeProp} }` : configType;
326
+ }
327
+ /**
328
+ * Which of the grouped request options an operation carries.
329
+ */
330
+ function getRequestGroups(node) {
331
+ const { path, query, header } = getOperationParameters(node);
332
+ return {
333
+ path: path.length > 0,
334
+ query: query.length > 0,
335
+ body: Boolean(node.requestBody?.content?.[0]?.schema),
336
+ headers: header.length > 0
337
+ };
338
+ }
339
+ /**
340
+ * Resolves which grouped request options an operation carries together with whether each group
341
+ * holds a required member. The grouped parameter stays optional only when nothing inside it is
342
+ * required, matching the generated `RequestConfig` type.
343
+ */
344
+ function getRequestGroupOptionality(node) {
345
+ const groups = getRequestGroups(node);
346
+ const { path, query, header } = getOperationParameters(node);
347
+ const hasRequiredPath = path.some((param) => param.required);
348
+ const hasRequiredQuery = query.some((param) => param.required);
349
+ const hasRequiredHeader = header.some((param) => param.required);
350
+ return {
351
+ groups,
352
+ hasRequiredPath,
353
+ hasRequiredQuery,
354
+ hasRequiredHeader,
355
+ isOptional: !hasRequiredPath && !hasRequiredQuery && !hasRequiredHeader && !groups.body
356
+ };
357
+ }
358
+ /**
359
+ * Builds the grouped `{ path, query, body, headers }` parameter for a generated client
360
+ * function, typed from the operation's `RequestConfig` (minus `url`). Only the groups the
361
+ * operation actually has are destructured. The trailing `config` parameter carries the
362
+ * runtime `RequestConfig` overrides plus `client`.
363
+ */
364
+ function buildRequestParamsSignature(node, resolver, options = {}) {
365
+ const { isConfigurable = true } = options;
366
+ const { groups, isOptional } = getRequestGroupOptionality(node);
367
+ const names = [
368
+ "path",
369
+ "query",
370
+ "body",
371
+ "headers"
372
+ ].filter((key) => groups[key]);
373
+ return {
374
+ signature: [names.length > 0 ? `{ ${names.join(", ")} }: ${resolver.resolveRequestConfigName(node)}${isOptional ? " = {}" : ""}` : null, isConfigurable ? `config: ${buildRequestConfigType(node)} = {}` : null].filter(Boolean).join(", "),
375
+ groups
376
+ };
377
+ }
258
378
  function buildOperationComments(node, options = {}) {
259
379
  const { link = "pathTemplate", linkPosition = "afterDeprecated", splitLines = false } = options;
260
380
  const linkComment = getOperationLink(node, link);
@@ -273,7 +393,7 @@ function buildOperationComments(node, options = {}) {
273
393
  return filteredComments.flatMap((text) => text.split(/\r?\n/).map((line) => line.trim())).filter((comment) => Boolean(comment));
274
394
  }
275
395
  function getOperationParameters(node, options = {}) {
276
- const params = ast.caseParams(node.parameters, options.paramsCasing);
396
+ const params = caseParams(node.parameters, options.paramsCasing === "original" ? void 0 : "camelcase");
277
397
  return {
278
398
  path: params.filter((param) => param.in === "path"),
279
399
  query: params.filter((param) => param.in === "query"),
@@ -289,48 +409,6 @@ function isSuccessStatusCode(statusCode) {
289
409
  const code = getStatusCodeNumber(statusCode);
290
410
  return code !== null && code >= 200 && code < 300;
291
411
  }
292
- function isErrorStatusCode(statusCode) {
293
- const code = getStatusCodeNumber(statusCode);
294
- return code !== null && code >= 400;
295
- }
296
- function resolveErrorNames(node, resolver) {
297
- return node.responses.filter((response) => isErrorStatusCode(response.statusCode)).map((response) => resolver.resolveResponseStatusName(node, response.statusCode));
298
- }
299
- function resolveStatusCodeNames(node, resolver) {
300
- return node.responses.map((response) => resolver.resolveResponseStatusName(node, response.statusCode));
301
- }
302
- const typeNamesByResolver = /* @__PURE__ */ new WeakMap();
303
- function resolveOperationTypeNames(node, resolver, options = {}) {
304
- const cacheKey = `${node.operationId}\0${options.paramsCasing ?? ""}\0${options.order ?? ""}\0${options.responseStatusNames ?? ""}\0${(options.exclude ?? []).join(",")}`;
305
- let byResolver = typeNamesByResolver.get(resolver);
306
- if (byResolver) {
307
- const cached = byResolver.get(cacheKey);
308
- if (cached) return cached;
309
- } else {
310
- byResolver = /* @__PURE__ */ new Map();
311
- typeNamesByResolver.set(resolver, byResolver);
312
- }
313
- const { path, query, header } = getOperationParameters(node, { paramsCasing: options.paramsCasing });
314
- const responseStatusNames = options.responseStatusNames === "error" ? resolveErrorNames(node, resolver) : options.responseStatusNames === false ? [] : resolveStatusCodeNames(node, resolver);
315
- const exclude = new Set(options.exclude ?? []);
316
- const paramNames = [
317
- ...path.map((param) => resolver.resolvePathParamsName(node, param)),
318
- ...query.map((param) => resolver.resolveQueryParamsName(node, param)),
319
- ...header.map((param) => resolver.resolveHeaderParamsName(node, param))
320
- ];
321
- const bodyAndResponseNames = [node.requestBody?.content?.[0]?.schema ? resolver.resolveDataName(node) : null, resolver.resolveResponseName(node)];
322
- const result = (options.order === "body-response-first" ? [
323
- ...bodyAndResponseNames,
324
- ...paramNames,
325
- ...responseStatusNames
326
- ] : [
327
- ...paramNames,
328
- ...bodyAndResponseNames,
329
- ...responseStatusNames
330
- ]).filter((name) => Boolean(name) && !exclude.has(name));
331
- byResolver.set(cacheKey, result);
332
- return result;
333
- }
334
412
  function findSuccessStatusCode(responses) {
335
413
  for (const response of responses) if (isSuccessStatusCode(response.statusCode)) return response.statusCode;
336
414
  return null;
@@ -367,76 +445,19 @@ function createGroupConfig(group) {
367
445
  };
368
446
  }
369
447
  //#endregion
370
- //#region ../../internals/shared/src/params.ts
371
- function buildParamsMapping(originalParams, mappedParams) {
372
- const mapping = {};
373
- let hasChanged = false;
374
- originalParams.forEach((param, i) => {
375
- const mappedName = mappedParams[i]?.name ?? param.name;
376
- mapping[param.name] = mappedName;
377
- if (param.name !== mappedName) hasChanged = true;
378
- });
379
- return hasChanged ? mapping : null;
380
- }
381
- function buildTransformedParamsMapping(params, transformName) {
382
- if (!params.length) return null;
383
- return buildParamsMapping(params, params.map((param) => ({
384
- ...param,
385
- name: transformName(param.name)
386
- })));
387
- }
388
- //#endregion
389
448
  //#region src/components/McpHandler.tsx
390
- /**
391
- * Generate a remapping statement: `const mappedX = x ? { "orig": x.camel, ... } : undefined`
392
- */
393
- function buildRemappingCode(mapping, varName, sourceName) {
394
- return `const ${varName} = ${sourceName} ? { ${Object.entries(mapping).map(([orig, camel]) => `"${orig}": ${sourceName}.${camel}`).join(", ")} } : undefined`;
395
- }
396
- const declarationPrinter = functionPrinter({ mode: "declaration" });
397
- function McpHandler({ name, node, resolver, baseURL, dataReturnType, paramsCasing }) {
449
+ const requestGroupOrder = [
450
+ "path",
451
+ "query",
452
+ "headers",
453
+ "body"
454
+ ];
455
+ function McpHandler({ name, clientName, node, resolver }) {
398
456
  if (!ast.isHttpOperationNode(node)) return null;
399
- const contentType = node.requestBody?.content?.[0]?.contentType;
400
- const isFormData = contentType === "multipart/form-data";
401
- const { query: queryParams, header: headerParams } = getOperationParameters(node, { paramsCasing });
402
- const { path: originalPathParams, query: originalQueryParams, header: originalHeaderParams } = getOperationParameters(node);
403
- const requestName = node.requestBody?.content?.[0]?.schema ? resolver.resolveDataName(node) : null;
404
- const responseName = resolver.resolveResponseName(node);
405
- const errorResponses = node.responses.filter((r) => Number(r.statusCode) >= 400).map((r) => resolver.resolveResponseStatusName(node, r.statusCode));
406
- const generics = [
407
- responseName,
408
- `ResponseErrorConfig<${errorResponses.length > 0 ? errorResponses.join(" | ") : "Error"}>`,
409
- requestName || "unknown"
410
- ].filter(Boolean);
411
- const paramsNode = ast.createOperationParams(node, {
412
- paramsType: "object",
413
- pathParamsType: "inline",
414
- resolver,
415
- paramsCasing
416
- });
417
- const baseParamsSignature = declarationPrinter.print(paramsNode) ?? "";
418
- const paramsSignature = baseParamsSignature ? `${baseParamsSignature}, request: RequestHandlerExtra<ServerRequest, ServerNotification>` : "request: RequestHandlerExtra<ServerRequest, ServerNotification>";
419
- const pathParamsMapping = paramsCasing ? buildTransformedParamsMapping(originalPathParams, camelCase) : null;
420
- const queryParamsMapping = paramsCasing ? buildTransformedParamsMapping(originalQueryParams, camelCase) : null;
421
- const headerParamsMapping = paramsCasing ? buildTransformedParamsMapping(originalHeaderParams, camelCase) : null;
422
- const contentTypeHeader = contentType && contentType !== "application/json" && contentType !== "multipart/form-data" ? `'Content-Type': '${contentType}'` : null;
423
- const headers = [headerParams.length ? headerParamsMapping ? "...mappedHeaders" : "...headers" : null, contentTypeHeader].filter(Boolean);
424
- const fetchConfig = [];
425
- fetchConfig.push(`method: ${JSON.stringify(node.method.toUpperCase())}`);
426
- fetchConfig.push(`url: ${Url.toTemplateString(node.path)}`);
427
- if (baseURL) fetchConfig.push(`baseURL: \`${baseURL}\``);
428
- if (queryParams.length) fetchConfig.push(queryParamsMapping ? "params: mappedParams" : "params");
429
- if (requestName) fetchConfig.push(`data: ${isFormData ? "formData as FormData" : "requestData"}`);
430
- if (headers.length) fetchConfig.push(`headers: { ${headers.join(", ")} }`);
431
- const callToolResult = `return {
432
- content: [
433
- {
434
- type: 'text',
435
- text: JSON.stringify(${dataReturnType === "data" ? "res.data" : "res"})
436
- }
437
- ],
438
- structuredContent: { data: res.data }
439
- }`;
457
+ const { signature, groups } = buildRequestParamsSignature(node, resolver, { isConfigurable: false });
458
+ const paramsSignature = [signature, "request: RequestHandlerExtra<ServerRequest, ServerNotification>"].filter(Boolean).join(", ");
459
+ const callArgs = requestGroupOrder.filter((key) => groups[key]);
460
+ const callConfig = callArgs.length ? `{ ${callArgs.join(", ")} }` : "{}";
440
461
  return /* @__PURE__ */ jsx(File.Source, {
441
462
  name,
442
463
  isExportable: true,
@@ -449,28 +470,17 @@ function McpHandler({ name, node, resolver, baseURL, dataReturnType, paramsCasin
449
470
  JSDoc: { comments: buildOperationComments(node) },
450
471
  returnType: "Promise<CallToolResult>",
451
472
  children: [
452
- "",
473
+ `const res = await ${clientName}(${callConfig})`,
453
474
  /* @__PURE__ */ jsx("br", {}),
454
- /* @__PURE__ */ jsx("br", {}),
455
- pathParamsMapping && Object.entries(pathParamsMapping).filter(([originalName, camelCaseName]) => originalName !== camelCaseName && isValidVarName(originalName)).map(([originalName, camelCaseName]) => `const ${originalName} = ${camelCaseName}`).join("\n"),
456
- pathParamsMapping && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("br", {}), /* @__PURE__ */ jsx("br", {})] }),
457
- queryParamsMapping && queryParams.length > 0 && /* @__PURE__ */ jsxs(Fragment, { children: [
458
- buildRemappingCode(queryParamsMapping, "mappedParams", "params"),
459
- /* @__PURE__ */ jsx("br", {}),
460
- /* @__PURE__ */ jsx("br", {})
461
- ] }),
462
- headerParamsMapping && headerParams.length > 0 && /* @__PURE__ */ jsxs(Fragment, { children: [
463
- buildRemappingCode(headerParamsMapping, "mappedHeaders", "headers"),
464
- /* @__PURE__ */ jsx("br", {}),
465
- /* @__PURE__ */ jsx("br", {})
466
- ] }),
467
- requestName && "const requestData = data",
468
- /* @__PURE__ */ jsx("br", {}),
469
- isFormData && requestName && "const formData = buildFormData(requestData)",
470
- /* @__PURE__ */ jsx("br", {}),
471
- `const res = await client<${generics.join(", ")}>({ ${fetchConfig.join(", ")} }, request)`,
472
- /* @__PURE__ */ jsx("br", {}),
473
- callToolResult
475
+ `return {
476
+ content: [
477
+ {
478
+ type: 'text',
479
+ text: JSON.stringify(res.data)
480
+ }
481
+ ],
482
+ structuredContent: { data: res.data }
483
+ }`
474
484
  ]
475
485
  })
476
486
  });
@@ -510,37 +520,37 @@ function zodExprFromSchemaNode(schema) {
510
520
  }
511
521
  //#endregion
512
522
  //#region src/components/Server.tsx
513
- const keysPrinter = functionPrinter({ mode: "keys" });
514
- function Server({ name, serverName, serverVersion, paramsCasing, operations }) {
523
+ const keysPrinter = functionPrinter({ mode: "call" });
524
+ function Server({ name, serverName, serverVersion, operations }) {
515
525
  const registrations = operations.map(({ tool, mcp, zod, node }) => {
516
- const { path: pathParams } = getOperationParameters(node, { paramsCasing });
517
- const pathEntries = [];
518
- const otherEntries = [];
519
- for (const p of pathParams) {
520
- const zodParam = zod.pathParams.find((zp) => zp.name === p.name);
521
- pathEntries.push({
522
- key: p.name,
523
- value: zodParam ? zodParam.schemaName : zodExprFromSchemaNode(p.schema)
526
+ const { path: pathParams } = getOperationParameters(node);
527
+ const entries = [];
528
+ if (pathParams.length) {
529
+ const pathFields = pathParams.map((p) => {
530
+ const zodParam = zod.pathParams.find((zp) => zp.name === p.name);
531
+ return `${JSON.stringify(p.name)}: ${zodParam ? zodParam.schemaName : zodExprFromSchemaNode(p.schema)}`;
532
+ }).join(", ");
533
+ entries.push({
534
+ key: "path",
535
+ value: `z.object({ ${pathFields} })`
524
536
  });
525
537
  }
526
- if (zod.requestName) otherEntries.push({
527
- key: "data",
528
- value: zod.requestName
529
- });
530
- if (zod.queryParams) otherEntries.push({
531
- key: "params",
538
+ if (zod.queryParams) entries.push({
539
+ key: "query",
532
540
  value: zodGroupExpr(zod.queryParams)
533
541
  });
534
- if (zod.headerParams) otherEntries.push({
542
+ if (zod.headerParams) entries.push({
535
543
  key: "headers",
536
544
  value: zodGroupExpr(zod.headerParams)
537
545
  });
538
- otherEntries.sort((a, b) => a.key.localeCompare(b.key));
539
- const entries = [...pathEntries, ...otherEntries];
540
- const paramsNode = entries.length ? ast.createFunctionParameters({ params: [ast.createParameterGroup({ properties: entries.map((e) => ast.createFunctionParameter({
541
- name: e.key,
546
+ if (zod.requestName) entries.push({
547
+ key: "body",
548
+ value: zod.requestName
549
+ });
550
+ const paramsNode = entries.length ? createFunctionParameters({ params: [createFunctionParameter({
551
+ name: createObjectBindingPattern({ elements: entries.map((e) => ({ name: e.key })) }),
542
552
  optional: false
543
- })) })] }) : null;
553
+ })] }) : null;
544
554
  const destructured = paramsNode ? keysPrinter.print(paramsNode) ?? "" : "";
545
555
  const inputSchema = entries.length ? `{ ${entries.map((e) => `${e.key}: ${e.value}`).join(", ")} }` : null;
546
556
  const outputSchema = zod.responseName;
@@ -602,12 +612,105 @@ return server`
602
612
  });
603
613
  }
604
614
  //#endregion
615
+ //#region ../../internals/client/src/resolveClient.ts
616
+ /**
617
+ * Resolves which client plugin a consumer (react-query, vue-query, swr, mcp) should call for an
618
+ * operation, shared so the selection rules and diagnostics stay in one place.
619
+ *
620
+ * Every client runtime lives in a dedicated client plugin, so a consumer always calls a registered
621
+ * contract client plugin (plugin-fetch or plugin-axios) and never emits its own inline client:
622
+ *
623
+ * - `contract` — a registered contract client plugin owns the `<op>` functions and the consumer
624
+ * imports and calls them.
625
+ * - `error` — no client plugin is registered, several are registered without a selector, or the
626
+ * requested one is missing.
627
+ *
628
+ * The `client` string selects explicitly (`'fetch'` / `'axios'`); when it is unset a lone registered
629
+ * contract client plugin is picked up automatically.
630
+ */
631
+ const pluginFetchName = "plugin-fetch";
632
+ const pluginAxiosName = "plugin-axios";
633
+ const selectorToPlugin = {
634
+ fetch: pluginFetchName,
635
+ axios: pluginAxiosName
636
+ };
637
+ const contractPlugins = [pluginFetchName, pluginAxiosName];
638
+ /**
639
+ * Applies the `client` resolution rules. See the module comment for the strategy shape.
640
+ *
641
+ * @example
642
+ * ```ts
643
+ * resolveClient({ client: 'fetch', pluginNames: ['plugin-ts', 'plugin-fetch'] })
644
+ * // { kind: 'contract', pluginName: 'plugin-fetch' }
645
+ * ```
646
+ *
647
+ * @example
648
+ * ```ts
649
+ * resolveClient({ client: undefined, pluginNames: ['plugin-ts'] })
650
+ * // { kind: 'error', message: 'No client plugin is registered…' }
651
+ * ```
652
+ */
653
+ function resolveClient(options) {
654
+ const { client, pluginNames } = options;
655
+ const has = (name) => pluginNames.includes(name);
656
+ if (client === "fetch" || client === "axios") {
657
+ const pluginName = selectorToPlugin[client];
658
+ if (!has(pluginName)) return {
659
+ kind: "error",
660
+ 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.`
661
+ };
662
+ return {
663
+ kind: "contract",
664
+ pluginName
665
+ };
666
+ }
667
+ const registered = contractPlugins.filter(has);
668
+ if (registered.length === 1) return {
669
+ kind: "contract",
670
+ pluginName: registered[0]
671
+ };
672
+ if (registered.length > 1) return {
673
+ kind: "error",
674
+ 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.`
675
+ };
676
+ return {
677
+ kind: "error",
678
+ 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."
679
+ };
680
+ }
681
+ //#endregion
682
+ //#region ../../internals/client/src/resolveClientOperation.ts
683
+ /**
684
+ * Resolves the contract client `<op>` a consumer (query hook, MCP handler) imports, by looking up
685
+ * the registered contract client plugin's resolver and output. Works for any contract client plugin
686
+ * (plugin-fetch or plugin-axios). Returns `null` when no contract plugin is in play (the inline
687
+ * path). The plugin injects `.kubb/client.ts` at the global output root, the same path consumers
688
+ * read `RequestConfig` / `ResponseErrorConfig` from.
689
+ */
690
+ function resolveClientOperation(options) {
691
+ const { clientPlugin, driver, node, root, output } = options;
692
+ if (!clientPlugin) return null;
693
+ const resolver = driver.getResolver(clientPlugin.pluginName);
694
+ if (!resolver) return null;
695
+ const plugin = driver.getPlugin(clientPlugin.pluginName);
696
+ const file = resolver.resolveFile(operationFileEntry(node, node.operationId), {
697
+ root,
698
+ output: plugin?.options?.output ?? output,
699
+ group: plugin?.options?.group ?? void 0
700
+ });
701
+ return {
702
+ name: resolver.resolveName(node.operationId),
703
+ path: file.path,
704
+ clientPath: path.resolve(root, ".kubb/client.ts")
705
+ };
706
+ }
707
+ //#endregion
605
708
  //#region src/generators/mcpGenerator.tsx
606
709
  /**
607
710
  * Built-in operation generator for `@kubb/plugin-mcp`. Emits one MCP tool
608
- * handler per OpenAPI operation, wiring the input Zod schema, the HTTP call,
609
- * and the response shape into a single function that an MCP server can
610
- * register as a callable tool.
711
+ * handler per OpenAPI operation. The handler takes the grouped `{ path, query,
712
+ * headers, body }` config and forwards it to the registered client plugin's
713
+ * (`@kubb/plugin-axios` / `@kubb/plugin-fetch`) `<op>` function.
611
714
  */
612
715
  const mcpGenerator = defineGenerator({
613
716
  name: "mcp",
@@ -615,14 +718,19 @@ const mcpGenerator = defineGenerator({
615
718
  operation(node, ctx) {
616
719
  if (!ast.isHttpOperationNode(node)) return null;
617
720
  const { resolver, driver, root } = ctx;
618
- const { output, client, paramsCasing, group } = ctx.options;
721
+ const { output, client, group } = ctx.options;
619
722
  const pluginTs = driver.getPlugin(pluginTsName);
620
723
  if (!pluginTs) return null;
621
724
  const tsResolver = driver.getResolver(pluginTsName);
622
- const importedTypeNames = resolveOperationTypeNames(node, tsResolver, {
623
- paramsCasing,
624
- responseStatusNames: "error"
725
+ const contractOp = resolveClientOperation({
726
+ clientPlugin: { pluginName: client.pluginName },
727
+ driver,
728
+ node,
729
+ root,
730
+ output
625
731
  });
732
+ if (!contractOp) return null;
733
+ const requestConfigName = tsResolver.resolveRequestConfigName(node);
626
734
  const meta = {
627
735
  name: resolver.resolveHandlerName(node),
628
736
  file: resolver.resolveFile({
@@ -651,8 +759,8 @@ const mcpGenerator = defineGenerator({
651
759
  path: meta.file.path,
652
760
  meta: meta.file.meta,
653
761
  children: [
654
- meta.fileTs && importedTypeNames.length > 0 && /* @__PURE__ */ jsx(File.Import, {
655
- name: Array.from(new Set(importedTypeNames)).sort(),
762
+ meta.fileTs && requestConfigName && /* @__PURE__ */ jsx(File.Import, {
763
+ name: [requestConfigName],
656
764
  root: meta.file.path,
657
765
  path: meta.fileTs.path,
658
766
  isTypeOnly: true
@@ -672,59 +780,15 @@ const mcpGenerator = defineGenerator({
672
780
  isTypeOnly: true
673
781
  }),
674
782
  /* @__PURE__ */ jsx(File.Import, {
675
- name: ["buildFormData"],
783
+ name: [contractOp.name],
676
784
  root: meta.file.path,
677
- path: path.resolve(root, ".kubb/config.ts")
785
+ path: contractOp.path
678
786
  }),
679
- client.importPath ? /* @__PURE__ */ jsxs(Fragment, { children: [
680
- /* @__PURE__ */ jsx(File.Import, {
681
- name: [
682
- "Client",
683
- "RequestConfig",
684
- "ResponseErrorConfig"
685
- ],
686
- path: client.importPath,
687
- isTypeOnly: true
688
- }),
689
- /* @__PURE__ */ jsx(File.Import, {
690
- name: "client",
691
- path: client.importPath
692
- }),
693
- client.dataReturnType === "full" && /* @__PURE__ */ jsx(File.Import, {
694
- name: ["ResponseConfig"],
695
- path: client.importPath,
696
- isTypeOnly: true
697
- })
698
- ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
699
- /* @__PURE__ */ jsx(File.Import, {
700
- name: [
701
- "Client",
702
- "RequestConfig",
703
- "ResponseErrorConfig"
704
- ],
705
- root: meta.file.path,
706
- path: path.resolve(root, ".kubb/client.ts"),
707
- isTypeOnly: true
708
- }),
709
- /* @__PURE__ */ jsx(File.Import, {
710
- name: ["client"],
711
- root: meta.file.path,
712
- path: path.resolve(root, ".kubb/client.ts")
713
- }),
714
- client.dataReturnType === "full" && /* @__PURE__ */ jsx(File.Import, {
715
- name: ["ResponseConfig"],
716
- root: meta.file.path,
717
- path: path.resolve(root, ".kubb/client.ts"),
718
- isTypeOnly: true
719
- })
720
- ] }),
721
787
  /* @__PURE__ */ jsx(McpHandler, {
722
788
  name: meta.name,
789
+ clientName: contractOp.name,
723
790
  node,
724
- resolver: tsResolver,
725
- baseURL: client.baseURL,
726
- dataReturnType: client.dataReturnType || "data",
727
- paramsCasing
791
+ resolver: tsResolver
728
792
  })
729
793
  ]
730
794
  });
@@ -744,7 +808,7 @@ const serverGenerator = defineGenerator({
744
808
  renderer: jsxRenderer,
745
809
  operations(nodes, ctx) {
746
810
  const { config, resolver, plugin, driver, root } = ctx;
747
- const { output, paramsCasing, group } = ctx.options;
811
+ const { output, group } = ctx.options;
748
812
  const pluginZod = driver.getPlugin(pluginZodName);
749
813
  if (!pluginZod) return;
750
814
  const zodResolver = driver.getResolver(pluginZodName);
@@ -760,7 +824,7 @@ const serverGenerator = defineGenerator({
760
824
  meta: { pluginName: plugin.name }
761
825
  };
762
826
  const operationsMapped = nodes.filter(ast.isHttpOperationNode).map((node) => {
763
- const { path: pathParams, query: queryParams, header: headerParams } = getOperationParameters(node, { paramsCasing });
827
+ const { path: pathParams, query: queryParams, header: headerParams } = getOperationParameters(node);
764
828
  const mcpFile = resolver.resolveFile({
765
829
  name: node.operationId,
766
830
  extname: ".ts",
@@ -866,7 +930,6 @@ const serverGenerator = defineGenerator({
866
930
  name,
867
931
  serverName: ctx.meta.title ?? "server",
868
932
  serverVersion: ctx.meta.version ?? "0.0.0",
869
- paramsCasing,
870
933
  operations: operationsMapped
871
934
  })
872
935
  ]
@@ -932,13 +995,13 @@ const pluginMcpName = "plugin-mcp";
932
995
  /**
933
996
  * Generates a Model Context Protocol (MCP) server from an OpenAPI spec. Every
934
997
  * operation becomes a typed MCP tool that AI assistants (Claude Desktop, Claude
935
- * Code, MCP-compatible clients) can call directly.
998
+ * Code, MCP-compatible clients) can call directly. Each handler calls a contract
999
+ * client `<op>` from a registered `@kubb/plugin-axios` or `@kubb/plugin-fetch`.
936
1000
  *
937
1001
  * @example
938
1002
  * ```ts
939
1003
  * import { defineConfig } from 'kubb'
940
1004
  * import { pluginTs } from '@kubb/plugin-ts'
941
- * import { pluginClient } from '@kubb/plugin-client'
942
1005
  * import { pluginZod } from '@kubb/plugin-zod'
943
1006
  * import { pluginMcp } from '@kubb/plugin-mcp'
944
1007
  *
@@ -947,11 +1010,10 @@ const pluginMcpName = "plugin-mcp";
947
1010
  * output: { path: './src/gen' },
948
1011
  * plugins: [
949
1012
  * pluginTs(),
950
- * pluginClient(),
951
1013
  * pluginZod(),
952
1014
  * pluginMcp({
953
1015
  * output: { path: './mcp' },
954
- * client: { baseURL: 'https://petstore.swagger.io/v2' },
1016
+ * baseURL: 'https://petstore.swagger.io/v2',
955
1017
  * }),
956
1018
  * ],
957
1019
  * })
@@ -961,9 +1023,7 @@ const pluginMcp = definePlugin((options) => {
961
1023
  const { output = {
962
1024
  path: "mcp",
963
1025
  barrel: { type: "named" }
964
- }, group, exclude = [], include, override = [], paramsCasing, client, resolver: userResolver, transformer: userTransformer, generators: userGenerators = [] } = options;
965
- const clientName = client?.client ?? "axios";
966
- const clientImportPath = client?.importPath ?? (!client?.bundle ? `@kubb/plugin-client/clients/${clientName}` : void 0);
1026
+ }, group, exclude = [], include, override = [], client, resolver: userResolver, macros: userMacros } = options;
967
1027
  const groupConfig = createGroupConfig(group);
968
1028
  return {
969
1029
  name: pluginMcpName,
@@ -974,51 +1034,28 @@ const pluginMcp = definePlugin((options) => {
974
1034
  ...resolverMcp,
975
1035
  ...userResolver
976
1036
  } : resolverMcp;
1037
+ const resolvedClient = resolveClient({
1038
+ client,
1039
+ pluginNames: (ctx.config.plugins ?? []).map((p) => p.name).filter((name) => Boolean(name))
1040
+ });
1041
+ if (resolvedClient.kind === "error") throw new Error(resolvedClient.message);
1042
+ const resolvedClientDescriptor = {
1043
+ kind: "contract",
1044
+ pluginName: resolvedClient.pluginName
1045
+ };
977
1046
  ctx.setOptions({
978
1047
  output,
979
1048
  exclude,
980
1049
  include,
981
1050
  override,
982
1051
  group: groupConfig,
983
- paramsCasing,
984
- client: {
985
- client: clientName,
986
- clientType: client?.clientType ?? "function",
987
- importPath: clientImportPath,
988
- dataReturnType: client?.dataReturnType ?? "data",
989
- bundle: client?.bundle,
990
- baseURL: client?.baseURL,
991
- paramsCasing: client?.paramsCasing
992
- },
1052
+ client: resolvedClientDescriptor,
993
1053
  resolver
994
1054
  });
995
1055
  ctx.setResolver(resolver);
996
- if (userTransformer) ctx.setTransformer(userTransformer);
1056
+ if (userMacros?.length) ctx.setMacros(userMacros);
997
1057
  ctx.addGenerator(mcpGenerator);
998
1058
  ctx.addGenerator(serverGenerator);
999
- for (const gen of userGenerators) ctx.addGenerator(gen);
1000
- const root = path.resolve(ctx.config.root, ctx.config.output.path);
1001
- const hasClientPlugin = ctx.config.plugins?.some((p) => p.name === pluginClientName);
1002
- if (client?.bundle && !hasClientPlugin && !clientImportPath) ctx.injectFile({
1003
- baseName: "client.ts",
1004
- path: path.resolve(root, ".kubb/client.ts"),
1005
- sources: [ast.createSource({
1006
- name: "client",
1007
- nodes: [ast.createText(clientName === "fetch" ? source$1 : source)],
1008
- isExportable: true,
1009
- isIndexable: true
1010
- })]
1011
- });
1012
- if (!hasClientPlugin) ctx.injectFile({
1013
- baseName: "config.ts",
1014
- path: path.resolve(root, ".kubb/config.ts"),
1015
- sources: [ast.createSource({
1016
- name: "config",
1017
- nodes: [ast.createText(source$2)],
1018
- isExportable: false,
1019
- isIndexable: false
1020
- })]
1021
- });
1022
1059
  } }
1023
1060
  };
1024
1061
  });