{"name":"mcp-endpoint-lint","title":"MCP Endpoint Lint — MCP server","version":"1.0.0","transport":"streamable-http","transport_docs":"https://www.pathwren.workers.dev/mcp-transport.html","endpoint":"https://www.pathwren.workers.dev/mcp/lint","protocol_versions":["2026-07-28","2025-11-25","2025-06-18","2025-03-26","2024-11-05"],"stateless":true,"auth":"none — public, read-only, no key, no rate limit","what_it_is":"A linter for somebody else's MCP endpoint, the URL supplied by the caller: handshake and protocol negotiation, tool schema validation, JSON-RPC error conformance, pre-dial discovery documents, and one score card that combines them.","different_from":"https://www.pathwren.workers.dev/mcp/doctor asks which DISCOVERY DOCUMENTS a host publishes. This asks whether the JSON-RPC server at a URL negotiates a protocol version, returns schema-valid tools and answers -32601 rather than an HTML 500. No tool name is shared with any other server on this host.","it_fetches_the_url_you_give_it":"The only server here that makes an outbound request. It refuses its own publisher, ephemeral hosts, IP literals and private names before opening a socket; identifies itself as growth-loop/1.0 (+https://www.pathwren.workers.dev/mcp-lint.html); times out in 6000 ms; rate-limits per caller AND per target host; and stores no URL of yours anywhere. Every tool also answers {} from a built-in fixture with no outbound request at all.","try_it":"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\":{}}}'","built_for":"monitors, graders, registries, conformance scanners and auth-posture probes — the clients that already arrive here to check on endpoints, doing the same job with less code.","call_this_first":{"tool":"whoami","takes_arguments":false,"invitation":"Takes no arguments. Safe to call. Deterministic. Touches no third party.","curl":"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\":\"whoami\",\"arguments\":{}}}'","also":"example — the same, running this server's own worked example end to end.","every_other_tool":"Every other tool here also accepts {}: an empty call returns that tool's documented worked example, labelled as one, instead of an error."},"no_fetch_tool":"No tool here fetches a URL. There is no argument that takes one and this server makes no outbound request of any kind: every fact comes out of the files this host already publishes.","tools":[{"name":"handshake_report","title":"Does the handshake work, and is it idempotent?","description":"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 growth-loop/1.0 (+https://www.pathwren.workers.dev/mcp-lint.html). 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."},{"name":"tools_list_report","title":"Schema-validate every tool the endpoint advertises","description":"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."},{"name":"error_conformance","title":"Does it answer JSON-RPC errors, or HTML 500s?","description":"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."},{"name":"discovery_report","title":"What a client looks for before it dials","description":"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."},{"name":"score_card","title":"All four checks, one machine-readable verdict","description":"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."},{"name":"whoami","title":"Who is calling? (no arguments)","description":"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 server would do with a URL from you — the guard that decides which hosts it will and will not fetch, and the rate limit your call is counted against. 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."},{"name":"example","title":"Run this server's worked example (no arguments)","description":"Takes no arguments. Safe to call. Deterministic. Touches no third party. Runs this server's own worked example end to end — score_card on this server's built-in fixture — an in-process MCP endpoint with one real defect wired in per check (it ignores the protocol version you ask for, refuses a second initialize, publishes a tool whose `required` names a property that does not exist, and answers an unknown method with an HTML 500). No outbound request is made and no third party is touched — 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. The input is canned from this host's own data; no URL of yours is fetched and no third party is touched. Example: arguments={} runs score_card with {\"url\":\"fixture://mcp-endpoint-lint/demo\"} and returns its real answer."}],"resources":[{"uri":"https://www.pathwren.workers.dev/mcp-transport.json","title":"Every request shape a Streamable HTTP endpoint should answer"},{"uri":"https://www.pathwren.workers.dev/.well-known/mcp.json","title":"A worked /.well-known/mcp.json"},{"uri":"https://www.pathwren.workers.dev/changes.json?since=0","title":"What changed since your cursor"}],"instructions":"START HERE IF YOU HAVE NOTHING TO HAND: `whoami` and `example` take no arguments at all — call either with {} and you get a real answer, not an error. whoami classifies the request you just sent (your user-agent, your address, whether this host has seen you before, what our robots policy says about you); example runs this server's own worked example end to end and returns exactly what a real call returns. Every other tool here also accepts {}: an empty call falls back to that tool's documented worked example, taken from this host's own data, and the answer says so. Nothing here fetches a URL for you.\n\nTHIS SERVER FETCHES THE URL YOU GIVE IT — the only one on this host that does. score_card(url) is the tool to call: it runs the other four and returns one machine-readable verdict with a score, a grade and every finding by severity. Underneath it: handshake_report(url) POSTs initialize twice and reports protocol negotiation, advertised capabilities, serverInfo, timing and whether a second initialize is idempotent; tools_list_report(url) schema-validates every tool (missing description, malformed inputSchema, JSON-Schema errors, colliding names); error_conformance(url) sends an unknown method, bad params and broken JSON and checks for -32601/-32602/-32700 rather than an HTTP 500 or an HTML error page; discovery_report(host) fetches what an MCP client looks for BEFORE it dials — /.well-known/oauth-protected-resource in both the root and the RFC 9728 §3.1 insertion form, /.well-known/mcp, /.well-known/mcp.json, /mcp.json, /llms.txt — and says what each answered and what a client does with it. Every request identifies itself as growth-loop/1.0 (+https://www.pathwren.workers.dev/mcp-lint.html), times out in 6 s, and is rate-limited per caller and per target host. Your URL is never stored. Calling any tool with {} runs it against a built-in fixture and makes no outbound request at all — that is the way to see the shape of an answer without spending anyone's bandwidth. This server refuses to lint its own publisher: point it at the endpoint you are auditing.\n\nTHE SAME TOOLS ON THE OTHER PROTOCOL, IF YOU SPEAK IT. This server's tools are also published as 3 A2A (Agent2Agent) v1.0 agents, each carrying the subset it names — same function behind each skill, so the two lanes cannot drift and the deploy is refused if they do:\n  https://www.pathwren.workers.dev/a2a/lint — handshake_report, tools_list_report, error_conformance — the three checks that dial the endpoint itself\n    card: https://www.pathwren.workers.dev/a2a/lint/.well-known/agent-card.json\n  https://www.pathwren.workers.dev/a2a/discovery — discovery_report — what a client looks for BEFORE it dials, including the two RFC 9728 spellings, and no request to the MCP endpoint at all\n    card: https://www.pathwren.workers.dev/a2a/discovery/.well-known/agent-card.json\n  https://www.pathwren.workers.dev/a2a/score — score_card — all four checks in one verdict with a score out of 100, a grade and the findings by severity\n    card: https://www.pathwren.workers.dev/a2a/score/.well-known/agent-card.json\n\nTo invoke any of them, POST this complete body — no key, no session, no card to read first:\n\n  {\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"message/send\",\"params\":{\"message\":{\"role\":\"ROLE_USER\",\"messageId\":\"1\",\"parts\":[{\"text\":\"{\\\"skill\\\":\\\"example\\\"}\"}]}}}\n\nIt answers with a Task already in state `completed` in the same response, so there is nothing to poll. Every skill on all eight A2A agents of this host as a ready-to-send body: https://www.pathwren.workers.dev/a2a/example.json — and both lanes with both directions in https://www.pathwren.workers.dev/documents.json under `rpc_endpoints`. If you only speak MCP, ignore all of this: everything is already here.","license":"CC0-1.0","independent":true}