You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
This commit was created on GitHub.com and signed with GitHub’s verified signature.
Added
Five modern Streamable HTTP readiness checks. Four LOW rules require
missing or contradictory standard request headers to receive HTTP 400 with
JSON-RPC -32020 (HeaderMismatch): a missing MCP-Protocol-Version, a missing Mcp-Method, and mismatched Mcp-Name
values on resources/read and prompts/get. The MEDIUM readiness_2026_no_get_stream rule checks that a modern-only endpoint
rejects the removed standalone GET stream with HTTP 405. All probes are
side-effect-free, HTTP-only, and skip when auth or an unimplemented optional
resource/prompt surface makes the behavior unobservable. The missing-version
and GET checks also skip dual-era endpoints, whose older-transport
compatibility is explicitly allowed by the specification. Dual-era
applicability uses explicit initialize-handshake provenance rather than
inferring it from a version discovered through the modern lifecycle.
A registry calibration found that 83–97% of judgeable modern-only servers
fail each header check, so LOW avoids multiplying four correlated early-
adoption findings into a 12-point HIGH penalty.
Sensitive MCP header detection. The new HIGH tools_mcp_headers_not_sensitive rule checks the MCP 2026-07-28 guidance
that passwords, API keys, tokens, and personally identifiable information
should not be marked with x-mcp-header, because network intermediaries can
observe the resulting values. Its deterministic matcher is deliberately
conservative: it reports strong credential phrases and high-confidence
personal or financial identifiers, while excluding ambiguous identifiers
and common non-secret pagination/token-count fields. Reports identify the
tool, schema path, header, matched metadata field, and sensitive category;
they never contain a credential value.
Cross-page cache-scope consistency. The new HIGH pagination_cache_scope_consistent rule traverses every paginated list
surface declared by a modern server and enforces the MCP 2026-07-28 MUST
that all pages of one list request use the same cacheScope. It observes
raw wire fields rather than the SDK model, whose default of private would
otherwise hide an omitted field. Single-page catalogs, legacy-only servers,
unimplemented optional resource-template listings, and traversals without
enough complete evidence skip rather than fail. The read-only traversal has
a 30-second total deadline and a 100-page-per-surface safety bound.
Four capability-aware pagination rules. For tools/list, resources/list, resources/templates/list, and prompts/list, mcpscore
now sends a fabricated opaque cursor and checks the MCP pagination
recommendation—present in every dated revision—that invalid cursors return
JSON-RPC -32602 (Invalid params). The probes are read-only and run against the established SDK
session for legacy and dual-era servers, avoiding false failures from
stateful servers that require initialization; modern-only servers use the
stateless probe path. Undeclared capabilities and unavailable observations
skip rather than fail. Because the resources capability does not separately
advertise the optional resource-template listing, a -32601 response from resources/templates/list also skips only that rule.
Fixed
security_error_data_leak now runs. It read a field (error_response)
that no code path ever produced, so it silently skipped on every audit. It
now scans the response body the malformed-request probe elicits — the
request most likely to make a server dump a stack trace, file path, or
secret — for sensitive-data leaks, and skips as insufficient data only when
no body was captured (stdio is not applicable). A leaked value is never
echoed back into the report (only the leak type and a count), because the
report is shareable and echoing the secret would re-leak it. Servers that
do not leak gain the 2 points they previously never had a chance to earn —
e.g. DeepWiki 76/85 → 78/87.
Corrected two stale rule docstrings that quoted an old severity-point scale
(10 points (CRITICAL) → 5, 5 points (MEDIUM) → 2); the weights are RuleSeverity (CRITICAL 5 / HIGH 3 / MEDIUM 2 / LOW 1).
Malformed-request scoring now has real evidence. The existing security_malformed_request_handling rule is backed by a safe raw probe.
It enforces the JSON-RPC 2.0 Parse error code (-32700) as the normative
requirement, and deliberately relaxes one strict-JSON-RPC point: the
response id may be null or absent (the id is unknowable when the
request never parsed, and a registry calibration found conforming servers
that omit it) — a calibrated interoperability allowance, not full Response
Object conformance. HTTP status is deliberately not constrained because
JSON-RPC is transport agnostic. A successful mandatory server/discover
control prevents auth, endpoint, or optional-capability failures from
becoming false verdicts. The probe streams and bounds the response body
(16 KB) so a hostile server cannot exhaust memory on the deliberately
malformed request.
Report rule order now matches its documentation. Rules sort by (group_order, group_name, rule_order, rule_id) — the tie-breaks the
attribute docs always promised. Previously the sort key was a single
integer with no tie-breakers, so the capabilities and security groups
(which share a group_order) interleaved in report output by import
order. Groups are now contiguous and the order is deterministic. Scores,
rule ids, and severities are unchanged — only the order of entries in results (and the generated rules reference) moves.
The rule registry now rejects an empty rule_id at registration (the
inherited default made the old check unreachable) and refuses to register
a retired rule_id — retired ids are never reused.
Removed
BaseRule.sort_order. Ordering is collection policy, now computed by
the internal rule_sort_key() at the sort sites, so a subclass override
can no longer corrupt the sort with a stale key shape. The Python class
surface is outside the stability contract
(which covers the CLI, exit codes, the JSON report schema, and rule_ids,
and explicitly reserves result positions for change in any release) —
but if you imported sort_order anyway, sort with (rule.group_order, rule.group_name, rule.rule_order, rule.rule_id).
AuditData.error_response. A dead constructor field — nothing in the
repo's history ever produced it, and its last reader
(security_error_data_leak) now scans the malformed-request probe's body
instead. Removed rather than kept as a phantom (its presence is what
produced the never-observed-behavior confusion in the first place). The
Python class surface is outside the stability contract as above; migration:
drop the kwarg — any code that constructed AuditData(error_response=...)
was setting a value no rule ever read.