Answers · MCP security
How do I test that my MCP server's tool errors don't leak environment variables or secrets?
Short answer: Plant fake canary secrets, make every tool fail on purpose, and search everything that comes back for the canaries. Start the server with each environment variable it reads set to a unique, obviously fake value (for example RS_CANARY_7f3a9c), and point upstream URLs at a stub that returns 401, 500, timeouts and malformed bodies. Then call each tool with bad arguments and through each failure path, and search the full JSON-RPC responses, both protocol errors and isError tool results, plus the server's stderr and logs, for any canary, Authorization header, token-bearing URL or stack trace. The usual leaks are an HTTP client error serialized whole (axios errors carry the request config, headers included, unless you set its redact option), an upstream URL with an api_key query parameter in the message, a stack trace or a dumped config object. Fix it by returning a short message, a stable error code and a request ID to the model, and logging the detail server-side with secrets redacted. Then keep the canary test in CI.
Checklist
- Inventory what the server can see: every environment variable, config file and token it loads. Each one needs a canary value in the test environment, and real secrets should never be present there.
- Use unique canaries: a distinct, greppable value per secret (a fake sk- key, a fake bearer token, a database URL with a fake password), so a hit tells you exactly which one leaked.
- Force every failure path: invalid and missing arguments, wrong types, oversized input, an upstream stub returning 401, 403, 404, 429 and 500, timeouts, DNS failure, malformed JSON, and a missing file or permission error for filesystem tools.
- Capture everything the client could see: the full JSON-RPC response for protocol errors and for tool results with isError: true, any structuredContent, progress and log notifications, and stderr for stdio servers, since clients may show or store it.
- Search the captures for canaries, Authorization or Cookie headers, URLs with credentials or key parameters, file paths and stack traces; fail the test on any hit.
- Fix at the boundary: wrap each tool handler so unexpected errors become a short message, a stable code and a request ID; never return error.toString() of an HTTP client error or a serialized error object.
- Redact before logging: strip known secret keys and header values from anything written to logs, set axios's redact option or build your own error object, and drop query strings from logged upstream URLs.
- Run it in CI on every change, alongside a test that the server starts without production secrets, so new tools and dependency upgrades can't quietly reintroduce a leak.
Why tool errors are a leak path
- The MCP specification reports tool execution errors inside the tool result with isError: true, and clients should pass them to the model so it can self-correct. Whatever the error text contains goes into the model's context, transcripts and sometimes the user's screen.
- A model that can read secrets from an error can be steered by a prompt injection into repeating them in a later tool call or message. CWE-209 describes this class: an error message that includes sensitive information about the environment, users or associated data.
- The spec's security considerations already say servers must sanitize tool outputs; error results are outputs too.
Where secrets usually hide in errors
- Serialized HTTP client errors: axios's AxiosError.toJSON includes the request config, with headers such as Authorization, unless the request sets the redact option. Returning JSON.stringify(err) sends it all to the model.
- URLs in messages: Python requests' raise_for_status builds messages like "401 Client Error: Unauthorized for url: ...", so an API key passed as a query parameter ends up in the error text.
- Stack traces and debug dumps: framework debug modes, unhandled-exception handlers and "print the config" fallbacks can include file paths, connection strings and environment values.
- Upstream error bodies passed straight through: a provider's error page can echo your request back, headers included.
Ask Rightshold (free, no key)
Add the MCP server https://www.rightshold.com/mcp to Claude, Cursor, ChatGPT, Grok or VS Code, then ask: “Before I connect this MCP server to my agent, is it a listed server, what tools does it expose, and have its tool definitions changed since they were recorded?”
{
"tool": "precall_check",
"arguments": {
"url": "<the MCP server's https URL>"
}
}- The answer combines the external registry entry, a live integrity probe against the recorded tool fingerprint, an invisible-text scan of the definitions and any security flags. It is evidence to decide from, never a finding that a server is safe.
- scan_text checks an error message or tool result for hidden text and instruction-like content before your agent acts on it.
- Rightshold's own API errors follow the pattern above: a stable error code, a request ID and a documentation link, listed in the public catalog at /v1/errors.
Rightshold does not run your server, see its environment or test its error handling. The canary test runs in your own CI; Rightshold checks a server's listing and tool definitions.
Sources
- MCP specification: Tools, Error Handling and Security Considerations (2025-11-25)
- CWE-209: Generation of Error Message Containing Sensitive Information
- CWE-526: Cleartext Storage of Sensitive Information in an Environment Variable
- OWASP Error Handling Cheat Sheet
- OWASP Logging Cheat Sheet
- axios source: AxiosError.toJSON and the redact option
- MCP Inspector (web UI and CLI for calling a server's tools)
Related
- How to keep .env secrets and API keys away from AI coding agents
- How to mitigate log-to-leak prompt injection in MCP
- How to vet an MCP server before installing it
- How to detect MCP tool description changes (rug pulls)
Not legal advice. Updated 2026-10-29. 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}}'
