Screen MCP tool calls in Cursor
Cursor starts each MCP server you configure as a local process and talks to it over stdio. agent-chaperone goes in front of that process. It reads each tool call before the server gets it and each result before the agent does, and nothing about the server changes.
This page covers MCP servers. Cursor's own terminal commands, file edits and web fetches do not go through MCP, so the proxy does not see them. From 0.4.0, hooks you register in Cursor reach them instead, with limits set by what Cursor lets a hook do: the configuration is in the hooks reference.
Install it #
npm install -g agent-chaperoneA global install means Cursor starts the proxy directly. Running it through npx works too, but adds a package lookup every time a server starts.
Put a server behind it #
Cursor reads servers from ~/.cursor/mcp.json for every project and from .cursor/mcp.json inside one project, under mcpServers. The editor and the Cursor CLI read the same files.
agent-chaperone wrap makes the edit. It prints what it would change and writes nothing until you add --write, and it keeps the original beside the file.
agent-chaperone wrap ~/.cursor/mcp.json
agent-chaperone wrap ~/.cursor/mcp.json --writeA server entry before and after:
{
"mcpServers": {
"filesystem": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/Users/me/project"]
}
}
}{
"mcpServers": {
"filesystem": {
"command": "agent-chaperone",
"args": ["--server", "filesystem", "--",
"npx", "-y", "@modelcontextprotocol/server-filesystem", "/Users/me/project"]
}
}
}Everything after -- is the server that would have run anyway. --server names the section of the policy file that applies, after the name you gave the server in Cursor. --unwrap takes it all back out.
Give it the API key #
The model screens need TYPESAFE_API_KEY. A server started by Cursor gets the environment Cursor itself was started with, so a key exported in your shell profile reaches the proxy once Cursor has been restarted. Cursor's help says the same of any variable a server needs.
To keep the key out of your shell profile, put it in a file of NAME=value lines and point the entry at it with envFile, which Cursor supports for local servers:
"filesystem": {
"command": "agent-chaperone",
"args": ["--server", "filesystem", "--", "npx", "-y", "@modelcontextprotocol/server-filesystem", "/Users/me/project"],
"envFile": "/Users/me/.config/agent-chaperone/env"
}The proxy removes its own keys before it starts the server behind it, so the server never receives TYPESAFE_API_KEY either way.
Without a key the proxy still starts, the deterministic rules still run, and every judgment records that no model was asked. It never refuses to start because a key is missing.
Check that it is working #
The server should show as connected under Customize, then MCPs. If it does not, the Output panel's MCP Logs has what the process printed. In the CLI, agent mcp list shows the same status.
Then use the server once from the agent and read what was decided:
agent-chaperone logEach line is one call or result, with the decision and, in shadow mode, what enforcing would have done instead. agent-chaperone log --follow prints them as they happen. A line ending in [not screened] was decided without a model, which means the key did not arrive; the proxy also says so in MCP Logs when it starts.
What this does not cover #
- Cursor's own tools, without hooks. Terminal commands, file edits and file reads that Cursor runs itself never cross an MCP connection. Hooks reach them, from 0.4.0, with three limits. A held call is refused, because Cursor does not act on a hook that asks, and it goes through once after
agent-chaperone approve <id>. A result judged unsafe is annotated and still reaches the model, because Cursor does not let a hook replace a result from its own tools.ReadandGrepresults reach a hook only as a summary, so a file the agent reads is not screened. The hooks were written against Cursor's published contract and have not been checked in a live run of Cursor yet. - A server that authenticates with headers. Headers written in the Cursor entry stay with Cursor and never reach the proxy, which is why
wrap, from 0.3.2, leaves such an entry alone. Put it behind the proxy by hand, as the MCP guide shows, and pass the token with--header-env. - A server that speaks only the older SSE transport.
wraptreats aurlentry as Streamable HTTP, the only transport the proxy reaches a URL over, so a server that answers over SSE alone will not connect once wrapped. - What a server does rather than what the call says. This is not a sandbox. It reads the call and the result, and a server that does something its arguments did not describe is outside it.
Next #
It starts in shadow mode, which blocks nothing. The MCP guide covers what each screen asks, how to read a shadow-mode log, and when to switch to enforce. The exact wording of every question is in the design document.