Lock down MCP servers: tool allowlists at the gate
An MCP server can advertise any tool it likes, and an agent will call what it is told to. Constle puts each declared server behind a gate outside the sandbox, where an allowlist of tool names decides which calls go through.
On this page
- What you'll get
- How the gate sits
- 1. Declare the server and its tools
- 2. Point the agent's MCP client at the gate
- 3. Validate
- 4. Run, and see what was refused
- What the gate refuses
- What this does not cover
- FAQ
- Can I allow some tools on a server and require approval for others?
- What does the agent see when a tool is refused?
- Can the agent find the real server URL?
- Does this work with MCP servers that use standard input and output?
Source release coming soon
The source and installers are not public yet, so there is nothing to install today. This guide documents Constle as it behaves now. More in Project status.
What you'll getLink to this section
- Each remote MCP server reachable only through a per-run gate; the real server URL never enters the sandbox.
- A tool allowlist per server: a
tools/callfor anything else is refused at the gate and recorded asmcp_tool_blocked. - Requests the gate cannot read unambiguously refused before they reach the server, and every forwarded tool call bracketed by audit events.
How the gate sitsLink to this section
The agent never learns where an MCP server is. For each server declared under mcp.servers, it receives one variable, CONSTLE_MCP_<ID>_URL, pointing at the gate, and the sandbox network has no direct path to the server. Every call therefore crosses the gate, which is where the tool allowlist, human approval and spend metering are applied. Step by step through one call: Architecture.
Streamable HTTP is the only transport the gate supports: a server's url must be http or https.
1. Declare the server and its toolsLink to this section
mcp:
servers:
- id: issues
url: https://mcp.issues.internal/mcp # host-side only; never enters the sandbox
tools: [list_issues, create_issue] # every other tool is refusedid: lowercase letters, digits,-and_. It names the variable the agent reads:issuesbecomesCONSTLE_MCP_ISSUES_URL(hyphens become underscores, and the name is uppercased).url: the real endpoint. Its host must not appear inallowed_hosts; an Agentfile that lists it there is rejected, because that entry would let the agent skip the gate.tools: exact tool names. Omit it and every tool passes (gated tools still wait for approval). List them and a server that grows a new tool cannot have it called until you add it here.
2. Point the agent's MCP client at the gateLink to this section
Configure the agent's MCP client to use the URL in CONSTLE_MCP_ISSUES_URL as the server's Streamable HTTP endpoint. How you do that depends on the agent or framework: a config file that reads an environment variable, or one line of code. The URL carries a per-run token, so read it from the environment at run time rather than writing it into the image. On the Docker backend the connection to the gate goes through the proxy variables Constle sets, like all other traffic, so the client must honour HTTP_PROXY.
3. ValidateLink to this section
constle validate agent.yamlValidation refuses a duplicate id, two ids that would name the same variable (foo-bar and foo_bar), a tool name with characters outside letters, digits, _, - and ., and a server host that also appears in allowed_hosts. The server URL's path is checked when the gate is built, at constle run, before any sandbox starts.
4. Run, and see what was refusedLink to this section
constle run agent.yaml
grep -E 'mcp_|tool_call' ~/.constle/logs/<agent>-$(date -u +%F).jsonl| Event | Written when |
|---|---|
tool_call_start, tool_call_end |
around every tool call forwarded to the server, with the server id, the tool name and the size of the arguments (never the arguments themselves) |
mcp_tool_blocked |
a tools/call names a tool that is not in tools, or differs from a gated tool's name only in case |
mcp_request_blocked |
a request the gate will not forward (below) |
What the gate refusesLink to this section
The gate inspects a copy of each request and forwards the original bytes, so it refuses anything two parsers could read differently:
| Request | Result |
|---|---|
A method other than POST, GET or DELETE |
405 · mcp_request_blocked |
A GET or DELETE with a body |
400 · mcp_request_blocked |
A POST that is not a JSON-RPC 2.0 message |
refused |
| A body with a repeated member, or two members that differ only in case, at any depth | 400 · mcp_request_blocked |
A method or tool name that differs from the one the gate matches only by case, whitespace or a control character |
refused |
| A path below the declared endpoint, or a query string | refused |
A protocol upgrade (Upgrade header), or a 101 from the server |
400, or 502 for the response |
What this does not coverLink to this section
- Only
tools/callis inspected. The allowlist and the gates apply to tool calls. Other MCP messages, such as listing tools or reading resources, are forwarded to the declared server as they are, so the agent can see tools it is not allowed to call, and can read what the server's resources expose. - Contents are not judged. The gate matches tool names; it does not inspect arguments for meaning, and it passes the server's responses, tool descriptions included, back to the agent unfiltered. A poisoned tool result still reaches the model. What it cannot do is widen the policy.
- Local MCP servers. A server the agent starts inside its own image, over standard input and output, is just another process in the sandbox: no gate, no allowlist, no metering. Its network traffic still goes through the egress proxy and
allowed_hosts. - Servers you did not declare. If an MCP server's host is in
allowed_hostsinstead of undermcp.servers, the agent can reach it directly as ordinary HTTPS, past the gate. Declare every remote server you want governed. - The server itself. The gate decides which calls reach a server; what the server then does is up to the server.
More in Known limitations and the Field reference §10.
FAQLink to this section
Can I allow some tools on a server and require approval for others?Link to this section
Yes. List every tool the agent may call under tools, and name the consequential ones under human_gates.require_approval_for. A listed tool that is not gated is forwarded straight away; a gated one waits for a person. See Require human approval for MCP tool calls.
What does the agent see when a tool is refused?Link to this section
The gate answers the call itself, as a JSON-RPC error, and the server never receives it. The refusal is written to the audit log as mcp_tool_blocked with the server id and the tool name.
Can the agent find the real server URL?Link to this section
No. It is never passed into the sandbox: the agent only has the gate's per-run address. Its host is also rejected in allowed_hosts, so there is no direct path to it either.
Does this work with MCP servers that use standard input and output?Link to this section
The gate supports Streamable HTTP only. A server that runs as a subprocess inside the image works as it would anywhere else, without the gate's controls.