Web scorecard schema

A web scorecard is the structured output of the website agent-readiness audit. It scores a website and its MCP server across six visible categories with a fairness-driven two-score model: a check that does not apply to a site is excluded from its headline score rather than counted against it, a present-but-broken surface costs more than an absent one, and a surface that works while violating a spec detail earns partial credit rather than the full penalty. This page documents every field a web scorecard carries.

The web scorecard is site-owned. Its schema_version is 0.5, independent of the CLI scorecard schema (currently 0.9) and of the agentnative spec spec_version. The CLI scorecard schema is documented separately at /scorecard-schema.

Top-level fields

{
  "schema_version": "0.5",
  "spec_version": "...",
  "target_url": "https://example.com/",
  "mcp_endpoint": "https://example.com/mcp",
  "mcp_discovery": [ ... ],
  "tool": { "name": "example.com", "url": "https://example.com/" },
  "audience": null,
  "audit_profile": null,
  "site_type": null,
  "public_listing": false,
  "vantage": { "network": "public", "credentialed": false },
  "follow_declarations": true,
  "declared_hosts": [ ... ],
  "registry_fingerprint": "3f2a9c1b7e40",
  "summary": { ... },
  "coverage_summary": { ... },
  "score_pct": 81,
  "score": { "relative": 81, "global": 63 },
  "categories": [ ... ],
  "results": [ ... ]
}
FieldTypeSourceMeaning
schema_versionstringengineVersion of the web-scorecard envelope. Site-owned, independent of the CLI schema.
spec_versionstringengineVersion of the agentnative spec the run scored against. Same value the CLI scorecard carries.
target_urlstringengineThe normalized audited URL: scheme, host, and a trailing slash. Web-specific.
mcp_endpointstring | nullengineThe discovered MCP endpoint (on a followed declared host when the site serves none), or null when none was found. Web-specific.
mcp_discoveryarrayengineThe discovery trail: each document read (server cards, AI catalog, API catalog) and common-path probe, and what it returned.
toolobjectengineWeb identity: { name, url }. No binary, install, tier, or language. See tool.
audiencenullengineAlways null for web targets; the audience classifier is a CLI concept.
audit_profilenullengineAlways null for web targets; audit profiles are a CLI concept.
site_typestring | nullengineThe declared site type the run scoped to: content, api, or null (everything ran).
public_listingbooleanengineThe submitter's opt-in to the public board listing. false unless explicitly set.
vantageobjectengineWhere the audit ran and whether it presented a credential. See vantage.
follow_declarationsboolean, optionalengineWhether the audit followed the hosts the site declares (its MCP server, its API host). Absent means no follow state was recorded.
declared_hostsarray, optionalengineThe declared-hosts trail: one entry per host the site declares, with how the audit treated it. Absent means no trail was recorded.
registry_fingerprintstring, optionalstoredThe first 12 characters of the fingerprint of the check registry the score was computed under. The engine never sets it.
summaryobjectderivedTally of check outcomes by status. See summary.
coverage_summaryobjectderivedMUST / SHOULD / MAY totals and how many were verified. See coverage_summary.
score_pctintegerderivedThe headline RELATIVE score, 0-100. Equals score.relative. See scoring.
scoreobjectderivedThe two-score pair { relative, global }. See scoring.
categoriesarrayderivedPer-category passed/counted rollups in display order. See categories.
resultsarray of result objengineOne entry per check. See results.

follow_declarations, declared_hosts, and registry_fingerprint are optional, and a reader treats a missing one as not evaluated rather than as a recorded value: a missing follow_declarations never reads as following on, a missing declared_hosts means no trail was recorded (an empty array is a recorded trail with nothing in it), and a missing registry_fingerprint means the registry version is unknown. The engine records follow_declarations and declared_hosts on every audit it completes, false with each declaration read as not followed when following was off, and never records registry_fingerprint; anc.dev adds it to every complete audit it returns or saves. The result page closes its checks with "Scored against registry <prefix>." and the markdown twin ends its freshness line with the same sentence, or with "Registry version not recorded." when the field is missing.

Response freshness

Freshness travels beside the scorecard, never inside it, so the scorecard schema owns the audit result and nothing else. Every successful per-target response carries the same three fields as siblings of scorecard: the terminal complete event from the browser audit stream, the audit_website and get_website_audit MCP results, and the four result-page WebMCP tools.

