feat: MCP server wrapping gateway HTTP API (12 tools)

This commit is contained in:
Claude Code
2026-03-28 18:59:48 -04:00
parent e8a23f2b11
commit 44c13f229c
2 changed files with 244 additions and 0 deletions
+244
View File
@@ -0,0 +1,244 @@
"""
Mortdecai Gateway MCP Server.
Wraps the gateway HTTP API as MCP tools so Claude can operate
Mortdecai natively. All game operations go through the gateway —
this server never touches Minecraft directly.
"""
import json
import subprocess
from pathlib import Path
import httpx
from mcp.server.fastmcp import FastMCP
GATEWAY_URL = "http://localhost:8500"
SCRIPTS_DIR = Path(__file__).parent.parent / "scripts"
mcp = FastMCP("mortdecai-gateway")
async def _get(path: str) -> dict:
async with httpx.AsyncClient(timeout=10) as client:
resp = await client.get(f"{GATEWAY_URL}{path}")
resp.raise_for_status()
return resp.json()
async def _post(path: str, body: dict | None = None) -> dict:
async with httpx.AsyncClient(timeout=90) as client:
resp = await client.post(f"{GATEWAY_URL}{path}", json=body or {})
resp.raise_for_status()
return resp.json()
async def _patch(path: str, body: dict | None = None) -> dict:
async with httpx.AsyncClient(timeout=10) as client:
resp = await client.patch(f"{GATEWAY_URL}{path}", json=body or {})
resp.raise_for_status()
return resp.json()
# --- Gateway lifecycle ---
@mcp.tool()
def gateway_start() -> str:
"""Start the Mortdecai gateway if not running."""
result = subprocess.run(
["bash", str(SCRIPTS_DIR / "start-gateway.sh")],
capture_output=True, text=True, timeout=30,
)
return result.stdout + result.stderr
@mcp.tool()
def gateway_stop() -> str:
"""Stop the Mortdecai gateway."""
result = subprocess.run(
["bash", str(SCRIPTS_DIR / "stop-gateway.sh")],
capture_output=True, text=True, timeout=10,
)
return result.stdout + result.stderr
@mcp.tool()
def gateway_restart() -> str:
"""Restart the Mortdecai gateway (stop + start)."""
stop = subprocess.run(
["bash", str(SCRIPTS_DIR / "stop-gateway.sh")],
capture_output=True, text=True, timeout=10,
)
import time; time.sleep(2)
start = subprocess.run(
["bash", str(SCRIPTS_DIR / "start-gateway.sh")],
capture_output=True, text=True, timeout=30,
)
return stop.stdout + start.stdout + start.stderr
@mcp.tool()
async def gateway_status() -> str:
"""Get full gateway status: providers, sessions, oracle state."""
try:
data = await _get("/v2/status")
return json.dumps(data, indent=2)
except httpx.ConnectError:
return "Gateway is DOWN — not reachable on port 8500"
except Exception as e:
return f"Error: {e}"
@mcp.tool()
async def gateway_health() -> str:
"""Quick health check — is the gateway alive?"""
try:
data = await _get("/v2/health")
return json.dumps(data, indent=2)
except httpx.ConnectError:
return "DOWN"
except Exception as e:
return f"Error: {e}"
# --- Player commands (through gateway) ---
@mcp.tool()
async def gateway_command(
player: str,
text: str,
mode: str = "sudo",
server: str = "dev",
) -> str:
"""Send a command through the gateway as if a player typed it in-game.
Args:
player: Minecraft player name
text: The command text (e.g. "give me a diamond")
mode: Command mode — sudo, pray, ask, or raw
server: Server target — dev or prod
"""
try:
data = await _post("/v2/quick", {
"player": player,
"text": text,
"server": server,
"command_type": mode,
})
return json.dumps(data, indent=2)
except Exception as e:
return f"Error: {e}"
# --- Brain management (hot-swap providers) ---
@mcp.tool()
async def gateway_brain_set(
role: str,
provider: str,
model: str,
) -> str:
"""Hot-swap the AI provider and model for a gateway role.
Args:
role: Agent role — eye, hand, voice, opus, architect, orchestrator
provider: Provider name — anthropic, codex, openai, ollama, regex
model: Model identifier (e.g. "gpt-5.1-codex-mini", "claude-opus-4-20250514")
"""
try:
data = await _patch(f"/v2/brain/{role}", {
"provider": provider,
"model": model,
})
return json.dumps(data, indent=2)
except Exception as e:
return f"Error: {e}"
@mcp.tool()
async def gateway_brain_save(role: str) -> str:
"""Persist a brain's current live override to agents.yaml on disk.
Args:
role: Agent role to save
"""
try:
data = await _post(f"/v2/brain/{role}/save")
return json.dumps(data, indent=2)
except Exception as e:
return f"Error: {e}"
@mcp.tool()
async def gateway_brain_reload(role: str) -> str:
"""Clear in-memory override, reload brain config from agents.yaml.
Args:
role: Agent role to reload
"""
try:
data = await _post(f"/v2/brain/{role}/reload")
return json.dumps(data, indent=2)
except Exception as e:
return f"Error: {e}"
# --- Session management ---
@mcp.tool()
async def gateway_sessions_clear(player: str, mode: str = "") -> str:
"""Clear sessions for a player. Optionally filter by mode.
Args:
player: Player name
mode: Optional mode filter (sudo, pray, ask, raw). Empty = all modes.
"""
try:
url = f"/v2/sessions/clear/{player}"
if mode:
url += f"?mode={mode}"
data = await _post(url)
return json.dumps(data, indent=2)
except Exception as e:
return f"Error: {e}"
@mcp.tool()
async def gateway_sessions_reset() -> str:
"""Clear ALL sessions for ALL players. Use with caution."""
try:
data = await _post("/v2/sessions/reset")
return json.dumps(data, indent=2)
except Exception as e:
return f"Error: {e}"
# --- Logs ---
@mcp.tool()
def gateway_logs(lines: int = 50) -> str:
"""Read recent gateway log output.
Args:
lines: Number of lines to read from the end (default 50)
"""
log_path = Path("/tmp/mortdecai-gateway.log")
if not log_path.exists():
return "No gateway log file found"
try:
result = subprocess.run(
["tail", f"-{lines}", str(log_path)],
capture_output=True, text=True, timeout=5,
)
return result.stdout or "Log file is empty"
except Exception as e:
return f"Error reading logs: {e}"
if __name__ == "__main__":
mcp.run(transport="stdio")