Skip to content

Streamable HTTP: malformed requests return raw pydantic errors to the client #3698

Open

Description

@john-mihailescu

Initial Checks

Release line

2.x (current stable)
Also applies to latest 1.x

Description

When the Streamable HTTP POST handler rejects a malformed body, it returns the raw exception text to the client (streamable_http.py L580-L596):

  • No "jsonrpc" field → "Validation error: 6 validation errors for union[JSONRPCRequest,...] ... input_value={...} ... https://errors.pydantic.dev/2.14/v/missing"
  • Not JSON → "Parse error: EOF while parsing a value at line 1 column 27"

A security audit of our MCP server flagged this: the errors reveal that the backend is Python, using pydantic (down to the exact version) and the MCP Python SDK, which helps an attacker enumerate the environment.

Expected: fixed messages (Parse error, Invalid JSON-RPC message), with detail in the server log only. The 2026-07-28 path already does this (_streamable_http_modern.py L415).

The same code is on v1.x, which we run. I’d like to fix both if assigned: a few lines plus tests, with no new API.

Related: #3557 (error code on these lines), #3572 (same pydantic text in tool-argument errors).

Disclosure: this issue was drafte by Claude Code and reviewed/iterated by myself and another senior developer. I ran the reproduction and verified the output myself.

Example Code

import json

from mcp.server import MCPServer
from mcp.server.transport_security import TransportSecuritySettings
from starlette.testclient import TestClient

server = MCPServer("repro")


@server.tool()
def echo(text: str) -> str:
    return text


app = server.streamable_http_app(
    stateless_http=True,
    transport_security=TransportSecuritySettings(enable_dns_rebinding_protection=False),
)
headers = {
    "Accept": "application/json, text/event-stream",
    "Content-Type": "application/json",
    "mcp-protocol-version": "2025-06-18",
}
with TestClient(app) as client:
    for body in (
        json.dumps({"id": 1, "method": "tools/call", "params": {"name": "echo", "arguments": {"text": "hi"}}}),
        json.dumps([{"jsonrpc": "2.0", "id": 1, "method": "tools/list"}]),
        '{"jsonrpc": "2.0", "id": 1,',
    ):
        response = client.post("/mcp", content=body, headers=headers)
        print(response.status_code, json.dumps(response.json(), indent=2))

Python & MCP Python SDK

Python 3.12.9
mcp 2.3.0
pydantic 2.14.0

Activity

  1. added
    bugSomething isn't working
    v2Affects the v2 line (2.x on main)
    v1Affects the v1.x maintenance line
    on Oct 13, 2026
  2. sattyamjjain commented on Oct 17, 2026

    @sattyamjjain

    There is a precedent in the same SDK for the fix you describe. The legacy SSE transport logs the detail and returns a fixed message: sse.py L273-276 at 9f66f37 does logger.exception("Failed to parse message") then Response("Could not parse message", status_code=400). The modern transport also already uses a fixed message=" Parse error" (_streamable_http_modern.py L415).
    https://github.com/modelcontextprotocol/python-sdk/blob/9f66f376d486e84e9810b368cf8be9d5908661ab/src/mcp/server/sse.py

    streamable_http.py still interpolates the exception at L583 (f"Parse error: {str(e)}") and L591 (f"Validation error: {str(e)}"), same on v1.x at L537 and L545.

    JSON-RPC 2.0 supports dropping the detail too: the message "SHOULD be limited to a concise single sentence", and data "may be omitted".
    https://www.jsonrpc.org/specification

  3. EngineeringWithJay commented on Oct 17, 2026

    @EngineeringWithJay

    A local in-process reproduction confirms the response-body leak on 2af9e3089ac66b35c1550c500aae86a82efb1e17 (Python 3.14.6, Pydantic 2.12.5, protocol header 2025-06-18). A missing jsonrpc field returns HTTP 400 with Validation error: 6 validation errors for union[...], including input values and Pydantic diagnostic links. Truncated JSON also includes parser details.

    There is a small test-only patch strengthening the two existing HTTP validation tests: five malformed bodies in both JSON and SSE response modes. It asserts the full JSON-RPC error envelope with the issue's requested Parse error / Invalid JSON-RPC message text and no diagnostic data fields. On the unchanged SDK, all ten cases fail at the public-message assertion, while the other 71 tests in that file pass; two runs give the same result. A temporary local sanitization/logging demo makes all 81 pass, but is not included in the proposed test diff. This does not establish that an upstream fix has landed.

    As the issue distinguishes, #3557 concerns error-code selection and #3572 concerns tool-argument errors. This patch leaves the current codes unchanged and does not cover tool arguments or claim v1.x validation.

    Would sharing the test-only patch with the reporter, or a coordinated test-only PR if useful and permitted by the contribution gate, help? The reporter has already offered to implement the fix; this is not a competing implementation. Are those exact messages and omitted error.data the intended contract? Should the tests also require server-side exception logging, or leave log shape/level and input redaction to a separate decision?

    AI disclosure: OpenCode generated the reproduction, tests, and this draft, and executed the local checks described above.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingv1Affects the v1.x maintenance linev2Affects the v2 line (2.x on main)

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions