CATALOGUE · /checks

Every check, explained

68 deterministic checks. Each entry says what the scanner looks at, what to change, and which document from Google, OpenAI, Anthropic, Cloudflare, the IETF or the W3C the advice comes from. A report links every finding here by its id; the same id reproduces that one check from the command line.

  1. 1. Three browser loads

    Headless Chrome opens your page without the WebMCP flag (what a visitor sees), with it forced on (what the code would expose), and with a capture hook (what the page authored). No tool is ever executed.

  2. 2. A few small fetches

    robots.txt, llms.txt, the sitemap, /.well-known/security.txt and /.well-known/ai-catalog.json (with the MCP Server Cards it lists), once each, through the same private-address guard as the browser. Together with the page's own metadata they make the agent-readiness checklist.

  3. 3. Rules, then a report

    Every finding carries a citation (tool, field, offset, snippet), a category, a fix and its sources. Only SECURITY, SPEC and DEPLOYMENT findings can be medium or above; readiness never touches the grade.

SECURITY 19

Exploitable or dangerous. Cited by threat, not by spec; these drive the grade.

CRITICAL SECURITY
desc-injection

Instruction-like text in tool metadata

Detects: A tool name, title or description contains phrases that address the calling model ("ignore previous instructions", "system prompt", "without telling the user").

Fix: Describe what the tool does, in the third person, and nothing else. Remove every sentence that tells an agent how to behave; behaviour belongs in your execute() code, not in text the model reads.

description: "Search the product catalogue by keyword and return up to 20 matches."
$ node scanner/bin/webmcp-scan.js https://your-site.com --check desc-injection
CRITICAL SECURITY
schema-description-injection

Instructions hidden in a schema description

Detects: A JSON Schema property description carries agent-directed instructions or exfiltration directives.

Fix: Parameter descriptions say what the value is ("ISO date", "SKU, 8 digits"). Delete anything that tells the model to read, send or pass data elsewhere.

properties: { date: { type: "string", format: "date", description: "Delivery date" } }
$ node scanner/bin/webmcp-scan.js https://your-site.com --check schema-description-injection
HIGH SECURITY
hidden-unicode

Invisible characters in metadata

Detects: A zero-width, bidi-override or tag character sits in a name, title or description where a reviewer cannot see it.

Fix: Strip non-printing characters from every string before registering: text.replace(/[\u200b-\u200f\u202a-\u202e\u2060-\u2064\ufeff]/g, "").

$ node scanner/bin/webmcp-scan.js https://your-site.com --check hidden-unicode
HIGH SECURITY
dangerous-capability

Tool advertises code or shell execution

Detects: The name or description says the tool runs commands, evaluates code or spawns processes.

Fix: Do not expose a general executor to agents. Replace it with narrow, named actions, validate every argument server-side, and mark the action consequential so the client asks the user first.

annotations: { consequentialHint: true, readOnlyHint: false }
$ node scanner/bin/webmcp-scan.js https://your-site.com --check dangerous-capability
MEDIUM SECURITY
schema-freeform-sink

Free-form input on a sink-like parameter

Detects: A parameter named like a path, URL, query or command accepts any string with no enum, pattern, format or length.

Fix: Constrain the schema (enum, pattern, format, maxLength) and validate again inside execute(): a schema is documentation for the agent, not authorisation.

url: { type: "string", format: "uri", pattern: "^https://example\\.com/", maxLength: 2048 }
$ node scanner/bin/webmcp-scan.js https://your-site.com --check schema-freeform-sink
MEDIUM SECURITY
missing-annotations

Mutating tool without readOnlyHint

Detects: A tool that creates, updates, deletes or sends does not declare readOnlyHint, so an agent cannot tell it from a safe read.

Fix: Set readOnlyHint explicitly on every tool: true for reads, false for anything that changes state.

annotations: { readOnlyHint: false }
$ node scanner/bin/webmcp-scan.js https://your-site.com --check missing-annotations
MEDIUM SECURITY
missing-consequential-hint

