@kubb/plugin-mcp 5.0.0-beta.15 → 5.0.0-beta.25

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kubb/plugin-mcp",
3
- "version": "5.0.0-beta.15",
3
+ "version": "5.0.0-beta.25",
4
4
  "description": "Generate Model Context Protocol (MCP) tool definitions from your OpenAPI specification. Expose your REST APIs as AI-callable tools for LLMs, Claude, ChatGPT, and other AI assistants.",
5
5
  "keywords": [
6
6
  "ai",
@@ -47,18 +47,18 @@
47
47
  "registry": "https://registry.npmjs.org/"
48
48
  },
49
49
  "dependencies": {
50
- "@kubb/core": "5.0.0-beta.19",
51
- "@kubb/renderer-jsx": "5.0.0-beta.19",
52
- "@kubb/plugin-client": "5.0.0-beta.15",
53
- "@kubb/plugin-ts": "5.0.0-beta.15",
54
- "@kubb/plugin-zod": "5.0.0-beta.15"
50
+ "@kubb/core": "5.0.0-beta.25",
51
+ "@kubb/renderer-jsx": "5.0.0-beta.25",
52
+ "@kubb/plugin-client": "5.0.0-beta.25",
53
+ "@kubb/plugin-ts": "5.0.0-beta.25",
54
+ "@kubb/plugin-zod": "5.0.0-beta.25"
55
55
  },
56
56
  "devDependencies": {
57
57
  "@internals/shared": "0.0.0",
58
58
  "@internals/utils": "0.0.0"
59
59
  },
60
60
  "peerDependencies": {
61
- "@kubb/renderer-jsx": "5.0.0-beta.19"
61
+ "@kubb/renderer-jsx": "5.0.0-beta.25"
62
62
  },
63
63
  "size-limit": [
64
64
  {
@@ -23,7 +23,7 @@ type Props = {
23
23
  /**
24
24
  * Base URL prepended to every generated request URL.
25
25
  */
26
- baseURL: string | undefined
26
+ baseURL: string | null | undefined
27
27
  /**
28
28
  * Return type when calling fetch.
29
29
  * - 'data' returns response data only.
@@ -57,7 +57,7 @@ export function McpHandler({ name, node, resolver, baseURL, dataReturnType, para
57
57
  const { query: queryParams, header: headerParams } = getOperationParameters(node, { paramsCasing })
58
58
  const { path: originalPathParams, query: originalQueryParams, header: originalHeaderParams } = getOperationParameters(node)
59
59
 
60
- const requestName = node.requestBody?.content?.[0]?.schema ? resolver.resolveDataName(node) : undefined
60
+ const requestName = node.requestBody?.content?.[0]?.schema ? resolver.resolveDataName(node) : null
61
61
  const responseName = resolver.resolveResponseName(node)
62
62
 
63
63
  const errorResponses = node.responses.filter((r) => Number(r.statusCode) >= 400).map((r) => resolver.resolveResponseStatusName(node, r.statusCode))
@@ -77,15 +77,15 @@ export function McpHandler({ name, node, resolver, baseURL, dataReturnType, para
77
77
  ? `${baseParamsSignature}, request: RequestHandlerExtra<ServerRequest, ServerNotification>`
78
78
  : 'request: RequestHandlerExtra<ServerRequest, ServerNotification>'
79
79
 
80
- const pathParamsMapping = paramsCasing ? buildTransformedParamsMapping(originalPathParams, camelCase) : undefined
81
- const queryParamsMapping = paramsCasing ? buildTransformedParamsMapping(originalQueryParams, camelCase) : undefined
82
- const headerParamsMapping = paramsCasing ? buildTransformedParamsMapping(originalHeaderParams, camelCase) : undefined
80
+ const pathParamsMapping = paramsCasing ? buildTransformedParamsMapping(originalPathParams, camelCase) : null
81
+ const queryParamsMapping = paramsCasing ? buildTransformedParamsMapping(originalQueryParams, camelCase) : null
82
+ const headerParamsMapping = paramsCasing ? buildTransformedParamsMapping(originalHeaderParams, camelCase) : null
83
83
 
84
84
  const contentTypeHeader =
85
- contentType && contentType !== 'application/json' && contentType !== 'multipart/form-data' ? `'Content-Type': '${contentType}'` : undefined
86
- const headers = [headerParams.length ? (headerParamsMapping ? '...mappedHeaders' : '...headers') : undefined, contentTypeHeader].filter(Boolean)
85
+ contentType && contentType !== 'application/json' && contentType !== 'multipart/form-data' ? `'Content-Type': '${contentType}'` : null
86
+ const headers = [headerParams.length ? (headerParamsMapping ? '...mappedHeaders' : '...headers') : null, contentTypeHeader].filter(Boolean)
87
87
 
88
- const fetchConfig: string[] = []
88
+ const fetchConfig: Array<string> = []
89
89
  fetchConfig.push(`method: ${JSON.stringify(node.method.toUpperCase())}`)
90
90
  fetchConfig.push(`url: ${urlPath.template}`)
91
91
  if (baseURL) fetchConfig.push(`baseURL: \`${baseURL}\``)
@@ -43,13 +43,13 @@ type Props = {
43
43
  /**
44
44
  * Query params — individual schemas to compose into `z.object({ ... })`.
45
45
  */
46
- queryParams?: string | Array<ZodParam>
46
+ queryParams?: string | Array<ZodParam> | null
47
47
  /**
48
48
  * Header params — individual schemas to compose into `z.object({ ... })`.
49
49
  */
50
- headerParams?: string | Array<ZodParam>
51
- requestName?: string
52
- responseName?: string
50
+ headerParams?: string | Array<ZodParam> | null
51
+ requestName?: string | null
52
+ responseName?: string | null
53
53
  }
54
54
  node: ast.OperationNode
55
55
  }>
@@ -104,10 +104,10 @@ export function Server({ name, serverName, serverVersion, paramsCasing, operatio
104
104
  }),
105
105
  ],
