Skip to content

feat(skills): mna skills install — detect AI clients and install the skill + MCP server - #7

Merged
akoso merged 3 commits into
mainfrom
feat/agent-integration-installer
Jul 31, 2026
Merged

feat(skills): mna skills install — detect AI clients and install the skill + MCP server#7
akoso merged 3 commits into
mainfrom
feat/agent-integration-installer

Conversation

@akoso

@akoso akoso commented Jul 31, 2026

Copy link
Copy Markdown
Collaborator

What

Wrangler-inspired agent integration. mna skills install detects the AI coding clients on the
machine, shows exactly what it will write where, and asks once. After a successful interactive
mna login the same offer appears behind a single y.

Replaces the manual cp -r skills/mna ~/.claude/skills/ instructions in the README (kept as a
fallback section).

Supported clients

Skill directories follow the same table Wrangler itself uses (via rosie-skills) for its
post-login install — verified first-hand against the trees wrangler login left in
~/.claude/skills/ and ~/.cursor/skills/ on the dev machine. MCP entry shapes were verified
per client, because they genuinely differ:

Client --client Skill MCP config MCP shape
Claude Code claude-code ~/.claude/skills/mna/ ~/.claude.jsonmcpServers {type:"http",url}
Cursor cursor ~/.cursor/skills/mna/ ~/.cursor/mcp.jsonmcpServers {url}
Claude Desktop claude-desktop claude_desktop_config.json (per-OS) npx -y mcp-remote
Windsurf / Devin Desktop windsurf ~/.codeium/windsurf/skills/mna/ ~/.codeium/windsurf/mcp_config.json {serverUrl}
VS Code (Copilot agent mode) vscode <user dir>/mcp.jsonservers {type:"http",url}
Shared agent dir agents, codex ~/.agents/skills/mna/
OpenCode opencode ~/.config/opencode/skills/mna/
Gemini CLI gemini-cli ~/.gemini/skills/mna/ ~/.gemini/settings.jsonmcpServers {httpUrl}

Cline was deliberately left out — its settings path is contested between the docs
(~/.cline/data/settings/…) and the shipping extension (VS Code globalStorage), so there is
no path we can write with confidence. ~/.codeium/windsurf/ is used for Windsurf because it is
the only probe that survives the June 2026 Devin Desktop rename.

Codex CLI correction (review follow-up): the first version of this PR shipped
~/.codex/skills/. OpenAI's docs document $HOME/.agents/skills and never mention
~/.codex/skills — that path comes from Cursor's compatibility docs, and is where most of the
folklore online traces to. The bogus client was removed; --client codex now aliases the shared
~/.agents entry. Windsurf, Gemini CLI and OpenCode paths were checked the same way and are
correct.

Claude Desktop on Linux is flagged (?): the Linux build is official (beta), but no
Anthropic doc says it reads ~/.config/Claude/claude_desktop_config.json — only the macOS and
Windows paths are documented. mna writes the conventional path and tells you it's unverified
rather than reporting "up-to-date" for somewhere the client may never read.

MCP servers point at https://mcp.mynextadventure.cloud/mcp. If credentials exist, the API
key is written into that client's config file in plain text
— as an X-API-Key header, or in
env for the mcp-remote bridge. That is simply how these clients take credentials; there is no
secret store to point them at. What mna does about it: any config it writes a key into is set
to mode 0600, and the CLI says so on screen before writing. Users who'd rather not have the key
on disk can use --no-mcp, or install before mna login and let the client do its own OAuth.

