Skip to main content
Workflows Library MCP Directory Realtime AI News Sponsor Tier Subscribe
Front Page / AI Tools / Deep Dive

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

Deepak Bagada

CEO, SaaSNext

Aug 19, 2026 Published
|
Aug 19, 2026 Updated
|
9 Minutes Reading Time
Core Takeaways for Founders & Builders
  • 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 to 0.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:read for list_theorems, proof:write for accepted check_lemma results, tactic:propose for propose_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_tactic always returns needs_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, signed proof: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.

Executive Briefing

Enjoyed this breakdown? Get our morning dispatch in your inbox.

Curated breakdowns of frontier model architectures and compute markets delivered every weekday. Zero fluff.

Frequently Asked Questions
No for check_lemma, compile_project, and check_import — those just run the compiler. Writing the lemma statements themselves needs Lean familiarity, but propose_tactic lowers the barrier by suggesting the next tactic while a human remains in the loop.
Yes. Lean 4 has toolchains for verified software — protocol implementations, payment logic, smart contracts, security-critical parsers — where termination and state-transition properties are proved. The same MCP server drives them; only the project and mathlib paths change.
An unreleased Anthropic frontier model tested 650 ideas across 60 subagents, spent 31M tokens, and produced findings on the Riemann Hypothesis that mathematicians confirmed and the team formalized in Lean — a demonstration that formalization, not prose, is now the default bar for AI-produced math.
Yes, once you keep lake build incremental and cache mathlib with lake exe cache get. Full mathlib builds are slow, but a project builds in seconds to a few minutes, which is why compile_project is designed as a CI gate.
Because a proof is a guarantee. A wrong tactic suggestion silently accepted becomes a false confidence — worse than a loud failure. The human-in-the-loop gate means every tactic lands as a proposal, is reviewed, and only then reaches the proof registry.
Deepak Bagada
Author Profile

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.

Related Intelligence Analysis

Briefing AI Tools

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...

Deepak Bagada Deepak Bagada
12m read
Breaking AI Tools

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...

Deepak Bagada Deepak Bagada
4m read
Audio Briefing
Accessibility Preferences
High Contrast Mode
Accessible Reading Font

Keyboard Shortcuts

Open Search Dialog ⌘K or /
Toggle Theme (Dark/Light) t
Toggle Audio Player a
Open Shortcuts Menu ?
Close Active Dialog Esc