Zero-Trust Authorization and Discovery for Enterprise MCP
TL;DR Highlight
Combining tool-list filtering with call-time checks cut forbidden-tool attempts from 21.1% to 0% in MCP.
Who Should Read
Backend developers running an internal MCP server for both human SSO users and service-account agents who worry about prompt injection.
Core Mechanics
- Don't rely on model refusals for security
- Under injected prompts, forbidden-tool attempt rates ranged from 0% to 50% across models.
- Refusal behavior is controlled by the provider and shifts across versions, so the enterprise should own the authorization boundary.
- Filtering list_tools alone isn't enough
- With filter-only, a scripted client sending tools/call directly still executed the forbidden tool.
- Models repeated the hidden tool's name in up to 94% of settings.
- Call-time enforcement (the decorator) is still needed.
- One permission declaration governs listing and invocation
- A single @require_permissions annotation drives both the list_tools filter and the call-time check.
- The same declaration also feeds the pre-auth metadata, so the three surfaces stay consistent.
- One server can serve humans and agents
- SSO tokens in the Authorization header and service-account tokens in another header are validated independently.
- The request is admitted if any backend succeeds, scopes are unioned, and it errors only if all fail.
Evidence
- Attack attempts dropped from 21.1% to 0%
- Across 4 LLMs and 2160 attempts, the baseline (in-body check only) had 152/720 (21.1%) attempts.
- Filter-only and with-extensions both had 0/720.
- Baseline attempt rates varied by model
- claude-sonnet-4-6 had 0/180, claude-opus-4-7 had 3/180, and gpt-5 had 59/180.
- gemini-2.5-pro had 90/180 (50%).
- Filter-only is bypassable by direct calls
- A non-LLM scripted direct call to restart_pod executed under filter-only.
- It was blocked under baseline and with-extensions.
- Cache effect in a 7-day production trace
- 96.1% of verification requests were served from local memory.
- p50 was 9.9 ms versus 296 ms on a miss, roughly 30x faster.
How to Apply
- Audit in-body permission checks in your tools
- Today, check in code whether each admin tool verifies permissions at call time.
- Add a check via decorator or at the top of the function wherever it is missing.
- Filter list_tools by caller permissions
- Split tools by role and remove admin tools from the viewer's listing.
- Make the filter and the call-time check read one shared permission declaration.
- Find every tool with no permission declaration
- In this design, components without an annotation are open to all authenticated callers.
- List registered tools, find the ones missing declarations, and attach the required permissions.
- Match cache TTL to your revocation needs
- If revocation must take effect immediately, set TTL to 0 or add a revocation signal.
- Key the cache by a SHA-256 hash of the token, never the raw token.
Try It
# Paste into Claude Code or ChatGPT (simplified version of the paper's method: a code-review prompt)
Below is our MCP server code. Review it from a zero-trust perspective.
{{MCP server code}}
Summarize in a table:
1. Per tool: does list_tools visibility vary with the caller's permissions?
2. Per tool: is there a permission check at call time (would a direct tools/call execute even if the tool is not listed)?
3. Tools with no permission declaration that are open to every authenticated user
4. The auth token cache TTL and how long a revoked token can still pass
For each problematic tool, propose a fix that ties the list filter and the call-time check to one permission declaration.
Next step: after applying the fix, send tools/call directly with a viewer token and confirm you get a 403.Terminology
Related Papers
Same Simulation, Different Evidence: Where Retrieved Tool Output Costs LLM Agents Accuracy
Saving long tool output to files cut GPT-5.1 accuracy by 11.3 points; naming the authoritative file restored it.
Fake News, Real Crashes: Information Injection Attacks on LLM Financial Trading Agents
Fake headlines hijacked LLM trading agents 62% of the time versus 14% for prompt injection.
Harness Engineering 101: The LLM is the brain, the harness is the body
An agent is a loop managing a JSON array; 16 chapters build one from scratch
Harness2 by Google Research – Recursive Agent Harnessing
Recursively refining the harness with fixed model weights raised benchmark scores by up to 11.8 points.
Ousted Savannah police officer gave ICE agent husband access to Flock system
A police officer shared her Flock login with her ICE-agent husband; an audit found 983 searches and she was fired
Tmux and Claude Code: Sessions, Send-Keys, and the Ceiling
Running Claude Code agents in tmux works well, but limits appear around four agents.
Original Abstract (Expand)
LLM agents translate natural-language context, which may include attacker-controlled text, into privileged tool calls, so authorization must remain effective even when an agent is prompt-injected or adversarially steered. The Model Context Protocol (MCP) has become a widely adopted interface for this boundary, yet its official SDKs' authentication and authorization primitives fall short of enterprise zero-trust requirements, most acutely a dual-persona model in which one server must serve human users (corporate SSO) and automated agents (service-account credentials on a different header). We conduct a systematic gap analysis of six surveyed MCP SDKs (Python, TypeScript, Go, Rust, C#, Swift) and identify three structural shortcomings: credential extraction bound to a single Authorization header, complicating dual-persona deployment without custom middleware; the absence of pre-authentication tool discovery; and the lack of fine-grained per-tool authorization in the base SDKs. We close these gaps with composable extensions to FastMCP: cross-header credential normalization for enterprise deployments serving both human and service-account callers, cached token verification across heterogeneous IdPs, an unauthenticated metadata endpoint for credential-free registry discovery, and permission-filtered tool visibility kept consistent with per-tool invocation enforcement by a single declarative annotation, all without modifying the protocol or SDK internals. Across four frontier LLMs over 2160 attempts, an in-body-check-only server still exposes forbidden tools (152/720, 21.1%), whereas permission-aware visibility drives the rate to 0/720; visibility-only filtering remained bypassable by scripted clients, while models referenced the hidden tool by name in up to 94% of settings when inferable from the prompt, confirming that discovery controls cannot replace invocation-time enforcement.