I Read the Source of 50 MCP Servers. 34 Set No Tool Annotations, and One README Tells the Client to Ask First.
Aug 15, 2026 · 7 min read · by Jordan Kwan
TL;DR: Almost none of them. The MCP spec ships four optional tool annotations, readOnlyHint, destructiveHint, idempotentHint and openWorldHint, so a client can tell a lookup from a deletion and prompt you before the second one. On August 15, 2026 I read the source and README of 50 widely-listed MCP servers. 34 of 50 set no annotation of any kind. All 13 archived reference servers set none, including the ones exposing merge_pull_request and slack_post_message. Exactly 1 of 50 READMEs documents a confirmation step before execution, and 1 of 50 gives least-privilege credential guidance.
I have already audited what the official registry knows about these servers: whether there is a repository, whether the link resolves, whether anyone published a security policy. That was about paperwork. This is a different question with a harder edge. Assume the code is fine and the maintainer is honest. Does the server tell your client which of its tools can destroy something?
What is the annotation supposed to do?
The tool annotations are four optional booleans a server attaches to each tool. I read the canonical schema rather than the prose docs, because the prose does not enumerate them. The defaults are the interesting part:
readOnlyHint, default falsedestructiveHint, default true, and meaningful only whenreadOnlyHintis falseidempotentHint, default falseopenWorldHint, default true
The spec is direct about what these are for. Its tools page says "there SHOULD always be a human in the loop with the ability to deny tool invocations," tells applications to "present confirmation prompts to the user for operations," and tells clients they "SHOULD prompt for user confirmation on sensitive operations." Annotations are how a client knows which operations those are without reading English tool descriptions with a language model.
The same page also says: "clients MUST consider tool annotations to be untrusted unless they come from trusted servers." So this is not a security boundary. It is a labelling system, and a malicious server can lie in it. Labels are not the control anyway: 10 of 18 agent products now ship a deterministic limit that holds after the model has already believed the attacker. That is precisely why the interesting failure is not lying. It is silence.
What did I count, and how?
Fifty servers, chosen by a rule you can rebuild. The 7 active reference servers listed under "Reference Servers" in modelcontextprotocol/servers, the 13 archived ones now in servers-archived, and the first 30 alphabetical entries from the "Official Integrations" section of that same README. That section no longer exists at HEAD, since the README now points you at the official registry instead, so I pinned commit a080f401, which still lists 143 of them. Alphabetical order means entries 21 to 50 skew A through C: a nameable sample, not a popularity-weighted one.
For each, I downloaded the source tarball and searched the real source for the four hint names, their snake-case Python equivalents, and ToolAnnotations, excluding vendored code and tests. Then I fetched each README and searched for a documented confirmation requirement and for least-privilege credential guidance, and read every hit by hand.
Nothing was installed, run, or invoked. Every request above fetches a public file.
Two traps are worth naming, because they will produce wrong numbers in anyone else's count. A case-sensitive search for readOnlyHint undercounts every Go, C#, Java and Kotlin server, because those SDKs capitalize: bitrise-io/bitrise-mcp looked like a zero and is in fact the sample's most thorough annotator, setting all four hints across 81 tool files. And a string search misses abstraction: Azure/azure-mcp declares a ToolMetadata class with ReadOnly and Destructive properties and maps them in one loader, so 137 commands are annotated while the literal strings barely appear near a tool definition.
What did the count find?
| Check (n=50, read 2026-08-15) | Result |
|---|---|
| Set at least one annotation in source | 16 (32%) |
| Set none | 34 (68%) |
| Active reference servers annotating | 6 of 7 |
| Archived reference servers annotating | 0 of 13 |
| Official-integration servers annotating | 10 of 30 |
| README documents a confirmation step | 1 of 50 |
| README documents least-privilege credentials | 1 of 50 |
| README publishes a per-tool annotation table | 1 of 50 |
The archived column is the one to sit with. Those 13 were the canonical examples for most of MCP's life, and they are what a lot of third-party servers were copied from. The archived GitHub server exposes create_or_update_file, push_files, merge_pull_request and nine more mutating tools with zero annotations. The archived Slack server exposes slack_post_message; the archived SQLite server exposes write_query. None of them tell a client that any of that is different from a read.
Of the 33 servers where I could resolve both the tool inventory and the hint status, 24 expose a write, delete or send tool, and 10 of those 24 annotate none of it. I am not putting that ratio in the headline, because the 17 servers whose tool inventories I could not fully enumerate are all in the no-hint group, which biases that subset toward annotators. The clean number is 34 of 50.
The four singletons are the shape of the ecosystem. CircleCI's server is the only README documenting a gate: step 7 of its flow is "Confirmation, summarizes and confirms before execution." Alibaba Cloud's AnalyticDB MySQL server is the only one scoping the credential, running execute_sql in read-only mode by default and requiring ENABLE_SQL_WRITE_TOOLS=true to write. The reference filesystem server is the only one publishing a per-tool table of which hint it sets for which tool. And Aiven's server is the only one that just says it plainly: "This MCP server can create, modify, and delete Aiven services and data on your behalf. AI agents may execute destructive actions (dropping databases, deleting services, producing messages) based on their interpretation of your prompts."
Does anything actually read these annotations?
Partly, and less than you would hope. I checked VS Code's MCP documentation and it never mentions annotations, readOnlyHint or destructiveHint at all. What it says is "you might be asked to confirm each tool invocation," and that "when sandboxing is enabled, tool calls from the server are auto-approved." That is confirmation by tool identity and by sandbox, not by declared destructiveness.
The best consumer I found is a server consuming its own hints. Azure's loader filters the advertised tool list on ReadOnlyHint when a --read-only flag is set, so non-read-only tools are never exposed to the model. That is a stronger control than a prompt, and it only works because the server annotated itself first.
Now look back at the defaults. destructiveHint defaults to true. A strict reading says an unannotated write tool is already declared destructive, so a rigorous client should prompt on all of them. The real problem is the other half: readOnlyHint defaults to false, so an unannotated server's genuinely safe lookups are indistinguishable from its deletions. Everything looks maximally dangerous, which is exactly as useless for gating as everything looking safe. Clients respond by prompting on everything or nothing, and both roads end at a human clicking approve without reading.
What does this not prove?
It does not prove any of these servers is unsafe. A missing annotation is a missing label. mcp-server-fetch sets none and has nothing to annotate, several unannotated servers are read-only by construction, and a maintainer who skipped an optional field has not shipped a vulnerability.
It also does not measure deployment. "Exposes a write tool" and "is reachable without authentication" are separate risks and only their intersection is alarming; I measured the first, not the second. I am deliberately quoting no scanner-derived prevalence figure, because the published tool-poisoning numbers disagree by more than a factor of ten and I could not trace the false-positive rate that circulates beside them to a source I could read. A scanner hit count is not a breach rate.
What the count does show is that the safety metadata in MCP is optional, mostly unset, and mostly unconsumed, which means the thing standing between an agent and a merge_pull_request is the same thing that failed when an agent deleted a production database in nine seconds: a sentence in a prompt. That is a documentation artifact, not a control, and nobody's terms of service will cover you when it goes wrong. Setting four booleans costs a maintainer an afternoon. Thirty-four of fifty have not spent it.
Written by Jordan Kwan, founder of Reachium.
I build Reachium, the LinkedIn outreach platform behind the tactics you just read. Same brain, live product.
See what Reachium does ↗