(An earlier revision of this description implied the key was handled more discreetly than it is.
It wasn't, and the reviewer was right to call it out.)

Safety properties (all covered by tests)

  • Idempotent — byte-identical files are left alone; a second run prints Already up to date.
  • Merge, never clobber — only mcpServers.my-next-adventure is set. JSONC configs (VS Code,
    Gemini CLI) are edited in place via jsonc-parser, so // comments, trailing commas and key
    order survive. The object being merged into may be re-indented; nothing else is touched.
  • Atomic writes — temp file + rename(). An interrupt or a full disk cannot truncate a
    multi-megabyte ~/.claude.json.
  • Backups — anything modified is copied to <file>.mna-backup-<timestamp> at mode 0600
    (they can contain OAuth tokens), and only the 3 most recent are kept.
  • Refuses what it doesn't understand — a config that won't parse, or that has a non-object
    where mcpServers should be, is skipped with a clear message. Crucially this skips only the
    MCP entry
    : the skill files are independent and still install.
  • Isolated failures — one unwritable path doesn't abandon the rest; failures are reported per
    client (with the backup path) and the command exits non-zero.
  • --dry-run writes nothing; --json refuses to write without --yes, and emits JSON (not a
    bare stack trace) on failure; a non-TTY without --yes errors instead of hanging on a prompt.
  • Overwrites flip the confirmation default to no.
  • The post-login offer is skipped entirely for --json, non-TTY and CI, is skill-only (never
    touches MCP configs), doesn't re-ask once the skill is current, and is suppressible with
    mna config set skills.prompt false.

Review fixes (second commit)

The first commit had a bug worth naming: a blocked client silently skipped its skill files and
then reported success.
plan.blocked gated all of applyPlan but was only ever set by the MCP
change, so a corrupt ~/.claude.json produced a preview promising SKILL.md, wrote nothing, and
still printed ✓ Installed the mna skill for Claude Code, Cursor with exit 0. Fixed by scoping it
to mcpBlocked and building the summary from actual applied results. Also fixed in that pass:
non-atomic writes, world-readable configs/backups holding credentials, unpruned backups, no error
isolation, JSONC configs being refused as "corrupt", and setAtPath silently replacing a
non-object mcpServers. Each has a regression test.

One more found while verifying: jsonc-parser's main is a UMD bundle whose lazy
require('./impl/format') bundles into a broken dist/mna.js — the unit tests all passed
while the actual published artifact threw on startup. The import is pinned to the ESM entry, and
CI now builds the bundle and smoke-tests it on Node so this class of bug can't recur.

Implementation notes

  • The skill payload is inlined into the bundle with Bun's text loader (with { type: 'text' }),
    so skills install works identically from npm, the compiled Homebrew binary, and a source
    checkout — none of which agree on where skills/mna sits relative to the entrypoint.
  • New ~/.config/mna/settings.json for non-secret prefs, so skills.prompt survives logout
    and can be set before a first login. mna config get|set learned the key.
  • mna login gained --json (house convention: every command supports it), which also
    suppresses the prompt.

Demo transcript

$ mna skills list
CLIENT       ID           DETECTED  SKILL    MCP      SKILL PATH
───────────  ───────────  ────────  ───────  ───────  ────────────────────
Claude Code  claude-code  yes       missing  missing  ~/.claude/skills/mna
Cursor       cursor       yes       missing  missing  ~/.cursor/skills/mna

Run `mna skills install` to set these up.

$ mna skills install --dry-run
Detected AI clients: Claude Code, Cursor

Would write:

  Claude Code
    create     ~/.claude/skills/mna/SKILL.md
    create     ~/.claude/skills/mna/references/cli-and-schemas.md
    create     ~/.claude/skills/mna/references/research-and-costing.md
    create     ~/.claude.json  →  mcpServers.my-next-adventure

  Cursor
    create     ~/.cursor/skills/mna/SKILL.md
    create     ~/.cursor/skills/mna/references/cli-and-schemas.md
    create     ~/.cursor/skills/mna/references/research-and-costing.md
    create     ~/.cursor/mcp.json  →  mcpServers.my-next-adventure
  The MCP entry embeds your API key so the client can authenticate.

Dry run — nothing was written.

$ mna skills install
Detected AI clients: Claude Code, Cursor

mna will write:
  ... (same plan) ...

? Install the mna skill for Claude Code, Cursor? (Y/n) y
✓ created ~/.claude/skills/mna/SKILL.md
✓ created ~/.claude/skills/mna/references/cli-and-schemas.md
✓ created ~/.claude/skills/mna/references/research-and-costing.md
✓ updated ~/.claude.json
  backup: ~/.claude.json.mna-backup-20260731T054450
✓ created ~/.cursor/skills/mna/SKILL.md
✓ created ~/.cursor/skills/mna/references/cli-and-schemas.md
✓ created ~/.cursor/skills/mna/references/research-and-costing.md
✓ created ~/.cursor/mcp.json

✓ Installed the mna skill for Claude Code, Cursor.
  Restart the client (or reload skills) and ask it to plan a trip.

$ mna skills install --yes
✓ Already up to date for Claude Code, Cursor.

$ mna skills install --client gemini-cli    # piped, no TTY
✖ Not an interactive terminal. Re-run with --yes to install non-interactively.

Merging into an existing ~/.claude.json that already had mcpServers.existing:

{
  "numStartups": 42,
  "mcpServers": {
    "existing": { "command": "npx" },
    "my-next-adventure": {
      "type": "http",
      "url": "https://mcp.mynextadventure.cloud/mcp"
    }
  }
}

And the Claude Desktop bridge form, with credentials present:

{
  "mcpServers": {
    "my-next-adventure": {
      "command": "npx",
      "args": ["-y", "mcp-remote", "https://mcp.mynextadventure.cloud/mcp",
               "--header", "X-API-Key:${MNA_API_KEY}"],
      "env": { "MNA_API_KEY": "mna_test_key" }
    }
  }
}

Review round 3

  • The success line over-claimed again, in a new shape. succeeded meant "wrote anything",
    so a client whose three skill files all failed EACCES while its MCP entry landed still
    printed ✓ Installed the mna skill for Claude Code. Structural cause: AppliedChange carried
    no label, so the command couldn't tell a skill write from an MCP write. Added label; the
    summary is now split into "Installed the mna skill for …" (gated on real skill writes) and
    "Registered the MNA MCP server for …".

  • The dist smoke test didn't cover the class it was added for. On CI's pristine $HOME
    nothing is detected, so none of the three commands reached parse/modify/applyEdits. It
    now seeds a temp $HOME with a commented ~/.cursor/mcp.json and runs a real
    skills install --client cursor --yes against the built bundle, asserting the comment, the
    pre-existing server, the new entry and SKILL.md all survive. Confirmed in
    CI
    .

  • Skill paths are now cited, not asserted. skillPathVerified was set on zero clients, so
    the (?) marker was inert for skills while the README claimed every path was vendor-documented.
    I verified each against the vendor's own page and recorded the URL in the registry
    (skillPathDocs), surfaced in skills list --json and linked from the README table:

    Client Path Source
    Claude Code ~/.claude/skills/ docs — "Personal | ~/.claude/skills/<skill-name>/SKILL.md"
    Cursor ~/.cursor/skills/ docs
    Windsurf ~/.codeium/windsurf/skills/ docs — scope table, Global
    OpenCode ~/.config/opencode/skills/ docs
    Codex → agents ~/.agents/skills/ docs — USER scope
    Gemini CLI ~/.gemini/skills/ docs

    A test fails if a client with a skill directory has no citation. The ~/.agents comment no
    longer hedges — five vendors documenting the same path is not a spec guarantee, and it now says
    exactly that. (Worth noting the search snippets claimed Windsurf had no global skills dir;
    fetching the vendor page showed that was the cross-agent paragraph quoted out of context.)

  • Minors: ApplyError.backup populated so the "your original is at …" branch isn't dead;
    --json shapes shared between list and install; secrets written with mode 0600 at
    creation rather than chmod-after; backup names are millisecond-precision and collision-proof
    — a same-second name silently overwrote the earlier backup within a single run, which my own
    new test caught.

Verification

bun run typecheck + bun run lint + bun test all green — 128 tests, 0 fail (58 new across
changes, clients, plan, post-login-prompt, settings). New regression tests cover the
blocked-client no-op, atomic-write failure cleanup, 0600 modes on configs and backups, backup
retention, JSONC comment preservation, ConfigConflictError on a non-object mcpServers, and
per-change error isolation.

Exercised for real on macOS against throwaway $HOMEs:

  • The original bug, re-run: corrupt ~/.claude.json → skill files now install, the corrupt config
    is byte-identical afterwards, the MCP skip is reported on stderr, and the success line is
    accurate.
  • JSONC: a VS Code mcp.json with // comments and a trailing comma merged cleanly, comments
    intact, file and backup both 0600.
  • Error isolation: chmod 500 on ~/.claude/skills → Cursor still installed, Claude Code's three
    failures reported individually, success line named Cursor only, exit 2, --json ok:false.
  • --json without --yes → JSON error object, exit 1. skills uninstall --dry-run / --yes.
  • bun run build + node dist/mna.js verified working after the jsonc-parser fix.

The real ~/.claude/skills/ (11 Wrangler-installed skills), ~/.claude.json and ~/.cursor/
remain unmodified — no mna skill dir, no backup files, no mcpServers key added.

Not verified: the bun build --compile binaries run. They compile, but this sandbox SIGKILLs any
freshly-compiled binary (a hello-world does the same), so I can't claim the Homebrew artifact was
executed. The npm bundle on Node is verified and now gated in CI.

🤖 Generated with Claude Code

https://claude.ai/code/session_01AsdP2hvt4XQhYxkdNREXUq

akoso and others added 3 commits July 31, 2026 07:52
…e skill + MCP server

Wrangler-style agent integration. `mna skills install` detects the AI coding
clients on the machine, shows exactly what it will write where, and asks once.
After a successful interactive `mna login` the same offer appears behind a
single `y`.

Supported targets (skill dirs follow the same table Wrangler uses via
rosie-skills; MCP shapes verified per client):

  claude-code     ~/.claude/skills/mna         ~/.claude.json          {type:http,url}
  cursor          ~/.cursor/skills/mna         ~/.cursor/mcp.json      {url}
  claude-desktop  —                            claude_desktop_config   npx mcp-remote
  windsurf        ~/.codeium/windsurf/skills   mcp_config.json         {serverUrl}
  vscode          —                            <user>/mcp.json         servers.{type:http}
  agents          ~/.agents/skills/mna         —
  codex           ~/.codex/skills/mna          — (TOML, untouched)
  opencode        ~/.config/opencode/skills    —
  gemini-cli      ~/.gemini/skills/mna         ~/.gemini/settings.json {httpUrl}

Safety properties, all covered by tests:
- idempotent — byte-identical files are left alone
- JSON configs are merged into, never rewritten wholesale
- anything modified is copied to <file>.mna-backup-<timestamp> first
- an unparseable config blocks that client with a clear message instead of
  being replaced
- --dry-run touches nothing; --json refuses to write without --yes
- the post-login offer is skipped entirely for --json, non-TTY and CI, and is
  suppressible with `mna config set skills.prompt false`

The skill payload is inlined into the bundle via Bun's text loader, so install
works identically from npm, the compiled Homebrew binary, and a source checkout.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AsdP2hvt4XQhYxkdNREXUq
…C, 0600, error isolation

Addresses the FIX-FIRST review. The headline bug was a false success claim.

CRITICAL
- Blocked client silently skipped its skill files, then reported success.
  `plan.blocked` gated *all* of applyPlan but was only ever set by the MCP
  change, so a corrupt ~/.claude.json meant the preview promised SKILL.md,
  nothing was written, and the CLI still printed "✓ Installed ... for Claude
  Code, Cursor" with exit 0. Renamed to `mcpBlocked` and scoped to the MCP
  change alone; the success line is now built from the actual `applied`
  results, so it names only clients that really got something.
- Writes are now atomic (temp file + rename) instead of truncate-in-place.
  An interrupt or ENOSPC mid-write can no longer truncate ~/.claude.json,
  which holds Claude Code's entire user state and is routinely multi-MB.

IMPORTANT
- Credentials: any config we write an API key into is chmod 0600, and the
  CLI now says plainly that the key is stored in plain text. The earlier PR
  description implied otherwise; it has been corrected.
- Backups are 0600 (they can contain OAuth tokens) and pruned to the 3 most
  recent instead of accumulating forever.
- Per-client, per-change error isolation: one EACCES no longer aborts the
  run. Failures are collected, reported per client with the backup path, and
  the command exits non-zero.
- JSONC configs are merged, not refused. VS Code's mcp.json and Gemini CLI's
  settings.json routinely carry // comments; jsonc-parser edits the text in
  place so comments, key order, and surrounding formatting survive (the
  object being merged into may be re-indented — documented).
- A non-object at `mcpServers` is a hard stop (ConfigConflictError) rather
  than being silently replaced with {} and reported as "create".

HONESTY
- Verified every skill directory against vendor docs. One was wrong: Codex
  CLI reads $HOME/.agents/skills, NOT ~/.codex/skills — the latter is a
  third-party compatibility claim OpenAI's own docs never make. Removed the
  bogus client; `--client codex` now aliases the shared ~/.agents entry.
- Windsurf, Gemini CLI and OpenCode skill paths all confirmed correct.
- Claude Desktop's Linux config path is NOT vendor-documented (the build is
  official beta, the path is convention). Flagged (?) in the plan and table.
