"""Muse MCP server — JSON-RPC 2.0 engine with domain activation gate (§KD-5a.E).""" from __future__ import annotations import json import logging import pathlib from typing import Any from muse.core.repo import find_repo_root from muse.mcp.bootstrap import bootstrap_knowtation_mcp from muse.mcp.errors import ( INTERNAL_ERROR, INVALID_PARAMS, INVALID_REQUEST, METHOD_NOT_FOUND, MCPApplicationError, TOOL_ERROR, error_response, success_response, ) from muse.mcp.namespaces.knowtation import ( PROMPT_NAMES, TOOL_NAMES, KnowtationNamespace, ) from muse.plugins.knowtation.detector import classify_repo from muse.plugins.registry import read_domain logger = logging.getLogger(__name__) _PROTOCOL_VERSION = "2024-11-05" _SERVER_NAME = "muse-mcp" _SERVER_VERSION = "0.2.0" class MuseMCPServer: """Lightweight MCP server bound to a single Muse repository root.""" def __init__(self, root: pathlib.Path | None = None) -> None: self.root = root or find_repo_root() or pathlib.Path.cwd() self._domain = read_domain(self.root) self._knowtation: KnowtationNamespace | None = None self._detected = classify_repo(self.root) if self._domain == "knowtation": bootstrap_knowtation_mcp(self.root) self._knowtation = KnowtationNamespace(self.root) @property def capabilities(self) -> dict[str, Any]: """Advisory detection hint — never auto-mounts namespaces.""" return { "active_domain": self._domain, "detected_domain": self._detected.domain, "detected_confidence": self._detected.confidence, "knowtation_mounted": self._knowtation is not None, } def handle_request(self, raw: dict[str, Any]) -> dict[str, Any] | None: """Process one JSON-RPC request dict; return response or None (notification).""" req_id = raw.get("id") is_notification = "id" not in raw method = raw.get("method") if not isinstance(method, str): if is_notification: return None return error_response(req_id, INVALID_REQUEST, "Missing method") params = raw.get("params") if params is None: params_obj: dict[str, Any] = {} elif isinstance(params, dict): params_obj = params else: if is_notification: return None return error_response(req_id, INVALID_PARAMS, "params must be an object") try: result = self._dispatch(method, params_obj) if is_notification: return None return success_response(req_id, result) except MCPApplicationError as exc: if is_notification: return None return error_response( req_id, TOOL_ERROR, exc.message, data=exc.to_data(), ) except Exception: logger.exception("MCP internal error on method %s", method) if is_notification: return None return error_response(req_id, INTERNAL_ERROR, "Internal error") def _dispatch(self, method: str, params: dict[str, Any]) -> Any: match method: case "initialize": return { "protocolVersion": _PROTOCOL_VERSION, "capabilities": { "tools": {"listChanged": False}, "resources": {"subscribe": False, "listChanged": False}, "prompts": {"listChanged": False}, }, "serverInfo": {"name": _SERVER_NAME, "version": _SERVER_VERSION}, "meta": self.capabilities, } case "tools/list": if self._knowtation is None: return {"tools": []} return {"tools": self._knowtation.list_tools()} case "tools/call": name = params.get("name") if not isinstance(name, str): raise MCPApplicationError("INVALID_PARAMS", "tool name required") if self._knowtation is None: raise MCPApplicationError("DOMAIN_INACTIVE", "knowtation namespace not mounted") arguments = params.get("arguments") if arguments is None: arguments = {} if not isinstance(arguments, dict): raise MCPApplicationError("INVALID_PARAMS", "arguments must be object") content = self._knowtation.call_tool(name, arguments) return { "content": [{"type": "text", "text": json.dumps(content, default=str)}], "isError": False, } case "resources/list": if self._knowtation is None: return {"resources": []} return {"resources": self._knowtation.list_resources()} case "resources/read": uri = params.get("uri") if not isinstance(uri, str): raise MCPApplicationError("INVALID_PARAMS", "uri required") if self._knowtation is None: raise MCPApplicationError("DOMAIN_INACTIVE", "knowtation namespace not mounted") body = self._knowtation.read_resource(uri) return { "contents": [ { "uri": uri, "mimeType": "application/json", "text": json.dumps(body, default=str), } ] } case "prompts/list": if self._knowtation is None: return {"prompts": []} return {"prompts": self._knowtation.list_prompts()} case "prompts/get": name = params.get("name") if not isinstance(name, str): raise MCPApplicationError("INVALID_PARAMS", "prompt name required") if self._knowtation is None: raise MCPApplicationError("DOMAIN_INACTIVE", "knowtation namespace not mounted") arguments = params.get("arguments") or {} if not isinstance(arguments, dict): raise MCPApplicationError("INVALID_PARAMS", "arguments must be object") return self._knowtation.get_prompt(name, arguments) case "ping": return {} case _: raise MCPApplicationError("METHOD_NOT_FOUND", f"Unknown method: {method}") def expected_tool_count(domain: str) -> int: """Return expected mounted tool count for tests.""" return len(TOOL_NAMES) if domain == "knowtation" else 0 def expected_prompt_count(domain: str) -> int: return len(PROMPT_NAMES) if domain == "knowtation" else 0 __all__ = [ "MuseMCPServer", "expected_prompt_count", "expected_tool_count", ]