@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/README.md +39 -22
- package/dist/index.cjs +343 -205
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +82 -27
- package/dist/index.js +345 -207
- package/dist/index.js.map +1 -1
- package/extension.yaml +633 -160
- package/package.json +11 -11
- package/src/components/McpHandler.tsx +15 -20
- package/src/components/Server.tsx +76 -71
- package/src/generators/mcpGenerator.tsx +21 -23
- package/src/generators/serverGenerator.tsx +22 -22
- package/src/plugin.ts +38 -18
- package/src/resolvers/resolverMcp.ts +16 -6
- package/src/types.ts +27 -13
- package/src/utils.ts +15 -80
- /package/dist/{chunk--u3MIqq1.js → chunk-C0LytTxp.js} +0 -0
package/package.json
CHANGED
|
@@ -1,19 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kubb/plugin-mcp",
|
|
3
|
-
"version": "5.0.0-beta.
|
|
4
|
-
"description": "Model Context Protocol (MCP)
|
|
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
|
-
"
|
|
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.
|
|
53
|
-
"@kubb/renderer-jsx": "5.0.0-beta.
|
|
54
|
-
"@kubb/plugin-client": "5.0.0-beta.
|
|
55
|
-
"@kubb/plugin-ts": "5.0.0-beta.
|
|
56
|
-
"@kubb/plugin-zod": "5.0.0-beta.
|
|
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.
|
|
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 {
|
|
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
|
|
58
|
-
const
|
|
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
|
-
|
|
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 ?
|
|
87
|
-
const queryParamsMapping = paramsCasing ?
|
|
88
|
-
const headerParamsMapping = paramsCasing ?
|
|
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}'` :
|
|
92
|
-
const headers = [headerParams.length ? (headerParamsMapping ? '...mappedHeaders' : '...headers') :
|
|
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:
|
|
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
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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:
|
|
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
|
|
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.
|
|
40
|
-
file: resolver.resolveFile(
|
|
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={'
|
|
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/
|
|
68
|
+
path={path.resolve(root, '.kubb/client.ts')}
|
|
71
69
|
isTypeOnly
|
|
72
70
|
/>
|
|
73
|
-
<File.Import name={['
|
|
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/
|
|
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,
|
|
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:
|
|
18
|
+
renderer: jsxRendererSync,
|
|
19
19
|
operations(nodes, ctx) {
|
|
20
|
-
const {
|
|
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
|
|
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(
|
|
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) :
|
|
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) :
|
|
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.
|
|
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) :
|
|
82
|
-
headerParams: headerParams.length ? resolveParams(headerParams) :
|
|
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(
|
|
115
|
-
footer={resolver.resolveFooter(
|
|
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={
|
|
125
|
-
serverVersion={
|
|
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
|
-
"${
|
|
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 {
|
|
2
|
+
import { createGroupConfig } from '@internals/shared'
|
|
3
3
|
|
|
4
|
-
import { ast, definePlugin
|
|
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: '
|
|
91
|
-
path: path.resolve(root, '.kubb/
|
|
110
|
+
baseName: 'client.ts',
|
|
111
|
+
path: path.resolve(root, '.kubb/client.ts'),
|
|
92
112
|
sources: [
|
|
93
113
|
ast.createSource({
|
|
94
|
-
name: '
|
|
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
|
-
*
|
|
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
|
-
*
|
|
10
|
+
* @example Resolve a handler name
|
|
11
|
+
* ```ts
|
|
12
|
+
* import { resolverMcp } from '@kubb/plugin-mcp'
|
|
9
13
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
14
|
+
* resolverMcp.default('addPet', 'function') // 'addPetHandler'
|
|
15
|
+
* ```
|
|
12
16
|
*/
|
|
13
|
-
export const resolverMcp = defineResolver<PluginMcp>((
|
|
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
|
|
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
|
}))
|