Answers · MCP security
I build an MCP server. How do I change my tool descriptions or schemas without clients flagging it as a rug pull?
Last updated
TL;DR: Rug-pull defenses hash the tool definitions a user approved and flag any difference, so to a client your honest edit and an attacker's injected instruction look the same. You can't make a change invisible, and you shouldn't try. What you can do is make every change deliberate, versioned and quick to review. Ship definition changes only in releases and bump the server version you report in the initialize result (serverInfo.version). Never vary definitions per user, per session or per experiment, because each variant looks like a new definition to a client that pins hashes. Add a new tool rather than rewording or repurposing one people already approved. If your tool list can change mid-session, declare tools.listChanged and send notifications/tools/list_changed so clients re-fetch it. Publish a changelog that quotes the old and new text. In CI, snapshot your own tools/list and fail the build on any diff nobody reviewed, so no change ships by accident.
What should I check, step by step?
1. Treat every field a client may hash as a published contract: name, title, description, inputSchema, outputSchema and annotations. Clients and scanners hash different subsets. mcp-scan's tool pinning hashes tool definitions, and Rightshold's published fingerprint covers each tool's name, description and inputSchema plus the server's name and version. Assume a one-word edit to any of them will be flagged.
- Snapshot the full tools/list (every page) in CI as canonical JSON with keys sorted (RFC 8785 is one scheme) and fail the build on a diff nobody reviewed. A reviewed change updates the snapshot in the same pull request.
- Change definitions only in versioned releases, and bump serverInfo.version in the initialize result with each one, plus the version in server.json if you publish to the MCP Registry, so every change a client sees matches a version a user can look up.
- Serve identical definitions to every user and session. Don't template user names, dates, locale, feature flags, experiment arms or per-account lists (an enum of the user's projects, say) into descriptions or schemas. Return dynamic values from a read tool instead.
- Add rather than edit: a new tool shows up as one addition, which a client can hold for approval while the tools a user already approved keep working. Editing an existing tool flags the tool people depend on.
- Never reuse a tool name for different behaviour. Deprecate the old tool (say so in its description in one release, remove it in a later one) and give the new behaviour a new name.
- If your tool list can change while a session is open, declare "tools": {"listChanged": true} and send notifications/tools/list_changed when it does. If it can't, don't declare it.
- Publish a changelog entry for each release that changes definitions, quoting the old and new description and schema and saying why. That is what a reviewer reads when their client shows the diff.
- Keep descriptions plain: no zero-width, bidirectional-control or Unicode tag characters, HTML comments or base64 runs. Scanners flag them, and they make the diff unreadable.
- After each deploy, fetch tools/list from the live endpoint and compare it with the CI snapshot. Proxies, i18n layers and config can change what is actually served.
Why do clients flag an MCP server's legitimate tool description change as a rug pull?
Invariant Labs named the rug pull in its April 2025 tool-poisoning research: a server changes its tool descriptions after a user has approved them. Clients and scanners defend against it by recording what was approved and treating any difference as unapproved until a person looks. A hash can't tell intent, so a typo fix and an injected instruction raise the same alarm.
- That's the defense working, not a false positive to engineer around. Aim for every change a client sees to be one you meant to ship, tied to a version and quick to review.
- Annotations count too. The MCP specification says clients must treat tool annotations as untrusted unless they come from trusted servers, and flipping readOnlyHint or destructiveHint changes how a client may gate the tool, so expect a careful client to flag it.
What should MCP server builders avoid when changing tool definitions?
Don't hot-patch descriptions in production to steer model behaviour. Fix the wording in a release, with a version bump and a changelog line.
- Don't rely on the notification or the version bump to make a change acceptable. A remote server can change at any time, so a client's only real signal is comparing what you serve with what it recorded. listChanged and a new version make your change easier to review, not pre-approved.
- Don't localise or personalise descriptions per request. Every variant is a separate definition to a pinning client, and per-session variation is exactly what targeted poisoning looks like.
How do I check what my MCP server's tool definitions look like to clients after a deploy?
Rightshold's tool-definition fingerprint is an open specification at /security/spec: SHA-256 over canonical JSON of the server's name and version and each tool's name, description and inputSchema, sorted by tool name. You can compute it in CI without an account. Because the server version is part of it, a release reads as one change rather than a silent drift.
- precall_check probes your live endpoint and reports whether it is in Rightshold's external registry, whether its definitions match the recorded fingerprint (with a diff of tools added, removed, description changed or schema changed), the result of an invisible-text scan of every definition, and any tool-name collisions with other registered servers. Run it after each deploy. The answer is evidence about what you serve, never a safety rating.
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: “I just deployed a new release of my MCP server. Do the tool definitions it serves now match what was recorded, what exactly changed, and does any definition carry hidden text?”
{
"tool": "precall_check",
"arguments": {
"url": "<your MCP server's https URL>"
}
}- Free, no key. For a server not registered on Rightshold, the fingerprint of what it serves now is computed for the answer and not stored.
- check_tool_integrity re-probes a registered server and answers unchanged, changed (tools added or removed, description or schema changed, and whether the server's name or version changed) or unreachable, which is never reported as unchanged.
- scan_text checks one description before you ship it: zero-width and bidirectional characters, Unicode tag characters, hidden markup and base64 runs, with exact positions and a sanitized copy.
Rightshold doesn't run in your CI, publish your releases or notify your users' clients. precall_check and check_tool_integrity compare what your endpoint serves with a recorded fingerprint. Other clients and scanners hash their own choice of fields, so they may flag edits this fingerprint doesn't cover, such as a title, outputSchema or annotation change.
What are the sources?
- Invariant Labs: MCP Security Notification: Tool Poisoning Attacks (rug pulls, April 2025)
- Invariant Labs: Introducing MCP-Scan (tool pinning by hashing)
- MCP specification 2025-11-25: Tools (listChanged, notifications/tools/list_changed, annotations untrusted)
- MCP specification 2025-11-25: Lifecycle (serverInfo name and version in the initialize result)
- MCP Registry: server.json format (version)
- RFC 8785: JSON Canonicalization Scheme
- Rightshold agent-safety formats: tool-definition fingerprint (section 7)
Related
- How to detect MCP tool description changes (rug pulls)
- How to pin MCP server versions and review changes before upgrading
- Checking your MCP tool descriptions for hidden text before publishing
- Are MCP tool annotations like readOnlyHint a security guarantee?
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}}'
