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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/plugin.ts CHANGED
@@ -1,170 +1,122 @@
1
1
  import path from 'node:path'
2
2
  import { camelCase } from '@internals/utils'
3
- import { createPlugin, type Group, getBarrelFiles, getMode } from '@kubb/core'
3
+
4
+ import { ast, definePlugin, type Group } from '@kubb/core'
4
5
  import { pluginClientName } from '@kubb/plugin-client'
5
6
  import { source as axiosClientSource } from '@kubb/plugin-client/templates/clients/axios.source'
6
7
  import { source as fetchClientSource } from '@kubb/plugin-client/templates/clients/fetch.source'
7
8
  import { source as configSource } from '@kubb/plugin-client/templates/config.source'
8
- import { OperationGenerator, pluginOasName } from '@kubb/plugin-oas'
9
9
  import { pluginTsName } from '@kubb/plugin-ts'
10
10
  import { pluginZodName } from '@kubb/plugin-zod'
11
- import { mcpGenerator, serverGenerator } from './generators'
11
+ import { mcpGenerator } from './generators/mcpGenerator.tsx'
12
+ import { serverGenerator } from './generators/serverGenerator.tsx'
13
+ import { resolverMcp } from './resolvers/resolverMcp.ts'
12
14
  import type { PluginMcp } from './types.ts'
13
15
 
14
16
  export const pluginMcpName = 'plugin-mcp' satisfies PluginMcp['name']
15
17
 
