@kubb/plugin-mcp 5.0.0-alpha.9 → 5.0.0-beta.100

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -1,124 +1,880 @@
1
- Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_Server = require("./Server-DV9zFrUP.cjs");
3
- const require_generators = require("./generators-CWAFnA94.cjs");
1
+ Object.defineProperties(exports, {
2
+ __esModule: { value: true },
3
+ [Symbol.toStringTag]: { value: "Module" }
4
+ });
5
+ //#region \0rolldown/runtime.js
6
+ var __create = Object.create;
7
+ var __defProp = Object.defineProperty;
8
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
9
+ var __getOwnPropNames = Object.getOwnPropertyNames;
10
+ var __getProtoOf = Object.getPrototypeOf;
11
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
14
+ key = keys[i];
15
+ if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
16
+ get: ((k) => from[k]).bind(null, key),
17
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
18
+ });
19
+ }
20
+ return to;
21
+ };
22
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
23
+ value: mod,
24
+ enumerable: true
25
+ }) : target, mod));
26
+ //#endregion
27
+ let kubb_kit = require("kubb/kit");
4
28
  let node_path = require("node:path");
5
- node_path = require_Server.__toESM(node_path);
6
- let _kubb_core = require("@kubb/core");
7
- let _kubb_plugin_client = require("@kubb/plugin-client");
8
- let _kubb_plugin_client_templates_clients_axios_source = require("@kubb/plugin-client/templates/clients/axios.source");
9
- let _kubb_plugin_client_templates_clients_fetch_source = require("@kubb/plugin-client/templates/clients/fetch.source");
10
- let _kubb_plugin_client_templates_config_source = require("@kubb/plugin-client/templates/config.source");
11
- let _kubb_plugin_oas = require("@kubb/plugin-oas");
29
+ node_path = __toESM(node_path, 1);
30
+ let kubb_jsx = require("kubb/jsx");
31
+ let kubb_jsx_jsx_runtime = require("kubb/jsx/jsx-runtime");
12
32
  let _kubb_plugin_ts = require("@kubb/plugin-ts");
13
33
  let _kubb_plugin_zod = require("@kubb/plugin-zod");