FieldTypeMeaning
cachedbooleanResponse provenance: true for a served cache entry or a listing-only patch, false for a result the current call produced.
scored_atstring | nullThe authoritative instant the audit ran, as an ISO 8601 timestamp. null on a legacy entry stored before the stamp existed.
refresh_afterstring | nullscored_at plus the one-minute cache-reuse window. null whenever scored_at is.
{
  "cached": true,
  "scored_at": "2026-08-31T18:04:12.518Z",
  "refresh_after": "2026-08-31T18:05:12.518Z",
  "scorecard": { "...": "..." },
  "share_url": "/web/example.com"
}

refresh_after is derived from scored_at on every read rather than stored, so the stored stamp and a served refresh time cannot disagree. It states cache-expiry eligibility only: past that instant a repeat request stops reusing the cached entry and tries a fresh audit. It is not a promise that a fresh audit will run. The operator kill switch, the per-source rate limits, the browser form's Turnstile challenge, and probe failures all still apply, and a cached scorecard is served as data whenever one of them refuses.

An entry whose stamp is missing or unparseable reports both instants as null rather than synthesizing a recent scoring time, and counts as maximally stale, so the next on-demand request re-audits it.

The /web/<domain> page publishes the same three values twice: as visible prose under the score, and on a hidden data-web-audit-context element that also carries both scores and a count per status, which is what the page's WebMCP tools read. The markdown twin carries the prose form.

tool

Web identity. The CLI-only header fields (tier, language, repo, install) are absent on a web tool object.

"tool": { "name": "example.com", "url": "https://example.com/" }
FieldTypeMeaning
namestringThe audited domain (host), used as the display name.
urlstringThe normalized audited URL. Matches target_url.

vantage

Where the audit ran and what it could present. A score covers what an agent at that vantage can verify.

"vantage": { "network": "public", "credentialed": false }
FieldTypeMeaning
networkstringpublic for an anc.dev audit, run from the public internet; local for a run on the runner's own network.
credentialedbooleantrue when a local run presented a credential to the audited MCP endpoint; otherwise false.

Every anc.dev audit writes public and false. A local run, which no published anc release offers yet, writes local, with credentialed true when it presented a credential. The public board lists public-vantage scorecards only, so a local or credentialed score is never ranked beside a public one. A scorecard stored before the field existed carries no vantage; every such scorecard anc.dev holds is its own public audit.

declared_hosts

One entry per URL the site's discovery documents declare off its own origin, in declaration order: the AI catalog's MCP server-card entries, the card under the discovered endpoint, then the well-known cards, with the endpoints a followed card document names right after that document. Then come the API catalog's anchors in linkset order, then the description each API anchor declares. After them come the URLs the site's own MCP paths redirected the discovery POSTs to on another origin, in probe order. A URL declared twice keeps its first entry.

{
  "surface": "/.well-known/mcp/server-card.json",
  "kind": "mcp-endpoint",
  "url": "https://mcp.example.net/mcp",
  "host": "mcp.example.net",
  "outcome": "followed",
  "admitted_by": "card"
}
FieldTypeMeaning
surfacestringWhere the URL was declared: a path on the audited origin, an AI catalog entry or an API catalog link as a JSON Pointer, or a followed card's URL.
kindstringmcp-endpoint for an MCP server URL, card-document for a server card hosted off the audited origin, api-anchor for an API catalog anchor, api-description for the description an anchor's service-desc names.
urlstringThe declared URL.
hoststring, optionalThe declared URL's host.
final_urlstring, optionalWhere one redirect from the declared URL led, when it changed the URL, including a hop the audit refused.
outcomestringfollowed, reciprocity-refused, not-followed, blocked, unreachable, or budget-exceeded.
admitted_bystring, optionalOn a followed mcp-endpoint, what its own host publishes naming it: card, ai-catalog, or metadata.
causestring, optionalOn budget-exceeded, the limit reached: per-audit-cap, slice, or domain-budget.
reasonstring, optionalOn not-followed, why: templated-url, self-path, beyond-endpoint-of-record, follow-disabled, no-service-desc, or insecure-scheme.

