DEV Community

Cover image for User-Level Permission Controls for MCP Tool Access with an Enterprise MCP Gateway
Aleksei Aleinikov
Aleksei Aleinikov

Posted on • Originally published at alekseialeinikov.com

User-Level Permission Controls for MCP Tool Access with an Enterprise MCP Gateway

The Model Context Protocol solved a real problem: it gave AI models a clean, standard way to reach the outside world. Instead of gluing each model to each API by hand, you point a client at an MCP server and the model can suddenly list files, query a database, open a pull request, or issue a refund.

That is also the problem.

An MCP tool is not read-only context. It is a capability — a live button the model can press on someone's behalf. The moment you connect a payments server, "create an invoice" and "issue a refund" are one tool call away. And in most organizations the honest answer to "which user or agent can reach which of those tools?" is: nobody knows, and nothing stops them.

This article is about turning that question into an answer you can enforce. The short version: you do it at a gateway, and — for the tools that never pass through one — at the endpoint.

Why This Is a Permissions Problem, Not a Prompt Problem

The tempting fix is to handle tool access in the prompt: tell the agent "only use the read-only tools." That is not a control. It is a suggestion to a probabilistic system, and it collapses under a jailbreak, a confused-deputy attack, or a simple mistake.

Tool access is an authorization problem, and it has the same shape as every other authorization problem you already solve:

  • A subject — a user, a team, or an autonomous agent.
  • A resource — a specific MCP tool (create-invoice, not just "the billing server").
  • A decision — allow or deny, made by something the subject cannot talk its way past.

The mistake is to push that decision into each AI app. Every app has its own config, users can edit it, and there is a new app every month — Claude Desktop, Cursor, Codex, an internal agent someone wrote on Friday. Trusting each of them to police itself is the "every service does its own auth" anti-pattern, reborn for AI.

The fix is the same one platform teams have reached for every time before: move the decision to a choke point that every request has to pass through.

The Gateway: One Choke Point for Every Tool Call

An MCP gateway sits between your AI clients and your MCP servers. Every client — desktop app, IDE, custom agent — connects to the gateway instead of to the tools directly. The gateway is simultaneously an MCP client (to the real tool servers) and an MCP server (to your apps), so it can see and rule on every tool call in flight.

That single position gives you the thing per-app config can never provide: one place where the answer to "can this caller use this tool?" is decided, the same way, every time.

I'll use Bifrost as the concrete example — an open-source AI gateway that fronts 20+ model providers and includes an MCP gateway with per-request tool filtering — but the pattern applies to any gateway built on the same principles.

An MCP gateway as a single choke point: AI clients authenticate with a virtual key, the gateway applies a per-key allow-list of MCP tools, and enforces it at both inference and tool-execution time.

Virtual keys: attaching permissions to a caller

The unit of control is a virtual key. Instead of handing out raw provider keys, you issue each user, team, or agent a virtual key (sk-bf-…) that they send on every request:

curl -X POST http://localhost:8080/v1/chat/completions \
  -H "x-bf-vk: sk-bf-your-virtual-key" \
  -H "Content-Type: application/json" \
  -d '{ "model": "gpt-4o-mini", "messages": [{"role":"user","content":"Hello"}] }'
Enter fullscreen mode Exit fullscreen mode

The virtual key is where governance hangs: which models and providers it may use, its budget, its rate limits — and, the part we care about here, which MCP tools it is allowed to touch.

Deny-by-default tool filtering

This is the design decision that makes the whole thing a real control: a virtual key with no MCP configuration gets no tools. Access is not something you take away; it is something you explicitly grant.

For each MCP client attached to a key, you choose exactly what it may reach:

  • Specific tools — only the tools you name are available.
  • * wildcard — all current and future tools from that client.
  • Empty list — everything from that client is blocked.
  • Not configured at all — that client is invisible to the key.

A worked example. Say two MCP clients are connected to the gateway:

  • billing-client with tools [create-invoice, check-status, issue-refund]
  • support-client with tools [create-ticket, get-faq]

A read-only support agent's virtual key can be scoped like this:

{
  "mcp_clients": {
    "billing-client": ["check-status"],
    "support-client": ["*"]
  }
}
Enter fullscreen mode Exit fullscreen mode

That key can check an invoice's status and use every support tool — but create-invoice and issue-refund simply do not exist for it. A different key, for the finance team, grants billing-client: ["*"]. Same gateway, same tools, different reach per caller.

Enforced twice, so the model can't cheat

Here is the subtle part that separates a control from a filter. It would be easy to only hide the disallowed tools when presenting the toolset to the model. But a model that hallucinates a tool name — or an injected instruction that names one directly — would slip through.

So the allow-list is applied in two places:

  1. At inference time — the model is only ever offered the tools the key allows. It cannot choose what it cannot see.
  2. At tool-execution time — even if a call for a disallowed tool arrives anyway, the gateway rejects it before it reaches the server.