34
+ //#region ../../internals/shared/src/params.ts
35
+ /**
36
+ * Drops parameters that share the same name, keeping the first.
37
+ *
38
+ * A malformed spec can declare the same parameter name twice within one `in` location. Both would
39
+ * resolve to the same output property, so emitting both would yield an object type with a duplicate
40
+ * member, which TypeScript rejects. This is a defensive guard against that case, not a casing guard:
41
+ * parameter names flow through unchanged, so no two distinct names ever collide here anymore.
42
+ */
43
+ function dedupeParams(params) {
44
+ const seen = /* @__PURE__ */ new Set();
45
+ return params.filter((param) => {
46
+ if (seen.has(param.name)) return false;
47
+ seen.add(param.name);
48
+ return true;
49
+ });
50
+ }
51
+ //#endregion
52
+ //#region ../../internals/shared/src/operation.ts
53
+ /**
54
+ * Builds the `ResolverFileParams` every operation generator passes to
55
+ * `resolver.file`: a file named `name`, tagged by the operation's first
56
+ * tag (or `'default'`), at the operation's path. Centralizes the entry object
57
+ * that was repeated at dozens of call sites across the client and query plugins.
58
+ *
59
+ * @example
60
+ * ```ts
61
+ * resolver.file(operationFileEntry(node, node.operationId), { root, output, group })
62
+ * ```
63
+ */
64
+ function operationFileEntry(node, name, extname = ".ts") {
65
+ return {
66
+ name,
67
+ extname,
68
+ tag: node.tags[0] ?? "default",
69
+ path: node.path
70
+ };
71
+ }
72
+ function getOperationLink(node, link) {
73
+ if (!link) return null;
74
+ if (typeof link === "function") return link(node) ?? null;
75
+ return node.path ? `{@link ${kubb_kit.Url.toPath(node.path)}}` : null;
76
+ }
77
+ /**
78
+ * Derives the shared `ContentTypeInfo` shape from a list of content types, tracking whether several
79
+ * are present and the union, default, and form-data flags the client uses to pick one.
80
+ */
81
+ function buildContentTypeInfo(contentTypes) {
82
+ const isMultipleContentTypes = contentTypes.length > 1;
83
+ return {
84
+ contentTypes,
85
+ isMultipleContentTypes,
86
+ contentTypeUnion: isMultipleContentTypes ? contentTypes.map((ct) => JSON.stringify(ct)).join(" | ") : "",
87
+ defaultContentType: contentTypes[0] ?? "application/json",
88
+ hasFormData: contentTypes.some((ct) => ct === "multipart/form-data")
89
+ };
90
+ }
91
+ function getContentTypeInfo(node) {
92
+ return buildContentTypeInfo(node.requestBody?.content?.map((e) => e.contentType) ?? []);
93
+ }
94
+ /**
95
+ * The request-body counterpart for the primary success response: the content types it documents and
96
+ * whether several are present, so the client can let a caller pick which one to accept.
97
+ */
98
+ function getResponseContentTypeInfo(node) {
99
+ return buildContentTypeInfo(getPrimarySuccessResponse(node)?.content?.map((e) => e.contentType) ?? []);
100
+ }
101
+ function buildRequestConfigType(node) {
102
+ const request = getContentTypeInfo(node);
103
+ const response = getResponseContentTypeInfo(node);
104
+ const configType = `Partial<Omit<RequestConfig, 'path' | 'query' | 'body' | 'headers' | 'url'>>`;
105
+ const members = [request.isMultipleContentTypes ? `request?: ${request.contentTypeUnion}` : null, response.isMultipleContentTypes ? `response?: ${response.contentTypeUnion}` : null].filter(Boolean);
106
+ return members.length ? `${configType} & { contentType?: { ${members.join("; ")} } }` : configType;
107
+ }
108
+ /**
109
+ * Which of the grouped request options an operation carries.
110
+ */
111
+ function getRequestGroups(node) {
112
+ const { path, query, header } = getOperationParameters(node);
113
+ return {
114
+ path: path.length > 0,
115
+ query: query.length > 0,
116
+ body: Boolean(node.requestBody?.content?.[0]?.schema),
117
+ headers: header.length > 0
118
+ };
119
+ }
120
+ /**
121
+ * Resolves which grouped request options an operation carries together with whether each group
122
+ * holds a required member. The grouped parameter stays optional only when nothing inside it is
123
+ * required, matching the generated `RequestConfig` type.
124
+ */
125
+ function getRequestGroupOptionality(node) {
126
+ const groups = getRequestGroups(node);
127
+ const { path, query, header } = getOperationParameters(node);
128
+ const hasRequiredPath = path.some((param) => param.required);
129
+ const hasRequiredQuery = query.some((param) => param.required);
130
+ const hasRequiredHeader = header.some((param) => param.required);
131
+ return {
132
+ groups,
133
+ hasRequiredPath,
134
+ hasRequiredQuery,
135
+ hasRequiredHeader,
136
+ isOptional: !hasRequiredPath && !hasRequiredQuery && !hasRequiredHeader && !groups.body
137
+ };
138
+ }
139
+ /**
140
+ * Builds the grouped `{ path, query, body, headers }` parameter for a generated client
141
+ * function, typed from the operation's `Options` (minus `url`). Only the groups the
142
+ * operation actually has are destructured. The trailing `config` parameter carries the
143
+ * runtime `RequestConfig` overrides plus `client`.
144
+ */
145
+ function buildRequestParamsSignature(node, resolver, options = {}) {
146
+ const { isConfigurable = true } = options;
147
+ const { groups, isOptional } = getRequestGroupOptionality(node);
148
+ const names = [
149
+ "path",
150
+ "query",
151
+ "body",
152
+ "headers"
153
+ ].filter((key) => groups[key]);
154
+ return {
155
+ signature: [names.length > 0 ? `{ ${names.join(", ")} }: ${resolver.response.options(node)}${isOptional ? " = {}" : ""}` : null, isConfigurable ? `config: ${buildRequestConfigType(node)} = {}` : null].filter(Boolean).join(", "),
156
+ groups
157
+ };
158
+ }
159
+ function buildOperationComments(node, options = {}) {
160
+ const { link = "pathTemplate", linkPosition = "afterDeprecated", splitLines = false } = options;
161
+ const linkComment = getOperationLink(node, link);
162
+ const filteredComments = (linkPosition === "beforeDeprecated" ? [
163
+ node.description && `@description ${node.description}`,
164
+ node.summary && `@summary ${node.summary}`,
165
+ linkComment,
166
+ node.deprecated && "@deprecated"
167
+ ] : [
168
+ node.description && `@description ${node.description}`,
169
+ node.summary && `@summary ${node.summary}`,
170
+ node.deprecated && "@deprecated",
171
+ linkComment
172
+ ]).filter((comment) => Boolean(comment));
173
+ if (!splitLines) return filteredComments;
174
+ return filteredComments.flatMap((text) => text.split(/\r?\n/).map((line) => line.trim())).filter((comment) => Boolean(comment));
175
+ }
176
+ function getOperationParameters(node) {
177
+ return {
178
+ path: dedupeParams(node.parameters.filter((param) => param.in === "path")),
179
+ query: dedupeParams(node.parameters.filter((param) => param.in === "query")),
180
+ header: dedupeParams(node.parameters.filter((param) => param.in === "header")),
181
+ cookie: dedupeParams(node.parameters.filter((param) => param.in === "cookie"))
182
+ };
183
+ }
184
+ function getStatusCodeNumber(statusCode) {
185
+ const code = Number(statusCode);
186
+ return Number.isNaN(code) ? null : code;
187
+ }
188
+ function isSuccessStatusCode(statusCode) {
189
+ const code = getStatusCodeNumber(statusCode);
190
+ return code !== null && code >= 200 && code < 300;
191
+ }
192
+ function getSuccessResponses(responses) {
193
+ return responses.filter((response) => isSuccessStatusCode(response.statusCode));
194
+ }
195
+ function getOperationSuccessResponses(node) {
196
+ return getSuccessResponses(node.responses);
197
+ }
198
+ function getPrimarySuccessResponse(node) {
199
+ return getOperationSuccessResponses(node)[0] ?? null;
200
+ }
201
+ function findSuccessStatusCode(responses) {
202
+ for (const response of responses) if (isSuccessStatusCode(response.statusCode)) return response.statusCode;
203
+ return null;
204
+ }
205
+ //#endregion
206
+ //#region ../../internals/utils/src/casing.ts
207
+ /**
208
+ * Shared implementation for camelCase and PascalCase conversion.
209
+ * Splits on common word boundaries (spaces, hyphens, underscores, dots, slashes, colons)
210
+ * and capitalizes each word according to `pascal`.
211
+ *
212
+ * When `pascal` is `true` the first word is also capitalized (PascalCase), otherwise only subsequent words are.
213
+ */
214
+ function toCamelOrPascal(text, pascal) {
215
+ 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) => {
216
+ if (word.length > 1 && word === word.toUpperCase()) return word;
217
+ return (i === 0 && !pascal ? word.charAt(0).toLowerCase() : word.charAt(0).toUpperCase()) + word.slice(1);
218
+ }).join("").replace(/[^a-zA-Z0-9]/g, "");
219
+ }
220
+ /**
221
+ * Converts `text` to camelCase.
222
+ *
223
+ * @example Word boundaries
224
+ * `camelCase('hello-world') // 'helloWorld'`
225
+ *
226
+ * @example With a prefix
227
+ * `camelCase('tag', { prefix: 'create' }) // 'createTag'`
228
+ */
229
+ function camelCase(text, { prefix = "", suffix = "" } = {}) {
230
+ return toCamelOrPascal(`${prefix} ${text} ${suffix}`, false);
231
+ }
232
+ //#endregion
233
+ //#region ../../internals/shared/src/group.ts
234
+ /**
235
+ * Builds the `group` config a Kubb plugin passes to `ctx.setOptions`, applying the
236
+ * shared default naming so every plugin groups output consistently:
237
+ *
238
+ * - `path` groups use the second path segment (`/pet/findByStatus` → `pet`).
239
+ * - other groups use the camelCased group (`pet store` → `petStore`).
240
+ *
241
+ * A user-provided `group.name` always wins over the default namer, so callers stay in
242
+ * control of their output folders. Returns `null` when grouping is disabled, matching the
243
+ * per-plugin convention.
244
+ *
245
+ * @param group - The user-supplied group option, or `undefined` to disable grouping.
246
+ *
247
+ * @example
248
+ * ```ts
249
+ * createGroupConfig(group) // shared across every plugin
250
+ * ```
251
+ */
252
+ function createGroupConfig(group) {
253
+ if (!group) return null;
254
+ const defaultName = (ctx) => {
255
+ if (group.type === "path") return `${ctx.group.split("/")[1]}`;
256
+ return camelCase(ctx.group);
257
+ };
258
+ return {
259
+ ...group,
260
+ name: group.name ? group.name : defaultName
261
+ };
262
+ }
263
+ //#endregion
264
+ //#region src/components/McpHandler.tsx
265
+ const requestGroupOrder = [
266
+ "path",
267
+ "query",
268
+ "headers",
269
+ "body"
270
+ ];
271
+ function McpHandler({ name, clientName, node, resolver }) {
272
+ if (!kubb_kit.ast.isHttpOperationNode(node)) return null;
273
+ const { signature, groups } = buildRequestParamsSignature(node, resolver, { isConfigurable: false });
274
+ const paramsSignature = [signature, "request: RequestHandlerExtra<ServerRequest, ServerNotification>"].filter(Boolean).join(", ");
275
+ const callArgs = requestGroupOrder.filter((key) => groups[key]);
276
+ const callConfig = callArgs.length ? `{ ${callArgs.join(", ")} }` : "{}";
277
+ return /* @__PURE__ */ (0, kubb_jsx_jsx_runtime.jsx)(kubb_jsx.File.Source, {
278
+ name,
279
+ isExportable: true,
280
+ isIndexable: true,
281
+ children: /* @__PURE__ */ (0, kubb_jsx_jsx_runtime.jsxs)(kubb_jsx.Function, {
282
+ name,
283
+ async: true,
284
+ export: true,
285
+ params: paramsSignature,
286
+ JSDoc: { comments: buildOperationComments(node) },
287
+ returnType: "Promise<CallToolResult>",
288
+ children: [
289
+ `const res = await ${clientName}(${callConfig})`,
290
+ /* @__PURE__ */ (0, kubb_jsx_jsx_runtime.jsx)("br", {}),
291
+ `return {
292
+ content: [
293
+ {
294
+ type: 'text',
295
+ text: JSON.stringify(res.data)
296
+ }
297
+ ],
298
+ structuredContent: { data: res.data }
299
+ }`
300
+ ]
301
+ })
302
+ });
303
+ }
304
+ //#endregion
305
+ //#region src/utils.ts
306
+ /**
307
+ * Render a group param value — compose individual schemas into `z.object({ ... })`,
308
+ * or use a schema name string directly.
309
+ */
310
+ function zodGroupExpr(entry) {
311
+ if (typeof entry === "string") return entry;
312
+ return `z.object({ ${entry.map((p) => `${JSON.stringify(p.name)}: ${p.schemaName}`).join(", ")} })`;
313
+ }
314
+ /**
315
+ * Convert a SchemaNode type to an inline Zod expression string.
316
+ * Used as fallback when no named zod schema is available for a path parameter.
317
+ */
318
+ function zodExprFromSchemaNode(schema) {
319
+ const baseExpr = (() => {
320
+ if (schema.type === "enum") {
321
+ const rawValues = schema.namedEnumValues?.length ? schema.namedEnumValues.map((v) => v.value) : (schema.enumValues ?? []).filter((v) => v !== null);
322
+ if (rawValues.length > 0 && rawValues.every((v) => typeof v === "string")) return `z.enum([${rawValues.map((v) => JSON.stringify(v)).join(", ")}])`;
323
+ if (rawValues.length > 0) {
324
+ const literals = rawValues.map((v) => `z.literal(${JSON.stringify(v)})`);
325
+ return literals.length === 1 ? literals[0] : `z.union([${literals.join(", ")}])`;
326
+ }
327
+ return "z.string()";
328
+ }
329
+ if (schema.type === "integer") return "z.coerce.number()";
330
+ if (schema.type === "number") return "z.number()";
331
+ if (schema.type === "boolean") return "z.boolean()";
332
+ if (schema.type === "array") return "z.array(z.unknown())";
333
+ return "z.string()";
334
+ })();
335
+ return schema.nullable ? `${baseExpr}.nullable()` : baseExpr;
336
+ }
337
+ //#endregion
338
+ //#region src/components/Server.tsx
339
+ const keysPrinter = (0, _kubb_plugin_ts.functionPrinter)({ mode: "call" });
340
+ function Server({ name, serverName, serverVersion, operations }) {
341
+ const registrations = operations.map(({ tool, mcp, zod, node }) => {
342
+ const { path: pathParams } = getOperationParameters(node);
343
+ const entries = [];
344
+ if (pathParams.length) {
345
+ const pathFields = pathParams.map((p) => {
346
+ const zodParam = zod.pathParams.find((zp) => zp.name === p.name);
347
+ return `${JSON.stringify(p.name)}: ${zodParam ? zodParam.schemaName : zodExprFromSchemaNode(p.schema)}`;
348
+ }).join(", ");
349
+ entries.push({
350
+ key: "path",
351
+ value: `z.object({ ${pathFields} })`
352
+ });
353
+ }
354
+ if (zod.queryParams) entries.push({
355
+ key: "query",
356
+ value: zodGroupExpr(zod.queryParams)
357
+ });
358
+ if (zod.headerParams) entries.push({
359
+ key: "headers",
360
+ value: zodGroupExpr(zod.headerParams)
361
+ });
362
+ if (zod.requestName) entries.push({
363
+ key: "body",
364
+ value: zod.requestName
365
+ });
366
+ const paramsNode = entries.length ? (0, _kubb_plugin_ts.createFunctionParameters)({ params: [(0, _kubb_plugin_ts.createFunctionParameter)({
367
+ name: (0, _kubb_plugin_ts.createObjectBindingPattern)({ elements: entries.map((e) => ({ name: e.key })) }),
368
+ optional: false
369
+ })] }) : null;
370
+ const destructured = paramsNode ? keysPrinter.print(paramsNode) ?? "" : "";
371
+ const inputSchema = entries.length ? `{ ${entries.map((e) => `${e.key}: ${e.value}`).join(", ")} }` : null;
372
+ const outputSchema = zod.responseName;
373
+ const config = [
374
+ tool.title ? `title: ${JSON.stringify(tool.title)}` : null,
375
+ `description: ${JSON.stringify(tool.description)}`,
376
+ outputSchema ? `outputSchema: { data: ${outputSchema} }` : null
377
+ ].filter(Boolean).join(",\n ");
378
+ if (inputSchema) return `
379
+ server.registerTool(${JSON.stringify(tool.name)}, {
380
+ ${config},
381
+ inputSchema: ${inputSchema},
382
+ }, async (${destructured}, request) => {
383
+ return ${mcp.name}(${destructured}, request)
384
+ })
385
+ `;
386
+ return `
387
+ server.registerTool(${JSON.stringify(tool.name)}, {
388
+ ${config},
389
+ }, async (request) => {
390
+ return ${mcp.name}(request)
391
+ })
392
+ `;
393
+ }).filter(Boolean).join("\n");
394
+ return /* @__PURE__ */ (0, kubb_jsx_jsx_runtime.jsxs)(kubb_jsx.File.Source, {
395
+ name,
396
+ isExportable: true,
397
+ isIndexable: true,
398
+ children: [
399
+ /* @__PURE__ */ (0, kubb_jsx_jsx_runtime.jsx)(kubb_jsx.Function, {
400
+ name: "getServer",
401
+ export: true,
402
+ children: `const server = new McpServer({
403
+ name: '${serverName}',
404
+ version: '${serverVersion}',
405
+ })
406
+ ${registrations}
407
+ return server`
408
+ }),
409
+ /* @__PURE__ */ (0, kubb_jsx_jsx_runtime.jsx)(kubb_jsx.Const, {
410
+ name: "server",
411
+ export: true,
412
+ children: "getServer()"
413
+ }),
414
+ /* @__PURE__ */ (0, kubb_jsx_jsx_runtime.jsx)(kubb_jsx.Function, {
415
+ name: "startServer",
416
+ async: true,
417
+ export: true,
418
+ children: `try {
419
+ const transport = new StdioServerTransport()
420
+ await server.connect(transport)
421
+
422
+ } catch (error) {
423
+ console.error('Failed to start server:', error)
424
+ process.exit(1)
425
+ }`
426
+ })
427
+ ]
428
+ });
429
+ }
430
+ //#endregion
431
+ //#region ../../internals/client/src/resolveClient.ts
432
+ /**
433
+ * Resolves which client plugin a consumer (react-query, vue-query, swr, mcp) should call for an
434
+ * operation, shared so the selection rules and diagnostics stay in one place.
435
+ *
436
+ * Every client runtime lives in a dedicated client plugin, so a consumer always calls a registered
437
+ * contract client plugin (plugin-fetch or plugin-axios) and never emits its own inline client:
438
+ *
439
+ * - `contract` — a registered contract client plugin owns the `<op>` functions and the consumer
440
+ * imports and calls them.
441
+ * - `error` — no client plugin is registered, several are registered without a selector, or the
442
+ * requested one is missing.
443
+ *
444
+ * The `client` string selects explicitly (`'fetch'` / `'axios'`); when it is unset a lone registered
445
+ * contract client plugin is picked up automatically.
446
+ */
447
+ const pluginFetchName = "plugin-fetch";
448
+ const pluginAxiosName = "plugin-axios";
449
+ const selectorToPlugin = {
450
+ fetch: pluginFetchName,
451
+ axios: pluginAxiosName
452
+ };
453
+ const contractPlugins = [pluginFetchName, pluginAxiosName];
454
+ /**
455
+ * Applies the `client` resolution rules. See the module comment for the strategy shape.
456
+ *
457
+ * @example
458
+ * ```ts
459
+ * resolveClient({ client: 'fetch', pluginNames: ['plugin-ts', 'plugin-fetch'] })
460
+ * // { kind: 'contract', pluginName: 'plugin-fetch' }
461
+ * ```
462
+ *
463
+ * @example
464
+ * ```ts
465
+ * resolveClient({ client: undefined, pluginNames: ['plugin-ts'] })
466
+ * // { kind: 'error', message: 'No client plugin is registered…' }
467
+ * ```
468
+ */
469
+ function resolveClient(options) {
470
+ const { client, pluginNames } = options;
471
+ const has = (name) => pluginNames.includes(name);
472
+ if (client === "fetch" || client === "axios") {
473
+ const pluginName = selectorToPlugin[client];
474
+ if (!has(pluginName)) return {
475
+ kind: "error",
476
+ 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.`
477
+ };
478
+ return {
479
+ kind: "contract",
480
+ pluginName
481
+ };
482
+ }
483
+ const registered = contractPlugins.filter(has);
484
+ if (registered.length === 1) return {
485
+ kind: "contract",
486
+ pluginName: registered[0]
487
+ };
488
+ if (registered.length > 1) return {
489
+ kind: "error",
490
+ 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.`
491
+ };
492
+ return {
493
+ kind: "error",
494
+ 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."
495
+ };
496
+ }
497
+ /**
498
+ * Resolves the contract client during a consumer plugin's setup hook. Extracts the plugin names
499
+ * from the raw `plugins` config, applies {@link resolveClient}, and throws the diagnostic on a
500
+ * misconfiguration so every consumer fails fast with the same message.
501
+ */
502
+ function resolveContractClient(options) {
503
+ const { client, plugins = [] } = options;
504
+ const resolved = resolveClient({
505
+ client,
506
+ pluginNames: plugins.map((plugin) => plugin.name).filter((name) => Boolean(name))
507
+ });
508
+ if (resolved.kind === "error") throw new Error(resolved.message);
509
+ return resolved;
510
+ }
511
+ //#endregion
512
+ //#region ../../internals/client/src/resolveClientOperation.ts
513
+ /**
514
+ * Resolves the contract client `<op>` a consumer (query hook, MCP handler) imports, by looking up
515
+ * the registered contract client plugin's resolver and output. Works for any contract client plugin
516
+ * (plugin-fetch or plugin-axios). Returns `null` when no contract plugin is in play (the inline
517
+ * path). The plugin injects `.kubb/client.ts` at the global output root, the same path consumers
518
+ * read `RequestConfig` / `ResponseErrorConfig` from.
519
+ */
520
+ function resolveClientOperation(options) {
521
+ const { clientPlugin, driver, node, root, output } = options;
522
+ if (!clientPlugin) return null;
523
+ const resolver = driver.getResolver(clientPlugin.pluginName);
524
+ const plugin = driver.getPlugin(clientPlugin.pluginName);
525
+ const file = resolver.file({
526
+ ...operationFileEntry(node, node.operationId),
527
+ root,
528
+ output: plugin?.options?.output ?? output,
529
+ group: plugin?.options?.group ?? void 0
530
+ });
531
+ return {
532
+ name: resolver.name(node.operationId),
533
+ path: file.path,
534
+ clientPath: node_path.default.resolve(root, ".kubb/client.ts")
535
+ };
536
+ }
537
+ //#endregion
538
+ //#region src/generators/mcpGenerator.tsx
539
+ /**
540
+ * Built-in operation generator for `@kubb/plugin-mcp`. Emits one MCP tool
541
+ * handler per OpenAPI operation. The handler takes the grouped `{ path, query,
542
+ * headers, body }` config and forwards it to the registered client plugin's
543
+ * (`@kubb/plugin-axios` / `@kubb/plugin-fetch`) `<op>` function.
544
+ */
545
+ const mcpGenerator = (0, kubb_kit.defineGenerator)({
546
+ name: "mcp",
547
+ renderer: kubb_jsx.jsxRenderer,
548
+ operation(node, ctx) {
549
+ if (!kubb_kit.ast.isHttpOperationNode(node)) return null;
550
+ const { resolver, driver, root } = ctx;
551
+ const { output, client, group } = ctx.options;
552
+ const pluginTs = driver.getPlugin(_kubb_plugin_ts.pluginTsName);
553
+ if (!pluginTs) return null;
554
+ const tsResolver = driver.getResolver(_kubb_plugin_ts.pluginTsName);
555
+ const contractOp = resolveClientOperation({
556
+ clientPlugin: { pluginName: client.pluginName },
557
+ driver,
558
+ node,
559
+ root,
560
+ output
561
+ });
562
+ if (!contractOp) return null;
563
+ const optionsName = tsResolver.response.options(node);
564
+ const meta = {
565
+ name: resolver.handler.name(node),
566
+ file: resolver.file({
567
+ name: node.operationId,
568
+ extname: ".ts",
569
+ tag: node.tags[0] ?? "default",
570
+ path: node.path,
571
+ root,
572
+ output,
573
+ group: group ?? void 0
574
+ }),
575
+ fileTs: tsResolver.file({
576
+ name: node.operationId,
577
+ extname: ".ts",
578
+ tag: node.tags[0] ?? "default",
579
+ path: node.path,
580
+ root,
581
+ output: pluginTs.options?.output ?? output,
582
+ group: pluginTs.options?.group ?? void 0
583
+ })
584
+ };
585
+ return /* @__PURE__ */ (0, kubb_jsx_jsx_runtime.jsxs)(kubb_jsx.File, {
586
+ baseName: meta.file.baseName,
587
+ path: meta.file.path,
588
+ meta: meta.file.meta,
589
+ children: [
590
+ meta.fileTs && optionsName && /* @__PURE__ */ (0, kubb_jsx_jsx_runtime.jsx)(kubb_jsx.File.Import, {
591
+ name: [optionsName],
592
+ root: meta.file.path,
593
+ path: meta.fileTs.path,
594
+ isTypeOnly: true
595
+ }),
596
+ /* @__PURE__ */ (0, kubb_jsx_jsx_runtime.jsx)(kubb_jsx.File.Import, {
597
+ name: [
598
+ "CallToolResult",
599
+ "ServerNotification",
600
+ "ServerRequest"
601
+ ],
602
+ path: "@modelcontextprotocol/sdk/types",
603
+ isTypeOnly: true
604
+ }),
605
+ /* @__PURE__ */ (0, kubb_jsx_jsx_runtime.jsx)(kubb_jsx.File.Import, {
606
+ name: ["RequestHandlerExtra"],
607
+ path: "@modelcontextprotocol/sdk/shared/protocol",
608
+ isTypeOnly: true
609
+ }),
610
+ /* @__PURE__ */ (0, kubb_jsx_jsx_runtime.jsx)(kubb_jsx.File.Import, {
611
+ name: [contractOp.name],
612
+ root: meta.file.path,
613
+ path: contractOp.path
614
+ }),
615
+ /* @__PURE__ */ (0, kubb_jsx_jsx_runtime.jsx)(McpHandler, {
616
+ name: meta.name,
617
+ clientName: contractOp.name,
618
+ node,
619
+ resolver: tsResolver
620
+ })
621
+ ]
622
+ });
623
+ }
624
+ });
625
+ //#endregion
626
+ //#region src/generators/serverGenerator.tsx
627
+ /**
628
+ * Default v5 server generator for `@kubb/plugin-mcp`.
629
+ *
630
+ * Uses individual zod schemas for each param (e.g. `createPetsPathUuidSchema`, `createPetsQueryOffsetSchema`)
631
+ * and `resolveResponseStatusName` for per-status response schemas.
632
+ * Query and header params are composed into `z.object({ ... })` from individual schemas.
633
+ */
634
+ const serverGenerator = (0, kubb_kit.defineGenerator)({
635
+ name: "operations",
636
+ renderer: kubb_jsx.jsxRenderer,
637
+ operations(nodes, ctx) {
638
+ const { config, resolver, plugin, driver, root } = ctx;
639
+ const { output, group } = ctx.options;
640
+ const pluginZod = driver.getPlugin(_kubb_plugin_zod.pluginZodName);
641
+ if (!pluginZod) return;
642
+ const zodResolver = driver.getResolver(_kubb_plugin_zod.pluginZodName);
643
+ const name = "server";
644
+ const serverFile = {
645
+ baseName: "server.ts",
646
+ path: node_path.default.resolve(root, output.path, "server.ts"),
647
+ meta: { pluginName: plugin.name }
648
+ };
649
+ const jsonFile = {
650
+ baseName: ".mcp.json",
651
+ path: node_path.default.resolve(root, output.path, ".mcp.json"),
652
+ meta: { pluginName: plugin.name }
653
+ };
654
+ const operationsMapped = nodes.filter(kubb_kit.ast.isHttpOperationNode).map((node) => {
655
+ const { path: pathParams, query: queryParams, header: headerParams } = getOperationParameters(node);
656
+ const mcpFile = resolver.file({
657
+ name: node.operationId,
658
+ extname: ".ts",
659
+ tag: node.tags[0] ?? "default",
660
+ path: node.path,
661
+ root,
662
+ output,
663
+ group: group ?? void 0
664
+ });
665
+ const zodFile = zodResolver.file({
666
+ name: node.operationId,
667
+ extname: ".ts",
668
+ tag: node.tags[0] ?? "default",
669
+ path: node.path,
670
+ root,
671
+ output: pluginZod.options?.output ?? output,
672
+ group: pluginZod.options?.group ?? void 0
673
+ });
674
+ const requestName = node.requestBody?.content?.[0]?.schema ? zodResolver.response.body(node) : null;
675
+ const successStatus = findSuccessStatusCode(node.responses);
676
+ const responseName = successStatus ? zodResolver.response.status(node, successStatus) : null;
677
+ const resolveParams = (params) => params.map((p) => ({
678
+ name: p.name,
679
+ schemaName: zodResolver.param.name(node, p)
680
+ }));
681
+ return {
682
+ tool: {
683
+ name: node.operationId,
684
+ title: node.summary || void 0,
685
+ description: node.description || `Make a ${node.method.toUpperCase()} request to ${node.path}`
686
+ },
687
+ mcp: {
688
+ name: resolver.handler.name(node),
689
+ file: mcpFile
690
+ },
691
+ zod: {
692
+ pathParams: resolveParams(pathParams),
693
+ queryParams: queryParams.length ? resolveParams(queryParams) : null,
694
+ headerParams: headerParams.length ? resolveParams(headerParams) : null,
695
+ requestName,
696
+ responseName,
697
+ file: zodFile
698
+ },
699
+ node
700
+ };
701
+ });
702
+ const imports = operationsMapped.flatMap(({ mcp, zod }) => {
703
+ const zodNames = [
704
+ ...zod.pathParams.map((p) => p.schemaName),
705
+ ...(zod.queryParams ?? []).map((p) => p.schemaName),
706
+ ...(zod.headerParams ?? []).map((p) => p.schemaName),
707
+ zod.requestName,
708
+ zod.responseName
709
+ ].filter((name) => Boolean(name));
710
+ const uniqueNames = [...new Set(zodNames)].sort();
711
+ return [/* @__PURE__ */ (0, kubb_jsx_jsx_runtime.jsx)(kubb_jsx.File.Import, {
712
+ name: [mcp.name],
713
+ root: serverFile.path,
714
+ path: mcp.file.path
715
+ }, mcp.name), uniqueNames.length > 0 && /* @__PURE__ */ (0, kubb_jsx_jsx_runtime.jsx)(kubb_jsx.File.Import, {
716
+ name: uniqueNames,
717
+ root: serverFile.path,
718
+ path: zod.file.path
719
+ }, `zod-${mcp.name}`)].filter(Boolean);
720
+ });
721
+ return /* @__PURE__ */ (0, kubb_jsx_jsx_runtime.jsxs)(kubb_jsx_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, kubb_jsx_jsx_runtime.jsxs)(kubb_jsx.File, {
722
+ baseName: serverFile.baseName,
723
+ path: serverFile.path,
724
+ meta: serverFile.meta,
725
+ banner: resolver.default.banner(ctx.meta, {
726
+ output,
727
+ config,
728
+ file: {
729
+ path: serverFile.path,
730
+ baseName: serverFile.baseName
731
+ }
732
+ }),
733
+ footer: resolver.default.footer(ctx.meta, {
734
+ output,
735
+ config,
736
+ file: {
737
+ path: serverFile.path,
738
+ baseName: serverFile.baseName
739
+ }
740
+ }),
741
+ children: [
742
+ /* @__PURE__ */ (0, kubb_jsx_jsx_runtime.jsx)(kubb_jsx.File.Import, {
743
+ name: ["McpServer"],
744
+ path: "@modelcontextprotocol/sdk/server/mcp"
745
+ }),
746
+ /* @__PURE__ */ (0, kubb_jsx_jsx_runtime.jsx)(kubb_jsx.File.Import, {
747
+ name: ["z"],
748
+ path: "zod"
749
+ }),
750
+ /* @__PURE__ */ (0, kubb_jsx_jsx_runtime.jsx)(kubb_jsx.File.Import, {
751
+ name: ["StdioServerTransport"],
752
+ path: "@modelcontextprotocol/sdk/server/stdio"
753
+ }),
754
+ imports,
755
+ /* @__PURE__ */ (0, kubb_jsx_jsx_runtime.jsx)(Server, {
756
+ name,
757
+ serverName: ctx.meta.title ?? "server",
758
+ serverVersion: ctx.meta.version ?? "0.0.0",
759
+ operations: operationsMapped
760
+ })
761
+ ]
762
+ }), /* @__PURE__ */ (0, kubb_jsx_jsx_runtime.jsx)(kubb_jsx.File, {
763
+ baseName: jsonFile.baseName,
764
+ path: jsonFile.path,
765
+ meta: jsonFile.meta,
766
+ children: /* @__PURE__ */ (0, kubb_jsx_jsx_runtime.jsx)(kubb_jsx.File.Source, {
767
+ name,
768
+ children: `
769
+ {
770
+ "mcpServers": {
771
+ "${ctx.meta.title || "server"}": {
772
+ "type": "stdio",
773
+ "command": "npx",
774
+ "args": ["tsx", "${node_path.default.relative(node_path.default.dirname(jsonFile.path), serverFile.path)}"]
775
+ }
776
+ }
777
+ }
778
+ `
779
+ })
780
+ })] });
781
+ }
782
+ });
783
+ //#endregion
784
+ //#region src/resolvers/resolverMcp.ts
785
+ /**
786
+ * Default resolver used by `@kubb/plugin-mcp`. Decides the names and file
787
+ * paths for every generated MCP tool handler. Function names get a `Handler`
788
+ * suffix so an operation `addPet` becomes `addPetHandler`.
789
+ *
790
+ * @example Resolve a handler name
791
+ * ```ts
792
+ * import { resolverMcp } from '@kubb/plugin-mcp'
793
+ *
794
+ * resolverMcp.name('addPet') // 'addPetHandler'
795
+ * ```
796
+ */
797
+ const resolverMcp = (0, kubb_kit.createResolver)({
798
+ pluginName: "plugin-mcp",
799
+ name(name) {
800
+ return camelCase(name, { suffix: "handler" });
801
+ },
802
+ handler: { name(node) {
803
+ return this.name(node.operationId);
804
+ } }
805
+ });
806
+ //#endregion
14
807
  //#region src/plugin.ts