Consequential action not marked

Detects: A tool that books, buys, pays, transfers, deletes an account or submits an order does not set consequentialHint:true.

Fix: Add consequentialHint:true so a client can require user confirmation before executing. Keep the confirmation in your own UI as well; hints are advice, not access control.

annotations: { readOnlyHint: false, consequentialHint: true }
$ node scanner/bin/webmcp-scan.js https://your-site.com --check missing-consequential-hint
HIGH SECURITY
data-over-collection

Schema asks for a credential or identifier

Detects: An input parameter is named password, token, api_key, card number, SSN or similar.

Fix: Never route secrets through tool arguments. Use the session the user already has (cookies, OAuth) inside execute(), and remove the parameter from the schema.

$ node scanner/bin/webmcp-scan.js https://your-site.com --check data-over-collection
HIGH SECURITY
intent-mismatch

Destructive tool annotated read-only

Detects: The name or description mentions delete, remove, wipe or revoke, yet readOnlyHint is true.

Fix: Make the annotation match the behaviour: readOnlyHint:false, and consequentialHint:true if the action is hard to undo.

annotations: { readOnlyHint: false, consequentialHint: true }
$ node scanner/bin/webmcp-scan.js https://your-site.com --check intent-mismatch
MEDIUM SECURITY
name-duplicate

Two tools share one name

Detects: The same name is registered twice, possibly from different frames, so an agent cannot tell them apart and one can shadow the other.

Fix: Give every tool a unique name and register it once per document. Abort the previous registration (AbortController) before re-registering on route changes.

$ node scanner/bin/webmcp-scan.js https://your-site.com --check name-duplicate
HIGH SECURITY
schema.remote-ref

Schema references a remote $ref

Detects: inputSchema contains a $ref pointing at an http(s) URL, which any consumer that resolves refs will fetch, possibly from an internal address.

Fix: Inline the referenced schema, or use a same-document $ref ("#/$defs/item").

$defs: { item: { type: "object" } }, properties: { item: { $ref: "#/$defs/item" } }
$ node scanner/bin/webmcp-scan.js https://your-site.com --check schema.remote-ref
HIGH SECURITY
tls-plaintext

Remote MCP server over plain HTTP

Detects: The server endpoint is http://, so tool calls and arguments cross the network unencrypted.

Fix: Serve the endpoint over HTTPS and redirect http:// to it.

$ node scanner/bin/webmcp-scan.js https://your-site.com --check tls-plaintext
CRITICAL SECURITY
remote-no-auth

Mutating tools reachable without authentication

Detects: The scanner enumerated state-changing tools over an anonymous connection.

Fix: Require authentication (OAuth or an API key) before tools/list, and scope mutating tools to authenticated sessions.

$ node scanner/bin/webmcp-scan.js https://your-site.com --check remote-no-auth
MEDIUM SECURITY
drift-new-tool

New tool since last scan

Detects: A tool that was not in the previous scan has appeared.

Fix: Confirm the addition is intended and review it like any new code path exposed to agents.

$ node scanner/bin/webmcp-scan.js https://your-site.com --check drift-new-tool

SPEC 4

Violates a normative requirement of the WebMCP draft. Each carries the exact clause.

MEDIUM SPEC
spec.missing-required

Missing name or description

Detects: ModelContextTool requires both name and description; one is absent or empty.

Fix: Add both. The description is what the agent reads to decide whether to call the tool, so make it a full sentence.

$ node scanner/bin/webmcp-scan.js https://your-site.com --check spec.missing-required
MEDIUM SPEC
spec.inputschema-type

inputSchema is not an object

Detects: inputSchema was registered as a string, array or other non-object value.

Fix: Pass a JSON Schema object (not JSON text) to registerTool().

inputSchema: { type: "object", properties: { q: { type: "string" } }, required: ["q"] }
$ node scanner/bin/webmcp-scan.js https://your-site.com --check spec.inputschema-type

