@kubb/plugin-mcp 5.0.0-alpha.9 → 5.0.0-beta.10

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/package.json CHANGED
@@ -1,70 +1,65 @@
1
1
  {
2
2
  "name": "@kubb/plugin-mcp",
3
- "version": "5.0.0-alpha.9",
4
- "description": "Model Context Protocol (MCP) plugin for Kubb, generating MCP-compatible tools and schemas from OpenAPI specifications for AI assistants.",
3
+ "version": "5.0.0-beta.10",
4
+ "description": "Generate Model Context Protocol (MCP) tool definitions from your OpenAPI specification. Expose your REST APIs as AI-callable tools for LLMs, Claude, ChatGPT, and other AI assistants.",
5
5
  "keywords": [
6
- "mcp",
7
- "model-context-protocol",
8
6
  "ai",
9
- "llm",
10
- "claude",
11
7
  "ai-tools",
8
+ "code-generation",
9
+ "codegen",
10
+ "kubb",
11
+ "llm",
12
+ "mcp",
13
+ "model-context-protocol",
12
14
  "openapi",
13
15
  "swagger",
14
- "typescript",
15
- "code-generator",
16
- "codegen",
17
- "plugins",
18
- "kubb"
16
+ "typescript"
19
17
  ],
18
+ "license": "MIT",
19
+ "author": "stijnvanhulle",
20
20
  "repository": {
21
21
  "type": "git",
22
- "url": "git+https://github.com/kubb-labs/kubb.git",
22
+ "url": "git+https://github.com/kubb-labs/plugins.git",
23
23
  "directory": "packages/plugin-mcp"
24
24
  },
25
- "license": "MIT",
26
- "author": "stijnvanhulle",
27
- "sideEffects": false,
25
+ "files": [
26
+ "src",
27
+ "dist",
28
+ "extension.yaml",
29
+ "!/**/**.test.**",
30
+ "!/**/__tests__/**",
31
+ "!/**/__snapshots__/**"
32
+ ],
28
33
  "type": "module",
34
+ "sideEffects": false,
35
+ "main": "./dist/index.cjs",
36
+ "module": "./dist/index.js",
37
+ "types": "./dist/index.d.ts",
29
38
  "exports": {
30
39
  ".": {
31
40
  "import": "./dist/index.js",
32
41
  "require": "./dist/index.cjs"
33
42
  },
34
- "./components": {
35
- "import": "./dist/components.js",
36
- "require": "./dist/components.cjs"
37
- },
38
- "./generators": {
39
- "import": "./dist/generators.js",
40
- "require": "./dist/generators.cjs"
41
- },
42
43
  "./package.json": "./package.json"
43
44
  },
44
- "types": "./dist/index.d.ts",
45
- "typesVersions": {
46
- "*": {
47
- "utils": [
48
- "./dist/utils.d.ts"
49
- ],
50
- "hooks": [
51
- "./dist/hooks.d.ts"
52
- ],
53
- "components": [
54
- "./dist/components.d.ts"
55
- ],
56
- "generators": [
57
- "./dist/generators.d.ts"
58
- ]
59
- }
45
+ "publishConfig": {
46
+ "access": "public",
47
+ "registry": "https://registry.npmjs.org/"
48
+ },
49
+ "dependencies": {
50
+ "@kubb/core": "5.0.0-beta.10",
51
+ "@kubb/renderer-jsx": "5.0.0-beta.10",
52
+ "@kubb/plugin-client": "5.0.0-beta.10",
53
+ "@kubb/plugin-ts": "5.0.0-beta.10",
54
+ "@kubb/plugin-zod": "5.0.0-beta.10"
55
+ },
56
+ "devDependencies": {
57
+ "@internals/shared": "0.0.0",
58
+ "@internals/utils": "0.0.0"
59
+ },
60
+ "peerDependencies": {
61
+ "@kubb/renderer-jsx": "5.0.0-beta.10"
60
62
  },
61
- "files": [
62
- "src",
63
- "dist",
64
- "!/**/**.test.**",
65
- "!/**/__tests__/**",
66
- "!/**/__snapshots__/**"
67
- ],
68
63
  "size-limit": [
69
64
  {
70
65
  "path": "./dist/*.js",
@@ -72,32 +67,14 @@
72
67
  "gzip": true
73
68
  }
74
69
  ],
75
- "dependencies": {
76
- "@kubb/react-fabric": "0.14.0",
77
- "@kubb/core": "5.0.0-alpha.9",
78
- "@kubb/oas": "5.0.0-alpha.9",
79
- "@kubb/plugin-client": "5.0.0-alpha.9",
80
- "@kubb/plugin-oas": "5.0.0-alpha.9",
81
- "@kubb/plugin-ts": "5.0.0-alpha.9",
82
- "@kubb/plugin-zod": "5.0.0-alpha.9"
83
- },
84
- "devDependencies": {
85
- "@internals/utils": "0.0.0"
86
- },
87
70
  "engines": {
88
71
  "node": ">=22"
89
72
  },
90
- "publishConfig": {
91
- "access": "public",
92
- "registry": "https://registry.npmjs.org/"
93
- },
94
- "main": "./dist/index.cjs",
95
- "module": "./dist/index.js",
96
73
  "scripts": {
97
74
  "build": "tsdown && size-limit",
98
75
  "clean": "npx rimraf ./dist",
99
- "lint": "bun biome lint .",
100
- "lint:fix": "bun biome lint --fix --unsafe .",
76
+ "lint": "oxlint .",
77
+ "lint:fix": "oxlint --fix .",
101
78
  "release": "pnpm publish --no-git-check",
102
79
  "release:canary": "bash ../../.github/canary.sh && node ../../scripts/build.js canary && pnpm publish --no-git-check",
103
80
  "start": "tsdown --watch",
@@ -0,0 +1,167 @@
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 | 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
+ const urlPath = new URLPath(node.path)
54
+ const contentType = node.requestBody?.content?.[0]?.contentType
55
+ const isFormData = contentType === 'multipart/form-data'
56
+
57
+ const { query: queryParams, header: headerParams } = getOperationParameters(node, { paramsCasing })
58
+ const { path: originalPathParams, query: originalQueryParams, header: originalHeaderParams } = getOperationParameters(node)
59
+
60
+ const requestName = node.requestBody?.content?.[0]?.schema ? resolver.resolveDataName(node) : undefined
61
+ const responseName = resolver.resolveResponseName(node)
62
+
63
+ const errorResponses = node.responses.filter((r) => Number(r.statusCode) >= 400).map((r) => resolver.resolveResponseStatusName(node, r.statusCode))
64
+ const errorType = errorResponses.length > 0 ? errorResponses.join(' | ') : 'Error'
65
+
66
+ const TError = `ResponseErrorConfig<${errorType}>`
67
+ const generics = [responseName, TError, requestName || 'unknown'].filter(Boolean)
68
+
69
+ const paramsNode = ast.createOperationParams(node, {
70
+ paramsType: 'object',
71
+ pathParamsType: 'inline',
72
+ resolver,
73
+ paramsCasing,
74
+ })
75
+ const baseParamsSignature = declarationPrinter.print(paramsNode) ?? ''
76
+ const paramsSignature = baseParamsSignature
77
+ ? `${baseParamsSignature}, request: RequestHandlerExtra<ServerRequest, ServerNotification>`
78
+ : 'request: RequestHandlerExtra<ServerRequest, ServerNotification>'
79
+
80
+ const pathParamsMapping = paramsCasing ? buildTransformedParamsMapping(originalPathParams, camelCase) : undefined
81
+ const queryParamsMapping = paramsCasing ? buildTransformedParamsMapping(originalQueryParams, camelCase) : undefined
82
+ const headerParamsMapping = paramsCasing ? buildTransformedParamsMapping(originalHeaderParams, camelCase) : undefined
83
+
84
+ const contentTypeHeader =
85
+ contentType && contentType !== 'application/json' && contentType !== 'multipart/form-data' ? `'Content-Type': '${contentType}'` : undefined
86
+ const headers = [headerParams.length ? (headerParamsMapping ? '...mappedHeaders' : '...headers') : undefined, contentTypeHeader].filter(Boolean)
87
+
88
+ const fetchConfig: string[] = []
89
+ fetchConfig.push(`method: ${JSON.stringify(node.method.toUpperCase())}`)
90
+ fetchConfig.push(`url: ${urlPath.template}`)
91
+ if (baseURL) fetchConfig.push(`baseURL: \`${baseURL}\``)
92
+ if (queryParams.length) fetchConfig.push(queryParamsMapping ? 'params: mappedParams' : 'params')
93
+ if (requestName) fetchConfig.push(`data: ${isFormData ? 'formData as FormData' : 'requestData'}`)
94
+ if (headers.length) fetchConfig.push(`headers: { ${headers.join(', ')} }`)
95
+
96
+ const callToolResult =
97
+ dataReturnType === 'data'
98
+ ? `return {
99
+ content: [
100
+ {
101
+ type: 'text',
102
+ text: JSON.stringify(res.data)
103
+ }
104
+ ],
105
+ structuredContent: { data: res.data }
106
+ }`
107
+ : `return {
108
+ content: [
109
+ {
110
+ type: 'text',
111
+ text: JSON.stringify(res)
112
+ }
113
+ ],
114
+ structuredContent: { data: res.data }
115
+ }`
116
+
117
+ return (
118
+ <File.Source name={name} isExportable isIndexable>
119
+ <Function
120
+ name={name}
121
+ async
122
+ export
123
+ params={paramsSignature}
124
+ JSDoc={{
125
+ comments: buildOperationComments(node),
126
+ }}
127
+ returnType={'Promise<CallToolResult>'}
128
+ >
129
+ {''}
130
+ <br />
131
+ <br />
132
+ {pathParamsMapping &&
133
+ Object.entries(pathParamsMapping)
134
+ .filter(([originalName, camelCaseName]) => originalName !== camelCaseName && isValidVarName(originalName))
135
+ .map(([originalName, camelCaseName]) => `const ${originalName} = ${camelCaseName}`)
136
+ .join('\n')}
137
+ {pathParamsMapping && (
138
+ <>
139
+ <br />
140
+ <br />
141
+ </>
142
+ )}
143
+ {queryParamsMapping && queryParams.length > 0 && (
144
+ <>
145
+ {buildRemappingCode(queryParamsMapping, 'mappedParams', 'params')}
146
+ <br />
147
+ <br />
148
+ </>
149
+ )}
150
+ {headerParamsMapping && headerParams.length > 0 && (
151
+ <>
152
+ {buildRemappingCode(headerParamsMapping, 'mappedHeaders', 'headers')}
153
+ <br />
154
+ <br />
155
+ </>
156
+ )}
157
+ {requestName && 'const requestData = data'}
158
+ <br />
159
+ {isFormData && requestName && 'const formData = buildFormData(requestData)'}
160
+ <br />
161
+ {`const res = await fetch<${generics.join(', ')}>({ ${fetchConfig.join(', ')} }, request)`}
162
+ <br />
163
+ {callToolResult}
164
+ </Function>
165
+ </File.Source>
166
+ )
167
+ }
@@ -1,16 +1,33 @@
1
- import { camelCase, isValidVarName } from '@internals/utils'
2
- import type { KubbFile } from '@kubb/fabric-core/types'
3
- import type { SchemaObject } from '@kubb/oas'
4
- import type { OperationSchemas } from '@kubb/plugin-oas'
5
- import { isOptional } from '@kubb/plugin-oas/utils'
6
- import { Const, File, FunctionParams } from '@kubb/react-fabric'
7
- import type { FabricReactNode } from '@kubb/react-fabric/types'
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'
8
9
 
9
10
  type Props = {
11
+ /**
12
+ * Variable name for the MCP server instance (e.g. 'server').
13
+ */
10
14
  name: string
15
+ /**
16
+ * Human-readable server name passed to `new McpServer({ name })`.
17
+ */
11
18
  serverName: string
19
+ /**
20
+ * Semantic version string passed to `new McpServer({ version })`.
21
+ */
12
22
  serverVersion: string
13
- paramsCasing?: 'camelcase'
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
+ */
14
31
  operations: Array<{
15
32
  tool: {
16
33
  name: string
@@ -19,99 +36,28 @@ type Props = {
19
36
  }
20
37
  mcp: {
21
38
  name: string
22
- file: KubbFile.File
39
+ file: ast.FileNode
23
40
  }
24
41
  zod: {
25
- name: string
26
- file: KubbFile.File
27
- schemas: OperationSchemas
28
- }
29
- type: {
30
- schemas: OperationSchemas
42
+ pathParams: Array<ZodParam>
43
+ /**
44
+ * Query params — individual schemas to compose into `z.object({ ... })`.
45
+ */
46
+ queryParams?: string | Array<ZodParam>
47
+ /**
48
+ * Header params — individual schemas to compose into `z.object({ ... })`.
49
+ */
50
+ headerParams?: string | Array<ZodParam>
51
+ requestName?: string
52
+ responseName?: string
31
53
  }
54
+ node: ast.OperationNode
32
55
  }>
33
56
  }
34
57
 
35
- type GetParamsProps = {
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
- }
58
+ const keysPrinter = functionPrinter({ mode: 'keys' })
69
59
 
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
- }
113
-
114
- export function Server({ name, serverName, serverVersion, paramsCasing, operations }: Props): FabricReactNode {
60
+ export function Server({ name, serverName, serverVersion, paramsCasing, operations }: Props): KubbReactNode {
115
61
  return (
116
62
  <File.Source name={name} isExportable isIndexable>
117
63
  <Const name={'server'} export>
@@ -124,9 +70,45 @@ export function Server({ name, serverName, serverVersion, paramsCasing, operatio
124
70
  </Const>
125
71
 
126
72
  {operations
127
- .map(({ tool, mcp, zod }) => {
128
- const paramsClient = getParams({ schemas: zod.schemas, paramsCasing })
129
- const outputSchema = zod.schemas.response?.name
73
+ .map(({ tool, mcp, zod, node }) => {
74
+ const { path: pathParams } = getOperationParameters(node, { paramsCasing })
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 (zod.schemas.request?.name || zod.schemas.headerParams?.name || zod.schemas.queryParams?.name || zod.schemas.pathParams?.name) {
121
+ if (inputSchema) {
140
122
  return `
141
123
  server.registerTool(${JSON.stringify(tool.name)}, {
142
124
  ${config},
143
- inputSchema: ${paramsClient.toObjectValue()},
144
- }, async (${paramsClient.toObject()}) => {
145
- return ${mcp.name}(${paramsClient.toObject()})
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}(${paramsClient.toObject()})
135
+ }, async (request) => {
136
+ return ${mcp.name}(request)
155
137
  })
156
138
  `
157
139
  })
158
140
  .filter(Boolean)}
159
141
 
160
- {`
161
- export async function startServer() {
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
  }