# F1 — `ccam lanes mcp sync`

**Status:** approved 2026-08-05. Roadmap it belongs to:
`docs/superpowers/plans/2026-08-03-shipyard-parity-lanes.md` (subsystem **F**).

## Problem

`.claude/skills/ship-feature-lane/SKILL.md`'s Setup section documents today's fallback for a lane missing its required Playwright MCP servers: "tell the human to configure `.mcp.json` manually and restart the session." Shipyard's source, `lane-mcp-sync.sh` (`~/MyDrive/Projects/ResearchAndDevelopment/AgentWorkflow/bin/lane-mcp-sync.sh`), automates this — but its source does three distinct things, only two of which belong in this repo's architecture. This is the first of F's four pieces (mcp-sync, CI-wait, tracker, dev-QC); it has no dependency on the other three.

## Scope

**In scope:**
1. `server/lib/lane-mcp.js` — reads the lane's `source_repo`'s already-configured MCP servers from `~/.claude.json`, relocates any absolute path under `source_repo` to the lane's own `cwd`, pins a `playwright`-family server's `--output-dir` to the lane's `.playwright-mcp`, writes `<lane.cwd>/.mcp.json`.
2. Seeding the lane's `.playwright-mcp/profiles/*` Chromium profile directories from the source repo's own (preserves saved MCP browser logins — a real QA-account login only needs to happen once per machine, not once per lane).
3. `.mcp.json` added to the lane's local `.git/info/exclude`.
4. `ccam lanes mcp sync [<id>]` CLI + `POST /api/lanes/:id/mcp/sync` route.
5. `SKILL.md` Setup section edit: replace the manual-fallback text with the real command.
6. `docs/LANES.md` + roadmap progress line.

**Deliberately out of scope (deviates from Shipyard, by explicit decision — not an oversight):**
- **Permission/auto-approval writing.** Shipyard's script also writes broad `allow` rules into `<lane>/.claude/settings.local.json` — `enableAllProjectMcpServers`, an `mcp__<server>` rule per synced server, a `gh pr comment/review/create/edit/view/diff/list/checks/status` + `git push` allow-list, and an `autoMode.allow` bypass-style rule that names the `ticketer`/`pr-reviewer` sub-agents specifically. CCAM has no existing pattern anywhere of a backend process writing permission grants on a session's behalf — every other primitive in this codebase (`proof-link`, `agents install`, `sync-base`) is explicit-and-inert until a session calls it, never a permission change. `pr-reviewer` isn't ported (no driving skill exists for it — confirmed during F1's own scoping conversation) and `ticketer` isn't built yet (F3), so the auto-mode rule would reference agents that don't exist. A human approving `gh pr comment`/`git push` through the normal permission prompt, same as any other command, is the correct default until a real need for automation is demonstrated.
- **The `mcp-node-exec.sh` node-version wrapper.** Shipyard's script routes `npx`/`node`-based MCP servers through a wrapper that resolves the newest Node ≥ 18 at spawn time, working around an nvm/PATH resolution bug specific to that environment. Not a problem this repo has evidence of; add it later if it actually bites someone (YAGNI).
- **Merging with an existing `.mcp.json`.** This is a full overwrite from the source repo's current MCP config, same "reinstall, not merge" shape E3's `agents install` already established — not a per-server incremental sync.

## Decisions

| | Choice | Rationale |
|---|---|---|
| MCP server source | `~/.claude.json`'s `projects[lane.source_repo].mcpServers` | `source_repo` is already a lane field (every managed lane has one). This mirrors Shipyard's own approach exactly — the human configures MCP servers project-scope for the source repo once (normal Claude Code usage), and this command just relocates that config into each lane. |
| Missing config | Throws `ENOMCPCONFIG` with a clear message pointing at `SKILL.md`'s Setup section, not a silent no-op | A lane with zero MCP servers synced would silently break Stage 3/6 later, at a much less debuggable point. Fail loud, fail early, matching `requireProfile`'s existing shape for a missing profile. |
| Overwrite semantics | Full overwrite of `.mcp.json` on every call, profiles seeded only if the destination directory doesn't already exist | Matches E3's "reinstall, not merge" for `.mcp.json` (config should always reflect the source repo's current state) but "never clobber a working login" for profiles (an existing profile dir means a session already logged in there — copying over it would lose that). |
| `--output-dir` pinning | Only added to a server whose args reference `@playwright/mcp` and don't already declare `--output-dir` | Direct port of Shipyard's own conditional — this is what makes proof screenshots land in the lane's own canonical `.playwright-mcp/proof` (the same path the proof gallery, C, already reads) instead of wherever the server's cwd happens to be. |
| Permission/auto-approval writes | Dropped entirely (see Scope) | No existing CCAM pattern for a backend command granting permissions; the two agents the original rules named aren't both in scope (`pr-reviewer` isn't ported at all, `ticketer` is a later task). |