Page is not a secure context

Detects: The page is served over plain http:// on a non-local host, and document.modelContext is [SecureContext].

Fix: Serve the page over HTTPS. No token or flag makes WebMCP available on an insecure origin.

$ node scanner/bin/webmcp-scan.js https://your-site.com --check spec.insecure-context

DEPLOYMENT 3

Whether a real visitor's browser can see the integration at all.

HIGH DEPLOYMENT
deploy.dark-integration

Tools register only behind the flag

Detects: With the WebMCP flag forced on, tools register; without it, document.modelContext is absent. No real visitor sees the integration.

Fix: Register the origin for the WebMCP origin trial and serve the token in an Origin-Trial response header or a <meta http-equiv="origin-trial"> tag on every page that registers tools. Re-audit: the status should turn Live.

<meta http-equiv="origin-trial" content="TOKEN_FROM_CHROME_ORIGIN_TRIALS">
$ node scanner/bin/webmcp-scan.js https://your-site.com --check deploy.dark-integration
MEDIUM DEPLOYMENT
deploy.origin-trial-token

Origin-Trial token expired or for another origin

Detects: A token is present but is expired, or was issued for a different origin than the page.

Fix: Issue a fresh token for this exact origin (scheme, host and port) in the Chrome origin trials console and replace the old one.

$ node scanner/bin/webmcp-scan.js https://your-site.com --check deploy.origin-trial-token

COMPATIBILITY 6

Legacy or pre-standard surfaces that current browsers and agents ignore.

LOW COMPATIBILITY
compat.legacy-entrypoint

Registered via navigator.modelContext

Detects: The tool was registered through the pre-standard navigator surface, which agents reading the current API never see.

Fix: Register through document.modelContext.registerTool() and feature-detect it.

const mc = document.modelContext; if (mc?.registerTool) await mc.registerTool(tool, { signal });
$ node scanner/bin/webmcp-scan.js https://your-site.com --check compat.legacy-entrypoint
LOW COMPATIBILITY
compat.provide-context

Registered via removed provideContext()

Detects: provideContext({tools}) is not in the current IDL.

Fix: Call registerTool() once per tool instead.

$ node scanner/bin/webmcp-scan.js https://your-site.com --check compat.provide-context
MEDIUM COMPATIBILITY
compat.pre-241-schema

inputSchema exposed as a JSON string

Detects: The schema reached the agent as text rather than an object (legacy implementation shape).

Fix: Register with an object; if the string comes from the browser's getTools() read-back, nothing to change on your side.

$ node scanner/bin/webmcp-scan.js https://your-site.com --check compat.pre-241-schema
LOW COMPATIBILITY
compat.root-annotations

Hint placed at the tool root

Detects: readOnlyHint or another hint sits on the tool object itself, where WebIDL silently drops it.

Fix: Move the hints into the annotations member.

annotations: { readOnlyHint: true, untrustedContentHint: true }
$ node scanner/bin/webmcp-scan.js https://your-site.com --check compat.root-annotations
LOW COMPATIBILITY
compat.mcp-only-hints

MCP-only hint has no effect in WebMCP

Detects: destructiveHint, idempotentHint or openWorldHint are MCP annotations, not WebMCP ones; browsers ignore them.

Fix: Express the intent with the WebMCP set: readOnlyHint, consequentialHint, untrustedContentHint, debugging.

$ node scanner/bin/webmcp-scan.js https://your-site.com --check compat.mcp-only-hints
LOW COMPATIBILITY
exposedto-invalid

exposedTo entry is not an origin

Detects: An exposedTo value does not parse as scheme://host[:port].

Fix: Use full origins only, no paths, no wildcards inside the host.

exposedTo: ["https://partner.example"]
$ node scanner/bin/webmcp-scan.js https://your-site.com --check exposedto-invalid

AGENT-UX 5

Affects whether a model picks and calls the tool correctly. Never above low.

