summaryrefslogtreecommitdiff
path: root/src/sailfish_devel_mcp/server.py
diff options
context:
space:
mode:
authorAndrew Branson <andrew.branson@jolla.com>2026-06-29 16:40:30 +0200
committerAndrew Branson <andrew.branson@jolla.com>2026-06-29 17:50:20 +0200
commiteb92561bd685961889cc553cd96db4a569eefec5 (patch)
treea4df385847e712d1dfb428f28d23df58b9ed298d /src/sailfish_devel_mcp/server.py
parent21dcc7e6413cf1e93a37cce2098a86c2a3ab5e5a (diff)
Add remote Android build tools and request logging
Add Android build host config and MCP tools for remote AppSupport builds. Extend device RPM installs to copy dependency sets and install them together. Log MCP request and tool-call lifecycle events, and document the config.
Diffstat (limited to 'src/sailfish_devel_mcp/server.py')
-rw-r--r--src/sailfish_devel_mcp/server.py79
1 files changed, 77 insertions, 2 deletions
diff --git a/src/sailfish_devel_mcp/server.py b/src/sailfish_devel_mcp/server.py
index 7982b21..cabbb77 100644
--- a/src/sailfish_devel_mcp/server.py
+++ b/src/sailfish_devel_mcp/server.py
@@ -1,8 +1,10 @@
from __future__ import annotations
import argparse
+from datetime import datetime
import json
import sys
+import time
from typing import Any, TextIO
from . import __version__
@@ -16,6 +18,23 @@ PROTOCOL_VERSIONS = [
"2025-03-26",
"2024-11-05",
]
+SERVER_NAME = "sailfish-devel-mcp"
+
+
+def _log_value(value: Any, max_length: int = 200) -> str:
+ text = str(value).replace("\r", "\\r").replace("\n", "\\n")
+ if len(text) > max_length:
+ return text[: max_length - 3] + "..."
+ return text
+
+
+def _log_line(message: str) -> None:
+ timestamp = datetime.now().astimezone().isoformat(timespec="seconds")
+ print(f"[{timestamp}] {SERVER_NAME} {message}", file=sys.stderr, flush=True)
+
+
+def _duration_ms(start: float) -> int:
+ return int((time.monotonic() - start) * 1000)
class JsonRpcError(Exception):
@@ -33,26 +52,47 @@ class McpServer:
def handle(self, message: dict[str, Any]) -> dict[str, Any] | None:
if not isinstance(message, dict):
+ _log_line("mcp request rejected reason=non_object")
raise JsonRpcError(-32600, "JSON-RPC message must be an object")
request_id = message.get("id")
method = message.get("method")
if not method:
+ _log_line(f"mcp request rejected id={_log_value(request_id)} reason=missing_method")
raise JsonRpcError(-32600, "JSON-RPC message is missing method")
if request_id is None:
+ _log_line(f"mcp notification method={_log_value(method)}")
self._handle_notification(method)
return None
+ started = time.monotonic()
+ _log_line(f"mcp request start id={_log_value(request_id)} method={_log_value(method)}")
try:
result = self._dispatch(method, message.get("params") or {})
+ _log_line(
+ "mcp request finish "
+ f"id={_log_value(request_id)} method={_log_value(method)} "
+ f"status=ok duration_ms={_duration_ms(started)}"
+ )
return {"jsonrpc": "2.0", "id": request_id, "result": result}
except JsonRpcError as exc:
+ _log_line(
+ "mcp request finish "
+ f"id={_log_value(request_id)} method={_log_value(method)} "
+ f"status=jsonrpc_error code={exc.code} duration_ms={_duration_ms(started)}"
+ )
error: dict[str, Any] = {"code": exc.code, "message": exc.message}
if exc.data is not None:
error["data"] = exc.data
return {"jsonrpc": "2.0", "id": request_id, "error": error}
except Exception as exc: # pragma: no cover - defensive protocol boundary
+ _log_line(
+ "mcp request finish "
+ f"id={_log_value(request_id)} method={_log_value(method)} "
+ f"status=exception exception={type(exc).__name__} "
+ f"duration_ms={_duration_ms(started)}"
+ )
return {
"jsonrpc": "2.0",
"id": request_id,
@@ -84,9 +124,22 @@ class McpServer:
def _initialize(self, params: Any) -> dict[str, Any]:
requested = ""
+ client_name = ""
+ client_version = ""
if isinstance(params, dict):
requested = str(params.get("protocolVersion") or "")
+ client_info = params.get("clientInfo")
+ if isinstance(client_info, dict):
+ client_name = str(client_info.get("name") or "")
+ client_version = str(client_info.get("version") or "")
protocol = requested if requested in PROTOCOL_VERSIONS else PROTOCOL_VERSIONS[0]
+ _log_line(
+ "mcp initialize "
+ f"client={_log_value(client_name or 'unknown')} "
+ f"client_version={_log_value(client_version or 'unknown')} "
+ f"requested_protocol={_log_value(requested or 'unspecified')} "
+ f"selected_protocol={protocol}"
+ )
return {
"protocolVersion": protocol,
"capabilities": {
@@ -95,7 +148,7 @@ class McpServer:
"prompts": {"listChanged": False},
},
"serverInfo": {
- "name": "sailfish-devel-mcp",
+ "name": SERVER_NAME,
"version": __version__,
},
"instructions": (
@@ -114,12 +167,33 @@ class McpServer:
raise JsonRpcError(-32602, f"unknown tool: {name}")
args = params.get("arguments") or {}
if not isinstance(args, dict):
+ _log_line(f"mcp tool call rejected tool={_log_value(name)} reason=arguments_not_object")
return tool_error("tool arguments must be an object")
+ arg_keys = ",".join(sorted(str(key) for key in args.keys())) or "-"
+ started = time.monotonic()
+ _log_line(f"mcp tool call start tool={_log_value(name)} arg_keys={_log_value(arg_keys)}")
try:
- return self.registry[name].handler(args)
+ result = self.registry[name].handler(args)
+ is_error = bool(result.get("isError")) if isinstance(result, dict) else False
+ _log_line(
+ "mcp tool call finish "
+ f"tool={_log_value(name)} is_error={str(is_error).lower()} "
+ f"duration_ms={_duration_ms(started)}"
+ )
+ return result
except ValueError as exc:
+ _log_line(
+ "mcp tool call finish "
+ f"tool={_log_value(name)} is_error=true exception=ValueError "
+ f"duration_ms={_duration_ms(started)}"
+ )
return tool_error(str(exc))
except Exception as exc:
+ _log_line(
+ "mcp tool call finish "
+ f"tool={_log_value(name)} is_error=true exception={type(exc).__name__} "
+ f"duration_ms={_duration_ms(started)}"
+ )
return tool_error(
f"{name} failed: {exc}",
{
@@ -216,6 +290,7 @@ def main(argv: list[str] | None = None) -> None:
print(json.dumps(config.public_dict(), indent=2, sort_keys=True))
return
+ _log_line(f"mcp server ready name={SERVER_NAME} version={__version__}")
run_stdio(McpServer(config))