argsbarg 1.4.0 → 1.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/index.ts CHANGED
@@ -7,8 +7,19 @@ It gives consumers one stable import path without forcing them to know the inter
7
7
  module layout.
8
8
  */
9
9
 
10
+ export { cliInvoke } from "./invoke.ts";
11
+ export type { CliInvokeKind, CliInvokeResult } from "./invoke.ts";
10
12
  export { CliContext } from "./context.ts";
11
13
  export { cliErrWithHelp, cliRun } from "./runtime";
12
14
  export { CliFallbackMode, CliOptionKind, CliSchemaValidationError } from "./types.ts";
13
- export type { CliCommand, CliHandler, CliMcpServerConfig, CliMcpToolConfig, CliOption, CliPositional } from "./types.ts";
15
+ export type {
16
+ CliCommand,
17
+ CliHandler,
18
+ CliInvocation,
19
+ CliMcpResource,
20
+ CliMcpServerConfig,
21
+ CliMcpToolConfig,
22
+ CliOption,
23
+ CliPositional,
24
+ } from "./types.ts";
14
25
  export { isInteractiveTty } from "./utils.ts";
package/src/invoke.ts CHANGED
@@ -108,7 +108,7 @@ export async function cliInvoke(root: CliCommand, argv: string[]): Promise<CliIn
108
108
  }
109
109
 
110
110
  const handler = current.handler;
111
- const ctx = new CliContext(root.key, pr.path, pr.args, pr.opts, root);
111
+ const ctx = new CliContext(root.key, pr.path, pr.args, pr.opts, root, "mcp");
112
112
 
113
113
  let stdout = "";
114
114
  let stderr = "";
package/src/mcp/env.ts ADDED
@@ -0,0 +1,99 @@
1
+ /*
2
+ This module bootstraps process.env for MCP servers from login shell and .env files.
3
+ */
4
+
5
+ import { readFileSync } from "node:fs";
6
+ import { spawnSync } from "node:child_process";
7
+
8
+ /** Parses `env` stdout from a login shell into a key/value map. */
9
+ export function captureShellEnv(shell: string): Record<string, string> {
10
+ const result = spawnSync(shell, ["-l", "-c", "env"], {
11
+ encoding: "utf8",
12
+ timeout: 5000,
13
+ });
14
+ if (result.error || result.status !== 0) {
15
+ return {};
16
+ }
17
+ const env: Record<string, string> = {};
18
+ for (const line of result.stdout.split("\n")) {
19
+ const eq = line.indexOf("=");
20
+ if (eq > 0) {
21
+ env[line.slice(0, eq)] = line.slice(eq + 1);
22
+ }
23
+ }
24
+ return env;
25
+ }
26
+
27
+ /** Merges captured shell env into process.env (PATH merged; host wins for other keys). */
28
+ export function applyShellEnv(env: Record<string, string>): void {
29
+ for (const [key, val] of Object.entries(env)) {
30
+ if (key === "PATH") {
31
+ const existing = process.env.PATH ?? "";
32
+ const existingParts = new Set(existing.split(":"));
33
+ const shellOnly = val.split(":").filter((p) => p.length > 0 && !existingParts.has(p));
34
+ if (shellOnly.length > 0) {
35
+ process.env.PATH = [...shellOnly, existing].join(":");
36
+ }
37
+ } else if (process.env[key] === undefined) {
38
+ process.env[key] = val;
39
+ }
40
+ }
41
+ }
42
+
43
+ /** Loads a .env file into process.env (always overwrites). Warns on stderr if missing. */
44
+ export function loadEnvFile(envFile: string): void {
45
+ const resolved = envFile.startsWith("~")
46
+ ? envFile.replace("~", process.env.HOME ?? "")
47
+ : envFile;
48
+ let text: string;
49
+ try {
50
+ text = readFileSync(resolved, "utf8");
51
+ } catch {
52
+ process.stderr.write(`[argsbarg] envFile not found: ${envFile}\n`);
53
+ return;
54
+ }
55
+ for (const line of text.split("\n")) {
56
+ const trimmed = line.trim();
57
+ if (!trimmed || trimmed.startsWith("#")) {
58
+ continue;
59
+ }
60
+ const eq = trimmed.indexOf("=");
61
+ if (eq < 1) {
62
+ continue;
63
+ }
64
+ const key = trimmed.slice(0, eq).trim();
65
+ let val = trimmed.slice(eq + 1).trim();
66
+ if (
67
+ (val.startsWith('"') && val.endsWith('"')) ||
68
+ (val.startsWith("'") && val.endsWith("'"))
69
+ ) {
70
+ val = val.slice(1, -1);
71
+ }
72
+ if (key) {
73
+ process.env[key] = val;
74
+ }
75
+ }
76
+ }
77
+
78
+ /** Applies mcpServer shellEnv and envFile bootstrap in order. */
79
+ export function bootstrapMcpEnv(config: {
80
+ shellEnv?: boolean | string;
81
+ envFile?: string;
82
+ }): void {
83
+ const shellEnvCfg = config.shellEnv;
84
+ if (shellEnvCfg) {
85
+ const shell =
86
+ typeof shellEnvCfg === "string"
87
+ ? shellEnvCfg
88
+ : (process.env.SHELL ?? (process.platform === "darwin" ? "/bin/zsh" : "/bin/bash"));
89
+ const captured = captureShellEnv(shell);
90
+ if (Object.keys(captured).length === 0) {
91
+ process.stderr.write(`[argsbarg] shellEnv: failed to capture shell environment from ${shell}\n`);
92
+ } else {
93
+ applyShellEnv(captured);
94
+ }
95
+ }
96
+ if (config.envFile) {
97
+ loadEnvFile(config.envFile);
98
+ }
99
+ }
package/src/mcp/server.ts CHANGED
@@ -4,13 +4,12 @@ resources, and ping. Responses are newline-delimited JSON on stdout only.
4
4
  */
