argsbarg 1.3.1 → 1.4.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/.cursor/plans/mcp_v1.1_polish_e9656029.plan.md +260 -0
- package/CHANGELOG.md +16 -1
- package/README.md +17 -2
- package/docs/mcp.md +211 -0
- package/examples/nested.ts +1 -0
- package/index.d.ts +22 -0
- package/package.json +1 -1
- package/src/completion.ts +21 -3
- package/src/index.test.ts +353 -0
- package/src/index.ts +1 -1
- package/src/invoke.ts +192 -0
- package/src/mcp/result.ts +57 -0
- package/src/mcp/server.ts +226 -0
- package/src/mcp/tools.ts +209 -0
- package/src/mcp.ts +24 -0
- package/src/parse.ts +2 -2
- package/src/runtime.ts +25 -1
- package/src/types.ts +24 -0
- package/src/validate.ts +16 -1
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: MCP v1.1 polish
|
|
3
|
+
overview: "Ship four agent-focused MCP improvements: richer tool descriptions with CLI paths, per-leaf `mcpEnabled` opt-out, stderr surfaced on successful tool calls, and `structuredContent` when handler stdout is valid JSON."
|
|
4
|
+
todos:
|
|
5
|
+
- id: types-mcpEnabled
|
|
6
|
+
content: "Add mcpEnabled?: boolean to CliCommandBase; validate leaf-only in validate.ts"
|
|
7
|
+
status: completed
|
|
8
|
+
- id: tool-descriptions
|
|
9
|
+
content: Add mcpToolDescription(path, rootKey, description); enrich descriptions; filter mcpEnabled === false
|
|
10
|
+
status: completed
|
|
11
|
+
- id: result-builder
|
|
12
|
+
content: Create src/mcp/result.ts with buildToolCallSuccess (stderr blocks + JSON structuredContent)
|
|
13
|
+
status: completed
|
|
14
|
+
- id: server-wire
|
|
15
|
+
content: Wire buildToolCallSuccess into tools/call success path in server.ts
|
|
16
|
+
status: completed
|
|
17
|
+
- id: tests
|
|
18
|
+
content: Unit tests for description, mcpEnabled, result builder; subprocess tests for JSON structuredContent and stderr-on-success
|
|
19
|
+
status: completed
|
|
20
|
+
- id: docs-changelog
|
|
21
|
+
content: Update docs/mcp.md and CHANGELOG.md; run just typegen and just test
|
|
22
|
+
status: completed
|
|
23
|
+
isProject: false
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
# MCP follow-up improvements plan
|
|
27
|
+
|
|
28
|
+
Four targeted enhancements to the existing opt-in MCP server. No new dependencies, no protocol transport changes, no public export of internals beyond the new `mcpEnabled` schema field.
|
|
29
|
+
|
|
30
|
+
```mermaid
|
|
31
|
+
flowchart LR
|
|
32
|
+
subgraph toolsList [tools/list]
|
|
33
|
+
Walk[collectMcpTools]
|
|
34
|
+
Desc["path + description"]
|
|
35
|
+
Filter[mcpEnabled filter]
|
|
36
|
+
end
|
|
37
|
+
subgraph toolCall [tools/call]
|
|
38
|
+
Invoke[cliInvoke]
|
|
39
|
+
Build[buildToolCallResult]
|
|
40
|
+
Resp["content + structuredContent?"]
|
|
41
|
+
end
|
|
42
|
+
Walk --> Filter --> Desc
|
|
43
|
+
Invoke --> Build --> Resp
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## 1. CLI path in tool descriptions
|
|
49
|
+
|
|
50
|
+
**Goal:** Agents see the human invocation path without reading the schema resource first.
|
|
51
|
+
|
|
52
|
+
**Format:**
|
|
53
|
+
|
|
54
|
+
| Path | Leaf description | MCP `description` |
|
|
55
|
+
| --- | --- | --- |
|
|
56
|
+
| `["stat","owner","lookup"]` | `Resolve owner info.` | `stat owner lookup — Resolve owner info.` |
|
|
57
|
+
| `["read"]` | `Print the first line…` | `read — Print the first line…` |
|
|
58
|
+
| `[]` (root leaf app) | `Tiny demo.` | `{root.key} — Tiny demo.` |
|
|
59
|
+
|
|
60
|
+
Use em dash (`—`) between path and description. Path segments are raw `key` values (not sanitized tool names). Every tool description uses the `prefix — description` form — there is no bare-description fallback.
|
|
61
|
+
|
|
62
|
+
**Implementation:**
|
|
63
|
+
|
|
64
|
+
- Add helper in [`src/mcp/tools.ts`](src/mcp/tools.ts). The helper **must** receive `rootKey` so root-leaf apps (empty path) can use the program name as the prefix:
|
|
65
|
+
|
|
66
|
+
```typescript
|
|
67
|
+
/** Builds MCP tool description: "{cli path} — {description}". */
|
|
68
|
+
function mcpToolDescription(path: string[], rootKey: string, description: string): string {
|
|
69
|
+
const prefix = path.length > 0 ? path.join(" ") : rootKey;
|
|
70
|
+
return `${prefix} — ${description}`;
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
- Call from `collectMcpTools` when building each `McpToolDef.description`:
|
|
75
|
+
|
|
76
|
+
```typescript
|
|
77
|
+
description: mcpToolDescription(path, root.key, cmd.description),
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
- `tools/list` already maps `t.description` — no server change needed.
|
|
81
|
+
|
|
82
|
+
**Tests:** Update [`src/index.test.ts`](src/index.test.ts):
|
|
83
|
+
|
|
84
|
+
- `stat_owner_lookup` description is `stat owner lookup — Resolve owner info.`
|
|
85
|
+
- If testing a root-leaf fixture, description is `{root.key} — {description}`.
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## 2. Per-leaf opt-out via `mcpEnabled`
|
|
90
|
+
|
|
91
|
+
**Goal:** Authors can keep CLI commands for humans while hiding them from MCP tool discovery.
|
|
92
|
+
|
|
93
|
+
**Schema change** in [`src/types.ts`](src/types.ts):
|
|
94
|
+
|
|
95
|
+
```typescript
|
|
96
|
+
export interface CliCommandBase {
|
|
97
|
+
// ...existing fields...
|
|
98
|
+
/** Leaf-only. When `false`, omit this command from MCP tools (default: exposed). */
|
|
99
|
+
mcpEnabled?: boolean;
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
**Semantics:**
|
|
104
|
+
|
|
105
|
+
- Omitted or `true` → leaf is exposed (current behavior).
|
|
106
|
+
- `false` → skipped in `collectMcpTools` walk.
|
|
107
|
+
- Root `mcp?: CliMcpConfig` unchanged — it enables the server; `mcpEnabled` is unrelated.
|
|
108
|
+
|
|
109
|
+
**Validation** in [`src/validate.ts`](src/validate.ts) `walkCommand`:
|
|
110
|
+
|
|
111
|
+
- If `isRoot && cmd.mcpEnabled !== undefined` → throw `mcpEnabled is only supported on leaf commands`.
|
|
112
|
+
- If routing node (has `commands`, no `handler`) and `mcpEnabled !== undefined` → same error.
|
|
113
|
+
- No validation needed for `false` vs `true` beyond that.
|
|
114
|
+
|
|
115
|
+
**Tool collection** in [`src/mcp/tools.ts`](src/mcp/tools.ts) `walk`:
|
|
116
|
+
|
|
117
|
+
```typescript
|
|
118
|
+
if ("handler" in cmd && cmd.handler) {
|
|
119
|
+
if (cmd.key === "completion" || cmd.key === "mcp") return;
|
|
120
|
+
if (cmd.mcpEnabled === false) return;
|
|
121
|
+
// push tool...
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
**Schema export:** Leave [`src/schema.ts`](src/schema.ts) unchanged for v1 — `--schema` still lists all commands (CLI discovery). Only MCP `tools/list` respects `mcpEnabled`. Document this distinction in [`docs/mcp.md`](docs/mcp.md).
|
|
126
|
+
|
|
127
|
+
**Example (optional):** Add a hidden leaf in `nestedMcpFixture` only (not `examples/nested.ts`) with `mcpEnabled: false` to prove filtering without cluttering the demo app.
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
## 3. stderr on successful tool calls
|
|
132
|
+
|
|
133
|
+
**Goal:** Warnings and diagnostic output on stderr are not silently dropped when `exitCode === 0`.
|
|
134
|
+
|
|
135
|
+
**Current behavior** ([`src/mcp/server.ts`](src/mcp/server.ts) ~L133–141): success returns only `invokeResult.stdout`.
|
|
136
|
+
|
|
137
|
+
**New behavior:** Extract a small builder (new file [`src/mcp/result.ts`](src/mcp/result.ts) recommended to keep server readable):
|
|
138
|
+
|
|
139
|
+
```typescript
|
|
140
|
+
export interface McpToolCallSuccess {
|
|
141
|
+
content: { type: "text"; text: string }[];
|
|
142
|
+
structuredContent?: unknown;
|
|
143
|
+
isError: false;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function buildToolCallSuccess(stdout: string, stderr: string): McpToolCallSuccess
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
**stderr formatting rules:**
|
|
150
|
+
|
|
151
|
+
- If `stderr.trim()` is empty → single `content` item with stdout (or empty string if stdout empty).
|
|
152
|
+
- If stderr non-empty → **two** `content` items:
|
|
153
|
+
1. `{ type: "text", text: stdout }` (may be empty string)
|
|
154
|
+
2. `{ type: "text", text: stderr.trim() }` — **raw stderr text, no `stderr:` prefix**
|
|
155
|
+
|
|
156
|
+
Use multiple content blocks rather than concatenating into one string so hosts can distinguish streams; the second block’s position in the array is the signal that it came from stderr. Some MCP hosts render content blocks with their own labeling — a literal `stderr:` prefix would leak into user-visible tool output. Existing failure path (~L144–151) already prefers stderr — leave as-is.
|
|
157
|
+
|
|
158
|
+
**Tests:**
|
|
159
|
+
|
|
160
|
+
- Unit test `buildToolCallSuccess` with stdout-only, stderr-only, both.
|
|
161
|
+
- Subprocess test: add a fixture leaf (in test fixture or temporary nested command) that `console.warn`s on stderr but exits 0; assert two content blocks and `isError: false`.
|
|
162
|
+
|
|
163
|
+
---
|
|
164
|
+
|
|
165
|
+
## 4. `structuredContent` when stdout is valid JSON
|
|
166
|
+
|
|
167
|
+
**Goal:** When handlers emit JSON (e.g. `nested.ts` with `--json`), MCP clients get machine-readable output per [MCP tools spec](https://modelcontextprotocol.io/specification/draft/server/tools).
|
|
168
|
+
|
|
169
|
+
**Parsing rules** (in `buildToolCallSuccess`):
|
|
170
|
+
|
|
171
|
+
1. Let `trimmed = stdout.trim()`.
|
|
172
|
+
2. If `trimmed.length === 0` → no `structuredContent`.
|
|
173
|
+
3. Try `JSON.parse(trimmed)`.
|
|
174
|
+
4. On success → set `structuredContent` to the parsed value (object, array, or primitive — all valid per 2025 spec).
|
|
175
|
+
5. On `SyntaxError` → no `structuredContent` (plain text handlers unchanged).
|
|
176
|
+
6. **Always** keep `content[0].text` as the raw stdout string when stdout is non-empty (spec: structured tools SHOULD also return serialized JSON in `content` — we already have the raw stdout which satisfies this for JSON handlers).
|
|
177
|
+
|
|
178
|
+
**Primitive footgun (document, do not guard):** `JSON.parse("true")` yields `structuredContent: true`. A handler that intentionally prints the string `true` as human-readable output would get machine-typed output. This is spec-correct and rare in practice. Document in [`docs/mcp.md`](docs/mcp.md) rather than limiting to `typeof parsed === "object"` — that would reject valid JSON arrays and primitives that the 2025 spec explicitly allows.
|
|
179
|
+
|
|
180
|
+
**Do not** auto-generate `outputSchema` in this release — that requires schema author input or inference and is out of scope.
|
|
181
|
+
|
|
182
|
+
**Server wiring** in [`src/mcp/server.ts`](src/mcp/server.ts):
|
|
183
|
+
|
|
184
|
+
```typescript
|
|
185
|
+
const result = buildToolCallSuccess(invokeResult.stdout, invokeResult.stderr);
|
|
186
|
+
writeResponse({ jsonrpc: "2.0", id, result });
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
Spread `structuredContent` onto result only when defined.
|
|
190
|
+
|
|
191
|
+
**Tests:**
|
|
192
|
+
|
|
193
|
+
- Unit: `buildToolCallSuccess('{"a":1}\n', '')` → `structuredContent: { a: 1 }`, content text preserved.
|
|
194
|
+
- Unit: `buildToolCallSuccess('lookup user=x\n', '')` → no `structuredContent`.
|
|
195
|
+
- Unit: `buildToolCallSuccess('true\n', '')` → `structuredContent: true` (documents primitive behavior).
|
|
196
|
+
- Subprocess: extend existing `stat_owner_lookup` test or add parallel test with `json: true`:
|
|
197
|
+
|
|
198
|
+
```typescript
|
|
199
|
+
params: {
|
|
200
|
+
name: "stat_owner_lookup",
|
|
201
|
+
arguments: { path: readme, "user-name": "test", json: true },
|
|
202
|
+
}
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
Assert `result.structuredContent` deep-equals `{ user: "test", path: readme }` and `content[0].text` is valid JSON string.
|
|
206
|
+
|
|
207
|
+
---
|
|
208
|
+
|
|
209
|
+
## Documentation and changelog
|
|
210
|
+
|
|
211
|
+
Update [`docs/mcp.md`](docs/mcp.md):
|
|
212
|
+
|
|
213
|
+
- **Tool names** section: document new description format with example (including root-leaf `{root.key} — …` case).
|
|
214
|
+
- **Per-leaf visibility**: `mcpEnabled: false` on leaves; note `--schema` still includes the command.
|
|
215
|
+
- **Tool results**: stderr on success as a second raw-text content block (no prefix); `structuredContent` when stdout is valid JSON (including primitives — document footgun); link to MCP spec.
|
|
216
|
+
- **Short flags**: one-line note that tool args use long option names only (unchanged, but good to document while editing).
|
|
217
|
+
|
|
218
|
+
Update [`CHANGELOG.md`](CHANGELOG.md) under `[Unreleased]`:
|
|
219
|
+
|
|
220
|
+
- Richer MCP tool descriptions with CLI paths.
|
|
221
|
+
- `mcpEnabled` leaf opt-out.
|
|
222
|
+
- MCP tool success responses include stderr when present.
|
|
223
|
+
- MCP tool success responses include `structuredContent` for JSON stdout.
|
|
224
|
+
|
|
225
|
+
Trim [`README.md`](README.md) MCP blurb only if needed — one line pointing to docs is enough.
|
|
226
|
+
|
|
227
|
+
---
|
|
228
|
+
|
|
229
|
+
## Type declarations and release hygiene
|
|
230
|
+
|
|
231
|
+
- Run `just typegen` after [`src/types.ts`](src/types.ts) change so [`index.d.ts`](index.d.ts) exports `mcpEnabled`.
|
|
232
|
+
- Run `just test` (typecheck + lint + `bun test`).
|
|
233
|
+
|
|
234
|
+
**Public API surface:** Only `mcpEnabled?: boolean` on `CliCommandBase` is new. `cliInvoke`, `buildToolCallSuccess`, and MCP runtime remain internal.
|
|
235
|
+
|
|
236
|
+
---
|
|
237
|
+
|
|
238
|
+
## File change summary
|
|
239
|
+
|
|
240
|
+
| File | Changes |
|
|
241
|
+
| --- | --- |
|
|
242
|
+
| [`src/types.ts`](src/types.ts) | Add `mcpEnabled?: boolean` with JSDoc |
|
|
243
|
+
| [`src/validate.ts`](src/validate.ts) | Reject `mcpEnabled` on root and routing nodes |
|
|
244
|
+
| [`src/mcp/tools.ts`](src/mcp/tools.ts) | `mcpToolDescription`, filter `mcpEnabled === false` |
|
|
245
|
+
| [`src/mcp/result.ts`](src/mcp/result.ts) | **New** — `buildToolCallSuccess` |
|
|
246
|
+
| [`src/mcp/server.ts`](src/mcp/server.ts) | Use builder for success path |
|
|
247
|
+
| [`src/index.test.ts`](src/index.test.ts) | Unit + subprocess tests for all four features |
|
|
248
|
+
| [`docs/mcp.md`](docs/mcp.md) | Document behavior |
|
|
249
|
+
| [`CHANGELOG.md`](CHANGELOG.md) | Unreleased entries |
|
|
250
|
+
| [`index.d.ts`](index.d.ts) | Regenerated via typegen |
|
|
251
|
+
|
|
252
|
+
---
|
|
253
|
+
|
|
254
|
+
## Out of scope (explicitly deferred)
|
|
255
|
+
|
|
256
|
+
- Tool list caching
|
|
257
|
+
- `outputSchema` on tools
|
|
258
|
+
- Hiding `mcpEnabled: false` commands from `--schema`
|
|
259
|
+
- Group-level `mcpEnabled` inheritance
|
|
260
|
+
- `mcp.json` / Cursor config changes
|
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [1.4.0] - 2026-06-19
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **Opt-in MCP** — set `mcpServer: {}` on the program root to enable `myapp mcp`, a stdio MCP server (tools + `argsbarg://schema` resource). Hand-rolled JSON-RPC; zero new dependencies.
|
|
15
|
+
- **MCP tool descriptions** — `tools/list` descriptions include the CLI path (e.g. `stat owner lookup — Resolve owner info.`).
|
|
16
|
+
- **`mcpTool` leaf opt-out** — set `mcpTool: { enabled: false }` on a leaf to omit it from MCP tools while keeping it in the CLI and `--schema`.
|
|
17
|
+
- **MCP stderr on success** — successful tool calls return a second content block when the handler wrote to stderr.
|
|
18
|
+
- **MCP `structuredContent`** — when handler stdout is valid JSON, tool results include parsed `structuredContent` alongside text content.
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
|
|
22
|
+
- **Parent-scoped options before positionals** — nested commands accept flags from ancestor nodes when options appear before positional arguments (required for MCP tool argv layout).
|
|
23
|
+
|
|
10
24
|
## [1.3.1] - 2026-06-19
|
|
11
25
|
|
|
12
26
|
### Fixed
|
|
@@ -84,7 +98,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
84
98
|
- Migrate schemas: rename every `children` property to **`commands`**; move positional definitions to **`CliPositional`** objects on `positionals` and strip `positional` / `argMin` / `argMax` from flag definitions under `options` (flags only carry `name`, `description`, `kind`, and optional `shortName`).
|
|
85
99
|
- Imports: use `CliPositional` where needed; replace `CliOptionDef` with `CliOption` or `CliPositional` as appropriate.
|
|
86
100
|
|
|
87
|
-
[Unreleased]: https://github.com/bdombro/bun-argsbarg/compare/v1.
|
|
101
|
+
[Unreleased]: https://github.com/bdombro/bun-argsbarg/compare/v1.4.0...HEAD
|
|
102
|
+
[1.4.0]: https://github.com/bdombro/bun-argsbarg/releases/tag/v1.4.0
|
|
88
103
|
[1.3.1]: https://github.com/bdombro/bun-argsbarg/releases/tag/v1.3.1
|
|
89
104
|
[1.3.0]: https://github.com/bdombro/bun-argsbarg/releases/tag/v1.3.0
|
|
90
105
|
[1.2.1]: https://github.com/bdombro/bun-argsbarg/releases/tag/v1.2.1
|
package/README.md
CHANGED
|
@@ -10,9 +10,15 @@ Build beautiful, well-behaved CLI apps with Bun — **no third-party runtime dep
|
|
|
10
10
|
|
|
11
11
|
Why another CLI parser?
|
|
12
12
|
|
|
13
|
-
*Schema-first*
|
|
13
|
+
*Schema-first* — define your entire CLI’s structure, commands, options, and help in a single, explicit data model, making the command-line interface centralized, clear, and self-describing upfront.
|
|
14
14
|
|
|
15
|
-
*
|
|
15
|
+
*Beautiful `-h` screens* — scoped help at any routing depth, rendered in rounded UTF-8 boxes with tables, terminal-width wrapping, and color when stdout is a TTY. Errors print in red with contextual help on stderr.
|
|
16
|
+
|
|
17
|
+
*Shell completions* — `completion bash` and `completion zsh` built-ins generate installable scripts from your schema so users get tab completion for commands, flags, and positionals without extra tooling.
|
|
18
|
+
|
|
19
|
+
*Optional MCP server* — set `mcpServer: {}` on the program root to expose leaf commands as MCP tools and the full CLI tree as a schema resource (`myapp mcp` over stdio). See [docs/mcp.md](docs/mcp.md).
|
|
20
|
+
|
|
21
|
+
*Bun-optimized* — built from the ground up for Bun and TypeScript, leveraging Bun’s performance and modern JavaScript features without any extra dependencies.
|
|
16
22
|
|
|
17
23
|
Also checkout ArgsBarg for [cpp](https://github.com/bdombro/cpp-argsbarg), [nim](https://github.com/bdombro/nim-argsbarg), and [swift](https://github.com/bdombro/swift-argsbarg)!
|
|
18
24
|
|
|
@@ -90,11 +96,20 @@ Every app gets:
|
|
|
90
96
|
- `-h` / `--help` at any routing depth (scoped help).
|
|
91
97
|
- **`--schema`** at the program root — print the full command tree as JSON (for tooling and agents).
|
|
92
98
|
- **`completion bash` / `completion zsh`** — print shell completion scripts to stdout (injected by `cliRun`).
|
|
99
|
+
- **`mcp`** — when `mcpServer: {}` is set on the program root, run an MCP server over stdio (`myapp mcp`).
|
|
93
100
|
|
|
94
101
|
Do not declare a top-level command named **`completion`** — it is reserved for this built-in.
|
|
102
|
+
Do not declare a top-level command named **`mcp`** — it is reserved when MCP is enabled.
|
|
95
103
|
Do not declare an option named **`schema`** — it is reserved for `--schema`.
|
|
96
104
|
|
|
97
105
|
|
|
106
|
+
### MCP (AI agents)
|
|
107
|
+
|
|
108
|
+
Opt in on the program root with `mcpServer: {}` (or `{ name, version, … }`), then run `myapp mcp` for a stdio MCP server. Each leaf command becomes a tool; the CLI tree is available as resource `argsbarg://schema`.
|
|
109
|
+
|
|
110
|
+
See **[docs/mcp.md](docs/mcp.md)** for configuration, Cursor setup, tool naming, argument mapping, and protocol details.
|
|
111
|
+
|
|
112
|
+
|
|
98
113
|
### Shell completions
|
|
99
114
|
|
|
100
115
|
```bash
|
package/docs/mcp.md
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
# MCP server
|
|
2
|
+
|
|
3
|
+
ArgsBarg can expose your CLI to AI agents through the [Model Context Protocol (MCP)](https://modelcontextprotocol.io/). Each **leaf command** becomes an MCP tool; the full command tree is available as a schema resource. The server speaks JSON-RPC over stdio — one JSON object per line on stdin and stdout.
|
|
4
|
+
|
|
5
|
+
MCP is **opt-in**. Apps that do not set `mcpServer` on the program root behave exactly as before.
|
|
6
|
+
|
|
7
|
+
## Quick start
|
|
8
|
+
|
|
9
|
+
1. Add `mcpServer` to your program root:
|
|
10
|
+
|
|
11
|
+
```typescript
|
|
12
|
+
const cli: CliCommand = {
|
|
13
|
+
key: "myapp",
|
|
14
|
+
description: "My app.",
|
|
15
|
+
mcpServer: { name: "myapp", version: "1.0.0" },
|
|
16
|
+
commands: [/* ... */],
|
|
17
|
+
};
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
`mcpServer: {}` is enough to enable the server. Optional fields override defaults (see [Configuration](#configuration)).
|
|
21
|
+
|
|
22
|
+
2. Run the MCP server:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
myapp mcp
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
The process reads NDJSON requests from stdin and writes NDJSON responses to stdout. It stays alive until stdin closes.
|
|
29
|
+
|
|
30
|
+
3. Point your MCP client at that command. See [Client setup](#client-setup).
|
|
31
|
+
|
|
32
|
+
The `examples/nested.ts` demo enables MCP — try:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
bun run examples/nested.ts mcp
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Client setup
|
|
39
|
+
|
|
40
|
+
### Cursor
|
|
41
|
+
|
|
42
|
+
Add a server entry under `mcpServers` in your Cursor MCP config:
|
|
43
|
+
|
|
44
|
+
```json
|
|
45
|
+
{
|
|
46
|
+
"mcpServers": {
|
|
47
|
+
"myapp": {
|
|
48
|
+
"command": "bun",
|
|
49
|
+
"args": ["run", "myapp.ts", "mcp"]
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Use your real binary or script path. For a compiled CLI, `command` can be the installed binary and `args` can be `["mcp"]` only.
|
|
56
|
+
|
|
57
|
+
### Other MCP hosts
|
|
58
|
+
|
|
59
|
+
Any host that spawns a subprocess and wires stdin/stdout works the same way: the **command** is your app, and **`mcp`** is the subcommand that starts the server.
|
|
60
|
+
|
|
61
|
+
## Configuration
|
|
62
|
+
|
|
63
|
+
Set `mcpServer` on the **program root only** (the `CliCommand` passed to `cliRun`). Validation rejects `mcpServer` on nested nodes.
|
|
64
|
+
|
|
65
|
+
| Field | Default | Purpose |
|
|
66
|
+
| --- | --- | --- |
|
|
67
|
+
| `name` | root `key` | `serverInfo.name` in the `initialize` response |
|
|
68
|
+
| `version` | `package.json` `version` in cwd, else `"0.0.0"` | `serverInfo.version` |
|
|
69
|
+
| `schemaResourceUri` | `"argsbarg://schema"` | URI for the schema resource |
|
|
70
|
+
|
|
71
|
+
Example with all fields:
|
|
72
|
+
|
|
73
|
+
```typescript
|
|
74
|
+
mcpServer: {
|
|
75
|
+
name: "nested-demo",
|
|
76
|
+
version: "1.0.0",
|
|
77
|
+
schemaResourceUri: "argsbarg://schema",
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Tools
|
|
82
|
+
|
|
83
|
+
Every **user-defined leaf command** in your schema becomes one MCP tool. Built-ins (`completion`, `mcp`) are not exposed as tools.
|
|
84
|
+
|
|
85
|
+
### Tool names
|
|
86
|
+
|
|
87
|
+
Tool names are derived from the command path, with each segment sanitized (non-alphanumeric characters become `_`) and joined with `_`.
|
|
88
|
+
|
|
89
|
+
| CLI invocation | Tool name |
|
|
90
|
+
| --- | --- |
|
|
91
|
+
| `myapp deploy` | `deploy` |
|
|
92
|
+
| `myapp stat owner lookup` | `stat_owner_lookup` |
|
|
93
|
+
| `nested.ts read` | `read` |
|
|
94
|
+
|
|
95
|
+
### Tool descriptions
|
|
96
|
+
|
|
97
|
+
Each tool’s `description` includes the human CLI path and the leaf’s help text, separated by an em dash:
|
|
98
|
+
|
|
99
|
+
| CLI path | MCP `description` |
|
|
100
|
+
| --- | --- |
|
|
101
|
+
| `stat owner lookup` | `stat owner lookup — Resolve owner info.` |
|
|
102
|
+
| `read` | `read — Print the first line of each file.` |
|
|
103
|
+
| (root leaf app) | `{root.key} — Tiny demo.` |
|
|
104
|
+
|
|
105
|
+
### Per-leaf visibility
|
|
106
|
+
|
|
107
|
+
Set `mcpTool: { enabled: false }` on a **leaf command** to hide it from `tools/list` while keeping it in the CLI and in `--schema` output:
|
|
108
|
+
|
|
109
|
+
```typescript
|
|
110
|
+
{
|
|
111
|
+
key: "debug",
|
|
112
|
+
description: "Internal diagnostics.",
|
|
113
|
+
mcpTool: { enabled: false },
|
|
114
|
+
handler: () => { /* ... */ },
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Omitted or `enabled: true` exposes the command (default). `mcpTool` is only valid on leaves — not on the program root or routing groups.
|
|
119
|
+
|
|
120
|
+
### Tool arguments
|
|
121
|
+
|
|
122
|
+
Each tool’s `inputSchema` is a JSON Schema object built from your CLI definition:
|
|
123
|
+
|
|
124
|
+
- **Options** — parent-scoped flags are included (e.g. `stat`’s `--json` appears on `stat_owner_lookup`). Presence options are `boolean`; string and number options match their `CliOptionKind`. Required options are listed in `required`.
|
|
125
|
+
- **Positionals** — one property per `CliPositional` on the leaf. Single-slot positionals are `string`; varargs tails (`argMax: 0`) are `string[]`. Required positionals are listed in `required`.
|
|
126
|
+
|
|
127
|
+
Arguments are a **flat JSON object** keyed by option and positional names (same names as in your schema, including hyphenated option names like `"user-name"`).
|
|
128
|
+
|
|
129
|
+
Example for `nested.ts stat owner lookup`:
|
|
130
|
+
|
|
131
|
+
```json
|
|
132
|
+
{
|
|
133
|
+
"path": "/path/to/file",
|
|
134
|
+
"user-name": "alice",
|
|
135
|
+
"json": true
|
|
136
|
+
}
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
This maps to argv: `stat owner lookup --json --user-name alice /path/to/file`.
|
|
140
|
+
|
|
141
|
+
Tool arguments use **long option names** only (`user-name`, not `-u`). Short aliases from your schema are not accepted in MCP tool calls.
|
|
142
|
+
|
|
143
|
+
### Tool results
|
|
144
|
+
|
|
145
|
+
On success (`isError: false`):
|
|
146
|
+
|
|
147
|
+
- **stdout** — first `content` text block with the handler’s captured stdout (raw, unchanged).
|
|
148
|
+
- **stderr** — when non-empty, a second `content` text block with trimmed stderr (no prefix). The block’s position signals stderr; hosts may label it themselves.
|
|
149
|
+
- **structuredContent** — when trimmed stdout is valid JSON, the parsed value is also returned per the [MCP tools spec](https://modelcontextprotocol.io/specification/draft/server/tools). Objects and arrays from flags like `--json` are the common case. JSON **primitives** (`true`, `42`, `"hello"`) are parsed too — a handler that prints the literal string `true` as human text would get `structuredContent: true`. Prefer objects for machine-readable output.
|
|
150
|
+
|
|
151
|
+
On failure (parse error, validation error, non-zero exit, thrown error), the message is returned as text content with `isError: true`. Handler stderr is included when present.
|
|
152
|
+
|
|
153
|
+
Help and `--schema` are not available through tool calls; use the schema resource or run the CLI directly for those.
|
|
154
|
+
|
|
155
|
+
## Schema resource
|
|
156
|
+
|
|
157
|
+
The server advertises one MCP resource: your full CLI tree as JSON — the same output as `myapp --schema`.
|
|
158
|
+
|
|
159
|
+
| Property | Value |
|
|
160
|
+
| --- | --- |
|
|
161
|
+
| Default URI | `argsbarg://schema` |
|
|
162
|
+
| MIME type | `application/json` |
|
|
163
|
+
| Contents | `cliSchemaJson(root)` — handlers omitted, built-ins excluded |
|
|
164
|
+
|
|
165
|
+
Agents can read this resource to discover commands, options, and positionals without guessing tool shapes.
|
|
166
|
+
|
|
167
|
+
## Protocol
|
|
168
|
+
|
|
169
|
+
- **Transport:** stdio, newline-delimited JSON (NDJSON).
|
|
170
|
+
- **JSON-RPC:** version `2.0`.
|
|
171
|
+
- **MCP protocol version:** `2024-11-05` (reported in `initialize`).
|
|
172
|
+
|
|
173
|
+
### Supported methods
|
|
174
|
+
|
|
175
|
+
| Method | Description |
|
|
176
|
+
| --- | --- |
|
|
177
|
+
| `initialize` | Returns capabilities (`tools`, `resources`) and `serverInfo`. |
|
|
178
|
+
| `notifications/initialized` | Acknowledged; no response (notification). |
|
|
179
|
+
| `ping` | Returns `{}`. |
|
|
180
|
+
| `tools/list` | Lists all tools with `name`, `description`, `inputSchema`. |
|
|
181
|
+
| `tools/call` | Runs a leaf handler; params: `name`, `arguments` (object). |
|
|
182
|
+
| `resources/list` | Lists the schema resource. |
|
|
183
|
+
| `resources/read` | Returns schema JSON; params: `uri`. |
|
|
184
|
+
|
|
185
|
+
Requests without an `id` are treated as notifications and do not receive a response (except `notifications/initialized`, which is ignored after parsing).
|
|
186
|
+
|
|
187
|
+
### Manual smoke test
|
|
188
|
+
|
|
189
|
+
```bash
|
|
190
|
+
printf '%s\n' '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}' | bun run examples/nested.ts mcp
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
You should get one JSON line on stdout with `result.capabilities` and `result.serverInfo`.
|
|
194
|
+
|
|
195
|
+
## Reserved names
|
|
196
|
+
|
|
197
|
+
When MCP is enabled:
|
|
198
|
+
|
|
199
|
+
- Do not declare a top-level command named **`mcp`** — it is reserved for the built-in subcommand.
|
|
200
|
+
- Do not declare a top-level command named **`completion`** — reserved for shell completions.
|
|
201
|
+
- Do not declare an option named **`schema`** — reserved for `--schema`.
|
|
202
|
+
|
|
203
|
+
Running `myapp mcp` without `mcpServer` on the root fails with an error (exit 1).
|
|
204
|
+
|
|
205
|
+
## Design notes
|
|
206
|
+
|
|
207
|
+
- **Zero extra dependencies** — hand-rolled NDJSON JSON-RPC on top of ArgsBarg’s existing parser and schema.
|
|
208
|
+
- **Same handlers** — tool calls run your real leaf handlers via an internal invoke path that captures stdout/stderr and does not exit the process, so the MCP server can handle many requests in one process.
|
|
209
|
+
- **User schema only** — tool dispatch uses your program root, not merged presentation builtins.
|
|
210
|
+
|
|
211
|
+
For the `--schema` export used by the resource, see the main README built-ins section.
|
package/examples/nested.ts
CHANGED
|
@@ -12,6 +12,7 @@ import { cliRun, CliCommand, CliOptionKind, CliFallbackMode } from "../src/index
|
|
|
12
12
|
const cli: CliCommand = {
|
|
13
13
|
key: "nested.ts",
|
|
14
14
|
description: "Nested groups demo.",
|
|
15
|
+
mcpServer: { name: "nested-demo", version: "1.0.0" },
|
|
15
16
|
commands: [
|
|
16
17
|
{
|
|
17
18
|
key: "stat",
|
package/index.d.ts
CHANGED
|
@@ -66,6 +66,24 @@ export interface CliPositional {
|
|
|
66
66
|
*/
|
|
67
67
|
argMax?: number;
|
|
68
68
|
}
|
|
69
|
+
/**
|
|
70
|
+
* Root-only. Enables `myapp mcp` and MCP stdio server metadata.
|
|
71
|
+
*/
|
|
72
|
+
export interface CliMcpServerConfig {
|
|
73
|
+
/** `initialize` serverInfo.name (default: root `key`). */
|
|
74
|
+
name?: string;
|
|
75
|
+
/** `initialize` serverInfo.version (default: see resolveMcpVersion). */
|
|
76
|
+
version?: string;
|
|
77
|
+
/** Resource URI for schema export (default: `"argsbarg://schema"`). */
|
|
78
|
+
schemaResourceUri?: string;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Leaf-only. Controls how this command appears as an MCP tool.
|
|
82
|
+
*/
|
|
83
|
+
export interface CliMcpToolConfig {
|
|
84
|
+
/** When `false`, omit from `tools/list` (default: exposed). */
|
|
85
|
+
enabled?: boolean;
|
|
86
|
+
}
|
|
69
87
|
/**
|
|
70
88
|
* Base properties shared by all command nodes.
|
|
71
89
|
*/
|
|
@@ -78,6 +96,10 @@ export interface CliCommandBase {
|
|
|
78
96
|
notes?: string;
|
|
79
97
|
/** Global or command-level flags/options. */
|
|
80
98
|
options?: CliOption[];
|
|
99
|
+
/** Root-only. When set, enables the `mcp` built-in subcommand. */
|
|
100
|
+
mcpServer?: CliMcpServerConfig;
|
|
101
|
+
/** Leaf-only. Per-tool MCP exposure and metadata. */
|
|
102
|
+
mcpTool?: CliMcpToolConfig;
|
|
81
103
|
}
|
|
82
104
|
/**
|
|
83
105
|
* A command node: either a routing group (has commands) or a leaf (has handler).
|
package/package.json
CHANGED
package/src/completion.ts
CHANGED
|
@@ -522,7 +522,7 @@ export function completionZshScript(schema: CliCommand): string {
|
|
|
522
522
|
}
|
|
523
523
|
|
|
524
524
|
/**
|
|
525
|
-
* Returns a schema suitable for help display, including
|
|
525
|
+
* Returns a schema suitable for help display, including reserved built-in subtrees.
|
|
526
526
|
* Routing roots get `completion` merged; leaf roots are wrapped as a tiny router.
|
|
527
527
|
*/
|
|
528
528
|
export function cliPresentationRoot(root: CliCommand): CliCommand {
|
|
@@ -534,15 +534,33 @@ export function cliPresentationRoot(root: CliCommand): CliCommand {
|
|
|
534
534
|
key: root.key,
|
|
535
535
|
description: root.description,
|
|
536
536
|
options: root.options,
|
|
537
|
-
commands:
|
|
537
|
+
commands: presentationBuiltins(root),
|
|
538
538
|
} as CliCommand;
|
|
539
539
|
}
|
|
540
540
|
return {
|
|
541
541
|
...root,
|
|
542
|
-
commands: [...(root.commands ?? []),
|
|
542
|
+
commands: [...(root.commands ?? []), ...presentationBuiltins(root)],
|
|
543
543
|
} as CliCommand;
|
|
544
544
|
}
|
|
545
545
|
|
|
546
|
+
/** Built-in commands shown in help and merged for routing CLIs. */
|
|
547
|
+
function presentationBuiltins(root: CliCommand): CliCommand[] {
|
|
548
|
+
const cmds: CliCommand[] = [cliBuiltinCompletionGroup(root.key)];
|
|
549
|
+
if (root.mcpServer !== undefined) {
|
|
550
|
+
cmds.push(cliBuiltinMcpCommand());
|
|
551
|
+
}
|
|
552
|
+
return cmds;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
/** Builds the static `mcp` leaf command (merged when `root.mcpServer` is set). */
|
|
556
|
+
export function cliBuiltinMcpCommand(): CliCommand {
|
|
557
|
+
return {
|
|
558
|
+
key: "mcp",
|
|
559
|
+
description: "Run as an MCP server over stdio (for AI agents).",
|
|
560
|
+
handler: () => {},
|
|
561
|
+
};
|
|
562
|
+
}
|
|
563
|
+
|
|
546
564
|
/**
|
|
547
565
|
* Builds the static `completion` / `bash` / `zsh` command subtree (merged into the program root at runtime).
|
|
548
566
|
*/
|