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.
Files changed (73) hide show
  1. package/CHANGELOG.md +46 -1
  2. package/README.md +33 -25
  3. package/docs/README.md +2 -1
  4. package/docs/api-server.md +141 -0
  5. package/docs/bundled-docs.md +24 -10
  6. package/docs/cli-program.md +17 -2
  7. package/docs/config-schema.md +18 -8
  8. package/docs/developing.md +1 -1
  9. package/docs/mcp.md +5 -5
  10. package/docs/output-schema.md +78 -47
  11. package/examples/full-example/README.md +15 -6
  12. package/examples/full-example/docs/README.md +27 -0
  13. package/examples/full-example/docs/api.md +511 -0
  14. package/examples/full-example/docs/cli-schema.json +453 -0
  15. package/examples/full-example/docs/http.md +81 -0
  16. package/examples/full-example/docs/mcp.md +159 -0
  17. package/examples/full-example/docs/openapi.json +222 -0
  18. package/examples/full-example/docs/skill.md +46 -0
  19. package/examples/full-example/justfile +6 -2
  20. package/examples/full-example/schemas/generated/app-config.json +1 -1
  21. package/examples/full-example/schemas/generated/status.json +1 -1
  22. package/examples/full-example/scripts/dev-formula.ts +1 -1
  23. package/examples/full-example/scripts/schemagen/discover-schema-roots.test.ts +3 -3
  24. package/examples/full-example/scripts/schemagen/discover-schema-roots.ts +90 -35
  25. package/examples/full-example/scripts/schemagen/naming.ts +17 -0
  26. package/examples/full-example/scripts/schemagen.ts +14 -3
  27. package/examples/full-example/src/commands/echo/command.ts +6 -1
  28. package/examples/full-example/src/commands/status/schema-types.ts +14 -0
  29. package/examples/full-example/src/commands/status/types.ts +1 -11
  30. package/examples/full-example/src/{types.ts → config/schema-types.ts} +3 -2
  31. package/examples/full-example/src/program.ts +3 -0
  32. package/examples/mcp-test.ts +13 -2
  33. package/examples/nested.ts +12 -3
  34. package/examples/servers.ts +72 -0
  35. package/index.d.ts +66 -7
  36. package/package.json +17 -17
  37. package/src/api/openapi.ts +117 -0
  38. package/src/api/result.ts +111 -0
  39. package/src/api/schema-deref.test.ts +99 -0
  40. package/src/api/schema-deref.ts +76 -0
  41. package/src/api/server.ts +120 -0
  42. package/src/api.integration.test.ts +441 -0
  43. package/src/builtins/api.ts +38 -0
  44. package/src/builtins/dispatch.ts +26 -0
  45. package/src/builtins/registry.ts +4 -0
  46. package/src/capabilities.ts +12 -1
  47. package/src/cli-errors.ts +3 -0
  48. package/src/cli-tool/full-example-capabilities.test.ts +3 -0
  49. package/src/cli.ts +60 -8
  50. package/src/config.integration.test.ts +22 -4
  51. package/src/context.ts +29 -1
  52. package/src/docs/api-guide.ts +2 -2
  53. package/src/docs/builtin.ts +11 -1
  54. package/src/docs/docs.test.ts +70 -12
  55. package/src/docs/http-guide.ts +132 -0
  56. package/src/docs/mcp-guide.ts +3 -3
  57. package/src/docs/mcp-resources.ts +5 -2
  58. package/src/docs/resolve.ts +26 -2
  59. package/src/docs/save.ts +5 -2
  60. package/src/headless/tool-call.ts +157 -0
  61. package/src/headless.test.ts +4 -2
  62. package/src/headless.ts +10 -5
  63. package/src/index.ts +5 -0
  64. package/src/mcp/result.ts +39 -34
  65. package/src/mcp/server.ts +18 -36
  66. package/src/mcp/tools.ts +14 -3
  67. package/src/mcp.integration.test.ts +46 -39
  68. package/src/parse.test.ts +16 -6
  69. package/src/respond.ts +48 -0
  70. package/src/schema.ts +1 -1
  71. package/src/skill/generate.ts +1 -1
  72. package/src/types.ts +46 -4
  73. package/src/validate.ts +7 -0
