Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion src/mcp/shared/auth.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
from typing import Any, Literal

from pydantic import AnyHttpUrl, AnyUrl, BaseModel, Field, field_validator
from pydantic import AnyHttpUrl, AnyUrl, BaseModel, ConfigDict, Field, field_validator

from mcp.types.jsonrpc import HIDE_INPUT_IN_ERRORS


class OAuthToken(BaseModel):
"""See https://datatracker.ietf.org/doc/html/rfc6749#section-5.1"""

model_config = ConfigDict(hide_input_in_errors=HIDE_INPUT_IN_ERRORS)

access_token: str
token_type: Literal["Bearer"] = "Bearer"
expires_in: int | None = None
Expand Down Expand Up @@ -37,6 +41,8 @@ class OAuthClientMetadata(BaseModel):
See https://datatracker.ietf.org/doc/html/rfc7591#section-2
"""

model_config = ConfigDict(hide_input_in_errors=HIDE_INPUT_IN_ERRORS)

redirect_uris: list[AnyUrl] | None = Field(..., min_length=1)
# supported auth methods for the token endpoint
token_endpoint_auth_method: (
Expand Down Expand Up @@ -105,6 +111,8 @@ class OAuthMetadata(BaseModel):
See https://datatracker.ietf.org/doc/html/rfc8414#section-2
"""

model_config = ConfigDict(hide_input_in_errors=HIDE_INPUT_IN_ERRORS)

issuer: AnyHttpUrl
authorization_endpoint: AnyHttpUrl
token_endpoint: AnyHttpUrl
Expand Down Expand Up @@ -134,6 +142,8 @@ class ProtectedResourceMetadata(BaseModel):
See https://datatracker.ietf.org/doc/html/rfc9728#section-2
"""

model_config = ConfigDict(hide_input_in_errors=HIDE_INPUT_IN_ERRORS)

resource: AnyHttpUrl
authorization_servers: list[AnyHttpUrl] = Field(..., min_length=1)
jwks_uri: AnyHttpUrl | None = None
Expand Down
15 changes: 13 additions & 2 deletions src/mcp/types/jsonrpc.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,18 @@

from __future__ import annotations

import os
from typing import Annotated, Any, Literal

from pydantic import BaseModel, Field, TypeAdapter
from pydantic import BaseModel, ConfigDict, Field, TypeAdapter

HIDE_INPUT_IN_ERRORS = os.environ.get("MCP_HIDE_INPUT_IN_ERRORS", "").lower() in ("1", "true")
"""When True, pydantic ValidationError reprs omit the ``input_value`` field.

Set the ``MCP_HIDE_INPUT_IN_ERRORS`` environment variable to ``1`` or ``true`` before
importing the SDK to prevent raw request/response payloads from appearing in logs
when JSON parsing or validation fails. The error type and location are still shown.
"""

RequestId = Annotated[int, Field(strict=True)] | str
"""The ID of a JSON-RPC request."""
Expand Down Expand Up @@ -80,4 +89,6 @@ class JSONRPCError(BaseModel):


JSONRPCMessage = JSONRPCRequest | JSONRPCNotification | JSONRPCResponse | JSONRPCError
jsonrpc_message_adapter: TypeAdapter[JSONRPCMessage] = TypeAdapter(JSONRPCMessage)
jsonrpc_message_adapter: TypeAdapter[JSONRPCMessage] = TypeAdapter(
JSONRPCMessage, config=ConfigDict(hide_input_in_errors=HIDE_INPUT_IN_ERRORS)
)
51 changes: 51 additions & 0 deletions tests/test_types.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import subprocess
import sys
from typing import Any

import pytest
from pydantic import ValidationError

from mcp.types import (
LATEST_PROTOCOL_VERSION,
Expand Down Expand Up @@ -360,3 +363,51 @@ def test_list_tools_result_preserves_json_schema_2020_12_fields():
assert tool.input_schema["$schema"] == "https://json-schema.org/draft/2020-12/schema"
assert "$defs" in tool.input_schema
assert tool.input_schema["additionalProperties"] is False


def test_validation_error_shows_input_by_default():
"""By default, ValidationError repr includes input_value (pydantic's default behavior)."""
with pytest.raises(ValidationError) as exc_info:
jsonrpc_message_adapter.validate_json('{"result":{"content":"SECRET-PAYLOAD')
assert "input_value" in repr(exc_info.value)
assert "SECRET-PAYLOAD" in repr(exc_info.value)


_HIDE_INPUT_CHECK_SCRIPT = """
import pydantic
from mcp.types import jsonrpc_message_adapter
from mcp.shared.auth import OAuthToken, OAuthMetadata, ProtectedResourceMetadata, OAuthClientMetadata

def check(fn, name):
try:
fn()
except pydantic.ValidationError as e:
err = repr(e)
assert "input_value" not in err, f"{name}: input_value leaked: {err!r}"
assert "SECRET" not in err, f"{name}: payload leaked: {err!r}"
# still useful: error type/location present
assert "json_invalid" in err or "validation error" in err
else:
raise AssertionError(f"{name}: expected ValidationError")

check(lambda: jsonrpc_message_adapter.validate_json('{"result":"SECRET'), "jsonrpc_message_adapter")
check(lambda: OAuthToken.model_validate_json('{"access_token":"SECRET'), "OAuthToken")
check(lambda: OAuthMetadata.model_validate_json('{"issuer":"SECRET'), "OAuthMetadata")
check(lambda: ProtectedResourceMetadata.model_validate_json('{"resource":"SECRET'), "ProtectedResourceMetadata")
check(lambda: OAuthClientMetadata.model_validate_json('{"redirect_uris":"SECRET'), "OAuthClientMetadata")
print("OK")
"""


@pytest.mark.parametrize("env_value", ["1", "true", "True", "TRUE"])
def test_hide_input_in_errors_env_var(env_value: str):
"""When MCP_HIDE_INPUT_IN_ERRORS is set, ValidationError repr omits input_value."""
result = subprocess.run(
[sys.executable, "-c", _HIDE_INPUT_CHECK_SCRIPT],
env={"MCP_HIDE_INPUT_IN_ERRORS": env_value},
capture_output=True,
text=True,
check=False,
)
assert result.returncode == 0, f"stdout={result.stdout!r} stderr={result.stderr!r}"
assert result.stdout.strip() == "OK"
Loading