JULY 21, 2026 · MCP · SECURITY · CHECKLIST

MCP Security Checklist 2026: 12 Checks Before You Connect a Remote Server

Scope of this checklist. Every protocol statement below was verified on 21 July 2026 against the Model Context Protocol specification, revision 2025-11-25. MCP uses date-based version identifiers, and new revisions supersede old ones — 2025-11-25 was the current revision on the date of writing and will not remain so indefinitely. Check the specification's versioning page and re-verify before relying on any statement here.

Connecting a remote MCP server to an agent is not like adding a dependency. A dependency runs code you chose. An MCP server supplies tool descriptions that steer a model and returns content the model will read. The specification is blunt about what that means:

"Tools represent arbitrary code execution and must be treated with appropriate caution."

— MCP specification, Security and Trust & Safety

And the line that determines everything below:

"While MCP itself cannot enforce these security principles at the protocol level, implementors SHOULD …"

— MCP specification, Implementation Guidelines

MCP does not enforce its own security model. Every control here is something you implement client-side or verify before connecting. No registry, catalogue or directory — including ours — changes that.

Primary sources: the MCP security best practices (revision 2025-11-25) · NVD entry for CVE-2025-6514.

What changed by 2026

Three things differ from the mid-2025 material most MCP security writing still reflects.

The revision is 2025-11-25. Versions are YYYY-MM-DD strings marking the last backwards-incompatible change.

The official Security Best Practices document names eight attack classes: Confused Deputy · Token Passthrough · SSRF · Session Hijacking · Local MCP Server Compromise · OAuth Authorization URL Validation · stdio Transport Security in Proxy Scenarios · Scope Minimization.

Dynamic Client Registration is a fallback, not the expected path — see check 3.

The threat model in one paragraph

Your agent host is the trusted component. The MCP server is semi-trusted at best: it controls tool names, descriptions, annotations and output. Three of those four reach the model as text. The realistic adversary is not someone breaking TLS — it is a server that describes a destructive tool as harmless, or returns output containing instructions the model then follows. Add the network position (your client fetches URLs the server chooses) and the credential position (your client holds tokens the server would like to reuse), and you get the four surfaces this checklist covers: description, output, network, credential.


The 12 checks

1. Server identity and provenance

Establish who operates the server before reading a single tool description: domain ownership, a documented operator, a published security contact. Then note what this does not establish — that the code behind the domain is what the operator says it is.

Both NVD-confirmed MCP CVEs are supply-chain, not protocol, failures: CVE-2025-6514 (mcp-remote 0.0.5–0.1.15, OS command injection via a crafted authorization_endpoint, CVSS 9.6) and CVE-2025-49596 (MCP Inspector < 0.14.1, RCE from missing authentication between client and proxy, CVSS 9.4). Neither required a malicious protocol message.

2. Transport and TLS

Two standard transports: stdio and Streamable HTTP. The HTTP+SSE transport from revision 2024-11-05 is deprecated. For HTTP transports the spec is normative about DNS rebinding — servers MUST validate the Origin header on all incoming connections, and return HTTP 403 for an invalid Origin — and about local binding: bind to 127.0.0.1, not 0.0.0.0. Evaluating a remote server, you cannot verify these internally, but you can confirm it offers no plaintext endpoint and rejects a wrong Origin.

3. Authentication and authorization

These are two different questions and the checklist keeps them apart. Authorization is OPTIONAL in MCP overall; HTTP transports SHOULD conform, stdio SHOULD NOT (use environment credentials).

