MCP Security for Engineers: Threat Model, Attack Surface, and Hardening
My lens on MCP security comes from backend and distributed systems. For most of my career, a service boundary meant schemas, credentials, logs and a piece of deterministic code deciding what happened next.
In an agentic system, content crossing that boundary can also influence the next action. While building document-centric AI systems, I kept returning to an uncomfortable fact: retrieved text is no longer passive once it enters model context. It can compete with the instructions that are supposed to govern the system.
This is why MCP interests me. Model Context Protocol gives AI applications a clean way to discover and call tools. That interoperability is genuinely useful. It also makes it very easy to connect a non-deterministic planner to systems that read private data, change state and communicate outside the application.
When I review an MCP-based architecture, I therefore start with one question:
Can untrusted content change the action we execute without passing an independent authorization or approval check?
If the answer is yes, I do not consider the system production-ready yet. The rest of this article explains the threat model and hardening order I use to close that gap.
Executive summary
For security purposes, I treat every remote MCP server as a semi-trusted service and every tool response as potentially untrusted input. I separate the problem into four layers:
- Transport and identity (who is talking to whom)
- Authorization and policy (which action is allowed, for whom, under which constraints)
- Prompt and data boundaries (which content can influence planning)
- Observability and response (how I explain and contain a bad sequence)
The important distinction is simple: authenticated transport does not imply authorized action. A valid tool call can still be the wrong call, made for the wrong reason, with data that should never have crossed that boundary.
MCP changes the boundary, not the fundamentals
I do not think MCP invents a completely new class of security. Identity, least privilege, input validation, auditability and incident response still matter. What changes is the component making the decision.
In a conventional backend, reviewed code decides which API to call and how to build the request. In an agentic system, a model may select the tool, infer the arguments and decide whether to continue based on text it just retrieved. Some of that text may come from a web page, a support ticket, a document or another tool.
The official MCP architecture deliberately focuses on context exchange; it does not prescribe how an AI application must use that context. That separation is sensible, but it means the host application remains responsible for the policy around model behavior.
For me, the threat model must therefore include:
- Tool names and descriptions influencing tool selection
- Tool responses or retrieved documents influencing later calls
- A low-risk tool providing data to a high-impact tool
- Tokens being accepted by, or passed to, the wrong resource
- Sensitive data reaching an external sink through exported logs, messages or generated files
- A sequence of individually valid calls producing an unauthorized outcome
A composite design-review scenario
Picture a design review. This is a deliberately composite scenario, not a story about one specific client or incident.
An internal support agent has three tools:
- Search the knowledge base
- Read a customer record
- Draft and send an email
Each tool looks reasonable in isolation. Search is read-only. Customer access is already part of support work. Email is necessary to answer the user.
Now assume a retrieved document contains an instruction telling the model to ignore the original task, fetch additional customer fields and include them in a message to an external address. If the host treats retrieved text as trusted instruction, the model can chain three permitted capabilities into one impermissible outcome.
Nothing “hacked” the email API. No access token was necessarily invalid. The failure happened in the composition layer: data became instruction, and authorization was too coarse to catch the difference.
This is the distinction I care about most: tool security is not workflow security.
My minimal formal model
I find a small amount of notation useful because it forces the architecture discussion to become precise.
Let:
- be the observations available at time : user input, retrieved content and tool responses
- be the next action proposed by the model
- be the history visible to the agent
- be the model policy that proposes the next action
- be an independent gate implementing controls
- be the actions that violate the user’s intent or the system policy in that context
An attacker does not need to rewrite the application. It may be enough to transform the visible history through malicious retrieved content. I call that transformation , where belongs to a bounded attack set .
The residual risk after applying the controls is:
The model may still propose a harmful action. The security failure occurs when the independent gate also allows it. In practice I minimize while keeping benign task utility above an acceptable threshold; “block every tool” is safe but useless.
The execution invariant is even easier to use in a review:
Each term maps to a control and a test. This is where the formal model earns its place: it separates what the model proposes from what the system executes, instead of merely making the prompt longer.
The four attack surfaces I review first
1. Tools with more authority than the task
The easiest architecture is often a global registry in which every agent can see every tool. It is also the architecture that makes me most uncomfortable.
I prefer a host-side, model-visible allowlist per user, task and environment, backed by server-side checks against the authorization context. A support workflow should not even see deployment tools. An exploratory task should not inherit production write access. A read operation and a destructive write should not share one catch-all scope.
The model can propose an action. It should not be the final authority that approves it.
2. Data that quietly becomes instruction
Indirect prompt injection is a boundary failure: content intended as data influences the control flow of the application.
I assign provenance and trust labels in the host, outside fields controlled by the server. I keep structured data separate from free-form content and minimize how much raw external text enters the planning context. When the system must process untrusted text, I assume it may contain instructions.
Instruction stripping or a sanitizer model may reduce exposure, but I do not treat either as an authorization boundary. The reliable control is still outside the model: what action is permitted, with which arguments, on which resource, for which user.
3. Safe tools that compose into an unsafe workflow
A read-only tool can retrieve a secret. A messaging tool can transmit it. A file tool can persist it. Each permission may be defensible alone; the data flow between them may not be.
I explicitly model cross-tool flows such as:
- External content -> internal search
- Internal record -> generated message
- Credential store -> model-visible text
- Read-only source -> write-capable destination
Separately, an MCP proxy can become a confused deputy in its OAuth flow. Tokens must be audience-bound to the intended resource; an MCP server must not pass a client token through to an upstream API and hope the downstream service interprets it correctly.
4. Traces that explain nothing
When an agent takes several steps, a conventional request log is not enough. I want to reconstruct the sequence without logging secrets or private model reasoning.
A useful trace connects:
- The user request and authenticated principal
- The tools exposed for that task
- The proposed action and validated arguments
- The authorization or approval decision
- The tool result metadata and trust level
- The next action, final outcome and trace ID
If I cannot answer “why was this call allowed?” after the event, the system is not observable enough for production.
The hardening order I prefer
I harden from the outside in. It is tempting to begin with prompt filters because they are visible and easy to demo. Identity and authorization give me a firmer base.
Layer A: Establish identity and protect transport
For remote HTTP servers, I follow the MCP authorization specification instead of inventing a token flow: OAuth 2.1-based authorization, protected-resource metadata, PKCE, resource indicators and audience validation. I use short-lived credentials and never put access tokens in URLs or logs.
For local STDIO servers, the boundary moves to process launch, environment variables, filesystem and network permissions, sandboxing, user consent and the trust placed in the executable itself.
I also keep one rule visible in reviews: a session identifier is not authentication.
Layer B: Make authorization deterministic
I enforce permissions at the tool boundary, not in natural-language instructions alone.
- Deny by default
- Expose only the capabilities needed for the current task
- Validate arguments against both a schema and business policy
- Bind tokens and approvals to the intended resource and action
- Use step-up authorization for higher-impact capabilities
- Require explicit approval for irreversible or externally visible actions
The approval must be specific. “Allow agent?” is weak. “Send this message to this recipient with these attachments?” is a real decision.
Layer C: Preserve provenance at the prompt and data boundary
I keep external content, system policy and user instructions distinguishable for as long as possible.
- Label source, trust level and sensitivity
- Prefer structured outputs over unrestricted text
- Keep secrets out of model-visible free-form fields
- Validate high-risk outputs with deterministic code
- Treat retrieved instructions as data unless policy explicitly says otherwise
- Test with poisoned documents, tool descriptions and responses
This does not make prompt injection disappear. It prevents prompt handling from being the only thing standing between untrusted content and a privileged action.
Layer D: Design for investigation and containment
I add trace IDs across the entire tool sequence, redact credentials at ingestion and log authorization decisions in a form an engineer can inspect.
I also want operational controls before launch:
- Disable one tool, server, tenant or capability without redeploying everything
- Revoke credentials quickly
- Stop an in-flight workflow
- Quarantine suspicious output
- Replay a trace safely in a non-production environment
Security is not only prevention. It is the ability to reduce blast radius when prevention fails.
The trade-offs I am willing to make
Every control has a cost. Narrow scopes complicate authorization. Approvals add friction. Rich traces cost money and require careful redaction. Isolation can increase latency.
I do not aim for zero friction. I aim for friction proportional to consequence:
- A scoped, read-only lookup can usually run without interruption
- Access to sensitive data deserves tighter policy and better audit
- External communication needs a clear recipient and content preview
- Destructive or financial actions need stronger authentication, approval and idempotency
- Background autonomy needs pre-approved boundaries, budgets and a reliable stop mechanism
The controls I will not trade away are deterministic authorization, resource-bound credentials and enough traceability to explain a high-impact action. Model quality cannot compensate for their absence.
My MCP production review checklist
Before I approve an MCP workflow for production, I want clear answers to these questions:
- Which content sources are untrusted, and is their provenance preserved?
- Which tools can this user and this task actually see?
- Are tokens scoped and audience-bound to the intended MCP server?
- Does every high-impact tool enforce policy outside the model?
- Can sensitive data move from a read tool to a write or communication tool?
- Are arguments validated against business rules, not only JSON Schema?
- Does approval show the exact action, target and data involved?
- Can the team reconstruct the sequence without logging credentials or private reasoning?
- Can one compromised tool or tenant be disabled quickly?
- Have we tested poisoned tool descriptions, retrieved documents and tool responses?
If several answers are vague, I would keep the system in a controlled environment and fix the authority model before adding more tools.
Reading list
Protocol and implementation
- MCP architecture overview
- MCP tools specification, revision 2025-11-25
- MCP authorization specification
- MCP security best practices
- RFC 8707: Resource Indicators for OAuth 2.0
Agent security
- Not what you’ve signed up for: Compromising Real-World LLM-Integrated Applications with Indirect Prompt Injection
- MCP Security Bench: Benchmarking Attacks Against Model Context Protocol in LLM Agents
Mathematical framing
- Robust Dynamic Programming
- Learning Models with Uniform Performance via Distributionally Robust Optimization
The question I leave in the room
MCP makes tools interoperable. It does not make authority safe.
The final question in my reviews is still the one from the beginning:
Can untrusted content change the action we execute without passing an independent authorization or approval check?
If it can, I add a boundary. If I cannot explain where that boundary is enforced, who owns it and how it fails, I am not done.
That mindset served me in backend systems, financial workflows and cloud platforms before agents became part of the stack. I expect it to remain useful long after today’s MCP implementation details change.