MCP server security: field notes from reading thirteen of them
Over August 2026 I read the source of thirteen 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
Thirteen production MCP servers, all shipped by funded companies and actively maintained, read across August 2026:
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 thirteen 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.
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: thirteen servers, all org-owned and actively maintained, read across three passes in August 2026. Findings verified by running the code where running it 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.