Skip to main content
Glama
README.md
# Lean MCP

An MCP server that compiles Lean 4 code with the local Lean toolchain and
returns the compiler's feedback in a structured response. It exposes a single
tool, `lean_check`. Mathlib and the basic Lean library are available to the
compiled snippets.

## What it is

`lean-mcp` is a small, self-contained Model Context Protocol (MCP) server for
proof work. A client (for example OpenCode) sends Lean source code to the
`lean_check` tool; the server writes the snippet into a Lake project, elaborates
it with the real Lean compiler, and returns a structured verdict. Success is
never taken from the model: a declaration counts as proved only if the Lean
elaborator accepts it. The server speaks MCP over stdio (`src/index.ts`).

## Features

- **One tool with a fixed contract.** `lean_check` accepts a single `code`
  string and returns exactly `status`, `summary` and `diagnostics`. The output
  shape is declared with Zod so `structuredContent` stays spec-compliant
  (`src/index.ts`).
- **Real compilation.** Snippets are elaborated by the local Lean toolchain
  through `lake env lean --json`; the verdict is the compiler's, not the
  model's (`src/lean.ts`).
- **Parsed, structured diagnostics.** Lean's JSON messages are parsed and
  rendered as `line:col: severity: text` strings (`src/lean.ts`).
- **Errors, warnings and information all reach the model.** A rejected proof
  returns its errors; a successful file keeps its warnings and `information`
  output, so the answers to `#check` / `#print` / `#eval` are not dropped
  (`src/lean.ts`).
- **`sorry` is flagged.** A file that compiles with `sorry` returns
  `status: "success"`, but the `summary` states that the proof is unfinished, so
  a hollow proof is not mistaken for a real one (`src/lean.ts`).
- **Actionable failure hints.** On failure the `summary` appends a one-line hint
  keyed to the failure class (`unsolved goals`, `unknown identifier`, syntax
  errors, and others) (`src/lean.ts`).
- **Serialized execution.** Concurrent tool calls run through a queue and each
  gets its own scratch file, so results cannot cross (`src/lean.ts`).
- **Timeouts terminate the whole process tree.** A stuck tactic is stopped with
  SIGTERM, then SIGKILL, and the server never blocks (`src/lean.ts`,
  `src/config.ts`).
- **Clean shutdown, no orphans.** SIGTERM, SIGINT, SIGHUP or a closed stdin
  aborts in-flight compilations by process group, so no `lake` or `lean`
  subprocess is left running (`src/index.ts`, `src/lean.ts`).
- **Structured failures when misconfigured.** A missing or read-only project
  directory returns a structured tool error instead of a raw OS error
  (`src/lean.ts`), and startup environment problems are reported on stderr
  (`src/index.ts`).

## Tech stack

| Component | Version | Source |
|---|---|---|
| Node.js | >= 20 | `package.json` (`engines`) |
| TypeScript | ^5.7 | `package.json` (`devDependencies`) |
| `@modelcontextprotocol/sdk` | ^1.12.0 | `package.json` (`dependencies`) |
| `zod` | ^3.23.8 | `package.json` (`dependencies`) |
| Lean 4 | `v4.34.1` (via elan) | `lean-project/lean-toolchain` |
| Mathlib | `v4.34.1` | `lean-project/lakefile.lean` |
| Lake | shipped with the Lean toolchain | `lean-project/lakefile.lean` |

## Architecture

```
MCP client (e.g. OpenCode)
        |  stdio: tools/call lean_check { code }
        v
src/index.ts   McpServer "lean-mcp", registerTool "lean_check"
        |
        v
src/lean.ts    LeanChecker: serial queue, timeout, process-group kill
        |  writes  <LEAN_MCP_PROJECT_DIR>/.lean-mcp-tmp/Check_<uuid>.lean
        v
lake env lean --json <file>        cwd = LEAN_MCP_PROJECT_DIR
        |  newline-delimited JSON messages on stdout
        v
parseJsonMessages -> classify -> { status, summary, diagnostics }
```

The entry point is `dist/index.js` (`package.json`, `bin` and `start`). The
scratch file is written inside the Lake project so the snippet picks up Mathlib
through `LEAN_PATH` (`src/lean.ts`).

## Getting started

### Prerequisites

- Node.js >= 20 (`package.json`).
- Lean 4 `v4.34.1`, installed with elan (`lean-project/lean-toolchain`).

### Install and build

```bash
npm install
npm run build
```

### Lean toolchain and Mathlib

```bash
# Lean 4 toolchain (matches lean-project/lean-toolchain)
elan toolchain install leanprover/lean4:v4.34.1

# Fetch Mathlib and its prebuilt .olean files
cd lean-project
lake exe cache get
lake build
```

Once `lake build` has run, `lean_check` can compile code against Mathlib.
Compilation time depends on the Mathlib cache being present
(`src/lean.ts`).

### Connect it to OpenCode

Add the server under `mcp.servers` in `~/.config/opencode/opencode.json`:

```json
{
  "mcp": {
    "servers": {
      "lean": {
        "type": "local",
        "command": ["node", "/path/to/lean-mcp/dist/index.js"],
        "environment": {
          "LEAN_MCP_PROJECT_DIR": "/path/to/lean-mcp/lean-project"
        }
      }
    }
  }
}
```

`command` is the executable followed by its arguments; naming the built entry
point `dist/index.js` is what starts the server. The server speaks MCP over
stdio (`src/index.ts`). Theorem search is deliberately not reimplemented here:
that is a separate server (`lean-explore`, a hosted MCP), and `lean_check` works
without it.

## Usage

### `lean_check`

**Input**

```json
{ "code": "theorem add_two : 1 + 1 = 2 := by rfl" }
```

> Note: `example` is a reserved keyword in Lean, so `theorem example : ...` is
> invalid. Use a named theorem such as `theorem add_two : ...`, or
> `example : ...` for an anonymous one.

**Successful response**

```json
{
  "status": "success",
  "summary": "Lean kodu başarıyla derlendi.",
  "diagnostics": []
}
```

(`summary` reads: "Lean compiled the code successfully.")

**Failed response**

```json
{
  "status": "error",
  "summary": "Lean kodu derlenemedi. İpucu: bilinmeyen bir isim kullanıldı; doğru lemma/sabit adını `#check` veya `lean-explore` ile doğrulayın.",
  "diagnostics": ["3:11: error: Unknown constant `Nat.succcccc`"]
}
```

(`summary` reads: "Lean could not compile the code. Hint: an unknown name was
used; verify the correct lemma/constant name with `#check` or `lean-explore`.")

The `summary` and hint strings above are the exact text `src/lean.ts` produces
in the current build; they are quoted verbatim. The compiler messages in
`diagnostics` come from Lean itself. `status` is only ever `success` or `error`.
Success is decided by whether the code actually compiles; the model's own
assessment is not used (`src/lean.ts`).

**`isError` semantics.** The tool is marked `isError: true` only when the *tool*
fails: the Lean toolchain is missing or the compilation times out. The compiler
rejecting the code is the normal, expected outcome and is returned as
`isError: false` with `status: "error"`. The model then treats each failed
attempt as a compiler verdict to iterate on, not as a tool failure
(`src/index.ts`).

**Information and warning messages.** Messages other than `severity: "error"`
reach the model in `diagnostics`. This covers `#check` / `#print` / `#eval`
output (for example `#check Nat.add_comm` gives
`Nat.add_comm (n m : Nat) : n + m = m + n`) and the
``declaration uses `sorry` `` warning. A file containing `sorry` compiles
(`status: "success"`) but the `summary` says so, so an empty proof is not taken
as finished (`src/lean.ts`).

**Failure hints.** When compilation fails, the `summary` carries a one-line hint
keyed to the failure class (for example `unsolved goals` gets a tactic
suggestion; `unknown identifier` means verify the name with `#check` or
`lean-explore`; a syntax error means check the notation). Warning and information
messages that accompany the
error are kept in `diagnostics`, so the model gets maximum context from a single
attempt (`src/lean.ts`).

### Configuration

| Variable | Default | Description |
|---|---|---|
| `LEAN_MCP_PROJECT_DIR` | `./lean-project` | Directory of the Lake project to elaborate against |
| `LEAN_MCP_TIMEOUT_MS` | `180000` | Time limit for a single compilation attempt |
| `LEAN_MCP_MAX_OUTPUT_BYTES` | `262144` | Upper bound on captured compiler output |
| `ELAN_HOME` | `~/.elan` | elan installation directory; its `bin` is added to `PATH` |

### Design notes

- **JSON output parsing.** Snippets are compiled with `lake env lean --json`.
  Lean's JSON messages parse reliably; the receiver treats the proof as failed
  when any message has `severity: "error"`. If only warnings and information
  are present the code is considered compiled, and the warnings are returned in
  `diagnostics` (`src/lean.ts`).
- **Serial execution.** Concurrent tool calls run through a queue, so every call
  gets its own unique temporary file and scratch files do not interleave
  (`src/lean.ts`).
- **Timeout.** `simp`, `decide` and `norm_num` calls can run forever. On timeout
  the process group is stopped with SIGTERM and then SIGKILL, and the model gets
  an `error` with a simplification suggestion (`src/lean.ts`, `src/config.ts`).
