An admin once asked me which scopes I had granted the External Client App behind an MCP server. I answered properly — I listed them, explained why each was there, and said no more had been granted than the connection needed. It was a good answer to the question asked.
Walking away, I realised neither of us had asked the question that actually decides what happens. Not “what may this connection do?” but “what does the Apex behind each tool do once it is called?” The first has a tidy answer in Setup. The second is in a file, and nobody had opened it.
The sentence in the title is half true
Let me start with what Hosted MCP is, in one paragraph, so this stands on its own.
The Model Context Protocol is an open standard that lets an AI client — Claude, or another model host — call tools that a system exposes. Salesforce Hosted MCP Servers use it to expose your org’s Apex as tools a model can call. The connection goes through an External Client App, which is where you, the admin, choose the OAuth scopes it receives.
So the sentence people reach for — scopes are the only thing between a tool and your org — is not wrong so much as misaimed. Scopes stand between the connection and your org. Between the tool and your data stands the Apex. Two boundaries, at two layers, and in most reviews only the first gets read.
Scopes bound the connection. The Apex bounds the data.
What a scope is, and what it bounds
A scope is a named slice of capability granted to an app. You choose them when you register the External Client App, and they are a real boundary: if a capability was never granted, no phrasing by the model can conjure it. The token cannot do that thing.
Scopes are also visible: they sit in Setup as a list an admin can read without help, which makes least privilege easy to apply.
There is a second layer, which I wrote about when covering rollout: every MCP call runs as an authenticated user, through their own login, so the person’s permissions apply and the audit trail carries a real name. That is genuinely reassuring, and it is where most people stop.
It is also where the trap hides.
What a scope cannot see
A scope does not know which records a tool will touch. It does not know which fields the tool reads or writes. And — this is the important one — it does not know in whose name the query inside the class actually runs.
That last point deserves care, because “the call runs as the user” and “the query respects the user’s permissions” are not the same statement. The call is authenticated as the user; what the Apex does with that context is decided inside the Apex. If the code queries in system mode, it reads what it asks for, regardless of what the user may see. The connection was authorised. The data was not.
The asymmetry that matters
Once you separate the two layers, an asymmetry appears, and it is the most useful thing to hold in your head when reviewing an MCP rollout.
A generous scope on a narrow tool is survivable. Suppose the connection carries more capability than it needs, but the only tool exposed looks up an order’s status by number and returns four fields. The generous scope is untidy and worth fixing — the tool can still only do the one thing it was written to do. The blast radius is the tool, not the scope.
A narrow scope on a tool whose Apex runs in system mode is not survivable. Now the connection carries exactly one capability, and it looks disciplined in Setup. But the one door you opened leads to a class that was never asked to enforce anything — so within its reach, the user’s permissions are simply not consulted. Narrowness in the layer you can see does not compensate for absence in the layer you did not read.
This is why “we granted minimal scopes” is a reassuring sentence that does not, on its own, mean much. It describes the door. It says nothing about the room.
Why the Apex forgets to enforce anything
It is worth understanding why so much Apex behaves this way, because it is not carelessness.
For most of the platform’s history, Apex ran in system mode by default: plain SOQL and DML did not enforce object- or field-level permissions at the Apex layer. That exists for real reasons — a trigger stamping an audit field must write it even for users who cannot edit it by hand — and generations of good code were written under it.
From API version 67.0 onward, user mode became the default for plain operations. But the API version is stamped on each class, and it does not change when your org upgrades. So an org can hold two classes running the identical query with different effective access, purely because of the number in each file’s metadata.
So the version stamp on the class behind an MCP tool is part of your security posture. A strange thing to have to say, and true. (I wrote about user mode and system mode separately.)
The way out is the same in both directions: stop relying on the default. Say what you mean in the statement itself, so the behaviour does not depend on a number nobody reads:
// explicit — independent of the class's API version
List<Order> orders = [SELECT Id, Status FROM Order WHERE OrderNumber = :num WITH USER_MODE];
Reviewing a tool before you expose it
Here is the review I would run before any Apex method becomes a tool. Three questions, in order.
What does this method actually touch? Objects and fields, reads and writes, directly and through anything it calls. If you cannot answer in a sentence, the method is too big to be a tool.
In whose name does it query? Look for an explicit mode on every SOQL and DML statement. If there is none, look at the class’s API version — then go and make it explicit anyway.
Would I be comfortable if this ran a hundred times? A tool is called by something that does not get tired. Narrow tools survive that; broad ones do not.
Your next step
Take one Apex method you would consider exposing as a tool, in a sandbox or a Developer Edition org. Do not open Setup yet. Open the class.
Write down every object and field it touches, then find every SOQL and DML statement and check whether the mode is stated explicitly. Where it is not, add it. Only then configure the scopes.
That order changes the question you ask an admin. Instead of “which scopes did you grant?”, you can say what the tool touches, on whose authority, and what stops it doing more. That is what the admin was reaching for all along — I had just not noticed it was a question about Apex, not Setup.