summaryrefslogtreecommitdiff
path: root/src/sailfish_devel_mcp/server.py
diff options
context:
space:
mode:
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))