- **Temporary files.** Snippets are written under `.lean-mcp-tmp/` inside the
  project (so Mathlib is on Lean's `LEAN_PATH`) and removed after every call. On
  startup the server clears `Check_*.lean` files left by a previous crash
  (`src/lean.ts`).

### Operational notes

- **Clean shutdown.** On SIGTERM / SIGINT / SIGHUP or a closed stdio connection
  the server terminates every in-flight compilation together with its process
  group (server, then `lake`, then `lean`) and exits immediately. This prevents a stuck
  tactic from continuing as an orphan process after the server dies. Operational
  messages go only to stderr; stdout is reserved for the MCP protocol
  (`src/index.ts`, `src/lean.ts`).
- **Startup check.** On start the server checks that `LEAN_MCP_PROJECT_DIR`
  exists and that `lake` is reachable; it prints a warning to stderr when
  something is wrong (it does not stop; every call returns a structured error)
  (`src/index.ts`).
- **Configuration errors are returned structurally.** When the project directory
  is missing or not writable, `lean_check` returns
  `{"status":"error","summary":"Lean çalıştırılamadı.","diagnostics":[...]}`
  with `isError: true`, and the cause and project directory appear in
  `diagnostics` (`src/lean.ts`).
- **Security note.** This server runs **every Lean snippet** supplied by the
  model or the calling client on the local machine; `#eval` executes arbitrary
  computation. Expose it only to a trusted party (a trusted model provider), and
  not to remote or anonymous clients (`src/lean.ts`).
- **Rebuild.** After `npm run build`, a running MCP process keeps the previous
  code in memory; restart the server (or reconnect the MCP connection in your
  client) for changes to take effect.

## Status

Lean 4.34.1 and Mathlib v4.34.1 are installed and the cache has been fetched
(`lean-project/lean-toolchain`, `lean-project/lakefile.lean`).

Verified behaviors:

- Tool contract: only `lean_check`, only a `code` input, only
  `status` / `summary` / `diagnostics` in the output (`scripts/smoke.mjs`,
  `scripts/smoke-call.mjs`).
- A real compiler error is returned in `diagnostics` with its exact text (for
  example `2:29: error: unsolved goals ⊢ False`) (`src/test/integration.test.ts`).
- With `import Mathlib`, tactics such as `norm_num` and `omega` work
  (`src/test/integration.test.ts`).
- `theorem x : P := by sorry` compiles (Lean exit code 0) but the
  ``declaration uses `sorry` `` warning is passed to the model in `diagnostics`
  and the `summary` states that the proof is unfinished; a hollow proof does not
  look finished (`src/lean.ts`, `src/test/integration.test.ts`).
- `information`-level output such as `#check` / `#print` / `#eval` is returned
  in `diagnostics`, so the model can query lemma statements
  (`src/lean.ts`, `src/test/integration.test.ts`).
- A wrong proof returns `status: "error"` with `isError: false` (the tool ran,
  the code did not compile); `isError: true` appears only when the toolchain is
  missing or a check times out (`src/index.ts`, `src/lean.ts`).
- A timeout never locks up the server; the process is terminated and the model
  is advised to simplify (`scripts/robustness.mjs`).
- Concurrent calls are serialized and no scratch files leak
  (`scripts/stress.mjs`, `src/test/integration.test.ts`).
- The same snippet with and without Mathlib tactics gives different results,
  proving the import path really works (`src/test/integration.test.ts`).

Not done yet (out of scope for this pass): wiring into an OpenCode
configuration and an end-to-end model trial through OpenChamber.

## Development

```bash
npm run build          # compile TypeScript
npm test               # parser unit tests (do not need Lean)
npm run check          # tsc + unit tests + smoke (fast CI)
npm run test:all       # check + integration + smoke:call + stress + robustness
npm run test:integration   # tests against the real compiler
npm run smoke          # stdio handshake and tool schema check
npm run smoke:call     # end-to-end verification of a tool call
npm run smoke:stress   # concurrent calls and scratch-file leak check
npm run robustness     # production behaviors: broken project dir, process-tree
                       # cleanup during SIGTERM, timeout regression
```

Integration tests skip themselves automatically when `lake` is not on `PATH`.
They also check whether Mathlib has been built; if
`.lake/packages/mathlib` has not been fetched, that single test is skipped
(`src/test/integration.test.ts`).

## License

GPL-3.0-only. If you modify and distribute this repository, your derivative work
must also be licensed under GPL-3.0. See the [LICENSE](LICENSE) file for the full
source text.

Lean, Mathlib and the MCP SDK are out of scope; they are distributed under their
own licenses.

TDQS

A3.7/5.0

Scored across 1 tool

Disambiguation5/5

There is only one tool, so no overlap or misselection is possible. An agent cannot confuse lean_check with anything else.

Naming Consistency5/5

A single snake_case noun-scoped name (lean_check) is internally consistent and clearly readable. Consistency cannot be violated with one tool.

Tool Count2/5

A server named 'Lean MCP' implies a broader Lean workflow, yet it exposes only one execution tool. There is no tool for separate diagnostics retrieval, file handling, or environment information, making the surface very thin.

Completeness3/5

Execution plus diagnostics return covers the core 'run Lean code' loop, so the primary use case is served. However, related operations such as managing source files, listing library/version info, or iterative editing are absent, leaving notable gaps.

Maintenance

ActivityMaintained
ResponsivenessNo issues