Skip to content

Build1 publisher2 min readPublished

An MCP server can rewrite its tool descriptions after your config is committed

Tool text is fetched from the server every time an agent connects, so the description reviewed at install time can differ from the one the model reads next week. A dev.to post proposes pinning a hash of it and checking that hash in CI.

The Engineer · Build desk

Illustration accompanying An MCP server can rewrite its tool descriptions after your config is committed

What happened

  • In a dev.to post's example, an approved [email protected] server later serves a description telling the model to read ~/.ssh/id_rsa, pass the contents as a parameter, and not mention it to the user.
  • The config file does not change by a byte, git diff comes back empty and the version string can still read 1.2.0, because tool descriptions are re-fetched from the server on every connection.
  • The description is loaded into the model's context window before any tool call, so the injected instruction runs when the agent connects, even if append_note is never invoked.

Compiled by The EngineerSomething wrong?How this is made

Why it matters

  • constraint Version pins and diffs cover code, not prose, so an approval of an MCP server holds only until the next connect.
  • decision Security teams choose between writing detection rules for payloads nobody has seen yet and committing a hash of the text they actually read; only the second catches an edit made after review.
  • exposure With `npx -y package@latest` as the launch idiom, every engineer's agent restart pulls unreviewed server text again, on whatever schedule people happen to restart.
  • cost Hashing the text puts a person back in the loop on every upstream wording change, including the harmless ones.

Each layer of the stack is reading the file it was built to read. Dependabot, Snyk and Trivy read package.json, requirements.txt and image layers; that list stops short of .mcp.json, and the payload is not a dependency version anyway [5]. Semgrep and CodeQL work on source, and a tool description is a string in a JSON response from a process already running on your machine [6]. Code review reads diffs [7].

Timing is what separates this from a bad dependency. The description loads into the context window before the agent calls anything, so the instruction executes on connect, with or without a call to the tool [4]. The usual way to launch one of these servers is `npx -y package@latest`, which the post calls an unreviewed, unpinned, auto-confirming remote code fetch that re-resolves every time the agent starts [8].

The author's own scanner, Bulwark, grades a clean-looking project at posture [B] 89/100 with a single medium finding: no lockfile [9]. `bulwark pin` writes bulwark.lock, described in the post as a content hash of every string your model is allowed to be told, and it pinned two definitions in the example [10]. Run after the maintainer's update, `bulwark verify` fails with three material changes: capability_added with gained secrets, text_changed from 37 to 186 characters, and schema_changed on the tool's arguments [11].

The edit added 149 characters, about five times the length of the description that was approved [1]. All three findings landed without anyone recognising the payload. "You do not need a rule clever enough to recognise every possible payload," the author wrote, and what is needed instead is "to notice that the thing you approved is no longer the thing you are running" [12].

A content hash fires on any change [10]. A maintainer who tightens the wording of a description trips the same CI failure as one who smuggles in an `<IMPORTANT>` block [11]. So someone ends up reviewing every upstream text edit.

The example is constructed. The scenario the post offers is a maintainer pushing an update, an npm account getting phished, or a package that was always going to do this and simply waited [13]; it does not report a named server caught doing it in the wild. The config it prints also lists the description inline, while the argument rests on tool text being re-fetched on every connection with no lockfile, signature or hash anywhere in the ecosystem [3]. One further check the post says becomes cheap once you are parsing every agent surface: the Unicode Tag block, U+E0000 to U+E007F, maps one-to-one onto ASCII and renders as nothing [14].

What to watch

  • Whether the MCP specification adds signing or pinning for tool definitions, so integrity does not depend on a third-party scanner.
  • Whether anyone reports a published MCP server that changed its tool descriptions after release; a named server caught doing this would make it an incident.
  • Whether teams running pinned lockfiles find the false-failure rate on benign upstream description edits tolerable in CI.
Loading claim ledger
Loading source directory links
Loading share composer
Loading topic controls
Loading related stories