A coding agent can pass the repository gate, stay under the cost ceiling, and still call a destructive tool halfway through execution. The PR shows the final state; it does not necessarily show every effect produced between two calls to the model.
The result we want is verifiable, not a promise tucked into the prompt:
tools/listexposeswrite_noteandread_ledger, but notrm_rf;- a direct call to
rm_rfgets a JSON-RPC error before touching the disk; write_notewrites exactly one note perrequest_id;read_ledgerobserves the state after the mutation;- a test kills the server before commit and retries the call without duplicating the effect;
- every decision goes into an append-only JSONL file with an editorial SHA-256 chain.
This complements the gates before the first PR and the cost circuit breaker. The new part is the tool-call boundary: that is where the server can say "no" without negotiating with the model.
Minimum context: MCP 2026-07-28 and Python SDK 2.1.1
The example uses Python 3.11, works on Python 3.10 and later, and pins mcp==2.1.1, released on August 25, 2026. In SDK v2, the high-level class is MCPServer; FastMCP belongs to the v1 line. This combination is documented in the SDK v2 notes and on the 2.1.1 package page.
uv init gated-mcp
cd gated-mcp
uv add "mcp==2.1.1"
With the stdio transport, the host starts the server as a subprocess. stdin and stdout carry JSON-RPC messages, one per line, without Content-Length; logs go to stderr. So, no print() calls in the server. A harmless-looking print("subiu") eventually becomes part of the protocol. Protocols, as usual, do not care for improvisation.
The current anatomy is short:
- the client may query
server/discover; - it calls
tools/listto get the available tools; - it sends
tools/callwith a name and arguments; - it receives either a result or a JSON-RPC error.
In the MCP 2026-07-28 revision, there is no initialization handshake. Every request carries the protocol version and client capabilities in _meta, and every server must implement