Skip to main content
Glama
hol-lsp-mcp

hol-lsp-mcp

by briancmlee

hol-lsp-mcp

Status: experimental. This tool was written largely by an AI coding agent (Claude) and has not yet been reviewed by a human. Expect rough edges; use at your own risk.

Reviewed up to commit: none yet.

A small MCP server that lets coding agents such as Claude Code use HOL4's built-in language server, bin/hol lsp.

The tools mirror the server's LSP methods one-to-one, with the same names and meanings, so if you know hol lsp there is nothing new to learn. The wrapper only adds what an agent can't easily do itself:

  • Server lifecycle. It runs one hol lsp --lsp-check-proofs process per file, because the server serves only one script. Each process starts in the file's directory, so it picks up that directory's Holmakefile and HOLHEAP. Servers stay alive between calls. At most HOL_LSP_MCP_MAX_SERVERS run at once, and the least recently used idle one is shut down to make room. A server that has died is restarted on the next call, and the response includes its stderr tail. All servers are shut down when the MCP server exits.

  • File sync from disk. The agent edits files with its normal tools. Every call re-reads the file and sends didOpen or a full-text didChange if the file has changed.

  • Waiting for results, for as long as the agent chooses. diagnostics waits for the compile of the current text to finish, then until no proof is still checking and the server has been quiet for about a second. It gives up after wait seconds (default 25) and says INCOMPLETE, but that cancels nothing: the server carries on, and the next call picks up where this one stopped. For each proof still checking it says how long the wrapper has seen it checking (counted from the last edit at most), next to how long the slowest finished proof in the file took, so that a looping proof stands out. If the file hasn't changed since a settled answer, it returns immediately. Other calls on the same file do not queue behind a waiting diagnostics.

  • Memory and looping proofs: two limits. Poly/ML lets a process grow to 80% of RAM by default, so one looping proof can take the whole machine (a server has been seen at 13 GB). The hard limit is Poly/ML's --maxheap, given to every server: at the cap Poly/ML interrupts the server's threads, so the proof fails with Interrupt, or, if Poly cannot recover (Run out of store ... Failed to recover - exiting, as seen with a looping rewrite under a 1500 MB cap), the server exits and the next call restarts it. Either way the machine keeps its memory. The soft limits only report: diagnostics says PROBABLY LOOPING: proof X (line N) has been checking for M, ...; server footprint G GB. Cancel it or restart? when a proof has been checking for over 3 minutes, or for ten times as long as the slowest finished proof (and at least a minute), and SERVER LARGE when the server's footprint is over the soft limit. Nothing is ever stopped: edit or cheat the proof to cancel it, or call restart. The footprint is the physical footprint (compressed and swapped pages included), not RSS.

  • Positions. All lines and columns are 1-based, and columns count Unicode characters, which is how an agent sees a file full of ∀, ⇒ and ‘’. The wrapper negotiates utf-8 with the server and converts in both directions. It handles utf-16 too.

  • Flagging cheat. The server's pool checks hypotheses, not oracle tags, so it reports a proof that uses cheat as proved. diagnostics therefore also scans the file text for cheat as a whole word, skipping comments and string literals. If it finds any, the verdict is never OK: it reads CHEATS: 2 proofs use cheat (foo, bar), and each use is listed with its enclosing theorem and line.

  • Text output. Results are concise text written for an agent's context, not raw JSON.

Tools

tool

server method

returns

diagnostics(file, wait=25)

textDocument/publishDiagnostics + $/proofStates

One-line verdict (never OK if the text uses cheat), proof tally (proved/failed/suspended/diverged/cheated/checking) with the non-proved ones named, then the diagnostics. Returns once the compile has finished and the proof pool has settled, or after wait seconds with what it has so far, marked INCOMPLETE (see the example below).

goalState(file, line, column)

$/hol/goalState

theorem, step, context tags, note, error, goals (pretty without ANSI escapes); says so plainly when the result is null

eval(file, code, timeout=120)

$/eval

errors and warnings with positions in code, compilerOut/toplevelOut, plus any stderr the server wrote meanwhile (see below)

hover(file, line, column)

textDocument/hover

the hover text

definition(file, line, column)

textDocument/definition

path:line:column

references(file, line, column)

textDocument/references

path:line:column. Current HOL does not provide this, and the tool says so.

documentSymbol(file)

textDocument/documentSymbol

outline: line, name, kind, detail

workspaceSymbol(file, query)

