Screen MCP tool calls before they run

An MCP server is a process your agent can ask to do things, and the client decides which requests to send it. agent-chaperone puts a screen on that wire. It reads each tool call before the server sees it, and each result before the agent does, using the same policy file and the same log for both directions.

Nothing about the server changes, and nothing about the client changes except one line of configuration. Everything that is not a tool call, a tool result or a resource read passes through untouched, including the tool list.

Put a server behind it #

For a server that runs as a local process, put agent-chaperone in front of the command that was already there. Everything after -- is the server that would have run anyway.

{
  "mcpServers": {
    "filesystem": {
      "command": "npx",
      "args": ["-y", "agent-chaperone", "--",
               "npx", "-y", "@modelcontextprotocol/server-filesystem", "."]
    }
  }
}

For a server that answers over Streamable HTTP somewhere else, give it the URL instead. The proxy still speaks stdio to the client, so the client does not know the difference.

{
  "mcpServers": {
    "github": {
      "command": "npx",
      "args": ["-y", "agent-chaperone", "--", "https://api.githubcopilot.com/mcp/"]
    }
  }
}

To do that to a configuration file you already have, agent-chaperone wrap makes the edit for you. Give it the path to the client's configuration file, such as Claude Code's ~/.claude.json. It prints what it would change and writes nothing until you add --write, and it keeps the original beside the file, because a client will not start without this file and a bad edit breaks every server at once. --unwrap takes it back out, and running either twice changes nothing.

agent-chaperone wrap ~/.claude.json
agent-chaperone wrap ~/.claude.json --write

Screening needs an API key for the model backend. Without one the proxy still starts and the deterministic rules still run, and every judgment records that no model was asked. It never refuses to start because a key is missing.

What crosses the proxy #

Requests and responses are paired by their JSON-RPC id, so a result is screened together with the arguments that produced it rather than on its own.

What the pre-call screen asks #

Deterministic rules run first and can settle the call without asking a model anything. Those are the per-server allow and deny lists, the MCP annotations a server attaches to its own tools, regexes for obviously dangerous shell forms, and regexes for secret-shaped strings, which are replaced with a placeholder before anything is sent anywhere.

What survives that goes out as one request carrying independent questions.

The decision is code, first match wins: a deny list hit blocks, an allow list that does not name the tool blocks, then exfiltration or a secret in the arguments, then a destructive change, then the policy, then the task. Anything left over is forwarded.

The exact wording of every question is in the design document, because a screen whose questions you cannot read is asking you to trust a summary of them.

What comes back is screened too #

A tool result is text somebody else wrote, and the agent reads it as input. The post-result screen looks for hidden regions first, which is zero-width and bidirectional characters, HTML comments, hidden blocks and base64 runs, then asks whether any part of the result is written to instruct the reader rather than to inform it. At 0.5 the result is annotated and the agent still reads it. At 0.8, and only when the harm judgment also reaches 1.5, it is withheld.

That half has its own guide, with the measured counts for what it catches and misses.

Run it in shadow first #

Shadow is the default, and it blocks nothing. Every judgment is written to a local log with the probabilities that produced it, and the entries marked as would-have are exactly the list of things that change if you switch.

agent-chaperone report

That leads with the count of decisions enforcement would have stopped and did not. If you disagree with a line, move the threshold rather than the mode, and you do not have to guess at the new number: replay decides again over judgments already recorded, under a policy you are considering, and says which way each one moves. No model is asked twice.

agent-chaperone replay --policy candidate.yaml

When the log stops surprising you, set mode: enforce in the policy file. In enforce a held call does not run, and the agent is handed a message saying a person is deciding, along with a token that names that one call. Approving a write to one path does not release a write to another, and the token expires.

What a proxy cannot see #

Read this part before deciding the configuration above covers you.

One more thing worth knowing before turning this on: the arguments and results being screened go to the configured model backend, so that content leaves the machine. Secret-shaped strings are replaced first, and screen_results: false turns screening off for a server whose content has to stay put.

Next #