@kubb/plugin-mcp 5.0.0-beta.4 → 5.0.0-beta.42

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,19 +1,17 @@
1
1
  {
2
2
  "name": "@kubb/plugin-mcp",
3
- "version": "5.0.0-beta.4",
4
- "description": "Model Context Protocol (MCP) plugin for Kubb, generating MCP-compatible tools and schemas from OpenAPI specifications for AI assistants.",
3
+ "version": "5.0.0-beta.42",
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",
7
7
  "ai-tools",
8
- "claude",
9
- "code-generator",
8
+ "code-generation",
10
9
  "codegen",
11
10
  "kubb",
12
11
  "llm",
13
12
  "mcp",
14
13
  "model-context-protocol",
15
14
  "openapi",
16
- "plugins",
17
15
  "swagger",
18
16
  "typescript"
19
17
  ],
@@ -49,17 +47,18 @@
49
47
  "registry": "https://registry.npmjs.org/"
50
48
  },
51
49
  "dependencies": {
52
- "@kubb/core": "5.0.0-beta.4",
53
- "@kubb/renderer-jsx": "5.0.0-beta.4",
54
- "@kubb/plugin-client": "5.0.0-beta.4",
55
- "@kubb/plugin-ts": "5.0.0-beta.4",
56
- "@kubb/plugin-zod": "5.0.0-beta.4"
50
+ "@kubb/core": "5.0.0-beta.42",
51
+ "@kubb/renderer-jsx": "5.0.0-beta.42",
52
+ "@kubb/plugin-client": "5.0.0-beta.42",
53
+ "@kubb/plugin-ts": "5.0.0-beta.42",
54
+ "@kubb/plugin-zod": "5.0.0-beta.42"
57
55
  },
58
56
  "devDependencies": {
57
+ "@internals/shared": "0.0.0",
59
58
  "@internals/utils": "0.0.0"
60
59
  },
61
60
  "peerDependencies": {
62
- "@kubb/renderer-jsx": "5.0.0-beta.4"
61
+ "@kubb/renderer-jsx": "5.0.0-beta.42"
63
62
  },
