@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.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import "./chunk
|
|
1
|
+
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";
|
|
@@ -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 // undefined
|
|
229
|
-
* ```
|
|
230
|
-
*/
|
|
231
|
-
get params() {
|
|
232
|
-
return this.getParams();
|
|
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,127 +246,144 @@ 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
|
-
return `\`${prefix}${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
|
-
}
|
|
273
|
-
/**
|
|
274
|
-
* Extracts all `{param}` segments from the path and returns them as a key-value map.
|
|
275
|
-
* An optional `replacer` transforms each parameter name in both key and value positions.
|
|
276
|
-
* Returns `undefined` when no path parameters are found.
|
|
277
|
-
*
|
|
278
|
-
* @example
|
|
279
|
-
* ```ts
|
|
280
|
-
* new URLPath('/pet/{petId}/tag/{tagId}').getParams()
|
|
281
|
-
* // { petId: 'petId', tagId: 'tagId' }
|
|
282
|
-
* ```
|
|
283
|
-
*/
|
|
284
|
-
getParams(replacer) {
|
|
285
|
-
const params = {};
|
|
286
|
-
this.#eachParam((_raw, param) => {
|
|
287
|
-
const key = replacer ? replacer(param) : param;
|
|
288
|
-
params[key] = key;
|
|
289
|
-
});
|
|
290
|
-
return Object.keys(params).length > 0 ? params : void 0;
|
|
291
|
-
}
|
|
292
|
-
/** Converts the OpenAPI path to Express-style colon syntax.
|
|
293
|
-
*
|
|
294
|
-
* @example
|
|
295
|
-
* ```ts
|
|
296
|
-
* new URLPath('/pet/{petId}').toURLPath() // '/pet/:petId'
|
|
297
|
-
* ```
|
|
298
|
-
*/
|
|
299
|
-
toURLPath() {
|
|
300
|
-
return this.path.replace(/\{([^}]+)\}/g, ":$1");
|
|
301
|
-
}
|
|
302
249
|
};
|
|
303
250
|
//#endregion
|
|
304
|
-
//#region src/
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
const code = Number(res.statusCode);
|
|
311
|
-
if (code >= 200 && code < 300) return res.statusCode;
|
|
312
|
-
}
|
|
251
|
+
//#region ../../internals/shared/src/operation.ts
|
|
252
|
+
function getOperationLink(node, link) {
|
|
253
|
+
if (!link) return null;
|
|
254
|
+
if (typeof link === "function") return link(node) ?? null;
|
|
255
|
+
if (link === "urlPath") return node.path ? `{@link ${Url.toPath(node.path)}}` : null;
|
|
256
|
+
return node.path ? `{@link ${node.path.replaceAll("{", ":").replaceAll("}", "")}}` : null;
|
|
313
257
|
}
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
* Build JSDoc comment lines from an OperationNode.
|
|
324
|
-
*/
|
|
325
|
-
function getComments(node) {
|
|
326
|
-
return [
|
|
258
|
+
function buildOperationComments(node, options = {}) {
|
|
259
|
+
const { link = "pathTemplate", linkPosition = "afterDeprecated", splitLines = false } = options;
|
|
260
|
+
const linkComment = getOperationLink(node, link);
|
|
261
|
+
const filteredComments = (linkPosition === "beforeDeprecated" ? [
|
|
262
|
+
node.description && `@description ${node.description}`,
|
|
263
|
+
node.summary && `@summary ${node.summary}`,
|
|
264
|
+
linkComment,
|
|
265
|
+
node.deprecated && "@deprecated"
|
|
266
|
+
] : [
|
|
327
267
|
node.description && `@description ${node.description}`,
|
|
328
268
|
node.summary && `@summary ${node.summary}`,
|
|
329
269
|
node.deprecated && "@deprecated",
|
|
330
|
-
|
|
331
|
-
].filter((
|
|
270
|
+
linkComment
|
|
271
|
+
]).filter((comment) => Boolean(comment));
|
|
272
|
+
if (!splitLines) return filteredComments;
|
|
273
|
+
return filteredComments.flatMap((text) => text.split(/\r?\n/).map((line) => line.trim())).filter((comment) => Boolean(comment));
|
|
332
274
|
}
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
275
|
+
function getOperationParameters(node, options = {}) {
|
|
276
|
+
const params = ast.caseParams(node.parameters, options.paramsCasing);
|
|
277
|
+
return {
|
|
278
|
+
path: params.filter((param) => param.in === "path"),
|
|
279
|
+
query: params.filter((param) => param.in === "query"),
|
|
280
|
+
header: params.filter((param) => param.in === "header"),
|
|
281
|
+
cookie: params.filter((param) => param.in === "cookie")
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
function getStatusCodeNumber(statusCode) {
|
|
285
|
+
const code = Number(statusCode);
|
|
286
|
+
return Number.isNaN(code) ? null : code;
|
|
287
|
+
}
|
|
288
|
+
function isSuccessStatusCode(statusCode) {
|
|
289
|
+
const code = getStatusCodeNumber(statusCode);
|
|
290
|
+
return code !== null && code >= 200 && code < 300;
|
|
291
|
+
}
|
|
292
|
+
function isErrorStatusCode(statusCode) {
|
|
293
|
+
const code = getStatusCodeNumber(statusCode);
|
|
294
|
+
return code !== null && code >= 400;
|
|
295
|
+
}
|
|
296
|
+
function resolveErrorNames(node, resolver) {
|
|
297
|
+
return node.responses.filter((response) => isErrorStatusCode(response.statusCode)).map((response) => resolver.resolveResponseStatusName(node, response.statusCode));
|
|
298
|
+
}
|
|
299
|
+
function resolveStatusCodeNames(node, resolver) {
|
|
300
|
+
return node.responses.map((response) => resolver.resolveResponseStatusName(node, response.statusCode));
|
|
301
|
+
}
|
|
302
|
+
const typeNamesByResolver = /* @__PURE__ */ new WeakMap();
|
|
303
|
+
function resolveOperationTypeNames(node, resolver, options = {}) {
|
|
304
|
+
const cacheKey = `${node.operationId}\0${options.paramsCasing ?? ""}\0${options.order ?? ""}\0${options.responseStatusNames ?? ""}\0${(options.exclude ?? []).join(",")}`;
|
|
305
|
+
let byResolver = typeNamesByResolver.get(resolver);
|
|
306
|
+
if (byResolver) {
|
|
307
|
+
const cached = byResolver.get(cacheKey);
|
|
308
|
+
if (cached) return cached;
|
|
309
|
+
} else {
|
|
310
|
+
byResolver = /* @__PURE__ */ new Map();
|
|
311
|
+
typeNamesByResolver.set(resolver, byResolver);
|
|
345
312
|
}
|
|
346
|
-
|
|
313
|
+
const { path, query, header } = getOperationParameters(node, { paramsCasing: options.paramsCasing });
|
|
314
|
+
const responseStatusNames = options.responseStatusNames === "error" ? resolveErrorNames(node, resolver) : options.responseStatusNames === false ? [] : resolveStatusCodeNames(node, resolver);
|
|
315
|
+
const exclude = new Set(options.exclude ?? []);
|
|
316
|
+
const paramNames = [
|
|
317
|
+
...path.map((param) => resolver.resolvePathParamsName(node, param)),
|
|
318
|
+
...query.map((param) => resolver.resolveQueryParamsName(node, param)),
|
|
319
|
+
...header.map((param) => resolver.resolveHeaderParamsName(node, param))
|
|
320
|
+
];
|
|
321
|
+
const bodyAndResponseNames = [node.requestBody?.content?.[0]?.schema ? resolver.resolveDataName(node) : null, resolver.resolveResponseName(node)];
|
|
322
|
+
const result = (options.order === "body-response-first" ? [
|
|
323
|
+
...bodyAndResponseNames,
|
|
324
|
+
...paramNames,
|
|
325
|
+
...responseStatusNames
|
|
326
|
+
] : [
|
|
327
|
+
...paramNames,
|
|
328
|
+
...bodyAndResponseNames,
|
|
329
|
+
...responseStatusNames
|
|
330
|
+
]).filter((name) => Boolean(name) && !exclude.has(name));
|
|
331
|
+
byResolver.set(cacheKey, result);
|
|
332
|
+
return result;
|
|
333
|
+
}
|
|
334
|
+
function findSuccessStatusCode(responses) {
|
|
335
|
+
for (const response of responses) if (isSuccessStatusCode(response.statusCode)) return response.statusCode;
|
|
336
|
+
return null;
|
|
347
337
|
}
|
|
338
|
+
//#endregion
|
|
339
|
+
//#region ../../internals/shared/src/group.ts
|
|
348
340
|
/**
|
|
349
|
-
*
|
|
350
|
-
*
|
|
341
|
+
* Builds the `group` config a Kubb plugin passes to `ctx.setOptions`, applying the
|
|
342
|
+
* shared default naming so every plugin groups output consistently:
|
|
343
|
+
*
|
|
344
|
+
* - `path` groups use the second path segment (`/pet/findByStatus` → `pet`).
|
|
345
|
+
* - other groups use the camelCased group (`pet store` → `petStore`).
|
|
346
|
+
*
|
|
347
|
+
* A user-provided `group.name` always wins over the default namer, so callers stay in
|
|
348
|
+
* control of their output folders. Returns `null` when grouping is disabled, matching the
|
|
349
|
+
* per-plugin convention.
|
|
350
|
+
*
|
|
351
|
+
* @param group - The user-supplied group option, or `undefined` to disable grouping.
|
|
352
|
+
*
|
|
353
|
+
* @example
|
|
354
|
+
* ```ts
|
|
355
|
+
* createGroupConfig(group) // shared across every plugin
|
|
356
|
+
* ```
|
|
351
357
|
*/
|
|
352
|
-
function
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
358
|
+
function createGroupConfig(group) {
|
|
359
|
+
if (!group) return null;
|
|
360
|
+
const defaultName = (ctx) => {
|
|
361
|
+
if (group.type === "path") return `${ctx.group.split("/")[1]}`;
|
|
362
|
+
return camelCase(ctx.group);
|
|
363
|
+
};
|
|
364
|
+
return {
|
|
365
|
+
...group,
|
|
366
|
+
name: group.name ? group.name : defaultName
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
//#endregion
|
|
370
|
+
//#region ../../internals/shared/src/params.ts
|
|
371
|
+
function buildParamsMapping(originalParams, mappedParams) {
|
|
372
|
+
const mapping = {};
|
|
373
|
+
let hasChanged = false;
|
|
374
|
+
originalParams.forEach((param, i) => {
|
|
375
|
+
const mappedName = mappedParams[i]?.name ?? param.name;
|
|
376
|
+
mapping[param.name] = mappedName;
|
|
377
|
+
if (param.name !== mappedName) hasChanged = true;
|
|
378
|
+
});
|
|
379
|
+
return hasChanged ? mapping : null;
|
|
380
|
+
}
|
|
381
|
+
function buildTransformedParamsMapping(params, transformName) {
|
|
382
|
+
if (!params.length) return null;
|
|
383
|
+
return buildParamsMapping(params, params.map((param) => ({
|
|
384
|
+
...param,
|
|
385
|
+
name: transformName(param.name)
|
|
386
|
+
})));
|
|
380
387
|
}
|
|
381
388
|
//#endregion
|
|
382
389
|
//#region src/components/McpHandler.tsx
|
|
@@ -388,16 +395,12 @@ function buildRemappingCode(mapping, varName, sourceName) {
|
|
|
388
395
|
}
|
|
389
396
|
const declarationPrinter = functionPrinter({ mode: "declaration" });
|
|
390
397
|
function McpHandler({ name, node, resolver, baseURL, dataReturnType, paramsCasing }) {
|
|
391
|
-
|
|
398
|
+
if (!ast.isHttpOperationNode(node)) return null;
|
|
392
399
|
const contentType = node.requestBody?.content?.[0]?.contentType;
|
|
393
400
|
const isFormData = contentType === "multipart/form-data";
|
|
394
|
-
const
|
|
395
|
-
const
|
|
396
|
-
const
|
|
397
|
-
const originalPathParams = node.parameters.filter((p) => p.in === "path");
|
|
398
|
-
const originalQueryParams = node.parameters.filter((p) => p.in === "query");
|
|
399
|
-
const originalHeaderParams = node.parameters.filter((p) => p.in === "header");
|
|
400
|
-
const requestName = node.requestBody?.content?.[0]?.schema ? resolver.resolveDataName(node) : void 0;
|
|
401
|
+
const { query: queryParams, header: headerParams } = getOperationParameters(node, { paramsCasing });
|
|
402
|
+
const { path: originalPathParams, query: originalQueryParams, header: originalHeaderParams } = getOperationParameters(node);
|
|
403
|
+
const requestName = node.requestBody?.content?.[0]?.schema ? resolver.resolveDataName(node) : null;
|
|
401
404
|
const responseName = resolver.resolveResponseName(node);
|
|
402
405
|
const errorResponses = node.responses.filter((r) => Number(r.statusCode) >= 400).map((r) => resolver.resolveResponseStatusName(node, r.statusCode));
|
|
403
406
|
const generics = [
|
|
@@ -413,35 +416,27 @@ function McpHandler({ name, node, resolver, baseURL, dataReturnType, paramsCasin
|
|
|
413
416
|
});
|
|
414
417
|
const baseParamsSignature = declarationPrinter.print(paramsNode) ?? "";
|
|
415
418
|
const paramsSignature = baseParamsSignature ? `${baseParamsSignature}, request: RequestHandlerExtra<ServerRequest, ServerNotification>` : "request: RequestHandlerExtra<ServerRequest, ServerNotification>";
|
|
416
|
-
const pathParamsMapping = paramsCasing ?
|
|
417
|
-
const queryParamsMapping = paramsCasing ?
|
|
418
|
-
const headerParamsMapping = paramsCasing ?
|
|
419
|
-
const contentTypeHeader = contentType && contentType !== "application/json" && contentType !== "multipart/form-data" ? `'Content-Type': '${contentType}'` :
|
|
420
|
-
const headers = [headerParams.length ? headerParamsMapping ? "...mappedHeaders" : "...headers" :
|
|
419
|
+
const pathParamsMapping = paramsCasing ? buildTransformedParamsMapping(originalPathParams, camelCase) : null;
|
|
420
|
+
const queryParamsMapping = paramsCasing ? buildTransformedParamsMapping(originalQueryParams, camelCase) : null;
|
|
421
|
+
const headerParamsMapping = paramsCasing ? buildTransformedParamsMapping(originalHeaderParams, camelCase) : null;
|
|
422
|
+
const contentTypeHeader = contentType && contentType !== "application/json" && contentType !== "multipart/form-data" ? `'Content-Type': '${contentType}'` : null;
|
|
423
|
+
const headers = [headerParams.length ? headerParamsMapping ? "...mappedHeaders" : "...headers" : null, contentTypeHeader].filter(Boolean);
|
|
421
424
|
const fetchConfig = [];
|
|
422
425
|
fetchConfig.push(`method: ${JSON.stringify(node.method.toUpperCase())}`);
|
|
423
|
-
fetchConfig.push(`url: ${
|
|
426
|
+
fetchConfig.push(`url: ${Url.toTemplateString(node.path)}`);
|
|
424
427
|
if (baseURL) fetchConfig.push(`baseURL: \`${baseURL}\``);
|
|
425
428
|
if (queryParams.length) fetchConfig.push(queryParamsMapping ? "params: mappedParams" : "params");
|
|
426
429
|
if (requestName) fetchConfig.push(`data: ${isFormData ? "formData as FormData" : "requestData"}`);
|
|
427
430
|
if (headers.length) fetchConfig.push(`headers: { ${headers.join(", ")} }`);
|
|
428
|
-
const callToolResult =
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
content: [
|
|
438
|
-
{
|
|
439
|
-
type: 'text',
|
|
440
|
-
text: JSON.stringify(res)
|
|
441
|
-
}
|
|
442
|
-
],
|
|
443
|
-
structuredContent: { data: res.data }
|
|
444
|
-
}`;
|
|
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
|
+
}`;
|
|
445
440
|
return /* @__PURE__ */ jsx(File.Source, {
|
|
446
441
|
name,
|
|
447
442
|
isExportable: true,
|
|
@@ -451,7 +446,7 @@ function McpHandler({ name, node, resolver, baseURL, dataReturnType, paramsCasin
|
|
|
451
446
|
async: true,
|
|
452
447
|
export: true,
|
|
453
448
|
params: paramsSignature,
|
|
454
|
-
JSDoc: { comments:
|
|
449
|
+
JSDoc: { comments: buildOperationComments(node) },
|
|
455
450
|
returnType: "Promise<CallToolResult>",
|
|
456
451
|
children: [
|
|
457
452
|
"",
|
|
@@ -473,7 +468,7 @@ function McpHandler({ name, node, resolver, baseURL, dataReturnType, paramsCasin
|
|
|
473
468
|
/* @__PURE__ */ jsx("br", {}),
|
|
474
469
|
isFormData && requestName && "const formData = buildFormData(requestData)",
|
|
475
470
|
/* @__PURE__ */ jsx("br", {}),
|
|
476
|
-
`const res = await
|
|
471
|
+
`const res = await client<${generics.join(", ")}>({ ${fetchConfig.join(", ")} }, request)`,
|
|
477
472
|
/* @__PURE__ */ jsx("br", {}),
|
|
478
473
|
callToolResult
|
|
479
474
|
]
|
|
@@ -481,62 +476,80 @@ function McpHandler({ name, node, resolver, baseURL, dataReturnType, paramsCasin
|
|
|
481
476
|
});
|
|
482
477
|
}
|
|
483
478
|
//#endregion
|
|
479
|
+
//#region src/utils.ts
|
|
480
|
+
/**
|
|
481
|
+
* Render a group param value — compose individual schemas into `z.object({ ... })`,
|
|
482
|
+
* or use a schema name string directly.
|
|
483
|
+
*/
|
|
484
|
+
function zodGroupExpr(entry) {
|
|
485
|
+
if (typeof entry === "string") return entry;
|
|
486
|
+
return `z.object({ ${entry.map((p) => `${JSON.stringify(p.name)}: ${p.schemaName}`).join(", ")} })`;
|
|
487
|
+
}
|
|
488
|
+
/**
|
|
489
|
+
* Convert a SchemaNode type to an inline Zod expression string.
|
|
490
|
+
* Used as fallback when no named zod schema is available for a path parameter.
|
|
491
|
+
*/
|
|
492
|
+
function zodExprFromSchemaNode(schema) {
|
|
493
|
+
const baseExpr = (() => {
|
|
494
|
+
if (schema.type === "enum") {
|
|
495
|
+
const rawValues = schema.namedEnumValues?.length ? schema.namedEnumValues.map((v) => v.value) : (schema.enumValues ?? []).filter((v) => v !== null);
|
|
496
|
+
if (rawValues.length > 0 && rawValues.every((v) => typeof v === "string")) return `z.enum([${rawValues.map((v) => JSON.stringify(v)).join(", ")}])`;
|
|
497
|
+
if (rawValues.length > 0) {
|
|
498
|
+
const literals = rawValues.map((v) => `z.literal(${JSON.stringify(v)})`);
|
|
499
|
+
return literals.length === 1 ? literals[0] : `z.union([${literals.join(", ")}])`;
|
|
500
|
+
}
|
|
501
|
+
return "z.string()";
|
|
502
|
+
}
|
|
503
|
+
if (schema.type === "integer") return "z.coerce.number()";
|
|
504
|
+
if (schema.type === "number") return "z.number()";
|
|
505
|
+
if (schema.type === "boolean") return "z.boolean()";
|
|
506
|
+
if (schema.type === "array") return "z.array(z.unknown())";
|
|
507
|
+
return "z.string()";
|
|
508
|
+
})();
|
|
509
|
+
return schema.nullable ? `${baseExpr}.nullable()` : baseExpr;
|
|
510
|
+
}
|
|
511
|
+
//#endregion
|
|
484
512
|
//#region src/components/Server.tsx
|
|
485
513
|
const keysPrinter = functionPrinter({ mode: "keys" });
|
|
486
514
|
function Server({ name, serverName, serverVersion, paramsCasing, operations }) {
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
otherEntries.sort((a, b) => a.key.localeCompare(b.key));
|
|
526
|
-
const entries = [...pathEntries, ...otherEntries];
|
|
527
|
-
const paramsNode = entries.length ? ast.createFunctionParameters({ params: [ast.createParameterGroup({ properties: entries.map((e) => ast.createFunctionParameter({
|
|
528
|
-
name: e.key,
|
|
529
|
-
optional: false
|
|
530
|
-
})) })] }) : void 0;
|
|
531
|
-
const destructured = paramsNode ? keysPrinter.print(paramsNode) ?? "" : "";
|
|
532
|
-
const inputSchema = entries.length ? `{ ${entries.map((e) => `${e.key}: ${e.value}`).join(", ")} }` : void 0;
|
|
533
|
-
const outputSchema = zod.responseName;
|
|
534
|
-
const config = [
|
|
535
|
-
tool.title ? `title: ${JSON.stringify(tool.title)}` : null,
|
|
536
|
-
`description: ${JSON.stringify(tool.description)}`,
|
|
537
|
-
outputSchema ? `outputSchema: { data: ${outputSchema} }` : null
|
|
538
|
-
].filter(Boolean).join(",\n ");
|
|
539
|
-
if (inputSchema) return `
|
|
515
|
+
const registrations = operations.map(({ tool, mcp, zod, node }) => {
|
|
516
|
+
const { path: pathParams } = getOperationParameters(node, { paramsCasing });
|
|
517
|
+
const pathEntries = [];
|
|
518
|
+
const otherEntries = [];
|
|
519
|
+
for (const p of pathParams) {
|
|
520
|
+
const zodParam = zod.pathParams.find((zp) => zp.name === p.name);
|
|
521
|
+
pathEntries.push({
|
|
522
|
+
key: p.name,
|
|
523
|
+
value: zodParam ? zodParam.schemaName : zodExprFromSchemaNode(p.schema)
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
if (zod.requestName) otherEntries.push({
|
|
527
|
+
key: "data",
|
|
528
|
+
value: zod.requestName
|
|
529
|
+
});
|
|
530
|
+
if (zod.queryParams) otherEntries.push({
|
|
531
|
+
key: "params",
|
|
532
|
+
value: zodGroupExpr(zod.queryParams)
|
|
533
|
+
});
|
|
534
|
+
if (zod.headerParams) otherEntries.push({
|
|
535
|
+
key: "headers",
|
|
536
|
+
value: zodGroupExpr(zod.headerParams)
|
|
537
|
+
});
|
|
538
|
+
otherEntries.sort((a, b) => a.key.localeCompare(b.key));
|
|
539
|
+
const entries = [...pathEntries, ...otherEntries];
|
|
540
|
+
const paramsNode = entries.length ? ast.createFunctionParameters({ params: [ast.createParameterGroup({ properties: entries.map((e) => ast.createFunctionParameter({
|
|
541
|
+
name: e.key,
|
|
542
|
+
optional: false
|
|
543
|
+
})) })] }) : null;
|
|
544
|
+
const destructured = paramsNode ? keysPrinter.print(paramsNode) ?? "" : "";
|
|
545
|
+
const inputSchema = entries.length ? `{ ${entries.map((e) => `${e.key}: ${e.value}`).join(", ")} }` : null;
|
|
546
|
+
const outputSchema = zod.responseName;
|
|
547
|
+
const config = [
|
|
548
|
+
tool.title ? `title: ${JSON.stringify(tool.title)}` : null,
|
|
549
|
+
`description: ${JSON.stringify(tool.description)}`,
|
|
550
|
+
outputSchema ? `outputSchema: { data: ${outputSchema} }` : null
|
|
551
|
+
].filter(Boolean).join(",\n ");
|
|
552
|
+
if (inputSchema) return `
|
|
540
553
|
server.registerTool(${JSON.stringify(tool.name)}, {
|
|
541
554
|
${config},
|
|
542
555
|
inputSchema: ${inputSchema},
|
|
@@ -544,14 +557,34 @@ server.registerTool(${JSON.stringify(tool.name)}, {
|
|
|
544
557
|
return ${mcp.name}(${destructured}, request)
|
|
545
558
|
})
|
|
546
559
|
`;
|
|
547
|
-
|
|
560
|
+
return `
|
|
548
561
|
server.registerTool(${JSON.stringify(tool.name)}, {
|
|
549
562
|
${config},
|
|
550
563
|
}, async (request) => {
|
|
551
564
|
return ${mcp.name}(request)
|
|
552
565
|
})
|
|
553
566
|
`;
|
|
554
|
-
|
|
567
|
+
}).filter(Boolean).join("\n");
|
|
568
|
+
return /* @__PURE__ */ jsxs(File.Source, {
|
|
569
|
+
name,
|
|
570
|
+
isExportable: true,
|
|
571
|
+
isIndexable: true,
|
|
572
|
+
children: [
|
|
573
|
+
/* @__PURE__ */ jsx(Function, {
|
|
574
|
+
name: "getServer",
|
|
575
|
+
export: true,
|
|
576
|
+
children: `const server = new McpServer({
|
|
577
|
+
name: '${serverName}',
|
|
578
|
+
version: '${serverVersion}',
|
|
579
|
+
})
|
|
580
|
+
${registrations}
|
|
581
|
+
return server`
|
|
582
|
+
}),
|
|
583
|
+
/* @__PURE__ */ jsx(Const, {
|
|
584
|
+
name: "server",
|
|
585
|
+
export: true,
|
|
586
|
+
children: "getServer()"
|
|
587
|
+
}),
|
|
555
588
|
/* @__PURE__ */ jsx(Function, {
|
|
556
589
|
name: "startServer",
|
|
557
590
|
async: true,
|
|
@@ -570,29 +603,28 @@ server.registerTool(${JSON.stringify(tool.name)}, {
|
|
|
570
603
|
}
|
|
571
604
|
//#endregion
|
|
572
605
|
//#region src/generators/mcpGenerator.tsx
|
|
606
|
+
/**
|
|
607
|
+
* Built-in operation generator for `@kubb/plugin-mcp`. Emits one MCP tool
|
|
608
|
+
* handler per OpenAPI operation, wiring the input Zod schema, the HTTP call,
|
|
609
|
+
* and the response shape into a single function that an MCP server can
|
|
610
|
+
* register as a callable tool.
|
|
611
|
+
*/
|
|
573
612
|
const mcpGenerator = defineGenerator({
|
|
574
613
|
name: "mcp",
|
|
575
614
|
renderer: jsxRenderer,
|
|
576
615
|
operation(node, ctx) {
|
|
616
|
+
if (!ast.isHttpOperationNode(node)) return null;
|
|
577
617
|
const { resolver, driver, root } = ctx;
|
|
578
618
|
const { output, client, paramsCasing, group } = ctx.options;
|
|
579
619
|
const pluginTs = driver.getPlugin(pluginTsName);
|
|
580
620
|
if (!pluginTs) return null;
|
|
581
621
|
const tsResolver = driver.getResolver(pluginTsName);
|
|
582
|
-
const
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
const importedTypeNames = [
|
|
587
|
-
...pathParams.map((p) => tsResolver.resolvePathParamsName(node, p)),
|
|
588
|
-
...queryParams.map((p) => tsResolver.resolveQueryParamsName(node, p)),
|
|
589
|
-
...headerParams.map((p) => tsResolver.resolveHeaderParamsName(node, p)),
|
|
590
|
-
node.requestBody?.content?.[0]?.schema ? tsResolver.resolveDataName(node) : void 0,
|
|
591
|
-
tsResolver.resolveResponseName(node),
|
|
592
|
-
...node.responses.filter((r) => Number(r.statusCode) >= 400).map((r) => tsResolver.resolveResponseStatusName(node, r.statusCode))
|
|
593
|
-
].filter(Boolean);
|
|
622
|
+
const importedTypeNames = resolveOperationTypeNames(node, tsResolver, {
|
|
623
|
+
paramsCasing,
|
|
624
|
+
responseStatusNames: "error"
|
|
625
|
+
});
|
|
594
626
|
const meta = {
|
|
595
|
-
name: resolver.
|
|
627
|
+
name: resolver.resolveHandlerName(node),
|
|
596
628
|
file: resolver.resolveFile({
|
|
597
629
|
name: node.operationId,
|
|
598
630
|
extname: ".ts",
|
|
@@ -601,7 +633,7 @@ const mcpGenerator = defineGenerator({
|
|
|
601
633
|
}, {
|
|
602
634
|
root,
|
|
603
635
|
output,
|
|
604
|
-
group
|
|
636
|
+
group: group ?? void 0
|
|
605
637
|
}),
|
|
606
638
|
fileTs: tsResolver.resolveFile({
|
|
607
639
|
name: node.operationId,
|
|
@@ -611,7 +643,7 @@ const mcpGenerator = defineGenerator({
|
|
|
611
643
|
}, {
|
|
612
644
|
root,
|
|
613
645
|
output: pluginTs.options?.output ?? output,
|
|
614
|
-
group: pluginTs.options?.group
|
|
646
|
+
group: pluginTs.options?.group ?? void 0
|
|
615
647
|
})
|
|
616
648
|
};
|
|
617
649
|
return /* @__PURE__ */ jsxs(File, {
|
|
@@ -655,7 +687,7 @@ const mcpGenerator = defineGenerator({
|
|
|
655
687
|
isTypeOnly: true
|
|
656
688
|
}),
|
|
657
689
|
/* @__PURE__ */ jsx(File.Import, {
|
|
658
|
-
name: "
|
|
690
|
+
name: "client",
|
|
659
691
|
path: client.importPath
|
|
660
692
|
}),
|
|
661
693
|
client.dataReturnType === "full" && /* @__PURE__ */ jsx(File.Import, {
|
|
@@ -671,18 +703,18 @@ const mcpGenerator = defineGenerator({
|
|
|
671
703
|
"ResponseErrorConfig"
|
|
672
704
|
],
|
|
673
705
|
root: meta.file.path,
|
|
674
|
-
path: path.resolve(root, ".kubb/
|
|
706
|
+
path: path.resolve(root, ".kubb/client.ts"),
|
|
675
707
|
isTypeOnly: true
|
|
676
708
|
}),
|
|
677
709
|
/* @__PURE__ */ jsx(File.Import, {
|
|
678
|
-
name: ["
|
|
710
|
+
name: ["client"],
|
|
679
711
|
root: meta.file.path,
|
|
680
|
-
path: path.resolve(root, ".kubb/
|
|
712
|
+
path: path.resolve(root, ".kubb/client.ts")
|
|
681
713
|
}),
|
|
682
714
|
client.dataReturnType === "full" && /* @__PURE__ */ jsx(File.Import, {
|
|
683
715
|
name: ["ResponseConfig"],
|
|
684
716
|
root: meta.file.path,
|
|
685
|
-
path: path.resolve(root, ".kubb/
|
|
717
|
+
path: path.resolve(root, ".kubb/client.ts"),
|
|
686
718
|
isTypeOnly: true
|
|
687
719
|
})
|
|
688
720
|
] }),
|
|
@@ -711,7 +743,7 @@ const serverGenerator = defineGenerator({
|
|
|
711
743
|
name: "operations",
|
|
712
744
|
renderer: jsxRenderer,
|
|
713
745
|
operations(nodes, ctx) {
|
|
714
|
-
const {
|
|
746
|
+
const { config, resolver, plugin, driver, root } = ctx;
|
|
715
747
|
const { output, paramsCasing, group } = ctx.options;
|
|
716
748
|
const pluginZod = driver.getPlugin(pluginZodName);
|
|
717
749
|
if (!pluginZod) return;
|
|
@@ -727,11 +759,8 @@ const serverGenerator = defineGenerator({
|
|
|
727
759
|
path: path.resolve(root, output.path, ".mcp.json"),
|
|
728
760
|
meta: { pluginName: plugin.name }
|
|
729
761
|
};
|
|
730
|
-
const operationsMapped = nodes.map((node) => {
|
|
731
|
-
const
|
|
732
|
-
const pathParams = casedParams.filter((p) => p.in === "path");
|
|
733
|
-
const queryParams = casedParams.filter((p) => p.in === "query");
|
|
734
|
-
const headerParams = casedParams.filter((p) => p.in === "header");
|
|
762
|
+
const operationsMapped = nodes.filter(ast.isHttpOperationNode).map((node) => {
|
|
763
|
+
const { path: pathParams, query: queryParams, header: headerParams } = getOperationParameters(node, { paramsCasing });
|
|
735
764
|
const mcpFile = resolver.resolveFile({
|
|
736
765
|
name: node.operationId,
|
|
737
766
|
extname: ".ts",
|
|
@@ -740,7 +769,7 @@ const serverGenerator = defineGenerator({
|
|
|
740
769
|
}, {
|
|
741
770
|
root,
|
|
742
771
|
output,
|
|
743
|
-
group
|
|
772
|
+
group: group ?? void 0
|
|
744
773
|
});
|
|
745
774
|
const zodFile = zodResolver.resolveFile({
|
|
746
775
|
name: node.operationId,
|
|
@@ -750,11 +779,11 @@ const serverGenerator = defineGenerator({
|
|
|
750
779
|
}, {
|
|
751
780
|
root,
|
|
752
781
|
output: pluginZod.options?.output ?? output,
|
|
753
|
-
group: pluginZod.options?.group
|
|
782
|
+
group: pluginZod.options?.group ?? void 0
|
|
754
783
|
});
|
|
755
|
-
const requestName = node.requestBody?.content?.[0]?.schema ? zodResolver.resolveDataName(node) :
|
|
784
|
+
const requestName = node.requestBody?.content?.[0]?.schema ? zodResolver.resolveDataName(node) : null;
|
|
756
785
|
const successStatus = findSuccessStatusCode(node.responses);
|
|
757
|
-
const responseName = successStatus ? zodResolver.resolveResponseStatusName(node, successStatus) :
|
|
786
|
+
const responseName = successStatus ? zodResolver.resolveResponseStatusName(node, successStatus) : null;
|
|
758
787
|
const resolveParams = (params) => params.map((p) => ({
|
|
759
788
|
name: p.name,
|
|
760
789
|
schemaName: zodResolver.resolveParamName(node, p)
|
|
@@ -766,13 +795,13 @@ const serverGenerator = defineGenerator({
|
|
|
766
795
|
description: node.description || `Make a ${node.method.toUpperCase()} request to ${node.path}`
|
|
767
796
|
},
|
|
768
797
|
mcp: {
|
|
769
|
-
name: resolver.
|
|
798
|
+
name: resolver.resolveHandlerName(node),
|
|
770
799
|
file: mcpFile
|
|
771
800
|
},
|
|
772
801
|
zod: {
|
|
773
802
|
pathParams: resolveParams(pathParams),
|
|
774
|
-
queryParams: queryParams.length ? resolveParams(queryParams) :
|
|
775
|
-
headerParams: headerParams.length ? resolveParams(headerParams) :
|
|
803
|
+
queryParams: queryParams.length ? resolveParams(queryParams) : null,
|
|
804
|
+
headerParams: headerParams.length ? resolveParams(headerParams) : null,
|
|
776
805
|
requestName,
|
|
777
806
|
responseName,
|
|
778
807
|
file: zodFile
|
|
@@ -787,7 +816,7 @@ const serverGenerator = defineGenerator({
|
|
|
787
816
|
...(zod.headerParams ?? []).map((p) => p.schemaName),
|
|
788
817
|
zod.requestName,
|
|
789
818
|
zod.responseName
|
|
790
|
-
].filter(Boolean);
|
|
819
|
+
].filter((name) => Boolean(name));
|
|
791
820
|
const uniqueNames = [...new Set(zodNames)].sort();
|
|
792
821
|
return [/* @__PURE__ */ jsx(File.Import, {
|
|
793
822
|
name: [mcp.name],
|
|
@@ -803,13 +832,21 @@ const serverGenerator = defineGenerator({
|
|
|
803
832
|
baseName: serverFile.baseName,
|
|
804
833
|
path: serverFile.path,
|
|
805
834
|
meta: serverFile.meta,
|
|
806
|
-
banner: resolver.resolveBanner(
|
|
835
|
+
banner: resolver.resolveBanner(ctx.meta, {
|
|
807
836
|
output,
|
|
808
|
-
config
|
|
837
|
+
config,
|
|
838
|
+
file: {
|
|
839
|
+
path: serverFile.path,
|
|
840
|
+
baseName: serverFile.baseName
|
|
841
|
+
}
|
|
809
842
|
}),
|
|
810
|
-
footer: resolver.resolveFooter(
|
|
843
|
+
footer: resolver.resolveFooter(ctx.meta, {
|
|
811
844
|
output,
|
|
812
|
-
config
|
|
845
|
+
config,
|
|
846
|
+
file: {
|
|
847
|
+
path: serverFile.path,
|
|
848
|
+
baseName: serverFile.baseName
|
|
849
|
+
}
|
|
813
850
|
}),
|
|
814
851
|
children: [
|
|
815
852
|
/* @__PURE__ */ jsx(File.Import, {
|
|
@@ -827,8 +864,8 @@ const serverGenerator = defineGenerator({
|
|
|
827
864
|
imports,
|
|
828
865
|
/* @__PURE__ */ jsx(Server, {
|
|
829
866
|
name,
|
|
830
|
-
serverName:
|
|
831
|
-
serverVersion:
|
|
867
|
+
serverName: ctx.meta.title ?? "server",
|
|
868
|
+
serverVersion: ctx.meta.version ?? "0.0.0",
|
|
832
869
|
paramsCasing,
|
|
833
870
|
operations: operationsMapped
|
|
834
871
|
})
|
|
@@ -842,7 +879,7 @@ const serverGenerator = defineGenerator({
|
|
|
842
879
|
children: `
|
|
843
880
|
{
|
|
844
881
|
"mcpServers": {
|
|
845
|
-
"${
|
|
882
|
+
"${ctx.meta.title || "server"}": {
|
|
846
883
|
"type": "stdio",
|
|
847
884
|
"command": "npx",
|
|
848
885
|
"args": ["tsx", "${path.relative(path.dirname(jsonFile.path), serverFile.path)}"]
|
|
@@ -857,41 +894,77 @@ const serverGenerator = defineGenerator({
|
|
|
857
894
|
//#endregion
|
|
858
895
|
//#region src/resolvers/resolverMcp.ts
|
|
859
896
|
/**
|
|
860
|
-
*
|
|
897
|
+
* Default resolver used by `@kubb/plugin-mcp`. Decides the names and file
|
|
898
|
+
* paths for every generated MCP tool handler. Function names get a `Handler`
|
|
899
|
+
* suffix so an operation `addPet` becomes `addPetHandler`.
|
|
861
900
|
*
|
|
862
|
-
*
|
|
901
|
+
* @example Resolve a handler name
|
|
902
|
+
* ```ts
|
|
903
|
+
* import { resolverMcp } from '@kubb/plugin-mcp'
|
|
863
904
|
*
|
|
864
|
-
*
|
|
865
|
-
*
|
|
905
|
+
* resolverMcp.default('addPet', 'function') // 'addPetHandler'
|
|
906
|
+
* ```
|
|
866
907
|
*/
|
|
867
|
-
const resolverMcp = defineResolver((
|
|
908
|
+
const resolverMcp = defineResolver(() => ({
|
|
868
909
|
name: "default",
|
|
869
910
|
pluginName: "plugin-mcp",
|
|
870
911
|
default(name, type) {
|
|
871
|
-
if (type === "file") return
|
|
912
|
+
if (type === "file") return toFilePath(name);
|
|
872
913
|
return camelCase(name, { suffix: "handler" });
|
|
873
914
|
},
|
|
874
915
|
resolveName(name) {
|
|
875
|
-
return
|
|
916
|
+
return this.default(name, "function");
|
|
917
|
+
},
|
|
918
|
+
resolvePathName(name, type) {
|
|
919
|
+
return this.default(name, type);
|
|
920
|
+
},
|
|
921
|
+
resolveHandlerName(node) {
|
|
922
|
+
return this.resolveName(node.operationId);
|
|
876
923
|
}
|
|
877
924
|
}));
|
|
878
925
|
//#endregion
|
|
879
926
|
//#region src/plugin.ts
|
|
927
|
+
/**
|
|
928
|
+
* Canonical plugin name for `@kubb/plugin-mcp`. Used for driver lookups and
|
|
929
|
+
* cross-plugin dependency references.
|
|
930
|
+
*/
|
|
880
931
|
const pluginMcpName = "plugin-mcp";
|
|
932
|
+
/**
|
|
933
|
+
* Generates a Model Context Protocol (MCP) server from an OpenAPI spec. Every
|
|
934
|
+
* operation becomes a typed MCP tool that AI assistants (Claude Desktop, Claude
|
|
935
|
+
* Code, MCP-compatible clients) can call directly.
|
|
936
|
+
*
|
|
937
|
+
* @example
|
|
938
|
+
* ```ts
|
|
939
|
+
* import { defineConfig } from 'kubb'
|
|
940
|
+
* import { pluginTs } from '@kubb/plugin-ts'
|
|
941
|
+
* import { pluginClient } from '@kubb/plugin-client'
|
|
942
|
+
* import { pluginZod } from '@kubb/plugin-zod'
|
|
943
|
+
* import { pluginMcp } from '@kubb/plugin-mcp'
|
|
944
|
+
*
|
|
945
|
+
* export default defineConfig({
|
|
946
|
+
* input: { path: './petStore.yaml' },
|
|
947
|
+
* output: { path: './src/gen' },
|
|
948
|
+
* plugins: [
|
|
949
|
+
* pluginTs(),
|
|
950
|
+
* pluginClient(),
|
|
951
|
+
* pluginZod(),
|
|
952
|
+
* pluginMcp({
|
|
953
|
+
* output: { path: './mcp' },
|
|
954
|
+
* client: { baseURL: 'https://petstore.swagger.io/v2' },
|
|
955
|
+
* }),
|
|
956
|
+
* ],
|
|
957
|
+
* })
|
|
958
|
+
* ```
|
|
959
|
+
*/
|
|
881
960
|
const pluginMcp = definePlugin((options) => {
|
|
882
961
|
const { output = {
|
|
883
962
|
path: "mcp",
|
|
884
|
-
|
|
963
|
+
barrel: { type: "named" }
|
|
885
964
|
}, group, exclude = [], include, override = [], paramsCasing, client, resolver: userResolver, transformer: userTransformer, generators: userGenerators = [] } = options;
|
|
886
965
|
const clientName = client?.client ?? "axios";
|
|
887
966
|
const clientImportPath = client?.importPath ?? (!client?.bundle ? `@kubb/plugin-client/clients/${clientName}` : void 0);
|
|
888
|
-
const groupConfig = group
|
|
889
|
-
...group,
|
|
890
|
-
name: group.name ? group.name : (ctx) => {
|
|
891
|
-
if (group.type === "path") return `${ctx.group.split("/")[1]}`;
|
|
892
|
-
return `${camelCase(ctx.group)}Requests`;
|
|
893
|
-
}
|
|
894
|
-
} : void 0;
|
|
967
|
+
const groupConfig = createGroupConfig(group);
|
|
895
968
|
return {
|
|
896
969
|
name: pluginMcpName,
|
|
897
970
|
options,
|
|
@@ -927,10 +1000,10 @@ const pluginMcp = definePlugin((options) => {
|
|
|
927
1000
|
const root = path.resolve(ctx.config.root, ctx.config.output.path);
|
|
928
1001
|
const hasClientPlugin = ctx.config.plugins?.some((p) => p.name === pluginClientName);
|
|
929
1002
|
if (client?.bundle && !hasClientPlugin && !clientImportPath) ctx.injectFile({
|
|
930
|
-
baseName: "
|
|
931
|
-
path: path.resolve(root, ".kubb/
|
|
1003
|
+
baseName: "client.ts",
|
|
1004
|
+
path: path.resolve(root, ".kubb/client.ts"),
|
|
932
1005
|
sources: [ast.createSource({
|
|
933
|
-
name: "
|
|
1006
|
+
name: "client",
|
|
934
1007
|
nodes: [ast.createText(clientName === "fetch" ? source$1 : source)],
|
|
935
1008
|
isExportable: true,
|
|
936
1009
|
isIndexable: true
|