@kubb/plugin-mcp 5.0.0-beta.10 → 5.0.0-beta.100

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
@@ -24,316 +24,137 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
24
24
  enumerable: true
25
25
  }) : target, mod));
26
26
  //#endregion
27
+ let kubb_kit = require("kubb/kit");
27
28
  let node_path = require("node:path");
28
- let node_path$1 = __toESM(node_path, 1);
29
- node_path = __toESM(node_path);
30
- let _kubb_core = require("@kubb/core");
29
+ node_path = __toESM(node_path, 1);
30
+ let kubb_jsx = require("kubb/jsx");
31
+ let kubb_jsx_jsx_runtime = require("kubb/jsx/jsx-runtime");
31
32
  let _kubb_plugin_ts = require("@kubb/plugin-ts");
32
- let _kubb_renderer_jsx = require("@kubb/renderer-jsx");
33
- let _kubb_renderer_jsx_jsx_runtime = require("@kubb/renderer-jsx/jsx-runtime");
34
33
  let _kubb_plugin_zod = require("@kubb/plugin-zod");
35
- let _kubb_plugin_client = require("@kubb/plugin-client");
36
- let _kubb_plugin_client_templates_clients_axios_source = require("@kubb/plugin-client/templates/clients/axios.source");
37
- let _kubb_plugin_client_templates_clients_fetch_source = require("@kubb/plugin-client/templates/clients/fetch.source");
38
- let _kubb_plugin_client_templates_config_source = require("@kubb/plugin-client/templates/config.source");
39
- //#region ../../internals/utils/src/casing.ts
34
+ //#region ../../internals/shared/src/params.ts
40
35
  /**
41
- * Shared implementation for camelCase and PascalCase conversion.
42
- * Splits on common word boundaries (spaces, hyphens, underscores, dots, slashes, colons)
43
- * and capitalizes each word according to `pascal`.
36
+ * Drops parameters that share the same name, keeping the first.
44
37
  *
45
- * When `pascal` is `true` the first word is also capitalized (PascalCase), otherwise only subsequent words are.
38
+ * A malformed spec can declare the same parameter name twice within one `in` location. Both would
39
+ * resolve to the same output property, so emitting both would yield an object type with a duplicate
40
+ * member, which TypeScript rejects. This is a defensive guard against that case, not a casing guard:
41
+ * parameter names flow through unchanged, so no two distinct names ever collide here anymore.
46
42
  */
47
- function toCamelOrPascal(text, pascal) {
48
- return text.trim().replace(/([a-z\d])([A-Z])/g, "$1 $2").replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2").replace(/(\d)([a-z])/g, "$1 $2").split(/[\s\-_./\\:]+/).filter(Boolean).map((word, i) => {
49
- if (word.length > 1 && word === word.toUpperCase()) return word;
50
- if (i === 0 && !pascal) return word.charAt(0).toLowerCase() + word.slice(1);
51
- return word.charAt(0).toUpperCase() + word.slice(1);
52
- }).join("").replace(/[^a-zA-Z0-9]/g, "");
43
+ function dedupeParams(params) {
44
+ const seen = /* @__PURE__ */ new Set();
45
+ return params.filter((param) => {
46
+ if (seen.has(param.name)) return false;
47
+ seen.add(param.name);
48
+ return true;
49
+ });
53
50
  }
51
+ //#endregion
52
+ //#region ../../internals/shared/src/operation.ts
54
53
  /**
55
- * Splits `text` on `.` and applies `transformPart` to each segment.
56
- * The last segment receives `isLast = true`, all earlier segments receive `false`.
57
- * Segments are joined with `/` to form a file path.
54
+ * Builds the `ResolverFileParams` every operation generator passes to
55
+ * `resolver.file`: a file named `name`, tagged by the operation's first
56
+ * tag (or `'default'`), at the operation's path. Centralizes the entry object
57
+ * that was repeated at dozens of call sites across the client and query plugins.
58
58
  *
59
- * Only splits on dots followed by a letter so that version numbers
60
- * embedded in operationIds (e.g. `v2025.0`) are kept intact.
59
+ * @example
60
+ * ```ts
61
+ * resolver.file(operationFileEntry(node, node.operationId), { root, output, group })
62
+ * ```
61
63
  */
62
- function applyToFileParts(text, transformPart) {
63
- const parts = text.split(/\.(?=[a-zA-Z])/);
64
- return parts.map((part, i) => transformPart(part, i === parts.length - 1)).join("/");
64
+ function operationFileEntry(node, name, extname = ".ts") {
65
+ return {
66
+ name,
67
+ extname,
68
+ tag: node.tags[0] ?? "default",
69
+ path: node.path
70
+ };
71
+ }
72
+ function getOperationLink(node, link) {
73
+ if (!link) return null;
74
+ if (typeof link === "function") return link(node) ?? null;
75
+ return node.path ? `{@link ${kubb_kit.Url.toPath(node.path)}}` : null;
65
76
  }
66
77
  /**
67
- * Converts `text` to camelCase.
68
- * When `isFile` is `true`, dot-separated segments are each cased independently and joined with `/`.
69
- *
70
- * @example
71
- * camelCase('hello-world') // 'helloWorld'
72
- * camelCase('pet.petId', { isFile: true }) // 'pet/petId'
78
+ * Derives the shared `ContentTypeInfo` shape from a list of content types, tracking whether several
79
+ * are present and the union, default, and form-data flags the client uses to pick one.
73
80
  */
74
- function camelCase(text, { isFile, prefix = "", suffix = "" } = {}) {
75
- if (isFile) return applyToFileParts(text, (part, isLast) => camelCase(part, isLast ? {
76
- prefix,
77
- suffix
78
- } : {}));
79
- return toCamelOrPascal(`${prefix} ${text} ${suffix}`, false);
81
+ function buildContentTypeInfo(contentTypes) {
82
+ const isMultipleContentTypes = contentTypes.length > 1;
83
+ return {
84
+ contentTypes,
85
+ isMultipleContentTypes,
86
+ contentTypeUnion: isMultipleContentTypes ? contentTypes.map((ct) => JSON.stringify(ct)).join(" | ") : "",
87
+ defaultContentType: contentTypes[0] ?? "application/json",
88
+ hasFormData: contentTypes.some((ct) => ct === "multipart/form-data")
89
+ };
90
+ }
91
+ function getContentTypeInfo(node) {
92
+ return buildContentTypeInfo(node.requestBody?.content?.map((e) => e.contentType) ?? []);
80
93
  }
81
- //#endregion
82
- //#region ../../internals/utils/src/reserved.ts
83
94
  /**
84
- * JavaScript and Java reserved words.
85
- * @link https://github.com/jonschlinkert/reserved/blob/master/index.js
95
+ * The request-body counterpart for the primary success response: the content types it documents and
96
+ * whether several are present, so the client can let a caller pick which one to accept.
86
97
  */
87
- const reservedWords = new Set([
88
- "abstract",
89
- "arguments",
90
- "boolean",
91
- "break",
92
- "byte",
93
- "case",
94
- "catch",
95
- "char",
96
- "class",
97
- "const",
98
- "continue",
99
- "debugger",
100
- "default",
101
- "delete",
102
- "do",
103
- "double",
104
- "else",
105
- "enum",
106
- "eval",
107
- "export",
108
- "extends",
109
- "false",
110
- "final",
111
- "finally",
112
- "float",
113
- "for",
114
- "function",
115
- "goto",
116
- "if",
117
- "implements",
118
- "import",
119
- "in",
120
- "instanceof",
121
- "int",
122
- "interface",
123
- "let",
124
- "long",
125
- "native",
126
- "new",
127
- "null",
128
- "package",
129
- "private",
130
- "protected",
131
- "public",
132
- "return",
133
- "short",
134
- "static",
135
- "super",
136
- "switch",
137
- "synchronized",
138
- "this",
139
- "throw",
140
- "throws",
141
- "transient",
142
- "true",
143
- "try",
144
- "typeof",
145
- "var",
146
- "void",
147
- "volatile",
148
- "while",
149
- "with",
150
- "yield",
151
- "Array",
152
- "Date",
153
- "hasOwnProperty",
154
- "Infinity",
155
- "isFinite",
156
- "isNaN",
157
- "isPrototypeOf",
158
- "length",
159
- "Math",
160
- "name",
161
- "NaN",
162
- "Number",
163
- "Object",
164
- "prototype",
165
- "String",
166
- "toString",
167
- "undefined",
168
- "valueOf"
169
- ]);
98
+ function getResponseContentTypeInfo(node) {
99
+ return buildContentTypeInfo(getPrimarySuccessResponse(node)?.content?.map((e) => e.contentType) ?? []);
100
+ }
101
+ function buildRequestConfigType(node) {
102
+ const request = getContentTypeInfo(node);
103
+ const response = getResponseContentTypeInfo(node);
104
+ const configType = `Partial<Omit<RequestConfig, 'path' | 'query' | 'body' | 'headers' | 'url'>>`;
105
+ const members = [request.isMultipleContentTypes ? `request?: ${request.contentTypeUnion}` : null, response.isMultipleContentTypes ? `response?: ${response.contentTypeUnion}` : null].filter(Boolean);
106
+ return members.length ? `${configType} & { contentType?: { ${members.join("; ")} } }` : configType;
107
+ }
170
108
  /**
171
- * Returns `true` when `name` is a syntactically valid JavaScript variable name.
172
- *
173
- * @example
174
- * ```ts
175
- * isValidVarName('status') // true
176
- * isValidVarName('class') // false (reserved word)
177
- * isValidVarName('42foo') // false (starts with digit)
178
- * ```
109
+ * Which of the grouped request options an operation carries.
179
110
  */
180
- function isValidVarName(name) {
181
- if (!name || reservedWords.has(name)) return false;
182
- return /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name);
111
+ function getRequestGroups(node) {
112
+ const { path, query, header } = getOperationParameters(node);
113
+ return {
114
+ path: path.length > 0,
115
+ query: query.length > 0,
116
+ body: Boolean(node.requestBody?.content?.[0]?.schema),
117
+ headers: header.length > 0
118
+ };
183
119
  }
184
- //#endregion
185
- //#region ../../internals/utils/src/urlPath.ts
186
120
  /**
187
- * Parses and transforms an OpenAPI/Swagger path string into various URL formats.
188
- *
189
- * @example
190
- * const p = new URLPath('/pet/{petId}')
191
- * p.URL // '/pet/:petId'
192
- * p.template // '`/pet/${petId}`'
121
+ * Resolves which grouped request options an operation carries together with whether each group
122
+ * holds a required member. The grouped parameter stays optional only when nothing inside it is
123
+ * required, matching the generated `RequestConfig` type.
193
124
  */
194
- var URLPath = class {
195
- /**
196
- * The raw OpenAPI/Swagger path string, e.g. `/pet/{petId}`.
197
- */
198
- path;
199
- #options;
200
- constructor(path, options = {}) {
201
- this.path = path;
202
- this.#options = options;
203
- }
204
- /** Converts the OpenAPI path to Express-style colon syntax, e.g. `/pet/{petId}` → `/pet/:petId`.
205
- *
206
- * @example
207
- * ```ts
208
- * new URLPath('/pet/{petId}').URL // '/pet/:petId'
209
- * ```
210
- */
211
- get URL() {
212
- return this.toURLPath();
213
- }
214
- /** Returns `true` when `path` is a fully-qualified URL (e.g. starts with `https://`).
215
- *
216
- * @example
217
- * ```ts
218
- * new URLPath('https://petstore.swagger.io/v2/pet').isURL // true
219
- * new URLPath('/pet/{petId}').isURL // false
220
- * ```
221
- */
222
- get isURL() {
223
- try {
224
- return !!new URL(this.path).href;
225
- } catch {
226
- return false;
227
- }
228
- }
229
- /**
230
- * Converts the OpenAPI path to a TypeScript template literal string.
231
- *
232
- * @example
233
- * new URLPath('/pet/{petId}').template // '`/pet/${petId}`'
234
- * new URLPath('/account/monetary-accountID').template // '`/account/${monetaryAccountId}`'
235
- */
236
- get template() {
237
- return this.toTemplateString();
238
- }
239
- /** Returns the path and its extracted params as a structured `URLObject`, or as a stringified expression when `stringify` is set.
240
- *
241
- * @example
242
- * ```ts
243
- * new URLPath('/pet/{petId}').object
244
- * // { url: '/pet/:petId', params: { petId: 'petId' } }
245
- * ```
246
- */
247
- get object() {
248
- return this.toObject();
249
- }
250
- /** Returns a map of path parameter names, or `undefined` when the path has no parameters.
251
- *
252
- * @example
253
- * ```ts
254
- * new URLPath('/pet/{petId}').params // { petId: 'petId' }
255
- * new URLPath('/pet').params // undefined
256
- * ```
257
- */
258
- get params() {
259
- return this.toParamsObject();
260
- }
261
- #transformParam(raw) {
262
- const param = isValidVarName(raw) ? raw : camelCase(raw);
263
- return this.#options.casing === "camelcase" ? camelCase(param) : param;
264
- }
265
- /**
266
- * Iterates over every `{param}` token in `path`, calling `fn` with the raw token and transformed name.
267
- */
268
- #eachParam(fn) {
269
- for (const match of this.path.matchAll(/\{([^}]+)\}/g)) {
270
- const raw = match[1];
271
- fn(raw, this.#transformParam(raw));
272
- }
273
- }
274
- toObject({ type = "path", replacer, stringify } = {}) {
275
- const object = {
276
- url: type === "path" ? this.toURLPath() : this.toTemplateString({ replacer }),
277
- params: this.toParamsObject()
278
- };
279
- if (stringify) {
280
- if (type === "template") return JSON.stringify(object).replaceAll("'", "").replaceAll(`"`, "");
281
- if (object.params) return `{ url: '${object.url}', params: ${JSON.stringify(object.params).replaceAll("'", "").replaceAll(`"`, "")} }`;
282
- return `{ url: '${object.url}' }`;
283
- }
284
- return object;
285
- }
286
- /**
287
- * Converts the OpenAPI path to a TypeScript template literal string.
288
- * An optional `replacer` can transform each extracted parameter name before interpolation.
289
- *
290
- * @example
291
- * new URLPath('/pet/{petId}').toTemplateString() // '`/pet/${petId}`'
292
- */
293
- toTemplateString({ prefix = "", replacer } = {}) {
294
- return `\`${prefix}${this.path.split(/\{([^}]+)\}/).map((part, i) => {
295
- if (i % 2 === 0) return part;
296
- const param = this.#transformParam(part);
297
- return `\${${replacer ? replacer(param) : param}}`;
298
- }).join("")}\``;
299
- }
300
- /**
301
- * Extracts all `{param}` segments from the path and returns them as a key-value map.
302
- * An optional `replacer` transforms each parameter name in both key and value positions.
303
- * Returns `undefined` when no path parameters are found.
304
- *
305
- * @example
306
- * ```ts
307
- * new URLPath('/pet/{petId}/tag/{tagId}').toParamsObject()
308
- * // { petId: 'petId', tagId: 'tagId' }
309
- * ```
310
- */
311
- toParamsObject(replacer) {
312
- const params = {};
313
- this.#eachParam((_raw, param) => {
314
- const key = replacer ? replacer(param) : param;
315
- params[key] = key;
316
- });
317
- return Object.keys(params).length > 0 ? params : void 0;
318
- }
319
- /** Converts the OpenAPI path to Express-style colon syntax.
320
- *
321
- * @example
322
- * ```ts
323
- * new URLPath('/pet/{petId}').toURLPath() // '/pet/:petId'
324
- * ```
325
- */
326
- toURLPath() {
327
- return this.path.replace(/\{([^}]+)\}/g, ":$1");
328
- }
329
- };
330
- //#endregion
331
- //#region ../../internals/shared/src/operation.ts
332
- function getOperationLink(node, link) {
333
- if (!link) return;
334
- if (typeof link === "function") return link(node);
335
- if (link === "urlPath") return node.path ? `{@link ${new URLPath(node.path).URL}}` : void 0;
336
- return `{@link ${node.path.replaceAll("{", ":").replaceAll("}", "")}}`;
125
+ function getRequestGroupOptionality(node) {
126
+ const groups = getRequestGroups(node);
127
+ const { path, query, header } = getOperationParameters(node);
128
+ const hasRequiredPath = path.some((param) => param.required);
129
+ const hasRequiredQuery = query.some((param) => param.required);
130
+ const hasRequiredHeader = header.some((param) => param.required);
131
+ return {
132
+ groups,
133
+ hasRequiredPath,
134
+ hasRequiredQuery,
135
+ hasRequiredHeader,
136
+ isOptional: !hasRequiredPath && !hasRequiredQuery && !hasRequiredHeader && !groups.body
137
+ };
138
+ }
139
+ /**
140
+ * Builds the grouped `{ path, query, body, headers }` parameter for a generated client
141
+ * function, typed from the operation's `Options` (minus `url`). Only the groups the
142
+ * operation actually has are destructured. The trailing `config` parameter carries the
143
+ * runtime `RequestConfig` overrides plus `client`.
144
+ */
145
+ function buildRequestParamsSignature(node, resolver, options = {}) {
146
+ const { isConfigurable = true } = options;
147
+ const { groups, isOptional } = getRequestGroupOptionality(node);
148
+ const names = [
149
+ "path",
150
+ "query",
151
+ "body",
152
+ "headers"
153
+ ].filter((key) => groups[key]);
154
+ return {
155
+ signature: [names.length > 0 ? `{ ${names.join(", ")} }: ${resolver.response.options(node)}${isOptional ? " = {}" : ""}` : null, isConfigurable ? `config: ${buildRequestConfigType(node)} = {}` : null].filter(Boolean).join(", "),
156
+ groups
157
+ };
337
158
  }
338
159
  function buildOperationComments(node, options = {}) {
339
160
  const { link = "pathTemplate", linkPosition = "afterDeprecated", splitLines = false } = options;
@@ -352,140 +173,112 @@ function buildOperationComments(node, options = {}) {
352
173
  if (!splitLines) return filteredComments;
353
174
  return filteredComments.flatMap((text) => text.split(/\r?\n/).map((line) => line.trim())).filter((comment) => Boolean(comment));
354
175
  }
355
- function getOperationParameters(node, options = {}) {
356
- const params = _kubb_core.ast.caseParams(node.parameters, options.paramsCasing);
176
+ function getOperationParameters(node) {
357
177
  return {
358
- path: params.filter((param) => param.in === "path"),
359
- query: params.filter((param) => param.in === "query"),
360
- header: params.filter((param) => param.in === "header"),
361
- cookie: params.filter((param) => param.in === "cookie")
178
+ path: dedupeParams(node.parameters.filter((param) => param.in === "path")),
179
+ query: dedupeParams(node.parameters.filter((param) => param.in === "query")),
180
+ header: dedupeParams(node.parameters.filter((param) => param.in === "header")),
181
+ cookie: dedupeParams(node.parameters.filter((param) => param.in === "cookie"))
362
182
  };
363
183
  }
364
184
  function getStatusCodeNumber(statusCode) {
365
185
  const code = Number(statusCode);
366
- return Number.isNaN(code) ? void 0 : code;
186
+ return Number.isNaN(code) ? null : code;
367
187
  }
368
188
  function isSuccessStatusCode(statusCode) {
369
189
  const code = getStatusCodeNumber(statusCode);
370
- return code !== void 0 && code >= 200 && code < 300;
371
- }
372
- function isErrorStatusCode(statusCode) {
373
- const code = getStatusCodeNumber(statusCode);
374
- return code !== void 0 && code >= 400;
190
+ return code !== null && code >= 200 && code < 300;
375
191
  }
376
- function resolveErrorNames(node, resolver) {
377
- return node.responses.filter((response) => isErrorStatusCode(response.statusCode)).map((response) => resolver.resolveResponseStatusName(node, response.statusCode));
192
+ function getSuccessResponses(responses) {
193
+ return responses.filter((response) => isSuccessStatusCode(response.statusCode));
378
194
  }
379
- function resolveStatusCodeNames(node, resolver) {
380
- return node.responses.map((response) => resolver.resolveResponseStatusName(node, response.statusCode));
195
+ function getOperationSuccessResponses(node) {
196
+ return getSuccessResponses(node.responses);
381
197
  }
382
- function resolveOperationTypeNames(node, resolver, options = {}) {
383
- const { path, query, header } = getOperationParameters(node, { paramsCasing: options.paramsCasing });
384
- const responseStatusNames = options.responseStatusNames === "error" ? resolveErrorNames(node, resolver) : options.responseStatusNames === false ? [] : resolveStatusCodeNames(node, resolver);
385
- const exclude = new Set(options.exclude ?? []);
386
- const paramNames = [
387
- ...path.map((param) => resolver.resolvePathParamsName(node, param)),
388
- ...query.map((param) => resolver.resolveQueryParamsName(node, param)),
389
- ...header.map((param) => resolver.resolveHeaderParamsName(node, param))
390
- ];
391
- const bodyAndResponseNames = [node.requestBody?.content?.[0]?.schema ? resolver.resolveDataName(node) : void 0, resolver.resolveResponseName(node)];
392
- return (options.order === "body-response-first" ? [
393
- ...bodyAndResponseNames,
394
- ...paramNames,
395
- ...responseStatusNames
396
- ] : [
397
- ...paramNames,
398
- ...bodyAndResponseNames,
399
- ...responseStatusNames
400
- ]).filter((name) => Boolean(name) && !exclude.has(name));
198
+ function getPrimarySuccessResponse(node) {
199
+ return getOperationSuccessResponses(node)[0] ?? null;
401
200
  }
402
201
  function findSuccessStatusCode(responses) {
403
202
  for (const response of responses) if (isSuccessStatusCode(response.statusCode)) return response.statusCode;
203
+ return null;
404
204
  }
405
205
  //#endregion
406
- //#region ../../internals/shared/src/params.ts
407
- function buildParamsMapping(originalParams, mappedParams) {
408
- const mapping = {};
409
- let hasChanged = false;
410
- originalParams.forEach((param, i) => {
411
- const mappedName = mappedParams[i]?.name ?? param.name;
412
- mapping[param.name] = mappedName;
413
- if (param.name !== mappedName) hasChanged = true;
414
- });
415
- return hasChanged ? mapping : void 0;
206
+ //#region ../../internals/utils/src/casing.ts
207
+ /**
208
+ * Shared implementation for camelCase and PascalCase conversion.
209
+ * Splits on common word boundaries (spaces, hyphens, underscores, dots, slashes, colons)
210
+ * and capitalizes each word according to `pascal`.
211
+ *
212
+ * When `pascal` is `true` the first word is also capitalized (PascalCase), otherwise only subsequent words are.
213
+ */
214
+ function toCamelOrPascal(text, pascal) {
215
+ return text.trim().replace(/([a-z\d])([A-Z])/g, "$1 $2").replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2").replace(/(\d)([a-z])/g, "$1 $2").split(/[\s\-_./\\:]+/).filter(Boolean).map((word, i) => {
216
+ if (word.length > 1 && word === word.toUpperCase()) return word;
217
+ return (i === 0 && !pascal ? word.charAt(0).toLowerCase() : word.charAt(0).toUpperCase()) + word.slice(1);
218
+ }).join("").replace(/[^a-zA-Z0-9]/g, "");
416
219
  }
417
- function buildTransformedParamsMapping(params, transformName) {
418
- if (!params.length) return;
419
- return buildParamsMapping(params, params.map((param) => ({
420
- ...param,
421
- name: transformName(param.name)
422
- })));
220
+ /**
221
+ * Converts `text` to camelCase.
222
+ *
223
+ * @example Word boundaries
224
+ * `camelCase('hello-world') // 'helloWorld'`
225
+ *
226
+ * @example With a prefix
227
+ * `camelCase('tag', { prefix: 'create' }) // 'createTag'`
228
+ */
229
+ function camelCase(text, { prefix = "", suffix = "" } = {}) {
230
+ return toCamelOrPascal(`${prefix} ${text} ${suffix}`, false);
423
231
  }
424
232
  //#endregion
425
- //#region src/components/McpHandler.tsx
233
+ //#region ../../internals/shared/src/group.ts
426
234
  /**
427
- * Generate a remapping statement: `const mappedX = x ? { "orig": x.camel, ... } : undefined`
235
+ * Builds the `group` config a Kubb plugin passes to `ctx.setOptions`, applying the
236
+ * shared default naming so every plugin groups output consistently:
237
+ *
238
+ * - `path` groups use the second path segment (`/pet/findByStatus` → `pet`).
239
+ * - other groups use the camelCased group (`pet store` → `petStore`).
240
+ *
241
+ * A user-provided `group.name` always wins over the default namer, so callers stay in
242
+ * control of their output folders. Returns `null` when grouping is disabled, matching the
243
+ * per-plugin convention.
244
+ *
245
+ * @param group - The user-supplied group option, or `undefined` to disable grouping.
246
+ *
247
+ * @example
248
+ * ```ts
249
+ * createGroupConfig(group) // shared across every plugin
250
+ * ```
428
251
  */
429
- function buildRemappingCode(mapping, varName, sourceName) {
430
- return `const ${varName} = ${sourceName} ? { ${Object.entries(mapping).map(([orig, camel]) => `"${orig}": ${sourceName}.${camel}`).join(", ")} } : undefined`;
252
+ function createGroupConfig(group) {
253
+ if (!group) return null;
254
+ const defaultName = (ctx) => {
255
+ if (group.type === "path") return `${ctx.group.split("/")[1]}`;
256
+ return camelCase(ctx.group);
257
+ };
258
+ return {
259
+ ...group,
260
+ name: group.name ? group.name : defaultName
261
+ };
431
262
  }
432
- const declarationPrinter = (0, _kubb_plugin_ts.functionPrinter)({ mode: "declaration" });
433
- function McpHandler({ name, node, resolver, baseURL, dataReturnType, paramsCasing }) {
434
- const urlPath = new URLPath(node.path);
435
- const contentType = node.requestBody?.content?.[0]?.contentType;
436
- const isFormData = contentType === "multipart/form-data";
437
- const { query: queryParams, header: headerParams } = getOperationParameters(node, { paramsCasing });
438
- const { path: originalPathParams, query: originalQueryParams, header: originalHeaderParams } = getOperationParameters(node);
439
- const requestName = node.requestBody?.content?.[0]?.schema ? resolver.resolveDataName(node) : void 0;
440
- const responseName = resolver.resolveResponseName(node);
441
- const errorResponses = node.responses.filter((r) => Number(r.statusCode) >= 400).map((r) => resolver.resolveResponseStatusName(node, r.statusCode));
442
- const generics = [
443
- responseName,
444
- `ResponseErrorConfig<${errorResponses.length > 0 ? errorResponses.join(" | ") : "Error"}>`,
445
- requestName || "unknown"
446
- ].filter(Boolean);
447
- const paramsNode = _kubb_core.ast.createOperationParams(node, {
448
- paramsType: "object",
449
- pathParamsType: "inline",
450
- resolver,
451
- paramsCasing
452
- });
453
- const baseParamsSignature = declarationPrinter.print(paramsNode) ?? "";
454
- const paramsSignature = baseParamsSignature ? `${baseParamsSignature}, request: RequestHandlerExtra<ServerRequest, ServerNotification>` : "request: RequestHandlerExtra<ServerRequest, ServerNotification>";
455
- const pathParamsMapping = paramsCasing ? buildTransformedParamsMapping(originalPathParams, camelCase) : void 0;
456
- const queryParamsMapping = paramsCasing ? buildTransformedParamsMapping(originalQueryParams, camelCase) : void 0;
457
- const headerParamsMapping = paramsCasing ? buildTransformedParamsMapping(originalHeaderParams, camelCase) : void 0;
458
- const contentTypeHeader = contentType && contentType !== "application/json" && contentType !== "multipart/form-data" ? `'Content-Type': '${contentType}'` : void 0;
459
- const headers = [headerParams.length ? headerParamsMapping ? "...mappedHeaders" : "...headers" : void 0, contentTypeHeader].filter(Boolean);
460
- const fetchConfig = [];
461
- fetchConfig.push(`method: ${JSON.stringify(node.method.toUpperCase())}`);
462
- fetchConfig.push(`url: ${urlPath.template}`);
463
- if (baseURL) fetchConfig.push(`baseURL: \`${baseURL}\``);
464
- if (queryParams.length) fetchConfig.push(queryParamsMapping ? "params: mappedParams" : "params");
465
- if (requestName) fetchConfig.push(`data: ${isFormData ? "formData as FormData" : "requestData"}`);
466
- if (headers.length) fetchConfig.push(`headers: { ${headers.join(", ")} }`);
467
- const callToolResult = dataReturnType === "data" ? `return {
468
- content: [
469
- {
470
- type: 'text',
471
- text: JSON.stringify(res.data)
472
- }
473
- ],
474
- structuredContent: { data: res.data }
475
- }` : `return {
476
- content: [
477
- {
478
- type: 'text',
479
- text: JSON.stringify(res)
480
- }
481
- ],
482
- structuredContent: { data: res.data }
483
- }`;
484
- return /* @__PURE__ */ (0, _kubb_renderer_jsx_jsx_runtime.jsx)(_kubb_renderer_jsx.File.Source, {
263
+ //#endregion
264
+ //#region src/components/McpHandler.tsx
265
+ const requestGroupOrder = [
266
+ "path",
267
+ "query",
268
+ "headers",
269
+ "body"
270
+ ];
271
+ function McpHandler({ name, clientName, node, resolver }) {
272
+ if (!kubb_kit.ast.isHttpOperationNode(node)) return null;
273
+ const { signature, groups } = buildRequestParamsSignature(node, resolver, { isConfigurable: false });
274
+ const paramsSignature = [signature, "request: RequestHandlerExtra<ServerRequest, ServerNotification>"].filter(Boolean).join(", ");
275
+ const callArgs = requestGroupOrder.filter((key) => groups[key]);
276
+ const callConfig = callArgs.length ? `{ ${callArgs.join(", ")} }` : "{}";
277
+ return /* @__PURE__ */ (0, kubb_jsx_jsx_runtime.jsx)(kubb_jsx.File.Source, {
485
278
  name,
486
279
  isExportable: true,
487
280
  isIndexable: true,
488
- children: /* @__PURE__ */ (0, _kubb_renderer_jsx_jsx_runtime.jsxs)(_kubb_renderer_jsx.Function, {
281
+ children: /* @__PURE__ */ (0, kubb_jsx_jsx_runtime.jsxs)(kubb_jsx.Function, {
489
282
  name,
490
283
  async: true,
491
284
  export: true,
@@ -493,28 +286,17 @@ function McpHandler({ name, node, resolver, baseURL, dataReturnType, paramsCasin
493
286
  JSDoc: { comments: buildOperationComments(node) },
494
287
  returnType: "Promise<CallToolResult>",
495
288
  children: [
496
- "",
497
- /* @__PURE__ */ (0, _kubb_renderer_jsx_jsx_runtime.jsx)("br", {}),
498
- /* @__PURE__ */ (0, _kubb_renderer_jsx_jsx_runtime.jsx)("br", {}),
499
- pathParamsMapping && Object.entries(pathParamsMapping).filter(([originalName, camelCaseName]) => originalName !== camelCaseName && isValidVarName(originalName)).map(([originalName, camelCaseName]) => `const ${originalName} = ${camelCaseName}`).join("\n"),
500
- 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", {})] }),
501
- queryParamsMapping && queryParams.length > 0 && /* @__PURE__ */ (0, _kubb_renderer_jsx_jsx_runtime.jsxs)(_kubb_renderer_jsx_jsx_runtime.Fragment, { children: [
502
- buildRemappingCode(queryParamsMapping, "mappedParams", "params"),
503
- /* @__PURE__ */ (0, _kubb_renderer_jsx_jsx_runtime.jsx)("br", {}),
504
- /* @__PURE__ */ (0, _kubb_renderer_jsx_jsx_runtime.jsx)("br", {})
505
- ] }),
506
- headerParamsMapping && headerParams.length > 0 && /* @__PURE__ */ (0, _kubb_renderer_jsx_jsx_runtime.jsxs)(_kubb_renderer_jsx_jsx_runtime.Fragment, { children: [
507
- buildRemappingCode(headerParamsMapping, "mappedHeaders", "headers"),
508
- /* @__PURE__ */ (0, _kubb_renderer_jsx_jsx_runtime.jsx)("br", {}),
509
- /* @__PURE__ */ (0, _kubb_renderer_jsx_jsx_runtime.jsx)("br", {})
510
- ] }),
511
- requestName && "const requestData = data",
512
- /* @__PURE__ */ (0, _kubb_renderer_jsx_jsx_runtime.jsx)("br", {}),
513
- isFormData && requestName && "const formData = buildFormData(requestData)",
514
- /* @__PURE__ */ (0, _kubb_renderer_jsx_jsx_runtime.jsx)("br", {}),
515
- `const res = await fetch<${generics.join(", ")}>({ ${fetchConfig.join(", ")} }, request)`,
516
- /* @__PURE__ */ (0, _kubb_renderer_jsx_jsx_runtime.jsx)("br", {}),
517
- callToolResult
289
+ `const res = await ${clientName}(${callConfig})`,
290
+ /* @__PURE__ */ (0, kubb_jsx_jsx_runtime.jsx)("br", {}),
291
+ `return {
292
+ content: [
293
+ {
294
+ type: 'text',
295
+ text: JSON.stringify(res.data)
296
+ }
297
+ ],
298
+ structuredContent: { data: res.data }
299
+ }`
518
300
  ]
519
301
  })
520
302
  });
@@ -534,91 +316,66 @@ function zodGroupExpr(entry) {
534
316
  * Used as fallback when no named zod schema is available for a path parameter.
535
317
  */
536
318
  function zodExprFromSchemaNode(schema) {
537
- let expr;
538
- switch (schema.type) {
539
- case "enum": {
319
+ const baseExpr = (() => {
320
+ if (schema.type === "enum") {
540
321
  const rawValues = schema.namedEnumValues?.length ? schema.namedEnumValues.map((v) => v.value) : (schema.enumValues ?? []).filter((v) => v !== null);
541
- if (rawValues.length > 0 && rawValues.every((v) => typeof v === "string")) expr = `z.enum([${rawValues.map((v) => JSON.stringify(v)).join(", ")}])`;
542
- else if (rawValues.length > 0) {
322
+ if (rawValues.length > 0 && rawValues.every((v) => typeof v === "string")) return `z.enum([${rawValues.map((v) => JSON.stringify(v)).join(", ")}])`;
323
+ if (rawValues.length > 0) {
543
324
  const literals = rawValues.map((v) => `z.literal(${JSON.stringify(v)})`);
544
- expr = literals.length === 1 ? literals[0] : `z.union([${literals.join(", ")}])`;
545
- } else expr = "z.string()";
546
- break;
325
+ return literals.length === 1 ? literals[0] : `z.union([${literals.join(", ")}])`;
326
+ }
327
+ return "z.string()";
547
328
  }
548
- case "integer":
549
- expr = "z.coerce.number()";
550
- break;
551
- case "number":
552
- expr = "z.number()";
553
- break;
554
- case "boolean":
555
- expr = "z.boolean()";
556
- break;
557
- case "array":
558
- expr = "z.array(z.unknown())";
559
- break;
560
- default: expr = "z.string()";
561
- }
562
- if (schema.nullable) expr = `${expr}.nullable()`;
563
- return expr;
329
+ if (schema.type === "integer") return "z.coerce.number()";
330
+ if (schema.type === "number") return "z.number()";
331
+ if (schema.type === "boolean") return "z.boolean()";
332
+ if (schema.type === "array") return "z.array(z.unknown())";
333
+ return "z.string()";
334
+ })();
335
+ return schema.nullable ? `${baseExpr}.nullable()` : baseExpr;
564
336
  }
565
337
  //#endregion
566
338
  //#region src/components/Server.tsx
567
- const keysPrinter = (0, _kubb_plugin_ts.functionPrinter)({ mode: "keys" });
568
- function Server({ name, serverName, serverVersion, paramsCasing, operations }) {
569
- return /* @__PURE__ */ (0, _kubb_renderer_jsx_jsx_runtime.jsxs)(_kubb_renderer_jsx.File.Source, {
570
- name,
571
- isExportable: true,
572
- isIndexable: true,
573
- children: [
574
- /* @__PURE__ */ (0, _kubb_renderer_jsx_jsx_runtime.jsx)(_kubb_renderer_jsx.Const, {
575
- name: "server",
576
- export: true,
577
- children: `
578
- new McpServer({
579
- name: '${serverName}',
580
- version: '${serverVersion}',
581
- })
582
- `
583
- }),
584
- operations.map(({ tool, mcp, zod, node }) => {
585
- const { path: pathParams } = getOperationParameters(node, { paramsCasing });
586
- const pathEntries = [];
587
- const otherEntries = [];
588
- for (const p of pathParams) {
589
- const zodParam = zod.pathParams.find((zp) => zp.name === p.name);
590
- pathEntries.push({
591
- key: p.name,
592
- value: zodParam ? zodParam.schemaName : zodExprFromSchemaNode(p.schema)
593
- });
594
- }
595
- if (zod.requestName) otherEntries.push({
596
- key: "data",
597
- value: zod.requestName
598
- });
599
- if (zod.queryParams) otherEntries.push({
600
- key: "params",
601
- value: zodGroupExpr(zod.queryParams)
602
- });
603
- if (zod.headerParams) otherEntries.push({
604
- key: "headers",
605
- value: zodGroupExpr(zod.headerParams)
606
- });
607
- otherEntries.sort((a, b) => a.key.localeCompare(b.key));
608
- const entries = [...pathEntries, ...otherEntries];
609
- const paramsNode = entries.length ? _kubb_core.ast.createFunctionParameters({ params: [_kubb_core.ast.createParameterGroup({ properties: entries.map((e) => _kubb_core.ast.createFunctionParameter({
610
- name: e.key,
611
- optional: false
612
- })) })] }) : void 0;
613
- const destructured = paramsNode ? keysPrinter.print(paramsNode) ?? "" : "";
614
- const inputSchema = entries.length ? `{ ${entries.map((e) => `${e.key}: ${e.value}`).join(", ")} }` : void 0;
615
- const outputSchema = zod.responseName;
616
- const config = [
617
- tool.title ? `title: ${JSON.stringify(tool.title)}` : null,
618
- `description: ${JSON.stringify(tool.description)}`,
619
- outputSchema ? `outputSchema: { data: ${outputSchema} }` : null
620
- ].filter(Boolean).join(",\n ");
621
- if (inputSchema) return `
339
+ const keysPrinter = (0, _kubb_plugin_ts.functionPrinter)({ mode: "call" });
340
+ function Server({ name, serverName, serverVersion, operations }) {
341
+ const registrations = operations.map(({ tool, mcp, zod, node }) => {
342
+ const { path: pathParams } = getOperationParameters(node);
343
+ const entries = [];
344
+ if (pathParams.length) {
345
+ const pathFields = pathParams.map((p) => {
346
+ const zodParam = zod.pathParams.find((zp) => zp.name === p.name);
347
+ return `${JSON.stringify(p.name)}: ${zodParam ? zodParam.schemaName : zodExprFromSchemaNode(p.schema)}`;
348
+ }).join(", ");
349
+ entries.push({
350
+ key: "path",
351
+ value: `z.object({ ${pathFields} })`
352
+ });
353
+ }
354
+ if (zod.queryParams) entries.push({
355
+ key: "query",
356
+ value: zodGroupExpr(zod.queryParams)
357
+ });
358
+ if (zod.headerParams) entries.push({
359
+ key: "headers",
360
+ value: zodGroupExpr(zod.headerParams)
361
+ });
362
+ if (zod.requestName) entries.push({
363
+ key: "body",
364
+ value: zod.requestName
365
+ });
366
+ const paramsNode = entries.length ? (0, _kubb_plugin_ts.createFunctionParameters)({ params: [(0, _kubb_plugin_ts.createFunctionParameter)({
367
+ name: (0, _kubb_plugin_ts.createObjectBindingPattern)({ elements: entries.map((e) => ({ name: e.key })) }),
368
+ optional: false
369
+ })] }) : null;
370
+ const destructured = paramsNode ? keysPrinter.print(paramsNode) ?? "" : "";
371
+ const inputSchema = entries.length ? `{ ${entries.map((e) => `${e.key}: ${e.value}`).join(", ")} }` : null;
372
+ const outputSchema = zod.responseName;
373
+ const config = [
374
+ tool.title ? `title: ${JSON.stringify(tool.title)}` : null,
375
+ `description: ${JSON.stringify(tool.description)}`,
376
+ outputSchema ? `outputSchema: { data: ${outputSchema} }` : null
377
+ ].filter(Boolean).join(",\n ");
378
+ if (inputSchema) return `
622
379
  server.registerTool(${JSON.stringify(tool.name)}, {
623
380
  ${config},
624
381
  inputSchema: ${inputSchema},
@@ -626,15 +383,35 @@ server.registerTool(${JSON.stringify(tool.name)}, {
626
383
  return ${mcp.name}(${destructured}, request)
627
384
  })
628
385
  `;
629
- return `
386
+ return `
630
387
  server.registerTool(${JSON.stringify(tool.name)}, {
631
388
  ${config},
632
389
  }, async (request) => {
633
390
  return ${mcp.name}(request)
634
391
  })
635
392
  `;
636
- }).filter(Boolean),
637
- /* @__PURE__ */ (0, _kubb_renderer_jsx_jsx_runtime.jsx)(_kubb_renderer_jsx.Function, {
393
+ }).filter(Boolean).join("\n");
394
+ return /* @__PURE__ */ (0, kubb_jsx_jsx_runtime.jsxs)(kubb_jsx.File.Source, {
395
+ name,
396
+ isExportable: true,
397
+ isIndexable: true,
398
+ children: [
399
+ /* @__PURE__ */ (0, kubb_jsx_jsx_runtime.jsx)(kubb_jsx.Function, {
400
+ name: "getServer",
401
+ export: true,
402
+ children: `const server = new McpServer({
403
+ name: '${serverName}',
404
+ version: '${serverVersion}',
405
+ })
406
+ ${registrations}
407
+ return server`
408
+ }),
409
+ /* @__PURE__ */ (0, kubb_jsx_jsx_runtime.jsx)(kubb_jsx.Const, {
410
+ name: "server",
411
+ export: true,
412
+ children: "getServer()"
413
+ }),
414
+ /* @__PURE__ */ (0, kubb_jsx_jsx_runtime.jsx)(kubb_jsx.Function, {
638
415
  name: "startServer",
639
416
  async: true,
640
417
  export: true,
@@ -651,55 +428,172 @@ server.registerTool(${JSON.stringify(tool.name)}, {
651
428
  });
652
429
  }
653
430
  //#endregion
431
+ //#region ../../internals/client/src/resolveClient.ts
432
+ /**
433
+ * Resolves which client plugin a consumer (react-query, vue-query, swr, mcp) should call for an
434
+ * operation, shared so the selection rules and diagnostics stay in one place.
435
+ *
436
+ * Every client runtime lives in a dedicated client plugin, so a consumer always calls a registered
437
+ * contract client plugin (plugin-fetch or plugin-axios) and never emits its own inline client:
438
+ *
439
+ * - `contract` — a registered contract client plugin owns the `<op>` functions and the consumer
440
+ * imports and calls them.
441
+ * - `error` — no client plugin is registered, several are registered without a selector, or the
442
+ * requested one is missing.
443
+ *
444
+ * The `client` string selects explicitly (`'fetch'` / `'axios'`); when it is unset a lone registered
445
+ * contract client plugin is picked up automatically.
446
+ */
447
+ const pluginFetchName = "plugin-fetch";
448
+ const pluginAxiosName = "plugin-axios";
449
+ const selectorToPlugin = {
450
+ fetch: pluginFetchName,
451
+ axios: pluginAxiosName
452
+ };
453
+ const contractPlugins = [pluginFetchName, pluginAxiosName];
454
+ /**
455
+ * Applies the `client` resolution rules. See the module comment for the strategy shape.
456
+ *
457
+ * @example
458
+ * ```ts
459
+ * resolveClient({ client: 'fetch', pluginNames: ['plugin-ts', 'plugin-fetch'] })
460
+ * // { kind: 'contract', pluginName: 'plugin-fetch' }
461
+ * ```
462
+ *
463
+ * @example
464
+ * ```ts
465
+ * resolveClient({ client: undefined, pluginNames: ['plugin-ts'] })
466
+ * // { kind: 'error', message: 'No client plugin is registered…' }
467
+ * ```
468
+ */
469
+ function resolveClient(options) {
470
+ const { client, pluginNames } = options;
471
+ const has = (name) => pluginNames.includes(name);
472
+ if (client === "fetch" || client === "axios") {
473
+ const pluginName = selectorToPlugin[client];
474
+ if (!has(pluginName)) return {
475
+ kind: "error",
476
+ 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.`
477
+ };
478
+ return {
479
+ kind: "contract",
480
+ pluginName
481
+ };
482
+ }
483
+ const registered = contractPlugins.filter(has);
484
+ if (registered.length === 1) return {
485
+ kind: "contract",
486
+ pluginName: registered[0]
487
+ };
488
+ if (registered.length > 1) return {
489
+ kind: "error",
490
+ 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.`
491
+ };
492
+ return {
493
+ kind: "error",
494
+ 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."
495
+ };
496
+ }
497
+ /**
498
+ * Resolves the contract client during a consumer plugin's setup hook. Extracts the plugin names
499
+ * from the raw `plugins` config, applies {@link resolveClient}, and throws the diagnostic on a
500
+ * misconfiguration so every consumer fails fast with the same message.
501
+ */
502
+ function resolveContractClient(options) {
503
+ const { client, plugins = [] } = options;
504
+ const resolved = resolveClient({
505
+ client,
506
+ pluginNames: plugins.map((plugin) => plugin.name).filter((name) => Boolean(name))
507
+ });
508
+ if (resolved.kind === "error") throw new Error(resolved.message);
509
+ return resolved;
510
+ }
511
+ //#endregion
512
+ //#region ../../internals/client/src/resolveClientOperation.ts
513
+ /**
514
+ * Resolves the contract client `<op>` a consumer (query hook, MCP handler) imports, by looking up
515
+ * the registered contract client plugin's resolver and output. Works for any contract client plugin
516
+ * (plugin-fetch or plugin-axios). Returns `null` when no contract plugin is in play (the inline
517
+ * path). The plugin injects `.kubb/client.ts` at the global output root, the same path consumers
518
+ * read `RequestConfig` / `ResponseErrorConfig` from.
519
+ */
520
+ function resolveClientOperation(options) {
521
+ const { clientPlugin, driver, node, root, output } = options;
522
+ if (!clientPlugin) return null;
523
+ const resolver = driver.getResolver(clientPlugin.pluginName);
524
+ const plugin = driver.getPlugin(clientPlugin.pluginName);
525
+ const file = resolver.file({
526
+ ...operationFileEntry(node, node.operationId),
527
+ root,
528
+ output: plugin?.options?.output ?? output,
529
+ group: plugin?.options?.group ?? void 0
530
+ });
531
+ return {
532
+ name: resolver.name(node.operationId),
533
+ path: file.path,
534
+ clientPath: node_path.default.resolve(root, ".kubb/client.ts")
535
+ };
536
+ }
537
+ //#endregion
654
538
  //#region src/generators/mcpGenerator.tsx
655
- const mcpGenerator = (0, _kubb_core.defineGenerator)({
539
+ /**
540
+ * Built-in operation generator for `@kubb/plugin-mcp`. Emits one MCP tool
541
+ * handler per OpenAPI operation. The handler takes the grouped `{ path, query,
542
+ * headers, body }` config and forwards it to the registered client plugin's
543
+ * (`@kubb/plugin-axios` / `@kubb/plugin-fetch`) `<op>` function.
544
+ */
545
+ const mcpGenerator = (0, kubb_kit.defineGenerator)({
656
546
  name: "mcp",
657
- renderer: _kubb_renderer_jsx.jsxRenderer,
547
+ renderer: kubb_jsx.jsxRenderer,
658
548
  operation(node, ctx) {
549
+ if (!kubb_kit.ast.isHttpOperationNode(node)) return null;
659
550
  const { resolver, driver, root } = ctx;
660
- const { output, client, paramsCasing, group } = ctx.options;
551
+ const { output, client, group } = ctx.options;
661
552
  const pluginTs = driver.getPlugin(_kubb_plugin_ts.pluginTsName);
662
553
  if (!pluginTs) return null;
663
554
  const tsResolver = driver.getResolver(_kubb_plugin_ts.pluginTsName);
664
- const importedTypeNames = resolveOperationTypeNames(node, tsResolver, {
665
- paramsCasing,
666
- responseStatusNames: "error"
555
+ const contractOp = resolveClientOperation({
556
+ clientPlugin: { pluginName: client.pluginName },
557
+ driver,
558
+ node,
559
+ root,
560
+ output
667
561
  });
562
+ if (!contractOp) return null;
563
+ const optionsName = tsResolver.response.options(node);
668
564
  const meta = {
669
- name: resolver.resolveHandlerName(node),
670
- file: resolver.resolveFile({
565
+ name: resolver.handler.name(node),
566
+ file: resolver.file({
671
567
  name: node.operationId,
672
568
  extname: ".ts",
673
569
  tag: node.tags[0] ?? "default",
674
- path: node.path
675
- }, {
570
+ path: node.path,
676
571
  root,
677
572
  output,
678
- group
573
+ group: group ?? void 0
679
574
  }),
680
- fileTs: tsResolver.resolveFile({
575
+ fileTs: tsResolver.file({
681
576
  name: node.operationId,
682
577
  extname: ".ts",
683
578
  tag: node.tags[0] ?? "default",
684
- path: node.path
685
- }, {
579
+ path: node.path,
686
580
  root,
687
581
  output: pluginTs.options?.output ?? output,
688
- group: pluginTs.options?.group
582
+ group: pluginTs.options?.group ?? void 0
689
583
  })
690
584
  };
691
- return /* @__PURE__ */ (0, _kubb_renderer_jsx_jsx_runtime.jsxs)(_kubb_renderer_jsx.File, {
585
+ return /* @__PURE__ */ (0, kubb_jsx_jsx_runtime.jsxs)(kubb_jsx.File, {
692
586
  baseName: meta.file.baseName,
693
587
  path: meta.file.path,
694
588
  meta: meta.file.meta,
695
589
  children: [
696
- meta.fileTs && importedTypeNames.length > 0 && /* @__PURE__ */ (0, _kubb_renderer_jsx_jsx_runtime.jsx)(_kubb_renderer_jsx.File.Import, {
697
- name: Array.from(new Set(importedTypeNames)).sort(),
590
+ meta.fileTs && optionsName && /* @__PURE__ */ (0, kubb_jsx_jsx_runtime.jsx)(kubb_jsx.File.Import, {
591
+ name: [optionsName],
698
592
  root: meta.file.path,
699
593
  path: meta.fileTs.path,
700
594
  isTypeOnly: true
701
595
  }),
702
- /* @__PURE__ */ (0, _kubb_renderer_jsx_jsx_runtime.jsx)(_kubb_renderer_jsx.File.Import, {
596
+ /* @__PURE__ */ (0, kubb_jsx_jsx_runtime.jsx)(kubb_jsx.File.Import, {
703
597
  name: [
704
598
  "CallToolResult",
705
599
  "ServerNotification",
@@ -708,65 +602,21 @@ const mcpGenerator = (0, _kubb_core.defineGenerator)({
708
602
  path: "@modelcontextprotocol/sdk/types",
709
603
  isTypeOnly: true
710
604
  }),
711
- /* @__PURE__ */ (0, _kubb_renderer_jsx_jsx_runtime.jsx)(_kubb_renderer_jsx.File.Import, {
605
+ /* @__PURE__ */ (0, kubb_jsx_jsx_runtime.jsx)(kubb_jsx.File.Import, {
712
606
  name: ["RequestHandlerExtra"],
713
607
  path: "@modelcontextprotocol/sdk/shared/protocol",
714
608
  isTypeOnly: true
715
609
  }),
716
- /* @__PURE__ */ (0, _kubb_renderer_jsx_jsx_runtime.jsx)(_kubb_renderer_jsx.File.Import, {
717
- name: ["buildFormData"],
610
+ /* @__PURE__ */ (0, kubb_jsx_jsx_runtime.jsx)(kubb_jsx.File.Import, {
611
+ name: [contractOp.name],
718
612
  root: meta.file.path,
719
- path: node_path.default.resolve(root, ".kubb/config.ts")
613
+ path: contractOp.path
720
614
  }),
721
- client.importPath ? /* @__PURE__ */ (0, _kubb_renderer_jsx_jsx_runtime.jsxs)(_kubb_renderer_jsx_jsx_runtime.Fragment, { children: [
722
- /* @__PURE__ */ (0, _kubb_renderer_jsx_jsx_runtime.jsx)(_kubb_renderer_jsx.File.Import, {
723
- name: [
724
- "Client",
725
- "RequestConfig",
726
- "ResponseErrorConfig"
727
- ],
728
- path: client.importPath,
729
- isTypeOnly: true
730
- }),
731
- /* @__PURE__ */ (0, _kubb_renderer_jsx_jsx_runtime.jsx)(_kubb_renderer_jsx.File.Import, {
732
- name: "fetch",
733
- path: client.importPath
734
- }),
735
- client.dataReturnType === "full" && /* @__PURE__ */ (0, _kubb_renderer_jsx_jsx_runtime.jsx)(_kubb_renderer_jsx.File.Import, {
736
- name: ["ResponseConfig"],
737
- path: client.importPath,
738
- isTypeOnly: true
739
- })
740
- ] }) : /* @__PURE__ */ (0, _kubb_renderer_jsx_jsx_runtime.jsxs)(_kubb_renderer_jsx_jsx_runtime.Fragment, { children: [
741
- /* @__PURE__ */ (0, _kubb_renderer_jsx_jsx_runtime.jsx)(_kubb_renderer_jsx.File.Import, {
742
- name: [
743
- "Client",
744
- "RequestConfig",
745
- "ResponseErrorConfig"
746
- ],
747
- root: meta.file.path,
748
- path: node_path.default.resolve(root, ".kubb/fetch.ts"),
749
- isTypeOnly: true
750
- }),
751
- /* @__PURE__ */ (0, _kubb_renderer_jsx_jsx_runtime.jsx)(_kubb_renderer_jsx.File.Import, {
752
- name: ["fetch"],
753
- root: meta.file.path,
754
- path: node_path.default.resolve(root, ".kubb/fetch.ts")
755
- }),
756
- client.dataReturnType === "full" && /* @__PURE__ */ (0, _kubb_renderer_jsx_jsx_runtime.jsx)(_kubb_renderer_jsx.File.Import, {
757
- name: ["ResponseConfig"],
758
- root: meta.file.path,
759
- path: node_path.default.resolve(root, ".kubb/fetch.ts"),
760
- isTypeOnly: true
761
- })
762
- ] }),
763
- /* @__PURE__ */ (0, _kubb_renderer_jsx_jsx_runtime.jsx)(McpHandler, {
615
+ /* @__PURE__ */ (0, kubb_jsx_jsx_runtime.jsx)(McpHandler, {
764
616
  name: meta.name,
617
+ clientName: contractOp.name,
765
618
  node,
766
- resolver: tsResolver,
767
- baseURL: client.baseURL,
768
- dataReturnType: client.dataReturnType || "data",
769
- paramsCasing
619
+ resolver: tsResolver
770
620
  })
771
621
  ]
772
622
  });
@@ -781,12 +631,12 @@ const mcpGenerator = (0, _kubb_core.defineGenerator)({
781
631
  * and `resolveResponseStatusName` for per-status response schemas.
782
632
  * Query and header params are composed into `z.object({ ... })` from individual schemas.
783
633
  */
784
- const serverGenerator = (0, _kubb_core.defineGenerator)({
634
+ const serverGenerator = (0, kubb_kit.defineGenerator)({
785
635
  name: "operations",
786
- renderer: _kubb_renderer_jsx.jsxRenderer,
636
+ renderer: kubb_jsx.jsxRenderer,
787
637
  operations(nodes, ctx) {
788
- const { adapter, config, resolver, plugin, driver, root } = ctx;
789
- const { output, paramsCasing, group } = ctx.options;
638
+ const { config, resolver, plugin, driver, root } = ctx;
639
+ const { output, group } = ctx.options;
790
640
  const pluginZod = driver.getPlugin(_kubb_plugin_zod.pluginZodName);
791
641
  if (!pluginZod) return;
792
642
  const zodResolver = driver.getResolver(_kubb_plugin_zod.pluginZodName);
@@ -801,34 +651,32 @@ const serverGenerator = (0, _kubb_core.defineGenerator)({
801
651
  path: node_path.default.resolve(root, output.path, ".mcp.json"),
802
652
  meta: { pluginName: plugin.name }
803
653
  };
804
- const operationsMapped = nodes.map((node) => {
805
- const { path: pathParams, query: queryParams, header: headerParams } = getOperationParameters(node, { paramsCasing });
806
- const mcpFile = resolver.resolveFile({
654
+ const operationsMapped = nodes.filter(kubb_kit.ast.isHttpOperationNode).map((node) => {
655
+ const { path: pathParams, query: queryParams, header: headerParams } = getOperationParameters(node);
656
+ const mcpFile = resolver.file({
807
657
  name: node.operationId,
808
658
  extname: ".ts",
809
659
  tag: node.tags[0] ?? "default",
810
- path: node.path
811
- }, {
660
+ path: node.path,
812
661
  root,
813
662
  output,
814
- group
663
+ group: group ?? void 0
815
664
  });
816
- const zodFile = zodResolver.resolveFile({
665
+ const zodFile = zodResolver.file({
817
666
  name: node.operationId,
818
667
  extname: ".ts",
819
668
  tag: node.tags[0] ?? "default",
820
- path: node.path
821
- }, {
669
+ path: node.path,
822
670
  root,
823
671
  output: pluginZod.options?.output ?? output,
824
- group: pluginZod.options?.group
672
+ group: pluginZod.options?.group ?? void 0
825
673
  });
826
- const requestName = node.requestBody?.content?.[0]?.schema ? zodResolver.resolveDataName(node) : void 0;
674
+ const requestName = node.requestBody?.content?.[0]?.schema ? zodResolver.response.body(node) : null;
827
675
  const successStatus = findSuccessStatusCode(node.responses);
828
- const responseName = successStatus ? zodResolver.resolveResponseStatusName(node, successStatus) : void 0;
676
+ const responseName = successStatus ? zodResolver.response.status(node, successStatus) : null;
829
677
  const resolveParams = (params) => params.map((p) => ({
830
678
  name: p.name,
831
- schemaName: zodResolver.resolveParamName(node, p)
679
+ schemaName: zodResolver.param.name(node, p)
832
680
  }));
833
681
  return {
834
682
  tool: {
@@ -837,13 +685,13 @@ const serverGenerator = (0, _kubb_core.defineGenerator)({
837
685
  description: node.description || `Make a ${node.method.toUpperCase()} request to ${node.path}`
838
686
  },
839
687
  mcp: {
840
- name: resolver.resolveHandlerName(node),
688
+ name: resolver.handler.name(node),
841
689
  file: mcpFile
842
690
  },
843
691
  zod: {
844
692
  pathParams: resolveParams(pathParams),
845
- queryParams: queryParams.length ? resolveParams(queryParams) : void 0,
846
- headerParams: headerParams.length ? resolveParams(headerParams) : void 0,
693
+ queryParams: queryParams.length ? resolveParams(queryParams) : null,
694
+ headerParams: headerParams.length ? resolveParams(headerParams) : null,
847
695
  requestName,
848
696
  responseName,
849
697
  file: zodFile
@@ -860,60 +708,67 @@ const serverGenerator = (0, _kubb_core.defineGenerator)({
860
708
  zod.responseName
861
709
  ].filter((name) => Boolean(name));
862
710
  const uniqueNames = [...new Set(zodNames)].sort();
863
- return [/* @__PURE__ */ (0, _kubb_renderer_jsx_jsx_runtime.jsx)(_kubb_renderer_jsx.File.Import, {
711
+ return [/* @__PURE__ */ (0, kubb_jsx_jsx_runtime.jsx)(kubb_jsx.File.Import, {
864
712
  name: [mcp.name],
865
713
  root: serverFile.path,
866
714
  path: mcp.file.path
867
- }, mcp.name), uniqueNames.length > 0 && /* @__PURE__ */ (0, _kubb_renderer_jsx_jsx_runtime.jsx)(_kubb_renderer_jsx.File.Import, {
715
+ }, mcp.name), uniqueNames.length > 0 && /* @__PURE__ */ (0, kubb_jsx_jsx_runtime.jsx)(kubb_jsx.File.Import, {
868
716
  name: uniqueNames,
869
717
  root: serverFile.path,
870
718
  path: zod.file.path
871
719
  }, `zod-${mcp.name}`)].filter(Boolean);
872
720
  });
873
- return /* @__PURE__ */ (0, _kubb_renderer_jsx_jsx_runtime.jsxs)(_kubb_renderer_jsx_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, _kubb_renderer_jsx_jsx_runtime.jsxs)(_kubb_renderer_jsx.File, {
721
+ return /* @__PURE__ */ (0, kubb_jsx_jsx_runtime.jsxs)(kubb_jsx_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, kubb_jsx_jsx_runtime.jsxs)(kubb_jsx.File, {
874
722
  baseName: serverFile.baseName,
875
723
  path: serverFile.path,
876
724
  meta: serverFile.meta,
877
- banner: resolver.resolveBanner(adapter.inputNode, {
725
+ banner: resolver.default.banner(ctx.meta, {
878
726
  output,
879
- config
727
+ config,
728
+ file: {
729
+ path: serverFile.path,
730
+ baseName: serverFile.baseName
731
+ }
880
732
  }),
881
- footer: resolver.resolveFooter(adapter.inputNode, {
733
+ footer: resolver.default.footer(ctx.meta, {
882
734
  output,
883
- config
735
+ config,
736
+ file: {
737
+ path: serverFile.path,
738
+ baseName: serverFile.baseName
739
+ }
884
740
  }),
885
741
  children: [
886
- /* @__PURE__ */ (0, _kubb_renderer_jsx_jsx_runtime.jsx)(_kubb_renderer_jsx.File.Import, {
742
+ /* @__PURE__ */ (0, kubb_jsx_jsx_runtime.jsx)(kubb_jsx.File.Import, {
887
743
  name: ["McpServer"],
888
744
  path: "@modelcontextprotocol/sdk/server/mcp"
889
745
  }),
890
- /* @__PURE__ */ (0, _kubb_renderer_jsx_jsx_runtime.jsx)(_kubb_renderer_jsx.File.Import, {
746
+ /* @__PURE__ */ (0, kubb_jsx_jsx_runtime.jsx)(kubb_jsx.File.Import, {
891
747
  name: ["z"],
892
748
  path: "zod"
893
749
  }),
894
- /* @__PURE__ */ (0, _kubb_renderer_jsx_jsx_runtime.jsx)(_kubb_renderer_jsx.File.Import, {
750
+ /* @__PURE__ */ (0, kubb_jsx_jsx_runtime.jsx)(kubb_jsx.File.Import, {
895
751
  name: ["StdioServerTransport"],
896
752
  path: "@modelcontextprotocol/sdk/server/stdio"
897
753
  }),
898
754
  imports,
899
- /* @__PURE__ */ (0, _kubb_renderer_jsx_jsx_runtime.jsx)(Server, {
755
+ /* @__PURE__ */ (0, kubb_jsx_jsx_runtime.jsx)(Server, {
900
756
  name,
901
- serverName: adapter.inputNode?.meta?.title ?? "server",
902
- serverVersion: adapter.inputNode?.meta?.version ?? "0.0.0",
903
- paramsCasing,
757
+ serverName: ctx.meta.title ?? "server",
758
+ serverVersion: ctx.meta.version ?? "0.0.0",
904
759
  operations: operationsMapped
905
760
  })
906
761
  ]
907
- }), /* @__PURE__ */ (0, _kubb_renderer_jsx_jsx_runtime.jsx)(_kubb_renderer_jsx.File, {
762
+ }), /* @__PURE__ */ (0, kubb_jsx_jsx_runtime.jsx)(kubb_jsx.File, {
908
763
  baseName: jsonFile.baseName,
909
764
  path: jsonFile.path,
910
765
  meta: jsonFile.meta,
911
- children: /* @__PURE__ */ (0, _kubb_renderer_jsx_jsx_runtime.jsx)(_kubb_renderer_jsx.File.Source, {
766
+ children: /* @__PURE__ */ (0, kubb_jsx_jsx_runtime.jsx)(kubb_jsx.File.Source, {
912
767
  name,
913
768
  children: `
914
769
  {
915
770
  "mcpServers": {
916
- "${adapter.inputNode?.meta?.title || "server"}": {
771
+ "${ctx.meta.title || "server"}": {
917
772
  "type": "stdio",
918
773
  "command": "npx",
919
774
  "args": ["tsx", "${node_path.default.relative(node_path.default.dirname(jsonFile.path), serverFile.path)}"]
@@ -928,101 +783,87 @@ const serverGenerator = (0, _kubb_core.defineGenerator)({
928
783
  //#endregion
929
784
  //#region src/resolvers/resolverMcp.ts
930
785
  /**
931
- * Naming convention resolver for MCP plugin.
786
+ * Default resolver used by `@kubb/plugin-mcp`. Decides the names and file
787
+ * paths for every generated MCP tool handler. Function names get a `Handler`
788
+ * suffix so an operation `addPet` becomes `addPetHandler`.
932
789
  *
933
- * Provides default naming helpers using camelCase with a `handler` suffix for functions.
790
+ * @example Resolve a handler name
791
+ * ```ts
792
+ * import { resolverMcp } from '@kubb/plugin-mcp'
934
793
  *
935
- * @example
936
- * `resolverMcp.default('addPet', 'function') // → 'addPetHandler'`
794
+ * resolverMcp.name('addPet') // 'addPetHandler'
795
+ * ```
937
796
  */
938
- const resolverMcp = (0, _kubb_core.defineResolver)(() => ({
939
- name: "default",
797
+ const resolverMcp = (0, kubb_kit.createResolver)({
940
798
  pluginName: "plugin-mcp",
941
- default(name, type) {
942
- if (type === "file") return camelCase(name, { isFile: true });
799
+ name(name) {
943
800
  return camelCase(name, { suffix: "handler" });
944
801
  },
945
- resolveName(name) {
946
- return this.default(name, "function");
947
- },
948
- resolvePathName(name, type) {
949
- return this.default(name, type);
950
- },
951
- resolveHandlerName(node) {
952
- return this.resolveName(node.operationId);
953
- }
954
- }));
802
+ handler: { name(node) {
803
+ return this.name(node.operationId);
804
+ } }
805
+ });
955
806
  //#endregion
956
807
  //#region src/plugin.ts
808
+ /**
809
+ * Canonical plugin name for `@kubb/plugin-mcp`. Used for driver lookups and
810
+ * cross-plugin dependency references.
811
+ */
957
812
  const pluginMcpName = "plugin-mcp";
958
- const pluginMcp = (0, _kubb_core.definePlugin)((options) => {
813
+ /**
814
+ * Generates a Model Context Protocol (MCP) server from an OpenAPI spec. Every
815
+ * operation becomes a typed MCP tool that AI assistants (Claude Desktop, Claude
816
+ * Code, MCP-compatible clients) can call directly. Each handler calls a contract
817
+ * client `<op>` from a registered `@kubb/plugin-axios` or `@kubb/plugin-fetch`.
818
+ *
819
+ * @example
820
+ * ```ts
821
+ * import { defineConfig } from 'kubb/config'
822
+ * import { pluginTs } from '@kubb/plugin-ts'
823
+ * import { pluginZod } from '@kubb/plugin-zod'
824
+ * import { pluginMcp } from '@kubb/plugin-mcp'
825
+ *
826
+ * export default defineConfig({
827
+ * input: './petStore.yaml',
828
+ * output: { path: './src/gen' },
829
+ * plugins: [
830
+ * pluginTs(),
831
+ * pluginZod(),
832
+ * pluginMcp({
833
+ * output: { path: './mcp' },
834
+ * baseURL: 'https://petstore.swagger.io/v2',
835
+ * }),
836
+ * ],
837
+ * })
838
+ * ```
839
+ */
840
+ const pluginMcp = (0, kubb_kit.definePlugin)((options) => {
959
841
  const { output = {
960
842
  path: "mcp",
961
- barrelType: "named"
962
- }, group, exclude = [], include, override = [], paramsCasing, client, resolver: userResolver, transformer: userTransformer, generators: userGenerators = [] } = options;
963
- const clientName = client?.client ?? "axios";
964
- const clientImportPath = client?.importPath ?? (!client?.bundle ? `@kubb/plugin-client/clients/${clientName}` : void 0);
965
- const groupConfig = group ? {
966
- ...group,
967
- name: group.name ? group.name : (ctx) => {
968
- if (group.type === "path") return `${ctx.group.split("/")[1]}`;
969
- return `${camelCase(ctx.group)}Requests`;
970
- }
971
- } : void 0;
843
+ barrel: { type: "named" }
844
+ }, group, exclude = [], include, override = [], client, resolver: userResolver, macros: userMacros } = options;
845
+ const groupConfig = createGroupConfig(group);
972
846
  return {
973
847
  name: pluginMcpName,
974
848
  options,
975
849
  dependencies: [_kubb_plugin_ts.pluginTsName, _kubb_plugin_zod.pluginZodName],
976
850
  hooks: { "kubb:plugin:setup"(ctx) {
977
- const resolver = userResolver ? {
978
- ...resolverMcp,
979
- ...userResolver
980
- } : resolverMcp;
851
+ const resolver = userResolver ? kubb_kit.Resolver.merge(resolverMcp, userResolver) : resolverMcp;
981
852
  ctx.setOptions({
982
853
  output,
983
854
  exclude,
984
855
  include,
985
856
  override,
986
857
  group: groupConfig,
987
- paramsCasing,
988
- client: {
989
- client: clientName,
990
- clientType: client?.clientType ?? "function",
991
- importPath: clientImportPath,
992
- dataReturnType: client?.dataReturnType ?? "data",
993
- bundle: client?.bundle,
994
- baseURL: client?.baseURL,
995
- paramsCasing: client?.paramsCasing
996
- },
858
+ client: resolveContractClient({
859
+ client,
860
+ plugins: ctx.config.plugins
861
+ }),
997
862
  resolver
998
863
  });
999
864
  ctx.setResolver(resolver);
1000
- if (userTransformer) ctx.setTransformer(userTransformer);
1001
- ctx.addGenerator(mcpGenerator);
1002
- ctx.addGenerator(serverGenerator);
1003
- for (const gen of userGenerators) ctx.addGenerator(gen);
1004
- const root = node_path$1.default.resolve(ctx.config.root, ctx.config.output.path);
1005
- const hasClientPlugin = ctx.config.plugins?.some((p) => p.name === _kubb_plugin_client.pluginClientName);
1006
- if (client?.bundle && !hasClientPlugin && !clientImportPath) ctx.injectFile({
1007
- baseName: "fetch.ts",
1008
- path: node_path$1.default.resolve(root, ".kubb/fetch.ts"),
1009
- sources: [_kubb_core.ast.createSource({
1010
- name: "fetch",
1011
- nodes: [_kubb_core.ast.createText(clientName === "fetch" ? _kubb_plugin_client_templates_clients_fetch_source.source : _kubb_plugin_client_templates_clients_axios_source.source)],
1012
- isExportable: true,
1013
- isIndexable: true
1014
- })]
1015
- });
1016
- if (!hasClientPlugin) ctx.injectFile({
1017
- baseName: "config.ts",
1018
- path: node_path$1.default.resolve(root, ".kubb/config.ts"),
1019
- sources: [_kubb_core.ast.createSource({
1020
- name: "config",
1021
- nodes: [_kubb_core.ast.createText(_kubb_plugin_client_templates_config_source.source)],
1022
- isExportable: false,
1023
- isIndexable: false
1024
- })]
1025
- });
865
+ if (userMacros?.length) ctx.setMacros(userMacros);
866
+ ctx.addGenerator(mcpGenerator, serverGenerator);
1026
867
  } }
1027
868
  };
1028
869
  });