Skip to main content
Build on 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:
Key a CI check or a dashboard on 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

A rule_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 reason disabled-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 in details.severity_default.
  • A configured score is comparable only across runs with the same config.sha256 and 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 --version prints mcpscore <version> to stdout, exits 0, and needs no target.
  • --json writes exactly one JSON document to stdout. All logs go to stderr.
  • --sarif FILE writes SARIF 2.1.0 with one result per failed rule, keyed by ruleId = rule_id. The severity-to-level mapping and the primaryLocationLineHash fingerprint (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 boolean authenticated. 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 in suggested_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.
  • 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_total counts 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:// or data: prefix only. A declared listChanged flag does not prove that notifications work.
  • Catalog collection failures are recorded in the report’s listing_errors, and in details.collection_error when a declaration rule fails. Outcomes are invalid_response, timeout, rpc_error, or collection_error. Validation locations and expected types are kept, without raw response values or exception messages.
  • Modern result objects with a missing or non-array tools field also get sanitized field-shape diagnostics. The probe outcome stays unsupported.
  • Failed protocol and probe checks keep their observations and can add details.expected for 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: transport at report level, and spec.negotiated_version and spec.session_protocol_version in the spec block. session_protocol_version is null unless a legacy session was established. Existing target_version, spec, and basis detail 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 originating probe_id, and paths relative to the JSON-RPC result. missing_or_null means the observation cannot distinguish omission from an explicit null. invalid_value means 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_tools keeps its legacy shape, keyed by name, with short problem labels and no values. offending_tool_indexes maps each name to all affected catalog indexes, including duplicate names. Unlike issues, this index mapping is complete.
Evidence copies in rules and probes mask credentials:
  • response_session_id becomes [redacted]. Absence stays null.
  • URL userinfo is removed.
  • These query keys are masked: access_token, refresh_token, id_token, token, api_key, apikey, client_secret, password, secret, authorization, and code. 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-Authenticate challenges, 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.
Human messages show bounded, quoted previews of values:
  • 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-Type values, 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.
These protections do not change the observations used for scoring, and they are not a general secret detector. Error text and public metadata can carry sensitive information outside these patterns. Review a report before you publish it.
  • A passing PKCE finding establishes advertised S256 support, 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_metadata URL first, wherever it is hosted, then the well-known locations. The metadata it finds must name the audited endpoint as its resource (RFC 9728 section 3.3).
  • From 2025-11-25 on, a 401 challenge without resource_metadata is not a finding on its own: the server may publish at a well-known location instead, and auth_protected_resource_metadata judges 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 issuer is 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.
  • 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 yanked detail 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