MCP Security: OAuth, Tool Permissions, and Prompt Injection

By

What is MCP security?

MCP security is the set of authorization, isolation, validation, approval, and audit controls that limit what Model Context Protocol clients and servers can read or change. It covers both remote OAuth-protected servers and local processes running with a user's privileges.

TL;DR

  • -MCP security is capability security: the real question is what a compromised model, client, server, or tool result can cause
  • -For remote HTTP servers, validate token issuer, audience, expiry, and scopes on every request; never pass the client's MCP token to an upstream API
  • -Authorization and tool approval are separate controls: scopes decide what a caller may do, while approval decides whether this invocation should happen now
  • -Treat tool descriptions, arguments, and results as untrusted; enforce policy in deterministic code rather than asking the model to police itself
  • -Run local servers with the smallest filesystem, network, and secret access possible, and keep audit events useful without storing raw credentials or unnecessary PII

An MCP server turns model output into capability. A calendar search is modest. Sending mail, running SQL, changing cloud infrastructure, or reading a home directory is not. The security question is therefore not “is this prompt safe?” It is:

If one component is fooled or compromised, what can it read, change, or send?

That framing catches failures OAuth alone cannot. A valid user can still be tricked into approving the wrong action. A trusted server can return poisoned content. A local server can be legitimate and still have far too much access.

This guide is the security layer for the broader MCP server architecture and production server guide. It follows the current MCP specification dated July 28, 2026; version-sensitive links below point to that revision rather than to an undated summary.

Draw the trust boundaries first

A typical remote path has four principals:

user → MCP host/client → MCP server → upstream API or database
                         ↘ model context and tool results

They do not share one identity or one trust level.

  • The host decides what context and tools the model can see.
  • The client speaks MCP and carries authorization for the current caller.
  • The server validates that caller and enforces tool policy.
  • The upstream system has its own identity and permission model.
  • The model proposes actions; it is not an authorization engine.

Write down which boundary checks identity, tenant, object ownership, scope, and user approval. If a box says “the prompt handles it,” the design has no hard control there.

Threat model: the failures worth designing for

FailureWhat it can becomePrimary control
Malicious tool descriptionThe model selects a tool or leaks context it should notTrust registry, review, tool allowlist, isolated contexts
Injection in a document or tool resultA read operation steers a later write operationTreat results as data, separate read and act stages, approve sensitive calls
Broad scopes or wildcard toolsOne token unlocks unrelated systemsPer-capability scopes and step-up authorization
Token passthroughA token is replayed at the wrong serviceAudience binding and separate upstream credentials
Unchecked tool argumentsSQL, command, path, URL, or tenant injectionSchema plus semantic validation and server-side policy
Guessable workflow handleCross-user state accessOpaque expiring handles bound to the authenticated principal
OAuth metadata SSRFAccess to internal services or cloud metadataHTTPS, destination validation, redirect checks, egress policy
Over-privileged local serverSource, SSH keys, or credentials become readableSandboxing and explicit filesystem/network grants
Verbose telemetryCredentials or customer data persist in logsField allowlist, redaction, access control, retention

This list is intentionally operational. “The model may hallucinate” is not a useful threat until it is connected to a capability and an impact.

Remote authorization: implement the resource-server boundary

For HTTP transports, MCP defines an OAuth-based flow. The current authorization specification requires Protected Resource Metadata for authorization-server discovery and Resource Indicators so the requested token is bound to the intended MCP server.

At the server, every protected request needs deterministic checks:

signature → issuer → audience/resource → expiry → scopes → tenant/object policy

The first five validate the credential. The last one validates the requested action. A token with tickets:write does not prove that the caller may update every ticket in every tenant.

Keep scopes close to capabilities

Avoid a single mcp:* or admin scope. Prefer names that match stable permissions:

issues:read
issues:comment
issues:close
deployments:read
deployments:promote

Start with discovery and read access. Request a stronger scope when the user first attempts a privileged operation. The current spec supports this through a 403 plus an insufficient_scope challenge. Its scope-minimization guidance explains why asking for everything up front increases blast radius and makes consent less meaningful.

Never pass the MCP token downstream

Token passthrough is not a shortcut. It erases the boundary between the MCP server and the API behind it. The MCP authorization spec requires the server to accept only tokens intended for itself and forbids transmitting other tokens onward.

If the server calls GitHub, Google, or an internal API, use a separate upstream token:

client --token audience:mcp.example--> MCP server
MCP server --different upstream token--> API

Store the mapping server-side. Never expose either token to the model, tool output, URL query string, or routine logs.

OAuth discovery is also an outbound request surface

A malicious server can put internal addresses in resource or authorization metadata. Validate every discovery URL and every redirect hop. Production clients should require HTTPS, reject private, loopback, link-local, and cloud metadata ranges unless a narrow local exception is explicit, and enforce the same policy after DNS resolution. An egress proxy is stronger than a hand-written list of string checks. The MCP SSRF guidance covers DNS rebinding and redirect-chain cases that simple URL validation misses.

Authorization is not tool approval

Authorization answers: may this principal use this capability? Approval answers: should this exact action run now with these arguments? You often need both.

A useful default policy is:

OperationDefault handling
Read from a narrow, trusted sourceAllow after normal authorization
Read from the open web, email, or uploaded filesAllow, but mark the result untrusted
Create a reversible draftShow the intended target and important fields
Send, publish, charge, delete, deploy, or change accessRequire explicit confirmation
Execute arbitrary code or queryDeny by default; expose a constrained operation instead

The MCP tools specification recommends a human able to deny invocations and asks clients to show tool inputs for sensitive operations. Tool annotations can improve the UI, but they are not enforcement. The tools specification requires clients to treat annotations as untrusted unless they come from a trusted server.

Approval must show the consequence, not an opaque JSON blob. “Send this message to [email protected]” is useful. “Run messages.execute?” is not. Bind the approval to a canonical form of the arguments so the target cannot change between preview and execution.

Treat all model-facing content as untrusted

Prompt injection often arrives indirectly: a fetched webpage, issue description, email, source comment, or MCP tool result tells the model to ignore its task and take a different action. The payload may come from a server you trust operationally because that server reads data you do not trust.

Do not rely on a stronger system prompt as the only defense. The practical controls sit around the model:

  1. Separate reading from acting. A component that summarizes arbitrary web content should not also hold deployment or messaging tools.
  2. Pass structured facts forward. Convert untrusted text into a typed record with provenance. Do not forward hidden HTML, instructions, or unnecessary raw content.
  3. Authorize each action server-side. The model cannot grant itself access by writing a persuasive tool argument.
  4. Require approval for consequential actions. Show destination, mutation, and source of the request.
  5. Constrain egress. If a tool only calls one API, it does not need arbitrary internet access.
  6. Validate rendered output. Strip dangerous HTML and do not auto-fetch model-made URLs containing private data.

The OWASP prompt injection guidance treats least privilege, structured separation, output validation, and human approval as defense in depth. None is a universal detector. Design for an injection eventually getting through, then limit what it can accomplish.

Build narrow tools, not a remote shell

The safest validation is a smaller interface. Compare:

execute_sql(query: string)

with:

get_invoice(invoice_id: InvoiceId)
mark_invoice_reviewed(invoice_id: InvoiceId, reason: ReviewReason)

The second design gives the server a place to enforce ownership, allowed transitions, and bounded values. JSON Schema catches shape errors, but it does not answer whether a path is inside an approved root or an object belongs to the caller.

For every tool, validate:

  • types, lengths, formats, enum values, and request size;
  • tenant and object ownership from verified identity, never from model claims;
  • paths after canonicalization, against an allowed root;
  • URLs after parsing and DNS resolution, with an egress policy;
  • query parameters through prepared statements or a fixed query builder;
  • business transitions and idempotency keys;
  • result size, content type, and fields returned to the model.

Use separate read and write tools. Rate-limit by principal and expensive operation. Put deadlines and cancellation on every upstream call. These controls also make the server easier to test and operate.

Local servers need process security, not pretend OAuth

The current MCP authorization spec says stdio implementations should obtain credentials from the environment rather than use the HTTP authorization flow. That does not make a local server safe. It runs as a process with whatever privileges the host gives it.

Before installing a local server:

  • inspect the exact command, arguments, package source, and pinned version;
  • avoid install commands that execute mutable packages on every launch;
  • grant only required directories, preferably read-only;
  • block network access unless the tool needs a named destination;
  • inject only the secrets that this server uses;
  • run risky parsers and code execution in a sandbox or container;
  • keep the server on stdio unless another local process genuinely needs HTTP access.