808
+ /**
809
+ * Canonical plugin name for `@kubb/plugin-mcp`. Used for driver lookups and
810
+ * cross-plugin dependency references.
811
+ */
15
812
  const pluginMcpName = "plugin-mcp";
16
- const pluginMcp = (0, _kubb_core.createPlugin)((options) => {
813
+ /**
814
+ * Generates a Model Context Protocol (MCP) server from an OpenAPI spec. Every
815
+ * operation becomes a typed MCP tool that AI assistants (Claude Desktop, Claude
816
+ * Code, MCP-compatible clients) can call directly. Each handler calls a contract
817
+ * client `<op>` from a registered `@kubb/plugin-axios` or `@kubb/plugin-fetch`.
818
+ *
819
+ * @example
820
+ * ```ts
821
+ * import { defineConfig } from 'kubb/config'
822
+ * import { pluginTs } from '@kubb/plugin-ts'
823
+ * import { pluginZod } from '@kubb/plugin-zod'
824
+ * import { pluginMcp } from '@kubb/plugin-mcp'
825
+ *
826
+ * export default defineConfig({
827
+ * input: './petStore.yaml',
828
+ * output: { path: './src/gen' },
829
+ * plugins: [
830
+ * pluginTs(),
831
+ * pluginZod(),
832
+ * pluginMcp({
833
+ * output: { path: './mcp' },
834
+ * baseURL: 'https://petstore.swagger.io/v2',
835
+ * }),
836
+ * ],
837
+ * })
838
+ * ```
839
+ */
840
+ const pluginMcp = (0, kubb_kit.definePlugin)((options) => {
17
841
  const { output = {
18
842
  path: "mcp",
19
- barrelType: "named"
20
- }, group, exclude = [], include, override = [], transformers = {}, generators = [require_generators.mcpGenerator, require_generators.serverGenerator].filter(Boolean), contentType, paramsCasing, client } = options;
21
- const clientName = client?.client ?? "axios";
22
- const clientImportPath = client?.importPath ?? (!client?.bundle ? `@kubb/plugin-client/clients/${clientName}` : void 0);
843
+ barrel: { type: "named" }
844
+ }, group, exclude = [], include, override = [], client, resolver: userResolver, macros: userMacros } = options;
845
+ const groupConfig = createGroupConfig(group);
23
846
  return {
24
847
  name: pluginMcpName,
25
- options: {
26
- output,
27
- group,
28
- paramsCasing,
29
- client: {
30
- client: clientName,
31
- clientType: client?.clientType ?? "function",
32
- importPath: clientImportPath,
33
- dataReturnType: client?.dataReturnType ?? "data",
34
- bundle: client?.bundle,
35
- baseURL: client?.baseURL,
36
- paramsCasing: client?.paramsCasing
37
- }
38
- },
39
- pre: [
40
- _kubb_plugin_oas.pluginOasName,
41
- _kubb_plugin_ts.pluginTsName,
42
- _kubb_plugin_zod.pluginZodName
43
- ].filter(Boolean),
44
- resolvePath(baseName, pathMode, options) {
45
- const root = node_path.default.resolve(this.config.root, this.config.output.path);
46
- if ((pathMode ?? (0, _kubb_core.getMode)(node_path.default.resolve(root, output.path))) === "single")
47
- /**
48
- * when output is a file then we will always append to the same file(output file), see fileManager.addOrAppend
49
- * Other plugins then need to call addOrAppend instead of just add from the fileManager class
50
- */
51
- return node_path.default.resolve(root, output.path);
52
- if (group && (options?.group?.path || options?.group?.tag)) {
53
- const groupName = group?.name ? group.name : (ctx) => {
54
- if (group?.type === "path") return `${ctx.group.split("/")[1]}`;
55
- return `${require_Server.camelCase(ctx.group)}Requests`;
56
- };
57
- return node_path.default.resolve(root, output.path, groupName({ group: group.type === "path" ? options.group.path : options.group.tag }), baseName);
58
- }
59
- return node_path.default.resolve(root, output.path, baseName);
60
- },
61
- resolveName(name, type) {
62
- const resolvedName = require_Server.camelCase(name, { isFile: type === "file" });
63
- if (type) return transformers?.name?.(resolvedName, type) || resolvedName;
64
- return resolvedName;
65
- },
66
- async install() {
67
- const root = node_path.default.resolve(this.config.root, this.config.output.path);
68
- const mode = (0, _kubb_core.getMode)(node_path.default.resolve(root, output.path));
69
- const oas = await this.getOas();
70
- const baseURL = await this.getBaseURL();
71
- if (baseURL) this.plugin.options.client.baseURL = baseURL;
72
- const hasClientPlugin = !!this.driver.getPluginByName(_kubb_plugin_client.pluginClientName);
73
- if (this.plugin.options.client.bundle && !hasClientPlugin && !this.plugin.options.client.importPath) await this.addFile({
74
- baseName: "fetch.ts",
75
- path: node_path.default.resolve(root, ".kubb/fetch.ts"),
76
- sources: [{
77
- name: "fetch",
78
- value: this.plugin.options.client.client === "fetch" ? _kubb_plugin_client_templates_clients_fetch_source.source : _kubb_plugin_client_templates_clients_axios_source.source,
79
- isExportable: true,
80
- isIndexable: true
81
- }],
82
- imports: [],
83
- exports: []
84
- });
85
- if (!hasClientPlugin) await this.addFile({
86
- baseName: "config.ts",
87
- path: node_path.default.resolve(root, ".kubb/config.ts"),
88
- sources: [{
89
- name: "config",
90
- value: _kubb_plugin_client_templates_config_source.source,
91
- isExportable: false,
92
- isIndexable: false
93
- }],
94
- imports: [],
95
- exports: []
96
- });
97
- const files = await new _kubb_plugin_oas.OperationGenerator(this.plugin.options, {
98
- fabric: this.fabric,
99
- oas,
100
- driver: this.driver,
101
- events: this.events,
102
- plugin: this.plugin,
103
- contentType,
848
+ options,
849
+ dependencies: [_kubb_plugin_ts.pluginTsName, _kubb_plugin_zod.pluginZodName],
850
+ hooks: { "kubb:plugin:setup"(ctx) {
851
+ const resolver = userResolver ? kubb_kit.Resolver.merge(resolverMcp, userResolver) : resolverMcp;
852
+ ctx.setOptions({
853
+ output,
104
854
  exclude,
105
855
  include,
106
856
  override,
107
- mode
108
- }).build(...generators);
109
- await this.upsertFile(...files);
110
- const barrelFiles = await (0, _kubb_core.getBarrelFiles)(this.fabric.files, {
111
- type: output.barrelType ?? "named",
112
- root,
113
- output,
114
- meta: { pluginName: this.plugin.name }
857
+ group: groupConfig,
858
+ client: resolveContractClient({
859
+ client,
860
+ plugins: ctx.config.plugins
861
+ }),
862
+ resolver
115
863
  });
116
- await this.upsertFile(...barrelFiles);
117
- }
864
+ ctx.setResolver(resolver);
865
+ if (userMacros?.length) ctx.setMacros(userMacros);
866
+ ctx.addGenerator(mcpGenerator, serverGenerator);
867
+ } }
118
868
  };
119
869
  });
120
870
  //#endregion
871
+ exports.McpHandler = McpHandler;
872
+ exports.Server = Server;
873
+ exports.default = pluginMcp;
874
+ exports.mcpGenerator = mcpGenerator;
121
875
  exports.pluginMcp = pluginMcp;
122
876
  exports.pluginMcpName = pluginMcpName;
877
+ exports.resolverMcp = resolverMcp;
878
+ exports.serverGenerator = serverGenerator;
123
879
 
124
880
  //# sourceMappingURL=index.cjs.map