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