Files
AgentHub/docs/superpowers/specs/2026-08-05-mcp-sync-design.md
nntrivi2001 17d56d0e01 refactor!: rebrand CCAM to AgentHub
Clean break, no compatibility alias. Renames the product name, the CLI
binary (`ccam` -> `agenthub`), the env var prefix (`CCAM_` ->
`AGENTHUB_`), the Prometheus metric namespace (`ccam_*` ->
`agenthub_*`), the tmux session prefix (`ccam-lane-<id>` ->
`agenthub-lane-<id>`), the per-repo runtime profile directory
(`.ccam/profile` -> `.agenthub/profile`), the lanes state root
(`~/.claude/ccam-lanes` -> `~/.claude/agenthub-lanes`), the bundled
plugins (`plugins/ccam*` -> `plugins/agenthub*`) and their slash
commands (`/ccam-doctor` -> `/agenthub-doctor`, `/ccam:*` ->
`/agenthub:*`).

Because there is no alias, an existing install has to be updated by
hand: rename its `.ccam/profile` directory and any `CCAM_*` variables,
and reinstall the plugin under its new name.

Two other name families are deliberately left alone here, since they are
not just strings: the `Claude-Code-Monitor` repository/marketplace name,
and the `agent-dashboard` npm package plus its `~/.claude/agent-dashboard`
runtime directory, which holds the live database and needs a migration
rather than a rename.

Verified: server 1406/1406, client 459/459, `grep -i ccam` clean across
tracked files, `openapi.yaml` regenerated (95 paths, unchanged count).
2026-09-07 12:09:15 +07:00

8.6 KiB

F1 — agenthub 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. agenthub 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. AgentHub 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 AgentHub 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 AgentHub Status
lane-mcp-sync.sh <N> agenthub lanes mcp sync [<id>] new in this task
ensure_proof_link "$DIR" (called at the top of sync_lane) not called — proof-link is agenthub 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. agenthub lanes mcp sync [<id>] — same resolveLaneArg pattern as lanes agents install.

Invariants

  1. AgentHub 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.