@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.js CHANGED
@@ -1,15 +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 { caseParams, createOperationParams } from "@kubb/ast/utils";
5
- import { functionPrinter, pluginTsName } from "@kubb/plugin-ts";
6
4
  import { Const, File, Function, jsxRenderer } from "@kubb/renderer-jsx";
7
5
  import { Fragment, jsx, jsxs } from "@kubb/renderer-jsx/jsx-runtime";
6
+ import { createFunctionParameter, createFunctionParameters, createObjectBindingPattern, functionPrinter, pluginTsName } from "@kubb/plugin-ts";
8
7
  import { pluginZodName } from "@kubb/plugin-zod";
9
- import { pluginClientName } from "@kubb/plugin-client";
10
- import { source } from "@kubb/plugin-client/templates/clients/axios.source";
11
- import { source as source$1 } from "@kubb/plugin-client/templates/clients/fetch.source";
12
- import { source as source$2 } from "@kubb/plugin-client/templates/config.source";
13
8
  //#region ../../internals/utils/src/casing.ts
14
9
  /**
15
10
  * Shared implementation for camelCase and PascalCase conversion.
@@ -222,6 +217,22 @@ var Url = class Url {
222
217
  return `\`${prefix ?? ""}${result}\``;
223
218
  }
224
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
+ /**
225
236
  * Returns the path and its extracted params as a structured `URLObject`, or as a stringified
226
237
  * expression when `stringify` is set.
227
238
  *
@@ -249,13 +260,121 @@ var Url = class Url {
249
260
  }
250
261
  };
251
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
252
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
+ }
253
304
  function getOperationLink(node, link) {
254
305
  if (!link) return null;
255
306
  if (typeof link === "function") return link(node) ?? null;
256
307
  if (link === "urlPath") return node.path ? `{@link ${Url.toPath(node.path)}}` : null;
257
308
  return node.path ? `{@link ${node.path.replaceAll("{", ":").replaceAll("}", "")}}` : null;
258
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
+ }
259
378
  function buildOperationComments(node, options = {}) {
260
379
  const { link = "pathTemplate", linkPosition = "afterDeprecated", splitLines = false } = options;
261
380
  const linkComment = getOperationLink(node, link);
@@ -274,7 +393,7 @@ function buildOperationComments(node, options = {}) {
274
393
  return filteredComments.flatMap((text) => text.split(/\r?\n/).map((line) => line.trim())).filter((comment) => Boolean(comment));
275
394
  }
276
395
  function getOperationParameters(node, options = {}) {
277
- const params = caseParams(node.parameters, options.paramsCasing);
396
+ const params = caseParams(node.parameters, options.paramsCasing === "original" ? void 0 : "camelcase");
278
397
  return {
279
398
  path: params.filter((param) => param.in === "path"),
280
399
  query: params.filter((param) => param.in === "query"),
@@ -290,48 +409,6 @@ function isSuccessStatusCode(statusCode) {
290
409
  const code = getStatusCodeNumber(statusCode);
291
410
  return code !== null && code >= 200 && code < 300;
292
411
  }
293
- function isErrorStatusCode(statusCode) {
294
- const code = getStatusCodeNumber(statusCode);
295
- return code !== null && code >= 400;
296
- }
297
- function resolveErrorNames(node, resolver) {
298
- return node.responses.filter((response) => isErrorStatusCode(response.statusCode)).map((response) => resolver.resolveResponseStatusName(node, response.statusCode));
299
- }
300
- function resolveStatusCodeNames(node, resolver) {
301
- return node.responses.map((response) => resolver.resolveResponseStatusName(node, response.statusCode));
302
- }
303
- const typeNamesByResolver = /* @__PURE__ */ new WeakMap();
304
- function resolveOperationTypeNames(node, resolver, options = {}) {
305
- const cacheKey = `${node.operationId}\0${options.paramsCasing ?? ""}\0${options.order ?? ""}\0${options.responseStatusNames ?? ""}\0${(options.exclude ?? []).join(",")}`;
306
- let byResolver = typeNamesByResolver.get(resolver);
307
- if (byResolver) {
308
- const cached = byResolver.get(cacheKey);
309
- if (cached) return cached;
310
- } else {
311
- byResolver = /* @__PURE__ */ new Map();
312
- typeNamesByResolver.set(resolver, byResolver);
313
- }
314
- const { path, query, header } = getOperationParameters(node, { paramsCasing: options.paramsCasing });
315
- const responseStatusNames = options.responseStatusNames === "error" ? resolveErrorNames(node, resolver) : options.responseStatusNames === false ? [] : resolveStatusCodeNames(node, resolver);
316
- const exclude = new Set(options.exclude ?? []);
317
- const paramNames = [
318
- ...path.map((param) => resolver.resolvePathParamsName(node, param)),
319
- ...query.map((param) => resolver.resolveQueryParamsName(node, param)),
320
- ...header.map((param) => resolver.resolveHeaderParamsName(node, param))
321
- ];
322
- const bodyAndResponseNames = [node.requestBody?.content?.[0]?.schema ? resolver.resolveDataName(node) : null, resolver.resolveResponseName(node)];
323
- const result = (options.order === "body-response-first" ? [
324
- ...bodyAndResponseNames,
325
- ...paramNames,
326
- ...responseStatusNames
327
- ] : [
328
- ...paramNames,
329
- ...bodyAndResponseNames,
330
- ...responseStatusNames
331
- ]).filter((name) => Boolean(name) && !exclude.has(name));
332
- byResolver.set(cacheKey, result);
333
- return result;
334
- }
335
412
  function findSuccessStatusCode(responses) {
336
413
  for (const response of responses) if (isSuccessStatusCode(response.statusCode)) return response.statusCode;
337
414
  return null;
@@ -368,76 +445,19 @@ function createGroupConfig(group) {
368
445
  };
369
446
  }
