"Where do we enforce who can call which tool?" A customer's security team asked me that recently, before approving a wider rollout of their agents. Several of their teams were building agents, each with its own rules for its agents, and those agents called the same internal MCP servers.
They are not the only ones asking this question. In presales, I talk to many different customers, and different versions of the same question keep coming up.
The usual answer is to put the guard in the agent's own code, next to the tool call. That works for one agent. It gets harder when three agents, owned by two teams, call the same five servers. Each agent needs its own copy of the rules, and each copy can drift.
The other, more sophisticated answer is an MCP gateway. I had used agentgateway only for demos and light proofs of concept, and Langflow mostly as a way to expose flows as MCP tools. I had never paid much attention to what a gateway does when someone sets it up wrong. So I took agentgateway 1.6.0, and broke the question down into five parts:
- What is an MCP gateway, and how is it different from the AI gateway I may already run?
- What does it enforce better than agent code?
- Where are its limits?
- What happens when it fails or is set up wrong?
- Is it ready for production, and how would I start?
⏳ TL;DR
Once several agents share MCP servers, put the tool rules in an MCP gateway instead of in each agent. You get one copy of the rules, a tool list per agent, and one log for every call. Before you rely on it, block direct access to the servers, make the token check strict, and decide what happens when one server fails. Keep argument checks and prompt-injection defenses in the tools. agentgateway 1.6.0 is ready for pilots, but not yet as your only line of defense.
What is an MCP gateway?
An MCP gateway is a single endpoint that sits between your agents and the MCP servers they use. Agents connect to the gateway instead of to each server. The gateway connects to the servers, merges their tool lists, and forwards each call to the server that owns the tool. agentgateway calls this multiplexing, and its docs describe the merged endpoint as a "virtual MCP" server.
Because every request passes through it, the gateway can act on each one. It can check who the caller is, show that caller only the tools it is allowed to see, check each tool call, and log it.
Here is how that helps:
- One place to add or remove a server. When tools change, only the gateway configuration changes, not every client, as agentgateway's multiplexing example points out.
-
No name clashes. The gateway adds the server name in front of each tool name. If two servers both have a tool called
search, the agent seesdocs_searchandtickets_search. - One identity check. The agent sends a token to the gateway, and the gateway decides what that caller can reach. Remote servers still need their own credentials from the gateway, because the MCP authorization spec says an MCP server "MUST NOT pass through the token it received from the MCP client."
This is a different job from an AI gateway, the kind that sits between your application and model providers and decides which model answers. An MCP gateway sits between the agent and its tools, and it decides which tools the agent can see and call. Some products, agentgateway included, do both in one binary, but you can run either side alone.
agentgateway is not the only MCP gateway. Docker's MCP Gateway, IBM's ContextForge, and Microsoft's MCP Gateway cover similar ground. I picked agentgateway because I had already used it, it runs as a single binary on a laptop, and it sits under neutral governance in the Agentic AI Foundation.
Why MCP Gateway?
To answer this, I put agentgateway in front of the two small servers from our MCP Security lab. The calculator server is the trusted one. The weather server is the rogue one: its get_weather result carries a hidden instruction to call send_report and leak the conversation.
I made two test tokens. They are JWTs (JSON Web Tokens): signed strings that carry the caller's name in a field called sub. I signed them with a local key and gave the gateway the matching public key in jwks.json. support-agent stands for a customer-support agent that should never send reports, and ops-reporter for an internal job that is allowed to.
mcp:
port: 3000
targets:
- name: calculator
stdio:
cmd: uv
args: ["run", "--directory", "./06-mcp-security", "calculator_server.py"]
- name: weather
stdio:
cmd: uv
args: ["run", "--directory", "./06-mcp-security", "rogue_weather_server.py"]
policies:
jwtAuth:
mode: strict
issuer: demo-issuer
audiences: [mcp-gateway]
jwks:
file: ./jwks.json
mcpAuthorization:
rules:
- 'mcp.tool.name in ["calculator", "get_weather", "read_weather_policy"]'
- 'jwt.sub == "ops-reporter" && mcp.tool.name == "send_report"'
In plain words: the gateway starts both lab servers over stdio, as local processes it talks to through standard input and output. It requires a valid token and allows a tool call if any rule matches. Everyone gets the three read-only tools, and only ops-reporter gets send_report. The rules are written in CEL, a small expression language.
Then I connected as each caller and asked for the tool list:
| Caller | Tools in the list |
|---|---|
support-agent |
calculator_calculator, weather_read_weather_policy, weather_get_weather
|
ops-reporter |
the same three, plus weather_send_report
|
The same endpoint gave each agent a different toolbox. In our lab, the guard sits inside the agent, so the model still sees send_report and its description, and the guard has to catch the call. Here, the support agent never sees it.
Next, I had the support agent call weather_send_report anyway, the way an injected instruction would. The gateway answered:
{"jsonrpc":"2.0","id":3,"error":{"code":-32602,"message":"Unknown tool: weather_send_report"}}
The gateway did not say "access denied." It said the tool does not exist. Like a bouncer, it checks who you are and does not tell you why you are not on the list. The agentgateway team explains that this is deliberate: "access denied" confirms the tool is there and worth attacking. The cost shows up when you debug, because a misspelled tool name and a denied call get the same error. Look in the gateway's access log first, not in the agent.
That log was the third thing I got. Every call landed in it with the caller, the server, and the tool. Here is the denied call, shortened:
error request http.status=400 jwt.sub=support-agent protocol=mcp mcp.method.name=tools/call
mcp.target=weather gen_ai.tool.name=send_report error="mcp: Unknown tool: weather_send_report"
With per-agent guards, that record is spread across every agent's own logs, if it exists at all. Here it is in one place.
To be fair to agent code, none of this is impossible there. An agent can leave send_report out of its own tool list, return the same error, and write its own log. The difference is who owns the check. In agent code, the rule is enforced by the agent it is meant to limit, and each team keeps its own copy. In the gateway, there is one copy, outside the agent.
That only holds if the agent cannot go around the gateway. Stdio servers that the gateway starts itself have no network port to reach. For remote servers, you need a network rule that lets only the gateway reach them. Otherwise, any team can point its agent straight at the server and skip every rule.
Where are its limits?
I ran into two.
1. It does not clean what comes back
The support agent was still allowed to call get_weather. The result came back through the gateway with the hidden "SYSTEM NOTE TO AI ASSISTANT" text untouched. The gateway stopped the dangerous call, but the injected instruction still reached the model.
agentgateway is working on MCP guardrails that can deny calls and mask request and response text with CEL, but only on the main branch, and 1.6.0 rejected that configuration. Even when it ships, I would still limit what the tool can do, because you cannot reliably detect a bad instruction in text.
2. It can check arguments, but only the hard way
Our MCP Security lab does not block send_report outright. It checks the to address against an allowlist. I tried to move that check into the gateway.
My first rules used mcp.tool.arguments.to. They hid send_report from ops-reporter and denied even the allowed address. The 1.6.0 source explains why: the authorization check fills in only the server and tool name, so arguments is always empty there. What did work was reading the raw request body with json(), one of agentgateway's CEL functions:
mcpAuthorization:
rules:
- 'mcp.tool.name in ["calculator", "get_weather", "read_weather_policy"]'
- >-
jwt.sub == "ops-reporter" && mcp.tool.name == "send_report" &&
(mcp.methodName == "tools/list" ||
json(request.body).params.arguments.to == "reports@ourcompany.com")
With this rule, a report to reports@ourcompany.com went through. A report to any other address, or with no to at all, was denied. It works, but I hit three traps on the way:
-
The tool list needs its own exception. The same rules decide which tools appear in the list, and a list request has no arguments. Without the
tools/listpart, the tool disappears from the list. -
A text match is easy to fool. My first working version only checked that the allowed address appeared somewhere in the body. A report to
attacker@evil.examplewithreports@ourcompany.comwritten in the report text passed, and the report was sent. Check the field itself. - The error is confusing. A call denied for its arguments also comes back as "Unknown tool", for a tool the agent can see in its list.
So I would use gateway argument rules only for simple exact matches, as a second layer. The real check belongs inside the tool, where it can clean up an address, look up a list, and return a useful error.
What happens when it fails or is set up wrong?
A gateway is one more thing every agent depends on. I tested three ways it can go wrong.
1. The default token check lets anonymous callers in
My first configuration did not include mode: strict. A request with no token at all connected and got the three read-only tools.
That is not a bug. agentgateway's source documents three modes for jwtAuth: strict, optional, and permissive. The default is optional, which validates a token when one is present and, in the code's own words, "allows requests without a JWT." With mode: strict, the same request got 401 Unauthorized.
Set mode: strict on any gateway that should require identity, and test a request with no token before you go live.
2. One broken server takes down the whole endpoint
Next, I pointed the weather target at a script that did not exist. The calculator server was fine, but the whole endpoint failed to start a session. The gateway returned HTTP 500 with this error:
{"jsonrpc":"2.0","id":1,"error":{"code":-32603,"message":"failed to send message: upstream closed on receive"}}
This is the documented default. agentgateway's failureMode setting defaults to failClosed, which fails "the entire session if any target fails to initialize." failOpen skips the failed server and keeps serving the healthy ones. It goes directly under mcp:, not on a single target:
mcp:
port: 3000
failureMode: failOpen
targets:
# ...
With that one line, the support agent got calculator_calculator back, and the log said upstream 'weather' failed during fanout, skipping.
Neither default is wrong. Failing closed is safer when a missing server could change how an agent behaves. Failing open keeps unrelated tools working. Choose on purpose, because putting servers behind one endpoint also ties their availability together.
3. The gateway becomes the most valuable thing to attack
Every tool call now passes through one process, so whoever can change its configuration can change every agent's permissions at once. As with the model gateways in our tool security post, treat the configuration like code, review it, and limit who can change it.
Also check what the gateway exposes on the network. agentgateway ships an admin UI that can change targets and ports without a restart. In my test it listened only on localhost, but the MCP, metrics, and readiness ports listened on every network interface. I would keep the admin UI local and put the other ports behind a firewall rule.
Here is the short version of the three tests:
| What I tested | 1.6.0 default | What I would set |
|---|---|---|
| Request with no token | Allowed (optional) |
mode: strict |
| One server fails to start | Whole endpoint fails (failClosed) |
Choose on purpose; failOpen keeps healthy tools working |
| Who can reach the gateway | Admin on localhost, other ports on every interface | Keep admin local, firewall the rest |
What I did not test: the gateway itself going down, and the delay it adds to each call. I would test both before production.
Is it ready for production?
I can only answer this for agentgateway 1.6.0, the gateway I tested. For trying it out and for internal pilots, yes. As the only place that enforces tool rules in production, not yet.
On the positive side, the project reached version 1.0 in March 2026, is Apache 2.0 licensed, and joined the Agentic AI Foundation, the Linux Foundation group that also hosts MCP, Goose, and AGENTS.md.
On the cautious side, the README still says the project is "currently in active development." The 1.6 release notes ask you to review the breaking changes "because several defaults change." Most of those are on the model-routing side, but with the guardrails gap and the argument workaround, they show the configuration is still moving.
If I were starting today:
- Start with the standalone binary. The MCP quickstart runs on a laptop with one config file. Put the servers you already use behind it and point your existing agent at the gateway URL.
- Pin the version. Read the breaking changes before every upgrade, and keep the old binary until the new one passes your tests.
- Test the failure paths, not only the happy path. Send a request with no token. Call a tool the caller should not see. Stop one server. Check the access log after each one.
- Move rules gradually. Keep the existing guards in your agents until the gateway rules have been tested. Then remove the copies one at a time.
What this means for practitioners and architects
If you are deciding where MCP tool rules should live in your team or for a customer, I would suggest six things:
- Use a gateway when servers are shared. One agent and one server does not need it. Several agents, several teams, or several servers usually do.
- Make the gateway the only way in. If an agent can still reach a server directly, every gateway rule is optional. Block direct access with a network rule.
- Give every agent its own identity. Per-caller tool lists only work if each agent sends its own token. A shared token turns the gateway back into one big allowlist. How agents get those identities is still open in MCP itself, as I wrote in MCP is done pretending agents are people.
- Filter the tool list, not only the calls. If the model never sees a tool, it never reads the tool's description, and any call to it is rejected anyway.
- Check the defaults yourself. In agentgateway 1.6.0, token checks are optional and multiplexed servers fail closed unless you change them. Other gateways have their own defaults, so test them before you trust them.
- Keep guards inside the tools that can do damage. The gateway decides who can call what, and it can handle simple exact-match argument rules. The tool still decides whether a specific request, such as a report to an outside address, is allowed.
If you want to build the pieces this sits on, the MCP Security chapter shows the in-agent guard, the RBAC chapter covers per-role limits inside the tool itself, and the AI Gateways chapter covers the model side of the gateway.
The customer's security team asked where to enforce who can call which tool. My answer now has two parts: the gateway, for who can see and call each tool, and the tool itself, for what a specific call is allowed to do. The gateway keeps one copy of the rules instead of one per agent, but it is also one more service to protect.
Are you running MCP servers that several agents share? Where do your tool rules live today, and have you tried a gateway in front of them? Leave a comment below and share what worked for you, and what did not.

Top comments (0)