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/.private/scratch.md +3 -1
- package/CHANGELOG.md +19 -1
- package/docs/bundled-docs.md +1 -1
- package/docs/cli-program.md +19 -0
- package/docs/mcp.md +23 -3
- package/index.d.ts +47 -0
- package/package.json +1 -1
- package/src/builtins/builtins.test.ts +9 -3
- package/src/builtins/completion-group.ts +18 -17
- package/src/builtins/dispatch.ts +18 -7
- package/src/builtins/export.ts +31 -17
- package/src/builtins/index.ts +1 -1
- package/src/builtins/install.ts +2 -1
- package/src/builtins/mcp.ts +38 -8
- package/src/builtins/presentation.ts +49 -11
- package/src/docs/api-guide.test.ts +30 -0
- package/src/docs/api-guide.ts +18 -0
- package/src/docs/builtin.ts +8 -15
- package/src/docs/docs.test.ts +6 -4
- package/src/help.ts +5 -4
- package/src/hidden-mcpb.test.ts +154 -0
- package/src/hidden.ts +32 -0
- package/src/index.test.ts +180 -5
- package/src/index.ts +3 -0
- package/src/invoke.ts +2 -2
- package/src/mcp/bundle.ts +251 -0
- package/src/mcp/server.ts +1 -0
- package/src/mcp/tools.ts +27 -10
- package/src/runtime.ts +3 -3
- package/src/schema.ts +26 -6
- package/src/skill/generate.ts +3 -5
- package/src/types.ts +33 -0
- package/src/validate.ts +16 -0
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("
|
|
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
|
|
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("
|
|
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("
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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 (
|
|
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
|
|
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(
|
|
71
|
+
process.stdout.write(cliHelpRender(cliParseRoot(program), pr.helpPath, false));
|
|
72
72
|
process.exit(pr.helpExplicit ? 0 : 1);
|
|
73
73
|
}
|
|
74
74
|
|