LOW AGENT-UX
schema.not-json-schema

inputSchema is not recognisable JSON Schema

Detects: The object has none of type, properties, $ref, oneOf, anyOf, allOf.

Fix: Write a real JSON Schema: type "object" with properties and required.

inputSchema: { type: "object", properties: {}, required: [] }
$ node scanner/bin/webmcp-scan.js https://your-site.com --check schema.not-json-schema
INFO AGENT-UX
schema.size

Very large inputSchema

Detects: The serialised schema exceeds 64 KB (info) or 512 KB (medium), a real context cost for every agent that reads it.

Fix: Split the tool, drop unused properties, and keep descriptions to one line each.

$ node scanner/bin/webmcp-scan.js https://your-site.com --check schema.size

Description under 20 characters

Detects: The description is too short for an agent to choose this tool over another.

Fix: One full sentence: what it does, what it needs, what it returns.

description: "Return the order status and tracking link for an order number."
$ node scanner/bin/webmcp-scan.js https://your-site.com --check agentux.description-short

Free-form string where a choice is implied

Detects: A select/filter/sort/status-style parameter accepts any string.

Fix: Use an enum for a bounded domain; models call enum parameters more accurately than free text.

status: { type: "string", enum: ["open", "shipped", "delivered"] }
$ node scanner/bin/webmcp-scan.js https://your-site.com --check agentux.freeform-string-args
LOW AGENT-UX
name.near-duplicate

Names collide once normalised

Detects: Two names differ only in case, hyphens, underscores or spaces.

Fix: Rename one so the two read differently to a model, not just to a parser.

$ node scanner/bin/webmcp-scan.js https://your-site.com --check name.near-duplicate

READINESS 26

Can an agent find and read the page at all: robots.txt, llms.txt, sitemap, metadata, and the MCP discovery chain (AI Catalog and Server Cards). Independent of WebMCP, never affects the grade.

LOW READINESS
site.page-unreadable

Page does not open for a bot

Detects: The audit browser received an error status or a bot-challenge page instead of the content.

Fix: Allow well-behaved automated visitors through your CDN or WAF (Cloudflare "Verified bots", published IP ranges for OpenAI, Anthropic and Perplexity fetchers), and make sure the page returns 200 without JavaScript challenges.

$ node scanner/bin/webmcp-scan.js https://your-site.com --check site.page-unreadable
INFO READINESS
site.robots-missing

No robots.txt

Detects: The site has no robots.txt. Crawlers assume everything is allowed, but the site states no policy and declares no sitemap.

Fix: Add a robots.txt at the site root that allows crawling, names the sitemap, and states your AI policy with a Content-Signal line.

User-agent: *
Allow: /
Content-Signal: search=yes, ai-input=yes
Sitemap: https://example.com/sitemap.xml
$ node scanner/bin/webmcp-scan.js https://your-site.com --check site.robots-missing
LOW READINESS
site.robots-unreachable

robots.txt cannot be fetched

Detects: robots.txt answers with a server error, a timeout or an HTML page.

Fix: Serve a plain-text robots.txt with HTTP 200. Google stops crawling a site while its robots.txt returns 5xx, so a broken file is worse than none.

$ node scanner/bin/webmcp-scan.js https://your-site.com --check site.robots-unreachable

robots.txt blocks on-demand assistant fetchers

Detects: ChatGPT-User, Claude-User or Perplexity-User are disallowed. These fetch a page only when a person asks an assistant to read it.

Fix: Allow the user-triggered fetchers unless you truly want assistants to refuse your page. Block training crawlers separately if that is the concern.

User-agent: ChatGPT-User
User-agent: Claude-User
User-agent: Perplexity-User
Allow: /
$ node scanner/bin/webmcp-scan.js https://your-site.com --check site.robots-blocks-user-fetchers

robots.txt blocks AI search crawlers

