What is the actual attack surface?
One sentence covers it: a tool-using agent translates natural language, which may contain attacker-controlled text, into privileged tool calls. Every other property of the system follows from that, including the uncomfortable one — that authorisation has to hold even when the agent has been successfully steered. A design that assumes the agent will only ever ask for what the user wanted has no security property at all.
That is a different threat model from the one most API authorisation was built for. The caller is not malicious and not trusted either; it is a component that can be persuaded. The question is therefore not whether the agent can be steered but what it is able to do once it has been.
What do the protocol SDKs give you, and what do they not?
The Model Context Protocol is now the widely adopted interface at exactly that boundary, which makes what it does and does not provide a practical matter rather than a design debate. A systematic gap analysis across six official SDKs — Python, TypeScript, Go, Rust, C# and Swift — reported three structural shortcomings.
- No pre-authentication tool discovery, so a client cannot learn what a server offers before holding credentials for it.
- No fine-grained per-tool authorisation in the base SDKs, which pushes the decision of which tools a caller may invoke outside the protocol layer.
- Authorisation primitives that assume the agent has not been steered by an attacker.
The remedies the authors propose are composable extensions rather than protocol changes: cross-header credential normalisation, cached token verification across different identity providers, an unauthenticated metadata endpoint for credential-free registry discovery, and permission-filtered tool visibility kept consistent with per-tool enforcement through a single declarative annotation.
Why is a filter not a boundary?
Because filtering what a caller can see and enforcing what a caller can invoke are two separate mechanisms in most deployments, and separate mechanisms drift. The visible list is updated when someone remembers; the enforcement path is updated when something breaks. After a few releases they disagree, and the disagreement is invisible until somebody calls a tool they could not see.
Binding both to one declaration is the proposal worth taking seriously, because it removes a category of bug rather than a specific instance of it. The same argument applies anywhere a system has a display rule and an enforcement rule for the same decision: if they are expressed twice, they will eventually say different things.
| Concern | In the base protocol SDKs | Where it has to live instead |
|---|---|---|
| Which tools exist | Discoverable only with credentials in hand | An unauthenticated metadata endpoint for registry discovery |
| Which tools a caller may invoke | Not fine-grained | Your own layer, or a per-tool annotation bound to visibility |
| Identity across providers | Not normalised | Cross-header credential normalisation and cached token verification |
| Behaviour after the agent is steered | Assumed not to happen | Enforcement that does not trust the caller's intent |
| Tool schema shape | Converged across vendors without a standard | Adapters of a few dozen lines, not a subsystem |
| Error and streaming semantics | Still divergent between vendors | Tested against each vendor; rarely documented |
What about portability?
Better than it looks, in one dimension and worse in another. Tool definitions have converged on a JSON Schema shape close enough across vendors that adapters are now a few dozen lines rather than a subsystem. Nobody published a standard; the shape won by being copied.
The divergence that remains is in error handling and in how partial or streamed tool calls are surfaced. Those are where portability actually breaks, and they are rarely covered in vendor documentation — which means they have to be established by testing rather than by reading.
Why do tool calls speed up more than prose?
Because structured output is the easiest thing in the world to guess. Field names, brackets, quotes and separators are close to determined by what came before, so a draft model's acceptance rate is high exactly where latency is most visible to a user waiting on a tool call. One published set of draft models, around 300 million parameters each with five decoder layers, reported 57% lower latency on function calling for a 2.6-billion-parameter target — a larger practical result than the throughput headline alongside it.
That gain is conditional, and the condition is worth knowing. Speculative decoding pays when verification is cheap relative to generation and the draft guesses well. On a memory-bandwidth-bound device running a model with one billion active parameters, the same architecture returned 1.18 times against 2.54 times on a datacentre accelerator. The technique nearly disappears where those conditions do not hold, so the only measurement worth acting on is one taken on your own hardware and traffic.
What should you check in your own stack?
- Can a caller invoke a tool it cannot see, or see a tool it cannot invoke? If the two rules are written twice, they will diverge.
- Does any authorisation decision depend on the agent having interpreted the request faithfully?
- What happens to a partial or streamed tool call on each vendor you support, and was that tested or assumed?
- Is the latency you care about on the tool-call path, and have you measured speculative decoding on your own traffic rather than a published mix?
- Can a client discover what a server offers without first holding credentials for it, and should it be able to?
Questions
- What is the core security problem with tool-using agents?
- They translate natural language, which may contain attacker-controlled text, into privileged tool calls. The consequence is that authorisation has to hold even when the agent has been successfully steered; a design that assumes a faithful agent has no security property.
- Does the Model Context Protocol handle per-tool authorisation?
- Not in the base SDKs. A gap analysis across the six official SDKs — Python, TypeScript, Go, Rust, C# and Swift — found no fine-grained per-tool authorisation and no pre-authentication tool discovery, which pushes the decision of which tools a caller may invoke outside the protocol layer.
- Why bind tool visibility and tool enforcement together?
- Because they are separate mechanisms in most deployments and they drift apart, so a caller ends up able to invoke something it cannot see. Expressing both through a single declarative annotation removes a category of bug rather than one instance of it.
- Are tool schemas portable between vendors?
- The shape is. Tool definitions converged on a JSON Schema form close enough that adapters are a few dozen lines rather than a subsystem, without anyone publishing a standard. What stays divergent is error handling and how partial or streamed calls are surfaced, and those are rarely documented.