<!-- Generated from /guides/mcp-security by scripts/make-markdown.mjs. The page is the source. -->

# 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

- **tools/call, on the way out.** The pre-call screen reads the tool name and the arguments, with secret shapes already replaced, and decides whether to forward or hold.
- **The result, on the way back.** The post-result screen reads what came back and decides whether the agent sees it as it arrived, with a warning attached, or not at all.
- **resources/read.** Screened as a result, because a resource body is text from the same place a result comes from.
- **tools/list.** Compared against the list that server first sent, so a description that was rewritten after you trusted it is reported. The list itself is never withheld.

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.

- **destructive.** Would running this delete, overwrite or irreversibly change data, files, records, infrastructure or account state. Held at 0.7.
- **exfiltration.** Do the arguments send local file contents, environment variables, credentials or private records to an external destination. Held at 0.6, and tested before the destructive arm.
- **secret_in_args.** Do the arguments carry a credential. This runs after the regexes, so it is the backstop for a shape they did not match.
- **policy_violation.** Does the call do something the policy prose forbids. Only sent when a policy is written, and not covered by the published numbers.
- **off_task.** Is the call unrelated to the task you recorded. Only sent when a task exists, held at a deliberately high threshold, and not covered by the published numbers either.
- **severity.** How much damage the call could do, from a read with no side effects to irreversible loss. This does not gate anything. It labels the hold, and a hold resting on a low-confidence severity is labelled uncertain rather than stated flatly.

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](https://github.com/agent-chaperone/agent-chaperone/blob/main/docs/design.md), 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](https://agentchaperone.dev/guides/prompt-injection), 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.

- **The client's own tools.** A proxy sees MCP traffic. It does not see the shell commands, file edits and web fetches a client runs itself, and on the clients people actually use those are where most of the damage lives. [Hooks cover that separately](https://agentchaperone.dev/guides/claude-code).
- **What a server does rather than what it said it would.** This is not a sandbox. It reads the call. A server that accepts a harmless-looking call and then does something else is outside it entirely, and OS-level isolation is what covers that. The two belong together.
- **Anything the annotations claim on their own.** A server describes its own tools, so its annotations are hints to the policy engine rather than a bypass. The MCP specification says to treat them as untrusted unless the server is trusted, and that is how they are treated.
- **Content that never leaves as a tool call.** A file you paste into your own message reaches the model without any tool running, so nothing here sees it and nothing records it.

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

- [The measured results](https://agentchaperone.dev/results), including every threshold and the misses named.
- [Why a tool allow-list does not catch a secret leaving](https://agentchaperone.dev/guides/secret-exfiltration).
- [The design document](https://github.com/agent-chaperone/agent-chaperone/blob/main/docs/design.md), for the exact wording of every question and the full policy file.

---

Source: https://agentchaperone.dev/guides/mcp-security
Put an MCP server behind a screening proxy with one change to a client configuration. What each screen asks, how to run it in shadow mode first, and what a proxy cannot see.
