MCP has become the default way AI agents connect to real systems: internal databases, business APIs, file systems, third-party services. Introduced in late 2024, it's since been adopted across OpenAI, Google DeepMind, Microsoft, and thousands of engineering teams, with SDK downloads now in the tens of millions per month, and it was donated to the Linux Foundation's Agentic AI Foundation as a vendor-neutral standard in December 2025. That scale of adoption is exactly why access control matters here: the same protocol that connects an agent to a tool is also, in many real deployments, the protocol connecting that agent to everything the tool can reach.
MCP provides the interoperability layer for discovering and invoking capabilities. It doesn't, on its own, provide granular enterprise authorization, that has to be implemented through the surrounding authorization and policy architecture a team builds around it. MCP authentication establishes identity. It does not, by itself, establish sufficiently granular tool-level authorization, and whether a given deployment enforces that granularity depends entirely on how the server and the surrounding infrastructure are configured. For a regulated enterprise, MCP server data exposure risk sits exactly there: in the assumption that connecting an agent to a tool and restricting what it can do with that tool are the same step. They aren't, and treating them as though they are is how an agent ends up with reach into capabilities nobody meant to grant it.
Here's what that gap actually looks like in practice, before any more of the theory behind it.
The setup. A finance team connects an AI agent to an internal MCP server. The server exposes three tools: get_invoice_status, a read-only tool; send_invoice_reminder, which drafts and sends an email; and issue_refund, which moves money.
The task. Someone asks the agent: "Check on invoice #4471 and follow up with the customer if it's overdue."
The authentication. The agent connects to the server and authenticates successfully. Nothing about this step is unusual, and nothing about it is wrong.
The gap. This particular server, like plenty of real MCP deployments, was never configured with tool-level authorization on top of that authentication. So successful authentication gives the agent reach into all three tools, get_invoice_status, send_invoice_reminder, and issue_refund, regardless of what the task actually needed. The task never mentioned refunds. The agent can still call that tool, because nothing at the server or infrastructure layer ever scoped it out.
Every step below is the fix for one specific point in this trace.
The three tools on that server aren't interchangeable, and restricting them as one bundle is the first mistake. get_invoice_status is a read-only tool. send_invoice_reminder takes an action with a real-world consequence: a stranger receives an email. issue_refund moves money. Treating "access to the server" as a single yes-or-no grant is how a restriction meant to cover invoice lookups quietly also covers refunds, because nothing separated the three at the point of connection.
That's also exactly the gap the trace above exposed. The agent's authentication was never the problem. It's authorization, the separate question of what an authenticated identity is actually allowed to do with each specific tool, that this deployment left unconfigured. Building real MCP server permissions means adding that layer deliberately, because a working login doesn't supply it automatically.
Applied to the trace: a task-scoped permission grant for this specific session includes get_invoice_status and send_invoice_reminder, and excludes issue_refund entirely, not because refunds are inherently dangerous everywhere, but because this task never needed that capability. AI agent data access should be scoped at the level of the specific tool being requested, not at the level of "can this agent talk to this server." With that scope in place, the agent calls get_invoice_status(invoice_id=4471), confirms the invoice is overdue, and moves to the next step. issue_refund was never reachable for this session in the first place, regardless of what the agent might have decided to do with it.
The agent is authorized to call send_invoice_reminder, so scope alone says the call is fine. But the draft the agent generates pulls context from the invoice record and includes the customer's full payment card number in the body of the reminder email, something a routine follow-up message never needed to contain. The scope check passes. The content is still wrong.
Runtime policy enforcement that evaluates the specifics of a request before it completes, not just whether the calling identity has the right scope, is what catches this. Access control decided the door was unlocked. It never inspected what got carried through it, which is a separate check the call's actual content still needs before it goes out.
If this finance agent shared a single service credential with every other agent in the deployment, the audit trail from this exact trace would be unreadable after the fact: a call to send_invoice_reminder attributed to "the finance integration" rather than to this specific session, this specific task, this specific scope grant. A distinct identity per agent is what makes the difference between "an agent did this" and "this agent, on this task, under this scope, did this."
That identity is also what makes ongoing review possible. Three months later, the finance team's process changes and invoice reminders now go out through a different system entirely. If nobody revisits this agent's scope, it still holds send_invoice_reminder access it no longer has any task that needs, quietly sitting there as reachable capability nobody's actively using or checking. Treating that scope grant as something to review on an ongoing basis, not something set once at connection and forgotten, is what keeps server capabilities matched to what an agent's task actually requires today rather than what it required when the session was first configured.
Now change the scenario slightly: a different task, for a different agent, genuinely does need issue_refund in scope. Scope and content checks still aren't the last word for a tool that moves money. Reducing what an agent can reach in the first place and routing what remains high-risk to a person are complementary: narrowing scope keeps issue_refund out of reach for tasks that never needed it, and human review catches the specific refund calls that do reach it, before money actually moves.
Data privacy controls that can't be demonstrated after the fact don't hold up during an actual review. Walk back through the trace and each step left something worth recording: the authentication event, the scope grant that excluded issue_refund, the get_invoice_status call and its result, the send_invoice_reminder call that got flagged for its content before sending. An evidence pipeline built to generate this as a byproduct of normal operation, rather than reconstructed after an incident, is what lets a reviewer answer "what did this agent actually do with invoice 4471" without guessing.
Restricting MCP access isn't about blocking MCP. It's about ensuring that every agent receives only the capabilities its task requires, that each invocation is evaluated in context rather than waved through on scope alone, and that the resulting decision is observable afterward rather than reconstructed from memory. The trace above, one finance agent, one MCP server, one task that only ever needed two of three available tools, is what that looks like when it's built deliberately instead of assumed to already be there.