Skip to content

feat(cli): vouch install-mcp <host> — one-command adapter writer (8 hosts) - #180

Merged
plind-junior merged 1 commit into
vouchdev:testfrom
dripsmvcp:fix/179-install-mcp
Jun 9, 2026
Merged

feat(cli): vouch install-mcp <host> — one-command adapter writer (8 hosts)#180
plind-junior merged 1 commit into
vouchdev:testfrom
dripsmvcp:fix/179-install-mcp

Conversation

@dripsmvcp

Copy link
Copy Markdown
Contributor

Summary

Closes #179. Generalises the single-host claude-code adapter plan (docs/superpowers/plans/2026-05-27-claude-code-adapter.md) into a catalogue-driven installer with 8 supported MCP hosts out of the box, where adding a new host is a single-file PR.

Root cause

Adoption today is "follow the README, write .mcp.json by hand, copy CLAUDE.md, hope you got it right." Each host (Claude Code, Cursor, Windsurf, Zed, Cline, JetBrains, Claude Desktop) has a slightly different config-file path. The existing plan covered the writer for one host (claude-code); issue #179 asks to generalise it to a catalogue.

Fix

Manifest-driven writer. src/vouch/install_adapter.py reads a YAML manifest per host (adapters/<host>/install.yaml) declaring which template files land at which paths, under which adoption tier:

