incur 0.3.4 → 0.3.6
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 +62 -1
- package/dist/Cli.d.ts +17 -7
- package/dist/Cli.d.ts.map +1 -1
- package/dist/Cli.js +435 -365
- 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/Filter.js +0 -18
- package/dist/Filter.js.map +1 -1
- package/dist/Help.d.ts +6 -0
- package/dist/Help.d.ts.map +1 -1
- package/dist/Help.js +35 -22
- 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/Parser.d.ts +2 -0
- package/dist/Parser.d.ts.map +1 -1
- package/dist/Parser.js +69 -37
- package/dist/Parser.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/bin.d.ts +1 -0
- package/dist/bin.d.ts.map +1 -1
- package/dist/bin.js +17 -2
- package/dist/bin.js.map +1 -1
- package/dist/internal/command.d.ts +118 -0
- package/dist/internal/command.d.ts.map +1 -0
- package/dist/internal/command.js +276 -0
- package/dist/internal/command.js.map +1 -0
- package/dist/internal/configSchema.d.ts +8 -0
- package/dist/internal/configSchema.d.ts.map +1 -0
- package/dist/internal/configSchema.js +57 -0
- package/dist/internal/configSchema.js.map +1 -0
- package/dist/internal/helpers.d.ts +5 -0
- package/dist/internal/helpers.d.ts.map +1 -0
- package/dist/internal/helpers.js +9 -0
- package/dist/internal/helpers.js.map +1 -0
- package/examples/npm/.npmrc.json +21 -0
- package/examples/npm/config.schema.json +137 -0
- package/package.json +1 -1
- package/src/Cli.test-d.ts +39 -0
- package/src/Cli.test.ts +704 -6
- package/src/Cli.ts +551 -448
- package/src/Completions.test.ts +35 -9
- package/src/Completions.ts +1 -2
- package/src/Filter.ts +0 -17
- package/src/Help.test.ts +77 -0
- package/src/Help.ts +39 -21
- package/src/Mcp.test.ts +143 -0
- package/src/Mcp.ts +92 -84
- package/src/Parser.test-d.ts +22 -0
- package/src/Parser.test.ts +89 -0
- package/src/Parser.ts +86 -35
- package/src/Skill.ts +5 -1
- package/src/SyncSkills.ts +11 -1
- package/src/bin.ts +21 -2
- package/src/e2e.test.ts +30 -17
- package/src/internal/command.ts +428 -0
- package/src/internal/configSchema.test.ts +193 -0
- package/src/internal/configSchema.ts +66 -0
- package/src/internal/helpers.ts +9 -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/Filter.ts
CHANGED
|
@@ -80,23 +80,6 @@ export function apply(data: unknown, paths: FilterPath[]): unknown {
|
|
|
80
80
|
return result
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
-
function resolve(data: unknown, segments: Segment[], index: number): unknown {
|
|
84
|
-
if (index >= segments.length) return data
|
|
85
|
-
const segment = segments[index]!
|
|
86
|
-
|
|
87
|
-
if ('key' in segment) {
|
|
88
|
-
if (typeof data !== 'object' || data === null) return undefined
|
|
89
|
-
const val = (data as Record<string, unknown>)[segment.key]
|
|
90
|
-
return resolve(val, segments, index + 1)
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
// slice segment
|
|
94
|
-
if (!Array.isArray(data)) return undefined
|
|
95
|
-
const sliced = data.slice(segment.start, segment.end)
|
|
96
|
-
if (index + 1 >= segments.length) return sliced
|
|
97
|
-
return sliced.map((item) => resolve(item, segments, index + 1))
|
|
98
|
-
}
|
|
99
|
-
|
|
100
83
|
function merge(
|
|
101
84
|
target: Record<string, unknown>,
|
|
102
85
|
data: unknown,
|
package/src/Help.test.ts
CHANGED
|
@@ -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({
|
|
@@ -130,6 +141,36 @@ describe('formatCommand', () => {
|
|
|
130
141
|
expect(result).toContain('[deprecated] Availability zone')
|
|
131
142
|
expect(result).not.toContain('[deprecated] Target region')
|
|
132
143
|
})
|
|
144
|
+
|
|
145
|
+
test('shows config global options when flag name is set', () => {
|
|
146
|
+
const result = Help.formatCommand('tool deploy', {
|
|
147
|
+
configFlag: 'config',
|
|
148
|
+
options: z.object({
|
|
149
|
+
env: z.enum(['staging', 'production']).describe('Target environment'),
|
|
150
|
+
}),
|
|
151
|
+
})
|
|
152
|
+
expect(result).toMatchInlineSnapshot(`
|
|
153
|
+
"tool deploy
|
|
154
|
+
|
|
155
|
+
Usage: tool deploy [options]
|
|
156
|
+
|
|
157
|
+
Options:
|
|
158
|
+
--env <staging|production> Target environment
|
|
159
|
+
|
|
160
|
+
Global Options:
|
|
161
|
+
--config <path> Load JSON option defaults from a file
|
|
162
|
+
--filter-output <keys> Filter output by key paths (e.g. foo,bar.baz,a[0,3])
|
|
163
|
+
--format <toon|json|yaml|md|jsonl> Output format
|
|
164
|
+
--help Show help
|
|
165
|
+
--llms, --llms-full Print LLM-readable manifest
|
|
166
|
+
--no-config Disable JSON option defaults for this run
|
|
167
|
+
--schema Show JSON Schema for command
|
|
168
|
+
--token-count Print token count of output (instead of output)
|
|
169
|
+
--token-limit <n> Limit output to n tokens
|
|
170
|
+
--token-offset <n> Skip first n tokens of output
|
|
171
|
+
--verbose Show full output envelope"
|
|
172
|
+
`)
|
|
173
|
+
})
|
|
133
174
|
})
|
|
134
175
|
|
|
135
176
|
describe('formatRoot', () => {
|
|
@@ -247,4 +288,40 @@ describe('formatRoot', () => {
|
|
|
247
288
|
--verbose Show full output envelope"
|
|
248
289
|
`)
|
|
249
290
|
})
|
|
291
|
+
|
|
292
|
+
test('formatRoot shows config global options when flag name is set', () => {
|
|
293
|
+
const result = Help.formatRoot('tool', {
|
|
294
|
+
configFlag: 'config',
|
|
295
|
+
root: true,
|
|
296
|
+
commands: [{ name: 'ping', description: 'Health check' }],
|
|
297
|
+
})
|
|
298
|
+
expect(result).toMatchInlineSnapshot(`
|
|
299
|
+
"tool
|
|
300
|
+
|
|
301
|
+
Usage: tool <command>
|
|
302
|
+
|
|
303
|
+
Commands:
|
|
304
|
+
ping Health check
|
|
305
|
+
|
|
306
|
+
Integrations:
|
|
307
|
+
completions Generate shell completion script
|
|
308
|
+
mcp add Register as MCP server
|
|
309
|
+
skills add Sync skill files to agents
|
|
310
|
+
|
|
311
|
+
Global Options:
|
|
312
|
+
--config <path> Load JSON option defaults from a file
|
|
313
|
+
--filter-output <keys> Filter output by key paths (e.g. foo,bar.baz,a[0,3])
|
|
314
|
+
--format <toon|json|yaml|md|jsonl> Output format
|
|
315
|
+
--help Show help
|
|
316
|
+
--llms, --llms-full Print LLM-readable manifest
|
|
317
|
+
--mcp Start as MCP stdio server
|
|
318
|
+
--no-config Disable JSON option defaults for this run
|
|
319
|
+
--schema Show JSON Schema for command
|
|
320
|
+
--token-count Print token count of output (instead of output)
|
|
321
|
+
--token-limit <n> Limit output to n tokens
|
|
322
|
+
--token-offset <n> Skip first n tokens of output
|
|
323
|
+
--verbose Show full output envelope
|
|
324
|
+
--version Show version"
|
|
325
|
+
`)
|
|
326
|
+
})
|
|
250
327
|
})
|
package/src/Help.ts
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { z } from 'zod'
|
|
2
2
|
|
|
3
|
+
import { builtinCommands } from './internal/command.js'
|
|
4
|
+
import { toKebab } from './internal/helpers.js'
|
|
5
|
+
|
|
3
6
|
/** Formats help text for a router CLI or command group. */
|
|
4
7
|
export function formatRoot(name: string, options: formatRoot.Options = {}): string {
|
|
5
|
-
const { aliases, description, version, commands = [], root = false } = options
|
|
8
|
+
const { aliases, configFlag, description, version, commands = [], root = false } = options
|
|
6
9
|
const lines: string[] = []
|
|
7
10
|
|
|
8
11
|
// Header
|
|
@@ -27,7 +30,7 @@ export function formatRoot(name: string, options: formatRoot.Options = {}): stri
|
|
|
27
30
|
}
|
|
28
31
|
}
|
|
29
32
|
|
|
30
|
-
lines.push(...globalOptionsLines(root))
|
|
33
|
+
lines.push(...globalOptionsLines(root, configFlag))
|
|
31
34
|
|
|
32
35
|
return lines.join('\n')
|
|
33
36
|
}
|
|
@@ -36,6 +39,8 @@ export declare namespace formatRoot {
|
|
|
36
39
|
type Options = {
|
|
37
40
|
/** Alternative binary names for this CLI. */
|
|
38
41
|
aliases?: string[] | undefined
|
|
42
|
+
/** Flag name for config file path (e.g. `'config'` renders `--config <path>`). */
|
|
43
|
+
configFlag?: string | undefined
|
|
39
44
|
/** Commands to list. */
|
|
40
45
|
commands?: { name: string; description?: string | undefined }[] | undefined
|
|
41
46
|
/** A short description of the CLI or group. */
|
|
@@ -55,6 +60,8 @@ export declare namespace formatCommand {
|
|
|
55
60
|
aliases?: string[] | undefined
|
|
56
61
|
/** Zod schema for positional arguments. */
|
|
57
62
|
args?: z.ZodObject<any> | undefined
|
|
63
|
+
/** Flag name for config file path (e.g. `'config'` renders `--config <path>`). */
|
|
64
|
+
configFlag?: string | undefined
|
|
58
65
|
/** Subcommands to list (for CLIs with both a root handler and subcommands). */
|
|
59
66
|
commands?: { name: string; description?: string | undefined }[] | undefined
|
|
60
67
|
/** A short description of what the command does. */
|
|
@@ -67,6 +74,8 @@ export declare namespace formatCommand {
|
|
|
67
74
|
examples?: { command: string; description?: string }[] | undefined
|
|
68
75
|
/** Plain text hint displayed after examples and before global options. */
|
|
69
76
|
hint?: string | undefined
|
|
77
|
+
/** Hide global options section. */
|
|
78
|
+
hideGlobalOptions?: boolean | undefined
|
|
70
79
|
/** Zod schema for named options/flags. */
|
|
71
80
|
options?: z.ZodObject<any> | undefined
|
|
72
81
|
/** Show root-level built-in commands and flags. */
|
|
@@ -90,6 +99,7 @@ export function formatCommand(name: string, options: formatCommand.Options = {})
|
|
|
90
99
|
const {
|
|
91
100
|
alias,
|
|
92
101
|
aliases,
|
|
102
|
+
configFlag,
|
|
93
103
|
description,
|
|
94
104
|
version,
|
|
95
105
|
args,
|
|
@@ -195,7 +205,7 @@ export function formatCommand(name: string, options: formatCommand.Options = {})
|
|
|
195
205
|
}
|
|
196
206
|
}
|
|
197
207
|
|
|
198
|
-
lines.push(...globalOptionsLines(root))
|
|
208
|
+
if (!options.hideGlobalOptions) lines.push(...globalOptionsLines(root, configFlag))
|
|
199
209
|
|
|
200
210
|
// Environment Variables
|
|
201
211
|
if (env) {
|
|
@@ -223,8 +233,11 @@ export function formatCommand(name: string, options: formatCommand.Options = {})
|
|
|
223
233
|
function buildSynopsis(name: string, args?: z.ZodObject<any>): string {
|
|
224
234
|
if (!args) return name
|
|
225
235
|
const parts = [name]
|
|
226
|
-
for (const [key, schema] of Object.entries(args.shape))
|
|
227
|
-
|
|
236
|
+
for (const [key, schema] of Object.entries(args.shape)) {
|
|
237
|
+
const type = resolveTypeName(schema)
|
|
238
|
+
const label = type.includes('|') ? type : key
|
|
239
|
+
parts.push((schema as z.ZodType)._zod.optout === 'optional' ? `[${label}]` : `<${label}>`)
|
|
240
|
+
}
|
|
228
241
|
return parts.join(' ')
|
|
229
242
|
}
|
|
230
243
|
|
|
@@ -320,30 +333,32 @@ function extractDeprecated(schema: unknown): boolean | undefined {
|
|
|
320
333
|
return meta?.deprecated === true ? true : undefined
|
|
321
334
|
}
|
|
322
335
|
|
|
323
|
-
/** Converts a camelCase string to kebab-case. */
|
|
324
|
-
function toKebab(str: string): string {
|
|
325
|
-
return str.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`)
|
|
326
|
-
}
|
|
327
|
-
|
|
328
336
|
/** Renders the built-in commands and global options block. Root-only items are hidden for subcommands. */
|
|
329
|
-
function globalOptionsLines(root = false): string[] {
|
|
337
|
+
function globalOptionsLines(root = false, configFlag?: string): string[] {
|
|
330
338
|
const lines: string[] = []
|
|
331
339
|
|
|
332
340
|
if (root) {
|
|
333
|
-
const builtins =
|
|
334
|
-
{ name:
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
341
|
+
const builtins = builtinCommands.flatMap((b) => {
|
|
342
|
+
if (!b.subcommands) return [{ name: b.name, desc: b.description }]
|
|
343
|
+
if (b.subcommands.length === 1)
|
|
344
|
+
return [
|
|
345
|
+
{ name: `${b.name} ${b.subcommands[0]!.name}`, desc: b.subcommands[0]!.description },
|
|
346
|
+
]
|
|
347
|
+
const names = b.subcommands.map((s) => s.name).join(', ')
|
|
348
|
+
return [{ name: b.name, desc: `${b.description} (${names})` }]
|
|
349
|
+
})
|
|
338
350
|
const maxCmd = Math.max(...builtins.map((b) => b.name.length))
|
|
339
351
|
lines.push(
|
|
340
352
|
'',
|
|
341
|
-
'
|
|
353
|
+
'Integrations:',
|
|
342
354
|
...builtins.map((b) => ` ${b.name}${' '.repeat(maxCmd - b.name.length)} ${b.desc}`),
|
|
343
355
|
)
|
|
344
356
|
}
|
|
345
357
|
|
|
346
358
|
const flags = [
|
|
359
|
+
...(configFlag
|
|
360
|
+
? [{ flag: `--${configFlag} <path>`, desc: 'Load JSON option defaults from a file' }]
|
|
361
|
+
: []),
|
|
347
362
|
{
|
|
348
363
|
flag: '--filter-output <keys>',
|
|
349
364
|
desc: 'Filter output by key paths (e.g. foo,bar.baz,a[0,3])',
|
|
@@ -352,13 +367,16 @@ function globalOptionsLines(root = false): string[] {
|
|
|
352
367
|
{ flag: '--help', desc: 'Show help' },
|
|
353
368
|
{ flag: '--llms, --llms-full', desc: 'Print LLM-readable manifest' },
|
|
354
369
|
...(root ? [{ flag: '--mcp', desc: 'Start as MCP stdio server' }] : []),
|
|
370
|
+
...(configFlag
|
|
371
|
+
? [{ flag: `--no-${configFlag}`, desc: 'Disable JSON option defaults for this run' }]
|
|
372
|
+
: []),
|
|
355
373
|
{ flag: '--schema', desc: 'Show JSON Schema for command' },
|
|
356
374
|
{ flag: '--token-count', desc: 'Print token count of output (instead of output)' },
|
|
357
375
|
{ flag: '--token-limit <n>', desc: 'Limit output to n tokens' },
|
|
358
376
|
{ flag: '--token-offset <n>', desc: 'Skip first n tokens of output' },
|
|
359
377
|
{ flag: '--verbose', desc: 'Show full output envelope' },
|
|
360
378
|
...(root ? [{ flag: '--version', desc: 'Show version' }] : []),
|
|
361
|
-
]
|
|
379
|
+
].sort((a, b) => a.flag.localeCompare(b.flag))
|
|
362
380
|
const maxLen = Math.max(...flags.map((f) => f.flag.length))
|
|
363
381
|
lines.push(
|
|
364
382
|
'',
|
|
@@ -369,8 +387,8 @@ function globalOptionsLines(root = false): string[] {
|
|
|
369
387
|
return lines
|
|
370
388
|
}
|
|
371
389
|
|
|
372
|
-
/** Redacts a value, showing only the last
|
|
390
|
+
/** Redacts a value, showing only the last 4 characters. */
|
|
373
391
|
function redact(value: string): string {
|
|
374
|
-
if (value.length <=
|
|
375
|
-
return
|
|
392
|
+
if (value.length <= 4) return '****'
|
|
393
|
+
return `****${value.slice(-4)}`
|
|
376
394
|
}
|
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()
|