## Command mapping

| Shipyard | CCAM | Status |
|---|---|---|
| `lane-mcp-sync.sh <N>` | `ccam lanes mcp sync [<id>]` | **new in this task** |
| `ensure_proof_link "$DIR"` (called at the top of `sync_lane`) | not called — proof-link is `ccam lanes proof-link`, a separate explicit primitive this repo already has; mcp-sync doesn't need to chain into it | n/a (already exists, not this task's job to invoke) |
| Chromium profile seeding | ported directly, same logic | **new in this task** |
| `.mcp.json` write + relocate + `--output-dir` pin | ported directly, same logic | **new in this task** |
| `settings.local.json` permission/autoMode writes | dropped | n/a (see Scope) |
| `mcp-node-exec.sh` wrapper routing | dropped | n/a (see Scope) |

## Design

### `server/lib/lane-mcp.js`

- `readSourceMcpServers(sourceRepo)` — reads `~/.claude.json`, returns `cfg.projects[sourceRepo]?.mcpServers`. Throws `ENOMCPCONFIG` if absent/empty.
- `relocateServers(servers, sourceRepo, laneDir)` — deep-walks the server config objects, replacing any string containing `sourceRepo` with the same string with `laneDir` substituted in (same recursive string/array/object walk Shipyard's Python does).
- `pinPlaywrightOutputDir(servers, laneDir)` — for each server whose `args` includes an entry starting with `@playwright/mcp` and has no `--output-dir` already, appends `["--output-dir", path.join(laneDir, ".playwright-mcp")]`.
- `seedProfiles(sourceRepo, laneDir)` — for each subdirectory of `<sourceRepo>/.playwright-mcp/profiles/`, if `<laneDir>/.playwright-mcp/profiles/<name>` doesn't already exist, recursively copy it there and delete any `Singleton*` files in the copy (stale lock files from the source's own last-run browser process — copying them would make the lane's browser fail to start, thinking another instance already holds the profile).
- `syncMcp(lane)` — orchestrates the above, writes `<lane.cwd>/.mcp.json` as `{mcpServers: <relocated+pinned servers>}`, appends `.mcp.json` to `.git/info/exclude` (same idempotent read-then-append pattern as E2/E3's `info/*` writes — though `.mcp.json` sits directly under the lane root, not `.git/info/`, so this reuses the exclude-file mechanics without needing `--git-common-dir` resolution the way `.git/info/exclude`'s own location does). Returns `{servers: string[], profilesSeeded: string[]}`.

### Route + CLI

`POST /api/lanes/:id/mcp/sync` — synchronous (JSON parse + a few file copies, not a long-running process), 200 with the `syncMcp` result, 400 `ENOMCPCONFIG` when the source repo has no MCP servers configured. `ccam lanes mcp sync [<id>]` — same `resolveLaneArg` pattern as `lanes agents install`.

## Invariants

1. **CCAM still does not orchestrate; this doesn't add a new automation surface either.** `mcp sync` copies static config and files; it never grants a permission, launches a process, or triggers anything.
2. **Never a silent partial state.** A lane with no source-repo MCP config gets a clear error, not an empty-but-present `.mcp.json`.
3. **A lane's existing browser-profile login is never overwritten**, only ever seeded once.

## Verify

Unit tests against a fake `~/.claude.json` (via `HOME` override in the test process, same isolation technique the rest of this test suite already uses for `DASHBOARD_DB_PATH`/`LANES_ROOT`) and a fixture source-repo directory with a `.playwright-mcp/profiles/` tree: relocation correctness, `--output-dir` pinning only where absent, profile seeding skips an existing destination, `Singleton*` files are stripped from a freshly-seeded copy, `.git/info/exclude` idempotency, and the `ENOMCPCONFIG` error path for a source repo with nothing configured.
