Forge playbook — MCP-as-client doctrine (dormant, opt-in)
Do not duplicate the config — defer to: config/orchestration/mcp-registry.json (the server catalog)
and config/orchestration/mcp-grants.json (the per-Boss least-privilege matrix) as the single source of
truth for tiers, servers, and grants. This file is doctrine/orchestration only — it explains how to use
those configs correctly, it does not re-list every server or re-derive the matrix.
Hard rules
- Nothing here installs, connects to, or activates any MCP server. The zero-dependency Forge default (fs/path only, no live network client) stays 100% intact until the owner explicitly opts a server in.
- A server is usable only when both are true: (1) the owner has opted it in (a marker naming the
server id — see "Opt-in flow" below), and (2) the requesting Boss's grant covers it (tier + server
id, per
mcp-grants.json). Either condition failing means dormant → honest native fallback. - Tier 3 (WRITE-PRIMITIVE) is never auto-used. Every tier-3 call routes through the existing
forge-actiongate.cjshard-gate classifier (the same one used for deploy/push/spend/DNS/etc.) — it is never bypassed and never reimplemented in parallel. - Never claim an MCP tool ran, was available, or was opted in when it wasn't. Absence is reported as a labeled native fallback, not silently skipped.
What "MCP-as-client" means here
Forge already ships one MCP artifact: forge-bin/forge-mcp.cjs, a read-only server that exposes this
project's own Forge run state (runs/events/reports/memory) to an external MCP host. That is Forge acting
as a server — unrelated to this skill.
This skill is the opposite direction: Forge (its Bosses) acting as an MCP client, consuming external MCP servers (docs lookup, web search, browser automation, GitHub, etc.) as extra tools during a task. The client side is deliberately dormant by default — a catalog and a validator, not a running connection.
The 4 capability tiers (least-privilege)
| Tier | Class | Examples | Notes |
|---|---|---|---|
| 0 | Read-only LOCAL | LSP/code-intelligence read (serena-lsp), local docs |
No network call at all. |
| 1 | Read-only REMOTE | web search (exa), docs fetch (context7), scrape (firecrawl), GitHub read (github-read) |
Network read, no writes; a credential (if any) is a read-scoped key only. |
| 2 | SANDBOXED action | browser-QA drive (playwright, chrome-devtools) |
Can click/type/screenshot a page it navigates to — ephemeral browser state, never a project or repo write. |
| 3 | WRITE-PRIMITIVE | any MCP write (github-write, file write, deploy, publish) |
Always routes through forge-actiongate.cjs; never granted by default to any Boss. |
Tiers and the full server catalog live in config/orchestration/mcp-registry.json. Every entry there
starts (and stays, until opted in) at "status": "not-installed" — "active" in that file is forbidden.
Per-Boss least-privilege model
Each of the 12 permanent Bosses (config/agents/agent-registry.json) has a max tier and an explicit
allow-list of server ids in config/orchestration/mcp-grants.json. Both conditions are checked
independently — a Boss can never get a tool above its max tier, and never a server outside its allow-list,
even if one side alone would technically allow it. A Boss with no entry in that file defaults to
max_tier: 0, allow_servers: [] (no grant at all — native fallback only); there is no implicit wide grant.
Current defaults (see mcp-grants.json for the authoritative, evolving list and the "why" per Boss):
search-boss and integration-boss get tier-1 docs/search; review-boss gets tier-1 read-only GitHub/docs;
test-boss and ui-boss get tier-2 sandboxed browser-QA; build-boss gets tier-0 local LSP only. No Boss gets
tier 3 by default — Boss, head-chef, seo-boss, security-boss, skill-boss, and docs-boss have no default
MCP grant at all in the current matrix.
"mcp-write is a write-primitive" → hard-gate
A tier-3 MCP tool call (e.g. github-write creating a commit/PR, or any MCP tool that writes files,
deploys, or publishes) is treated exactly like deploy, git-push, spend, or credential-rotate in
config/orchestration/hard-gates.json. It is classified through the existing
forge-actiongate.cjs::classify() — reuse that hard-gate, never duplicate its regex/logic — and
requires an explicit per-use owner_confirmed: true before proceeding, regardless of autonomy level.
There is no separate "MCP write" bypass path; write is write.
Defer-loading (token discipline)
Never pre-load every MCP tool schema for a task. A Boss that has a valid grant requests only the specific
tool(s) it needs for the current step via ToolSearch (or the equivalent narrow lookup), the same
discipline this project already applies to ECC/Codex/MCP loading generally (see the project's Token
Efficiency governance: "load only the tools needed for the current task; fetch deferred tool schemas on
demand"). A planner-style planLoad() step returns only the relevant tool(s) for the task at hand — never
the full catalog.
Opt-in flow (dormant until the owner acts)
- Nothing is active out of the box.
config/orchestration/mcp-registry.jsonis a catalog, not a connection — every entry starts at"status": "not-installed". - The owner opts a specific server in explicitly (naming its registry
id). Until that marker exists for a given server id, that server stays dormant for every Boss, no matter what its grant would allow. - See
config/mcp/.mcp.json.examplein this project for a worked, DORMANT example of what opting one server in looks like end-to-end (host wiring + the explicit opt-in marker) — copy and adapt it, do not activate it by editing this skill. - Opting in a server does not widen any Boss's grant. Opt-in only makes a server reachable;
mcp-grants.jsonstill decides who may use it and at what tier.
Requesting + validating a grant
Before a Boss calls any MCP tool it must validate, not assume, that it may:
- Confirm the server id has been opted in (step 2 above) — if not, stop here and use the native fallback.
- Look up its own entry in
config/orchestration/mcp-grants.json: is the server id in itsallow_servers, and is the server's tier (frommcp-registry.json)<= max_tier? Both must hold. - If the server is tier 3, additionally run it through
forge-actiongate.cjs::classify()and requireowner_confirmed: truebefore the call — no exception, even if steps 1–2 passed. - Only after all applicable checks pass does the Boss call the tool, and only the specific tool it needs (defer-loading, above) — never the server's full toolset just because access exists.
- Log the outcome honestly: a real MCP call is reported as used; a denied/ungranted/not-opted-in request is reported as a labeled native fallback (see below), never silently absorbed into "it worked."
Honest native fallback
When a server is absent, not opted in, or the requesting Boss's grant does not cover it, the Boss does not stall the task — it falls back to its existing native capability (repo read, Claude's own reasoning, WebSearch/WebFetch if separately available, manual QA, etc.) and says so explicitly in its report: which server/tool was wanted, why it wasn't available (not opted in / grant denied / tier too low), and what native path was used instead. Never claim an MCP tool ran, or imply a capability gap didn't exist, when a fallback was actually used.
Skills / commands / MCP
config/orchestration/mcp-registry.json (catalog), config/orchestration/mcp-grants.json (per-Boss
matrix), forge-bin/forge-actiongate.cjs (tier-3 hard-gate, reused not duplicated),
config/mcp/.mcp.json.example (dormant worked example — never a live .mcp.json).
Fan-out & flow
No dedicated team — this is a cross-cutting doctrine every Boss consults inline before attempting an MCP call. No fan-out of its own; it does not spawn agents.
Ship-readiness (unique)
Zero-dep default provably unbroken (no new runtime dependency, no auto-started connection); every tier-3
attempt is provably routed through forge-actiongate.cjs; no Boss ever exceeds its max_tier or
allow_servers; every MCP absence is reported as a labeled native fallback, never silently dropped or
falsely claimed as success.