From eb92561bd685961889cc553cd96db4a569eefec5 Mon Sep 17 00:00:00 2001 From: Andrew Branson Date: Mon, 29 Jun 2026 16:40:30 +0200 Subject: 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. --- src/sailfish_devel_mcp/server.py | 79 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 77 insertions(+), 2 deletions(-) (limited to 'src/sailfish_devel_mcp/server.py') 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)) -- cgit v1.2.3