The MCP project’s local-server guidance is blunt: a local server may execute with the client’s privileges. A convenient install button is therefore a code-execution consent flow, not a harmless configuration edit.

State handles are names, not credentials

Long workflows often return workflow_id, basket_id, or another handle for a later tool call. The July 2026 protocol is stateless at this layer. Possessing a handle must not be enough to access its state.

Generate opaque, unguessable handles; give them a bounded lifetime; and bind them on the server to the verified user and tenant. Check that binding on every call. Do not trust a user_id argument supplied beside the handle.

Audit without building a data leak

A useful audit event answers who attempted what, against which object, under which policy, and what happened:

{
  "event": "mcp.tool_call",
  "request_id": "req_01...",
  "principal_id": "usr_internal_42",
  "server": "billing-tools",
  "tool": "mark_invoice_reviewed",
  "target_id": "inv_7f...",
  "decision": "allowed",
  "approval_id": "apr_01...",
  "scope_set": ["invoices:review"],
  "result": "success",
  "duration_ms": 184
}

Do not log bearer tokens, authorization codes, cookies, secret tool arguments, full email bodies, or raw model context by default. Prefer stable internal IDs and field allowlists. Protect audit storage from the service it records, restrict access, set a retention period, and test deletion where policy requires it.

Security tests that belong in CI

Happy-path Inspector calls are not a security test. Add cases for:

  • missing, expired, wrong-issuer, and wrong-audience tokens;
  • valid token with insufficient scope;
  • cross-tenant object IDs and stolen state handles;
  • extra JSON properties, oversized input, path traversal, and injection strings;
  • URLs resolving to loopback, private, link-local, or redirected internal targets;
  • tool output containing indirect instructions and exfiltration links;
  • approval arguments modified after the preview;
  • duplicate writes, retries, timeouts, and cancellation;
  • logs and traces containing seeded fake secrets;
  • unavailable authorization, policy, audit, and upstream services.

Then run end-to-end agent scenarios. The complete MCP server testing guide covers contract snapshots, protocol-version matrices, transport failure injection, and conformance. Those tests prove that the server rejects bad requests; they do not prove that the host chooses a safe tool sequence. The agent evaluation guide covers trajectory and task-level regression gates.

Production review checklist

Before exposing a server, confirm:

  • Each trust boundary and data class has an owner.
  • Remote tokens are validated for issuer, audience, expiry, and scope.
  • Downstream APIs use separate credentials; token passthrough is impossible.
  • Tool access is derived from verified identity and policy.
  • Sensitive calls require an argument-bound user approval.
  • Tools expose narrow operations and reject unknown fields.
  • External content and tool results are treated as untrusted data.
  • Local processes have explicit filesystem, network, and secret grants.
  • Handles are opaque, expiring, and bound to the caller.
  • Rate limits, deadlines, output limits, and cancellation exist.
  • Audit events omit secrets and unnecessary personal data.
  • Revocation and emergency disable paths have been exercised.

No item makes an agent “prompt-injection proof.” Together they turn a model failure from an unrestricted incident into a rejected request, a bounded action, or an approval the user can stop.

Frequently Asked Questions

Does OAuth make an MCP server safe?
No. OAuth authenticates and authorizes the caller at the transport boundary. The server must still enforce object-level permissions, validate tool arguments, protect downstream credentials, and limit side effects. The client must still treat server content as untrusted and request approval for sensitive actions.
Should a local stdio MCP server implement OAuth?
Usually not. The current MCP authorization specification applies to HTTP transports and says stdio implementations should obtain credentials from the environment. A local server still needs process isolation, narrow filesystem and network access, safe secret injection, and explicit installation consent.
Can tool annotations replace an authorization policy?
No. Annotations such as read-only or destructive are hints supplied by the server. MCP clients must treat them as untrusted unless the server itself is trusted. Enforce access and approval rules outside the model and outside self-declared annotations.
Can prompt injection be solved with a stronger system prompt?
No. System instructions help define behavior but do not create a hard security boundary. Reduce impact with least privilege, isolation, deterministic argument checks, output handling, action approval, and adversarial tests.