argsbarg 1.3.1 → 1.4.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/.cursor/plans/mcp_v1.1_polish_e9656029.plan.md +260 -0
- package/.cursor/plans/mcp_v1.2_invocation_and_extensions_a4f82c1e.plan.md +647 -0
- package/CHANGELOG.md +30 -1
- package/README.md +21 -5
- package/docs/mcp.md +300 -0
- package/examples/mcp-test.ts +66 -0
- package/examples/nested.ts +1 -0
- package/index.d.ts +120 -23
- package/package.json +1 -1
- package/src/completion.ts +76 -4
- package/src/context.ts +4 -1
- package/src/help.ts +12 -2
- package/src/index.test.ts +692 -1
- package/src/index.ts +12 -1
- package/src/invoke.ts +192 -0
- package/src/mcp/env.ts +99 -0
- package/src/mcp/result.ts +57 -0
- package/src/mcp/server.ts +238 -0
- package/src/mcp/tools.ts +254 -0
- package/src/mcp.ts +28 -0
- package/src/parse.ts +17 -2
- package/src/runtime.ts +26 -2
- package/src/types.ts +81 -1
- package/src/validate.ts +54 -1
package/src/mcp/tools.ts
ADDED
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
/*
|
|
2
|
+
This module maps CliCommand leaf nodes to MCP tool definitions and converts
|
|
3
|
+
flat JSON tool arguments into argv for cliInvoke.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { readFileSync } from "node:fs";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
import { collectOptionDefs } from "../parse.ts";
|
|
9
|
+
import { cliSchemaJson } from "../schema.ts";
|
|
10
|
+
import { CliCommand, CliOption, CliOptionKind, CliPositional } from "../types.ts";
|
|
11
|
+
|
|
12
|
+
/** Default URI for the CLI schema MCP resource. */
|
|
13
|
+
export const MCP_SCHEMA_URI_DEFAULT = "argsbarg://schema";
|
|
14
|
+
|
|
15
|
+
/** One MCP tool derived from a leaf CLI command. */
|
|
16
|
+
export interface McpToolDef {
|
|
17
|
+
/** MCP tool name (underscore-separated path). */
|
|
18
|
+
name: string;
|
|
19
|
+
/** Tool description from the leaf command. */
|
|
20
|
+
description: string;
|
|
21
|
+
/** Command path segments from the program root. */
|
|
22
|
+
path: string[];
|
|
23
|
+
/** Leaf command node. */
|
|
24
|
+
leaf: CliCommand;
|
|
25
|
+
/** JSON Schema for tools/call arguments. */
|
|
26
|
+
inputSchema: Record<string, unknown>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Builds MCP tool description: "{cli path} — {description}". */
|
|
30
|
+
export function mcpToolDescription(path: string[], rootKey: string, description: string): string {
|
|
31
|
+
const prefix = path.length > 0 ? path.join(" ") : rootKey;
|
|
32
|
+
return `${prefix} — ${description}`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Sanitizes a command key segment for MCP tool names. */
|
|
36
|
+
export function sanitizeToolSegment(key: string): string {
|
|
37
|
+
return key.replace(/[^a-zA-Z0-9]/g, "_");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Builds the MCP tool name for a leaf at the given path. */
|
|
41
|
+
export function mcpToolName(root: CliCommand, path: string[]): string {
|
|
42
|
+
if (path.length === 0) {
|
|
43
|
+
return sanitizeToolSegment(root.key);
|
|
44
|
+
}
|
|
45
|
+
return path.map(sanitizeToolSegment).join("_");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** JSON Schema property for one option. */
|
|
49
|
+
function optionProperty(opt: CliOption): Record<string, unknown> {
|
|
50
|
+
const base = { description: opt.description };
|
|
51
|
+
switch (opt.kind) {
|
|
52
|
+
case CliOptionKind.Presence:
|
|
53
|
+
return { type: "boolean", ...base };
|
|
54
|
+
case CliOptionKind.String:
|
|
55
|
+
return { type: "string", ...base };
|
|
56
|
+
case CliOptionKind.Number:
|
|
57
|
+
return { type: "number", ...base };
|
|
58
|
+
case CliOptionKind.Enum:
|
|
59
|
+
return { type: "string", enum: opt.choices, ...base };
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** JSON Schema property for one positional slot. */
|
|
64
|
+
function positionalProperty(p: CliPositional): Record<string, unknown> {
|
|
65
|
+
const base = { description: p.description };
|
|
66
|
+
const { argMax = 1 } = p;
|
|
67
|
+
if (argMax === 0) {
|
|
68
|
+
return { type: "array", items: { type: "string" }, ...base };
|
|
69
|
+
}
|
|
70
|
+
return { type: "string", ...base };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Builds inputSchema for a leaf command. */
|
|
74
|
+
function buildInputSchema(root: CliCommand, path: string[], leaf: CliCommand): Record<string, unknown> {
|
|
75
|
+
const properties: Record<string, unknown> = {};
|
|
76
|
+
const required: string[] = [];
|
|
77
|
+
|
|
78
|
+
for (const opt of collectOptionDefs(root, path)) {
|
|
79
|
+
properties[opt.name] = optionProperty(opt);
|
|
80
|
+
if (opt.required) {
|
|
81
|
+
required.push(opt.name);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
for (const p of leaf.positionals ?? []) {
|
|
86
|
+
properties[p.name] = positionalProperty(p);
|
|
87
|
+
const { argMin = 1, argMax = 1 } = p;
|
|
88
|
+
if (argMax === 1 && argMin >= 1) {
|
|
89
|
+
required.push(p.name);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const schema: Record<string, unknown> = {
|
|
94
|
+
type: "object",
|
|
95
|
+
properties,
|
|
96
|
+
additionalProperties: false,
|
|
97
|
+
};
|
|
98
|
+
if (required.length > 0) {
|
|
99
|
+
schema.required = required;
|
|
100
|
+
}
|
|
101
|
+
return schema;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Resolves MCP tool description with optional override and requiresEnv suffix. */
|
|
105
|
+
function resolveToolDescription(root: CliCommand, path: string[], leaf: CliCommand): string {
|
|
106
|
+
if (leaf.mcpTool?.description) {
|
|
107
|
+
return leaf.mcpTool.description;
|
|
108
|
+
}
|
|
109
|
+
let desc = mcpToolDescription(path, root.key, leaf.description);
|
|
110
|
+
const env = leaf.mcpTool?.requiresEnv;
|
|
111
|
+
if (env && env.length > 0) {
|
|
112
|
+
desc += ` [requires env: ${env.join(", ")}]`;
|
|
113
|
+
}
|
|
114
|
+
return desc;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** One resolved MCP resource (built-in or user-defined). */
|
|
118
|
+
export interface McpResourceEntry {
|
|
119
|
+
uri: string;
|
|
120
|
+
name: string;
|
|
121
|
+
description?: string;
|
|
122
|
+
mimeType: string;
|
|
123
|
+
load: () => string;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Returns built-in schema resource plus user mcpServer.resources. */
|
|
127
|
+
export function allMcpResources(root: CliCommand): McpResourceEntry[] {
|
|
128
|
+
const schemaUri = resolveMcpSchemaUri(root);
|
|
129
|
+
const builtIn: McpResourceEntry = {
|
|
130
|
+
uri: schemaUri,
|
|
131
|
+
name: "cli-schema",
|
|
132
|
+
description: "Full CLI command tree (same as --schema).",
|
|
133
|
+
mimeType: "application/json",
|
|
134
|
+
load: () => cliSchemaJson(root),
|
|
135
|
+
};
|
|
136
|
+
const user = (root.mcpServer?.resources ?? []).map((r) => ({
|
|
137
|
+
uri: r.uri,
|
|
138
|
+
name: r.name,
|
|
139
|
+
description: r.description,
|
|
140
|
+
mimeType: r.mimeType ?? "text/plain",
|
|
141
|
+
load: r.load,
|
|
142
|
+
}));
|
|
143
|
+
return [builtIn, ...user];
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Recursively collects MCP tool definitions from user leaf commands. */
|
|
147
|
+
export function collectMcpTools(root: CliCommand): McpToolDef[] {
|
|
148
|
+
const out: McpToolDef[] = [];
|
|
149
|
+
|
|
150
|
+
/** Walks the command tree and appends leaf tools. */
|
|
151
|
+
function walk(cmd: CliCommand, path: string[]): void {
|
|
152
|
+
if ("handler" in cmd && cmd.handler) {
|
|
153
|
+
if (cmd.key === "completion" || cmd.key === "mcp") {
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
if (cmd.mcpTool?.enabled === false) {
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
out.push({
|
|
160
|
+
name: mcpToolName(root, path),
|
|
161
|
+
description: resolveToolDescription(root, path, cmd),
|
|
162
|
+
path,
|
|
163
|
+
leaf: cmd,
|
|
164
|
+
inputSchema: buildInputSchema(root, path, cmd),
|
|
165
|
+
});
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
for (const ch of cmd.commands ?? []) {
|
|
169
|
+
walk(ch, [...path, ch.key]);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if ("handler" in root && root.handler) {
|
|
174
|
+
walk(root, []);
|
|
175
|
+
} else {
|
|
176
|
+
for (const ch of root.commands ?? []) {
|
|
177
|
+
walk(ch, [ch.key]);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return out;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** Reads package.json version from cwd synchronously. */
|
|
185
|
+
function resolveMcpVersionFromPackageJson(): string | undefined {
|
|
186
|
+
try {
|
|
187
|
+
const text = readFileSync(join(process.cwd(), "package.json"), "utf8");
|
|
188
|
+
const version = (JSON.parse(text) as { version?: string }).version;
|
|
189
|
+
return typeof version === "string" ? version : undefined;
|
|
190
|
+
} catch {
|
|
191
|
+
return undefined;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** Resolves MCP server name and version for initialize. */
|
|
196
|
+
export function resolveMcpServerInfo(root: CliCommand): { name: string; version: string } {
|
|
197
|
+
return {
|
|
198
|
+
name: root.mcpServer?.name ?? root.key,
|
|
199
|
+
version: root.mcpServer?.version ?? resolveMcpVersionFromPackageJson() ?? "0.0.0",
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** Resolves the schema resource URI for this app. */
|
|
204
|
+
export function resolveMcpSchemaUri(root: CliCommand): string {
|
|
205
|
+
return root.mcpServer?.schemaResourceUri ?? MCP_SCHEMA_URI_DEFAULT;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** Converts flat MCP tool arguments to argv for cliInvoke. */
|
|
209
|
+
export function mcpToolCallToArgv(
|
|
210
|
+
root: CliCommand,
|
|
211
|
+
tool: McpToolDef,
|
|
212
|
+
args: Record<string, unknown>,
|
|
213
|
+
): string[] | { error: string } {
|
|
214
|
+
const argv = [...tool.path];
|
|
215
|
+
|
|
216
|
+
for (const opt of collectOptionDefs(root, tool.path)) {
|
|
217
|
+
const val = args[opt.name];
|
|
218
|
+
if (val === undefined) {
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
if (opt.kind === CliOptionKind.Presence) {
|
|
222
|
+
if (val === true) {
|
|
223
|
+
argv.push(`--${opt.name}`);
|
|
224
|
+
}
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
argv.push(`--${opt.name}`, String(val));
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
for (const p of tool.leaf.positionals ?? []) {
|
|
231
|
+
const val = args[p.name];
|
|
232
|
+
const { argMin = 1, argMax = 1 } = p;
|
|
233
|
+
|
|
234
|
+
if (argMax === 0) {
|
|
235
|
+
if (!Array.isArray(val)) {
|
|
236
|
+
return { error: `Missing argument: ${p.name}` };
|
|
237
|
+
}
|
|
238
|
+
for (const item of val) {
|
|
239
|
+
argv.push(String(item));
|
|
240
|
+
}
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (val === undefined) {
|
|
245
|
+
if (argMin >= 1) {
|
|
246
|
+
return { error: `Missing argument: ${p.name}` };
|
|
247
|
+
}
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
argv.push(String(val));
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return argv;
|
|
254
|
+
}
|
package/src/mcp.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/*
|
|
2
|
+
This module starts the ArgsBarg MCP stdio server for opt-in program roots.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { mcpServeStdioLoop } from "./mcp/server.ts";
|
|
6
|
+
import { bootstrapMcpEnv } from "./mcp/env.ts";
|
|
7
|
+
import { CliCommand } from "./types.ts";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Runs the MCP JSON-RPC server on stdin/stdout until stdin closes, then exits.
|
|
11
|
+
* Caller must ensure `root.mcpServer` is set.
|
|
12
|
+
*/
|
|
13
|
+
export async function cliMcpServeStdio(root: CliCommand): Promise<never> {
|
|
14
|
+
try {
|
|
15
|
+
if (root.mcpServer) {
|
|
16
|
+
bootstrapMcpEnv(root.mcpServer);
|
|
17
|
+
}
|
|
18
|
+
await mcpServeStdioLoop(root);
|
|
19
|
+
process.exit(0);
|
|
20
|
+
} catch (err) {
|
|
21
|
+
if (err instanceof Error) {
|
|
22
|
+
process.stderr.write(err.message + "\n");
|
|
23
|
+
} else {
|
|
24
|
+
process.stderr.write("MCP server error.\n");
|
|
25
|
+
}
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
}
|
package/src/parse.ts
CHANGED
|
@@ -217,7 +217,7 @@ function consumeOptions(
|
|
|
217
217
|
// ── Positional Collection ─────────────────────────────────────────────────────
|
|
218
218
|
|
|
219
219
|
/** Merges option defs from the program root along the routed command path. */
|
|
220
|
-
function collectOptionDefs(root: CliCommand, path: string[]): CliOption[] {
|
|
220
|
+
export function collectOptionDefs(root: CliCommand, path: string[]): CliOption[] {
|
|
221
221
|
let defs = [...(root.options ?? [])];
|
|
222
222
|
let cmds = root.commands ?? [];
|
|
223
223
|
|
|
@@ -458,7 +458,7 @@ export function parse(root: CliCommand, argv: string[]): ParseResult {
|
|
|
458
458
|
// Walk the command tree
|
|
459
459
|
while (true) {
|
|
460
460
|
if (!forcePositionals) {
|
|
461
|
-
const orep = consumeOptions(
|
|
461
|
+
const orep = consumeOptions(collectOptionDefs(root, path), false, argv, i, opts);
|
|
462
462
|
if (orep.report.err) {
|
|
463
463
|
return {
|
|
464
464
|
kind: ParseKind.Error,
|
|
@@ -601,6 +601,21 @@ export function postParseValidate(root: CliCommand, pr: ParseResult): ParseResul
|
|
|
601
601
|
};
|
|
602
602
|
}
|
|
603
603
|
}
|
|
604
|
+
if (d.kind === CliOptionKind.Enum) {
|
|
605
|
+
const choices = d.choices ?? [];
|
|
606
|
+
if (!choices.includes(v)) {
|
|
607
|
+
return {
|
|
608
|
+
kind: ParseKind.Error,
|
|
609
|
+
path: pr.path,
|
|
610
|
+
opts: {},
|
|
611
|
+
args: [],
|
|
612
|
+
helpExplicit: false,
|
|
613
|
+
helpPath: [],
|
|
614
|
+
errorMsg: `Option --${k}: '${v}' is not one of: ${choices.join(", ")}`,
|
|
615
|
+
errorHelpPath: pr.path,
|
|
616
|
+
};
|
|
617
|
+
}
|
|
618
|
+
}
|
|
604
619
|
}
|
|
605
620
|
|
|
606
621
|
return pr;
|
package/src/runtime.ts
CHANGED
|
@@ -7,9 +7,10 @@ It keeps execution flow out of the public barrel so the exported API stays small
|
|
|
7
7
|
the runtime responsibilities remain easy to reason about.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import { cliBuiltinCompletionGroup, cliPresentationRoot, completionBashScript, completionZshScript } from "./completion.ts";
|
|
10
|
+
import { cliBuiltinCompletionGroup, cliBuiltinMcpCommand, cliPresentationRoot, completionBashScript, completionZshScript } from "./completion.ts";
|
|
11
11
|
import { CliContext } from "./context.ts";
|
|
12
12
|
import { cliHelpRender } from "./help.ts";
|
|
13
|
+
import { cliMcpServeStdio } from "./mcp.ts";
|
|
13
14
|
import { parse, postParseValidate, ParseKind } from "./parse.ts";
|
|
14
15
|
import { cliSchemaJson } from "./schema.ts";
|
|
15
16
|
import { CliCommand } from "./types.ts";
|
|
@@ -46,6 +47,11 @@ export async function cliRun(root: CliCommand, argv: string[] = process.argv.sli
|
|
|
46
47
|
let parseRoot = root;
|
|
47
48
|
let isLeafCompletionIntercept = false;
|
|
48
49
|
|
|
50
|
+
if (root.handler && argv.length >= 1 && argv[0] === "mcp" && !root.mcpServer) {
|
|
51
|
+
process.stderr.write("Unknown command: mcp\n");
|
|
52
|
+
process.exit(1);
|
|
53
|
+
}
|
|
54
|
+
|
|
49
55
|
// Intercept completion for Leaf roots (since they can't natively have a completion subcommand)
|
|
50
56
|
// but wrap them in a dummy router so that the parser handles `-h` and errors correctly.
|
|
51
57
|
if (root.handler && argv.length >= 1 && argv[0] === "completion") {
|
|
@@ -55,6 +61,12 @@ export async function cliRun(root: CliCommand, argv: string[] = process.argv.sli
|
|
|
55
61
|
description: root.description,
|
|
56
62
|
commands: [cliBuiltinCompletionGroup(root.key)],
|
|
57
63
|
} as any;
|
|
64
|
+
} else if (root.handler && argv.length >= 1 && argv[0] === "mcp" && root.mcpServer) {
|
|
65
|
+
parseRoot = {
|
|
66
|
+
key: root.key,
|
|
67
|
+
description: root.description,
|
|
68
|
+
commands: [cliBuiltinMcpCommand()],
|
|
69
|
+
} as CliCommand;
|
|
58
70
|
} else {
|
|
59
71
|
parseRoot = cliRootMergedWithBuiltins(root);
|
|
60
72
|
}
|
|
@@ -97,6 +109,18 @@ export async function cliRun(root: CliCommand, argv: string[] = process.argv.sli
|
|
|
97
109
|
}
|
|
98
110
|
}
|
|
99
111
|
|
|
112
|
+
if (pr.path[0] === "mcp") {
|
|
113
|
+
if (!root.mcpServer) {
|
|
114
|
+
process.stderr.write("Internal error: mcp not enabled.\n");
|
|
115
|
+
process.exit(1);
|
|
116
|
+
}
|
|
117
|
+
if (pr.path.length !== 1) {
|
|
118
|
+
process.stderr.write("Unknown subcommand: mcp " + pr.path.slice(1).join(" ") + "\n");
|
|
119
|
+
process.exit(1);
|
|
120
|
+
}
|
|
121
|
+
await cliMcpServeStdio(root);
|
|
122
|
+
}
|
|
123
|
+
|
|
100
124
|
let current = parseRoot;
|
|
101
125
|
for (const seg of pr.path) {
|
|
102
126
|
const ch = (current.commands ?? []).find((candidate: CliCommand) => candidate.key === seg);
|
|
@@ -112,7 +136,7 @@ export async function cliRun(root: CliCommand, argv: string[] = process.argv.sli
|
|
|
112
136
|
process.exit(1);
|
|
113
137
|
}
|
|
114
138
|
|
|
115
|
-
const ctx = new CliContext(parseRoot.key, pr.path, pr.args, pr.opts, parseRoot);
|
|
139
|
+
const ctx = new CliContext(parseRoot.key, pr.path, pr.args, pr.opts, parseRoot, "cli");
|
|
116
140
|
try {
|
|
117
141
|
await Promise.resolve(current.handler(ctx));
|
|
118
142
|
process.exit(0);
|
package/src/types.ts
CHANGED
|
@@ -9,7 +9,12 @@ It gives the package one shared model for both library users and internal module
|
|
|
9
9
|
import type { CliContext } from "./context.ts";
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
|
-
*
|
|
12
|
+
* How a leaf handler was dispatched.
|
|
13
|
+
*/
|
|
14
|
+
export type CliInvocation = "cli" | "mcp";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Option kinds: presence (boolean flag), string (free-form text), number (strict double), or enum (fixed choices).
|
|
13
18
|
*/
|
|
14
19
|
export enum CliOptionKind {
|
|
15
20
|
/** Boolean flag: no value token (may be implicit `"1"` when set). */
|
|
@@ -18,6 +23,8 @@ export enum CliOptionKind {
|
|
|
18
23
|
String = "string",
|
|
19
24
|
/** Strict floating-point value (parsed at validation time). */
|
|
20
25
|
Number = "number",
|
|
26
|
+
/** Fixed set of allowed string values. Requires non-empty `choices` on the option. */
|
|
27
|
+
Enum = "enum",
|
|
21
28
|
}
|
|
22
29
|
|
|
23
30
|
/**
|
|
@@ -54,6 +61,11 @@ export interface CliOption {
|
|
|
54
61
|
shortName?: string;
|
|
55
62
|
/** Whether this option must be provided. Cannot be used with Presence kind. */
|
|
56
63
|
required?: boolean;
|
|
64
|
+
/**
|
|
65
|
+
* Allowed values. Required when kind === Enum; ignored otherwise.
|
|
66
|
+
* Must be a non-empty array of distinct non-empty strings.
|
|
67
|
+
*/
|
|
68
|
+
choices?: string[];
|
|
57
69
|
}
|
|
58
70
|
|
|
59
71
|
/**
|
|
@@ -78,6 +90,70 @@ export interface CliPositional {
|
|
|
78
90
|
argMax?: number;
|
|
79
91
|
}
|
|
80
92
|
|
|
93
|
+
/**
|
|
94
|
+
* Root-only. Enables `myapp mcp` and MCP stdio server metadata.
|
|
95
|
+
*/
|
|
96
|
+
export interface CliMcpServerConfig {
|
|
97
|
+
/** `initialize` serverInfo.name (default: root `key`). */
|
|
98
|
+
name?: string;
|
|
99
|
+
/** `initialize` serverInfo.version (default: see resolveMcpVersion). */
|
|
100
|
+
version?: string;
|
|
101
|
+
/** Resource URI for schema export (default: `"argsbarg://schema"`). */
|
|
102
|
+
schemaResourceUri?: string;
|
|
103
|
+
/**
|
|
104
|
+
* Capture the user's login shell environment at MCP server start and merge it
|
|
105
|
+
* into process.env. Solves missing PATH, nvm/rbenv shims, Homebrew binaries,
|
|
106
|
+
* and shell exports that MCP hosts (e.g. Cursor) don't inherit.
|
|
107
|
+
*/
|
|
108
|
+
shellEnv?: boolean | string;
|
|
109
|
+
/**
|
|
110
|
+
* Path to a .env file loaded into process.env at MCP server start, after shellEnv.
|
|
111
|
+
* Supports `~` expansion. Warns on stderr if the file does not exist.
|
|
112
|
+
* Always overwrites — envFile is authoritative for its keys.
|
|
113
|
+
*/
|
|
114
|
+
envFile?: string;
|
|
115
|
+
/**
|
|
116
|
+
* Custom MCP resources exposed alongside the built-in argsbarg://schema resource.
|
|
117
|
+
* URIs must be unique and must not equal schemaResourceUri.
|
|
118
|
+
*/
|
|
119
|
+
resources?: CliMcpResource[];
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* A custom MCP resource exposed under resources/list and resources/read.
|
|
124
|
+
*/
|
|
125
|
+
export interface CliMcpResource {
|
|
126
|
+
/** Resource URI (must be unique; must not equal schemaResourceUri). */
|
|
127
|
+
uri: string;
|
|
128
|
+
/** Short display name for resources/list. */
|
|
129
|
+
name: string;
|
|
130
|
+
/** Optional human description for resources/list. */
|
|
131
|
+
description?: string;
|
|
132
|
+
/** MIME type (default: "text/plain"). */
|
|
133
|
+
mimeType?: string;
|
|
134
|
+
/** Called at resources/read time; must return the resource body. */
|
|
135
|
+
load: () => string;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Leaf-only. Controls how this command appears as an MCP tool.
|
|
140
|
+
*/
|
|
141
|
+
export interface CliMcpToolConfig {
|
|
142
|
+
/** When `false`, omit from `tools/list` (default: exposed). */
|
|
143
|
+
enabled?: boolean;
|
|
144
|
+
/**
|
|
145
|
+
* Override the generated MCP tool description.
|
|
146
|
+
* Default: auto-generated from command path and description.
|
|
147
|
+
*/
|
|
148
|
+
description?: string;
|
|
149
|
+
/**
|
|
150
|
+
* Environment variable names required at runtime.
|
|
151
|
+
* Appended to auto-generated MCP tool descriptions; enforced at tools/call time.
|
|
152
|
+
* Empty string counts as absent.
|
|
153
|
+
*/
|
|
154
|
+
requiresEnv?: string[];
|
|
155
|
+
}
|
|
156
|
+
|
|
81
157
|
/**
|
|
82
158
|
* Base properties shared by all command nodes.
|
|
83
159
|
*/
|
|
@@ -90,6 +166,10 @@ export interface CliCommandBase {
|
|
|
90
166
|
notes?: string;
|
|
91
167
|
/** Global or command-level flags/options. */
|
|
92
168
|
options?: CliOption[];
|
|
169
|
+
/** Root-only. When set, enables the `mcp` built-in subcommand. */
|
|
170
|
+
mcpServer?: CliMcpServerConfig;
|
|
171
|
+
/** Leaf-only. Per-tool MCP exposure and metadata. */
|
|
172
|
+
mcpTool?: CliMcpToolConfig;
|
|
93
173
|
}
|
|
94
174
|
|
|
95
175
|
/**
|
package/src/validate.ts
CHANGED
|
@@ -12,8 +12,9 @@ import {
|
|
|
12
12
|
CliOptionKind,
|
|
13
13
|
CliSchemaValidationError,
|
|
14
14
|
} from "./types.ts";
|
|
15
|
+
import { MCP_SCHEMA_URI_DEFAULT } from "./mcp/tools.ts";
|
|
15
16
|
|
|
16
|
-
const reservedCommandNames = ["completion"];
|
|
17
|
+
const reservedCommandNames = ["completion", "mcp"];
|
|
17
18
|
|
|
18
19
|
/**
|
|
19
20
|
* Validates the static CliCommand tree against ArgBarg rules.
|
|
@@ -50,6 +51,34 @@ function walkCommand(cmd: CliCommand, isRoot: boolean = false): void {
|
|
|
50
51
|
);
|
|
51
52
|
}
|
|
52
53
|
|
|
54
|
+
if (!isRoot && cmd.mcpServer !== undefined) {
|
|
55
|
+
throw new CliSchemaValidationError(
|
|
56
|
+
"mcpServer is only supported on the program root (not on " + cmd.key + ")",
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const isLeaf = "handler" in cmd && !!cmd.handler;
|
|
61
|
+
if (!isLeaf && cmd.mcpTool !== undefined) {
|
|
62
|
+
throw new CliSchemaValidationError(
|
|
63
|
+
"mcpTool is only supported on leaf commands (not on " + cmd.key + ")",
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
if (isRoot && cmd.mcpTool !== undefined) {
|
|
67
|
+
throw new CliSchemaValidationError("mcpTool is only supported on leaf commands");
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (isRoot && cmd.mcpServer?.resources) {
|
|
71
|
+
const schemaUri = cmd.mcpServer.schemaResourceUri ?? MCP_SCHEMA_URI_DEFAULT;
|
|
72
|
+
const uris = cmd.mcpServer.resources.map((r) => r.uri);
|
|
73
|
+
if (uris.includes(schemaUri)) {
|
|
74
|
+
throw new CliSchemaValidationError(
|
|
75
|
+
`mcpServer.resources URI '${schemaUri}' conflicts with the built-in schema resource`,
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
if (new Set(uris).size !== uris.length) {
|
|
79
|
+
throw new CliSchemaValidationError("mcpServer.resources URIs must be unique");
|
|
80
|
+
}
|
|
81
|
+
}
|
|
53
82
|
|
|
54
83
|
// Check for duplicate child names
|
|
55
84
|
const seenNames = new Set<string>();
|
|
@@ -88,6 +117,30 @@ function walkCommand(cmd: CliCommand, isRoot: boolean = false): void {
|
|
|
88
117
|
}
|
|
89
118
|
seenShorts.add(opt.shortName);
|
|
90
119
|
}
|
|
120
|
+
|
|
121
|
+
if (opt.kind === CliOptionKind.Enum) {
|
|
122
|
+
if (!opt.choices || opt.choices.length === 0) {
|
|
123
|
+
throw new CliSchemaValidationError(
|
|
124
|
+
`Option '${opt.name}' on '${cmd.key}': Enum kind requires non-empty choices`,
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
if (new Set(opt.choices).size !== opt.choices.length) {
|
|
128
|
+
throw new CliSchemaValidationError(
|
|
129
|
+
`Option '${opt.name}' on '${cmd.key}': Enum choices must be distinct`,
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
for (const choice of opt.choices) {
|
|
133
|
+
if (choice.length === 0) {
|
|
134
|
+
throw new CliSchemaValidationError(
|
|
135
|
+
`Option '${opt.name}' on '${cmd.key}': Enum choices must be non-empty strings`,
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
} else if (opt.choices !== undefined) {
|
|
140
|
+
throw new CliSchemaValidationError(
|
|
141
|
+
`Option '${opt.name}' on '${cmd.key}': choices is only valid for Enum kind`,
|
|
142
|
+
);
|
|
143
|
+
}
|
|
91
144
|
}
|
|
92
145
|
|
|
93
146
|
// Validate positionals
|