argsbarg 3.3.14 → 3.4.1

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.
@@ -0,0 +1,154 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { tmpdir } from "node:os";
5
+ import { cliPresentationRoot, cliParseRoot } from "./builtins/presentation.ts";
6
+ import { exportPresentationBuiltins } from "./builtins/export.ts";
7
+ import { cliHelpRender } from "./help.ts";
8
+ import { cliSchemaExport } from "./schema.ts";
9
+ import { collectMcpTools } from "./mcp/tools.ts";
10
+ import { defaultMcpBundlePaths, generateMcpManifest, packMcpBundle } from "./mcp/bundle.ts";
11
+ import { CliOptionKind, type CliProgram } from "./types.ts";
12
+
13
+ const hiddenFixture: CliProgram = {
14
+ key: "myapp",
15
+ version: "1.0.0",
16
+ description: "Hidden demo.",
17
+ mcpServer: { enabled: true },
18
+ commands: [
19
+ {
20
+ key: "public",
21
+ description: "Visible command.",
22
+ handler: () => {},
23
+ },
24
+ {
25
+ key: "secret",
26
+ hidden: true,
27
+ description: "Hidden command.",
28
+ handler: () => {},
29
+ },
30
+ {
31
+ key: "flags",
32
+ description: "Command with hidden option.",
33
+ options: [
34
+ {
35
+ name: "visible",
36
+ description: "Shown in help.",
37
+ kind: CliOptionKind.Presence,
38
+ },
39
+ {
40
+ name: "secret-flag",
41
+ hidden: true,
42
+ description: "Hidden option.",
43
+ kind: CliOptionKind.Presence,
44
+ },
45
+ ],
46
+ handler: () => {},
47
+ },
48
+ ],
49
+ };
50
+
51
+ describe("hidden commands and options", () => {
52
+ test("parse root includes hidden commands", () => {
53
+ const parse = cliParseRoot(hiddenFixture);
54
+ expect(parse.commands?.map((c) => c.key)).toContain("secret");
55
+ });
56
+
57
+ test("presentation root omits hidden commands", () => {
58
+ const presentation = cliPresentationRoot(hiddenFixture);
59
+ const keys = presentation.commands?.map((c) => c.key) ?? [];
60
+ expect(keys).toContain("public");
61
+ expect(keys).not.toContain("secret");
62
+ });
63
+
64
+ test("root help omits hidden commands", () => {
65
+ const help = cliHelpRender(cliParseRoot(hiddenFixture), [], false);
66
+ expect(help).toContain("public");
67
+ expect(help).not.toContain("secret");
68
+ });
69
+
70
+ test("hidden command -h still works", () => {
71
+ const help = cliHelpRender(cliParseRoot(hiddenFixture), ["secret"], false);
72
+ expect(help).toContain("Hidden command.");
73
+ });
74
+
75
+ test("help omits hidden options", () => {
76
+ const help = cliHelpRender(cliParseRoot(hiddenFixture), ["flags"], false);
77
+ expect(help).toContain("--visible");
78
+ expect(help).not.toContain("secret-flag");
79
+ });
80
+
81
+ test("schema export omits hidden nodes and options", () => {
82
+ const schema = cliSchemaExport(hiddenFixture);
83
+ const keys = schema.commands?.map((c) => c.key) ?? [];
84
+ expect(keys).toContain("public");
85
+ expect(keys).not.toContain("secret");
86
+ const flags = schema.commands?.find((c) => c.key === "flags");
87
+ expect(flags?.options?.map((o) => o.name)).toEqual(["visible"]);
88
+ });
89
+
90
+ test("MCP tools omit hidden commands", () => {
91
+ const tools = collectMcpTools(hiddenFixture);
92
+ expect(tools.map((t) => t.name)).toEqual(["public", "flags"]);
93
+ });
94
+ });
95
+
96
+ describe("mcp router", () => {
97
+ test("presentation exposes mcp bundle but not hidden serve", () => {
98
+ const builtins = exportPresentationBuiltins(hiddenFixture);
99
+ const mcp = builtins.find((b) => b.key === "mcp");
100
+ expect(mcp).toBeDefined();
101
+ expect(mcp?.commands?.map((c) => c.key)).toEqual(["bundle"]);
102
+ expect(mcp?.fallbackCommand).toBe("serve");
103
+ });
104
+
105
+ test("mcp help lists bundle", () => {
106
+ const help = cliHelpRender(cliParseRoot(hiddenFixture), ["mcp"], false);
107
+ expect(help).toContain("bundle");
108
+ expect(help).not.toMatch(/│ serve\s/);
109
+ });
110
+ });
111
+
112
+ describe("mcp bundle", () => {
113
+ test("generateMcpManifest uses mcpServerId and binary entry", () => {
114
+ const manifest = generateMcpManifest(hiddenFixture, "myapp");
115
+ expect(manifest.name).toBe("myapp");
116
+ expect(manifest.manifest_version).toBe("0.3");
117
+ expect((manifest.server as { type: string }).type).toBe("binary");
118
+ expect((manifest.server as { entry_point: string }).entry_point).toBe("myapp");
119
+ const mcpConfig = (manifest.server as { mcp_config: { command: string; args: string[] } }).mcp_config;
120
+ expect(mcpConfig.command).toBe("${__dirname}/myapp");
121
+ expect(mcpConfig.args).toEqual(["mcp"]);
122
+ expect((manifest.compatibility as { platforms: string[] }).platforms).toEqual(["darwin"]);
123
+ });
124
+
125
+ test("defaultMcpBundlePaths", () => {
126
+ const cwd = "/tmp/work";
127
+ const paths = defaultMcpBundlePaths(hiddenFixture, cwd);
128
+ expect(paths.binaryPath).toBe(join(cwd, "dist", "myapp"));
129
+ expect(paths.outPath).toBe(join(cwd, "dist", "myapp.mcpb"));
130
+ });
131
+
132
+ test("packMcpBundle writes zip with manifest and binary on darwin", () => {
133
+ if (process.platform !== "darwin") {
134
+ return;
135
+ }
136
+ const work = mkdtempSync(join(tmpdir(), "mcpb-test-"));
137
+ try {
138
+ const dist = join(work, "dist");
139
+ mkdirSync(dist, { recursive: true });
140
+ const binaryPath = join(dist, "myapp");
141
+ writeFileSync(binaryPath, "#!/bin/sh\necho hi\n", { mode: 0o755 });
142
+
143
+ const outPath = packMcpBundle(hiddenFixture, { cwd: work });
144
+ expect(outPath).toBe(join(dist, "myapp.mcpb"));
145
+
146
+ const zip = readFileSync(outPath);
147
+ expect(zip.length).toBeGreaterThan(0);
148
+ expect(zip.indexOf(Buffer.from("manifest.json"))).toBeGreaterThanOrEqual(0);
149
+ expect(zip.indexOf(Buffer.from("myapp"))).toBeGreaterThanOrEqual(0);
150
+ } finally {
151
+ rmSync(work, { recursive: true, force: true });
152
+ }
153
+ });
154
+ });
package/src/hidden.ts ADDED
@@ -0,0 +1,32 @@
1
+ /*
2
+ Filters hidden commands and options from presentation surfaces (help, schema, completions).
3
+ Parsing still uses the full tree via cliParseRoot.
4
+ */
5
+
6
+ import type { CliNode, CliOption, CliRouter } from "./types.ts";
7
+ import { isCliLeaf, isCliRouter } from "./types.ts";
8
+
9
+ /** Options visible in help, schema, completions, and MCP tool schemas. */
10
+ export function visibleOptions(options: CliOption[] | undefined): CliOption[] {
11
+ return (options ?? []).filter((o) => !o.hidden);
12
+ }
13
+
14
+ /** Strips hidden commands and options from one node for presentation export. */
15
+ export function presentationNode(node: CliNode): CliNode | null {
16
+ if (node.hidden) {
17
+ return null;
18
+ }
19
+ const options = visibleOptions(node.options);
20
+ if (isCliRouter(node)) {
21
+ const commands = node.commands
22
+ .map((ch) => presentationNode(ch))
23
+ .filter((ch): ch is CliNode => ch !== null);
24
+ return { ...node, options, commands };
25
+ }
26
+ return { ...node, options };
27
+ }
28
+
29
+ /** Subcommands visible in help listings (parent may be any node in the parse tree). */
30
+ export function visibleSubcommands(cmds: CliNode[]): CliNode[] {
31
+ return cmds.filter((c) => !c.hidden);
32
+ }
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
 
@@ -853,6 +853,146 @@ test("collectMcpTools lists user leaf commands only", () => {
853
853
  expect(lookup.description).toBe("stat owner lookup — Resolve owner info.");
854
854
  });
855
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
+
856
996
  test("collectMcpTools merges parent options into inputSchema", () => {
857
997
  const tools = collectMcpTools(nestedMcpFixture);
858
998
  const lookup = tools.find((t) => t.name === "stat_owner_lookup")!;
@@ -861,6 +1001,40 @@ test("collectMcpTools merges parent options into inputSchema", () => {
861
1001
  expect(schema.required).toContain("path");
862
1002
  });
863
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
+
864
1038
  test("mcpToolCallToArgv builds nested lookup argv", () => {
865
1039
  const tools = collectMcpTools(nestedMcpFixture);
866
1040
  const lookup = tools.find((t) => t.name === "stat_owner_lookup")!;
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;