rule_id; look any of them up in the rules reference.
Measured on 13,547 public MCP servers that mcpscore 1.20.0 audited in full
on 2026-09-20 and 2026-09-21.
Start from a real server
The fixes below are applied to one small server, built with the MCP Python SDK 2.x and served over Streamable HTTP. Each step adds one change and re-audits it, so you can see what every change is worth. The code is Python; each step also names the protocol field the rule reads, which any SDK can set.server.py
Fix: line under it. The four steps below turn
most of them into passes.
Step 1: Describe your server
Clients show your server by what it sends when a session starts.serverInfo carries a
title for people, a version for bug reports, a websiteUrl, and
icons. Next to it, the top-level instructions field tells the model
when to use the server. Set all five where you create the server:
server.py
src starts with https:// or
data:, so a relative path or a plain http:// URL still fails
server_icons_present. websiteUrl and icons are judged on servers that
negotiate 2025-11-25 or later; on older revisions they are skipped and cost
nothing.
Step 2: Describe your tools
A model picks a tool from its description, its input schema and its annotations. Rules check that each tool carries them: atitle, at
least one behavior annotation, and a description on every input property.
The tool’s result is described the same way: the SDK turns -> list[str]
into an output schema with one property, result, and that property needs a
description too. Add them the same way you added the server’s metadata, on
the decorator, the parameter and the return type:
server.py
readOnlyHint to true
only when the tool cannot change anything: clients use it to decide whether
to ask the user before a call, and smoke mode calls only
read-only tools by default.
Step 3: Validate the Origin header
A browser page can reach a server on the user’s machine or private network through DNS rebinding. The MCP spec makes servers check theOrigin header
and, from revision 2025-11-25, answer a foreign one with HTTP 403. mcpscore
sends one request without Origin, which must succeed, and one with
Origin: https://mcpscore.invalid, which must be refused. On a server it
reaches at a loopback address such as 127.0.0.1, it also sends a foreign
Host header, which must be refused too.
The Python SDK enables this check on its own only when the server binds to
127.0.0.1 or localhost. A deployed server binds to 0.0.0.0, so you
turn it on and list your hosts and origins:
server.py
allowed_hosts is what refuses the foreign Host: a request whose Host
header is not in the list gets HTTP 421. Add every hostname your server is
reached by, including the one behind your load balancer. Requests without an
Origin header, which is what non-browser MCP clients send, pass the Origin
check; the Host check still applies to them.
For how a DNS-rebinding attack reaches a server and what the check does and
doesn’t protect, read
DNS rebinding and MCP.
Technical details
Technical details
The requirement is Transports §Security Warning in revisions
2025-03-26,
2025-06-18
and 2025-11-25,
and Streamable HTTP §Security & Endpoint in
2026-07-28.
Before 2025-11-25 any 4xx refusal passes; from 2025-11-25 the status must
be 403. mcpscore judges the foreign-origin request only after the control
request without
Origin was accepted, so a server that refuses everyone
gets no credit for refusing the probe. The Host rule,
security_host_header_validation, is judged the same way, and only on a
loopback URL: behind virtual hosting, a public endpoint routes a foreign
Host elsewhere instead of validating it. Any 4xx except 408 and 429
counts as a refusal; those two leave the rule unjudged.
Neither rule applies to stdio servers.Step 4: Reject cursors you never issued
List requests (tools/list, resources/list, resources/templates/list,
prompts/list) take an optional cursor for the next page. A cursor your
server never issued should be answered with JSON-RPC error -32602 (Invalid
params), per
Pagination §Error Handling. mcpscore sends a made-up cursor to each list your server declares
and expects that error back. Almost every server returns the first page
instead.
A server that returns each list in one page never issues a cursor, so any
cursor it receives is invalid. One middleware covers all four lists:
server.py
The finished server.py
The finished server.py
server.py
The remaining twelve points
The finished server still fails five kinds of rule. Each one is fixed by deploying, is a choice you can make on purpose, or sits in the SDK.Other fixes from the ranking
List-change notifications
capability_tools_list_changed, and its prompts and resources twins,
pass when the server declares listChanged: true for that
capability. The
declaration promises that the server sends a
notifications/.../list_changed
message whenever the list changes. If your catalog never changes after
startup, leave the declaration out and accept the one point per list.
Declaring it without sending the notifications makes clients cache a list
that goes stale.
Latest protocol revision
protocol_version_latest passes when the server speaks the newest MCP
revision, 2026-07-28, either on the negotiated connection or through the
2026-07-28 stateless lifecycle
next to the older handshake. The fix is an
SDK upgrade. The example above passes it on MCP Python SDK 2.2.0 without any
code for it. Keep the older handshake while your users’ clients still need
it.
Malformed requests
security_malformed_request_handling sends an HTTP body that is not valid
JSON. The server must answer with a JSON-RPC
parse error whose id is
null:
When a fix does not move the score
Symptom: the rule now passes, and the score line did not change. Cause: a rule that was skipped before, or is not applicable on your revision, adds to neither the score nor the maximum. Fix: comparemax_score too. The score is score/max_score, and both
numbers change when a rule starts applying.
Symptom: after step 3 the audit stops with HTTP error 421 from server
and Error connecting to the MCP server, exit code 2.
Cause: the host you audit through is not in allowed_hosts, so the SDK
refuses every request, with or without Origin.
Fix: add that hostname to allowed_hosts, with :* to allow any port.
Symptom: security_local_bind_loopback is skipped with
insufficient-data on a server bound to 127.0.0.1.
Cause: a firewall on the auditing machine drops connections to closed
ports instead of refusing them, such as macOS stealth mode, so mcpscore
cannot prove the port is closed on the network address.
Fix: none needed; a skipped rule costs nothing. The rule is judged on a
machine whose firewall refuses the connection.
Symptom: server_icons_present fails with Number of server icons whose src lacks an https:// or data: prefix: 1.
Cause: the icon src is relative or uses http://.
Fix: use an absolute https:// URL or a data: URI.
Recap
- Describe your server:
title,version,instructions,websiteUrl,icons. - Describe your tools: a
title, behavior annotations, and a description on every input and output property. - Validate the
Originheader and answer a foreign one with HTTP 403; list your hosts so a foreignHostis refused too. - Reject cursors you never issued with JSON-RPC
-32602.
What’s next
- Use from your coding agent to have Claude Code or Codex apply these fixes to your server and re-audit it.
- Configure rules to turn off a rule that does not fit
your server, such as
listChangedon a static catalog. - Use mcpscore in CI so the score cannot drop without someone noticing.
- Methodology explains how points add up to the score.