@kubb/plugin-mcp 5.0.0-alpha.9 → 5.0.0-beta.3
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/LICENSE +17 -10
- package/README.md +1 -4
- package/dist/index.cjs +957 -92
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +207 -4
- package/dist/index.js +923 -90
- package/dist/index.js.map +1 -1
- package/package.json +42 -64
- package/src/components/McpHandler.tsx +173 -0
- package/src/components/Server.tsx +89 -109
- package/src/generators/mcpGenerator.tsx +65 -84
- package/src/generators/serverGenerator.tsx +95 -58
- package/src/index.ts +11 -2
- package/src/plugin.ts +87 -135
- package/src/resolvers/resolverMcp.ts +25 -0
- package/src/types.ts +46 -28
- package/src/utils.ts +113 -0
- package/dist/Server-DV9zFrUP.cjs +0 -221
- package/dist/Server-DV9zFrUP.cjs.map +0 -1
- package/dist/Server-KWLMg0Lm.js +0 -173
- package/dist/Server-KWLMg0Lm.js.map +0 -1
- package/dist/components.cjs +0 -3
- package/dist/components.d.ts +0 -41
- package/dist/components.js +0 -2
- package/dist/generators-CWAFnA94.cjs +0 -285
- package/dist/generators-CWAFnA94.cjs.map +0 -1
- package/dist/generators-TtEOkDB1.js +0 -274
- package/dist/generators-TtEOkDB1.js.map +0 -1
- package/dist/generators.cjs +0 -4
- package/dist/generators.d.ts +0 -508
- package/dist/generators.js +0 -2
- package/dist/types-DXZDZ3vf.d.ts +0 -64
- package/src/components/index.ts +0 -1
- package/src/generators/index.ts +0 -2
|
@@ -1,16 +1,32 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
3
|
-
import
|
|
4
|
-
import type {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import
|
|
1
|
+
import { ast } from '@kubb/core'
|
|
2
|
+
import { functionPrinter } from '@kubb/plugin-ts'
|
|
3
|
+
import { Const, File, Function } from '@kubb/renderer-jsx'
|
|
4
|
+
import type { KubbReactNode } from '@kubb/renderer-jsx/types'
|
|
5
|
+
import type { PluginMcp } from '../types.ts'
|
|
6
|
+
import type { ZodParam } from '../utils.ts'
|
|
7
|
+
import { zodExprFromSchemaNode, zodGroupExpr } from '../utils.ts'
|
|
8
8
|
|
|
9
9
|
type Props = {
|
|
10
|
+
/**
|
|
11
|
+
* Variable name for the MCP server instance (e.g. 'server').
|
|
12
|
+
*/
|
|
10
13
|
name: string
|
|
14
|
+
/**
|
|
15
|
+
* Human-readable server name passed to `new McpServer({ name })`.
|
|
16
|
+
*/
|
|
11
17
|
serverName: string
|
|
18
|
+
/**
|
|
19
|
+
* Semantic version string passed to `new McpServer({ version })`.
|
|
20
|
+
*/
|
|
12
21
|
serverVersion: string
|
|
13
|
-
|
|
22
|
+
/**
|
|
23
|
+
* How to style your params.
|
|
24
|
+
*/
|
|
25
|
+
paramsCasing?: PluginMcp['resolvedOptions']['paramsCasing']
|
|
26
|
+
/**
|
|
27
|
+
* Operations to register as MCP tools, each carrying its handler,
|
|
28
|
+
* zod schema, and AST node metadata.
|
|
29
|
+
*/
|
|
14
30
|
operations: Array<{
|
|
15
31
|
tool: {
|
|
16
32
|
name: string
|
|
@@ -19,99 +35,28 @@ type Props = {
|
|
|
19
35
|
}
|
|
20
36
|
mcp: {
|
|
21
37
|
name: string
|
|
22
|
-
file:
|
|
38
|
+
file: ast.FileNode
|
|
23
39
|
}
|
|
24
40
|
zod: {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
41
|
+
pathParams: Array<ZodParam>
|
|
42
|
+
/**
|
|
43
|
+
* Query params — individual schemas to compose into `z.object({ ... })`.
|
|
44
|
+
*/
|
|
45
|
+
queryParams?: string | Array<ZodParam>
|
|
46
|
+
/**
|
|
47
|
+
* Header params — individual schemas to compose into `z.object({ ... })`.
|
|
48
|
+
*/
|
|
49
|
+
headerParams?: string | Array<ZodParam>
|
|
50
|
+
requestName?: string
|
|
51
|
+
responseName?: string
|
|
31
52
|
}
|
|
53
|
+
node: ast.OperationNode
|
|
32
54
|
}>
|
|
33
55
|
}
|
|
34
56
|
|
|
35
|
-
|
|
36
|
-
schemas: OperationSchemas
|
|
37
|
-
paramsCasing?: 'camelcase'
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function zodExprFromOasSchema(schema: SchemaObject): string {
|
|
41
|
-
const types = Array.isArray(schema.type) ? schema.type : [schema.type]
|
|
42
|
-
const baseType = types.find((t) => t && t !== 'null')
|
|
43
|
-
const isNullableType = types.includes('null')
|
|
44
|
-
|
|
45
|
-
let expr: string
|
|
46
|
-
switch (baseType) {
|
|
47
|
-
case 'integer':
|
|
48
|
-
expr = 'z.coerce.number()'
|
|
49
|
-
break
|
|
50
|
-
case 'number':
|
|
51
|
-
expr = 'z.number()'
|
|
52
|
-
break
|
|
53
|
-
case 'boolean':
|
|
54
|
-
expr = 'z.boolean()'
|
|
55
|
-
break
|
|
56
|
-
case 'array':
|
|
57
|
-
expr = 'z.array(z.unknown())'
|
|
58
|
-
break
|
|
59
|
-
default:
|
|
60
|
-
expr = 'z.string()'
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
if (isNullableType) {
|
|
64
|
-
expr = `${expr}.nullable()`
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
return expr
|
|
68
|
-
}
|
|
57
|
+
const keysPrinter = functionPrinter({ mode: 'keys' })
|
|
69
58
|
|
|
70
|
-
function
|
|
71
|
-
const pathParamProperties = schemas.pathParams?.schema?.properties ?? {}
|
|
72
|
-
const requiredFields = Array.isArray(schemas.pathParams?.schema?.required) ? schemas.pathParams.schema.required : []
|
|
73
|
-
|
|
74
|
-
const pathParamEntries = Object.entries(pathParamProperties).reduce<Record<string, { value: string; optional: boolean }>>(
|
|
75
|
-
(acc, [originalKey, propSchema]) => {
|
|
76
|
-
const key = paramsCasing === 'camelcase' || !isValidVarName(originalKey) ? camelCase(originalKey) : originalKey
|
|
77
|
-
acc[key] = {
|
|
78
|
-
value: zodExprFromOasSchema(propSchema as SchemaObject),
|
|
79
|
-
optional: !requiredFields.includes(originalKey),
|
|
80
|
-
}
|
|
81
|
-
return acc
|
|
82
|
-
},
|
|
83
|
-
{},
|
|
84
|
-
)
|
|
85
|
-
|
|
86
|
-
return FunctionParams.factory({
|
|
87
|
-
data: {
|
|
88
|
-
mode: 'object',
|
|
89
|
-
children: {
|
|
90
|
-
...pathParamEntries,
|
|
91
|
-
data: schemas.request?.name
|
|
92
|
-
? {
|
|
93
|
-
value: schemas.request?.name,
|
|
94
|
-
optional: isOptional(schemas.request?.schema),
|
|
95
|
-
}
|
|
96
|
-
: undefined,
|
|
97
|
-
params: schemas.queryParams?.name
|
|
98
|
-
? {
|
|
99
|
-
value: schemas.queryParams?.name,
|
|
100
|
-
optional: isOptional(schemas.queryParams?.schema),
|
|
101
|
-
}
|
|
102
|
-
: undefined,
|
|
103
|
-
headers: schemas.headerParams?.name
|
|
104
|
-
? {
|
|
105
|
-
value: schemas.headerParams?.name,
|
|
106
|
-
optional: isOptional(schemas.headerParams?.schema),
|
|
107
|
-
}
|
|
108
|
-
: undefined,
|
|
109
|
-
},
|
|
110
|
-
},
|
|
111
|
-
})
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
export function Server({ name, serverName, serverVersion, paramsCasing, operations }: Props): FabricReactNode {
|
|
59
|
+
export function Server({ name, serverName, serverVersion, paramsCasing, operations }: Props): KubbReactNode {
|
|
115
60
|
return (
|
|
116
61
|
<File.Source name={name} isExportable isIndexable>
|
|
117
62
|
<Const name={'server'} export>
|
|
@@ -124,9 +69,46 @@ export function Server({ name, serverName, serverVersion, paramsCasing, operatio
|
|
|
124
69
|
</Const>
|
|
125
70
|
|
|
126
71
|
{operations
|
|
127
|
-
.map(({ tool, mcp, zod }) => {
|
|
128
|
-
const
|
|
129
|
-
const
|
|
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
|
|
130
112
|
|
|
131
113
|
const config = [
|
|
132
114
|
tool.title ? `title: ${JSON.stringify(tool.title)}` : null,
|
|
@@ -136,13 +118,13 @@ export function Server({ name, serverName, serverVersion, paramsCasing, operatio
|
|
|
136
118
|
.filter(Boolean)
|
|
137
119
|
.join(',\n ')
|
|
138
120
|
|
|
139
|
-
if (
|
|
121
|
+
if (inputSchema) {
|
|
140
122
|
return `
|
|
141
123
|
server.registerTool(${JSON.stringify(tool.name)}, {
|
|
142
124
|
${config},
|
|
143
|
-
inputSchema: ${
|
|
144
|
-
}, async (${
|
|
145
|
-
return ${mcp.name}(${
|
|
125
|
+
inputSchema: ${inputSchema},
|
|
126
|
+
}, async (${destructured}, request) => {
|
|
127
|
+
return ${mcp.name}(${destructured}, request)
|
|
146
128
|
})
|
|
147
129
|
`
|
|
148
130
|
}
|
|
@@ -150,25 +132,23 @@ server.registerTool(${JSON.stringify(tool.name)}, {
|
|
|
150
132
|
return `
|
|
151
133
|
server.registerTool(${JSON.stringify(tool.name)}, {
|
|
152
134
|
${config},
|
|
153
|
-
}, async () => {
|
|
154
|
-
return ${mcp.name}(
|
|
135
|
+
}, async (request) => {
|
|
136
|
+
return ${mcp.name}(request)
|
|
155
137
|
})
|
|
156
138
|
`
|
|
157
139
|
})
|
|
158
140
|
.filter(Boolean)}
|
|
159
141
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
try {
|
|
142
|
+
<Function name="startServer" async export>
|
|
143
|
+
{`try {
|
|
163
144
|
const transport = new StdioServerTransport()
|
|
164
145
|
await server.connect(transport)
|
|
165
146
|
|
|
166
147
|
} catch (error) {
|
|
167
148
|
console.error('Failed to start server:', error)
|
|
168
149
|
process.exit(1)
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
`}
|
|
150
|
+
}`}
|
|
151
|
+
</Function>
|
|
172
152
|
</File.Source>
|
|
173
153
|
)
|
|
174
154
|
}
|
|
@@ -1,109 +1,90 @@
|
|
|
1
1
|
import path from 'node:path'
|
|
2
|
-
import {
|
|
3
|
-
import { createReactGenerator } from '@kubb/plugin-oas/generators'
|
|
4
|
-
import { useOas, useOperationManager } from '@kubb/plugin-oas/hooks'
|
|
5
|
-
import { getBanner, getFooter } from '@kubb/plugin-oas/utils'
|
|
2
|
+
import { ast, defineGenerator } from '@kubb/core'
|
|
6
3
|
import { pluginTsName } from '@kubb/plugin-ts'
|
|
7
|
-
import { File } from '@kubb/
|
|
8
|
-
import
|
|
4
|
+
import { File, jsxRenderer } from '@kubb/renderer-jsx'
|
|
5
|
+
import { McpHandler } from '../components/McpHandler.tsx'
|
|
6
|
+
import type { PluginMcp } from '../types.ts'
|
|
9
7
|
|
|
10
|
-
export const mcpGenerator =
|
|
8
|
+
export const mcpGenerator = defineGenerator<PluginMcp>({
|
|
11
9
|
name: 'mcp',
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
const
|
|
10
|
+
renderer: jsxRenderer,
|
|
11
|
+
operation(node, ctx) {
|
|
12
|
+
const { resolver, driver, root } = ctx
|
|
13
|
+
const { output, client, paramsCasing, group } = ctx.options
|
|
15
14
|
|
|
16
|
-
const
|
|
15
|
+
const pluginTs = driver.getPlugin(pluginTsName)
|
|
17
16
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
file: getFile(operation),
|
|
17
|
+
if (!pluginTs) {
|
|
18
|
+
return null
|
|
21
19
|
}
|
|
22
20
|
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
21
|
+
const tsResolver = driver.getResolver(pluginTsName)
|
|
22
|
+
|
|
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)
|
|
37
|
+
|
|
38
|
+
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 }),
|
|
41
|
+
fileTs: tsResolver.resolveFile(
|
|
42
|
+
{ name: node.operationId, extname: '.ts', tag: node.tags[0] ?? 'default', path: node.path },
|
|
43
|
+
{
|
|
44
|
+
root,
|
|
45
|
+
output: pluginTs.options?.output ?? output,
|
|
46
|
+
group: pluginTs.options?.group,
|
|
47
|
+
},
|
|
48
|
+
),
|
|
49
|
+
} as const
|
|
27
50
|
|
|
28
51
|
return (
|
|
29
|
-
<File
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
{
|
|
52
|
+
<File baseName={meta.file.baseName} path={meta.file.path} meta={meta.file.meta}>
|
|
53
|
+
{meta.fileTs && importedTypeNames.length > 0 && (
|
|
54
|
+
<File.Import name={Array.from(new Set(importedTypeNames)).sort()} root={meta.file.path} path={meta.fileTs.path} isTypeOnly />
|
|
55
|
+
)}
|
|
56
|
+
<File.Import name={['CallToolResult', 'ServerNotification', 'ServerRequest']} path={'@modelcontextprotocol/sdk/types'} isTypeOnly />
|
|
57
|
+
<File.Import name={['RequestHandlerExtra']} path={'@modelcontextprotocol/sdk/shared/protocol'} isTypeOnly />
|
|
58
|
+
<File.Import name={['buildFormData']} root={meta.file.path} path={path.resolve(root, '.kubb/config.ts')} />
|
|
59
|
+
{client.importPath ? (
|
|
37
60
|
<>
|
|
38
|
-
<File.Import name={'
|
|
39
|
-
<File.Import name={
|
|
40
|
-
{
|
|
61
|
+
<File.Import name={['Client', 'RequestConfig', 'ResponseErrorConfig']} path={client.importPath} isTypeOnly />
|
|
62
|
+
<File.Import name={'fetch'} path={client.importPath} />
|
|
63
|
+
{client.dataReturnType === 'full' && <File.Import name={['ResponseConfig']} path={client.importPath} isTypeOnly />}
|
|
41
64
|
</>
|
|
42
65
|
) : (
|
|
43
66
|
<>
|
|
44
|
-
<File.Import name={['fetch']} root={mcp.file.path} path={path.resolve(config.root, config.output.path, '.kubb/fetch.ts')} />
|
|
45
67
|
<File.Import
|
|
46
68
|
name={['Client', 'RequestConfig', 'ResponseErrorConfig']}
|
|
47
|
-
root={
|
|
48
|
-
path={path.resolve(
|
|
69
|
+
root={meta.file.path}
|
|
70
|
+
path={path.resolve(root, '.kubb/fetch.ts')}
|
|
49
71
|
isTypeOnly
|
|
50
72
|
/>
|
|
51
|
-
{
|
|
52
|
-
|
|
73
|
+
<File.Import name={['fetch']} root={meta.file.path} path={path.resolve(root, '.kubb/fetch.ts')} />
|
|
74
|
+
{client.dataReturnType === 'full' && (
|
|
75
|
+
<File.Import name={['ResponseConfig']} root={meta.file.path} path={path.resolve(root, '.kubb/fetch.ts')} isTypeOnly />
|
|
53
76
|
)}
|
|
54
77
|
</>
|
|
55
78
|
)}
|
|
56
|
-
<File.Import name={['buildFormData']} root={mcp.file.path} path={path.resolve(config.root, config.output.path, '.kubb/config.ts')} />
|
|
57
|
-
<File.Import name={['CallToolResult']} path={'@modelcontextprotocol/sdk/types'} isTypeOnly />
|
|
58
|
-
<File.Import
|
|
59
|
-
name={[
|
|
60
|
-
type.schemas.request?.name,
|
|
61
|
-
type.schemas.response.name,
|
|
62
|
-
type.schemas.pathParams?.name,
|
|
63
|
-
type.schemas.queryParams?.name,
|
|
64
|
-
type.schemas.headerParams?.name,
|
|
65
|
-
...(type.schemas.statusCodes?.map((item) => item.name) || []),
|
|
66
|
-
].filter(Boolean)}
|
|
67
|
-
root={mcp.file.path}
|
|
68
|
-
path={type.file.path}
|
|
69
|
-
isTypeOnly
|
|
70
|
-
/>
|
|
71
79
|
|
|
72
|
-
<
|
|
73
|
-
name={
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
baseURL={
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
dataReturnType={options.client.dataReturnType || 'data'}
|
|
81
|
-
paramsType={'object'}
|
|
82
|
-
paramsCasing={options.client?.paramsCasing || options.paramsCasing}
|
|
83
|
-
pathParamsType={'object'}
|
|
84
|
-
parser={'client'}
|
|
85
|
-
>
|
|
86
|
-
{options.client.dataReturnType === 'data' &&
|
|
87
|
-
`return {
|
|
88
|
-
content: [
|
|
89
|
-
{
|
|
90
|
-
type: 'text',
|
|
91
|
-
text: JSON.stringify(res.data)
|
|
92
|
-
}
|
|
93
|
-
],
|
|
94
|
-
structuredContent: { data: res.data }
|
|
95
|
-
}`}
|
|
96
|
-
{options.client.dataReturnType === 'full' &&
|
|
97
|
-
`return {
|
|
98
|
-
content: [
|
|
99
|
-
{
|
|
100
|
-
type: 'text',
|
|
101
|
-
text: JSON.stringify(res)
|
|
102
|
-
}
|
|
103
|
-
],
|
|
104
|
-
structuredContent: { data: res.data }
|
|
105
|
-
}`}
|
|
106
|
-
</Client>
|
|
80
|
+
<McpHandler
|
|
81
|
+
name={meta.name}
|
|
82
|
+
node={node}
|
|
83
|
+
resolver={tsResolver}
|
|
84
|
+
baseURL={client.baseURL}
|
|
85
|
+
dataReturnType={client.dataReturnType || 'data'}
|
|
86
|
+
paramsCasing={paramsCasing}
|
|
87
|
+
/>
|
|
107
88
|
</File>
|
|
108
89
|
)
|
|
109
90
|
},
|
|
@@ -1,81 +1,118 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
3
|
-
import { useOas, useOperationManager } from '@kubb/plugin-oas/hooks'
|
|
4
|
-
import { getBanner, getFooter } from '@kubb/plugin-oas/utils'
|
|
5
|
-
import { pluginTsName } from '@kubb/plugin-ts'
|
|
1
|
+
import path from 'node:path'
|
|
2
|
+
import { ast, defineGenerator } from '@kubb/core'
|
|
6
3
|
import { pluginZodName } from '@kubb/plugin-zod'
|
|
7
|
-
import { File } from '@kubb/
|
|
8
|
-
import { Server } from '../components/Server'
|
|
9
|
-
import type { PluginMcp } from '../types'
|
|
4
|
+
import { File, jsxRenderer } from '@kubb/renderer-jsx'
|
|
5
|
+
import { Server } from '../components/Server.tsx'
|
|
6
|
+
import type { PluginMcp } from '../types.ts'
|
|
7
|
+
import { findSuccessStatusCode } from '../utils.ts'
|
|
10
8
|
|
|
11
|
-
|
|
9
|
+
/**
|
|
10
|
+
* Default v5 server generator for `@kubb/plugin-mcp`.
|
|
11
|
+
*
|
|
12
|
+
* Uses individual zod schemas for each param (e.g. `createPetsPathUuidSchema`, `createPetsQueryOffsetSchema`)
|
|
13
|
+
* and `resolveResponseStatusName` for per-status response schemas.
|
|
14
|
+
* Query and header params are composed into `z.object({ ... })` from individual schemas.
|
|
15
|
+
*/
|
|
16
|
+
export const serverGenerator = defineGenerator<PluginMcp>({
|
|
12
17
|
name: 'operations',
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
const {
|
|
18
|
+
renderer: jsxRenderer,
|
|
19
|
+
operations(nodes, ctx) {
|
|
20
|
+
const { adapter, config, resolver, plugin, driver, root } = ctx
|
|
21
|
+
const { output, paramsCasing, group } = ctx.options
|
|
16
22
|
|
|
17
|
-
const
|
|
18
|
-
|
|
23
|
+
const pluginZod = driver.getPlugin(pluginZodName)
|
|
24
|
+
|
|
25
|
+
if (!pluginZod) {
|
|
26
|
+
return
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const zodResolver = driver.getResolver(pluginZodName)
|
|
19
30
|
|
|
20
31
|
const name = 'server'
|
|
21
|
-
const
|
|
32
|
+
const serverFilePath = path.resolve(root, output.path, 'server.ts')
|
|
33
|
+
const serverFile = {
|
|
34
|
+
baseName: 'server.ts' as const,
|
|
35
|
+
path: serverFilePath,
|
|
36
|
+
meta: { pluginName: plugin.name },
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const jsonFilePath = path.resolve(root, output.path, '.mcp.json')
|
|
40
|
+
const jsonFile = {
|
|
41
|
+
baseName: '.mcp.json' as const,
|
|
42
|
+
path: jsonFilePath,
|
|
43
|
+
meta: { pluginName: plugin.name },
|
|
44
|
+
}
|
|
22
45
|
|
|
23
|
-
const
|
|
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')
|
|
51
|
+
|
|
52
|
+
const mcpFile = resolver.resolveFile({ name: node.operationId, extname: '.ts', tag: node.tags[0] ?? 'default', path: node.path }, { root, output, group })
|
|
53
|
+
|
|
54
|
+
const zodFile = zodResolver.resolveFile(
|
|
55
|
+
{ name: node.operationId, extname: '.ts', tag: node.tags[0] ?? 'default', path: node.path },
|
|
56
|
+
{
|
|
57
|
+
root,
|
|
58
|
+
output: pluginZod.options?.output ?? output,
|
|
59
|
+
group: pluginZod.options?.group,
|
|
60
|
+
},
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
const requestName = node.requestBody?.content?.[0]?.schema ? zodResolver.resolveDataName(node) : undefined
|
|
64
|
+
const successStatus = findSuccessStatusCode(node.responses)
|
|
65
|
+
const responseName = successStatus ? zodResolver.resolveResponseStatusName(node, successStatus) : undefined
|
|
66
|
+
|
|
67
|
+
const resolveParams = (params: typeof pathParams) => params.map((p) => ({ name: p.name, schemaName: zodResolver.resolveParamName(node, p) }))
|
|
24
68
|
|
|
25
|
-
const operationsMapped = operations.map((operation) => {
|
|
26
69
|
return {
|
|
27
70
|
tool: {
|
|
28
|
-
name:
|
|
29
|
-
title:
|
|
30
|
-
description:
|
|
71
|
+
name: node.operationId,
|
|
72
|
+
title: node.summary || undefined,
|
|
73
|
+
description: node.description || `Make a ${node.method.toUpperCase()} request to ${node.path}`,
|
|
31
74
|
},
|
|
32
75
|
mcp: {
|
|
33
|
-
name:
|
|
34
|
-
|
|
35
|
-
suffix: 'handler',
|
|
36
|
-
}),
|
|
37
|
-
file: getFile(operation),
|
|
76
|
+
name: resolver.resolveName(node.operationId),
|
|
77
|
+
file: mcpFile,
|
|
38
78
|
},
|
|
39
79
|
zod: {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
file:
|
|
46
|
-
},
|
|
47
|
-
type: {
|
|
48
|
-
schemas: getSchemas(operation, { pluginName: pluginTsName, type: 'type' }),
|
|
80
|
+
pathParams: resolveParams(pathParams),
|
|
81
|
+
queryParams: queryParams.length ? resolveParams(queryParams) : undefined,
|
|
82
|
+
headerParams: headerParams.length ? resolveParams(headerParams) : undefined,
|
|
83
|
+
requestName,
|
|
84
|
+
responseName,
|
|
85
|
+
file: zodFile,
|
|
49
86
|
},
|
|
87
|
+
node: node,
|
|
50
88
|
}
|
|
51
89
|
})
|
|
52
90
|
|
|
53
91
|
const imports = operationsMapped.flatMap(({ mcp, zod }) => {
|
|
92
|
+
const zodNames = [
|
|
93
|
+
...zod.pathParams.map((p) => p.schemaName),
|
|
94
|
+
...(zod.queryParams ?? []).map((p) => p.schemaName),
|
|
95
|
+
...(zod.headerParams ?? []).map((p) => p.schemaName),
|
|
96
|
+
zod.requestName,
|
|
97
|
+
zod.responseName,
|
|
98
|
+
].filter(Boolean)
|
|
99
|
+
|
|
100
|
+
const uniqueNames = [...new Set(zodNames)].sort()
|
|
101
|
+
|
|
54
102
|
return [
|
|
55
|
-
<File.Import key={mcp.name} name={[mcp.name]} root={
|
|
56
|
-
<File.Import
|
|
57
|
-
|
|
58
|
-
name={[
|
|
59
|
-
zod.schemas.request?.name,
|
|
60
|
-
zod.schemas.pathParams?.name,
|
|
61
|
-
zod.schemas.queryParams?.name,
|
|
62
|
-
zod.schemas.headerParams?.name,
|
|
63
|
-
zod.schemas.response?.name,
|
|
64
|
-
].filter(Boolean)}
|
|
65
|
-
root={file.path}
|
|
66
|
-
path={zod.file.path}
|
|
67
|
-
/>,
|
|
68
|
-
]
|
|
103
|
+
<File.Import key={mcp.name} name={[mcp.name]} root={serverFile.path} path={mcp.file.path} />,
|
|
104
|
+
uniqueNames.length > 0 && <File.Import key={`zod-${mcp.name}`} name={uniqueNames} root={serverFile.path} path={zod.file.path} />,
|
|
105
|
+
].filter(Boolean)
|
|
69
106
|
})
|
|
70
107
|
|
|
71
108
|
return (
|
|
72
109
|
<>
|
|
73
110
|
<File
|
|
74
|
-
baseName={
|
|
75
|
-
path={
|
|
76
|
-
meta={
|
|
77
|
-
banner={
|
|
78
|
-
footer={
|
|
111
|
+
baseName={serverFile.baseName}
|
|
112
|
+
path={serverFile.path}
|
|
113
|
+
meta={serverFile.meta}
|
|
114
|
+
banner={resolver.resolveBanner(adapter.inputNode, { output, config })}
|
|
115
|
+
footer={resolver.resolveFooter(adapter.inputNode, { output, config })}
|
|
79
116
|
>
|
|
80
117
|
<File.Import name={['McpServer']} path={'@modelcontextprotocol/sdk/server/mcp'} />
|
|
81
118
|
<File.Import name={['z']} path={'zod'} />
|
|
@@ -84,9 +121,9 @@ export const serverGenerator = createReactGenerator<PluginMcp>({
|
|
|
84
121
|
{imports}
|
|
85
122
|
<Server
|
|
86
123
|
name={name}
|
|
87
|
-
serverName={
|
|
88
|
-
serverVersion={
|
|
89
|
-
paramsCasing={
|
|
124
|
+
serverName={adapter.inputNode?.meta?.title ?? 'server'}
|
|
125
|
+
serverVersion={adapter.inputNode?.meta?.version ?? '0.0.0'}
|
|
126
|
+
paramsCasing={paramsCasing}
|
|
90
127
|
operations={operationsMapped}
|
|
91
128
|
/>
|
|
92
129
|
</File>
|
|
@@ -96,10 +133,10 @@ export const serverGenerator = createReactGenerator<PluginMcp>({
|
|
|
96
133
|
{`
|
|
97
134
|
{
|
|
98
135
|
"mcpServers": {
|
|
99
|
-
"${
|
|
136
|
+
"${adapter.inputNode?.meta?.title || 'server'}": {
|
|
100
137
|
"type": "stdio",
|
|
101
138
|
"command": "npx",
|
|
102
|
-
"args": ["tsx", "${
|
|
139
|
+
"args": ["tsx", "${path.relative(path.dirname(jsonFile.path), serverFile.path)}"]
|
|
103
140
|
}
|
|
104
141
|
}
|
|
105
142
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,2 +1,11 @@
|
|
|
1
|
-
export {
|
|
2
|
-
export
|
|
1
|
+
export { McpHandler } from './components/McpHandler.tsx'
|
|
2
|
+
export { Server } from './components/Server.tsx'
|
|
3
|
+
|
|
4
|
+
export { mcpGenerator } from './generators/mcpGenerator.tsx'
|
|
5
|
+
export { serverGenerator } from './generators/serverGenerator.tsx'
|
|
6
|
+
|
|
7
|
+
export { default, pluginMcp, pluginMcpName } from './plugin.ts'
|
|
8
|
+
|
|
9
|
+
export { resolverMcp } from './resolvers/resolverMcp.ts'
|
|
10
|
+
|
|
11
|
+
export type { PluginMcp, ResolverMcp } from './types.ts'
|