argsbarg 5.1.16 → 6.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +32 -1
- package/README.md +32 -24
- 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/developing.md +1 -1
- package/docs/mcp.md +5 -5
- package/docs/output-schema.md +3 -3
- package/examples/full-example/README.md +8 -0
- 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/scripts/dev-formula.ts +1 -1
- package/examples/full-example/src/commands/echo/command.ts +6 -1
- 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 +1 -1
- package/src/api/openapi.ts +115 -0
- package/src/api/result.ts +89 -0
- package/src/api/server.ts +120 -0
- package/src/api.integration.test.ts +358 -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-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/resolve.ts +26 -2
- package/src/docs/save.ts +5 -2
- package/src/headless/tool-call.ts +147 -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
|
@@ -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
|
+
}
|
package/src/docs/mcp-guide.ts
CHANGED
|
@@ -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
|
);
|
package/src/docs/resolve.ts
CHANGED
|
@@ -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,147 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Shared headless tool dispatch for MCP and HTTP: config bootstrap, argv conversion, and invoke.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { apiErrorResponse, apiSuccessResponse } 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 = result.kind === "argv" || result.kind === "help" ? 400 : 500;
|
|
141
|
+
return apiErrorResponse(status, {
|
|
142
|
+
error: result.message,
|
|
143
|
+
exitCode: result.exitCode,
|
|
144
|
+
stdout: result.stdout,
|
|
145
|
+
stderr: result.stderr,
|
|
146
|
+
});
|
|
147
|
+
}
|
package/src/headless.test.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
/**
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
|
33
|
-
*
|
|
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
|
|
36
|
-
const
|
|
37
|
-
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
content: [{ type: "text", text: "" }],
|
|
59
|
+
structuredContent,
|
|
60
|
+
isError: false,
|
|
61
|
+
};
|
|
57
62
|
}
|
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
|
});
|