---
title: "MCP server — MCP Endpoint Lint"
description: "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."
canonical: "https://www.pathwren.workers.dev/mcp-lint.html"
url: "https://www.pathwren.workers.dev/mcp-lint.md"
format: "markdown"
source: "the bytes of /mcp-lint.html, in the build that wrote the page"
generator: "surfaces/ai-crawler-index/build.py"
generated: "2026-09-03T07:55:35+00:00"
license: "CC0-1.0"
---

# MCP Endpoint Lint — MCP server

> 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.

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`](https://www.jsonrpc.org/specification#error_object) 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.

```text
# 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
```

```text
# 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

| Tool | What it does |
| --- | --- |
| `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. |

## 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.

- **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](https://www.jsonrpc.org/specification#error_object) 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](https://www.rfc-editor.org/rfc/rfc9728.html#section-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.

## 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](https://www.pathwren.workers.dev/mcp-doctor.html) 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](https://www.pathwren.workers.dev/mcp.html), [crawler-log-triage](https://www.pathwren.workers.dev/mcp-triage.html),
[robots-policy-lint](https://www.pathwren.workers.dev/mcp-robots.html) or
[crawler-ip-verifier](https://www.pathwren.workers.dev/mcp-netcheck.html).

Protocol versions 2025-06-18, 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](https://registry.modelcontextprotocol.io/v0/servers?search=mcp-endpoint-lint) as `dev.workers.pathwren.www/mcp-endpoint-lint`.

## Sitemap

- [Full sitemap (XML)](https://www.pathwren.workers.dev/sitemap.xml) — every page, with dates
- [Full sitemap (markdown)](https://www.pathwren.workers.dev/sitemap.md) — the same map, readable
- [llms.txt](https://www.pathwren.workers.dev/llms.txt) — the whole host in one text file
- [documents.json](https://www.pathwren.workers.dev/documents.json) — every document, with its ETag
- [A2A agents](https://www.pathwren.workers.dev/a2a.html)
- [About and method](https://www.pathwren.workers.dev/about.html)
- [API](https://www.pathwren.workers.dev/api.html)
- [Changelog](https://www.pathwren.workers.dev/changelog.html)
- [Compliance](https://www.pathwren.workers.dev/compliance)
- [Contact](https://www.pathwren.workers.dev/contact)
- [Impressum · Anbieterkennzeichnung](https://www.pathwren.workers.dev/impressum)
- [AI Crawler Index](https://www.pathwren.workers.dev/index.html)
- [No model runs here](https://www.pathwren.workers.dev/inference.html)
- [Legal](https://www.pathwren.workers.dev/legal)
- [MCP server](https://www.pathwren.workers.dev/mcp-doctor.html)
- [MCP server](https://www.pathwren.workers.dev/mcp-lint.html)
- [MCP server](https://www.pathwren.workers.dev/mcp-netcheck.html)
- [MCP server](https://www.pathwren.workers.dev/mcp-robots.html)
- [MCP transport: the GET and HEAD leg](https://www.pathwren.workers.dev/mcp-transport.html)
- [MCP server](https://www.pathwren.workers.dev/mcp-triage.html)
- [MCP server](https://www.pathwren.workers.dev/mcp.html)
- [Packages](https://www.pathwren.workers.dev/packages.html)
- [Pricing](https://www.pathwren.workers.dev/pricing)
- [Privacy](https://www.pathwren.workers.dev/privacy.html)
- [API reference](https://www.pathwren.workers.dev/reference)
- [Access, keys and sign-up](https://www.pathwren.workers.dev/register)
- [Security posture](https://www.pathwren.workers.dev/security.html)
- [Upstream status](https://www.pathwren.workers.dev/status.html)
- [Terms of use](https://www.pathwren.workers.dev/terms.html)
- [Trust](https://www.pathwren.workers.dev/trust)

## Machine copies of this page

- [HTML (canonical)](https://www.pathwren.workers.dev/mcp-lint.html)
- [JSON](https://www.pathwren.workers.dev/mcp-lint.json)
- [Markdown](https://www.pathwren.workers.dev/mcp-lint.md) — this document

This document is a markdown rendering of [https://www.pathwren.workers.dev/mcp-lint.html](https://www.pathwren.workers.dev/mcp-lint.html), generated from that page's own bytes in the same build. The HTML page is canonical.