370
447
  //#endregion
371
- //#region ../../internals/shared/src/params.ts
372
- function buildParamsMapping(originalParams, mappedParams) {
373
- const mapping = {};
374
- let hasChanged = false;
375
- originalParams.forEach((param, i) => {
376
- const mappedName = mappedParams[i]?.name ?? param.name;
377
- mapping[param.name] = mappedName;
378
- if (param.name !== mappedName) hasChanged = true;
379
- });
380
- return hasChanged ? mapping : null;
381
- }
382
- function buildTransformedParamsMapping(params, transformName) {
383
- if (!params.length) return null;
384
- return buildParamsMapping(params, params.map((param) => ({
385
- ...param,
386
- name: transformName(param.name)
387
- })));
388
- }
389
- //#endregion
390
448
  //#region src/components/McpHandler.tsx
391
- /**
392
- * Generate a remapping statement: `const mappedX = x ? { "orig": x.camel, ... } : undefined`
393
- */
394
- function buildRemappingCode(mapping, varName, sourceName) {
395
- return `const ${varName} = ${sourceName} ? { ${Object.entries(mapping).map(([orig, camel]) => `"${orig}": ${sourceName}.${camel}`).join(", ")} } : undefined`;
396
- }
397
- const declarationPrinter = functionPrinter({ mode: "declaration" });
398
- 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 }) {
399
456
  if (!ast.isHttpOperationNode(node)) return null;
400
- const contentType = node.requestBody?.content?.[0]?.contentType;
401
- const isFormData = contentType === "multipart/form-data";
402
- const { query: queryParams, header: headerParams } = getOperationParameters(node, { paramsCasing });
403
- const { path: originalPathParams, query: originalQueryParams, header: originalHeaderParams } = getOperationParameters(node);
404
- const requestName = node.requestBody?.content?.[0]?.schema ? resolver.resolveDataName(node) : null;
405
- const responseName = resolver.resolveResponseName(node);
406
- const errorResponses = node.responses.filter((r) => Number(r.statusCode) >= 400).map((r) => resolver.resolveResponseStatusName(node, r.statusCode));
407
- const generics = [
408
- responseName,
409
- `ResponseErrorConfig<${errorResponses.length > 0 ? errorResponses.join(" | ") : "Error"}>`,
410
- requestName || "unknown"
411
- ].filter(Boolean);
412
- const paramsNode = createOperationParams(node, {
413
- paramsType: "object",
414
- pathParamsType: "inline",
415
- resolver,
416
- paramsCasing
417
- });
418
- const baseParamsSignature = declarationPrinter.print(paramsNode) ?? "";
419
- const paramsSignature = baseParamsSignature ? `${baseParamsSignature}, request: RequestHandlerExtra<ServerRequest, ServerNotification>` : "request: RequestHandlerExtra<ServerRequest, ServerNotification>";
420
- const pathParamsMapping = paramsCasing ? buildTransformedParamsMapping(originalPathParams, camelCase) : null;
421
- const queryParamsMapping = paramsCasing ? buildTransformedParamsMapping(originalQueryParams, camelCase) : null;
422
- const headerParamsMapping = paramsCasing ? buildTransformedParamsMapping(originalHeaderParams, camelCase) : null;
423
- const contentTypeHeader = contentType && contentType !== "application/json" && contentType !== "multipart/form-data" ? `'Content-Type': '${contentType}'` : null;
424
- const headers = [headerParams.length ? headerParamsMapping ? "...mappedHeaders" : "...headers" : null, contentTypeHeader].filter(Boolean);
425
- const fetchConfig = [];
426
- fetchConfig.push(`method: ${JSON.stringify(node.method.toUpperCase())}`);
427
- fetchConfig.push(`url: ${Url.toTemplateString(node.path)}`);
428
- if (baseURL) fetchConfig.push(`baseURL: \`${baseURL}\``);
429
- if (queryParams.length) fetchConfig.push(queryParamsMapping ? "params: mappedParams" : "params");
430
- if (requestName) fetchConfig.push(`data: ${isFormData ? "formData as FormData" : "requestData"}`);
431
- if (headers.length) fetchConfig.push(`headers: { ${headers.join(", ")} }`);
432
- const callToolResult = `return {
433
- content: [
434
- {
435
- type: 'text',
436
- text: JSON.stringify(${dataReturnType === "data" ? "res.data" : "res"})
437
- }
438
- ],
439
- structuredContent: { data: res.data }
440
- }`;
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(", ")} }` : "{}";
441
461
  return /* @__PURE__ */ jsx(File.Source, {
442
462
  name,
443
463
  isExportable: true,
@@ -450,28 +470,17 @@ function McpHandler({ name, node, resolver, baseURL, dataReturnType, paramsCasin
450
470
  JSDoc: { comments: buildOperationComments(node) },
451
471
  returnType: "Promise<CallToolResult>",
452
472
  children: [
453
- "",
473
+ `const res = await ${clientName}(${callConfig})`,
454
474
  /* @__PURE__ */ jsx("br", {}),
455
- /* @__PURE__ */ jsx("br", {}),
456
- pathParamsMapping && Object.entries(pathParamsMapping).filter(([originalName, camelCaseName]) => originalName !== camelCaseName && isValidVarName(originalName)).map(([originalName, camelCaseName]) => `const ${originalName} = ${camelCaseName}`).join("\n"),
457
- pathParamsMapping && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("br", {}), /* @__PURE__ */ jsx("br", {})] }),
458
- queryParamsMapping && queryParams.length > 0 && /* @__PURE__ */ jsxs(Fragment, { children: [
459
- buildRemappingCode(queryParamsMapping, "mappedParams", "params"),
460
- /* @__PURE__ */ jsx("br", {}),
461
- /* @__PURE__ */ jsx("br", {})
462
- ] }),
463
- headerParamsMapping && headerParams.length > 0 && /* @__PURE__ */ jsxs(Fragment, { children: [
464
- buildRemappingCode(headerParamsMapping, "mappedHeaders", "headers"),
465
- /* @__PURE__ */ jsx("br", {}),
466
- /* @__PURE__ */ jsx("br", {})
467
- ] }),
468
- requestName && "const requestData = data",
469
- /* @__PURE__ */ jsx("br", {}),
470
- isFormData && requestName && "const formData = buildFormData(requestData)",
471
- /* @__PURE__ */ jsx("br", {}),
472
- `const res = await client<${generics.join(", ")}>({ ${fetchConfig.join(", ")} }, request)`,
473
- /* @__PURE__ */ jsx("br", {}),
474
- callToolResult
475
+ `return {
476
+ content: [
477
+ {
478
+ type: 'text',
479
+ text: JSON.stringify(res.data)
480
+ }
481
+ ],
482
+ structuredContent: { data: res.data }
483
+ }`
475
484
  ]
476
485
  })
477
486
  });
@@ -512,37 +521,36 @@ function zodExprFromSchemaNode(schema) {
512
521
  //#endregion
513
522
  //#region src/components/Server.tsx
514
523
  const keysPrinter = functionPrinter({ mode: "call" });
515
- function Server({ name, serverName, serverVersion, paramsCasing, operations }) {
524
+ function Server({ name, serverName, serverVersion, operations }) {
516
525
  const registrations = operations.map(({ tool, mcp, zod, node }) => {
517
- const { path: pathParams } = getOperationParameters(node, { paramsCasing });
518
- const pathEntries = [];
519
- const otherEntries = [];
520
- for (const p of pathParams) {
521
- const zodParam = zod.pathParams.find((zp) => zp.name === p.name);
522
- pathEntries.push({
523
- key: p.name,
524
- 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} })`
525
536
  });
