What an MCP annotation is
An MCP server tells an AI application (the host, like an editor or a chat app) which tools it has. For each tool it sends a name, a description, an input schema and, optionally, a few annotations. Two of them matter here: readOnlyHint says the tool does not change anything, and destructiveHint says it may change or remove things.
Hosts can use these hints to decide when to stop and ask the user. A tool that reads build logs can run freely. A tool that rolls back a production deploy should not run because a model guessed it was a good idea.
They are hints, not a security boundary. The MCP spec says clients must treat annotations as untrusted unless they come from a trusted server. But when the server is the vendor’s own official one, the annotations are the only signal the host gets about which tools are dangerous.
What I reported
My scanner reads what a server sends in response to tools/list, the same message a host reads. For CircleCI’s server, tools that trigger pipelines, rerun workflows and roll back deploys came back with no annotations at all.
I checked the server by hand on 7 April and sent the report on 21 April. It had two other findings. I am leaving those out because they are not part of the published advisory.
One detail I got wrong at first
In the MCP schema, destructiveHint defaults to true. So on paper, a host should already treat an unannotated tool as possibly destructive. The real risk is narrower: if a host or agent framework auto-approves tools that carry no annotations, these tools run without a prompt. That is the impact CircleCI describes in the advisory, and it is part of why the severity is medium.
What CircleCI changed
The fix is commit 394581e from 10 July, titled “Add destructive hints for some MCP tools”. It does two things.
First, it adds destructiveHint: true to eight tools: the three in the advisory, a tool that runs evaluation tests, two prompt-template tools, and two tools that write files to disk.
Second, and more interesting to me, it changes how tools are registered. Before, the code that registered each tool with the MCP SDK passed four things: name, description, input schema and handler. There was no slot for annotations, so an annotation written in a tool file would never have reached the client.
- server.tool(
- tool.name,
- tool.description,
- { params: tool.inputSchema.optional() },
- wrappedHandler,
- );
+ const annotations = (tool as { annotations?: Record<string, unknown> }).annotations;
+ if (annotations) {
+ server.tool(tool.name, tool.description, { params: tool.inputSchema.optional() }, annotations, wrappedHandler);
+ } else {
+ server.tool(tool.name, tool.description, { params: tool.inputSchema.optional() }, wrappedHandler);
+ }This is the lesson I keep coming back to. Reading the tool files would not have been enough. You have to look at what actually goes over the wire.
Timeline
- 7 Apr 2026Manual check of the server’s tool list.
- 21 AprReport sent to CircleCI security.
- 22 AprCircleCI confirms receipt and asks which commit I tested.
- 10 JulFix commits land in the public repository.
- 16 JulCircleCI confirms the reported findings are remediated.
- 17 JulVersion bumped to 0.18.0.
- 20 JulAdvisory GHSA-8xjg-jpfh-5257 published with CVE-2026-95854.
What this shows, and what it doesn’t
Shows
- Before the fix, the server sent no annotations for tools that trigger, rerun and roll back pipelines.
- The registration code had no path for annotations to reach the client.
- CircleCI fixed both and published an advisory with a CVE.
Does not show
- That any specific host ran a rollback without asking. I did not test hosts.
- An exploit. A model still has to decide to call the tool.
- That annotations make a server safe. The spec says not to trust them from untrusted servers.
What I would do differently
- Put the exact version and commit in the first email. CircleCI asked for the commit on day one. It should have been there already.
- Read the spec defaults before writing the impact. The default for
destructiveHintchanges how serious a missing annotation is. The impact line should say “if the host auto-approves”, not imply that every host does. - Keep scanner labels out of the severity. My April report used severity words from an early version of the scanner. I now describe what I saw and leave severity to the maintainers.
If you maintain an MCP server
- Mark every tool that writes, deletes, deploys or triggers with
destructiveHint: true, and every pure read withreadOnlyHint: true. - Check what
tools/listreturns, not what your source files say. Wrappers and registration helpers can drop fields. - Add a test that lists the tools through a real client and checks the annotations, so a refactor cannot silently remove them.
const { tools } = await client.listTools();
for (const name of ["run_pipeline", "rerun_workflow", "run_rollback_pipeline"]) {
const tool = tools.find((t) => t.name === name);
expect(tool?.annotations?.destructiveHint).toBe(true);
}Thanks to the CircleCI security team for the fix and the advisory. Sources: the advisory, commit 394581e, the MCP schema, and my report thread with CircleCI. The other findings in the report are not described here.