Skip to content

feat: add MCP_HIDE_INPUT_IN_ERRORS env var to redact payloads from validation errors#2346

Draft
maxisbey wants to merge 1 commit intomainfrom
feat/hide-input-in-errors
Draft

feat: add MCP_HIDE_INPUT_IN_ERRORS env var to redact payloads from validation errors#2346
maxisbey wants to merge 1 commit intomainfrom
feat/hide-input-in-errors

Conversation

@maxisbey
Copy link
Contributor

Adds an opt-in MCP_HIDE_INPUT_IN_ERRORS environment variable that suppresses the input_value field in pydantic ValidationError reprs for models that parse untrusted external data.

Motivation and Context

Pydantic's ValidationError.__repr__ includes the raw input_value by default. When the SDK calls model_validate_json() on untrusted input and validation fails, any logger.exception(...) at the call site dumps the entire payload into logs.

Example from a production deployment — a truncated SSE message caused the client to log 55KB of tool result content:

pydantic_core._pydantic_core.ValidationError: 1 validation error for JSONRPCMessage
  Invalid JSON: EOF while parsing a string at line 1 column 55456
  [type=json_invalid, input_value='{"result":{"content":[{"...<55KB of payload>...', input_type=str]

This pattern exists at several call sites:

  • client/streamable_http.py — SSE and JSON response parsing → logger.exception("Error parsing SSE message")
  • client/sse.py, client/stdio.py, client/websocket.py — same pattern for jsonrpc_message_adapter.validate_json()
  • client/auth/oauth2.pyOAuthToken.model_validate_json()logger.exception("Invalid refresh response") (can leak token payloads)
  • client/auth/utils.py, server/auth/handlers/register.py — OAuth metadata and client registration parsing

Setting MCP_HIDE_INPUT_IN_ERRORS=1 before importing the SDK applies pydantic's hide_input_in_errors=True config so errors still show the type and location (e.g. EOF while parsing a string at line 1 column 55456) but omit the raw payload.

How Has This Been Tested?

  • test_validation_error_shows_input_by_default — verifies current behavior unchanged (input shown by default)
  • test_hide_input_in_errors_env_var — subprocess test verifying input_value is absent from errors for jsonrpc_message_adapter, OAuthToken, OAuthMetadata, ProtectedResourceMetadata, and OAuthClientMetadata when the env var is set (parametrized over 1/true/True/TRUE)

Subprocess is used because the env var is read at import time — monkeypatch.setenv would have no effect on the already-constructed TypeAdapter.

Breaking Changes

None. The default behavior is unchanged; the env var is opt-in.

Types of changes

  • New feature (non-breaking change which adds functionality)

Checklist

  • I have read the MCP Documentation
  • My code follows the repository's style guidelines
  • New and existing tests pass locally
  • I have added appropriate error handling
  • I have added or updated documentation as needed

Additional context

Why env var instead of a configure() function? The jsonrpc_message_adapter is a module-level TypeAdapter that's directly imported (from mcp.types import jsonrpc_message_adapter) in several transports. A runtime configure() that rebuilds the adapter would leave those direct imports holding stale references. Checking the env var at import time avoids this entirely with zero runtime cost.

Why config on the TypeAdapter and not the union's member models? Verified that setting hide_input_in_errors=True on JSONRPCRequest/JSONRPCResponse/etc. does not propagate to top-level JSON parse errors raised by the TypeAdapter — the config must be on the adapter itself. The OAuth models are validated directly via Model.model_validate_json(), so they get model_config individually.

Why not SecretStr? SecretStr only masks field values after successful parsing. The motivating error is a JSON parse failure (EOF while parsing) — validation fails before any field types run, so SecretStr never executes. The leaked data is also JSONRPCResponse.result: dict[str, Any] (arbitrary tool output), not a nameable secret field.

AI Disclaimer

…lidation errors

Pydantic's ValidationError repr includes the raw input_value by default.
When the SDK calls model_validate_json() on untrusted data (SSE messages,
OAuth responses) and validation fails, logger.exception() dumps the entire
payload into logs. This can leak sensitive tool output or OAuth tokens.

Setting MCP_HIDE_INPUT_IN_ERRORS=1 before importing the SDK applies
hide_input_in_errors=True to the jsonrpc_message_adapter TypeAdapter and
the OAuth models (OAuthToken, OAuthClientMetadata, OAuthMetadata,
ProtectedResourceMetadata). The error type and location remain in the
message; only the raw input is omitted.

Opt-in via env var to preserve the current debugging-friendly default.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant