summaryrefslogtreecommitdiff
path: root/src/sailfish_devel_mcp
diff options
context:
space:
mode:
authorAndrew Branson <andrew.branson@jolla.com>2026-06-02 00:18:14 +0200
committerAndrew Branson <andrew.branson@jolla.com>2026-06-02 00:18:14 +0200
commit5d24d33536d1041b4d9f107a5f5813bd43177530 (patch)
tree86b90f5e58747c31aaaff8ff5e52661421c7f69a /src/sailfish_devel_mcp
parent0cdf2d00cdc1a9af5f5b0b14241a2afe717661f6 (diff)
Harden MCP transport error handling
Normalize timeout output to text, convert tool exceptions into tool errors, and keep stdio running after response serialization failures.
Diffstat (limited to 'src/sailfish_devel_mcp')
-rw-r--r--src/sailfish_devel_mcp/runner.py20
-rw-r--r--src/sailfish_devel_mcp/server.py33
2 files changed, 45 insertions, 8 deletions
diff --git a/src/sailfish_devel_mcp/runner.py b/src/sailfish_devel_mcp/runner.py
index b0b88bf..74748f4 100644
--- a/src/sailfish_devel_mcp/runner.py
+++ b/src/sailfish_devel_mcp/runner.py
@@ -4,7 +4,7 @@ from dataclasses import dataclass
from pathlib import Path
import shlex
import subprocess
-from typing import Iterable, Sequence
+from typing import Any, Iterable, Sequence
from .config import DeviceConfig
@@ -39,6 +39,16 @@ def truncate(text: str, limit: int) -> tuple[str, bool]:
return text[:limit] + f"\n[truncated after {limit} characters]", True
+def ensure_text(value: Any) -> str:
+ if value is None:
+ return ""
+ if isinstance(value, str):
+ return value
+ if isinstance(value, bytes):
+ return value.decode("utf-8", errors="replace")
+ return str(value)
+
+
def run(
argv: Sequence[str],
*,
@@ -57,15 +67,15 @@ def run(
return CommandResult(
argv=tuple(str(arg) for arg in argv),
returncode=completed.returncode,
- stdout=completed.stdout,
- stderr=completed.stderr,
+ stdout=ensure_text(completed.stdout),
+ stderr=ensure_text(completed.stderr),
)
except subprocess.TimeoutExpired as exc:
return CommandResult(
argv=tuple(str(arg) for arg in argv),
returncode=124,
- stdout=exc.stdout or "",
- stderr=(exc.stderr or "") + f"\ncommand timed out after {timeout}s",
+ stdout=ensure_text(exc.stdout),
+ stderr=ensure_text(exc.stderr) + f"\ncommand timed out after {timeout}s",
)
diff --git a/src/sailfish_devel_mcp/server.py b/src/sailfish_devel_mcp/server.py
index 4fb2f91..7982b21 100644
--- a/src/sailfish_devel_mcp/server.py
+++ b/src/sailfish_devel_mcp/server.py
@@ -119,6 +119,15 @@ class McpServer:
return self.registry[name].handler(args)
except ValueError as exc:
return tool_error(str(exc))
+ except Exception as exc:
+ return tool_error(
+ f"{name} failed: {exc}",
+ {
+ "error": str(exc),
+ "exception": type(exc).__name__,
+ "tool": name,
+ },
+ )
def _resources_list(self) -> dict[str, Any]:
return {
@@ -158,6 +167,7 @@ def run_stdio(server: McpServer, stdin: TextIO = sys.stdin, stdout: TextIO = sys
for line in stdin:
if not line.strip():
continue
+ message: Any = None
try:
message = json.loads(line)
except json.JSONDecodeError as exc:
@@ -169,8 +179,26 @@ def run_stdio(server: McpServer, stdin: TextIO = sys.stdin, stdout: TextIO = sys
else:
response = server.handle(message)
if response is not None:
- stdout.write(json.dumps(response, separators=(",", ":")) + "\n")
- stdout.flush()
+ try:
+ response_text = json.dumps(response, separators=(",", ":"))
+ except TypeError as exc:
+ request_id = message.get("id") if isinstance(message, dict) else None
+ response_text = json.dumps(
+ {
+ "jsonrpc": "2.0",
+ "id": request_id,
+ "error": {
+ "code": -32603,
+ "message": f"response serialization failed: {exc}",
+ },
+ },
+ separators=(",", ":"),
+ )
+ try:
+ stdout.write(response_text + "\n")
+ stdout.flush()
+ except BrokenPipeError:
+ return
def main(argv: list[str] | None = None) -> None:
@@ -193,4 +221,3 @@ def main(argv: list[str] | None = None) -> None:
if __name__ == "__main__":
main()
-