{
 "name": "MCP server — MCP Endpoint Lint",
 "what": "Point it at any MCP endpoint: handshake and version negotiation, JSON-Schema validity of every advertised tool, JSON-RPC error conformance, and the discovery documents a client looks for before it dials. One call scores it out of 100. No key, no signup.",
 "url": "https://www.pathwren.workers.dev/mcp-lint.json",
 "twin_of": "https://www.pathwren.workers.dev/mcp-lint.html",
 "page": {
  "path": "/mcp-lint.html",
  "url": "https://www.pathwren.workers.dev/mcp-lint.html",
  "type": "text/html"
 },
 "generated_at": "2026-09-02T12:23:39+00:00",
 "generated_from": "the bytes of /mcp-lint.html, by surfaces/ai-crawler-index/build.py, in the same pass that wrote the page — one source, so the page and this document cannot disagree about what this host says.",
 "license": {
  "document": "CC0-1.0",
  "url": "https://creativecommons.org/publicdomain/zero/1.0/"
 },
 "access": {
  "api_key": "none",
  "account": "none",
  "rate_limit": "none",
  "cors": "*",
  "auth": "none — every document here is a public GET"
 },
 "commands": [
  "# no arguments, nothing to sign up for: the score card, run against this server's own",
  "# built-in broken fixture, so you can read the output shape before pointing it anywhere",
  "curl -s https://www.pathwren.workers.dev/mcp/lint \\",
  "  -H 'content-type: application/json' -H 'accept: application/json, text/event-stream' \\",
  "  -d '{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"tools/call\",\"params\":{\"name\":\"example\",\"arguments\":{}}}' \\",
  "  | jq -r '.result.structuredContent.answer.verdict | .score, .grade'",
  "35",
  "F",
  "# your endpoint, the real thing",
  "curl -s https://www.pathwren.workers.dev/mcp/lint \\",
  "  -H 'content-type: application/json' -H 'accept: application/json, text/event-stream' \\",
  "  -d '{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"tools/call\",\"params\":{\"name\":\"score_card\",",
  "       \"arguments\":{\"url\":\"https://mcp.example.com/mcp\"}}}' \\",
  "  | jq -r '.result.structuredContent.findings[] | \"\\(.severity)\\t\\(.code)\\t\\(.detail)\"'"
 ],
 "sections": [
  {
   "heading": "MCP Endpoint Lint — MCP server",
   "text": [
    "An MCP endpoint that answers is not an MCP endpoint that works. The failures that cost you users are quiet ones: a server that ignores the protocolVersion the client asked for and answers in its own, a tool whose required names a property its schema does not define, an unknown method that returns an HTML 500 where -32601 belongs. None of those is an outage. Each one makes some clients fail closed, silently, and you find out from a support ticket months later."
   ],
   "commands": [
    "# no arguments, nothing to sign up for: the score card, run against this server's own",
    "# built-in broken fixture, so you can read the output shape before pointing it anywhere",
    "curl -s https://www.pathwren.workers.dev/mcp/lint \\",
    "  -H 'content-type: application/json' -H 'accept: application/json, text/event-stream' \\",
    "  -d '{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"tools/call\",\"params\":{\"name\":\"example\",\"arguments\":{}}}' \\",
    "  | jq -r '.result.structuredContent.answer.verdict | .score, .grade'",
    "35",
    "F",
    "# your endpoint, the real thing",
    "curl -s https://www.pathwren.workers.dev/mcp/lint \\",
    "  -H 'content-type: application/json' -H 'accept: application/json, text/event-stream' \\",
    "  -d '{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"tools/call\",\"params\":{\"name\":\"score_card\",",
    "       \"arguments\":{\"url\":\"https://mcp.example.com/mcp\"}}}' \\",
    "  | jq -r '.result.structuredContent.findings[] | \"\\(.severity)\\t\\(.code)\\t\\(.detail)\"'"
   ],
   "tables": [],
   "links": [
    "https://www.jsonrpc.org/specification#error_object"
   ]
  },
  {
   "heading": "Tools",
   "text": [],
   "commands": [],
   "tables": [
    {
     "headers": [
      "Tool",
      "What it does"
     ],
     "rows": [
      [
       "handshake_report",
       "POST initialize to an MCP endpoint you name and report what came back: the protocol version it negotiated against the one you asked for, the capabilities it advertises, serverInfo, timing, the transport shape (JSON or SSE frame, session id, content-type), and — sent a second time, byte for byte — whether the server answers an identical initialize the same way. Also asks for a version nobody publishes, to see whether it negotiates down or agrees to anything. Makes 3 requests, 6 s timeout each, user-agent . Example: url='https://mcp.example.com/mcp' — or call with {} to run it against the built-in fixture, which makes no outbound request at all."
      ],
      [
       "tools_list_report",
       "Handshake, then tools/list, then check every tool it returns: a missing or empty description, a description too short to choose on, a missing inputSchema, an inputSchema that is not an object schema, JSON-Schema faults (an invalid type keyword, a `required` naming a property that is not in `properties`, a pattern that will not compile, an empty enum), names that break the character rule, and names that collide — exactly, or once case is ignored. Each finding names the tool and says what breaks. Makes 3 requests, 6 s timeout each. Example: url='https://mcp.example.com/mcp' — or {} for the built-in fixture, whose six tools carry one of each fault."
      ],
      [
       "error_conformance",
       "Send five things a real client eventually sends by accident and check what comes back: an unknown method (expects -32601), tools/call naming a tool that does not exist (expects -32602/-32601 or a result with isError), tools/call with no tool name (expects -32602), a body that is not JSON (expects -32700), and a body missing \"jsonrpc\":\"2.0\" (expects -32600). Reports the HTTP status and the error code side by side, and flags an HTML error page or a 5xx where a JSON-RPC error belongs — the failure that makes a client report the wrong cause. Makes 5 requests, 6 s each. Example: url='https://mcp.example.com/mcp' — or {} for the fixture, which fails two of the five."
      ],
      [
       "discovery_report",
       "GET the documents an MCP client reads BEFORE the handshake and report what each one answered: /.well-known/oauth-protected-resource in BOTH forms — the root form everyone publishes and the RFC 9728 §3.1 insertion form a spec-following client actually requests for a resource served under a path — /.well-known/oauth-authorization-server, /.well-known/mcp, /.well-known/mcp.json, /mcp.json and /llms.txt. Each result carries the status, the verdict (served, missing, gated, soft-404 — a 200 carrying an HTML error page — or invalid JSON) and what a client does with that document. Makes one GET per document, 6 s each. Example: host='mcp.example.com' or host='https://mcp.example.com/mcp' (a path turns on the RFC 9728 insertion-form check) — or {} for the built-in fixture."
      ],
      [
       "score_card",
       "Run handshake_report, tools_list_report, error_conformance and discovery_report against one endpoint and return a single verdict: a score out of 100 (handshake 30, tool schemas 30, error conformance 25, discovery 15), a letter grade, every finding by severity with the section it came from, and the full detail of each section underneath. This is the tool to call from a monitor: the score is stable enough to alert on and the findings say what to fix. Makes up to 19 requests to the URL you give it, 6 s each, and is rate-limited per caller and per target host. Example: url='https://mcp.example.com/mcp' — or {} to score the built-in fixture (it gets a D) with no outbound request at all."
      ],
      [
       "whoami",
       "Takes no arguments. Safe to call. Deterministic. Touches no third party. Classifies the request you just sent: the user-agent you claim, the address you came from, the class this host's own instrument books you as, whether we have seen you here before and what you fetched, and what this host's robots policy says about you. Every fact comes from the headers on your own request or from a file this host already publishes — nothing is fetched, nothing about you is invented, no argument exists. Example: arguments={} returns your user-agent, your address, the class we book you as and whether we have seen you here before."
      ],
      [
       "example",
       "Takes no arguments. Safe to call. Deterministic. Touches no third party. Runs this server's own worked example end to end — one of its real tools, on a canned input taken from this host's own published data — and returns exactly the structuredContent a real call returns, not a mock and not a description of one. Use it to see the shape of an answer before you decide what to send. No URL of yours is fetched and no third party is touched. Example: arguments={} runs it and returns the real answer."
      ]
     ]
    }
   ],
   "links": []
  },
  {
   "heading": "What it checks, and why each one bites",
   "text": [
    "Every finding names the JSON-RPC id it came from, the exact bytes that were wrong, and the consequence for a client — not a severity number on its own.",
    "The handshake (30/100). Does initialize answer, in what protocol version, and is the version the one that was asked for? Version negotiation is the single most common quiet break: the spec says a server that cannot speak the requested version answers with one it can, and a client that receives an unexpected version is entitled to hang up. It also sends initialize twice on two connections, because a second one that fails is a server holding session state that its transport says is stateless — the failure mode behind a working first call and a broken reconnect.",
    "Every advertised tool (30/100). tools/list, then each inputSchema compiled as a real JSON Schema: required naming a property that is not in properties, a type that is not a JSON Schema type, a pattern that does not compile, faults nested inside items. Then the names: characters outside [a-zA-Z0-9_-], two tools whose names differ only by case, a description that is empty. A model picks a tool by reading its description; an empty one is a tool no model will ever choose.",
    "Error conformance (25/100). Five deliberately wrong requests — malformed JSON, an unknown method, an unknown tool, a tool call with a missing required argument, a request with no id — each with the code JSON-RPC 2.0 §5.1 requires. What comes back is compared against it. An HTML error page where a JSON-RPC error object belongs is the finding that turns up most.",
    "Discovery, before anything is dialled (15/100). /.well-known/oauth-protected-resource in both forms — the root form everyone publishes, and the RFC 9728 §3.1 insertion form, where the well-known segment goes between the host and the resource path, which is the URL a spec-following client actually follows after a 401 — plus /.well-known/mcp, /.well-known/mcp.json, /mcp.json and /llms.txt. A 200 that returns HTML is reported as a soft-404, not as served."
   ],
   "commands": [],
   "tables": [],
   "links": [
    "https://www.jsonrpc.org/specification#error_object",
    "https://www.rfc-editor.org/rfc/rfc9728.html#section-3.1"
   ]
  },
  {
   "heading": "What it will not fetch",
   "text": [
    "It refuses to fetch this host. Point it at www.pathwren.workers.dev, at a subdomain of it, at an IP literal, at localhost, at a private name, at a URL carrying credentials, at an unusual port or at a non-HTTP scheme, and it returns a refusal before any socket is opened. Two different reasons, both worth stating plainly: a tool that fetches whatever URL a stranger hands it is an SSRF proxy with a friendly description, and one published by a host that counts its own requests would also be a way to manufacture its own traffic. This host counts arrivals and publishes what it learns; a request our own tool made on someone else's instruction is not a visit, and the cheapest way to never mistake one for the other is to never make it.",
    "A full score card is 18 requests to one host — three for the handshake, three for the tool list, five error probes, seven document fetches — never more, with a 6-second timeout each and a 64 KB body cap. Calls are rate-limited per caller, per target host and per edge location. Your URL is never stored — not in a database, not in a log line, not in a metric. It exists for the length of one request."
   ],
   "commands": [],
   "tables": [],
   "links": []
  },
  {
   "heading": "Every tool answers with no arguments at all",
   "text": [
    "Call any tool with {} and it runs its documented worked example against a fixture built into this server — a deliberately broken MCP endpoint that lives inside the same isolate, never on the network. It fails to negotiate the version it was asked for, returns -32600 on a second initialize, advertises a tool with an empty description and two that collide on case, and serves an HTML page where a JSON-RPC error belongs. Zero-argument calls therefore make no outbound request whatsoever: an agent exploring this server touches nobody. The lint code path is identical either way, so what you see in the fixture output is exactly what your endpoint would get."
   ],
   "commands": [],
   "tables": [],
   "links": []
  },
  {
   "heading": "How is this different from the other five?",
   "text": [
    "agent-discovery-doctor is the other server here that fetches: it asks what documents a host publishes — llms.txt, agent card, mcp.json — and never speaks a protocol. This one speaks JSON-RPC to an MCP endpoint and reports what the protocol did: negotiation, schemas, error codes. Different input (an endpoint URL, not a hostname), different failure caught, no tool name in common — with doctor or with ai-crawler-index, crawler-log-triage, robots-policy-lint or crawler-ip-verifier.",
    "Protocol versions 2026-07-28, 2025-11-25, 2025-06-18, 2025-03-26, 2024-11-05, negotiated per call. server/discover answers for clients on 2026-07-28, initialize for everyone else. Stateless, no key, no session. Listed in the official MCP Registry as dev.workers.pathwren.www/mcp-endpoint-lint."
   ],
   "commands": [],
   "tables": [],
   "links": [
    "/mcp-doctor.html",
    "/mcp.html",
    "/mcp-triage.html",
    "/mcp-robots.html",
    "/mcp-netcheck.html",
    "https://registry.modelcontextprotocol.io/v0/servers?search=mcp-endpoint-lint"
   ]
  }
 ],
 "machine_doors": [
  {
   "url": "https://www.pathwren.workers.dev/tools/?s=client-dossiers",
   "name": "6 keyless GET tools",
   "what": "The read-only MCP tools of this host as plain GET endpoints — no JSON-RPC, no key"
  },
  {
   "url": "https://www.pathwren.workers.dev/documents.json",
   "name": "documents.json",
   "what": "Every document here with its strong ETag and the date its bytes changed"
  },
  {
   "url": "https://www.pathwren.workers.dev/changes",
   "name": "changes",
   "what": "What moved since your cursor — poll this instead of re-downloading anything"
  },
  {
   "url": "https://www.pathwren.workers.dev/llms.txt",
   "name": "llms.txt",
   "what": "The whole map in one text file"
  },
  {
   "url": "https://www.pathwren.workers.dev/openapi.json",
   "name": "openapi.json",
   "what": "Every read endpoint, described formally"
  },
  {
   "url": "https://www.pathwren.workers.dev/.well-known/agent-card.json",
   "name": "agent card",
   "what": "A2A agent card"
  },
  {
   "url": "https://www.pathwren.workers.dev/mcp",
   "name": "mcp",
   "what": "MCP over JSON-RPC (POST)"
  },
  {
   "url": "https://www.pathwren.workers.dev/a2a",
   "name": "a2a",
   "what": "A2A (POST message/send)"
  }
 ],
 "links": [
  {
   "rel": "self",
   "href": "https://www.pathwren.workers.dev/mcp-lint.json",
   "type": "application/json"
  },
  {
   "rel": "describes",
   "href": "https://www.pathwren.workers.dev/mcp-lint.html",
   "type": "text/html",
   "title": "The page this document is the JSON twin of: MCP server — MCP Endpoint Lint"
  },
  {
   "rel": "changes",
   "href": "https://www.pathwren.workers.dev/changes.json?since=105",
   "type": "application/json",
   "title": "What changed since your cursor — poll this instead of re-downloading this document",
   "cursor_param": "since",
   "head_cursor": 105,
   "min_poll_seconds": 21600,
   "how": "Read `cursor` from the response and send it back as `since`. It advances only when something really changed, so an unchanged answer is proof rather than luck — about 2.5 KB, or a 304 with no body if you send back the ETag."
  },
  {
   "rel": "related",
   "href": "https://www.pathwren.workers.dev/documents.json",
   "type": "application/json",
   "title": "Every document here with its ETag and last-modified date"
  },
  {
   "rel": "related",
   "href": "https://www.pathwren.workers.dev/data/agents.json",
   "type": "application/json",
   "title": "Every crawler record in one file"
  },
  {
   "rel": "service-desc",
   "href": "https://www.pathwren.workers.dev/openapi.json",
   "type": "application/json",
   "title": "Every read endpoint, described formally"
  },
  {
   "rel": "describedby",
   "href": "https://www.pathwren.workers.dev/llms.txt",
   "type": "text/plain",
   "title": "The whole map in one text file"
  }
 ]
}