@kubb/plugin-mcp 5.0.0-beta.42 → 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 `null` 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 // null
256
- * ```
257
- */
258
- get params() {
259
- return this.toParamsObject();
260
- }
261
- #transformParam(raw) {
262
- const param = isValidVarName(raw) ? raw : camelCase(raw);
263
- return this.#options.casing === "camelcase" ? camelCase(param) : param;
264
- }
265
- /**
266
- * Iterates over every `{param}` token in `path`, calling `fn` with the raw token and transformed name.
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.toParamsObject()
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,57 +273,13 @@ 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
- const result = 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
- return `\`${prefix ?? ""}${result}\``;
300
- }
301
- /**
302
- * Extracts all `{param}` segments from the path and returns them as a key-value map.
303
- * An optional `replacer` transforms each parameter name in both key and value positions.
304
- * Returns `undefined` when no path parameters are found.
305
- *
306
- * @example
307
- * ```ts
308
- * new URLPath('/pet/{petId}/tag/{tagId}').toParamsObject()
309
- * // { petId: 'petId', tagId: 'tagId' }
310
- * ```
311
- */
312
- toParamsObject(replacer) {
313
- const params = {};
314
- this.#eachParam((_raw, param) => {
315
- const key = replacer ? replacer(param) : param;
316
- params[key] = key;
317
- });
318
- return Object.keys(params).length > 0 ? params : null;
319
- }
320
- /** Converts the OpenAPI path to Express-style colon syntax.
321
- *
322
- * @example
323
- * ```ts
324
- * new URLPath('/pet/{petId}').toURLPath() // '/pet/:petId'
325
- * ```
326
- */
327
- toURLPath() {
328
- return this.path.replace(/\{([^}]+)\}/g, ":$1");
329
- }
330
276
  };
331
277
  //#endregion
332
278
  //#region ../../internals/shared/src/operation.ts
333
279
  function getOperationLink(node, link) {
334
280
  if (!link) return null;
335
281
  if (typeof link === "function") return link(node) ?? null;
336
- if (link === "urlPath") return node.path ? `{@link ${new URLPath(node.path).URL}}` : null;
282
+ if (link === "urlPath") return node.path ? `{@link ${Url.toPath(node.path)}}` : null;
337
283
  return node.path ? `{@link ${node.path.replaceAll("{", ":").replaceAll("}", "")}}` : null;
338
284
  }