3a — Protected Resource Metadata (the resource server's own document). The server MUST implement RFC 9728 Protected Resource Metadata, and the client MUST use it to discover which authorization server to talk to. This is a statement by the resource about itself. For a worked example, see our RFC 9728 protected-resource metadata.

3b — Authorization Server discovery (a separate document, a separate party). The authorization server MUST provide RFC 8414 metadata or OpenID Connect Discovery 1.0, and clients MUST support both. Fetching 3b is not verifying 3a: one tells you where to go, the other tells you what that destination supports. Verify both, and confirm the authorization server named in 3a is one you actually trust.

3c — Client registration, in priority order. This is the biggest 2026 change:

PriorityMechanismNormative status
1Pre-registration (out-of-band)Preferred where available
2OAuth Client ID Metadata DocumentsSHOULD — the preferred discoverable path
3RFC 7591 Dynamic Client RegistrationMAY only — "included for backwards compatibility with earlier versions of the MCP authorization spec"

A 2026 checklist requiring DCR support is testing for a fallback and treating it as a requirement.

3d — Token handling. Clients MUST implement RFC 8707 Resource Indicators, sending the resource parameter in both authorization and token requests regardless of whether the authorization server supports it. Servers MUST validate that a token was issued for them as intended audience. Clients MUST implement PKCE with S256, MUST verify support via AS metadata, and MUST refuse to proceed if code_challenge_methods_supported is absent. Access tokens MUST NOT appear in the URI query string.

And the rule most often broken:

"MCP servers MUST NOT accept any tokens that were not explicitly issued for the MCP server."

— Security Best Practices, Token Passthrough

Servers MUST NOT use sessions for authentication.

4. Capability and tool scope

Enumerate tools/list and read every inputSchema — which MUST be a valid JSON Schema object. Ask whether the declared scope matches the advertised purpose. A "read the weather" server that also exposes a filesystem write tool has told you something. Apply scope minimisation; it is one of the eight named threat sections. See also the MCP integration docs.

5. Tool poisoning and misleading descriptions

The spec is unusually direct:

"In particular, descriptions of tool behavior such as annotations should be considered untrusted, unless obtained from a trusted server."

Tool Safety

Annotation fields and defaults live in schema.ts, and the defaults are deliberately pessimistic:

AnnotationDefaultRead as
readOnlyHintfalseassume it writes
destructiveHinttrueassume it destroys
idempotentHintfalseassume repeats compound
openWorldHinttrueassume it reaches the open internet

The schema states all of these are hints, not guaranteed faithful — including title. Treat annotations as the server's marketing copy for its own tools.

6. Prompt injection via tool output

Tool output is untrusted input that reaches the model. Map this to LLM01 Prompt Injection and LLM06 Excessive Agency in the OWASP Top 10 for LLM Applications, 2025 edition. The spec requires servers to sanitise outputs and clients to validate results. Practically: never let tool output re-enter context as a system instruction, and never let it select the next tool call unreviewed.

7. SSRF and network boundaries

When your client fetches OAuth-related URLs the server supplies, it MUST consider SSRF risk, and SHOULD block private and reserved ranges — 10/8, 172.16/12, 192.168/16, 127/8, 169.254/16, fc00::/7, fe80::/10. Clients MUST accept only http:// and https:// authorization URLs, MUST reject javascript:, data:, file: and vbscript:, and MUST NOT use shell commands to open URLs — precisely the CVE-2025-6514 failure mode.

8. Secrets and credential handoff

Revision 2025-11-25 adds elicitation with a hard rule: servers MUST NOT use form-mode elicitation to request passwords, API keys, access tokens or payment credentials — URL mode MUST be used. Clients MUST NOT auto-prefetch an elicitation URL, MUST NOT open it without explicit consent, and MUST display the full URL. The spec documents the resulting phishing chain: an attacker triggers elicitation, the victim completes a third-party OAuth flow, and tokens bind to the attacker's identity.

9. Human approval for irreversible actions

"Hosts must obtain explicit user consent before invoking any tool."

Tool Safety

There SHOULD always be a human able to deny a tool invocation, and likewise for sampling requests. Because destructiveHint defaults to true, the defensible default is: any tool not explicitly reviewed is treated as destructive and requires approval. Where those decisions are made by an agent governance policy layer rather than a person, the same rule applies to the layer.

10. Logging, receipts and auditability

The spec asks clients to log tool usage. For any tool that moves money or mutates external state, a log line is the weakest useful artefact — bind request, response and a verifiable timestamp together. The honest limit: a log you write yourself proves what your client did, not what the server did.

11. Versioning and supply chain

MCP-Protocol-Version MUST be sent on all post-initialisation HTTP requests. If missing, the server SHOULD assume 2025-03-26; an invalid value MUST return 400. That fallback is a quiet hazard — an omitted header silently downgrades you to a March 2025 revision.

Compare the revision a server declares against the current one, and pin dependency versions: both 2026 MCP CVEs were version-bounded.

12. Revocation, kill switch and incident response

Know how you disconnect. Terminated sessions MUST return HTTP 404, after which the client MUST re-initialise; clients SHOULD send HTTP DELETE to end a session. Confirm the operator publishes a security contact before you need it — ours is set out in our responsible disclosure policy. For the organisational layer, CISA's Careful Adoption of Agentic AI Services (1 May 2026, with ASD's ACSC and other partners) is the current agency guidance.


Decision table: Allow / Review / Block

