Skip to content
Closed
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
8 changes: 7 additions & 1 deletion src/mcp/server/streamable_http.py
Original file line number Diff line number Diff line change
Expand Up @@ -587,10 +587,16 @@ async def _handle_post_request(self, scope: Scope, request: Request, receive: Re
try:
message = jsonrpc_message_adapter.validate_python(raw_message, by_name=False)
except ValidationError as e:
# A body that parsed as JSON but is not a JSON-RPC message has no
# params to be invalid, so JSON-RPC 2.0 reserves -32600
# (INVALID_REQUEST) for it; -32602 would misreport it as bad params.
# A batch (a JSON array) keeps INVALID_PARAMS, as pinned by the
# hosting conformance test for unsupported batch bodies.
error_code = INVALID_PARAMS if isinstance(raw_message, list) else INVALID_REQUEST
response = self._create_error_response(
f"Validation error: {str(e)}",
HTTPStatus.BAD_REQUEST,
INVALID_PARAMS,
error_code,
)
await response(scope, receive, send)
return
Expand Down
33 changes: 33 additions & 0 deletions tests/shared/test_streamable_http.py
Original file line number Diff line number Diff line change
Expand Up @@ -512,6 +512,39 @@ async def test_json_parsing(basic_app: Starlette) -> None:
assert "Validation error" in response.text


@pytest.mark.anyio
async def test_non_request_body_is_invalid_request(basic_app: Starlette) -> None:
"""A body that is not a JSON-RPC Request is INVALID_REQUEST, not INVALID_PARAMS.

JSON-RPC 2.0 reserves -32602 for invalid params and -32600 for a message
that is not a valid Request; a body with no params to be invalid belongs
to the latter (issue #3557). A batch (a JSON array) keeps INVALID_PARAMS,
as pinned by the hosting conformance test for unsupported batch bodies.
"""
async with make_client(basic_app) as client:
response = await client.post(
"/mcp",
headers={
"Accept": "application/json, text/event-stream",
"Content-Type": "application/json",
},
json={"foo": "bar"},
)
assert response.status_code == 400
assert response.json()["error"]["code"] == INVALID_REQUEST

batched = await client.post(
"/mcp",
headers={
"Accept": "application/json, text/event-stream",
"Content-Type": "application/json",
},
json=[{"jsonrpc": "2.0", "id": 1, "method": "tools/list"}],
)
assert batched.status_code == 400
assert batched.json()["error"]["code"] == INVALID_PARAMS


@pytest.mark.anyio
async def test_method_not_allowed(basic_app: Starlette) -> None:
"""Unsupported HTTP methods are rejected with 405."""
Expand Down
Loading