Files
nntrivi2001 4da7ef0453 docs(lanes): design F1 — ccam lanes mcp sync (F)
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).
2026-08-05 15:10:42 +07:00

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:

  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.jsonenableAllProjectMcpServers, 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.