SignalAllowReviewBlock
TransportStreamable HTTP over TLSstdio via local proxyplaintext HTTP; deprecated HTTP+SSE only
Resource metadata (3a)RFC 9728 document present and correctpresent but incompleteabsent
AS discovery (3b)RFC 8414 or OIDC Discovery available and trustedreachable, operator unfamiliarAS named that you do not trust
Registration (3c)pre-registration or Client ID Metadata DocumentDCR-onlyno registration path at all
Token handling (3d)PKCE S256 + resource param + audience validationAPI key over TLS, scopedtoken passthrough accepted; token in query string
Protocol versioncurrent revision declaredone revision behindheader absent → silent 2025-03-26 fallback
Tool annotationspresent and matching observed behaviour under testpresent, untestedcontradicting observed behaviour
Tool scopeminimal, matches purposebroad but documentedundocumented destructive tools
ElicitationURL mode for credentialsform mode, non-sensitive fieldsform mode requesting secrets
Output handlingsanitised, never re-injected as instructionreviewed per calloutput selects next tool unreviewed
Operatornamed, security contact publishednamed, no contactanonymous

Rule: one Block hit blocks. Review hits do not accumulate into Allow.


A reproducible test: what discovery metadata proves

Discovery documents are self-declarations. A registry can publish a manifest that is internally plausible and still worthless — most often because more than one process writes it, so the document you fetch depends on when you fetch it.

The test below is a constructed teaching example, not a report of any observed incident. It runs entirely offline against two local fixture files, so you can reproduce it in thirty seconds and then point the same logic at a real endpoint you are evaluating.

fixture_a.json — one poll:

{
  "manifest_version": "1.2",
  "generated_at": "2026-03-01T12:00:00Z",
  "summary": { "servers_online": 42, "tools_available": 512, "categories": 6 },
  "servers": []
}

fixture_b.json — the next poll, two minutes later:

{
  "manifest_version": "1.2",
  "generated_at": "2026-03-01T12:02:00+00:00",
  "summary": { "total_servers": 0, "servers_online": 0, "total_tools": 0, "categories": {} },
  "servers": []
}

fixture_c.json — a third poll, used below to show the middle case:

{
  "manifest_version": "1.2",
  "generated_at": "2026-03-01T12:04:00Z",
  "summary": { "servers_online": 39, "tools_available": 512, "categories": 6 },
  "servers": []
}

check_manifest.py — compares four dimensions and returns a verdict:

#!/usr/bin/env python3
"""Compare two polls of a discovery manifest. Offline, stdlib only."""
import hashlib, json, re, sys

