@kubb/plugin-mcp 5.0.0-beta.3 → 5.0.0-beta.31

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.3",
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.31",
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
  ],
@@ -27,7 +25,7 @@
27
25
  "files": [
28
26
  "src",
29
27
  "dist",
30
- "plugin.json",
28
+ "extension.yaml",
31
29
  "!/**/**.test.**",
32
30
  "!/**/__tests__/**",
33
31
  "!/**/__snapshots__/**"
@@ -49,17 +47,18 @@
49
47
  "registry": "https://registry.npmjs.org/"
50
48
  },
51
49
  "dependencies": {
52
- "@kubb/core": "5.0.0-beta.3",
53
- "@kubb/renderer-jsx": "5.0.0-beta.3",
54
- "@kubb/plugin-client": "5.0.0-beta.3",
55
- "@kubb/plugin-ts": "5.0.0-beta.3",
56
- "@kubb/plugin-zod": "5.0.0-beta.3"
50
+ "@kubb/core": "5.0.0-beta.31",
51
+ "@kubb/renderer-jsx": "5.0.0-beta.31",
52
+ "@kubb/plugin-client": "5.0.0-beta.31",
53
+ "@kubb/plugin-ts": "5.0.0-beta.31",
54
+ "@kubb/plugin-zod": "5.0.0-beta.31"
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.3"
61
+ "@kubb/renderer-jsx": "5.0.0-beta.31"
63
62
  },
64
63
  "size-limit": [
65
64
  {
@@ -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
  }>
@@ -70,8 +71,7 @@ export function Server({ name, serverName, serverVersion, paramsCasing, operatio
70
71
 
71
72
  {operations
72
73
  .map(({ tool, mcp, zod, node }) => {
73
- const casedParams = ast.caseParams(node.parameters, paramsCasing)
74
- const pathParams = casedParams.filter((p) => p.in === 'path')
74
+ const { path: pathParams } = getOperationParameters(node, { paramsCasing })
75
75
 
76
76
  const pathEntries: Array<{ key: string; value: string }> = []
77
77
  const otherEntries: Array<{ key: string; value: string }> = []
@@ -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 = [
@@ -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', honorName: true })
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
  }))
package/src/types.ts CHANGED
@@ -6,54 +6,68 @@ import type { ClientImportPath, PluginClient } from '@kubb/plugin-client'
6
6
  */
7
7
  export type ResolverMcp = Resolver & {
8
8
  /**
9
- * Resolves the handler function name for an operation.
9
+ * Resolves the base handler function name for an operation.
10
10
  *
11
11
  * @example Resolving handler function names
12
12
  * `resolver.resolveName('show pet by id') // -> 'showPetByIdHandler'`
13
13
  */
14
14
  resolveName(this: ResolverMcp, name: string): string
15
+ /**
16
+ * Resolves the output file name for an MCP module.
17
+ */
18
+ resolvePathName(this: ResolverMcp, name: string, type?: 'file' | 'function' | 'type' | 'const'): string
19
+ /**
20
+ * Resolves the handler function name for an operation.
21
+ */
22
+ resolveHandlerName(this: ResolverMcp, node: ast.OperationNode): string
15
23
  }
16
24
 
17
25
  export type Options = {
18
26
  /**
19
- * Specify the export location for the files and define the behavior of the output.
20
- * @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' } }
21
30
  */
22
31
  output?: Output
23
32
  /**
24
- * 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.
25
35
  */
26
36
  client?: ClientImportPath & Pick<PluginClient['options'], 'clientType' | 'dataReturnType' | 'baseURL' | 'bundle' | 'paramsCasing'>
27
37
  /**
28
- * 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`.
29
42
  */
30
43
  paramsCasing?: 'camelcase'
31
44
  /**
32
- * Group the MCP requests based on the provided name.
45
+ * Split generated files into subfolders based on the operation's tag.
33
46
  */
34
47
  group?: Group
35
48
  /**
36
- * Tags, operations, or paths to exclude from generation.
49
+ * Skip operations matching at least one entry in the list.
37
50
  */
38
51
  exclude?: Array<Exclude>
39
52
  /**
40
- * Tags, operations, or paths to include in generation.
53
+ * Restrict generation to operations matching at least one entry in the list.
41
54
  */
42
55
  include?: Array<Include>
43
56
  /**
44
- * Override options for specific tags, operations, or paths.
57
+ * Apply a different options object to operations matching a pattern.
45
58
  */
46
59
  override?: Array<Override<ResolvedOptions>>
47
60
  /**
48
- * 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`.
49
63
  */
50
64
  resolver?: Partial<ResolverMcp> & ThisType<ResolverMcp>
51
65
  /**
52
- * AST visitor to transform generated nodes.
66
+ * AST visitor applied to each operation node before printing.
53
67
  */
54
68
  transformer?: ast.Visitor
55
69
  /**
56
- * Additional generators alongside the default generators.
70
+ * Custom generators that run alongside the built-in MCP generators.
57
71
  */
58
72
  generators?: Array<Generator<PluginMcp>>
59
73
  }
@@ -63,7 +77,7 @@ type ResolvedOptions = {
63
77
  exclude: Array<Exclude>
64
78
  include: Array<Include> | undefined
65
79
  override: Array<Override<ResolvedOptions>>
66
- group: Group | undefined
80
+ group: Group | null
67
81
  client: Pick<PluginClient['options'], 'client' | 'clientType' | 'dataReturnType' | 'importPath' | 'baseURL' | 'bundle' | 'paramsCasing'>
68
82
  paramsCasing: Options['paramsCasing']
69
83
  resolver: ResolverMcp