152 lines
6.1 KiB
Python
152 lines
6.1 KiB
Python
"""Dependency-free MCP stdio transport for the local Labyricorn repository."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import logging
|
|
import sys
|
|
from typing import Any, TextIO
|
|
|
|
from . import __version__
|
|
from .errors import LabyricornMcpError
|
|
from .safety import RepositoryGuard
|
|
from .tools import ToolRegistry
|
|
|
|
|
|
SUPPORTED_PROTOCOLS = ("2025-11-25", "2025-06-18", "2025-03-26", "2024-11-05")
|
|
LATEST_PROTOCOL = SUPPORTED_PROTOCOLS[0]
|
|
|
|
|
|
class StdioMcpServer:
|
|
def __init__(self, guard: RepositoryGuard, *, input_stream: TextIO | None = None, output_stream: TextIO | None = None):
|
|
self.guard = guard
|
|
self.registry = ToolRegistry(guard)
|
|
self.input = input_stream or sys.stdin
|
|
self.output = output_stream or sys.stdout
|
|
self.initialized = False
|
|
self.client_initialized = False
|
|
self.protocol_version: str | None = None
|
|
self.log = logging.getLogger("labyricorn_mcp")
|
|
|
|
def serve_forever(self) -> int:
|
|
self.log.info("server started for repository %s", self.guard.root)
|
|
for raw_line in self.input:
|
|
line = raw_line.rstrip("\r\n")
|
|
if not line:
|
|
continue
|
|
try:
|
|
message = json.loads(line)
|
|
except json.JSONDecodeError:
|
|
self._write_error(None, -32700, "Parse error")
|
|
continue
|
|
if not isinstance(message, dict) or isinstance(message.get("id"), (dict, list)):
|
|
self._write_error(message.get("id") if isinstance(message, dict) else None, -32600, "Invalid Request")
|
|
continue
|
|
self._handle(message)
|
|
self.log.info("server stopped after stdin closed")
|
|
return 0
|
|
|
|
def _handle(self, message: dict[str, Any]) -> None:
|
|
method = message.get("method")
|
|
request_id = message.get("id")
|
|
is_notification = "id" not in message
|
|
if message.get("jsonrpc") != "2.0" or not isinstance(method, str):
|
|
if not is_notification:
|
|
self._write_error(request_id, -32600, "Invalid Request")
|
|
return
|
|
params = message.get("params", {})
|
|
if not isinstance(params, dict):
|
|
if not is_notification:
|
|
self._write_error(request_id, -32602, "Invalid params")
|
|
return
|
|
|
|
if is_notification:
|
|
if method == "notifications/initialized" and self.initialized:
|
|
self.client_initialized = True
|
|
return
|
|
|
|
if method == "initialize":
|
|
self._initialize(request_id, params)
|
|
return
|
|
if not self.initialized:
|
|
self._write_error(request_id, -32002, "Server is not initialized")
|
|
return
|
|
if method == "ping":
|
|
self._write_result(request_id, {})
|
|
elif method == "tools/list":
|
|
self._write_result(request_id, {"tools": self.registry.tools})
|
|
elif method == "tools/call":
|
|
self._call_tool(request_id, params)
|
|
else:
|
|
self._write_error(request_id, -32601, "Method not found")
|
|
|
|
def _initialize(self, request_id: Any, params: dict[str, Any]) -> None:
|
|
if self.initialized:
|
|
self._write_error(request_id, -32600, "Server is already initialized")
|
|
return
|
|
requested = params.get("protocolVersion")
|
|
self.protocol_version = requested if requested in SUPPORTED_PROTOCOLS else LATEST_PROTOCOL
|
|
self.initialized = True
|
|
self._write_result(
|
|
request_id,
|
|
{
|
|
"protocolVersion": self.protocol_version,
|
|
"capabilities": {"tools": {"listChanged": False}},
|
|
"serverInfo": {"name": "labyricorn-mcp", "version": __version__},
|
|
"instructions": (
|
|
"This server manages only the configured local Labyricorn Git checkout. "
|
|
"It has no production administration capability. Commits and pushes are separate explicit tools."
|
|
),
|
|
},
|
|
)
|
|
|
|
def _call_tool(self, request_id: Any, params: dict[str, Any]) -> None:
|
|
name = params.get("name")
|
|
if not isinstance(name, str):
|
|
self._write_error(request_id, -32602, "Tool name is required")
|
|
return
|
|
self.log.info("tool invocation: %s", name)
|
|
try:
|
|
result = self.registry.call(name, params.get("arguments"))
|
|
except LabyricornMcpError as exc:
|
|
self.log.warning("tool rejected: %s code=%s", name, exc.code)
|
|
payload = {"error": exc.as_dict()}
|
|
self._write_result(
|
|
request_id,
|
|
{
|
|
"content": [{"type": "text", "text": json.dumps(payload, ensure_ascii=False)}],
|
|
"structuredContent": payload,
|
|
"isError": True,
|
|
},
|
|
)
|
|
except Exception:
|
|
self.log.exception("unexpected tool failure: %s", name)
|
|
payload = {"error": {"code": "INTERNAL_ERROR", "message": "The tool failed unexpectedly; see local stderr diagnostics."}}
|
|
self._write_result(
|
|
request_id,
|
|
{
|
|
"content": [{"type": "text", "text": json.dumps(payload)}],
|
|
"structuredContent": payload,
|
|
"isError": True,
|
|
},
|
|
)
|
|
else:
|
|
self._write_result(
|
|
request_id,
|
|
{
|
|
"content": [{"type": "text", "text": json.dumps(result, ensure_ascii=False, default=str)}],
|
|
"structuredContent": result,
|
|
"isError": False,
|
|
},
|
|
)
|
|
|
|
def _write_result(self, request_id: Any, result: dict[str, Any]) -> None:
|
|
self._write({"jsonrpc": "2.0", "id": request_id, "result": result})
|
|
|
|
def _write_error(self, request_id: Any, code: int, message: str) -> None:
|
|
self._write({"jsonrpc": "2.0", "id": request_id, "error": {"code": code, "message": message}})
|
|
|
|
def _write(self, message: dict[str, Any]) -> None:
|
|
self.output.write(json.dumps(message, ensure_ascii=False, separators=(",", ":")) + "\n")
|
|
self.output.flush()
|