Skip to content

Non-ASCII base64 Mcp-Name header returns 500 instead of HEADER_MISMATCH #3817

Open

Description

@jbrown9513

Initial Checks

Release line

2.x (current stable)

Description

A tools/call whose Mcp-Name header uses the =?base64?...?= form with a non-ASCII byte in the payload gets a 500. It is one failed request, and the server keeps running.

The docstring on decode_header_value says "A malformed sentinel (bad base64, non-canonical base64, or bad UTF-8) yields None". None should become a HEADER_MISMATCH rejection.

It catches only binascii.Error, but b64decode raises a plain ValueError for non-ASCII text:

try:
decoded = base64.b64decode(payload, validate=True)
except binascii.Error:
return None

The modern POST handler does not catch it:

verdict = classify_inbound_request(decoded, headers=dict(request.headers))

The Mcp-Param-* check calls the same function, from reading the code:

decoded = decode_header_value(raw)

except ValueError: fixes it, since binascii.Error is a subclass.

Results on 2.3.0, with the script below. inbound.py is unchanged on main at af4c6f4.

b'echo' 200
b'=?base64?ZWNobw==?=' 200
b'=?base64?\xe9?=' 500

Example Code

import anyio, httpx2 as httpx
from mcp.server.mcpserver import MCPServer
from mcp_types.version import LATEST_MODERN_VERSION

mcp = MCPServer("demo")

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

async def main():
    app = mcp.streamable_http_app()
    body = {"jsonrpc": "2.0", "id": 1, "method": "tools/call",
            "params": {"name": "echo", "arguments": {"text": "hi"},
                       "_meta": {"io.modelcontextprotocol/protocolVersion": LATEST_MODERN_VERSION,
                                 "io.modelcontextprotocol/clientCapabilities": {}}}}
    async with app.router.lifespan_context(app):
        transport = httpx.ASGITransport(app=app, raise_app_exceptions=False)
        async with httpx.AsyncClient(transport=transport, base_url="http://127.0.0.1:8000") as c:
            for name in [b"echo", b"=?base64?ZWNobw==?=", b"=?base64?\xe9?="]:
                r = await c.post("/mcp", json=body, headers=[(b"mcp-protocol-version", LATEST_MODERN_VERSION.encode()),
                    (b"accept", b"application/json, text/event-stream"), (b"mcp-method", b"tools/call"), (b"mcp-name", name)])
                print(name, r.status_code)

anyio.run(main)

Python & MCP Python SDK

Python 3.12.12, MCP Python SDK 2.3.0

AI disclosure

refuzz, an LLM-driven fuzzer I built (Meta Muse Spark), found this bug. Claude Code wrote this report and ran the reproduction on 2.3.0.

Activity

  1. added
    bugSomething isn't working
    spec-2026-07-28Concerns the SDK's implementation of the 2026-07-28 MCP spec revision
    v2Affects the v2 line (2.x on main)
    on Nov 4, 2026
  2. media-ctrl commented on Nov 5, 2026

    @media-ctrl

    Confirmed the exception split on CPython 3.14.9, and it is narrower than the report suggests in a way that decides where the fix belongs.

    binascii.Error is a subclass of ValueError (issubclass(binascii.Error, ValueError) is True), so except ValueError covers the alphabet and padding failures as well. Measured with the exact call the function makes, b64decode(payload, validate=True):

    • 'ZWNobw' -> binascii.Error: Incorrect padding
    • '!!!!' -> binascii.Error: Only base64 data is allowed
    • b'\xe9' -> binascii.Error: Only base64 data is allowed (already caught today)
    • '\xe9' as str -> ValueError: string argument should contain only ASCII characters (the escape)

    So the same corrupt header is a HEADER_MISMATCH or a 500 depending only on the type of the argument. The mechanism is worth naming in the patch: for a str payload the failure happens in _bytes_from_decode_data, at the ASCII encode, which runs before validate=True ever inspects the alphabet.

    The str form is the contract, not an edge case. The signature at line 162 is decode_header_value(value: str | None), payload comes out of _B64_SENTINEL.fullmatch as a str, and ASGI hands the handler latin-1 decoded header text, so a byte at or above 0x80 arrives as U+0080 to U+00FF and takes the uncaught path. _streamable_http_modern.py:436 passes dict(request.headers) straight into classify_inbound_request, and Starlette header values are str.

    Placing the fix inside decode_header_value rather than at the POST handler is right for a second reason: the function is exported (__all__, line 50), and the body-value comparison at line 467 calls it on a client-controlled header just as directly as the Mcp-Param-* check at line 584. Those are the only two call sites and both reach it with str, so one change covers both and stops third-party importers getting an exception out of a function whose docstring promises None.

    Three things to put in the patch: the except ValueError; a test that feeds the str form "=?base64?\xe9?=" and asserts is None, since the byte form already passes and would not catch a regression; and a decision on whether a non-ASCII payload should be rejected before decode at all, because after the catch a corrupt and an empty payload are indistinguishable to every caller.

    One detail that needs no change: no message edit is required. Once the exception reaches the existing return None, the caller already emits HEADER_MISMATCH with "carries a malformed base64 sentinel value", which is the right code for this case.

  3. techaurah commented on Nov 5, 2026

    @techaurah

    Hi, we are TechAura. We can reproduce this and implement the scoped fix in decode_header_value — catching ValueError so a non-ASCII base64 sentinel returns None and becomes a HEADER_MISMATCH rejection instead of a 500 — with a test for the string form. Could a maintainer please assign this issue to @techaurah? We will open a PR only after assignment.

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 workingspec-2026-07-28Concerns the SDK's implementation of the 2026-07-28 MCP spec revisionv2Affects 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