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 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.3...HEAD
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
  [![npm version](https://img.shields.io/npm/v/argsbarg.svg)](https://www.npmjs.com/package/argsbarg)
7
7
  [![Bun](https://img.shields.io/badge/Bun-%23000000.svg?logo=bun&logoColor=white)](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
@@ -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`. `load()` runs synchronously at `resources/read` time.
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: "# consumer-app\n\nKitchen-sink argsbarg reference. Copy this layout into a new CLI repo.\n",
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 () => ({
@@ -45,6 +45,12 @@ const program = {
45
45
  },
46
46
  ],
47
47
  },
48
+ docs: {
49
+ enabled: true,
50
+ topics: {
51
+ readme: { text: "# MCP test readme\n" },
52
+ },
53
+ },
48
54
  commands: [
49
55
  {
50
56
  key: "echo-env",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "argsbarg",
3
- "version": "4.0.3",
3
+ "version": "4.0.4",
4
4
  "type": "module",
5
5
  "engines": {
6
6
  "bun": ">=1.3"
@@ -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
+ }
@@ -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 expected entries", () => {
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 zipPath = paths.pluginZipPath;
68
- expect(readFileSync(zipPath).length).toBeGreaterThan(0);
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 { generateSkillBundle } from "../skill/generate.ts";
21
- import { applySkillBundleHints } from "../skill/hint.ts";
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, sanitizeToolSegment } from "./tools.ts";
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 skillDirName = sanitizeToolSegment(program.key);
120
- const bundle = generateSkillBundle(program, "claude");
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", skillDirName), { recursive: true });
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", skillDirName, "SKILL.md"), hinted.skillMd);
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 the built-in schema resource/);
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 the built-in schema resource/);
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();
@@ -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 { collectMcpTools, type McpToolDef, sanitizeToolSegment } from "../mcp/tools.ts";
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
- if (uris.includes(schemaUri)) {
214
- throw new CliSchemaValidationError(
215
- `mcpServer.resources URI '${schemaUri}' conflicts with the built-in schema resource`,
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");