- Machinery added to mark any unverified path rather than print "up-to-date"
  for somewhere a client may never read.

ALSO
- `mna skills uninstall`.
- --json failures emit JSON and a non-zero exit.
- CI builds dist/mna.js and smoke-tests it on Node: jsonc-parser's UMD main
  has a lazy require that bundles into a broken artifact, which unit tests
  could never have caught. Import pinned to the ESM entry.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AsdP2hvt4XQhYxkdNREXUq
…skill paths

Third review round.

N1 — success line still over-claimed, in a new shape. `succeeded` was
"any change written", with no notion of *what* was written, so a client whose
three skill files all failed EACCES but whose MCP entry landed still produced
"✓ Installed the mna skill for Claude Code". Reproduced before fixing. Root
cause was structural: AppliedChange carried no label, so the command literally
could not tell a skill write from an MCP write. Added `label` to AppliedChange
and split the summary into "Installed the mna skill for …" (gated on real skill
writes) and "Registered the MNA MCP server for …".

N2 — the dist smoke test didn't cover the class it was added for. On CI's
pristine $HOME no client is detected, so none of the three commands ever
reached parse/modify/applyEdits and a lazy require inside them would still have
shipped. The step now seeds a temp $HOME with a commented ~/.cursor/mcp.json,
runs a real `skills install --client cursor --yes` against the built bundle,
and asserts the comment, the existing server, the new entry and the SKILL.md
all survive. Verified by running the extracted script locally.

