MCP compliance rules
79 rules against the 2026-07-28 specification. Each links to the exact spec sentence it comes from — see the migration guide for the full picture of what changed.
DSC — Discovery and capabilities
| Rule | What we check | Level | If it fails | Spec-traced |
|---|---|---|---|---|
| MCP-DSC-001 | server/discover is implemented | MUST | fail | yes |
| MCP-DSC-002 | DiscoverResult advertises supported protocol versions | MUST | fail | yes |
| MCP-DSC-003 | Advertised versions include a currently-supported revision | MAY | info | no — best practice |
| MCP-DSC-004 | Extension identifiers use a valid non-reserved reverse-DNS prefix | MUST | fail | yes |
| MCP-DSC-005 | Results carry io.modelcontextprotocol/serverInfo in _meta | SHOULD | warn | yes |
HDR — Request metadata headers
| Rule | What we check | Level | If it fails | Spec-traced |
|---|---|---|---|---|
| MCP-HDR-001 | Missing Mcp-Method rejected with 400 + HeaderMismatch | MUST | fail | yes |
| MCP-HDR-002 | Missing Mcp-Name on tools/call rejected with 400 + HeaderMismatch | MUST | fail | yes |
| MCP-HDR-003 | Mcp-Name header must match the request body | MUST | fail | yes |
| MCP-HDR-004 | Missing MCP-Protocol-Version rejected (modern-only servers) | MUST | fail | yes |
| MCP-HDR-005 | MCP-Protocol-Version header must match _meta.protocolVersion | MUST | fail | yes |
| MCP-HDR-006 | Header names compared case-insensitively | MUST | fail | yes |
| MCP-HDR-007 | Base64 sentinel-encoded header values decoded before comparison | MUST | fail | yes |
| MCP-HDR-008 | Mcp-Param-{Name} validated against body when x-mcp-header is declared | MUST | fail | yes |
| MCP-HDR-009 | Mcp-Param-{Name} containing invalid characters is rejected | MUST | fail | yes |
| MCP-HDR-010 | Header validation runs before method dispatch | MAY | warn | no — best practice |
STL — Statelessness and session removal
| Rule | What we check | Level | If it fails | Spec-traced |
|---|---|---|---|---|
| MCP-STL-001 | Server never mints or echoes Mcp-Session-Id | MUST_NOT | fail | yes |
| MCP-STL-002 | A client-supplied Mcp-Session-Id is ignored, not required | MUST_NOT | fail | yes |
| MCP-STL-003 | A cold modern request succeeds with no prior handshake | MUST | fail | yes |
| MCP-STL-004 | Server does not rely on prior requests over the same connection | MUST_NOT | fail | yes |
| MCP-STL-005 | GET on the MCP endpoint returns 405 | SHOULD | warn | yes |
| MCP-STL-006 | DELETE on the MCP endpoint returns 405 | SHOULD | warn | yes |
| MCP-STL-007 | Last-Event-ID is ignored — streams are not resumable | SHOULD | warn | yes |
| MCP-STL-008 | tools/list does not vary per connection | MUST | fail | yes |
RES — Result and message shape
| Rule | What we check | Level | If it fails | Spec-traced |
|---|---|---|---|---|
| MCP-RES-001 | Every result includes resultType | MUST | fail | yes |
| MCP-RES-002 | resultType is a value defined by core or an advertised extension | MUST | fail | yes |
| MCP-RES-003 | Server sends no independent JSON-RPC requests on a response stream | MUST_NOT | fail | yes |
| MCP-RES-004 | Accepted notification POST returns 202 with no body | MUST | fail | yes |
| MCP-RES-005 | Response is application/json or text/event-stream; SSE terminates with the final response | MUST | fail | yes |
| MCP-RES-006 | SSE responses set X-Accel-Buffering: no | SHOULD | warn | yes |
| MCP-RES-007 | tools/list returns a deterministic order | SHOULD | warn | yes |
ERR — Error codes
| Rule | What we check | Level | If it fails | Spec-traced |
|---|---|---|---|---|
| MCP-ERR-001 | Unsupported version returns -32022 with data.supported[] | MUST | fail | yes |
| MCP-ERR-002 | Unknown method returns HTTP 404 and JSON-RPC -32601 | MUST | fail | yes |
| MCP-ERR-003 | Resource-not-found uses -32602, not the retired -32002 | MUST_NOT | fail | yes |
| MCP-ERR-004 | The retired -32042 code is never emitted | MUST_NOT | fail | yes |
| MCP-ERR-005 | No undefined code is emitted in the reserved -32020…-32099 range | MUST_NOT | fail | yes |
| MCP-ERR-006 | Missing required _meta field returns HTTP 400 + JSON-RPC -32602 | MUST | fail | yes |
| MCP-ERR-007 | Missing client capability returns -32021 with data.requiredCapabilities | MUST | fail | yes |
| MCP-ERR-008 | New implementation-defined codes avoid the legacy -32000…-32019 range | SHOULD_NOT | warn | yes |
CAC — Caching metadata
| Rule | What we check | Level | If it fails | Spec-traced |
|---|---|---|---|---|
| MCP-CAC-001 | ttlMs present on all five cacheable results | MUST | fail | yes |
| MCP-CAC-002 | cacheScope present on all five cacheable results | MUST | fail | yes |
| MCP-CAC-003 | cacheScope is exactly "public" or "private" | MUST | fail | yes |
| MCP-CAC-004 | ttlMs is a non-negative number | MUST | fail | yes |
| MCP-CAC-005 | Authenticated or personalised results are not marked cacheScope: public | MAY | warn | yes |
SCH — Tool/prompt schema and x-mcp-header
| Rule | What we check | Level | If it fails | Spec-traced |
|---|---|---|---|---|
| MCP-SCH-001 | x-mcp-header is non-empty and matches RFC 9110 token syntax | MUST | fail | yes |
| MCP-SCH-002 | x-mcp-header contains no control characters, CR, or LF | MUST_NOT | fail | yes |
| MCP-SCH-003 | x-mcp-header values are case-insensitively unique per inputSchema | MUST | fail | yes |
| MCP-SCH-004 | x-mcp-header only on string/integer/boolean — never number | MUST | fail | yes |
| MCP-SCH-005 | x-mcp-header property is statically reachable via properties only | MUST_NOT | fail | yes |
| MCP-SCH-006 | inputSchema/outputSchema validate against the declared or default 2020-12 dialect | MUST | fail | yes |
| MCP-SCH-007 | Tool, prompt, and resource names are header-safe ASCII | SHOULD | warn | yes |
AUT — Authorization
| Rule | What we check | Level | If it fails | Spec-traced |
|---|---|---|---|---|
| MCP-AUT-001 | Unauthenticated request returns 401 with WWW-Authenticate resource_metadata | MUST | fail | yes |
| MCP-AUT-002 | RFC 9728 Protected Resource Metadata is served and valid | MUST | fail | yes |
| MCP-AUT-003 | PRM resource equals the canonical server URI | MUST | fail | yes |
| MCP-AUT-004 | Each listed authorization server exposes RFC 8414 or OIDC discovery metadata | MUST | fail | yes |
| MCP-AUT-005 | AS advertises authorization_response_iss_parameter_supported: true | SHOULD | warn | yes |
| MCP-AUT-006 | WWW-Authenticate includes a scope parameter | SHOULD | warn | yes |
| MCP-AUT-007 | Insufficient scope returns 403 with error="insufficient_scope" | SHOULD | warn | yes |
| MCP-AUT-008 | Tokens with a foreign audience are rejected | MUST | fail | yes |
| MCP-AUT-009 | offline_access absent from scopes_supported / challenge scope | SHOULD_NOT | info | yes |
| MCP-AUT-010 | AS advertises S256 in code_challenge_methods_supported | MUST | fail | yes |
| MCP-AUT-011 | AS supports Client ID Metadata Documents, not DCR alone | SHOULD | warn | yes |
SEC — Security posture
| Rule | What we check | Level | If it fails | Spec-traced |
|---|---|---|---|---|
| MCP-SEC-001 | Invalid Origin header returns HTTP 403 | MUST | fail | yes |
| MCP-SEC-002 | Endpoint is HTTPS with a valid, unexpired certificate chain | MAY | fail | yes |
| MCP-SEC-003 | Error messages do not echo request headers or credentials | MAY | warn | no — best practice |
| MCP-SEC-004 | icons[].src is https: or data:, same-origin, no unsafe scheme | MUST | fail | yes |
| MCP-SEC-005 | No $ref in tool schemas resolves to a network URI | MUST_NOT | fail | yes |
| MCP-SEC-006 | Schema depth and subschema count stay within safe bounds | SHOULD | warn | yes |
| MCP-SEC-007 | Server does not reflect the Authorization header into responses | MAY | fail | no — best practice |
DEP — Deprecated and removed features
| Rule | What we check | Level | If it fails | Spec-traced |
|---|---|---|---|---|
| MCP-DEP-001 | ping is no longer implemented | MAY | warn | yes |
| MCP-DEP-002 | logging/setLevel is no longer implemented | MAY | warn | yes |
| MCP-DEP-003 | resources/subscribe and resources/unsubscribe replaced by subscriptions/listen | MAY | warn | yes |
| MCP-DEP-004 | No legacy HTTP+SSE endpoint is served | SHOULD_NOT | warn | yes |
| MCP-DEP-005 | Roots / Sampling / Logging capabilities are not advertised | MAY | info | yes |
| MCP-DEP-006 | Server accepts initialize (dual-era support) | MAY | info | yes |
| MCP-DEP-007 | A modern-only server names its supported versions in the initialize error | SHOULD | warn | yes |
REG — Registry readiness
| Rule | What we check | Level | If it fails | Spec-traced |
|---|---|---|---|---|
| MCP-REG-001 | server.json present and valid against the registry schema | MAY | warn | yes |
| MCP-REG-002 | Namespace ownership is verifiable (GitHub or DNS TXT) | MAY | warn | yes |
| MCP-REG-003 | version is present and valid semver | MAY | warn | yes |
| MCP-REG-004 | Declared remote endpoint matches the probed URL | MAY | warn | yes |