MCP security · Free tool
MCP Server Self-Check
How exposed is that MCP server? Work through a short guided check of one server or an agent's whole tool set: reachability, the Rule of Two, and drift. You get two independent verdicts and a plain-text report to paste into a ticket. Free, no sign-up, and your answers never leave your browser.
CyberDesserts tools
MCP server self-check
A short guided check, two independent verdicts, one copyable report. No single number: reachability and the Rule of Two are different findings and stay separate.
This check runs entirely in your browser. Nothing you enter is sent anywhere, stored, or logged. The check probes nothing: you run the commands, the page only does arithmetic on your answers.
Faint outline: assumed worst until answered
How this works
Questions appear as you answer, in four short parts. Nothing is computed until you have answered everything that applies.
- Scope. Say whether you are checking one server or an agent's whole tool set. This changes what the verdict means, so it comes first.
- Part 1, reachability. Who can reach this server, and what do they get without a credential? Each question comes with the command that answers it, and a tag saying where to run it: your own machine, the server host, or outside the network. You run the commands, then report what came back. This page never probes anything itself.
- Part 2, the Rule of Two. Could a poisoned input make the agent misuse its tools? No commands and nothing to look up: you tick what your tools can do from plain lists, and the page works out the risk properties for you. The idea gets a one-minute explanation before any ticking starts.
- Part 3, drift, then the report. Whether you would notice the server changing under you. Then two verdicts, computed separately and never combined into one number, and a plain-text report to paste into a ticket.
Are you checking one MCP server, or everything one agent is connected to?
The Rule of Two applies to a session, not to a server. An agent connected to three servers can hold all three properties across them while no single server holds more than one.
Confirm you have the right address
What this step does: establishes the exact address this server answers on, before anything is tested against it. MCP endpoints are fussy about the path: /mcp and /mcp/ are different addresses, and a server that answers one may redirect the other, or return nothing at all. Probe the wrong form and every later check reads as a false negative. Every later command inherits this answer, so it comes first.
Run from your own machine
# Try the address from your client config, then the trailing-slash variant
curl -sS -o /dev/null -w '%{http_code} -> %{redirect_url}\n' https://your-host/mcp
curl -sS -o /dev/null -w '%{http_code} -> %{redirect_url}\n' https://your-host/mcp/
What you might see
200 -> 405 -> 401 ->
A status with nothing after the arrow: that address answers directly. Answer: no redirect. Use that exact form in every command below. The number itself does not matter yet, later steps read it.
307 -> https://your-host/mcp/
Any status starting with 3 and an address after the arrow: the server wants the other form. Answer: a redirect. Write the target down, it replaces your-host/mcp in every command below.
404 -> 404 -> (both forms)
Neither form is the endpoint. Check the client configuration or the server's documentation for the real path: some servers serve /sse, /api/mcp or a custom route. Re-run this step with that path before continuing; every answer below depends on probing an address the server actually serves.
How does the client reach this server?
What this step does: establishes whether there is a network listener at all. No command needed, the answer is already written in your client configuration.
Check on the machine running your MCP client
Open the client configuration (claude_desktop_config.json, mcp.json or equivalent). A command key means stdio. A url key means HTTP.
What you might see
"github": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-github"]
}
A command key: the client launches the server as a local subprocess. Answer: stdio.
"github": {
"url": "https://mcp.example.com/mcp"
}
A url key: the client talks to the server over the network. Answer: Streamable HTTP.
"github": {
"url": "https://mcp.example.com/sse"
}
A url ending in /sse, or docs that describe separate SSE and message endpoints. Answer: legacy HTTP+SSE.
What is the listener bound to?
What this step does: shows which network interfaces the listener accepts connections from. Loopback means only this host can talk to it; every interface means anything that can route to the host can.
Run on the server host itself
# Linux ss -tlnp | grep -E ':(3000|8000|8080|8443)' # macOS lsof -nP -iTCP -sTCP:LISTEN | grep -iE 'node|python|mcp'
What you might see
LISTEN 0 4096 127.0.0.1:8000 users:(("python",pid=812,fd=6))
The address starts 127.0.0.1. Only programs on this machine can connect. Answer: loopback only.
LISTEN 0 4096 0.0.0.0:8000 users:(("node",pid=291,fd=22))
The address starts 0.0.0.0 or [::]. Anything that can route to this machine can connect. Answer: every interface.
(the listener is loopback, but nginx, Caddy or a cloud load balancer forwards public traffic to it)
The binding looks contained but the internet still gets through the front door. Answer: behind a reverse proxy on a public host.
(no output at all)
The server may use a different port: re-run without the grep to see every listener, and look for your server's process name. Still unclear? Answer: not sure, and the tool assumes the worst on your behalf.
Can an outsider list the tools?
What this step does: asks the server for its tool list while carrying no credential, which is exactly what an attacker's first request looks like. A refusal here is a pass.
Run from outside the boundary, not from the host
curl -sS -D - -X POST https://your-host/mcp \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
What you might see
HTTP/2 401 www-authenticate: Bearer error="invalid_token", ...
The first line carries 401 or 403. The server said no to a stranger, which is what you want. Answer: refused. Keep this output, step 1.3b reads its headers.
HTTP/2 200
content-type: application/json
{"jsonrpc":"2.0","id":1,"result":{"tools":[{"name":"read_file", ...
A 200 and a "tools" list. The server handed its capabilities to an unauthenticated stranger. Answer: 200 with a tools array.
curl: (7) Failed to connect to your-host port 443 curl: (28) Connection timed out
Nothing answered from where you stood. Answer: connection refused or timeout. Only meaningful if you really were outside the boundary.
Is the refusal telling clients how to authenticate?
What this step does: checks whether that 401 came from a real authorisation implementation. A 401 on its own does not show the server implements the authorisation specification, it shows something said no. Both answers are already in front of you: the first in the headers step 1.3 just printed, the second from one more command.
Same position as step 1.3, outside the boundary
What you might see
www-authenticate: Bearer error="invalid_token", resource_metadata="https://your-host/.well-known/oauth-protected-resource/mcp"
The header names a resource_metadata address. The server is telling clients exactly how to authenticate. First question: yes.
www-authenticate: Bearer realm="mcp" (or no www-authenticate line at all)
A refusal with no pointer. First question: no, then the second command below decides what that means.
Which specification era is the server in?
What this step does: establishes which protocol revision the server actually speaks, because later verdicts depend on it. The 2025-03-26 revision shipped OAuth 2.1 authorisation, and 2026-07-28 changed how a gate can be built.
Run from outside; add your bearer token if step 1.3 was refused
A correctly configured server rejects these at the authorisation layer before method routing, so both probes return 401. If that happens, re-run with -H 'Authorization: Bearer YOUR_TOKEN', or answer "authorisation blocked both probes" and the report will record the version as unknown rather than as a finding.
# Probe 1, legacy handshake. Propose the NEWEST version you know, not an old one.
curl -sS -X POST https://your-host/mcp \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{
"protocolVersion":"2025-11-25","capabilities":{},
"clientInfo":{"name":"self-check","version":"1.0"}}}'
# Probe 2, modern discovery
curl -sS -X POST https://your-host/mcp \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-H 'MCP-Protocol-Version: 2026-07-28' \
-d '{"jsonrpc":"2.0","id":1,"method":"server/discover"}'
What you might see, and how to read the pair
{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2025-06-18", ...}}
A probe that answers with a protocolVersion. Note the value: that is the negotiated revision for the next question.
{"jsonrpc":"2.0","id":1,"error":{"code":-32601,"message":"Method not found"}}
A probe the server does not recognise. This is a signal, not a failure: which probe got it decides the era.
Probe 1 answers, probe 2 method not found = Legacy Probe 1 and probe 2 both answer = Dual era Probe 1 method not found, probe 2 answers = Modern
The reading rule for the pair.
HTTP/2 401 (both probes)
Authorisation answered before the protocol could. Answer: authorisation blocked both probes, or re-run both with your bearer token.
initialize response is negotiated, not declared: it reflects what the client proposed. Propose old and you measure old. Propose the newest revision you know of and read down: if the server answers with something older, that is its actual ceiling.An agent should hold at most two of these three properties at once
Published by Meta on 31 October 2025 as the Agents Rule of Two. The three properties are the ingredients of a prompt injection attack. Hold any two and the attack is missing a step: content can be read but nothing sensitive is in reach, or sensitive things are in reach but nothing gets out. Hold all three in one session and a single poisoned input can read your systems and send what it finds out of the building.
Untrustworthy input
A tool reads content an outsider can influence: the attacker's way in.
Not yet answeredSensitive systems
A tool can reach something where unintended access would matter: what the attacker is after.
Not yet answeredState change or outward communication
A tool can alter something or send something out: the attacker's way to act or exfiltrate.
Not yet answeredYou do not need to work out the properties yourself. The tick lists below ask what your tools can do in plain terms, and the page derives the properties from your ticks. The cards above fill in as you go.
Tick anything any tool in scope can do
Capability, not intent: tick it if a tool could do it, even if you never ask it to. The page works out the three properties from your ticks, and the cards above fill in as you go. If nothing in a group applies, tick "none of these" so the check knows the group is answered.
What stands between a poisoned input and those tools?
One hard line here. The first four are mechanisms that work whether or not the model is fooled, and each one takes a property out of the attacker's reach. The fifth is software or instructions trying to spot the attack, which fails exactly when the attack is good. Tick what is actually in place.
These move the verdict
This does not move the verdict
Or
Do these calls run without a person watching?
This step checks whether a person is part of the loop when these calls run. Counts as unattended: scheduled jobs, CI triggers, background agents, or a client where you have selected approve-all or "don't ask again" for these tools. That last one is why this is a separate question: a permanently dismissed approval prompt is not a gate, and nothing in the server config records that it was dismissed.
Where did this server come from?
What this step does: establishes who controls when this server changes, and how much warning you get. A server you approved once can ship new tools, new descriptions or new scopes in its next release. Where it comes from decides whether anyone tells you first, and whether you could audit or rebuild what you are actually running.
Do you have a recorded tools/list response you could diff against today's?
What this step does: checks whether you could tell that this server changed. tools/list is the server's own inventory of its tools, their descriptions and their parameters; it is exactly what your agent sees and trusts. An already-approved server can change that inventory without prompting anyone for re-approval. A saved copy from the day you approved it is the baseline, and a diff against today's response is how drift shows up.
Report and hardening plan
Want the reasoning behind each question?
The companion article covers MCP security best practices in depth: transport and authentication, tool poisoning, why the Rule of Two matters, and how to catch a server drifting under you.
Read MCP security best practices on the blog →