@kubb/plugin-mcp 5.0.0-alpha.26 → 5.0.0-alpha.28
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/dist/index.cjs +1041 -61
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +221 -2
- package/dist/index.js +1014 -62
- package/dist/index.js.map +1 -1
- package/package.json +6 -31
- package/src/components/McpHandler.tsx +171 -0
- package/src/components/Server.tsx +86 -104
- package/src/generators/mcpGenerator.tsx +70 -83
- package/src/generators/serverGenerator.tsx +99 -57
- package/src/generators/serverGeneratorLegacy.tsx +144 -0
- package/src/index.ts +11 -1
- package/src/plugin.ts +76 -82
- package/src/presets.ts +25 -0
- package/src/resolvers/resolverMcp.ts +29 -0
- package/src/types.ts +51 -21
- package/src/utils.ts +97 -0
- package/dist/Server-EjbvNRYy.cjs +0 -231
- package/dist/Server-EjbvNRYy.cjs.map +0 -1
- package/dist/Server-kM1BVYP3.js +0 -183
- package/dist/Server-kM1BVYP3.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-Cc2InAz4.js +0 -274
- package/dist/generators-Cc2InAz4.js.map +0 -1
- package/dist/generators-CzP7VKQw.cjs +0 -285
- package/dist/generators-CzP7VKQw.cjs.map +0 -1
- package/dist/generators.cjs +0 -4
- package/dist/generators.d.ts +0 -12
- package/dist/generators.js +0 -2
- package/dist/types-DNDVb19t.d.ts +0 -64
- package/src/components/index.ts +0 -1
- package/src/generators/index.ts +0 -2
|
@@ -1,16 +1,34 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { caseParams, createFunctionParameter, createFunctionParameters, createParameterGroup } from '@kubb/ast'
|
|
2
|
+
import type { OperationNode } from '@kubb/ast/types'
|
|
2
3
|
import type { FabricFile } from '@kubb/fabric-core/types'
|
|
3
|
-
import
|
|
4
|
-
import
|
|
5
|
-
import { isOptional } from '@kubb/plugin-oas/utils'
|
|
6
|
-
import { Const, File, FunctionParams } from '@kubb/react-fabric'
|
|
4
|
+
import { functionPrinter } from '@kubb/plugin-ts'
|
|
5
|
+
import { Const, File, Function } from '@kubb/react-fabric'
|
|
7
6
|
import type { FabricReactNode } from '@kubb/react-fabric/types'
|
|
7
|
+
import type { PluginMcp } from '../types.ts'
|
|
8
|
+
import type { ZodParam } from '../utils.ts'
|
|
9
|
+
import { zodExprFromSchemaNode, zodGroupExpr } from '../utils.ts'
|
|
8
10
|
|
|
9
11
|
type Props = {
|
|
12
|
+
/**
|
|
13
|
+
* Variable name for the MCP server instance (e.g. 'server').
|
|
14
|
+
*/
|
|
10
15
|
name: string
|
|
16
|
+
/**
|
|
17
|
+
* Human-readable server name passed to `new McpServer({ name })`.
|
|
18
|
+
*/
|
|
11
19
|
serverName: string
|
|
20
|
+
/**
|
|
21
|
+
* Semantic version string passed to `new McpServer({ version })`.
|
|
22
|
+
*/
|
|
12
23
|
serverVersion: string
|
|
13
|
-
|
|
24
|
+
/**
|
|
25
|
+
* How to style your params.
|
|
26
|
+
*/
|
|
27
|
+
paramsCasing?: PluginMcp['resolvedOptions']['paramsCasing']
|
|
28
|
+
/**
|
|
29
|
+
* Operations to register as MCP tools, each carrying its handler,
|
|
30
|
+
* zod schema, and AST node metadata.
|
|
31
|
+
*/
|
|
14
32
|
operations: Array<{
|
|
15
33
|
tool: {
|
|
16
34
|
name: string
|
|
@@ -22,94 +40,23 @@ type Props = {
|
|
|
22
40
|
file: FabricFile.File
|
|
23
41
|
}
|
|
24
42
|
zod: {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
43
|
+
pathParams: Array<ZodParam>
|
|
44
|
+
/**
|
|
45
|
+
* Query params — either a group schema name (kubbV4) or individual schemas to compose (v5).
|
|
46
|
+
*/
|
|
47
|
+
queryParams?: string | Array<ZodParam>
|
|
48
|
+
/**
|
|
49
|
+
* Header params — either a group schema name (kubbV4) or individual schemas to compose (v5).
|
|
50
|
+
*/
|
|
51
|
+
headerParams?: string | Array<ZodParam>
|
|
52
|
+
requestName?: string
|
|
53
|
+
responseName?: string
|
|
31
54
|
}
|
|
55
|
+
node: OperationNode
|
|
32
56
|
}>
|
|
33
57
|
}
|
|
34
58
|
|
|
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
|
-
}
|
|
69
|
-
|
|
70
|
-
function getParams({ schemas, paramsCasing }: GetParamsProps) {
|
|
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
|
-
}
|
|
59
|
+
const keysPrinter = functionPrinter({ mode: 'keys' })
|
|
113
60
|
|
|
114
61
|
export function Server({ name, serverName, serverVersion, paramsCasing, operations }: Props): FabricReactNode {
|
|
115
62
|
return (
|
|
@@ -124,9 +71,46 @@ export function Server({ name, serverName, serverVersion, paramsCasing, operatio
|
|
|
124
71
|
</Const>
|
|
125
72
|
|
|
126
73
|
{operations
|
|
127
|
-
.map(({ tool, mcp, zod }) => {
|
|
128
|
-
const
|
|
129
|
-
const
|
|
74
|
+
.map(({ tool, mcp, zod, node }) => {
|
|
75
|
+
const casedParams = caseParams(node.parameters, paramsCasing)
|
|
76
|
+
const pathParams = casedParams.filter((p) => p.in === 'path')
|
|
77
|
+
|
|
78
|
+
const pathEntries: Array<{ key: string; value: string }> = []
|
|
79
|
+
const otherEntries: Array<{ key: string; value: string }> = []
|
|
80
|
+
|
|
81
|
+
for (const p of pathParams) {
|
|
82
|
+
const zodParam = zod.pathParams.find((zp) => zp.name === p.name)
|
|
83
|
+
pathEntries.push({ key: p.name, value: zodParam ? zodParam.schemaName : zodExprFromSchemaNode(p.schema) })
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (zod.requestName) {
|
|
87
|
+
otherEntries.push({ key: 'data', value: zod.requestName })
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (zod.queryParams) {
|
|
91
|
+
otherEntries.push({ key: 'params', value: zodGroupExpr(zod.queryParams) })
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (zod.headerParams) {
|
|
95
|
+
otherEntries.push({ key: 'headers', value: zodGroupExpr(zod.headerParams) })
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
otherEntries.sort((a, b) => a.key.localeCompare(b.key))
|
|
99
|
+
const entries = [...pathEntries, ...otherEntries]
|
|
100
|
+
|
|
101
|
+
const paramsNode = entries.length
|
|
102
|
+
? createFunctionParameters({
|
|
103
|
+
params: [
|
|
104
|
+
createParameterGroup({
|
|
105
|
+
properties: entries.map((e) => createFunctionParameter({ name: e.key, optional: false })),
|
|
106
|
+
}),
|
|
107
|
+
],
|
|
108
|
+
})
|
|
109
|
+
: undefined
|
|
110
|
+
|
|
111
|
+
const destructured = paramsNode ? (keysPrinter.print(paramsNode) ?? '') : ''
|
|
112
|
+
const inputSchema = entries.length ? `{ ${entries.map((e) => `${e.key}: ${e.value}`).join(', ')} }` : undefined
|
|
113
|
+
const outputSchema = zod.responseName
|
|
130
114
|
|
|
131
115
|
const config = [
|
|
132
116
|
tool.title ? `title: ${JSON.stringify(tool.title)}` : null,
|
|
@@ -136,13 +120,13 @@ export function Server({ name, serverName, serverVersion, paramsCasing, operatio
|
|
|
136
120
|
.filter(Boolean)
|
|
137
121
|
.join(',\n ')
|
|
138
122
|
|
|
139
|
-
if (
|
|
123
|
+
if (inputSchema) {
|
|
140
124
|
return `
|
|
141
125
|
server.registerTool(${JSON.stringify(tool.name)}, {
|
|
142
126
|
${config},
|
|
143
|
-
inputSchema: ${
|
|
144
|
-
}, async (${
|
|
145
|
-
return ${mcp.name}(${
|
|
127
|
+
inputSchema: ${inputSchema},
|
|
128
|
+
}, async (${destructured}) => {
|
|
129
|
+
return ${mcp.name}(${destructured})
|
|
146
130
|
})
|
|
147
131
|
`
|
|
148
132
|
}
|
|
@@ -151,24 +135,22 @@ server.registerTool(${JSON.stringify(tool.name)}, {
|
|
|
151
135
|
server.registerTool(${JSON.stringify(tool.name)}, {
|
|
152
136
|
${config},
|
|
153
137
|
}, async () => {
|
|
154
|
-
return ${mcp.name}(
|
|
138
|
+
return ${mcp.name}()
|
|
155
139
|
})
|
|
156
140
|
`
|
|
157
141
|
})
|
|
158
142
|
.filter(Boolean)}
|
|
159
143
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
try {
|
|
144
|
+
<Function name="startServer" async export>
|
|
145
|
+
{`try {
|
|
163
146
|
const transport = new StdioServerTransport()
|
|
164
147
|
await server.connect(transport)
|
|
165
148
|
|
|
166
149
|
} catch (error) {
|
|
167
150
|
console.error('Failed to start server:', error)
|
|
168
151
|
process.exit(1)
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
`}
|
|
152
|
+
}`}
|
|
153
|
+
</Function>
|
|
172
154
|
</File.Source>
|
|
173
155
|
)
|
|
174
156
|
}
|
|
@@ -1,109 +1,96 @@
|
|
|
1
1
|
import path from 'node:path'
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import { getBanner, getFooter } from '@kubb/plugin-oas/utils'
|
|
2
|
+
import { caseParams, transform } from '@kubb/ast'
|
|
3
|
+
import { defineGenerator } from '@kubb/core'
|
|
4
|
+
import type { PluginTs } from '@kubb/plugin-ts'
|
|
6
5
|
import { pluginTsName } from '@kubb/plugin-ts'
|
|
7
6
|
import { File } from '@kubb/react-fabric'
|
|
8
|
-
import
|
|
7
|
+
import { McpHandler } from '../components/McpHandler.tsx'
|
|
8
|
+
import type { PluginMcp } from '../types.ts'
|
|
9
9
|
|
|
10
|
-
export const mcpGenerator =
|
|
10
|
+
export const mcpGenerator = defineGenerator<PluginMcp>({
|
|
11
11
|
name: 'mcp',
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
const
|
|
12
|
+
type: 'react',
|
|
13
|
+
Operation({ node, options, config, driver, resolver, plugin }) {
|
|
14
|
+
const { output, client, paramsCasing, group } = options
|
|
15
|
+
const root = path.resolve(config.root, config.output.path)
|
|
15
16
|
|
|
16
|
-
const
|
|
17
|
+
const pluginTs = driver.getPlugin<PluginTs>(pluginTsName)
|
|
17
18
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
file: getFile(operation),
|
|
19
|
+
if (!pluginTs?.resolver) {
|
|
20
|
+
return null
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
23
|
+
const transformedNode = plugin.transformer ? transform(node, plugin.transformer) : node
|
|
24
|
+
|
|
25
|
+
const casedParams = caseParams(transformedNode.parameters, paramsCasing)
|
|
26
|
+
|
|
27
|
+
const pathParams = casedParams.filter((p) => p.in === 'path')
|
|
28
|
+
const queryParams = casedParams.filter((p) => p.in === 'query')
|
|
29
|
+
const headerParams = casedParams.filter((p) => p.in === 'header')
|
|
30
|
+
|
|
31
|
+
const importedTypeNames = [
|
|
32
|
+
...pathParams.map((p) => pluginTs.resolver.resolvePathParamsName(transformedNode, p)),
|
|
33
|
+
...queryParams.map((p) => pluginTs.resolver.resolveQueryParamsName(transformedNode, p)),
|
|
34
|
+
...headerParams.map((p) => pluginTs.resolver.resolveHeaderParamsName(transformedNode, p)),
|
|
35
|
+
transformedNode.requestBody?.schema ? pluginTs.resolver.resolveDataName(transformedNode) : undefined,
|
|
36
|
+
pluginTs.resolver.resolveResponseName(transformedNode),
|
|
37
|
+
...transformedNode.responses
|
|
38
|
+
.filter((r) => Number(r.statusCode) >= 400)
|
|
39
|
+
.map((r) => pluginTs.resolver.resolveResponseStatusName(transformedNode, r.statusCode)),
|
|
40
|
+
].filter(Boolean)
|
|
41
|
+
|
|
42
|
+
const meta = {
|
|
43
|
+
name: resolver.resolveName(transformedNode.operationId),
|
|
44
|
+
file: resolver.resolveFile(
|
|
45
|
+
{ name: transformedNode.operationId, extname: '.ts', tag: transformedNode.tags[0] ?? 'default', path: transformedNode.path },
|
|
46
|
+
{ root, output, group },
|
|
47
|
+
),
|
|
48
|
+
fileTs: pluginTs.resolver.resolveFile(
|
|
49
|
+
{ name: transformedNode.operationId, extname: '.ts', tag: transformedNode.tags[0] ?? 'default', path: transformedNode.path },
|
|
50
|
+
{
|
|
51
|
+
root,
|
|
52
|
+
output: pluginTs.options?.output ?? output,
|
|
53
|
+
group: pluginTs.options?.group,
|
|
54
|
+
},
|
|
55
|
+
),
|
|
56
|
+
} as const
|
|
27
57
|
|
|
28
58
|
return (
|
|
29
|
-
<File
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
{options.client.importPath ? (
|
|
59
|
+
<File baseName={meta.file.baseName} path={meta.file.path} meta={meta.file.meta}>
|
|
60
|
+
{meta.fileTs && importedTypeNames.length > 0 && (
|
|
61
|
+
<File.Import name={Array.from(new Set(importedTypeNames)).sort()} root={meta.file.path} path={meta.fileTs.path} isTypeOnly />
|
|
62
|
+
)}
|
|
63
|
+
<File.Import name={['CallToolResult']} path={'@modelcontextprotocol/sdk/types'} isTypeOnly />
|
|
64
|
+
<File.Import name={['buildFormData']} root={meta.file.path} path={path.resolve(root, '.kubb/config.ts')} />
|
|
65
|
+
{client.importPath ? (
|
|
37
66
|
<>
|
|
38
|
-
<File.Import name={'
|
|
39
|
-
<File.Import name={
|
|
40
|
-
{
|
|
67
|
+
<File.Import name={['Client', 'RequestConfig', 'ResponseErrorConfig']} path={client.importPath} isTypeOnly />
|
|
68
|
+
<File.Import name={'fetch'} path={client.importPath} />
|
|
69
|
+
{client.dataReturnType === 'full' && <File.Import name={['ResponseConfig']} path={client.importPath} isTypeOnly />}
|
|
41
70
|
</>
|
|
42
71
|
) : (
|
|
43
72
|
<>
|
|
44
|
-
<File.Import name={['fetch']} root={mcp.file.path} path={path.resolve(config.root, config.output.path, '.kubb/fetch.ts')} />
|
|
45
73
|
<File.Import
|
|
46
74
|
name={['Client', 'RequestConfig', 'ResponseErrorConfig']}
|
|
47
|
-
root={
|
|
48
|
-
path={path.resolve(
|
|
75
|
+
root={meta.file.path}
|
|
76
|
+
path={path.resolve(root, '.kubb/fetch.ts')}
|
|
49
77
|
isTypeOnly
|
|
50
78
|
/>
|
|
51
|
-
{
|
|
52
|
-
|
|
79
|
+
<File.Import name={['fetch']} root={meta.file.path} path={path.resolve(root, '.kubb/fetch.ts')} />
|
|
80
|
+
{client.dataReturnType === 'full' && (
|
|
81
|
+
<File.Import name={['ResponseConfig']} root={meta.file.path} path={path.resolve(root, '.kubb/fetch.ts')} isTypeOnly />
|
|
53
82
|
)}
|
|
54
83
|
</>
|
|
55
84
|
)}
|
|
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
85
|
|
|
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>
|
|
86
|
+
<McpHandler
|
|
87
|
+
name={meta.name}
|
|
88
|
+
node={transformedNode}
|
|
89
|
+
resolver={pluginTs.resolver}
|
|
90
|
+
baseURL={client.baseURL}
|
|
91
|
+
dataReturnType={client.dataReturnType || 'data'}
|
|
92
|
+
paramsCasing={paramsCasing}
|
|
93
|
+
/>
|
|
107
94
|
</File>
|
|
108
95
|
)
|
|
109
96
|
},
|
|
@@ -1,82 +1,124 @@
|
|
|
1
1
|
import path from 'node:path'
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import { getBanner, getFooter } from '@kubb/plugin-oas/utils'
|
|
6
|
-
import { pluginTsName } from '@kubb/plugin-ts'
|
|
2
|
+
import { caseParams, transform } from '@kubb/ast'
|
|
3
|
+
import { defineGenerator } from '@kubb/core'
|
|
4
|
+
import type { PluginZod } from '@kubb/plugin-zod'
|
|
7
5
|
import { pluginZodName } from '@kubb/plugin-zod'
|
|
8
6
|
import { File } from '@kubb/react-fabric'
|
|
9
|
-
import { Server } from '../components/Server'
|
|
10
|
-
import type { PluginMcp } from '../types'
|
|
7
|
+
import { Server } from '../components/Server.tsx'
|
|
8
|
+
import type { PluginMcp } from '../types.ts'
|
|
9
|
+
import { findSuccessStatusCode } from '../utils.ts'
|
|
11
10
|
|
|
12
|
-
|
|
11
|
+
/**
|
|
12
|
+
* Default server generator for `compatibilityPreset: 'default'` (v5).
|
|
13
|
+
*
|
|
14
|
+
* Uses individual zod schemas for each param (e.g. `createPetsPathUuidSchema`, `createPetsQueryOffsetSchema`)
|
|
15
|
+
* and `resolveResponseStatusName` for per-status response schemas.
|
|
16
|
+
* Query and header params are composed into `z.object({ ... })` from individual schemas.
|
|
17
|
+
*/
|
|
18
|
+
export const serverGenerator = defineGenerator<PluginMcp>({
|
|
13
19
|
name: 'operations',
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
const {
|
|
20
|
+
type: 'react',
|
|
21
|
+
Operations({ nodes, adapter, options, config, driver, resolver, plugin }) {
|
|
22
|
+
const { output, paramsCasing, group } = options
|
|
23
|
+
const root = path.resolve(config.root, config.output.path)
|
|
17
24
|
|
|
18
|
-
const
|
|
19
|
-
|
|
25
|
+
const pluginZod = driver.getPlugin<PluginZod>(pluginZodName)
|
|
26
|
+
|
|
27
|
+
if (!pluginZod?.resolver) {
|
|
28
|
+
return
|
|
29
|
+
}
|
|
20
30
|
|
|
21
31
|
const name = 'server'
|
|
22
|
-
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
|
+
}
|
|
45
|
+
|
|
46
|
+
const operationsMapped = nodes.map((node) => {
|
|
47
|
+
const transformedNode = plugin.transformer ? transform(node, plugin.transformer) : node
|
|
23
48
|
|
|
24
|
-
|
|
49
|
+
const casedParams = caseParams(transformedNode.parameters, paramsCasing)
|
|
50
|
+
const pathParams = casedParams.filter((p) => p.in === 'path')
|
|
51
|
+
const queryParams = casedParams.filter((p) => p.in === 'query')
|
|
52
|
+
const headerParams = casedParams.filter((p) => p.in === 'header')
|
|
53
|
+
|
|
54
|
+
const mcpFile = resolver.resolveFile(
|
|
55
|
+
{ name: transformedNode.operationId, extname: '.ts', tag: transformedNode.tags[0] ?? 'default', path: transformedNode.path },
|
|
56
|
+
{ root, output, group },
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
const zodFile = pluginZod.resolver.resolveFile(
|
|
60
|
+
{ name: transformedNode.operationId, extname: '.ts', tag: transformedNode.tags[0] ?? 'default', path: transformedNode.path },
|
|
61
|
+
{
|
|
62
|
+
root,
|
|
63
|
+
output: pluginZod.options?.output ?? output,
|
|
64
|
+
group: pluginZod.options?.group,
|
|
65
|
+
},
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
const requestName = transformedNode.requestBody?.schema ? pluginZod.resolver.resolveDataName(transformedNode) : undefined
|
|
69
|
+
const successStatus = findSuccessStatusCode(transformedNode.responses)
|
|
70
|
+
const responseName = successStatus ? pluginZod.resolver.resolveResponseStatusName(transformedNode, successStatus) : undefined
|
|
71
|
+
|
|
72
|
+
const resolveParams = (params: typeof pathParams) =>
|
|
73
|
+
params.map((p) => ({ name: p.name, schemaName: pluginZod.resolver.resolveParamName(transformedNode, p) }))
|
|
25
74
|
|
|
26
|
-
const operationsMapped = operations.map((operation) => {
|
|
27
75
|
return {
|
|
28
76
|
tool: {
|
|
29
|
-
name:
|
|
30
|
-
title:
|
|
31
|
-
description:
|
|
77
|
+
name: transformedNode.operationId,
|
|
78
|
+
title: transformedNode.summary || undefined,
|
|
79
|
+
description: transformedNode.description || `Make a ${transformedNode.method.toUpperCase()} request to ${transformedNode.path}`,
|
|
32
80
|
},
|
|
33
81
|
mcp: {
|
|
34
|
-
name:
|
|
35
|
-
|
|
36
|
-
suffix: 'handler',
|
|
37
|
-
}),
|
|
38
|
-
file: getFile(operation),
|
|
82
|
+
name: resolver.resolveName(transformedNode.operationId),
|
|
83
|
+
file: mcpFile,
|
|
39
84
|
},
|
|
40
85
|
zod: {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
file:
|
|
47
|
-
},
|
|
48
|
-
type: {
|
|
49
|
-
schemas: getSchemas(operation, { pluginName: pluginTsName, type: 'type' }),
|
|
86
|
+
pathParams: resolveParams(pathParams),
|
|
87
|
+
queryParams: queryParams.length ? resolveParams(queryParams) : undefined,
|
|
88
|
+
headerParams: headerParams.length ? resolveParams(headerParams) : undefined,
|
|
89
|
+
requestName,
|
|
90
|
+
responseName,
|
|
91
|
+
file: zodFile,
|
|
50
92
|
},
|
|
93
|
+
node: transformedNode,
|
|
51
94
|
}
|
|
52
95
|
})
|
|
53
96
|
|
|
54
97
|
const imports = operationsMapped.flatMap(({ mcp, zod }) => {
|
|
98
|
+
const zodNames = [
|
|
99
|
+
...zod.pathParams.map((p) => p.schemaName),
|
|
100
|
+
...(zod.queryParams ?? []).map((p) => p.schemaName),
|
|
101
|
+
...(zod.headerParams ?? []).map((p) => p.schemaName),
|
|
102
|
+
zod.requestName,
|
|
103
|
+
zod.responseName,
|
|
104
|
+
].filter(Boolean)
|
|
105
|
+
|
|
106
|
+
const uniqueNames = [...new Set(zodNames)].sort()
|
|
107
|
+
|
|
55
108
|
return [
|
|
56
|
-
<File.Import key={mcp.name} name={[mcp.name]} root={
|
|
57
|
-
<File.Import
|
|
58
|
-
|
|
59
|
-
name={[
|
|
60
|
-
zod.schemas.request?.name,
|
|
61
|
-
zod.schemas.pathParams?.name,
|
|
62
|
-
zod.schemas.queryParams?.name,
|
|
63
|
-
zod.schemas.headerParams?.name,
|
|
64
|
-
zod.schemas.response?.name,
|
|
65
|
-
].filter(Boolean)}
|
|
66
|
-
root={file.path}
|
|
67
|
-
path={zod.file.path}
|
|
68
|
-
/>,
|
|
69
|
-
]
|
|
109
|
+
<File.Import key={mcp.name} name={[mcp.name]} root={serverFile.path} path={mcp.file.path} />,
|
|
110
|
+
uniqueNames.length > 0 && <File.Import key={`zod-${mcp.name}`} name={uniqueNames} root={serverFile.path} path={zod.file.path} />,
|
|
111
|
+
].filter(Boolean)
|
|
70
112
|
})
|
|
71
113
|
|
|
72
114
|
return (
|
|
73
115
|
<>
|
|
74
116
|
<File
|
|
75
|
-
baseName={
|
|
76
|
-
path={
|
|
77
|
-
meta={
|
|
78
|
-
banner={
|
|
79
|
-
footer={
|
|
117
|
+
baseName={serverFile.baseName}
|
|
118
|
+
path={serverFile.path}
|
|
119
|
+
meta={serverFile.meta}
|
|
120
|
+
banner={resolver.resolveBanner(adapter.rootNode, { output, config })}
|
|
121
|
+
footer={resolver.resolveFooter(adapter.rootNode, { output, config })}
|
|
80
122
|
>
|
|
81
123
|
<File.Import name={['McpServer']} path={'@modelcontextprotocol/sdk/server/mcp'} />
|
|
82
124
|
<File.Import name={['z']} path={'zod'} />
|
|
@@ -85,9 +127,9 @@ export const serverGenerator = createReactGenerator<PluginMcp>({
|
|
|
85
127
|
{imports}
|
|
86
128
|
<Server
|
|
87
129
|
name={name}
|
|
88
|
-
serverName={
|
|
89
|
-
serverVersion={
|
|
90
|
-
paramsCasing={
|
|
130
|
+
serverName={adapter.rootNode?.meta?.title ?? 'server'}
|
|
131
|
+
serverVersion={adapter.rootNode?.meta?.version ?? '0.0.0'}
|
|
132
|
+
paramsCasing={paramsCasing}
|
|
91
133
|
operations={operationsMapped}
|
|
92
134
|
/>
|
|
93
135
|
</File>
|
|
@@ -97,10 +139,10 @@ export const serverGenerator = createReactGenerator<PluginMcp>({
|
|
|
97
139
|
{`
|
|
98
140
|
{
|
|
99
141
|
"mcpServers": {
|
|
100
|
-
"${
|
|
142
|
+
"${adapter.rootNode?.meta?.title || 'server'}": {
|
|
101
143
|
"type": "stdio",
|
|
102
144
|
"command": "npx",
|
|
103
|
-
"args": ["tsx", "${path.relative(path.dirname(jsonFile.path),
|
|
145
|
+
"args": ["tsx", "${path.relative(path.dirname(jsonFile.path), serverFile.path)}"]
|
|
104
146
|
}
|
|
105
147
|
}
|
|
106
148
|
}
|