An MCP endpoint on another host receives a wire probe only after that host confirms it: a SEP-2127 card at <endpoint>/server-card, an entry in the host's own /.well-known/ai-catalog.json, or RFC 9728 protected-resource metadata whose resource is the endpoint. Every way a host can fail to confirm reads reciprocity-refused and records nothing more. A card-document is read, not confirmed: it reads unreachable when its host gives no response at all (a refused connection, a DNS failure, a timeout), and reciprocity-refused when the answer names no endpoint. The first confirmed endpoint in declaration order is the endpoint of record unless the audited site serves its own; an endpoint or card document declared after it, or an endpoint confirmed while the site serves its own, reads not-followed. A URL the SSRF guard refuses, a private address or an IP literal, or a redirect hop to one, reads blocked and is never requested; its rows read declared-host-blocked. A URL anc cannot request at all, one that does not parse or whose scheme is not http or https, is never requested and reads reciprocity-refused. anc sends no plaintext request: an http URL, declared or linked, and any redirect to http, is never requested. A declared URL or redirect hop on http that the guard admits reads not-followed with reason insecure-scheme, and no row is evaluated at it. An API row whose declared descriptions or hosts are all on http reads absent, naming the URL, because a surface declared over plaintext alone earns no more than a missing one; beside one anc can evaluate, an http target is left out. What a host publishes to confirm an endpoint is its consent to be probed, and consent read over plaintext could be forged by anyone on the network path, so every document anc reads to confirm an endpoint is read over https.

An API catalog anchor is an API host the API rows evaluate when its service-desc names a description other than an MCP surface; an anchor with no such service-desc reads not-followed with reason no-service-desc and is never requested. anc reads only the first eight contexts of the catalog's linkset: an anchor in a later context is never read, so it has no entry and no API row evaluates it. An API anchor host is not confirmed the way an MCP endpoint is: it reads followed once the audit admits it under its caps and budgets, and then receives one GET, at a path its description documents or else a nonsense path, appended to the path of the description's first server URL when that URL is on the anchor's origin and to the anchor's own path otherwise, which both hygiene rows read; that GET takes only redirects that keep the scheme, host, and port. An api-description is read once, up to 512 KiB, and scored by the OpenAPI row: it reads unreachable when its host gives no response at all or when the one redirect hop anc takes answers with another redirect, and followed on any other answer. A description larger than 512 KiB served with a JSON or YAML content type, or a JSON document under any content type but HTML, counts as present even when its openapi or swagger key lies past the bytes read, unless its opening bytes name another format (AsyncAPI, GraphQL introspection, Postman, RAML); its evidence is marked truncated. While the catalog lists API anchors, the API rows evaluate at them instead of the audited site, so no hygiene probe reaches the audited site unless an anchor is on it.

anc never re-sends a probe of the site's own MCP endpoint to another origin. When one of the site's MCP paths answers a discovery POST with a redirect to another origin, mcp_discovery records the probe's status and redirect target, and the target joins this trail as an mcp-endpoint whose surface is that path; like any declared endpoint, it receives no wire probe until its own host confirms it. A redirect that keeps the scheme, host, and port (/mcp to /mcp/) is followed. A later probe of the site's endpoint by any method other than GET or HEAD, answered with a redirect to another origin, is not followed and reads as an error; a GET or HEAD probe takes the redirect response itself as its answer. A row not evaluated because of a declared host names that host in hosts and host, or the host its redirect led to.

The two-score model

Both scores derive from the same per-check outcomes and cover what an agent at the audit's vantage can verify; the engine computes them and consumers read the values straight from the JSON.

Global measures how much of the whole surface a site exposes, so a check that does not apply to a site still counts in its global denominator and earns nothing, whatever its n_a reason: a missing MCP endpoint, a declared site type, a deliberate no-CORS posture, or any other. A site without MCP therefore sees what adding MCP is worth. skip and error rows stay in the global denominator the same way, and so does a check the audit could not reach: sign-in that blocks it, or a declared host that is private or unreachable, leaves the row n_a with that reason. A check that reached the server and got a definitive answer scores on that answer.

Only alternatives leave the global denominator. Alternatives are site designs no single site can satisfy at once; a limit on what the audit could reach never forms or joins one. MCP access is the only group. An endpoint presents the protected design when a token-less handshake (initialize or server/discover) draws a 401 that RFC 9728 protected-resource metadata on the endpoint's own host backs, or, when neither handshake drew a 401 or a JSON-RPC result, when the request that found the endpoint drew one. The three sign-in checks (mcp-auth-challenge, mcp-auth-servers, mcp-auth-enforced) count only for a site that presents the protected design or a site with no MCP endpoint. A check that needs a session presents the open design whenever the audit evaluates it; the open design owns no checks, and an endpoint can present both. The engine reads which designs a site presents from its rows alone, so a stored scorecard recomputes its own denominator.