526
537
  }
527
- if (zod.requestName) otherEntries.push({
528
- key: "data",
529
- value: zod.requestName
530
- });
531
- if (zod.queryParams) otherEntries.push({
532
- key: "params",
538
+ if (zod.queryParams) entries.push({
539
+ key: "query",
533
540
  value: zodGroupExpr(zod.queryParams)
534
541
  });
535
- if (zod.headerParams) otherEntries.push({
542
+ if (zod.headerParams) entries.push({
536
543
  key: "headers",
537
544
  value: zodGroupExpr(zod.headerParams)
538
545
  });
539
- otherEntries.sort((a, b) => a.key.localeCompare(b.key));
540
- const entries = [...pathEntries, ...otherEntries];
541
- const paramsNode = entries.length ? ast.factory.createFunctionParameters({ params: [{
542
- kind: "FunctionParameter",
543
- name: ast.factory.createObjectBindingPattern({ elements: entries.map((e) => ({ 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 })) }),
544
552
  optional: false
545
- }] }) : null;
553
+ })] }) : null;
546
554
  const destructured = paramsNode ? keysPrinter.print(paramsNode) ?? "" : "";
547
555
  const inputSchema = entries.length ? `{ ${entries.map((e) => `${e.key}: ${e.value}`).join(", ")} }` : null;
