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. Three rules check that each tool carries them: atitle, at
least one behavior annotation, and a description on every input property.
Add them the same way you added the server’s metadata, on the decorator and
the parameter:
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.
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 enforced too: 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.
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 rule does not apply 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 nine points
The finished server still fails three kinds of rule. Each one is either fixed by deploying or is a choice you can make on purpose.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: 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 property. - Validate the
Originheader and answer a foreign one with HTTP 403. - Reject cursors you never issued with JSON-RPC
-32602.
What’s next
- 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.