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. Three rules check that each tool carries them: a title, 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
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. 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:
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.
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
That’s it. Four changes took the server from 122/151 to 142/151. If your server does paginate, validate the cursor where you decode it and raise the same error when decoding fails.
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:
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: 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 Origin header 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 listChanged on a static catalog.
  • Use mcpscore in CI so the score cannot drop without someone noticing.
  • Methodology explains how points add up to the score.