ISO_Z = re.compile(r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$")

def load(p):
    raw = open(p, "rb").read()
    return raw, hashlib.sha256(raw).hexdigest(), json.loads(raw)

def keyset(o, prefix=""):
    out = set()
    if isinstance(o, dict):
        for k, v in o.items():
            out.add(prefix + k)
            out |= keyset(v, prefix + k + ".")
    return out

def ts_format(s):
    return "Z" if ISO_Z.match(s or "") else "offset" if s else "missing"

a_raw, a_sha, a = load(sys.argv[1])
b_raw, b_sha, b = load(sys.argv[2])

digest_same = a_sha == b_sha
ka, kb = keyset(a), keyset(b)
schema_same = ka == kb
ts_same = ts_format(a.get("generated_at")) == ts_format(b.get("generated_at"))

online_a = a.get("summary", {}).get("servers_online")
online_b = b.get("summary", {}).get("servers_online")

print(f"digest      : {a_sha[:12]} vs {b_sha[:12]}  -> {'same' if digest_same else 'DIFFER'}")
print(f"schema keys : {'same' if schema_same else 'DIFFER'}")
if not schema_same:
    print(f"  only in A : {sorted(ka - kb)}")
    print(f"  only in B : {sorted(kb - ka)}")
print(f"timestamp   : {ts_format(a.get('generated_at'))} vs "
      f"{ts_format(b.get('generated_at'))} -> {'same' if ts_same else 'DIFFER'}")
print(f"servers_online: {online_a} vs {online_b}")

if digest_same:
    verdict = "ALLOW   - stable across polls"
elif not schema_same or not ts_same:
    verdict = ("BLOCK   - field names and/or timestamp format change between polls: "
               "more than one writer, no single canonical contract")
elif online_a != online_b:
    verdict = "REVIEW  - values move between polls; establish the refresh model before relying on them"
else:
    verdict = "REVIEW  - content changed without a value change"
print("\nVERDICT:", verdict)

Run it:

$ python3 check_manifest.py fixture_a.json fixture_b.json
digest      : d8c81b1856bb vs 935eac6f8249  -> DIFFER
schema keys : DIFFER
  only in A : ['summary.tools_available']
  only in B : ['summary.total_servers', 'summary.total_tools']
timestamp   : Z vs offset -> DIFFER
servers_online: 42 vs 0

VERDICT: BLOCK   - field names and/or timestamp format change between polls:
         more than one writer, no single canonical contract

A third fixture shows the middle case — same schema, same timestamp format, only the value moved (42 → 39):

$ python3 check_manifest.py fixture_a.json fixture_c.json
digest      : d8c81b1856bb vs 7538468e45a8  -> DIFFER
schema keys : same
timestamp   : Z vs Z -> same
servers_online: 42 vs 39

VERDICT: REVIEW  - values move between polls; establish the refresh model before
         relying on them

And comparing a file with itself returns ALLOW - stable across polls. All three verdict paths are reachable, which is worth confirming before you trust any check script.

Why this is a Block and not a Review. A value moving from 42 to 0 could be a real outage. But the field names changed too (tools_availabletotal_tools), and so did the timestamp format (Z+00:00). Values change because the world changes; schemas and timestamp formats change because the code that wrote them changed. Two shapes alternating across polls means two writers and no single canonical contract — and a consumer polling on its own schedule will silently get one or the other.

Point the same four checks at any registry you are evaluating: fetch twice a few minutes apart, diff digest, key set, timestamp format and values. It costs a minute and tells you whether the numbers you are about to trust have an owner.

What discovery metadata proves: that a document existed at a URL at a moment in time, and what its publisher chose to assert.
What it does not prove: that the numbers are current, internally consistent, or produced by the system they describe.

Six words people use interchangeably, and shouldn't

TermWhat it establishesWhat it does not
Registeredan entry exists in a cataloguethat anything is running
Publicly reachablean endpoint answered a requestthat it authenticated you, or served tools
Authenticatedthe server established who you arethat you are permitted to do anything
Authorizeda token grants specific scopes for this resourcethat the scopes are minimal or correctly enforced
Tool-servingtools/list returned toolsthat the tools work, or are safe
Trustworthya judgement, always scoped and datedanything transferable to another party

Authentication and authorization are the pair most often collapsed. A server can authenticate you perfectly and still hand you a token with far broader scopes than the task needs — which is what scope minimisation, one of the eight named threats, is about.

Most registry claims live in rows one and two while being read as row six. That applies to the capability catalogue we publish as much as to anyone else's.


Copy-paste checklist

[ ]  1  Operator identified; security contact published
[ ]  2  TLS enforced; not HTTP+SSE-only; Origin validation verified
[ ]  3a RFC 9728 protected resource metadata present
[ ]  3b Authorization server discovery (RFC 8414 or OIDC) verified separately
[ ]  3c Registration path: pre-registration or Client ID Metadata Document (DCR = fallback)
[ ]  3d PKCE S256; resource parameter sent; audience validated; no token passthrough
[ ]  4  tools/list enumerated; every inputSchema read; scope minimised
[ ]  5  Annotations treated as untrusted; destructiveHint assumed true
[ ]  6  Tool output never re-injected as instruction (OWASP LLM01 / LLM06)
[ ]  7  Private/reserved IP ranges blocked; only http/https URLs opened
[ ]  8  No credentials via form-mode elicitation; no URL auto-prefetch
[ ]  9  Human approval required for every unreviewed / destructive tool
[ ] 10  Tool calls logged with request, response and timestamp
[ ] 11  MCP-Protocol-Version sent explicitly; dependency versions pinned
[ ] 12  Disconnect path tested; incident contact known

What this proves — and what it does not prove

It proves: these checks are traceable to normative statements in MCP revision 2025-11-25 and to two NVD-published CVEs. Anyone can re-derive them from the cited sections. The fixture comparison is reproducible offline.

It does not prove: that a server passing all twelve is safe. The checks are point-in-time against a server that can change after you connect. Several can only be verified client-side — you are inferring server behaviour from its responses.

About ToolOracle specifically — the accurate statements, and only these:

ToolOracle does not enforce any control in this checklist on your behalf, does not security-review the servers it indexes, and being listed in any ToolOracle catalogue is not a safety signal. Registration is registration. Human review remains required.

Apply the checks in this article to us as well.


Sources

Primary — Model Context Protocol specification, revision 2025-11-25: Security and Trust & Safety · Security Best Practices · Authorization · Transports · Tools · Elicitation · Versioning. Primary — NVD: CVE-2025-6514, CVE-2025-49596. Secondary — OWASP Top 10 for LLM Applications, 2025 edition (LLM01, LLM06); CISA, Careful Adoption of Agentic AI Services, 1 May 2026.

Verified 2026-07-21 against MCP revision 2025-11-25. Re-verify before relying on any protocol statement here.

MCP Security Checklist OAuth RFC 9728 Prompt Injection Tool Poisoning SSRF

Registration is registration

ToolOracle indexes MCP servers and publishes discovery documents. It does not security-review what it indexes. Run the twelve checks yourself — including against us.

Open the integration docs