workspace/symbol (in that file's server)

name, kind, theory, location

completion(file, line, column)

textDocument/completion

labels with details, first 50

retryCompile(file)

$/hol/retryCompile

sends the notification; call diagnostics next

restart(file, max_heap_mb=None)

(wrapper)

kills and restarts that file's server, e.g. after its dependencies were rebuilt; max_heap_mb sets that file's heap cap from then on (0 for none)

An unfinished answer looks like this:

INCOMPLETE after 25s: 48/50 proved, 2 still checking
The server keeps checking; call diagnostics again to keep waiting.
Proofs: 48 proved, 2 checking (50 total)
  par_actions_cong (line 120): checking for 4m12s (finished proofs here took ≤3s)
  freeze_agree (line 131): checking for 4m12s (finished proofs here took ≤3s)
Diagnostics: none

Related MCP server: LSP MCP

Setup

You need Python 3.10 or later and a built HOL (bin/hol and bin/hol.state).

cd hol-lsp-mcp
python3 -m venv .venv
.venv/bin/pip install -e .

Environment variables:

variable

default

meaning

HOL_BIN

$HOLDIR/bin/hol, else ~/HOL/bin/hol

the hol executable

HOL_LSP_EXTRA_PATH

unset

prepended to PATH for the server processes (e.g. ~/.local/polyml/bin)

HOL_LSP_MCP_MAX_SERVERS

3

how many servers may run at once. Each one holds a HOL heap.

HOL_LSP_MCP_MAX_HEAP_MB

3000

hard limit: Poly/ML --maxheap for each server; 0 for none. A file that needs more gets it from restart(file, max_heap_mb=...)

HOL_LSP_MCP_SOFT_MB

2000

soft limit: server footprint above which diagnostics reports SERVER LARGE

HOL_LSP_MCP_SOFT_SECS

180

soft limit: how long a proof may check before diagnostics reports it as probably looping

HOL_LSP_IDLE_SECS

900

idle shutdown: a server with no tool calls for this long is stopped, freeing its heap; the next call on the file starts a fresh one and says so in its reply. Not while a call is waiting on it or a proof is still checking (a probably-looping proof does not count). 0 disables it.

HOL_LSP_GOALSTATE_BUDGET_MS

10000

passed to the servers: how long (ms) HOL may spend walking a tactic for goalState. HOL's own default, 1500, is too short for slow tactics such as CF's xmatch; HOL ignores the variable before its lsp-walker-budget patch (see docs/upstream-issues.md).

Register it with Claude Code at local scope (the default scope):

claude mcp add hol-lsp \
  -e HOL_BIN=$HOME/HOL/bin/hol \
  -e HOL_LSP_EXTRA_PATH=$HOME/.local/polyml/bin \
  -- $HOME/Programming/hol-lsp-mcp/.venv/bin/hol-lsp-mcp serve

Trying it without an agent

The CLI uses the same core code. Each run starts a fresh server, so the position-based commands first wait for the compile to finish (--no-wait skips this).

.venv/bin/hol-lsp-mcp diagnostics path/to/fooScript.sml
.venv/bin/hol-lsp-mcp goalState path/to/fooScript.sml 42 5
.venv/bin/hol-lsp-mcp eval path/to/fooScript.sml 'PolyML.print (DB.find "ADD_COMM");'
.venv/bin/hol-lsp-mcp hover path/to/fooScript.sml 42 5
.venv/bin/hol-lsp-mcp workspaceSymbol path/to/fooScript.sml ADD_COMM

Relation to HOL's language server

hol lsp is HOL's own LSP server (see tools-poly/lsp/README.md and Manual/Developers/lsp-server.md in the HOL tree). This project is a client for it, like HOL's eglot and VS Code clients. As both of those do, it turns proof checking on (--lsp-check-proofs), so the server's worker pool really runs every proof after the fast elaboration pass.

Some server behaviour worth knowing:

  • One server per script. A server is pinned to the dependencies it loaded first. After you rebuild a dependency with Holmake, call restart. A dependency that was missing is re-read on retryCompile.

  • cheat is tallied as proved. The pool's verdict compares hypotheses, not oracle tags, so replaying cheat succeeds. The wrapper's text scan, described above, makes up for this. In the tally, cheated means "not being checked": the proof was dropped by an edit, or its tactic didn't compile and it is taken on trust.

  • The tally is accumulated from transitions. The server never retracts a name, so the wrapper hides cheated entries whose name no longer appears in the file. Otherwise deleted or renamed theorems would stay in the tally.

  • eval runs in the server process, not in the file's namespace: it sees loaded theories and libraries, but not the file's own vals. It does not echo bindings. PolyML.print output comes back in the reply, while print output goes to the server's stderr, which the wrapper captures and appends. The server's automatic holdep preloading may log harmless lines such as Cannot find file PolyML.ui.

  • An edit that runs from one proof into the next used to leave the second proof's old check running. The server decides from where an edit starts whether it is confined to one proof. If it is, the server cancels that proof's check and keeps the checks of the proofs below, matching them by name. So if one write changed two proofs, the second one's old tactic kept running, and if that tactic looped, the proof stayed checking forever. The wrapper avoids this. When a change starts inside a proof body and runs past its QED, it first sends a copy of the new text with a blank line inserted before that declaration, then the real text. The server takes the first as a statement edit and re-checks everything below. A fix for the server is described in docs/upstream-issues.md.

  • Hover returns nothing on a line that contains a string literal with non-ASCII characters, e.g. val p = ("∀", foo). This happens at every offset on that line, so it isn't a position-conversion problem.

Tests

.venv/bin/python -m unittest discover -s tests       # unit tests (no HOL needed)
.venv/bin/python tests/e2e_hol_lsp.py -v              # against a real hol lsp

The unit tests cover framing, position conversion, the cheat scan, and the session and lifecycle logic against a fake server (tests/fake_hol_lsp.py), including waiting without blocking other calls. The end-to-end script skips cleanly if HOL isn't built. Set HOL_LSP_E2E_VERBOSE=1 to see every tool's output.

License

BSD-3-Clause; see LICENSE.

Available Tools

11 tools
completionC

textDocument/completion: names in scope and theorem names (first 50).

Inputs: file (required), line (required), column (required).

definitionC

textDocument/definition: where the identifier is defined (path:line:column).

Inputs: file (required), line (required), column (required).

diagnosticsA

textDocument/publishDiagnostics + $/proofStates: a one-line verdict, the proof tally and the diagnostics. Returns once the compile has finished and no proof is still checking, or after wait seconds with INCOMPLETE and how long each unfinished proof has been checking. Nothing is cancelled: call again to keep waiting. (Clients may move calls longer than about two minutes to the background.)

Inputs: file (required), wait.

documentSymbolB

textDocument/documentSymbol: the file's outline (works even if it does not compile).

Inputs: file (required).

evalA

$/eval: compile and run SML in the file's server process. Bindings are not echoed: use print or PolyML.print. Sees loaded theories/libraries, not the file's own declarations; changes persist in the process.

Inputs: code (required), file (required), timeout.

goalStateC

$/hol/goalState: the goal state at a position inside Proof ... QED.

Inputs: file (required), line (required), column (required).

hoverC

textDocument/hover: type, value and documentation of the identifier.

Inputs: file (required), line (required), column (required).

referencesC

textDocument/references (says so if the server does not support it; current HOL does not).

Inputs: file (required), line (required), column (required).

restartA

Restart this file's server (e.g. after its dependencies were rebuilt, or when diagnostics reports a probably looping proof). max_heap_mb sets this file's heap cap from now on (0 for none); by default the server's configured cap applies.

Inputs: file (required), max_heap_mb.

retryCompileC

$/hol/retryCompile: compile a file blocked on a missing dependency again.

Inputs: file (required).

workspaceSymbolC

workspace/symbol in this file's server: stored theorems matching a substring (>= 2 chars).

Inputs: file (required), query (required).

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

Loading tool schema changelog…

    TDQS

    B3.2/5.0

    Scored across 11 tools

    Disambiguation5/5

    Each tool targets a clearly distinct operation: diagnostics for compile/proof status, goalState for interactive proof goals, eval for running SML, and standard LSP tools for hover, definition, references, symbols, and completion. retryCompile and restart are also distinguishable by context (retry a blocked file vs. restart the server). No two tools appear to do the same thing.

    Naming Consistency4/5

    Names follow a consistent lowerCamelCase convention for multi-word tools (goalState, documentSymbol, workspaceSymbol, retryCompile) and single lowercase words elsewhere (diagnostics, eval, hover, definition, references, completion, restart). There is minor semantic variation between noun-style LSP names and verb-style action names, but no mixing of casing or separator styles.

    Tool Count5/5

    11 tools is well-scoped for an LSP server focused on HOL theorem proving. The set covers core language intelligence (hover, definition, symbols, completion), HOL-specific interactions (diagnostics, goalState, eval), and recovery operations (retryCompile, restart) without redundancy.

    Completeness4/5

    The surface covers the main LSP capabilities an agent needs for HOL files, including compile diagnostics, proof goal inspection, evaluation, symbol search, and recovery. Minor gaps exist: references is documented as unsupported, and there is no direct tactic application or rename/code-action support, but agents can work around these by editing files and recompiling.

    Related MCP Connectors

    Related MCP Servers

    • A
      license
      B
      quality
      A
      maintenance
      Enables LLM agents to interact with the Lean theorem prover through the Language Server Protocol, providing tools for analyzing Lean projects, accessing diagnostics, goal states, documentation, and searching for theorems using both local and external search services.
      22
      14,692 PyPI
      549
      MIT
    • A
      license
      Not graded
      quality
      D
      maintenance
      Provides AI agents with language-aware code analysis through the Language Server Protocol, enabling tasks like getting code insights and diagnostics.
      7 npm
      193
      MIT
    • A
      license
      Not graded
      quality
      D
      maintenance
      Wraps the CodeQL Language Server Protocol to enable LLM agents to write CodeQL queries with intelligent code completion, hover information, and other language features.
      8
      MIT
    • A
      license
      Not graded
      quality
      B
      maintenance
      Enables coding agents to access Language Server Protocol operations such as hover, definitions, references, and rename through typed MCP tools.
      2
      Apache 2.0