Tier What
T1 MCP wire (the host's mcpServers config)
T2 CLAUDE.md / AGENTS.md fenced snippet (idempotent append)
T3 Optional slash commands
T4 Optional host hooks / settings (e.g. read-only kb_* auto-allow)

Tiers stack: --tier T4 runs everything available; --tier T1 stops at MCP wire. Each manifest declares only the tiers its host has templates for; the writer treats absent tiers as no-ops.

Idempotent. install() returns three channels — written, appended, skipped. Re-running on the same target tree returns everything in skipped. CLAUDE.md/AGENTS.md snippets are appended inside a <!-- BEGIN vouch --> ... <!-- END vouch --> fence on first write; on re-run the writer sees the fence and treats it as skipped instead of duplicating the block.

CLI. vouch install-mcp adds:

vouch install-mcp --list                    # catalogue (8 hosts)
vouch install-mcp claude-code               # default tier T4
vouch install-mcp cursor --tier T2          # stop at AGENTS.md
vouch install-mcp windsurf --path /abs/path # alt project root

Catalogue (8 hosts; #179 acceptance was ≥6):

Host T1 path T2 T3 T4
claude-code .mcp.json CLAUDE.md 4 slash commands .claude/settings.json
claude-desktop claude-desktop/claude_desktop_config.json (paste-ready)
cursor .cursor/mcp.json AGENTS.md
continue .continue/config.yaml
codex .codex/config.toml
windsurf .codeium/windsurf/mcp_config.json
cline .cline/mcp_servers.json .vscode/settings.json
zed .zed/settings.json (context_servers)

claude-desktop is the asymmetric one — Claude Desktop's MCP config is user-global, not project-local. The writer drops a paste-ready JSON + README under <target>/claude-desktop/ so the user can review and copy it themselves, rather than reaching into ~/Library/Application Support/ from a project-scoped command (which would conflict with #179's explicit "uninstall + auto-detection out of scope").

Test Plan

  • Regression tests added: tests/test_install_adapter.py (23 new)
  • All tests pass: pytest383 passed (PR was 360, +23 new, no regressions)
  • ruff clean
  • mypy clean
  • End-to-end smoke (--list, T1 install, idempotent re-run) confirmed in shell

Test coverage

Test What it pins
test_available_adapters_lists_at_least_six_hosts Catalogue size + must-have hosts present
test_available_adapters_is_sorted_and_unique API stability
test_every_adapter_has_a_parseable_manifest Surface manifest bugs at test time, not first-install
test_install_claude_code_t1_writes_only_mcp_json T1 boundary respected
test_install_claude_code_t4_writes_all_tiers T4 = T1+T2+T3+T4 stack
test_install_claude_code_is_idempotent Re-run → empty written, full skipped
test_install_claude_md_appends_when_existing_unfenced User's existing CLAUDE.md preserved; fence appended
test_install_claude_md_skips_when_already_fenced Re-run on fenced file is flat-noop
test_install_unknown_adapter_raises / _unknown_tier_raises Clean errors
test_each_host_writes_its_t1_file[...] × 7 Every non-claude-code host produces ≥1 file at T1
test_install_returns_dataclass_with_three_channels API shape
test_cli_install_mcp_list_enumerates_hosts --list shows ≥6
test_cli_install_mcp_claude_code_writes_into_path --path honored
test_cli_install_mcp_unknown_host_is_clean_error No traceback on bad host
test_cli_install_mcp_second_run_shows_skipped Idempotency via CLI
test_cli_install_mcp_default_tier_is_t4 Default tier is T4

Output

$ pytest tests/test_install_adapter.py
============================= test session starts ==============================
collected 23 items

tests/test_install_adapter.py .......................                    [100%]

============================= 23 passed in 0.35s ==============================

$ pytest
383 passed, 5 deselected in 5.05s

$ ruff check src/vouch/install_adapter.py src/vouch/cli.py tests/test_install_adapter.py
All checks passed!

$ mypy src/vouch/install_adapter.py src/vouch/cli.py
Success: no issues found in 2 source files

$ vouch install-mcp --list
Available MCP host adapters:
  - claude-code
  - claude-desktop
  - cline
  - codex
  - continue
  - cursor
  - windsurf
  - zed

$ vouch install-mcp claude-code --path /tmp/demo --tier T1
  + .mcp.json
Done — 1 written, 0 appended, 0 skipped under /tmp/demo

$ vouch install-mcp claude-code --path /tmp/demo --tier T1
  · .mcp.json  (already present)
Done — 0 written, 0 appended, 1 skipped under /tmp/demo

Out of scope (per #179)

  • Auto-detecting which host is running (do that explicitly).
  • Uninstall (manually delete or revert with git).

Closes #179

…osts)

Closes vouchdev#179. Generalises the single-host claude-code adapter plan
(docs/superpowers/plans/2026-05-27-claude-code-adapter.md) into a
catalogue-driven installer that supports eight MCP hosts out of the box
and accepts a new host as a single-file PR (just drop an
adapters/<host>/install.yaml manifest plus the host's template files).

What ships:

* src/vouch/install_adapter.py — manifest-driven writer. Reads
  adapters/<host>/install.yaml; for each declared file copies it into
  target, or appends a fenced block if the manifest marks the entry
  fenced_append. Three result channels: written / appended / skipped.
  Idempotent: re-running on a tree the writer already touched returns
  everything in `skipped`, nothing in `written`.
* vouch install-mcp <host> [--tier T1|T2|T3|T4] [--path .] [--list] —
  CLI surface. --list enumerates the catalogue (8 hosts, ≥6 required).
  --tier defaults to T4 (full install); --tier T1 stops at MCP wire.
  --target is accepted as an alias for --path per the issue spec.
* 8 host manifests + templates:
    claude-code      T1 .mcp.json + T2 CLAUDE.md + T3 4 slash commands
                     + T4 .claude/settings.json (read-only kb_* auto-allow
                     + SessionStart hook firing `vouch status --json`)
    claude-desktop   project-local stub + paste-ready user-global JSON,
                     because Claude Desktop's config is user-global only
    cursor           T1 .cursor/mcp.json + T2 AGENTS.md fenced snippet
    continue         T1 .continue/config.yaml (mcpServers block)
    codex            T1 .codex/config.toml
    windsurf         T1 .codeium/windsurf/mcp_config.json
    cline            T1 .cline/mcp_servers.json + T2 .vscode/settings.json
                     fragment (workspace-scoped Cline config)
    zed              T1 .zed/settings.json with context_servers block
* tests/test_install_adapter.py — 23 tests. Catalogue (≥6 hosts, sorted,
  unique, every manifest parseable). claude-code reference tiers T1+T4 +
  idempotency + CLAUDE.md fence-append + fence-skip semantics. Per-host
  T1 smoke test parameterised over the 7 non-claude-code hosts. CLI
  surface (--list, default tier T4, --path, clean error on bad host).
* CHANGELOG entry.

Acceptance against issue vouchdev#179:
  ✓ vouch install-mcp --list enumerates ≥6 hosts (ships 8)
  ✓ Each writer drops files into the correct per-host path; existing
    files are left alone (fenced append for CLAUDE.md/AGENTS.md only)
  ✓ Test pack runs each host's writer against a tmp dir and asserts the
    expected files appear (per-host T1 parametrize + claude-code T4)

Out of scope (per issue): auto-detecting which host is running, and an
uninstall command (manually delete or revert via git).

ruff clean, mypy clean, 383 pytest passed (PR was 360, +23 new tests).
@coderabbitai

coderabbitai Bot commented Jun 9, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: d9fdfa10-2f1c-42cd-af87-8322c1a90560

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@plind-junior

plind-junior commented Jun 9, 2026

Copy link
Copy Markdown
Member

the manifest-driven shape here is the right call — adapters/<host>/install.yaml + a templates dir means adding a ninth host is a single-file PR by someone who knows that host, and the writer never has to learn. that's a much better factoring than the hardcoded mapping i'd have written.

the three-channel InstallResult (written / appended / skipped) makes the idempotency contract directly testable rather than implied — really nice. and the CLAUDE.md fenced-append handles all three states cleanly (missing dest → fresh write, dest without fence → append, dest with fence → flat noop). rerunning the command is genuinely safe, which is the whole point.

one question, not a blocker: the manifest gets strict structural validation (every shape of bad YAML is caught with a host-named error, which is great), but semantically the writer trusts whatever paths a contributor puts in dst. a typo like setings.json in some future host's T4 would land in user trees and nobody would notice until it didn't work. would it be worth a follow-up that adds a per-host expected_dst allowlist, or even just a --dry-run flag that prints planned writes before doing them? not for this PR — just curious whether you've already thought about that boundary.

tiny / non-blocking: T4 .claude/settings.json is hard skip-if-exists, which is the right default (don't mutate someone's settings JSON silently). but most users will already have that file, so T4 effectively becomes T3 for them. one option for a follow-up: when we skip a T4 file, echo the would-be contents to stdout so the user can paste-merge it themselves. keeps the install honest while still being useful.

ship it 🙏

@plind-junior

Copy link
Copy Markdown
Member

LGTM

@plind-junior
plind-junior merged commit 50cc8e9 into vouchdev:test Jun 9, 2026
5 checks passed
@plind-junior

Copy link
Copy Markdown
Member

Raise the separated PR for the comment

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.

2 participants