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
4 changes: 4 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,10 @@ This file defines how coding agents should work in this repository.
surface as explicit startup-unavailable errors (direct JSON-RPC `-32000`,
REST `503`, MCP tool `isError=true`) while arbitrary unexpected
startup/runtime failures remain internal errors.
- Direct JSON-RPC and MCP tool `list_gpus` calls must classify expected
`DeviceEnumerationUnavailableError` failures as startup-unavailable, matching
REST `/api/gpus` `503` behavior; malformed GPU listing payloads remain
internal service contract errors.
- CLI service JSON commands (`status`, `stop`, `list-gpus`) must print structured JSON objects that downstream tools can parse with one decode, including `{"error": "..."}` objects for service/runtime errors after CLI parsing succeeds.
- CLI service RPC clients must reject malformed JSON-RPC service envelopes
(wrong `jsonrpc`, mismatched `id`, `id: null` responses to a request with a
Expand Down
9 changes: 5 additions & 4 deletions docs/guides/mcp.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,8 +94,9 @@ Successful direct-method responses are KeepGPU JSON-RPC envelopes with
For direct JSON-RPC calls, public validation failures and unknown parameters
return JSON-RPC `-32602 Invalid params`. Expected startup-unavailable
conditions, such as an unsupported controller platform, no usable visible GPUs,
or failed CUDA/ROCm visible-device enumeration, return `-32000` with the
startup message. Unexpected server failures use `-32603 Internal error`.
or failed CUDA/ROCm visible-device enumeration during `start_keep` or
`list_gpus`, return `-32000` with the startup message. Unexpected server
failures use `-32603 Internal error`.

Explicit `gpu_ids` that are validly shaped but outside the service process's
current visible GPU count are public validation failures. Direct JSON-RPC
Expand All @@ -104,8 +105,8 @@ returns `-32602`, while MCP `tools/call` returns a tool result with

MCP `tools/call` responses keep protocol envelopes successful for normal tool
results, public tool-input validation failures, and expected hardware/platform
startup-unavailable failures, including failed CUDA/ROCm device enumeration.
Those tool-level failures return
startup-unavailable failures, including failed CUDA/ROCm device enumeration
during `start_keep` or `list_gpus`. Those tool-level failures return
`result.isError=true` with the message in tool content. Protocol shape errors,
such as unknown tools, still return JSON-RPC errors such as `-32602`, and
unexpected internal controller/runtime failures return JSON-RPC
Expand Down
30 changes: 30 additions & 0 deletions docs/plans/mcp-list-gpus-unavailable.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# MCP list_gpus Unavailable Plan

## Background

`GET /api/gpus` already treats expected `DeviceEnumerationUnavailableError`
as a structured startup-unavailable response. Direct JSON-RPC `list_gpus` and
MCP `tools/call list_gpus` still route the same expected failure through the
generic internal-error path.

## Goal

Keep GPU enumeration failure semantics consistent across public service
surfaces: direct JSON-RPC should return `-32000`, and MCP tool calls should
return a successful protocol envelope with `result.isError=true`.

## Solution

- Add regression tests for direct JSON-RPC `list_gpus`.
- Add regression tests for MCP `tools/call list_gpus`.
- Classify `DeviceEnumerationUnavailableError` alongside other expected
startup-unavailable service failures.
- Update service docs and agent guidance.

## Checks

- `PYTHONPATH=$PWD/src pytest tests/mcp/test_server.py -q -k 'list_gpus or startup_unavailable or tools_call'`
- `PYTHONPATH=$PWD/src pytest tests/mcp tests/utilities/test_gpu_info.py -q`
- `PYTHONPATH=$PWD/src pytest -q`
- `PYTHONPATH=$PWD/src mkdocs build --strict`
- `pre-commit run --all-files --show-diff-on-failure`
2 changes: 1 addition & 1 deletion src/keep_gpu/mcp/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -823,7 +823,7 @@ def _call_keepgpu_method(
return server.list_gpus()
except SessionInputError as exc:
raise JSONRPCError(JSONRPC_INVALID_PARAMS, str(exc)) from exc
except SessionStartupUnavailable as exc:
except (SessionStartupUnavailable, DeviceEnumerationUnavailableError) as exc:
raise JSONRPCError(JSONRPC_STARTUP_UNAVAILABLE, str(exc)) from exc
raise JSONRPCError(JSONRPC_METHOD_NOT_FOUND, f"Unknown method: {method}")

Expand Down
50 changes: 50 additions & 0 deletions tests/mcp/test_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -1122,6 +1122,28 @@ def test_jsonrpc_list_gpus_rejects_malformed_gpu_record(
assert message_fragment in resp["error"]["message"]


def test_jsonrpc_list_gpus_device_enumeration_unavailable_returns_public_code(
monkeypatch,
):
monkeypatch.setattr(
server_module,
"get_gpu_info",
lambda: (_ for _ in ()).throw(
pm.DeviceEnumerationUnavailableError("Unable to enumerate visible GPUs")
),
)
server = make_server()
req = {"jsonrpc": "2.0", "id": 23, "method": "list_gpus", "params": {}}

resp = _handle_request(server, req)

assert resp["jsonrpc"] == "2.0"
assert resp["id"] == 23
assert "result" not in resp
assert resp["error"]["code"] == JSONRPC_STARTUP_UNAVAILABLE
assert "Unable to enumerate visible GPUs" in resp["error"]["message"]


def test_mcp_initialize_returns_server_capabilities():
server = make_server()
req = {
Expand Down Expand Up @@ -1386,6 +1408,34 @@ def test_mcp_tools_call_list_gpus_rejects_malformed_gpu_record(monkeypatch):
assert "visible_id" in resp["error"]["message"]


def test_mcp_tools_call_list_gpus_device_enumeration_unavailable_returns_tool_error(
monkeypatch,
):
monkeypatch.setattr(
server_module,
"get_gpu_info",
lambda: (_ for _ in ()).throw(
pm.DeviceEnumerationUnavailableError("Unable to enumerate visible GPUs")
),
)
server = make_server()
req = {
"jsonrpc": "2.0",
"id": 24,
"method": "tools/call",
"params": {"name": "list_gpus", "arguments": {}},
}

resp = _handle_request(server, req)

assert resp["jsonrpc"] == "2.0"
assert resp["id"] == 24
assert "result" in resp
result = resp["result"]
assert result["isError"] is True
assert "Unable to enumerate visible GPUs" in result["content"][0]["text"]


def test_mcp_tools_call_unknown_tool_returns_protocol_error():
server = make_server()
req = {
Expand Down
Loading