194 lines
7.6 KiB
Python
194 lines
7.6 KiB
Python
#!/usr/bin/env python3
|
|
"""Compact MCP facade for the Athena host operator."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
import socket
|
|
import sys
|
|
from typing import Any
|
|
|
|
|
|
VERSION = "3.0.0"
|
|
SOCKET_PATH = os.environ.get("ATHENA_OPERATOR_SOCKET", "/operator/operator.sock")
|
|
|
|
if hasattr(sys.stdin, "reconfigure"):
|
|
sys.stdin.reconfigure(encoding="utf-8", errors="replace")
|
|
if hasattr(sys.stdout, "reconfigure"):
|
|
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
|
|
|
|
|
TOOLS = [
|
|
{
|
|
"name": "athena_operator_inspect",
|
|
"description": "START HERE once for Athena work. Inspect the requested live area without changing it.",
|
|
"inputSchema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"subject": {"type": "string", "enum": ["overview", "git_status", "containers", "models", "jobs"], "default": "overview"},
|
|
},
|
|
"additionalProperties": False,
|
|
},
|
|
},
|
|
{
|
|
"name": "athena_operator_search_source",
|
|
"description": "Find the exact source path and line before reading or editing it. Returns at most 20 matches.",
|
|
"inputSchema": {
|
|
"type": "object",
|
|
"properties": {"query": {"type": "string", "minLength": 1, "maxLength": 120}},
|
|
"required": ["query"],
|
|
"additionalProperties": False,
|
|
},
|
|
},
|
|
{
|
|
"name": "athena_operator_read_source",
|
|
"description": (
|
|
"Read only the needed section of one known source file. Default 80 and maximum "
|
|
"200 lines. A not-found result must not be retried by guessing more paths."
|
|
),
|
|
"inputSchema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"path": {"type": "string", "pattern": "^[A-Za-z0-9_.+/-]{1,240}$"},
|
|
"start_line": {"type": "integer", "minimum": 1, "maximum": 1000000, "default": 1},
|
|
"line_count": {"type": "integer", "minimum": 1, "maximum": 200, "default": 80},
|
|
},
|
|
"required": ["path"],
|
|
"additionalProperties": False,
|
|
},
|
|
},
|
|
{
|
|
"name": "athena_operator_terminal",
|
|
"description": (
|
|
"General bounded terminal on Athena for focused Docker, Git, file, HTTP, model "
|
|
"and test work when no structured operation fits. Limit noisy output yourself. "
|
|
"Power control and Athena reachability changes are blocked."
|
|
),
|
|
"inputSchema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"command": {"type": "string", "minLength": 1, "maxLength": 8000},
|
|
"cwd": {"type": "string", "maxLength": 500, "default": "/opt/mike-ai/stack"},
|
|
"timeout_seconds": {"type": "integer", "minimum": 1, "maximum": 3600, "default": 300},
|
|
"max_output_chars": {"type": "integer", "minimum": 1000, "maximum": 30000, "default": 8000},
|
|
},
|
|
"required": ["command"],
|
|
"additionalProperties": False,
|
|
},
|
|
},
|
|
{
|
|
"name": "athena_operator_change",
|
|
"description": (
|
|
"Apply one durable change that the user has requested. Operations: patch_update, "
|
|
"file_update, mcp_release, run_checks, compose_deploy, container_action, "
|
|
"openwebui_sync, git_publish, model_download, benchmark, recovery. Use a small "
|
|
"patch for edits and file_update only for a new or intentionally replaced file. "
|
|
"mcp_release can perform the complete MCP build/test/deploy/publish workflow."
|
|
),
|
|
"inputSchema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"operation": {
|
|
"type": "string",
|
|
"enum": ["patch_update", "file_update", "mcp_release", "run_checks", "compose_deploy", "container_action", "openwebui_sync", "git_publish", "model_download", "benchmark", "recovery"],
|
|
},
|
|
"payload": {"type": "object"},
|
|
},
|
|
"required": ["operation", "payload"],
|
|
"additionalProperties": False,
|
|
},
|
|
},
|
|
{
|
|
"name": "athena_operator_job",
|
|
"description": "Poll one asynchronous change returned by athena_operator_change. Do not start a duplicate job.",
|
|
"inputSchema": {
|
|
"type": "object",
|
|
"properties": {"job_id": {"type": "string", "pattern": "^[0-9a-f]{32}$"}},
|
|
"required": ["job_id"],
|
|
"additionalProperties": False,
|
|
},
|
|
},
|
|
]
|
|
|
|
|
|
def request(action: str, arguments: dict[str, Any]) -> dict[str, Any]:
|
|
payload = json.dumps({"action": action, "arguments": arguments}, ensure_ascii=False, separators=(",", ":")).encode() + b"\n"
|
|
with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as client:
|
|
client.settimeout(3700 if action == "change" else 30)
|
|
client.connect(SOCKET_PATH)
|
|
client.sendall(payload)
|
|
chunks = bytearray()
|
|
while not chunks.endswith(b"\n"):
|
|
part = client.recv(65536)
|
|
if not part:
|
|
break
|
|
chunks.extend(part)
|
|
if len(chunks) > 2_000_000:
|
|
raise RuntimeError("operator response exceeds limit")
|
|
response = json.loads(chunks)
|
|
if not response.get("ok"):
|
|
raise RuntimeError(response.get("error", "operator request failed"))
|
|
return response["result"]
|
|
|
|
|
|
def call(name: str, arguments: dict[str, Any]) -> dict[str, Any]:
|
|
mapping = {
|
|
"athena_operator_inspect": "inspect",
|
|
"athena_operator_search_source": "search_source",
|
|
"athena_operator_read_source": "read_source",
|
|
"athena_operator_terminal": "terminal",
|
|
"athena_operator_change": "change",
|
|
"athena_operator_job": "job",
|
|
}
|
|
action = mapping.get(name)
|
|
if action is None:
|
|
return {"ok": False, "error": "unknown tool", "retry": False}
|
|
return request(action, arguments)
|
|
|
|
|
|
def emit(request_id: Any, result: Any = None, error: dict[str, Any] | None = None) -> None:
|
|
message = {"jsonrpc": "2.0", "id": request_id}
|
|
message["error" if error else "result"] = error or result
|
|
sys.stdout.write(json.dumps(message, ensure_ascii=False, separators=(",", ":")) + "\n")
|
|
sys.stdout.flush()
|
|
|
|
|
|
def handle(message: dict[str, Any]) -> None:
|
|
method, request_id = message.get("method"), message.get("id")
|
|
if method == "initialize":
|
|
emit(request_id, {
|
|
"protocolVersion": message.get("params", {}).get("protocolVersion", "2024-11-05"),
|
|
"capabilities": {"tools": {"listChanged": False}},
|
|
"serverInfo": {"name": "mike-ai-athena-operator", "version": VERSION},
|
|
})
|
|
elif method == "tools/list":
|
|
emit(request_id, {"tools": TOOLS})
|
|
elif method == "tools/call":
|
|
params = message.get("params") or {}
|
|
try:
|
|
value = call(str(params.get("name", "")), params.get("arguments") or {})
|
|
emit(request_id, {
|
|
"content": [{"type": "text", "text": json.dumps(value, ensure_ascii=False, separators=(",", ":"))}],
|
|
"structuredContent": value,
|
|
"isError": False,
|
|
})
|
|
except Exception as exc:
|
|
emit(request_id, {"content": [{"type": "text", "text": f"ERROR: {exc}"}], "isError": True})
|
|
elif request_id is not None:
|
|
emit(request_id, error={"code": -32601, "message": "method not found"})
|
|
|
|
|
|
def main() -> None:
|
|
for line in sys.stdin:
|
|
try:
|
|
if line.strip():
|
|
handle(json.loads(line))
|
|
except Exception as exc:
|
|
sys.stderr.write(f"MCP input error: {exc}\n")
|
|
sys.stderr.flush()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|