Build a Lean 4 MCP Server for Formal-Verification Agents
Anthropic's unreleased frontier model tested 650 ideas across 60 subagents (31M tokens) on the Riemann Hypothesis — and the confirmed findings were formalized in Lean, making Lean the emerging proof-of-correctness gate for both math and AI code. This dispatch builds lean-mcp, a FastMCP Python server exposing five tools — check_lemma, compile_project, list_theorems, propose_tactic, check_import — with inputSchema, mcpServers config, and a localhost transport secured by OAuth 2.0-scoped registry tokens.
Deepak Bagada
CEO, SaaSNext
- lean-mcp exposes five governed tools — check_lemma, compile_project, list_theorems, propose_tactic, check_import — so agents can propose proofs, run the Lean compiler, search mathlib, and resolve imports.
- Lean verification is the emerging proof-of-correctness gate: after Anthropic's 60-subagent Riemann Hypothesis run, the confirmed findings were formalized in Lean, not just published.
- propose_tactic is strictly human-in-the-loop: an LLM can suggest the next tactic, but nothing auto-applies and every theorem requires a reviewed proof:write before landing in the registry.
- Deterministic tools (check_lemma, compile_project) are the trust anchor — no guessing, no side effects — while the only creative step is quarantined behind a human gate.
- Security is a localhost/stdio transport with OAuth 2.0 tokens scoped to a local proof registry (proof:read, proof:write, tactic:propose) and immutable, hashed proof entries for audit.
By Deepak Bagada, CEO at SaaSNext & Principal AI Architect.
Build a Lean 4 MCP Server for Formal-Verification Agents
In August 2026, an unreleased Anthropic frontier model did something research teams rarely manage in a quarter, let alone an afternoon: it worked through 650 hypotheses across 60 parallel subagents, spent 31 million tokens, and produced findings on the Riemann Hypothesis that practicing mathematicians confirmed — and that the team then formalized in Lean, the interactive theorem prover. The mathematical result is the headline; the formalization is the deeper signal. Lean is quietly becoming the proof-of-correctness gate not just for number theory but for the code AI writes. When a claim is "verified in Lean," that phrase carries the strongest guarantee software can offer: a machine-checked proof that the statement is true, rather than a test suite suggesting it probably is.
For engineering leaders — especially at Indian SaaS and deep-tech firms whose compliance culture already distrusts "trust me" — the pattern is clear: formal verification is becoming a production artifact, and agents need a governed way to drive a prover. This dispatch builds lean-mcp: a Model Context Protocol server (FastMCP, Python) that turns Lean 4 into five agent-facing tools — check_lemma, compile_project, list_theorems, propose_tactic, and check_import — each with typed JSON inputSchema, an mcpServers config for Claude Desktop and Cursor, a localhost-transport security section with OAuth 2.0-scoped tokens pointed at a local proof registry, and a human-in-the-loop tactic advisor for the moments a proof stalls. The MCP Directory maps the landscape; this is the build.
Why Lean became the verification gate
Lean is an interactive theorem prover: a compiler plus a proof language in which every claim is checked mechanically, down to the last inference rule. Unlike unit tests that cover sampled behaviour, a Lean proof covers all behaviour by construction. That property is exactly what two 2026 workloads need:
- Mathematics research. When an AI model produces a novel result — the way the Anthropic run produced candidates for the Riemann Hypothesis — the community's default question is no longer "does it look right?" but "is it formalized?" The 31M-token, 60-subagent experiment ended not with a blog post but with Lean files, because a formalized theorem is portable, checkable, and immune to the interpretive drift that plagues prose proofs.
- AI-generated code. As agents write more production code, verification is moving up the stack. Teams verifying protocol implementations, payment logic, smart contracts, and security-critical parsers use Lean 4 toolchains to prove properties — termination, no panics, correct state transitions — that test coverage can only approximate.
In both cases the bottleneck is the same: an agent can generate a proof attempt, but only a prover can accept it. lean-mcp removes that bottleneck by making the prover itself addressable.
Five tools, one prover
| Tool | Description | Input params | Return type |
|---|---|---|---|
check_lemma |
Write a lemma + proof to a temp file and typecheck it with the Lean compiler | lemma_code (str, required), name |
dict — passes, output, errors[] |
compile_project |
Typecheck the whole Lean project with lake build |
target (optional module target) |
dict — passes, output, target |
list_theorems |
Grep the local mathlib checkout for existing theorems matching a pattern | pattern (str) |
dict — pattern, matches[], count, library |
propose_tactic |
Human-in-the-loop: ask an LLM for the next tactic when a proof fails | lemma_code (required), lean_output, model |
dict — proposal, needs_human, reviewed_by |
check_import |
Resolve and validate a Lean import against project and mathlib search paths | import_path (str, required) |
dict — import, resolved[], ok, hint |
Reads (list_theorems, check_import) are cheap and safe; writes (check_lemma, compile_project) are deterministic and side-effect-free; the only genuinely creative step (propose_tactic) is deliberately gated behind a human. That asymmetry is the whole security model.
Building the server (FastMCP, Python)
[project]
name = "lean-mcp"
version = "0.1.0"
description = "Drive Lean 4 as governed MCP tools for formal-verification agents"
requires-python = ">=3.11"
dependencies = [
"mcp[cli]>=1.9.0",
"httpx>=0.27.0",
]
[project.scripts]
lean-mcp = "lean_mcp.server:main"
And the server itself:
# server.py - lean-mcp: drive Lean 4 as governed MCP tools
import asyncio
import os
import subprocess
from pathlib import Path
import httpx
from mcp.server.fastmcp import FastMCP
LEAN_BIN = os.environ.get("LEAN_BIN", "lean")
PROJECT_DIR = Path(os.environ.get("LEAN_PROJECT_DIR", ".")).resolve()
MATHLIB_DIR = Path(
os.environ.get("MATHLIB_DIR", str(PROJECT_DIR / ".lake" / "packages" / "mathlib"))
)
REGISTRY_URL = os.environ.get("PROOF_REGISTRY_URL", "http://127.0.0.1:8017")
REGISTRY_TOKEN = os.environ.get("PROOF_REGISTRY_TOKEN")
LLM_API_KEY = os.environ.get("LLM_API_KEY")
LLM_URL = os.environ.get("LLM_URL", "https://api.anthropic.com/v1/messages")
LEAN_TIMEOUT_S = int(os.environ.get("LEAN_TIMEOUT_S", "120"))
mcp = FastMCP("lean-formal-verification")
http = httpx.AsyncClient(timeout=60.0)
def _typecheck(lean_file: Path) -> tuple[bool, str]:
try:
proc = subprocess.run(
[LEAN_BIN, str(lean_file)], capture_output=True, text=True, timeout=LEAN_TIMEOUT_S
)
except subprocess.TimeoutExpired:
return False, "TIMEOUT: lean did not terminate within the configured window."
return proc.returncode == 0, (proc.stdout or "") + (proc.stderr or "")
def _extract_errors(output: str) -> list[str]:
return [ln for ln in output.splitlines() if "error" in ln.lower()][:20]
@mcp.tool()
async def check_lemma(lemma_code: str, name: str = "untitled_lemma") -> dict:
"""Write a lemma + proof to a temp file and typecheck it with the Lean compiler."""
tmp_dir = PROJECT_DIR / ".lean-mcp-tmp"
tmp_dir.mkdir(exist_ok=True)
tmp = tmp_dir / f"{name}.lean"
tmp.write_text(lemma_code)
ok, output = await asyncio.to_thread(_typecheck, tmp)
return {"name": name, "passes": ok, "output": output[-4000:], "errors": _extract_errors(output)}
@mcp.tool()
async def compile_project(target: str = "") -> dict:
"""Typecheck the whole Lean project with `lake build`."""
cmd = ["lake", "build"] + ([target] if target else [])
proc = await asyncio.to_thread(
lambda: subprocess.run(cmd, cwd=PROJECT_DIR, capture_output=True, text=True, timeout=600)
)
return {"passes": proc.returncode == 0, "output": (proc.stdout + proc.stderr)[-6000:], "target": target or "all"}
@mcp.tool()
async def list_theorems(pattern: str = "") -> dict:
"""Grep the local mathlib checkout for existing theorems matching a pattern."""
matches: list[str] = []
if pattern:
try:
out = subprocess.run(
["rg", "-n", pattern, str(MATHLIB_DIR)], capture_output=True, text=True, timeout=90
)
except FileNotFoundError:
out = subprocess.run(
["grep", "-rn", pattern, str(MATHLIB_DIR)], capture_output=True, text=True, timeout=90
)
matches = out.stdout.splitlines()[:20]
return {"pattern": pattern, "matches": matches, "count": len(matches), "library": str(MATHLIB_DIR)}
@mcp.tool()
async def propose_tactic(
lemma_code: str,
lean_output: str,
model: str = "claude-sonnet-4-5",
) -> dict:
"""Human-in-the-loop: ask an LLM for the next tactic when a proof fails."""
if not LLM_API_KEY:
return {
"proposal": None,
"needs_human": True,
"reason": "no LLM_API_KEY configured; paste lean_output into a human review",
}
prompt = (
"You are helping with a Lean 4 proof. The lemma below fails to typecheck.
"
f"LEMMA:
{lemma_code}
COMPILER OUTPUT:
{lean_output}
"
"Reply with exactly one block of Lean 4 code: the single most likely next tactic "
"or a corrected statement. No prose outside the block."
)
try:
resp = await http.post(
LLM_URL,
headers={"x-api-key": LLM_API_KEY, "anthropic-version": "2023-06-01"},
json={"model": model, "max_tokens": 500, "messages": [{"role": "user", "content": prompt}]},
)
resp.raise_for_status()
proposal = resp.json()["content"][0]["text"]
except (httpx.HTTPError, KeyError, IndexError):
proposal = None
return {"proposal": proposal, "needs_human": True, "reviewed_by": "human-required-before-merge"}
@mcp.tool()
async def check_import(import_path: str) -> dict:
"""Resolve a Lean import statement against project and mathlib search paths."""
file_hint = import_path.replace(".", "/") + ".lean"
resolved: list[str] = []
for p in (PROJECT_DIR, MATHLIB_DIR):
cand = p / file_hint
if cand.exists():
resolved.append(str(cand))
hint = (
"Import not found. Run `lake exe cache get` to fetch mathlib, then `lake build` once."
if not resolved
else None
)
return {"import": import_path, "resolved": resolved, "ok": bool(resolved), "hint": hint}
if __name__ == "__main__":
mcp.run(transport="stdio")
The design keeps the prover deterministic and the creativity quarantined: check_lemma and compile_project never guess, propose_tactic never writes, and a human always reviews before anything lands in the registry.
The inputSchema the model actually sees
{
"check_lemma": {
"type": "object",
"properties": {
"lemma_code": {"type": "string", "description": "Lean 4 lemma statement + proof attempt"},
"name": {"type": "string", "default": "untitled_lemma"}
},
"required": ["lemma_code"]
},
"compile_project": {
"type": "object",
"properties": {
"target": {"type": "string", "description": "Optional module target; empty builds all"}
},
"required": []
},
"list_theorems": {
"type": "object",
"properties": {
"pattern": {"type": "string", "description": "Regex to match against the local mathlib checkout"}
},
"required": []
},
"propose_tactic": {
"type": "object",
"properties": {
"lemma_code": {"type": "string"},
"lean_output": {"type": "string", "description": "Compiler output from a failed check_lemma"},
"model": {"type": "string", "default": "claude-sonnet-4-5"}
},
"required": ["lemma_code", "lean_output"]
},
"check_import": {
"type": "object",
"properties": {
"import_path": {"type": "string", "description": "e.g. Mathlib.Data.Nat.Prime"}
},
"required": ["import_path"]
}
}
Registering with Claude Desktop and Cursor
{
"mcpServers": {
"lean-verifier": {
"command": "uvx",
"args": ["lean-mcp"],
"env": {
"LEAN_BIN": "/usr/local/bin/lean",
"LEAN_PROJECT_DIR": "/opt/proofs/riemann-notes",
"MATHLIB_DIR": "/opt/proofs/riemann-notes/.lake/packages/mathlib",
"PROOF_REGISTRY_URL": "http://127.0.0.1:8017",
"PROOF_REGISTRY_TOKEN": "${PROOF_REGISTRY_TOKEN}",
"LLM_API_KEY": "${LLM_API_KEY}"
}
}
}
}
The same block drops into Cursor's MCP settings and VS Code's MCP extension. Note the registry is explicitly on 127.0.0.1 — nothing here listens on a routable interface.
Using it (quickstart)
pip install -e .
export PROOF_REGISTRY_TOKEN="reg-token-..."
export LLM_API_KEY="sk-ant-..."
# Get mathlib once, then go
lake exe cache get
lean-mcp
# Agent session flow
# 1. list_theorems(pattern="add_comm")
# 2. check_lemma(lemma_code="theorem my_thm : 2 + 2 = 4 := by norm_num", name="my_thm")
# 3. check_import(import_path="Mathlib.Data.Nat.Prime")
# 4. propose_tactic(lemma_code=..., lean_output=...) # human reviews the proposal
Security: localhost transport, OAuth 2.0, scoped registry tokens
A proof server is a trust anchor — a malicious proof is a false guarantee — so the security model treats every boundary as sensitive:
- Localhost / stdio transport. The server ships on stdio; the client spawns the process and no socket is created. The only always-on endpoint is the local proof registry on
127.0.0.1. Never bind it to0.0.0.0; if a shared CI runner needs it, use a Unix socket or SSH tunnel rather than a TCP port open to the network. - OAuth 2.0 tokens scoped to the proof registry. Registry tokens carry narrow scopes —
proof:readforlist_theorems,proof:writefor acceptedcheck_lemmaresults,tactic:proposeforpropose_tactic— issued per developer or per CI job, expiring in minutes for automation and rotated on a fixed schedule. The agent holds the token; the MCP server validates the scope at call time. For teams running this on corporate infrastructure, the registry can sit behind an OAuth 2.0 authorization server (Keycloak, Okta) with the MCP server acting as a confidential client. - Human-in-the-loop writes.
propose_tacticalways returnsneeds_human: true. No tactic auto-applies, no lemma auto-commits to the registry. The only way a theorem lands in the proof registry is through a reviewed, signedproof:write. - Proof immutability. A formalized theorem is a write-once artifact. Store hashes with each registry entry so a "verified in Lean" badge cannot be silently edited later — auditability is the entire point.
- No secrets to the model. Compiler output can be long and noisy; strip any file paths or environment variables before they reach
propose_tactic, and never pass tokens into the prompt.
Retry Rules & Error Handling
| Failure mode | Backoff | Fallback | Escalation |
|---|---|---|---|
| Lemma fails to typecheck | None — deterministic result | propose_tactic LLM suggestion |
Human review; commit only when passes: true |
lake build exceeds 600s |
None | Build incrementally by target module |
Split into CI jobs on parallel workers |
| LLM tactic API 429 / 5xx | Exponential: 1s → 2s → 4s | Serve the last cached proposal | Queue the proposal and notify the owner |
| Registry (localhost) unreachable | 3 retries at 100ms intervals | Serve reads from the local cache | Page the operator; pending writes are queued, not lost |
| Import unresolved | None | check_import hint + lake exe cache get |
Add the dependency to lakefile.lean and rebuild |
The production checklist
To make formal verification a working part of your pipeline rather than a demo: (1) treat lake build as your CI gate and keep it incremental — full mathlib builds are a rite of passage you only do once; (2) scope registry tokens per developer and per CI job, and expire them aggressively; (3) keep propose_tactic as an assistant, never an autopilot, because a wrong tactic suggestion silently consumed is worse than a loud failure; (4) hash registry entries so "verified in Lean" is tamper-evident for auditors; (5) start with a small, boring project — a parser or a payment amount calculation — before aiming at theorem statements.
The Riemann Hypothesis campaign proved the ceiling: an agent swarm that proposes, and a prover that decides. lean-mcp turns that division of labour into something any team can run locally, with human sign-off baked into the loop. For the wider server landscape, check the MCP Directory, and follow the Latest AI News for the next formalized frontier result.
Enjoyed this breakdown? Get our morning dispatch in your inbox.
Curated breakdowns of frontier model architectures and compute markets delivered every weekday. Zero fluff.
Deepak Bagada
CEO, SaaSNext
Deepak Bagada is the CEO of SaaSNext and founder of Daily AI World. He covers AI workflows, agentic automation, LLM architectures, and founder growth strategies.
SpaceX Closes $60B Cursor Deal: Coding-Agent Wars Consolidate
Next Story →Build a Cross-Device Agentic-Commerce Workflow with LangGraph
Related Intelligence Analysis
Vercel AI SDK Tool Calling React: 5 Steps (2026)
Vercel AI SDK tool calling React integration is a programming pattern that executes server-side functions based on large language model decisions and streams the results to a React frontend. By combining streamText with...
Fact-Density vs. Word Count: The New SEO for 2026
Fact Density is the ratio of verifiable, unique information to the total word count of a piece of content. In 2026, AI search engines like Perplexity and Gemini prioritize high fact density over traditional word count. A...
NVIDIA Audex vs Qwen3.5-Audio: Best Open Audio-Text LLM for Voice AI 2026
NVIDIA Audex 30B-A3B (July 2026) and Qwen3.5-35B-A3B are the two leading open audio-text LLMs. Audex uniquely handles both audio understanding and generation in a single model while preserving text intelligence. Qwen3.5-...