64
63
  "size-limit": [
65
64
  {
@@ -78,6 +77,7 @@
78
77
  "lint:fix": "oxlint --fix .",
79
78
  "release": "pnpm publish --no-git-check",
80
79
  "release:canary": "bash ../../.github/canary.sh && node ../../scripts/build.js canary && pnpm publish --no-git-check",
80
+ "release:stage": "pnpm stage publish --no-git-check",
81
81
  "start": "tsdown --watch",
82
82
  "test": "vitest --passWithNoTests",
83
83
  "typecheck": "tsc -p ./tsconfig.json --noEmit --emitDeclarationOnly false"
@@ -1,11 +1,11 @@
1
- import { isValidVarName, URLPath } from '@internals/utils'
1
+ import { buildOperationComments, buildTransformedParamsMapping, getOperationParameters } from '@internals/shared'
2
+ import { camelCase, isValidVarName, URLPath } from '@internals/utils'
2
3
  import { ast } from '@kubb/core'
3
4
  import type { ResolverTs } from '@kubb/plugin-ts'
4
5
  import { functionPrinter } from '@kubb/plugin-ts'
5
6
  import { File, Function } from '@kubb/renderer-jsx'
6
7
  import type { KubbReactNode } from '@kubb/renderer-jsx/types'
7
8
  import type { PluginMcp } from '../types.ts'
8
- import { getComments, getParamsMapping } from '../utils.ts'
9
9
 
10
10
  type Props = {
11
11
  /**
@@ -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.
@@ -50,20 +50,15 @@ function buildRemappingCode(mapping: Record<string, string>, varName: string, so
50
50
  const declarationPrinter = functionPrinter({ mode: 'declaration' })
51
51
 
52
52
  export function McpHandler({ name, node, resolver, baseURL, dataReturnType, paramsCasing }: Props): KubbReactNode {
53
+ if (!ast.isHttpOperationNode(node)) return null
53
54
  const urlPath = new URLPath(node.path)
54
55
  const contentType = node.requestBody?.content?.[0]?.contentType
55
56
  const isFormData = contentType === 'multipart/form-data'
56
57
 
57
- const casedParams = ast.caseParams(node.parameters, paramsCasing)
58
- const queryParams = casedParams.filter((p) => p.in === 'query')
59
- const headerParams = casedParams.filter((p) => p.in === 'header')
58
+ const { query: queryParams, header: headerParams } = getOperationParameters(node, { paramsCasing })
59
+ const { path: originalPathParams, query: originalQueryParams, header: originalHeaderParams } = getOperationParameters(node)
60
60
 
61
- // Use original (uncased) parameters for mapping so original→camelCase difference is detected
62
- const originalPathParams = node.parameters.filter((p) => p.in === 'path')
63
- const originalQueryParams = node.parameters.filter((p) => p.in === 'query')
64
- const originalHeaderParams = node.parameters.filter((p) => p.in === 'header')
65
-
66
- const requestName = node.requestBody?.content?.[0]?.schema ? resolver.resolveDataName(node) : undefined
61
+ const requestName = node.requestBody?.content?.[0]?.schema ? resolver.resolveDataName(node) : null
67
62
  const responseName = resolver.resolveResponseName(node)
68
63
 
69
64
  const errorResponses = node.responses.filter((r) => Number(r.statusCode) >= 400).map((r) => resolver.resolveResponseStatusName(node, r.statusCode))
@@ -83,15 +78,15 @@ export function McpHandler({ name, node, resolver, baseURL, dataReturnType, para
83
78
  ? `${baseParamsSignature}, request: RequestHandlerExtra<ServerRequest, ServerNotification>`
84
79
  : 'request: RequestHandlerExtra<ServerRequest, ServerNotification>'
85
80
 
86
- const pathParamsMapping = paramsCasing ? getParamsMapping(originalPathParams) : undefined
87
- const queryParamsMapping = paramsCasing ? getParamsMapping(originalQueryParams) : undefined
88
- const headerParamsMapping = paramsCasing ? getParamsMapping(originalHeaderParams) : undefined
81
+ const pathParamsMapping = paramsCasing ? buildTransformedParamsMapping(originalPathParams, camelCase) : null
82
+ const queryParamsMapping = paramsCasing ? buildTransformedParamsMapping(originalQueryParams, camelCase) : null
83
+ const headerParamsMapping = paramsCasing ? buildTransformedParamsMapping(originalHeaderParams, camelCase) : null
89
84
 
90
85
  const contentTypeHeader =
91
- contentType && contentType !== 'application/json' && contentType !== 'multipart/form-data' ? `'Content-Type': '${contentType}'` : undefined
92
- const headers = [headerParams.length ? (headerParamsMapping ? '...mappedHeaders' : '...headers') : undefined, contentTypeHeader].filter(Boolean)
86
+ contentType && contentType !== 'application/json' && contentType !== 'multipart/form-data' ? `'Content-Type': '${contentType}'` : null
87
+ const headers = [headerParams.length ? (headerParamsMapping ? '...mappedHeaders' : '...headers') : null, contentTypeHeader].filter(Boolean)
93
88
 
94
- const fetchConfig: string[] = []
89
+ const fetchConfig: Array<string> = []
95
90
  fetchConfig.push(`method: ${JSON.stringify(node.method.toUpperCase())}`)
96
91
  fetchConfig.push(`url: ${urlPath.template}`)
97
92
  if (baseURL) fetchConfig.push(`baseURL: \`${baseURL}\``)
@@ -128,7 +123,7 @@ export function McpHandler({ name, node, resolver, baseURL, dataReturnType, para
128
123
  export
129
124
  params={paramsSignature}
130
125
  JSDoc={{
131
- comments: getComments(node),
126
+ comments: buildOperationComments(node),
132
127
  }}
133
128
  returnType={'Promise<CallToolResult>'}
134
129
  >
@@ -164,7 +159,7 @@ export function McpHandler({ name, node, resolver, baseURL, dataReturnType, para
164
159
  <br />
165
160
  {isFormData && requestName && 'const formData = buildFormData(requestData)'}
166
161
  <br />
167
- {`const res = await fetch<${generics.join(', ')}>({ ${fetchConfig.join(', ')} }, request)`}
162
+ {`const res = await client<${generics.join(', ')}>({ ${fetchConfig.join(', ')} }, request)`}
168
163
  <br />
169
164
  {callToolResult}
170
165
  </Function>
@@ -1,3 +1,4 @@
1
+ import { getOperationParameters } from '@internals/shared'
1
2
  import { ast } from '@kubb/core'
2
3
  import { functionPrinter } from '@kubb/plugin-ts'
3
4
  import { Const, File, Function } from '@kubb/renderer-jsx'
@@ -42,13 +43,13 @@ type Props = {
42
43
  /**
43
44
  * Query params — individual schemas to compose into `z.object({ ... })`.
44
45
  */
45
- queryParams?: string | Array<ZodParam>
46
+ queryParams?: string | Array<ZodParam> | null
46
47
  /**
47
48
  * Header params — individual schemas to compose into `z.object({ ... })`.
48
49
  */
49
- headerParams?: string | Array<ZodParam>
50
- requestName?: string
51
- responseName?: string
50
+ headerParams?: string | Array<ZodParam> | null
51
+ requestName?: string | null
52
+ responseName?: string | null
52
53
  }
53
54
  node: ast.OperationNode
54
55
  }>
@@ -57,69 +58,57 @@ type Props = {
57
58
  const keysPrinter = functionPrinter({ mode: 'keys' })
58
59
 
59
60
  export function Server({ name, serverName, serverVersion, paramsCasing, operations }: Props): KubbReactNode {
60
- return (
61
- <File.Source name={name} isExportable isIndexable>
62
- <Const name={'server'} export>
63
- {`
64
- new McpServer({
65
- name: '${serverName}',
66
- version: '${serverVersion}',
67
- })
68
- `}
69
- </Const>
70
-
71
- {operations
72
- .map(({ tool, mcp, zod, node }) => {
73
- const casedParams = ast.caseParams(node.parameters, paramsCasing)
74
- const pathParams = casedParams.filter((p) => p.in === 'path')
75
-
76
- const pathEntries: Array<{ key: string; value: string }> = []
77
- const otherEntries: Array<{ key: string; value: string }> = []
78
-
79
- for (const p of pathParams) {
80
- const zodParam = zod.pathParams.find((zp) => zp.name === p.name)
81
- pathEntries.push({ key: p.name, value: zodParam ? zodParam.schemaName : zodExprFromSchemaNode(p.schema) })
82
- }
83
-
84
- if (zod.requestName) {
85
- otherEntries.push({ key: 'data', value: zod.requestName })
86
- }
87
-
88
- if (zod.queryParams) {
89
- otherEntries.push({ key: 'params', value: zodGroupExpr(zod.queryParams) })
90
- }
91
-
92
- if (zod.headerParams) {
93
- otherEntries.push({ key: 'headers', value: zodGroupExpr(zod.headerParams) })
94
- }
95
-
96
- otherEntries.sort((a, b) => a.key.localeCompare(b.key))
97
- const entries = [...pathEntries, ...otherEntries]
98
-
99
- const paramsNode = entries.length
100
- ? ast.createFunctionParameters({
101
- params: [
102
- ast.createParameterGroup({
103
- properties: entries.map((e) => ast.createFunctionParameter({ name: e.key, optional: false })),
104
- }),
105
- ],
106
- })
107
- : undefined
108
-
109
- const destructured = paramsNode ? (keysPrinter.print(paramsNode) ?? '') : ''
110
- const inputSchema = entries.length ? `{ ${entries.map((e) => `${e.key}: ${e.value}`).join(', ')} }` : undefined
111
- const outputSchema = zod.responseName
112
-
113
- const config = [
114
- tool.title ? `title: ${JSON.stringify(tool.title)}` : null,
115
- `description: ${JSON.stringify(tool.description)}`,
116
- outputSchema ? `outputSchema: { data: ${outputSchema} }` : null,
117
- ]
118
- .filter(Boolean)
119
- .join(',\n ')
120
-
121
- if (inputSchema) {
122
- return `
61
+ const registrations = operations
62
+ .map(({ tool, mcp, zod, node }) => {
63
+ const { path: pathParams } = getOperationParameters(node, { paramsCasing })
64
+
65
+ const pathEntries: Array<{ key: string; value: string }> = []
66
+ const otherEntries: Array<{ key: string; value: string }> = []
67
+
68
+ for (const p of pathParams) {
69
+ const zodParam = zod.pathParams.find((zp) => zp.name === p.name)
70
+ pathEntries.push({ key: p.name, value: zodParam ? zodParam.schemaName : zodExprFromSchemaNode(p.schema) })
71
+ }
72
+
73
+ if (zod.requestName) {
74
+ otherEntries.push({ key: 'data', value: zod.requestName })
75
+ }
76
+
77
+ if (zod.queryParams) {
78
+ otherEntries.push({ key: 'params', value: zodGroupExpr(zod.queryParams) })
79
+ }
80
+
81
+ if (zod.headerParams) {
82
+ otherEntries.push({ key: 'headers', value: zodGroupExpr(zod.headerParams) })
83
+ }
84
+
85
+ otherEntries.sort((a, b) => a.key.localeCompare(b.key))
86
+ const entries = [...pathEntries, ...otherEntries]
87
+
88
+ const paramsNode = entries.length
89
+ ? ast.createFunctionParameters({
90
+ params: [
91
+ ast.createParameterGroup({
92
+ properties: entries.map((e) => ast.createFunctionParameter({ name: e.key, optional: false })),
93
+ }),
94
+ ],
95
+ })
96
+ : null
97
+
98
+ const destructured = paramsNode ? (keysPrinter.print(paramsNode) ?? '') : ''
99
+ const inputSchema = entries.length ? `{ ${entries.map((e) => `${e.key}: ${e.value}`).join(', ')} }` : null
100
+ const outputSchema = zod.responseName
101
+
102
+ const config = [
103
+ tool.title ? `title: ${JSON.stringify(tool.title)}` : null,
104
+ `description: ${JSON.stringify(tool.description)}`,
105
+ outputSchema ? `outputSchema: { data: ${outputSchema} }` : null,
106
+ ]
107
+ .filter(Boolean)
108
+ .join(',\n ')
109
+
110
+ if (inputSchema) {
111
+ return `
123
112
  server.registerTool(${JSON.stringify(tool.name)}, {
124
113
  ${config},
125
114
  inputSchema: ${inputSchema},
@@ -127,17 +116,33 @@ server.registerTool(${JSON.stringify(tool.name)}, {
127
116
  return ${mcp.name}(${destructured}, request)
128
117
  })
129
118
  `
130
- }
119
+ }
131
120
 
132
- return `
121
+ return `
133
122
  server.registerTool(${JSON.stringify(tool.name)}, {
134
123
  ${config},
135
124
  }, async (request) => {
136
125
  return ${mcp.name}(request)
137
126
  })
138
127
  `
139
- })
140
- .filter(Boolean)}
128
+ })
129
+ .filter(Boolean)
130
+ .join('\n')
131
+
132
+ return (
133
+ <File.Source name={name} isExportable isIndexable>
134
+ <Function name="getServer" export>
135
+ {`const server = new McpServer({
136
+ name: '${serverName}',
137
+ version: '${serverVersion}',
138
+ })
139
+ ${registrations}
140
+ return server`}
141
+ </Function>
142
+
143
+ <Const name={'server'} export>
144
+ {'getServer()'}
145
+ </Const>
141
146
 
142
147
  <Function name="startServer" async export>
143
148
  {`try {
@@ -1,14 +1,22 @@
1
1
  import path from 'node:path'
2
+ import { resolveOperationTypeNames } from '@internals/shared'
2
3
  import { ast, defineGenerator } from '@kubb/core'
3
4
  import { pluginTsName } from '@kubb/plugin-ts'
4
- import { File, jsxRenderer } from '@kubb/renderer-jsx'
5
+ import { File, jsxRendererSync } from '@kubb/renderer-jsx'
5
6
  import { McpHandler } from '../components/McpHandler.tsx'
6
7
  import type { PluginMcp } from '../types.ts'
7
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
+ */
8
15
  export const mcpGenerator = defineGenerator<PluginMcp>({
9
16
  name: 'mcp',
10
- renderer: jsxRenderer,
17
+ renderer: jsxRendererSync,
11
18
  operation(node, ctx) {
19
+ if (!ast.isHttpOperationNode(node)) return null
12
20
  const { resolver, driver, root } = ctx
13
21
  const { output, client, paramsCasing, group } = ctx.options
14
22
 
@@ -20,30 +28,20 @@ export const mcpGenerator = defineGenerator<PluginMcp>({
20
28
 
21
29
  const tsResolver = driver.getResolver(pluginTsName)
22
30
 
23
- const casedParams = ast.caseParams(node.parameters, paramsCasing)
24
-
25
- const pathParams = casedParams.filter((p) => p.in === 'path')
26
- const queryParams = casedParams.filter((p) => p.in === 'query')
27
- const headerParams = casedParams.filter((p) => p.in === 'header')
28
-
29
- const importedTypeNames = [
30
- ...pathParams.map((p) => tsResolver.resolvePathParamsName(node, p)),
31
- ...queryParams.map((p) => tsResolver.resolveQueryParamsName(node, p)),
32
- ...headerParams.map((p) => tsResolver.resolveHeaderParamsName(node, p)),
33
- node.requestBody?.content?.[0]?.schema ? tsResolver.resolveDataName(node) : undefined,
34
- tsResolver.resolveResponseName(node),
35
- ...node.responses.filter((r) => Number(r.statusCode) >= 400).map((r) => tsResolver.resolveResponseStatusName(node, r.statusCode)),
36
- ].filter(Boolean)
31
+ const importedTypeNames = resolveOperationTypeNames(node, tsResolver, { paramsCasing, responseStatusNames: 'error' })
37
32
 
38
33
  const meta = {
39
- name: resolver.resolveName(node.operationId),
40
- file: resolver.resolveFile({ name: node.operationId, extname: '.ts', tag: node.tags[0] ?? 'default', path: node.path }, { root, output, group }),
34
+ name: resolver.resolveHandlerName(node),
35
+ file: resolver.resolveFile(
36
+ { name: node.operationId, extname: '.ts', tag: node.tags[0] ?? 'default', path: node.path },
37
+ { root, output, group: group ?? undefined },
38
+ ),
41
39
  fileTs: tsResolver.resolveFile(
42
40
  { name: node.operationId, extname: '.ts', tag: node.tags[0] ?? 'default', path: node.path },
43
41
  {
44
42
  root,
45
43
  output: pluginTs.options?.output ?? output,
46
- group: pluginTs.options?.group,
44
+ group: pluginTs.options?.group ?? undefined,
47
45
  },
48
46
  ),
49
47
  } as const
@@ -59,7 +57,7 @@ export const mcpGenerator = defineGenerator<PluginMcp>({
59
57
  {client.importPath ? (
60
58
  <>
61
59
  <File.Import name={['Client', 'RequestConfig', 'ResponseErrorConfig']} path={client.importPath} isTypeOnly />
62
- <File.Import name={'fetch'} path={client.importPath} />
60
+ <File.Import name={'client'} path={client.importPath} />
63
61
  {client.dataReturnType === 'full' && <File.Import name={['ResponseConfig']} path={client.importPath} isTypeOnly />}
64
62
  </>
65
63
  ) : (
@@ -67,12 +65,12 @@ export const mcpGenerator = defineGenerator<PluginMcp>({
67
65
  <File.Import
68
66
  name={['Client', 'RequestConfig', 'ResponseErrorConfig']}
69
67
  root={meta.file.path}
70
- path={path.resolve(root, '.kubb/fetch.ts')}
68
+ path={path.resolve(root, '.kubb/client.ts')}
71
69
  isTypeOnly
72
70
  />
73
- <File.Import name={['fetch']} root={meta.file.path} path={path.resolve(root, '.kubb/fetch.ts')} />
71
+ <File.Import name={['client']} root={meta.file.path} path={path.resolve(root, '.kubb/client.ts')} />
74
72
  {client.dataReturnType === 'full' && (
75
- <File.Import name={['ResponseConfig']} root={meta.file.path} path={path.resolve(root, '.kubb/fetch.ts')} isTypeOnly />
73
+ <File.Import name={['ResponseConfig']} root={meta.file.path} path={path.resolve(root, '.kubb/client.ts')} isTypeOnly />
76
74
  )}
77
75
  </>
78
76
  )}
@@ -1,10 +1,10 @@
1
1
  import path from 'node:path'
2
+ import { findSuccessStatusCode, getOperationParameters } from '@internals/shared'
2
3
  import { ast, defineGenerator } from '@kubb/core'
3
4
  import { pluginZodName } from '@kubb/plugin-zod'
4
- import { File, jsxRenderer } from '@kubb/renderer-jsx'
5
+ import { File, jsxRendererSync } from '@kubb/renderer-jsx'
5
6
  import { Server } from '../components/Server.tsx'
6
7
  import type { PluginMcp } from '../types.ts'
7
- import { findSuccessStatusCode } from '../utils.ts'
8
8
 
9
9
  /**
10
10
  * Default v5 server generator for `@kubb/plugin-mcp`.
@@ -15,9 +15,9 @@ import { findSuccessStatusCode } from '../utils.ts'
15
15
  */
16
16
  export const serverGenerator = defineGenerator<PluginMcp>({
17
17
  name: 'operations',
18
- renderer: jsxRenderer,
18
+ renderer: jsxRendererSync,
19
19
  operations(nodes, ctx) {
20
- const { adapter, config, resolver, plugin, driver, root } = 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)
@@ -43,26 +43,26 @@ export const serverGenerator = defineGenerator<PluginMcp>({
43
43
  meta: { pluginName: plugin.name },
44
44
  }
45
45
 
46
- const operationsMapped = nodes.map((node) => {
47
- const casedParams = ast.caseParams(node.parameters, paramsCasing)
48
- const pathParams = casedParams.filter((p) => p.in === 'path')
49
- const queryParams = casedParams.filter((p) => p.in === 'query')
50
- const headerParams = casedParams.filter((p) => p.in === 'header')
46
+ const operationsMapped = nodes.filter(ast.isHttpOperationNode).map((node) => {
47
+ const { path: pathParams, query: queryParams, header: headerParams } = getOperationParameters(node, { paramsCasing })
51
48
 
52
- 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
+ )
53
53
 
54
54
  const zodFile = zodResolver.resolveFile(
55
55
  { name: node.operationId, extname: '.ts', tag: node.tags[0] ?? 'default', path: node.path },
56
56
  {
57
57
  root,
58
58
  output: pluginZod.options?.output ?? output,
59
- group: pluginZod.options?.group,
59
+ group: pluginZod.options?.group ?? undefined,
60
60
  },
61
61
  )
62
62
 
63
- const requestName = node.requestBody?.content?.[0]?.schema ? zodResolver.resolveDataName(node) : undefined
63
+ const requestName = node.requestBody?.content?.[0]?.schema ? zodResolver.resolveDataName(node) : null
64
64
  const successStatus = findSuccessStatusCode(node.responses)
65
- const responseName = successStatus ? zodResolver.resolveResponseStatusName(node, successStatus) : undefined
65
+ const responseName = successStatus ? zodResolver.resolveResponseStatusName(node, successStatus) : null
66
66
 
67
67
  const resolveParams = (params: typeof pathParams) => params.map((p) => ({ name: p.name, schemaName: zodResolver.resolveParamName(node, p) }))
68
68
 
@@ -73,13 +73,13 @@ export const serverGenerator = defineGenerator<PluginMcp>({
73
73
  description: node.description || `Make a ${node.method.toUpperCase()} request to ${node.path}`,
74
74
  },
75
75
  mcp: {
76
- name: resolver.resolveName(node.operationId),
76
+ name: resolver.resolveHandlerName(node),
77
77
  file: mcpFile,
78
78
  },
79
79
  zod: {
80
80
  pathParams: resolveParams(pathParams),
81
- queryParams: queryParams.length ? resolveParams(queryParams) : undefined,
82
- headerParams: headerParams.length ? resolveParams(headerParams) : undefined,
81
+ queryParams: queryParams.length ? resolveParams(queryParams) : null,
82
+ headerParams: headerParams.length ? resolveParams(headerParams) : null,
83
83
  requestName,
84
84
  responseName,
85
85
  file: zodFile,
@@ -95,7 +95,7 @@ export const serverGenerator = defineGenerator<PluginMcp>({
95
95
  ...(zod.headerParams ?? []).map((p) => p.schemaName),
96
96
  zod.requestName,
97
97
  zod.responseName,
98
- ].filter(Boolean)
98
+ ].filter((name): name is string => Boolean(name))
99
99
 
100
100
  const uniqueNames = [...new Set(zodNames)].sort()
101
101
 
@@ -111,8 +111,8 @@ export const serverGenerator = defineGenerator<PluginMcp>({
111
111
  baseName={serverFile.baseName}
112
112
  path={serverFile.path}
113
113
  meta={serverFile.meta}
114
- banner={resolver.resolveBanner(adapter.inputNode, { output, config })}
115
- footer={resolver.resolveFooter(adapter.inputNode, { output, config })}
114
+ banner={resolver.resolveBanner(ctx.meta, { output, config, file: { path: serverFile.path, baseName: serverFile.baseName } })}
115
+ footer={resolver.resolveFooter(ctx.meta, { output, config, file: { path: serverFile.path, baseName: serverFile.baseName } })}
116
116
  >
117
117
  <File.Import name={['McpServer']} path={'@modelcontextprotocol/sdk/server/mcp'} />
118
118
  <File.Import name={['z']} path={'zod'} />
@@ -121,8 +121,8 @@ export const serverGenerator = defineGenerator<PluginMcp>({
121
121
  {imports}
122
122
  <Server
123
123
  name={name}
124
- serverName={adapter.inputNode?.meta?.title ?? 'server'}
125
- serverVersion={adapter.inputNode?.meta?.version ?? '0.0.0'}
124
+ serverName={ctx.meta.title ?? 'server'}
125
+ serverVersion={ctx.meta.version ?? '0.0.0'}
126
126
  paramsCasing={paramsCasing}
127
127
  operations={operationsMapped}
128
128
  />
@@ -133,7 +133,7 @@ export const serverGenerator = defineGenerator<PluginMcp>({
133
133
  {`
134
134
  {
135
135
  "mcpServers": {
136
- "${adapter.inputNode?.meta?.title || 'server'}": {
136
+ "${ctx.meta.title || 'server'}": {
137
137
  "type": "stdio",
138
138
  "command": "npx",
139
139
  "args": ["tsx", "${path.relative(path.dirname(jsonFile.path), serverFile.path)}"]
package/src/plugin.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import path from 'node:path'
2
- import { camelCase } from '@internals/utils'
2
+ import { createGroupConfig } from '@internals/shared'
3
3
 
4
- import { ast, definePlugin, type Group } from '@kubb/core'
4
+ import { ast, definePlugin } from '@kubb/core'
5
5
  import { pluginClientName } from '@kubb/plugin-client'
6
6
  import { source as axiosClientSource } from '@kubb/plugin-client/templates/clients/axios.source'
7
7
  import { source as fetchClientSource } from '@kubb/plugin-client/templates/clients/fetch.source'
@@ -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' },
@@ -32,19 +64,7 @@ export const pluginMcp = definePlugin<PluginMcp>((options) => {
32
64
  const clientName = client?.client ?? 'axios'
33
65
  const clientImportPath = client?.importPath ?? (!client?.bundle ? `@kubb/plugin-client/clients/${clientName}` : undefined)
34
66
 
35
- const groupConfig = group
36
- ? ({
37
- ...group,
38
- name: group.name
39
- ? group.name
40
- : (ctx: { group: string }) => {
41
- if (group.type === 'path') {
42
- return `${ctx.group.split('/')[1]}`
43
- }
44
- return `${camelCase(ctx.group)}Requests`
45
- },
46
- } satisfies Group)
47
- : undefined
67
+ const groupConfig = createGroupConfig(group, { suffix: 'Requests' })
48
68
 
49
69
  return {
50
70
  name: pluginMcpName,
@@ -87,11 +107,11 @@ export const pluginMcp = definePlugin<PluginMcp>((options) => {
87
107
 
88
108
  if (client?.bundle && !hasClientPlugin && !clientImportPath) {
89
109
  ctx.injectFile({
90
- baseName: 'fetch.ts',
91
- path: path.resolve(root, '.kubb/fetch.ts'),
110
+ baseName: 'client.ts',
111
+ path: path.resolve(root, '.kubb/client.ts'),
92
112
  sources: [
93
113
  ast.createSource({
94
- name: 'fetch',
114
+ name: 'client',
95
115
  nodes: [ast.createText(clientName === 'fetch' ? fetchClientSource : axiosClientSource)],
96
116
  isExportable: true,
97
117
  isIndexable: true,
@@ -3,14 +3,18 @@ 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
- export const resolverMcp = defineResolver<PluginMcp>((ctx) => ({
17
+ export const resolverMcp = defineResolver<PluginMcp>(() => ({
14
18
  name: 'default',
15
19
  pluginName: 'plugin-mcp',
16
20
  default(name, type) {
@@ -20,6 +24,12 @@ export const resolverMcp = defineResolver<PluginMcp>((ctx) => ({
20
24
  return camelCase(name, { suffix: 'handler' })
21
25
  },
22
26
  resolveName(name) {
23
- return ctx.default(name, 'function')
27
+ return this.default(name, 'function')
28
+ },
29
+ resolvePathName(name, type) {
30
+ return this.default(name, type)
31
+ },
32
+ resolveHandlerName(node) {
33
+ return this.resolveName(node.operationId)
24
34
  },
25
35
  }))