@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 +102 -167
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +8 -12
- package/dist/index.js +103 -168
- package/dist/index.js.map +1 -1
- package/package.json +9 -17
- package/src/components/McpHandler.tsx +12 -23
- package/src/generators/mcpGenerator.tsx +2 -2
- package/src/generators/serverGenerator.tsx +2 -2
- package/src/plugin.ts +2 -2
- package/src/resolvers/resolverMcp.ts +2 -2
- package/src/types.ts +8 -12
- package/extension.yaml +0 -943
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,
|
|
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
|
-
|
|
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
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
27
|
+
* Converts `text` to camelCase.
|
|
28
|
+
*
|
|
29
|
+
* @example Word boundaries
|
|
30
|
+
* `camelCase('hello-world') // 'helloWorld'`
|
|
31
31
|
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
32
|
+
* @example With a prefix
|
|
33
|
+
* `camelCase('tag', { prefix: 'create' }) // 'createTag'`
|
|
34
34
|
*/
|
|
35
|
-
function
|
|
36
|
-
|
|
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
|
-
*
|
|
41
|
-
*
|
|
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
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
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
|
|
48
|
-
|
|
49
|
-
|
|
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/
|
|
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
|
-
*
|
|
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
|
|
184
|
+
var Url = class Url {
|
|
168
185
|
/**
|
|
169
|
-
*
|
|
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
|
-
*
|
|
181
|
-
*
|
|
182
|
-
* ```
|
|
189
|
+
* Url.canParse('https://petstore.swagger.io/v2') // true
|
|
190
|
+
* Url.canParse('/pet/{petId}') // false
|
|
183
191
|
*/
|
|
184
|
-
|
|
185
|
-
return
|
|
192
|
+
static canParse(url, base) {
|
|
193
|
+
return URL.canParse(url, base);
|
|
186
194
|
}
|
|
187
|
-
/**
|
|
195
|
+
/**
|
|
196
|
+
* Converts an OpenAPI/Swagger path to Express-style colon syntax.
|
|
188
197
|
*
|
|
189
198
|
* @example
|
|
190
|
-
*
|
|
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
|
-
|
|
196
|
-
|
|
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
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
|
|
221
|
-
|
|
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
|
-
/**
|
|
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
|
-
*
|
|
227
|
-
*
|
|
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
|
-
|
|
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" ?
|
|
250
|
-
|
|
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 ${
|
|
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
|
|
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
|
|
411
|
-
* createGroupConfig(group, { suffix: 'Requests' }) // plugin-cypress, plugin-mcp
|
|
355
|
+
* createGroupConfig(group) // shared across every plugin
|
|
412
356
|
* ```
|
|
413
357
|
*/
|
|
414
|
-
function createGroupConfig(group
|
|
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
|
|
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: ${
|
|
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 =
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
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:
|
|
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:
|
|
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
|
|
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
|
-
|
|
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
|
|
967
|
+
const groupConfig = createGroupConfig(group);
|
|
1033
968
|
return {
|
|
1034
969
|
name: pluginMcpName,
|
|
1035
970
|
options,
|