339
285
  function buildOperationComments(node, options = {}) {
@@ -423,26 +369,24 @@ function findSuccessStatusCode(responses) {
423
369
  * shared default naming so every plugin groups output consistently:
424
370
  *
425
371
  * - `path` groups use the second path segment (`/pet/findByStatus` → `pet`).
426
- * - other groups use `${camelCase(group)}${suffix}` (e.g. `petController`).
372
+ * - other groups use the camelCased group (`pet store``petStore`).
427
373
  *
428
374
  * A user-provided `group.name` always wins over the default namer, so callers stay in
429
375
  * control of their output folders. Returns `null` when grouping is disabled, matching the
430
376
  * per-plugin convention.
431
377
  *
432
378
  * @param group - The user-supplied group option, or `undefined` to disable grouping.
433
- * @param options.suffix - Appended to non-`path` group names, e.g. `'Controller'` or `'Requests'`.
434
379
  *
435
380
  * @example
436
381
  * ```ts
437
- * createGroupConfig(group, { suffix: 'Controller' }) // plugin-ts, plugin-client, …
438
- * createGroupConfig(group, { suffix: 'Requests' }) // plugin-cypress, plugin-mcp
382
+ * createGroupConfig(group) // shared across every plugin
439
383
  * ```
440
384
  */
441
- function createGroupConfig(group, options) {
385
+ function createGroupConfig(group) {
442
386
  if (!group) return null;
443
387
  const defaultName = (ctx) => {
444
388
  if (group.type === "path") return `${ctx.group.split("/")[1]}`;
445
- return `${camelCase(ctx.group)}${options.suffix}`;
389
+ return camelCase(ctx.group);
446
390
  };
447
391
  return {
448
392
  ...group,
@@ -479,7 +423,6 @@ function buildRemappingCode(mapping, varName, sourceName) {
479
423
  const declarationPrinter = (0, _kubb_plugin_ts.functionPrinter)({ mode: "declaration" });
480
424
  function McpHandler({ name, node, resolver, baseURL, dataReturnType, paramsCasing }) {
481
425
  if (!_kubb_core.ast.isHttpOperationNode(node)) return null;
482
- const urlPath = new URLPath(node.path);
483
426
  const contentType = node.requestBody?.content?.[0]?.contentType;
484
427
  const isFormData = contentType === "multipart/form-data";
485
428
  const { query: queryParams, header: headerParams } = getOperationParameters(node, { paramsCasing });
@@ -507,28 +450,20 @@ function McpHandler({ name, node, resolver, baseURL, dataReturnType, paramsCasin
507
450
  const headers = [headerParams.length ? headerParamsMapping ? "...mappedHeaders" : "...headers" : null, contentTypeHeader].filter(Boolean);
508
451
  const fetchConfig = [];
509
452
  fetchConfig.push(`method: ${JSON.stringify(node.method.toUpperCase())}`);
510
- fetchConfig.push(`url: ${urlPath.template}`);
453
+ fetchConfig.push(`url: ${Url.toTemplateString(node.path)}`);
511
454
  if (baseURL) fetchConfig.push(`baseURL: \`${baseURL}\``);
512
455
  if (queryParams.length) fetchConfig.push(queryParamsMapping ? "params: mappedParams" : "params");
513
456
  if (requestName) fetchConfig.push(`data: ${isFormData ? "formData as FormData" : "requestData"}`);
514
457
  if (headers.length) fetchConfig.push(`headers: { ${headers.join(", ")} }`);
515
- const callToolResult = dataReturnType === "data" ? `return {
516
- content: [
517
- {
518
- type: 'text',
519
- text: JSON.stringify(res.data)
520
- }
521
- ],
522
- structuredContent: { data: res.data }
523
- }` : `return {
524
- content: [
525
- {
526
- type: 'text',
527
- text: JSON.stringify(res)
528
- }
529
- ],
530
- structuredContent: { data: res.data }
531
- }`;
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
+ }`;
532
467
  return /* @__PURE__ */ (0, _kubb_renderer_jsx_jsx_runtime.jsx)(_kubb_renderer_jsx.File.Source, {
533
468
  name,
534
469
  isExportable: true,
@@ -703,7 +638,7 @@ return server`
703
638
  */
704
639
  const mcpGenerator = (0, _kubb_core.defineGenerator)({
705
640
  name: "mcp",
706
- renderer: _kubb_renderer_jsx.jsxRendererSync,
641
+ renderer: _kubb_renderer_jsx.jsxRenderer,
707
642
  operation(node, ctx) {
708
643
  if (!_kubb_core.ast.isHttpOperationNode(node)) return null;
709
644
  const { resolver, driver, root } = ctx;
@@ -833,7 +768,7 @@ const mcpGenerator = (0, _kubb_core.defineGenerator)({
833
768
  */
834
769
  const serverGenerator = (0, _kubb_core.defineGenerator)({
835
770
  name: "operations",
836
- renderer: _kubb_renderer_jsx.jsxRendererSync,
771
+ renderer: _kubb_renderer_jsx.jsxRenderer,
837
772
  operations(nodes, ctx) {
838
773
  const { config, resolver, plugin, driver, root } = ctx;
839
774
  const { output, paramsCasing, group } = ctx.options;
@@ -1001,7 +936,7 @@ const resolverMcp = (0, _kubb_core.defineResolver)(() => ({
1001
936
  name: "default",
1002
937
  pluginName: "plugin-mcp",
1003
938
  default(name, type) {
1004
- if (type === "file") return camelCase(name, { isFile: true });
939
+ if (type === "file") return toFilePath(name);
1005
940
  return camelCase(name, { suffix: "handler" });
1006
941
  },
1007
942
  resolveName(name) {
@@ -1052,11 +987,11 @@ const pluginMcpName = "plugin-mcp";
1052
987
  const pluginMcp = (0, _kubb_core.definePlugin)((options) => {
1053
988
  const { output = {
1054
989
  path: "mcp",
1055
- barrelType: "named"
990
+ barrel: { type: "named" }
1056
991
  }, group, exclude = [], include, override = [], paramsCasing, client, resolver: userResolver, transformer: userTransformer, generators: userGenerators = [] } = options;
1057
992
  const clientName = client?.client ?? "axios";
1058
993
  const clientImportPath = client?.importPath ?? (!client?.bundle ? `@kubb/plugin-client/clients/${clientName}` : void 0);
1059
- const groupConfig = createGroupConfig(group, { suffix: "Requests" });
994
+ const groupConfig = createGroupConfig(group);
1060
995
  return {
1061
996
  name: pluginMcpName,
1062
997
  options,