12. MCP distribution via the plugin at user scope¶
- Status: Accepted
- Date: 2026-07-01
- Deciders: kristof (owner), Claude (orchestrator)
- Relates: ADR-0006, ADR-0011, distribution, issue #14
Context¶
commonwealth init used to register the MCP server with a raw claude mcp add commonwealth …
(no --scope). That has two defects:
- Local scope. Without
--scope,claude mcp adddefaults to LOCAL scope — the registration is private to the single directoryinitran in and is invisible to/mcpin any other session. A team-brain that only works in one folder is useless. - A pinned brain. The registration baked a STATIC
COMMONWEALTH_BRAIN_DIR=<dir>into its env, so every repo that shared the one registration would talk to one brain — defeating the per-repo routing the registry (ADR-0011) exists to provide.
The plugin (packages/plugin) already declares the commonwealth MCP server and the
SessionStart/SessionEnd hooks. A Claude Code plugin installs at USER scope (global) and its
SessionStart hook receives the real session cwd on stdin, which the MCP server process cannot
see reliably. That is exactly the shape per-repo routing needs.
Decision¶
- The MCP is distributed via the plugin, installed at USER scope through a repo marketplace.
The repo root ships
.claude-plugin/marketplace.jsondeclaring thecommonwealthplugin at./packages/plugin. Teammates runclaude plugin marketplace add kristoffeys/commonwealththenclaude plugin install commonwealth@commonwealth; the plugin'scommonwealthserver is then available globally in every session. - Per-repo brain routing is dynamic, from two independent resolvers:
- the SessionStart hook resolves the real session cwd → the brain via the registry and injects context (it can see cwd; the server cannot); and
- the MCP server resolves its own brain via
@cmnwlth/core.resolveBrainDir(cwd)at startup (packages/mcp/src/brain.ts), so the tools (search/read/remember) hit the correct brain. An explicitCOMMONWEALTH_BRAIN_DIRstill wins (the plugin/daemon may set it); otherwise the registry maps cwd → brain; a null result degrades to cwd (never crash). - The raw local-scope
claude mcp addis REMOVED.init'sOnboardDeps.registerMcp(brainDir)is replaced byinstallPlugin(): it refreshes the vendored bundle, adds the marketplace (idempotent — skips if acommonwealthmarketplace exists), installs the plugin (idempotent — skips if already installed), and cleans up any STALE rawcommonwealthMCP registration (claude mcp remove commonwealth -s local) so it can't shadow the plugin's server. Every spawn is best-effort and never throws; a missingclaudedegrades to a skipped note.
Consequences¶
- The MCP is global — available in every session, not just the install dir.
/mcpshows it everywhere. - One install serves every repo; the brain is chosen per session by the hook and per process by the server, both via the registry. No brain is pinned into the registration.
- Teammates install with two commands (
claude plugin marketplace add kristoffeys/commonwealthandclaude plugin install commonwealth@commonwealth), or an admin auto-provisions the marketplace + plugin through managed settings (distribution doc §1). initno longer needs the brain dir for the MCP step; the step is idempotent and safe to re-run.- The
--no-pluginflag gates the step (--no-mcpkept as a backward-compatible alias).
Alternatives considered¶
- User-scope raw
claude mcp add -s user— rejected: it fixes the visibility problem but still can't route per-repo (one registration, one env), and it skips the SessionStart/SessionEnd hooks that make the auto-bridge work. The plugin bundles both the server AND the hooks. - Keep local scope, register once per project — rejected: N registrations to manage, each pinned to one brain, none visible across sessions.
Supersedes nothing (it corrects an implementation detail of the distribution approach; ADR-0006 and the distribution doc already named the plugin as the delivery vehicle).