@@ -0,0 +1,132 @@
1
+ import { resolveApiListenAddress } from "../api/server.ts";
2
+ import { defaultConfigEntryTitle } from "../config/entry.ts";
3
+ import { displayAppConfigPath } from "../config/file.ts";
4
+ import { collectMcpTools, type McpToolDef } from "../mcp/tools.ts";
5
+ import { collectOptionDefs } from "../parse.ts";
6
+ import { CliOptionKind, type CliProgram } from "../types.ts";
7
+
8
+ /** Formats one exposed tool for the auto-generated HTTP guide. */
9
+ function formatToolLine(root: CliProgram, tool: McpToolDef): string {
10
+ const cliPath = tool.path.length > 0 ? `${root.key} ${tool.path.join(" ")}` : root.key;
11
+ let line = `- \`${tool.apiName}\` (MCP: \`${tool.name}\`, CLI: \`${cliPath}\`) — ${tool.description}`;
12
+ const opts = collectOptionDefs(root, tool.path);
13
+ const flags = opts.filter((o) => o.kind === CliOptionKind.Presence).map((o) => `--${o.name}`);
14
+ if (flags.length > 0) {
15
+ line += ` (flags: ${flags.join(", ")})`;
16
+ }
17
+ return line;
18
+ }
19
+
20
+ /** Generates the auto `docs http` markdown guide from schema and API config. */
21
+ export function generateHttpGuide(root: CliProgram): string {
22
+ const api = root.apiServer;
23
+ if (!api) {
24
+ throw new Error("HTTP API server not enabled");
25
+ }
26
+
27
+ const tools = collectMcpTools(root);
28
+ const { hostname, port } = resolveApiListenAddress(root);
29
+ const baseUrl = `http://${hostname}:${port}`;
30
+
31
+ const lines: string[] = [
32
+ `# HTTP API (${root.key})`,
33
+ "",
34
+ `${root.key} exposes the same callable tools over HTTP as MCP.`,
35
+ "",
36
+ "## Running",
37
+ "",
38
+ "```bash",
39
+ `${root.key} api`,
40
+ "```",
41
+ "",
42
+ `Listens on **${baseUrl}** by default (\`apiServer.host\` / \`apiServer.port\`).`,
43
+ "",
44
+ "Bind is localhost-only in v0 — use a reverse proxy for remote access.",
45
+ "",
46
+ "## Endpoints",
47
+ "",
48
+ "| Method | Path | Purpose |",
49
+ "| --- | --- | --- |",
50
+ "| `GET` | `/health` | Liveness check |",
51
+ "| `GET` | `/openapi.json` | OpenAPI 3.1 document (tool paths and request shapes) |",
52
+ "| `GET` | `/openapi-browser` | Interactive Scalar API reference |",
53
+ "| `POST` | `/tools/:name` | Invoke with flat JSON args object in the body |",
54
+ "| `OPTIONS` | `*` | CORS preflight |",
55
+ "",
56
+ "Replace `{tool-key}` below with a path segment from `openapi.json` (`paths` keys are `/tools/{tool-key}`). Match body keys to that tool's `requestBody` schema in the spec.",
57
+ "",
58
+ "## Examples",
59
+ "",
60
+ "```bash",
61
+ `curl -s ${baseUrl}/health`,
62
+ `curl -s ${baseUrl}/openapi.json`,
63
+ `curl -s -X POST ${baseUrl}/tools/{tool-key} \\`,
64
+ ' -H "content-type: application/json" \\',
65
+ " -d '{...}'",
66
+ "```",
67
+ "",
68
+ "## Responses",
69
+ "",
70
+ "Success (`200`): raw response body (JSON object, string, or binary). No `{ ok, stdout }` envelope.",
71
+ "",
72
+ "Handlers must use `ctx.respond()` or return a value for API/MCP tool calls.",
73
+ "",
74
+ 'Errors use `{ "error": "..." }` with `400`, `404`, `503`, or `500`.',
75
+ "",
76
+ ];
77
+
78
+ if (root.appConfig?.entries && Object.keys(root.appConfig.entries).length > 0) {
79
+ lines.push("## Configuration", "");
80
+ lines.push(
81
+ `Configure before first use: \`${root.key} configure\`.`,
82
+ "",
83
+ `Default config file: \`${displayAppConfigPath(root)}\`.`,
84
+ "",
85
+ );
86
+ for (const [key, entry] of Object.entries(root.appConfig.entries)) {
87
+ const label = entry.title ?? defaultConfigEntryTitle(key);
88
+ const req = entry.required === false ? "optional" : "required";
89
+ const envNote = entry.env ? ` → env \`${entry.env}\`` : "";
90
+ lines.push(`- **${label}** (\`${key}\`, ${req}${envNote}) — ${entry.description}`);
91
+ }
92
+ lines.push("");
93
+ }
94
+
95
+ lines.push("## Exposed tools", "");
96
+
97
+ if (tools.length === 0) {
98
+ lines.push("(No tools exposed.)", "");
99
+ } else {
100
+ for (const tool of tools) {
101
+ lines.push(formatToolLine(root, tool));
102
+ }
103
+ lines.push("");
104
+ }
105
+
106
+ lines.push(
107
+ "## Tool arguments",
108
+ "",
109
+ "POST bodies are a flat JSON object keyed by long option and positional names (hyphenated option names are valid keys).",
110
+ "",
111
+ `For HTTP clients, use **\`GET /openapi.json\`** (or **\`GET /openapi-browser\`**) for per-tool request shapes — each \`POST /tools/{name}\` path has a \`requestBody\` schema.`,
112
+ "",
113
+ "Varargs positionals accept a JSON array of strings (not a comma-separated string).",
114
+ "Options with `format: comma-list` accept a comma-separated string or JSON array.",
115
+ "Options with a schema `default` are applied when omitted.",
116
+ "",
117
+ `Shell invocation reference: \`${root.key} docs api\`. Full CLI tree JSON: \`${root.key} docs cli-schema\`.`,
118
+ "",
119
+ "## OpenAPI",
120
+ "",
121
+ "The HTTP API is described in OpenAPI 3.1.",
122
+ "",
123
+ `- **Browse** — [${baseUrl}/openapi-browser](${baseUrl}/openapi-browser) (Scalar UI; loads \`/openapi.json\`)`,
124
+ `- **Fetch** — \`curl -s ${baseUrl}/openapi.json\``,
125
+ `- **Save offline** — \`${root.key} docs openapi --save\` → \`./docs/openapi.json\` (or \`just docgen\` in app repos)`,
126
+ "",
127
+ "Use the spec to discover tool names (`paths`) and request/response shapes before calling `POST /tools/:name`.",
128
+ "",
129
+ );
130
+
131
+ return lines.join("\n");
132
+ }
@@ -202,7 +202,7 @@ export function generateMcpGuide(root: CliProgram): string {
202
202
  "|-----------|---------|",
203
203
  "| `tools/list` | Callable tools for exposed leaf commands |",
204
204
  "| `tools/call` | Runs handlers headlessly; JSON stdout becomes `structuredContent` when valid |",
205
- `| Schema resource | \`${schemaUri}\` — same JSON as \`${root.key} docs schema\` |`,
205
+ `| Schema resource | \`${schemaUri}\` — same JSON as \`${root.key} docs cli-schema\` |`,
206
206
  );
