Rightshold™ Sign up

Answers · MCP security

Why would my MCP server get flagged for tool poisoning, and how do I check my tool descriptions for hidden text before I publish?

Last updated

TL;DR: Scanners and cautious clients flag tool definitions that contain text a reviewer can't see, because tool-poisoning attacks hide instructions that way. The usual culprits are zero-width characters, bidirectional controls, Unicode tag characters (U+E0000–U+E007F, which most interfaces render as nothing but models can read), HTML or markdown comments, CSS-hidden markup and long base64 runs, plus instruction-like phrasing aimed at the model, such as "ignore previous instructions" or "don't tell the user". Honest servers trip these by accident. Text pasted from documents, web pages or chat tools can carry zero-width spaces or a byte-order mark, generated docstrings keep HTML comments, and a description that says "always call this tool first" reads like steering. Before each release, dump tools/list and scan every string a model reads: names, titles, descriptions, every description nested in the schemas, enum values and defaults. Fail CI on invisible characters, hidden markup and encoded blobs, and rewrite directive wording into plain statements of what the tool does. scan_text checks for the classes Rightshold publishes in its open spec, free and with no key.

What should I check, step by step?

1. Dump the full tools/list your built server returns (every page) as JSON in CI. Scan the generated output, not just the source, because templating and build steps can add characters.

  1. Scan every string a model reads, not only the top-level description: the tool name and title, each property description inside inputSchema and outputSchema at any depth, enum values and default strings.
  2. Fail the build on invisible code points: zero-width characters (U+200B–U+200D, U+2060, U+FEFF), bidirectional controls (U+202A–U+202E, U+2066–U+2069), tag characters (U+E0000–U+E007F), control characters other than tab, line feed and carriage return, and private-use or unassigned code points.
  3. Remove markup that hides content: HTML comments, markdown reference-style comments, and elements hidden with display:none, visibility:hidden, zero font size or text coloured like its background. Most clients show descriptions as plain text, so markup in them mostly serves to hide things.
  4. Don't put base64 or other encoded blobs in descriptions. If a parameter needs an example, show a short readable one.
  5. Rewrite directive phrasing as facts. Don't write "Always call this before any other tool"; write "Needs a project_id from list_projects." Never instruct the model about other tools or servers, the conversation or the user; text that tries to steer how other tools are used is what tool-shadowing checks look for.
  6. Keep tool and parameter names to the characters the MCP spec recommends (A–Z, a–z, 0–9, underscore, hyphen and dot), which also rules out names that mix Latin letters with look-alike Cyrillic or Greek ones.
  7. Re-run the scan against the live endpoint after deploy, then keep the clean output as your CI snapshot so a later change shows up as a diff.

Why do MCP scanners flag hidden text in tool descriptions?

Invariant Labs' April 2025 tool-poisoning disclosure showed instructions placed in a tool description that the model follows and the user never sees, because client interfaces show a summary or a short name rather than the full text the model reads. Anything that renders as nothing widens that gap, so scanners treat its presence as a finding whatever the intent.

What does a clean, scanner-safe MCP tool description look like?

Plain sentences: what the tool does, what each parameter means, what it returns, any side effects (writes, sends, deletes, spends) and its limits. The MCP spec says there should always be a human able to deny tool invocations, and clear side-effect wording is what lets a client and a user make that call.

How do I scan my MCP tool descriptions for hidden characters before publishing?

scan_text (MCP) or POST /v1/security/scan-text with {"text": "..."} takes up to 100,000 characters. It reports each deterministic finding (invisible characters, hidden markup, base64 blobs, which are decoded and re-scanned one level) with exact code-point positions and a visible excerpt such as ⟨U+200B ZERO WIDTH SPACE⟩, and returns a sanitized copy. Two heuristic classes, mixed-script identifiers and instruction-like language, are labelled heuristic and never certain.

How do I ask Rightshold about this (free, no key)?

Add the MCP server https://www.rightshold.com/mcp to Claude, Cursor, ChatGPT, Grok or VS Code, then ask: “Here is a tool description from my MCP server, exactly as tools/list returns it. Does it contain anything a reviewer can't see, or wording a scanner would read as instructions to the model?”

One-click setup

{
  "tool": "scan_text",
  "arguments": {
    "text": "<the description, or the whole tools/list JSON>"
  }
}

scan_text finds the classes in Rightshold's published spec. It doesn't judge what a tool does, review your code, or predict how a particular client or third-party scanner will score your server.

What are the sources?

Related

Not legal advice. JSON · Markdown · All answers

Get a free API key in one call

No card and no email needed. 100 free credits; the license and policy checks stay free without a key.

curl -s -X POST https://www.rightshold.com/v1/accounts/signup -H "Content-Type: application/json" -d '{"accept_terms":true}'
export RIGHTSHOLD_API_KEY=rs_...   # the api_key from the reply (shown once)
curl -s https://www.rightshold.com/v1/licenses/check -H "Authorization: Bearer $RIGHTSHOLD_API_KEY" \
  -H "Content-Type: application/json" -d '{"license":"MIT","intended_use":{"commercial_use":true}}'