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.
Files changed (68) hide show
  1. package/README.md +62 -1
  2. package/dist/Cli.d.ts +17 -7
  3. package/dist/Cli.d.ts.map +1 -1
  4. package/dist/Cli.js +435 -365
  5. package/dist/Cli.js.map +1 -1
  6. package/dist/Completions.d.ts +1 -2
  7. package/dist/Completions.d.ts.map +1 -1
  8. package/dist/Completions.js.map +1 -1
  9. package/dist/Filter.js +0 -18
  10. package/dist/Filter.js.map +1 -1
  11. package/dist/Help.d.ts +6 -0
  12. package/dist/Help.d.ts.map +1 -1
  13. package/dist/Help.js +35 -22
  14. package/dist/Help.js.map +1 -1
  15. package/dist/Mcp.d.ts +25 -5
  16. package/dist/Mcp.d.ts.map +1 -1
  17. package/dist/Mcp.js +61 -69
  18. package/dist/Mcp.js.map +1 -1
  19. package/dist/Parser.d.ts +2 -0
  20. package/dist/Parser.d.ts.map +1 -1
  21. package/dist/Parser.js +69 -37
  22. package/dist/Parser.js.map +1 -1
  23. package/dist/Skill.d.ts.map +1 -1
  24. package/dist/Skill.js +5 -1
  25. package/dist/Skill.js.map +1 -1
  26. package/dist/SyncSkills.d.ts.map +1 -1
  27. package/dist/SyncSkills.js +10 -1
  28. package/dist/SyncSkills.js.map +1 -1
  29. package/dist/bin.d.ts +1 -0
  30. package/dist/bin.d.ts.map +1 -1
  31. package/dist/bin.js +17 -2
  32. package/dist/bin.js.map +1 -1
  33. package/dist/internal/command.d.ts +118 -0
  34. package/dist/internal/command.d.ts.map +1 -0
  35. package/dist/internal/command.js +276 -0
  36. package/dist/internal/command.js.map +1 -0
  37. package/dist/internal/configSchema.d.ts +8 -0
  38. package/dist/internal/configSchema.d.ts.map +1 -0
  39. package/dist/internal/configSchema.js +57 -0
  40. package/dist/internal/configSchema.js.map +1 -0
  41. package/dist/internal/helpers.d.ts +5 -0
  42. package/dist/internal/helpers.d.ts.map +1 -0
  43. package/dist/internal/helpers.js +9 -0
  44. package/dist/internal/helpers.js.map +1 -0
  45. package/examples/npm/.npmrc.json +21 -0
  46. package/examples/npm/config.schema.json +137 -0
  47. package/package.json +1 -1
  48. package/src/Cli.test-d.ts +39 -0
  49. package/src/Cli.test.ts +704 -6
  50. package/src/Cli.ts +551 -448
  51. package/src/Completions.test.ts +35 -9
  52. package/src/Completions.ts +1 -2
  53. package/src/Filter.ts +0 -17
  54. package/src/Help.test.ts +77 -0
  55. package/src/Help.ts +39 -21
  56. package/src/Mcp.test.ts +143 -0
  57. package/src/Mcp.ts +92 -84
  58. package/src/Parser.test-d.ts +22 -0
  59. package/src/Parser.test.ts +89 -0
  60. package/src/Parser.ts +86 -35
  61. package/src/Skill.ts +5 -1
  62. package/src/SyncSkills.ts +11 -1
  63. package/src/bin.ts +21 -2
  64. package/src/e2e.test.ts +30 -17
  65. package/src/internal/command.ts +428 -0
  66. package/src/internal/configSchema.test.ts +193 -0
  67. package/src/internal/configSchema.ts +66 -0
  68. package/src/internal/helpers.ts +9 -0
@@ -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 <shell>
297
+ Usage: mycli completions <bash|fish|nushell|zsh>
298
298
 
299
- Shells:
300
- bash
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('errors on missing shell argument', async () => {
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('Missing shell argument')
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', '--'], {
@@ -1,7 +1,6 @@
1
1
  import type { z } from 'zod'
2
2
 
3
- /** Supported shells for completion generation. */
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
- parts.push((schema as z.ZodType)._zod.optout === 'optional' ? `[${key}]` : `<${key}>`)
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: 'completions', desc: 'Generate shell completion script' },
335
- { name: 'mcp add', desc: 'Register as MCP server' },
336
- { name: 'skills add', desc: 'Sync skill files to agents' },
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
- 'Built-in Commands:',
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 3 characters. */
390
+ /** Redacts a value, showing only the last 4 characters. */
373
391
  function redact(value: string): string {
374
- if (value.length <= 3) return '••••'
375
- return `••••${value.slice(-3)}`
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()