Detects: OAI-SearchBot, Claude-SearchBot, PerplexityBot, Googlebot, Bingbot or Applebot are disallowed, so the site is not cited in AI search answers.

Fix: Allow the search crawlers; each vendor documents that training opt-outs are separate tokens (GPTBot, ClaudeBot, Google-Extended, Applebot-Extended).

User-agent: OAI-SearchBot
User-agent: Claude-SearchBot
User-agent: PerplexityBot
Allow: /
$ node scanner/bin/webmcp-scan.js https://your-site.com --check site.robots-blocks-search-agents

Training crawlers blocked (policy noted)

Detects: GPTBot, ClaudeBot, Google-Extended, Applebot-Extended or CCBot are disallowed. Reported for completeness, not as a defect.

Fix: Nothing to fix if intended. Each vendor documents that blocking the training token does not affect search inclusion or user-requested fetches.

$ node scanner/bin/webmcp-scan.js https://your-site.com --check site.robots-blocks-training
INFO READINESS
site.ai-policy-unstated

No stated AI policy in robots.txt

Detects: robots.txt names no AI agent and carries no Content-Signal line, so every vendor applies its own default.

Fix: State the policy explicitly, whichever way you decide. Cloudflare's Content-Signal line expresses search, ai-input and ai-train preferences in one line; the IETF aipref group is standardising the same vocabulary.

Content-Signal: search=yes, ai-input=yes, ai-train=no
$ node scanner/bin/webmcp-scan.js https://your-site.com --check site.ai-policy-unstated
LOW READINESS
site.sitemap-missing

No sitemap

Detects: Neither the robots.txt Sitemap: line nor /sitemap.xml returns a sitemap.

Fix: Generate a sitemap.xml listing every public URL and declare it in robots.txt.

<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
  <url><loc>https://example.com/</loc></url>
</urlset>
$ node scanner/bin/webmcp-scan.js https://your-site.com --check site.sitemap-missing
INFO READINESS
site.robots-no-sitemap

Sitemap not declared in robots.txt

Detects: A sitemap exists at the default location but robots.txt does not point at it.

Fix: Add the line to robots.txt.

Sitemap: https://example.com/sitemap.xml
$ node scanner/bin/webmcp-scan.js https://your-site.com --check site.robots-no-sitemap
INFO READINESS
site.llms-txt-missing

No llms.txt

Detects: There is no /llms.txt. Optional, but it is the one file written for agents: a markdown index of what the site is and which pages matter.

Fix: Add /llms.txt: an H1 with the site name, a one-line blockquote summary, then H2 sections of markdown links to the pages an agent should read. Google Search ignores the file (neither helps nor harms ranking); Chrome Lighthouse audits for it.

# Jane Doe

> Senior backend engineer (Go, Postgres), Berlin. CV, projects and contact.