5
5
 
6
6
  import { cliInvoke } from "../invoke.ts";
7
- import { cliSchemaJson } from "../schema.ts";
8
7
  import { CliCommand } from "../types.ts";
9
8
  import { buildToolCallSuccess } from "./result.ts";
10
9
  import {
10
+ allMcpResources,
11
11
  collectMcpTools,
12
12
  mcpToolCallToArgv,
13
- resolveMcpSchemaUri,
14
13
  resolveMcpServerInfo,
15
14
  } from "./tools.ts";
16
15
 
@@ -118,6 +117,18 @@ async function handleRequestLine(root: CliCommand, line: string): Promise<void>
118
117
  writeError(id, -32602, `Unknown tool: ${name}`);
119
118
  return;
120
119
  }
120
+ const missingEnv = (tool.leaf.mcpTool?.requiresEnv ?? []).filter((k) => !process.env[k]);
121
+ if (missingEnv.length > 0) {
122
+ writeResponse({
123
+ jsonrpc: "2.0",
124
+ id,
125
+ result: {
126
+ content: [{ type: "text", text: `Missing required env: ${missingEnv.join(", ")}` }],
127
+ isError: true,
128
+ },
129
+ });
130
+ return;
131
+ }
121
132
  const argvResult = mcpToolCallToArgv(root, tool, (rawArgs ?? {}) as Record<string, unknown>);
122
133
  if ("error" in argvResult) {
123
134
  writeResponse({
@@ -152,21 +163,13 @@ async function handleRequestLine(root: CliCommand, line: string): Promise<void>
152
163
  }
153
164
 
154
165
  if (method === "resources/list") {
155
- const uri = resolveMcpSchemaUri(root);
156
- writeResponse({
157
- jsonrpc: "2.0",
158
- id,
159
- result: {
160
- resources: [
161
- {
162
- uri,
163
- name: "cli-schema",
164
- description: "Full CLI command tree (same as --schema).",
165
- mimeType: "application/json",
166
- },
167
- ],
168
- },
169
- });
166
+ const resources = allMcpResources(root).map((r) => ({
167
+ uri: r.uri,
168
+ name: r.name,
169
+ description: r.description,
170
+ mimeType: r.mimeType,
171
+ }));
172
+ writeResponse({ jsonrpc: "2.0", id, result: { resources } });
170
173
  return;
171
174
  }
172
175
 
@@ -176,20 +179,29 @@ async function handleRequestLine(root: CliCommand, line: string): Promise<void>
176
179
  writeError(id, -32602, "Invalid params: uri required");
177
180
  return;
178
181
  }
179
- const expected = resolveMcpSchemaUri(root);
180
- if (uri !== expected) {
182
+ const all = allMcpResources(root);
183
+ const found = all.find((r) => r.uri === uri);
184
+ if (!found) {
181
185
  writeError(id, -32602, `Unknown resource: ${uri}`);
182
186
  return;
183
187
  }
188
+ let text: string;
189
+ try {
190
+ text = found.load();
191
+ } catch (err) {
192
+ const message = err instanceof Error ? err.message : String(err);
193
+ writeError(id, -32603, `Resource load failed: ${message}`);
194
+ return;
195
+ }
184
196
  writeResponse({
185
197
  jsonrpc: "2.0",
186
198
  id,
187
199
  result: {
188
200
  contents: [
189
201
  {
190
- uri: expected,
191
- mimeType: "application/json",
192
- text: cliSchemaJson(root).trimEnd(),
202
+ uri: found.uri,
203
+ mimeType: found.mimeType,
204
+ text,
193
205
  },
194
206
  ],
195
207
  },
package/src/mcp/tools.ts CHANGED
@@ -6,6 +6,7 @@ flat JSON tool arguments into argv for cliInvoke.
6
6
  import { readFileSync } from "node:fs";
7
7
  import { join } from "node:path";
8
8
  import { collectOptionDefs } from "../parse.ts";
9
+ import { cliSchemaJson } from "../schema.ts";
9
10
  import { CliCommand, CliOption, CliOptionKind, CliPositional } from "../types.ts";
10
11
 
11
12
  /** Default URI for the CLI schema MCP resource. */
@@ -54,6 +55,8 @@ function optionProperty(opt: CliOption): Record<string, unknown> {
54
55
  return { type: "string", ...base };
55
56
  case CliOptionKind.Number:
56
57
  return { type: "number", ...base };
58
+ case CliOptionKind.Enum:
59
+ return { type: "string", enum: opt.choices, ...base };
57
60
  }
58
61
  }
59
62
 
@@ -98,6 +101,48 @@ function buildInputSchema(root: CliCommand, path: string[], leaf: CliCommand): R
98
101
  return schema;
99
102
  }
100
103
 
104
+ /** Resolves MCP tool description with optional override and requiresEnv suffix. */
105
+ function resolveToolDescription(root: CliCommand, path: string[], leaf: CliCommand): string {
106
+ if (leaf.mcpTool?.description) {
107
+ return leaf.mcpTool.description;
108
+ }
109
+ let desc = mcpToolDescription(path, root.key, leaf.description);
110
+ const env = leaf.mcpTool?.requiresEnv;
111
+ if (env && env.length > 0) {
112
+ desc += ` [requires env: ${env.join(", ")}]`;
113
+ }
114
+ return desc;
115
+ }
116
+
117
+ /** One resolved MCP resource (built-in or user-defined). */
118
+ export interface McpResourceEntry {
119
+ uri: string;
120
+ name: string;
121
+ description?: string;
122
+ mimeType: string;
123
+ load: () => string;
124
+ }
125
+
126
+ /** Returns built-in schema resource plus user mcpServer.resources. */
127
+ export function allMcpResources(root: CliCommand): McpResourceEntry[] {
128
+ const schemaUri = resolveMcpSchemaUri(root);
129
+ const builtIn: McpResourceEntry = {
130
+ uri: schemaUri,
131
+ name: "cli-schema",
132
+ description: "Full CLI command tree (same as --schema).",
133
+ mimeType: "application/json",
134
+ load: () => cliSchemaJson(root),
135
+ };
136
+ const user = (root.mcpServer?.resources ?? []).map((r) => ({
137
+ uri: r.uri,
138
+ name: r.name,
139
+ description: r.description,
140
+ mimeType: r.mimeType ?? "text/plain",
141
+ load: r.load,
142
+ }));
143
+ return [builtIn, ...user];
144
+ }
145
+
101
146
  /** Recursively collects MCP tool definitions from user leaf commands. */
102
147
  export function collectMcpTools(root: CliCommand): McpToolDef[] {
103
148
  const out: McpToolDef[] = [];
@@ -113,7 +158,7 @@ export function collectMcpTools(root: CliCommand): McpToolDef[] {
113
158
  }
114
159
  out.push({
115
160
  name: mcpToolName(root, path),
116
- description: mcpToolDescription(path, root.key, cmd.description),
161
+ description: resolveToolDescription(root, path, cmd),
117
162
  path,
118
163
  leaf: cmd,
119
164
  inputSchema: buildInputSchema(root, path, cmd),
@@ -187,12 +232,20 @@ export function mcpToolCallToArgv(
187
232
  const { argMin = 1, argMax = 1 } = p;
188
233
 
189
234
  if (argMax === 0) {
190
- if (!Array.isArray(val)) {
191
- return { error: `Missing argument: ${p.name}` };
192
- }
193
- for (const item of val) {
194
- argv.push(String(item));
235
+ const raw = args[p.name];
236
+ let items: string[];
237
+ if (Array.isArray(raw)) {
238
+ items = raw.map(String);
239
+ } else if (typeof raw === "string") {
240
+ items = raw.includes(",")
241
+ ? raw.split(",").map((s) => s.trim()).filter(Boolean)
242
+ : raw.trim()
243
+ ? [raw.trim()]
244
+ : [];
245
+ } else {
246
+ items = [];
195
247
  }
248
+ argv.push(...items);
196
249
  continue;
197
250
  }
198
251
 
package/src/mcp.ts CHANGED
@@ -3,6 +3,7 @@ This module starts the ArgsBarg MCP stdio server for opt-in program roots.
3
3
  */
4
4
 
5
5
  import { mcpServeStdioLoop } from "./mcp/server.ts";
6
+ import { bootstrapMcpEnv } from "./mcp/env.ts";
6
7
  import { CliCommand } from "./types.ts";
7
8
 
8
9
  /**
@@ -11,6 +12,9 @@ import { CliCommand } from "./types.ts";
11
12
  */
12
13
  export async function cliMcpServeStdio(root: CliCommand): Promise<never> {
13
14
  try {
15
+ if (root.mcpServer) {
16
+ bootstrapMcpEnv(root.mcpServer);
17
+ }
14
18
  await mcpServeStdioLoop(root);
15
19
  process.exit(0);
16
20
  } catch (err) {
package/src/parse.ts CHANGED
@@ -231,11 +231,6 @@ export function collectOptionDefs(root: CliCommand, path: string[]): CliOption[]
231
231
  return defs;
232
232
  }
233
233
 
234
- /** True when every positional slot has bounded arity (no `argMax: 0` varargs tail). */
235
- function allowsTrailingOptions(positionals: CliCommand["positionals"]): boolean {
236
- return (positionals ?? []).every((p) => (p.argMax ?? 1) !== 0);
237
- }
238
-
239
234
  /** Fills `args` for a leaf from `startIdx` according to `node.positionals`. */
240
235
  function finishLeaf(
241
236
  node: CliCommand,
@@ -244,7 +239,7 @@ function finishLeaf(
244
239
  path: string[],
245
240
  opts: Record<string, string>,
246
241
  optionDefs: CliOption[],
247
- forcePositionals: boolean,
242
+ forcePositionalsIn: boolean,
248
243
  ): ParseResult {
249
244
  /** Builds a parse error for positional consumption failures. */
250
245
  function errorResult(msg: string): ParseResult {
@@ -263,6 +258,7 @@ function finishLeaf(
263
258
 
264
259
  let idx = startIdx;
265
260
  const args: string[] = [];
261
+ let forcePositionals = forcePositionalsIn;
266
262
 
267
263
  for (const p of node.positionals ?? []) {
268
264
  const { argMin = 1, argMax = 1 } = p;
@@ -288,9 +284,37 @@ function finishLeaf(
288
284
  let count = 0;
289
285
  if (argMax === 0) {
290
286
  while (idx < argv.length) {
291
- args.push(argv[idx]);
292
- idx += 1;
293
- count += 1;
287
+ const tok = argv[idx];
288
+
289
+ if (!forcePositionals && tok === "--") {
290
+ forcePositionals = true;
291
+ idx++;
292
+ continue;
293
+ }
294
+
295
+ if (!forcePositionals && isHelpTok(tok)) {
296
+ return helpResult(path, true);
297
+ }
298
+
299
+ if (!forcePositionals && tok.startsWith("-")) {
300
+ // MUST be false — lenient mode swallows unknown flags as positionals silently
301
+ const tailRep = consumeOptions(optionDefs, false, argv, idx, opts);
302
+ if (tailRep.report.err) {
303
+ return errorResult(tailRep.report.err);
304
+ }
305
+ if (tailRep.report.sawDoubleDash) {
306
+ forcePositionals = true;
307
+ }
308
+ if (tailRep.nextIndex > idx) {
309
+ idx = tailRep.nextIndex;
310
+ continue;
311
+ }
312
+ return errorResult(`Unexpected option token: ${tok}`);
313
+ }
314
+
315
+ args.push(tok);
316
+ idx++;
317
+ count++;
294
318
  }
295
319
  } else {
296
320
  while (count < argMax && idx < argv.length) {
@@ -305,7 +329,7 @@ function finishLeaf(
305
329
  }
306
330
 
307
331
  if (idx < argv.length) {
308
- if (forcePositionals || !allowsTrailingOptions(node.positionals)) {
332
+ if (forcePositionals) {
309
333
  return errorResult("Unexpected extra arguments");
310
334
  }
311
335
 
@@ -483,6 +507,19 @@ export function parse(root: CliCommand, argv: string[]): ParseResult {
483
507
 
484
508
  if (i >= argv.length) {
485
509
  if ((current.commands ?? []).length > 0) {
510
+ const fb = current.fallbackCommand;
511
+ const fm = current.fallbackMode ?? CliFallbackMode.MissingOnly;
512
+ if (
513
+ fb !== undefined &&
514
+ (fm === CliFallbackMode.MissingOnly || fm === CliFallbackMode.MissingOrUnknown)
515
+ ) {
516
+ const fbNode = findChild(current.commands ?? [], fb);
517
+ if (fbNode) {
518
+ path.push(fb);
519
+ current = fbNode;
520
+ continue;
521
+ }
522
+ }
486
523
  return helpResult(path, false);
487
524
  }
488
525
  return finishLeaf(current, i, argv, path, opts, collectOptionDefs(root, path), forcePositionals);
@@ -513,6 +550,21 @@ export function parse(root: CliCommand, argv: string[]): ParseResult {
513
550
  }
514
551
 
515
552
  if ((current.commands ?? []).length > 0) {
553
+ const fb = current.fallbackCommand;
554
+ const fm = current.fallbackMode ?? CliFallbackMode.MissingOnly;
555
+ const canRouteUnknown =
556
+ fb !== undefined &&
557
+ (fm === CliFallbackMode.MissingOrUnknown || fm === CliFallbackMode.UnknownOnly);
558
+
559
+ if (canRouteUnknown) {
560
+ const fbNode = findChild(current.commands ?? [], fb!);
561
+ if (fbNode) {
562
+ path.push(fb!);
563
+ current = fbNode;
564
+ continue;
565
+ }
566
+ }
567
+
516
568
  return {
517
569
  kind: ParseKind.Error,
518
570
  path,
@@ -601,6 +653,21 @@ export function postParseValidate(root: CliCommand, pr: ParseResult): ParseResul
601
653
  };
602
654
  }
603
655
  }
656
+ if (d.kind === CliOptionKind.Enum) {
657
+ const choices = d.choices ?? [];
658
+ if (!choices.includes(v)) {
659
+ return {
660
+ kind: ParseKind.Error,
661
+ path: pr.path,
662
+ opts: {},
663
+ args: [],
664
+ helpExplicit: false,
665
+ helpPath: [],
666
+ errorMsg: `Option --${k}: '${v}' is not one of: ${choices.join(", ")}`,
667
+ errorHelpPath: pr.path,
668
+ };
669
+ }
670
+ }
604
671
  }
605
672
 
606
673
  return pr;
package/src/runtime.ts CHANGED
@@ -136,7 +136,7 @@ export async function cliRun(root: CliCommand, argv: string[] = process.argv.sli
136
136
  process.exit(1);
137
137
  }
138
138
 
139
- const ctx = new CliContext(parseRoot.key, pr.path, pr.args, pr.opts, parseRoot);
139
+ const ctx = new CliContext(parseRoot.key, pr.path, pr.args, pr.opts, parseRoot, "cli");
140
140
  try {
141
141
  await Promise.resolve(current.handler(ctx));
142
142
  process.exit(0);
package/src/types.ts CHANGED
@@ -9,7 +9,12 @@ It gives the package one shared model for both library users and internal module
9
9
  import type { CliContext } from "./context.ts";
10
10
 
11
11
  /**
12
- * Option kinds: presence (boolean flag), string (free-form text), or number (strict double).
12
+ * How a leaf handler was dispatched.
13
+ */
14
+ export type CliInvocation = "cli" | "mcp";
15
+
16
+ /**
17
+ * Option kinds: presence (boolean flag), string (free-form text), number (strict double), or enum (fixed choices).
13
18
  */
14
19
  export enum CliOptionKind {
15
20
  /** Boolean flag: no value token (may be implicit `"1"` when set). */
@@ -18,24 +23,25 @@ export enum CliOptionKind {
18
23
  String = "string",
19
24
  /** Strict floating-point value (parsed at validation time). */
20
25
  Number = "number",
26
+ /** Fixed set of allowed string values. Requires non-empty `choices` on the option. */
27
+ Enum = "enum",
21
28
  }
22
29
 
23
30
  /**
24
- * When fallbackCommand is used for missing or unknown top-level tokens.
25
- * Only the program root may set a non-default mode or a non-nil fallbackCommand.
31
+ * When `fallbackCommand` is used for missing or unknown subcommand tokens at a routing node.
26
32
  */
27
33
  export enum CliFallbackMode {
28
34
  /**
29
- * If argv has no first subcommand, route to `fallbackCommand`; if the first token is unknown, error.
35
+ * If argv has no next subcommand, route to `fallbackCommand`; if the token is unknown, error.
30
36
  */
31
37
  MissingOnly = "missingOnly",
32
38
  /**
33
- * If argv has no first subcommand or the first token is not a known child, route to `fallbackCommand`.
39
+ * If argv has no next subcommand or the token is not a known child, route to `fallbackCommand`.
34
40
  */
35
41
  MissingOrUnknown = "missingOrUnknown",
36
42
  /**
37
- * If the first token is present but not a known child, route to `fallbackCommand`.
38
- * When the first subcommand token is missing (empty argv), do not use fallback (implicit root help).
43
+ * If the next token is present but not a known child, route to `fallbackCommand`.
44
+ * When the subcommand token is missing (exhausted argv), do not use fallback (implicit scoped help).
39
45
  */
40
46
  UnknownOnly = "unknownOnly",
41
47
  }
@@ -54,6 +60,11 @@ export interface CliOption {
54
60
  shortName?: string;
55
61
  /** Whether this option must be provided. Cannot be used with Presence kind. */
56
62
  required?: boolean;
63
+ /**
64
+ * Allowed values. Required when kind === Enum; ignored otherwise.
65
+ * Must be a non-empty array of distinct non-empty strings.
66
+ */
67
+ choices?: string[];
57
68
  }
58
69
 
59
70
  /**
@@ -88,6 +99,39 @@ export interface CliMcpServerConfig {
88
99
  version?: string;
89
100
  /** Resource URI for schema export (default: `"argsbarg://schema"`). */
90
101
  schemaResourceUri?: string;
102
+ /**
103
+ * Capture the user's login shell environment at MCP server start and merge it
104
+ * into process.env. Solves missing PATH, nvm/rbenv shims, Homebrew binaries,
105
+ * and shell exports that MCP hosts (e.g. Cursor) don't inherit.
106
+ */
107
+ shellEnv?: boolean | string;
108
+ /**
109
+ * Path to a .env file loaded into process.env at MCP server start, after shellEnv.
110
+ * Supports `~` expansion. Warns on stderr if the file does not exist.
111
+ * Always overwrites — envFile is authoritative for its keys.
112
+ */
113
+ envFile?: string;
114
+ /**
115
+ * Custom MCP resources exposed alongside the built-in argsbarg://schema resource.
116
+ * URIs must be unique and must not equal schemaResourceUri.
117
+ */
118
+ resources?: CliMcpResource[];
119
+ }
120
+
121
+ /**
122
+ * A custom MCP resource exposed under resources/list and resources/read.
123
+ */
124
+ export interface CliMcpResource {
125
+ /** Resource URI (must be unique; must not equal schemaResourceUri). */
126
+ uri: string;
127
+ /** Short display name for resources/list. */
128
+ name: string;
129
+ /** Optional human description for resources/list. */
130
+ description?: string;
131
+ /** MIME type (default: "text/plain"). */
132
+ mimeType?: string;
133
+ /** Called at resources/read time; must return the resource body. */
134
+ load: () => string;
91
135
  }
92
136
 
93
137
  /**
@@ -96,6 +140,17 @@ export interface CliMcpServerConfig {
96
140
  export interface CliMcpToolConfig {
97
141
  /** When `false`, omit from `tools/list` (default: exposed). */
98
142
  enabled?: boolean;
143
+ /**
144
+ * Override the generated MCP tool description.
145
+ * Default: auto-generated from command path and description.
146
+ */
147
+ description?: string;
148
+ /**
149
+ * Environment variable names required at runtime.
150
+ * Appended to auto-generated MCP tool descriptions; enforced at tools/call time.
151
+ * Empty string counts as absent.
152
+ */
153
+ requiresEnv?: string[];
99
154
  }
100
155
 
101
156
  /**
@@ -130,17 +185,17 @@ export type CliCommand =
130
185
  positionals?: CliPositional[];
131
186
  /** Nested subcommands (empty for leaf commands). */
132
187
  commands?: never;
133
- /** Default top-level subcommand (routing commands only). */
188
+ /** Default subcommand (routing commands only). */
134
189
  fallbackCommand?: never;
135
- /** How fallbackCommand is applied (routing commands only). */
190
+ /** How fallbackCommand is applied at this routing node (routing commands only). */
136
191
  fallbackMode?: never;
137
192
  })
138
193
  | (CliCommandBase & {
139
194
  /** Nested subcommands. */
140
195
  commands: CliCommand[];
141
- /** Default top-level subcommand when argv omits a command or uses an unknown first token. */
196
+ /** Default subcommand when argv omits a command or uses an unknown token at this routing node. */
142
197
  fallbackCommand?: string;
143
- /** How fallbackCommand is applied. */
198
+ /** How fallbackCommand is applied at this routing node (not root-only). */
144
199
  fallbackMode?: CliFallbackMode;
145
200
  /** Handler function (leaf commands only). */
146
201
  handler?: never;