Skip to content
Merged
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
3 changes: 3 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,9 @@ This file defines how coding agents should work in this repository.
- JSON-RPC handlers must reject explicit request versions other than `"2.0"`
with `-32600 Invalid Request` for request messages while preserving
omitted-version legacy/internal calls and silent id-less notifications.
- HTTP JSON-RPC endpoints (`/` and `/rpc`) must return JSON-RPC envelopes for
protocol parse errors, including `jsonrpc`, `id`, and numeric `error.code`;
REST routes keep REST-shaped structured JSON errors.
- For stdio MCP, stdout must contain only JSON protocol messages; diagnostics
and human logs belong on stderr.

Expand Down
3 changes: 3 additions & 0 deletions docs/guides/mcp.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@ HTTP mode is KeepGPU's local JSON-RPC/REST/dashboard service. It accepts the
same JSON-RPC message shapes at `/rpc`, but it is not a Streamable HTTP MCP
endpoint.

Malformed HTTP JSON-RPC bodies return a JSON-RPC `-32700 Parse error` envelope
with `id: null`; REST routes keep REST-shaped JSON errors.

```bash
curl -X POST http://127.0.0.1:8765/rpc \
-H "content-type: application/json" \
Expand Down
35 changes: 35 additions & 0 deletions docs/plans/http-jsonrpc-parse-error-envelope.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# HTTP JSON-RPC Parse Error Envelope Plan

## Background

HTTP `POST /rpc` and `POST /` are JSON-RPC compatibility endpoints. Before this
fix, malformed JSON bodies were rejected by the shared HTTP body parser before
the handler distinguished REST from JSON-RPC routes, so JSON-RPC clients saw a
REST-shaped HTTP 400 body such as `{"error":{"message":"Bad request: ..."}}`.
That body has no `jsonrpc`, `id`, or numeric JSON-RPC error code.

## Solution

Keep REST `/api/sessions` parse failures as structured HTTP 400 errors, but map
parse failures on `/` and `/rpc` to a JSON-RPC parse-error envelope:
`{"jsonrpc":"2.0","id":null,"error":{"code":-32700,"message":"..."}}`.

## Tasks

- [x] Add a failing HTTP JSON-RPC regression test for malformed `/rpc` JSON.
- [x] Confirm the test fails before implementation.
- [x] Return `_jsonrpc_error(None, JSONRPC_PARSE_ERROR, ...)` for `/` and `/rpc`
parse failures.
- [x] Document JSON-RPC parse-error envelopes in the MCP guide and POST handler
docstring.
- [x] Update `AGENTS.md` with the protocol boundary guideline.
- [x] Run targeted tests and `git diff --check`.
- [x] Commit with `fix(mcp): return jsonrpc parse error envelopes`.

## Verification Notes

- RED: `PYTHONPATH=$PWD/src pytest tests/mcp/test_http_api.py::test_http_jsonrpc_parse_error_returns_jsonrpc_envelope -q` failed because the route returned HTTP 400.
- GREEN: `PYTHONPATH=$PWD/src pytest tests/mcp/test_http_api.py::test_http_jsonrpc_parse_error_returns_jsonrpc_envelope -q` passed after routing `/` and `/rpc` parse failures through the JSON-RPC envelope path.
- Final targeted suite: `PYTHONPATH=$PWD/src pytest tests/mcp/test_http_api.py -q` passed with 51 tests.
- Broader MCP suite: `PYTHONPATH=$PWD/src pytest tests/mcp -q` passed with 142 tests.
- Hygiene: `pre-commit run --all-files`, `PYTHONPATH=$PWD/src mkdocs build`, and `git diff --check` passed.
11 changes: 8 additions & 3 deletions src/keep_gpu/mcp/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -888,10 +888,10 @@ def do_GET(self): # noqa: N802

def do_POST(self): # noqa: N802
"""
Handle an HTTP JSON-RPC request and write a JSON response.
Handle HTTP JSON-RPC and REST session POST requests.

Expects application/json bodies containing {"method", "params", "id"}.
Returns 400 with an error object if parsing fails.
JSON-RPC parse failures return JSON-RPC parse-error envelopes. REST
session parse failures return HTTP 400 with a structured error object.
"""
parsed = urlparse(self.path)
path = parsed.path
Expand All @@ -914,6 +914,11 @@ def do_POST(self): # noqa: N802
UnicodeDecodeError,
TypeError,
) as exc:
if path in ("/", "/rpc"):
self._json_response(
200, _jsonrpc_error(None, JSONRPC_PARSE_ERROR, str(exc))
)
return
self._json_response(400, {"error": {"message": f"Bad request: {exc}"}})
return

Expand Down
19 changes: 19 additions & 0 deletions tests/mcp/test_http_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,25 @@ def _request_raw(method, url, data=None):
return exc.code, json.loads(body) if body else {}


@pytest.mark.parametrize("rpc_path", ["/", "/rpc"])
def test_http_jsonrpc_parse_error_returns_jsonrpc_envelope(rpc_path):
server = make_server()
httpd, thread, base = _start_http_server(server)

try:
status, payload = _request_raw("POST", f"{base}{rpc_path}", b"{")
finally:
httpd.shutdown()
httpd.server_close()
server.shutdown()
thread.join(timeout=2)

assert status == 200
assert payload["jsonrpc"] == "2.0"
assert payload["id"] is None
assert payload["error"]["code"] == -32700


def test_http_health_and_static_index():
server = make_server()

Expand Down
Loading