mirror of
https://github.com/semantica-agi/semantica.git
synced 2026-08-29 04:26:20 +00:00
Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>
224 lines
7.1 KiB
Python
224 lines
7.1 KiB
Python
"""
|
|
Semantica MCP Server — JSON-RPC 2.0 over stdio.
|
|
|
|
Implements the Model Context Protocol so any MCP-compatible AI tool
|
|
(Claude Code, Cursor, Windsurf, Cline, Continue, VS Code Copilot, etc.)
|
|
can interact with the Semantica knowledge graph.
|
|
|
|
Run:
|
|
python -m mcp # via __main__.py
|
|
python -m mcp.server # direct
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import logging
|
|
import sys
|
|
from typing import Any
|
|
|
|
from mcp.resources import RESOURCE_DEFINITIONS, handle_resource_read
|
|
from mcp.tools import TOOL_DEFINITIONS
|
|
|
|
log = logging.getLogger("semantica.mcp.server")
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Helpers
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def _ok(request_id: Any, result: Any) -> dict:
|
|
return {"jsonrpc": "2.0", "id": request_id, "result": result}
|
|
|
|
|
|
def _err(request_id: Any, code: int, message: str, data: Any = None) -> dict:
|
|
error: dict = {"code": code, "message": message}
|
|
if data is not None:
|
|
error["data"] = data
|
|
return {"jsonrpc": "2.0", "id": request_id, "error": error}
|
|
|
|
|
|
# JSON-RPC error codes
|
|
_PARSE_ERROR = -32700
|
|
_METHOD_NOT_FOUND = -32601
|
|
_INVALID_PARAMS = -32602
|
|
_INTERNAL_ERROR = -32603
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Tool dispatch index
|
|
# ---------------------------------------------------------------------------
|
|
|
|
_TOOL_INDEX: dict[str, dict] = {t["name"]: t for t in TOOL_DEFINITIONS}
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Request handlers
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def _handle_initialize(req_id: Any, params: dict) -> dict:
|
|
return _ok(req_id, {
|
|
"protocolVersion": "2024-11-05",
|
|
"capabilities": {
|
|
"tools": {},
|
|
"resources": {},
|
|
},
|
|
"serverInfo": {
|
|
"name": "semantica-mcp",
|
|
"version": "0.4.0",
|
|
},
|
|
})
|
|
|
|
|
|
def _handle_tools_list(req_id: Any, _params: dict) -> dict:
|
|
tools = [
|
|
{
|
|
"name": t["name"],
|
|
"description": t["description"],
|
|
"inputSchema": t["inputSchema"],
|
|
}
|
|
for t in TOOL_DEFINITIONS
|
|
]
|
|
return _ok(req_id, {"tools": tools})
|
|
|
|
|
|
def _handle_tools_call(req_id: Any, params: dict) -> dict:
|
|
name = params.get("name", "")
|
|
args = params.get("arguments", {}) or {}
|
|
|
|
tool = _TOOL_INDEX.get(name)
|
|
if tool is None:
|
|
return _err(req_id, _METHOD_NOT_FOUND, f"Unknown tool: {name}")
|
|
|
|
try:
|
|
result = tool["_handler"](args)
|
|
except Exception as exc:
|
|
log.exception("Tool %s raised an exception", name)
|
|
return _err(req_id, _INTERNAL_ERROR, str(exc))
|
|
|
|
# MCP spec: content must be a list of content items
|
|
return _ok(req_id, {
|
|
"content": [{"type": "text", "text": json.dumps(result, ensure_ascii=False)}],
|
|
"isError": "error" in result,
|
|
})
|
|
|
|
|
|
def _handle_resources_list(req_id: Any, _params: dict) -> dict:
|
|
return _ok(req_id, {"resources": RESOURCE_DEFINITIONS})
|
|
|
|
|
|
def _handle_resources_read(req_id: Any, params: dict) -> dict:
|
|
uri = params.get("uri", "").strip()
|
|
if not uri:
|
|
return _err(req_id, _INVALID_PARAMS, "uri is required")
|
|
resource = handle_resource_read(uri)
|
|
return _ok(req_id, {
|
|
"contents": [
|
|
{
|
|
"uri": resource["uri"],
|
|
"mimeType": resource.get("mimeType", "application/json"),
|
|
"text": resource.get("text", ""),
|
|
}
|
|
]
|
|
})
|
|
|
|
|
|
def _handle_ping(req_id: Any, _params: dict) -> dict:
|
|
return _ok(req_id, {})
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Dispatch table
|
|
# ---------------------------------------------------------------------------
|
|
|
|
_DISPATCH = {
|
|
"initialize": _handle_initialize,
|
|
"tools/list": _handle_tools_list,
|
|
"tools/call": _handle_tools_call,
|
|
"resources/list": _handle_resources_list,
|
|
"resources/read": _handle_resources_read,
|
|
"ping": _handle_ping,
|
|
}
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Main server class
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class SemanticaMCPServer:
|
|
"""Semantica MCP server — reads JSON-RPC requests from stdin, writes to stdout."""
|
|
|
|
def __init__(self, *, debug: bool = False) -> None:
|
|
level = logging.DEBUG if debug else logging.WARNING
|
|
logging.basicConfig(stream=sys.stderr, level=level,
|
|
format="%(name)s %(levelname)s %(message)s")
|
|
|
|
# ------------------------------------------------------------------
|
|
def dispatch(self, request: dict) -> dict | None:
|
|
"""Process one JSON-RPC request and return a response dict (or None for notifications)."""
|
|
req_id = request.get("id") # None for notifications
|
|
method = request.get("method", "")
|
|
params = request.get("params") or {}
|
|
|
|
handler = _DISPATCH.get(method)
|
|
if handler is None:
|
|
if req_id is None:
|
|
return None # Notification — ignore unknown methods silently
|
|
return _err(req_id, _METHOD_NOT_FOUND, f"Method not found: {method}")
|
|
|
|
try:
|
|
return handler(req_id, params)
|
|
except Exception as exc:
|
|
log.exception("Unhandled error in method %s", method)
|
|
if req_id is None:
|
|
return None
|
|
return _err(req_id, _INTERNAL_ERROR, str(exc))
|
|
|
|
# ------------------------------------------------------------------
|
|
def run(self) -> None:
|
|
"""Start the stdio event loop."""
|
|
log.info("Semantica MCP server starting (stdio)")
|
|
for raw_line in sys.stdin:
|
|
raw_line = raw_line.strip()
|
|
if not raw_line:
|
|
continue
|
|
try:
|
|
request = json.loads(raw_line)
|
|
except json.JSONDecodeError as exc:
|
|
response = _err(None, _PARSE_ERROR, f"Parse error: {exc}")
|
|
_write(response)
|
|
continue
|
|
|
|
if isinstance(request, list):
|
|
# Batch request
|
|
responses = []
|
|
for req in request:
|
|
resp = self.dispatch(req)
|
|
if resp is not None:
|
|
responses.append(resp)
|
|
if responses:
|
|
_write(responses)
|
|
else:
|
|
resp = self.dispatch(request)
|
|
if resp is not None:
|
|
_write(resp)
|
|
|
|
|
|
def _write(obj: Any) -> None:
|
|
sys.stdout.write(json.dumps(obj, ensure_ascii=False) + "\n")
|
|
sys.stdout.flush()
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Entry point
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def main() -> None:
|
|
import argparse
|
|
parser = argparse.ArgumentParser(description="Semantica MCP Server")
|
|
parser.add_argument("--debug", action="store_true", help="Enable debug logging")
|
|
args = parser.parse_args()
|
|
SemanticaMCPServer(debug=args.debug).run()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|