argsbarg 5.1.16 → 6.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 +46 -1
- package/README.md +33 -25
- package/docs/README.md +2 -1
- package/docs/api-server.md +141 -0
- package/docs/bundled-docs.md +24 -10
- package/docs/cli-program.md +17 -2
- package/docs/config-schema.md +18 -8
- package/docs/developing.md +1 -1
- package/docs/mcp.md +5 -5
- package/docs/output-schema.md +78 -47
- package/examples/full-example/README.md +15 -6
- package/examples/full-example/docs/README.md +27 -0
- package/examples/full-example/docs/api.md +511 -0
- package/examples/full-example/docs/cli-schema.json +453 -0
- package/examples/full-example/docs/http.md +81 -0
- package/examples/full-example/docs/mcp.md +159 -0
- package/examples/full-example/docs/openapi.json +222 -0
- package/examples/full-example/docs/skill.md +46 -0
- package/examples/full-example/justfile +6 -2
- package/examples/full-example/schemas/generated/app-config.json +1 -1
- package/examples/full-example/schemas/generated/status.json +1 -1
- package/examples/full-example/scripts/dev-formula.ts +1 -1
- package/examples/full-example/scripts/schemagen/discover-schema-roots.test.ts +3 -3
- package/examples/full-example/scripts/schemagen/discover-schema-roots.ts +90 -35
- package/examples/full-example/scripts/schemagen/naming.ts +17 -0
- package/examples/full-example/scripts/schemagen.ts +14 -3
- package/examples/full-example/src/commands/echo/command.ts +6 -1
- package/examples/full-example/src/commands/status/schema-types.ts +14 -0
- package/examples/full-example/src/commands/status/types.ts +1 -11
- package/examples/full-example/src/{types.ts → config/schema-types.ts} +3 -2
- package/examples/full-example/src/program.ts +3 -0
- package/examples/mcp-test.ts +13 -2
- package/examples/nested.ts +12 -3
- package/examples/servers.ts +72 -0
- package/index.d.ts +66 -7
- package/package.json +17 -17
- package/src/api/openapi.ts +117 -0
- package/src/api/result.ts +111 -0
- package/src/api/schema-deref.test.ts +99 -0
- package/src/api/schema-deref.ts +76 -0
- package/src/api/server.ts +120 -0
- package/src/api.integration.test.ts +441 -0
- package/src/builtins/api.ts +38 -0
- package/src/builtins/dispatch.ts +26 -0
- package/src/builtins/registry.ts +4 -0
- package/src/capabilities.ts +12 -1
- package/src/cli-errors.ts +3 -0
- package/src/cli-tool/full-example-capabilities.test.ts +3 -0
- package/src/cli.ts +60 -8
- package/src/config.integration.test.ts +22 -4
- package/src/context.ts +29 -1
- package/src/docs/api-guide.ts +2 -2
- package/src/docs/builtin.ts +11 -1
- package/src/docs/docs.test.ts +70 -12
- package/src/docs/http-guide.ts +132 -0
- package/src/docs/mcp-guide.ts +3 -3
- package/src/docs/mcp-resources.ts +5 -2
- package/src/docs/resolve.ts +26 -2
- package/src/docs/save.ts +5 -2
- package/src/headless/tool-call.ts +157 -0
- package/src/headless.test.ts +4 -2
- package/src/headless.ts +10 -5
- package/src/index.ts +5 -0
- package/src/mcp/result.ts +39 -34
- package/src/mcp/server.ts +18 -36
- package/src/mcp/tools.ts +14 -3
- package/src/mcp.integration.test.ts +46 -39
- package/src/parse.test.ts +16 -6
- package/src/respond.ts +48 -0
- package/src/schema.ts +1 -1
- package/src/skill/generate.ts +1 -1
- package/src/types.ts +46 -4
- package/src/validate.ts +7 -0
package/src/mcp/server.ts
CHANGED
|
@@ -4,10 +4,8 @@ resources, and ping. Responses are newline-delimited JSON on stdout only.
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import type { Cli } from "../cli.ts";
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import { buildToolCallSuccess } from "./result.ts";
|
|
10
|
-
import { allMcpResources, collectMcpTools, mcpToolCallToArgv, resolveMcpServerInfo } from "./tools.ts";
|
|
7
|
+
import { executeHeadlessToolCall, lookupHeadlessTool } from "../headless/tool-call.ts";
|
|
8
|
+
import { allMcpResources, collectMcpTools, resolveMcpServerInfo } from "./tools.ts";
|
|
11
9
|
|
|
12
10
|
const MCP_PROTOCOL_VERSION = "2024-11-05";
|
|
13
11
|
|
|
@@ -109,57 +107,41 @@ async function handleRequestLine(cli: Cli, line: string): Promise<void> {
|
|
|
109
107
|
writeError(id, -32602, "Invalid params: arguments must be an object");
|
|
110
108
|
return;
|
|
111
109
|
}
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
const { resolved } = bootstrapAppConfig(root, { validateFile: false });
|
|
119
|
-
const missingConfig = missingRequiredConfig(root, resolved);
|
|
120
|
-
if (missingConfig.length > 0) {
|
|
121
|
-
writeResponse({
|
|
122
|
-
jsonrpc: "2.0",
|
|
123
|
-
id,
|
|
124
|
-
result: {
|
|
125
|
-
content: [
|
|
126
|
-
{
|
|
127
|
-
type: "text",
|
|
128
|
-
text: formatMcpMissingConfigMessage(root, missingConfig),
|
|
129
|
-
},
|
|
130
|
-
],
|
|
131
|
-
isError: true,
|
|
132
|
-
},
|
|
133
|
-
});
|
|
134
|
-
return;
|
|
135
|
-
}
|
|
136
|
-
const argvResult = mcpToolCallToArgv(root, tool, (rawArgs ?? {}) as Record<string, unknown>);
|
|
137
|
-
if ("error" in argvResult) {
|
|
110
|
+
const lookup = lookupHeadlessTool(root, name);
|
|
111
|
+
if (!lookup.ok) {
|
|
112
|
+
if (lookup.kind === "unknown") {
|
|
113
|
+
writeError(id, -32602, lookup.message);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
138
116
|
writeResponse({
|
|
139
117
|
jsonrpc: "2.0",
|
|
140
118
|
id,
|
|
141
119
|
result: {
|
|
142
|
-
content: [{ type: "text", text:
|
|
120
|
+
content: [{ type: "text", text: lookup.message }],
|
|
143
121
|
isError: true,
|
|
144
122
|
},
|
|
145
123
|
});
|
|
146
124
|
return;
|
|
147
125
|
}
|
|
148
|
-
const invokeResult = await
|
|
149
|
-
|
|
126
|
+
const invokeResult = await executeHeadlessToolCall(
|
|
127
|
+
cli,
|
|
128
|
+
lookup.tool,
|
|
129
|
+
(rawArgs ?? {}) as Record<string, unknown>,
|
|
130
|
+
"mcp",
|
|
131
|
+
);
|
|
132
|
+
if (invokeResult.ok) {
|
|
150
133
|
writeResponse({
|
|
151
134
|
jsonrpc: "2.0",
|
|
152
135
|
id,
|
|
153
|
-
result:
|
|
136
|
+
result: invokeResult.mcpResult,
|
|
154
137
|
});
|
|
155
138
|
return;
|
|
156
139
|
}
|
|
157
|
-
const errText = invokeResult.stderr.trim() || invokeResult.errorMsg || `Exit code ${invokeResult.exitCode}`;
|
|
158
140
|
writeResponse({
|
|
159
141
|
jsonrpc: "2.0",
|
|
160
142
|
id,
|
|
161
143
|
result: {
|
|
162
|
-
content: [{ type: "text", text:
|
|
144
|
+
content: [{ type: "text", text: invokeResult.message }],
|
|
163
145
|
isError: true,
|
|
164
146
|
},
|
|
165
147
|
});
|
package/src/mcp/tools.ts
CHANGED
|
@@ -41,8 +41,10 @@ export function mcpServerId(root: CliProgram): string {
|
|
|
41
41
|
|
|
42
42
|
/** One MCP tool derived from a leaf CLI command. */
|
|
43
43
|
export interface McpToolDef {
|
|
44
|
-
/** MCP tool name (underscore-separated
|
|
44
|
+
/** MCP tool name (underscore-separated, sanitized segments). */
|
|
45
45
|
name: string;
|
|
46
|
+
/** HTTP API tool name (hyphen-separated path; preserves command key spelling). */
|
|
47
|
+
apiName: string;
|
|
46
48
|
/** Tool description from the leaf command. */
|
|
47
49
|
description: string;
|
|
48
50
|
/** Command path segments from the program root. */
|
|
@@ -69,6 +71,14 @@ export function mcpToolName(root: CliProgram, path: string[]): string {
|
|
|
69
71
|
return path.map(sanitizeToolSegment).join("_");
|
|
70
72
|
}
|
|
71
73
|
|
|
74
|
+
/** Builds the HTTP API tool name for a leaf at the given path (hyphen-joined, unsanitized). */
|
|
75
|
+
export function apiToolName(root: CliProgram, path: string[]): string {
|
|
76
|
+
if (path.length === 0) {
|
|
77
|
+
return root.key;
|
|
78
|
+
}
|
|
79
|
+
return path.join("-");
|
|
80
|
+
}
|
|
81
|
+
|
|
72
82
|
/** JSON Schema property for one option. */
|
|
73
83
|
function optionProperty(opt: CliOption): Record<string, unknown> {
|
|
74
84
|
const base: Record<string, unknown> = { description: opt.description };
|
|
@@ -197,7 +207,7 @@ export function allMcpResources(root: CliProgram): McpResourceEntry[] {
|
|
|
197
207
|
const builtIn: McpResourceEntry = {
|
|
198
208
|
uri: schemaUri,
|
|
199
209
|
name: "cli-schema",
|
|
200
|
-
description: "Full CLI command tree (same as docs schema).",
|
|
210
|
+
description: "Full CLI command tree (same as docs cli-schema).",
|
|
201
211
|
mimeType: "application/json",
|
|
202
212
|
load: () => cliSchemaJson(root),
|
|
203
213
|
};
|
|
@@ -227,10 +237,11 @@ export function collectMcpTools(root: CliProgram): McpToolDef[] {
|
|
|
227
237
|
const outputSchema = leafOutputSchema(cmd);
|
|
228
238
|
out.push({
|
|
229
239
|
name: mcpToolName(root, path),
|
|
240
|
+
apiName: apiToolName(root, path),
|
|
230
241
|
description: resolveToolDescription(root, path, cmd),
|
|
231
242
|
path,
|
|
232
243
|
leaf: cmd,
|
|
233
|
-
inputSchema: buildInputSchema(root, path, cmd),
|
|
244
|
+
inputSchema: cmd.inputSchema ?? buildInputSchema(root, path, cmd),
|
|
234
245
|
...(outputSchema === undefined ? {} : { outputSchema }),
|
|
235
246
|
});
|
|
236
247
|
return;
|
|
@@ -6,8 +6,15 @@ import { expect, test } from "bun:test";
|
|
|
6
6
|
import { join } from "node:path";
|
|
7
7
|
import { $ } from "bun";
|
|
8
8
|
import type { CliProgram } from "./index.ts";
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
9
|
+
import { buildToolCallSuccessFromResponse } from "./mcp/result.ts";
|
|
10
|
+
import {
|
|
11
|
+
apiToolName,
|
|
12
|
+
collectMcpTools,
|
|
13
|
+
mcpToolCallToArgv,
|
|
14
|
+
mcpToolDescription,
|
|
15
|
+
mcpToolName,
|
|
16
|
+
sanitizeToolSegment,
|
|
17
|
+
} from "./mcp/tools.ts";
|
|
11
18
|
import { cliSchemaExport } from "./schema.ts";
|
|
12
19
|
import { mcpRequest, nestedMcpFixture, testProgram } from "./test-fixtures.ts";
|
|
13
20
|
import { cliValidateProgram } from "./validate.ts";
|
|
@@ -24,17 +31,25 @@ test("mcpToolDescription formats CLI path and root-leaf prefix", () => {
|
|
|
24
31
|
expect(mcpToolDescription([], "helloapp", "Tiny demo.")).toBe("helloapp — Tiny demo.");
|
|
25
32
|
});
|
|
26
33
|
|
|
34
|
+
test("apiToolName hyphen-joins path; mcpToolName sanitizes to underscores", () => {
|
|
35
|
+
expect(apiToolName(nestedMcpFixture, ["stat", "owner", "lookup"])).toBe("stat-owner-lookup");
|
|
36
|
+
expect(mcpToolName(nestedMcpFixture, ["stat", "owner", "lookup"])).toBe("stat_owner_lookup");
|
|
37
|
+
expect(apiToolName(nestedMcpFixture, ["render-invoice"])).toBe("render-invoice");
|
|
38
|
+
expect(mcpToolName(nestedMcpFixture, ["render-invoice"])).toBe("render_invoice");
|
|
39
|
+
});
|
|
40
|
+
|
|
27
41
|
test("collectMcpTools lists user leaf commands only", () => {
|
|
28
42
|
const tools = collectMcpTools(nestedMcpFixture);
|
|
29
43
|
const names = tools.map((t) => t.name);
|
|
30
44
|
expect(names).toContain("stat_owner_lookup");
|
|
45
|
+
const lookup = tools.find((t) => t.name === "stat_owner_lookup")!;
|
|
46
|
+
expect(lookup.apiName).toBe("stat-owner-lookup");
|
|
47
|
+
expect(lookup.description).toBe("stat owner lookup — Resolve owner info.");
|
|
31
48
|
expect(names).toContain("read");
|
|
32
49
|
expect(names).not.toContain("hidden");
|
|
33
50
|
expect(names).not.toContain("configure");
|
|
34
51
|
expect(names).not.toContain("mcp");
|
|
35
52
|
expect(names).not.toContain("completion");
|
|
36
|
-
const lookup = tools.find((t) => t.name === "stat_owner_lookup")!;
|
|
37
|
-
expect(lookup.description).toBe("stat owner lookup — Resolve owner info.");
|
|
38
53
|
});
|
|
39
54
|
|
|
40
55
|
/** Tests that collectMcpTools appends leaf notes to MCP tool description. */
|
|
@@ -345,44 +360,35 @@ test("mcpTool on routing node is rejected", () => {
|
|
|
345
360
|
expect(() => cliValidateProgram(root)).toThrow(/mcpTool is only supported on leaf commands/);
|
|
346
361
|
});
|
|
347
362
|
|
|
348
|
-
test("
|
|
349
|
-
const result =
|
|
363
|
+
test("buildToolCallSuccessFromResponse maps JSON object", () => {
|
|
364
|
+
const result = buildToolCallSuccessFromResponse({ body: { a: 1 } });
|
|
350
365
|
expect(result.isError).toBe(false);
|
|
351
|
-
expect(result.content).toEqual([{ type: "text", text: "hello\n" }]);
|
|
352
|
-
expect(result.structuredContent).toBeUndefined();
|
|
353
|
-
});
|
|
354
|
-
|
|
355
|
-
test("buildToolCallSuccess adds stderr as second content block", () => {
|
|
356
|
-
const result = buildToolCallSuccess("out\n", "warn\n");
|
|
357
|
-
expect(result.content).toEqual([
|
|
358
|
-
{ type: "text", text: "out\n" },
|
|
359
|
-
{ type: "text", text: "warn" },
|
|
360
|
-
]);
|
|
361
|
-
expect(result.structuredContent).toBeUndefined();
|
|
362
|
-
});
|
|
363
|
-
|
|
364
|
-
test("buildToolCallSuccess stderr-only still includes stdout slot", () => {
|
|
365
|
-
const result = buildToolCallSuccess("", "warn\n");
|
|
366
|
-
expect(result.content).toEqual([
|
|
367
|
-
{ type: "text", text: "" },
|
|
368
|
-
{ type: "text", text: "warn" },
|
|
369
|
-
]);
|
|
370
|
-
});
|
|
371
|
-
|
|
372
|
-
test("buildToolCallSuccess parses JSON structuredContent", () => {
|
|
373
|
-
const result = buildToolCallSuccess('{"a":1}\n', "");
|
|
374
366
|
expect(result.structuredContent).toEqual({ a: 1 });
|
|
375
|
-
expect(result.content[0]?.text).toBe(
|
|
367
|
+
expect(result.content[0]?.text).toBe("");
|
|
376
368
|
});
|
|
377
369
|
|
|
378
|
-
test("
|
|
379
|
-
const result =
|
|
380
|
-
|
|
370
|
+
test("buildToolCallSuccessFromResponse maps string body", () => {
|
|
371
|
+
const result = buildToolCallSuccessFromResponse({
|
|
372
|
+
body: "lookup user=x",
|
|
373
|
+
contentType: "text/plain; charset=utf-8",
|
|
374
|
+
});
|
|
375
|
+
expect(result.structuredContent).toEqual({
|
|
376
|
+
content: "lookup user=x",
|
|
377
|
+
contentType: "text/plain; charset=utf-8",
|
|
378
|
+
});
|
|
381
379
|
});
|
|
382
380
|
|
|
383
|
-
test("
|
|
384
|
-
const
|
|
385
|
-
|
|
381
|
+
test("buildToolCallSuccessFromResponse maps binary body as base64", () => {
|
|
382
|
+
const bytes = new Uint8Array([0x25, 0x50, 0x44, 0x46]);
|
|
383
|
+
const result = buildToolCallSuccessFromResponse({
|
|
384
|
+
body: bytes,
|
|
385
|
+
contentType: "application/pdf",
|
|
386
|
+
});
|
|
387
|
+
expect(result.structuredContent).toEqual({
|
|
388
|
+
data: "JVBERg==",
|
|
389
|
+
contentType: "application/pdf",
|
|
390
|
+
encoding: "base64",
|
|
391
|
+
});
|
|
386
392
|
});
|
|
387
393
|
|
|
388
394
|
test("MCP initialize returns tools and resources capabilities", async () => {
|
|
@@ -425,9 +431,11 @@ test("MCP tools/call runs stat_owner_lookup", async () => {
|
|
|
425
431
|
},
|
|
426
432
|
},
|
|
427
433
|
]);
|
|
428
|
-
const res = responses.get(4) as {
|
|
434
|
+
const res = responses.get(4) as {
|
|
435
|
+
result: { content: { text: string }[]; structuredContent?: { content: string }; isError: boolean };
|
|
436
|
+
};
|
|
429
437
|
expect(res.result.isError).toBe(false);
|
|
430
|
-
expect(res.result.content
|
|
438
|
+
expect(res.result.structuredContent?.content).toContain("lookup user=test");
|
|
431
439
|
});
|
|
432
440
|
|
|
433
441
|
/** MCP tools/call returns structuredContent for JSON stdout. */
|
|
@@ -453,7 +461,6 @@ test("MCP tools/call returns structuredContent for JSON stdout", async () => {
|
|
|
453
461
|
};
|
|
454
462
|
expect(res.result.isError).toBe(false);
|
|
455
463
|
expect(res.result.structuredContent).toEqual({ user: "test", path: readme });
|
|
456
|
-
expect(JSON.parse(res.result.content[0]?.text.trim())).toEqual({ user: "test", path: readme });
|
|
457
464
|
});
|
|
458
465
|
|
|
459
466
|
/** MCP tools/call errors on missing required positional. */
|
package/src/parse.test.ts
CHANGED
|
@@ -503,8 +503,8 @@ test("leaf completion help prints correctly", async () => {
|
|
|
503
503
|
});
|
|
504
504
|
|
|
505
505
|
/** Docs schema exports JSON for nested CLIs. */
|
|
506
|
-
test("docs schema exports JSON for nested CLIs", async () => {
|
|
507
|
-
const { stdout, stderr, exitCode } = await $`bun run examples/nested.ts docs schema`.nothrow().quiet();
|
|
506
|
+
test("docs cli-schema exports JSON for nested CLIs", async () => {
|
|
507
|
+
const { stdout, stderr, exitCode } = await $`bun run examples/nested.ts docs cli-schema`.nothrow().quiet();
|
|
508
508
|
expect(exitCode).toBe(0);
|
|
509
509
|
expect(stderr.toString()).toBe("");
|
|
510
510
|
|
|
@@ -520,8 +520,8 @@ test("docs schema exports JSON for nested CLIs", async () => {
|
|
|
520
520
|
});
|
|
521
521
|
|
|
522
522
|
/** Docs schema exports JSON for leaf roots. */
|
|
523
|
-
test("docs schema exports JSON for leaf roots", async () => {
|
|
524
|
-
const { stdout, exitCode } = await $`bun run examples/minimal.ts docs schema`.nothrow().quiet();
|
|
523
|
+
test("docs cli-schema exports JSON for leaf roots", async () => {
|
|
524
|
+
const { stdout, exitCode } = await $`bun run examples/minimal.ts docs cli-schema`.nothrow().quiet();
|
|
525
525
|
expect(exitCode).toBe(0);
|
|
526
526
|
|
|
527
527
|
const schema = JSON.parse(stdout.toString());
|
|
@@ -658,8 +658,8 @@ test("docs help lists schema, api, and skill subcommands", () => {
|
|
|
658
658
|
],
|
|
659
659
|
});
|
|
660
660
|
const help = cliHelpRender(cliPresentationRoot(root), ["docs"], false);
|
|
661
|
-
expect(help).toContain("schema");
|
|
662
|
-
expect(help).toContain("Print the full command tree as JSON.");
|
|
661
|
+
expect(help).toContain("cli-schema");
|
|
662
|
+
expect(help).toContain("Print the full CLI command tree as JSON.");
|
|
663
663
|
expect(help).toContain("api");
|
|
664
664
|
expect(help).toContain("markdown");
|
|
665
665
|
expect(help).toContain("skill");
|
|
@@ -814,6 +814,16 @@ test("cliValidateProgram rejects empty mcpServer", () => {
|
|
|
814
814
|
expect(() => cliValidateProgram(root)).toThrow(/mcpServer requires enabled: true/);
|
|
815
815
|
});
|
|
816
816
|
|
|
817
|
+
test("cliValidateProgram rejects empty apiServer", () => {
|
|
818
|
+
const root = testProgram({
|
|
819
|
+
key: "app",
|
|
820
|
+
description: "",
|
|
821
|
+
apiServer: {} as { enabled: boolean },
|
|
822
|
+
handler: () => {},
|
|
823
|
+
});
|
|
824
|
+
expect(() => cliValidateProgram(root)).toThrow(/apiServer requires enabled: true/);
|
|
825
|
+
});
|
|
826
|
+
|
|
817
827
|
test("resolveMcpSchemaUri uses sanitized root key", () => {
|
|
818
828
|
const root = testProgram({
|
|
819
829
|
key: "nested.ts",
|
package/src/respond.ts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Helpers for ctx.respond(): content-type defaults and CLI stdout serialization.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { CliRespondBody, CliRespondOptions } from "./types.ts";
|
|
6
|
+
|
|
7
|
+
/** Fills default contentType on respond options based on body shape. */
|
|
8
|
+
export function normalizeRespondOptions(opts: CliRespondOptions): CliRespondOptions {
|
|
9
|
+
if (opts.contentType !== undefined) {
|
|
10
|
+
return opts;
|
|
11
|
+
}
|
|
12
|
+
const body = opts.body;
|
|
13
|
+
if (body instanceof Uint8Array) {
|
|
14
|
+
throw new Error("ctx.respond() with Uint8Array body requires an explicit contentType");
|
|
15
|
+
}
|
|
16
|
+
if (typeof body === "string") {
|
|
17
|
+
return { ...opts, contentType: "text/plain; charset=utf-8" };
|
|
18
|
+
}
|
|
19
|
+
return { ...opts, contentType: "application/json; charset=utf-8" };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Writes a respond body to process.stdout for CLI invocations. */
|
|
23
|
+
export function writeRespondBodyToStdout(body: CliRespondBody): void {
|
|
24
|
+
if (body instanceof Uint8Array) {
|
|
25
|
+
process.stdout.write(body);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
if (typeof body === "string") {
|
|
29
|
+
process.stdout.write(body);
|
|
30
|
+
if (!body.endsWith("\n")) {
|
|
31
|
+
process.stdout.write("\n");
|
|
32
|
+
}
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
process.stdout.write(`${JSON.stringify(body, null, 2)}\n`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Encodes binary respond bodies as base64 for MCP structuredContent. */
|
|
39
|
+
export function encodeRespondBodyBase64(body: Uint8Array): string {
|
|
40
|
+
if (typeof Buffer !== "undefined") {
|
|
41
|
+
return Buffer.from(body).toString("base64");
|
|
42
|
+
}
|
|
43
|
+
let binary = "";
|
|
44
|
+
for (const byte of body) {
|
|
45
|
+
binary += String.fromCharCode(byte);
|
|
46
|
+
}
|
|
47
|
+
return btoa(binary);
|
|
48
|
+
}
|
package/src/schema.ts
CHANGED
|
@@ -7,7 +7,7 @@ import { cliResolveNotes } from "./help.ts";
|
|
|
7
7
|
import { visibleOptions } from "./hidden.ts";
|
|
8
8
|
import { type CliNode, type CliProgram, isCliLeaf, isCliRouter, leafOutputSchema } from "./types.ts";
|
|
9
9
|
|
|
10
|
-
const RESERVED = new Set(["completion", "configure", "docs", "mcp", "version"]);
|
|
10
|
+
const RESERVED = new Set(["api", "completion", "configure", "docs", "mcp", "version"]);
|
|
11
11
|
|
|
12
12
|
function exportCommand(cmd: CliNode, root: CliProgram): CliSchemaExport | null {
|
|
13
13
|
if (cmd.hidden) {
|
package/src/skill/generate.ts
CHANGED
|
@@ -218,7 +218,7 @@ function buildPluginSkillMd(root: CliProgram, dirName: string): string {
|
|
|
218
218
|
"",
|
|
219
219
|
`- Server id: \`${serverId}\` (configured in plugin \`.mcp.json\`)`,
|
|
220
220
|
"- Tool names and argument shapes come from MCP `tools/list`",
|
|
221
|
-
`- Full schema: \`${schemaUri}\` (same as \`${root.key} docs schema\`)`,
|
|
221
|
+
`- Full schema: \`${schemaUri}\` (same as \`${root.key} docs cli-schema\`)`,
|
|
222
222
|
"",
|
|
223
223
|
];
|
|
224
224
|
|
package/src/types.ts
CHANGED
|
@@ -9,7 +9,7 @@ import type { CliContext } from "./context.ts";
|
|
|
9
9
|
/**
|
|
10
10
|
* How a leaf handler was dispatched.
|
|
11
11
|
*/
|
|
12
|
-
export type CliInvocation = "cli" | "mcp";
|
|
12
|
+
export type CliInvocation = "cli" | "mcp" | "api";
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
15
|
* Option kinds: presence (boolean flag), string (free-form text), number (strict double), or enum (fixed choices).
|
|
@@ -154,6 +154,42 @@ export interface CliMcpServerConfig {
|
|
|
154
154
|
bundle?: CliMcpBundleConfig;
|
|
155
155
|
}
|
|
156
156
|
|
|
157
|
+
/**
|
|
158
|
+
* Enables `myapp api` and the HTTP tool server (program root only).
|
|
159
|
+
* Must include `enabled: true`; omit `apiServer` entirely to disable HTTP.
|
|
160
|
+
*/
|
|
161
|
+
export interface CliApiServerConfig {
|
|
162
|
+
/** When `true`, enables the `api` built-in and HTTP tool server. */
|
|
163
|
+
enabled: boolean;
|
|
164
|
+
/** Listen host (default: `127.0.0.1`). */
|
|
165
|
+
host?: string;
|
|
166
|
+
/** Listen port (default: `3000`). */
|
|
167
|
+
port?: number;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Declarative HTTP response hints for a leaf (used by OpenAPI and default headers).
|
|
172
|
+
*/
|
|
173
|
+
export interface CliApiResponseConfig {
|
|
174
|
+
/** Default success Content-Type (default: `application/json`). */
|
|
175
|
+
contentType?: string;
|
|
176
|
+
/** Optional Content-Disposition (e.g. `attachment; filename="invoice.pdf"`). */
|
|
177
|
+
contentDisposition?: string;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** Body types accepted by {@link CliContext.respond}. */
|
|
181
|
+
export type CliRespondBody = string | Uint8Array | Record<string, unknown> | unknown[];
|
|
182
|
+
|
|
183
|
+
/** Options for {@link CliContext.respond} and headless invoke results. */
|
|
184
|
+
export interface CliRespondOptions {
|
|
185
|
+
body: CliRespondBody;
|
|
186
|
+
/** Default: `application/json` for objects/arrays, `text/plain` for strings; binary requires explicit type. */
|
|
187
|
+
contentType?: string;
|
|
188
|
+
/** HTTP status (default: 200). */
|
|
189
|
+
status?: number;
|
|
190
|
+
headers?: Record<string, string>;
|
|
191
|
+
}
|
|
192
|
+
|
|
157
193
|
/**
|
|
158
194
|
* A custom MCP resource exposed under resources/list and resources/read.
|
|
159
195
|
*/
|
|
@@ -385,9 +421,13 @@ export type CliLeaf = CliNodeBase & {
|
|
|
385
421
|
positionals?: CliPositional[];
|
|
386
422
|
/**
|
|
387
423
|
* JSON Schema for structured stdout (e.g. with `--json` or MCP when the handler emits JSON).
|
|
388
|
-
* Exported in `docs schema`, `docs api`, and MCP `tools/list`; not validated at runtime yet.
|
|
424
|
+
* Exported in `docs cli-schema`, `docs api`, and MCP `tools/list`; not validated at runtime yet.
|
|
389
425
|
*/
|
|
390
426
|
outputSchema?: Record<string, unknown>;
|
|
427
|
+
/** JSON Schema for MCP/HTTP tool arguments (flat object). */
|
|
428
|
+
inputSchema?: Record<string, unknown>;
|
|
429
|
+
/** Declarative HTTP response metadata (Content-Type, Content-Disposition). */
|
|
430
|
+
apiResponse?: CliApiResponseConfig;
|
|
391
431
|
/** Per-tool MCP exposure and metadata. */
|
|
392
432
|
mcpTool?: CliMcpToolConfig;
|
|
393
433
|
};
|
|
@@ -420,6 +460,8 @@ export type CliProgram = CliNode & {
|
|
|
420
460
|
appConfig?: CliAppConfig;
|
|
421
461
|
/** When set with `enabled: true`, enables the `mcp` built-in subcommand. */
|
|
422
462
|
mcpServer?: CliMcpServerConfig;
|
|
463
|
+
/** When set with `enabled: true`, enables the `api` built-in HTTP server. */
|
|
464
|
+
apiServer?: CliApiServerConfig;
|
|
423
465
|
/** Opt-out and defaults for `configure`. */
|
|
424
466
|
configure?: CliConfigureConfig;
|
|
425
467
|
/** Opt-out for shell completion generation (`completion bash|zsh|fish`). */
|
|
@@ -445,9 +487,9 @@ export function leafOutputSchema(leaf: CliLeaf): Record<string, unknown> | undef
|
|
|
445
487
|
|
|
446
488
|
/**
|
|
447
489
|
* Handler closure type for leaf commands.
|
|
448
|
-
* Supports
|
|
490
|
+
* Supports sync and async handlers; non-undefined return values become implicit JSON responses for headless invocations.
|
|
449
491
|
*/
|
|
450
|
-
export type CliHandler = (ctx: CliContext) =>
|
|
492
|
+
export type CliHandler = (ctx: CliContext) => unknown | Promise<unknown>;
|
|
451
493
|
|
|
452
494
|
/**
|
|
453
495
|
* Error thrown when the static CLI tree violates ArgsBarg rules.
|
package/src/validate.ts
CHANGED
|
@@ -180,6 +180,10 @@ export function cliValidateProgram(program: CliProgram): void {
|
|
|
180
180
|
throw new CliSchemaValidationError("mcpServer requires enabled: true; omit mcpServer to disable MCP");
|
|
181
181
|
}
|
|
182
182
|
|
|
183
|
+
if (program.apiServer !== undefined && program.apiServer.enabled !== true) {
|
|
184
|
+
throw new CliSchemaValidationError("apiServer requires enabled: true; omit apiServer to disable HTTP API");
|
|
185
|
+
}
|
|
186
|
+
|
|
183
187
|
if (program.docs !== undefined && program.docs.enabled !== true) {
|
|
184
188
|
throw new CliSchemaValidationError("docs requires enabled: true; omit docs to disable bundled documentation");
|
|
185
189
|
}
|
|
@@ -216,6 +220,9 @@ function walkNode(node: CliNode, program: CliProgram, isRoot: boolean): void {
|
|
|
216
220
|
if (rogue.mcpServer !== undefined) {
|
|
217
221
|
throw new CliSchemaValidationError(`mcpServer is only supported on the program root (not on ${node.key})`);
|
|
218
222
|
}
|
|
223
|
+
if (rogue.apiServer !== undefined) {
|
|
224
|
+
throw new CliSchemaValidationError(`apiServer is only supported on the program root (not on ${node.key})`);
|
|
225
|
+
}
|
|
219
226
|
if (rogue.configure !== undefined) {
|
|
220
227
|
throw new CliSchemaValidationError(`configure is only supported on the program root (not on ${node.key})`);
|
|
221
228
|
}
|