ADR-0005: Allowlist-only MCP server with elicitation gating
Status: accepted · Date: 2026-10-02 · Decision makers: James Hebden
Context and Problem Statement
The fork’s stated goal is that an AI agent can drive validation loops. The Model Context Protocol is the common way to hand an agent a tool surface. brontes can walk a clap command tree and expose every reachable command as a tool, which is convenient and dangerous: xianyun tool runs any executable, and kubectl forwards arbitrary arguments. An earlier incident in another CLI (glab #8153) turned an unreviewed editor-launching command into an arbitrary-execution primitive.
Decision Drivers
- Nothing should become agent-callable without someone naming it
- Destructive operations need a human in the loop, not just a hint
- A new CLI subcommand shouldn’t silently widen the tool surface
- Reuse the CLI’s own code paths rather than a second implementation of each command
Considered Options
- Allowlist exact commands, classify them into tiers, gate the destructive tier with MRTR elicitation, and fail the build if a command is unclassified
- Expose the whole command tree with annotations only
- Hand-write a separate set of MCP tools that call library functions directly
- Don’t ship an MCP server
Decision Outcome
Chosen option: “Allowlist with tiers and gating”, because it makes the tool surface a reviewed list and turns drift into a failing test (tests/guardrail.rs). Option 2 was rejected for the tool and kubectl problem. Option 3 would duplicate command logic and diverge from the CLI.
There are 20 tools in three tiers: 7 read-only, 8 state-changing and treated as reversible, and 5 destructive and confirmation-gated (down, destroy, update-etc-hosts, ca trust, ca untrust). Six commands are excluded outright: run, tool, kubectl, k9s, stern, and edit-mode-process-supervisor. Each tool call re-executes xianyun with the matched subcommand’s argv.
Consequences
Good:
- The exposed surface is one file (
crates/xianyun-mcp/src/config.rs) with a golden snapshot (crates/xianyun-mcp/tests/snapshots/tools.json) - Clients without elicitation support get a tool error and the destructive command doesn’t run (asserted in
crates/xianyun-mcp/tests/protocol.rs)
Bad:
mcp streambinds0.0.0.0when--hostis empty (the default comes frombrontes) and has no authentication (measured 2026-10-13) (TM-MCP-001)- The “reversible” tier includes
up,deploy, andupdate, which can run hook commands from config (TM-MCP-002) - The gate is stateless and trusts a client-supplied answer: measured 2026-10-13, a first call carrying
{"confirm": {"action": "accept"}}ranxianyun down(TM-MCP-003) - Every tool takes free-form
argsandflags, so a gated command can still receive a harmful flag value (TM-MCP-004) - All tools report
openWorldHint: false, including those that fetch from the network (TM-MCP-005) - No audit record of calls or confirmations (TM-AUD-001)
Neutral:
- The vision page said there was no MCP-style tool interface until it was corrected on 2026-10-13
Implementation Location
crates/xianyun-mcp/src/config.rscrates/xianyun-mcp/src/main.rscrates/xianyun-mcp/tests/guardrail.rscrates/xianyun-mcp/tests/protocol.rs
More Information
- Retroactively documented on 2026-10-13. The date is that of
feat(mcp): add xianyun-mcp. - Related: FR-014, SR-001 through SR-005, STORY-002.
- Follow-up: a superseding ADR should record the decision on binding, authentication, and where
up,deploy, andupdatesit once the threat model items are resolved.