incur 0.3.3 → 0.3.5
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/README.md +11 -11
- package/dist/Cli.d.ts +2 -7
- package/dist/Cli.d.ts.map +1 -1
- package/dist/Cli.js +234 -359
- package/dist/Cli.js.map +1 -1
- package/dist/Completions.d.ts +1 -2
- package/dist/Completions.d.ts.map +1 -1
- package/dist/Completions.js.map +1 -1
- package/dist/Help.d.ts +2 -0
- package/dist/Help.d.ts.map +1 -1
- package/dist/Help.js +20 -10
- package/dist/Help.js.map +1 -1
- package/dist/Mcp.d.ts +25 -5
- package/dist/Mcp.d.ts.map +1 -1
- package/dist/Mcp.js +61 -69
- package/dist/Mcp.js.map +1 -1
- package/dist/Skill.d.ts.map +1 -1
- package/dist/Skill.js +5 -1
- package/dist/Skill.js.map +1 -1
- package/dist/SyncSkills.d.ts.map +1 -1
- package/dist/SyncSkills.js +10 -1
- package/dist/SyncSkills.js.map +1 -1
- package/dist/internal/command.d.ts +116 -0
- package/dist/internal/command.d.ts.map +1 -0
- package/dist/internal/command.js +275 -0
- package/dist/internal/command.js.map +1 -0
- package/package.json +1 -1
- package/src/Cli.test.ts +165 -18
- package/src/Cli.ts +288 -439
- package/src/Completions.test.ts +35 -9
- package/src/Completions.ts +1 -2
- package/src/Help.test.ts +18 -7
- package/src/Help.ts +21 -10
- package/src/Mcp.test.ts +143 -0
- package/src/Mcp.ts +92 -84
- package/src/Skill.ts +5 -1
- package/src/SyncSkills.ts +11 -1
- package/src/e2e.test.ts +40 -27
- package/src/internal/command.ts +425 -0
package/src/Completions.test.ts
CHANGED
|
@@ -294,27 +294,24 @@ describe('completions built-in command', () => {
|
|
|
294
294
|
expect(output).toMatchInlineSnapshot(`
|
|
295
295
|
"mycli completions — Generate shell completion script
|
|
296
296
|
|
|
297
|
-
Usage: mycli completions <
|
|
297
|
+
Usage: mycli completions <bash|fish|nushell|zsh>
|
|
298
298
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
fish
|
|
302
|
-
nushell
|
|
303
|
-
zsh
|
|
299
|
+
Arguments:
|
|
300
|
+
shell Shell to generate completions for
|
|
304
301
|
|
|
305
302
|
Setup:
|
|
306
303
|
bash eval "$(mycli completions bash)" # add to ~/.bashrc
|
|
307
|
-
zsh eval "$(mycli completions zsh)" # add to ~/.zshrc
|
|
308
304
|
fish mycli completions fish | source # add to ~/.config/fish/config.fish
|
|
309
305
|
nushell see \`mycli completions nushell\` # add to config.nu
|
|
306
|
+
zsh eval "$(mycli completions zsh)" # add to ~/.zshrc
|
|
310
307
|
"
|
|
311
308
|
`)
|
|
312
309
|
})
|
|
313
310
|
|
|
314
|
-
test('
|
|
311
|
+
test('shows help on missing shell argument', async () => {
|
|
315
312
|
const cli = makeCli()
|
|
316
313
|
const output = await serve(cli, ['completions'])
|
|
317
|
-
expect(output).toContain('
|
|
314
|
+
expect(output).toContain('Generate shell completion script')
|
|
318
315
|
})
|
|
319
316
|
|
|
320
317
|
test('errors on unknown shell', async () => {
|
|
@@ -391,6 +388,35 @@ describe('serve integration', () => {
|
|
|
391
388
|
expect(output).toContain('db')
|
|
392
389
|
})
|
|
393
390
|
|
|
391
|
+
test('COMPLETE=bash includes built-in commands at root', async () => {
|
|
392
|
+
const cli = makeCli()
|
|
393
|
+
const output = await serve(cli, ['--', 'mycli', ''], {
|
|
394
|
+
COMPLETE: 'bash',
|
|
395
|
+
_COMPLETE_INDEX: '1',
|
|
396
|
+
})
|
|
397
|
+
expect(output).toContain('completions')
|
|
398
|
+
expect(output).toContain('mcp')
|
|
399
|
+
expect(output).toContain('skills')
|
|
400
|
+
})
|
|
401
|
+
|
|
402
|
+
test('COMPLETE=bash suggests add for skills subcommand', async () => {
|
|
403
|
+
const cli = makeCli()
|
|
404
|
+
const output = await serve(cli, ['--', 'mycli', 'skills', ''], {
|
|
405
|
+
COMPLETE: 'bash',
|
|
406
|
+
_COMPLETE_INDEX: '2',
|
|
407
|
+
})
|
|
408
|
+
expect(output).toContain('add')
|
|
409
|
+
})
|
|
410
|
+
|
|
411
|
+
test('COMPLETE=bash suggests add for mcp subcommand', async () => {
|
|
412
|
+
const cli = makeCli()
|
|
413
|
+
const output = await serve(cli, ['--', 'mycli', 'mcp', ''], {
|
|
414
|
+
COMPLETE: 'bash',
|
|
415
|
+
_COMPLETE_INDEX: '2',
|
|
416
|
+
})
|
|
417
|
+
expect(output).toContain('add')
|
|
418
|
+
})
|
|
419
|
+
|
|
394
420
|
test('COMPLETE=zsh with words outputs candidates in zsh format', async () => {
|
|
395
421
|
const cli = makeCli()
|
|
396
422
|
const output = await serve(cli, ['--', 'mycli', '--'], {
|
package/src/Completions.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import type { z } from 'zod'
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
export type Shell = 'bash' | 'fish' | 'nushell' | 'zsh'
|
|
3
|
+
import type { Shell } from './internal/command.js'
|
|
5
4
|
|
|
6
5
|
/** A completion candidate with an optional description. */
|
|
7
6
|
export type Candidate = {
|
package/src/Help.test.ts
CHANGED
|
@@ -29,7 +29,7 @@ describe('formatCommand', () => {
|
|
|
29
29
|
--format <toon|json|yaml|md|jsonl> Output format
|
|
30
30
|
--help Show help
|
|
31
31
|
--llms, --llms-full Print LLM-readable manifest
|
|
32
|
-
--schema Show JSON Schema for
|
|
32
|
+
--schema Show JSON Schema for command
|
|
33
33
|
--token-count Print token count of output (instead of output)
|
|
34
34
|
--token-limit <n> Limit output to n tokens
|
|
35
35
|
--token-offset <n> Skip first n tokens of output
|
|
@@ -51,7 +51,7 @@ describe('formatCommand', () => {
|
|
|
51
51
|
--format <toon|json|yaml|md|jsonl> Output format
|
|
52
52
|
--help Show help
|
|
53
53
|
--llms, --llms-full Print LLM-readable manifest
|
|
54
|
-
--schema Show JSON Schema for
|
|
54
|
+
--schema Show JSON Schema for command
|
|
55
55
|
--token-count Print token count of output (instead of output)
|
|
56
56
|
--token-limit <n> Limit output to n tokens
|
|
57
57
|
--token-offset <n> Skip first n tokens of output
|
|
@@ -80,7 +80,7 @@ describe('formatCommand', () => {
|
|
|
80
80
|
--format <toon|json|yaml|md|jsonl> Output format
|
|
81
81
|
--help Show help
|
|
82
82
|
--llms, --llms-full Print LLM-readable manifest
|
|
83
|
-
--schema Show JSON Schema for
|
|
83
|
+
--schema Show JSON Schema for command
|
|
84
84
|
--token-count Print token count of output (instead of output)
|
|
85
85
|
--token-limit <n> Limit output to n tokens
|
|
86
86
|
--token-offset <n> Skip first n tokens of output
|
|
@@ -88,6 +88,17 @@ describe('formatCommand', () => {
|
|
|
88
88
|
`)
|
|
89
89
|
})
|
|
90
90
|
|
|
91
|
+
test('synopsis uses key name for non-union args and expanded values for enums', () => {
|
|
92
|
+
const result = Help.formatCommand('tool run', {
|
|
93
|
+
args: z.object({
|
|
94
|
+
port: z.number().describe('Port number'),
|
|
95
|
+
verbose: z.boolean().optional().describe('Verbose'),
|
|
96
|
+
mode: z.enum(['fast', 'slow']).describe('Mode'),
|
|
97
|
+
}),
|
|
98
|
+
})
|
|
99
|
+
expect(result).toContain('Usage: tool run <port> [verbose] <fast|slow>')
|
|
100
|
+
})
|
|
101
|
+
|
|
91
102
|
test('shows count type in help for meta count', () => {
|
|
92
103
|
const result = Help.formatCommand('tool run', {
|
|
93
104
|
options: z.object({
|
|
@@ -157,7 +168,7 @@ describe('formatRoot', () => {
|
|
|
157
168
|
--format <toon|json|yaml|md|jsonl> Output format
|
|
158
169
|
--help Show help
|
|
159
170
|
--llms, --llms-full Print LLM-readable manifest
|
|
160
|
-
--schema Show JSON Schema for
|
|
171
|
+
--schema Show JSON Schema for command
|
|
161
172
|
--token-count Print token count of output (instead of output)
|
|
162
173
|
--token-limit <n> Limit output to n tokens
|
|
163
174
|
--token-offset <n> Skip first n tokens of output
|
|
@@ -182,7 +193,7 @@ describe('formatRoot', () => {
|
|
|
182
193
|
--format <toon|json|yaml|md|jsonl> Output format
|
|
183
194
|
--help Show help
|
|
184
195
|
--llms, --llms-full Print LLM-readable manifest
|
|
185
|
-
--schema Show JSON Schema for
|
|
196
|
+
--schema Show JSON Schema for command
|
|
186
197
|
--token-count Print token count of output (instead of output)
|
|
187
198
|
--token-limit <n> Limit output to n tokens
|
|
188
199
|
--token-offset <n> Skip first n tokens of output
|
|
@@ -211,7 +222,7 @@ describe('formatRoot', () => {
|
|
|
211
222
|
--format <toon|json|yaml|md|jsonl> Output format
|
|
212
223
|
--help Show help
|
|
213
224
|
--llms, --llms-full Print LLM-readable manifest
|
|
214
|
-
--schema Show JSON Schema for
|
|
225
|
+
--schema Show JSON Schema for command
|
|
215
226
|
--token-count Print token count of output (instead of output)
|
|
216
227
|
--token-limit <n> Limit output to n tokens
|
|
217
228
|
--token-offset <n> Skip first n tokens of output
|
|
@@ -240,7 +251,7 @@ describe('formatRoot', () => {
|
|
|
240
251
|
--format <toon|json|yaml|md|jsonl> Output format
|
|
241
252
|
--help Show help
|
|
242
253
|
--llms, --llms-full Print LLM-readable manifest
|
|
243
|
-
--schema Show JSON Schema for
|
|
254
|
+
--schema Show JSON Schema for command
|
|
244
255
|
--token-count Print token count of output (instead of output)
|
|
245
256
|
--token-limit <n> Limit output to n tokens
|
|
246
257
|
--token-offset <n> Skip first n tokens of output
|
package/src/Help.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { z } from 'zod'
|
|
2
2
|
|
|
3
|
+
import { builtinCommands } from './internal/command.js'
|
|
4
|
+
|
|
3
5
|
/** Formats help text for a router CLI or command group. */
|
|
4
6
|
export function formatRoot(name: string, options: formatRoot.Options = {}): string {
|
|
5
7
|
const { aliases, description, version, commands = [], root = false } = options
|
|
@@ -67,6 +69,8 @@ export declare namespace formatCommand {
|
|
|
67
69
|
examples?: { command: string; description?: string }[] | undefined
|
|
68
70
|
/** Plain text hint displayed after examples and before global options. */
|
|
69
71
|
hint?: string | undefined
|
|
72
|
+
/** Hide global options section. */
|
|
73
|
+
hideGlobalOptions?: boolean | undefined
|
|
70
74
|
/** Zod schema for named options/flags. */
|
|
71
75
|
options?: z.ZodObject<any> | undefined
|
|
72
76
|
/** Show root-level built-in commands and flags. */
|
|
@@ -195,7 +199,7 @@ export function formatCommand(name: string, options: formatCommand.Options = {})
|
|
|
195
199
|
}
|
|
196
200
|
}
|
|
197
201
|
|
|
198
|
-
lines.push(...globalOptionsLines(root))
|
|
202
|
+
if (!options.hideGlobalOptions) lines.push(...globalOptionsLines(root))
|
|
199
203
|
|
|
200
204
|
// Environment Variables
|
|
201
205
|
if (env) {
|
|
@@ -223,8 +227,11 @@ export function formatCommand(name: string, options: formatCommand.Options = {})
|
|
|
223
227
|
function buildSynopsis(name: string, args?: z.ZodObject<any>): string {
|
|
224
228
|
if (!args) return name
|
|
225
229
|
const parts = [name]
|
|
226
|
-
for (const [key, schema] of Object.entries(args.shape))
|
|
227
|
-
|
|
230
|
+
for (const [key, schema] of Object.entries(args.shape)) {
|
|
231
|
+
const type = resolveTypeName(schema)
|
|
232
|
+
const label = type.includes('|') ? type : key
|
|
233
|
+
parts.push((schema as z.ZodType)._zod.optout === 'optional' ? `[${label}]` : `<${label}>`)
|
|
234
|
+
}
|
|
228
235
|
return parts.join(' ')
|
|
229
236
|
}
|
|
230
237
|
|
|
@@ -330,15 +337,19 @@ function globalOptionsLines(root = false): string[] {
|
|
|
330
337
|
const lines: string[] = []
|
|
331
338
|
|
|
332
339
|
if (root) {
|
|
333
|
-
const builtins =
|
|
334
|
-
{ name:
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
340
|
+
const builtins = builtinCommands.flatMap((b) => {
|
|
341
|
+
if (!b.subcommands) return [{ name: b.name, desc: b.description }]
|
|
342
|
+
if (b.subcommands.length === 1)
|
|
343
|
+
return [
|
|
344
|
+
{ name: `${b.name} ${b.subcommands[0]!.name}`, desc: b.subcommands[0]!.description },
|
|
345
|
+
]
|
|
346
|
+
const names = b.subcommands.map((s) => s.name).join(', ')
|
|
347
|
+
return [{ name: b.name, desc: `${b.description} (${names})` }]
|
|
348
|
+
})
|
|
338
349
|
const maxCmd = Math.max(...builtins.map((b) => b.name.length))
|
|
339
350
|
lines.push(
|
|
340
351
|
'',
|
|
341
|
-
'
|
|
352
|
+
'Integrations:',
|
|
342
353
|
...builtins.map((b) => ` ${b.name}${' '.repeat(maxCmd - b.name.length)} ${b.desc}`),
|
|
343
354
|
)
|
|
344
355
|
}
|
|
@@ -352,7 +363,7 @@ function globalOptionsLines(root = false): string[] {
|
|
|
352
363
|
{ flag: '--help', desc: 'Show help' },
|
|
353
364
|
{ flag: '--llms, --llms-full', desc: 'Print LLM-readable manifest' },
|
|
354
365
|
...(root ? [{ flag: '--mcp', desc: 'Start as MCP stdio server' }] : []),
|
|
355
|
-
{ flag: '--schema', desc: 'Show JSON Schema for
|
|
366
|
+
{ flag: '--schema', desc: 'Show JSON Schema for command' },
|
|
356
367
|
{ flag: '--token-count', desc: 'Print token count of output (instead of output)' },
|
|
357
368
|
{ flag: '--token-limit <n>', desc: 'Limit output to n tokens' },
|
|
358
369
|
{ flag: '--token-offset <n>', desc: 'Skip first n tokens of output' },
|
package/src/Mcp.test.ts
CHANGED
|
@@ -216,6 +216,149 @@ describe('Mcp', () => {
|
|
|
216
216
|
])
|
|
217
217
|
})
|
|
218
218
|
|
|
219
|
+
test('middleware runs for tool calls', async () => {
|
|
220
|
+
const commands = new Map<string, any>()
|
|
221
|
+
commands.set('secret', {
|
|
222
|
+
description: 'Protected command',
|
|
223
|
+
run: () => ({ secret: 'data' }),
|
|
224
|
+
})
|
|
225
|
+
const middlewares = [
|
|
226
|
+
async (_c: any, next: () => Promise<void>) => {
|
|
227
|
+
_c.set('ran', true)
|
|
228
|
+
await next()
|
|
229
|
+
},
|
|
230
|
+
]
|
|
231
|
+
const input = new PassThrough()
|
|
232
|
+
const output = new PassThrough()
|
|
233
|
+
const chunks: string[] = []
|
|
234
|
+
output.on('data', (chunk: Buffer) => chunks.push(chunk.toString()))
|
|
235
|
+
|
|
236
|
+
const done = Mcp.serve('test-cli', '1.0.0', commands, {
|
|
237
|
+
input,
|
|
238
|
+
output,
|
|
239
|
+
middlewares,
|
|
240
|
+
vars: z.object({ ran: z.boolean().default(false) }),
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
input.write(
|
|
244
|
+
`${JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'initialize', params: initParams })}\n`,
|
|
245
|
+
)
|
|
246
|
+
await new Promise((r) => setTimeout(r, 10))
|
|
247
|
+
input.write(
|
|
248
|
+
`${JSON.stringify({ jsonrpc: '2.0', id: 2, method: 'tools/call', params: { name: 'secret', arguments: {} } })}\n`,
|
|
249
|
+
)
|
|
250
|
+
await new Promise((r) => setTimeout(r, 20))
|
|
251
|
+
input.end()
|
|
252
|
+
await done
|
|
253
|
+
|
|
254
|
+
const responses = chunks.map((c) => JSON.parse(c.trim()))
|
|
255
|
+
const callRes = responses.find((r: any) => r.id === 2)
|
|
256
|
+
expect(callRes.result.content).toEqual([{ type: 'text', text: '{"secret":"data"}' }])
|
|
257
|
+
})
|
|
258
|
+
|
|
259
|
+
test('middleware error blocks tool call', async () => {
|
|
260
|
+
const commands = new Map<string, any>()
|
|
261
|
+
commands.set('secret', {
|
|
262
|
+
description: 'Protected',
|
|
263
|
+
run: () => ({ secret: true }),
|
|
264
|
+
})
|
|
265
|
+
const middlewares = [
|
|
266
|
+
(c: any) => {
|
|
267
|
+
c.error({ code: 'FORBIDDEN', message: 'not allowed' })
|
|
268
|
+
},
|
|
269
|
+
]
|
|
270
|
+
const input = new PassThrough()
|
|
271
|
+
const output = new PassThrough()
|
|
272
|
+
const chunks: string[] = []
|
|
273
|
+
output.on('data', (chunk: Buffer) => chunks.push(chunk.toString()))
|
|
274
|
+
|
|
275
|
+
const done = Mcp.serve('test-cli', '1.0.0', commands, {
|
|
276
|
+
input,
|
|
277
|
+
output,
|
|
278
|
+
middlewares,
|
|
279
|
+
})
|
|
280
|
+
|
|
281
|
+
input.write(
|
|
282
|
+
`${JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'initialize', params: initParams })}\n`,
|
|
283
|
+
)
|
|
284
|
+
await new Promise((r) => setTimeout(r, 10))
|
|
285
|
+
input.write(
|
|
286
|
+
`${JSON.stringify({ jsonrpc: '2.0', id: 2, method: 'tools/call', params: { name: 'secret', arguments: {} } })}\n`,
|
|
287
|
+
)
|
|
288
|
+
await new Promise((r) => setTimeout(r, 20))
|
|
289
|
+
input.end()
|
|
290
|
+
await done
|
|
291
|
+
|
|
292
|
+
const responses = chunks.map((c) => JSON.parse(c.trim()))
|
|
293
|
+
const callRes = responses.find((r: any) => r.id === 2)
|
|
294
|
+
expect(callRes.result.isError).toBe(true)
|
|
295
|
+
expect(callRes.result.content[0].text).toBe('not allowed')
|
|
296
|
+
})
|
|
297
|
+
|
|
298
|
+
test('group middleware runs for nested tool calls', async () => {
|
|
299
|
+
const commands = new Map<string, any>()
|
|
300
|
+
const groupMiddleware = async (c: any, next: () => Promise<void>) => {
|
|
301
|
+
c.set('group', 'admin')
|
|
302
|
+
await next()
|
|
303
|
+
}
|
|
304
|
+
commands.set('admin', {
|
|
305
|
+
_group: true,
|
|
306
|
+
description: 'Admin commands',
|
|
307
|
+
middlewares: [groupMiddleware],
|
|
308
|
+
commands: new Map([
|
|
309
|
+
[
|
|
310
|
+
'status',
|
|
311
|
+
{
|
|
312
|
+
description: 'Admin status',
|
|
313
|
+
run: (c: any) => ({ group: c.var.group }),
|
|
314
|
+
},
|
|
315
|
+
],
|
|
316
|
+
]),
|
|
317
|
+
})
|
|
318
|
+
|
|
319
|
+
const input = new PassThrough()
|
|
320
|
+
const output = new PassThrough()
|
|
321
|
+
const chunks: string[] = []
|
|
322
|
+
output.on('data', (chunk: Buffer) => chunks.push(chunk.toString()))
|
|
323
|
+
|
|
324
|
+
const done = Mcp.serve('test-cli', '1.0.0', commands, {
|
|
325
|
+
input,
|
|
326
|
+
output,
|
|
327
|
+
vars: z.object({ group: z.string().default('none') }),
|
|
328
|
+
})
|
|
329
|
+
|
|
330
|
+
input.write(
|
|
331
|
+
`${JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'initialize', params: initParams })}\n`,
|
|
332
|
+
)
|
|
333
|
+
await new Promise((r) => setTimeout(r, 10))
|
|
334
|
+
input.write(
|
|
335
|
+
`${JSON.stringify({ jsonrpc: '2.0', id: 2, method: 'tools/call', params: { name: 'admin_status', arguments: {} } })}\n`,
|
|
336
|
+
)
|
|
337
|
+
await new Promise((r) => setTimeout(r, 20))
|
|
338
|
+
input.end()
|
|
339
|
+
await done
|
|
340
|
+
|
|
341
|
+
const responses = chunks.map((c) => JSON.parse(c.trim()))
|
|
342
|
+
const callRes = responses.find((r: any) => r.id === 2)
|
|
343
|
+
expect(callRes.result.content).toEqual([{ type: 'text', text: '{"group":"admin"}' }])
|
|
344
|
+
})
|
|
345
|
+
|
|
346
|
+
test('env schema is parsed for tool calls', async () => {
|
|
347
|
+
const commands = new Map<string, any>()
|
|
348
|
+
commands.set('check-env', {
|
|
349
|
+
description: 'Check env',
|
|
350
|
+
env: z.object({ MY_VAR: z.string().default('default-val') }),
|
|
351
|
+
run: (c: any) => ({ val: c.env.MY_VAR }),
|
|
352
|
+
})
|
|
353
|
+
|
|
354
|
+
const [, res] = await mcpSession(commands, [
|
|
355
|
+
{ id: 1, method: 'initialize', params: initParams },
|
|
356
|
+
{ id: 2, method: 'tools/call', params: { name: 'check-env', arguments: {} } },
|
|
357
|
+
])
|
|
358
|
+
const data = JSON.parse(res.result.content[0].text)
|
|
359
|
+
expect(data.val).toBe('default-val')
|
|
360
|
+
})
|
|
361
|
+
|
|
219
362
|
test('streaming command sends progress notifications', async () => {
|
|
220
363
|
const input = new PassThrough()
|
|
221
364
|
const output = new PassThrough()
|
package/src/Mcp.ts
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
|
|
2
2
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
|
|
3
3
|
import type { Readable, Writable } from 'node:stream'
|
|
4
|
+
import type { z } from 'zod'
|
|
4
5
|
|
|
6
|
+
import * as Command from './internal/command.js'
|
|
7
|
+
import type { Handler as MiddlewareHandler } from './middleware.js'
|
|
5
8
|
import * as Schema from './Schema.js'
|
|
6
9
|
|
|
7
10
|
/** Starts a stdio MCP server that exposes commands as tools. */
|
|
@@ -25,12 +28,20 @@ export async function serve(
|
|
|
25
28
|
{
|
|
26
29
|
...(tool.description ? { description: tool.description } : undefined),
|
|
27
30
|
...(hasInput ? { inputSchema: mergedShape } : undefined),
|
|
28
|
-
|
|
31
|
+
...(tool.outputSchema ? { outputSchema: tool.outputSchema } : undefined),
|
|
32
|
+
} as never,
|
|
29
33
|
async (...callArgs: any[]) => {
|
|
30
34
|
// registerTool passes (args, extra) when inputSchema is set, (extra) when not
|
|
31
35
|
const params = hasInput ? (callArgs[0] as Record<string, unknown>) : {}
|
|
32
36
|
const extra = hasInput ? callArgs[1] : callArgs[0]
|
|
33
|
-
return callTool(tool, params,
|
|
37
|
+
return callTool(tool, params, {
|
|
38
|
+
extra,
|
|
39
|
+
name,
|
|
40
|
+
version,
|
|
41
|
+
middlewares: options.middlewares,
|
|
42
|
+
env: options.env,
|
|
43
|
+
vars: options.vars,
|
|
44
|
+
})
|
|
34
45
|
},
|
|
35
46
|
)
|
|
36
47
|
}
|
|
@@ -44,10 +55,18 @@ export async function serve(
|
|
|
44
55
|
export declare namespace serve {
|
|
45
56
|
/** Options for the MCP server. */
|
|
46
57
|
type Options = {
|
|
58
|
+
/** CLI-level env schema. */
|
|
59
|
+
env?: z.ZodObject<any> | undefined
|
|
47
60
|
/** Override input stream. Defaults to `process.stdin`. */
|
|
48
61
|
input?: Readable | undefined
|
|
62
|
+
/** Middleware handlers registered on the root CLI. */
|
|
63
|
+
middlewares?: MiddlewareHandler[] | undefined
|
|
49
64
|
/** Override output stream. Defaults to `process.stdout`. */
|
|
50
65
|
output?: Writable | undefined
|
|
66
|
+
/** Vars schema for middleware variables. */
|
|
67
|
+
vars?: z.ZodObject<any> | undefined
|
|
68
|
+
/** CLI version string. */
|
|
69
|
+
version?: string | undefined
|
|
51
70
|
}
|
|
52
71
|
}
|
|
53
72
|
|
|
@@ -55,83 +74,73 @@ export declare namespace serve {
|
|
|
55
74
|
export async function callTool(
|
|
56
75
|
tool: ToolEntry,
|
|
57
76
|
params: Record<string, unknown>,
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
77
|
+
options: {
|
|
78
|
+
extra?: {
|
|
79
|
+
_meta?: { progressToken?: string | number }
|
|
80
|
+
sendNotification?: (n: any) => Promise<void>
|
|
81
|
+
}
|
|
82
|
+
name?: string | undefined
|
|
83
|
+
version?: string | undefined
|
|
84
|
+
middlewares?: MiddlewareHandler[] | undefined
|
|
85
|
+
env?: z.ZodObject<any> | undefined
|
|
86
|
+
vars?: z.ZodObject<any> | undefined
|
|
87
|
+
} = {},
|
|
88
|
+
): Promise<{ content: { type: 'text'; text: string }[]; structuredContent?: Record<string, unknown>; isError?: boolean }> {
|
|
89
|
+
const allMiddleware = [
|
|
90
|
+
...(options.middlewares ?? []),
|
|
91
|
+
...((tool.middlewares as MiddlewareHandler[] | undefined) ?? []),
|
|
92
|
+
...((tool.command.middleware as MiddlewareHandler[] | undefined) ?? []),
|
|
93
|
+
]
|
|
94
|
+
|
|
95
|
+
const result = await Command.execute(tool.command, {
|
|
96
|
+
agent: true,
|
|
97
|
+
argv: [],
|
|
98
|
+
env: options.env,
|
|
99
|
+
format: 'json',
|
|
100
|
+
formatExplicit: true,
|
|
101
|
+
inputOptions: params,
|
|
102
|
+
middlewares: allMiddleware,
|
|
103
|
+
name: options.name ?? tool.name,
|
|
104
|
+
parseMode: 'flat',
|
|
105
|
+
path: tool.name,
|
|
106
|
+
vars: options.vars,
|
|
107
|
+
version: options.version,
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
if ('stream' in result) {
|
|
82
111
|
// Streaming: send progress notifications per chunk, then return buffered result
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
for await (const chunk of
|
|
88
|
-
if (typeof chunk === 'object' && chunk !== null && sentinel in chunk) {
|
|
89
|
-
const tagged = chunk as any
|
|
90
|
-
if (tagged[sentinel] === 'error')
|
|
91
|
-
return {
|
|
92
|
-
content: [{ type: 'text', text: tagged.message ?? 'Command failed' }],
|
|
93
|
-
isError: true,
|
|
94
|
-
}
|
|
95
|
-
}
|
|
112
|
+
const chunks: unknown[] = []
|
|
113
|
+
const progressToken = options.extra?._meta?.progressToken
|
|
114
|
+
let i = 0
|
|
115
|
+
try {
|
|
116
|
+
for await (const chunk of result.stream) {
|
|
96
117
|
chunks.push(chunk)
|
|
97
|
-
if (progressToken !== undefined && extra?.sendNotification)
|
|
98
|
-
await extra.sendNotification({
|
|
118
|
+
if (progressToken !== undefined && options.extra?.sendNotification)
|
|
119
|
+
await options.extra.sendNotification({
|
|
99
120
|
method: 'notifications/progress' as const,
|
|
100
121
|
params: { progressToken, progress: ++i, message: JSON.stringify(chunk) },
|
|
101
122
|
})
|
|
102
123
|
}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
if (typeof awaited === 'object' && awaited !== null && sentinel in awaited) {
|
|
109
|
-
const tagged = awaited as any
|
|
110
|
-
if (tagged[sentinel] === 'error')
|
|
111
|
-
return {
|
|
112
|
-
content: [{ type: 'text', text: tagged.message ?? 'Command failed' }],
|
|
113
|
-
isError: true,
|
|
114
|
-
}
|
|
115
|
-
return { content: [{ type: 'text', text: JSON.stringify(tagged.data ?? null) }] }
|
|
124
|
+
} catch (err) {
|
|
125
|
+
return {
|
|
126
|
+
content: [{ type: 'text', text: err instanceof Error ? err.message : String(err) }],
|
|
127
|
+
isError: true,
|
|
128
|
+
}
|
|
116
129
|
}
|
|
130
|
+
return { content: [{ type: 'text', text: JSON.stringify(chunks) }] }
|
|
131
|
+
}
|
|
117
132
|
|
|
118
|
-
|
|
119
|
-
} catch (err) {
|
|
133
|
+
if (!result.ok)
|
|
120
134
|
return {
|
|
121
|
-
content: [{ type: 'text', text:
|
|
135
|
+
content: [{ type: 'text', text: result.error.message ?? 'Command failed' }],
|
|
122
136
|
isError: true,
|
|
123
137
|
}
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
138
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
Symbol.asyncIterator in value &&
|
|
133
|
-
typeof (value as any).next === 'function'
|
|
134
|
-
)
|
|
139
|
+
const data = result.data ?? null
|
|
140
|
+
return {
|
|
141
|
+
content: [{ type: 'text', text: JSON.stringify(data) }],
|
|
142
|
+
...(data !== null && tool.outputSchema ? { structuredContent: data as Record<string, unknown> } : undefined),
|
|
143
|
+
}
|
|
135
144
|
}
|
|
136
145
|
|
|
137
146
|
/** @internal A resolved tool entry from the command tree. */
|
|
@@ -139,21 +148,34 @@ export type ToolEntry = {
|
|
|
139
148
|
name: string
|
|
140
149
|
description?: string | undefined
|
|
141
150
|
inputSchema: { type: 'object'; properties: Record<string, unknown>; required?: string[] }
|
|
151
|
+
outputSchema?: Record<string, unknown> | undefined
|
|
142
152
|
command: any
|
|
153
|
+
middlewares?: MiddlewareHandler[] | undefined
|
|
143
154
|
}
|
|
144
155
|
|
|
145
156
|
/** @internal Recursively collects leaf commands as tool entries. */
|
|
146
|
-
export function collectTools(
|
|
157
|
+
export function collectTools(
|
|
158
|
+
commands: Map<string, any>,
|
|
159
|
+
prefix: string[],
|
|
160
|
+
parentMiddlewares: MiddlewareHandler[] = [],
|
|
161
|
+
): ToolEntry[] {
|
|
147
162
|
const result: ToolEntry[] = []
|
|
148
163
|
for (const [name, entry] of commands) {
|
|
149
164
|
const path = [...prefix, name]
|
|
150
|
-
if ('_group' in entry && entry._group)
|
|
151
|
-
|
|
165
|
+
if ('_group' in entry && entry._group) {
|
|
166
|
+
const groupMw = [
|
|
167
|
+
...parentMiddlewares,
|
|
168
|
+
...((entry.middlewares as MiddlewareHandler[] | undefined) ?? []),
|
|
169
|
+
]
|
|
170
|
+
result.push(...collectTools(entry.commands, path, groupMw))
|
|
171
|
+
} else {
|
|
152
172
|
result.push({
|
|
153
173
|
name: path.join('_'),
|
|
154
174
|
description: entry.description,
|
|
155
175
|
inputSchema: buildToolSchema(entry.args, entry.options),
|
|
176
|
+
...(entry.output ? { outputSchema: Schema.toJsonSchema(entry.output) as Record<string, unknown> } : undefined),
|
|
156
177
|
command: entry,
|
|
178
|
+
...(parentMiddlewares.length > 0 ? { middlewares: parentMiddlewares } : undefined),
|
|
157
179
|
})
|
|
158
180
|
}
|
|
159
181
|
}
|
|
@@ -179,17 +201,3 @@ function buildToolSchema(
|
|
|
179
201
|
return { type: 'object', properties }
|
|
180
202
|
}
|
|
181
203
|
|
|
182
|
-
/** @internal Splits flat params into args vs options using schema shapes. */
|
|
183
|
-
function splitParams(
|
|
184
|
-
params: Record<string, unknown>,
|
|
185
|
-
command: any,
|
|
186
|
-
): { args: Record<string, unknown>; options: Record<string, unknown> } {
|
|
187
|
-
const argKeys = new Set(command.args ? Object.keys(command.args.shape) : [])
|
|
188
|
-
const a: Record<string, unknown> = {}
|
|
189
|
-
const o: Record<string, unknown> = {}
|
|
190
|
-
for (const [key, value] of Object.entries(params)) {
|
|
191
|
-
if (argKeys.has(key)) a[key] = value
|
|
192
|
-
else o[key] = value
|
|
193
|
-
}
|
|
194
|
-
return { args: a, options: o }
|
|
195
|
-
}
|
package/src/Skill.ts
CHANGED
|
@@ -127,7 +127,11 @@ function renderGroup(
|
|
|
127
127
|
? `${descParts.join('. ')}. Run \`${title} --help\` for usage details.`
|
|
128
128
|
: `Run \`${title} --help\` for usage details.`
|
|
129
129
|
|
|
130
|
-
const slug = title
|
|
130
|
+
const slug = title
|
|
131
|
+
.toLowerCase()
|
|
132
|
+
.replace(/[^a-z0-9-]+/g, '-')
|
|
133
|
+
.replace(/-{2,}/g, '-')
|
|
134
|
+
.replace(/^-|-$/g, '')
|
|
131
135
|
const fm = ['---', `name: ${slug}`]
|
|
132
136
|
fm.push(`description: ${description}`)
|
|
133
137
|
fm.push(`requires_bin: ${cli}`)
|
package/src/SyncSkills.ts
CHANGED
|
@@ -150,7 +150,17 @@ function collectEntries(
|
|
|
150
150
|
function resolvePackageRoot(): string {
|
|
151
151
|
const bin = process.argv[1]
|
|
152
152
|
if (!bin) return process.cwd()
|
|
153
|
-
let dir = path.dirname(
|
|
153
|
+
let dir = path.dirname(
|
|
154
|
+
(() => {
|
|
155
|
+
try {
|
|
156
|
+
// resolve symlinks for normal bin scripts
|
|
157
|
+
return fsSync.realpathSync(bin)
|
|
158
|
+
} catch {
|
|
159
|
+
// Bun compiled binaries use a virtual `/$bunfs/` path for argv[1]
|
|
160
|
+
return process.execPath
|
|
161
|
+
}
|
|
162
|
+
})(),
|
|
163
|
+
)
|
|
154
164
|
const root = path.parse(dir).root
|
|
155
165
|
while (dir !== root) {
|
|
156
166
|
try {
|