argsbarg 4.0.3 → 4.0.4
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 +13 -1
- package/README.md +1 -1
- package/docs/ai-skills.md +2 -0
- package/docs/bundled-docs.md +1 -0
- package/docs/mcp.md +17 -2
- package/examples/consumer-app/README.md +1 -0
- package/examples/consumer-app/src/program.ts +2 -10
- package/examples/mcp-test.ts +6 -0
- package/package.json +1 -1
- package/src/docs/mcp-guide.ts +14 -3
- package/src/docs/mcp-resources.test.ts +63 -0
- package/src/docs/mcp-resources.ts +68 -0
- package/src/mcp/claude.test.ts +9 -3
- package/src/mcp/claude.ts +7 -9
- package/src/mcp/tools.ts +4 -1
- package/src/mcp.integration.test.ts +17 -0
- package/src/parse.test.ts +45 -3
- package/src/skill/generate.ts +71 -1
- package/src/skill/hint.ts +5 -0
- package/src/validate.ts +9 -4
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [4.0.4] - 2026-06-25
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **MCP docs topic resources** — when `docs.enabled` and `mcpServer.enabled`, each user `docs.topics` key is auto-exposed as `<mcpId>://docs/<topicKey>` (`text/markdown`, same body as `myapp docs <topic>`). Built-in `docs schema` / `api` / `skill` / `mcp` are not auto-exposed.
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
|
|
18
|
+
- **Claude Code plugin skill** — `mcp bundle` plugin zip includes an MCP routing `SKILL.md` only (no shell catalog, no `reference.md`). `install --skill` unchanged.
|
|
19
|
+
- **Validation** — `mcpServer.resources` URIs that collide with auto docs topic resources are rejected at schema validation time.
|
|
20
|
+
|
|
10
21
|
## [4.0.3] - 2026-06-24
|
|
11
22
|
|
|
12
23
|
|
|
@@ -458,7 +469,8 @@ const cli = { ... } satisfies CliProgram; // or : CliProgram
|
|
|
458
469
|
- 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`).
|
|
459
470
|
- Imports: use `CliPositional` where needed; replace `CliOptionDef` with `CliOption` or `CliPositional` as appropriate.
|
|
460
471
|
|
|
461
|
-
[Unreleased]: https://github.com/bdombro/bun-argsbarg/compare/v4.0.
|
|
472
|
+
[Unreleased]: https://github.com/bdombro/bun-argsbarg/compare/v4.0.4...HEAD
|
|
473
|
+
[4.0.4]: https://github.com/bdombro/bun-argsbarg/releases/tag/v4.0.4
|
|
462
474
|
[4.0.3]: https://github.com/bdombro/bun-argsbarg/releases/tag/v4.0.3
|
|
463
475
|
[4.0.2]: https://github.com/bdombro/bun-argsbarg/releases/tag/v4.0.2
|
|
464
476
|
[4.0.1]: https://github.com/bdombro/bun-argsbarg/releases/tag/v4.0.1
|
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
[](https://www.npmjs.com/package/argsbarg)
|
|
7
7
|
[](https://bun.sh)
|
|
8
8
|
|
|
9
|
-
Build beautiful, well-behaved CLI apps with Bun — **no third-party runtime dependencies**.
|
|
9
|
+
Build beautiful, well-behaved CLI+MCP apps with Bun — **no third-party runtime dependencies**.
|
|
10
10
|
|
|
11
11
|
Why another CLI parser?
|
|
12
12
|
|
package/docs/ai-skills.md
CHANGED
|
@@ -48,6 +48,8 @@ Skills describe **shell invocation only** — no MCP setup, `mcp.json`, or `tool
|
|
|
48
48
|
|
|
49
49
|
`SKILL.md` is the routing index; `reference.md` matches `docs api`. Prefer `install --skill` over `docs skill` for agents. See [cli-program.md](cli-program.md).
|
|
50
50
|
|
|
51
|
+
**Claude Code plugin** (`mcp bundle` → `dist/claude-plugin/<name>.zip`) ships a separate MCP pointer skill (`SKILL.md` only) that routes agents to the bundled MCP server. `install --skill` remains shell-only.
|
|
52
|
+
|
|
51
53
|
See also:
|
|
52
54
|
|
|
53
55
|
- [Bundled docs](bundled-docs.md) — `docs` config and compile-time imports
|
package/docs/bundled-docs.md
CHANGED
|
@@ -104,6 +104,7 @@ All `docs` subcommands are hidden from MCP `tools/list` (`mcpTool: { enabled: fa
|
|
|
104
104
|
| `docs api` | Print command tree markdown to stdout |
|
|
105
105
|
| `docs schema` | Print command tree JSON to stdout |
|
|
106
106
|
| `docs` | Bundled markdown topics on stdout |
|
|
107
|
+
| MCP docs topic resources | User `docs.topics` on the MCP wire (`<mcpId>://docs/<topic>`) when docs + MCP enabled |
|
|
107
108
|
| `mcp` | Callable tools + schema resource |
|
|
108
109
|
|
|
109
110
|
Do not declare a top-level command named **`docs`** when `docs.enabled` is `true` — it is reserved.
|
package/docs/mcp.md
CHANGED
|
@@ -244,6 +244,20 @@ The built-in schema resource (default URI `<sanitized-key>://schema`, e.g. `nest
|
|
|
244
244
|
| MIME type | `application/json` |
|
|
245
245
|
| Contents | `cliSchemaJson(root)` — handlers omitted, built-ins excluded |
|
|
246
246
|
|
|
247
|
+
### Auto docs topic resources
|
|
248
|
+
|
|
249
|
+
When both **`docs.enabled`** and **`mcpServer.enabled`** are true, each user key in **`docs.topics`** is also exposed as an MCP resource:
|
|
250
|
+
|
|
251
|
+
| Property | Value |
|
|
252
|
+
| --- | --- |
|
|
253
|
+
| URI | `<sanitized root key>://docs/<topicKey>` (e.g. `myapp://docs/readme`) |
|
|
254
|
+
| MIME type | `text/markdown` |
|
|
255
|
+
| Contents | Same body as `myapp docs <topicKey>` |
|
|
256
|
+
|
|
257
|
+
Built-in docs subcommands (`schema`, `api`, `skill`, `mcp`) are **not** auto-exposed — use the schema resource, `install --skill`, or CLI `docs` instead. `docs` subcommands remain hidden from MCP `tools/list`.
|
|
258
|
+
|
|
259
|
+
Custom `mcpServer.resources` URIs must not collide with the schema URI or any auto docs topic URI (validated at program compile time).
|
|
260
|
+
|
|
247
261
|
Add custom resources on the program root:
|
|
248
262
|
|
|
249
263
|
```typescript
|
|
@@ -261,7 +275,7 @@ mcpServer: {
|
|
|
261
275
|
},
|
|
262
276
|
```
|
|
263
277
|
|
|
264
|
-
URIs must be unique and must not equal `schemaResourceUri
|
|
278
|
+
URIs must be unique and must not equal `schemaResourceUri` or any auto docs topic URI (`<mcpId>://docs/<topicKey>`). `load()` runs synchronously at `resources/read` time.
|
|
265
279
|
|
|
266
280
|
## Invocation context
|
|
267
281
|
|
|
@@ -384,9 +398,10 @@ Manifest metadata is generated from your schema (`mcpServerId`, tools, `program.
|
|
|
384
398
|
.mcp.json
|
|
385
399
|
bin/myapp
|
|
386
400
|
skills/<dirName>/SKILL.md
|
|
387
|
-
skills/<dirName>/reference.md
|
|
388
401
|
```
|
|
389
402
|
|
|
403
|
+
The bundled `SKILL.md` is an **MCP routing stub** — it tells Claude to use the plugin’s MCP toolset (server id, `tools/list`, schema resource). It is not a shell CLI catalog and does not include `reference.md`. Use **`install --skill`** for a persisted shell-oriented skill bundle.
|
|
404
|
+
|
|
390
405
|
Load locally with `claude --plugin-dir ./dist/claude-plugin/myapp.zip`.
|
|
391
406
|
|
|
392
407
|
Bare **`myapp mcp`** still runs the stdio MCP server (unchanged for `install --mcp` and MCP hosts). Use **`install --mcp`** for Cursor, Claude Code, Claude Desktop, and OpenCode JSON config.
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
| `outputSchema` | `src/commands/status/types.ts` (`StatusJsonOutput`) → `schemas/outputSchemas.ts` |
|
|
12
12
|
| Schemagen | `scripts/schemagen.ts` + `scripts/schemagen/discover-schema-roots.ts` |
|
|
13
13
|
| Handler access | `ctx.appConfig` in `src/program.ts` |
|
|
14
|
+
| MCP doc topics | `docs.topics` auto-exposed as `<key>://docs/<topic>` resources when docs + MCP enabled |
|
|
14
15
|
| Package import | `from "argsbarg"` (not relative to argsbarg `src/`) |
|
|
15
16
|
|
|
16
17
|
## Quick start (in this repo)
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
CliOptionKind,
|
|
9
9
|
type CliProgram,
|
|
10
10
|
} from "argsbarg";
|
|
11
|
+
import readmeText from "../README.md" with { type: "text" };
|
|
11
12
|
import { APP_CONFIG_JSON_SCHEMA } from "../schemas/configSchemas.ts";
|
|
12
13
|
import { STATUS_JSON_OUTPUT_SCHEMA } from "../schemas/outputSchemas.ts";
|
|
13
14
|
import type { StatusJsonOutput } from "./commands/status/types.ts";
|
|
@@ -46,21 +47,12 @@ export const program = {
|
|
|
46
47
|
enabled: true,
|
|
47
48
|
topics: {
|
|
48
49
|
readme: {
|
|
49
|
-
text:
|
|
50
|
+
text: readmeText,
|
|
50
51
|
},
|
|
51
52
|
},
|
|
52
53
|
},
|
|
53
54
|
mcpServer: {
|
|
54
55
|
enabled: true,
|
|
55
|
-
resources: [
|
|
56
|
-
{
|
|
57
|
-
uri: "consumer-app://readme",
|
|
58
|
-
name: "readme",
|
|
59
|
-
description: "Bundled readme topic.",
|
|
60
|
-
mimeType: "text/plain",
|
|
61
|
-
load: () => "# consumer-app\n\nKitchen-sink reference.\n",
|
|
62
|
-
},
|
|
63
|
-
],
|
|
64
56
|
},
|
|
65
57
|
install: {
|
|
66
58
|
updateGetLatest: async () => ({
|
package/examples/mcp-test.ts
CHANGED
package/package.json
CHANGED
package/src/docs/mcp-guide.ts
CHANGED
|
@@ -10,6 +10,8 @@ import {
|
|
|
10
10
|
} from "../mcp/tools.ts";
|
|
11
11
|
import { collectOptionDefs } from "../parse.ts";
|
|
12
12
|
import { CliOptionKind, type CliProgram } from "../types.ts";
|
|
13
|
+
import { resolveDocsTopicResourceUri } from "./mcp-resources.ts";
|
|
14
|
+
import { docsEnabled, docsUserTopicKeys } from "./resolve.ts";
|
|
13
15
|
|
|
14
16
|
/** Extra host notes for generated `docs mcp` (manual fallbacks and ChatGPT Connectors). */
|
|
15
17
|
function appendManualHostSetup(lines: string[], root: CliProgram, serverId: string): void {
|
|
@@ -210,10 +212,19 @@ export function generateMcpGuide(root: CliProgram): string {
|
|
|
210
212
|
"| `tools/list` | Callable tools for exposed leaf commands |",
|
|
211
213
|
"| `tools/call` | Runs handlers headlessly; JSON stdout becomes `structuredContent` when valid |",
|
|
212
214
|
`| Schema resource | \`${schemaUri}\` — same JSON as \`${root.key} docs schema\` |`,
|
|
213
|
-
"",
|
|
214
|
-
"## Exposed tools",
|
|
215
|
-
"",
|
|
216
215
|
);
|
|
216
|
+
if (docsEnabled(root)) {
|
|
217
|
+
const docs = root.docs;
|
|
218
|
+
if (docs) {
|
|
219
|
+
for (const key of docsUserTopicKeys(docs)) {
|
|
220
|
+
const uri = resolveDocsTopicResourceUri(root, key);
|
|
221
|
+
lines.push(
|
|
222
|
+
`| Docs topic \`${key}\` | \`${uri}\` — same markdown as \`${root.key} docs ${key}\` |`,
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
lines.push("", "## Exposed tools", "");
|
|
217
228
|
|
|
218
229
|
if (tools.length === 0) {
|
|
219
230
|
lines.push("(No MCP tools exposed.)", "");
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { expect, test } from "bun:test";
|
|
2
|
+
import type { CliProgram } from "../types.ts";
|
|
3
|
+
import {
|
|
4
|
+
defaultDocsTopicResourceUri,
|
|
5
|
+
docsMcpResources,
|
|
6
|
+
reservedDocsTopicResourceUris,
|
|
7
|
+
resolveDocsTopicResourceUri,
|
|
8
|
+
} from "./mcp-resources.ts";
|
|
9
|
+
|
|
10
|
+
function fixture(opts?: { docs?: boolean; mcp?: boolean }): CliProgram {
|
|
11
|
+
const docs = opts?.docs !== false;
|
|
12
|
+
const mcp = opts?.mcp !== false;
|
|
13
|
+
return {
|
|
14
|
+
key: "my-app",
|
|
15
|
+
version: "1.0.0",
|
|
16
|
+
description: "Test.",
|
|
17
|
+
...(docs
|
|
18
|
+
? {
|
|
19
|
+
docs: {
|
|
20
|
+
enabled: true,
|
|
21
|
+
topics: {
|
|
22
|
+
readme: { text: "# Readme\n", description: "User guide." },
|
|
23
|
+
arch: { text: "# Arch\n" },
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
}
|
|
27
|
+
: {}),
|
|
28
|
+
...(mcp ? { mcpServer: { enabled: true } } : {}),
|
|
29
|
+
handler: () => {},
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
test("defaultDocsTopicResourceUri", () => {
|
|
34
|
+
expect(defaultDocsTopicResourceUri("my_app", "readme")).toBe("my_app://docs/readme");
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("resolveDocsTopicResourceUri sanitizes program key", () => {
|
|
38
|
+
expect(resolveDocsTopicResourceUri(fixture(), "readme")).toBe("my_app://docs/readme");
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("docsMcpResources when docs and MCP enabled", () => {
|
|
42
|
+
const resources = docsMcpResources(fixture());
|
|
43
|
+
expect(resources.map((r) => r.uri)).toEqual(["my_app://docs/readme", "my_app://docs/arch"]);
|
|
44
|
+
expect(resources[0]?.name).toBe("readme");
|
|
45
|
+
expect(resources[0]?.mimeType).toBe("text/markdown");
|
|
46
|
+
expect(resources[0]?.description).toBe("User guide.");
|
|
47
|
+
expect(resources[0]?.load()).toBe("# Readme\n");
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("docsMcpResources empty when docs disabled", () => {
|
|
51
|
+
expect(docsMcpResources(fixture({ docs: false }))).toEqual([]);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("docsMcpResources empty when MCP disabled", () => {
|
|
55
|
+
expect(docsMcpResources(fixture({ mcp: false }))).toEqual([]);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("reservedDocsTopicResourceUris matches docsMcpResources URIs", () => {
|
|
59
|
+
const program = fixture();
|
|
60
|
+
expect(reservedDocsTopicResourceUris(program)).toEqual(
|
|
61
|
+
docsMcpResources(program).map((r) => r.uri),
|
|
62
|
+
);
|
|
63
|
+
});
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Auto MCP resources for user docs.topics when docs and MCP are both enabled.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { CliProgram } from "../types.ts";
|
|
6
|
+
import {
|
|
7
|
+
docsEnabled,
|
|
8
|
+
docsTopicContent,
|
|
9
|
+
docsTopicDescription,
|
|
10
|
+
docsUserTopicKeys,
|
|
11
|
+
} from "./resolve.ts";
|
|
12
|
+
|
|
13
|
+
/** Default URI pattern for a docs topic MCP resource (`<mcpId>://docs/<topicKey>`). */
|
|
14
|
+
export function defaultDocsTopicResourceUri(mcpId: string, topicKey: string): string {
|
|
15
|
+
return `${mcpId}://docs/${topicKey}`;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Sanitized MCP server id from program key (matches mcpServerId in mcp/tools.ts). */
|
|
19
|
+
function mcpIdFromProgram(program: CliProgram): string {
|
|
20
|
+
return program.key.replace(/[^a-zA-Z0-9]/g, "_");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Resolved URI for one user docs topic resource. */
|
|
24
|
+
export function resolveDocsTopicResourceUri(program: CliProgram, topicKey: string): string {
|
|
25
|
+
return defaultDocsTopicResourceUri(mcpIdFromProgram(program), topicKey);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** All auto-generated docs topic resources (empty when docs or MCP disabled). */
|
|
29
|
+
export function docsMcpResources(program: CliProgram): {
|
|
30
|
+
uri: string;
|
|
31
|
+
name: string;
|
|
32
|
+
description?: string;
|
|
33
|
+
mimeType: string;
|
|
34
|
+
load: () => string;
|
|
35
|
+
}[] {
|
|
36
|
+
if (!docsEnabled(program) || program.mcpServer?.enabled !== true) {
|
|
37
|
+
return [];
|
|
38
|
+
}
|
|
39
|
+
const docs = program.docs;
|
|
40
|
+
if (!docs) {
|
|
41
|
+
return [];
|
|
42
|
+
}
|
|
43
|
+
return docsUserTopicKeys(docs).map((key) => {
|
|
44
|
+
const topic = docs.topics[key];
|
|
45
|
+
if (!topic) {
|
|
46
|
+
throw new Error(`docs topic missing: ${key}`);
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
uri: resolveDocsTopicResourceUri(program, key),
|
|
50
|
+
name: key,
|
|
51
|
+
description: docsTopicDescription(key, topic.description),
|
|
52
|
+
mimeType: "text/markdown",
|
|
53
|
+
load: () => docsTopicContent(program, key),
|
|
54
|
+
};
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Reserved MCP resource URIs from auto docs topics (for validation). */
|
|
59
|
+
export function reservedDocsTopicResourceUris(program: CliProgram): string[] {
|
|
60
|
+
if (!docsEnabled(program) || program.mcpServer?.enabled !== true) {
|
|
61
|
+
return [];
|
|
62
|
+
}
|
|
63
|
+
const docs = program.docs;
|
|
64
|
+
if (!docs) {
|
|
65
|
+
return [];
|
|
66
|
+
}
|
|
67
|
+
return docsUserTopicKeys(docs).map((key) => resolveDocsTopicResourceUri(program, key));
|
|
68
|
+
}
|
package/src/mcp/claude.test.ts
CHANGED
|
@@ -55,7 +55,7 @@ describe("claude plugin", () => {
|
|
|
55
55
|
expect(paths.pluginZipPath).toBe(join(cwd, "dist", "claude-plugin", "myapp.zip"));
|
|
56
56
|
});
|
|
57
57
|
|
|
58
|
-
test("packClaudePlugin writes zip with
|
|
58
|
+
test("packClaudePlugin writes zip with MCP pointer skill only", () => {
|
|
59
59
|
const work = mkdtempSync(join(tmpdir(), "claude-plugin-test-"));
|
|
60
60
|
try {
|
|
61
61
|
const dist = join(work, "dist");
|
|
@@ -64,8 +64,14 @@ describe("claude plugin", () => {
|
|
|
64
64
|
writeFileSync(binaryPath, "#!/bin/sh\n", { mode: 0o755 });
|
|
65
65
|
const paths = defaultClaudePluginPaths(configFixture, work);
|
|
66
66
|
packClaudePlugin(configFixture, { cwd: work, binaryPath });
|
|
67
|
-
const
|
|
68
|
-
expect(
|
|
67
|
+
const zip = readFileSync(paths.pluginZipPath);
|
|
68
|
+
expect(zip.length).toBeGreaterThan(0);
|
|
69
|
+
const zipText = zip.toString("utf8");
|
|
70
|
+
expect(zipText).toContain("skills/myapp/SKILL.md");
|
|
71
|
+
expect(zipText).toContain("MCP toolset");
|
|
72
|
+
expect(zipText).toContain("Server id: `myapp`");
|
|
73
|
+
expect(zipText).not.toContain("skills/myapp/reference.md");
|
|
74
|
+
expect(zipText).not.toContain("Invoke via shell");
|
|
69
75
|
} finally {
|
|
70
76
|
rmSync(work, { recursive: true, force: true });
|
|
71
77
|
}
|
package/src/mcp/claude.ts
CHANGED
|
@@ -17,11 +17,11 @@ import {
|
|
|
17
17
|
import { tmpdir } from "node:os";
|
|
18
18
|
import { basename, join, relative, resolve } from "node:path";
|
|
19
19
|
import { buildPluginMcpEnvMapping, buildProgramUserConfig } from "../config/manifest.ts";
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
20
|
+
import { generatePluginSkillBundle } from "../skill/generate.ts";
|
|
21
|
+
import { applyPluginSkillHint } from "../skill/hint.ts";
|
|
22
22
|
import type { CliMcpBundleConfig, CliProgram } from "../types.ts";
|
|
23
23
|
import { defaultMcpBundlePaths, type PackMcpBundleOpts } from "./bundle.ts";
|
|
24
|
-
import { mcpServerId
|
|
24
|
+
import { mcpServerId } from "./tools.ts";
|
|
25
25
|
import { zipStore } from "./zip.ts";
|
|
26
26
|
|
|
27
27
|
const DIST_DIR = "dist";
|
|
@@ -116,13 +116,12 @@ function writePluginTree(
|
|
|
116
116
|
binaryPath: string,
|
|
117
117
|
binaryName: string,
|
|
118
118
|
): void {
|
|
119
|
-
const
|
|
120
|
-
const
|
|
121
|
-
const hinted = applySkillBundleHints(program, bundle.skillMd, bundle.referenceMd);
|
|
119
|
+
const bundle = generatePluginSkillBundle(program);
|
|
120
|
+
const skillMd = applyPluginSkillHint(program, bundle.skillMd);
|
|
122
121
|
|
|
123
122
|
mkdirSync(join(pluginRoot, ".claude-plugin"), { recursive: true });
|
|
124
123
|
mkdirSync(join(pluginRoot, "bin"), { recursive: true });
|
|
125
|
-
mkdirSync(join(pluginRoot, "skills",
|
|
124
|
+
mkdirSync(join(pluginRoot, "skills", bundle.dirName), { recursive: true });
|
|
126
125
|
|
|
127
126
|
writeFileSync(
|
|
128
127
|
join(pluginRoot, ".claude-plugin", "plugin.json"),
|
|
@@ -133,8 +132,7 @@ function writePluginTree(
|
|
|
133
132
|
`${JSON.stringify(generatePluginMcpJson(program, binaryName), null, 2)}\n`,
|
|
134
133
|
);
|
|
135
134
|
cpSync(binaryPath, join(pluginRoot, "bin", binaryName), { mode: 0o755 });
|
|
136
|
-
writeFileSync(join(pluginRoot, "skills",
|
|
137
|
-
writeFileSync(join(pluginRoot, "skills", skillDirName, "reference.md"), hinted.referenceMd);
|
|
135
|
+
writeFileSync(join(pluginRoot, "skills", bundle.dirName, "SKILL.md"), skillMd);
|
|
138
136
|
}
|
|
139
137
|
|
|
140
138
|
/**
|
package/src/mcp/tools.ts
CHANGED
|
@@ -3,6 +3,7 @@ This module maps CliProgram leaf nodes to MCP tool definitions and converts
|
|
|
3
3
|
flat JSON tool arguments into argv for Cli.invoke.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
import { docsMcpResources } from "../docs/mcp-resources.ts";
|
|
6
7
|
import { cliResolveNotes } from "../help.ts";
|
|
7
8
|
import { visibleOptions } from "../hidden.ts";
|
|
8
9
|
import { collectOptionDefs } from "../parse.ts";
|
|
@@ -26,6 +27,8 @@ export function defaultMcpSchemaUri(mcpId: string): string {
|
|
|
26
27
|
return `${mcpId}://schema`;
|
|
27
28
|
}
|
|
28
29
|
|
|
30
|
+
export { defaultDocsTopicResourceUri, resolveDocsTopicResourceUri } from "../docs/mcp-resources.ts";
|
|
31
|
+
|
|
29
32
|
/** Sanitizes a command key segment for MCP tool names and server identity. */
|
|
30
33
|
export function sanitizeToolSegment(key: string): string {
|
|
31
34
|
return key.replace(/[^a-zA-Z0-9]/g, "_");
|
|
@@ -209,7 +212,7 @@ export function allMcpResources(root: CliProgram): McpResourceEntry[] {
|
|
|
209
212
|
mimeType: r.mimeType ?? "text/plain",
|
|
210
213
|
load: r.load,
|
|
211
214
|
}));
|
|
212
|
-
return [builtIn, ...user];
|
|
215
|
+
return [builtIn, ...docsMcpResources(root), ...user];
|
|
213
216
|
}
|
|
214
217
|
|
|
215
218
|
/** Recursively collects MCP tool definitions from user leaf commands. */
|
|
@@ -480,9 +480,26 @@ test("MCP resources/list includes custom resource", async () => {
|
|
|
480
480
|
const res = responses.get(10) as { result: { resources: { uri: string }[] } };
|
|
481
481
|
const uris = res.result.resources.map((r) => r.uri);
|
|
482
482
|
expect(uris).toContain("mcp_test://schema");
|
|
483
|
+
expect(uris).toContain("mcp_test://docs/readme");
|
|
483
484
|
expect(uris).toContain("test://hello");
|
|
484
485
|
});
|
|
485
486
|
|
|
487
|
+
test("MCP resources/read returns docs topic resource body", async () => {
|
|
488
|
+
const responses = await mcpRequest(
|
|
489
|
+
[
|
|
490
|
+
{
|
|
491
|
+
jsonrpc: "2.0",
|
|
492
|
+
id: 13,
|
|
493
|
+
method: "resources/read",
|
|
494
|
+
params: { uri: "mcp_test://docs/readme" },
|
|
495
|
+
},
|
|
496
|
+
],
|
|
497
|
+
{ script: "examples/mcp-test.ts" },
|
|
498
|
+
);
|
|
499
|
+
const res = responses.get(13) as { result: { contents: { text: string }[] } };
|
|
500
|
+
expect(res.result.contents[0]?.text).toBe("# MCP test readme\n");
|
|
501
|
+
});
|
|
502
|
+
|
|
486
503
|
test("MCP resources/read returns custom resource body", async () => {
|
|
487
504
|
const responses = await mcpRequest(
|
|
488
505
|
[{ jsonrpc: "2.0", id: 11, method: "resources/read", params: { uri: "test://hello" } }],
|
package/src/parse.test.ts
CHANGED
|
@@ -20,7 +20,7 @@ import {
|
|
|
20
20
|
} from "./mcp/tools.ts";
|
|
21
21
|
import { ParseKind, parse, postParseValidate } from "./parse.ts";
|
|
22
22
|
import { cliSchemaJson } from "./schema.ts";
|
|
23
|
-
import { generateSkillBundle } from "./skill/generate.ts";
|
|
23
|
+
import { generatePluginSkillBundle, generateSkillBundle } from "./skill/generate.ts";
|
|
24
24
|
import { cliSkillInstall } from "./skill/install.ts";
|
|
25
25
|
import {
|
|
26
26
|
enumMcpFixture,
|
|
@@ -835,7 +835,7 @@ test("cliValidateProgram rejects resource URI matching default schema URI", () =
|
|
|
835
835
|
},
|
|
836
836
|
commands: [{ key: "x", description: "", handler: () => {} }],
|
|
837
837
|
});
|
|
838
|
-
expect(() => cliValidateProgram(root)).toThrow(/conflicts with
|
|
838
|
+
expect(() => cliValidateProgram(root)).toThrow(/conflicts with built-in schema resource/);
|
|
839
839
|
});
|
|
840
840
|
|
|
841
841
|
test("cliValidateProgram rejects resource URI matching schemaResourceUri", () => {
|
|
@@ -849,7 +849,21 @@ test("cliValidateProgram rejects resource URI matching schemaResourceUri", () =>
|
|
|
849
849
|
},
|
|
850
850
|
commands: [{ key: "x", description: "", handler: () => {} }],
|
|
851
851
|
});
|
|
852
|
-
expect(() => cliValidateProgram(root)).toThrow(/conflicts with
|
|
852
|
+
expect(() => cliValidateProgram(root)).toThrow(/conflicts with built-in schema resource/);
|
|
853
|
+
});
|
|
854
|
+
|
|
855
|
+
test("cliValidateProgram rejects resource URI matching auto docs topic", () => {
|
|
856
|
+
const root = testProgram({
|
|
857
|
+
key: "app",
|
|
858
|
+
description: "",
|
|
859
|
+
docs: { enabled: true, topics: { readme: { text: "# r\n" } } },
|
|
860
|
+
mcpServer: {
|
|
861
|
+
enabled: true,
|
|
862
|
+
resources: [{ uri: "app://docs/readme", name: "dup", load: () => "" }],
|
|
863
|
+
},
|
|
864
|
+
commands: [{ key: "x", description: "", handler: () => {} }],
|
|
865
|
+
});
|
|
866
|
+
expect(() => cliValidateProgram(root)).toThrow(/conflicts with auto docs topic resource/);
|
|
853
867
|
});
|
|
854
868
|
|
|
855
869
|
test("allMcpResources includes custom resources", () => {
|
|
@@ -867,6 +881,20 @@ test("allMcpResources includes custom resources", () => {
|
|
|
867
881
|
expect(resources.map((r) => r.uri)).toContain("test://x");
|
|
868
882
|
});
|
|
869
883
|
|
|
884
|
+
test("allMcpResources includes docs topic resources", () => {
|
|
885
|
+
const root = testProgram({
|
|
886
|
+
key: "app",
|
|
887
|
+
description: "",
|
|
888
|
+
docs: { enabled: true, topics: { readme: { text: "# hi\n" } } },
|
|
889
|
+
mcpServer: { enabled: true },
|
|
890
|
+
commands: [{ key: "leaf", description: "", handler: () => {} }],
|
|
891
|
+
});
|
|
892
|
+
const resources = allMcpResources(root);
|
|
893
|
+
expect(resources.map((r) => r.uri)).toContain("app://docs/readme");
|
|
894
|
+
const readme = resources.find((r) => r.uri === "app://docs/readme");
|
|
895
|
+
expect(readme?.load()).toBe("# hi\n");
|
|
896
|
+
});
|
|
897
|
+
|
|
870
898
|
test("applyShellEnv merges PATH and preserves host vars", () => {
|
|
871
899
|
const origPath = process.env.PATH ?? "";
|
|
872
900
|
const origHome = process.env.HOME;
|
|
@@ -1116,6 +1144,20 @@ test("generateSkillBundle includes frontmatter and compact command index", () =>
|
|
|
1116
1144
|
expect(bundle.referenceMd).not.toContain("```json");
|
|
1117
1145
|
});
|
|
1118
1146
|
|
|
1147
|
+
test("generatePluginSkillBundle is MCP routing stub without shell catalog", () => {
|
|
1148
|
+
const bundle = generatePluginSkillBundle(nestedMcpFixture);
|
|
1149
|
+
expect(bundle.dirName).toBe("nested_ts");
|
|
1150
|
+
expect(bundle.skillMd).toMatch(/^---\nname: nested_ts\n/);
|
|
1151
|
+
expect(bundle.skillMd).toContain("MCP toolset");
|
|
1152
|
+
expect(bundle.skillMd).toContain("Server id: `nested_ts`");
|
|
1153
|
+
expect(bundle.skillMd).toContain("nested_ts://schema");
|
|
1154
|
+
expect(bundle.skillMd).toContain("tools/list");
|
|
1155
|
+
expect(bundle.skillMd).not.toContain("Invoke via shell");
|
|
1156
|
+
expect(bundle.skillMd).not.toContain("reference.md");
|
|
1157
|
+
expect(bundle.skillMd).not.toContain("`nested.ts stat owner lookup <path>`");
|
|
1158
|
+
expect(bundle.skillMd).not.toContain("## Commands");
|
|
1159
|
+
});
|
|
1160
|
+
|
|
1119
1161
|
test("cliSkillInstall writes project Cursor skill files", () => {
|
|
1120
1162
|
const cwd = mkdtempSync(join(tmpdir(), "argsbarg-skill-"));
|
|
1121
1163
|
const prev = process.cwd();
|
package/src/skill/generate.ts
CHANGED
|
@@ -4,7 +4,13 @@ This module generates Agent Skills content (SKILL.md + reference.md) from a CLI
|
|
|
4
4
|
|
|
5
5
|
import { defaultConfigEntryTitle } from "../config/entry.ts";
|
|
6
6
|
import { generateApiGuide } from "../docs/api-guide.ts";
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
collectMcpTools,
|
|
9
|
+
type McpToolDef,
|
|
10
|
+
mcpServerId,
|
|
11
|
+
resolveMcpSchemaUri,
|
|
12
|
+
sanitizeToolSegment,
|
|
13
|
+
} from "../mcp/tools.ts";
|
|
8
14
|
import { collectOptionDefs } from "../parse.ts";
|
|
9
15
|
import { CliOptionKind, type CliProgram } from "../types.ts";
|
|
10
16
|
|
|
@@ -16,12 +22,28 @@ export interface SkillBundle {
|
|
|
16
22
|
referenceMd: string;
|
|
17
23
|
}
|
|
18
24
|
|
|
25
|
+
/** MCP routing skill for Claude Code plugin zips (SKILL.md only). */
|
|
26
|
+
export interface PluginSkillBundle {
|
|
27
|
+
dirName: string;
|
|
28
|
+
skillMd: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
19
31
|
/** Truncates text to maxLen with ellipsis. */
|
|
20
32
|
function truncate(text: string, maxLen: number): string {
|
|
21
33
|
if (text.length <= maxLen) return text;
|
|
22
34
|
return `${text.slice(0, maxLen - 1)}…`;
|
|
23
35
|
}
|
|
24
36
|
|
|
37
|
+
/** Builds MCP-oriented skill description for Claude plugin YAML frontmatter. */
|
|
38
|
+
function pluginSkillDescription(root: CliProgram): string {
|
|
39
|
+
const tools = collectMcpTools(root);
|
|
40
|
+
const paths = tools.map((t) => (t.path.length > 0 ? t.path.join(" ") : root.key));
|
|
41
|
+
const sample = paths.slice(0, 5).join(", ");
|
|
42
|
+
const more = paths.length > 5 ? `, and ${paths.length - 5} more` : "";
|
|
43
|
+
const desc = `Use the ${root.key} MCP toolset (${sample}${more}). Use when the user mentions ${root.key}${paths.length > 0 ? `, ${paths.slice(0, 3).join(", ")}` : ""}, or related tasks.`;
|
|
44
|
+
return truncate(desc, 1024);
|
|
45
|
+
}
|
|
46
|
+
|
|
25
47
|
/** Builds third-person skill description for YAML frontmatter. */
|
|
26
48
|
function skillDescription(root: CliProgram): string {
|
|
27
49
|
const tools = collectMcpTools(root);
|
|
@@ -159,6 +181,54 @@ function buildReferenceMd(root: CliProgram): string {
|
|
|
159
181
|
return generateApiGuide(root);
|
|
160
182
|
}
|
|
161
183
|
|
|
184
|
+
/** Builds MCP routing SKILL.md for Claude Code plugin zips. */
|
|
185
|
+
function buildPluginSkillMd(root: CliProgram, dirName: string): string {
|
|
186
|
+
const name = sanitizeToolSegment(root.key);
|
|
187
|
+
const description = pluginSkillDescription(root);
|
|
188
|
+
const serverId = mcpServerId(root);
|
|
189
|
+
const schemaUri = resolveMcpSchemaUri(root);
|
|
190
|
+
|
|
191
|
+
const lines: string[] = [
|
|
192
|
+
"---",
|
|
193
|
+
`name: ${name}`,
|
|
194
|
+
`description: ${description}`,
|
|
195
|
+
"---",
|
|
196
|
+
"",
|
|
197
|
+
`# ${root.key}`,
|
|
198
|
+
"",
|
|
199
|
+
root.description,
|
|
200
|
+
"",
|
|
201
|
+
"## Execution",
|
|
202
|
+
"",
|
|
203
|
+
"This plugin bundles an MCP server. Use MCP tools to fulfill requests.",
|
|
204
|
+
"",
|
|
205
|
+
`- Server id: \`${serverId}\` (configured in plugin \`.mcp.json\`)`,
|
|
206
|
+
"- Tool names and argument shapes come from MCP `tools/list`",
|
|
207
|
+
`- Full schema: \`${schemaUri}\` (same as \`${root.key} docs schema\`)`,
|
|
208
|
+
"",
|
|
209
|
+
];
|
|
210
|
+
|
|
211
|
+
lines.push(...buildConfigurationSection(root));
|
|
212
|
+
|
|
213
|
+
lines.push(
|
|
214
|
+
"## Claude Code plugin",
|
|
215
|
+
"",
|
|
216
|
+
`Invoke with \`/${dirName}\` or let Claude auto-match from the description.`,
|
|
217
|
+
"",
|
|
218
|
+
);
|
|
219
|
+
|
|
220
|
+
return lines.join("\n");
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/** Generates MCP routing SKILL.md for Claude Code plugin zips. */
|
|
224
|
+
export function generatePluginSkillBundle(root: CliProgram): PluginSkillBundle {
|
|
225
|
+
const dirName = sanitizeToolSegment(root.key);
|
|
226
|
+
return {
|
|
227
|
+
dirName,
|
|
228
|
+
skillMd: buildPluginSkillMd(root, dirName),
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
162
232
|
/** Generates SKILL.md and reference.md for Cursor or Claude Code. */
|
|
163
233
|
export function generateSkillBundle(root: CliProgram, target: SkillTarget): SkillBundle {
|
|
164
234
|
const dirName = sanitizeToolSegment(root.key);
|
package/src/skill/hint.ts
CHANGED
|
@@ -58,3 +58,8 @@ export function applySkillBundleHints(
|
|
|
58
58
|
referenceMd: insertGeneratedHint(referenceMd, hint),
|
|
59
59
|
};
|
|
60
60
|
}
|
|
61
|
+
|
|
62
|
+
/** Applies bundle hint to plugin SKILL.md (after frontmatter). */
|
|
63
|
+
export function applyPluginSkillHint(program: CliProgram, skillMd: string): string {
|
|
64
|
+
return insertGeneratedHint(skillMd, skillBundleHint(program), { afterFrontmatter: true });
|
|
65
|
+
}
|
package/src/validate.ts
CHANGED
|
@@ -3,6 +3,7 @@ This module validates CLI schemas before execution.
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { reservedCommandNames, resolveCapabilities } from "./capabilities.ts";
|
|
6
|
+
import { reservedDocsTopicResourceUris } from "./docs/mcp-resources.ts";
|
|
6
7
|
import { DOCS_BUILTIN_TOPIC_KEYS } from "./docs/resolve.ts";
|
|
7
8
|
import { validateFormatValue } from "./formats.ts";
|
|
8
9
|
import { resolveMcpSchemaUri } from "./mcp/tools.ts";
|
|
@@ -209,11 +210,15 @@ function walkNode(node: CliNode, program: CliProgram, isRoot: boolean): void {
|
|
|
209
210
|
|
|
210
211
|
if (isRoot && program.mcpServer?.enabled === true && program.mcpServer.resources) {
|
|
211
212
|
const schemaUri = resolveMcpSchemaUri(program);
|
|
213
|
+
const reserved = new Set([schemaUri, ...reservedDocsTopicResourceUris(program)]);
|
|
212
214
|
const uris = program.mcpServer.resources.map((r) => r.uri);
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
215
|
+
for (const uri of uris) {
|
|
216
|
+
if (reserved.has(uri)) {
|
|
217
|
+
const kind = uri === schemaUri ? "built-in schema resource" : "auto docs topic resource";
|
|
218
|
+
throw new CliSchemaValidationError(
|
|
219
|
+
`mcpServer.resources URI '${uri}' conflicts with ${kind}`,
|
|
220
|
+
);
|
|
221
|
+
}
|
|
217
222
|
}
|
|
218
223
|
if (new Set(uris).size !== uris.length) {
|
|
219
224
|
throw new CliSchemaValidationError("mcpServer.resources URIs must be unique");
|