207
207
  if (docsEnabled(root)) {
208
208
  const docs = root.docs;
@@ -228,7 +228,7 @@ export function generateMcpGuide(root: CliProgram): string {
228
228
  "## Tool arguments",
229
229
  "",
230
230
  "Arguments are a flat JSON object keyed by long option and positional names (hyphenated option names are valid keys).",
231
- `See \`${root.key} docs schema\` or the schema resource for per-tool shapes.`,
231
+ `See \`${root.key} docs cli-schema\` or the schema resource for per-tool shapes.`,
232
232
  "",
233
233
  "Varargs positionals accept a JSON array of strings (not a comma-separated string).",
234
234
  "Options with `format: comma-list` accept a comma-separated string or JSON array.",
@@ -236,7 +236,7 @@ export function generateMcpGuide(root: CliProgram): string {
236
236
  "",
237
237
  "## Protocol",
238
238
  "",
239
- "Stdio NDJSON JSON-RPC. Help and `docs schema` are not available through tool calls.",
239
+ "Stdio NDJSON JSON-RPC. Help and `docs cli-schema` are not available through tool calls.",
240
240
  `Run \`${root.key} docs\` for bundled user documentation.`,
241
241
  "",
242
242
  );
@@ -3,7 +3,7 @@ Auto MCP resources for user docs.topics when docs and MCP are both enabled.
3
3
  */
4
4
 
5
5
  import type { CliProgram } from "../types.ts";
6
- import { docsEnabled, docsTopicContent, docsTopicDescription, docsUserTopicKeys } from "./resolve.ts";
6
+ import { docsEnabled, docsTopicDescription, docsTopicText, docsUserTopicKeys } from "./resolve.ts";
7
7
 
8
8
  /** Default URI pattern for a docs topic MCP resource (`<mcpId>://docs/<topicKey>`). */
9
9
  export function defaultDocsTopicResourceUri(mcpId: string, topicKey: string): string {
@@ -45,7 +45,10 @@ export function docsMcpResources(program: CliProgram): {
45
45
  name: key,
46
46
  description: docsTopicDescription(key, topic.description),
47
47
  mimeType: "text/markdown",
48
- load: () => docsTopicContent(program, key),
48
+ load: () => {
49
+ const text = docsTopicText(program, key);
50
+ return text.endsWith("\n") ? text : `${text}\n`;
51
+ },
49
52
  };
50
53
  });
51
54
  }
