What MCP is and where tool calls happen
The Model Context Protocol (MCP) is an open protocol that connects an AI application to servers offering tools, data and prompts. An MCP client, for example Codex, usually starts a local server as a child process and exchanges JSON-RPC messages with it over stdin and stdout, or it connects to a remote server over HTTP.
Two messages matter for access control. A tools/list request is how the client learns which tools a server offers and how each one is described to the model. A tools/call request carries the name of one tool and the arguments the model chose for it. Every control in this guide is a decision about one of those two messages.
Why tool access is the real MCP security risk
Tools are where a model’s output turns into effects: a write, send, merge or delete tool changes something outside the conversation, often irreversibly. Read tools carry a different risk, because whatever they return lands in the model’s context and can end up in a later write or send.
Take an example agent with a filesystem server and an email server. Each tool looks harmless alone. Together they let a confused or manipulated agent read a credentials file and mail it somewhere. The risk sits in the combination, so the unit to design around is the set of tools each agent can reach.
Give each agent an allowlist of the tools it needs
Least privilege for agents means deciding tool access per agent and per task, not per server. A code review agent needs to read files and post a comment; it does not need to push commits. A nightly report agent needs to query a database and write one file; it does not need a shell. Start from the task, list the calls a correct run would make, and grant those.
A denylist names the tools an agent may not use and permits the rest; an allowlist names the tools it may use and refuses the rest. The difference shows when a server changes, for example when a new version adds a write tool. Under a denylist that tool is available the moment it appears. Under an allowlist it stays refused until someone decides otherwise.
Denylists still help to narrow a broad allowlist. A workable pattern:
- Allow by pattern where a server’s naming makes that safe, for example tools starting with read_ or list_.
- Deny the specific tools inside that pattern you do not want, with deny outranking allow.
- Match the tool names the server itself reports, since those arrive in each call.
- Read the new tools/list when you upgrade a server, before granting anything in it.
Set a tool call budget per session
An allowlist decides which tools an agent can use; a budget decides how many calls it can make. A loop that calls a permitted search tool thousands of times is still a problem, for cost, for the rate limits of the service behind the tool, and for the amount of data it pulls into context. A cap on tool calls per session turns a runaway loop into a bounded one. Set it from what a normal session uses, with headroom, and treat reaching it as something to look into.
Require approval for specific MCP tool calls
Some tools should run only after a person says yes: merging a pull request, sending an external email, issuing a refund. For those, the gateway holds the call, asks a person, and forwards it only on approval. A call that nobody answers should be refused after a timeout, never run by default.
The approval has to bind to the exact call, meaning the tool name and its arguments. Approving merges in general is a different decision from approving the merge of one pull request into main, and an approval should never be reusable for a different call to the same tool. Hash the tool name with its canonicalized arguments, attach the approval to that digest, and let it be used once.
How a gateway should refuse a tool call
Two details decide whether restrictions work in practice. First, hide refused tools. If the tools/list answer still includes a tool the agent may not use, the model will plan around it, call it, get refused and try again, wasting turns. Filter the list so the model sees only what it can call, and keep every other field of the answer intact, including paging cursors.
Second, refuse in a way the model can read. MCP can report a failure in two ways: a JSON-RPC error, which says the request itself could not be handled, and a tool result with isError set to true, which says the tool ran and failed. Some clients treat a protocol error as fatal to the session. A tool error result with a plain reason lets the model change course instead.
{"jsonrpc":"2.0","id":7,"result":{"isError":true,"content":[{"type":"text","text":"The tool write_file is not on the allowlist for this run."}]}}Log every tool call and close sessions that end abruptly
Record every call the gateway sees, allowed or refused, with the tool name, the time and the decision. Refusals matter most: a run of refused calls is often the first sign that an agent is confused or being steered. Decide deliberately whether to log arguments. They are the most useful detail in an investigation, and they are also where file contents, customer data and secrets travel.
Plan for sessions that do not end cleanly. MCP clients stop servers in different ways, and some, Codex for one, simply kill the server process when they are done with it. A gateway that reports only on a clean exit leaves those sessions looking as if they are still running. Something that outlives the session has to notice the process has gone and close the record.
Put a gateway in front of an MCP server with Toolcaise Connect
Toolcaise Connect is a command-line connector that runs on the agent’s machine, and its mcp command is a gateway for one MCP server. In the client’s configuration you replace the server’s command with toolcaise-connect mcp, add the rules, and pass the original command after --. The client launches the gateway, the gateway launches the server, and every tools/call passes through it. Everything else is forwarded byte for byte.
toolcaise-connect mcp --name "Repo files" --allow-tools 'read_*,list_*' --deny-tools 'read_multiple_files' --max-tool-calls 500 -- npx -y @modelcontextprotocol/server-filesystem /srv/repo{
"mcpServers": {
"repo-files": {
"command": "toolcaise-connect",
"args": ["mcp", "--name", "Repo files", "--allow-tools", "read_*,list_*", "--max-tool-calls", "500",
"--", "npx", "-y", "@modelcontextprotocol/server-filesystem", "/srv/repo"]
}
}
}Patterns use * as a wildcard and ignore case, and deny outranks allow. The --max-tool-calls flag is one budget for the whole session across every tool. A --policy file can hold the same rules in its tools section, and flags add to that file rather than replace it, so a machine-wide deny list cannot be dropped by passing a flag. The rules are enforced on the machine and keep working when Toolcaise cannot be reached.
A refused call never reaches the server: the model gets a tool error result giving the reason, and refused tools are removed from the tools/list answer. With --name, each session is reported as a run of that agent with a tool span for every call, measured at the gateway. A refused call becomes a failed span plus a guard.tool_denied event carrying the reason.
Dashboard tool rules and approval holds
From Connect 0.3, with --remote-control and --name, a session takes tool rules from the agent’s policy in the dashboard and merges them with the machine’s own, stricter side winning:
- every deny from either side applies
- a tool must be on both allowlists when both sides have one
- the smaller positive call budget holds
- either side can require approval, and neither can waive the other’s
The dashboard can narrow what a machine permits but never widen it. Pause refuses new tool calls with a reason the model can read, and stop ends the MCP server and the session.
toolcaise-connect mcp --name "GitHub tools" --remote-control --require-approval 'merge_*' -- npx -y @modelcontextprotocol/server-githubA call to a tool matching --require-approval is held while a person decides in the dashboard, with the approval bound to that exact call: the tool and its arguments, keys sorted, hashed. Other calls keep flowing meanwhile. Denied and unanswered calls are refused (after ten minutes by default), and without --remote-control a call that needs approval is refused rather than run.
What the Toolcaise MCP gateway does not cover
It gates by tool name, not by argument. A permitted tool called with dangerous arguments is still permitted. Approval holds bind to the exact arguments, but the arguments never leave the machine, so the approver sees the tool name and not what it was called with, and needs another way to see what is about to happen, for example the pull request itself.
It covers only the servers you put behind it. A server the client launches directly is outside it, and so is a remote server reached over HTTP, because the gateway launches a local server as a command and speaks the stdio transport. A session lasts as long as the server does, so a long-lived client produces one long run rather than one per task.
It cannot judge whether a call is wise. It enforces the rules you wrote and records what happened. Deciding that an agent should not have merged that branch still takes a person or a separate check.
Frequently asked questions
How do I restrict which tools an MCP server exposes to an agent?
Put a gateway between the client and the server. It filters the tools/list answer so the model sees only permitted tools, and refuses any call to another tool before it reaches the server. With Toolcaise Connect you replace the server’s command in the client configuration with toolcaise-connect mcp, add --allow-tools or --deny-tools patterns, and pass the original command after --.
Is an MCP allowlist or denylist safer?
An allowlist. A denylist permits every tool you did not name, including tools a server adds in a later version, so a new write or delete tool becomes available without anyone deciding it should be. An allowlist refuses anything it does not name. Use a denylist to narrow a broad allowlist, with deny outranking allow, rather than as the only control.
Can I require human approval before an MCP tool call runs?
Yes, if something sits on the call path and can hold it, and refuses the call when nobody answers. In Toolcaise Connect 0.3 and later, calls to tools matching --require-approval wait for a person’s decision in the dashboard, with the approval bound to the exact call (the tool name and its arguments, hashed), so it cannot release a different call.
What should an agent see when an MCP tool call is blocked?
A tool result with isError set to true and a plain-language reason, rather than a JSON-RPC protocol error. The error result tells the model the tool refused, so it can read why and try another approach. A protocol error says the request itself failed, and some clients treat that as fatal to the session. Refused tools should also be left out of the tool list.
Does an MCP gateway check the arguments of a tool call?
The Toolcaise gateway does not. It allows, refuses and budgets calls by tool name, so a permitted tool is permitted whatever its arguments. Approval holds put a person in front of risky calls: the approval is bound to the exact arguments, although they stay on the machine and the approver sees only the tool name.