The gateway does this by translating the key's configuration into a strict x-bf-mcp-include-tools allow-list on the request, and re-checking it at execution. A tool that is off the list is not merely hidden — it is unreachable.

Binding Permissions to Real People, Not Anonymous Keys

A virtual key is only as good as your knowledge of who holds it. Scope a key perfectly and then email it around, and you are back to shared secrets.

That is why per-user MCP control has to connect to your identity provider. In Bifrost's enterprise tier this is OIDC + SCIM: users sign in with their corporate account, and their Bifrost role, team, and business-unit membership are provisioned automatically from the IdP — Okta, Entra ID, Keycloak, Google Workspace, and others.

The mapping is attribute-driven, so a claim from the IdP becomes a Bifrost role:

{
  "attribute": "department",
  "value": "Finance",
  "role": "developer",
  "team": "Finance"
}
Enter fullscreen mode Exit fullscreen mode

Now the chain is complete and self-maintaining:

corporate identity → team → virtual key → MCP tool allow-list.

When someone joins Finance, they inherit the finance team's tool access. When they leave, SCIM deprovisions them and the access evaporates — no orphaned key still able to issue refunds. Permissions follow the person, which is the only version of "user-level control" that survives contact with a real org.

This is the same principle that killed long-lived cloud credentials. If you still hand out static service-account keys, the identity-bound approach applies there too — see Kill Your Service Account Keys: Workload Identity Federation in 2026.

The Blind Spot: Tools That Never Touch the Gateway

A gateway governs everything that flows through it. The uncomfortable truth is that a growing share of MCP usage doesn't.

A developer adds a filesystem MCP server to Claude Desktop. Another wires a database server straight into Cursor. A third installs something they found on GitHub into Codex. None of it points at your gateway. Each is a local process on a laptop with real capabilities — reading files, hitting internal APIs, taking actions — and it is completely invisible to your central policy. This is shadow MCP, and it is exactly where the gateway model runs out of road.

You cannot solve this from the server side, because these tools were never configured to go through a server. You have to solve it on the machine.

A single control plane, enforced everywhere: Bifrost defines identity, virtual keys and MCP policy centrally; an Edge agent on each machine syncs that policy and enforces allow or deny on local MCP servers directly on the device.

Extending policy to the endpoint

This is what an endpoint agent like Bifrost Edge is for. It runs on every machine — macOS, Windows, Linux — and does two things that a gateway alone cannot:

  • Discovery. It reads the MCP configuration inside each AI app on the device and reports it back, building a fleet-wide inventory. For the first time you can answer "which MCP servers are actually running across our org?" with data instead of a guess.
  • On-device enforcement. Allowing or denying a server is not advisory. When you deny one, Edge enforces that decision on the machine, so the tool cannot be used even by an app that had it configured before the policy existed.

The governance model does not change — it extends. The same identities, the same virtual keys, the same allow/deny decisions defined centrally are now enforced on traffic that never would have reached the gateway: desktop chat apps, browser AI, coding agents in the terminal, and the local MCP servers they connect to. A new server appearing on a laptop can trigger an approval request in the admin console instead of silently gaining access.

Gateway and endpoint are the two halves of one control: the gateway is the policy plane for everything that routes through it; the endpoint agent is the last mile for everything that doesn't.

Putting It Together

Here is the whole progression, from no control to real least-privilege for agents:

No control Gateway only Gateway + endpoint
Who decides tool access Each app, each user Central allow-list per virtual key Central allow-list, enforced everywhere
Default posture Allow-all Deny-by-default Deny-by-default
Tied to identity No Via OIDC/SCIM → key Via OIDC/SCIM → key
Enforcement point None At the gateway (inference + execution) Gateway and on the device
Local IDE MCP servers Invisible Invisible Inventoried + allow/deny
Leaver offboarding Manual, easy to miss Key deprovisioned Key deprovisioned + device enforced

The principles underneath are not new — they are the ones platform and security engineers have applied to every capable system before this one:

  1. Least privilege. Deny-by-default; grant the specific tools a caller needs and nothing more.
  2. One choke point. Decide access in a single place every request passes through, not scattered across apps.
  3. Tie capability to identity. Permissions belong to a person or an agent with an owner, not to a floating secret.
  4. Enforce where the action happens. At the gateway for routed traffic; on the device for everything else.

MCP made AI agents genuinely capable of doing things. That is exactly why "which user or agent can reach which tool" stops being a nice-to-have and becomes the same kind of question you already ask about database roles and cloud IAM. An enterprise MCP gateway — deny-by-default, virtual-key scoped, identity-bound, and extended to the endpoint — is how you get an answer you can actually enforce.

If you want to see the mechanics in detail: Bifrost is the AI gateway used throughout this article, the gateway docs cover virtual keys and MCP tool filtering, and the Bifrost Edge docs cover endpoint discovery and on-device enforcement.


Originally published on my blog, where I keep it up to date: Read the full, always-current version →

I write about platform engineering, Google Cloud and cloud security — in English and German. If this was useful, more here.

Top comments (0)