Skip to content
Open
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
2 changes: 2 additions & 0 deletions docs/design/host-adapters.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,5 @@ ordinary external Stores are read-only to the agent shell. Controlled runs isola
state while reusing only an explicit auth file. Hook and MCP writes require a live compatibility
preflight. Results are not attributable until native memory is isolated and the tested build
passes that preflight.

The stdio adapter returns JSON-RPC parse or invalid-request errors for malformed frames and continues reading subsequent requests. An explicit null request ID receives a response, while requests without an ID retain notification behavior. Error codes and envelope rules follow https://www.jsonrpc.org/specification.
22 changes: 18 additions & 4 deletions packages/mcp/src/agent_memory/mcp/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,17 @@
ERROR_INVALID_PARAMS = -32602


def handle(store: Store, request: dict[str, object]) -> dict[str, object] | None:
method = str(request.get("method") or "")
def handle(store: Store, request: object) -> dict[str, object] | None:
if (
not isinstance(request, dict)
or request.get("jsonrpc") != JSONRPC
or not isinstance(request.get("method"), str)
or isinstance(request.get("id"), (dict, list, bool))
):
return _error(None, -32600, {"message": "Invalid Request"})
method = request["method"]
request_id = request.get("id")
if request_id is None:
if "id" not in request:
return None
try:
result = _route(store, method, request.get("params") or {})
Expand All @@ -42,7 +49,14 @@ def serve(store: Store, stream_in: TextIO, stream_out: TextIO) -> None:
for line in stream_in:
if not line.strip():
continue
response = handle(store, json.loads(line))
try:
request = json.loads(line)
except json.JSONDecodeError:
response: dict[str, object] | None = _error(
None, -32700, {"message": "Parse error"}
)
else:
response = handle(store, request)
if response is None:
continue
stream_out.write(json.dumps(response) + "\n")
Expand Down
33 changes: 33 additions & 0 deletions tests/unit/test_regression_mcp_invalid_framing.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import io
import json

import pytest
from agent_memory.mcp import server


@pytest.mark.parametrize(
"malformed,code",
[
("{", -32700),
("null", -32600),
('"text"', -32600),
("7", -32600),
('{"jsonrpc":"2.0","method":7,"id":1}', -32600),
],
)
def test_bad_frame_reports_protocol_error_and_next_request_still_runs(store, malformed, code):
following = {"jsonrpc": "2.0", "id": 8, "method": "tools/list"}
output = io.StringIO()
server.serve(store, io.StringIO(malformed + "\n" + json.dumps(following) + "\n"), output)
responses = [json.loads(line) for line in output.getvalue().splitlines()]
assert responses[0]["error"]["code"] == code
assert responses[0]["id"] is None
assert responses[1]["id"] == 8
assert responses[1]["result"]["tools"]


def test_explicit_null_id_receives_a_response(store):
response = server.handle(store, {"jsonrpc": "2.0", "id": None, "method": "tools/list"})
assert response is not None
assert response["id"] is None
assert response["result"]["tools"]