N3 — honesty. `skillPathVerified` was set on zero clients, so the (?) marker
and both legends were inert for skills while the README asserted every path was
vendor-documented. Rather than soften the claim, I verified each path against
the vendor's own page and recorded the URL in the registry as `skillPathDocs`:

  claude-code  code.claude.com/docs/en/skills        "Personal | ~/.claude/skills/…"
  cursor       cursor.com/docs/skills                user: ~/.cursor/skills/
  windsurf     docs.devin.ai/desktop/cascade/skills  scope table: Global | ~/.codeium/windsurf/skills/
  opencode     opencode.ai/docs/skills/              "~/.config/opencode/skills/*/SKILL.md"
  agents       learn.chatgpt.com/docs/build-skills   Codex USER scope = $HOME/.agents/skills
  gemini-cli   github.com/google-gemini/gemini-cli   "User skills: ~/.gemini/skills/"

A test now fails if a client with a skill directory has no citation. The URLs
are surfaced in `skills list --json`, so the README's claim is checkable. The
~/.agents comment no longer reads as a hedge: five vendors documenting the same
path is not the same as a spec guarantee, and it now says exactly that. The (?)
machinery stays live via Claude Desktop's undocumented Linux config path.

Minors: ApplyError.backup is populated (ApplyFailedError carries it) so the
"your original is at …" branch is no longer dead — proved with a pure
toApplyError test, since an atomic rename makes it unreachable via the
filesystem; --json shapes shared between list and install via clientJsonView;
secrets are written with mode 0600 at creation instead of chmod-after, closing
a brief world-readable window; backup names are millisecond-precision and
collision-proof (a second-granularity name silently overwrote the earlier
backup within one run).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AsdP2hvt4XQhYxkdNREXUq
@akoso
akoso merged commit 6154c9b into main Jul 31, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant