argsbarg 3.3.13 → 3.4.0

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.test.ts CHANGED
@@ -26,7 +26,7 @@ import { buildToolCallSuccess } from "./mcp/result.ts";
26
26
  import { generateSkillBundle } from "./skill/generate.ts";
27
27
  import { cliSkillInstall } from "./skill/install.ts";
28
28
  import { ParseKind, parse, postParseValidate } from "./parse.ts";
29
- import { cliSchemaJson } from "./schema.ts";
29
+ import { cliSchemaExport, cliSchemaJson } from "./schema.ts";
30
30
  import { cliValidateProgram } from "./validate.ts";
31
31
  import { expect, test } from "bun:test";
32
32
 
@@ -496,7 +496,7 @@ test("leaf completion help prints correctly", async () => {
496
496
  const out = stdout.toString();
497
497
  expect(exitCode).toBe(0);
498
498
  expect(out).toContain("Show help for this command.");
499
- expect(out).toContain("Output is the whole script.");
499
+ expect(out).toContain("Manual install:");
500
500
  expect(stderr.toString()).toBe("");
501
501
  });
502
502
 
@@ -658,7 +658,7 @@ test("docs help lists schema, api, and skill subcommands", () => {
658
658
  expect(help).toContain("api");
659
659
  expect(help).toContain("markdown");
660
660
  expect(help).toContain("skill");
661
- expect(help).toContain("SKILL.md");
661
+ expect(help).toContain("reference agent SKILL");
662
662
  });
663
663
 
664
664
  test("root help omits legacy --schema flag", () => {
@@ -690,7 +690,8 @@ test("root help shows agent docs hint when docs enabled", () => {
690
690
  commands: [{ key: "run", description: "Run.", handler: () => {} }],
691
691
  });
692
692
  const help = cliHelpRender(cliPresentationRoot(root), [], false);
693
- expect(help).toContain("Agents: run `myapp docs skill` to learn how to use this app");
693
+ expect(help).toContain("For AI agents: `myapp docs skill`.");
694
+ expect(help).not.toContain("install --skill");
694
695
  });
695
696
 
696
697
  test("root help omits agent hint when docs disabled", () => {
@@ -852,6 +853,146 @@ test("collectMcpTools lists user leaf commands only", () => {
852
853
  expect(lookup.description).toBe("stat owner lookup — Resolve owner info.");
853
854
  });
854
855
 
856
+ test("collectMcpTools appends leaf notes to MCP tool description", () => {
857
+ const root = testProgram({
858
+ key: "app",
859
+ version: "1.0.0",
860
+ description: "Notes demo.",
861
+ mcpServer: { enabled: true },
862
+ commands: [
863
+ {
864
+ key: "run",
865
+ description: "Run.",
866
+ notes: "Use `--json` for structured output.",
867
+ handler: () => {},
868
+ },
869
+ ],
870
+ });
871
+ const tools = collectMcpTools(root);
872
+ expect(tools[0]!.description).toBe("run — Run.\n\nUse `--json` for structured output.");
873
+ });
874
+
875
+ test("collectMcpTools appends notes after mcpTool.description override", () => {
876
+ const root = testProgram({
877
+ key: "app",
878
+ version: "1.0.0",
879
+ description: "Notes demo.",
880
+ mcpServer: { enabled: true },
881
+ commands: [
882
+ {
883
+ key: "run",
884
+ description: "Run.",
885
+ notes: "Operational hint.",
886
+ mcpTool: { description: "Custom MCP text." },
887
+ handler: () => {},
888
+ },
889
+ ],
890
+ });
891
+ const tools = collectMcpTools(root);
892
+ expect(tools[0]!.description).toBe("Custom MCP text.\n\nOperational hint.");
893
+ });
894
+
895
+ test("collectMcpTools resolves {argsbarg:program} in appended notes", () => {
896
+ const root = testProgram({
897
+ key: "myapp",
898
+ version: "1.0.0",
899
+ description: "Notes demo.",
900
+ mcpServer: { enabled: true },
901
+ commands: [
902
+ {
903
+ key: "run",
904
+ description: "Run.",
905
+ notes: "See `{argsbarg:program} docs api`.",
906
+ handler: () => {},
907
+ },
908
+ ],
909
+ });
910
+ const tools = collectMcpTools(root);
911
+ expect(tools[0]!.description).toContain("See `myapp docs api`.");
912
+ });
913
+
914
+ test("cliSchemaExport includes leaf outputSchema", () => {
915
+ const root = testProgram({
916
+ key: "app",
917
+ version: "1.0.0",
918
+ description: "Schema export demo.",
919
+ mcpServer: { enabled: true },
920
+ commands: [
921
+ {
922
+ key: "run",
923
+ description: "Run.",
924
+ outputSchema: {
925
+ type: "object",
926
+ properties: { ok: { type: "boolean" } },
927
+ },
928
+ handler: () => {},
929
+ },
930
+ ],
931
+ });
932
+ const schema = cliSchemaExport(root);
933
+ expect(schema.commands![0]!.outputSchema).toEqual({
934
+ type: "object",
935
+ properties: { ok: { type: "boolean" } },
936
+ });
937
+ });
938
+
939
+ test("cliSchemaExport accepts legacy mcpTool.outputSchema", () => {
940
+ const root = testProgram({
941
+ key: "app",
942
+ version: "1.0.0",
943
+ description: "Schema export demo.",
944
+ commands: [
945
+ {
946
+ key: "run",
947
+ description: "Run.",
948
+ mcpTool: {
949
+ outputSchema: { type: "object", properties: { id: { type: "string" } } },
950
+ },
951
+ handler: () => {},
952
+ },
953
+ ],
954
+ });
955
+ expect(cliSchemaExport(root).commands![0]!.outputSchema).toEqual({
956
+ type: "object",
957
+ properties: { id: { type: "string" } },
958
+ });
959
+ });
960
+
961
+ test("outputSchema must be a JSON Schema object", () => {
962
+ const root = testProgram({
963
+ key: "app",
964
+ version: "1.0.0",
965
+ description: "Bad output schema.",
966
+ commands: [
967
+ {
968
+ key: "run",
969
+ description: "Run.",
970
+ outputSchema: [] as unknown as Record<string, unknown>,
971
+ handler: () => {},
972
+ },
973
+ ],
974
+ });
975
+ expect(() => cliValidateProgram(root)).toThrow(/outputSchema must be a JSON Schema object/);
976
+ });
977
+
978
+ test("outputSchema cannot be set on both leaf and mcpTool", () => {
979
+ const root = testProgram({
980
+ key: "app",
981
+ version: "1.0.0",
982
+ description: "Duplicate output schema.",
983
+ commands: [
984
+ {
985
+ key: "run",
986
+ description: "Run.",
987
+ outputSchema: { type: "object" },
988
+ mcpTool: { outputSchema: { type: "object" } },
989
+ handler: () => {},
990
+ },
991
+ ],
992
+ });
993
+ expect(() => cliValidateProgram(root)).toThrow(/Set outputSchema on the leaf only/);
994
+ });
995
+
855
996
  test("collectMcpTools merges parent options into inputSchema", () => {
856
997
  const tools = collectMcpTools(nestedMcpFixture);
857
998
  const lookup = tools.find((t) => t.name === "stat_owner_lookup")!;
@@ -860,6 +1001,40 @@ test("collectMcpTools merges parent options into inputSchema", () => {
860
1001
  expect(schema.required).toContain("path");
861
1002
  });
862
1003
 
1004
+ test("collectMcpTools includes outputSchema when set on leaf", () => {
1005
+ const root = testProgram({
1006
+ key: "app",
1007
+ version: "1.0.0",
1008
+ description: "Output schema demo.",
1009
+ mcpServer: { enabled: true },
1010
+ commands: [
1011
+ {
1012
+ key: "run",
1013
+ description: "Run with JSON output.",
1014
+ outputSchema: {
1015
+ type: "object",
1016
+ properties: { ok: { type: "boolean" } },
1017
+ required: ["ok"],
1018
+ },
1019
+ handler: () => {},
1020
+ },
1021
+ ],
1022
+ });
1023
+ const tools = collectMcpTools(root);
1024
+ expect(tools).toHaveLength(1);
1025
+ expect(tools[0]!.outputSchema).toEqual({
1026
+ type: "object",
1027
+ properties: { ok: { type: "boolean" } },
1028
+ required: ["ok"],
1029
+ });
1030
+ });
1031
+
1032
+ test("collectMcpTools omits outputSchema when leaf has none", () => {
1033
+ const tools = collectMcpTools(nestedMcpFixture);
1034
+ const lookup = tools.find((t) => t.name === "stat_owner_lookup")!;
1035
+ expect(lookup.outputSchema).toBeUndefined();
1036
+ });
1037
+
863
1038
  test("mcpToolCallToArgv builds nested lookup argv", () => {
864
1039
  const tools = collectMcpTools(nestedMcpFixture);
865
1040
  const lookup = tools.find((t) => t.name === "stat_owner_lookup")!;
@@ -1815,7 +1990,7 @@ test("generateSkillBundle includes frontmatter and compact command index", () =>
1815
1990
  expect(bundle.skillMd).toContain("## Commands");
1816
1991
  expect(bundle.skillMd).toContain("`nested.ts stat owner lookup <path>`");
1817
1992
  expect(bundle.skillMd).toContain("Invoke via shell:");
1818
- expect(bundle.skillMd).toContain("read `reference.md`");
1993
+ expect(bundle.skillMd).toContain("For full detail, open `reference.md`");
1819
1994
  expect(bundle.skillMd).not.toContain("#### Options");
1820
1995
  expect(bundle.skillMd).not.toContain("CLI API reference");
1821
1996
  expect(bundle.skillMd).not.toContain("mcp.json");
package/src/index.ts CHANGED
@@ -18,6 +18,7 @@ export type {
18
18
  CliInvocation,
19
19
  CliMcpResource,
20
20
  CliMcpServerConfig,
21
+ CliMcpBundleConfig,
21
22
  CliMcpToolConfig,
22
23
  CliInstallConfig,
23
24
  CliUpdateArtifact,
@@ -45,3 +46,5 @@ export {
45
46
  parseReleaseTag,
46
47
  } from "./install/gh-release-update.ts";
47
48
  export type { GhReleaseUpdateConfig, GhVersionCheckConfig } from "./install/gh-release-update.ts";
49
+ export { generateMcpManifest, packMcpBundle, defaultMcpBundlePaths } from "./mcp/bundle.ts";
50
+ export type { McpBundlePaths, PackMcpBundleOpts } from "./mcp/bundle.ts";
package/src/invoke.ts CHANGED
@@ -6,7 +6,7 @@ process.exit so MCP tool calls can run handlers repeatedly.
6
6
 
7
7
  import { CliContext } from "./context.ts";
8
8
  import { builtinInterceptRoot, dispatchBuiltin } from "./builtins/dispatch.ts";
9
- import { cliPresentationRoot } from "./builtins/presentation.ts";
9
+ import { cliParseRoot, cliPresentationRoot } from "./builtins/presentation.ts";
10
10
  import { parse, postParseValidate, ParseKind } from "./parse.ts";
11
11
  import { type CliNode, type CliProgram, type CliRouter, isCliLeaf, isCliRouter } from "./types.ts";
12
12
  import { format } from "node:util";
@@ -65,7 +65,7 @@ export async function cliInvoke(root: CliProgram, argv: string[]): Promise<CliIn
65
65
  isLeafCompletionIntercept = intercept.isLeafCompletionIntercept;
66
66
  }
67
67
  } else {
68
- parseRoot = cliPresentationRoot(root);
68
+ parseRoot = cliParseRoot(root);
69
69
  }
70
70
 
71
71
  let pr = parse(parseRoot, argv);
@@ -0,0 +1,251 @@
1
+ /*
2
+ Packs a CLI program into an MCP Bundle (`.mcpb`) for Claude Desktop.
3
+ macOS-only v1: expects `dist/<program.key>` as the compiled binary input.
4
+ */
5
+
6
+ import { cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
7
+ import { tmpdir } from "node:os";
8
+ import { basename, join, resolve } from "node:path";
9
+ import { collectMcpTools, mcpServerId } from "./tools.ts";
10
+ import type { CliMcpBundleConfig, CliProgram } from "../types.ts";
11
+
12
+ const MANIFEST_VERSION = "0.3";
13
+ const DIST_DIR = "dist";
14
+
15
+ /** Resolved paths for `mcp bundle`. */
16
+ export interface McpBundlePaths {
17
+ binaryPath: string;
18
+ outPath: string;
19
+ binaryName: string;
20
+ }
21
+
22
+ /** Default `dist/<key>` binary and `dist/<key>.mcpb` output under cwd. */
23
+ export function defaultMcpBundlePaths(program: CliProgram, cwd = process.cwd()): McpBundlePaths {
24
+ const binaryName = program.key;
25
+ const dist = join(cwd, DIST_DIR);
26
+ return {
27
+ binaryName,
28
+ binaryPath: join(dist, binaryName),
29
+ outPath: join(dist, `${binaryName}.mcpb`),
30
+ };
31
+ }
32
+
33
+ /** Collects unique env var names from MCP tool `requiresEnv` for manifest `user_config`. */
34
+ function collectUserConfigEnvVars(program: CliProgram): string[] {
35
+ const names = new Set<string>();
36
+ for (const tool of collectMcpTools(program)) {
37
+ for (const env of tool.leaf.mcpTool?.requiresEnv ?? []) {
38
+ if (env.length > 0) {
39
+ names.add(env);
40
+ }
41
+ }
42
+ }
43
+ return [...names].sort();
44
+ }
45
+
46
+ /** Builds manifest `user_config` entries for required environment variables. */
47
+ function buildUserConfig(program: CliProgram): Record<string, unknown> | undefined {
48
+ const envVars = collectUserConfigEnvVars(program);
49
+ if (envVars.length === 0) {
50
+ return undefined;
51
+ }
52
+ const out: Record<string, unknown> = {};
53
+ for (const name of envVars) {
54
+ const key = name.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_|_$/g, "") || "env_var";
55
+ out[key] = {
56
+ type: "string",
57
+ title: name,
58
+ description: `Value for environment variable ${name}`,
59
+ sensitive: /key|token|secret|password/i.test(name),
60
+ required: true,
61
+ };
62
+ }
63
+ return out;
64
+ }
65
+
66
+ /** Default author when `mcpServer.bundle.author` is unset. */
67
+ function defaultAuthor(bundle?: CliMcpBundleConfig): { name: string; email?: string; url?: string } {
68
+ return bundle?.author ?? { name: "Unknown" };
69
+ }
70
+
71
+ /** Generates MCPB `manifest.json` object from program schema and MCP tools. */
72
+ export function generateMcpManifest(program: CliProgram, binaryName: string): Record<string, unknown> {
73
+ const bundle = program.mcpServer?.bundle;
74
+ const tools = collectMcpTools(program).map((t) => ({
75
+ name: t.name,
76
+ description: t.description.split("\n")[0] ?? t.description,
77
+ }));
78
+
79
+ const manifest: Record<string, unknown> = {
80
+ manifest_version: MANIFEST_VERSION,
81
+ name: mcpServerId(program),
82
+ version: program.version,
83
+ description: program.description,
84
+ author: defaultAuthor(bundle),
85
+ server: {
86
+ type: "binary",
87
+ entry_point: binaryName,
88
+ mcp_config: {
89
+ command: `\${__dirname}/${binaryName}`,
90
+ args: ["mcp"],
91
+ },
92
+ },
93
+ tools,
94
+ tools_generated: false,
95
+ compatibility: {
96
+ claude_desktop: ">=0.10.0",
97
+ platforms: ["darwin"],
98
+ },
99
+ };
100
+
101
+ const longDescription = bundle?.longDescription ?? program.description;
102
+ if (longDescription !== program.description) {
103
+ manifest.long_description = longDescription;
104
+ }
105
+
106
+ const userConfig = buildUserConfig(program);
107
+ if (userConfig) {
108
+ manifest.user_config = userConfig;
109
+ }
110
+
111
+ if (bundle?.icon) {
112
+ manifest.icon = basename(bundle.icon);
113
+ }
114
+
115
+ return manifest;
116
+ }
117
+
118
+ /** CRC-32 table for ZIP local headers (store method). */
119
+ function crc32(data: Buffer): number {
120
+ let crc = 0xffffffff;
121
+ for (let i = 0; i < data.length; i++) {
122
+ crc ^= data[i]!;
123
+ for (let j = 0; j < 8; j++) {
124
+ crc = (crc >>> 1) ^ (crc & 1 ? 0xedb88320 : 0);
125
+ }
126
+ }
127
+ return (crc ^ 0xffffffff) >>> 0;
128
+ }
129
+
130
+ /** Writes a minimal ZIP (store, no compression) with one or more files. */
131
+ function zipStore(files: { name: string; data: Buffer }[]): Buffer {
132
+ const parts: Buffer[] = [];
133
+ const central: Buffer[] = [];
134
+ let offset = 0;
135
+
136
+ for (const file of files) {
137
+ const nameBuf = Buffer.from(file.name, "utf8");
138
+ const crc = crc32(file.data);
139
+ const local = Buffer.alloc(30 + nameBuf.length);
140
+ local.writeUInt32LE(0x04034b50, 0);
141
+ local.writeUInt16LE(20, 4);
142
+ local.writeUInt16LE(0, 6);
143
+ local.writeUInt16LE(0, 8);
144
+ local.writeUInt16LE(0, 10);
145
+ local.writeUInt16LE(0, 12);
146
+ local.writeUInt32LE(crc, 14);
147
+ local.writeUInt32LE(file.data.length, 18);
148
+ local.writeUInt32LE(file.data.length, 22);
149
+ local.writeUInt32LE(nameBuf.length, 26);
150
+ local.writeUInt16LE(0, 28);
151
+ nameBuf.copy(local, 30);
152
+
153
+ const centralHdr = Buffer.alloc(46 + nameBuf.length);
154
+ centralHdr.writeUInt32LE(0x02014b50, 0);
155
+ centralHdr.writeUInt16LE(20, 4);
156
+ centralHdr.writeUInt16LE(20, 6);
157
+ centralHdr.writeUInt16LE(0, 8);
158
+ centralHdr.writeUInt16LE(0, 10);
159
+ centralHdr.writeUInt16LE(0, 12);
160
+ centralHdr.writeUInt16LE(0, 14);
161
+ centralHdr.writeUInt32LE(crc, 16);
162
+ centralHdr.writeUInt32LE(file.data.length, 20);
163
+ centralHdr.writeUInt32LE(file.data.length, 24);
164
+ centralHdr.writeUInt32LE(nameBuf.length, 28);
165
+ centralHdr.writeUInt16LE(0, 30);
166
+ centralHdr.writeUInt16LE(0, 32);
167
+ centralHdr.writeUInt16LE(0, 34);
168
+ centralHdr.writeUInt16LE(0, 36);
169
+ centralHdr.writeUInt32LE(0, 38);
170
+ centralHdr.writeUInt32LE(offset, 42);
171
+ nameBuf.copy(centralHdr, 46);
172
+
173
+ parts.push(local, file.data);
174
+ central.push(centralHdr);
175
+ offset += local.length + file.data.length;
176
+ }
177
+
178
+ const centralStart = offset;
179
+ const centralBuf = Buffer.concat(central);
180
+ const end = Buffer.alloc(22);
181
+ end.writeUInt32LE(0x06054b50, 0);
182
+ end.writeUInt16LE(0, 4);
183
+ end.writeUInt16LE(0, 6);
184
+ end.writeUInt16LE(files.length, 8);
185
+ end.writeUInt16LE(files.length, 10);
186
+ end.writeUInt32LE(centralBuf.length, 12);
187
+ end.writeUInt32LE(centralStart, 16);
188
+ end.writeUInt16LE(0, 20);
189
+
190
+ return Buffer.concat([...parts, centralBuf, end]);
191
+ }
192
+
193
+ export interface PackMcpBundleOpts {
194
+ cwd?: string;
195
+ binaryPath?: string;
196
+ outPath?: string;
197
+ }
198
+
199
+ /**
200
+ * Stages manifest + binary (+ optional icon) and writes a `.mcpb` ZIP.
201
+ * macOS-only v1; requires the compiled binary to exist.
202
+ */
203
+ export function packMcpBundle(program: CliProgram, opts: PackMcpBundleOpts = {}): string {
204
+ if (process.platform !== "darwin") {
205
+ throw new Error("mcp bundle is macOS-only in this release. Build the bundle on darwin.");
206
+ }
207
+
208
+ const cwd = opts.cwd ?? process.cwd();
209
+ const defaults = defaultMcpBundlePaths(program, cwd);
210
+ const binaryPath = resolve(cwd, opts.binaryPath ?? defaults.binaryPath);
211
+ const outPath = resolve(cwd, opts.outPath ?? defaults.outPath);
212
+ const binaryName = basename(binaryPath);
213
+
214
+ if (!existsSync(binaryPath)) {
215
+ throw new Error(`Binary not found: ${binaryPath}. Build with compile first (expected dist/${program.key}).`);
216
+ }
217
+
218
+ const staging = mkdtempSync(join(tmpdir(), "mcpb-"));
219
+ try {
220
+ const stagedBinary = join(staging, binaryName);
221
+ cpSync(binaryPath, stagedBinary, { mode: 0o755 });
222
+
223
+ const manifest = generateMcpManifest(program, binaryName);
224
+ const files: { name: string; data: Buffer }[] = [
225
+ { name: "manifest.json", data: Buffer.from(`${JSON.stringify(manifest, null, 2)}\n`, "utf8") },
226
+ { name: binaryName, data: readFileSync(stagedBinary) },
227
+ ];
228
+
229
+ const iconRel = program.mcpServer?.bundle?.icon;
230
+ if (iconRel) {
231
+ const iconSrc = resolve(cwd, iconRel);
232
+ if (!existsSync(iconSrc)) {
233
+ throw new Error(`Bundle icon not found: ${iconRel}`);
234
+ }
235
+ const iconName = basename(iconRel);
236
+ files.push({ name: iconName, data: readFileSync(iconSrc) });
237
+ }
238
+
239
+ mkdirSync(join(outPath, ".."), { recursive: true });
240
+ writeFileSync(outPath, zipStore(files));
241
+ return outPath;
242
+ } finally {
243
+ rmSync(staging, { recursive: true, force: true });
244
+ }
245
+ }
246
+
247
+ /** Runs `mcp bundle`: writes `dist/<key>.mcpb` and prints the path on stdout. */
248
+ export function runMcpBundle(program: CliProgram): void {
249
+ const outPath = packMcpBundle(program);
250
+ process.stdout.write(`${outPath}\n`);
251
+ }
package/src/mcp/server.ts CHANGED
@@ -95,6 +95,7 @@ async function handleRequestLine(root: CliProgram, line: string): Promise<void>
95
95
  name: t.name,
96
96
  description: t.description,
97
97
  inputSchema: t.inputSchema,
98
+ ...(t.outputSchema === undefined ? {} : { outputSchema: t.outputSchema }),
98
99
  }));
99
100
  writeResponse({ jsonrpc: "2.0", id, result: { tools } });
100
101
  return;
package/src/mcp/tools.ts CHANGED
@@ -3,9 +3,11 @@ This module maps CliProgram leaf nodes to MCP tool definitions and converts
3
3
  flat JSON tool arguments into argv for cliInvoke.
4
4
  */
5
5
 
6
+ import { cliResolveNotes } from "../help.ts";
6
7
  import { collectOptionDefs } from "../parse.ts";
8
+ import { visibleOptions } from "../hidden.ts";
7
9
  import { cliSchemaJson } from "../schema.ts";
8
- import { CliProgram, CliLeaf, CliNode, CliOption, CliOptionKind, CliPositional, isCliLeaf, isCliRouter } from "../types.ts";
10
+ import { CliProgram, CliLeaf, CliNode, CliOption, CliOptionKind, CliPositional, isCliLeaf, isCliRouter, leafOutputSchema } from "../types.ts";
9
11
 
10
12
  /** Default URI pattern for the CLI schema MCP resource (`<mcpId>://schema`). */
11
13
  export function defaultMcpSchemaUri(mcpId: string): string {
@@ -34,6 +36,8 @@ export interface McpToolDef {
34
36
  leaf: CliLeaf;
35
37
  /** JSON Schema for tools/call arguments. */
36
38
  inputSchema: Record<string, unknown>;
39
+ /** JSON Schema for structured tool results when set on the leaf `mcpTool`. */
40
+ outputSchema?: Record<string, unknown>;
37
41
  }
38
42
 
39
43
  /** Builds MCP tool description: "{cli path} — {description}". */
@@ -80,7 +84,7 @@ function buildInputSchema(root: CliProgram, path: string[], leaf: CliLeaf): Reco
80
84
  const properties: Record<string, unknown> = {};
81
85
  const required: string[] = [];
82
86
 
83
- for (const opt of collectOptionDefs(root, path)) {
87
+ for (const opt of visibleOptions(collectOptionDefs(root, path))) {
84
88
  properties[opt.name] = optionProperty(opt);
85
89
  if (opt.required) {
86
90
  required.push(opt.name);
@@ -106,15 +110,21 @@ function buildInputSchema(root: CliProgram, path: string[], leaf: CliLeaf): Reco
106
110
  return schema;
107
111
  }
108
112
 
109
- /** Resolves MCP tool description with optional override and requiresEnv suffix. */
113
+ /** Resolves MCP tool description with optional override, requiresEnv suffix, and leaf notes. */
110
114
  function resolveToolDescription(root: CliProgram, path: string[], leaf: CliLeaf): string {
115
+ let desc: string;
111
116
  if (leaf.mcpTool?.description) {
112
- return leaf.mcpTool.description;
117
+ desc = leaf.mcpTool.description;
118
+ } else {
119
+ desc = mcpToolDescription(path, root.key, leaf.description);
120
+ const env = leaf.mcpTool?.requiresEnv;
121
+ if (env && env.length > 0) {
122
+ desc += ` [requires env: ${env.join(", ")}]`;
123
+ }
113
124
  }
114
- let desc = mcpToolDescription(path, root.key, leaf.description);
115
- const env = leaf.mcpTool?.requiresEnv;
116
- if (env && env.length > 0) {
117
- desc += ` [requires env: ${env.join(", ")}]`;
125
+ const notes = (leaf.notes ?? "").trim();
126
+ if (notes.length > 0) {
127
+ desc += `\n\n${cliResolveNotes(notes, root.key)}`;
118
128
  }
119
129
  return desc;
120
130
  }
@@ -155,18 +165,25 @@ export function collectMcpTools(root: CliProgram): McpToolDef[] {
155
165
  /** Walks the command tree and appends leaf tools. */
156
166
  function walk(cmd: CliNode, path: string[]): void {
157
167
  if (isCliLeaf(cmd)) {
158
- if (cmd.key === "completion" || cmd.key === "install" || cmd.key === "mcp" || cmd.key === "version") {
168
+ if (
169
+ cmd.key === "completion" ||
170
+ cmd.key === "install" ||
171
+ cmd.key === "mcp" ||
172
+ cmd.key === "version"
173
+ ) {
159
174
  return;
160
175
  }
161
- if (cmd.mcpTool?.enabled === false) {
176
+ if (cmd.hidden || cmd.mcpTool?.enabled === false) {
162
177
  return;
163
178
  }
179
+ const outputSchema = leafOutputSchema(cmd);
164
180
  out.push({
165
181
  name: mcpToolName(root, path),
166
182
  description: resolveToolDescription(root, path, cmd),
167
183
  path,
168
184
  leaf: cmd,
169
185
  inputSchema: buildInputSchema(root, path, cmd),
186
+ ...(outputSchema === undefined ? {} : { outputSchema }),
170
187
  });
171
188
  return;
172
189
  }
package/src/runtime.ts CHANGED
@@ -4,7 +4,7 @@ This module runs parsed commands, help, errors, completion, and leaf handlers.
4
4
 
5
5
  import { resolveCapabilities } from "./capabilities.ts";
6
6
  import { builtinInterceptRoot, dispatchBuiltin } from "./builtins/dispatch.ts";
7
- import { cliPresentationRoot } from "./builtins/presentation.ts";
7
+ import { cliParseRoot, cliPresentationRoot } from "./builtins/presentation.ts";
8
8
  import type { CliRouter } from "./types.ts";
9
9
  import { type CliNode, type CliProgram, isCliLeaf, isCliRouter } from "./types.ts";
10
10
  import { CliContext } from "./context.ts";
@@ -13,7 +13,7 @@ import { parse, postParseValidate, ParseKind } from "./parse.ts";
13
13
  import { cliValidateProgram } from "./validate.ts";
14
14
 
15
15
  function cliRootMergedWithBuiltins(program: CliProgram): CliRouter {
16
- return cliPresentationRoot(program);
16
+ return cliParseRoot(program);
17
17
  }
18
18
 
19
19
  export async function cliRun(program: CliProgram, argv: string[] = process.argv.slice(2)): Promise<never> {
@@ -68,7 +68,7 @@ export async function cliRun(program: CliProgram, argv: string[] = process.argv.
68
68
  pr = postParseValidate(parseRoot, pr);
69
69
 
70
70
  if (pr.kind === ParseKind.Help) {
71
- process.stdout.write(cliHelpRender(cliPresentationRoot(program), pr.helpPath, false));
71
+ process.stdout.write(cliHelpRender(cliParseRoot(program), pr.helpPath, false));
72
72
  process.exit(pr.helpExplicit ? 0 : 1);
73
73
  }
74
74