Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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

  1. Allowlist exact commands, classify them into tiers, gate the destructive tier with MRTR elicitation, and fail the build if a command is unclassified
  2. Expose the whole command tree with annotations only
  3. Hand-write a separate set of MCP tools that call library functions directly
  4. 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 stream binds 0.0.0.0 when --host is empty (the default comes from brontes) and has no authentication (measured 2026-10-13) (TM-MCP-001)
  • The “reversible” tier includes up, deploy, and update, 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"}} ran xianyun down (TM-MCP-003)
  • Every tool takes free-form args and flags, 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.rs
  • crates/xianyun-mcp/src/main.rs
  • crates/xianyun-mcp/tests/guardrail.rs
  • crates/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, and update sit once the threat model items are resolved.