@@ -1,11 +1,13 @@
1
+ import { openApiJson } from "../api/openapi.ts";
1
2
  import { cliSchemaJson } from "../schema.ts";
2
3
  import { generateSkillBundle } from "../skill/generate.ts";
3
4
  import type { CliDocsConfig, CliProgram } from "../types.ts";
4
5
  import { generateApiGuide } from "./api-guide.ts";
6
+ import { generateHttpGuide } from "./http-guide.ts";
5
7
  import { generateMcpGuide } from "./mcp-guide.ts";
6
8
 
7
9
  /** Built-in docs subcommand keys not allowed in `docs.topics`. */
8
- export const DOCS_BUILTIN_TOPIC_KEYS = ["mcp", "all", "schema", "api", "skill"] as const;
10
+ export const DOCS_BUILTIN_TOPIC_KEYS = ["http", "mcp", "all", "cli-schema", "api", "skill", "openapi"] as const;
9
11
 
10
12
  export type DocsBuiltinTopicKey = (typeof DOCS_BUILTIN_TOPIC_KEYS)[number];
11
13
 
@@ -43,6 +45,16 @@ export function docsIncludesMcpTopic(program: CliProgram): boolean {
43
45
  return docsEnabled(program) && program.mcpServer?.enabled === true;
44
46
  }
45
47
 
48
+ /** Whether HTTP auto-guide topic is included. */
49
+ export function docsIncludesHttpTopic(program: CliProgram): boolean {
50
+ return docsEnabled(program) && program.apiServer?.enabled === true;
51
+ }
52
+
53
+ /** Whether OpenAPI export topic is included. */
54
+ export function docsIncludesOpenApiTopic(program: CliProgram): boolean {
55
+ return docsIncludesHttpTopic(program);
56
+ }
57
+
46
58
  /** Leaf help description for a user topic. */
