MCP server security is the set of controls that limits what an AI client can do through your Model Context Protocol (MCP) server. These controls decide which tokens the server accepts, what it does with them, what state it hands back and what text it puts in front of a model. For a SaaS company shipping a remote MCP server, a ChatGPT plugin or a Claude connector, the MCP security best practices in the current specification (revision 2026-07-28) come down to five rules. Accept only tokens issued for your server, never forward them, request the narrowest scopes, bind state handles to the verified user, and treat tool descriptions and results as untrusted. This guide maps each threat to the spec’s Security Best Practices and Authorization pages, then gives a checklist with a test for every row. It is the security gate of our guide to production-ready AI agents.

Which revision this guide follows

The MCP versioning page says “The current protocol version is 2026-07-28”, and the maintainers’ release post is dated 28 July 2026. The official MCP security best practices page now lives under /docs/2026-07-28/, and the old /specification/ path returns a 308 redirect.

Summaries often credit this release with more than it changed:

  • Stateless operation is new. The initialize handshake and the Mcp-Session-Id header are gone (changelog, SEP-2567 and SEP-2575).
  • Client ID Metadata Documents (CIMD) arrived in 2025-11-25 as the recommended registration method. This revision deprecates Dynamic Client Registration (DCR, RFC 7591) in their favour.
  • Elicitation dates from 2025-06-18 and URL mode from 2025-11-25, but it now travels through Multi Round-Trip Requests.
  • MCP Apps is an official extension with its own specification, dated 2026-01-26, outside the core revision.

