@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/README.md +39 -22
- package/dist/index.cjs +422 -349
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +85 -34
- package/dist/index.js +423 -350
- package/dist/index.js.map +1 -1
- package/package.json +13 -21
- package/src/components/McpHandler.tsx +26 -42
- package/src/components/Server.tsx +76 -71
- package/src/generators/mcpGenerator.tsx +19 -21
- package/src/generators/serverGenerator.tsx +20 -20
- package/src/plugin.ts +39 -19
- package/src/resolvers/resolverMcp.ts +18 -8
- package/src/types.ts +31 -21
- package/src/utils.ts +15 -80
- package/extension.yaml +0 -470
- /package/dist/{chunk--u3MIqq1.js → chunk-C0LytTxp.js} +0 -0
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
|
-
|
|
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
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
54
|
+
* Converts `text` to camelCase.
|
|
55
|
+
*
|
|
56
|
+
* @example Word boundaries
|
|
57
|
+
* `camelCase('hello-world') // 'helloWorld'`
|
|
58
58
|
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
59
|
+
* @example With a prefix
|
|
60
|
+
* `camelCase('tag', { prefix: 'create' }) // 'createTag'`
|
|
61
61
|
*/
|
|
62
|
-
function
|
|
63
|
-
|
|
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
|
-
*
|
|
68
|
-
*
|
|
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
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
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
|
|
75
|
-
|
|
76
|
-
|
|
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/
|
|
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
|
-
*
|
|
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
|
|
211
|
+
var Url = class Url {
|
|
195
212
|
/**
|
|
196
|
-
*
|
|
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
|
-
*
|
|
208
|
-
*
|
|
209
|
-
* ```
|
|
216
|
+
* Url.canParse('https://petstore.swagger.io/v2') // true
|
|
217
|
+
* Url.canParse('/pet/{petId}') // false
|
|
210
218
|
*/
|
|
211
|
-
|
|
212
|
-
return
|
|
219
|
+
static canParse(url, base) {
|
|
220
|
+
return URL.canParse(url, base);
|
|
213
221
|
}
|
|
214
|
-
/**
|
|
222
|
+
/**
|
|
223
|
+
* Converts an OpenAPI/Swagger path to Express-style colon syntax.
|
|
215
224
|
*
|
|
216
225
|
* @example
|
|
217
|
-
*
|
|
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
|
-
|
|
223
|
-
|
|
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
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
|
|
248
|
-
|
|
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
|
-
/**
|
|
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
|
-
*
|
|
254
|
-
*
|
|
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
|
-
|
|
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" ?
|
|
277
|
-
|
|
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/
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
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
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
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
|
-
|
|
358
|
-
].filter((
|
|
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
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
377
|
-
*
|
|
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
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
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
|
-
|
|
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
|
|
422
|
-
const
|
|
423
|
-
const
|
|
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 ?
|
|
444
|
-
const queryParamsMapping = paramsCasing ?
|
|
445
|
-
const headerParamsMapping = paramsCasing ?
|
|
446
|
-
const contentTypeHeader = contentType && contentType !== "application/json" && contentType !== "multipart/form-data" ? `'Content-Type': '${contentType}'` :
|
|
447
|
-
const headers = [headerParams.length ? headerParamsMapping ? "...mappedHeaders" : "...headers" :
|
|
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: ${
|
|
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 =
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
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:
|
|
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
|
|
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
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
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.
|
|
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: "
|
|
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/
|
|
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: ["
|
|
737
|
+
name: ["client"],
|
|
706
738
|
root: meta.file.path,
|
|
707
|
-
path: node_path.default.resolve(root, ".kubb/
|
|
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/
|
|
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 {
|
|
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
|
|
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) :
|
|
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) :
|
|
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.
|
|
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) :
|
|
802
|
-
headerParams: headerParams.length ? resolveParams(headerParams) :
|
|
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(
|
|
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(
|
|
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:
|
|
858
|
-
serverVersion:
|
|
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
|
-
"${
|
|
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
|
-
*
|
|
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
|
-
*
|
|
928
|
+
* @example Resolve a handler name
|
|
929
|
+
* ```ts
|
|
930
|
+
* import { resolverMcp } from '@kubb/plugin-mcp'
|
|
890
931
|
*
|
|
891
|
-
*
|
|
892
|
-
*
|
|
932
|
+
* resolverMcp.default('addPet', 'function') // 'addPetHandler'
|
|
933
|
+
* ```
|
|
893
934
|
*/
|
|
894
|
-
const resolverMcp = (0, _kubb_core.defineResolver)((
|
|
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
|
|
939
|
+
if (type === "file") return toFilePath(name);
|
|
899
940
|
return camelCase(name, { suffix: "handler" });
|
|
900
941
|
},
|
|
901
942
|
resolveName(name) {
|
|
902
|
-
return
|
|
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
|
-
|
|
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: "
|
|
958
|
-
path: node_path$1.default.resolve(root, ".kubb/
|
|
1030
|
+
baseName: "client.ts",
|
|
1031
|
+
path: node_path$1.default.resolve(root, ".kubb/client.ts"),
|
|
959
1032
|
sources: [_kubb_core.ast.createSource({
|
|
960
|
-
name: "
|
|
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
|