@kubb/plugin-mcp 5.0.0-beta.64 → 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 +296 -262
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +126 -119
- package/dist/index.js +296 -261
- package/dist/index.js.map +1 -1
- package/package.json +10 -11
- /package/dist/{chunk-C0LytTxp.js → rolldown-runtime-C0LytTxp.js} +0 -0
package/dist/index.js
CHANGED
|
@@ -1,15 +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 { caseParams, createOperationParams } from "@kubb/ast/utils";
|
|
5
|
-
import { functionPrinter, pluginTsName } from "@kubb/plugin-ts";
|
|
6
4
|
import { Const, File, Function, jsxRenderer } from "@kubb/renderer-jsx";
|
|
7
5
|
import { Fragment, jsx, jsxs } from "@kubb/renderer-jsx/jsx-runtime";
|
|
6
|
+
import { createFunctionParameter, createFunctionParameters, createObjectBindingPattern, functionPrinter, pluginTsName } from "@kubb/plugin-ts";
|
|
8
7
|
import { pluginZodName } from "@kubb/plugin-zod";
|
|
9
|
-
import { pluginClientName } from "@kubb/plugin-client";
|
|
10
|
-
import { source } from "@kubb/plugin-client/templates/clients/axios.source";
|
|
11
|
-
import { source as source$1 } from "@kubb/plugin-client/templates/clients/fetch.source";
|
|
12
|
-
import { source as source$2 } from "@kubb/plugin-client/templates/config.source";
|
|
13
8
|
//#region ../../internals/utils/src/casing.ts
|
|
14
9
|
/**
|
|
15
10
|
* Shared implementation for camelCase and PascalCase conversion.
|
|
@@ -222,6 +217,22 @@ var Url = class Url {
|
|
|
222
217
|
return `\`${prefix ?? ""}${result}\``;
|
|
223
218
|
}
|
|
224
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
|
+
/**
|
|
225
236
|
* Returns the path and its extracted params as a structured `URLObject`, or as a stringified
|
|
226
237
|
* expression when `stringify` is set.
|
|
227
238
|
*
|
|
@@ -249,13 +260,121 @@ var Url = class Url {
|
|
|
249
260
|
}
|
|
250
261
|
};
|
|
251
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
|
|
252
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
|
+
}
|
|
253
304
|
function getOperationLink(node, link) {
|
|
254
305
|
if (!link) return null;
|
|
255
306
|
if (typeof link === "function") return link(node) ?? null;
|
|
256
307
|
if (link === "urlPath") return node.path ? `{@link ${Url.toPath(node.path)}}` : null;
|
|
257
308
|
return node.path ? `{@link ${node.path.replaceAll("{", ":").replaceAll("}", "")}}` : null;
|
|
258
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
|
+
}
|
|
259
378
|
function buildOperationComments(node, options = {}) {
|
|
260
379
|
const { link = "pathTemplate", linkPosition = "afterDeprecated", splitLines = false } = options;
|
|
261
380
|
const linkComment = getOperationLink(node, link);
|
|
@@ -274,7 +393,7 @@ function buildOperationComments(node, options = {}) {
|
|
|
274
393
|
return filteredComments.flatMap((text) => text.split(/\r?\n/).map((line) => line.trim())).filter((comment) => Boolean(comment));
|
|
275
394
|
}
|
|
276
395
|
function getOperationParameters(node, options = {}) {
|
|
277
|
-
const params = caseParams(node.parameters, options.paramsCasing);
|
|
396
|
+
const params = caseParams(node.parameters, options.paramsCasing === "original" ? void 0 : "camelcase");
|
|
278
397
|
return {
|
|
279
398
|
path: params.filter((param) => param.in === "path"),
|
|
280
399
|
query: params.filter((param) => param.in === "query"),
|
|
@@ -290,48 +409,6 @@ function isSuccessStatusCode(statusCode) {
|
|
|
290
409
|
const code = getStatusCodeNumber(statusCode);
|
|
291
410
|
return code !== null && code >= 200 && code < 300;
|
|
292
411
|
}
|
|
293
|
-
function isErrorStatusCode(statusCode) {
|
|
294
|
-
const code = getStatusCodeNumber(statusCode);
|
|
295
|
-
return code !== null && code >= 400;
|
|
296
|
-
}
|
|
297
|
-
function resolveErrorNames(node, resolver) {
|
|
298
|
-
return node.responses.filter((response) => isErrorStatusCode(response.statusCode)).map((response) => resolver.resolveResponseStatusName(node, response.statusCode));
|
|
299
|
-
}
|
|
300
|
-
function resolveStatusCodeNames(node, resolver) {
|
|
301
|
-
return node.responses.map((response) => resolver.resolveResponseStatusName(node, response.statusCode));
|
|
302
|
-
}
|
|
303
|
-
const typeNamesByResolver = /* @__PURE__ */ new WeakMap();
|
|
304
|
-
function resolveOperationTypeNames(node, resolver, options = {}) {
|
|
305
|
-
const cacheKey = `${node.operationId}\0${options.paramsCasing ?? ""}\0${options.order ?? ""}\0${options.responseStatusNames ?? ""}\0${(options.exclude ?? []).join(",")}`;
|
|
306
|
-
let byResolver = typeNamesByResolver.get(resolver);
|
|
307
|
-
if (byResolver) {
|
|
308
|
-
const cached = byResolver.get(cacheKey);
|
|
309
|
-
if (cached) return cached;
|
|
310
|
-
} else {
|
|
311
|
-
byResolver = /* @__PURE__ */ new Map();
|
|
312
|
-
typeNamesByResolver.set(resolver, byResolver);
|
|
313
|
-
}
|
|
314
|
-
const { path, query, header } = getOperationParameters(node, { paramsCasing: options.paramsCasing });
|
|
315
|
-
const responseStatusNames = options.responseStatusNames === "error" ? resolveErrorNames(node, resolver) : options.responseStatusNames === false ? [] : resolveStatusCodeNames(node, resolver);
|
|
316
|
-
const exclude = new Set(options.exclude ?? []);
|
|
317
|
-
const paramNames = [
|
|
318
|
-
...path.map((param) => resolver.resolvePathParamsName(node, param)),
|
|
319
|
-
...query.map((param) => resolver.resolveQueryParamsName(node, param)),
|
|
320
|
-
...header.map((param) => resolver.resolveHeaderParamsName(node, param))
|
|
321
|
-
];
|
|
322
|
-
const bodyAndResponseNames = [node.requestBody?.content?.[0]?.schema ? resolver.resolveDataName(node) : null, resolver.resolveResponseName(node)];
|
|
323
|
-
const result = (options.order === "body-response-first" ? [
|
|
324
|
-
...bodyAndResponseNames,
|
|
325
|
-
...paramNames,
|
|
326
|
-
...responseStatusNames
|
|
327
|
-
] : [
|
|
328
|
-
...paramNames,
|
|
329
|
-
...bodyAndResponseNames,
|
|
330
|
-
...responseStatusNames
|
|
331
|
-
]).filter((name) => Boolean(name) && !exclude.has(name));
|
|
332
|
-
byResolver.set(cacheKey, result);
|
|
333
|
-
return result;
|
|
334
|
-
}
|
|
335
412
|
function findSuccessStatusCode(responses) {
|
|
336
413
|
for (const response of responses) if (isSuccessStatusCode(response.statusCode)) return response.statusCode;
|
|
337
414
|
return null;
|
|
@@ -368,76 +445,19 @@ function createGroupConfig(group) {
|
|
|
368
445
|
};
|
|
369
446
|
}
|
|
370
447
|
//#endregion
|
|
371
|
-
//#region ../../internals/shared/src/params.ts
|
|
372
|
-
function buildParamsMapping(originalParams, mappedParams) {
|
|
373
|
-
const mapping = {};
|
|
374
|
-
let hasChanged = false;
|
|
375
|
-
originalParams.forEach((param, i) => {
|
|
376
|
-
const mappedName = mappedParams[i]?.name ?? param.name;
|
|
377
|
-
mapping[param.name] = mappedName;
|
|
378
|
-
if (param.name !== mappedName) hasChanged = true;
|
|
379
|
-
});
|
|
380
|
-
return hasChanged ? mapping : null;
|
|
381
|
-
}
|
|
382
|
-
function buildTransformedParamsMapping(params, transformName) {
|
|
383
|
-
if (!params.length) return null;
|
|
384
|
-
return buildParamsMapping(params, params.map((param) => ({
|
|
385
|
-
...param,
|
|
386
|
-
name: transformName(param.name)
|
|
387
|
-
})));
|
|
388
|
-
}
|
|
389
|
-
//#endregion
|
|
390
448
|
//#region src/components/McpHandler.tsx
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
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 }) {
|
|
399
456
|
if (!ast.isHttpOperationNode(node)) return null;
|
|
400
|
-
const
|
|
401
|
-
const
|
|
402
|
-
const
|
|
403
|
-
const
|
|
404
|
-
const requestName = node.requestBody?.content?.[0]?.schema ? resolver.resolveDataName(node) : null;
|
|
405
|
-
const responseName = resolver.resolveResponseName(node);
|
|
406
|
-
const errorResponses = node.responses.filter((r) => Number(r.statusCode) >= 400).map((r) => resolver.resolveResponseStatusName(node, r.statusCode));
|
|
407
|
-
const generics = [
|
|
408
|
-
responseName,
|
|
409
|
-
`ResponseErrorConfig<${errorResponses.length > 0 ? errorResponses.join(" | ") : "Error"}>`,
|
|
410
|
-
requestName || "unknown"
|
|
411
|
-
].filter(Boolean);
|
|
412
|
-
const paramsNode = createOperationParams(node, {
|
|
413
|
-
paramsType: "object",
|
|
414
|
-
pathParamsType: "inline",
|
|
415
|
-
resolver,
|
|
416
|
-
paramsCasing
|
|
417
|
-
});
|
|
418
|
-
const baseParamsSignature = declarationPrinter.print(paramsNode) ?? "";
|
|
419
|
-
const paramsSignature = baseParamsSignature ? `${baseParamsSignature}, request: RequestHandlerExtra<ServerRequest, ServerNotification>` : "request: RequestHandlerExtra<ServerRequest, ServerNotification>";
|
|
420
|
-
const pathParamsMapping = paramsCasing ? buildTransformedParamsMapping(originalPathParams, camelCase) : null;
|
|
421
|
-
const queryParamsMapping = paramsCasing ? buildTransformedParamsMapping(originalQueryParams, camelCase) : null;
|
|
422
|
-
const headerParamsMapping = paramsCasing ? buildTransformedParamsMapping(originalHeaderParams, camelCase) : null;
|
|
423
|
-
const contentTypeHeader = contentType && contentType !== "application/json" && contentType !== "multipart/form-data" ? `'Content-Type': '${contentType}'` : null;
|
|
424
|
-
const headers = [headerParams.length ? headerParamsMapping ? "...mappedHeaders" : "...headers" : null, contentTypeHeader].filter(Boolean);
|
|
425
|
-
const fetchConfig = [];
|
|
426
|
-
fetchConfig.push(`method: ${JSON.stringify(node.method.toUpperCase())}`);
|
|
427
|
-
fetchConfig.push(`url: ${Url.toTemplateString(node.path)}`);
|
|
428
|
-
if (baseURL) fetchConfig.push(`baseURL: \`${baseURL}\``);
|
|
429
|
-
if (queryParams.length) fetchConfig.push(queryParamsMapping ? "params: mappedParams" : "params");
|
|
430
|
-
if (requestName) fetchConfig.push(`data: ${isFormData ? "formData as FormData" : "requestData"}`);
|
|
431
|
-
if (headers.length) fetchConfig.push(`headers: { ${headers.join(", ")} }`);
|
|
432
|
-
const callToolResult = `return {
|
|
433
|
-
content: [
|
|
434
|
-
{
|
|
435
|
-
type: 'text',
|
|
436
|
-
text: JSON.stringify(${dataReturnType === "data" ? "res.data" : "res"})
|
|
437
|
-
}
|
|
438
|
-
],
|
|
439
|
-
structuredContent: { data: res.data }
|
|
440
|
-
}`;
|
|
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(", ")} }` : "{}";
|
|
441
461
|
return /* @__PURE__ */ jsx(File.Source, {
|
|
442
462
|
name,
|
|
443
463
|
isExportable: true,
|
|
@@ -450,28 +470,17 @@ function McpHandler({ name, node, resolver, baseURL, dataReturnType, paramsCasin
|
|
|
450
470
|
JSDoc: { comments: buildOperationComments(node) },
|
|
451
471
|
returnType: "Promise<CallToolResult>",
|
|
452
472
|
children: [
|
|
453
|
-
|
|
473
|
+
`const res = await ${clientName}(${callConfig})`,
|
|
454
474
|
/* @__PURE__ */ jsx("br", {}),
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
buildRemappingCode(headerParamsMapping, "mappedHeaders", "headers"),
|
|
465
|
-
/* @__PURE__ */ jsx("br", {}),
|
|
466
|
-
/* @__PURE__ */ jsx("br", {})
|
|
467
|
-
] }),
|
|
468
|
-
requestName && "const requestData = data",
|
|
469
|
-
/* @__PURE__ */ jsx("br", {}),
|
|
470
|
-
isFormData && requestName && "const formData = buildFormData(requestData)",
|
|
471
|
-
/* @__PURE__ */ jsx("br", {}),
|
|
472
|
-
`const res = await client<${generics.join(", ")}>({ ${fetchConfig.join(", ")} }, request)`,
|
|
473
|
-
/* @__PURE__ */ jsx("br", {}),
|
|
474
|
-
callToolResult
|
|
475
|
+
`return {
|
|
476
|
+
content: [
|
|
477
|
+
{
|
|
478
|
+
type: 'text',
|
|
479
|
+
text: JSON.stringify(res.data)
|
|
480
|
+
}
|
|
481
|
+
],
|
|
482
|
+
structuredContent: { data: res.data }
|
|
483
|
+
}`
|
|
475
484
|
]
|
|
476
485
|
})
|
|
477
486
|
});
|
|
@@ -512,37 +521,36 @@ function zodExprFromSchemaNode(schema) {
|
|
|
512
521
|
//#endregion
|
|
513
522
|
//#region src/components/Server.tsx
|
|
514
523
|
const keysPrinter = functionPrinter({ mode: "call" });
|
|
515
|
-
function Server({ name, serverName, serverVersion,
|
|
524
|
+
function Server({ name, serverName, serverVersion, operations }) {
|
|
516
525
|
const registrations = operations.map(({ tool, mcp, zod, node }) => {
|
|
517
|
-
const { path: pathParams } = getOperationParameters(node
|
|
518
|
-
const
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
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} })`
|
|
525
536
|
});
|
|
526
537
|
}
|
|
527
|
-
if (zod.
|
|
528
|
-
key: "
|
|
529
|
-
value: zod.requestName
|
|
530
|
-
});
|
|
531
|
-
if (zod.queryParams) otherEntries.push({
|
|
532
|
-
key: "params",
|
|
538
|
+
if (zod.queryParams) entries.push({
|
|
539
|
+
key: "query",
|
|
533
540
|
value: zodGroupExpr(zod.queryParams)
|
|
534
541
|
});
|
|
535
|
-
if (zod.headerParams)
|
|
542
|
+
if (zod.headerParams) entries.push({
|
|
536
543
|
key: "headers",
|
|
537
544
|
value: zodGroupExpr(zod.headerParams)
|
|
538
545
|
});
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
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 })) }),
|
|
544
552
|
optional: false
|
|
545
|
-
}] }) : null;
|
|
553
|
+
})] }) : null;
|
|
546
554
|
const destructured = paramsNode ? keysPrinter.print(paramsNode) ?? "" : "";
|
|
547
555
|
const inputSchema = entries.length ? `{ ${entries.map((e) => `${e.key}: ${e.value}`).join(", ")} }` : null;
|
|
548
556
|
const outputSchema = zod.responseName;
|
|
@@ -604,12 +612,105 @@ return server`
|
|
|
604
612
|
});
|
|
605
613
|
}
|
|
606
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
|
|
607
708
|
//#region src/generators/mcpGenerator.tsx
|
|
608
709
|
/**
|
|
609
710
|
* Built-in operation generator for `@kubb/plugin-mcp`. Emits one MCP tool
|
|
610
|
-
* handler per OpenAPI operation
|
|
611
|
-
*
|
|
612
|
-
*
|
|
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.
|
|
613
714
|
*/
|
|
614
715
|
const mcpGenerator = defineGenerator({
|
|
615
716
|
name: "mcp",
|
|
@@ -617,14 +718,19 @@ const mcpGenerator = defineGenerator({
|
|
|
617
718
|
operation(node, ctx) {
|
|
618
719
|
if (!ast.isHttpOperationNode(node)) return null;
|
|
619
720
|
const { resolver, driver, root } = ctx;
|
|
620
|
-
const { output, client,
|
|
721
|
+
const { output, client, group } = ctx.options;
|
|
621
722
|
const pluginTs = driver.getPlugin(pluginTsName);
|
|
622
723
|
if (!pluginTs) return null;
|
|
623
724
|
const tsResolver = driver.getResolver(pluginTsName);
|
|
624
|
-
const
|
|
625
|
-
|
|
626
|
-
|
|
725
|
+
const contractOp = resolveClientOperation({
|
|
726
|
+
clientPlugin: { pluginName: client.pluginName },
|
|
727
|
+
driver,
|
|
728
|
+
node,
|
|
729
|
+
root,
|
|
730
|
+
output
|
|
627
731
|
});
|
|
732
|
+
if (!contractOp) return null;
|
|
733
|
+
const requestConfigName = tsResolver.resolveRequestConfigName(node);
|
|
628
734
|
const meta = {
|
|
629
735
|
name: resolver.resolveHandlerName(node),
|
|
630
736
|
file: resolver.resolveFile({
|
|
@@ -653,8 +759,8 @@ const mcpGenerator = defineGenerator({
|
|
|
653
759
|
path: meta.file.path,
|
|
654
760
|
meta: meta.file.meta,
|
|
655
761
|
children: [
|
|
656
|
-
meta.fileTs &&
|
|
657
|
-
name:
|
|
762
|
+
meta.fileTs && requestConfigName && /* @__PURE__ */ jsx(File.Import, {
|
|
763
|
+
name: [requestConfigName],
|
|
658
764
|
root: meta.file.path,
|
|
659
765
|
path: meta.fileTs.path,
|
|
660
766
|
isTypeOnly: true
|
|
@@ -674,59 +780,15 @@ const mcpGenerator = defineGenerator({
|
|
|
674
780
|
isTypeOnly: true
|
|
675
781
|
}),
|
|
676
782
|
/* @__PURE__ */ jsx(File.Import, {
|
|
677
|
-
name: [
|
|
783
|
+
name: [contractOp.name],
|
|
678
784
|
root: meta.file.path,
|
|
679
|
-
path: path
|
|
785
|
+
path: contractOp.path
|
|
680
786
|
}),
|
|
681
|
-
client.importPath ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
682
|
-
/* @__PURE__ */ jsx(File.Import, {
|
|
683
|
-
name: [
|
|
684
|
-
"Client",
|
|
685
|
-
"RequestConfig",
|
|
686
|
-
"ResponseErrorConfig"
|
|
687
|
-
],
|
|
688
|
-
path: client.importPath,
|
|
689
|
-
isTypeOnly: true
|
|
690
|
-
}),
|
|
691
|
-
/* @__PURE__ */ jsx(File.Import, {
|
|
692
|
-
name: "client",
|
|
693
|
-
path: client.importPath
|
|
694
|
-
}),
|
|
695
|
-
client.dataReturnType === "full" && /* @__PURE__ */ jsx(File.Import, {
|
|
696
|
-
name: ["ResponseConfig"],
|
|
697
|
-
path: client.importPath,
|
|
698
|
-
isTypeOnly: true
|
|
699
|
-
})
|
|
700
|
-
] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
701
|
-
/* @__PURE__ */ jsx(File.Import, {
|
|
702
|
-
name: [
|
|
703
|
-
"Client",
|
|
704
|
-
"RequestConfig",
|
|
705
|
-
"ResponseErrorConfig"
|
|
706
|
-
],
|
|
707
|
-
root: meta.file.path,
|
|
708
|
-
path: path.resolve(root, ".kubb/client.ts"),
|
|
709
|
-
isTypeOnly: true
|
|
710
|
-
}),
|
|
711
|
-
/* @__PURE__ */ jsx(File.Import, {
|
|
712
|
-
name: ["client"],
|
|
713
|
-
root: meta.file.path,
|
|
714
|
-
path: path.resolve(root, ".kubb/client.ts")
|
|
715
|
-
}),
|
|
716
|
-
client.dataReturnType === "full" && /* @__PURE__ */ jsx(File.Import, {
|
|
717
|
-
name: ["ResponseConfig"],
|
|
718
|
-
root: meta.file.path,
|
|
719
|
-
path: path.resolve(root, ".kubb/client.ts"),
|
|
720
|
-
isTypeOnly: true
|
|
721
|
-
})
|
|
722
|
-
] }),
|
|
723
787
|
/* @__PURE__ */ jsx(McpHandler, {
|
|
724
788
|
name: meta.name,
|
|
789
|
+
clientName: contractOp.name,
|
|
725
790
|
node,
|
|
726
|
-
resolver: tsResolver
|
|
727
|
-
baseURL: client.baseURL,
|
|
728
|
-
dataReturnType: client.dataReturnType || "data",
|
|
729
|
-
paramsCasing
|
|
791
|
+
resolver: tsResolver
|
|
730
792
|
})
|
|
731
793
|
]
|
|
732
794
|
});
|
|
@@ -746,7 +808,7 @@ const serverGenerator = defineGenerator({
|
|
|
746
808
|
renderer: jsxRenderer,
|
|
747
809
|
operations(nodes, ctx) {
|
|
748
810
|
const { config, resolver, plugin, driver, root } = ctx;
|
|
749
|
-
const { output,
|
|
811
|
+
const { output, group } = ctx.options;
|
|
750
812
|
const pluginZod = driver.getPlugin(pluginZodName);
|
|
751
813
|
if (!pluginZod) return;
|
|
752
814
|
const zodResolver = driver.getResolver(pluginZodName);
|
|
@@ -762,7 +824,7 @@ const serverGenerator = defineGenerator({
|
|
|
762
824
|
meta: { pluginName: plugin.name }
|
|
763
825
|
};
|
|
764
826
|
const operationsMapped = nodes.filter(ast.isHttpOperationNode).map((node) => {
|
|
765
|
-
const { path: pathParams, query: queryParams, header: headerParams } = getOperationParameters(node
|
|
827
|
+
const { path: pathParams, query: queryParams, header: headerParams } = getOperationParameters(node);
|
|
766
828
|
const mcpFile = resolver.resolveFile({
|
|
767
829
|
name: node.operationId,
|
|
768
830
|
extname: ".ts",
|
|
@@ -868,7 +930,6 @@ const serverGenerator = defineGenerator({
|
|
|
868
930
|
name,
|
|
869
931
|
serverName: ctx.meta.title ?? "server",
|
|
870
932
|
serverVersion: ctx.meta.version ?? "0.0.0",
|
|
871
|
-
paramsCasing,
|
|
872
933
|
operations: operationsMapped
|
|
873
934
|
})
|
|
874
935
|
]
|
|
@@ -934,13 +995,13 @@ const pluginMcpName = "plugin-mcp";
|
|
|
934
995
|
/**
|
|
935
996
|
* Generates a Model Context Protocol (MCP) server from an OpenAPI spec. Every
|
|
936
997
|
* operation becomes a typed MCP tool that AI assistants (Claude Desktop, Claude
|
|
937
|
-
* 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`.
|
|
938
1000
|
*
|
|
939
1001
|
* @example
|
|
940
1002
|
* ```ts
|
|
941
1003
|
* import { defineConfig } from 'kubb'
|
|
942
1004
|
* import { pluginTs } from '@kubb/plugin-ts'
|
|
943
|
-
* import { pluginClient } from '@kubb/plugin-client'
|
|
944
1005
|
* import { pluginZod } from '@kubb/plugin-zod'
|
|
945
1006
|
* import { pluginMcp } from '@kubb/plugin-mcp'
|
|
946
1007
|
*
|
|
@@ -949,11 +1010,10 @@ const pluginMcpName = "plugin-mcp";
|
|
|
949
1010
|
* output: { path: './src/gen' },
|
|
950
1011
|
* plugins: [
|
|
951
1012
|
* pluginTs(),
|
|
952
|
-
* pluginClient(),
|
|
953
1013
|
* pluginZod(),
|
|
954
1014
|
* pluginMcp({
|
|
955
1015
|
* output: { path: './mcp' },
|
|
956
|
-
*
|
|
1016
|
+
* baseURL: 'https://petstore.swagger.io/v2',
|
|
957
1017
|
* }),
|
|
958
1018
|
* ],
|
|
959
1019
|
* })
|
|
@@ -963,9 +1023,7 @@ const pluginMcp = definePlugin((options) => {
|
|
|
963
1023
|
const { output = {
|
|
964
1024
|
path: "mcp",
|
|
965
1025
|
barrel: { type: "named" }
|
|
966
|
-
}, group, exclude = [], include, override = [],
|
|
967
|
-
const clientName = client?.client ?? "axios";
|
|
968
|
-
const clientImportPath = client?.importPath ?? (!client?.bundle ? `@kubb/plugin-client/clients/${clientName}` : void 0);
|
|
1026
|
+
}, group, exclude = [], include, override = [], client, resolver: userResolver, macros: userMacros } = options;
|
|
969
1027
|
const groupConfig = createGroupConfig(group);
|
|
970
1028
|
return {
|
|
971
1029
|
name: pluginMcpName,
|
|
@@ -976,51 +1034,28 @@ const pluginMcp = definePlugin((options) => {
|
|
|
976
1034
|
...resolverMcp,
|
|
977
1035
|
...userResolver
|
|
978
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
|
+
};
|
|
979
1046
|
ctx.setOptions({
|
|
980
1047
|
output,
|
|
981
1048
|
exclude,
|
|
982
1049
|
include,
|
|
983
1050
|
override,
|
|
984
1051
|
group: groupConfig,
|
|
985
|
-
|
|
986
|
-
client: {
|
|
987
|
-
client: clientName,
|
|
988
|
-
clientType: client?.clientType ?? "function",
|
|
989
|
-
importPath: clientImportPath,
|
|
990
|
-
dataReturnType: client?.dataReturnType ?? "data",
|
|
991
|
-
bundle: client?.bundle,
|
|
992
|
-
baseURL: client?.baseURL,
|
|
993
|
-
paramsCasing: client?.paramsCasing
|
|
994
|
-
},
|
|
1052
|
+
client: resolvedClientDescriptor,
|
|
995
1053
|
resolver
|
|
996
1054
|
});
|
|
997
1055
|
ctx.setResolver(resolver);
|
|
998
1056
|
if (userMacros?.length) ctx.setMacros(userMacros);
|
|
999
1057
|
ctx.addGenerator(mcpGenerator);
|
|
1000
1058
|
ctx.addGenerator(serverGenerator);
|
|
1001
|
-
for (const gen of userGenerators) ctx.addGenerator(gen);
|
|
1002
|
-
const root = path.resolve(ctx.config.root, ctx.config.output.path);
|
|
1003
|
-
const hasClientPlugin = ctx.config.plugins?.some((p) => p.name === pluginClientName);
|
|
1004
|
-
if (client?.bundle && !hasClientPlugin && !clientImportPath) ctx.injectFile({
|
|
1005
|
-
baseName: "client.ts",
|
|
1006
|
-
path: path.resolve(root, ".kubb/client.ts"),
|
|
1007
|
-
sources: [ast.factory.createSource({
|
|
1008
|
-
name: "client",
|
|
1009
|
-
nodes: [ast.factory.createText(clientName === "fetch" ? source$1 : source)],
|
|
1010
|
-
isExportable: true,
|
|
1011
|
-
isIndexable: true
|
|
1012
|
-
})]
|
|
1013
|
-
});
|
|
1014
|
-
if (!hasClientPlugin) ctx.injectFile({
|
|
1015
|
-
baseName: "config.ts",
|
|
1016
|
-
path: path.resolve(root, ".kubb/config.ts"),
|
|
1017
|
-
sources: [ast.factory.createSource({
|
|
1018
|
-
name: "config",
|
|
1019
|
-
nodes: [ast.factory.createText(source$2)],
|
|
1020
|
-
isExportable: false,
|
|
1021
|
-
isIndexable: false
|
|
1022
|
-
})]
|
|
1023
|
-
});
|
|
1024
1059
|
} }
|
|
1025
1060
|
};
|
|
1026
1061
|
});
|