548
556
  const outputSchema = zod.responseName;
@@ -604,12 +612,105 @@ return server`
604
612
  });
605
613
  }
606
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
607
708
  //#region src/generators/mcpGenerator.tsx
608
709
  /**
609
710
  * Built-in operation generator for `@kubb/plugin-mcp`. Emits one MCP tool
610
- * handler per OpenAPI operation, wiring the input Zod schema, the HTTP call,
611
- * and the response shape into a single function that an MCP server can
612
- * 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.
613
714
  */
614
715
  const mcpGenerator = defineGenerator({
615
716
  name: "mcp",
@@ -617,14 +718,19 @@ const mcpGenerator = defineGenerator({
617
718
  operation(node, ctx) {
618
719
  if (!ast.isHttpOperationNode(node)) return null;
619
720
  const { resolver, driver, root } = ctx;
620
- const { output, client, paramsCasing, group } = ctx.options;
721
+ const { output, client, group } = ctx.options;
621
722
  const pluginTs = driver.getPlugin(pluginTsName);
622
723
  if (!pluginTs) return null;
623
724
  const tsResolver = driver.getResolver(pluginTsName);
624
- const importedTypeNames = resolveOperationTypeNames(node, tsResolver, {
625
- paramsCasing,
626
- responseStatusNames: "error"
725
+ const contractOp = resolveClientOperation({
726
+ clientPlugin: { pluginName: client.pluginName },
727
+ driver,
728
+ node,
729
+ root,
730
+ output
627
731
  });
732
+ if (!contractOp) return null;
733
+ const requestConfigName = tsResolver.resolveRequestConfigName(node);
628
734
  const meta = {
629
735
  name: resolver.resolveHandlerName(node),
630
736
  file: resolver.resolveFile({
@@ -653,8 +759,8 @@ const mcpGenerator = defineGenerator({
653
759
  path: meta.file.path,
654
760
  meta: meta.file.meta,
655
761
  children: [
656
- meta.fileTs && importedTypeNames.length > 0 && /* @__PURE__ */ jsx(File.Import, {
657
- name: Array.from(new Set(importedTypeNames)).sort(),
762
+ meta.fileTs && requestConfigName && /* @__PURE__ */ jsx(File.Import, {
763
+ name: [requestConfigName],
658
764
  root: meta.file.path,
659
765
  path: meta.fileTs.path,
660
766
  isTypeOnly: true
@@ -674,59 +780,15 @@ const mcpGenerator = defineGenerator({
674
780
  isTypeOnly: true
675
781
  }),
676
782
  /* @__PURE__ */ jsx(File.Import, {
677
- name: ["buildFormData"],
783
+ name: [contractOp.name],
678
784
  root: meta.file.path,
679
- path: path.resolve(root, ".kubb/config.ts")
785
+ path: contractOp.path
680
786
  }),
681
- client.importPath ? /* @__PURE__ */ jsxs(Fragment, { children: [
682
- /* @__PURE__ */ jsx(File.Import, {
683
- name: [
684
- "Client",
685
- "RequestConfig",
686
- "ResponseErrorConfig"
687
- ],
688
- path: client.importPath,
689
- isTypeOnly: true
690
- }),
691
- /* @__PURE__ */ jsx(File.Import, {
692
- name: "client",
693
- path: client.importPath
694
- }),
695
- client.dataReturnType === "full" && /* @__PURE__ */ jsx(File.Import, {
696
- name: ["ResponseConfig"],
697
- path: client.importPath,
698
- isTypeOnly: true
699
- })
700
- ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
701
- /* @__PURE__ */ jsx(File.Import, {
702
- name: [
703
- "Client",
704
- "RequestConfig",
705
- "ResponseErrorConfig"
706
- ],
707
- root: meta.file.path,
708
- path: path.resolve(root, ".kubb/client.ts"),
709
- isTypeOnly: true
710
- }),
711
- /* @__PURE__ */ jsx(File.Import, {
712
- name: ["client"],
713
- root: meta.file.path,
714
- path: path.resolve(root, ".kubb/client.ts")
715
- }),
716
- client.dataReturnType === "full" && /* @__PURE__ */ jsx(File.Import, {
717
- name: ["ResponseConfig"],
718
- root: meta.file.path,
719
- path: path.resolve(root, ".kubb/client.ts"),
720
- isTypeOnly: true
721
- })
722
- ] }),
723
787
  /* @__PURE__ */ jsx(McpHandler, {
724
788
  name: meta.name,
789
+ clientName: contractOp.name,
725
790
  node,
726
- resolver: tsResolver,
727
- baseURL: client.baseURL,
728
- dataReturnType: client.dataReturnType || "data",
729
- paramsCasing
791
+ resolver: tsResolver
730
792
  })
731
793
  ]
732
794
  });
@@ -746,7 +808,7 @@ const serverGenerator = defineGenerator({
746
808
  renderer: jsxRenderer,
747
809
  operations(nodes, ctx) {
748
810
  const { config, resolver, plugin, driver, root } = ctx;
749
- const { output, paramsCasing, group } = ctx.options;
811
+ const { output, group } = ctx.options;
750
812
  const pluginZod = driver.getPlugin(pluginZodName);
751
813
  if (!pluginZod) return;
752
814
  const zodResolver = driver.getResolver(pluginZodName);
@@ -762,7 +824,7 @@ const serverGenerator = defineGenerator({
762
824
  meta: { pluginName: plugin.name }
763
825
  };
764
826
  const operationsMapped = nodes.filter(ast.isHttpOperationNode).map((node) => {
765
- const { path: pathParams, query: queryParams, header: headerParams } = getOperationParameters(node, { paramsCasing });
827
+ const { path: pathParams, query: queryParams, header: headerParams } = getOperationParameters(node);
766
828
  const mcpFile = resolver.resolveFile({
767
829
  name: node.operationId,
768
830
  extname: ".ts",
@@ -868,7 +930,6 @@ const serverGenerator = defineGenerator({
868
930
  name,
869
931
  serverName: ctx.meta.title ?? "server",
870
932
  serverVersion: ctx.meta.version ?? "0.0.0",
871
- paramsCasing,
872
933
  operations: operationsMapped
873
934
  })
874
935
  ]
@@ -934,13 +995,13 @@ const pluginMcpName = "plugin-mcp";
934
995
  /**
935
996
  * Generates a Model Context Protocol (MCP) server from an OpenAPI spec. Every
936
997
  * operation becomes a typed MCP tool that AI assistants (Claude Desktop, Claude
937
- * 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`.
938
1000
  *
