MCP security best practices: the 2026 checklist
MCP security is the work of stopping an agent’s connection to a Model Context Protocol server from becoming a route into your data. In practice that means four things: prove the code you run was built from the source you read, authenticate the endpoint and encrypt the connection, bind every access token to the server it was issued for, and treat everything a server sends back as untrusted text.
Two checklists follow, and you only need one of them at a time. Use the remote endpoint checklist for a server you connect to over HTTPS, and the packaged server checklist for one you download and execute locally. Every row names the check, the command that tests it, and the output that counts as a pass.
These are the checks we run. On 29 July 2026 we scored 18,689 servers from the official MCP registry: the median best score was 27 out of 100, and 123 of them, 0.7% of those scored, reached the 80-plus band we label high trust. Most published MCP servers do not pass the lists below.
Everything here is current for spec revision 2026-07-28, which is a large enough break from earlier revisions that some widely repeated MCP security advice no longer describes the protocol. If you are working from older guidance, read what changed before you act on it.
Checklist: remote MCP endpoints
A remote server is an endpoint you hand context and tool calls to. You never run its code, so the risk sits in the connection, the operator, and the authorisation model.
Checks marked critical are disqualifying rather than costly: a confirmed failure floors the whole score to zero instead of deducting from it, because a high score has to mean safe and not mostly safe. Replace HOST with your hostname and PATH with your MCP path.
| Check | How to verify | A pass looks like | Fix guide |
|---|---|---|---|
| TLS certificate is trusted and name-matched (critical) | openssl s_client -connect HOST:443 -servername HOST -verify_hostname HOST </dev/null 2>&1 | grep "Verify return code" | Verify return code: 0 (ok). Anything else is an untrusted chain or a hostname mismatch | tls |
| Certificate is not near expiry | openssl s_client -connect HOST:443 -servername HOST </dev/null 2>/dev/null | openssl x509 -noout -dates | A notAfter date comfortably in the future | tls |
| Plaintext HTTP is refused (critical) | curl -sI http://HOST/PATH | A 301 or 308 redirect to the https:// origin. Serving MCP in the clear is treated exactly like a broken certificate | https |
| HSTS is set | curl -sI https://HOST/PATH | grep -i strict-transport-security | A max-age of at least 15552000, six months | hsts |
| Protected-resource metadata is published | curl -s https://HOST/.well-known/oauth-protected-resource/PATH, then the bare /.well-known/oauth-protected-resource | JSON naming your authorization_servers and resource, per RFC 9728 | authentication |
| PKCE is mandatory | Fetch the authorisation server metadata (RFC 8414) and read code_challenge_methods_supported | Includes S256. If the field is absent, clients must refuse to proceed | authentication |
| Client registration offers CIMD, not only DCR | Read client_id_metadata_document_supported in the same metadata | Advertised as true. Dynamic Client Registration alone is deprecated as of this revision | authentication |
The authorisation server returns iss | Inspect the redirect back from the authorisation server | An iss parameter (RFC 9207) that your client compares against the recorded issuer before redeeming the code | authentication |
| Tokens are audience-bound | Mint a token for a different resource and replay it against your endpoint | The server rejects it. This is the one control you cannot test from outside | authentication |
| The domain is signed | delv HOST or dig +dnssec HOST | fully validated, or an AD flag. Neither result tells you a domain is unsigned unless your resolver validates, so check against a validating resolver | dnssec |
| Streamable HTTP, not HTTP+SSE | Check which transport your endpoint answers on | Streamable HTTP. The HTTP+SSE transport is deprecated, and stdio is not a remote transport at all | transport |
| A current protocol revision | Read the versions reported by server/discover | 2026-07-28, or 2025-11-25 for the one-revision grace period we allow implementers | protocol version |
| List caching is scoped correctly | Inspect ttlMs and cacheScope on your tools/list response | cacheScope is private whenever the tool list varies by caller. public invites shared intermediaries to cache your definitions | transport |
Audience binding deserves the extra sentence, because it only ever fails when someone presents a token issued elsewhere, and by then it has already worked. The requirement is that servers “MUST only accept tokens specifically intended for themselves and MUST reject tokens that do not include them in the audience claim or otherwise verify that they are the intended recipient of the token”, per the authorization security considerations, with the resource parameter defined by RFC 8707. The same section forbids passing a token you received onward to a third party. Our authentication fix guide walks the flow end to end.
Two notes on reading the specification itself. OAuth 2.1 is still an IETF draft rather than a published RFC, so cite it as one. And MUST and SHOULD in the spec carry their BCP 14 meanings, which is why the difference between the two is worth reading slowly.
Checklist: packaged MCP servers
A packaged server is code you download and execute locally, which makes it a dependency with execution rights on your machine. The checks move from the connection to the artifact and the repository behind it.
| Check | How to verify | A pass looks like | Fix guide |
|---|---|---|---|
| No malware verdict against the published artifact (critical) | Read the supply-chain section of the server’s VerifyMCP listing, where we aggregate vendor malware verdicts, or scan the artifact yourself | No critical verdict from any vendor. A clean result is “nobody has caught this yet”, which is why provenance matters as much | malware |
| No unpatched critical CVEs in the resolved production tree (critical) | osv-scanner scan source -r . | No critical findings that have a fixed version available. Without -r it will not descend into subdirectories | vulnerabilities |
| Signed build provenance | gh attestation verify ARTIFACT --owner OWNER for GitHub-built artifacts | A verified attestation naming the source repository and the workflow that built it | build provenance |
| Registry signatures check out | npm audit signatures | Every dependency reports a verified registry signature or attestation. It reports counts, not repository names | build provenance |
| No install-time scripts | npm view PACKAGE scripts | No preinstall, install or postinstall. This covers the top-level package only, not its dependencies | install scripts |
| A reachable, verifiable source repository | Open the repository URL from the package manifest | It resolves, and the code there is the code the artifact claims to be built from | repository |
| Dependencies are maintained, not abandoned | Check the release recency of the resolved tree | Active releases rather than packages last touched years ago | maintenance |
| An OSI-approved licence | npm view PACKAGE license | A recognised OSI identifier, not UNLICENSED or a missing field | licence |
| A published security disclosure policy | Look for SECURITY.md in the repository | A stated way to report a vulnerability privately | security policy |
| A committed lockfile | Check the repository for package-lock.json, uv.lock, or your ecosystem’s equivalent | Present and current | lockfile |
An unpatched critical CVE only counts as disqualifying when a fixed version actually exists. Where a maintainer cannot upgrade out of the problem because nobody has published a fix anywhere, it is scored as a serious negative rather than a hard zero, because the alternative punishes maintainers for someone else’s unfixed advisory.
The lockfile row looks like the weakest on the list and is closer to a precondition for the rest. A scan of a dependency tree tells you nothing durable if the next install resolves a different tree.
Provenance is worth as much as malware scanning, for a reason that is easy to miss. A clean malware verdict tells you nobody has caught this artifact yet. Provenance tells you the artifact came from the repository you just read. SLSA defines the build levels, in-toto defines the attestation format, and OSV is where the vulnerability data comes from.
MCP security risks and considerations
The checklists above are the controls. These are the attacks they exist to stop, split by where the mitigation lives, because a protocol-level risk and a supply-chain risk are not fixed in the same place.
Protocol-level risks
- Prompt injection through tool results. Tool output is text the model reads, so a compromised or hostile server can steer an agent. This is OWASP LLM01, and the spec is unambiguous about the metadata too: clients “MUST consider tool annotations to be untrusted unless they come from trusted servers”.
- Tool poisoning. Instructions hidden in a tool description rather than in its output. The OWASP MCP cheat sheet is the clearest vendor-neutral reference.
- Rug pulls. A tool definition that changes after the user approved it. If you are writing a client, pin definitions by hash.
- Tool shadowing. With several servers connected at once, one can define a tool that displaces another’s.
- Confused deputy. A proxy with a static client ID forwarding to a third-party authorisation server without per-client consent.
- Token passthrough. Accepting a token issued for someone else, or forwarding yours onward. The spec forbids both.
- Server-side request forgery. Servers that fetch URLs on request will reach cloud metadata endpoints and private ranges unless you block them. The spec’s SSRF guidance lists the ranges, and the OWASP SSRF cheat sheet covers the technique.
Supply-chain risks
- Malware in the published artifact, which is not always the same code as the repository.
- Unpatched vulnerabilities in the resolved production dependency tree.
- Install-time scripts, which execute before you have run anything deliberately.
- Unprovable provenance, where nothing binds the artifact you installed to the source you reviewed.
What changed in 2026-07-28, and why older advice misses
The current revision moved enough that guidance written for earlier revisions can point you at the wrong control. These are the changes that alter the checklists.
Protocol sessions are gone. The changelog removes protocol-level sessions and the Mcp-Session-Id header. Servers needing state across calls now mint explicit handles and pass them as ordinary tool arguments. “Session hijacking” is therefore the wrong frame for a 2026 server, and the replacement rule is blunt: an MCP server must not treat possession of a state handle as authentication. A handle names some state. It is not proof of who is asking.
Dynamic Client Registration is deprecated. RFC 7591 registration survives only for backwards compatibility, replaced by Client ID Metadata Documents, where your client ID is an HTTPS URL the authorisation server fetches on demand. CIMD is the better default and it is not free: the authorisation server now fetches a URL the client controls, which makes the authorisation server itself an SSRF target, and it cannot on its own stop localhost redirect-URI impersonation.
Issuer validation is explicit. Authorisation servers should return an iss parameter, and clients must validate any iss they receive against the recorded issuer before redeeming the authorisation code. This is the defence against mix-up attacks, where a client is tricked into redeeming a code at the wrong authorisation server.
List results are cacheable. tools/list, prompts/list and the resource listings now carry ttlMs and cacheScope. A cacheScope of public means shared intermediaries may cache your tool definitions, which is a new and largely undiscussed path to both a rug pull, where the definition an agent acts on is not the one you published, and to tenant bleed on multi-tenant deployments. If your tool list varies by caller, it is not public.
Streams are no longer resumable. With Last-Event-ID removed, a broken response stream loses the in-flight request and the client must re-issue it under a new request ID. Any tool with side effects now needs to be idempotent, or flaky networks will give you duplicate writes.
There are two new headers to defend with. Streamable HTTP POSTs carry Mcp-Method and Mcp-Name. Gateways, firewalls and rate limiters can route and meter on those without parsing the JSON body, which is the cheapest access control most deployments are not yet using.
What the registry actually looks like
Figures from our scan of the official MCP registry on 29 July 2026, covering 18,689 scored servers out of 19,048 listed. Live numbers are on the statistics page.
| Band | Servers | Share of scored |
|---|---|---|
| High trust, 80 and above | 123 | 0.7% |
| Medium, 60 to 79 | 2,806 | 15.0% |
| Low, below 60 | 15,760 | 84.3% |
Protocol adoption is the sharper finding, and it is the reason the deprecations above are worth acting on early rather than late. Of 8,801 components whose declared version we actually observed, 5,950 were on 2025-11-25 and 1 had moved to 2026-07-28 the day after it shipped. Version adoption in this ecosystem is slow.
Two caveats on those counts, because they are easy to misread. Protocol versions are a property of an endpoint, so they count components rather than servers, and a component whose version we never observed, because it was unreachable, auth-gated, or never probed, contributes to neither side of the ratio. Separately, 352 listed servers carry no component at all, so they can never be scored and sit outside every percentage above.
Where to start
If you maintain a server, the fastest wins are the ones that cost least and count most: publish build provenance, declare a source repository, and make sure your TLS certificate and HTTPS redirect are clean. Find your server in the directory, where every finding links to the guide that fixes it.
If you are choosing a server, run the checks above against the two or three you are considering, and read how we score so you know what a number does and does not tell you. For the threat model underneath all of this, see why MCP server security is hard and the risks that matter.