MCP tool allowlist vs per-call authorization
An MCP tool allowlist is a fixed list of the tools a Model Context Protocol (MCP) server or gateway exposes, set before any session starts. Per-call authorization is a decision made on every tool call about whether this identity may make this call now, with these arguments, under the policy in force. The allowlist bounds what an agent can see, and per-call authorization decides what it can do.
Most MCP deployments start with an allowlist, and they’re right to. Trimming a
GitHub server down to the read tools takes minutes, shrinks the context the
model has to reason over, and removes the tools nobody should be calling. The
trouble starts when the allowlist is asked to answer questions it was never
built for: whether a contractor and a staff engineer should get different
tools, whether run_query is safe when the query is a DELETE, or whether a
person offboarded an hour ago still has a live session. An allowlist answers
one question once, for everyone who shares it, and per-call authorization
answers every other access question at the moment the call is made. The
practical design keeps both and makes the call-time check authoritative.
What does each control decide?
Per-call authorization decides a tool call from five inputs, and an allowlist decides from one of them.
- Identity. The person or agent identity behind the session, with its current directory groups and roles.
- Tool. Which tool, on which server, and what that tool is known to do, from annotations someone trusted has recorded.
- Arguments. What the call asks for: the repository, the recipient, the amount, the SQL statement.
- Time and state. Whether the grant still holds right now: the person is still active in the directory, the session hasn’t been revoked, and no block has been added since the session began.
- Policy. Which rule allowed or denied the call, recorded so the decision can be explained to an auditor or an incident responder.
A tool allowlist decides the second input, ahead of time, for every identity that uses the list. That’s a real decision with real value. It is also the only one of the five that doesn’t change between the moment the list is written and the moment a tool is called.
What is an MCP tool allowlist?
An MCP tool allowlist is the set of tools a server or gateway returns when a
client sends tools/list, and accepts when a client sends tools/call.
Everything else on the upstream server is hidden and refused. Gateways
implement it as configuration attached to a server, a portal, or a profile:
- Docker’s MCP Gateway manages
a tool allowlist per profile,
in dot notation, with commands like
docker mcp profile tools production --enable github.list_repos. Per Docker’s profile documentation, every tool is enabled unless explicitly disabled, so a restrictive profile starts with--disable-alland adds tools back. - Cloudflare’s
MCP server portals
expose all of a server’s tools by default and let an administrator turn
individual tools off per portal, or invert the default with
default_disabled: trueso only listed tools appear. Cloudflare’s docs state that turned-off tools are removed from the portal’s list and can’t be called. - Tag-based filtering narrows the list a client asks for. A client connects
with a parameter such as
?tags=billingand receives only the tools taggedbilling. This is curation chosen by the client, which makes it useful for context and useless as a security boundary, since a client that omits the parameter gets the full list.
For plenty of deployments, an allowlist is enough. It is deterministic, cheap to evaluate, and easy to review, because the whole policy fits on one screen. It also keeps tool definitions out of the model’s context, and Cloudflare’s portal docs give that as the reason to curate: the less external context exposed to the model, the better its responses tend to be. For one team on one server, where every tool is read-only and everyone should see the same set, the allowlist is the whole policy, and a per-call policy engine adds a hop and a rule set that change no outcomes.
What is per-call authorization for MCP?
Per-call authorization is a policy decision on each tools/call request,
made after the client names the tool and supplies its arguments and before
the request reaches the upstream server. A denied call returns an error to the
client, and the server never sees it.
The MCP specification leaves room for this without prescribing it. Its
authorization section
defines how a server answers a request whose token lacks a scope: 403 Forbidden with error="insufficient_scope" and the scopes the operation
needs, so the client can step up. It also says the required scopes “may be
determined dynamically based on the specific request arguments and context.”
The specification expects authorization to depend on what a call asks for,
and it leaves the mapping from tools and arguments to scopes to whoever runs
the server. How MCP authorization works
covers the OAuth flow itself.
Gateways implement per-call authorization at different depths:
- Kong’s
AI MCP Proxy plugin
evaluates allow and deny lists of Consumers and Consumer Groups, with a
default ACL for every tool and an optional ACL per tool. Deny entries are
checked first, a denied call returns
403 Forbiddenbefore it reaches the upstream server, and every attempt is written to an audit log. - IBM’s
ContextForge runs a
two-layer model: token scoping controls what a caller can see, then an RBAC
check controls what it can do, with permissions such as
tools.readandtools.executeheld by team-scoped roles. Its plugin framework addstool_pre_invokehooks, including OPA and Cedar policy plugins, which receive the tool name and arguments and can stop a call. - Cloudflare checks identity when a user signs in to a portal, with Access policy selectors such as Emails, Groups, Country, and Device Posture deciding who reaches the portal and which servers they see. Routing portal traffic through Cloudflare Gateway adds data loss prevention scanning on each tool call request and response, which blocks matching calls.
ContextForge’s split between seeing and doing is the clearest statement of the framework on this page: visibility and permission are separate questions. A gateway that answers only the first has authenticated the caller and then applied the same list it would apply to anyone.
Where do MCP tool allowlists fail?
Allowlists fail in five predictable places. Each one comes from the list being written once, before the identity, the arguments, or the current state are known.
The list is the same for everyone who shares it. A list attached to a server or portal gives every user of that server or portal the same tools. To give support engineers the read tools and platform engineers the write tools, you need two lists and a way to route each person to the right one. Cloudflare curates tools per portal, so different tool sets for different groups means separate portals with separate Access policies. Each new role adds another list to keep in sync, and the lists drift apart in ways nobody reviews.
A tool name doesn’t describe a call. Allowing run_query allows a
SELECT and a DROP TABLE. Allowing send_email allows a message to a
colleague and a message to an external address with a customer export
attached. Tools that wrap a general capability, such as a shell, a query
engine, or an HTTP client, are the ones agents find most useful and the ones
whose risk lives entirely in the arguments. An allowlist can keep the tool or
remove it, and it can’t tell one use from another.
Hiding a tool and blocking it are different controls. Removing a tool from
the tools/list response keeps a well-behaved agent from planning around it.
It does nothing about a client that sends tools/call with the tool’s name
anyway, and nothing about a client that
reaches the upstream server without going through the gateway. Cloudflare’s documentation is direct about the
second case: users blocked by a portal’s Access policies
can still connect to the server through its direct URL
unless Access is configured as the server’s OAuth provider. The same is true of
any gateway, because a control only holds on the path the call takes.
Tools change after the list is written. MCP servers can add tools at any
time and announce it with notifications/tools/list_changed. An
allow-by-default list, which is the default in both Docker profiles and
Cloudflare portals, exposes the new tool with no review. A deny-by-default list
hides it until someone updates the list, which is safer and slower. A tool can
also keep its name while its description changes, which is the rug pull
described in MCP tool poisoning, and a name
match will not notice.
The list doesn’t know what time it is. An allowlist has no notion of a person being deprovisioned, a session being revoked, or a block being added after the session started. If access is decided when the session connects and never again, a session opened before an offboarding keeps working until it expires.
Where does per-call authorization fall short?
Per-call authorization closes the five gaps above and introduces five costs of its own: the policy, the annotations it trusts, argument rules, latency, and coverage.
Someone has to write the policy. Directory groups don’t arrive mapped to
create_refund. Somebody decides which roles may call which tools on which
servers, and the work grows with every tool. Tool classes help: a rule that
denies every tool annotated as destructive covers tools that don’t exist yet,
as long as the annotations can be trusted.
Rules built on annotations depend on trusted annotations. The MCP
specification says clients
must consider tool annotations untrusted
unless they come from trusted servers. A third-party server that marks a
destructive tool readOnlyHint: true walks it past a read-only rule. For
servers you don’t control, the annotations the gateway evaluates should be
recorded or reviewed by an administrator, not read from the server on every
sync.
Argument rules are expensive to get right. Deterministic argument policy,
such as a Rego or Cedar rule that permits refunds under a set amount, needs
knowledge of each tool’s schema and breaks when the schema changes. Content
scanning for secrets and personal data covers more tools with less work and
produces false positives. Model-based judges handle intent, such as whether a
curl is an exfiltration attempt, and they are probabilistic, so a judge’s
deny is a classification with a confidence attached. Writing argument rules for
a handful of high-risk tools, and relying on identity and tool rules for the
rest, is a reasonable place to stop.
Every call pays for the check. A role-and-tool lookup is cheap. Argument inspection costs more, and a model-based judge adds a model call. A gateway also has to choose what happens when a check can’t finish in time: failing closed stops work during an outage, and failing open lets unchecked calls through. Both are defensible, and the gateway’s behavior should be known and documented before the first outage tests it.
The check only covers calls on its path. A local stdio MCP server configured on a laptop never passes through a gateway, so no gateway policy applies to it. Closing that gap is a discovery problem, covered in shadow MCP and shadow AI.
A denied call is also a worse experience for the agent than a missing tool.
An agent that sees a tool plans around it, calls it, receives a 403, and
has to recover mid-task. That cost is the strongest argument for running both
controls instead of choosing one.
How do tool allowlists and per-call authorization work together?
The two controls answer different questions, and the questions arrive in a fixed order. Each one has a natural place to be answered.
| Question | Answered by | Why there |
|---|---|---|
| Should this tool exist for anyone in the organization? | Allowlist on the server or gateway | Unused tools cost context and add surface for no benefit |
| Should this identity ever see this tool? | Per-identity filtering of tools/list | The agent doesn’t plan around a tool it will be denied |
| May this identity make this call, with these arguments, now? | Per-call authorization on tools/call | The only point where identity, arguments, and current state are known together |
| What happened, and under which rule? | The decision log | Audit, incident response, and tuning the policy |
Three rules keep the layers consistent:
- Derive the filtered list and the call check from one policy. If the list shows a user the tools their roles permit, and the call check evaluates the same roles, the list is a preview of the decision. When the two read from different sources, a tool can appear and then be denied, or be hidden and still callable.
- Make the call check authoritative. Treat list filtering as a usability layer. Kong’s documentation describes discovery filtered against the default ACL and invocation checked against a tool’s own ACL where one exists, so the invocation check is the one that holds. Kong also notes that a per-tool ACL replaces the default ACL rather than merging with it, so it has to list every allowed subject again.
- Put argument and state checks only at call time. Nothing about a call’s
arguments is known at
tools/list, and the grant’s state can change between listing and calling. These checks belong ontools/call, scoped to the tools that need them.
Under this model, the server-level allowlist becomes the coarse outer boundary an administrator sets once, and the per-identity view of it is computed from roles rather than maintained by hand. Adding a role means adding grants to the policy that already drives both checks.
How do MCP gateways compare on allowlists and per-call authorization?
Each of the four gateways below covers the allowlist layer and at least part of the per-call layer. They differ in where identity enters and whether arguments are part of the decision. Each row reflects the vendor’s current documentation.
| Gateway | Tool exposure | Identity in the decision | Call-time check | Argument inspection |
|---|---|---|---|---|
| Docker MCP Gateway | Tool allowlist per profile, all tools on by default | Profile selected per gateway run; Docker AI Governance is invite-only | Configurable interceptors (--interceptor) | --block-secrets, on by default, blocks secrets sent to or from tools |
| Cloudflare MCP server portals | Tool toggles per portal, optional deny-by-default | Access policies at portal sign-in and per server | Turned-off tools refused; DLP block through Gateway routing | DLP profiles on requests and responses |
| Kong AI MCP Proxy | Discovery filtered by default ACL | Consumers and Consumer Groups from Kong auth plugins | Per-tool ACL, deny first, 403 on denial | Guardrails on MCP traffic listed as not supported |
| IBM ContextForge | Token scoping by team and visibility | Team-scoped RBAC roles | RBAC permission check, tool_pre_invoke plugins | OPA and Cedar plugins receive arguments |
Each gateway is the obvious choice for a different reader. Docker gives a developer a curated, containerized tool set on their own machine with almost no configuration. Kong brings MCP under the same Consumer model a team already uses for its APIs. ContextForge has the most expressive per-call policy of the four, through policy-as-code plugins, and its OPA plugin needs an OPA server running beside the gateway. Cloudflare puts identity, device posture, and DLP from an existing Zero Trust deployment in front of MCP, and documents that independent MFA, purpose justification, and temporary authentication are not enforced for servers authorized through a portal.
Does per-call authorization require a self-hosted gateway?
No. Per-call authorization has to run on the path every tool call takes, and that can be a gateway you operate, a gateway a vendor operates, or the MCP server itself. Docker’s MCP Gateway and IBM’s ContextForge are open source and run where you deploy them. That gives you control of network placement, and it hands you the work of patching, scaling, and running a policy engine next to the gateway. A managed gateway moves that work to the vendor and puts tool call traffic through a service the vendor operates.
The requirement behind a self-hosting request is often narrower than self-hosting. If the concern is that a gateway carrying tool arguments and results shouldn’t have a public listener, a managed gateway reachable only over a private network route can meet it. If the concern is that no vendor may operate the component at all, only self-hosting meets it. Separating the two is worth doing before a procurement decision, and what is a private MCP gateway walks through the difference between private, self-hosted, and tunneled deployments, and what each costs.
Need to self-host?
Self-hosting is available for Enterprise customers. Book time with our team to learn more.
A note on Speakeasy
The Speakeasy AI Control Plane runs both layers
from one set of role grants, on a gateway Speakeasy manages by default. In the
role editor,
the mcp:connect scope narrows to All servers, Specific projects,
Specific servers, or Specific tools, and tools can be chosen by name or
by annotation, so a role can be limited to read-only tools on a billing server
without listing them. Remote and tunneled servers can be permissioned by tool
name once their tool metadata is recorded, and the annotations the gateway
evaluates on those servers are the ones an administrator recorded, not the
ones the server reports. Every tool call through the
MCP gateway passes a second check against the
caller’s grants after the session connects, as the
enterprise identity guide
describes. With Directory Sync, role membership follows
identity provider groups, and deprovisioning a user in the identity provider removes their
access.
Each scope has a blocking counterpart, such as mcp:blocked_connect, which
withdraws the scope for the matching servers or tools even when another role
grants it, and a deny always beats an allow. The same rules appear on each
server’s Team Access tab,
and where authorization challenge logging is enabled for the organization,
denied calls are recorded as challenges that an admin can turn into a precise
grant. Shadow AI,
generally available under Guardrails for every organization, applies the
allowlist idea to clients: each coding harness or assistant is Approved or
Blocked at the gateway, and a Blocked one is refused when it authenticates, on
every MCP server in the organization.
Three limits are worth knowing before you rely on it. The role check evaluates identity, tool name, and annotation, not arguments. Argument inspection runs through MCP-scoped guardrail policies, which are enabled per organization on request. Each check reads the first 50 KiB of a request or result, and a call goes through when a scan can’t finish in time or the content is larger than that. Tag-based tool filtering is client-chosen curation for context, not an access control. And because the gateway is managed by default, teams that need it off the public internet should know that private access over a Tailscale tailnet requires an Enterprise organization, with each server set to public-only, dual, or private-only. For a team whose security review turns on network exposure, that requirement decides whether the gateway fits, and the private MCP gateway resource covers the trade-offs in full.
The tool-level grants shipped in the April 2026 tool-level RBAC release. If you’re deciding where your allowlist should end and your per-call policy should begin, we’d be interested to hear how you’re drawing that line. Talk to us.