Handshake and inventory
Connect, list tools, lint the schemas. A server with no tools exits here without spending anything on probes.
Is there anything here at all?
MCP server vetting for agents
Adding an MCP server to Claude Code is one line of JSON. After that line, a server you have never audited describes its own tools to your agent, and your agent believes the description.
Graded by being used, not by being read. Do not trust the letter. Replay the tape.
Every scanner reads the manifest. None of them use the server.
Reading catches a missing field or a stale protocol version. It does not catch the thing that actually costs you: an agent picking the wrong tool because two descriptions say nearly the same thing, or burning three turns on an error that never names the field it is complaining about.
How a grade is built
Weighted 30, 50 and 20. The layer that only reads the manifest is deliberately the smallest of the three.
Wraps the mcpscore CLI: protocol version, TLS, schema
validity, annotations, pagination. Its raw output is a score over a
moving denominator, because rules get skipped per server, so
it is normalised before anything is compared. A server scoring 78 out
of 91 is worse than one scoring 64 out of 73, and only the normalised
number shows it.
A real agent, a pinned model, temperature 0, three runs per probe. The probes drive the server the way your agent would and score what actually happens. This is the half of the grade that reading cannot produce, and it is the reason the rest of this exists.
The same cold task, re-run with the drafted recipe in hand. Scored as recovered headroom rather than raw point difference, so a server that was already strong is not punished for having little room left to improve.
A layer that did not run is not a zero. If the guidance delta was never measured, its weight is removed and the other two renormalise to 37.5 and 62.5. Scoring an unmeasured layer as zero would drag every honest partial audit into a failing band, which is a way of lying with arithmetic.
A grade is relative to the model that produced it. The model id is stored on the audit, printed in the report, and rendered on the badge. Grades from different models are not comparable, and the report says so rather than leaving you to find out.
What gets measured
Two of them do not always run. A probe that did not apply is recorded with its reason and left out of the average, rather than counted as a failure the server never earned.
Connect, list tools, lint the schemas. A server with no tools exits here without spending anything on probes.
Is there anything here at all?
A fresh agent, the server's own descriptions, and a task built from what you said you needed it for. Scored on first-try tool choice, first-try schema validity, completion, and how much wandering it took.
Can an agent that has never seen this succeed on the first try?
Fires only when two tool descriptions genuinely overlap. The task is worded in the wrong tool's vocabulary, so only a description that draws a real line will save it.
Do these two descriptions actually distinguish themselves?
Omit a required parameter on purpose, then measure the error. What is graded here is the message, not the agent. "Invalid input" and "missing required repoName (string, e.g. facebook/react)" are the same failure and completely different products.
Can an agent fix itself from this error in two turns?
Two steps, where the second must consume a real value the first returned. Servers that look fine tool by tool fall over here, when an id comes back under a different name than the next tool asks for. Skipped under three tools.
Do these outputs compose, or only look like they should?
Scan-only. Reads every description and schema string an agent sees before choosing a tool, looking for content shaped like an instruction rather than documentation. It never calls a tool: we do not execute a server to find out whether it is hostile.
Is this documentation, or is it giving my agent orders?
Two findings cap a grade at F outright: injection-shaped content in the advertised strings, and a transport that is not TLS. A hard fail overrides everything else, and a hard-failed server with a 90 percent static result is still an F.
Because an F is a public accusation about somebody else's software, the sniffer is deliberately conservative, and every hit records both the pattern that matched and the offending text, verbatim, so the claim can be checked rather than taken on trust.
The part that runs on your machine
The grading service is the interesting half. The hook is the half that protects you. It runs before every MCP tool call, reads a local file, and blocks anything it cannot vouch for.
> summarise what the acme-helper server can tell us about this repo · calling mcp__acme_helper__search BLOCKED by doorman doorman: 'acme_helper' is UNKNOWN. Blocking until it has been graded. An ungraded MCP server is not a trusted one. Nothing about 'mcp__acme_helper__search' has been verified: not its tool descriptions, not its error handling, not whether its descriptions contain instructions aimed at you. To grade it: /vet <server-url> > /vet https://acme-helper.example.com/mcp "search our internal docs" queued · claimed by runner · mcpscore 64/73 = 87.67% probe cold_open 55 · ambiguity 33 · bad_input 45 · chain skipped VERDICT: C (61.4/100) Why: agent reached for 'search_all' when 'search_docs' was the fit error for missing 'query' does not name the field 'search_all' and 'search_docs' overlap 41% Recipe drafted: 3 rules. Proposed registry diff below.
Read and exactly one MCP tool. The agent that decides what to trust does not also carry capabilities an untrusted server could talk it into using.Live viewport
Grading does not happen in the browser, and it does not happen in the Worker either. The static layer shells out to a Python CLI, which a Cloudflare Worker cannot do, so the request goes into a queue and a probe runner claims it. The ledger polls every two seconds. There is no websocket here on purpose: streaming would need Durable Objects, and this runs on the free tier.
The URL must be https. A plaintext transport is a hard fail, so it is refused before any work is queued.
Polling every two seconds.
What you get back
Every audit produces the same bundle, hashed with a canonical serialisation so two runs over the same evidence agree on any machine. Change a transcript and the hash changes. That is the only thing that makes an anchor worth anything.