16
- export const pluginMcp = createPlugin<PluginMcp>((options) => {
18
+ export const pluginMcp = definePlugin<PluginMcp>((options) => {
17
19
  const {
18
20
  output = { path: 'mcp', barrelType: 'named' },
19
21
  group,
20
22
  exclude = [],
21
23
  include,
22
24
  override = [],
23
- transformers = {},
24
- generators = [mcpGenerator, serverGenerator].filter(Boolean),
25
- contentType,
26
25
  paramsCasing,
27
26
  client,
27
+ resolver: userResolver,
28
+ transformer: userTransformer,
29
+ generators: userGenerators = [],
28
30
  } = options
29
31
 
30
32
  const clientName = client?.client ?? 'axios'
31
33
  const clientImportPath = client?.importPath ?? (!client?.bundle ? `@kubb/plugin-client/clients/${clientName}` : undefined)
32
34
 
33
- return {
34
- name: pluginMcpName,
35
- options: {
36
- output,
37
- group,
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
- },
48
- },
49
- pre: [pluginOasName, pluginTsName, pluginZodName].filter(Boolean),
50
- resolvePath(baseName, pathMode, options) {
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
35
+ const groupConfig = group
36
+ ? ({
37
+ ...group,
38
+ name: group.name
64
39
  ? group.name
65
- : (ctx) => {
66
- if (group?.type === 'path') {
40
+ : (ctx: { group: string }) => {
41
+ if (group.type === 'path') {
67
42
  return `${ctx.group.split('/')[1]}`
68
43
  }
69
44
  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)
83
- },
84
- resolveName(name, type) {
85
- const resolvedName = camelCase(name, {
86
- isFile: type === 'file',
87
- })
88
-
89
- if (type) {
90
- return transformers?.name?.(resolvedName, type) || resolvedName
91
- }
92
-
93
- return resolvedName
94
- },
95
- async install() {
96
- const root = path.resolve(this.config.root, this.config.output.path)
97
- const mode = getMode(path.resolve(root, output.path))
98
- const oas = await this.getOas()
99
- const baseURL = await this.getBaseURL()
100
-
101
- if (baseURL) {
102
- this.plugin.options.client.baseURL = baseURL
103
- }
104
-
105
- const hasClientPlugin = !!this.driver.getPluginByName(pluginClientName)
106
-
107
- if (this.plugin.options.client.bundle && !hasClientPlugin && !this.plugin.options.client.importPath) {
108
- // pre add bundled fetch
109
- await this.addFile({
110
- baseName: 'fetch.ts',
111
- path: path.resolve(root, '.kubb/fetch.ts'),
112
- sources: [
113
- {
114
- name: 'fetch',
115
- value: this.plugin.options.client.client === 'fetch' ? fetchClientSource : axiosClientSource,
116
- isExportable: true,
117
- isIndexable: true,
118
45
  },
119
- ],
120
- imports: [],
121
- exports: [],
122
- })
123
- }
46
+ } satisfies Group)
47
+ : undefined
124
48
 
125
- if (!hasClientPlugin) {
126
- await this.addFile({
127
- baseName: 'config.ts',
128
- path: path.resolve(root, '.kubb/config.ts'),
129
- sources: [
130
- {
131
- name: 'config',
132
- value: configSource,
133
- isExportable: false,
134
- isIndexable: false,
135
- },
136
- ],
137
- imports: [],
138
- exports: [],
49
+ return {
50
+ name: pluginMcpName,
51
+ options,
52
+ dependencies: [pluginTsName, pluginZodName],
53
+ hooks: {
54
+ 'kubb:plugin:setup'(ctx) {
55
+ const resolver = userResolver ? { ...resolverMcp, ...userResolver } : resolverMcp
56
+
57
+ ctx.setOptions({
58
+ output,
59
+ exclude,
60
+ include,
61
+ override,
62
+ group: groupConfig,
63
+ paramsCasing,
64
+ client: {
65
+ client: clientName,
66
+ clientType: client?.clientType ?? 'function',
67
+ importPath: clientImportPath,
68
+ dataReturnType: client?.dataReturnType ?? 'data',
69
+ bundle: client?.bundle,
70
+ baseURL: client?.baseURL,
71
+ paramsCasing: client?.paramsCasing,
72
+ },
73
+ resolver,
139
74
  })
140
- }
141
-
142
- const operationGenerator = new OperationGenerator(this.plugin.options, {
143
- fabric: this.fabric,
144
- oas,
145
- driver: this.driver,
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)
75
+ ctx.setResolver(resolver)
76
+ if (userTransformer) {
77
+ ctx.setTransformer(userTransformer)
78
+ }
79
+ ctx.addGenerator(mcpGenerator)
80
+ ctx.addGenerator(serverGenerator)
81
+ for (const gen of userGenerators) {
82
+ ctx.addGenerator(gen)
83
+ }
84
+
85
+ const root = path.resolve(ctx.config.root, ctx.config.output.path)
86
+ const hasClientPlugin = ctx.config.plugins?.some((p) => p.name === pluginClientName)
87
+
88
+ if (client?.bundle && !hasClientPlugin && !clientImportPath) {
89
+ ctx.injectFile({
90
+ baseName: 'fetch.ts',
91
+ path: path.resolve(root, '.kubb/fetch.ts'),
92
+ sources: [
93
+ ast.createSource({
94
+ name: 'fetch',
95
+ nodes: [ast.createText(clientName === 'fetch' ? fetchClientSource : axiosClientSource)],
96
+ isExportable: true,
97
+ isIndexable: true,
98
+ }),
99
+ ],
100
+ })
101
+ }
102
+
103
+ if (!hasClientPlugin) {
104
+ ctx.injectFile({
105
+ baseName: 'config.ts',
106
+ path: path.resolve(root, '.kubb/config.ts'),
107
+ sources: [
108
+ ast.createSource({
109
+ name: 'config',
110
+ nodes: [ast.createText(configSource)],
111
+ isExportable: false,
112
+ isIndexable: false,
113
+ }),
114
+ ],
115
+ })
116
+ }
117
+ },
168
118
  },
169
119
  }
170
120
  })
121
+
122
+ export default pluginMcp
@@ -0,0 +1,25 @@
1
+ import { camelCase } from '@internals/utils'
2
+ import { defineResolver } from '@kubb/core'
3
+ import type { PluginMcp } from '../types.ts'
4
+
5
+ /**
6
+ * Naming convention resolver for MCP plugin.
7
+ *
8
+ * Provides default naming helpers using camelCase with a `handler` suffix for functions.
9
+ *
10
+ * @example
11
+ * `resolverMcp.default('addPet', 'function') // → 'addPetHandler'`
12
+ */
13
+ export const resolverMcp = defineResolver<PluginMcp>((ctx) => ({
14
+ name: 'default',
15
+ pluginName: 'plugin-mcp',
16
+ default(name, type) {
17
+ if (type === 'file') {
18
+ return camelCase(name, { isFile: true })
19
+ }
20
+ return camelCase(name, { suffix: 'handler' })
21
+ },
22
+ resolveName(name) {
23
+ return ctx.default(name, 'function')
24
+ },
25
+ }))
package/src/types.ts CHANGED
@@ -1,62 +1,80 @@
1
- import type { Group, Output, PluginFactoryOptions, ResolveNameParams } from '@kubb/core'
2
-
3
- import type { contentType, Oas } from '@kubb/oas'
1
+ import type { ast, Exclude, Generator, Group, Include, Output, Override, PluginFactoryOptions, Resolver } from '@kubb/core'
4
2
  import type { ClientImportPath, PluginClient } from '@kubb/plugin-client'
5
- import type { Exclude, Include, Override, ResolvePathOptions } from '@kubb/plugin-oas'
6
- import type { Generator } from '@kubb/plugin-oas/generators'
3
+
4
+ /**
5
+ * Resolver for MCP that provides naming methods for handler functions.
6
+ */
7
+ export type ResolverMcp = Resolver & {
8
+ /**
9
+ * Resolves the handler function name for an operation.
10
+ *
11
+ * @example Resolving handler function names
12
+ * `resolver.resolveName('show pet by id') // -> 'showPetByIdHandler'`
13
+ */
14
+ resolveName(this: ResolverMcp, name: string): string
15
+ }
7
16
 
8
17
  export type Options = {
9
18
  /**
10
- * Specify the export location for the files and define the behavior of the output
19
+ * Specify the export location for the files and define the behavior of the output.
11
20
  * @default { path: 'mcp', barrelType: 'named' }
12
21
  */
13
- output?: Output<Oas>
22
+ output?: Output
14
23
  /**
15
- * Define which contentType should be used.
16
- * By default, the first JSON valid mediaType is used
24
+ * Client configuration for HTTP request generation.
17
25
  */
18
- contentType?: contentType
19
26
  client?: ClientImportPath & Pick<PluginClient['options'], 'clientType' | 'dataReturnType' | 'baseURL' | 'bundle' | 'paramsCasing'>
20
27
  /**
21
- * Transform parameter names to a specific casing format.
22
- * When set to 'camelcase', parameter names in path, query, and header params will be transformed to camelCase.
23
- * This should match the paramsCasing setting used in @kubb/plugin-ts.
24
- * @default undefined
28
+ * Apply casing to parameter names to match your configuration.
25
29
  */
26
30
  paramsCasing?: 'camelcase'
27
31
  /**
28
- * Group the mcp requests based on the provided name.
32
+ * Group the MCP requests based on the provided name.
29
33
  */
30
34
  group?: Group
31
35
  /**
32
- * Array containing exclude parameters to exclude/skip tags/operations/methods/paths.
36
+ * Tags, operations, or paths to exclude from generation.
33
37
  */
34
38
  exclude?: Array<Exclude>
35
39
  /**
36
- * Array containing include parameters to include tags/operations/methods/paths.
40
+ * Tags, operations, or paths to include in generation.
37
41
  */
38
42
  include?: Array<Include>
39
43
  /**
40
- * Array containing override parameters to override `options` based on tags/operations/methods/paths.
44
+ * Override options for specific tags, operations, or paths.
41
45
  */
42
46
  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
47
  /**
50
- * Define some generators next to the Mcp generators.
48
+ * Override naming conventions for function names and types.
49
+ */
50
+ resolver?: Partial<ResolverMcp> & ThisType<ResolverMcp>
51
+ /**
52
+ * AST visitor to transform generated nodes.
53
+ */
54
+ transformer?: ast.Visitor
55
+ /**
56
+ * Additional generators alongside the default generators.
51
57
  */
52
58
  generators?: Array<Generator<PluginMcp>>
53
59
  }
54
60
 
55
61
  type ResolvedOptions = {
56
- output: Output<Oas>
57
- group: Options['group']
62
+ output: Output
63
+ exclude: Array<Exclude>
64
+ include: Array<Include> | undefined
65
+ override: Array<Override<ResolvedOptions>>
66
+ group: Group | undefined
58
67
  client: Pick<PluginClient['options'], 'client' | 'clientType' | 'dataReturnType' | 'importPath' | 'baseURL' | 'bundle' | 'paramsCasing'>
59
68
  paramsCasing: Options['paramsCasing']
69
+ resolver: ResolverMcp
60
70
  }
61
71
 
62
- export type PluginMcp = PluginFactoryOptions<'plugin-mcp', Options, ResolvedOptions, never, ResolvePathOptions>
72
+ export type PluginMcp = PluginFactoryOptions<'plugin-mcp', Options, ResolvedOptions, ResolverMcp>
73
+
74
+ declare global {
75
+ namespace Kubb {
76
+ interface PluginRegistry {
77
+ 'plugin-mcp': PluginMcp
78
+ }
79
+ }
80
+ }
package/src/utils.ts ADDED
@@ -0,0 +1,113 @@
1
+ import { camelCase } from '@internals/utils'
2
+ import type { ast } from '@kubb/core'
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 }>): ast.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 ast.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 — compose individual schemas into `z.object({ ... })`,
24
+ * or use a schema name string directly.
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: ast.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: ast.SchemaNode): string {
74
+ let expr: string
75
+ switch (schema.type) {
76
+ case 'enum': {
77
+ // namedEnumValues takes priority over enumValues
78
+ const rawValues: Array<string | number | boolean> = schema.namedEnumValues?.length
79
+ ? schema.namedEnumValues.map((v) => v.value)
80
+ : (schema.enumValues ?? []).filter((v): v is string | number | boolean => v !== null)
81
+
82
+ if (rawValues.length > 0 && rawValues.every((v) => typeof v === 'string')) {
83
+ expr = `z.enum([${rawValues.map((v) => JSON.stringify(v)).join(', ')}])`
84
+ } else if (rawValues.length > 0) {
85
+ const literals = rawValues.map((v) => `z.literal(${JSON.stringify(v)})`)
86
+ expr = literals.length === 1 ? literals[0]! : `z.union([${literals.join(', ')}])`
87
+ } else {
88
+ expr = 'z.string()'
89
+ }
90
+ break
91
+ }
92
+ case 'integer':
93
+ expr = 'z.coerce.number()'
94
+ break
95
+ case 'number':
96
+ expr = 'z.number()'
97
+ break
98
+ case 'boolean':
99
+ expr = 'z.boolean()'
100
+ break
101
+ case 'array':
102
+ expr = 'z.array(z.unknown())'
103
+ break
104
+ default:
105
+ expr = 'z.string()'
106
+ }
107
+
108
+ if (schema.nullable) {
109
+ expr = `${expr}.nullable()`
110
+ }
111
+
112
+ return expr
113
+ }