@kubb/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.
@@ -1,213 +1,146 @@
1
- import { AsyncEventEmitter } from '@internals/utils'
2
- import { type Config, type KubbEvents, safeBuild, setup } from '@kubb/core'
3
- import type { CallToolResult } from '@modelcontextprotocol/sdk/types.d.ts'
4
- import type { z } from 'zod'
5
- import type { generateSchema } from '../schemas/generateSchema.ts'
1
+ import { AsyncEventEmitter, toError } from '@internals/utils'
2
+ import { type Config, createKubb, type KubbHooks } from '@kubb/core'
3
+ import { defineTool } from 'tmcp/tool'
4
+ import { tool } from 'tmcp/utils'
5
+ import type * as v from 'valibot'
6
+ import { generateSchema } from '../schemas/generateSchema.ts'
6
7
  import { NotifyTypes } from '../types.ts'
7
8
  import { loadUserConfig } from '../utils/loadUserConfig.ts'
8
9
  import { resolveCwd } from '../utils/resolveCwd.ts'
9
10
  import { resolveUserConfig } from '../utils/resolveUserConfig.ts'
10
11
 
11
- interface NotificationHandler {
12
- sendNotification(method: string, params: unknown): Promise<void>
13
- }
14
-
15
- /**
16
- * Build tool that generates code from OpenAPI specs using Kubb.
17
- * Sends real-time notifications of build progress and events.
18
- */
19
- export async function generate(schema: z.infer<typeof generateSchema>, handler: NotificationHandler): Promise<CallToolResult> {
20
- const { config: configPath, input, output, logLevel } = schema
21
-
22
- try {
23
- const events = new AsyncEventEmitter<KubbEvents>()
24
- const messages: string[] = []
25
-
26
- // Helper to send notifications
27
- const notify = async (type: string, message: string, data?: Record<string, unknown>) => {
28
- messages.push(`${type}: ${message}`)
29
-
30
- await handler.sendNotification('kubb/progress', {
31
- type,
32
- message,
33
- timestamp: new Date().toISOString(),
34
- ...data,
35
- })
36
- }
12
+ export const generateTool = defineTool(
13
+ {
14
+ name: 'generate',
15
+ description: 'Generate OpenAPI spec helpers using Kubb configuration',
16
+ schema: generateSchema,
17
+ },
18
+ async function generate(schema: v.InferInput<typeof generateSchema>) {
19
+ const { config: configPath, input, output, logLevel } = schema
37
20
 
38
- // Capture events for output and send notifications
39
- events.on('info', async (message: string) => {
40
- await notify(NotifyTypes.INFO, message)
41
- })
21
+ try {
22
+ const hooks = new AsyncEventEmitter<KubbHooks>()
23
+ const messages: string[] = []
42
24
 
43
- events.on('success', async (message: string) => {
44
- await notify(NotifyTypes.SUCCESS, message)
45
- })
25
+ const notify = async (type: string, message: string, _data?: Record<string, unknown>) => {
26
+ messages.push(`${type}: ${message}`)
27
+ }
46
28
 
47
- events.on('error', async (error: Error) => {
48
- await notify(NotifyTypes.ERROR, error.message, { stack: error.stack })
49
- })
29
+ hooks.on('kubb:info', async ({ message }: { message: string }) => {
30
+ await notify(NotifyTypes.INFO, message)
31
+ })
50
32
 
51
- events.on('warn', async (message: string) => {
52
- await notify(NotifyTypes.WARN, message)
53
- })
33
+ hooks.on('kubb:success', async ({ message }: { message: string }) => {
34
+ await notify(NotifyTypes.SUCCESS, message)
35
+ })
54
36
 
55
- // Plugin lifecycle events
56
- events.on('plugin:start', async ({ name }: { name: string }) => {
57
- await notify(NotifyTypes.PLUGIN_START, `Plugin starting: ${name}`)
58
- })
37
+ hooks.on('kubb:error', async ({ error }: { error: Error }) => {
38
+ await notify(NotifyTypes.ERROR, error.message)
39
+ })
59
40
 
60
- events.on('plugin:end', async ({ name, duration }: { name: string; duration?: number }) => {
61
- await notify(NotifyTypes.PLUGIN_END, `Plugin finished: ${name}`, { duration })
62
- })
41
+ hooks.on('kubb:warn', async ({ message }: { message: string }) => {
42
+ await notify(NotifyTypes.WARN, message)
43
+ })
63
44
 
64
- // File processing events
65
- events.on('files:processing:start', async () => {
66
- await notify(NotifyTypes.FILES_START, 'Starting file processing')
67
- })
45
+ hooks.on('kubb:plugin:start', async ({ plugin }) => {
46
+ await notify(NotifyTypes.PLUGIN_START, `Plugin starting: ${plugin.name}`)
47
+ })
68
48
 
69
- events.on('file:processing:update', async ({ file }: { file: { name: string } }) => {
70
- await notify(NotifyTypes.FILE_UPDATE, `Processing file: ${file.name}`)
71
- })
49
+ hooks.on('kubb:plugin:end', async ({ plugin, duration }) => {
50
+ await notify(NotifyTypes.PLUGIN_END, `Plugin finished: ${plugin.name}`, { duration })
51
+ })
72
52
 
73
- events.on('files:processing:end', async () => {
74
- await notify(NotifyTypes.FILES_END, 'File processing complete')
75
- })
53
+ hooks.on('kubb:files:processing:start', async () => {
54
+ await notify(NotifyTypes.FILES_START, 'Starting file processing')
55
+ })
76
56
 
77
- // Generation events
78
- events.on('generation:start', async () => {
79
- await notify(NotifyTypes.GENERATION_START, 'Generation started')
80
- })
57
+ hooks.on('kubb:file:processing:update', async ({ file }: { file: { name: string } }) => {
58
+ await notify(NotifyTypes.FILE_UPDATE, `Processing file: ${file.name}`)
59
+ })
81
60
 
82
- events.on('generation:end', async () => {
83
- await notify(NotifyTypes.GENERATION_END, 'Generation ended')
84
- })
61
+ hooks.on('kubb:files:processing:end', async () => {
62
+ await notify(NotifyTypes.FILES_END, 'File processing complete')
63
+ })
85
64
 
86
- // Load and process configuration
87
- let userConfig: Config
88
- let cwd: string
65
+ hooks.on('kubb:generation:start', async () => {
66
+ await notify(NotifyTypes.GENERATION_START, 'Generation started')
67
+ })
89
68
 
90
- try {
91
- const configResult = await loadUserConfig(configPath, { notify })
92
- userConfig = configResult.userConfig
93
- cwd = configResult.cwd
69
+ hooks.on('kubb:generation:end', async () => {
70
+ await notify(NotifyTypes.GENERATION_END, 'Generation ended')
71
+ })
94
72
 
95
- if (Array.isArray(userConfig) && userConfig.length) {
96
- throw new Error('Array type in kubb.config.ts is not supported in this tool. Please provide a single configuration object.')
73
+ let userConfig: Config
74
+ let cwd: string
75
+
76
+ try {
77
+ const configResult = await loadUserConfig(configPath, { notify })
78
+ userConfig = configResult.userConfig
79
+ cwd = configResult.cwd
80
+
81
+ if (Array.isArray(userConfig)) {
82
+ throw new Error('Array type in kubb.config.ts is not supported in this tool. Please provide a single configuration object.')
83
+ }
84
+
85
+ userConfig = await resolveUserConfig(userConfig, {
86
+ configPath,
87
+ logLevel,
88
+ })
89
+ } catch (error) {
90
+ const errorMessage = error instanceof Error ? error.message : String(error)
91
+ await notify(NotifyTypes.CONFIG_ERROR, errorMessage)
92
+ return tool.error(errorMessage)
97
93
  }
98
94
 
99
- userConfig = await resolveUserConfig(userConfig, { configPath, logLevel })
100
- } catch (error) {
101
- const errorMessage = error instanceof Error ? error.message : String(error)
102
- await notify(NotifyTypes.CONFIG_ERROR, errorMessage)
103
- return {
104
- content: [
105
- {
106
- type: 'text',
107
- text: errorMessage,
108
- },
109
- ],
110
- isError: true,
95
+ const inputPath = input ?? (userConfig.input && 'path' in userConfig.input ? userConfig.input.path : undefined)
96
+
97
+ const config: Config = {
98
+ ...userConfig,
99
+ root: resolveCwd(userConfig, cwd),
100
+ input: inputPath
101
+ ? {
102
+ ...userConfig.input,
103
+ path: inputPath,
104
+ }
105
+ : userConfig.input,
106
+ output: output
107
+ ? {
108
+ ...userConfig.output,
109
+ path: output,
110
+ }
111
+ : userConfig.output,
111
112
  }
112
- }
113
113
 
114
- const inputPath = input ?? ('path' in userConfig.input ? userConfig.input.path : undefined)
115
-
116
- // Override config with CLI options
117
- const config: Config = {
118
- ...userConfig,
119
- root: resolveCwd(userConfig, cwd),
120
- input: inputPath
121
- ? {
122
- ...userConfig.input,
123
- path: inputPath,
124
- }
125
- : userConfig.input,
126
- output: output
127
- ? {
128
- ...userConfig.output,
129
- path: output,
130
- }
131
- : userConfig.output,
132
- }
114
+ await notify(NotifyTypes.CONFIG_READY, 'Configuration ready')
115
+ await notify(NotifyTypes.SETUP_START, 'Setting up Kubb')
133
116
 
134
- await notify(NotifyTypes.CONFIG_READY, 'Configuration ready', {
135
- root: config.root,
136
- })
137
-
138
- // Setup and build
139
- await notify(NotifyTypes.SETUP_START, 'Setting up Kubb')
140
-
141
- const { fabric, driver, sources } = await setup({
142
- config,
143
- events,
144
- })
145
- await notify(NotifyTypes.SETUP_END, 'Kubb setup complete')
146
-
147
- await notify(NotifyTypes.BUILD_START, 'Starting build')
148
- const { files, failedPlugins, error } = await safeBuild(
149
- {
150
- config,
151
- events,
152
- },
153
- { driver, fabric, events, sources },
154
- )
155
- await notify(NotifyTypes.BUILD_END, `Build complete - Generated ${files.length} files`)
156
-
157
- if (error || failedPlugins.size > 0) {
158
- const allErrors: Error[] = [
159
- error,
160
- ...Array.from(failedPlugins)
161
- .filter((it) => it.error)
162
- .map((it) => it.error),
163
- ].filter(Boolean)
164
-
165
- await notify(NotifyTypes.BUILD_FAILED, `Build failed with ${allErrors.length} error(s)`, {
166
- errorCount: allErrors.length,
167
- errors: allErrors.map((err) => err.message),
168
- })
117
+ const kubb = createKubb(config, { hooks })
118
+ await kubb.setup()
119
+ await notify(NotifyTypes.SETUP_END, 'Kubb setup complete')
169
120
 
170
- return {
171
- content: [
172
- {
173
- type: 'text',
174
- text: `Build failed:\n${allErrors.map((err) => err.message).join('\n')}\n\n${messages.join('\n')}`,
175
- },
176
- ],
177
- isError: true,
121
+ await notify(NotifyTypes.BUILD_START, 'Starting build')
122
+ const { files, failedPlugins, error } = await kubb.safeBuild()
123
+ await notify(NotifyTypes.BUILD_END, `Build complete - Generated ${files.length} files`)
124
+
125
+ if (error || failedPlugins.size > 0) {
126
+ const allErrors: Error[] = [
127
+ error,
128
+ ...Array.from(failedPlugins)
129
+ .filter((it) => it.error)
130
+ .map((it) => it.error),
131
+ ].filter(Boolean)
132
+
133
+ await notify(NotifyTypes.BUILD_FAILED, `Build failed with ${allErrors.length} error(s)`)
134
+
135
+ return tool.error(`Build failed:\n${allErrors.map((err) => err.message).join('\n')}\n\n${messages.join('\n')}`)
178
136
  }
179
- }
180
137
 
181
- await notify(NotifyTypes.BUILD_SUCCESS, `Build completed successfully - Generated ${files.length} files`, {
182
- filesCount: files.length,
183
- })
184
-
185
- return {
186
- content: [
187
- {
188
- type: 'text',
189
- text: `Build completed successfully!\n\nGenerated ${files.length} files\n\n${messages.join('\n')}`,
190
- },
191
- ],
192
- }
193
- } catch (caughtError) {
194
- const error = caughtError as Error
195
-
196
- await handler.sendNotification('kubb/progress', {
197
- type: NotifyTypes.FATAL_ERROR,
198
- message: error.message,
199
- stack: error.stack,
200
- timestamp: new Date().toISOString(),
201
- })
202
-
203
- return {
204
- content: [
205
- {
206
- type: 'text',
207
- text: `Build error: ${error.message}\n${error.stack || ''}`,
208
- },
209
- ],
210
- isError: true,
138
+ await notify(NotifyTypes.BUILD_SUCCESS, `Build completed successfully - Generated ${files.length} files`)
139
+
140
+ return tool.text(`Build completed successfully!\n\nGenerated ${files.length} files\n\n${messages.join('\n')}`)
141
+ } catch (caughtError) {
142
+ const error = toError(caughtError)
143
+ return tool.error(`Build error: ${error.message}\n${error.stack ?? ''}`)
211
144
  }
212
- }
213
- }
145
+ },
146
+ )
@@ -0,0 +1,37 @@
1
+ import fs from 'node:fs'
2
+ import path from 'node:path'
3
+ import process from 'node:process'
4
+ import { availablePlugins, generateConfigFile, KUBB_CONFIG_FILENAME, type PluginOption } from '@internals/shared'
5
+ import { defineTool } from 'tmcp/tool'
6
+ import { tool } from 'tmcp/utils'
7
+ import { initSchema } from '../schemas/initSchema.ts'
8
+
9
+ export function resolvePlugins(pluginsFlag: string | undefined): PluginOption[] {
10
+ if (!pluginsFlag) {
11
+ return []
12
+ }
13
+ const requested = pluginsFlag
14
+ .split(',')
15
+ .map((v) => v.trim())
16
+ .filter(Boolean)
17
+ return availablePlugins.filter((p) => requested.includes(p.value))
18
+ }
19
+
20
+ export const initTool = defineTool(
21
+ {
22
+ name: 'init',
23
+ description: 'Scaffold a kubb.config.ts in the current directory (non-interactive). Does not install packages.',
24
+ schema: initSchema,
25
+ },
26
+ async ({ input = './openapi.yaml', output = './src/gen', plugins }) => {
27
+ const selected = resolvePlugins(plugins)
28
+ const content = generateConfigFile({ selectedPlugins: selected, inputPath: input, outputPath: output })
29
+ const dest = path.join(process.cwd(), KUBB_CONFIG_FILENAME)
30
+ if (fs.existsSync(dest)) {
31
+ return tool.error(`${KUBB_CONFIG_FILENAME} already exists at ${dest}. Delete it first before running init again.`)
32
+ }
33
+ fs.writeFileSync(dest, content, 'utf-8')
34
+ const packageList = ['kubb', ...selected.map((p) => p.packageName)].join(' ')
35
+ return tool.text(`Created kubb.config.ts\n\nInstall packages:\n npm install ${packageList}\n\nThen run:\n npx kubb generate`)
36
+ },
37
+ )
@@ -0,0 +1,25 @@
1
+ import { defineTool } from 'tmcp/tool'
2
+ import { tool } from 'tmcp/utils'
3
+ import { validateSchema } from '../schemas/validateSchema.ts'
4
+
5
+ export const validateTool = defineTool(
6
+ {
7
+ name: 'validate',
8
+ description: 'Validate an OpenAPI/Swagger specification file or URL',
9
+ schema: validateSchema,
10
+ },
11
+ async ({ input }) => {
12
+ let mod: typeof import('@kubb/adapter-oas')
13
+ try {
14
+ mod = await import('@kubb/adapter-oas')
15
+ } catch {
16
+ return tool.error('The validate tool requires @kubb/adapter-oas.\nInstall: npm install @kubb/adapter-oas')
17
+ }
18
+ try {
19
+ await mod.adapterOas().validate(input, { throwOnError: true })
20
+ return tool.text(`Validation successful: ${input}`)
21
+ } catch (err) {
22
+ return tool.error(`Validation failed:\n${err instanceof Error ? err.message : String(err)}`)
23
+ }
24
+ },
25
+ )
@@ -1,55 +1,78 @@
1
+ import { existsSync } from 'node:fs'
1
2
  import path from 'node:path'
2
3
  import type { Config } from '@kubb/core'
3
- import createJiti from 'jiti'
4
+ import { createJiti } from 'jiti'
5
+ import { ALLOWED_CONFIG_EXTENSIONS } from '../constants.ts'
4
6
  import { NotifyTypes } from '../types.ts'
5
7
 
6
8
  type NotifyFunction = (type: string, message: string, data?: Record<string, unknown>) => Promise<void>
7
9
 
8
10
  const jiti = createJiti(import.meta.url, {
9
- sourceMaps: true,
11
+ jsx: {
12
+ runtime: 'automatic',
13
+ importSource: '@kubb/renderer-jsx',
14
+ },
15
+ moduleCache: false,
10
16
  })
11
17
 
12
- /**
13
- * Load the user configuration from the specified path or current directory
14
- */
15
- export async function loadUserConfig(configPath: string | undefined, { notify }: { notify: NotifyFunction }): Promise<{ userConfig: Config; cwd: string }> {
16
- let userConfig: Config | undefined
17
- let cwd: string
18
+ const loadedModules = new Map<string, unknown>()
18
19
 
19
- if (configPath) {
20
- // Resolve the config path to absolute path and get its directory
21
- cwd = path.dirname(path.resolve(configPath))
20
+ async function loadModule(filePath: string): Promise<unknown> {
21
+ const ext = path.extname(filePath)
22
+ if (!ALLOWED_CONFIG_EXTENSIONS.has(ext)) {
23
+ throw new Error(`Invalid config file extension "${ext}". Allowed: ${[...ALLOWED_CONFIG_EXTENSIONS].join(', ')}`)
24
+ }
25
+ if (loadedModules.has(filePath)) {
26
+ return loadedModules.get(filePath)
27
+ }
28
+ const mod = await jiti.import(filePath, { default: true })
29
+ loadedModules.set(filePath, mod)
30
+ return mod
31
+ }
22
32
 
23
- // Try to load from path
33
+ export async function loadUserConfig(configPath: string | undefined, { notify }: { notify: NotifyFunction }): Promise<{ userConfig: Config; cwd: string }> {
34
+ if (configPath) {
35
+ const ext = path.extname(configPath)
36
+ if (!ALLOWED_CONFIG_EXTENSIONS.has(ext)) {
37
+ const msg = `Invalid config file extension "${ext}". Allowed: ${[...ALLOWED_CONFIG_EXTENSIONS].join(', ')}`
38
+ await notify(NotifyTypes.CONFIG_ERROR, msg)
39
+ throw new Error(msg)
40
+ }
41
+ const base = path.resolve(process.cwd())
42
+ const resolvedConfigPath = path.resolve(base, configPath)
43
+ const relative = path.relative(base, resolvedConfigPath)
44
+ if (relative.startsWith('..') || path.isAbsolute(relative)) {
45
+ const msg = 'Invalid config file path: must be within the current working directory'
46
+ await notify(NotifyTypes.CONFIG_ERROR, msg)
47
+ throw new Error(msg)
48
+ }
49
+ const cwd = path.dirname(resolvedConfigPath)
24
50
  try {
25
- userConfig = await jiti.import(configPath, { default: true })
26
- await notify(NotifyTypes.CONFIG_LOADED, `Loaded config from ${configPath}`)
51
+ const userConfig = (await loadModule(resolvedConfigPath)) as Config
52
+ await notify(NotifyTypes.CONFIG_LOADED, `Loaded config from ${resolvedConfigPath}`)
53
+ return { userConfig, cwd }
27
54
  } catch (error) {
28
- await notify(NotifyTypes.CONFIG_ERROR, `Failed to load config: ${error instanceof Error ? error.message : String(error)}`)
29
- throw new Error(`Failed to load config: ${error instanceof Error ? error.message : String(error)}`)
30
- }
31
- } else {
32
- // Look for kubb.config in current directory with various extensions
33
- cwd = process.cwd()
34
- const configFileNames = ['kubb.config.ts', 'kubb.config.js', 'kubb.config.cjs']
35
-
36
- for (const configFileName of configFileNames) {
37
- try {
38
- const configFilePath = path.resolve(process.cwd(), configFileName)
39
- userConfig = await jiti.import(configFilePath, { default: true })
40
- await notify(NotifyTypes.CONFIG_LOADED, `Loaded ${configFileName} from current directory`)
41
- break
42
- } catch {
43
- // Continue trying next config file
44
- }
55
+ const msg = `Failed to load config: ${error instanceof Error ? error.message : String(error)}`
56
+ await notify(NotifyTypes.CONFIG_ERROR, msg)
57
+ throw new Error(msg)
45
58
  }
59
+ }
46
60
 
47
- if (!userConfig) {
48
- await notify(NotifyTypes.CONFIG_ERROR, 'No config file found')
61
+ const cwd = process.cwd()
62
+ const configFileNames = ['kubb.config.ts', 'kubb.config.mts', 'kubb.config.cts', 'kubb.config.js', 'kubb.config.cjs']
49
63
 
50
- throw new Error(`No config file found. Please provide a config path or create one of: ${configFileNames.join(', ')}`)
64
+ for (const configFileName of configFileNames) {
65
+ const configFilePath = path.resolve(process.cwd(), configFileName)
66
+ if (!existsSync(configFilePath)) continue
67
+ try {
68
+ const userConfig = (await loadModule(configFilePath)) as Config
69
+ await notify(NotifyTypes.CONFIG_LOADED, `Loaded ${configFileName} from current directory`)
70
+ return { userConfig, cwd }
71
+ } catch (err) {
72
+ await notify(NotifyTypes.CONFIG_ERROR, `Failed to load ${configFileName}: ${err instanceof Error ? err.message : String(err)}`)
51
73
  }
52
74
  }
53
75
 
54
- return { userConfig: userConfig!, cwd }
76
+ await notify(NotifyTypes.CONFIG_ERROR, 'No config file found')
77
+ throw new Error(`No config file found. Please provide a config path or create one of: ${configFileNames.join(', ')}`)
55
78
  }
@@ -1,25 +1,13 @@
1
1
  import { isPromise } from '@internals/utils'
2
- import type { CLIOptions, Config, UserConfig } from '@kubb/core'
2
+ import type { CLIOptions, Config, PossibleConfig } from '@kubb/core'
3
3
 
4
4
  export type ResolveUserConfigOptions = {
5
5
  configPath?: string
6
6
  logLevel?: string
7
7
  }
8
8
 
9
- /**
10
- * Resolve the config by handling function configs and returning the final configuration
11
- */
12
- export async function resolveUserConfig(userConfig: UserConfig, options: ResolveUserConfigOptions): Promise<Config> {
13
- let kubbUserConfig = Promise.resolve(userConfig) as Promise<UserConfig>
14
-
15
- if (typeof userConfig === 'function') {
16
- const possiblePromise = (userConfig as any)({ logLevel: options.logLevel, config: options.configPath } as CLIOptions)
17
- if (isPromise(possiblePromise)) {
18
- kubbUserConfig = possiblePromise
19
- } else {
20
- kubbUserConfig = Promise.resolve(possiblePromise)
21
- }
22
- }
23
-
24
- return (await kubbUserConfig) as Config
9
+ export async function resolveUserConfig(config: PossibleConfig<CLIOptions>, options: ResolveUserConfigOptions): Promise<Config> {
10
+ const result = typeof config === 'function' ? config({ logLevel: options.logLevel as CLIOptions['logLevel'], config: options.configPath }) : config
11
+ const resolved = isPromise(result) ? await result : result
12
+ return (Array.isArray(resolved) ? resolved[0] : resolved) as Config
25
13
  }