## CV
- [Resume](https://example.com/cv.md): experience and skills

## Projects
- [Project A](https://example.com/projects/a.md): what it is and why it matters
$ node scanner/bin/webmcp-scan.js https://your-site.com --check site.llms-txt-missing
INFO READINESS
site.security-txt-missing

No security.txt

Detects: There is no /.well-known/security.txt, so a researcher who finds a problem has no documented contact.

Fix: Add the file with the two required fields.

Contact: mailto:[email protected]
Expires: 2027-12-31T23:59:59.000Z
$ node scanner/bin/webmcp-scan.js https://your-site.com --check site.security-txt-missing

security.txt is incomplete or expired

Detects: The file lacks Contact or Expires, or Expires is in the past.

Fix: Add the missing field or move Expires forward (RFC 9116 recommends under a year ahead).

$ node scanner/bin/webmcp-scan.js https://your-site.com --check site.security-txt-invalid
LOW READINESS
site.mcp-catalog-invalid

AI Catalog is malformed or errors

Detects: /.well-known/ai-catalog.json exists but is not valid JSON, has no entries array, or returns a server error, so a client looking for MCP servers on the domain stops there.

Fix: Serve JSON like {"specVersion": "1.0", "entries": [{"identifier": "urn:air:example.com:mcp:jobs", "type": "application/mcp-server-card+json", "url": "https://example.com/mcp/server-card"}]}, ideally as application/ai-catalog+json. The Server Card extension is experimental; this never affects the grade.

$ node scanner/bin/webmcp-scan.js https://your-site.com --check site.mcp-catalog-invalid

MCP Server Card cannot be used

Detects: A Server Card listed in the AI Catalog could not be fetched or does not follow the schema: $schema, a reverse-DNS name with one slash, a version (not a range), a description of at most 100 characters, and remotes with a streamable-http URL.

Fix: Host the card at <your MCP endpoint>/server-card (for https://example.com/mcp that is https://example.com/mcp/server-card) and fill the required fields. Cards carry identity and connection details only, never tools. The Builder generates a valid card.

$ node scanner/bin/webmcp-scan.js https://your-site.com --check site.mcp-server-card-invalid

Server Card has no CORS headers

Detects: The hosted Server Card answers without Access-Control-Allow-Origin, so a browser-based client cannot read it.

Fix: Send Access-Control-Allow-Origin: *, Access-Control-Allow-Methods: GET, Access-Control-Allow-Headers: Content-Type, If-None-Match and Access-Control-Expose-Headers: ETag. Safe, because the card is public and read-only. Add Cache-Control: public, max-age=3600 and an ETag too.

$ node scanner/bin/webmcp-scan.js https://your-site.com --check site.mcp-server-card-cors
INFO READINESS
site.llms-txt-no-mcp

llms.txt does not mention the MCP server

Detects: The domain advertises an MCP server in its AI Catalog, but llms.txt never points at it.

Fix: Add a "## MCP" section with links to the endpoint and its Server Card. llms.txt has no official MCP field; this is a convention, not a requirement.

$ node scanner/bin/webmcp-scan.js https://your-site.com --check site.llms-txt-no-mcp
INFO READINESS
site.description-missing

No meta description

Detects: There is no meta description, so each consumer writes its own summary.

Fix: Add one sentence that says what the page offers.

<meta name="description" content="CV and projects of Jane Doe, senior backend engineer (Go, Postgres).">
$ node scanner/bin/webmcp-scan.js https://your-site.com --check site.description-missing
INFO READINESS
site.canonical-missing

No canonical link

Detects: No rel="canonical", so www/non-www and http/https spellings compete as separate pages.

Fix: Declare the one URL you want indexed.

<link rel="canonical" href="https://example.com/">
$ node scanner/bin/webmcp-scan.js https://your-site.com --check site.canonical-missing

No structured data

Detects: No schema.org JSON-LD on the page.

Fix: Add a JSON-LD block that names the entity the page is about (Person, Organization, Article, Product).

<script type="application/ld+json">{"@context":"https://schema.org","@type":"Person","name":"Jane Doe","jobTitle":"Backend engineer","url":"https://example.com/"}</script>
$ node scanner/bin/webmcp-scan.js https://your-site.com --check site.structured-data-missing
INFO READINESS
site.open-graph-missing

No Open Graph tags

Detects: No og:title/og:description/og:image, so shared links render without a preview.

Fix: Add og:title, og:description and og:image to the page head.

<meta property="og:title" content="Jane Doe">
<meta property="og:description" content="Backend engineer, Berlin">
<meta property="og:image" content="https://example.com/card.png">
$ node scanner/bin/webmcp-scan.js https://your-site.com --check site.open-graph-missing
INFO READINESS
site.lang-missing

No lang attribute

Detects: <html> declares no language.

Fix: Add lang to the html element.

<html lang="en">
$ node scanner/bin/webmcp-scan.js https://your-site.com --check site.lang-missing

Want an agent to fix all of this for you?

Copy the instructions on the For agents page into your repository. Claude Code, Codex or Cursor will generate the files and re-run the audit.

Open For agents