Scopes the first of F's four pieces to .mcp.json relocation + Chromium profile seeding only. Drops Shipyard's permission/auto-approval writes (no existing CCAM pattern for a backend command granting permissions, and the rules named agents not in scope: pr-reviewer has no driving skill to port, ticketer is a later task) and the node-version wrapper (environment-specific workaround, no evidence this repo needs it).
8.6 KiB
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:
server/lib/lane-mcp.js— reads the lane'ssource_repo's already-configured MCP servers from~/.claude.json, relocates any absolute path undersource_repoto the lane's owncwd, pins aplaywright-family server's--output-dirto the lane's.playwright-mcp, writes<lane.cwd>/.mcp.json.- 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). .mcp.jsonadded to the lane's local.git/info/exclude.ccam lanes mcp sync [<id>]CLI +POST /api/lanes/:id/mcp/syncroute.SKILL.mdSetup section edit: replace the manual-fallback text with the real command.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
allowrules into<lane>/.claude/settings.local.json—enableAllProjectMcpServers, anmcp__<server>rule per synced server, agh pr comment/review/create/edit/view/diff/list/checks/status+git pushallow-list, and anautoMode.allowbypass-style rule that names theticketer/pr-reviewersub-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-reviewerisn't ported (no driving skill exists for it — confirmed during F1's own scoping conversation) andticketerisn't built yet (F3), so the auto-mode rule would reference agents that don't exist. A human approvinggh pr comment/git pushthrough 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.shnode-version wrapper. Shipyard's script routesnpx/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'sagents installalready 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, returnscfg.projects[sourceRepo]?.mcpServers. ThrowsENOMCPCONFIGif absent/empty.relocateServers(servers, sourceRepo, laneDir)— deep-walks the server config objects, replacing any string containingsourceRepowith the same string withlaneDirsubstituted in (same recursive string/array/object walk Shipyard's Python does).pinPlaywrightOutputDir(servers, laneDir)— for each server whoseargsincludes an entry starting with@playwright/mcpand has no--output-diralready, 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 anySingleton*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.jsonas{mcpServers: <relocated+pinned servers>}, appends.mcp.jsonto.git/info/exclude(same idempotent read-then-append pattern as E2/E3'sinfo/*writes — though.mcp.jsonsits directly under the lane root, not.git/info/, so this reuses the exclude-file mechanics without needing--git-common-dirresolution 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
- CCAM still does not orchestrate; this doesn't add a new automation surface either.
mcp synccopies static config and files; it never grants a permission, launches a process, or triggers anything. - Never a silent partial state. A lane with no source-repo MCP config gets a clear error, not an empty-but-present
.mcp.json. - 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.