Sign-in blocks a check on either design, and the check reads n_a with reason auth-required:

A protected server's global on a public audit tops out near 68, because the session and handshake rows its sign-in blocks stay in its denominator; a local run that presents a credential evaluates them.

Per applicable check, with per-tier difficulty weights (currently 5 for MUST, 3 for SHOULD, 1 for MAY):

categories

Per-category rollups in the fixed display order. counted excludes n_a / skip / error rows, so a category with nothing applicable reads 0/0.

categories[] and results[].category are re-derived from the current registry at read time, so every render of a cached scorecard (the audit_website and get_website_audit MCP tools, the /web/<domain> page, and its .md twin) reflects the current category shape regardless of when it was cached. The score and score_pct reflect the registry at audit time; re-grouping the categories earns no points and never changes the stored score.

"categories": [
  { "id": "discoverability", "name": "Discoverability", "passed": 4, "counted": 5 },
  { "id": "content-for-agents", "name": "Content for agents", "passed": 7, "counted": 8 },
  { "id": "bot-crawl-policy", "name": "Bot & crawl policy", "passed": 3, "counted": 3 },
  { "id": "api", "name": "API", "passed": 2, "counted": 3 },
  { "id": "mcp", "name": "MCP", "passed": 4, "counted": 6 },
  { "id": "agent-discovery-auth", "name": "Agent discovery & auth", "passed": 3, "counted": 3 }
]

coverage_summary

How many checks applied at each keyword level and how many passed. n_a / skip / error checks are excluded from the totals.

"coverage_summary": {
  "must":   { "total": 2,  "verified": 2 },
  "should": { "total": 15, "verified": 9 },
  "may":    { "total": 10, "verified": 7 }
}

summary

A tally of every check by its final status.

"summary": { "pass": 23, "noncompliant": 3, "broken": 2, "absent": 4, "n_a": 7, "skip": 0, "error": 0 }

results

One object per check.

