curl -s https://www.pathwren.workers.dev/mcp-lint.json   # this page, as JSON

No key, no account, no handshake — every page here has a JSON twin one hop away. Machine doors: 6 keyless GET tools · documents.json · changes · llms.txt · openapi.json · agent card · mcp · a2a

MCP Endpoint Lint — MCP server

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.

# 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)"'

Tools

ToolWhat it does
handshake_reportPOST 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_reportHandshake, 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_conformanceSend 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_reportGET 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_cardRun 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.
whoamiTakes 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.
exampleTakes 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.

What it checks, and why each one bites

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.

What it will not fetch

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.

Every tool answers with no arguments at all

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.

How is this different from the other five?

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.