@kubb/plugin-mcp 5.0.0-beta.56 → 5.0.0-beta.73
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 +2 -2
- package/dist/index.cjs +298 -262
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +126 -119
- package/dist/index.js +298 -261
- package/dist/index.js.map +1 -1
- package/package.json +10 -11
- package/src/components/McpHandler.tsx +0 -157
- package/src/components/Server.tsx +0 -159
- package/src/generators/mcpGenerator.tsx +0 -89
- package/src/generators/serverGenerator.tsx +0 -149
- package/src/index.ts +0 -11
- package/src/plugin.ts +0 -142
- package/src/resolvers/resolverMcp.ts +0 -35
- package/src/types.ts +0 -90
- package/src/utils.ts +0 -48
- /package/dist/{chunk-C0LytTxp.js → rolldown-runtime-C0LytTxp.js} +0 -0
package/dist/index.js
CHANGED
|
@@ -1,14 +1,10 @@
|
|
|
1
|
-
import "./
|
|
1
|
+
import "./rolldown-runtime-C0LytTxp.js";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { ast, defineGenerator, definePlugin, defineResolver } from "@kubb/core";
|
|
4
|
-
import { functionPrinter, pluginTsName } from "@kubb/plugin-ts";
|
|
5
4
|
import { Const, File, Function, jsxRenderer } from "@kubb/renderer-jsx";
|
|
6
5
|
import { Fragment, jsx, jsxs } from "@kubb/renderer-jsx/jsx-runtime";
|
|
6
|
+
import { createFunctionParameter, createFunctionParameters, createObjectBindingPattern, functionPrinter, pluginTsName } from "@kubb/plugin-ts";
|
|
7
7
|
import { pluginZodName } from "@kubb/plugin-zod";
|
|
8
|
-
import { pluginClientName } from "@kubb/plugin-client";
|
|
9
|
-
import { source } from "@kubb/plugin-client/templates/clients/axios.source";
|
|
10
|
-
import { source as source$1 } from "@kubb/plugin-client/templates/clients/fetch.source";
|
|
11
|
-
import { source as source$2 } from "@kubb/plugin-client/templates/config.source";
|
|
12
8
|
//#region ../../internals/utils/src/casing.ts
|
|
13
9
|
/**
|
|
14
10
|
* Shared implementation for camelCase and PascalCase conversion.
|
|
@@ -221,6 +217,22 @@ var Url = class Url {
|
|
|
221
217
|
return `\`${prefix ?? ""}${result}\``;
|
|
222
218
|
}
|
|
223
219
|
/**
|
|
220
|
+
* Converts an OpenAPI/Swagger path to a template literal that reads each parameter off the
|
|
221
|
+
* grouped `path` request option, e.g. `/pet/{petId}` becomes `` `/pet/${path.petId}` ``. Parameter
|
|
222
|
+
* names are camelCased to match the generated `path` type, and `prefix` is prepended inside the
|
|
223
|
+
* literal. Shared by the client and cypress generators that pass a grouped `path` object.
|
|
224
|
+
*
|
|
225
|
+
* @example
|
|
226
|
+
* Url.toGroupedTemplateString('/pet/{petId}') // '`/pet/${path.petId}`'
|
|
227
|
+
*/
|
|
228
|
+
static toGroupedTemplateString(path, { prefix } = {}) {
|
|
229
|
+
return Url.toTemplateString(path, {
|
|
230
|
+
prefix,
|
|
231
|
+
casing: "camelcase",
|
|
232
|
+
replacer: (name) => `path.${name}`
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
224
236
|
* Returns the path and its extracted params as a structured `URLObject`, or as a stringified
|
|
225
237
|
* expression when `stringify` is set.
|
|
226
238
|
*
|
|
@@ -248,13 +260,121 @@ var Url = class Url {
|
|
|
248
260
|
}
|
|
249
261
|
};
|
|
250
262
|
//#endregion
|
|
263
|
+
//#region ../../internals/shared/src/params.ts
|
|
264
|
+
const caseParamsCache = /* @__PURE__ */ new WeakMap();
|
|
265
|
+
/**
|
|
266
|
+
* Applies camelCase to parameter names and returns a new array without mutating the input.
|
|
267
|
+
*
|
|
268
|
+
* Run it before handing parameters to schema builders so output property keys get the right casing
|
|
269
|
+
* while `OperationNode.parameters` stays intact for other consumers. When `casing` is unset, the
|
|
270
|
+
* original array is returned unchanged. Results are cached per input array.
|
|
271
|
+
*/
|
|
272
|
+
function caseParams(params, casing) {
|
|
273
|
+
if (!casing) return params;
|
|
274
|
+
const cached = caseParamsCache.get(params);
|
|
275
|
+
if (cached) return cached;
|
|
276
|
+
const result = params.map((param) => ({
|
|
277
|
+
...param,
|
|
278
|
+
name: camelCase(param.name)
|
|
279
|
+
}));
|
|
280
|
+
caseParamsCache.set(params, result);
|
|
281
|
+
return result;
|
|
282
|
+
}
|
|
283
|
+
//#endregion
|
|
251
284
|
//#region ../../internals/shared/src/operation.ts
|
|
285
|
+
/**
|
|
286
|
+
* Builds the `ResolverFileParams` every operation generator passes to
|
|
287
|
+
* `resolver.resolveFile`: a file named `name`, tagged by the operation's first
|
|
288
|
+
* tag (or `'default'`), at the operation's path. Centralizes the entry object
|
|
289
|
+
* that was repeated at dozens of call sites across the client and query plugins.
|
|
290
|
+
*
|
|
291
|
+
* @example
|
|
292
|
+
* ```ts
|
|
293
|
+
* resolver.resolveFile(operationFileEntry(node, node.operationId), { root, output, group })
|
|
294
|
+
* ```
|
|
295
|
+
*/
|
|
296
|
+
function operationFileEntry(node, name, extname = ".ts") {
|
|
297
|
+
return {
|
|
298
|
+
name,
|
|
299
|
+
extname,
|
|
300
|
+
tag: node.tags[0] ?? "default",
|
|
301
|
+
path: node.path
|
|
302
|
+
};
|
|
303
|
+
}
|
|
252
304
|
function getOperationLink(node, link) {
|
|
253
305
|
if (!link) return null;
|
|
254
306
|
if (typeof link === "function") return link(node) ?? null;
|
|
255
307
|
if (link === "urlPath") return node.path ? `{@link ${Url.toPath(node.path)}}` : null;
|
|
256
308
|
return node.path ? `{@link ${node.path.replaceAll("{", ":").replaceAll("}", "")}}` : null;
|
|
257
309
|
}
|
|
310
|
+
function getContentTypeInfo(node) {
|
|
311
|
+
const contentTypes = node.requestBody?.content?.map((e) => e.contentType) ?? [];
|
|
312
|
+
const isMultipleContentTypes = contentTypes.length > 1;
|
|
313
|
+
return {
|
|
314
|
+
contentTypes,
|
|
315
|
+
isMultipleContentTypes,
|
|
316
|
+
contentTypeUnion: isMultipleContentTypes ? contentTypes.map((ct) => JSON.stringify(ct)).join(" | ") : "",
|
|
317
|
+
defaultContentType: contentTypes[0] ?? "application/json",
|
|
318
|
+
hasFormData: contentTypes.some((ct) => ct === "multipart/form-data")
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
function buildRequestConfigType(node) {
|
|
322
|
+
const { isMultipleContentTypes, contentTypeUnion } = getContentTypeInfo(node);
|
|
323
|
+
const configType = `Partial<Omit<RequestConfig, 'path' | 'query' | 'body' | 'headers' | 'url'>>`;
|
|
324
|
+
const contentTypeProp = isMultipleContentTypes ? `contentType?: ${contentTypeUnion}` : null;
|
|
325
|
+
return contentTypeProp ? `${configType} & { ${contentTypeProp} }` : configType;
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* Which of the grouped request options an operation carries.
|
|
329
|
+
*/
|
|
330
|
+
function getRequestGroups(node) {
|
|
331
|
+
const { path, query, header } = getOperationParameters(node);
|
|
332
|
+
return {
|
|
333
|
+
path: path.length > 0,
|
|
334
|
+
query: query.length > 0,
|
|
335
|
+
body: Boolean(node.requestBody?.content?.[0]?.schema),
|
|
336
|
+
headers: header.length > 0
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* Resolves which grouped request options an operation carries together with whether each group
|
|
341
|
+
* holds a required member. The grouped parameter stays optional only when nothing inside it is
|
|
342
|
+
* required, matching the generated `RequestConfig` type.
|
|
343
|
+
*/
|
|
344
|
+
function getRequestGroupOptionality(node) {
|
|
345
|
+
const groups = getRequestGroups(node);
|
|
346
|
+
const { path, query, header } = getOperationParameters(node);
|
|
347
|
+
const hasRequiredPath = path.some((param) => param.required);
|
|
348
|
+
const hasRequiredQuery = query.some((param) => param.required);
|
|
349
|
+
const hasRequiredHeader = header.some((param) => param.required);
|
|
350
|
+
return {
|
|
351
|
+
groups,
|
|
352
|
+
hasRequiredPath,
|
|
353
|
+
hasRequiredQuery,
|
|
354
|
+
hasRequiredHeader,
|
|
355
|
+
isOptional: !hasRequiredPath && !hasRequiredQuery && !hasRequiredHeader && !groups.body
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
/**
|
|
359
|
+
* Builds the grouped `{ path, query, body, headers }` parameter for a generated client
|
|
360
|
+
* function, typed from the operation's `RequestConfig` (minus `url`). Only the groups the
|
|
361
|
+
* operation actually has are destructured. The trailing `config` parameter carries the
|
|
362
|
+
* runtime `RequestConfig` overrides plus `client`.
|
|
363
|
+
*/
|
|
364
|
+
function buildRequestParamsSignature(node, resolver, options = {}) {
|
|
365
|
+
const { isConfigurable = true } = options;
|
|
366
|
+
const { groups, isOptional } = getRequestGroupOptionality(node);
|
|
367
|
+
const names = [
|
|
368
|
+
"path",
|
|
369
|
+
"query",
|
|
370
|
+
"body",
|
|
371
|
+
"headers"
|
|
372
|
+
].filter((key) => groups[key]);
|
|
373
|
+
return {
|
|
374
|
+
signature: [names.length > 0 ? `{ ${names.join(", ")} }: ${resolver.resolveRequestConfigName(node)}${isOptional ? " = {}" : ""}` : null, isConfigurable ? `config: ${buildRequestConfigType(node)} = {}` : null].filter(Boolean).join(", "),
|
|
375
|
+
groups
|
|
376
|
+
};
|
|
377
|
+
}
|
|
258
378
|
function buildOperationComments(node, options = {}) {
|
|
259
379
|
const { link = "pathTemplate", linkPosition = "afterDeprecated", splitLines = false } = options;
|
|
260
380
|
const linkComment = getOperationLink(node, link);
|
|
@@ -273,7 +393,7 @@ function buildOperationComments(node, options = {}) {
|
|
|
273
393
|
return filteredComments.flatMap((text) => text.split(/\r?\n/).map((line) => line.trim())).filter((comment) => Boolean(comment));
|
|
274
394
|
}
|
|
275
395
|
function getOperationParameters(node, options = {}) {
|
|
276
|
-
const params =
|
|
396
|
+
const params = caseParams(node.parameters, options.paramsCasing === "original" ? void 0 : "camelcase");
|
|
277
397
|
return {
|
|
278
398
|
path: params.filter((param) => param.in === "path"),
|
|
279
399
|
query: params.filter((param) => param.in === "query"),
|
|
@@ -289,48 +409,6 @@ function isSuccessStatusCode(statusCode) {
|
|
|
289
409
|
const code = getStatusCodeNumber(statusCode);
|
|
290
410
|
return code !== null && code >= 200 && code < 300;
|
|
291
411
|
}
|
|
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);
|
|
312
|
-
}
|
|
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
412
|
function findSuccessStatusCode(responses) {
|
|
335
413
|
for (const response of responses) if (isSuccessStatusCode(response.statusCode)) return response.statusCode;
|
|
336
414
|
return null;
|
|
@@ -367,76 +445,19 @@ function createGroupConfig(group) {
|
|
|
367
445
|
};
|
|
368
446
|
}
|
|
369
447
|
//#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
|
-
})));
|
|
387
|
-
}
|
|
388
|
-
//#endregion
|
|
389
448
|
//#region src/components/McpHandler.tsx
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
function McpHandler({ name, node, resolver, baseURL, dataReturnType, paramsCasing }) {
|
|
449
|
+
const requestGroupOrder = [
|
|
450
|
+
"path",
|
|
451
|
+
"query",
|
|
452
|
+
"headers",
|
|
453
|
+
"body"
|
|
454
|
+
];
|
|
455
|
+
function McpHandler({ name, clientName, node, resolver }) {
|
|
398
456
|
if (!ast.isHttpOperationNode(node)) return null;
|
|
399
|
-
const
|
|
400
|
-
const
|
|
401
|
-
const
|
|
402
|
-
const
|
|
403
|
-
const requestName = node.requestBody?.content?.[0]?.schema ? resolver.resolveDataName(node) : null;
|
|
404
|
-
const responseName = resolver.resolveResponseName(node);
|
|
405
|
-
const errorResponses = node.responses.filter((r) => Number(r.statusCode) >= 400).map((r) => resolver.resolveResponseStatusName(node, r.statusCode));
|
|
406
|
-
const generics = [
|
|
407
|
-
responseName,
|
|
408
|
-
`ResponseErrorConfig<${errorResponses.length > 0 ? errorResponses.join(" | ") : "Error"}>`,
|
|
409
|
-
requestName || "unknown"
|
|
410
|
-
].filter(Boolean);
|
|
411
|
-
const paramsNode = ast.createOperationParams(node, {
|
|
412
|
-
paramsType: "object",
|
|
413
|
-
pathParamsType: "inline",
|
|
414
|
-
resolver,
|
|
415
|
-
paramsCasing
|
|
416
|
-
});
|
|
417
|
-
const baseParamsSignature = declarationPrinter.print(paramsNode) ?? "";
|
|
418
|
-
const paramsSignature = baseParamsSignature ? `${baseParamsSignature}, request: RequestHandlerExtra<ServerRequest, ServerNotification>` : "request: RequestHandlerExtra<ServerRequest, ServerNotification>";
|
|
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);
|
|
424
|
-
const fetchConfig = [];
|
|
425
|
-
fetchConfig.push(`method: ${JSON.stringify(node.method.toUpperCase())}`);
|
|
426
|
-
fetchConfig.push(`url: ${Url.toTemplateString(node.path)}`);
|
|
427
|
-
if (baseURL) fetchConfig.push(`baseURL: \`${baseURL}\``);
|
|
428
|
-
if (queryParams.length) fetchConfig.push(queryParamsMapping ? "params: mappedParams" : "params");
|
|
429
|
-
if (requestName) fetchConfig.push(`data: ${isFormData ? "formData as FormData" : "requestData"}`);
|
|
430
|
-
if (headers.length) fetchConfig.push(`headers: { ${headers.join(", ")} }`);
|
|
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
|
-
}`;
|
|
457
|
+
const { signature, groups } = buildRequestParamsSignature(node, resolver, { isConfigurable: false });
|
|
458
|
+
const paramsSignature = [signature, "request: RequestHandlerExtra<ServerRequest, ServerNotification>"].filter(Boolean).join(", ");
|
|
459
|
+
const callArgs = requestGroupOrder.filter((key) => groups[key]);
|
|
460
|
+
const callConfig = callArgs.length ? `{ ${callArgs.join(", ")} }` : "{}";
|
|
440
461
|
return /* @__PURE__ */ jsx(File.Source, {
|
|
441
462
|
name,
|
|
442
463
|
isExportable: true,
|
|
@@ -449,28 +470,17 @@ function McpHandler({ name, node, resolver, baseURL, dataReturnType, paramsCasin
|
|
|
449
470
|
JSDoc: { comments: buildOperationComments(node) },
|
|
450
471
|
returnType: "Promise<CallToolResult>",
|
|
451
472
|
children: [
|
|
452
|
-
|
|
473
|
+
`const res = await ${clientName}(${callConfig})`,
|
|
453
474
|
/* @__PURE__ */ jsx("br", {}),
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
buildRemappingCode(headerParamsMapping, "mappedHeaders", "headers"),
|
|
464
|
-
/* @__PURE__ */ jsx("br", {}),
|
|
465
|
-
/* @__PURE__ */ jsx("br", {})
|
|
466
|
-
] }),
|
|
467
|
-
requestName && "const requestData = data",
|
|
468
|
-
/* @__PURE__ */ jsx("br", {}),
|
|
469
|
-
isFormData && requestName && "const formData = buildFormData(requestData)",
|
|
470
|
-
/* @__PURE__ */ jsx("br", {}),
|
|
471
|
-
`const res = await client<${generics.join(", ")}>({ ${fetchConfig.join(", ")} }, request)`,
|
|
472
|
-
/* @__PURE__ */ jsx("br", {}),
|
|
473
|
-
callToolResult
|
|
475
|
+
`return {
|
|
476
|
+
content: [
|
|
477
|
+
{
|
|
478
|
+
type: 'text',
|
|
479
|
+
text: JSON.stringify(res.data)
|
|
480
|
+
}
|
|
481
|
+
],
|
|
482
|
+
structuredContent: { data: res.data }
|
|
483
|
+
}`
|
|
474
484
|
]
|
|
475
485
|
})
|
|
476
486
|
});
|
|
@@ -510,37 +520,37 @@ function zodExprFromSchemaNode(schema) {
|
|
|
510
520
|
}
|
|
511
521
|
//#endregion
|
|
512
522
|
//#region src/components/Server.tsx
|
|
513
|
-
const keysPrinter = functionPrinter({ mode: "
|
|
514
|
-
function Server({ name, serverName, serverVersion,
|
|
523
|
+
const keysPrinter = functionPrinter({ mode: "call" });
|
|
524
|
+
function Server({ name, serverName, serverVersion, operations }) {
|
|
515
525
|
const registrations = operations.map(({ tool, mcp, zod, node }) => {
|
|
516
|
-
const { path: pathParams } = getOperationParameters(node
|
|
517
|
-
const
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
526
|
+
const { path: pathParams } = getOperationParameters(node);
|
|
527
|
+
const entries = [];
|
|
528
|
+
if (pathParams.length) {
|
|
529
|
+
const pathFields = pathParams.map((p) => {
|
|
530
|
+
const zodParam = zod.pathParams.find((zp) => zp.name === p.name);
|
|
531
|
+
return `${JSON.stringify(p.name)}: ${zodParam ? zodParam.schemaName : zodExprFromSchemaNode(p.schema)}`;
|
|
532
|
+
}).join(", ");
|
|
533
|
+
entries.push({
|
|
534
|
+
key: "path",
|
|
535
|
+
value: `z.object({ ${pathFields} })`
|
|
524
536
|
});
|
|
525
537
|
}
|
|
526
|
-
if (zod.
|
|
527
|
-
key: "
|
|
528
|
-
value: zod.requestName
|
|
529
|
-
});
|
|
530
|
-
if (zod.queryParams) otherEntries.push({
|
|
531
|
-
key: "params",
|
|
538
|
+
if (zod.queryParams) entries.push({
|
|
539
|
+
key: "query",
|
|
532
540
|
value: zodGroupExpr(zod.queryParams)
|
|
533
541
|
});
|
|
534
|
-
if (zod.headerParams)
|
|
542
|
+
if (zod.headerParams) entries.push({
|
|
535
543
|
key: "headers",
|
|
536
544
|
value: zodGroupExpr(zod.headerParams)
|
|
537
545
|
});
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
546
|
+
if (zod.requestName) entries.push({
|
|
547
|
+
key: "body",
|
|
548
|
+
value: zod.requestName
|
|
549
|
+
});
|
|
550
|
+
const paramsNode = entries.length ? createFunctionParameters({ params: [createFunctionParameter({
|
|
551
|
+
name: createObjectBindingPattern({ elements: entries.map((e) => ({ name: e.key })) }),
|
|
542
552
|
optional: false
|
|
543
|
-
})
|
|
553
|
+
})] }) : null;
|
|
544
554
|
const destructured = paramsNode ? keysPrinter.print(paramsNode) ?? "" : "";
|
|
545
555
|
const inputSchema = entries.length ? `{ ${entries.map((e) => `${e.key}: ${e.value}`).join(", ")} }` : null;
|
|
546
556
|
const outputSchema = zod.responseName;
|
|
@@ -602,12 +612,105 @@ return server`
|
|
|
602
612
|
});
|
|
603
613
|
}
|
|
604
614
|
//#endregion
|
|
615
|
+
//#region ../../internals/client/src/resolveClient.ts
|
|
616
|
+
/**
|
|
617
|
+
* Resolves which client plugin a consumer (react-query, vue-query, swr, mcp) should call for an
|
|
618
|
+
* operation, shared so the selection rules and diagnostics stay in one place.
|
|
619
|
+
*
|
|
620
|
+
* Every client runtime lives in a dedicated client plugin, so a consumer always calls a registered
|
|
621
|
+
* contract client plugin (plugin-fetch or plugin-axios) and never emits its own inline client:
|
|
622
|
+
*
|
|
623
|
+
* - `contract` — a registered contract client plugin owns the `<op>` functions and the consumer
|
|
624
|
+
* imports and calls them.
|
|
625
|
+
* - `error` — no client plugin is registered, several are registered without a selector, or the
|
|
626
|
+
* requested one is missing.
|
|
627
|
+
*
|
|
628
|
+
* The `client` string selects explicitly (`'fetch'` / `'axios'`); when it is unset a lone registered
|
|
629
|
+
* contract client plugin is picked up automatically.
|
|
630
|
+
*/
|
|
631
|
+
const pluginFetchName = "plugin-fetch";
|
|
632
|
+
const pluginAxiosName = "plugin-axios";
|
|
633
|
+
const selectorToPlugin = {
|
|
634
|
+
fetch: pluginFetchName,
|
|
635
|
+
axios: pluginAxiosName
|
|
636
|
+
};
|
|
637
|
+
const contractPlugins = [pluginFetchName, pluginAxiosName];
|
|
638
|
+
/**
|
|
639
|
+
* Applies the `client` resolution rules. See the module comment for the strategy shape.
|
|
640
|
+
*
|
|
641
|
+
* @example
|
|
642
|
+
* ```ts
|
|
643
|
+
* resolveClient({ client: 'fetch', pluginNames: ['plugin-ts', 'plugin-fetch'] })
|
|
644
|
+
* // { kind: 'contract', pluginName: 'plugin-fetch' }
|
|
645
|
+
* ```
|
|
646
|
+
*
|
|
647
|
+
* @example
|
|
648
|
+
* ```ts
|
|
649
|
+
* resolveClient({ client: undefined, pluginNames: ['plugin-ts'] })
|
|
650
|
+
* // { kind: 'error', message: 'No client plugin is registered…' }
|
|
651
|
+
* ```
|
|
652
|
+
*/
|
|
653
|
+
function resolveClient(options) {
|
|
654
|
+
const { client, pluginNames } = options;
|
|
655
|
+
const has = (name) => pluginNames.includes(name);
|
|
656
|
+
if (client === "fetch" || client === "axios") {
|
|
657
|
+
const pluginName = selectorToPlugin[client];
|
|
658
|
+
if (!has(pluginName)) return {
|
|
659
|
+
kind: "error",
|
|
660
|
+
message: `\`client: '${client}'\` is set but \`@kubb/plugin-${client}\` is not registered in \`plugins\`. Add it, or drop \`client\` to use a different client plugin.`
|
|
661
|
+
};
|
|
662
|
+
return {
|
|
663
|
+
kind: "contract",
|
|
664
|
+
pluginName
|
|
665
|
+
};
|
|
666
|
+
}
|
|
667
|
+
const registered = contractPlugins.filter(has);
|
|
668
|
+
if (registered.length === 1) return {
|
|
669
|
+
kind: "contract",
|
|
670
|
+
pluginName: registered[0]
|
|
671
|
+
};
|
|
672
|
+
if (registered.length > 1) return {
|
|
673
|
+
kind: "error",
|
|
674
|
+
message: `Multiple client plugins are registered (${registered.map((name) => `\`@kubb/${name}\``).join(", ")}). Set \`client: 'fetch' | 'axios'\` to choose which client the hooks call, or register a single client plugin.`
|
|
675
|
+
};
|
|
676
|
+
return {
|
|
677
|
+
kind: "error",
|
|
678
|
+
message: "No client plugin is registered. Add `@kubb/plugin-axios` or `@kubb/plugin-fetch` to `plugins` so the generated code has an HTTP client to call."
|
|
679
|
+
};
|
|
680
|
+
}
|
|
681
|
+
//#endregion
|
|
682
|
+
//#region ../../internals/client/src/resolveClientOperation.ts
|
|
683
|
+
/**
|
|
684
|
+
* Resolves the contract client `<op>` a consumer (query hook, MCP handler) imports, by looking up
|
|
685
|
+
* the registered contract client plugin's resolver and output. Works for any contract client plugin
|
|
686
|
+
* (plugin-fetch or plugin-axios). Returns `null` when no contract plugin is in play (the inline
|
|
687
|
+
* path). The plugin injects `.kubb/client.ts` at the global output root, the same path consumers
|
|
688
|
+
* read `RequestConfig` / `ResponseErrorConfig` from.
|
|
689
|
+
*/
|
|
690
|
+
function resolveClientOperation(options) {
|
|
691
|
+
const { clientPlugin, driver, node, root, output } = options;
|
|
692
|
+
if (!clientPlugin) return null;
|
|
693
|
+
const resolver = driver.getResolver(clientPlugin.pluginName);
|
|
694
|
+
if (!resolver) return null;
|
|
695
|
+
const plugin = driver.getPlugin(clientPlugin.pluginName);
|
|
696
|
+
const file = resolver.resolveFile(operationFileEntry(node, node.operationId), {
|
|
697
|
+
root,
|
|
698
|
+
output: plugin?.options?.output ?? output,
|
|
699
|
+
group: plugin?.options?.group ?? void 0
|
|
700
|
+
});
|
|
701
|
+
return {
|
|
702
|
+
name: resolver.resolveName(node.operationId),
|
|
703
|
+
path: file.path,
|
|
704
|
+
clientPath: path.resolve(root, ".kubb/client.ts")
|
|
705
|
+
};
|
|
706
|
+
}
|
|
707
|
+
//#endregion
|
|
605
708
|
//#region src/generators/mcpGenerator.tsx
|
|
606
709
|
/**
|
|
607
710
|
* Built-in operation generator for `@kubb/plugin-mcp`. Emits one MCP tool
|
|
608
|
-
* handler per OpenAPI operation
|
|
609
|
-
*
|
|
610
|
-
*
|
|
711
|
+
* handler per OpenAPI operation. The handler takes the grouped `{ path, query,
|
|
712
|
+
* headers, body }` config and forwards it to the registered client plugin's
|
|
713
|
+
* (`@kubb/plugin-axios` / `@kubb/plugin-fetch`) `<op>` function.
|
|
611
714
|
*/
|
|
612
715
|
const mcpGenerator = defineGenerator({
|
|
613
716
|
name: "mcp",
|
|
@@ -615,14 +718,19 @@ const mcpGenerator = defineGenerator({
|
|
|
615
718
|
operation(node, ctx) {
|
|
616
719
|
if (!ast.isHttpOperationNode(node)) return null;
|
|
617
720
|
const { resolver, driver, root } = ctx;
|
|
618
|
-
const { output, client,
|
|
721
|
+
const { output, client, group } = ctx.options;
|
|
619
722
|
const pluginTs = driver.getPlugin(pluginTsName);
|
|
620
723
|
if (!pluginTs) return null;
|
|
621
724
|
const tsResolver = driver.getResolver(pluginTsName);
|
|
622
|
-
const
|
|
623
|
-
|
|
624
|
-
|
|
725
|
+
const contractOp = resolveClientOperation({
|
|
726
|
+
clientPlugin: { pluginName: client.pluginName },
|
|
727
|
+
driver,
|
|
728
|
+
node,
|
|
729
|
+
root,
|
|
730
|
+
output
|
|
625
731
|
});
|
|
732
|
+
if (!contractOp) return null;
|
|
733
|
+
const requestConfigName = tsResolver.resolveRequestConfigName(node);
|
|
626
734
|
const meta = {
|
|
627
735
|
name: resolver.resolveHandlerName(node),
|
|
628
736
|
file: resolver.resolveFile({
|
|
@@ -651,8 +759,8 @@ const mcpGenerator = defineGenerator({
|
|
|
651
759
|
path: meta.file.path,
|
|
652
760
|
meta: meta.file.meta,
|
|
653
761
|
children: [
|
|
654
|
-
meta.fileTs &&
|
|
655
|
-
name:
|
|
762
|
+
meta.fileTs && requestConfigName && /* @__PURE__ */ jsx(File.Import, {
|
|
763
|
+
name: [requestConfigName],
|
|
656
764
|
root: meta.file.path,
|
|
657
765
|
path: meta.fileTs.path,
|
|
658
766
|
isTypeOnly: true
|
|
@@ -672,59 +780,15 @@ const mcpGenerator = defineGenerator({
|
|
|
672
780
|
isTypeOnly: true
|
|
673
781
|
}),
|
|
674
782
|
/* @__PURE__ */ jsx(File.Import, {
|
|
675
|
-
name: [
|
|
783
|
+
name: [contractOp.name],
|
|
676
784
|
root: meta.file.path,
|
|
677
|
-
path: path
|
|
785
|
+
path: contractOp.path
|
|
678
786
|
}),
|
|
679
|
-
client.importPath ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
680
|
-
/* @__PURE__ */ jsx(File.Import, {
|
|
681
|
-
name: [
|
|
682
|
-
"Client",
|
|
683
|
-
"RequestConfig",
|
|
684
|
-
"ResponseErrorConfig"
|
|
685
|
-
],
|
|
686
|
-
path: client.importPath,
|
|
687
|
-
isTypeOnly: true
|
|
688
|
-
}),
|
|
689
|
-
/* @__PURE__ */ jsx(File.Import, {
|
|
690
|
-
name: "client",
|
|
691
|
-
path: client.importPath
|
|
692
|
-
}),
|
|
693
|
-
client.dataReturnType === "full" && /* @__PURE__ */ jsx(File.Import, {
|
|
694
|
-
name: ["ResponseConfig"],
|
|
695
|
-
path: client.importPath,
|
|
696
|
-
isTypeOnly: true
|
|
697
|
-
})
|
|
698
|
-
] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
699
|
-
/* @__PURE__ */ jsx(File.Import, {
|
|
700
|
-
name: [
|
|
701
|
-
"Client",
|
|
702
|
-
"RequestConfig",
|
|
703
|
-
"ResponseErrorConfig"
|
|
704
|
-
],
|
|
705
|
-
root: meta.file.path,
|
|
706
|
-
path: path.resolve(root, ".kubb/client.ts"),
|
|
707
|
-
isTypeOnly: true
|
|
708
|
-
}),
|
|
709
|
-
/* @__PURE__ */ jsx(File.Import, {
|
|
710
|
-
name: ["client"],
|
|
711
|
-
root: meta.file.path,
|
|
712
|
-
path: path.resolve(root, ".kubb/client.ts")
|
|
713
|
-
}),
|
|
714
|
-
client.dataReturnType === "full" && /* @__PURE__ */ jsx(File.Import, {
|
|
715
|
-
name: ["ResponseConfig"],
|
|
716
|
-
root: meta.file.path,
|
|
717
|
-
path: path.resolve(root, ".kubb/client.ts"),
|
|
718
|
-
isTypeOnly: true
|
|
719
|
-
})
|
|
720
|
-
] }),
|
|
721
787
|
/* @__PURE__ */ jsx(McpHandler, {
|
|
722
788
|
name: meta.name,
|
|
789
|
+
clientName: contractOp.name,
|
|
723
790
|
node,
|
|
724
|
-
resolver: tsResolver
|
|
725
|
-
baseURL: client.baseURL,
|
|
726
|
-
dataReturnType: client.dataReturnType || "data",
|
|
727
|
-
paramsCasing
|
|
791
|
+
resolver: tsResolver
|
|
728
792
|
})
|
|
729
793
|
]
|
|
730
794
|
});
|
|
@@ -744,7 +808,7 @@ const serverGenerator = defineGenerator({
|
|
|
744
808
|
renderer: jsxRenderer,
|
|
745
809
|
operations(nodes, ctx) {
|
|
746
810
|
const { config, resolver, plugin, driver, root } = ctx;
|
|
747
|
-
const { output,
|
|
811
|
+
const { output, group } = ctx.options;
|
|
748
812
|
const pluginZod = driver.getPlugin(pluginZodName);
|
|
749
813
|
if (!pluginZod) return;
|
|
750
814
|
const zodResolver = driver.getResolver(pluginZodName);
|
|
@@ -760,7 +824,7 @@ const serverGenerator = defineGenerator({
|
|
|
760
824
|
meta: { pluginName: plugin.name }
|
|
761
825
|
};
|
|
762
826
|
const operationsMapped = nodes.filter(ast.isHttpOperationNode).map((node) => {
|
|
763
|
-
const { path: pathParams, query: queryParams, header: headerParams } = getOperationParameters(node
|
|
827
|
+
const { path: pathParams, query: queryParams, header: headerParams } = getOperationParameters(node);
|
|
764
828
|
const mcpFile = resolver.resolveFile({
|
|
765
829
|
name: node.operationId,
|
|
766
830
|
extname: ".ts",
|
|
@@ -866,7 +930,6 @@ const serverGenerator = defineGenerator({
|
|
|
866
930
|
name,
|
|
867
931
|
serverName: ctx.meta.title ?? "server",
|
|
868
932
|
serverVersion: ctx.meta.version ?? "0.0.0",
|
|
869
|
-
paramsCasing,
|
|
870
933
|
operations: operationsMapped
|
|
871
934
|
})
|
|
872
935
|
]
|
|
@@ -932,13 +995,13 @@ const pluginMcpName = "plugin-mcp";
|
|
|
932
995
|
/**
|
|
933
996
|
* Generates a Model Context Protocol (MCP) server from an OpenAPI spec. Every
|
|
934
997
|
* operation becomes a typed MCP tool that AI assistants (Claude Desktop, Claude
|
|
935
|
-
* Code, MCP-compatible clients) can call directly.
|
|
998
|
+
* Code, MCP-compatible clients) can call directly. Each handler calls a contract
|
|
999
|
+
* client `<op>` from a registered `@kubb/plugin-axios` or `@kubb/plugin-fetch`.
|
|
936
1000
|
*
|
|
937
1001
|
* @example
|
|
938
1002
|
* ```ts
|
|
939
1003
|
* import { defineConfig } from 'kubb'
|
|
940
1004
|
* import { pluginTs } from '@kubb/plugin-ts'
|
|
941
|
-
* import { pluginClient } from '@kubb/plugin-client'
|
|
942
1005
|
* import { pluginZod } from '@kubb/plugin-zod'
|
|
943
1006
|
* import { pluginMcp } from '@kubb/plugin-mcp'
|
|
944
1007
|
*
|
|
@@ -947,11 +1010,10 @@ const pluginMcpName = "plugin-mcp";
|
|
|
947
1010
|
* output: { path: './src/gen' },
|
|
948
1011
|
* plugins: [
|
|
949
1012
|
* pluginTs(),
|
|
950
|
-
* pluginClient(),
|
|
951
1013
|
* pluginZod(),
|
|
952
1014
|
* pluginMcp({
|
|
953
1015
|
* output: { path: './mcp' },
|
|
954
|
-
*
|
|
1016
|
+
* baseURL: 'https://petstore.swagger.io/v2',
|
|
955
1017
|
* }),
|
|
956
1018
|
* ],
|
|
957
1019
|
* })
|
|
@@ -961,9 +1023,7 @@ const pluginMcp = definePlugin((options) => {
|
|
|
961
1023
|
const { output = {
|
|
962
1024
|
path: "mcp",
|
|
963
1025
|
barrel: { type: "named" }
|
|
964
|
-
}, group, exclude = [], include, override = [],
|
|
965
|
-
const clientName = client?.client ?? "axios";
|
|
966
|
-
const clientImportPath = client?.importPath ?? (!client?.bundle ? `@kubb/plugin-client/clients/${clientName}` : void 0);
|
|
1026
|
+
}, group, exclude = [], include, override = [], client, resolver: userResolver, macros: userMacros } = options;
|
|
967
1027
|
const groupConfig = createGroupConfig(group);
|
|
968
1028
|
return {
|
|
969
1029
|
name: pluginMcpName,
|
|
@@ -974,51 +1034,28 @@ const pluginMcp = definePlugin((options) => {
|
|
|
974
1034
|
...resolverMcp,
|
|
975
1035
|
...userResolver
|
|
976
1036
|
} : resolverMcp;
|
|
1037
|
+
const resolvedClient = resolveClient({
|
|
1038
|
+
client,
|
|
1039
|
+
pluginNames: (ctx.config.plugins ?? []).map((p) => p.name).filter((name) => Boolean(name))
|
|
1040
|
+
});
|
|
1041
|
+
if (resolvedClient.kind === "error") throw new Error(resolvedClient.message);
|
|
1042
|
+
const resolvedClientDescriptor = {
|
|
1043
|
+
kind: "contract",
|
|
1044
|
+
pluginName: resolvedClient.pluginName
|
|
1045
|
+
};
|
|
977
1046
|
ctx.setOptions({
|
|
978
1047
|
output,
|
|
979
1048
|
exclude,
|
|
980
1049
|
include,
|
|
981
1050
|
override,
|
|
982
1051
|
group: groupConfig,
|
|
983
|
-
|
|
984
|
-
client: {
|
|
985
|
-
client: clientName,
|
|
986
|
-
clientType: client?.clientType ?? "function",
|
|
987
|
-
importPath: clientImportPath,
|
|
988
|
-
dataReturnType: client?.dataReturnType ?? "data",
|
|
989
|
-
bundle: client?.bundle,
|
|
990
|
-
baseURL: client?.baseURL,
|
|
991
|
-
paramsCasing: client?.paramsCasing
|
|
992
|
-
},
|
|
1052
|
+
client: resolvedClientDescriptor,
|
|
993
1053
|
resolver
|
|
994
1054
|
});
|
|
995
1055
|
ctx.setResolver(resolver);
|
|
996
|
-
if (
|
|
1056
|
+
if (userMacros?.length) ctx.setMacros(userMacros);
|
|
997
1057
|
ctx.addGenerator(mcpGenerator);
|
|
998
1058
|
ctx.addGenerator(serverGenerator);
|
|
999
|
-
for (const gen of userGenerators) ctx.addGenerator(gen);
|
|
1000
|
-
const root = path.resolve(ctx.config.root, ctx.config.output.path);
|
|
1001
|
-
const hasClientPlugin = ctx.config.plugins?.some((p) => p.name === pluginClientName);
|
|
1002
|
-
if (client?.bundle && !hasClientPlugin && !clientImportPath) ctx.injectFile({
|
|
1003
|
-
baseName: "client.ts",
|
|
1004
|
-
path: path.resolve(root, ".kubb/client.ts"),
|
|
1005
|
-
sources: [ast.createSource({
|
|
1006
|
-
name: "client",
|
|
1007
|
-
nodes: [ast.createText(clientName === "fetch" ? source$1 : source)],
|
|
1008
|
-
isExportable: true,
|
|
1009
|
-
isIndexable: true
|
|
1010
|
-
})]
|
|
1011
|
-
});
|
|
1012
|
-
if (!hasClientPlugin) ctx.injectFile({
|
|
1013
|
-
baseName: "config.ts",
|
|
1014
|
-
path: path.resolve(root, ".kubb/config.ts"),
|
|
1015
|
-
sources: [ast.createSource({
|
|
1016
|
-
name: "config",
|
|
1017
|
-
nodes: [ast.createText(source$2)],
|
|
1018
|
-
isExportable: false,
|
|
1019
|
-
isIndexable: false
|
|
1020
|
-
})]
|
|
1021
|
-
});
|
|
1022
1059
|
} }
|
|
1023
1060
|
};
|
|
1024
1061
|
});
|