Um coding agent pode passar pelo gate do repositório, respeitar o teto de custo e ainda chamar uma tool destrutiva no meio da execução. O PR mostra o estado final; não mostra necessariamente cada efeito produzido entre duas chamadas ao modelo.
O resultado que queremos é verificável, não uma promessa no prompt:
tools/listexpõewrite_noteeread_ledger, mas nãorm_rf;- uma chamada direta a
rm_rfrecebe erro JSON-RPC antes de tocar o disco; write_notegrava uma única nota porrequest_id;read_ledgerobserva o estado depois da mutação;- um teste mata o servidor antes do commit e repete a chamada sem duplicar o efeito;
- cada decisão entra em um JSONL append-only com uma cadeia SHA-256 editorial.
Isso complementa os gates antes do primeiro PR e o circuit breaker de custo. O ponto novo é a fronteira da chamada de tool: ali o servidor consegue dizer “não” sem negociar com o modelo.
Contexto mínimo: MCP 2026-07-28 e Python SDK 2.1.1
O exemplo usa Python 3.11, funciona a partir do Python 3.10 e fixa mcp==2.1.1, versão publicada em 25 de agosto de 2026. No SDK v2, a classe de alto nível é MCPServer; FastMCP pertence à linha v1. A combinação está documentada nas notas do SDK v2 e na página do pacote 2.1.1.
uv init gated-mcp
cd gated-mcp
uv add "mcp==2.1.1"
No transporte stdio, o host inicia o servidor como subprocesso. stdin e stdout carregam mensagens JSON-RPC, uma por linha, sem Content-Length; logs ficam em stderr. Portanto, nada de print() no servidor. Um print("subiu") parece inocente até virar parte do protocolo. O protocolo, como de costume, não aprecia improviso.
A anatomia atual é curta:
- o cliente pode consultar
server/discover; - chama
tools/listpara obter as tools disponíveis; - envia
tools/callcom nome e argumentos; - recebe um resultado ou um erro JSON-RPC.
Na revisão MCP 2026-07-28, não existe handshake de inicialização. Cada request leva em _meta a versão do protocolo e as capacidades do cliente, e todo servidor deve implementar