UI–MCP parity — whatever the UI can do, the handle can do
Parent: service-handle
Status:built. The mechanism is
internal/infra/facadeparity— each domain declares its ops as data (fp.Op: id, description, input schema, kind, reach) and every face is a projection of that one registry (convergence-inbound-and-outbound). Enforced byparitymanifestplus a ratchet. The owner-MCP↔admin gap it exposed was real and larger than estimated — 56, not the ~15 guessed — and was paid down 56→0. Re-checked 2026-09-07 at 36789537d:paritymanifest.KnownMCPGaps()returns the empty list (gaps.go:17) and the ratchet (outward_test.go) turns red on any new admin route without an MCP twin; the MCP face is grown from the dispatcher (routes/mcphandle/from_dispatcher.go:45), and intentional asymmetries arefp.Only(reason, facade)—keypairs.create/list/deleteareOnly(FacadeAdmin)exactly as predicted below (manifest_table.go:41-49). Before that (until 2026-07) the roadmap tracked "MCP vs HTTP admin parity" under the externalization migration with no enforcement mechanism — the admin HTTP surface and the service handle's tool surface were two hand-maintained lists, and drift between them was silent. The three options below are the analysis that led to the build; ② shipped asfp.Op(not theCommand{}sketch) and ① shipped as the ratchet +Only(...)exemptions.
The invariant wanted
Every owner operation reachable through the admin UI is also reachable through the service handle (StandMeet-as-MCP-server) — so the owner's AI session is never second-class to the browser. Exceptions must be explicit, never accidental.
Three mechanisms, increasing strength
① The parity gate (cheap — build first). A CI test walks the chi admin route table × OwnerMCPBindings() and checks both against an explicit mapping table with an exemption list; any unmapped, unexempted admin endpoint fails the build. Same species as check-connector-boundary (mechanical-guardrails). The exemption list is itself review material — drift either gets fixed or gets confessed in writing. Legitimate exemptions exist: keypair creation (circular — you need a keypair to call the handle), login/claim (bootstrap), upload-shaped UX.
② Declare once, project twice (structural — the real fix). Drift exists because there are two hand-written surfaces. Register each owner operation once — Command{name, input schema, handler} — and derive both the HTTP route and the MCP tool from the same registry. Parity stops being a checked property and becomes unviolatable by construction. This is the platform's own manifest philosophy applied to itself (OwnerMCPBindings is already a walked declaration; the HTTP side just hasn't joined it), and single-source-of-truth discipline (judgment-audit dimension 3: derived things aren't hand-written).
③ UI drinks from the handle (radical — not recommended now). Make the admin UI itself a client of the service handle: parity by definition. Philosophically consistent with entry-agnostic-agent ("the UI is just another consumer"), but the costs are real — internal calls pay wire-protocol tax, session-cookie ↔ Sigv1 bridging, and upload/SSE shapes fit MCP awkwardly. Mechanism ② gets the same guarantee in-process without the tax.
Recommendation
② as the rule for everything new, ① as the gate over the legacy and the exemptions. ② kills the drift class; ① catches regressions and keeps the exemption list honest. Revisit ③ only if a real second UI (IM gateway) makes "UI as consumer" load-bearing anyway.