argsbarg 3.6.4 → 4.0.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.
- package/CHANGELOG.md +32 -2
- package/README.md +21 -9
- package/docs/README.md +12 -8
- package/docs/bundled-docs.md +1 -1
- package/docs/cli-program.md +62 -2
- package/docs/config-schema.md +192 -0
- package/docs/developing.md +13 -0
- package/docs/install.md +38 -1
- package/docs/mcp.md +43 -19
- package/docs/output-schema.md +74 -52
- package/docs/templates/cursor/rules/cli-program.mdc +10 -5
- package/examples/config-app/main.ts +20 -0
- package/examples/config-app/program.ts +81 -0
- package/examples/config-app/schema.ts +37 -0
- package/examples/config-app/types.ts +19 -0
- package/examples/consumer-app/README.md +56 -0
- package/examples/consumer-app/bun.lock +75 -0
- package/examples/consumer-app/capabilities.test.ts +69 -0
- package/examples/consumer-app/package.json +17 -0
- package/examples/consumer-app/schemas/configSchemas.ts +6 -0
- package/examples/consumer-app/schemas/generated/app-config.json +40 -0
- package/examples/consumer-app/schemas/generated/status.json +28 -0
- package/examples/consumer-app/schemas/outputSchemas.ts +6 -0
- package/examples/consumer-app/scripts/schemagen/discover-schema-roots.test.ts +25 -0
- package/examples/consumer-app/scripts/schemagen/discover-schema-roots.ts +93 -0
- package/examples/consumer-app/scripts/schemagen/naming.ts +82 -0
- package/examples/consumer-app/scripts/schemagen.ts +76 -0
- package/examples/consumer-app/src/commands/status/types.ts +11 -0
- package/examples/consumer-app/src/main.ts +15 -0
- package/examples/consumer-app/src/program.ts +116 -0
- package/examples/consumer-app/src/types.ts +23 -0
- package/examples/consumer-app/tsconfig.json +14 -0
- package/examples/formats.ts +10 -3
- package/examples/mcp-test.ts +27 -8
- package/examples/minimal.ts +4 -3
- package/examples/nested.ts +5 -4
- package/examples/option-required.ts +8 -4
- package/index.d.ts +158 -75
- package/package.json +1 -1
- package/src/builtins/builtins.test.ts +13 -0
- package/src/builtins/config.test.ts +82 -0
- package/src/builtins/config.ts +220 -0
- package/src/builtins/dispatch.ts +18 -9
- package/src/builtins/export.ts +8 -33
- package/src/builtins/index.ts +1 -0
- package/src/builtins/install.ts +13 -0
- package/src/builtins/mcp.ts +1 -1
- package/src/builtins/presentation.ts +2 -17
- package/src/builtins/registry.ts +40 -0
- package/src/capabilities.ts +46 -0
- package/src/cli-errors.ts +15 -0
- package/src/cli.ts +389 -0
- package/src/config/bootstrap.ts +265 -0
- package/src/config/context.test.ts +79 -0
- package/src/config/context.ts +110 -0
- package/src/config/entry.ts +81 -0
- package/src/config/file.test.ts +112 -0
- package/src/config/file.ts +120 -0
- package/src/config/manifest.ts +62 -0
- package/src/config/resolve.test.ts +88 -0
- package/src/config/resolve.ts +167 -0
- package/src/config/schema.ts +101 -0
- package/src/config/validate.test.ts +63 -0
- package/src/config/validate.ts +292 -0
- package/src/config.integration.test.ts +100 -0
- package/src/context.ts +5 -0
- package/src/docs/docs.test.ts +16 -16
- package/src/docs/mcp-guide.ts +35 -11
- package/src/hidden-mcpb.test.ts +40 -2
- package/src/index.ts +4 -3
- package/src/install/index.ts +46 -3
- package/src/install/paths.ts +5 -20
- package/src/install/plan.ts +6 -0
- package/src/install/status.ts +12 -0
- package/src/install/uninstall.ts +11 -0
- package/src/install/update.test.ts +5 -5
- package/src/invoke.test.ts +207 -0
- package/src/mcp/bundle.ts +9 -116
- package/src/mcp/claude.test.ts +73 -0
- package/src/mcp/claude.ts +168 -0
- package/src/mcp/env.ts +3 -37
- package/src/mcp/server.ts +18 -10
- package/src/mcp/tools.ts +3 -7
- package/src/mcp/zip.ts +82 -0
- package/src/mcp.integration.test.ts +502 -0
- package/src/{index.test.ts → parse.test.ts} +24 -935
- package/src/paths/host.ts +40 -0
- package/src/schema.ts +1 -1
- package/src/skill/generate.ts +18 -5
- package/src/skill/hint.ts +18 -0
- package/src/skill/install.ts +1 -5
- package/src/test-fixtures.ts +192 -0
- package/src/types.ts +39 -13
- package/src/validate.ts +70 -0
- package/src/completion.ts +0 -13
- package/src/invoke.ts +0 -217
- package/src/mcp.ts +0 -28
- package/src/runtime.ts +0 -134
|
@@ -1,52 +1,36 @@
|
|
|
1
1
|
/*
|
|
2
|
-
|
|
3
|
-
It exercises the public API rather than internal helpers so the tests follow the same
|
|
4
|
-
paths that users and example CLIs take.
|
|
5
|
-
|
|
6
|
-
It keeps the CLI contract stable by catching routing, option handling, and generated
|
|
7
|
-
shell output regressions.
|
|
2
|
+
Domain-specific regression tests (split from index.test.ts).
|
|
8
3
|
*/
|
|
9
4
|
|
|
10
5
|
import { expect, test } from "bun:test";
|
|
6
|
+
import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
7
|
+
import { tmpdir } from "node:os";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
import { $ } from "bun";
|
|
10
|
+
import { completionBashScript, completionZshScript } from "./builtins/index.ts";
|
|
11
11
|
import { cliPresentationRoot } from "./builtins/presentation.ts";
|
|
12
|
-
import { completionBashScript, completionZshScript } from "./completion.ts";
|
|
13
12
|
import { cliHelpRender } from "./help.ts";
|
|
14
|
-
import {
|
|
15
|
-
|
|
16
|
-
CliFallbackMode,
|
|
17
|
-
CliOptionKind,
|
|
18
|
-
type CliProgram,
|
|
19
|
-
cliInvoke,
|
|
20
|
-
} from "./index.ts";
|
|
21
|
-
import { applyShellEnv, loadEnvFile } from "./mcp/env.ts";
|
|
22
|
-
import { buildToolCallSuccess } from "./mcp/result.ts";
|
|
13
|
+
import { Cli, CliFallbackMode, CliOptionKind, type CliProgram } from "./index.ts";
|
|
14
|
+
import { applyShellEnv } from "./mcp/env.ts";
|
|
23
15
|
import {
|
|
24
16
|
allMcpResources,
|
|
25
17
|
collectMcpTools,
|
|
26
18
|
mcpToolCallToArgv,
|
|
27
|
-
mcpToolDescription,
|
|
28
19
|
resolveMcpSchemaUri,
|
|
29
|
-
sanitizeToolSegment,
|
|
30
20
|
} from "./mcp/tools.ts";
|
|
31
21
|
import { ParseKind, parse, postParseValidate } from "./parse.ts";
|
|
32
|
-
import {
|
|
22
|
+
import { cliSchemaJson } from "./schema.ts";
|
|
33
23
|
import { generateSkillBundle } from "./skill/generate.ts";
|
|
34
24
|
import { cliSkillInstall } from "./skill/install.ts";
|
|
35
|
-
import
|
|
36
|
-
|
|
25
|
+
import {
|
|
26
|
+
enumMcpFixture,
|
|
27
|
+
nestedDocsFallbackFixture,
|
|
28
|
+
nestedMcpFixture,
|
|
29
|
+
testProgram,
|
|
30
|
+
varargsReadFixture,
|
|
31
|
+
} from "./test-fixtures.ts";
|
|
37
32
|
import { cliValidateProgram } from "./validate.ts";
|
|
38
33
|
|
|
39
|
-
function testProgram(
|
|
40
|
-
prog: Record<string, unknown> & { key: string; description: string },
|
|
41
|
-
): CliProgram {
|
|
42
|
-
return { version: "0.0.0", ...prog } as CliProgram;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
46
|
-
import { tmpdir } from "node:os";
|
|
47
|
-
import { join } from "node:path";
|
|
48
|
-
import { $ } from "bun";
|
|
49
|
-
|
|
50
34
|
test("bundled short presence flags", () => {
|
|
51
35
|
const root = testProgram({
|
|
52
36
|
key: "app",
|
|
@@ -740,613 +724,6 @@ test("root help includes program notes and agent hint", () => {
|
|
|
740
724
|
expect(help).toContain("myapp docs skill");
|
|
741
725
|
});
|
|
742
726
|
|
|
743
|
-
const nestedMcpFixture = testProgram({
|
|
744
|
-
key: "nested.ts",
|
|
745
|
-
description: "Nested groups demo.",
|
|
746
|
-
version: "1.0.0",
|
|
747
|
-
mcpServer: { enabled: true },
|
|
748
|
-
commands: [
|
|
749
|
-
{
|
|
750
|
-
key: "stat",
|
|
751
|
-
description: "File metadata.",
|
|
752
|
-
options: [
|
|
753
|
-
{
|
|
754
|
-
name: "json",
|
|
755
|
-
description: "Emit handler output as JSON.",
|
|
756
|
-
kind: CliOptionKind.Presence,
|
|
757
|
-
},
|
|
758
|
-
],
|
|
759
|
-
commands: [
|
|
760
|
-
{
|
|
761
|
-
key: "owner",
|
|
762
|
-
description: "Ownership helpers.",
|
|
763
|
-
commands: [
|
|
764
|
-
{
|
|
765
|
-
key: "lookup",
|
|
766
|
-
description: "Resolve owner info.",
|
|
767
|
-
options: [
|
|
768
|
-
{
|
|
769
|
-
name: "user-name",
|
|
770
|
-
description: "User to look up.",
|
|
771
|
-
kind: CliOptionKind.String,
|
|
772
|
-
shortName: "u",
|
|
773
|
-
},
|
|
774
|
-
],
|
|
775
|
-
positionals: [
|
|
776
|
-
{
|
|
777
|
-
name: "path",
|
|
778
|
-
description: "File or directory.",
|
|
779
|
-
kind: CliOptionKind.String,
|
|
780
|
-
},
|
|
781
|
-
],
|
|
782
|
-
handler: () => {},
|
|
783
|
-
},
|
|
784
|
-
],
|
|
785
|
-
},
|
|
786
|
-
],
|
|
787
|
-
},
|
|
788
|
-
{
|
|
789
|
-
key: "read",
|
|
790
|
-
description: "Print the first line of each file.",
|
|
791
|
-
positionals: [
|
|
792
|
-
{
|
|
793
|
-
name: "files",
|
|
794
|
-
description: "Paths to read.",
|
|
795
|
-
kind: CliOptionKind.String,
|
|
796
|
-
argMax: 0,
|
|
797
|
-
},
|
|
798
|
-
],
|
|
799
|
-
handler: () => {},
|
|
800
|
-
},
|
|
801
|
-
{
|
|
802
|
-
key: "hidden",
|
|
803
|
-
description: "Internal debug.",
|
|
804
|
-
mcpTool: { enabled: false },
|
|
805
|
-
handler: () => {},
|
|
806
|
-
},
|
|
807
|
-
],
|
|
808
|
-
fallbackCommand: "read",
|
|
809
|
-
fallbackMode: CliFallbackMode.MissingOrUnknown,
|
|
810
|
-
});
|
|
811
|
-
|
|
812
|
-
/** Sends NDJSON MCP requests to a subprocess and collects responses by id. */
|
|
813
|
-
async function mcpRequest(
|
|
814
|
-
requests: object[],
|
|
815
|
-
opts?: { script?: string; env?: Record<string, string> },
|
|
816
|
-
): Promise<Map<string | number, object>> {
|
|
817
|
-
const script = opts?.script ?? "examples/nested.ts";
|
|
818
|
-
const proc = Bun.spawn(["bun", "run", script, "mcp"], {
|
|
819
|
-
stdin: "pipe",
|
|
820
|
-
stdout: "pipe",
|
|
821
|
-
stderr: "pipe",
|
|
822
|
-
env: opts?.env ? { ...process.env, ...opts.env } : process.env,
|
|
823
|
-
});
|
|
824
|
-
|
|
825
|
-
const input = requests.map((r) => `${JSON.stringify(r)}\n`).join("");
|
|
826
|
-
proc.stdin.write(input);
|
|
827
|
-
proc.stdin.end();
|
|
828
|
-
|
|
829
|
-
const timeout = setTimeout(() => proc.kill(), 10_000);
|
|
830
|
-
const stdout = await new Response(proc.stdout).text();
|
|
831
|
-
await proc.exited;
|
|
832
|
-
clearTimeout(timeout);
|
|
833
|
-
|
|
834
|
-
const byId = new Map<string | number, object>();
|
|
835
|
-
for (const line of stdout.split("\n")) {
|
|
836
|
-
const trimmed = line.trim();
|
|
837
|
-
if (!trimmed) {
|
|
838
|
-
continue;
|
|
839
|
-
}
|
|
840
|
-
const msg = JSON.parse(trimmed) as { id?: string | number };
|
|
841
|
-
if (msg.id !== undefined) {
|
|
842
|
-
byId.set(msg.id, msg);
|
|
843
|
-
}
|
|
844
|
-
}
|
|
845
|
-
return byId;
|
|
846
|
-
}
|
|
847
|
-
|
|
848
|
-
test("sanitizeToolSegment normalizes dotted app keys", () => {
|
|
849
|
-
expect(sanitizeToolSegment("minimal.ts")).toBe("minimal_ts");
|
|
850
|
-
});
|
|
851
|
-
|
|
852
|
-
test("mcpToolDescription formats CLI path and root-leaf prefix", () => {
|
|
853
|
-
expect(mcpToolDescription(["stat", "owner", "lookup"], "nested.ts", "Resolve owner info.")).toBe(
|
|
854
|
-
"stat owner lookup — Resolve owner info.",
|
|
855
|
-
);
|
|
856
|
-
expect(mcpToolDescription(["read"], "nested.ts", "Print files.")).toBe("read — Print files.");
|
|
857
|
-
expect(mcpToolDescription([], "helloapp", "Tiny demo.")).toBe("helloapp — Tiny demo.");
|
|
858
|
-
});
|
|
859
|
-
|
|
860
|
-
test("collectMcpTools lists user leaf commands only", () => {
|
|
861
|
-
const tools = collectMcpTools(nestedMcpFixture);
|
|
862
|
-
const names = tools.map((t) => t.name);
|
|
863
|
-
expect(names).toContain("stat_owner_lookup");
|
|
864
|
-
expect(names).toContain("read");
|
|
865
|
-
expect(names).not.toContain("hidden");
|
|
866
|
-
expect(names).not.toContain("install");
|
|
867
|
-
expect(names).not.toContain("mcp");
|
|
868
|
-
expect(names).not.toContain("completion");
|
|
869
|
-
const lookup = tools.find((t) => t.name === "stat_owner_lookup")!;
|
|
870
|
-
expect(lookup.description).toBe("stat owner lookup — Resolve owner info.");
|
|
871
|
-
});
|
|
872
|
-
|
|
873
|
-
test("collectMcpTools appends leaf notes to MCP tool description", () => {
|
|
874
|
-
const root = testProgram({
|
|
875
|
-
key: "app",
|
|
876
|
-
version: "1.0.0",
|
|
877
|
-
description: "Notes demo.",
|
|
878
|
-
mcpServer: { enabled: true },
|
|
879
|
-
commands: [
|
|
880
|
-
{
|
|
881
|
-
key: "run",
|
|
882
|
-
description: "Run.",
|
|
883
|
-
notes: "Use `--json` for structured output.",
|
|
884
|
-
handler: () => {},
|
|
885
|
-
},
|
|
886
|
-
],
|
|
887
|
-
});
|
|
888
|
-
const tools = collectMcpTools(root);
|
|
889
|
-
expect(tools[0]?.description).toBe("run — Run.\n\nUse `--json` for structured output.");
|
|
890
|
-
});
|
|
891
|
-
|
|
892
|
-
test("collectMcpTools appends notes after mcpTool.description override", () => {
|
|
893
|
-
const root = testProgram({
|
|
894
|
-
key: "app",
|
|
895
|
-
version: "1.0.0",
|
|
896
|
-
description: "Notes demo.",
|
|
897
|
-
mcpServer: { enabled: true },
|
|
898
|
-
commands: [
|
|
899
|
-
{
|
|
900
|
-
key: "run",
|
|
901
|
-
description: "Run.",
|
|
902
|
-
notes: "Operational hint.",
|
|
903
|
-
mcpTool: { description: "Custom MCP text." },
|
|
904
|
-
handler: () => {},
|
|
905
|
-
},
|
|
906
|
-
],
|
|
907
|
-
});
|
|
908
|
-
const tools = collectMcpTools(root);
|
|
909
|
-
expect(tools[0]?.description).toBe("Custom MCP text.\n\nOperational hint.");
|
|
910
|
-
});
|
|
911
|
-
|
|
912
|
-
test("collectMcpTools resolves {argsbarg:program} in appended notes", () => {
|
|
913
|
-
const root = testProgram({
|
|
914
|
-
key: "myapp",
|
|
915
|
-
version: "1.0.0",
|
|
916
|
-
description: "Notes demo.",
|
|
917
|
-
mcpServer: { enabled: true },
|
|
918
|
-
commands: [
|
|
919
|
-
{
|
|
920
|
-
key: "run",
|
|
921
|
-
description: "Run.",
|
|
922
|
-
notes: "See `{argsbarg:program} docs api`.",
|
|
923
|
-
handler: () => {},
|
|
924
|
-
},
|
|
925
|
-
],
|
|
926
|
-
});
|
|
927
|
-
const tools = collectMcpTools(root);
|
|
928
|
-
expect(tools[0]?.description).toContain("See `myapp docs api`.");
|
|
929
|
-
});
|
|
930
|
-
|
|
931
|
-
test("cliSchemaExport includes leaf outputSchema", () => {
|
|
932
|
-
const root = testProgram({
|
|
933
|
-
key: "app",
|
|
934
|
-
version: "1.0.0",
|
|
935
|
-
description: "Schema export demo.",
|
|
936
|
-
mcpServer: { enabled: true },
|
|
937
|
-
commands: [
|
|
938
|
-
{
|
|
939
|
-
key: "run",
|
|
940
|
-
description: "Run.",
|
|
941
|
-
outputSchema: {
|
|
942
|
-
type: "object",
|
|
943
|
-
properties: { ok: { type: "boolean" } },
|
|
944
|
-
},
|
|
945
|
-
handler: () => {},
|
|
946
|
-
},
|
|
947
|
-
],
|
|
948
|
-
});
|
|
949
|
-
const schema = cliSchemaExport(root);
|
|
950
|
-
expect(schema.commands?.[0]?.outputSchema).toEqual({
|
|
951
|
-
type: "object",
|
|
952
|
-
properties: { ok: { type: "boolean" } },
|
|
953
|
-
});
|
|
954
|
-
});
|
|
955
|
-
|
|
956
|
-
test("cliSchemaExport accepts legacy mcpTool.outputSchema", () => {
|
|
957
|
-
const root = testProgram({
|
|
958
|
-
key: "app",
|
|
959
|
-
version: "1.0.0",
|
|
960
|
-
description: "Schema export demo.",
|
|
961
|
-
commands: [
|
|
962
|
-
{
|
|
963
|
-
key: "run",
|
|
964
|
-
description: "Run.",
|
|
965
|
-
mcpTool: {
|
|
966
|
-
outputSchema: { type: "object", properties: { id: { type: "string" } } },
|
|
967
|
-
},
|
|
968
|
-
handler: () => {},
|
|
969
|
-
},
|
|
970
|
-
],
|
|
971
|
-
});
|
|
972
|
-
expect(cliSchemaExport(root).commands?.[0]?.outputSchema).toEqual({
|
|
973
|
-
type: "object",
|
|
974
|
-
properties: { id: { type: "string" } },
|
|
975
|
-
});
|
|
976
|
-
});
|
|
977
|
-
|
|
978
|
-
test("outputSchema must be a JSON Schema object", () => {
|
|
979
|
-
const root = testProgram({
|
|
980
|
-
key: "app",
|
|
981
|
-
version: "1.0.0",
|
|
982
|
-
description: "Bad output schema.",
|
|
983
|
-
commands: [
|
|
984
|
-
{
|
|
985
|
-
key: "run",
|
|
986
|
-
description: "Run.",
|
|
987
|
-
outputSchema: [] as unknown as Record<string, unknown>,
|
|
988
|
-
handler: () => {},
|
|
989
|
-
},
|
|
990
|
-
],
|
|
991
|
-
});
|
|
992
|
-
expect(() => cliValidateProgram(root)).toThrow(/outputSchema must be a JSON Schema object/);
|
|
993
|
-
});
|
|
994
|
-
|
|
995
|
-
test("outputSchema cannot be set on both leaf and mcpTool", () => {
|
|
996
|
-
const root = testProgram({
|
|
997
|
-
key: "app",
|
|
998
|
-
version: "1.0.0",
|
|
999
|
-
description: "Duplicate output schema.",
|
|
1000
|
-
commands: [
|
|
1001
|
-
{
|
|
1002
|
-
key: "run",
|
|
1003
|
-
description: "Run.",
|
|
1004
|
-
outputSchema: { type: "object" },
|
|
1005
|
-
mcpTool: { outputSchema: { type: "object" } },
|
|
1006
|
-
handler: () => {},
|
|
1007
|
-
},
|
|
1008
|
-
],
|
|
1009
|
-
});
|
|
1010
|
-
expect(() => cliValidateProgram(root)).toThrow(/Set outputSchema on the leaf only/);
|
|
1011
|
-
});
|
|
1012
|
-
|
|
1013
|
-
test("collectMcpTools merges parent options into inputSchema", () => {
|
|
1014
|
-
const tools = collectMcpTools(nestedMcpFixture);
|
|
1015
|
-
const lookup = tools.find((t) => t.name === "stat_owner_lookup")!;
|
|
1016
|
-
const schema = lookup.inputSchema as { properties: Record<string, unknown>; required?: string[] };
|
|
1017
|
-
expect(schema.properties.json).toBeDefined();
|
|
1018
|
-
expect(schema.required).toContain("path");
|
|
1019
|
-
});
|
|
1020
|
-
|
|
1021
|
-
test("collectMcpTools includes outputSchema when set on leaf", () => {
|
|
1022
|
-
const root = testProgram({
|
|
1023
|
-
key: "app",
|
|
1024
|
-
version: "1.0.0",
|
|
1025
|
-
description: "Output schema demo.",
|
|
1026
|
-
mcpServer: { enabled: true },
|
|
1027
|
-
commands: [
|
|
1028
|
-
{
|
|
1029
|
-
key: "run",
|
|
1030
|
-
description: "Run with JSON output.",
|
|
1031
|
-
outputSchema: {
|
|
1032
|
-
type: "object",
|
|
1033
|
-
properties: { ok: { type: "boolean" } },
|
|
1034
|
-
required: ["ok"],
|
|
1035
|
-
},
|
|
1036
|
-
handler: () => {},
|
|
1037
|
-
},
|
|
1038
|
-
],
|
|
1039
|
-
});
|
|
1040
|
-
const tools = collectMcpTools(root);
|
|
1041
|
-
expect(tools).toHaveLength(1);
|
|
1042
|
-
expect(tools[0]?.outputSchema).toEqual({
|
|
1043
|
-
type: "object",
|
|
1044
|
-
properties: { ok: { type: "boolean" } },
|
|
1045
|
-
required: ["ok"],
|
|
1046
|
-
});
|
|
1047
|
-
});
|
|
1048
|
-
|
|
1049
|
-
test("collectMcpTools omits outputSchema when leaf has none", () => {
|
|
1050
|
-
const tools = collectMcpTools(nestedMcpFixture);
|
|
1051
|
-
const lookup = tools.find((t) => t.name === "stat_owner_lookup")!;
|
|
1052
|
-
expect(lookup.outputSchema).toBeUndefined();
|
|
1053
|
-
});
|
|
1054
|
-
|
|
1055
|
-
test("mcpToolCallToArgv builds nested lookup argv", () => {
|
|
1056
|
-
const tools = collectMcpTools(nestedMcpFixture);
|
|
1057
|
-
const lookup = tools.find((t) => t.name === "stat_owner_lookup")!;
|
|
1058
|
-
const argv = mcpToolCallToArgv(nestedMcpFixture, lookup, {
|
|
1059
|
-
"user-name": "alice",
|
|
1060
|
-
path: "./x",
|
|
1061
|
-
json: true,
|
|
1062
|
-
});
|
|
1063
|
-
expect(argv).toEqual(["stat", "owner", "lookup", "--json", "--user-name", "alice", "./x"]);
|
|
1064
|
-
});
|
|
1065
|
-
|
|
1066
|
-
test("mcpToolCallToArgv expands varargs positionals", () => {
|
|
1067
|
-
const tools = collectMcpTools(nestedMcpFixture);
|
|
1068
|
-
const read = tools.find((t) => t.name === "read")!;
|
|
1069
|
-
const argv = mcpToolCallToArgv(nestedMcpFixture, read, { files: ["a", "b"] });
|
|
1070
|
-
expect(argv).toEqual(["read", "a", "b"]);
|
|
1071
|
-
});
|
|
1072
|
-
|
|
1073
|
-
test("reserved command name install is rejected", () => {
|
|
1074
|
-
const root = testProgram({
|
|
1075
|
-
key: "app",
|
|
1076
|
-
description: "",
|
|
1077
|
-
commands: [
|
|
1078
|
-
{
|
|
1079
|
-
key: "install",
|
|
1080
|
-
description: "bad",
|
|
1081
|
-
handler: () => {},
|
|
1082
|
-
},
|
|
1083
|
-
],
|
|
1084
|
-
});
|
|
1085
|
-
expect(() => cliValidateProgram(root)).toThrow(/Reserved command name: install/);
|
|
1086
|
-
});
|
|
1087
|
-
|
|
1088
|
-
test("top-level command name mcp is allowed without mcpServer", () => {
|
|
1089
|
-
const root = testProgram({
|
|
1090
|
-
key: "app",
|
|
1091
|
-
description: "",
|
|
1092
|
-
commands: [
|
|
1093
|
-
{
|
|
1094
|
-
key: "mcp",
|
|
1095
|
-
description: "user command",
|
|
1096
|
-
handler: () => {},
|
|
1097
|
-
},
|
|
1098
|
-
],
|
|
1099
|
-
});
|
|
1100
|
-
expect(() => cliValidateProgram(root)).not.toThrow();
|
|
1101
|
-
});
|
|
1102
|
-
|
|
1103
|
-
test("top-level command name mcp is rejected when mcpServer is enabled", () => {
|
|
1104
|
-
const root = testProgram({
|
|
1105
|
-
key: "app",
|
|
1106
|
-
description: "",
|
|
1107
|
-
mcpServer: { enabled: true },
|
|
1108
|
-
commands: [
|
|
1109
|
-
{
|
|
1110
|
-
key: "mcp",
|
|
1111
|
-
description: "user command",
|
|
1112
|
-
handler: () => {},
|
|
1113
|
-
},
|
|
1114
|
-
],
|
|
1115
|
-
});
|
|
1116
|
-
expect(() => cliValidateProgram(root)).toThrow(/Reserved command name: mcp/);
|
|
1117
|
-
});
|
|
1118
|
-
|
|
1119
|
-
test("mcpServer on non-root node is rejected", () => {
|
|
1120
|
-
const root = {
|
|
1121
|
-
key: "app",
|
|
1122
|
-
version: "0.0.0",
|
|
1123
|
-
description: "",
|
|
1124
|
-
commands: [
|
|
1125
|
-
{
|
|
1126
|
-
key: "x",
|
|
1127
|
-
description: "cmd",
|
|
1128
|
-
mcpServer: { enabled: true },
|
|
1129
|
-
handler: () => {},
|
|
1130
|
-
},
|
|
1131
|
-
],
|
|
1132
|
-
} as unknown as CliProgram;
|
|
1133
|
-
expect(() => cliValidateProgram(root)).toThrow(/mcpServer is only supported on the program root/);
|
|
1134
|
-
});
|
|
1135
|
-
|
|
1136
|
-
test("mcpTool on root is rejected", () => {
|
|
1137
|
-
const root = testProgram({
|
|
1138
|
-
key: "app",
|
|
1139
|
-
description: "",
|
|
1140
|
-
mcpTool: { enabled: false },
|
|
1141
|
-
handler: () => {},
|
|
1142
|
-
});
|
|
1143
|
-
expect(() => cliValidateProgram(root)).toThrow(/mcpTool is only supported on leaf commands/);
|
|
1144
|
-
});
|
|
1145
|
-
|
|
1146
|
-
test("mcpTool on routing node is rejected", () => {
|
|
1147
|
-
const root = testProgram({
|
|
1148
|
-
key: "app",
|
|
1149
|
-
description: "",
|
|
1150
|
-
commands: [
|
|
1151
|
-
{
|
|
1152
|
-
key: "group",
|
|
1153
|
-
description: "group",
|
|
1154
|
-
mcpTool: { enabled: false },
|
|
1155
|
-
commands: [
|
|
1156
|
-
{
|
|
1157
|
-
key: "leaf",
|
|
1158
|
-
description: "leaf",
|
|
1159
|
-
handler: () => {},
|
|
1160
|
-
},
|
|
1161
|
-
],
|
|
1162
|
-
},
|
|
1163
|
-
],
|
|
1164
|
-
});
|
|
1165
|
-
expect(() => cliValidateProgram(root)).toThrow(/mcpTool is only supported on leaf commands/);
|
|
1166
|
-
});
|
|
1167
|
-
|
|
1168
|
-
test("buildToolCallSuccess returns stdout only", () => {
|
|
1169
|
-
const result = buildToolCallSuccess("hello\n", "");
|
|
1170
|
-
expect(result.isError).toBe(false);
|
|
1171
|
-
expect(result.content).toEqual([{ type: "text", text: "hello\n" }]);
|
|
1172
|
-
expect(result.structuredContent).toBeUndefined();
|
|
1173
|
-
});
|
|
1174
|
-
|
|
1175
|
-
test("buildToolCallSuccess adds stderr as second content block", () => {
|
|
1176
|
-
const result = buildToolCallSuccess("out\n", "warn\n");
|
|
1177
|
-
expect(result.content).toEqual([
|
|
1178
|
-
{ type: "text", text: "out\n" },
|
|
1179
|
-
{ type: "text", text: "warn" },
|
|
1180
|
-
]);
|
|
1181
|
-
expect(result.structuredContent).toBeUndefined();
|
|
1182
|
-
});
|
|
1183
|
-
|
|
1184
|
-
test("buildToolCallSuccess stderr-only still includes stdout slot", () => {
|
|
1185
|
-
const result = buildToolCallSuccess("", "warn\n");
|
|
1186
|
-
expect(result.content).toEqual([
|
|
1187
|
-
{ type: "text", text: "" },
|
|
1188
|
-
{ type: "text", text: "warn" },
|
|
1189
|
-
]);
|
|
1190
|
-
});
|
|
1191
|
-
|
|
1192
|
-
test("buildToolCallSuccess parses JSON structuredContent", () => {
|
|
1193
|
-
const result = buildToolCallSuccess('{"a":1}\n', "");
|
|
1194
|
-
expect(result.structuredContent).toEqual({ a: 1 });
|
|
1195
|
-
expect(result.content[0]?.text).toBe('{"a":1}\n');
|
|
1196
|
-
});
|
|
1197
|
-
|
|
1198
|
-
test("buildToolCallSuccess skips structuredContent for plain text", () => {
|
|
1199
|
-
const result = buildToolCallSuccess("lookup user=x\n", "");
|
|
1200
|
-
expect(result.structuredContent).toBeUndefined();
|
|
1201
|
-
});
|
|
1202
|
-
|
|
1203
|
-
test("buildToolCallSuccess parses JSON primitives", () => {
|
|
1204
|
-
const result = buildToolCallSuccess("true\n", "");
|
|
1205
|
-
expect(result.structuredContent).toBe(true);
|
|
1206
|
-
});
|
|
1207
|
-
|
|
1208
|
-
test("MCP initialize returns tools and resources capabilities", async () => {
|
|
1209
|
-
const responses = await mcpRequest([{ jsonrpc: "2.0", id: 1, method: "initialize", params: {} }]);
|
|
1210
|
-
const res = responses.get(1) as { result: { capabilities: Record<string, unknown> } };
|
|
1211
|
-
expect(res.result.capabilities.tools).toBeDefined();
|
|
1212
|
-
expect(res.result.capabilities.resources).toBeDefined();
|
|
1213
|
-
});
|
|
1214
|
-
|
|
1215
|
-
test("MCP tools/list includes stat_owner_lookup", async () => {
|
|
1216
|
-
const responses = await mcpRequest([{ jsonrpc: "2.0", id: 2, method: "tools/list", params: {} }]);
|
|
1217
|
-
const res = responses.get(2) as {
|
|
1218
|
-
result: { tools: { name: string; inputSchema: { required?: string[] } }[] };
|
|
1219
|
-
};
|
|
1220
|
-
const lookup = res.result.tools.find((t) => t.name === "stat_owner_lookup");
|
|
1221
|
-
expect(lookup).toBeDefined();
|
|
1222
|
-
expect(lookup?.inputSchema.required).toContain("path");
|
|
1223
|
-
});
|
|
1224
|
-
|
|
1225
|
-
test("MCP resources/read returns schema JSON", async () => {
|
|
1226
|
-
const responses = await mcpRequest([
|
|
1227
|
-
{ jsonrpc: "2.0", id: 3, method: "resources/read", params: { uri: "nested_ts://schema" } },
|
|
1228
|
-
]);
|
|
1229
|
-
const res = responses.get(3) as { result: { contents: { text: string }[] } };
|
|
1230
|
-
const schema = JSON.parse(res.result.contents[0]?.text);
|
|
1231
|
-
expect(schema.key).toBe("nested.ts");
|
|
1232
|
-
});
|
|
1233
|
-
|
|
1234
|
-
test("MCP tools/call runs stat_owner_lookup", async () => {
|
|
1235
|
-
const readme = join(import.meta.dir, "..", "README.md");
|
|
1236
|
-
const responses = await mcpRequest([
|
|
1237
|
-
{
|
|
1238
|
-
jsonrpc: "2.0",
|
|
1239
|
-
id: 4,
|
|
1240
|
-
method: "tools/call",
|
|
1241
|
-
params: {
|
|
1242
|
-
name: "stat_owner_lookup",
|
|
1243
|
-
arguments: { path: readme, "user-name": "test" },
|
|
1244
|
-
},
|
|
1245
|
-
},
|
|
1246
|
-
]);
|
|
1247
|
-
const res = responses.get(4) as { result: { content: { text: string }[]; isError: boolean } };
|
|
1248
|
-
expect(res.result.isError).toBe(false);
|
|
1249
|
-
expect(res.result.content[0]?.text).toContain("lookup user=test");
|
|
1250
|
-
});
|
|
1251
|
-
|
|
1252
|
-
test("MCP tools/call returns structuredContent for JSON stdout", async () => {
|
|
1253
|
-
const readme = join(import.meta.dir, "..", "README.md");
|
|
1254
|
-
const responses = await mcpRequest([
|
|
1255
|
-
{
|
|
1256
|
-
jsonrpc: "2.0",
|
|
1257
|
-
id: 6,
|
|
1258
|
-
method: "tools/call",
|
|
1259
|
-
params: {
|
|
1260
|
-
name: "stat_owner_lookup",
|
|
1261
|
-
arguments: { path: readme, "user-name": "test", json: true },
|
|
1262
|
-
},
|
|
1263
|
-
},
|
|
1264
|
-
]);
|
|
1265
|
-
const res = responses.get(6) as {
|
|
1266
|
-
result: {
|
|
1267
|
-
content: { text: string }[];
|
|
1268
|
-
structuredContent?: { user: string; path: string };
|
|
1269
|
-
isError: boolean;
|
|
1270
|
-
};
|
|
1271
|
-
};
|
|
1272
|
-
expect(res.result.isError).toBe(false);
|
|
1273
|
-
expect(res.result.structuredContent).toEqual({ user: "test", path: readme });
|
|
1274
|
-
expect(JSON.parse(res.result.content[0]?.text.trim())).toEqual({ user: "test", path: readme });
|
|
1275
|
-
});
|
|
1276
|
-
|
|
1277
|
-
test("MCP tools/call errors on missing required positional", async () => {
|
|
1278
|
-
const responses = await mcpRequest([
|
|
1279
|
-
{
|
|
1280
|
-
jsonrpc: "2.0",
|
|
1281
|
-
id: 5,
|
|
1282
|
-
method: "tools/call",
|
|
1283
|
-
params: { name: "stat_owner_lookup", arguments: { "user-name": "test" } },
|
|
1284
|
-
},
|
|
1285
|
-
]);
|
|
1286
|
-
const res = responses.get(5) as { result: { isError: boolean; content: { text: string }[] } };
|
|
1287
|
-
expect(res.result.isError).toBe(true);
|
|
1288
|
-
expect(res.result.content[0]?.text).toContain("Missing argument: path");
|
|
1289
|
-
});
|
|
1290
|
-
|
|
1291
|
-
test("MCP ping returns empty result", async () => {
|
|
1292
|
-
const responses = await mcpRequest([{ jsonrpc: "2.0", id: 99, method: "ping", params: {} }]);
|
|
1293
|
-
const res = responses.get(99) as { result: Record<string, never> };
|
|
1294
|
-
expect(res.result).toEqual({});
|
|
1295
|
-
});
|
|
1296
|
-
|
|
1297
|
-
test("minimal.ts mcp without opt-in fails", async () => {
|
|
1298
|
-
const { stderr, exitCode } = await $`bun run examples/minimal.ts mcp`.nothrow().quiet();
|
|
1299
|
-
expect(exitCode).toBe(1);
|
|
1300
|
-
expect(stderr.toString()).toContain("MCP is not enabled");
|
|
1301
|
-
});
|
|
1302
|
-
|
|
1303
|
-
test("ctx.invocation is cli via cliRun", async () => {
|
|
1304
|
-
const indexPath = join(import.meta.dir, "index.ts");
|
|
1305
|
-
const { stdout } = await $`bun -e ${`
|
|
1306
|
-
import { cliRun, CliProgram } from ${JSON.stringify(indexPath)};
|
|
1307
|
-
const cli = { key: "t", description: "d", version: "0.0.0", handler: (ctx) => console.log(ctx.invocation) };
|
|
1308
|
-
await cliRun(cli, []);
|
|
1309
|
-
`}`.quiet();
|
|
1310
|
-
expect(stdout.toString().trim()).toBe("cli");
|
|
1311
|
-
});
|
|
1312
|
-
|
|
1313
|
-
test("ctx.invocation is mcp via cliInvoke", async () => {
|
|
1314
|
-
let seen = "";
|
|
1315
|
-
const root = testProgram({
|
|
1316
|
-
key: "app",
|
|
1317
|
-
description: "",
|
|
1318
|
-
handler: (ctx: CliContext) => {
|
|
1319
|
-
seen = ctx.invocation;
|
|
1320
|
-
},
|
|
1321
|
-
});
|
|
1322
|
-
cliValidateProgram(root);
|
|
1323
|
-
const result = await cliInvoke(root, []);
|
|
1324
|
-
expect(result.kind).toBe("ok");
|
|
1325
|
-
expect(seen).toBe("mcp");
|
|
1326
|
-
});
|
|
1327
|
-
|
|
1328
|
-
const enumMcpFixture = testProgram({
|
|
1329
|
-
key: "app",
|
|
1330
|
-
description: "",
|
|
1331
|
-
mcpServer: { enabled: true },
|
|
1332
|
-
commands: [
|
|
1333
|
-
{
|
|
1334
|
-
key: "run",
|
|
1335
|
-
description: "Run with mode.",
|
|
1336
|
-
options: [
|
|
1337
|
-
{
|
|
1338
|
-
name: "mode",
|
|
1339
|
-
description: "Mode.",
|
|
1340
|
-
kind: CliOptionKind.Enum,
|
|
1341
|
-
choices: ["dev", "prod"],
|
|
1342
|
-
required: true,
|
|
1343
|
-
},
|
|
1344
|
-
],
|
|
1345
|
-
handler: () => {},
|
|
1346
|
-
},
|
|
1347
|
-
],
|
|
1348
|
-
});
|
|
1349
|
-
|
|
1350
727
|
test("Enum option inputSchema includes enum array", () => {
|
|
1351
728
|
const tools = collectMcpTools(enumMcpFixture);
|
|
1352
729
|
const run = tools.find((t) => t.name === "run")!;
|
|
@@ -1354,50 +731,6 @@ test("Enum option inputSchema includes enum array", () => {
|
|
|
1354
731
|
expect(schema.properties.mode.enum).toEqual(["dev", "prod"]);
|
|
1355
732
|
});
|
|
1356
733
|
|
|
1357
|
-
test("cliInvoke rejects invalid Enum value", async () => {
|
|
1358
|
-
const root = testProgram({
|
|
1359
|
-
key: "app",
|
|
1360
|
-
description: "",
|
|
1361
|
-
handler: () => {},
|
|
1362
|
-
options: [
|
|
1363
|
-
{
|
|
1364
|
-
name: "mode",
|
|
1365
|
-
description: "Mode.",
|
|
1366
|
-
kind: CliOptionKind.Enum,
|
|
1367
|
-
choices: ["dev", "prod"],
|
|
1368
|
-
required: true,
|
|
1369
|
-
},
|
|
1370
|
-
],
|
|
1371
|
-
});
|
|
1372
|
-
cliValidateProgram(root);
|
|
1373
|
-
const result = await cliInvoke(root, ["--mode", "staging"]);
|
|
1374
|
-
expect(result.kind).toBe("error");
|
|
1375
|
-
expect(result.errorMsg).toContain("not one of");
|
|
1376
|
-
});
|
|
1377
|
-
|
|
1378
|
-
test("cliInvoke accepts valid Enum value", async () => {
|
|
1379
|
-
const root = testProgram({
|
|
1380
|
-
key: "app",
|
|
1381
|
-
description: "",
|
|
1382
|
-
handler: (ctx: CliContext) => {
|
|
1383
|
-
console.log(ctx.stringOpt("mode"));
|
|
1384
|
-
},
|
|
1385
|
-
options: [
|
|
1386
|
-
{
|
|
1387
|
-
name: "mode",
|
|
1388
|
-
description: "Mode.",
|
|
1389
|
-
kind: CliOptionKind.Enum,
|
|
1390
|
-
choices: ["dev", "prod"],
|
|
1391
|
-
required: true,
|
|
1392
|
-
},
|
|
1393
|
-
],
|
|
1394
|
-
});
|
|
1395
|
-
cliValidateProgram(root);
|
|
1396
|
-
const result = await cliInvoke(root, ["--mode", "dev"]);
|
|
1397
|
-
expect(result.kind).toBe("ok");
|
|
1398
|
-
expect(result.stdout.trim()).toBe("dev");
|
|
1399
|
-
});
|
|
1400
|
-
|
|
1401
734
|
test("cliValidateProgram rejects Enum with no choices", () => {
|
|
1402
735
|
const root = testProgram({
|
|
1403
736
|
key: "app",
|
|
@@ -1418,7 +751,7 @@ test("cliValidateProgram rejects Enum with duplicate choices", () => {
|
|
|
1418
751
|
expect(() => cliValidateProgram(root)).toThrow(/choices must be distinct/);
|
|
1419
752
|
});
|
|
1420
753
|
|
|
1421
|
-
test("mcpTool.description override wins without
|
|
754
|
+
test("mcpTool.description override wins without env suffix", () => {
|
|
1422
755
|
const root = testProgram({
|
|
1423
756
|
key: "app",
|
|
1424
757
|
description: "",
|
|
@@ -1427,7 +760,7 @@ test("mcpTool.description override wins without requiresEnv suffix", () => {
|
|
|
1427
760
|
{
|
|
1428
761
|
key: "x",
|
|
1429
762
|
description: "Leaf desc.",
|
|
1430
|
-
mcpTool: { description: "custom"
|
|
763
|
+
mcpTool: { description: "custom" },
|
|
1431
764
|
handler: () => {},
|
|
1432
765
|
},
|
|
1433
766
|
],
|
|
@@ -1436,22 +769,14 @@ test("mcpTool.description override wins without requiresEnv suffix", () => {
|
|
|
1436
769
|
expect(tools[0]?.description).toBe("custom");
|
|
1437
770
|
});
|
|
1438
771
|
|
|
1439
|
-
test("
|
|
772
|
+
test("cliValidateProgram requires program.appConfig description", () => {
|
|
1440
773
|
const root = testProgram({
|
|
1441
774
|
key: "app",
|
|
1442
775
|
description: "",
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
{
|
|
1446
|
-
key: "x",
|
|
1447
|
-
description: "Leaf desc.",
|
|
1448
|
-
mcpTool: { requiresEnv: ["TOKEN"] },
|
|
1449
|
-
handler: () => {},
|
|
1450
|
-
},
|
|
1451
|
-
],
|
|
776
|
+
appConfig: { entries: { token: {} as { description: string } } },
|
|
777
|
+
handler: () => {},
|
|
1452
778
|
});
|
|
1453
|
-
|
|
1454
|
-
expect(tools[0]?.description).toContain("[requires env: TOKEN]");
|
|
779
|
+
expect(() => cliValidateProgram(root)).toThrow(/description must be a non-empty string/);
|
|
1455
780
|
});
|
|
1456
781
|
|
|
1457
782
|
test("cliValidateProgram rejects duplicate mcpResources URIs", () => {
|
|
@@ -1561,16 +886,6 @@ test("applyShellEnv merges PATH and preserves host vars", () => {
|
|
|
1561
886
|
delete process.env.NEWVAR;
|
|
1562
887
|
});
|
|
1563
888
|
|
|
1564
|
-
test("loadEnvFile overwrites existing keys", () => {
|
|
1565
|
-
const dir = mkdtempSync(join(tmpdir(), "argsbarg-env-"));
|
|
1566
|
-
const file = join(dir, ".env");
|
|
1567
|
-
writeFileSync(file, "FOO=fromfile\n", "utf8");
|
|
1568
|
-
process.env.FOO = "original";
|
|
1569
|
-
loadEnvFile(file);
|
|
1570
|
-
expect(process.env.FOO).toBe("fromfile");
|
|
1571
|
-
delete process.env.FOO;
|
|
1572
|
-
});
|
|
1573
|
-
|
|
1574
889
|
test("Enum completions list choices in bash script", () => {
|
|
1575
890
|
const root = testProgram({
|
|
1576
891
|
key: "app",
|
|
@@ -1592,151 +907,6 @@ test("Enum completions list choices in bash script", () => {
|
|
|
1592
907
|
expect(bash).toContain("prod");
|
|
1593
908
|
});
|
|
1594
909
|
|
|
1595
|
-
test("MCP resources/list includes custom resource", async () => {
|
|
1596
|
-
const responses = await mcpRequest(
|
|
1597
|
-
[{ jsonrpc: "2.0", id: 10, method: "resources/list", params: {} }],
|
|
1598
|
-
{ script: "examples/mcp-test.ts" },
|
|
1599
|
-
);
|
|
1600
|
-
const res = responses.get(10) as { result: { resources: { uri: string }[] } };
|
|
1601
|
-
const uris = res.result.resources.map((r) => r.uri);
|
|
1602
|
-
expect(uris).toContain("mcp_test://schema");
|
|
1603
|
-
expect(uris).toContain("test://hello");
|
|
1604
|
-
});
|
|
1605
|
-
|
|
1606
|
-
test("MCP resources/read returns custom resource body", async () => {
|
|
1607
|
-
const responses = await mcpRequest(
|
|
1608
|
-
[{ jsonrpc: "2.0", id: 11, method: "resources/read", params: { uri: "test://hello" } }],
|
|
1609
|
-
{ script: "examples/mcp-test.ts" },
|
|
1610
|
-
);
|
|
1611
|
-
const res = responses.get(11) as { result: { contents: { text: string }[] } };
|
|
1612
|
-
expect(res.result.contents[0]?.text).toBe("hello resource");
|
|
1613
|
-
});
|
|
1614
|
-
|
|
1615
|
-
test("MCP resources/read unknown URI returns error", async () => {
|
|
1616
|
-
const responses = await mcpRequest(
|
|
1617
|
-
[{ jsonrpc: "2.0", id: 12, method: "resources/read", params: { uri: "missing://nope" } }],
|
|
1618
|
-
{ script: "examples/mcp-test.ts" },
|
|
1619
|
-
);
|
|
1620
|
-
const res = responses.get(12) as { error: { code: number } };
|
|
1621
|
-
expect(res.error.code).toBe(-32602);
|
|
1622
|
-
});
|
|
1623
|
-
|
|
1624
|
-
test("MCP requiresEnv fails when env missing", async () => {
|
|
1625
|
-
const responses = await mcpRequest(
|
|
1626
|
-
[
|
|
1627
|
-
{
|
|
1628
|
-
jsonrpc: "2.0",
|
|
1629
|
-
id: 13,
|
|
1630
|
-
method: "tools/call",
|
|
1631
|
-
params: { name: "echo_env", arguments: { name: "ARGS_TEST_SECRET" } },
|
|
1632
|
-
},
|
|
1633
|
-
],
|
|
1634
|
-
{ script: "examples/mcp-test.ts", env: { ARGS_TEST_SECRET: "" } },
|
|
1635
|
-
);
|
|
1636
|
-
const res = responses.get(13) as { result: { isError: boolean; content: { text: string }[] } };
|
|
1637
|
-
expect(res.result.isError).toBe(true);
|
|
1638
|
-
expect(res.result.content[0]?.text).toContain("ARGS_TEST_SECRET");
|
|
1639
|
-
});
|
|
1640
|
-
|
|
1641
|
-
test("MCP requiresEnv succeeds when env present", async () => {
|
|
1642
|
-
const responses = await mcpRequest(
|
|
1643
|
-
[
|
|
1644
|
-
{
|
|
1645
|
-
jsonrpc: "2.0",
|
|
1646
|
-
id: 14,
|
|
1647
|
-
method: "tools/call",
|
|
1648
|
-
params: { name: "echo_env", arguments: { name: "ARGS_TEST_SECRET" } },
|
|
1649
|
-
},
|
|
1650
|
-
],
|
|
1651
|
-
{ script: "examples/mcp-test.ts", env: { ARGS_TEST_SECRET: "sekrit" } },
|
|
1652
|
-
);
|
|
1653
|
-
const res = responses.get(14) as { result: { isError: boolean; content: { text: string }[] } };
|
|
1654
|
-
expect(res.result.isError).toBe(false);
|
|
1655
|
-
expect(res.result.content[0]?.text.trim()).toBe("sekrit");
|
|
1656
|
-
});
|
|
1657
|
-
|
|
1658
|
-
test("MCP envFile loads vars for tool handlers", async () => {
|
|
1659
|
-
const dir = mkdtempSync(join(tmpdir(), "argsbarg-mcp-"));
|
|
1660
|
-
const envFile = join(dir, "mcp.env");
|
|
1661
|
-
writeFileSync(envFile, "ARGS_FILE_TOKEN=file-value\n", "utf8");
|
|
1662
|
-
const responses = await mcpRequest(
|
|
1663
|
-
[
|
|
1664
|
-
{
|
|
1665
|
-
jsonrpc: "2.0",
|
|
1666
|
-
id: 15,
|
|
1667
|
-
method: "tools/call",
|
|
1668
|
-
params: { name: "echo_env", arguments: { name: "ARGS_FILE_TOKEN" } },
|
|
1669
|
-
},
|
|
1670
|
-
],
|
|
1671
|
-
{
|
|
1672
|
-
script: "examples/mcp-test.ts",
|
|
1673
|
-
env: { ARGS_TEST_ENV_FILE: envFile, ARGS_TEST_SECRET: "present" },
|
|
1674
|
-
},
|
|
1675
|
-
);
|
|
1676
|
-
const res = responses.get(15) as { result: { isError: boolean; content: { text: string }[] } };
|
|
1677
|
-
expect(res.result.isError).toBe(false);
|
|
1678
|
-
expect(res.result.content[0]?.text.trim()).toBe("file-value");
|
|
1679
|
-
});
|
|
1680
|
-
|
|
1681
|
-
// ── v1.3 parser ergonomics ────────────────────────────────────────────────────
|
|
1682
|
-
|
|
1683
|
-
function varargsReadFixture(): CliProgram {
|
|
1684
|
-
return testProgram({
|
|
1685
|
-
key: "app",
|
|
1686
|
-
description: "",
|
|
1687
|
-
commands: [
|
|
1688
|
-
{
|
|
1689
|
-
key: "read",
|
|
1690
|
-
description: "Read files.",
|
|
1691
|
-
options: [
|
|
1692
|
-
{
|
|
1693
|
-
name: "json",
|
|
1694
|
-
description: "",
|
|
1695
|
-
kind: CliOptionKind.Presence,
|
|
1696
|
-
},
|
|
1697
|
-
],
|
|
1698
|
-
positionals: [
|
|
1699
|
-
{
|
|
1700
|
-
name: "files",
|
|
1701
|
-
description: "",
|
|
1702
|
-
kind: CliOptionKind.String,
|
|
1703
|
-
argMin: 0,
|
|
1704
|
-
argMax: 0,
|
|
1705
|
-
},
|
|
1706
|
-
],
|
|
1707
|
-
handler: () => {},
|
|
1708
|
-
},
|
|
1709
|
-
],
|
|
1710
|
-
});
|
|
1711
|
-
}
|
|
1712
|
-
|
|
1713
|
-
function nestedDocsFallbackFixture(): CliProgram {
|
|
1714
|
-
return testProgram({
|
|
1715
|
-
key: "app",
|
|
1716
|
-
description: "",
|
|
1717
|
-
commands: [
|
|
1718
|
-
{
|
|
1719
|
-
key: "docs",
|
|
1720
|
-
description: "Documentation commands.",
|
|
1721
|
-
fallbackCommand: "guide",
|
|
1722
|
-
fallbackMode: CliFallbackMode.MissingOnly,
|
|
1723
|
-
commands: [
|
|
1724
|
-
{
|
|
1725
|
-
key: "guide",
|
|
1726
|
-
description: "User guide.",
|
|
1727
|
-
handler: () => {},
|
|
1728
|
-
},
|
|
1729
|
-
{
|
|
1730
|
-
key: "api",
|
|
1731
|
-
description: "API reference.",
|
|
1732
|
-
handler: () => {},
|
|
1733
|
-
},
|
|
1734
|
-
],
|
|
1735
|
-
},
|
|
1736
|
-
],
|
|
1737
|
-
});
|
|
1738
|
-
}
|
|
1739
|
-
|
|
1740
910
|
test("nested fallback routes to default when argv exhausted at router", () => {
|
|
1741
911
|
const root = nestedDocsFallbackFixture();
|
|
1742
912
|
cliValidateProgram(root);
|
|
@@ -1835,7 +1005,7 @@ test("nested router scoped help does not route to fallback", () => {
|
|
|
1835
1005
|
expect(help).toContain("guide");
|
|
1836
1006
|
});
|
|
1837
1007
|
|
|
1838
|
-
test("varargs trailing option after positionals via
|
|
1008
|
+
test("varargs trailing option after positionals via Cli.invoke", async () => {
|
|
1839
1009
|
const root = varargsReadFixture();
|
|
1840
1010
|
cliValidateProgram(root);
|
|
1841
1011
|
const pr = postParseValidate(root, parse(root, ["read", "file.txt", "--json"]));
|
|
@@ -1874,92 +1044,11 @@ test("varargs double dash forces positional", () => {
|
|
|
1874
1044
|
test("varargs unknown flag errors", async () => {
|
|
1875
1045
|
const root = varargsReadFixture();
|
|
1876
1046
|
cliValidateProgram(root);
|
|
1877
|
-
const result = await
|
|
1047
|
+
const result = await new Cli(root).invoke(["read", "--unknown"]);
|
|
1878
1048
|
expect(result.kind).toBe("error");
|
|
1879
1049
|
expect(result.stderr).toContain("--unknown");
|
|
1880
1050
|
});
|
|
1881
1051
|
|
|
1882
|
-
test("varargs scoped help in tail", () => {
|
|
1883
|
-
const root = varargsReadFixture();
|
|
1884
|
-
cliValidateProgram(root);
|
|
1885
|
-
const pr = parse(root, ["read", "file.txt", "--help"]);
|
|
1886
|
-
expect(pr.kind).toBe(ParseKind.Help);
|
|
1887
|
-
expect(pr.helpPath).toContain("read");
|
|
1888
|
-
expect(pr.helpExplicit).toBe(true);
|
|
1889
|
-
});
|
|
1890
|
-
|
|
1891
|
-
test("ctx.positional returns single slot value", async () => {
|
|
1892
|
-
const root = testProgram({
|
|
1893
|
-
key: "app",
|
|
1894
|
-
description: "",
|
|
1895
|
-
commands: [
|
|
1896
|
-
{
|
|
1897
|
-
key: "x",
|
|
1898
|
-
description: "",
|
|
1899
|
-
positionals: [{ name: "path", description: "", kind: CliOptionKind.String }],
|
|
1900
|
-
handler: (ctx: CliContext) => {
|
|
1901
|
-
captured = ctx.positional("path");
|
|
1902
|
-
},
|
|
1903
|
-
},
|
|
1904
|
-
],
|
|
1905
|
-
});
|
|
1906
|
-
let captured: string | string[] | undefined;
|
|
1907
|
-
cliValidateProgram(root);
|
|
1908
|
-
await cliInvoke(root, ["x", "./file"]);
|
|
1909
|
-
expect(captured).toBe("./file");
|
|
1910
|
-
});
|
|
1911
|
-
|
|
1912
|
-
test("ctx.positional returns varargs array", async () => {
|
|
1913
|
-
const root = varargsReadFixture();
|
|
1914
|
-
let captured: string | string[] | undefined;
|
|
1915
|
-
if (isCliRouter(root)) {
|
|
1916
|
-
(root.commands[0] as CliLeaf).handler = (ctx) => {
|
|
1917
|
-
captured = ctx.positional("files");
|
|
1918
|
-
};
|
|
1919
|
-
}
|
|
1920
|
-
cliValidateProgram(root);
|
|
1921
|
-
await cliInvoke(root, ["read", "a.txt", "b.txt"]);
|
|
1922
|
-
expect(captured).toEqual(["a.txt", "b.txt"]);
|
|
1923
|
-
});
|
|
1924
|
-
|
|
1925
|
-
test("ctx.positional returns undefined for absent optional slot", async () => {
|
|
1926
|
-
const root = testProgram({
|
|
1927
|
-
key: "app",
|
|
1928
|
-
description: "",
|
|
1929
|
-
commands: [
|
|
1930
|
-
{
|
|
1931
|
-
key: "x",
|
|
1932
|
-
description: "",
|
|
1933
|
-
positionals: [
|
|
1934
|
-
{ name: "opt", description: "", kind: CliOptionKind.String, argMin: 0, argMax: 1 },
|
|
1935
|
-
],
|
|
1936
|
-
handler: (ctx: CliContext) => {
|
|
1937
|
-
captured = ctx.positional("opt");
|
|
1938
|
-
},
|
|
1939
|
-
},
|
|
1940
|
-
],
|
|
1941
|
-
});
|
|
1942
|
-
let captured: string | string[] | undefined;
|
|
1943
|
-
cliValidateProgram(root);
|
|
1944
|
-
await cliInvoke(root, ["x"]);
|
|
1945
|
-
expect(captured).toBeUndefined();
|
|
1946
|
-
});
|
|
1947
|
-
|
|
1948
|
-
test("ctx.positional varargs matches ctx.args", async () => {
|
|
1949
|
-
const root = varargsReadFixture();
|
|
1950
|
-
let positional: string | string[] | undefined;
|
|
1951
|
-
let args: string[] = [];
|
|
1952
|
-
if (isCliRouter(root)) {
|
|
1953
|
-
(root.commands[0] as CliLeaf).handler = (ctx) => {
|
|
1954
|
-
positional = ctx.positional("files");
|
|
1955
|
-
args = ctx.args;
|
|
1956
|
-
};
|
|
1957
|
-
}
|
|
1958
|
-
cliValidateProgram(root);
|
|
1959
|
-
await cliInvoke(root, ["read", "a.txt", "b.txt"]);
|
|
1960
|
-
expect(positional).toEqual(args);
|
|
1961
|
-
});
|
|
1962
|
-
|
|
1963
1052
|
test("mcpToolCallToArgv rejects comma-separated string for varargs", () => {
|
|
1964
1053
|
const tools = collectMcpTools(nestedMcpFixture);
|
|
1965
1054
|
const read = tools.find((t) => t.name === "read")!;
|