mirror of
https://github.com/allaunthefox/Research-Stack.git
synced 2026-07-31 03:05:21 +00:00
- Replace repo .mcp.json with minimal config (10 essential servers instead of 29). Full backup retained as .mcp.json.full. - Remove inline MCP section from ~/.config/opencode/config.json so OpenCode loads only repo .mcp.json (also removes hardcoded Linear token leak). - Add singleton guards to opencode_prover_mcp.py, token_saver_mcp.py, and remote_lean_proof_mcp.py to prevent duplicate instances when multiple IDE configs reference them. Build: N/A (Python shims)
317 lines
11 KiB
Python
317 lines
11 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
TokenSaver MCP — intercepts token-burning operations, routes to free local compute.
|
|
|
|
Free tier (runs on neon CPU or qfox-1 build server, 0 token cost):
|
|
- verify_build lake build verification
|
|
- classify_proof RRC alignment classification
|
|
- compile_check Lean snippet compilation
|
|
- linter_check Lean linter pass
|
|
- generate_proof DeepSeek-Prover on neon (CPU, free)
|
|
- format_code Lean code formatting (lake fmt)
|
|
- doc_lookup Mathlib doc search (local cache)
|
|
|
|
Paid fallback (only if free tier fails):
|
|
- generate_proof → OpenRouter DeepSeek V4 Flash
|
|
|
|
Usage: python3 token_saver_mcp.py
|
|
Register in opencode.json as a local MCP server.
|
|
Any MCP-compatible agent (Claude Code, OpenCode, Cursor) can call it.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import fcntl
|
|
import json
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
import urllib.error
|
|
import urllib.request
|
|
from pathlib import Path
|
|
|
|
from mcp.server.fastmcp import FastMCP
|
|
|
|
mcp = FastMCP("token-saver", log_level="WARNING")
|
|
|
|
|
|
def _ensure_singleton() -> None:
|
|
"""Prevent multiple instances of this MCP server from running simultaneously."""
|
|
lock_path = Path.home() / ".local" / "share" / "opencode" / "token_saver_mcp.lock"
|
|
lock_path.parent.mkdir(parents=True, exist_ok=True)
|
|
try:
|
|
fd = os.open(str(lock_path), os.O_CREAT | os.O_RDWR)
|
|
fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
|
except (OSError, BlockingIOError):
|
|
print("token_saver_mcp is already running; exiting singleton instance.", file=sys.stderr)
|
|
sys.exit(0)
|
|
|
|
|
|
# ── Resource URLs (configurable via env) ────────────────────────────────
|
|
|
|
BUILD_SERVER = os.environ.get("BUILD_SERVER_URL", "http://100.88.57.96:8765")
|
|
NEON_OLLAMA = os.environ.get("NEON_OLLAMA_URL", "http://100.92.88.64:11434")
|
|
NEON_MODEL = os.environ.get("NEON_MODEL",
|
|
"hf.co/irmma/DeepSeek-Prover-V2-7B-Q4_K_M-GGUF")
|
|
LAKE_WORKDIR = os.environ.get("LAKE_WORKDIR",
|
|
"/home/allaun/Research Stack/0-Core-Formalism/lean/Semantics")
|
|
|
|
# ── Registered tools ────────────────────────────────────────────────────
|
|
|
|
TOOL_MANIFEST = """
|
|
All tools in this server run on free local compute (neon CPU / qfox-1 build server).
|
|
Zero API tokens consumed unless explicitly marked [PAID].
|
|
|
|
verify_build(module) → build server (free)
|
|
classify_proof(label,shape) → rrc-watchdog (free)
|
|
compile_check(code) → lake env lean (free)
|
|
linter_check(code) → lake build --warnings (free)
|
|
generate_proof(context) → neon Ollama → OpenRouter fallback [free → PAID]
|
|
format_code(code) → lake fmt (free)
|
|
doc_lookup(query) → local mathlib doc cache (free)
|
|
"""
|
|
|
|
|
|
def _call_build_server(module: str, timeout: int = 600) -> str:
|
|
"""Call the local build server. 0 tokens."""
|
|
try:
|
|
resp = urllib.request.urlopen(f"{BUILD_SERVER}/{module}", timeout=timeout)
|
|
return resp.read().decode()
|
|
except Exception as e:
|
|
return json.dumps({"passed": False, "error": str(e)[:200]})
|
|
|
|
|
|
def _call_neon(prompt: str, raw: bool = True) -> str:
|
|
"""Call neon's local Ollama model. 0 tokens."""
|
|
body = json.dumps({
|
|
"model": NEON_MODEL,
|
|
"prompt": prompt,
|
|
"raw": raw,
|
|
"stream": False,
|
|
"options": {"temperature": 0, "num_predict": 512, "num_thread": 18},
|
|
}).encode()
|
|
try:
|
|
req = urllib.request.Request(
|
|
f"{NEON_OLLAMA}/api/generate", data=body,
|
|
headers={"Content-Type": "application/json"}, method="POST",
|
|
)
|
|
with urllib.request.urlopen(req, timeout=300) as resp:
|
|
return json.loads(resp.read()).get("response", "")
|
|
except Exception as e:
|
|
return f"# neon error: {e}"
|
|
|
|
|
|
def _call_openrouter(prompt: str) -> str:
|
|
"""Fallback: paid OpenRouter API."""
|
|
auth_path = Path.home() / ".local" / "share" / "opencode" / "auth.json"
|
|
key = ""
|
|
try:
|
|
auth = json.loads(auth_path.read_text())
|
|
key = auth.get("openrouter", {}).get("key", "")
|
|
except Exception:
|
|
key = os.environ.get("OPENROUTER_API_KEY", "")
|
|
if not key:
|
|
return "# No API key configured"
|
|
|
|
body = json.dumps({
|
|
"model": "deepseek/deepseek-v4-flash",
|
|
"messages": [{"role": "user", "content": prompt}],
|
|
"temperature": 0.3, "max_tokens": 4096,
|
|
}).encode()
|
|
headers = {"Content-Type": "application/json", "Authorization": f"Bearer {key}"}
|
|
|
|
try:
|
|
req = urllib.request.Request(
|
|
"https://openrouter.ai/api/v1/chat/completions", data=body,
|
|
headers=headers, method="POST",
|
|
)
|
|
with urllib.request.urlopen(req, timeout=180) as resp:
|
|
return json.loads(resp.read())["choices"][0]["message"]["content"]
|
|
except Exception as e:
|
|
return f"# OpenRouter error: {e}"
|
|
|
|
|
|
def _extract_proof(text: str) -> str:
|
|
lines = text.split("\n")
|
|
in_proof = False
|
|
proof_lines: list[str] = []
|
|
for line in lines:
|
|
s = line.strip()
|
|
if s.startswith(":= by"):
|
|
in_proof = True
|
|
proof_lines = [s]
|
|
continue
|
|
if in_proof and not s.startswith("```"):
|
|
proof_lines.append(s)
|
|
return "\n".join(proof_lines) if proof_lines else ""
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
# Tools
|
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
@mcp.tool()
|
|
def verify_build(module: str) -> str:
|
|
"""[FREE] Verify a Lean module compiles via local build server."""
|
|
return _call_build_server(module)
|
|
|
|
|
|
@mcp.tool()
|
|
def classify_proof(pist_label: str = "", rrc_shape: str = "logogramProjection") -> str:
|
|
"""[FREE] Classify a proof through RRC alignment gate (local rrc-watchdog)."""
|
|
watchdog = os.path.join(LAKE_WORKDIR, ".lake/build/bin/rrc-watchdog")
|
|
if not os.path.exists(watchdog):
|
|
return json.dumps({"error": "rrc-watchdog not built", "score": 0})
|
|
try:
|
|
result = subprocess.run(
|
|
[watchdog, "--pist-label", pist_label or "none",
|
|
"--exact-label", pist_label or "none", "--rrc-shape", rrc_shape],
|
|
capture_output=True, text=True, timeout=30,
|
|
)
|
|
return result.stdout or json.dumps({"error": "empty", "score": 0})
|
|
except Exception as e:
|
|
return json.dumps({"error": str(e)[:200], "score": 0})
|
|
|
|
|
|
@mcp.tool()
|
|
def compile_check(code: str) -> str:
|
|
"""[FREE] Compile-check a Lean snippet locally. 0 tokens."""
|
|
import tempfile, shutil
|
|
tmp_dir = Path(tempfile.mkdtemp())
|
|
try:
|
|
tmp_file = tmp_dir / "check.lean"
|
|
tmp_file.write_text(code)
|
|
result = subprocess.run(
|
|
["lake", "env", "lean", str(tmp_file)],
|
|
capture_output=True, text=True, timeout=120, cwd=LAKE_WORKDIR,
|
|
)
|
|
ok = result.returncode == 0
|
|
errors = "\n".join(
|
|
l for l in (result.stdout + result.stderr).split("\n")
|
|
if "error:" in l or "warning:" in l
|
|
)[:2000]
|
|
return json.dumps({"passed": ok, "errors": errors})
|
|
finally:
|
|
shutil.rmtree(tmp_dir, ignore_errors=True)
|
|
|
|
|
|
@mcp.tool()
|
|
def linter_check(code: str) -> str:
|
|
"""[FREE] Run Lean linter on a snippet. 0 tokens."""
|
|
import tempfile, shutil
|
|
tmp_dir = Path(tempfile.mkdtemp())
|
|
try:
|
|
tmp_file = tmp_dir / "check.lean"
|
|
tmp_file.write_text(code)
|
|
result = subprocess.run(
|
|
["lake", "env", "lean", f"--lint={str(tmp_file)}"],
|
|
capture_output=True, text=True, timeout=120, cwd=LAKE_WORKDIR,
|
|
)
|
|
return json.dumps({
|
|
"passed": result.returncode == 0,
|
|
"warnings": "\n".join(
|
|
l for l in (result.stdout + result.stderr).split("\n")
|
|
if "warning:" in l
|
|
)[:2000],
|
|
})
|
|
finally:
|
|
shutil.rmtree(tmp_dir, ignore_errors=True)
|
|
|
|
|
|
@mcp.tool()
|
|
def generate_proof(theorem_context: str, line_no: int = 0) -> str:
|
|
"""[FREE → PAID] Generate a Lean proof. Tries neon's CPU model first (0 tokens),
|
|
falls back to OpenRouter DeepSeek V4 Flash (token cost)."""
|
|
# Free tier: neon's DeepSeek-Prover with raw completion
|
|
prompt = f"theorem {theorem_context} := by"
|
|
result = _call_neon(prompt)
|
|
proof = _extract_proof(result)
|
|
|
|
if proof:
|
|
return proof
|
|
|
|
# Paid fallback: OpenRouter
|
|
paid_prompt = (
|
|
"You are a Lean 4 theorem prover. "
|
|
"Output ONLY the proof block starting with `:= by`. "
|
|
"No markdown fences.\n\n"
|
|
f"Complete:\n\n{theorem_context}"
|
|
)
|
|
result = _call_openrouter(paid_prompt)
|
|
proof = _extract_proof(result)
|
|
return proof or result[:500]
|
|
|
|
|
|
@mcp.tool()
|
|
def format_code(code: str) -> str:
|
|
"""[FREE] Format Lean code using lake fmt. 0 tokens."""
|
|
import tempfile, shutil
|
|
tmp_dir = Path(tempfile.mkdtemp())
|
|
try:
|
|
tmp_file = tmp_dir / "check.lean"
|
|
tmp_file.write_text(code)
|
|
result = subprocess.run(
|
|
["lake", "fmt", str(tmp_file)],
|
|
capture_output=True, text=True, timeout=30, cwd=LAKE_WORKDIR,
|
|
)
|
|
if result.returncode == 0:
|
|
return tmp_file.read_text()
|
|
return code # Return original if fmt fails
|
|
finally:
|
|
shutil.rmtree(tmp_dir, ignore_errors=True)
|
|
|
|
|
|
@mcp.tool()
|
|
def doc_lookup(query: str) -> str:
|
|
"""[FREE] Look up a Lean/mathlib symbol in local docs. 0 tokens.
|
|
Uses a temp file because `lean` doesn't support inline -e evaluation."""
|
|
import tempfile, shutil
|
|
tmp_dir = Path(tempfile.mkdtemp())
|
|
try:
|
|
tmp_file = tmp_dir / "check.lean"
|
|
tmp_file.write_text(f"#check {query}\n")
|
|
result = subprocess.run(
|
|
["lake", "env", "lean", str(tmp_file)],
|
|
capture_output=True, text=True, timeout=30, cwd=LAKE_WORKDIR,
|
|
)
|
|
output = (result.stdout + result.stderr)[:2000]
|
|
if not output.strip():
|
|
return f"Symbol '{query}' not found"
|
|
# Strip absolute tmp path noise from error messages
|
|
lines = []
|
|
for line in output.split("\n"):
|
|
cleaned = line.replace(str(tmp_dir) + "/", "")
|
|
if cleaned.strip():
|
|
lines.append(cleaned)
|
|
return "\n".join(lines[-10:]) # last 10 lines = signal
|
|
except Exception as e:
|
|
return f"Error: {e}"
|
|
finally:
|
|
shutil.rmtree(tmp_dir, ignore_errors=True)
|
|
|
|
|
|
@mcp.tool()
|
|
def health() -> str:
|
|
"""Check which free resources are reachable."""
|
|
results = {}
|
|
# Build server
|
|
try:
|
|
urllib.request.urlopen(f"{BUILD_SERVER}/Semantics", timeout=5)
|
|
results["build_server"] = "up"
|
|
except Exception:
|
|
results["build_server"] = "down"
|
|
# Neon Ollama
|
|
try:
|
|
urllib.request.urlopen(f"{NEON_OLLAMA}/api/tags", timeout=5)
|
|
results["neon_ollama"] = "up"
|
|
except Exception:
|
|
results["neon_ollama"] = "down"
|
|
# rrc-watchdog
|
|
watchdog = os.path.join(LAKE_WORKDIR, ".lake/build/bin/rrc-watchdog")
|
|
results["rrc_watchdog"] = "present" if os.path.exists(watchdog) else "missing"
|
|
return json.dumps(results)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
_ensure_singleton()
|
|
mcp.run(transport="stdio")
|