Research-Stack/4-Infrastructure/infra/remote_lean_proof_mcp.py
allaun 318db01e31 refactor(infra): decommission AWS deployment artifacts
Remove EC2/RDS deployment scripts, EC2 watchdog, and RDS substrate
provisioning files. Drop the legacy AWS API MCP server from all IDE
configs. Replace hardcoded AWS proof-server and RDS default endpoints
with localhost/env-only placeholders.

Deleted:
- 4-Infrastructure/infra/deploy_aws_language_proof_server.sh
- 4-Infrastructure/infra/aws_language_proof_server_user_data.sh
- 4-Infrastructure/infra/ec2-configuration.nix
- 4-Infrastructure/infra/ec2_idle_watchdog.py
- 4-Infrastructure/infra/ec2-idle-watchdog.service
- 4-Infrastructure/infra/ec2-idle-watchdog.timer
- 4-Infrastructure/deploy/rds-substrate/*

Changed:
- .mcp.json, .cursor/mcp.json, .roo/mcp.json, .vscode/mcp.json:
  remove aws server, make remote-lean-proof URL env-only
- .vscode/settings.json: remoteLeanProof.url -> localhost
- 4-Infrastructure/infra/remote_lean_proof_mcp.py: default URL -> localhost
- ene-rds/ene-session-sync: default RDS_HOST -> localhost
- RECOVERY.md, INFRASTRUCTURE.md, TODO_MAP.md: remove AWS/EC2 refs

Build: cargo check ene-rds OK; python3 -m py_compile OK; JSON valid
2026-06-19 22:42:34 -05:00

302 lines
9.9 KiB
Python

#!/usr/bin/env python3
"""MCP client shim for the Netcup Lean proof server."""
from __future__ import annotations
import hashlib
import json
import os
import sys
import urllib.error
import urllib.parse
import urllib.request
from pathlib import Path
from typing import Any
SERVER_NAME = "remote-lean-proof"
SERVER_VERSION = "0.1.0"
DEFAULT_URL = "http://localhost:8787"
DEFAULT_TOKEN_FILE = Path.home() / ".config/ene/language-proof-server.token"
def json_text(data: Any) -> list[dict[str, str]]:
return [{"type": "text", "text": json.dumps(data, indent=2, sort_keys=True)}]
def token() -> str:
direct = os.environ.get("PROOF_SERVER_TOKEN")
if direct:
return direct.strip()
path = Path(os.environ.get("PROOF_SERVER_TOKEN_FILE", str(DEFAULT_TOKEN_FILE))).expanduser()
try:
return path.read_text().strip()
except FileNotFoundError:
return ""
def base_url() -> str:
return os.environ.get("PROOF_SERVER_URL", DEFAULT_URL).rstrip("/")
def proof_urls() -> list[str]:
raw = os.environ.get("PROOF_SERVER_URLS", "").strip()
urls = [item.strip().rstrip("/") for item in raw.split(",") if item.strip()]
if not urls:
urls = [base_url()]
return list(dict.fromkeys(urls))
def ordered_urls(payload: dict[str, Any] | None = None) -> list[str]:
urls = proof_urls()
if len(urls) <= 1 or payload is None:
return urls
key = str(payload.get("name") or payload.get("target") or payload.get("agent") or "")
offset = int(hashlib.sha256(key.encode("utf-8")).hexdigest(), 16) % len(urls)
return urls[offset:] + urls[:offset]
def request_json_from(
url: str,
path: str,
payload: dict[str, Any] | None = None,
timeout: int = 120,
) -> dict[str, Any]:
headers = {"Accept": "application/json", "User-Agent": f"{SERVER_NAME}/{SERVER_VERSION}"}
body = None
method = "GET"
if payload is not None:
body = json.dumps(payload).encode("utf-8")
headers["Content-Type"] = "application/json"
method = "POST"
auth = token()
if auth:
headers["Authorization"] = f"Bearer {auth}"
req = urllib.request.Request(f"{url}{path}", data=body, headers=headers, method=method)
try:
with urllib.request.urlopen(req, timeout=timeout) as response:
data = json.loads(response.read().decode("utf-8"))
if isinstance(data, dict):
data.setdefault("proof_server_url", url)
return data
except urllib.error.HTTPError as exc:
text = exc.read().decode("utf-8", errors="replace")
try:
data = json.loads(text)
except json.JSONDecodeError:
data = {"error": text}
data["http_status"] = exc.code
data["ok"] = False
data["proof_server_url"] = url
return data
except Exception as exc:
return {"ok": False, "error": f"{type(exc).__name__}: {exc}", "proof_server_url": url}
def request_json(path: str, payload: dict[str, Any] | None = None, timeout: int = 120) -> dict[str, Any]:
failures = []
for url in ordered_urls(payload):
data = request_json_from(url, path, payload, timeout)
if data.get("ok"):
return data
failures.append(data)
return {"ok": False, "error": "all proof servers failed", "failures": failures}
def tool_status(_: dict[str, Any]) -> dict[str, Any]:
nodes = [request_json_from(url, "/health", timeout=10) for url in proof_urls()]
healthy = [node for node in nodes if node.get("ok")]
return {
"ok": bool(healthy),
"server": SERVER_NAME,
"version": SERVER_VERSION,
"proof_server_url": base_url(),
"proof_server_urls": proof_urls(),
"token_configured": bool(token()),
"health": healthy[0] if healthy else (nodes[0] if nodes else {"ok": False}),
"nodes": nodes,
}
def _ene_fetch(url: str, path: str, timeout: int = 8) -> dict[str, Any] | str:
"""Fetch from ENE API. Returns parsed JSON dict, raw string, or error dict."""
full_url = f"{url}{path}"
try:
req = urllib.request.Request(
full_url,
headers={"Accept": "application/json"},
method="GET",
)
with urllib.request.urlopen(req, timeout=timeout) as resp:
status = resp.status
content_type = resp.headers.get("content-type", "")
raw = resp.read().decode("utf-8")
except urllib.error.HTTPError as exc:
raw = exc.read().decode("utf-8", errors="replace")
try:
data = json.loads(raw)
except json.JSONDecodeError:
data = {
"ok": False,
"error": "non-json response from ENE API",
"body": raw,
}
if isinstance(data, dict):
data["http_status"] = exc.code
data["url"] = full_url
return data
except Exception as exc:
return {"ok": False, "error": f"{type(exc).__name__}: {exc}", "url": full_url}
else:
try:
return json.loads(raw)
except json.JSONDecodeError:
return {
"ok": False,
"error": "non-json response from ENE API",
"status": status,
"content_type": content_type,
"body": raw,
"url": full_url,
}
def ene_query(query: str, limit: int = 5) -> dict[str, Any]:
"""Query the local ENE API for context relevant to *query*."""
url = os.environ.get("ENE_API_URL", "http://127.0.0.1:3000")
search_data = _ene_fetch(url, f"/search?q={urllib.parse.quote(query)}&limit={limit}&semantic=false")
wiki_data = _ene_fetch(url, f"/wiki/search?q={urllib.parse.quote(query)}&limit={limit}")
sessions_data = _ene_fetch(url, "/sessions?limit=3")
return {
"ok": True,
"search": search_data,
"wiki": wiki_data,
"recent_sessions": sessions_data,
"query": query,
}
def tool_check(args: dict[str, Any]) -> dict[str, Any]:
name = args.get("name") or "agent_check"
code = args.get("code") or ""
ene_ctx = ene_query(name)
payload = {
"name": name,
"code": code,
"timeout_s": args.get("timeout_s", 120),
"agent": args.get("agent") or "hermes",
"ene_context": ene_ctx,
}
return request_json("/lean/check", payload, timeout=int(payload["timeout_s"]) + 15)
def tool_build(args: dict[str, Any]) -> dict[str, Any]:
target = args.get("target") or ""
payload = {
"target": target,
"timeout_s": args.get("timeout_s", 300),
"agent": args.get("agent") or "hermes",
"ene_context": ene_query(f"lake build {target}".strip()),
}
return request_json("/lake/build", payload, timeout=int(payload["timeout_s"]) + 15)
TOOLS = {
"proof_status": {
"description": "Return health and token status for the remote Lean proof server.",
"inputSchema": {"type": "object", "properties": {}},
"handler": tool_status,
},
"lean_check": {
"description": "Check an inline Lean file on the remote proof server and return its receipt.",
"inputSchema": {
"type": "object",
"required": ["code"],
"properties": {
"code": {"type": "string"},
"name": {"type": "string"},
"timeout_s": {"type": "integer", "minimum": 1, "maximum": 900},
"agent": {"type": "string"},
},
},
"handler": tool_check,
},
"lake_build": {
"description": "Run an allowlisted lake build target on the remote proof server.",
"inputSchema": {
"type": "object",
"properties": {
"target": {"type": "string"},
"timeout_s": {"type": "integer", "minimum": 1, "maximum": 1800},
"agent": {"type": "string"},
},
},
"handler": tool_build,
},
}
def handle(message: dict[str, Any]) -> dict[str, Any] | None:
method = message.get("method")
msg_id = message.get("id")
if method == "initialize":
return {
"jsonrpc": "2.0",
"id": msg_id,
"result": {
"protocolVersion": "2024-11-05",
"capabilities": {"tools": {}},
"serverInfo": {"name": SERVER_NAME, "version": SERVER_VERSION},
},
}
if method == "tools/list":
return {
"jsonrpc": "2.0",
"id": msg_id,
"result": {
"tools": [
{
"name": name,
"description": data["description"],
"inputSchema": data["inputSchema"],
}
for name, data in TOOLS.items()
]
},
}
if method == "tools/call":
params = message.get("params") or {}
name = params.get("name")
args = params.get("arguments") or {}
if name not in TOOLS:
result = {"ok": False, "error": f"unknown tool: {name}"}
else:
result = TOOLS[name]["handler"](args)
return {"jsonrpc": "2.0", "id": msg_id, "result": {"content": json_text(result)}}
if method and method.startswith("notifications/"):
return None
return {
"jsonrpc": "2.0",
"id": msg_id,
"error": {"code": -32601, "message": f"method not found: {method}"},
}
def main() -> None:
for line in sys.stdin:
if not line.strip():
continue
try:
response = handle(json.loads(line))
except Exception as exc:
response = {
"jsonrpc": "2.0",
"id": None,
"error": {"code": -32000, "message": f"{type(exc).__name__}: {exc}"},
}
if response is not None:
print(json.dumps(response, separators=(",", ":")), flush=True)
if __name__ == "__main__":
main()