MCP server security: field notes from reading twenty-six of them
Between August and October 2026 I read the source of twenty-six production Model Context Protocol servers — all shipped by funded companies, all in use, none toy projects. I went in expecting the picture the security-tooling vendors paint: unguarded destructive tools, path traversal everywhere, prompt injection straight through to the model. That is not what I found. The ecosystem has hardened faster than the fear narrative admits, and the gaps that remain are narrower and more specific than “MCP is insecure” suggests.
These are the notes. Where a server does something well I name it, because good work should be citable. Where I found a gap I describe the class and not the product — several of these were disclosed privately to the maintainer, and a public writeup is not the place to burn a team before they have had a chance to fix it.
The servers
Twenty-six production MCP servers, all shipped by funded companies and actively maintained, read between August and October 2026:
Five of these — Sentry, MongoDB, Neon, Grafana and Elastic — are written up in full as public assessments.
What “read in full” means
For each server I cloned the repository, read the tool-registration code and the handlers behind the destructive tools, and — where a claim could be checked by running it rather than by reading it — I ran it. When a server said “read-only”, I looked for the thing that enforced it, not the sentence that promised it. When a resolver claimed to contain writes to a directory, I fed it ../ and an absolute path and watched where the file actually landed.
Finding 1 — tool annotations are now the norm
The single most encouraging result. ToolAnnotations — the machine-readable hints (readOnlyHint, destructiveHint, openWorldHint) that let a client decide which tools to gate — have gone from rare to expected among serious servers.
One cost-management server registers 127 tools, through a wrapper whose TypeScript type makes all three hints non-optional. You physically cannot add a tool to that codebase without declaring whether it is read-only, destructive, and open-world. 57 of those tools mutate state; not one is missing a hint. Not discipline, which fails — a type that refuses to compile without the answer.
A project-management server marks destructive=True on every delete action and ships a conformance test that fails the build if a delete is ever added without it. The guarantee is not “we remembered”; it is “CI won’t let us forget”. A notebook server that literally executes code annotates all eighteen of its tools. An enterprise productivity server derives a read-only mode by filtering the exposed tool list on the readOnlyHint, so a locked-down deployment can’t call a mutating tool because the mutating tools aren’t offered.
The lesson for anyone still building: annotations are table stakes now. Shipping without them no longer reads as “early”, it reads as “behind”.
Finding 2 — path handling is mostly done right
The classic MCP file-write bug is a tool that takes a caller-supplied path, resolves it, and writes — without checking the result stayed inside an allowed directory. I went looking for it specifically, because I have found and fixed it in my own code twice and it is easy to get subtly wrong.
Most servers that touch the filesystem now get it right. The best example: a mobile-automation server whose screenshot tool resolves symlinks first and then checks the resolved path against an allowlist of roots — case-insensitively on Windows, which is the detail people miss. Resolve, then check; check the destination, not the link. It is exactly the shape I arrived at independently in my own file-deletion server — a good sign the practice is converging on something real rather than on one person’s taste.
Finding 3 — where the gaps actually are
They exist, but they are specific, and they cluster in three places.
The README–reality gap. A server states a safety property in prose — “read only by default”, “hard to misuse” — and it is true today but nothing enforces it. No annotation declares it, so a client can’t act on it; and sometimes a full write path sits in the codebase, unreachable but written and waiting, one wired-up call away from making the README false. What is missing is the machine check that keeps the promise real after the next contributor, or the next fork.
Untrusted content returned unmarked. Servers whose purpose is reading the open web frequently return page content into the model’s context with nothing marking it as untrusted. That is the product working as designed; the gap is that openWorldHint exists to say “what comes back is not under your user’s control” and it often goes undeclared. On a tool that reads arbitrary pages, that hint is the difference between a host that treats the result as data and one that treats it as instructions.
The occasional real hole. One server let a caller choose the output path for a written result with no containment at all — a write-anywhere primitive reachable through ordinary use, triggered by content the server was designed to read. That one I reported privately. It was the exception across all twenty-six servers, not the rule, which is the point.
Finding 4 — the gap that is worth money
Put the three findings together and a pattern falls out that matters more than any single bug.
The teams have internalised the practices — annotations, allowlists, least-privilege, read-only modes. What they lack, nearly universally, is independent verification: a second pair of eyes that read the whole thing and wrote down what they found, with a test behind each finding. Their own CI proves their own assumptions. It cannot prove the assumption they didn’t think to make.
That gap is not a criticism. It is the normal state of any codebase that grew faster than anyone had time to audit it. And it is exactly the thing an enterprise customer’s security questionnaire asks about — not “is your code good?” but “who checked it, and can I see what they found?”
And when the answer to that is “a scanner already did”, be careful with it. While I was writing this, an automated index that scores public MCP servers for trust graded one of my own servers as deficient — its three tools flagged as “missing” their annotation hints — on a server that in fact declares all four hints, correctly, on every tool, including the destructive one. The scanner had matched the protocol’s camelCase hint names (readOnlyHint) against the source, which, being Python, spells them in snake_case (read_only_hint) exactly as the SDK requires, and reported them absent. A tool that cannot reliably tell whether a safeguard is even present is in no position to tell you whether it is correct. That is the whole difference between scanning a server and reading one — and, for the person whose customer is asking, it is the difference worth paying for.
Finding 5 — what the wider set added
Reading more servers against the same method sharpened the points above and added a few of its own.
Annotations are common, not universal. Most of the servers I read declare tool annotations, but a few declare none at all. Finding 1 describes the norm among teams that have looked at the problem, not a guarantee.
Server-enforced confirmation is still rare. The clearest example I found of a server refusing to run a flagged tool without an explicit confirmation is MongoDB’s: an elicitation flow, a configurable list of tools that require it, a read-only mode beside it, and over 230 test files behind all of it. Most others label their dangerous tools and trust the host to notice the label. That is the clearest remaining gap in the set.
Access models worth copying. Neon carries read-only in the OAuth scope itself, so a read-only connection is never offered a writing tool. Grafana’s read-only mode removes the write tools from the list instead of leaving them present and refusing. Sentry puts a human approval step at connect time and backs it with roughly 160 test files. Each puts the guarantee in the grant or the surface, not in a check someone has to remember.
Untrusted content, again and again. Servers whose job is fetching content from the open web keep returning it to the model with nothing marking it as untrusted. It is the most consistent gap I found, repeated across servers from different companies.
Good practice worth naming. One hosted search server re-verifies its OAuth token on every request rather than trusting a session set up once, and strips credentials before a request reaches its analytics layer. A code-analysis server passes arguments to its command-line tool as an argument list with no shell, and adds a guard against option injection on top. A scraping platform runs the official MCP conformance suite against two spec revisions. One analytics database enforces read-only on the server side and then checks the query a second time. A password manager spawns its commands without a shell and only from an allowlist.
Across all twenty-six I found exactly one real hole, the unbounded output path above, and it was reported privately. The picture holds: the gap is not that these servers are insecure. It is that almost none of them can show it.
What this means
If you are building an MCP server: the bar has risen. Annotations and correct path handling are no longer things that make you look careful; their absence is a thing that makes you look behind. The good news is the practices are now well established enough to copy from the servers named above.
If you sell software with an MCP server in it: your code is probably fine. The question you will be asked is not whether it is fine, but whether anyone independent can say so. Right now, for almost everyone, the honest answer is no — and that answer is worth closing, because the person asking is usually a customer you are trying to win.
That’s the work I do.
Read the whole server, and write up what I find with a test behind each finding — a document you can forward to whoever asked. If that’s useful, find me at les-k.github.io or lesliekadenge@gmail.com.
Method, in full: twenty-six servers, all org-owned and actively maintained, read across several passes between August and October 2026. Findings come from reading the source and the tests, with the code run where that was possible. Named examples are cited for good practice; gaps are described in the aggregate and, where they rose to the level of a security issue, were disclosed privately to the maintainer before this was written. Corrections welcome.