@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.js CHANGED
@@ -2,7 +2,7 @@ import "./chunk-C0LytTxp.js";
2
2
  import path from "node:path";
3
3
  import { ast, defineGenerator, definePlugin, defineResolver } from "@kubb/core";
4
4
  import { functionPrinter, pluginTsName } from "@kubb/plugin-ts";
5
- import { Const, File, Function, jsxRendererSync } from "@kubb/renderer-jsx";
5
+ import { Const, File, Function, jsxRenderer } from "@kubb/renderer-jsx";
6
6
  import { Fragment, jsx, jsxs } from "@kubb/renderer-jsx/jsx-runtime";
7
7
  import { pluginZodName } from "@kubb/plugin-zod";
8
8
  import { pluginClientName } from "@kubb/plugin-client";
@@ -20,36 +20,45 @@ import { source as source$2 } from "@kubb/plugin-client/templates/config.source"
20
20
  function toCamelOrPascal(text, pascal) {
21
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
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);
23
+ return (i === 0 && !pascal ? word.charAt(0).toLowerCase() : word.charAt(0).toUpperCase()) + word.slice(1);
25
24
  }).join("").replace(/[^a-zA-Z0-9]/g, "");
26
25
  }
27
26
  /**
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.
27
+ * Converts `text` to camelCase.
28
+ *
29
+ * @example Word boundaries
30
+ * `camelCase('hello-world') // 'helloWorld'`
31
31
  *
32
- * Only splits on dots followed by a letter so that version numbers
33
- * embedded in operationIds (e.g. `v2025.0`) are kept intact.
32
+ * @example With a prefix
33
+ * `camelCase('tag', { prefix: 'create' }) // 'createTag'`
34
34
  */
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("/");
35
+ function camelCase(text, { prefix = "", suffix = "" } = {}) {
36
+ return toCamelOrPascal(`${prefix} ${text} ${suffix}`, false);
38
37
  }
38
+ //#endregion
39
+ //#region ../../internals/utils/src/fs.ts
39
40
  /**
40
- * Converts `text` to camelCase.
41
- * When `isFile` is `true`, dot-separated segments are each cased independently and joined with `/`.
41
+ * Builds a nested file path from a dotted name. Splits on dots that precede a letter
42
+ * (so version numbers embedded in operationIds like `v2025.0` stay intact), camelCases
43
+ * every earlier segment, applies `caseLast` to the final segment, and joins with `/`.
42
44
  *
43
- * @example
44
- * camelCase('hello-world') // 'helloWorld'
45
- * camelCase('pet.petId', { isFile: true }) // 'pet/petId'
45
+ * Empty segments are dropped before joining. They arise when the name starts with a dot
46
+ * followed by a letter (e.g. `..Schema` splits into `['..', 'Schema']` and `'..'` cases to
47
+ * an empty string). Without this a leading `/` would form, which `path.resolve` reads as an
48
+ * absolute path, letting generated files escape the configured output directory.
49
+ *
50
+ * @example Nested path from a dotted name
51
+ * `toFilePath('pet.petId') // 'pet/petId'`
52
+ *
53
+ * @example PascalCase the final segment
54
+ * `toFilePath('pet.Pet', pascalCase) // 'pet/Pet'`
55
+ *
56
+ * @example Suffix applied to the final segment only
57
+ * `toFilePath('tag.tag', (part) => camelCase(part, { suffix: 'schema' })) // 'tag/tagSchema'`
46
58
  */
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);
59
+ function toFilePath(name, caseLast = camelCase) {
60
+ const parts = name.split(/\.(?=[a-zA-Z])/);
61
+ return parts.map((part, i) => i === parts.length - 1 ? caseLast(part) : camelCase(part)).filter(Boolean).join("/");
53
62
  }
54
63
  //#endregion
55
64
  //#region ../../internals/utils/src/reserved.ts
@@ -155,99 +164,80 @@ function isValidVarName(name) {
155
164
  return /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name);
156
165
  }
157
166
  //#endregion