106
106
  })
107
- : undefined
107
+ : null
108
108
 
109
109
  const destructured = paramsNode ? (keysPrinter.print(paramsNode) ?? '') : ''
110
- const inputSchema = entries.length ? `{ ${entries.map((e) => `${e.key}: ${e.value}`).join(', ')} }` : undefined
110
+ const inputSchema = entries.length ? `{ ${entries.map((e) => `${e.key}: ${e.value}`).join(', ')} }` : null
111
111
  const outputSchema = zod.responseName
112
112
 
113
113
  const config = [
@@ -6,6 +6,12 @@ import { File, jsxRendererSync } from '@kubb/renderer-jsx'
6
6
  import { McpHandler } from '../components/McpHandler.tsx'
7
7
  import type { PluginMcp } from '../types.ts'
8
8
 
9
+ /**
10
+ * Built-in operation generator for `@kubb/plugin-mcp`. Emits one MCP tool
11
+ * handler per OpenAPI operation, wiring the input Zod schema, the HTTP call,
12
+ * and the response shape into a single function that an MCP server can
13
+ * register as a callable tool.
14
+ */
9
15
  export const mcpGenerator = defineGenerator<PluginMcp>({
10
16
  name: 'mcp',
11
17
  renderer: jsxRendererSync,
@@ -25,13 +31,16 @@ export const mcpGenerator = defineGenerator<PluginMcp>({
25
31
 
26
32
  const meta = {
27
33
  name: resolver.resolveHandlerName(node),
28
- file: resolver.resolveFile({ name: node.operationId, extname: '.ts', tag: node.tags[0] ?? 'default', path: node.path }, { root, output, group }),
34
+ file: resolver.resolveFile(
35
+ { name: node.operationId, extname: '.ts', tag: node.tags[0] ?? 'default', path: node.path },
36
+ { root, output, group: group ?? undefined },
37
+ ),
29
38
  fileTs: tsResolver.resolveFile(
30
39
  { name: node.operationId, extname: '.ts', tag: node.tags[0] ?? 'default', path: node.path },
31
40
  {
32
41
  root,
33
42
  output: pluginTs.options?.output ?? output,
34
- group: pluginTs.options?.group,
43
+ group: pluginTs.options?.group ?? undefined,
35
44
  },
36
45
  ),
37
46
  } as const
@@ -17,7 +17,7 @@ export const serverGenerator = defineGenerator<PluginMcp>({
17
17
  name: 'operations',
18
18
  renderer: jsxRendererSync,
19
19
  operations(nodes, ctx) {
20
- const { config, resolver, plugin, driver, root, inputNode } = ctx
20
+ const { config, resolver, plugin, driver, root } = ctx
21
21
  const { output, paramsCasing, group } = ctx.options
22
22
 
23
23
  const pluginZod = driver.getPlugin(pluginZodName)
@@ -46,20 +46,23 @@ export const serverGenerator = defineGenerator<PluginMcp>({
46
46
  const operationsMapped = nodes.map((node) => {
47
47
  const { path: pathParams, query: queryParams, header: headerParams } = getOperationParameters(node, { paramsCasing })
48
48
 
49
- const mcpFile = resolver.resolveFile({ name: node.operationId, extname: '.ts', tag: node.tags[0] ?? 'default', path: node.path }, { root, output, group })
49
+ const mcpFile = resolver.resolveFile(
50
+ { name: node.operationId, extname: '.ts', tag: node.tags[0] ?? 'default', path: node.path },
51
+ { root, output, group: group ?? undefined },
52
+ )
50
53
 
51
54
  const zodFile = zodResolver.resolveFile(
52
55
  { name: node.operationId, extname: '.ts', tag: node.tags[0] ?? 'default', path: node.path },
53
56
  {
54
57
  root,
55
58
  output: pluginZod.options?.output ?? output,
56
- group: pluginZod.options?.group,
59
+ group: pluginZod.options?.group ?? undefined,
57
60
  },
58
61
  )
59
62
 
60
- const requestName = node.requestBody?.content?.[0]?.schema ? zodResolver.resolveDataName(node) : undefined
63
+ const requestName = node.requestBody?.content?.[0]?.schema ? zodResolver.resolveDataName(node) : null
61
64
  const successStatus = findSuccessStatusCode(node.responses)
62
- const responseName = successStatus ? zodResolver.resolveResponseStatusName(node, successStatus) : undefined
65
+ const responseName = successStatus ? zodResolver.resolveResponseStatusName(node, successStatus) : null
63
66
 
64
67
  const resolveParams = (params: typeof pathParams) => params.map((p) => ({ name: p.name, schemaName: zodResolver.resolveParamName(node, p) }))
65
68
 
@@ -75,8 +78,8 @@ export const serverGenerator = defineGenerator<PluginMcp>({
75
78
  },
76
79
  zod: {
77
80
  pathParams: resolveParams(pathParams),
78
- queryParams: queryParams.length ? resolveParams(queryParams) : undefined,
79
- headerParams: headerParams.length ? resolveParams(headerParams) : undefined,
81
+ queryParams: queryParams.length ? resolveParams(queryParams) : null,
82
+ headerParams: headerParams.length ? resolveParams(headerParams) : null,
80
83
  requestName,
81
84
  responseName,
82
85
  file: zodFile,
@@ -108,8 +111,8 @@ export const serverGenerator = defineGenerator<PluginMcp>({
108
111
  baseName={serverFile.baseName}
109
112
  path={serverFile.path}
110
113
  meta={serverFile.meta}
111
- banner={resolver.resolveBanner(inputNode, { output, config })}
112
- footer={resolver.resolveFooter(inputNode, { output, config })}
114
+ banner={resolver.resolveBanner(ctx.meta, { output, config })}
115
+ footer={resolver.resolveFooter(ctx.meta, { output, config })}
113
116
  >
114
117
  <File.Import name={['McpServer']} path={'@modelcontextprotocol/sdk/server/mcp'} />
115
118
  <File.Import name={['z']} path={'zod'} />
@@ -118,8 +121,8 @@ export const serverGenerator = defineGenerator<PluginMcp>({
118
121
  {imports}
119
122
  <Server
120
123
  name={name}
121
- serverName={inputNode.meta?.title ?? 'server'}
122
- serverVersion={inputNode.meta?.version ?? '0.0.0'}
124
+ serverName={ctx.meta.title ?? 'server'}
125
+ serverVersion={ctx.meta.version ?? '0.0.0'}
123
126
  paramsCasing={paramsCasing}
124
127
  operations={operationsMapped}
125
128
  />
@@ -130,7 +133,7 @@ export const serverGenerator = defineGenerator<PluginMcp>({
130
133
  {`
131
134
  {
132
135
  "mcpServers": {
133
- "${inputNode.meta?.title || 'server'}": {
136
+ "${ctx.meta.title || 'server'}": {
134
137
  "type": "stdio",
135
138
  "command": "npx",
136
139
  "args": ["tsx", "${path.relative(path.dirname(jsonFile.path), serverFile.path)}"]
package/src/plugin.ts CHANGED
@@ -13,8 +13,40 @@ import { serverGenerator } from './generators/serverGenerator.tsx'
13
13
  import { resolverMcp } from './resolvers/resolverMcp.ts'
14
14
  import type { PluginMcp } from './types.ts'
15
15
 
16
+ /**
17
+ * Canonical plugin name for `@kubb/plugin-mcp`. Used for driver lookups and
18
+ * cross-plugin dependency references.
19
+ */
16
20
  export const pluginMcpName = 'plugin-mcp' satisfies PluginMcp['name']
17
21
 
22
+ /**
23
+ * Generates a Model Context Protocol (MCP) server from an OpenAPI spec. Every
24
+ * operation becomes a typed MCP tool that AI assistants (Claude Desktop, Claude
25
+ * Code, MCP-compatible clients) can call directly.
26
+ *
27
+ * @example
28
+ * ```ts
29
+ * import { defineConfig } from 'kubb'
30
+ * import { pluginTs } from '@kubb/plugin-ts'
31
+ * import { pluginClient } from '@kubb/plugin-client'
32
+ * import { pluginZod } from '@kubb/plugin-zod'
33
+ * import { pluginMcp } from '@kubb/plugin-mcp'
34
+ *
35
+ * export default defineConfig({
36
+ * input: { path: './petStore.yaml' },
37
+ * output: { path: './src/gen' },
38
+ * plugins: [
39
+ * pluginTs(),
40
+ * pluginClient(),
41
+ * pluginZod(),
42
+ * pluginMcp({
43
+ * output: { path: './mcp' },
44
+ * client: { baseURL: 'https://petstore.swagger.io/v2' },
45
+ * }),
46
+ * ],
47
+ * })
48
+ * ```
49
+ */
18
50
  export const pluginMcp = definePlugin<PluginMcp>((options) => {
19
51
  const {
20
52
  output = { path: 'mcp', barrelType: 'named' },
@@ -44,7 +76,7 @@ export const pluginMcp = definePlugin<PluginMcp>((options) => {
44
76
  return `${camelCase(ctx.group)}Requests`
45
77
  },
46
78
  } satisfies Group)
47
- : undefined
79
+ : null
48
80
 
49
81
  return {
50
82
  name: pluginMcpName,
@@ -3,12 +3,16 @@ import { defineResolver } from '@kubb/core'
3
3
  import type { PluginMcp } from '../types.ts'
4
4
 
5
5
  /**
6
- * Naming convention resolver for MCP plugin.
6
+ * Default resolver used by `@kubb/plugin-mcp`. Decides the names and file
7
+ * paths for every generated MCP tool handler. Function names get a `Handler`
8
+ * suffix so an operation `addPet` becomes `addPetHandler`.
7
9
  *
8
- * Provides default naming helpers using camelCase with a `handler` suffix for functions.
10
+ * @example Resolve a handler name
11
+ * ```ts
12
+ * import { resolverMcp } from '@kubb/plugin-mcp'
9
13
  *
10
- * @example
11
- * `resolverMcp.default('addPet', 'function') // → 'addPetHandler'`
14
+ * resolverMcp.default('addPet', 'function') // 'addPetHandler'
15
+ * ```
12
16
  */
13
17
  export const resolverMcp = defineResolver<PluginMcp>(() => ({
14
18
  name: 'default',
package/src/types.ts CHANGED
@@ -24,44 +24,50 @@ export type ResolverMcp = Resolver & {
24
24
 
25
25
  export type Options = {
26
26
  /**
27
- * Specify the export location for the files and define the behavior of the output.
28
- * @default { path: 'mcp', barrelType: 'named' }
27
+ * Where the generated MCP tool handlers are written and how they are exported.
28
+ *
29
+ * @default { path: 'mcp', barrel: { type: 'named' } }
29
30
  */
30
31
  output?: Output
31
32
  /**
32
- * Client configuration for HTTP request generation.
33
+ * HTTP client used by each MCP handler to call the underlying API. Mirrors a
34
+ * subset of `pluginClient` options.
33
35
  */
34
36
  client?: ClientImportPath & Pick<PluginClient['options'], 'clientType' | 'dataReturnType' | 'baseURL' | 'bundle' | 'paramsCasing'>
35
37
  /**
36
- * Apply casing to parameter names to match your configuration.
38
+ * Rename parameter properties in the generated handlers. The HTTP layer still
39
+ * uses the original spec names; Kubb writes the mapping for you.
40
+ *
41
+ * @note Must match the value of `paramsCasing` on `@kubb/plugin-ts`.
37
42
  */
38
43
  paramsCasing?: 'camelcase'
39
44
  /**
40
- * Group the MCP requests based on the provided name.
45
+ * Split generated files into subfolders based on the operation's tag.
41
46
  */
42
47
  group?: Group
43
48
  /**
44
- * Tags, operations, or paths to exclude from generation.
49
+ * Skip operations matching at least one entry in the list.
45
50
  */
46
51
  exclude?: Array<Exclude>
47
52
  /**
48
- * Tags, operations, or paths to include in generation.
53
+ * Restrict generation to operations matching at least one entry in the list.
49
54
  */
50
55
  include?: Array<Include>
51
56
  /**
52
- * Override options for specific tags, operations, or paths.
57
+ * Apply a different options object to operations matching a pattern.
53
58
  */
54
59
  override?: Array<Override<ResolvedOptions>>
55
60
  /**
56
- * Override naming conventions for function names and types.
61
+ * Override how handler names and file paths are built. Methods you omit fall
62
+ * back to the default `resolverMcp`.
57
63
  */
58
64
  resolver?: Partial<ResolverMcp> & ThisType<ResolverMcp>
59
65
  /**
60
- * AST visitor to transform generated nodes.
66
+ * AST visitor applied to each operation node before printing.
61
67
  */
62
68
  transformer?: ast.Visitor
63
69
  /**
64
- * Additional generators alongside the default generators.
70
+ * Custom generators that run alongside the built-in MCP generators.
65
71
  */
66
72
  generators?: Array<Generator<PluginMcp>>
67
73
  }
@@ -71,7 +77,7 @@ type ResolvedOptions = {
71
77
  exclude: Array<Exclude>
72
78
  include: Array<Include> | undefined
73
79
  override: Array<Override<ResolvedOptions>>
74
- group: Group | undefined
80
+ group: Group | null
75
81
  client: Pick<PluginClient['options'], 'client' | 'clientType' | 'dataReturnType' | 'importPath' | 'baseURL' | 'bundle' | 'paramsCasing'>
76
82
  paramsCasing: Options['paramsCasing']
77
83
  resolver: ResolverMcp
package/src/utils.ts CHANGED
@@ -22,43 +22,27 @@ export function zodGroupExpr(entry: string | Array<ZodParam>): string {
22
22
  * Used as fallback when no named zod schema is available for a path parameter.
23
23
  */
24
24
  export function zodExprFromSchemaNode(schema: ast.SchemaNode): string {
25
- let expr: string
26
- switch (schema.type) {
27
- case 'enum': {
28
- // namedEnumValues takes priority over enumValues
25
+ const baseExpr = (() => {
26
+ if (schema.type === 'enum') {
29
27
  const rawValues: Array<string | number | boolean> = schema.namedEnumValues?.length
30
28
  ? schema.namedEnumValues.map((v) => v.value)
31
29
  : (schema.enumValues ?? []).filter((v): v is string | number | boolean => v !== null)
32
30
 
33
31
  if (rawValues.length > 0 && rawValues.every((v) => typeof v === 'string')) {
34
- expr = `z.enum([${rawValues.map((v) => JSON.stringify(v)).join(', ')}])`
35
- } else if (rawValues.length > 0) {
32
+ return `z.enum([${rawValues.map((v) => JSON.stringify(v)).join(', ')}])`
33
+ }
34
+ if (rawValues.length > 0) {
36
35
  const literals = rawValues.map((v) => `z.literal(${JSON.stringify(v)})`)
37
- expr = literals.length === 1 ? literals[0]! : `z.union([${literals.join(', ')}])`
38
- } else {
39
- expr = 'z.string()'
36
+ return literals.length === 1 ? literals[0]! : `z.union([${literals.join(', ')}])`
40
37
  }
41
- break
38
+ return 'z.string()'
42
39
  }
43
- case 'integer':
44
- expr = 'z.coerce.number()'
45
- break
46
- case 'number':
47
- expr = 'z.number()'
48
- break
49
- case 'boolean':
50
- expr = 'z.boolean()'
51
- break
52
- case 'array':
53
- expr = 'z.array(z.unknown())'
54
- break
55
- default:
56
- expr = 'z.string()'
57
- }
58
-
59
- if (schema.nullable) {
60
- expr = `${expr}.nullable()`
61
- }
40
+ if (schema.type === 'integer') return 'z.coerce.number()'
41
+ if (schema.type === 'number') return 'z.number()'
42
+ if (schema.type === 'boolean') return 'z.boolean()'
43
+ if (schema.type === 'array') return 'z.array(z.unknown())'
44
+ return 'z.string()'
45
+ })()
62
46
 
63
- return expr
47
+ return schema.nullable ? `${baseExpr}.nullable()` : baseExpr
64
48
  }