Security changes since 2025-11-25

  1. “Session Hijacking” became “State Handle Hijacking”.
  2. The new sections are Mix-Up Attacks, Localhost Redirect URI Impersonation, CIMD Trust Policies and SSRF Against Authorization Servers.
  3. Authorization servers SHOULD return iss per RFC 9207, and clients MUST check a present iss against the recorded issuer before redeeming a code (SEP-2468).
  4. DCR is deprecated (PR #2858), and the deprecation registry sets its earliest removal at the first revision released on or after 28 July 2027. Client credentials are bound to the issuer that minted them (SEP-2352). Our guide to MCP OAuth and CIMD covers the registration change, with conformance tests.
  5. Every Streamable HTTP POST carries Mcp-Method (plus Mcp-Name for tools/call, resources/read and prompts/get), and servers MUST reject headers that disagree with the body (SEP-2243).
  6. Roots, Sampling and Logging are deprecated (SEP-2577). Pass directories through tool parameters or server configuration, call the model provider’s API directly instead of Sampling, and log to stderr or OpenTelemetry. traceparent, tracestate and baggage are reserved _meta keys (SEP-414).
  7. Three changes move security work into your server code: request state that travels through the client, retries that replace stream resumption, and a cache scope on list and read results. They’re covered in What statelessness adds below.

What public measurements found

A First Measurement Study on Authentication Security in Real-World Remote MCP Servers (Zhou et al., arXiv, 21 May 2026) used cybersecurity search engines to find 7,973 live remote MCP servers, and 3,233 of them (40.55%) exposed tools with no authentication. They tested 119 OAuth-protected servers in depth and found at least one flaw in every one, 325 in total. The MCP OAuth findings cluster in registration and PKCE:

  • 115 servers (96.6%) had a DCR flaw, and in 114 the registration endpoint accepted any redirect_uri from an anonymous caller
  • 81 (68.1%) let an authorization request skip PKCE or downgrade it to plain
  • 72 (60.5%) did not show the redirect URI on the consent page.

Disclosure produced 9 CVE IDs. The study predates the current revision and used the 2025-11-25 spec as its reference point.

Astrix Security, a vendor now part of Cisco, published a narrower study on 15 October 2025. An LLM classified the README files of 5,205 GitHub repositories. About 53% of servers relied on static API keys or personal access tokens, and about 8.5% used OAuth. The classifier put 26.4% in an “Unknown” bucket, so read these as rough estimates. The maintainers’ roadmap (updated 22 August 2026) concedes the point, saying “Existing MCP servers lean on pasted API keys and long-lived refresh tokens.”

Who owns which control

When ChatGPT or Claude connects, the host is the MCP client and your server is an OAuth resource server. The Security Best Practices page mixes client and server duties, so sort them first:

  • Your MCP server: audience validation, no token passthrough, per-client consent if it proxies another API, state handle binding, scope challenges, tool text and audit logs.
  • Your authorization server (self-run or in an identity provider): PKCE enforcement, iss in responses, exact redirect URI matching, showing the redirect hostname and safe CIMD fetching.
  • The host: SSRF checks during discovery, authorization URL validation, confirmation prompts and whether to trust your tool annotations.
  • You again, if you ship a local package or run your own MCP client, such as an in-product agent that connects to servers your customers name.

ChatGPT and Claude publish their own rules. OpenAI’s authentication guide (its docs now call ChatGPT apps “plugins”) says ChatGPT prioritises CIMD, sends your canonical URI as resource, and uses a callback-ID-specific redirect URI unless your authorization server meets its iss requirements. Anthropic’s connector authentication page says Claude supports DCR and CIMD by default. Its beta static-header option sends one organisation-wide key, so “don’t use it to identify which user is calling”.

The threats, section by section

Token passthrough

The spec defines token passthrough as “an anti-pattern where an MCP server accepts tokens from an MCP client without validating that the tokens were properly issued to the MCP server and passes them through to the downstream API.” Its mitigation is “MCP servers MUST NOT accept any tokens that were not explicitly issued for the MCP server.” Token Handling adds “MCP servers MUST NOT accept or transit any other tokens”, and Access Token Privilege Restriction requires “a separate token, issued by the upstream authorization server” for upstream calls.

MCP token passthrough is the tempting SaaS shortcut, where the MCP server accepts a customer’s existing REST API token and forwards it. That breaks audience separation and corrupts your audit trail, because downstream logs “may show requests that appear to come from a different source with a different identity”. Have your authorization server put the MCP server’s canonical URI in aud, answer anything else with 401, and give the server its own credential for upstream calls. Our verdict scale treats token passthrough as a blocker.

The confused deputy problem

The confused deputy section covers an MCP proxy server that calls a third-party API, or your own older OAuth app, with a single static client ID while letting MCP clients register their own through DCR. The attack needs four conditions: the static client ID, DCR, a consent cookie set by the third-party authorization server, and no per-client consent at the proxy. An attacker registers a client with their own redirect_uri and sends the user a link. The cookie skips the consent screen, and the MCP authorization code lands on the attacker’s server.

The protections are MUSTs. Keep a per-user registry of approved client_id values and check it before the third-party flow. Show a consent page naming the client, the third-party scopes and the registered redirect_uri, with CSRF protection and frame-ancestors or X-Frame-Options: DENY. Use __Host- cookies with Secure, HttpOnly and SameSite=Lax, bound to the client_id. Match redirect URIs exactly. Store a random, single-use state only after consent, with a short expiry (the spec’s example is 10 minutes). To link third-party accounts mid-conversation, use URL mode elicitation. Third-party credentials “MUST NOT transit through the MCP client”, and the user who finishes the flow must be the one who started it.

SSRF during OAuth metadata discovery

The SSRF section is mostly a client duty. A malicious server can point its resource_metadata URL, its authorization_servers list or its endpoint URLs at internal targets such as the cloud metadata service at http://169.254.169.254/, and “MCP clients deployed to a server MUST consider SSRF risks”.

It becomes yours when your product contains an MCP client, and, new in this revision, when your authorization server supports CIMD (SSRF Against Authorization Servers), because it then “takes a URL as input from an unknown client and fetches that URL.” Allow HTTPS only. Block 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 127.0.0.0/8, ::1, 169.254.0.0/16, fc00::/7 and fe80::/10. Validate every redirect hop, pin DNS between check and use, and route fetches through an egress proxy such as Stripe’s Smokescreen, which the spec names. Use a tested library for IP checks. “Attackers exploit encoding tricks (octal, hex, IPv4-mapped IPv6) that custom parsers often miss.”

State handle hijacking

MCP is now stateless. A server that needs state across calls mints a handle, such as a basket ID, and receives it back as an ordinary tool argument. State handle hijacking means an attacker obtains or guesses that handle and acts on another user’s state. Servers “MUST NOT treat possession of a state handle as authentication”, SHOULD generate handles with a secure random number generator, and SHOULD store state under <user_id>:<handle>, “where the user ID is derived from the verified token rather than supplied by the client”. The Tools page is blunter, saying “a handle is a name, not a capability”. It asks unauthenticated servers for UUIDv4-level entropy and a bounded lifetime.

Randomness was a MUST for 2025-11-25 session IDs and is a SHOULD for handles, so check it yourself. A server that speaks only 2026-07-28 should ignore an incoming Mcp-Session-Id. If you still serve older clients, the 2025-11-25 session hijacking guidance applies to their sessions.

Scope minimization

The Scope Minimization attack is a stolen token carrying scopes such as files:*, db:* or admin:*, granted up front “because the MCP server exposed every scope in scopes_supported and the client requested them all.” The fallback is in the spec. If your 401 carries no scope parameter, the Scope Selection Strategy tells clients to request everything in scopes_supported, and Anthropic’s docs say Claude does exactly that.

Keep scopes_supported to “the minimal set of scopes necessary for basic functionality”, put scope in the first 401, and answer privileged calls with 403, error="insufficient_scope" and every scope the operation needs in one challenge. Avoid wildcard scopes such as *, all or full-access, leave offline_access out of your resource metadata, and log each step-up with a correlation ID. The spec’s list of common mistakes ends with “Treating claimed scopes in token as sufficient without server-side authorization logic.” Our verdict scale counts an over-broad credential as a blocker.

Local server compromise

Local MCP Server Compromise covers servers on the user’s machine. The threats it lists are malicious startup commands, malicious payloads and “an insecure local server that’s left running on localhost via DNS rebinding”. Most of its MUSTs fall on clients. It applies to you if you also publish a local package, a desktop extension or a bridge to your remote server. Prefer the stdio transport. Over HTTP, require an authorization token or use Unix domain sockets.

The Streamable HTTP rules add that servers “MUST validate the Origin header on all incoming connections”, answer an invalid one with 403, and bind local servers to 127.0.0.1. The Origin rule covers your remote endpoint too. The Local Server Security page lists the provenance signals users check: “a readable repository, signatures, an SBOM, a curated listing”. Publish all four.

Tool poisoning through descriptions and results

Neither the Security Best Practices page nor the Tools page says “tool poisoning”, but the spec states the premise in Tool Safety, where it says “descriptions of tool behavior such as annotations should be considered untrusted, unless obtained from a trusted server.” The MCP docs name the attacks. Tool descriptions are “instructions your model reads”, hidden directives work “without the poisoned tool ever being invoked”, and definitions can change after approval (a “rug pull”). OWASP’s MCP tool poisoning page covers the second path, tool results that carry instructions.

For a SaaS vendor the results path is larger, because your tools return text your customers’ users wrote, such as tickets, emails and documents. OWASP calls detecting injected instructions in free text “an open problem”, so plan for some getting through. The Tools security considerations require servers to validate all tool inputs, implement access controls, rate-limit tool invocations and sanitise tool outputs. In practice:

  • keep descriptions to what the tool does, under code review
  • return structuredContent with an outputSchema where you can
  • split read tools from write tools, and set readOnlyHint and destructiveHint truthfully, knowing hosts must not trust annotations from untrusted servers
  • enforce permission checks on the server, so an injected instruction can do no more than the user’s token allows.

Our verdict scale treats a prompt-injection path to an action that changes customer data or money, with no confirmation step, as a blocker.

Logging and audit

Audit rules sit in several sections. Token passthrough breaks accountability because the server “will be unable to identify or distinguish between MCP Clients”. Scope Minimization asks servers to log each step-up (“scope requested, granted subset”) with correlation IDs. Token Theft warns about tokens “cached or logged on the server”, and the Tools page asks clients to log tool usage for audit.

The Logging utility, now deprecated, only covered log messages sent to the client, but its security rule is a good floor for your own logs. That means no credentials or secrets, no personal identifying information and no “Internal system details that could aid attacks”. For each tool call, record the sub and client_id from the verified token, the tool name, scopes, decision and result status, plus any traceparent from _meta. Log io.modelcontextprotocol/clientInfo but never act on it, because the MCP 2026-07-28 specification’s _meta section calls it “self-reported by the sender and … not verified by the protocol”. Values mirrored into Mcp-Param-* headers are visible to every intermediary, which is why the spec says not to mark secrets or PII with x-mcp-header.

What statelessness adds (request state, retries and caches)

Without sessions, three jobs the transport used to do now sit in your code. The rest of the move is in our MCP stateless migration checklist. The Agentic AI Foundation’s post on hardening stateless MCP servers (Chaitanya Rahalkar, 7 October 2026) covers the same ground with tests.

Request state. In Multi Round-Trip Requests (SEP-2322), a server that needs more input, such as a user’s confirmation, returns requestState and the client echoes it back on the retry. Servers “MUST treat requestState as an attacker-controlled input”. If it influences authorization, resource access or business logic, they must protect its integrity with an HMAC or AEAD and reject state that fails verification. The spec also says to bind it to the authenticated principal, a short expiry and the originating request, and warns that this alone doesn’t make it single-use, so a one-time action must be enforced on the server.

Retries. The revision removes SSE resumability and Last-Event-ID (SEP-2575). “A broken response stream loses the in-flight request”, and the client re-issues it as a new request with a new ID (changelog). A write tool whose response was lost can therefore run twice, which means two refunds or two emails. Give every write tool an idempotency key in its arguments, or record completed work against a handle, so a repeat returns the first result.

Cache scope. Results of tools/list, prompts/list, resources/list, resources/read and resources/templates/list now carry ttlMs and cacheScope (SEP-2549). "public" lets shared intermediaries cache the result. A tool list filtered by the user’s permissions, or a resource holding one customer’s data, must be "private", and the label doesn’t enforce access, so check permissions on every call anyway.

The other sections, briefly

  • Mix-Up Attacks: emit iss in every authorization response, errors included, and set authorization_response_iss_parameter_supported: true. “PKCE alone does not prevent this attack.”
  • Localhost Redirect URI Impersonation: a CIMD “cannot prove which local process is listening on a localhost redirect URI”, so your authorization server must display the redirect URI hostname and SHOULD warn when only localhost URIs are registered.
  • CIMD Trust Policies: allowlist client domains for protected servers, or accept any HTTPS client_id for open ones.
  • OAuth Authorization URL Validation and stdio Transport Security in Proxy Scenarios: client and proxy duties, for anyone who ships either.
  • Authorization Code Protection: clients refuse to proceed without code_challenge_methods_supported, but only your authorization server can reject a request without PKCE or with plain. In the arXiv study, 68.1% of the servers accepted one or the other.

MCP security checklist mapped to the 2026-07-28 spec

Run each test against staging with test accounts. Days 6 and 7 of our ten-day production readiness review, and the whole of an MCP security audit, apply this review to every tool and MCP connection an agent can call. “Exit 3” and “exit 5” are MCP Inspector CLI exit codes, explained after the table.

Risk Spec section (linked) What to check How to test it
Token passthrough Token Passthrough, Token Handling aud equals your canonical server URI, and upstream calls use a separate token Test 2 below, then confirm no upstream call reuses the inbound Authorization header
Open data tools Security & Endpoint, Protocol Requirements Every tool touching customer data needs a token, and the 401 names resource_metadata Test 1 below, then call one data tool with no token and expect exit 3
Confused deputy Confused Deputy Problem Per-client consent before the upstream flow, a page that shows client, scopes and redirect_uri, and state set after consent Register a second client, open its authorization URL in a browser that consented for the first, and expect your consent page first
Open registration Dynamic Client Registration, Open Redirection CIMD supported, DCR off or restricted, and exact redirect matching Register with redirect_uris: ["https://attacker.example/cb"] and expect refusal, or a consent page showing that host
PKCE downgrade Authorization Code Protection Metadata lists S256, while no-PKCE and plain requests fail Send an authorization request without code_challenge, and expect an error and no code
Mix-up Authorization Response Validation iss in every response, and issuer identical to the resource metadata entry Test 4 below, then read iss on one real callback
SSRF in CIMD fetch SSRF Against Authorization Servers HTTPS only, private and link-local ranges blocked, each redirect hop checked, and an egress proxy Use a client_id URL you control that redirects to 169.254.169.254, and expect invalid_client or invalid_request and a block in proxy logs
SSRF in your own client Server-Side Request Forgery (SSRF) Same controls on resource_metadata, authorization_servers and endpoint URLs Point your client at a test server whose 401 names an internal resource_metadata URL, and expect refusal
State handle hijacking State Handle Hijacking, Stateful Tools Random, expiring handles stored as <user_id>:<handle>, with the user ID from the token, and legacy session IDs bound the same way Create a handle as user A, use it as user B, and expect a refusal (exit 5) and none of A’s data
Broad scopes Scope Minimization, Scope Challenge Handling Minimal scopes_supported, no wildcards or offline_access, scope in the 401, and one complete 403 challenge Call a write tool with a read-only token, and expect exit 3, status 403 and insufficient_scope
Token theft Token Theft Short-lived access tokens, refresh tokens rotated for public clients, and no tokens in URLs Redeem one refresh token twice, and expect invalid_grant the second time
Origin and local exposure Local MCP Server Compromise, Security & Endpoint Foreign Origin gets 403, and local builds use stdio, or 127.0.0.1 plus a token Test 5 below, and on a local build lsof -nP -iTCP -sTCP:LISTEN should show 127.0.0.1
Header and body mismatch Server Validation, x-mcp-header Mcp-Method and Mcp-Name match the body, with no secrets in x-mcp-header parameters Name one read-only tool in Mcp-Name and another in the body, then expect 400 and error -32020
Poisoned descriptions Tool Safety Descriptions describe only the tool, and changes are reviewed Test 3 below fails CI on any unreviewed change
Injected tool results Tools: Security Considerations Inputs validated, outputs sanitised, writes checked server-side, and separate read and write tools Seed a record with an instruction to call a write tool, read it through a real host, and check the trace for an unconfirmed write
Tampered request state Multi Round-Trip Requests requestState protected by HMAC or AEAD, bound to the user, an expiry and the originating request, with one-time actions enforced on the server Change one byte of requestState on the retry, replay it after expiry and replay it as user B, then expect each to fail
Duplicate writes on retry Changelog (SEP-2575) Every write tool takes an idempotency key or checks for completed work Cut the connection during a write tools/call, retry it, and check the effect happened once
Shared cache leakage Changelog (SEP-2549) Per-user lists and resources marked "private", and permissions checked on every call Fetch tools/list as two users with different permissions through the shared gateway, and expect different lists and no cross-user hits
Audit trail Logging: Security, _meta sub, client_id, tool, scopes, decision and traceparent per call, with no tokens, secrets or PII Send a request with a known traceparent and find it, then search logs for Bearer

How to test your server yourself

These commands cover the security rows. Our guide on how to test an MCP server gives the full test plan, from the Inspector to CI.

The MCP Inspector is the project’s reference testing tool. Version 2.11.0 (npm, 14 October 2026) needs Node 22.19.0 or newer. Its CLI exits 3 when the server answers 401 or 403, and 5 on a tool error. Its default protocol era is legacy, so pass --protocol-era modern to pin 2026-07-28, and --stored-auth-only so a run fails instead of opening a browser. Run it where no Inspector login for that URL is stored.

mi() { npx -y @modelcontextprotocol/inspector@2.11.0 --cli "$@"; }
MCP_URL=https://mcp.example.com/mcp
FLAGS=(--transport http --protocol-era modern --stored-auth-only)

# 1. No token: exit 3 unless you deliberately allow anonymous listing
mi "$MCP_URL" "${FLAGS[@]}" --method tools/list; echo "exit $?"

# 2. A valid token minted for a different API: expect exit 3 again
mi "$MCP_URL" "${FLAGS[@]}" --header "Authorization: Bearer $OTHER_API_TOKEN" \
  --method tools/list; echo "exit $?"

# 3. Commit a snapshot of tool definitions; CI fails when it drifts
mi "$MCP_URL" "${FLAGS[@]}" --header "Authorization: Bearer $TEST_TOKEN" \
  --method tools/list --format json \
  | jq -S '[.result.tools[] | {name, description, inputSchema, annotations}] | sort_by(.name)' \
  > tools.snapshot.json
git diff --exit-code tools.snapshot.json

# 4. Resource and authorization server metadata
curl -s https://mcp.example.com/.well-known/oauth-protected-resource/mcp \
  | jq '{resource, authorization_servers, scopes_supported}'
curl -s https://auth.example.com/.well-known/oauth-authorization-server \
  | jq '{issuer, code_challenge_methods_supported, authorization_response_iss_parameter_supported, client_id_metadata_document_supported, registration_endpoint}'

# 5. A foreign Origin should get 403
curl -s -o /dev/null -w '%{http_code}\n' -X POST "$MCP_URL" \
  -H 'Origin: https://evil.example' -H 'Content-Type: application/json' \
  -H 'Accept: application/json, text/event-stream' \
  -H 'MCP-Protocol-Version: 2026-07-28' -H 'Mcp-Method: tools/list' \
  -H "Authorization: Bearer $TEST_TOKEN" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientCapabilities":{}}}}'

For the state handle and scope rows, run --method tools/call with --tool-name, --tool-args-json and each user’s token. Appending 2>&1 | tail -1 | jq .error shows the failure class and HTTP status. Without an OS keychain, the Inspector stores the secrets it acquires in ~/.mcp-inspector/secrets.json, unencrypted unless you supply a key, so use short-lived test tokens.

Our view

Of the five rules, token audience is the one to check first, because it’s the cheapest to test and the most damaging to get wrong. One request with a token issued for another API tells you whether your server checks aud. If it doesn’t, every other control sits on a server that will act for whoever holds any token from your authorization server.

Second, stop treating tool descriptions as documentation. They’re instructions to a model, so review a change to one the way you’d review a change to a permission.

What this checklist won’t catch

  • A passing row shows that one control held for the inputs you tried. It can’t show that no other flaw exists, and no test catches every injected instruction.
  • The host decides on confirmation prompts, its own SSRF checks and whether to trust your annotations.
  • The spec says MCP “cannot enforce these security principles at the protocol level”, and authorization server internals are “beyond the scope of this specification”.
  • This list is dated to 2026-07-28. The roadmap’s priorities for the next release include DPoP and agent identity (SEP-1933, ID-JAG and RFC 8693 token exchange).
  • Neither study measured your server.
  • It covers the protocol layer only. For what a penetration test or a red team adds, see our comparison of AI red teaming and penetration testing.

If you’d rather we did this

Our MCP security audit applies this checklist to your server in ten working days, for USD 10,000. If you’re building one, see MCP server development.

Sources

All pages opened on 14 October 2026, except the Multi Round-Trip Requests page and the Agentic AI Foundation post, opened on 19 October 2026. MCP pages belong to revision 2026-07-28 (published 28 July 2026) unless stated.