158
- //#region ../../internals/utils/src/urlPath.ts
167
+ //#region ../../internals/utils/src/url.ts
168
+ function transformParam(raw, casing) {
169
+ const param = isValidVarName(raw) ? raw : camelCase(raw);
170
+ return casing === "camelcase" ? camelCase(param) : param;
171
+ }
172
+ function toParamsObject(path, { replacer, casing } = {}) {
173
+ const params = {};
174
+ for (const match of path.matchAll(/\{([^}]+)\}/g)) {
175
+ const param = transformParam(match[1], casing);
176
+ const key = replacer ? replacer(param) : param;
177
+ params[key] = key;
178
+ }
179
+ return Object.keys(params).length > 0 ? params : null;
180
+ }
159
181
  /**
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}`'
182
+ * Helpers for OpenAPI/Swagger paths, plus a thin wrapper over the native `URL`.
166
183
  */
167
- var URLPath = class {
184
+ var Url = class Url {
168
185
  /**
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`.
186
+ * Reports whether `url` is a parseable absolute URL. Delegates to the native `URL.canParse`.
178
187
  *
179
188
  * @example
180
- * ```ts
181
- * new URLPath('/pet/{petId}').URL // '/pet/:petId'
182
- * ```
189
+ * Url.canParse('https://petstore.swagger.io/v2') // true
190
+ * Url.canParse('/pet/{petId}') // false
183
191
  */
184
- get URL() {
185
- return this.toURLPath();
192
+ static canParse(url, base) {
193
+ return URL.canParse(url, base);
186
194
  }
187
- /** Returns `true` when `path` is a fully-qualified URL (e.g. starts with `https://`).
195
+ /**
196
+ * Converts an OpenAPI/Swagger path to Express-style colon syntax.
188
197
  *
189
198
  * @example
190
- * ```ts
191
- * new URLPath('https://petstore.swagger.io/v2/pet').isURL // true
192
- * new URLPath('/pet/{petId}').isURL // false
193
- * ```
199
+ * Url.toPath('/pet/{petId}') // '/pet/:petId'
194
200
  */
195
- get isURL() {
196
- try {
197
- return !!new URL(this.path).href;
198
- } catch {
199
- return false;
200
- }
201
+ static toPath(path) {
202
+ return path.replace(/\{([^}]+)\}/g, ":$1");
201
203
  }
202
204
  /**
203
- * Converts the OpenAPI path to a TypeScript template literal string.
205
+ * Converts an OpenAPI/Swagger path to a TypeScript template literal string.
206
+ * `prefix` is prepended inside the literal, `replacer` transforms each parameter name,
207
+ * and `casing` controls parameter identifier casing.
204
208
  *
205
209
  * @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.
210
+ * Url.toTemplateString('/pet/{petId}') // '`/pet/${petId}`'
213
211
  *
214
212
  * @example
215
- * ```ts
216
- * new URLPath('/pet/{petId}').object
217
- * // { url: '/pet/:petId', params: { petId: 'petId' } }
218
- * ```
213
+ * Url.toTemplateString('/pet/{petId}', { prefix: 'https://api' }) // '`https://api/pet/${petId}`'
219
214
  */
220
- get object() {
221
- return this.toObject();
215
+ static toTemplateString(path, { prefix, replacer, casing } = {}) {
216
+ const result = path.split(/\{([^}]+)\}/).map((part, i) => {
217
+ if (i % 2 === 0) return part;
218
+ const param = transformParam(part, casing);
219
+ return `\${${replacer ? replacer(param) : param}}`;
220
+ }).join("");
221
+ return `\`${prefix ?? ""}${result}\``;
222
222
  }
223
- /** Returns a map of path parameter names, or `null` when the path has no parameters.
223
+ /**
224
+ * Returns the path and its extracted params as a structured `URLObject`, or as a stringified
225
+ * expression when `stringify` is set.
224
226
  *
225
227
  * @example
226
- * ```ts
227
- * new URLPath('/pet/{petId}').params // { petId: 'petId' }
228
- * new URLPath('/pet').params // null
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.
228
+ * Url.toObject('/pet/{petId}')
229
+ * // { url: '/pet/:petId', params: { petId: 'petId' } }
240
230
  */
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 } = {}) {
231
+ static toObject(path, { type = "path", replacer, stringify, casing } = {}) {
248
232
  const object = {
249
- url: type === "path" ? this.toURLPath() : this.toTemplateString({ replacer }),
250
- params: this.toParamsObject()
233
+ url: type === "path" ? Url.toPath(path) : Url.toTemplateString(path, {
234
+ replacer,
235
+ casing
236
+ }),
237
+ params: toParamsObject(path, {
238
+ replacer,
239
+ casing
240
+ })
251
241
  };
252
242
  if (stringify) {
253
243
  if (type === "template") return JSON.stringify(object).replaceAll("'", "").replaceAll(`"`, "");
@@ -256,57 +246,13 @@ var URLPath = class {
256
246
  }
257
247
  return object;
258
248
  }
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
- const result = 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
- return `\`${prefix ?? ""}${result}\``;
273
- }
274
- /**
275
- * Extracts all `{param}` segments from the path and returns them as a key-value map.
276
- * An optional `replacer` transforms each parameter name in both key and value positions.
277
- * Returns `undefined` when no path parameters are found.
278
- *
279
- * @example
280
- * ```ts
281
- * new URLPath('/pet/{petId}/tag/{tagId}').toParamsObject()
282
- * // { petId: 'petId', tagId: 'tagId' }
283
- * ```
284
- */
285
- toParamsObject(replacer) {
286
- const params = {};
287
- this.#eachParam((_raw, param) => {
288
- const key = replacer ? replacer(param) : param;
289
- params[key] = key;
290
- });
291
- return Object.keys(params).length > 0 ? params : null;
292
- }
293
- /** Converts the OpenAPI path to Express-style colon syntax.
294
- *
295
- * @example
296
- * ```ts
297
- * new URLPath('/pet/{petId}').toURLPath() // '/pet/:petId'
298
- * ```
299
- */
300
- toURLPath() {
301
- return this.path.replace(/\{([^}]+)\}/g, ":$1");
302
- }
303
249
  };
