@kubb/plugin-mcp 5.0.0-beta.42 → 5.0.0-beta.64
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 +116 -179
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +10 -14
- package/dist/index.js +117 -180
- package/dist/index.js.map +1 -1
- package/package.json +10 -18
- package/extension.yaml +0 -943
- package/src/components/McpHandler.tsx +0 -168
- package/src/components/Server.tsx +0 -159
- package/src/generators/mcpGenerator.tsx +0 -89
- package/src/generators/serverGenerator.tsx +0 -149
- package/src/index.ts +0 -11
- package/src/plugin.ts +0 -142
- package/src/resolvers/resolverMcp.ts +0 -35
- package/src/types.ts +0 -94
- package/src/utils.ts +0 -48
|
@@ -1,168 +0,0 @@
|
|
|
1
|
-
import { buildOperationComments, buildTransformedParamsMapping, getOperationParameters } from '@internals/shared'
|
|
2
|
-
import { camelCase, isValidVarName, URLPath } from '@internals/utils'
|
|
3
|
-
import { ast } from '@kubb/core'
|
|
4
|
-
import type { ResolverTs } from '@kubb/plugin-ts'
|
|
5
|
-
import { functionPrinter } from '@kubb/plugin-ts'
|
|
6
|
-
import { File, Function } from '@kubb/renderer-jsx'
|
|
7
|
-
import type { KubbReactNode } from '@kubb/renderer-jsx/types'
|
|
8
|
-
import type { PluginMcp } from '../types.ts'
|
|
9
|
-
|
|
10
|
-
type Props = {
|
|
11
|
-
/**
|
|
12
|
-
* Name of the handler function.
|
|
13
|
-
*/
|
|
14
|
-
name: string
|
|
15
|
-
/**
|
|
16
|
-
* AST operation node.
|
|
17
|
-
*/
|
|
18
|
-
node: ast.OperationNode
|
|
19
|
-
/**
|
|
20
|
-
* TypeScript resolver for resolving param/data/response type names.
|
|
21
|
-
*/
|
|
22
|
-
resolver: ResolverTs
|
|
23
|
-
/**
|
|
24
|
-
* Base URL prepended to every generated request URL.
|
|
25
|
-
*/
|
|
26
|
-
baseURL: string | null | undefined
|
|
27
|
-
/**
|
|
28
|
-
* Return type when calling fetch.
|
|
29
|
-
* - 'data' returns response data only.
|
|
30
|
-
* - 'full' returns the full response object.
|
|
31
|
-
* @default 'data'
|
|
32
|
-
*/
|
|
33
|
-
dataReturnType: PluginMcp['resolvedOptions']['client']['dataReturnType']
|
|
34
|
-
/**
|
|
35
|
-
* How to style your params.
|
|
36
|
-
*/
|
|
37
|
-
paramsCasing?: PluginMcp['resolvedOptions']['paramsCasing']
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Generate a remapping statement: `const mappedX = x ? { "orig": x.camel, ... } : undefined`
|
|
42
|
-
*/
|
|
43
|
-
function buildRemappingCode(mapping: Record<string, string>, varName: string, sourceName: string): string {
|
|
44
|
-
const pairs = Object.entries(mapping)
|
|
45
|
-
.map(([orig, camel]) => `"${orig}": ${sourceName}.${camel}`)
|
|
46
|
-
.join(', ')
|
|
47
|
-
return `const ${varName} = ${sourceName} ? { ${pairs} } : undefined`
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
const declarationPrinter = functionPrinter({ mode: 'declaration' })
|
|
51
|
-
|
|
52
|
-
export function McpHandler({ name, node, resolver, baseURL, dataReturnType, paramsCasing }: Props): KubbReactNode {
|
|
53
|
-
if (!ast.isHttpOperationNode(node)) return null
|
|
54
|
-
const urlPath = new URLPath(node.path)
|
|
55
|
-
const contentType = node.requestBody?.content?.[0]?.contentType
|
|
56
|
-
const isFormData = contentType === 'multipart/form-data'
|
|
57
|
-
|
|
58
|
-
const { query: queryParams, header: headerParams } = getOperationParameters(node, { paramsCasing })
|
|
59
|
-
const { path: originalPathParams, query: originalQueryParams, header: originalHeaderParams } = getOperationParameters(node)
|
|
60
|
-
|
|
61
|
-
const requestName = node.requestBody?.content?.[0]?.schema ? resolver.resolveDataName(node) : null
|
|
62
|
-
const responseName = resolver.resolveResponseName(node)
|
|
63
|
-
|
|
64
|
-
const errorResponses = node.responses.filter((r) => Number(r.statusCode) >= 400).map((r) => resolver.resolveResponseStatusName(node, r.statusCode))
|
|
65
|
-
const errorType = errorResponses.length > 0 ? errorResponses.join(' | ') : 'Error'
|
|
66
|
-
|
|
67
|
-
const TError = `ResponseErrorConfig<${errorType}>`
|
|
68
|
-
const generics = [responseName, TError, requestName || 'unknown'].filter(Boolean)
|
|
69
|
-
|
|
70
|
-
const paramsNode = ast.createOperationParams(node, {
|
|
71
|
-
paramsType: 'object',
|
|
72
|
-
pathParamsType: 'inline',
|
|
73
|
-
resolver,
|
|
74
|
-
paramsCasing,
|
|
75
|
-
})
|
|
76
|
-
const baseParamsSignature = declarationPrinter.print(paramsNode) ?? ''
|
|
77
|
-
const paramsSignature = baseParamsSignature
|
|
78
|
-
? `${baseParamsSignature}, request: RequestHandlerExtra<ServerRequest, ServerNotification>`
|
|
79
|
-
: 'request: RequestHandlerExtra<ServerRequest, ServerNotification>'
|
|
80
|
-
|
|
81
|
-
const pathParamsMapping = paramsCasing ? buildTransformedParamsMapping(originalPathParams, camelCase) : null
|
|
82
|
-
const queryParamsMapping = paramsCasing ? buildTransformedParamsMapping(originalQueryParams, camelCase) : null
|
|
83
|
-
const headerParamsMapping = paramsCasing ? buildTransformedParamsMapping(originalHeaderParams, camelCase) : null
|
|
84
|
-
|
|
85
|
-
const contentTypeHeader =
|
|
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)
|
|
88
|
-
|
|
89
|
-
const fetchConfig: Array<string> = []
|
|
90
|
-
fetchConfig.push(`method: ${JSON.stringify(node.method.toUpperCase())}`)
|
|
91
|
-
fetchConfig.push(`url: ${urlPath.template}`)
|
|
92
|
-
if (baseURL) fetchConfig.push(`baseURL: \`${baseURL}\``)
|
|
93
|
-
if (queryParams.length) fetchConfig.push(queryParamsMapping ? 'params: mappedParams' : 'params')
|
|
94
|
-
if (requestName) fetchConfig.push(`data: ${isFormData ? 'formData as FormData' : 'requestData'}`)
|
|
95
|
-
if (headers.length) fetchConfig.push(`headers: { ${headers.join(', ')} }`)
|
|
96
|
-
|
|
97
|
-
const callToolResult =
|
|
98
|
-
dataReturnType === 'data'
|
|
99
|
-
? `return {
|
|
100
|
-
content: [
|
|
101
|
-
{
|
|
102
|
-
type: 'text',
|
|
103
|
-
text: JSON.stringify(res.data)
|
|
104
|
-
}
|
|
105
|
-
],
|
|
106
|
-
structuredContent: { data: res.data }
|
|
107
|
-
}`
|
|
108
|
-
: `return {
|
|
109
|
-
content: [
|
|
110
|
-
{
|
|
111
|
-
type: 'text',
|
|
112
|
-
text: JSON.stringify(res)
|
|
113
|
-
}
|
|
114
|
-
],
|
|
115
|
-
structuredContent: { data: res.data }
|
|
116
|
-
}`
|
|
117
|
-
|
|
118
|
-
return (
|
|
119
|
-
<File.Source name={name} isExportable isIndexable>
|
|
120
|
-
<Function
|
|
121
|
-
name={name}
|
|
122
|
-
async
|
|
123
|
-
export
|
|
124
|
-
params={paramsSignature}
|
|
125
|
-
JSDoc={{
|
|
126
|
-
comments: buildOperationComments(node),
|
|
127
|
-
}}
|
|
128
|
-
returnType={'Promise<CallToolResult>'}
|
|
129
|
-
>
|
|
130
|
-
{''}
|
|
131
|
-
<br />
|
|
132
|
-
<br />
|
|
133
|
-
{pathParamsMapping &&
|
|
134
|
-
Object.entries(pathParamsMapping)
|
|
135
|
-
.filter(([originalName, camelCaseName]) => originalName !== camelCaseName && isValidVarName(originalName))
|
|
136
|
-
.map(([originalName, camelCaseName]) => `const ${originalName} = ${camelCaseName}`)
|
|
137
|
-
.join('\n')}
|
|
138
|
-
{pathParamsMapping && (
|
|
139
|
-
<>
|
|
140
|
-
<br />
|
|
141
|
-
<br />
|
|
142
|
-
</>
|
|
143
|
-
)}
|
|
144
|
-
{queryParamsMapping && queryParams.length > 0 && (
|
|
145
|
-
<>
|
|
146
|
-
{buildRemappingCode(queryParamsMapping, 'mappedParams', 'params')}
|
|
147
|
-
<br />
|
|
148
|
-
<br />
|
|
149
|
-
</>
|
|
150
|
-
)}
|
|
151
|
-
{headerParamsMapping && headerParams.length > 0 && (
|
|
152
|
-
<>
|
|
153
|
-
{buildRemappingCode(headerParamsMapping, 'mappedHeaders', 'headers')}
|
|
154
|
-
<br />
|
|
155
|
-
<br />
|
|
156
|
-
</>
|
|
157
|
-
)}
|
|
158
|
-
{requestName && 'const requestData = data'}
|
|
159
|
-
<br />
|
|
160
|
-
{isFormData && requestName && 'const formData = buildFormData(requestData)'}
|
|
161
|
-
<br />
|
|
162
|
-
{`const res = await client<${generics.join(', ')}>({ ${fetchConfig.join(', ')} }, request)`}
|
|
163
|
-
<br />
|
|
164
|
-
{callToolResult}
|
|
165
|
-
</Function>
|
|
166
|
-
</File.Source>
|
|
167
|
-
)
|
|
168
|
-
}
|
|
@@ -1,159 +0,0 @@
|
|
|
1
|
-
import { getOperationParameters } from '@internals/shared'
|
|
2
|
-
import { ast } from '@kubb/core'
|
|
3
|
-
import { functionPrinter } from '@kubb/plugin-ts'
|
|
4
|
-
import { Const, File, Function } from '@kubb/renderer-jsx'
|
|
5
|
-
import type { KubbReactNode } from '@kubb/renderer-jsx/types'
|
|
6
|
-
import type { PluginMcp } from '../types.ts'
|
|
7
|
-
import type { ZodParam } from '../utils.ts'
|
|
8
|
-
import { zodExprFromSchemaNode, zodGroupExpr } from '../utils.ts'
|
|
9
|
-
|
|
10
|
-
type Props = {
|
|
11
|
-
/**
|
|
12
|
-
* Variable name for the MCP server instance (e.g. 'server').
|
|
13
|
-
*/
|
|
14
|
-
name: string
|
|
15
|
-
/**
|
|
16
|
-
* Human-readable server name passed to `new McpServer({ name })`.
|
|
17
|
-
*/
|
|
18
|
-
serverName: string
|
|
19
|
-
/**
|
|
20
|
-
* Semantic version string passed to `new McpServer({ version })`.
|
|
21
|
-
*/
|
|
22
|
-
serverVersion: string
|
|
23
|
-
/**
|
|
24
|
-
* How to style your params.
|
|
25
|
-
*/
|
|
26
|
-
paramsCasing?: PluginMcp['resolvedOptions']['paramsCasing']
|
|
27
|
-
/**
|
|
28
|
-
* Operations to register as MCP tools, each carrying its handler,
|
|
29
|
-
* zod schema, and AST node metadata.
|
|
30
|
-
*/
|
|
31
|
-
operations: Array<{
|
|
32
|
-
tool: {
|
|
33
|
-
name: string
|
|
34
|
-
title?: string
|
|
35
|
-
description: string
|
|
36
|
-
}
|
|
37
|
-
mcp: {
|
|
38
|
-
name: string
|
|
39
|
-
file: ast.FileNode
|
|
40
|
-
}
|
|
41
|
-
zod: {
|
|
42
|
-
pathParams: Array<ZodParam>
|
|
43
|
-
/**
|
|
44
|
-
* Query params — individual schemas to compose into `z.object({ ... })`.
|
|
45
|
-
*/
|
|
46
|
-
queryParams?: string | Array<ZodParam> | null
|
|
47
|
-
/**
|
|
48
|
-
* Header params — individual schemas to compose into `z.object({ ... })`.
|
|
49
|
-
*/
|
|
50
|
-
headerParams?: string | Array<ZodParam> | null
|
|
51
|
-
requestName?: string | null
|
|
52
|
-
responseName?: string | null
|
|
53
|
-
}
|
|
54
|
-
node: ast.OperationNode
|
|
55
|
-
}>
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
const keysPrinter = functionPrinter({ mode: 'keys' })
|
|
59
|
-
|
|
60
|
-
export function Server({ name, serverName, serverVersion, paramsCasing, operations }: Props): KubbReactNode {
|
|
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 `
|
|
112
|
-
server.registerTool(${JSON.stringify(tool.name)}, {
|
|
113
|
-
${config},
|
|
114
|
-
inputSchema: ${inputSchema},
|
|
115
|
-
}, async (${destructured}, request) => {
|
|
116
|
-
return ${mcp.name}(${destructured}, request)
|
|
117
|
-
})
|
|
118
|
-
`
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
return `
|
|
122
|
-
server.registerTool(${JSON.stringify(tool.name)}, {
|
|
123
|
-
${config},
|
|
124
|
-
}, async (request) => {
|
|
125
|
-
return ${mcp.name}(request)
|
|
126
|
-
})
|
|
127
|
-
`
|
|
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>
|
|
146
|
-
|
|
147
|
-
<Function name="startServer" async export>
|
|
148
|
-
{`try {
|
|
149
|
-
const transport = new StdioServerTransport()
|
|
150
|
-
await server.connect(transport)
|
|
151
|
-
|
|
152
|
-
} catch (error) {
|
|
153
|
-
console.error('Failed to start server:', error)
|
|
154
|
-
process.exit(1)
|
|
155
|
-
}`}
|
|
156
|
-
</Function>
|
|
157
|
-
</File.Source>
|
|
158
|
-
)
|
|
159
|
-
}
|
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
import path from 'node:path'
|
|
2
|
-
import { resolveOperationTypeNames } from '@internals/shared'
|
|
3
|
-
import { ast, defineGenerator } from '@kubb/core'
|
|
4
|
-
import { pluginTsName } from '@kubb/plugin-ts'
|
|
5
|
-
import { File, jsxRendererSync } from '@kubb/renderer-jsx'
|
|
6
|
-
import { McpHandler } from '../components/McpHandler.tsx'
|
|
7
|
-
import type { PluginMcp } from '../types.ts'
|
|
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
|
-
*/
|
|
15
|
-
export const mcpGenerator = defineGenerator<PluginMcp>({
|
|
16
|
-
name: 'mcp',
|
|
17
|
-
renderer: jsxRendererSync,
|
|
18
|
-
operation(node, ctx) {
|
|
19
|
-
if (!ast.isHttpOperationNode(node)) return null
|
|
20
|
-
const { resolver, driver, root } = ctx
|
|
21
|
-
const { output, client, paramsCasing, group } = ctx.options
|
|
22
|
-
|
|
23
|
-
const pluginTs = driver.getPlugin(pluginTsName)
|
|
24
|
-
|
|
25
|
-
if (!pluginTs) {
|
|
26
|
-
return null
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
const tsResolver = driver.getResolver(pluginTsName)
|
|
30
|
-
|
|
31
|
-
const importedTypeNames = resolveOperationTypeNames(node, tsResolver, { paramsCasing, responseStatusNames: 'error' })
|
|
32
|
-
|
|
33
|
-
const meta = {
|
|
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
|
-
),
|
|
39
|
-
fileTs: tsResolver.resolveFile(
|
|
40
|
-
{ name: node.operationId, extname: '.ts', tag: node.tags[0] ?? 'default', path: node.path },
|
|
41
|
-
{
|
|
42
|
-
root,
|
|
43
|
-
output: pluginTs.options?.output ?? output,
|
|
44
|
-
group: pluginTs.options?.group ?? undefined,
|
|
45
|
-
},
|
|
46
|
-
),
|
|
47
|
-
} as const
|
|
48
|
-
|
|
49
|
-
return (
|
|
50
|
-
<File baseName={meta.file.baseName} path={meta.file.path} meta={meta.file.meta}>
|
|
51
|
-
{meta.fileTs && importedTypeNames.length > 0 && (
|
|
52
|
-
<File.Import name={Array.from(new Set(importedTypeNames)).sort()} root={meta.file.path} path={meta.fileTs.path} isTypeOnly />
|
|
53
|
-
)}
|
|
54
|
-
<File.Import name={['CallToolResult', 'ServerNotification', 'ServerRequest']} path={'@modelcontextprotocol/sdk/types'} isTypeOnly />
|
|
55
|
-
<File.Import name={['RequestHandlerExtra']} path={'@modelcontextprotocol/sdk/shared/protocol'} isTypeOnly />
|
|
56
|
-
<File.Import name={['buildFormData']} root={meta.file.path} path={path.resolve(root, '.kubb/config.ts')} />
|
|
57
|
-
{client.importPath ? (
|
|
58
|
-
<>
|
|
59
|
-
<File.Import name={['Client', 'RequestConfig', 'ResponseErrorConfig']} path={client.importPath} isTypeOnly />
|
|
60
|
-
<File.Import name={'client'} path={client.importPath} />
|
|
61
|
-
{client.dataReturnType === 'full' && <File.Import name={['ResponseConfig']} path={client.importPath} isTypeOnly />}
|
|
62
|
-
</>
|
|
63
|
-
) : (
|
|
64
|
-
<>
|
|
65
|
-
<File.Import
|
|
66
|
-
name={['Client', 'RequestConfig', 'ResponseErrorConfig']}
|
|
67
|
-
root={meta.file.path}
|
|
68
|
-
path={path.resolve(root, '.kubb/client.ts')}
|
|
69
|
-
isTypeOnly
|
|
70
|
-
/>
|
|
71
|
-
<File.Import name={['client']} root={meta.file.path} path={path.resolve(root, '.kubb/client.ts')} />
|
|
72
|
-
{client.dataReturnType === 'full' && (
|
|
73
|
-
<File.Import name={['ResponseConfig']} root={meta.file.path} path={path.resolve(root, '.kubb/client.ts')} isTypeOnly />
|
|
74
|
-
)}
|
|
75
|
-
</>
|
|
76
|
-
)}
|
|
77
|
-
|
|
78
|
-
<McpHandler
|
|
79
|
-
name={meta.name}
|
|
80
|
-
node={node}
|
|
81
|
-
resolver={tsResolver}
|
|
82
|
-
baseURL={client.baseURL}
|
|
83
|
-
dataReturnType={client.dataReturnType || 'data'}
|
|
84
|
-
paramsCasing={paramsCasing}
|
|
85
|
-
/>
|
|
86
|
-
</File>
|
|
87
|
-
)
|
|
88
|
-
},
|
|
89
|
-
})
|
|
@@ -1,149 +0,0 @@
|
|
|
1
|
-
import path from 'node:path'
|
|
2
|
-
import { findSuccessStatusCode, getOperationParameters } from '@internals/shared'
|
|
3
|
-
import { ast, defineGenerator } from '@kubb/core'
|
|
4
|
-
import { pluginZodName } from '@kubb/plugin-zod'
|
|
5
|
-
import { File, jsxRendererSync } from '@kubb/renderer-jsx'
|
|
6
|
-
import { Server } from '../components/Server.tsx'
|
|
7
|
-
import type { PluginMcp } from '../types.ts'
|
|
8
|
-
|
|
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>({
|
|
17
|
-
name: 'operations',
|
|
18
|
-
renderer: jsxRendererSync,
|
|
19
|
-
operations(nodes, ctx) {
|
|
20
|
-
const { config, resolver, plugin, driver, root } = ctx
|
|
21
|
-
const { output, paramsCasing, group } = ctx.options
|
|
22
|
-
|
|
23
|
-
const pluginZod = driver.getPlugin(pluginZodName)
|
|
24
|
-
|
|
25
|
-
if (!pluginZod) {
|
|
26
|
-
return
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
const zodResolver = driver.getResolver(pluginZodName)
|
|
30
|
-
|
|
31
|
-
const name = 'server'
|
|
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.filter(ast.isHttpOperationNode).map((node) => {
|
|
47
|
-
const { path: pathParams, query: queryParams, header: headerParams } = getOperationParameters(node, { paramsCasing })
|
|
48
|
-
|
|
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
|
-
|
|
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 ?? undefined,
|
|
60
|
-
},
|
|
61
|
-
)
|
|
62
|
-
|
|
63
|
-
const requestName = node.requestBody?.content?.[0]?.schema ? zodResolver.resolveDataName(node) : null
|
|
64
|
-
const successStatus = findSuccessStatusCode(node.responses)
|
|
65
|
-
const responseName = successStatus ? zodResolver.resolveResponseStatusName(node, successStatus) : null
|
|
66
|
-
|
|
67
|
-
const resolveParams = (params: typeof pathParams) => params.map((p) => ({ name: p.name, schemaName: zodResolver.resolveParamName(node, p) }))
|
|
68
|
-
|
|
69
|
-
return {
|
|
70
|
-
tool: {
|
|
71
|
-
name: node.operationId,
|
|
72
|
-
title: node.summary || undefined,
|
|
73
|
-
description: node.description || `Make a ${node.method.toUpperCase()} request to ${node.path}`,
|
|
74
|
-
},
|
|
75
|
-
mcp: {
|
|
76
|
-
name: resolver.resolveHandlerName(node),
|
|
77
|
-
file: mcpFile,
|
|
78
|
-
},
|
|
79
|
-
zod: {
|
|
80
|
-
pathParams: resolveParams(pathParams),
|
|
81
|
-
queryParams: queryParams.length ? resolveParams(queryParams) : null,
|
|
82
|
-
headerParams: headerParams.length ? resolveParams(headerParams) : null,
|
|
83
|
-
requestName,
|
|
84
|
-
responseName,
|
|
85
|
-
file: zodFile,
|
|
86
|
-
},
|
|
87
|
-
node: node,
|
|
88
|
-
}
|
|
89
|
-
})
|
|
90
|
-
|
|
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((name): name is string => Boolean(name))
|
|
99
|
-
|
|
100
|
-
const uniqueNames = [...new Set(zodNames)].sort()
|
|
101
|
-
|
|
102
|
-
return [
|
|
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)
|
|
106
|
-
})
|
|
107
|
-
|
|
108
|
-
return (
|
|
109
|
-
<>
|
|
110
|
-
<File
|
|
111
|
-
baseName={serverFile.baseName}
|
|
112
|
-
path={serverFile.path}
|
|
113
|
-
meta={serverFile.meta}
|
|
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
|
-
>
|
|
117
|
-
<File.Import name={['McpServer']} path={'@modelcontextprotocol/sdk/server/mcp'} />
|
|
118
|
-
<File.Import name={['z']} path={'zod'} />
|
|
119
|
-
<File.Import name={['StdioServerTransport']} path={'@modelcontextprotocol/sdk/server/stdio'} />
|
|
120
|
-
|
|
121
|
-
{imports}
|
|
122
|
-
<Server
|
|
123
|
-
name={name}
|
|
124
|
-
serverName={ctx.meta.title ?? 'server'}
|
|
125
|
-
serverVersion={ctx.meta.version ?? '0.0.0'}
|
|
126
|
-
paramsCasing={paramsCasing}
|
|
127
|
-
operations={operationsMapped}
|
|
128
|
-
/>
|
|
129
|
-
</File>
|
|
130
|
-
|
|
131
|
-
<File baseName={jsonFile.baseName} path={jsonFile.path} meta={jsonFile.meta}>
|
|
132
|
-
<File.Source name={name}>
|
|
133
|
-
{`
|
|
134
|
-
{
|
|
135
|
-
"mcpServers": {
|
|
136
|
-
"${ctx.meta.title || 'server'}": {
|
|
137
|
-
"type": "stdio",
|
|
138
|
-
"command": "npx",
|
|
139
|
-
"args": ["tsx", "${path.relative(path.dirname(jsonFile.path), serverFile.path)}"]
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
`}
|
|
144
|
-
</File.Source>
|
|
145
|
-
</File>
|
|
146
|
-
</>
|
|
147
|
-
)
|
|
148
|
-
},
|
|
149
|
-
})
|
package/src/index.ts
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
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'
|