diff options
Diffstat (limited to 'src/sailfish_devel_mcp/server.py')
| -rw-r--r-- | src/sailfish_devel_mcp/server.py | 79 |
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)) |
