Repository navigation
Description
Initial Checks
- I confirm that I'm using the newest release of my line (the latest 2.x, or the latest 1.x if I'm still on v1)
- I confirm that I searched for my issue in https://github.com/modelcontextprotocol/python-sdk/issues before opening this issue
Release line
2.x (current stable)
Description
On the 2026-07-28 (modern) Streamable HTTP path, _write picks the HTTP status from ERROR_CODE_HTTP_STATUS for every JSON-RPC error, ignoring whether it came from the request-validation ladder or from a handler. Since that table maps -32602 INVALID_PARAMS -> 400, a handler-produced "resource not found" is returned as HTTP 400, but the spec requires it in-band on HTTP 200.
-32602 legitimately needs 400 for one origin (malformed request envelope) and 200 for the other (handler error), so one code→status table cannot express the rule.
Actual: 400 {"jsonrpc":"2.0","id":7800,"error":{"code":-32602,"message":"Resource not found: 'demo://does-not-exist'",...}}
Expected: same body, HTTP 200.
Why:
- Streamable HTTP 2026-07-28 (Sending Messages item 6): a JSON-RPC request MUST return
application/jsonortext/event-stream. Non-200 is assigned only to-32020 HeaderMismatch(400),-32021 MissingRequiredClientCapability(400),-32022 UnsupportedProtocolVersion(400),-32601 MethodNotFound(404). - A non-existent resource is
-32602(SEP-2164, resources error handling); no HTTP status is assigned → in-band on 200. -32602+400is required only for a malformed request envelope missing required_metafields (basic/index, Per-request protocol fields).
Root cause — src/mcp/server/_streamable_http_modern.py, _write:
status = ERROR_CODE_HTTP_STATUS.get(msg.error.code, _OK_STATUS) if isinstance(msg, JSONRPCError) else _OK_STATUSHandler errors reach _write via _to_jsonrpc_response; ladder rejections via _write_rejection. Both go through the same table (src/mcp/shared/inbound.py), which includes INVALID_PARAMS: 400 and whose docstring says it is consulted for "classifier-origin and handler-origin errors". That origin conflation is the bug.
Suggested fix: carry the origin into _write (flag set by _to_jsonrpc_response) and apply the spec's status table only to ladder rejections. For handler-origin errors, default to 200, applying a status only for codes the spec defines:
_HANDLER_ERROR_HTTP_STATUS = {
HEADER_MISMATCH: 400, # -32020
MISSING_REQUIRED_CLIENT_CAPABILITY: 400, # -32021
UNSUPPORTED_PROTOCOL_VERSION: 400, # -32022
METHOD_NOT_FOUND: 404, # -32601
}Verified locally: handler -32602 → 200, envelope -32602 → 400, Mcp-Method mismatch → 400.
Impact: any modern client reading a missing resource gets an HTTP transport error instead of the spec's in-band error. Fails MCPJam conformance check modern-resource-not-found-invalid-params.
Example Code
Minimal FastMCP app with one resource, then POST a modern resources/read for a missing URI:
import asyncio, httpx
from fastmcp import FastMCP
from fastmcp.server.auth.providers.jwt import StaticTokenVerifier
mcp = FastMCP("repro", auth=StaticTokenVerifier(
tokens={"tok": {"client_id": "x", "scopes": ["s"]}}))
@mcp.resource(uri="demo://widget", name="w", description="d")
async def widget() -> str:
return "<html></html>"
BODY = {"jsonrpc": "2.0", "id": 7800, "method": "resources/read",
"params": {"uri": "demo://does-not-exist", "_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientCapabilities": {},
"io.modelcontextprotocol/clientInfo": {"name": "t", "version": "1"}}}}
HEADERS = {"Authorization": "Bearer tok",
"Accept": "application/json, text/event-stream",
"Content-Type": "application/json",
"MCP-Protocol-Version": "2026-07-28",
"Mcp-Method": "resources/read", "Mcp-Name": "demo://does-not-exist"}
async def main():
app = mcp.http_app(path="/mcp", stateless_http=True, json_response=True)
async with app.lifespan(app):
async with httpx.AsyncClient(
transport=httpx.ASGITransport(app=app), base_url="http://test") as c:
r = await c.post("/mcp", json=BODY, headers=HEADERS)
print(r.status_code, r.text)
asyncio.run(main())Python & MCP Python SDK
mcp==2.3.0, Python 3.13, modern Streamable HTTP (MCP-Protocol-Version: 2026-07-28). Reproduced via fastmcp 4.1.6, but the defect is in the SDK transport.
AI disclosure: this issue was drafted with AI assistance from a reproduction I ran locally; I have reviewed and can explain its contents.
Activity
[-][bug] Modern Streamable HTTP: handler-produced -32602 answers HTTP 400 instead of 200[/-][+]Modern Streamable HTTP: handler-produced -32602 answers HTTP 400 instead of 200[/+]XiiiiaoT commentedon Nov 13, 2026
Thanks for the detailed report. I traced the current implementation and the reported status mismatch is consistent with the code.
_write_rejection()and then_write().serve_one(). Exceptions are converted by_to_jsonrpc_response()and also reach_write()._write()currently selects the HTTP status solely fromERROR_CODE_HTTP_STATUS, which mapsINVALID_PARAMSto 400. This makes an application-levelMCPError(INVALID_PARAMS)(such as the missing-resource path inMCPServer) indistinguishable from a malformed request-envelope rejection.I agree that HTTP status selection needs to account for the error's origin. The fix should preserve the protocol-defined statuses for
HEADER_MISMATCH(400),MISSING_REQUIRED_CLIENT_CAPABILITY(400),UNSUPPORTED_PROTOCOL_VERSION(400), andMETHOD_NOT_FOUND(404), while allowing a dispatchedINVALID_PARAMSerror to be returned in-band with HTTP 200.One existing test expectation may also need clarification:
test_modern_tools_call_mis_shaped_envelope_skips_validation_without_an_error_logcurrently expects HTTP 400 for anINVALID_PARAMSgenerated after the classifier accepts the request. Additionally,ServerRunner._on_requestperforms method-surface and typed-parameter validation before invoking the user handler, and those errors flow through the same_to_jsonrpc_response()path.Should all post-classifier JSON-RPC errors use HTTP 200 unless their error codes have an explicitly specified non-200 status, leaving pre-dispatch envelope rejections at HTTP 400? Or should application-handler errors be distinguished from kernel request-validation errors more narrowly?
This distinction will determine the correct implementation and regression tests.
AI assistance was used to help trace the implementation and prepare this technical analysis. I have reviewed the referenced code paths and test expectations.