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