Skip to main content
Most of the points MCP servers lose, they lose on the same dozen rules. The table ranks them by how many real servers fail each one. Start at the top of the list: every fix below is a few lines of code, and most are worth points on almost every server.
The rows are ordered by how many servers fail the rule. “Fails on” is the share of servers the rule applied to. A rule is one check, identified by its 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
Install the SDK, start the server, then audit it from a second terminal:
Every failed rule prints a 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
Restart the server and audit it again:
Eight points. An icon passes only when its 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: a title, 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
The maximum grows by 2: two rules that judge annotations apply only once a tool declares them, and the server passes both. Annotations must describe what the tool does. Set 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 the Origin 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
Check it with a request that carries a foreign origin:
The audit agrees, for both headers:
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.
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
That’s it. Four changes took the server from 140/175 to 165/177. If your server does paginate, validate the cursor where you decode it and raise the same error when decoding fails.
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:
The official SDKs do this already. Servers that fail it usually parse the body in their own web framework and return that framework’s error page. Put the parse inside a handler that returns the object above.

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: compare max_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 Origin header and answer a foreign one with HTTP 403; list your hosts so a foreign Host is refused too.
  • Reject cursors you never issued with JSON-RPC -32602.

What’s next