{
  "id": "llms-txt",
  "label": "/llms.txt present with a summary and link index",
  "category": "content-for-agents",
  "group": "P2",
  "layer": "web",
  "keyword": "should",
  "tier": "recommended",
  "principle": "P2",
  "status": "pass",
  "evidence": "https://example.com/llms.txt -> 200",
  "hosts": [{ "host": "example.com" }],
  "host": "example.com"
}
FieldTypeMeaning
idstringThe check id from the registry (e.g. llms-txt, mcp-initialize, mcp-modern-tools-list). The remediation-catalog and fix-skill key.
labelstringHuman-readable check title.
categorystringThe visible category slug (one of the categories[].id values). Drives the display grouping.
groupstringMirrors principle for shared-renderer compatibility.
layerstringAlways web for a web scorecard row.
keywordstringmust, should, or may, derived from the check's tier.
tierstringrequired, recommended, or optional (the keyword's source).
principlestringInternal principle tag P1 through P8. Kept as data; web surfaces neither display nor link it.
statusstringpass, noncompliant, broken, absent, n_a, skip, or error. noncompliant = works but violates a spec detail; broken = present but invalid; absent = not there. See statuses.
na_reasonstringPresent only on n_a rows, one of a closed set. See statuses for every value. Absent on handler-emitted n_a rows with nothing to probe.
unprobedbooleanPresent only when true: the row settled from an antecedent the audit did observe rather than from its own request, so the run holds no observation of the surface itself. It still scores, and it carries no remediation object, because a fix prompt would name a defect nothing observed.
advisorystringPresent only on a pass row that passed in a shape a newer one replaces, from a closed set: superseded marks a server card in the SEP-1649 shape, which earns full credit while SEP-2127 is the shape to publish. The result page shows the row open with a note naming where SEP-2127 puts the card; the row carries no remediation object.
evidencestring | nullA compact human-readable summary of what the probe observed.
hostsarrayThe distinct hosts the row's evidence was requested from, as { host } objects in evidence order. Empty when the row settled without a request (an unmet antecedent, a skipped check) or its evidence names no URL; a URL the SSRF guard refused names no host. An n_a row whose reason names a declared host (follow-disabled through declared-host-budget-exceeded) names instead the declared host it could not be evaluated at, or where that host redirected, whether or not anc sent it a request; follow-disabled sends none. A row that evaluated several API anchor targets on more than one host gives each entry that host's own status, and its na_reason when that is n_a; the row's status is the worst among the hosts it evaluated, and a host not evaluated neither passes nor fails it.
hoststringPresent only when hosts has exactly one entry: that host. A row carrying neither hosts nor host reads as evaluated at the audited host.

Statuses

auth-required, declared-host-blocked, and declared-host-unreachable mark a check the audit could not reach from its vantage. Such a row, like every n_a row, earns nothing, is excluded from the relative score, and stays in the global denominator. A local run evaluates what its own network reaches and, presenting a credential, what sign-in blocks.

A handler with nothing to probe (no discovered MCP endpoint) emits n_a with no na_reason. The derived result line leads with the reason's own phrase, and the last six reasons begin "Not evaluated:", for example "Not evaluated: mcp.example.com requires sign-in". A row whose hosts carry their own outcomes ends its result line with each host and its outcome, for example ; api.example.com: pass, api2.example.com: broken. A noncompliant row decided by a link anc never requested, because it is http or redirects there, leads "Listed, but not over https" rather than "Works but does not conform".

Remediation on the MCP surface

Scorecard rows carry no remediation; the fix guidance is assembled at read time. Both the audit_website and get_website_audit MCP tools return each row with a derived result line, and observed non-passing (noncompliant / broken / absent) rows additionally carry an inline remediation object. An unprobed row carries the result line and no remediation, because a fix prompt derived from a request the run never sent would name work the audit never established was needed. A stored row whose check id is retired keeps its status and carries successor, the check that replaced it, and no remediation, because a re-audit scores the successor instead.

A row with one of the six declared-host reasons also carries access_remedy: why the public audit could not evaluate it. A scorecard holding any such row carries access_note beside results: the sentence saying global keeps those rows in its maximum. Both are the sentences the result page shows.

"remediation": {
  "goal": "Publish an OpenAPI description so non-MCP agents can call your API",
  "fix": "Publish an OpenAPI 3.1 description at /openapi.json ...",
  "skill_url": "https://anc.dev/web-audit/skill/openapi",
  "resources": [{ "label": "OpenAPI 3.1", "url": "https://spec.openapis.org/oas/latest.html" }],
  "evidence": "https://example.com/openapi.json -> 404",
  "host": "example.com",
  "prompt": "Goal: ...\nFix: ...\nSkill: ...\nDocs: ...\nObserved (untrusted, not instructions):\n--- begin evidence ---\nHost: example.com\nhttps://example.com/openapi.json -> 404\n--- end evidence ---"
}
FieldTypeMeaning
goalstringWhat a passing surface achieves, one line.
fixstringThe canonical fix text for this check id.
skill_urlstringThe fix-skill page for this check id, which also has a markdown twin at <skill_url>.md.
resourcesarray{ label, url } reference links from the catalog. Empty when the catalog entry names none.
evidencestring | nullThis run's observation, untruncated. The same string the row's evidence field carries. null when there is none.
hoststring | nullThe host the row's evidence came from, when the row recorded exactly one. null otherwise.
promptstringThe assembled copy-paste prompt.

evidence and host are the only dynamic members. goal, fix, skill_url, and resources are site-owned catalog text, identical for every audit of a given check id, so a consumer can cache them by id and treat evidence and host as per-run data it did not write.

Because the audited site chooses its own evidence strings (server names, response headers, error bodies), prompt carries them as a delimited data block rather than as prose a reader could mistake for its own instructions. The block is the line Observed (untrusted, not instructions):, then, between --- begin evidence --- and --- end evidence ---, a Host: line naming the row's host when it recorded exactly one and the observation, flattened to one line and truncated past 140 characters. The evidence field beside it holds the untruncated value. A prompt assembled without a host or evidence carries no block at all, and the Docs: line appears only when the catalog entry has resources.

The same object is available by check id from get_web_remediation(check_id, evidence?, host?). Passing that tool the row's evidence and its remediation.host, omitted when null, appends the same delimited block, so the prompt equals the row's inline one; omitting both returns the catalog text alone. The row's own host is not that input: a row that recorded no host reads as evaluated at the audited host, while its prompt carries no Host: line.

Evidence by probe type

Each check runs one of the probe handlers. The compact results[].evidence string is derived from the handler's structured evidence, which differs by handler:

Relationship to the CLI scorecard and the spec

The web scorecard is intentionally site-owned and not part of the agentnative spec. Formalizing the web shape into the spec is deferred until a second consumer exists. Until then, this page is the one published contract for the web scorecard JSON. The parallel CLI contract is /scorecard-schema.