939
1001
  * @example
940
1002
  * ```ts
941
1003
  * import { defineConfig } from 'kubb'
942
1004
  * import { pluginTs } from '@kubb/plugin-ts'
943
- * import { pluginClient } from '@kubb/plugin-client'
944
1005
  * import { pluginZod } from '@kubb/plugin-zod'
945
1006
  * import { pluginMcp } from '@kubb/plugin-mcp'
946
1007
  *
@@ -949,11 +1010,10 @@ const pluginMcpName = "plugin-mcp";
949
1010
  * output: { path: './src/gen' },
950
1011
  * plugins: [
951
1012
  * pluginTs(),
952
- * pluginClient(),
953
1013
  * pluginZod(),
954
1014
  * pluginMcp({
955
1015
  * output: { path: './mcp' },
956
- * client: { baseURL: 'https://petstore.swagger.io/v2' },
1016
+ * baseURL: 'https://petstore.swagger.io/v2',
957
1017
  * }),
958
1018
  * ],
959
1019
  * })
@@ -963,9 +1023,7 @@ const pluginMcp = definePlugin((options) => {
963
1023
  const { output = {
964
1024
  path: "mcp",
965
1025
  barrel: { type: "named" }
966
- }, group, exclude = [], include, override = [], paramsCasing, client, resolver: userResolver, macros: userMacros, generators: userGenerators = [] } = options;
967
- const clientName = client?.client ?? "axios";
968
- const clientImportPath = client?.importPath ?? (!client?.bundle ? `@kubb/plugin-client/clients/${clientName}` : void 0);
1026
+ }, group, exclude = [], include, override = [], client, resolver: userResolver, macros: userMacros } = options;
969
1027
  const groupConfig = createGroupConfig(group);
970
1028
  return {
971
1029
  name: pluginMcpName,
@@ -976,51 +1034,28 @@ const pluginMcp = definePlugin((options) => {
976
1034
  ...resolverMcp,
977
1035
  ...userResolver
978
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
+ };
979
1046
  ctx.setOptions({
980
1047
  output,
981
1048
  exclude,
982
1049
  include,
983
1050
  override,
984
1051
  group: groupConfig,
985
- paramsCasing,
986
- client: {
987
- client: clientName,
988
- clientType: client?.clientType ?? "function",
989
- importPath: clientImportPath,
990
- dataReturnType: client?.dataReturnType ?? "data",
991
- bundle: client?.bundle,
992
- baseURL: client?.baseURL,
993
- paramsCasing: client?.paramsCasing
994
- },
1052
+ client: resolvedClientDescriptor,
995
1053
  resolver
996
1054
  });
997
1055
  ctx.setResolver(resolver);
998
1056
  if (userMacros?.length) ctx.setMacros(userMacros);
999
1057
  ctx.addGenerator(mcpGenerator);
1000
1058
  ctx.addGenerator(serverGenerator);
1001
- for (const gen of userGenerators) ctx.addGenerator(gen);
1002
- const root = path.resolve(ctx.config.root, ctx.config.output.path);
1003
- const hasClientPlugin = ctx.config.plugins?.some((p) => p.name === pluginClientName);
1004
- if (client?.bundle && !hasClientPlugin && !clientImportPath) ctx.injectFile({
1005
- baseName: "client.ts",
1006
- path: path.resolve(root, ".kubb/client.ts"),
1007
- sources: [ast.factory.createSource({
1008
- name: "client",
1009
- nodes: [ast.factory.createText(clientName === "fetch" ? source$1 : source)],
1010
- isExportable: true,
1011
- isIndexable: true
1012
- })]
1013
- });
1014
- if (!hasClientPlugin) ctx.injectFile({
1015
- baseName: "config.ts",
1016
- path: path.resolve(root, ".kubb/config.ts"),
1017
- sources: [ast.factory.createSource({
1018
- name: "config",
1019
- nodes: [ast.factory.createText(source$2)],
1020
- isExportable: false,
1021
- isIndexable: false
1022
- })]
1023
- });
1024
1059
  } }
1025
1060
  };
1026
1061
  });