@kubb/plugin-mcp 5.0.0-beta.4 → 5.0.0-beta.56

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
@@ -47,36 +47,45 @@ let _kubb_plugin_client_templates_config_source = require("@kubb/plugin-client/t
47
47
  function toCamelOrPascal(text, pascal) {
48
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
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);
50
+ return (i === 0 && !pascal ? word.charAt(0).toLowerCase() : word.charAt(0).toUpperCase()) + word.slice(1);
52
51
  }).join("").replace(/[^a-zA-Z0-9]/g, "");
53
52
  }
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
+ * Converts `text` to camelCase.
55
+ *
56
+ * @example Word boundaries
57
+ * `camelCase('hello-world') // 'helloWorld'`
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 With a prefix
60
+ * `camelCase('tag', { prefix: 'create' }) // 'createTag'`
61
61
  */
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("/");
62
+ function camelCase(text, { prefix = "", suffix = "" } = {}) {
63
+ return toCamelOrPascal(`${prefix} ${text} ${suffix}`, false);
65
64
  }
65
+ //#endregion
66
+ //#region ../../internals/utils/src/fs.ts
66
67
  /**
67
- * Converts `text` to camelCase.
68
- * When `isFile` is `true`, dot-separated segments are each cased independently and joined with `/`.
68
+ * Builds a nested file path from a dotted name. Splits on dots that precede a letter
69
+ * (so version numbers embedded in operationIds like `v2025.0` stay intact), camelCases
70
+ * every earlier segment, applies `caseLast` to the final segment, and joins with `/`.
69
71
  *
70
- * @example
71
- * camelCase('hello-world') // 'helloWorld'
72
- * camelCase('pet.petId', { isFile: true }) // 'pet/petId'
72
+ * Empty segments are dropped before joining. They arise when the name starts with a dot
73
+ * followed by a letter (e.g. `..Schema` splits into `['..', 'Schema']` and `'..'` cases to
74
+ * an empty string). Without this a leading `/` would form, which `path.resolve` reads as an
75
+ * absolute path, letting generated files escape the configured output directory.
76
+ *
77
+ * @example Nested path from a dotted name
78
+ * `toFilePath('pet.petId') // 'pet/petId'`
79
+ *
80
+ * @example PascalCase the final segment
81
+ * `toFilePath('pet.Pet', pascalCase) // 'pet/Pet'`
82
+ *
83
+ * @example Suffix applied to the final segment only
84
+ * `toFilePath('tag.tag', (part) => camelCase(part, { suffix: 'schema' })) // 'tag/tagSchema'`
73
85
  */
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);
86
+ function toFilePath(name, caseLast = camelCase) {
87
+ const parts = name.split(/\.(?=[a-zA-Z])/);
88
+ return parts.map((part, i) => i === parts.length - 1 ? caseLast(part) : camelCase(part)).filter(Boolean).join("/");
80
89
  }
81
90
  //#endregion
82
91
  //#region ../../internals/utils/src/reserved.ts
@@ -182,99 +191,80 @@ function isValidVarName(name) {
182
191
  return /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name);
183
192
  }
184
193
  //#endregion
185
- //#region ../../internals/utils/src/urlPath.ts
194
+ //#region ../../internals/utils/src/url.ts
195
+ function transformParam(raw, casing) {
196
+ const param = isValidVarName(raw) ? raw : camelCase(raw);
197
+ return casing === "camelcase" ? camelCase(param) : param;
198
+ }
199
+ function toParamsObject(path, { replacer, casing } = {}) {
200
+ const params = {};
201
+ for (const match of path.matchAll(/\{([^}]+)\}/g)) {
202
+ const param = transformParam(match[1], casing);
203
+ const key = replacer ? replacer(param) : param;
204
+ params[key] = key;
205
+ }
206
+ return Object.keys(params).length > 0 ? params : null;
207
+ }
186
208
  /**
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}`'
209
+ * Helpers for OpenAPI/Swagger paths, plus a thin wrapper over the native `URL`.
193
210
  */