47
59
  export function docsTopicDescription(key: string, custom?: string): string {
48
60
  if (custom) {
@@ -67,6 +79,12 @@ export function docsTopicText(program: CliProgram, topic: string): string {
67
79
  }
68
80
  return generateMcpGuide(program);
69
81
  }
82
+ if (topic === "http") {
83
+ if (!docsIncludesHttpTopic(program)) {
84
+ throw new Error("Unknown docs topic 'http'.");
85
+ }
86
+ return generateHttpGuide(program);
87
+ }
70
88
  const entry = docs.topics[topic];
71
89
  if (!entry) {
72
90
  throw new Error(`Unknown docs topic '${topic}'.`);
@@ -76,9 +94,15 @@ export function docsTopicText(program: CliProgram, topic: string): string {
76
94
 
77
95
  /** Full file body for a docs topic (stdout or `--save`). */
78
96
  export function docsTopicContent(program: CliProgram, topic: string): string {
79
- if (topic === "schema") {
97
+ if (topic === "cli-schema") {
80
98
  return cliSchemaJson(program);
81
99
  }
100
+ if (topic === "openapi") {
101
+ if (!docsIncludesOpenApiTopic(program)) {
102
+ throw new Error("Unknown docs topic 'openapi'.");
103
+ }
104
+ return openApiJson(program);
105
+ }
82
106
  if (topic === "api") {
83
107
  return generateApiGuide(program);
84
108
  }
package/src/docs/save.ts CHANGED
@@ -36,8 +36,11 @@ export function docsTopicContentForSave(program: CliProgram, topic: string): str
36
36
 
37
37
  /** Filename for a saved docs topic. */
38
38
  export function docsSaveFilename(topic: string): string {
39
- if (topic === "schema") {
40
- return "schema.json";
39
+ if (topic === "cli-schema") {
40
+ return "cli-schema.json";
41
+ }
42
+ if (topic === "openapi") {
43
+ return "openapi.json";
41
44
  }
42
45
  return `${topic}.md`;
43
46
  }
@@ -0,0 +1,157 @@
1
+ /*
2
+ Shared headless tool dispatch for MCP and HTTP: config bootstrap, argv conversion, and invoke.
3
+ */
4
+
5
+ import { apiErrorResponse, apiSuccessResponse, firstErrorLine } from "../api/result.ts";
6
+ import type { Cli, CliInvokeResult } from "../cli.ts";
7
+ import { bootstrapAppConfig } from "../config/bootstrap.ts";
8
+ import { formatMcpMissingConfigMessage, missingRequiredConfig } from "../config/resolve.ts";
9
+ import { buildToolCallSuccessFromResponse } from "../mcp/result.ts";
10
+ import { collectMcpTools, type McpToolDef, mcpToolCallToArgv } from "../mcp/tools.ts";
11
+ import type { CliInvocation, CliProgram } from "../types.ts";
12
+
13
+ /** Outcome of resolving a tool name against the program schema. */
14
+ export type ToolLookupResult =
15
+ | { ok: true; tool: McpToolDef }
16
+ | { ok: false; kind: "unknown"; message: string }
17
+ | { ok: false; kind: "missing_config"; message: string };
18
+
19
+ /** Successful headless tool invocation payload shared by MCP and HTTP. */
20
+ export interface HeadlessToolCallSuccess {
21
+ ok: true;
22
+ response: NonNullable<CliInvokeResult["response"]>;
23
+ mcpResult: ReturnType<typeof buildToolCallSuccessFromResponse>;
24
+ }
25
+
26
+ /** Failed headless tool invocation payload shared by MCP and HTTP. */
27
+ export interface HeadlessToolCallFailure {
28
+ ok: false;
29
+ kind: "argv" | "invoke" | "help";
30
+ message: string;
31
+ exitCode: number;
32
+ stdout: string;
33
+ stderr: string;
34
+ invokeResult?: CliInvokeResult;
35
+ }
36
+
37
+ export type HeadlessToolCallResult = HeadlessToolCallSuccess | HeadlessToolCallFailure;
38
+
39
+ /** Finds an exposed tool by MCP or HTTP API tool name. */
40
+ export function lookupHeadlessTool(
41
+ program: CliProgram,
42
+ toolName: string,
43
+ invocation: CliInvocation = "mcp",
44
+ ): ToolLookupResult {
45
+ const tools = collectMcpTools(program);
46
+ const tool =
47
+ invocation === "api" ? tools.find((t) => t.apiName === toolName) : tools.find((t) => t.name === toolName);
48
+ if (!tool) {
49
+ return { ok: false, kind: "unknown", message: `Unknown tool: ${toolName}` };
50
+ }
51
+ const { resolved } = bootstrapAppConfig(program, { validateFile: false });
52
+ const missingConfig = missingRequiredConfig(program, resolved);
53
+ if (missingConfig.length > 0) {
54
+ return {
55
+ ok: false,
56
+ kind: "missing_config",
57
+ message: formatMcpMissingConfigMessage(program, missingConfig),
58
+ };
59
+ }
60
+ return { ok: true, tool };
61
+ }
62
+
63
+ /**
64
+ * Converts flat tool arguments to argv and invokes the leaf handler headlessly.
65
+ */
66
+ export async function executeHeadlessToolCall(
67
+ cli: Cli,
68
+ tool: McpToolDef,
69
+ args: Record<string, unknown>,
70
+ invocation: CliInvocation,
71
+ ): Promise<HeadlessToolCallResult> {
72
+ const argvResult = mcpToolCallToArgv(cli.program, tool, args);
73
+ if ("error" in argvResult) {
74
+ return {
75
+ ok: false,
76
+ kind: "argv",
77
+ message: argvResult.error,
78
+ exitCode: 1,
79
+ stdout: "",
80
+ stderr: "",
81
+ };
82
+ }
83
+
84
+ const invokeResult = await cli.invoke(argvResult, { invocation, toolArgs: args });
85
+ if (invokeResult.kind === "help") {
86
+ return {
87
+ ok: false,
88
+ kind: "help",
89
+ message: invokeResult.errorMsg ?? "Help is not available via tool calls.",
90
+ exitCode: invokeResult.exitCode,
91
+ stdout: invokeResult.stdout,
92
+ stderr: invokeResult.stderr,
93
+ invokeResult,
94
+ };
95
+ }
96
+
97
+ if (invokeResult.kind === "ok" && invokeResult.exitCode === 0 && invokeResult.response) {
98
+ const mcpResult = buildToolCallSuccessFromResponse(invokeResult.response);
99
+ return {
100
+ ok: true,
101
+ response: invokeResult.response,
102
+ mcpResult,
103
+ };
104
+ }
105
+
106
+ if (invokeResult.kind === "ok" && invokeResult.exitCode === 0) {
107
+ return {
108
+ ok: false,
109
+ kind: "invoke",
110
+ message: "Handler did not call ctx.respond() or return a value",
111
+ exitCode: 1,
112
+ stdout: invokeResult.stdout,
113
+ stderr: invokeResult.stderr,
114
+ invokeResult,
115
+ };
116
+ }
117
+
118
+ const message = invokeResult.errorMsg ?? (invokeResult.stderr.trim() || `Exit code ${invokeResult.exitCode}`);
119
+ return {
120
+ ok: false,
121
+ kind: "invoke",
122
+ message,
123
+ exitCode: invokeResult.exitCode,
124
+ stdout: invokeResult.stdout,
125
+ stderr: invokeResult.stderr,
126
+ invokeResult,
127
+ };
128
+ }
129
+
130
+ /** Maps a headless success result to an HTTP Response. */
131
+ export function headlessSuccessToHttpResponse(
132
+ result: HeadlessToolCallSuccess,
133
+ leafApiResponse?: import("../types.ts").CliApiResponseConfig,
134
+ ): Response {
135
+ return apiSuccessResponse(result.response, leafApiResponse);
136
+ }
137
+
138
+ /** Maps a headless failure result to a JSON HTTP error Response. */
139
+ export function headlessFailureToHttpResponse(result: HeadlessToolCallFailure): Response {
140
+ const status = resolveHttpErrorStatus(result);
141
+ return apiErrorResponse(status, {
142
+ error: firstErrorLine(result.message),
143
+ });
144
+ }
145
+
146
+ function resolveHttpErrorStatus(result: HeadlessToolCallFailure): number {
147
+ if (result.kind === "argv" || result.kind === "help") {
148
+ return 400;
149
+ }
150
+ if (result.kind === "invoke" && result.message.includes("ctx.respond()")) {
151
+ return 500;
152
+ }
153
+ if (result.exitCode === 1) {
154
+ return 400;
155
+ }
156
+ return 500;
157
+ }
@@ -12,14 +12,16 @@ import {
12
12
  wantsExplicitJson,
13
13
  } from "./headless.ts";
14
14
 
15
- test("wantsExplicitJson includes MCP invocation", () => {
15
+ test("wantsExplicitJson includes MCP and API invocation", () => {
16
16
  expect(wantsExplicitJson({ invocation: "cli" }, false)).toBe(false);
17
17
  expect(wantsExplicitJson({ invocation: "mcp" }, false)).toBe(true);
18
+ expect(wantsExplicitJson({ invocation: "api" }, false)).toBe(true);
18
19
  expect(wantsExplicitJson({ invocation: "cli" }, true)).toBe(true);
19
20
  });
20
21
 
21
- test("shouldRunHeadless is true for MCP and json", () => {
22
+ test("shouldRunHeadless is true for MCP, API, and json", () => {
22
23
  expect(shouldRunHeadless({ invocation: "mcp" }, false)).toBe(true);
24
+ expect(shouldRunHeadless({ invocation: "api" }, false)).toBe(true);
23
25
  expect(shouldRunHeadless({ invocation: "cli" }, true)).toBe(true);
24
26
  expect(shouldRunHeadless({ invocation: "cli" }, false, true)).toBe(true);
25
27
  expect(shouldRunHeadless({ invocation: "cli" }, false, false, false)).toBe(true);
package/src/headless.ts CHANGED
@@ -4,9 +4,14 @@ import { isInteractiveTty } from "./utils.ts";
4
4
  /** Minimal context for headless routing helpers. */
5
5
  export type HeadlessContext = Pick<CliContext, "invocation">;
6
6
 
7
- /** True when `--json` was passed or the handler was invoked via MCP. */
7
+ /** True when the handler was invoked via MCP or HTTP API. */
8
+ function isToolInvocation(invocation: CliContext["invocation"]): boolean {
9
+ return invocation === "mcp" || invocation === "api";
10
+ }
11
+
12
+ /** True when `--json` was passed or the handler was invoked headlessly over MCP/HTTP. */
8
13
  export function wantsExplicitJson(ctx: HeadlessContext, hasJsonFlag: boolean): boolean {
9
- return hasJsonFlag || ctx.invocation === "mcp";
14
+ return hasJsonFlag || isToolInvocation(ctx.invocation);
10
15
  }
11
16
 
12
17
  /**
@@ -19,7 +24,7 @@ export function shouldRunHeadless(
19
24
  hasDryRunFlag = false,
20
25
  interactive: boolean = isInteractiveTty,
21
26
  ): boolean {
22
- if (ctx.invocation === "mcp") return true;
27
+ if (isToolInvocation(ctx.invocation)) return true;
23
28
  if (hasJsonFlag || hasDryRunFlag) return true;
24
29
  return !interactive;
25
30
  }
@@ -35,7 +40,7 @@ export function shouldRunHeadlessWithPositionals(
35
40
  hasDryRunFlag = false,
36
41
  interactive: boolean = isInteractiveTty,
37
42
  ): boolean {
38
- if (ctx.invocation === "mcp") return true;
43
+ if (isToolInvocation(ctx.invocation)) return true;
39
44
  if (hasJsonFlag || hasDryRunFlag) return true;
40
45
  return !interactive && positionals.length > 0;
41
46
  }
@@ -49,7 +54,7 @@ export function shouldRunHeadlessWithYes(
49
54
  opts: { yes: boolean; hasRequiredArgs: boolean; dryRun?: boolean },
50
55
  interactive: boolean = isInteractiveTty,
51
56
  ): boolean {
52
- if (ctx.invocation === "mcp") {
57
+ if (isToolInvocation(ctx.invocation)) {
53
58
  return opts.hasRequiredArgs && (opts.yes || Boolean(opts.dryRun));
54
59
  }
55
60
  if (opts.dryRun && opts.hasRequiredArgs) return true;
package/src/index.ts CHANGED
@@ -7,6 +7,7 @@ It gives consumers one stable import path without forcing them to know the inter
7
7
  module layout.
8
8
  */
9
9
 
10
+ export { generateOpenApi, openApiJson } from "./api/openapi.ts";
10
11
  export { Cli, type CliInvokeKind, type CliInvokeResult } from "./cli.ts";
11
12
  export { cliErrWithHelp } from "./cli-errors.ts";
12
13
  export { displayAppConfigPath, resolveAppConfigPath } from "./config/file.ts";
@@ -30,6 +31,8 @@ export {
30
31
  export type { McpBundlePaths, PackMcpBundleOpts } from "./mcp/bundle.ts";
31
32
  export { defaultMcpBundlePaths, generateMcpManifest, packMcpBundle } from "./mcp/bundle.ts";
32
33
  export type {
34
+ CliApiResponseConfig,
35
+ CliApiServerConfig,
33
36
  CliAppConfig,
34
37
  CliAppConfigEntry,
35
38
  CliAppConfigResolveContext,
@@ -47,6 +50,8 @@ export type {
47
50
  CliOption,
48
51
  CliPositional,
49
52
  CliProgram,
53
+ CliRespondBody,
54
+ CliRespondOptions,
50
55
  InstallAgentIntegration,
51
56
  InstallTargetSpec,
52
57
  ResolvedInstallTarget,
package/src/mcp/result.ts CHANGED
@@ -1,7 +1,10 @@
1
1
  /*
2
- This module builds MCP tools/call success results from captured handler output.
2
+ This module builds MCP tools/call success results from handler respond payloads.
3
3
  */
4
4
 
5
+ import { encodeRespondBodyBase64 } from "../respond.ts";
6
+ import type { CliRespondOptions } from "../types.ts";
7
+
5
8
  /** Text content block in an MCP tool result. */
6
9
  export interface McpTextContent {
7
10
  type: "text";
@@ -15,43 +18,45 @@ export interface McpToolCallSuccess {
15
18
  isError: false;
16
19
  }
17
20
 
18
- /** Parses stdout as JSON when the full trimmed string is valid JSON. */
19
- function parseStructuredStdout(stdout: string): unknown | undefined {
20
- const trimmed = stdout.trim();
21
- if (trimmed.length === 0) {
22
- return undefined;
23
- }
24
- try {
25
- return JSON.parse(trimmed) as unknown;
26
- } catch {
27
- return undefined;
28
- }
21
+ /** Canonical MCP structuredContent for string respond bodies. */
22
+ export interface McpStringRespondContent {
23
+ content: string;
24
+ contentType: string;
25
+ }
26
+
27
+ /** Canonical MCP structuredContent for binary respond bodies. */
28
+ export interface McpBinaryRespondContent {
29
+ data: string;
30
+ contentType: string;
31
+ encoding: "base64";
29
32
  }
30
33
 
31
34
  /**
32
- * Builds a successful tools/call result from captured handler stdout/stderr.
33
- * stderr is a second content block when non-empty; structuredContent is set when stdout is JSON.
35
+ * Builds a successful tools/call result from a headless respond payload.
36
+ * Binary bodies are encoded as base64 in structuredContent.
34
37
  */
35
- export function buildToolCallSuccess(stdout: string, stderr: string): McpToolCallSuccess {
36
- const content: McpTextContent[] = [];
37
- if (stdout.length > 0) {
38
- content.push({ type: "text", text: stdout });
39
- }
40
- const errText = stderr.trim();
41
- if (errText.length > 0) {
42
- if (content.length === 0) {
43
- content.push({ type: "text", text: "" });
44
- }
45
- content.push({ type: "text", text: errText });
46
- }
47
- if (content.length === 0) {
48
- content.push({ type: "text", text: "" });
49
- }
38
+ export function buildToolCallSuccessFromResponse(response: CliRespondOptions): McpToolCallSuccess {
39
+ const { body, contentType = "application/json; charset=utf-8" } = response;
40
+ let structuredContent: unknown;
50
41
 
51
- const structuredContent = parseStructuredStdout(stdout);
52
- const result: McpToolCallSuccess = { content, isError: false };
53
- if (structuredContent !== undefined) {
54
- result.structuredContent = structuredContent;
42
+ if (body instanceof Uint8Array) {
43
+ structuredContent = {
44
+ data: encodeRespondBodyBase64(body),
45
+ contentType,
46
+ encoding: "base64",
47
+ } satisfies McpBinaryRespondContent;
48
+ } else if (typeof body === "string") {
49
+ structuredContent = {
50
+ content: body,
51
+ contentType,
52
+ } satisfies McpStringRespondContent;
53
+ } else {
54
+ structuredContent = body;
55
55
  }
56
- return result;
56
+
57
+ return {
58
+ content: [{ type: "text", text: "" }],
59
+ structuredContent,
60
+ isError: false,
61
+ };
57
62
  }