ADR 0045: The MCP server is core infrastructure, not a feature toggle
Amendments.
- Vocabulary: current spellings are
done(formerlyfinish),mapwhere it names the command, config, or tree (formerlydocs), andAgent file(formerlyCompiled agent file); the decisions below are unchanged.- ADR 0101 — features retired:
[features]has since been removed in full: every subsystem and MCP tool registers unconditionally, so the feature-gated map/worktree tools and feature-aware instructions described below are historical. Setup-state refusals still apply, and provider MCP wiring remains unconditional. This record's core-infrastructure decision stands.
Status: accepted; revises the feature set from ADR 0020 (which introduced [features], mcp among them) and builds on ADR 0041 and ADR 0031
Context
§ADR 0020 introduced [features] — per-subsystem on/off switches — and listed mcp among them, so a project could set [features].mcp = false to keep discern from wiring its MCP server into each agent's config. That made sense when the MCP integration was new and unproven.
Three things have since changed what mcp actually toggles:
- The server is a zero-cost thin adapter. It is bundled in the one binary, launched on demand (
discern mcp), and every tool is a like-for-like call into the same verb cores the CLI runs. Nothing is installed and nothing runs until a client connects — "available" costs nothing. - The shipped guidance is now built around it. The Agent files are MCP-first (prefer the
discern_*tools), with a CLI fallback that already covers any client that does not connect. There is one operating model, and it assumes the tools exist. - The flag conflated two different questions. "Is the server available?" — now always, it is the spine — versus "does this client connect?" — which varies and is handled entirely by the CLI fallback. A
[features].mcp = falseno longer expresses a coherent product state: it would force a second, conditional operating-model phrasing inbase.mdfor a configuration that contradicts how discern now works.
The toggle also carried real machinery: refresh wired the server when the feature was on and removed it (unwireProviderMcp / the provider registry's unregister hook) when off, reporting the change as mcp_removed.
Decision
§MCP is core infrastructure, like the quality gate, the config surface, and doctor — always on, never a [features] member.
- The feature is gone from the SSOT.
mcpis dropped fromFEATURES(shared/features.ts) and from the schema's[features]block (config_schema.ts). Because the status feature snapshot, itsStatusFeaturesSchema, anddoctor's "all features on" line all derive fromFEATURES, they follow automatically; the result-schema faithfulness test (ADR 0041) is the guard that they stay in lockstep. - Wiring is unconditional.
refresh/upgrade/ worktree-setup always (re-)establish the server for every configured agent (idempotently, best-effort). Themcpverb is registered unconditionally, likedoneandstatus. - The removal path goes with the toggle. With no "off" state, the feature-off branch, the now permanently-empty
mcp_removedresult field, and the thereby-orphanedunwireProviderMcp/unregisterClaudeCodeMcp/ registryunregisterhook are dead code — kept only, they would misrepresent a capability discern no longer has. They are removed wholesale (single source of truth; no dead code). What stays gated is unchanged: themap- andworktrees-gated MCP tools still appear only when their own feature is on. - Schema 10 → 11 drops the key. A migration deletes
[features].mcp(comment-preserving, idempotent) so an existingmcp = true|falseconfig is cleaned on the nextdiscern upgrade. - The strict-schema transition is graceful.
[features]is az.strictObject, so an un-migrated config still carryingmcpfails the typed load — but the failure is caught at the top-level config chokepoint and rendered as a friendly per-issue diagnostic, and a tailored message (mirroring the[worktree]dead-adapter precedent) tells the reader the MCP server is core now and to rundiscern upgrade.upgrade/doctorread through the raw, un-validated reader, so they are never blocked by the leftover key.
Consequences
§- One operating model.
base.mdis unconditionally MCP-first, which is now simply correct — there is no incoherent "MCP off" branch to phrase. - The shipped guidance needed no change. It never referenced the toggle; it is now aligned with reality rather than ahead of it.
- Less surface, less dead code. The register/unregister symmetry is gone on purpose: the registry models only what the product does (register), and the refresh result no longer carries an always-empty
mcp_removed. - First removal of a feature key. Every prior schema change was additive, so an old config stayed valid against the new strict schema; this is the first that makes a leftover key invalid. The friendly diagnostic + the raw-reader upgrade path are what keep that transition from being a cryptic crash — a pattern future feature removals can reuse.
- No way to opt out of wiring. A project that genuinely does not want the
.mcp.jsonentry no longer has a config switch. This is intended: the server is free unless a client connects, and the entry is the discoverability mechanism the MCP-first guidance assumes.
Alternatives considered
§- Keep the toggle, leave it defaulting on. Rejected: it still forces a conditional second operating model in the guidance for a state that no longer makes product sense, and it keeps the removal machinery alive for a path nothing triggers.
- Make
[features]non-strict so leftover keys are silently ignored. Avoids the transition entirely, but throws away the write-time typo protection strict parsing buys (features.bogusshould be an error). The friendly diagnostic plus the migration give a better transition without weakening the schema. - Drop the feature but keep
unwireProviderMcpas a registry utility. Rejected: with no caller it is dead code that only its own unit tests exercise; register/unregister symmetry is not worth retaining a capability the product never invokes.