rule_id, the report schema, the CLI shape, and the exit codes.
Those change only in a major version. Messages, rule names, and the absolute
score move in minor releases, on purpose. This contract has been in force
since 1.1.0.
Here is a real --json report, trimmed to one result. The stable parts are
the ones your code should read:
rule_id, passed, severity_value, and
the counts. Show message, rule_name, and suggested_fix to people. The
next sections say exactly which fields are which.
Stable
Rule identifiers
Arule_id such as tools_annotations_present is never renamed and never
reused for a different check. Rules are added in any release. A rule can be
retired and stop running, but its rule_id keeps its meaning forever. The
rules reference lists retired rules too.
Report schema
The--json report carries schema_version, currently 1. Within a schema
version, existing fields keep their names, types, and meanings. New fields
appear in any release, so ignore fields you do not know. A removal or a type
change bumps schema_version.
spec.catalog_versions lists catalogs judged on a revision other than the
negotiated one, keyed by listing name. It is empty unless a legacy listing
failed and the stateless modern listing served it.
Package audits
package is null for every server audit. When it is an object, the report
is a package audit. Its score comes from the packaging rules alone, a rule set
separate from server audits, so anything that compares or aggregates scores
must branch on this field.
For a package audit,
transport and spec.negotiated_version are both
null.
Skills facts
skills reports what a host decides about, so mcpscore shows it without
scoring it:
Smoke results
smoke is absent unless the run used --smoke. Smoke results
never count in score or max_score. A smoke failure is reported through
exit code 4.
A check whose call completed sets
details.called: true. A check that
received a result also carries a bounded details.response with
content_blocks, content_types, structured_content, is_error, and
first_text capped at 80 code points. Do not require details.response.
check_id values are stable in the same way rule_ids are.
Configuration
config is absent unless a mcpscore.toml or [tool.mcpscore] table was
applied (configure rules). When present, the score was
computed under that policy:
- Rules turned off did not run. They appear in
skipped_rules[]with reasondisabled-by-config, unless the engine could not have judged the rule anyway, in which case the canonical skip reason is kept. - Re-ranked rules carry their configured severity in
results[], with the default indetails.severity_default. - A configured score is comparable only across runs with the same
config.sha256and engine version. The badge and mcpscore.dev never apply a configuration.
CLI interface
The invocation shape is stable:mcpscore <target> with --json,
--header, --token, and the MCPSCORE_TOKEN environment variable. New
flags are added. Existing flags keep their meaning.
mcpscore --versionprintsmcpscore <version>to stdout, exits0, and needs no target.--jsonwrites exactly one JSON document to stdout. All logs go to stderr.--sarif FILEwrites SARIF 2.1.0 with oneresultper failed rule, keyed byruleId=rule_id. The severity-to-level mapping and theprimaryLocationLineHashfingerprint (a hash of the rule and the target) keep their meaning, so code scanning alerts survive an upgrade.
For what to do about each code, see troubleshooting.
Credentials
Header and token values are never logged and never written to the report. The report records only the booleanauthenticated. This is a permanent
commitment.
Evolving by design
mcpscore grows its checks rather than freezing the number early. A calibrated
scoring frame will revisit this once the rule set settles. The
methodology explains how rules
apply per spec revision.
Versioning
mcpscore follows SemVer. Breaking anything in the Stable section needs a major version. Rule additions and score movement are minor-version changes. Pre-releases (bN) can change anything.
Failure guidance
A failed rule can carry two kinds of guidance: a repair hint insuggested_fix, and structured evidence in details.issues. Neither changes
a verdict, an applicability decision, or a score weight.
Repair hints: suggested_fix
Hints cover tool, resource, resource-template, prompt, server-metadata,
capability, protocol, transport, auth and security, pagination,
catalog-stability, readiness, and package rules. A hint for optional metadata
or list-change advice says it is a quality recommendation in the message,
not a protocol mandate.
Requirement strength is not inferred from severity or from the presence of a
hint. Read the rule’s
details.basis and the revisions it applies to. A hint
is advice for that finding.
Structured evidence: details.issues
Schema and catalog failures keep their existing detail keys and add a bounded
list of issues. Each issue names where the problem is and what was expected:
At most 20 issues are included.
issues_total counts every affected field or
occurrence, and issues_omitted counts those past the limit. Each tool
contributes its first failing predicate. Messages name the first affected
field, and the full bounded evidence stays in details.
Technical details: issue paths and catalog evidence
Technical details: issue paths and catalog evidence
- Prompt arguments and icons include their own indexes in the path.
- Server metadata paths start at
/serverInfo,/instructions, or/capabilities, with no catalog index. An empty-tools finding uses/tools, relative to the catalog result. - Stored paths keep their exact JSON Pointer characters, including newlines, quotes, and backslashes. Only CLI rendering escapes them.
- Issues leave out publisher names, URI values, image data, and arbitrary schema values. Indexes still identify entries with missing or duplicate names.
issues_totalcounts affected fields or occurrences, which can differ from a legacy count of distinct names or duplicate identifiers.- Schema diagnostics explain the existing validator. They do not extend it to complete JSON Schema conformance. A tool’s optional output schema is checked only when present.
- Existing detail keys are kept for compatibility and are not covered by the evidence bound.
- An optional field with an invalid declared value still needs repair.
- Header uniqueness groups annotations by tool name. Resolve duplicate tool names before treating a collision across entries as an error within one tool.
- Server icons are checked for an
https://ordata:prefix only. A declaredlistChangedflag does not prove that notifications work. - Catalog collection failures are recorded in the report’s
listing_errors, and indetails.collection_errorwhen a declaration rule fails. Outcomes areinvalid_response,timeout,rpc_error, orcollection_error. Validation locations and expected types are kept, without raw response values or exception messages. - Modern result objects with a missing or non-array
toolsfield also get sanitized field-shape diagnostics. The probe outcome stays unsupported.
Technical details: protocol and probe evidence
Technical details: protocol and probe evidence
- Failed protocol and probe checks keep their observations and can add
details.expectedfor the fields checked. - Human messages include observed error codes and statuses only when present. Stdio messages leave out the HTTP status.
- Cursor probes distinguish a returned page from an unexpected response, rather than inferring success from a missing error code.
- Audit context is recorded once:
transportat report level, andspec.negotiated_versionandspec.session_protocol_versionin thespecblock.session_protocol_versionisnullunless a legacy session was established. Existingtarget_version,spec, andbasisdetail fields keep the rule’s revision and source. - Unavailable probes still skip. A hint never turns missing evidence into a failure.
- Readiness result-field issues use
entity_kind: "response", the originatingprobe_id, and paths relative to the JSON-RPCresult.missing_or_nullmeans the observation cannot distinguish omission from an explicit null.invalid_valuemeans a value was observed and failed the existing predicate. - Readiness tool-schema issues use catalog indexes and paths relative to each tool, with the same 20-issue and 255-code-point bounds. A network-reference finding identifies the containing schema. It does not prove a fetch.
- For readiness schema failures,
offending_toolskeeps its legacy shape, keyed by name, with short problem labels and no values.offending_tool_indexesmaps each name to all affected catalog indexes, including duplicate names. Unlikeissues, this index mapping is complete.
Technical details: credential masking and message previews
Technical details: credential masking and message previews
Evidence copies in rules and probes mask credentials:
response_session_idbecomes[redacted]. Absence staysnull.- URL userinfo is removed.
- These query keys are masked:
access_token,refresh_token,id_token,token,api_key,apikey,client_secret,password,secret,authorization, andcode. Matching is case-insensitive after URL decoding, and treats hyphens like underscores. - Every
scheme://URL is matched case-insensitively, including PostgreSQL and HTTP URLs, dictionary keys, and URLs inside challenge headers and exception text. - Public query parameters, spelling, order, fragments,
WWW-Authenticatechallenges, and diagnostic exception reasons stay available. - When masked dictionary keys collide, every entry is kept with a
deterministic
[masked name N]suffix. Literal keys are reserved first, and summary maps with the same source names use the same aliases. These aliases are report labels, not tool names. Use catalog indexes to identify tools.
- A preview holds at most 60 source code points, with a visible truncation marker. A value that is a single URL keeps up to 200. A list renders item by item, at most 10 items, then a count.
- Printable Unicode stays readable. Quotes, backslashes, and nonprinting characters are escaped, so a rendered preview can be longer than the bound.
- Messages can show server identity values on passes, and up to three duplicate tool names on failures. Website URL values are not echoed in passing messages.
- Previews of resource and issuer identifiers, version lists,
Content-Typevalues, and schema-validator reasons appear after credential masking. Validator previews appear only in the human message, never in structured summaries. - The full values stay in the legacy detail keys. The preview is a display bound, not secret redaction.
Technical details: what a passing check proves
Technical details: what a passing check proves
- A passing PKCE finding establishes advertised
S256support, not an executed authorization flow. - An error-hygiene pass means no configured pattern was found in the sampled response.
- Catalog-stability passes compare two connections made with the same authorization.
- Modern-protocol hints keep valid legacy paths working. Readiness promotion into the main score is unchanged by hints.
- Authorization discovery follows the order a client uses: the challenge’s
resource_metadataURL first, wherever it is hosted, then the well-known locations. The metadata it finds must name the audited endpoint as itsresource(RFC 9728 section 3.3). - From 2025-11-25 on, a 401 challenge without
resource_metadatais not a finding on its own: the server may publish at a well-known location instead, andauth_protected_resource_metadatajudges whether it does. On 2025-06-18 the parameter is required. - Issuer metadata is looked up in the
MCP discovery order,
including path insertion for an issuer with a path. A document counts only
when its
issueris identical to the listed issuer; a difference in the trailing slash alone is accepted and reported. A failure lists the locations tried; it does not prove that no valid document exists elsewhere. - Discovery URLs that point a public server at loopback, link-local or private addresses are reported, not fetched. Host names are resolved first. A lookup that times out is not fetched either, and is reported as unreachable.
Technical details: package evidence
Technical details: package evidence
- Package hints separate publisher metadata changes from consumer choices.
- npm deprecation and PyPI yanking keep their registry-specific names and
the publisher’s notice when available, in
details.withdrawal_reason. - Notice previews allow 200 code points, so replacement URLs in the notice
stay useful. Masking and control-character escaping happen before display.
Longer notices are visibly truncated, and the full masked notice stays in
details. - Existing
yankeddetail values and verdicts are unchanged. Public values use bounded quoted previews, and URL credentials are masked in messages and details. - A missing license or repository URL describes the metadata. It says nothing about legal permissions or whether the source exists elsewhere.
- Package audits never run the package and cannot confirm its MCP behavior.
What’s next
CLI reference
Every flag and exit code, with defaults
Scoring methodology
How the score is computed, and why it depends on the rule set