194
- var URLPath = class {
211
+ var Url = class Url {
195
212
  /**
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`.
213
+ * Reports whether `url` is a parseable absolute URL. Delegates to the native `URL.canParse`.
205
214
  *
206
215
  * @example
207
- * ```ts
208
- * new URLPath('/pet/{petId}').URL // '/pet/:petId'
209
- * ```
216
+ * Url.canParse('https://petstore.swagger.io/v2') // true
217
+ * Url.canParse('/pet/{petId}') // false
210
218
  */
211
- get URL() {
212
- return this.toURLPath();
219
+ static canParse(url, base) {
220
+ return URL.canParse(url, base);
213
221
  }
214
- /** Returns `true` when `path` is a fully-qualified URL (e.g. starts with `https://`).
222
+ /**
223
+ * Converts an OpenAPI/Swagger path to Express-style colon syntax.
215
224
  *
216
225
  * @example
217
- * ```ts
218
- * new URLPath('https://petstore.swagger.io/v2/pet').isURL // true
219
- * new URLPath('/pet/{petId}').isURL // false
220
- * ```
226
+ * Url.toPath('/pet/{petId}') // '/pet/:petId'
221
227
  */
222
- get isURL() {
223
- try {
224
- return !!new URL(this.path).href;
225
- } catch {
226
- return false;
227
- }
228
+ static toPath(path) {
229
+ return path.replace(/\{([^}]+)\}/g, ":$1");
228
230
  }
229
231
  /**
230
- * Converts the OpenAPI path to a TypeScript template literal string.
232
+ * Converts an OpenAPI/Swagger path to a TypeScript template literal string.
233
+ * `prefix` is prepended inside the literal, `replacer` transforms each parameter name,
234
+ * and `casing` controls parameter identifier casing.
231
235
  *
232
236
  * @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.
237
+ * Url.toTemplateString('/pet/{petId}') // '`/pet/${petId}`'
240
238
  *
241
239
  * @example
242
- * ```ts
243
- * new URLPath('/pet/{petId}').object
244
- * // { url: '/pet/:petId', params: { petId: 'petId' } }
245
- * ```
240
+ * Url.toTemplateString('/pet/{petId}', { prefix: 'https://api' }) // '`https://api/pet/${petId}`'
246
241
  */
247
- get object() {
248
- return this.toObject();
242
+ static toTemplateString(path, { prefix, replacer, casing } = {}) {
243
+ const result = path.split(/\{([^}]+)\}/).map((part, i) => {
244
+ if (i % 2 === 0) return part;
245
+ const param = transformParam(part, casing);
246
+ return `\${${replacer ? replacer(param) : param}}`;
247
+ }).join("");
248
+ return `\`${prefix ?? ""}${result}\``;
249
249
  }
250
- /** Returns a map of path parameter names, or `undefined` when the path has no parameters.
250
+ /**
251
+ * Returns the path and its extracted params as a structured `URLObject`, or as a stringified
252
+ * expression when `stringify` is set.
251
253
  *
252
254
  * @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.getParams();
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.
255
+ * Url.toObject('/pet/{petId}')
256
+ * // { url: '/pet/:petId', params: { petId: 'petId' } }
267
257
  */
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 } = {}) {
258
+ static toObject(path, { type = "path", replacer, stringify, casing } = {}) {
275
259
  const object = {
276
- url: type === "path" ? this.toURLPath() : this.toTemplateString({ replacer }),
277
- params: this.getParams()
260
+ url: type === "path" ? Url.toPath(path) : Url.toTemplateString(path, {
261
+ replacer,
262
+ casing
263
+ }),
264
+ params: toParamsObject(path, {
265
+ replacer,
266
+ casing
267
+ })
278
268
  };
279
269
  if (stringify) {
280
270
  if (type === "template") return JSON.stringify(object).replaceAll("'", "").replaceAll(`"`, "");
@@ -283,127 +273,144 @@ var URLPath = class {
283
273
  }
284
274
  return object;
285
275
  }
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}').getParams()
308
- * // { petId: 'petId', tagId: 'tagId' }
309
- * ```
310
- */
311
- getParams(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
276
  };
330
277
  //#endregion
331
- //#region src/utils.ts
332
- /**
333
- * Find the first 2xx response status code from an operation's responses.
334
- */
335
- function findSuccessStatusCode(responses) {
336
- for (const res of responses) {
337
- const code = Number(res.statusCode);
338
- if (code >= 200 && code < 300) return res.statusCode;
339
- }
278
+ //#region ../../internals/shared/src/operation.ts
279
+ function getOperationLink(node, link) {
280
+ if (!link) return null;
281
+ if (typeof link === "function") return link(node) ?? null;
282
+ if (link === "urlPath") return node.path ? `{@link ${Url.toPath(node.path)}}` : null;
283
+ return node.path ? `{@link ${node.path.replaceAll("{", ":").replaceAll("}", "")}}` : null;
340
284
  }
341
- /**
342
- * Render a group param value compose individual schemas into `z.object({ ... })`,
343
- * or use a schema name string directly.
344
- */
345
- function zodGroupExpr(entry) {
346
- if (typeof entry === "string") return entry;
347
- return `z.object({ ${entry.map((p) => `${JSON.stringify(p.name)}: ${p.schemaName}`).join(", ")} })`;
348
- }
349
- /**
350
- * Build JSDoc comment lines from an OperationNode.
351
- */
352
- function getComments(node) {
353
- return [
285
+ function buildOperationComments(node, options = {}) {
286
+ const { link = "pathTemplate", linkPosition = "afterDeprecated", splitLines = false } = options;
287
+ const linkComment = getOperationLink(node, link);
288
+ const filteredComments = (linkPosition === "beforeDeprecated" ? [
289
+ node.description && `@description ${node.description}`,
290
+ node.summary && `@summary ${node.summary}`,
291
+ linkComment,
292
+ node.deprecated && "@deprecated"
293
+ ] : [
354
294
  node.description && `@description ${node.description}`,
355
295
  node.summary && `@summary ${node.summary}`,
356
296
  node.deprecated && "@deprecated",
357
- `{@link ${node.path.replaceAll("{", ":").replaceAll("}", "")}}`
358
- ].filter((x) => Boolean(x));
297
+ linkComment
298
+ ]).filter((comment) => Boolean(comment));
299
+ if (!splitLines) return filteredComments;
300
+ return filteredComments.flatMap((text) => text.split(/\r?\n/).map((line) => line.trim())).filter((comment) => Boolean(comment));
359
301
  }
360
- /**
361
- * Build a mapping of original param names → camelCase names.
362
- * Returns `undefined` when no names actually change (no remapping needed).
363
- */
364
- function getParamsMapping(params) {
365
- if (!params.length) return;
366
- const mapping = {};
367
- let hasDifference = false;
368
- for (const p of params) {
369
- const camelName = camelCase(p.name);
370
- mapping[p.name] = camelName;
371
- if (p.name !== camelName) hasDifference = true;
302
+ function getOperationParameters(node, options = {}) {
303
+ const params = _kubb_core.ast.caseParams(node.parameters, options.paramsCasing);
304
+ return {
305
+ path: params.filter((param) => param.in === "path"),
306
+ query: params.filter((param) => param.in === "query"),
307
+ header: params.filter((param) => param.in === "header"),
308
+ cookie: params.filter((param) => param.in === "cookie")
309
+ };
310
+ }
311
+ function getStatusCodeNumber(statusCode) {
312
+ const code = Number(statusCode);
313
+ return Number.isNaN(code) ? null : code;
314
+ }
315
+ function isSuccessStatusCode(statusCode) {
316
+ const code = getStatusCodeNumber(statusCode);
317
+ return code !== null && code >= 200 && code < 300;
318
+ }
319
+ function isErrorStatusCode(statusCode) {
320
+ const code = getStatusCodeNumber(statusCode);
321
+ return code !== null && code >= 400;
322
+ }
323
+ function resolveErrorNames(node, resolver) {
324
+ return node.responses.filter((response) => isErrorStatusCode(response.statusCode)).map((response) => resolver.resolveResponseStatusName(node, response.statusCode));
325
+ }
326
+ function resolveStatusCodeNames(node, resolver) {
327
+ return node.responses.map((response) => resolver.resolveResponseStatusName(node, response.statusCode));
328
+ }
329
+ const typeNamesByResolver = /* @__PURE__ */ new WeakMap();
330
+ function resolveOperationTypeNames(node, resolver, options = {}) {
331
+ const cacheKey = `${node.operationId}\0${options.paramsCasing ?? ""}\0${options.order ?? ""}\0${options.responseStatusNames ?? ""}\0${(options.exclude ?? []).join(",")}`;
332
+ let byResolver = typeNamesByResolver.get(resolver);
333
+ if (byResolver) {
334
+ const cached = byResolver.get(cacheKey);
335
+ if (cached) return cached;
336
+ } else {
337
+ byResolver = /* @__PURE__ */ new Map();
338
+ typeNamesByResolver.set(resolver, byResolver);
372
339
  }
373
- return hasDifference ? mapping : void 0;
340
+ const { path, query, header } = getOperationParameters(node, { paramsCasing: options.paramsCasing });
341
+ const responseStatusNames = options.responseStatusNames === "error" ? resolveErrorNames(node, resolver) : options.responseStatusNames === false ? [] : resolveStatusCodeNames(node, resolver);
342
+ const exclude = new Set(options.exclude ?? []);
343
+ const paramNames = [
344
+ ...path.map((param) => resolver.resolvePathParamsName(node, param)),
345
+ ...query.map((param) => resolver.resolveQueryParamsName(node, param)),
346
+ ...header.map((param) => resolver.resolveHeaderParamsName(node, param))
347
+ ];
348
+ const bodyAndResponseNames = [node.requestBody?.content?.[0]?.schema ? resolver.resolveDataName(node) : null, resolver.resolveResponseName(node)];
349
+ const result = (options.order === "body-response-first" ? [
350
+ ...bodyAndResponseNames,
351
+ ...paramNames,
352
+ ...responseStatusNames
353
+ ] : [
354
+ ...paramNames,
355
+ ...bodyAndResponseNames,
356
+ ...responseStatusNames
357
+ ]).filter((name) => Boolean(name) && !exclude.has(name));
358
+ byResolver.set(cacheKey, result);
359
+ return result;
360
+ }
361
+ function findSuccessStatusCode(responses) {
362
+ for (const response of responses) if (isSuccessStatusCode(response.statusCode)) return response.statusCode;
363
+ return null;
374
364
  }
365
+ //#endregion
366
+ //#region ../../internals/shared/src/group.ts
375
367
  /**
376
- * Convert a SchemaNode type to an inline Zod expression string.
377
- * Used as fallback when no named zod schema is available for a path parameter.
368
+ * Builds the `group` config a Kubb plugin passes to `ctx.setOptions`, applying the
369
+ * shared default naming so every plugin groups output consistently:
370
+ *
371
+ * - `path` groups use the second path segment (`/pet/findByStatus` → `pet`).
372
+ * - other groups use the camelCased group (`pet store` → `petStore`).
373
+ *
374
+ * A user-provided `group.name` always wins over the default namer, so callers stay in
375
+ * control of their output folders. Returns `null` when grouping is disabled, matching the
376
+ * per-plugin convention.
377
+ *
378
+ * @param group - The user-supplied group option, or `undefined` to disable grouping.
379
+ *
380
+ * @example
381
+ * ```ts
382
+ * createGroupConfig(group) // shared across every plugin
383
+ * ```
378
384
  */
379
- function zodExprFromSchemaNode(schema) {
380
- let expr;
381
- switch (schema.type) {
382
- case "enum": {
383
- const rawValues = schema.namedEnumValues?.length ? schema.namedEnumValues.map((v) => v.value) : (schema.enumValues ?? []).filter((v) => v !== null);
384
- if (rawValues.length > 0 && rawValues.every((v) => typeof v === "string")) expr = `z.enum([${rawValues.map((v) => JSON.stringify(v)).join(", ")}])`;
385
- else if (rawValues.length > 0) {
386
- const literals = rawValues.map((v) => `z.literal(${JSON.stringify(v)})`);
387
- expr = literals.length === 1 ? literals[0] : `z.union([${literals.join(", ")}])`;
388
- } else expr = "z.string()";
389
- break;
390
- }
391
- case "integer":
392
- expr = "z.coerce.number()";
393
- break;
394
- case "number":
395
- expr = "z.number()";
396
- break;
397
- case "boolean":
398
- expr = "z.boolean()";
399
- break;
400
- case "array":
401
- expr = "z.array(z.unknown())";
402
- break;
403
- default: expr = "z.string()";
404
- }
405
- if (schema.nullable) expr = `${expr}.nullable()`;
406
- return expr;
385
+ function createGroupConfig(group) {
386
+ if (!group) return null;
387
+ const defaultName = (ctx) => {
388
+ if (group.type === "path") return `${ctx.group.split("/")[1]}`;
389
+ return camelCase(ctx.group);
390
+ };
391
+ return {
392
+ ...group,
393
+ name: group.name ? group.name : defaultName
394
+ };
395
+ }
396
+ //#endregion
397
+ //#region ../../internals/shared/src/params.ts
398
+ function buildParamsMapping(originalParams, mappedParams) {
399
+ const mapping = {};
400
+ let hasChanged = false;
401
+ originalParams.forEach((param, i) => {
402
+ const mappedName = mappedParams[i]?.name ?? param.name;
403
+ mapping[param.name] = mappedName;
404
+ if (param.name !== mappedName) hasChanged = true;
405
+ });
406
+ return hasChanged ? mapping : null;
407
+ }
408
+ function buildTransformedParamsMapping(params, transformName) {
409
+ if (!params.length) return null;
410
+ return buildParamsMapping(params, params.map((param) => ({
411
+ ...param,
412
+ name: transformName(param.name)
413
+ })));
407
414
  }
408
415
  //#endregion
409
416
  //#region src/components/McpHandler.tsx
@@ -415,16 +422,12 @@ function buildRemappingCode(mapping, varName, sourceName) {
415
422
  }
416
423
  const declarationPrinter = (0, _kubb_plugin_ts.functionPrinter)({ mode: "declaration" });
417
424
  function McpHandler({ name, node, resolver, baseURL, dataReturnType, paramsCasing }) {
418
- const urlPath = new URLPath(node.path);
425
+ if (!_kubb_core.ast.isHttpOperationNode(node)) return null;
419
426
  const contentType = node.requestBody?.content?.[0]?.contentType;
420
427
  const isFormData = contentType === "multipart/form-data";
421
- const casedParams = _kubb_core.ast.caseParams(node.parameters, paramsCasing);
422
- const queryParams = casedParams.filter((p) => p.in === "query");
423
- const headerParams = casedParams.filter((p) => p.in === "header");
424
- const originalPathParams = node.parameters.filter((p) => p.in === "path");
425
- const originalQueryParams = node.parameters.filter((p) => p.in === "query");
426
- const originalHeaderParams = node.parameters.filter((p) => p.in === "header");
427
- const requestName = node.requestBody?.content?.[0]?.schema ? resolver.resolveDataName(node) : void 0;
428
+ const { query: queryParams, header: headerParams } = getOperationParameters(node, { paramsCasing });
429
+ const { path: originalPathParams, query: originalQueryParams, header: originalHeaderParams } = getOperationParameters(node);
430
+ const requestName = node.requestBody?.content?.[0]?.schema ? resolver.resolveDataName(node) : null;
428
431
  const responseName = resolver.resolveResponseName(node);
429
432
  const errorResponses = node.responses.filter((r) => Number(r.statusCode) >= 400).map((r) => resolver.resolveResponseStatusName(node, r.statusCode));
430
433
  const generics = [
@@ -440,35 +443,27 @@ function McpHandler({ name, node, resolver, baseURL, dataReturnType, paramsCasin
440
443
  });
441
444
  const baseParamsSignature = declarationPrinter.print(paramsNode) ?? "";
442
445
  const paramsSignature = baseParamsSignature ? `${baseParamsSignature}, request: RequestHandlerExtra<ServerRequest, ServerNotification>` : "request: RequestHandlerExtra<ServerRequest, ServerNotification>";
443
- const pathParamsMapping = paramsCasing ? getParamsMapping(originalPathParams) : void 0;
444
- const queryParamsMapping = paramsCasing ? getParamsMapping(originalQueryParams) : void 0;
445
- const headerParamsMapping = paramsCasing ? getParamsMapping(originalHeaderParams) : void 0;
446
- const contentTypeHeader = contentType && contentType !== "application/json" && contentType !== "multipart/form-data" ? `'Content-Type': '${contentType}'` : void 0;
447
- const headers = [headerParams.length ? headerParamsMapping ? "...mappedHeaders" : "...headers" : void 0, contentTypeHeader].filter(Boolean);
446
+ const pathParamsMapping = paramsCasing ? buildTransformedParamsMapping(originalPathParams, camelCase) : null;
447
+ const queryParamsMapping = paramsCasing ? buildTransformedParamsMapping(originalQueryParams, camelCase) : null;
448
+ const headerParamsMapping = paramsCasing ? buildTransformedParamsMapping(originalHeaderParams, camelCase) : null;
449
+ const contentTypeHeader = contentType && contentType !== "application/json" && contentType !== "multipart/form-data" ? `'Content-Type': '${contentType}'` : null;
450
+ const headers = [headerParams.length ? headerParamsMapping ? "...mappedHeaders" : "...headers" : null, contentTypeHeader].filter(Boolean);
448
451
  const fetchConfig = [];
449
452
  fetchConfig.push(`method: ${JSON.stringify(node.method.toUpperCase())}`);
450
- fetchConfig.push(`url: ${urlPath.template}`);
453
+ fetchConfig.push(`url: ${Url.toTemplateString(node.path)}`);
451
454
  if (baseURL) fetchConfig.push(`baseURL: \`${baseURL}\``);
452
455
  if (queryParams.length) fetchConfig.push(queryParamsMapping ? "params: mappedParams" : "params");
453
456
  if (requestName) fetchConfig.push(`data: ${isFormData ? "formData as FormData" : "requestData"}`);
454
457
  if (headers.length) fetchConfig.push(`headers: { ${headers.join(", ")} }`);
455
- const callToolResult = dataReturnType === "data" ? `return {
456
- content: [
457
- {
458
- type: 'text',
459
- text: JSON.stringify(res.data)
460
- }
461
- ],
462
- structuredContent: { data: res.data }
463
- }` : `return {
464
- content: [
465
- {
466
- type: 'text',
467
- text: JSON.stringify(res)
468
- }
469
- ],
470
- structuredContent: { data: res.data }
471
- }`;
458
+ const callToolResult = `return {
459
+ content: [
460
+ {
461
+ type: 'text',
462
+ text: JSON.stringify(${dataReturnType === "data" ? "res.data" : "res"})
463
+ }
464
+ ],
465
+ structuredContent: { data: res.data }
466
+ }`;
472
467
  return /* @__PURE__ */ (0, _kubb_renderer_jsx_jsx_runtime.jsx)(_kubb_renderer_jsx.File.Source, {
473
468
  name,
474
469
  isExportable: true,
@@ -478,7 +473,7 @@ function McpHandler({ name, node, resolver, baseURL, dataReturnType, paramsCasin
478
473
  async: true,
479
474
  export: true,
480
475
  params: paramsSignature,
481
- JSDoc: { comments: getComments(node) },
476
+ JSDoc: { comments: buildOperationComments(node) },
482
477
  returnType: "Promise<CallToolResult>",
483
478
  children: [
484
479
  "",
@@ -500,7 +495,7 @@ function McpHandler({ name, node, resolver, baseURL, dataReturnType, paramsCasin
500
495
  /* @__PURE__ */ (0, _kubb_renderer_jsx_jsx_runtime.jsx)("br", {}),
501
496
  isFormData && requestName && "const formData = buildFormData(requestData)",
502
497
  /* @__PURE__ */ (0, _kubb_renderer_jsx_jsx_runtime.jsx)("br", {}),
503
- `const res = await fetch<${generics.join(", ")}>({ ${fetchConfig.join(", ")} }, request)`,
498
+ `const res = await client<${generics.join(", ")}>({ ${fetchConfig.join(", ")} }, request)`,
504
499
  /* @__PURE__ */ (0, _kubb_renderer_jsx_jsx_runtime.jsx)("br", {}),
505
500
  callToolResult
506
501
  ]
@@ -508,62 +503,80 @@ function McpHandler({ name, node, resolver, baseURL, dataReturnType, paramsCasin
508
503
  });
509
504
  }
510
505
  //#endregion
506
+ //#region src/utils.ts
507
+ /**
508
+ * Render a group param value — compose individual schemas into `z.object({ ... })`,
509
+ * or use a schema name string directly.
510
+ */
511
+ function zodGroupExpr(entry) {
512
+ if (typeof entry === "string") return entry;
513
+ return `z.object({ ${entry.map((p) => `${JSON.stringify(p.name)}: ${p.schemaName}`).join(", ")} })`;
514
+ }
515
+ /**
516
+ * Convert a SchemaNode type to an inline Zod expression string.
517
+ * Used as fallback when no named zod schema is available for a path parameter.
518
+ */
519
+ function zodExprFromSchemaNode(schema) {
520
+ const baseExpr = (() => {
521
+ if (schema.type === "enum") {
522
+ const rawValues = schema.namedEnumValues?.length ? schema.namedEnumValues.map((v) => v.value) : (schema.enumValues ?? []).filter((v) => v !== null);
523
+ if (rawValues.length > 0 && rawValues.every((v) => typeof v === "string")) return `z.enum([${rawValues.map((v) => JSON.stringify(v)).join(", ")}])`;
524
+ if (rawValues.length > 0) {
525
+ const literals = rawValues.map((v) => `z.literal(${JSON.stringify(v)})`);
526
+ return literals.length === 1 ? literals[0] : `z.union([${literals.join(", ")}])`;
527
+ }
528
+ return "z.string()";
529
+ }
530
+ if (schema.type === "integer") return "z.coerce.number()";
531
+ if (schema.type === "number") return "z.number()";
532
+ if (schema.type === "boolean") return "z.boolean()";
533
+ if (schema.type === "array") return "z.array(z.unknown())";
534
+ return "z.string()";
535
+ })();
536
+ return schema.nullable ? `${baseExpr}.nullable()` : baseExpr;
537
+ }
538
+ //#endregion
511
539
  //#region src/components/Server.tsx
512
540
  const keysPrinter = (0, _kubb_plugin_ts.functionPrinter)({ mode: "keys" });
513
541
  function Server({ name, serverName, serverVersion, paramsCasing, operations }) {
514
- return /* @__PURE__ */ (0, _kubb_renderer_jsx_jsx_runtime.jsxs)(_kubb_renderer_jsx.File.Source, {
515
- name,
516
- isExportable: true,
517
- isIndexable: true,
518
- children: [
519
- /* @__PURE__ */ (0, _kubb_renderer_jsx_jsx_runtime.jsx)(_kubb_renderer_jsx.Const, {
520
- name: "server",
521
- export: true,
522
- children: `
523
- new McpServer({
524
- name: '${serverName}',
525
- version: '${serverVersion}',
526
- })
527
- `
528
- }),
529
- operations.map(({ tool, mcp, zod, node }) => {
530
- const pathParams = _kubb_core.ast.caseParams(node.parameters, paramsCasing).filter((p) => p.in === "path");
531
- const pathEntries = [];
532
- const otherEntries = [];
533
- for (const p of pathParams) {
534
- const zodParam = zod.pathParams.find((zp) => zp.name === p.name);
535
- pathEntries.push({
536
- key: p.name,
537
- value: zodParam ? zodParam.schemaName : zodExprFromSchemaNode(p.schema)
538
- });
539
- }
540
- if (zod.requestName) otherEntries.push({
541
- key: "data",
542
- value: zod.requestName
543
- });
544
- if (zod.queryParams) otherEntries.push({
545
- key: "params",
546
- value: zodGroupExpr(zod.queryParams)
547
- });
548
- if (zod.headerParams) otherEntries.push({
549
- key: "headers",
550
- value: zodGroupExpr(zod.headerParams)
551
- });
552
- otherEntries.sort((a, b) => a.key.localeCompare(b.key));
553
- const entries = [...pathEntries, ...otherEntries];
554
- const paramsNode = entries.length ? _kubb_core.ast.createFunctionParameters({ params: [_kubb_core.ast.createParameterGroup({ properties: entries.map((e) => _kubb_core.ast.createFunctionParameter({
555
- name: e.key,
556
- optional: false
557
- })) })] }) : void 0;
558
- const destructured = paramsNode ? keysPrinter.print(paramsNode) ?? "" : "";
559
- const inputSchema = entries.length ? `{ ${entries.map((e) => `${e.key}: ${e.value}`).join(", ")} }` : void 0;
560
- const outputSchema = zod.responseName;
561
- const config = [
562
- tool.title ? `title: ${JSON.stringify(tool.title)}` : null,
563
- `description: ${JSON.stringify(tool.description)}`,
564
- outputSchema ? `outputSchema: { data: ${outputSchema} }` : null
565
- ].filter(Boolean).join(",\n ");
566
- if (inputSchema) return `
542
+ const registrations = operations.map(({ tool, mcp, zod, node }) => {
543
+ const { path: pathParams } = getOperationParameters(node, { paramsCasing });
544
+ const pathEntries = [];
545
+ const otherEntries = [];
546
+ for (const p of pathParams) {
547
+ const zodParam = zod.pathParams.find((zp) => zp.name === p.name);
548
+ pathEntries.push({
549
+ key: p.name,
550
+ value: zodParam ? zodParam.schemaName : zodExprFromSchemaNode(p.schema)
551
+ });
552
+ }
553
+ if (zod.requestName) otherEntries.push({
554
+ key: "data",
555
+ value: zod.requestName
556
+ });
557
+ if (zod.queryParams) otherEntries.push({
558
+ key: "params",
559
+ value: zodGroupExpr(zod.queryParams)
560
+ });
561
+ if (zod.headerParams) otherEntries.push({
562
+ key: "headers",
563
+ value: zodGroupExpr(zod.headerParams)
564
+ });
565
+ otherEntries.sort((a, b) => a.key.localeCompare(b.key));
566
+ const entries = [...pathEntries, ...otherEntries];
567
+ const paramsNode = entries.length ? _kubb_core.ast.createFunctionParameters({ params: [_kubb_core.ast.createParameterGroup({ properties: entries.map((e) => _kubb_core.ast.createFunctionParameter({
568
+ name: e.key,
569
+ optional: false
570
+ })) })] }) : null;
571
+ const destructured = paramsNode ? keysPrinter.print(paramsNode) ?? "" : "";
572
+ const inputSchema = entries.length ? `{ ${entries.map((e) => `${e.key}: ${e.value}`).join(", ")} }` : null;
573
+ const outputSchema = zod.responseName;
574
+ const config = [
575
+ tool.title ? `title: ${JSON.stringify(tool.title)}` : null,
576
+ `description: ${JSON.stringify(tool.description)}`,
577
+ outputSchema ? `outputSchema: { data: ${outputSchema} }` : null
578
+ ].filter(Boolean).join(",\n ");
579
+ if (inputSchema) return `
567
580
  server.registerTool(${JSON.stringify(tool.name)}, {
568
581
  ${config},
569
582
  inputSchema: ${inputSchema},
@@ -571,14 +584,34 @@ server.registerTool(${JSON.stringify(tool.name)}, {
571
584
  return ${mcp.name}(${destructured}, request)
572
585
  })
573
586
  `;
574
- return `
587
+ return `
575
588
  server.registerTool(${JSON.stringify(tool.name)}, {
576
589
  ${config},
577
590
  }, async (request) => {
578
591
  return ${mcp.name}(request)
579
592
  })
580
593
  `;
581
- }).filter(Boolean),
594
+ }).filter(Boolean).join("\n");
595
+ return /* @__PURE__ */ (0, _kubb_renderer_jsx_jsx_runtime.jsxs)(_kubb_renderer_jsx.File.Source, {
596
+ name,
597
+ isExportable: true,
598
+ isIndexable: true,
599
+ children: [
600
+ /* @__PURE__ */ (0, _kubb_renderer_jsx_jsx_runtime.jsx)(_kubb_renderer_jsx.Function, {
601
+ name: "getServer",
602
+ export: true,
603
+ children: `const server = new McpServer({
604
+ name: '${serverName}',
605
+ version: '${serverVersion}',
606
+ })
607
+ ${registrations}
608
+ return server`
609
+ }),
610
+ /* @__PURE__ */ (0, _kubb_renderer_jsx_jsx_runtime.jsx)(_kubb_renderer_jsx.Const, {
611
+ name: "server",
612
+ export: true,
613
+ children: "getServer()"
614
+ }),
582
615
  /* @__PURE__ */ (0, _kubb_renderer_jsx_jsx_runtime.jsx)(_kubb_renderer_jsx.Function, {
583
616
  name: "startServer",
584
617
  async: true,
@@ -597,29 +630,28 @@ server.registerTool(${JSON.stringify(tool.name)}, {
597
630
  }
598
631
  //#endregion
599
632
  //#region src/generators/mcpGenerator.tsx
633
+ /**
634
+ * Built-in operation generator for `@kubb/plugin-mcp`. Emits one MCP tool
635
+ * handler per OpenAPI operation, wiring the input Zod schema, the HTTP call,
636
+ * and the response shape into a single function that an MCP server can
637
+ * register as a callable tool.
638
+ */
600
639
  const mcpGenerator = (0, _kubb_core.defineGenerator)({
601
640
  name: "mcp",
602
641
  renderer: _kubb_renderer_jsx.jsxRenderer,
603
642
  operation(node, ctx) {
643
+ if (!_kubb_core.ast.isHttpOperationNode(node)) return null;
604
644
  const { resolver, driver, root } = ctx;
605
645
  const { output, client, paramsCasing, group } = ctx.options;
606
646
  const pluginTs = driver.getPlugin(_kubb_plugin_ts.pluginTsName);
607
647
  if (!pluginTs) return null;
608
648
  const tsResolver = driver.getResolver(_kubb_plugin_ts.pluginTsName);
609
- const casedParams = _kubb_core.ast.caseParams(node.parameters, paramsCasing);
610
- const pathParams = casedParams.filter((p) => p.in === "path");
611
- const queryParams = casedParams.filter((p) => p.in === "query");
612
- const headerParams = casedParams.filter((p) => p.in === "header");
613
- const importedTypeNames = [
614
- ...pathParams.map((p) => tsResolver.resolvePathParamsName(node, p)),
615
- ...queryParams.map((p) => tsResolver.resolveQueryParamsName(node, p)),
616
- ...headerParams.map((p) => tsResolver.resolveHeaderParamsName(node, p)),
617
- node.requestBody?.content?.[0]?.schema ? tsResolver.resolveDataName(node) : void 0,
618
- tsResolver.resolveResponseName(node),
619
- ...node.responses.filter((r) => Number(r.statusCode) >= 400).map((r) => tsResolver.resolveResponseStatusName(node, r.statusCode))
620
- ].filter(Boolean);
649
+ const importedTypeNames = resolveOperationTypeNames(node, tsResolver, {
650
+ paramsCasing,
651
+ responseStatusNames: "error"
652
+ });
621
653
  const meta = {
622
- name: resolver.resolveName(node.operationId),
654
+ name: resolver.resolveHandlerName(node),
623
655
  file: resolver.resolveFile({
624
656
  name: node.operationId,
625
657
  extname: ".ts",
@@ -628,7 +660,7 @@ const mcpGenerator = (0, _kubb_core.defineGenerator)({
628
660
  }, {
629
661
  root,
630
662
  output,
631
- group
663
+ group: group ?? void 0
632
664
  }),
633
665
  fileTs: tsResolver.resolveFile({
634
666
  name: node.operationId,
@@ -638,7 +670,7 @@ const mcpGenerator = (0, _kubb_core.defineGenerator)({
638
670
  }, {
639
671
  root,
640
672
  output: pluginTs.options?.output ?? output,
641
- group: pluginTs.options?.group
673
+ group: pluginTs.options?.group ?? void 0
642
674
  })
643
675
  };
644
676
  return /* @__PURE__ */ (0, _kubb_renderer_jsx_jsx_runtime.jsxs)(_kubb_renderer_jsx.File, {
@@ -682,7 +714,7 @@ const mcpGenerator = (0, _kubb_core.defineGenerator)({
682
714
  isTypeOnly: true
683
715
  }),
684
716
  /* @__PURE__ */ (0, _kubb_renderer_jsx_jsx_runtime.jsx)(_kubb_renderer_jsx.File.Import, {
685
- name: "fetch",
717
+ name: "client",
686
718
  path: client.importPath
687
719
  }),
688
720
  client.dataReturnType === "full" && /* @__PURE__ */ (0, _kubb_renderer_jsx_jsx_runtime.jsx)(_kubb_renderer_jsx.File.Import, {
@@ -698,18 +730,18 @@ const mcpGenerator = (0, _kubb_core.defineGenerator)({
698
730
  "ResponseErrorConfig"
699
731
  ],
700
732
  root: meta.file.path,
701
- path: node_path.default.resolve(root, ".kubb/fetch.ts"),
733
+ path: node_path.default.resolve(root, ".kubb/client.ts"),
702
734
  isTypeOnly: true
703
735
  }),
704
736
  /* @__PURE__ */ (0, _kubb_renderer_jsx_jsx_runtime.jsx)(_kubb_renderer_jsx.File.Import, {
705
- name: ["fetch"],
737
+ name: ["client"],
706
738
  root: meta.file.path,
707
- path: node_path.default.resolve(root, ".kubb/fetch.ts")
739
+ path: node_path.default.resolve(root, ".kubb/client.ts")
708
740
  }),
709
741
  client.dataReturnType === "full" && /* @__PURE__ */ (0, _kubb_renderer_jsx_jsx_runtime.jsx)(_kubb_renderer_jsx.File.Import, {
710
742
  name: ["ResponseConfig"],
711
743
  root: meta.file.path,
712
- path: node_path.default.resolve(root, ".kubb/fetch.ts"),
744
+ path: node_path.default.resolve(root, ".kubb/client.ts"),
713
745
  isTypeOnly: true
714
746
  })
715
747
  ] }),
@@ -738,7 +770,7 @@ const serverGenerator = (0, _kubb_core.defineGenerator)({
738
770
  name: "operations",
739
771
  renderer: _kubb_renderer_jsx.jsxRenderer,
740
772
  operations(nodes, ctx) {
741
- const { adapter, config, resolver, plugin, driver, root } = ctx;
773
+ const { config, resolver, plugin, driver, root } = ctx;
742
774
  const { output, paramsCasing, group } = ctx.options;
743
775
  const pluginZod = driver.getPlugin(_kubb_plugin_zod.pluginZodName);
744
776
  if (!pluginZod) return;
@@ -754,11 +786,8 @@ const serverGenerator = (0, _kubb_core.defineGenerator)({
754
786
  path: node_path.default.resolve(root, output.path, ".mcp.json"),
755
787
  meta: { pluginName: plugin.name }
756
788
  };
757
- const operationsMapped = nodes.map((node) => {
758
- const casedParams = _kubb_core.ast.caseParams(node.parameters, paramsCasing);
759
- const pathParams = casedParams.filter((p) => p.in === "path");
760
- const queryParams = casedParams.filter((p) => p.in === "query");
761
- const headerParams = casedParams.filter((p) => p.in === "header");
789
+ const operationsMapped = nodes.filter(_kubb_core.ast.isHttpOperationNode).map((node) => {
790
+ const { path: pathParams, query: queryParams, header: headerParams } = getOperationParameters(node, { paramsCasing });
762
791
  const mcpFile = resolver.resolveFile({
763
792
  name: node.operationId,
764
793
  extname: ".ts",
@@ -767,7 +796,7 @@ const serverGenerator = (0, _kubb_core.defineGenerator)({
767
796
  }, {
768
797
  root,
769
798
  output,
770
- group
799
+ group: group ?? void 0
771
800
  });
772
801
  const zodFile = zodResolver.resolveFile({
773
802
  name: node.operationId,
@@ -777,11 +806,11 @@ const serverGenerator = (0, _kubb_core.defineGenerator)({
777
806
  }, {
778
807
  root,
779
808
  output: pluginZod.options?.output ?? output,
780
- group: pluginZod.options?.group
809
+ group: pluginZod.options?.group ?? void 0
781
810
  });
782
- const requestName = node.requestBody?.content?.[0]?.schema ? zodResolver.resolveDataName(node) : void 0;
811
+ const requestName = node.requestBody?.content?.[0]?.schema ? zodResolver.resolveDataName(node) : null;
783
812
  const successStatus = findSuccessStatusCode(node.responses);
784
- const responseName = successStatus ? zodResolver.resolveResponseStatusName(node, successStatus) : void 0;
813
+ const responseName = successStatus ? zodResolver.resolveResponseStatusName(node, successStatus) : null;
785
814
  const resolveParams = (params) => params.map((p) => ({
786
815
  name: p.name,
787
816
  schemaName: zodResolver.resolveParamName(node, p)
@@ -793,13 +822,13 @@ const serverGenerator = (0, _kubb_core.defineGenerator)({
793
822
  description: node.description || `Make a ${node.method.toUpperCase()} request to ${node.path}`
794
823
  },
795
824
  mcp: {
796
- name: resolver.resolveName(node.operationId),
825
+ name: resolver.resolveHandlerName(node),
797
826
  file: mcpFile
798
827
  },
799
828
  zod: {
800
829
  pathParams: resolveParams(pathParams),
801
- queryParams: queryParams.length ? resolveParams(queryParams) : void 0,
802
- headerParams: headerParams.length ? resolveParams(headerParams) : void 0,
830
+ queryParams: queryParams.length ? resolveParams(queryParams) : null,
831
+ headerParams: headerParams.length ? resolveParams(headerParams) : null,
803
832
  requestName,
804
833
  responseName,
805
834
  file: zodFile
@@ -814,7 +843,7 @@ const serverGenerator = (0, _kubb_core.defineGenerator)({
814
843
  ...(zod.headerParams ?? []).map((p) => p.schemaName),
815
844
  zod.requestName,
816
845
  zod.responseName
817
- ].filter(Boolean);
846
+ ].filter((name) => Boolean(name));
818
847
  const uniqueNames = [...new Set(zodNames)].sort();
819
848
  return [/* @__PURE__ */ (0, _kubb_renderer_jsx_jsx_runtime.jsx)(_kubb_renderer_jsx.File.Import, {
820
849
  name: [mcp.name],
@@ -830,13 +859,21 @@ const serverGenerator = (0, _kubb_core.defineGenerator)({
830
859
  baseName: serverFile.baseName,
831
860
  path: serverFile.path,
832
861
  meta: serverFile.meta,
833
- banner: resolver.resolveBanner(adapter.inputNode, {
862
+ banner: resolver.resolveBanner(ctx.meta, {
834
863
  output,
835
- config
864
+ config,
865
+ file: {
866
+ path: serverFile.path,
867
+ baseName: serverFile.baseName
868
+ }
836
869
  }),
837
- footer: resolver.resolveFooter(adapter.inputNode, {
870
+ footer: resolver.resolveFooter(ctx.meta, {
838
871
  output,
839
- config
872
+ config,
873
+ file: {
874
+ path: serverFile.path,
875
+ baseName: serverFile.baseName
876
+ }
840
877
  }),
841
878
  children: [
842
879
  /* @__PURE__ */ (0, _kubb_renderer_jsx_jsx_runtime.jsx)(_kubb_renderer_jsx.File.Import, {
@@ -854,8 +891,8 @@ const serverGenerator = (0, _kubb_core.defineGenerator)({
854
891
  imports,
855
892
  /* @__PURE__ */ (0, _kubb_renderer_jsx_jsx_runtime.jsx)(Server, {
856
893
  name,
857
- serverName: adapter.inputNode?.meta?.title ?? "server",
858
- serverVersion: adapter.inputNode?.meta?.version ?? "0.0.0",
894
+ serverName: ctx.meta.title ?? "server",
895
+ serverVersion: ctx.meta.version ?? "0.0.0",
859
896
  paramsCasing,
860
897
  operations: operationsMapped
861
898
  })
@@ -869,7 +906,7 @@ const serverGenerator = (0, _kubb_core.defineGenerator)({
869
906
  children: `
870
907
  {
871
908
  "mcpServers": {
872
- "${adapter.inputNode?.meta?.title || "server"}": {
909
+ "${ctx.meta.title || "server"}": {
873
910
  "type": "stdio",
874
911
  "command": "npx",
875
912
  "args": ["tsx", "${node_path.default.relative(node_path.default.dirname(jsonFile.path), serverFile.path)}"]
@@ -884,41 +921,77 @@ const serverGenerator = (0, _kubb_core.defineGenerator)({
884
921
  //#endregion
885
922
  //#region src/resolvers/resolverMcp.ts
886
923
  /**
887
- * Naming convention resolver for MCP plugin.
924
+ * Default resolver used by `@kubb/plugin-mcp`. Decides the names and file
925
+ * paths for every generated MCP tool handler. Function names get a `Handler`
926
+ * suffix so an operation `addPet` becomes `addPetHandler`.
888
927
  *
889
- * Provides default naming helpers using camelCase with a `handler` suffix for functions.
928
+ * @example Resolve a handler name
929
+ * ```ts
930
+ * import { resolverMcp } from '@kubb/plugin-mcp'
890
931
  *
891
- * @example
892
- * `resolverMcp.default('addPet', 'function') // → 'addPetHandler'`
932
+ * resolverMcp.default('addPet', 'function') // 'addPetHandler'
933
+ * ```
893
934
  */
894
- const resolverMcp = (0, _kubb_core.defineResolver)((ctx) => ({
935
+ const resolverMcp = (0, _kubb_core.defineResolver)(() => ({
895
936
  name: "default",
896
937
  pluginName: "plugin-mcp",
897
938
  default(name, type) {
898
- if (type === "file") return camelCase(name, { isFile: true });
939
+ if (type === "file") return toFilePath(name);
899
940
  return camelCase(name, { suffix: "handler" });
900
941
  },
901
942
  resolveName(name) {
902
- return ctx.default(name, "function");
943
+ return this.default(name, "function");
944
+ },
945
+ resolvePathName(name, type) {
946
+ return this.default(name, type);
947
+ },
948
+ resolveHandlerName(node) {
949
+ return this.resolveName(node.operationId);
903
950
  }
904
951
  }));
905
952
  //#endregion
906
953
  //#region src/plugin.ts
954
+ /**
955
+ * Canonical plugin name for `@kubb/plugin-mcp`. Used for driver lookups and
956
+ * cross-plugin dependency references.
957
+ */
907
958
  const pluginMcpName = "plugin-mcp";
959
+ /**
960
+ * Generates a Model Context Protocol (MCP) server from an OpenAPI spec. Every
961
+ * operation becomes a typed MCP tool that AI assistants (Claude Desktop, Claude
962
+ * Code, MCP-compatible clients) can call directly.
963
+ *
964
+ * @example
965
+ * ```ts
966
+ * import { defineConfig } from 'kubb'
967
+ * import { pluginTs } from '@kubb/plugin-ts'
968
+ * import { pluginClient } from '@kubb/plugin-client'
969
+ * import { pluginZod } from '@kubb/plugin-zod'
970
+ * import { pluginMcp } from '@kubb/plugin-mcp'
971
+ *
972
+ * export default defineConfig({
973
+ * input: { path: './petStore.yaml' },
974
+ * output: { path: './src/gen' },
975
+ * plugins: [
976
+ * pluginTs(),
977
+ * pluginClient(),
978
+ * pluginZod(),
979
+ * pluginMcp({
980
+ * output: { path: './mcp' },
981
+ * client: { baseURL: 'https://petstore.swagger.io/v2' },
982
+ * }),
983
+ * ],
984
+ * })
985
+ * ```
986
+ */
908
987
  const pluginMcp = (0, _kubb_core.definePlugin)((options) => {
909
988
  const { output = {
910
989
  path: "mcp",
911
- barrelType: "named"
990
+ barrel: { type: "named" }
912
991
  }, group, exclude = [], include, override = [], paramsCasing, client, resolver: userResolver, transformer: userTransformer, generators: userGenerators = [] } = options;
913
992
  const clientName = client?.client ?? "axios";
914
993
  const clientImportPath = client?.importPath ?? (!client?.bundle ? `@kubb/plugin-client/clients/${clientName}` : void 0);
915
- const groupConfig = group ? {
916
- ...group,
917
- name: group.name ? group.name : (ctx) => {
918
- if (group.type === "path") return `${ctx.group.split("/")[1]}`;
919
- return `${camelCase(ctx.group)}Requests`;
920
- }
921
- } : void 0;
994
+ const groupConfig = createGroupConfig(group);
922
995
  return {
923
996
  name: pluginMcpName,
924
997
  options,
@@ -954,10 +1027,10 @@ const pluginMcp = (0, _kubb_core.definePlugin)((options) => {
954
1027
  const root = node_path$1.default.resolve(ctx.config.root, ctx.config.output.path);
955
1028
  const hasClientPlugin = ctx.config.plugins?.some((p) => p.name === _kubb_plugin_client.pluginClientName);
956
1029
  if (client?.bundle && !hasClientPlugin && !clientImportPath) ctx.injectFile({
957
- baseName: "fetch.ts",
958
- path: node_path$1.default.resolve(root, ".kubb/fetch.ts"),
1030
+ baseName: "client.ts",
1031
+ path: node_path$1.default.resolve(root, ".kubb/client.ts"),
959
1032
  sources: [_kubb_core.ast.createSource({
960
- name: "fetch",
1033
+ name: "client",
961
1034
  nodes: [_kubb_core.ast.createText(clientName === "fetch" ? _kubb_plugin_client_templates_clients_fetch_source.source : _kubb_plugin_client_templates_clients_axios_source.source)],
962
1035
  isExportable: true,
963
1036
  isIndexable: true