@kubb/plugin-mcp 5.0.0-alpha.3 → 5.0.0-alpha.31
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +1027 -73
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +232 -4
- package/dist/index.js +1000 -74
- package/dist/index.js.map +1 -1
- package/package.json +7 -32
- package/src/components/McpHandler.tsx +171 -0
- package/src/components/Server.tsx +87 -105
- package/src/generators/mcpGenerator.tsx +61 -83
- package/src/generators/serverGenerator.tsx +92 -57
- package/src/generators/serverGeneratorLegacy.tsx +138 -0
- package/src/index.ts +11 -1
- package/src/plugin.ts +71 -97
- package/src/presets.ts +25 -0
- package/src/resolvers/resolverMcp.ts +29 -0
- package/src/types.ts +63 -22
- package/src/utils.ts +97 -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-BFPqVSmg.js +0 -274
- package/dist/generators-BFPqVSmg.js.map +0 -1
- package/dist/generators-DQalD1bu.cjs +0 -285
- package/dist/generators-DQalD1bu.cjs.map +0 -1
- package/dist/generators.cjs +0 -4
- package/dist/generators.d.ts +0 -503
- package/dist/generators.js +0 -2
- package/dist/types-CblxgOvZ.d.ts +0 -64
- package/src/components/index.ts +0 -1
- package/src/generators/index.ts +0 -2
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import path from 'node:path'
|
|
2
|
+
import { caseParams } from '@kubb/ast'
|
|
3
|
+
import { defineGenerator } from '@kubb/core'
|
|
4
|
+
import { pluginZodName } from '@kubb/plugin-zod'
|
|
5
|
+
import { File } from '@kubb/react-fabric'
|
|
6
|
+
import { Server } from '../components/Server.tsx'
|
|
7
|
+
import type { PluginMcp } from '../types.ts'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Legacy server generator for `compatibilityPreset: 'kubbV4'`.
|
|
11
|
+
*
|
|
12
|
+
* Uses grouped zod schemas for query/header params (e.g. `createPetsQueryParamsSchema`)
|
|
13
|
+
* and `resolveResponseName` for the combined response schema.
|
|
14
|
+
* Path params are always rendered inline (no named imports).
|
|
15
|
+
*/
|
|
16
|
+
export const serverGeneratorLegacy = defineGenerator<PluginMcp>({
|
|
17
|
+
name: 'operations',
|
|
18
|
+
operations(nodes, options) {
|
|
19
|
+
const { adapter, config, resolver, plugin, driver, root } = this
|
|
20
|
+
const { output, paramsCasing, group } = options
|
|
21
|
+
|
|
22
|
+
const pluginZod = driver.getPlugin(pluginZodName)
|
|
23
|
+
|
|
24
|
+
if (!pluginZod?.resolver) {
|
|
25
|
+
return
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const name = 'server'
|
|
29
|
+
const serverFilePath = path.resolve(root, output.path, 'server.ts')
|
|
30
|
+
const serverFile = {
|
|
31
|
+
baseName: 'server.ts' as const,
|
|
32
|
+
path: serverFilePath,
|
|
33
|
+
meta: { pluginName: plugin.name },
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const jsonFilePath = path.resolve(root, output.path, '.mcp.json')
|
|
37
|
+
const jsonFile = {
|
|
38
|
+
baseName: '.mcp.json' as const,
|
|
39
|
+
path: jsonFilePath,
|
|
40
|
+
meta: { pluginName: plugin.name },
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const operationsMapped = nodes.map((node) => {
|
|
44
|
+
const casedParams = caseParams(node.parameters, paramsCasing)
|
|
45
|
+
const queryParams = casedParams.filter((p) => p.in === 'query')
|
|
46
|
+
const headerParams = casedParams.filter((p) => p.in === 'header')
|
|
47
|
+
|
|
48
|
+
const mcpFile = resolver.resolveFile({ name: node.operationId, extname: '.ts', tag: node.tags[0] ?? 'default', path: node.path }, { root, output, group })
|
|
49
|
+
|
|
50
|
+
const zodFile = pluginZod?.resolver.resolveFile(
|
|
51
|
+
{ name: node.operationId, extname: '.ts', tag: node.tags[0] ?? 'default', path: node.path },
|
|
52
|
+
{
|
|
53
|
+
root,
|
|
54
|
+
output: pluginZod?.options?.output ?? output,
|
|
55
|
+
group: pluginZod?.options?.group,
|
|
56
|
+
},
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
const requestName = node.requestBody?.schema ? pluginZod?.resolver.resolveDataName(node) : undefined
|
|
60
|
+
const responseName = pluginZod?.resolver.resolveResponseName(node)
|
|
61
|
+
|
|
62
|
+
const zodQueryParams = queryParams.length ? pluginZod?.resolver.resolveQueryParamsName(node, queryParams[0]!) : undefined
|
|
63
|
+
|
|
64
|
+
const zodHeaderParams = headerParams.length ? pluginZod?.resolver.resolveHeaderParamsName(node, headerParams[0]!) : undefined
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
tool: {
|
|
68
|
+
name: node.operationId,
|
|
69
|
+
title: node.summary || undefined,
|
|
70
|
+
description: node.description || `Make a ${node.method.toUpperCase()} request to ${node.path}`,
|
|
71
|
+
},
|
|
72
|
+
mcp: {
|
|
73
|
+
name: resolver.resolveName(node.operationId),
|
|
74
|
+
file: mcpFile,
|
|
75
|
+
},
|
|
76
|
+
zod: {
|
|
77
|
+
pathParams: [],
|
|
78
|
+
queryParams: zodQueryParams,
|
|
79
|
+
headerParams: zodHeaderParams,
|
|
80
|
+
requestName,
|
|
81
|
+
responseName,
|
|
82
|
+
file: zodFile,
|
|
83
|
+
},
|
|
84
|
+
node: node,
|
|
85
|
+
}
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
const imports = operationsMapped.flatMap(({ mcp, zod }) => {
|
|
89
|
+
const zodNames = [zod.queryParams, zod.headerParams, zod.requestName, zod.responseName].filter(Boolean) as string[]
|
|
90
|
+
|
|
91
|
+
return [
|
|
92
|
+
<File.Import key={mcp.name} name={[mcp.name]} root={serverFile.path} path={mcp.file.path} />,
|
|
93
|
+
zod.file && zodNames.length > 0 && <File.Import key={`zod-${mcp.name}`} name={zodNames.sort()} root={serverFile.path} path={zod.file.path} />,
|
|
94
|
+
].filter(Boolean)
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
return (
|
|
98
|
+
<>
|
|
99
|
+
<File
|
|
100
|
+
baseName={serverFile.baseName}
|
|
101
|
+
path={serverFile.path}
|
|
102
|
+
meta={serverFile.meta}
|
|
103
|
+
banner={resolver.resolveBanner(adapter.rootNode, { output, config })}
|
|
104
|
+
footer={resolver.resolveFooter(adapter.rootNode, { output, config })}
|
|
105
|
+
>
|
|
106
|
+
<File.Import name={['McpServer']} path={'@modelcontextprotocol/sdk/server/mcp'} />
|
|
107
|
+
<File.Import name={['z']} path={'zod'} />
|
|
108
|
+
<File.Import name={['StdioServerTransport']} path={'@modelcontextprotocol/sdk/server/stdio'} />
|
|
109
|
+
|
|
110
|
+
{imports}
|
|
111
|
+
<Server
|
|
112
|
+
name={name}
|
|
113
|
+
serverName={adapter.rootNode?.meta?.title ?? 'server'}
|
|
114
|
+
serverVersion={(adapter.document as { openapi?: string })?.openapi ?? adapter.rootNode?.meta?.version ?? '0.0.0'}
|
|
115
|
+
paramsCasing={paramsCasing}
|
|
116
|
+
operations={operationsMapped}
|
|
117
|
+
/>
|
|
118
|
+
</File>
|
|
119
|
+
|
|
120
|
+
<File baseName={jsonFile.baseName} path={jsonFile.path} meta={jsonFile.meta}>
|
|
121
|
+
<File.Source name={name}>
|
|
122
|
+
{`
|
|
123
|
+
{
|
|
124
|
+
"mcpServers": {
|
|
125
|
+
"${adapter.rootNode?.meta?.title || 'server'}": {
|
|
126
|
+
"type": "stdio",
|
|
127
|
+
"command": "npx",
|
|
128
|
+
"args": ["tsx", "${path.relative(path.dirname(jsonFile.path), serverFile.path)}"]
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
`}
|
|
133
|
+
</File.Source>
|
|
134
|
+
</File>
|
|
135
|
+
</>
|
|
136
|
+
)
|
|
137
|
+
},
|
|
138
|
+
})
|
package/src/index.ts
CHANGED
|
@@ -1,2 +1,12 @@
|
|
|
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
|
+
export { serverGeneratorLegacy } from './generators/serverGeneratorLegacy.tsx'
|
|
7
|
+
|
|
1
8
|
export { pluginMcp, pluginMcpName } from './plugin.ts'
|
|
2
|
-
|
|
9
|
+
|
|
10
|
+
export { resolverMcp } from './resolvers/resolverMcp.ts'
|
|
11
|
+
|
|
12
|
+
export type { PluginMcp, ResolverMcp } from './types.ts'
|
package/src/plugin.ts
CHANGED
|
@@ -1,111 +1,110 @@
|
|
|
1
1
|
import path from 'node:path'
|
|
2
2
|
import { camelCase } from '@internals/utils'
|
|
3
|
-
import {
|
|
3
|
+
import { createPlugin, type Group, getPreset, mergeGenerators } from '@kubb/core'
|
|
4
4
|
import { pluginClientName } from '@kubb/plugin-client'
|
|
5
5
|
import { source as axiosClientSource } from '@kubb/plugin-client/templates/clients/axios.source'
|
|
6
6
|
import { source as fetchClientSource } from '@kubb/plugin-client/templates/clients/fetch.source'
|
|
7
7
|
import { source as configSource } from '@kubb/plugin-client/templates/config.source'
|
|
8
|
-
import { OperationGenerator, pluginOasName } from '@kubb/plugin-oas'
|
|
9
8
|
import { pluginTsName } from '@kubb/plugin-ts'
|
|
10
9
|
import { pluginZodName } from '@kubb/plugin-zod'
|
|
11
|
-
import {
|
|
10
|
+
import { version } from '../package.json'
|
|
11
|
+
import { presets } from './presets.ts'
|
|
12
12
|
import type { PluginMcp } from './types.ts'
|
|
13
13
|
|
|
14
14
|
export const pluginMcpName = 'plugin-mcp' satisfies PluginMcp['name']
|
|
15
15
|
|
|
16
|
-
export const pluginMcp =
|
|
16
|
+
export const pluginMcp = createPlugin<PluginMcp>((options) => {
|
|
17
17
|
const {
|
|
18
18
|
output = { path: 'mcp', barrelType: 'named' },
|
|
19
19
|
group,
|
|
20
20
|
exclude = [],
|
|
21
21
|
include,
|
|
22
22
|
override = [],
|
|
23
|
-
transformers = {},
|
|
24
|
-
generators = [mcpGenerator, serverGenerator].filter(Boolean),
|
|
25
|
-
contentType,
|
|
26
23
|
paramsCasing,
|
|
27
24
|
client,
|
|
25
|
+
compatibilityPreset = 'default',
|
|
26
|
+
resolver: userResolver,
|
|
27
|
+
transformer: userTransformer,
|
|
28
|
+
generators: userGenerators = [],
|
|
28
29
|
} = options
|
|
29
30
|
|
|
30
31
|
const clientName = client?.client ?? 'axios'
|
|
31
32
|
const clientImportPath = client?.importPath ?? (!client?.bundle ? `@kubb/plugin-client/clients/${clientName}` : undefined)
|
|
32
33
|
|
|
34
|
+
const preset = getPreset({
|
|
35
|
+
preset: compatibilityPreset,
|
|
36
|
+
presets,
|
|
37
|
+
resolver: userResolver,
|
|
38
|
+
transformer: userTransformer,
|
|
39
|
+
generators: userGenerators,
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
const generators = preset.generators ?? []
|
|
43
|
+
const mergedGenerator = mergeGenerators(generators)
|
|
44
|
+
|
|
33
45
|
return {
|
|
34
46
|
name: pluginMcpName,
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
paramsCasing,
|
|
39
|
-
client: {
|
|
40
|
-
client: clientName,
|
|
41
|
-
clientType: client?.clientType ?? 'function',
|
|
42
|
-
importPath: clientImportPath,
|
|
43
|
-
dataReturnType: client?.dataReturnType ?? 'data',
|
|
44
|
-
bundle: client?.bundle,
|
|
45
|
-
baseURL: client?.baseURL,
|
|
46
|
-
paramsCasing: client?.paramsCasing,
|
|
47
|
-
},
|
|
47
|
+
version,
|
|
48
|
+
get resolver() {
|
|
49
|
+
return preset.resolver
|
|
48
50
|
},
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
const root = path.resolve(this.config.root, this.config.output.path)
|
|
52
|
-
const mode = pathMode ?? getMode(path.resolve(root, output.path))
|
|
53
|
-
|
|
54
|
-
if (mode === 'single') {
|
|
55
|
-
/**
|
|
56
|
-
* when output is a file then we will always append to the same file(output file), see fileManager.addOrAppend
|
|
57
|
-
* Other plugins then need to call addOrAppend instead of just add from the fileManager class
|
|
58
|
-
*/
|
|
59
|
-
return path.resolve(root, output.path)
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
if (group && (options?.group?.path || options?.group?.tag)) {
|
|
63
|
-
const groupName: Group['name'] = group?.name
|
|
64
|
-
? group.name
|
|
65
|
-
: (ctx) => {
|
|
66
|
-
if (group?.type === 'path') {
|
|
67
|
-
return `${ctx.group.split('/')[1]}`
|
|
68
|
-
}
|
|
69
|
-
return `${camelCase(ctx.group)}Requests`
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
return path.resolve(
|
|
73
|
-
root,
|
|
74
|
-
output.path,
|
|
75
|
-
groupName({
|
|
76
|
-
group: group.type === 'path' ? options.group.path! : options.group.tag!,
|
|
77
|
-
}),
|
|
78
|
-
baseName,
|
|
79
|
-
)
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
return path.resolve(root, output.path, baseName)
|
|
51
|
+
get transformer() {
|
|
52
|
+
return preset.transformer
|
|
83
53
|
},
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
54
|
+
get options() {
|
|
55
|
+
return {
|
|
56
|
+
output,
|
|
57
|
+
exclude,
|
|
58
|
+
include,
|
|
59
|
+
override,
|
|
60
|
+
group: group
|
|
61
|
+
? ({
|
|
62
|
+
...group,
|
|
63
|
+
name: group.name
|
|
64
|
+
? group.name
|
|
65
|
+
: (ctx: { group: string }) => {
|
|
66
|
+
if (group.type === 'path') {
|
|
67
|
+
return `${ctx.group.split('/')[1]}`
|
|
68
|
+
}
|
|
69
|
+
return `${camelCase(ctx.group)}Requests`
|
|
70
|
+
},
|
|
71
|
+
} satisfies Group)
|
|
72
|
+
: undefined,
|
|
73
|
+
paramsCasing,
|
|
74
|
+
client: {
|
|
75
|
+
client: clientName,
|
|
76
|
+
clientType: client?.clientType ?? 'function',
|
|
77
|
+
importPath: clientImportPath,
|
|
78
|
+
dataReturnType: client?.dataReturnType ?? 'data',
|
|
79
|
+
bundle: client?.bundle,
|
|
80
|
+
baseURL: client?.baseURL,
|
|
81
|
+
paramsCasing: client?.paramsCasing,
|
|
82
|
+
},
|
|
83
|
+
resolver: preset.resolver,
|
|
91
84
|
}
|
|
92
|
-
|
|
93
|
-
return resolvedName
|
|
94
85
|
},
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
86
|
+
pre: [pluginTsName, pluginZodName].filter(Boolean),
|
|
87
|
+
async schema(node, options) {
|
|
88
|
+
return mergedGenerator.schema?.call(this, node, options)
|
|
89
|
+
},
|
|
90
|
+
async operation(node, options) {
|
|
91
|
+
return mergedGenerator.operation?.call(this, node, options)
|
|
92
|
+
},
|
|
93
|
+
async operations(nodes, options) {
|
|
94
|
+
return mergedGenerator.operations?.call(this, nodes, options)
|
|
95
|
+
},
|
|
96
|
+
async buildStart() {
|
|
97
|
+
const { adapter, driver } = this
|
|
98
|
+
const root = this.root
|
|
100
99
|
|
|
100
|
+
const baseURL = adapter?.rootNode?.meta?.baseURL
|
|
101
101
|
if (baseURL) {
|
|
102
|
-
this.plugin.options.client.baseURL = baseURL
|
|
102
|
+
this.plugin.options.client.baseURL = this.plugin.options.client.baseURL || baseURL
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
-
const hasClientPlugin = !!
|
|
105
|
+
const hasClientPlugin = !!driver.getPlugin(pluginClientName)
|
|
106
106
|
|
|
107
107
|
if (this.plugin.options.client.bundle && !hasClientPlugin && !this.plugin.options.client.importPath) {
|
|
108
|
-
// pre add bundled fetch
|
|
109
108
|
await this.addFile({
|
|
110
109
|
baseName: 'fetch.ts',
|
|
111
110
|
path: path.resolve(root, '.kubb/fetch.ts'),
|
|
@@ -139,32 +138,7 @@ export const pluginMcp = definePlugin<PluginMcp>((options) => {
|
|
|
139
138
|
})
|
|
140
139
|
}
|
|
141
140
|
|
|
142
|
-
|
|
143
|
-
fabric: this.fabric,
|
|
144
|
-
oas,
|
|
145
|
-
pluginManager: this.pluginManager,
|
|
146
|
-
events: this.events,
|
|
147
|
-
plugin: this.plugin,
|
|
148
|
-
contentType,
|
|
149
|
-
exclude,
|
|
150
|
-
include,
|
|
151
|
-
override,
|
|
152
|
-
mode,
|
|
153
|
-
})
|
|
154
|
-
|
|
155
|
-
const files = await operationGenerator.build(...generators)
|
|
156
|
-
await this.upsertFile(...files)
|
|
157
|
-
|
|
158
|
-
const barrelFiles = await getBarrelFiles(this.fabric.files, {
|
|
159
|
-
type: output.barrelType ?? 'named',
|
|
160
|
-
root,
|
|
161
|
-
output,
|
|
162
|
-
meta: {
|
|
163
|
-
pluginName: this.plugin.name,
|
|
164
|
-
},
|
|
165
|
-
})
|
|
166
|
-
|
|
167
|
-
await this.upsertFile(...barrelFiles)
|
|
141
|
+
await this.openInStudio({ ast: true })
|
|
168
142
|
},
|
|
169
143
|
}
|
|
170
144
|
})
|
package/src/presets.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { definePresets } from '@kubb/core'
|
|
2
|
+
import { mcpGenerator } from './generators/mcpGenerator.tsx'
|
|
3
|
+
import { serverGenerator } from './generators/serverGenerator.tsx'
|
|
4
|
+
import { serverGeneratorLegacy } from './generators/serverGeneratorLegacy.tsx'
|
|
5
|
+
import { resolverMcp } from './resolvers/resolverMcp.ts'
|
|
6
|
+
import type { ResolverMcp } from './types.ts'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Built-in preset registry for `@kubb/plugin-mcp`.
|
|
10
|
+
*
|
|
11
|
+
* - `default` — v5 naming with individual zod schemas and per-status responses.
|
|
12
|
+
* - `kubbV4` — legacy naming with grouped zod schemas and combined responses.
|
|
13
|
+
*/
|
|
14
|
+
export const presets = definePresets<ResolverMcp>({
|
|
15
|
+
default: {
|
|
16
|
+
name: 'default',
|
|
17
|
+
resolver: resolverMcp,
|
|
18
|
+
generators: [mcpGenerator, serverGenerator],
|
|
19
|
+
},
|
|
20
|
+
kubbV4: {
|
|
21
|
+
name: 'kubbV4',
|
|
22
|
+
resolver: resolverMcp,
|
|
23
|
+
generators: [mcpGenerator, serverGeneratorLegacy],
|
|
24
|
+
},
|
|
25
|
+
})
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { camelCase } from '@internals/utils'
|
|
2
|
+
import { defineResolver } from '@kubb/core'
|
|
3
|
+
import type { PluginMcp } from '../types.ts'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Resolver for `@kubb/plugin-mcp` that provides the default naming
|
|
7
|
+
* and path-resolution helpers used by the plugin.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```ts
|
|
11
|
+
* import { resolverMcp } from '@kubb/plugin-mcp'
|
|
12
|
+
*
|
|
13
|
+
* resolverMcp.default('addPet', 'function') // -> 'addPetHandler'
|
|
14
|
+
* resolverMcp.resolveName('show pet by id') // -> 'showPetByIdHandler'
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
export const resolverMcp = defineResolver<PluginMcp>(() => ({
|
|
18
|
+
name: 'default',
|
|
19
|
+
pluginName: 'plugin-mcp',
|
|
20
|
+
default(name, type) {
|
|
21
|
+
if (type === 'file') {
|
|
22
|
+
return camelCase(name, { isFile: true })
|
|
23
|
+
}
|
|
24
|
+
return camelCase(name, { suffix: 'handler' })
|
|
25
|
+
},
|
|
26
|
+
resolveName(name) {
|
|
27
|
+
return this.default(name, 'function')
|
|
28
|
+
},
|
|
29
|
+
}))
|
package/src/types.ts
CHANGED
|
@@ -1,21 +1,41 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import type { Visitor } from '@kubb/ast/types'
|
|
2
|
+
import type {
|
|
3
|
+
CompatibilityPreset,
|
|
4
|
+
Exclude,
|
|
5
|
+
Generator,
|
|
6
|
+
Group,
|
|
7
|
+
Include,
|
|
8
|
+
Output,
|
|
9
|
+
Override,
|
|
10
|
+
PluginFactoryOptions,
|
|
11
|
+
ResolvePathOptions,
|
|
12
|
+
Resolver,
|
|
13
|
+
UserGroup,
|
|
14
|
+
} from '@kubb/core'
|
|
4
15
|
import type { ClientImportPath, PluginClient } from '@kubb/plugin-client'
|
|
5
|
-
|
|
6
|
-
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The concrete resolver type for `@kubb/plugin-mcp`.
|
|
19
|
+
* Extends the base `Resolver` with a `resolveName` helper for MCP handler function names.
|
|
20
|
+
*/
|
|
21
|
+
export type ResolverMcp = Resolver & {
|
|
22
|
+
/**
|
|
23
|
+
* Resolves the handler function name for a given raw operation name.
|
|
24
|
+
* @example
|
|
25
|
+
* resolver.resolveName('show pet by id') // -> 'showPetByIdHandler'
|
|
26
|
+
*/
|
|
27
|
+
resolveName(this: ResolverMcp, name: string): string
|
|
28
|
+
}
|
|
7
29
|
|
|
8
30
|
export type Options = {
|
|
9
31
|
/**
|
|
10
|
-
* Specify the export location for the files and define the behavior of the output
|
|
32
|
+
* Specify the export location for the files and define the behavior of the output.
|
|
11
33
|
* @default { path: 'mcp', barrelType: 'named' }
|
|
12
34
|
*/
|
|
13
|
-
output?: Output
|
|
35
|
+
output?: Output
|
|
14
36
|
/**
|
|
15
|
-
*
|
|
16
|
-
* By default, the first JSON valid mediaType is used
|
|
37
|
+
* Client configuration for HTTP request generation.
|
|
17
38
|
*/
|
|
18
|
-
contentType?: contentType
|
|
19
39
|
client?: ClientImportPath & Pick<PluginClient['options'], 'clientType' | 'dataReturnType' | 'baseURL' | 'bundle' | 'paramsCasing'>
|
|
20
40
|
/**
|
|
21
41
|
* Transform parameter names to a specific casing format.
|
|
@@ -25,9 +45,9 @@ export type Options = {
|
|
|
25
45
|
*/
|
|
26
46
|
paramsCasing?: 'camelcase'
|
|
27
47
|
/**
|
|
28
|
-
* Group the
|
|
48
|
+
* Group the MCP requests based on the provided name.
|
|
29
49
|
*/
|
|
30
|
-
group?:
|
|
50
|
+
group?: UserGroup
|
|
31
51
|
/**
|
|
32
52
|
* Array containing exclude parameters to exclude/skip tags/operations/methods/paths.
|
|
33
53
|
*/
|
|
@@ -40,23 +60,44 @@ export type Options = {
|
|
|
40
60
|
* Array containing override parameters to override `options` based on tags/operations/methods/paths.
|
|
41
61
|
*/
|
|
42
62
|
override?: Array<Override<ResolvedOptions>>
|
|
43
|
-
transformers?: {
|
|
44
|
-
/**
|
|
45
|
-
* Customize the names based on the type that is provided by the plugin.
|
|
46
|
-
*/
|
|
47
|
-
name?: (name: ResolveNameParams['name'], type?: ResolveNameParams['type']) => string
|
|
48
|
-
}
|
|
49
63
|
/**
|
|
50
|
-
*
|
|
64
|
+
* Apply a compatibility naming preset.
|
|
65
|
+
* @default 'default'
|
|
66
|
+
*/
|
|
67
|
+
compatibilityPreset?: CompatibilityPreset
|
|
68
|
+
/**
|
|
69
|
+
* A single resolver whose methods override the default resolver's naming conventions.
|
|
70
|
+
* When a method returns `null` or `undefined`, the default resolver's result is used instead.
|
|
71
|
+
*/
|
|
72
|
+
resolver?: Partial<ResolverMcp> & ThisType<ResolverMcp>
|
|
73
|
+
/**
|
|
74
|
+
* A single AST visitor applied before printing.
|
|
75
|
+
* When a visitor method returns `null` or `undefined`, the preset transformer's result is used instead.
|
|
76
|
+
*/
|
|
77
|
+
transformer?: Visitor
|
|
78
|
+
/**
|
|
79
|
+
* Define some generators next to the default MCP generators.
|
|
51
80
|
*/
|
|
52
81
|
generators?: Array<Generator<PluginMcp>>
|
|
53
82
|
}
|
|
54
83
|
|
|
55
84
|
type ResolvedOptions = {
|
|
56
|
-
output: Output
|
|
57
|
-
|
|
85
|
+
output: Output
|
|
86
|
+
exclude: Array<Exclude>
|
|
87
|
+
include: Array<Include> | undefined
|
|
88
|
+
override: Array<Override<ResolvedOptions>>
|
|
89
|
+
group: Group | undefined
|
|
58
90
|
client: Pick<PluginClient['options'], 'client' | 'clientType' | 'dataReturnType' | 'importPath' | 'baseURL' | 'bundle' | 'paramsCasing'>
|
|
59
91
|
paramsCasing: Options['paramsCasing']
|
|
92
|
+
resolver: ResolverMcp
|
|
60
93
|
}
|
|
61
94
|
|
|
62
|
-
export type PluginMcp = PluginFactoryOptions<'plugin-mcp', Options, ResolvedOptions, never, ResolvePathOptions>
|
|
95
|
+
export type PluginMcp = PluginFactoryOptions<'plugin-mcp', Options, ResolvedOptions, never, ResolvePathOptions, ResolverMcp>
|
|
96
|
+
|
|
97
|
+
declare global {
|
|
98
|
+
namespace Kubb {
|
|
99
|
+
interface PluginRegistry {
|
|
100
|
+
'plugin-mcp': PluginMcp
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
package/src/utils.ts
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { camelCase } from '@internals/utils'
|
|
2
|
+
import type { OperationNode, SchemaNode, StatusCode } from '@kubb/ast/types'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Find the first 2xx response status code from an operation's responses.
|
|
6
|
+
*/
|
|
7
|
+
export function findSuccessStatusCode(responses: Array<{ statusCode: number | string }>): StatusCode | undefined {
|
|
8
|
+
for (const res of responses) {
|
|
9
|
+
const code = Number(res.statusCode)
|
|
10
|
+
if (code >= 200 && code < 300) {
|
|
11
|
+
return res.statusCode as StatusCode
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
return undefined
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export type ZodParam = {
|
|
18
|
+
name: string
|
|
19
|
+
schemaName: string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Render a group param value — either a group schema name directly (kubbV4),
|
|
24
|
+
* or compose individual schemas into `z.object({ ... })` (v5).
|
|
25
|
+
*/
|
|
26
|
+
export function zodGroupExpr(entry: string | Array<ZodParam>): string {
|
|
27
|
+
if (typeof entry === 'string') {
|
|
28
|
+
return entry
|
|
29
|
+
}
|
|
30
|
+
const entries = entry.map((p) => `${JSON.stringify(p.name)}: ${p.schemaName}`)
|
|
31
|
+
return `z.object({ ${entries.join(', ')} })`
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Build JSDoc comment lines from an OperationNode.
|
|
36
|
+
*/
|
|
37
|
+
export function getComments(node: OperationNode): Array<string> {
|
|
38
|
+
return [
|
|
39
|
+
node.description && `@description ${node.description}`,
|
|
40
|
+
node.summary && `@summary ${node.summary}`,
|
|
41
|
+
node.deprecated && '@deprecated',
|
|
42
|
+
`{@link ${node.path.replaceAll('{', ':').replaceAll('}', '')}}`,
|
|
43
|
+
].filter((x): x is string => Boolean(x))
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Build a mapping of original param names → camelCase names.
|
|
48
|
+
* Returns `undefined` when no names actually change (no remapping needed).
|
|
49
|
+
*/
|
|
50
|
+
export function getParamsMapping(params: Array<{ name: string }>): Record<string, string> | undefined {
|
|
51
|
+
if (!params.length) {
|
|
52
|
+
return undefined
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const mapping: Record<string, string> = {}
|
|
56
|
+
let hasDifference = false
|
|
57
|
+
|
|
58
|
+
for (const p of params) {
|
|
59
|
+
const camelName = camelCase(p.name)
|
|
60
|
+
mapping[p.name] = camelName
|
|
61
|
+
if (p.name !== camelName) {
|
|
62
|
+
hasDifference = true
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return hasDifference ? mapping : undefined
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Convert a SchemaNode type to an inline Zod expression string.
|
|
71
|
+
* Used as fallback when no named zod schema is available for a path parameter.
|
|
72
|
+
*/
|
|
73
|
+
export function zodExprFromSchemaNode(schema: SchemaNode): string {
|
|
74
|
+
let expr: string
|
|
75
|
+
switch (schema.type) {
|
|
76
|
+
case 'integer':
|
|
77
|
+
expr = 'z.coerce.number()'
|
|
78
|
+
break
|
|
79
|
+
case 'number':
|
|
80
|
+
expr = 'z.number()'
|
|
81
|
+
break
|
|
82
|
+
case 'boolean':
|
|
83
|
+
expr = 'z.boolean()'
|
|
84
|
+
break
|
|
85
|
+
case 'array':
|
|
86
|
+
expr = 'z.array(z.unknown())'
|
|
87
|
+
break
|
|
88
|
+
default:
|
|
89
|
+
expr = 'z.string()'
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (schema.nullable) {
|
|
93
|
+
expr = `${expr}.nullable()`
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return expr
|
|
97
|
+
}
|