304
250
  //#endregion
305
251
  //#region ../../internals/shared/src/operation.ts
306
252
  function getOperationLink(node, link) {
307
253
  if (!link) return null;
308
254
  if (typeof link === "function") return link(node) ?? null;
309
- if (link === "urlPath") return node.path ? `{@link ${new URLPath(node.path).URL}}` : null;
255
+ if (link === "urlPath") return node.path ? `{@link ${Url.toPath(node.path)}}` : null;
310
256
  return node.path ? `{@link ${node.path.replaceAll("{", ":").replaceAll("}", "")}}` : null;
311
257
  }
312
258
  function buildOperationComments(node, options = {}) {
@@ -396,26 +342,24 @@ function findSuccessStatusCode(responses) {
396
342
  * shared default naming so every plugin groups output consistently:
397
343
  *
398
344
  * - `path` groups use the second path segment (`/pet/findByStatus` → `pet`).
399
- * - other groups use `${camelCase(group)}${suffix}` (e.g. `petController`).
345
+ * - other groups use the camelCased group (`pet store``petStore`).
400
346
  *
401
347
  * A user-provided `group.name` always wins over the default namer, so callers stay in
402
348
  * control of their output folders. Returns `null` when grouping is disabled, matching the
403
349
  * per-plugin convention.
404
350
  *
405
351
  * @param group - The user-supplied group option, or `undefined` to disable grouping.
406
- * @param options.suffix - Appended to non-`path` group names, e.g. `'Controller'` or `'Requests'`.
407
352
  *
408
353
  * @example
409
354
  * ```ts
410
- * createGroupConfig(group, { suffix: 'Controller' }) // plugin-ts, plugin-client, …
411
- * createGroupConfig(group, { suffix: 'Requests' }) // plugin-cypress, plugin-mcp
355
+ * createGroupConfig(group) // shared across every plugin
412
356
  * ```
413
357
  */
414
- function createGroupConfig(group, options) {
358
+ function createGroupConfig(group) {
415
359
  if (!group) return null;
416
360
  const defaultName = (ctx) => {
417
361
  if (group.type === "path") return `${ctx.group.split("/")[1]}`;
418
- return `${camelCase(ctx.group)}${options.suffix}`;
362
+ return camelCase(ctx.group);
419
363
  };
420
364
  return {
421
365
  ...group,
@@ -452,7 +396,6 @@ function buildRemappingCode(mapping, varName, sourceName) {
452
396
  const declarationPrinter = functionPrinter({ mode: "declaration" });
453
397
  function McpHandler({ name, node, resolver, baseURL, dataReturnType, paramsCasing }) {
454
398
  if (!ast.isHttpOperationNode(node)) return null;
455
- const urlPath = new URLPath(node.path);
456
399
  const contentType = node.requestBody?.content?.[0]?.contentType;
457
400
  const isFormData = contentType === "multipart/form-data";
458
401
  const { query: queryParams, header: headerParams } = getOperationParameters(node, { paramsCasing });
@@ -480,28 +423,20 @@ function McpHandler({ name, node, resolver, baseURL, dataReturnType, paramsCasin
480
423
  const headers = [headerParams.length ? headerParamsMapping ? "...mappedHeaders" : "...headers" : null, contentTypeHeader].filter(Boolean);
481
424
  const fetchConfig = [];
482
425
  fetchConfig.push(`method: ${JSON.stringify(node.method.toUpperCase())}`);
483
- fetchConfig.push(`url: ${urlPath.template}`);
426
+ fetchConfig.push(`url: ${Url.toTemplateString(node.path)}`);
484
427
  if (baseURL) fetchConfig.push(`baseURL: \`${baseURL}\``);
485
428
  if (queryParams.length) fetchConfig.push(queryParamsMapping ? "params: mappedParams" : "params");
486
429
  if (requestName) fetchConfig.push(`data: ${isFormData ? "formData as FormData" : "requestData"}`);
487
430
  if (headers.length) fetchConfig.push(`headers: { ${headers.join(", ")} }`);
488
- const callToolResult = dataReturnType === "data" ? `return {
489
- content: [
490
- {
491
- type: 'text',
492
- text: JSON.stringify(res.data)
493
- }
494
- ],
495
- structuredContent: { data: res.data }
496
- }` : `return {
497
- content: [
498
- {
499
- type: 'text',
500
- text: JSON.stringify(res)
501
- }
502
- ],
503
- structuredContent: { data: res.data }
504
- }`;
431
+ const callToolResult = `return {
432
+ content: [
433
+ {
434
+ type: 'text',
435
+ text: JSON.stringify(${dataReturnType === "data" ? "res.data" : "res"})
436
+ }
437
+ ],
438
+ structuredContent: { data: res.data }
439
+ }`;
505
440
  return /* @__PURE__ */ jsx(File.Source, {
506
441
  name,
507
442
  isExportable: true,
@@ -676,7 +611,7 @@ return server`
676
611
  */
677
612
  const mcpGenerator = defineGenerator({
678
613
  name: "mcp",
679
- renderer: jsxRendererSync,
614
+ renderer: jsxRenderer,
680
615
  operation(node, ctx) {
681
616
  if (!ast.isHttpOperationNode(node)) return null;
682
617
  const { resolver, driver, root } = ctx;
@@ -806,7 +741,7 @@ const mcpGenerator = defineGenerator({
806
741
  */
807
742
  const serverGenerator = defineGenerator({
808
743
  name: "operations",
809
- renderer: jsxRendererSync,
744
+ renderer: jsxRenderer,
810
745
  operations(nodes, ctx) {
811
746
  const { config, resolver, plugin, driver, root } = ctx;
812
747
  const { output, paramsCasing, group } = ctx.options;
@@ -974,7 +909,7 @@ const resolverMcp = defineResolver(() => ({
974
909
  name: "default",
975
910
  pluginName: "plugin-mcp",
976
911
  default(name, type) {
977
- if (type === "file") return camelCase(name, { isFile: true });
912
+ if (type === "file") return toFilePath(name);
978
913
  return camelCase(name, { suffix: "handler" });
979
914
  },
980
915
  resolveName(name) {
@@ -1025,11 +960,11 @@ const pluginMcpName = "plugin-mcp";
1025
960
  const pluginMcp = definePlugin((options) => {
1026
961
  const { output = {
1027
962
  path: "mcp",
1028
- barrelType: "named"
963
+ barrel: { type: "named" }
1029
964
  }, group, exclude = [], include, override = [], paramsCasing, client, resolver: userResolver, transformer: userTransformer, generators: userGenerators = [] } = options;
1030
965
  const clientName = client?.client ?? "axios";
1031
966
  const clientImportPath = client?.importPath ?? (!client?.bundle ? `@kubb/plugin-client/clients/${clientName}` : void 0);
1032
- const groupConfig = createGroupConfig(group, { suffix: "Requests" });
967
+ const groupConfig = createGroupConfig(group);
1033
968
  return {
1034
969
  name: pluginMcpName,
1035
970
  options,