lean-lsp-mcp 0.19.1__tar.gz → 0.20.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {lean_lsp_mcp-0.19.1 → lean_lsp_mcp-0.20.0}/PKG-INFO +50 -4
- lean_lsp_mcp-0.19.1/src/lean_lsp_mcp.egg-info/PKG-INFO → lean_lsp_mcp-0.20.0/README.md +46 -26
- {lean_lsp_mcp-0.19.1 → lean_lsp_mcp-0.20.0}/pyproject.toml +9 -2
- {lean_lsp_mcp-0.19.1 → lean_lsp_mcp-0.20.0}/src/lean_lsp_mcp/__init__.py +14 -0
- {lean_lsp_mcp-0.19.1 → lean_lsp_mcp-0.20.0}/src/lean_lsp_mcp/instructions.py +11 -10
- {lean_lsp_mcp-0.19.1 → lean_lsp_mcp-0.20.0}/src/lean_lsp_mcp/loogle.py +9 -3
- lean_lsp_mcp-0.20.0/src/lean_lsp_mcp/repl.py +260 -0
- {lean_lsp_mcp-0.19.1 → lean_lsp_mcp-0.20.0}/src/lean_lsp_mcp/server.py +161 -28
- lean_lsp_mcp-0.19.1/README.md → lean_lsp_mcp-0.20.0/src/lean_lsp_mcp.egg-info/PKG-INFO +72 -3
- {lean_lsp_mcp-0.19.1 → lean_lsp_mcp-0.20.0}/src/lean_lsp_mcp.egg-info/SOURCES.txt +2 -0
- {lean_lsp_mcp-0.19.1 → lean_lsp_mcp-0.20.0}/src/lean_lsp_mcp.egg-info/requires.txt +4 -0
- lean_lsp_mcp-0.20.0/tests/test_logging.py +230 -0
- lean_lsp_mcp-0.20.0/tests/test_repl.py +217 -0
- lean_lsp_mcp-0.19.1/tests/test_logging.py +0 -90
- {lean_lsp_mcp-0.19.1 → lean_lsp_mcp-0.20.0}/LICENSE +0 -0
- {lean_lsp_mcp-0.19.1 → lean_lsp_mcp-0.20.0}/setup.cfg +0 -0
- {lean_lsp_mcp-0.19.1 → lean_lsp_mcp-0.20.0}/src/lean_lsp_mcp/__main__.py +0 -0
- {lean_lsp_mcp-0.19.1 → lean_lsp_mcp-0.20.0}/src/lean_lsp_mcp/client_utils.py +0 -0
- {lean_lsp_mcp-0.19.1 → lean_lsp_mcp-0.20.0}/src/lean_lsp_mcp/file_utils.py +0 -0
- {lean_lsp_mcp-0.19.1 → lean_lsp_mcp-0.20.0}/src/lean_lsp_mcp/models.py +0 -0
- {lean_lsp_mcp-0.19.1 → lean_lsp_mcp-0.20.0}/src/lean_lsp_mcp/outline_utils.py +0 -0
- {lean_lsp_mcp-0.19.1 → lean_lsp_mcp-0.20.0}/src/lean_lsp_mcp/profile_utils.py +0 -0
- {lean_lsp_mcp-0.19.1 → lean_lsp_mcp-0.20.0}/src/lean_lsp_mcp/search_utils.py +0 -0
- {lean_lsp_mcp-0.19.1 → lean_lsp_mcp-0.20.0}/src/lean_lsp_mcp/utils.py +0 -0
- {lean_lsp_mcp-0.19.1 → lean_lsp_mcp-0.20.0}/src/lean_lsp_mcp.egg-info/dependency_links.txt +0 -0
- {lean_lsp_mcp-0.19.1 → lean_lsp_mcp-0.20.0}/src/lean_lsp_mcp.egg-info/entry_points.txt +0 -0
- {lean_lsp_mcp-0.19.1 → lean_lsp_mcp-0.20.0}/src/lean_lsp_mcp.egg-info/top_level.txt +0 -0
- {lean_lsp_mcp-0.19.1 → lean_lsp_mcp-0.20.0}/tests/test_diagnostic_line_range.py +0 -0
- {lean_lsp_mcp-0.19.1 → lean_lsp_mcp-0.20.0}/tests/test_editor_tools.py +0 -0
- {lean_lsp_mcp-0.19.1 → lean_lsp_mcp-0.20.0}/tests/test_error_handling.py +0 -0
- {lean_lsp_mcp-0.19.1 → lean_lsp_mcp-0.20.0}/tests/test_file_caching.py +0 -0
- {lean_lsp_mcp-0.19.1 → lean_lsp_mcp-0.20.0}/tests/test_misc_tools.py +0 -0
- {lean_lsp_mcp-0.19.1 → lean_lsp_mcp-0.20.0}/tests/test_outline.py +0 -0
- {lean_lsp_mcp-0.19.1 → lean_lsp_mcp-0.20.0}/tests/test_profile.py +0 -0
- {lean_lsp_mcp-0.19.1 → lean_lsp_mcp-0.20.0}/tests/test_project_tools.py +0 -0
- {lean_lsp_mcp-0.19.1 → lean_lsp_mcp-0.20.0}/tests/test_search_tools.py +0 -0
- {lean_lsp_mcp-0.19.1 → lean_lsp_mcp-0.20.0}/tests/test_structured_output.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: lean-lsp-mcp
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.20.0
|
|
4
4
|
Summary: Lean Theorem Prover MCP
|
|
5
5
|
Author-email: Oliver Dressler <hey@oli.show>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -11,6 +11,9 @@ License-File: LICENSE
|
|
|
11
11
|
Requires-Dist: leanclient==0.9.2
|
|
12
12
|
Requires-Dist: mcp[cli]==1.25.0
|
|
13
13
|
Requires-Dist: orjson>=3.11.1
|
|
14
|
+
Requires-Dist: certifi>=2024.0.0
|
|
15
|
+
Provides-Extra: yaml
|
|
16
|
+
Requires-Dist: PyYAML>=6.0; extra == "yaml"
|
|
14
17
|
Provides-Extra: lint
|
|
15
18
|
Requires-Dist: ruff>=0.2.0; extra == "lint"
|
|
16
19
|
Provides-Extra: dev
|
|
@@ -266,8 +269,10 @@ l1c1-l1c6, severity: 3
|
|
|
266
269
|
|
|
267
270
|
#### lean_multi_attempt
|
|
268
271
|
|
|
269
|
-
Attempt multiple
|
|
270
|
-
|
|
272
|
+
Attempt multiple tactics on a line and return goal state and diagnostics for each.
|
|
273
|
+
Useful to screen different proof attempts before committing to one.
|
|
274
|
+
|
|
275
|
+
When `LEAN_REPL=true`, uses the REPL tactic mode for up to 5x faster execution (see [Environment Variables](#environment-variables)).
|
|
271
276
|
|
|
272
277
|
<details>
|
|
273
278
|
<summary>Example output (attempting `rw [Nat.pow_sub (Fintype.card_pos_of_nonempty S)]` and `by_contra h_neq`)</summary>
|
|
@@ -483,7 +488,12 @@ This MCP server works out-of-the-box without any configuration. However, a few o
|
|
|
483
488
|
### Environment Variables
|
|
484
489
|
|
|
485
490
|
- `LEAN_LOG_LEVEL`: Log level for the server. Options are "INFO", "WARNING", "ERROR", "NONE". Defaults to "INFO".
|
|
491
|
+
- `LEAN_LOG_FILE_CONFIG`: Config file path for logging, with priority over `LEAN_LOG_LEVEL`. If not set, logs are printed to stdout.
|
|
486
492
|
- `LEAN_PROJECT_PATH`: Path to your Lean project root. Set this if the server cannot automatically detect your project.
|
|
493
|
+
- `LEAN_REPL`: Set to `true`, `1`, or `yes` to enable fast REPL-based `lean_multi_attempt` (~5x faster, see [REPL Setup](#repl-setup)).
|
|
494
|
+
- `LEAN_REPL_PATH`: Path to the `repl` binary. Auto-detected from `.lake/packages/repl/` if not set.
|
|
495
|
+
- `LEAN_REPL_TIMEOUT`: Per-command timeout in seconds (default: 60).
|
|
496
|
+
- `LEAN_REPL_MEM_MB`: Max memory per REPL in MB (default: 8192). Only enforced on Linux/macOS.
|
|
487
497
|
- `LEAN_LSP_MCP_TOKEN`: Secret token for bearer authentication when using `streamable-http` or `sse` transport.
|
|
488
498
|
- `LEAN_STATE_SEARCH_URL`: URL for a self-hosted [premise-search.com](https://premise-search.com) instance.
|
|
489
499
|
- `LEAN_HAMMER_URL`: URL for a self-hosted [Lean Hammer Premise Search](https://github.com/hanwenzhu/lean-premise-server) instance.
|
|
@@ -544,6 +554,36 @@ uvx lean-lsp-mcp --transport streamable-http
|
|
|
544
554
|
|
|
545
555
|
Clients should then include the token in the `Authorization` header.
|
|
546
556
|
|
|
557
|
+
### REPL Setup
|
|
558
|
+
|
|
559
|
+
Enable fast REPL-based `lean_multi_attempt` (~5x faster). Uses [leanprover-community/repl](https://github.com/leanprover-community/repl) tactic mode.
|
|
560
|
+
|
|
561
|
+
**1. Add REPL to your Lean project's `lakefile.toml`:**
|
|
562
|
+
|
|
563
|
+
```toml
|
|
564
|
+
[[require]]
|
|
565
|
+
name = "repl"
|
|
566
|
+
git = "https://github.com/leanprover-community/repl"
|
|
567
|
+
rev = "v4.25.0" # Match your Lean version
|
|
568
|
+
```
|
|
569
|
+
|
|
570
|
+
**2. Build it:**
|
|
571
|
+
|
|
572
|
+
```bash
|
|
573
|
+
lake build repl
|
|
574
|
+
```
|
|
575
|
+
|
|
576
|
+
**3. Enable via CLI or environment variable:**
|
|
577
|
+
|
|
578
|
+
```bash
|
|
579
|
+
uvx lean-lsp-mcp --repl
|
|
580
|
+
|
|
581
|
+
# Or via environment variable
|
|
582
|
+
export LEAN_REPL=true
|
|
583
|
+
```
|
|
584
|
+
|
|
585
|
+
The REPL binary is auto-detected from `.lake/packages/repl/`. Falls back to LSP if not found.
|
|
586
|
+
|
|
547
587
|
### Local Loogle
|
|
548
588
|
|
|
549
589
|
Run loogle locally to avoid the remote API's rate limit (3 req/30s). First run takes ~5-10 minutes to build; subsequent runs start in seconds.
|
|
@@ -590,9 +630,15 @@ uv sync --all-extras
|
|
|
590
630
|
uv run pytest tests
|
|
591
631
|
```
|
|
592
632
|
|
|
593
|
-
## Publications using lean-lsp-mcp
|
|
633
|
+
## Publications and Formalization Projects using lean-lsp-mcp
|
|
594
634
|
|
|
595
635
|
- Ax-Prover: A Deep Reasoning Agentic Framework for Theorem Proving in Mathematics and Quantum Physics [arxiv](https://arxiv.org/abs/2510.12787)
|
|
636
|
+
- Numina-Lean-Agent: An Open and General Agentic Reasoning System for Formal Mathematics [arxiv](https://arxiv.org/abs/2601.14027) [github](https://github.com/project-numina/numina-lean-agent)
|
|
637
|
+
- A Group-Theoretic Approach to Shannon Capacity of Graphs and a Limit Theorem from Lattice Packings [github](https://github.com/jzuiddam/GroupTheoreticShannonCapacity/)
|
|
638
|
+
|
|
639
|
+
## Talks
|
|
640
|
+
|
|
641
|
+
lean-lsp-mcp: Tools for agentic interaction with Lean (Lean Together 2026) [youtube](https://www.youtube.com/watch?v=uttbYaTaF-E)
|
|
596
642
|
|
|
597
643
|
## Related Projects
|
|
598
644
|
|
|
@@ -1,26 +1,3 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: lean-lsp-mcp
|
|
3
|
-
Version: 0.19.1
|
|
4
|
-
Summary: Lean Theorem Prover MCP
|
|
5
|
-
Author-email: Oliver Dressler <hey@oli.show>
|
|
6
|
-
License-Expression: MIT
|
|
7
|
-
Project-URL: Repository, https://github.com/oOo0oOo/lean-lsp-mcp
|
|
8
|
-
Requires-Python: >=3.10
|
|
9
|
-
Description-Content-Type: text/markdown
|
|
10
|
-
License-File: LICENSE
|
|
11
|
-
Requires-Dist: leanclient==0.9.2
|
|
12
|
-
Requires-Dist: mcp[cli]==1.25.0
|
|
13
|
-
Requires-Dist: orjson>=3.11.1
|
|
14
|
-
Provides-Extra: lint
|
|
15
|
-
Requires-Dist: ruff>=0.2.0; extra == "lint"
|
|
16
|
-
Provides-Extra: dev
|
|
17
|
-
Requires-Dist: ruff>=0.2.0; extra == "dev"
|
|
18
|
-
Requires-Dist: pytest>=8.3; extra == "dev"
|
|
19
|
-
Requires-Dist: anyio>=4.4; extra == "dev"
|
|
20
|
-
Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
|
|
21
|
-
Requires-Dist: pytest-timeout>=2.3; extra == "dev"
|
|
22
|
-
Dynamic: license-file
|
|
23
|
-
|
|
24
1
|
<h1 align="center">
|
|
25
2
|
lean-lsp-mcp
|
|
26
3
|
</h1>
|
|
@@ -266,8 +243,10 @@ l1c1-l1c6, severity: 3
|
|
|
266
243
|
|
|
267
244
|
#### lean_multi_attempt
|
|
268
245
|
|
|
269
|
-
Attempt multiple
|
|
270
|
-
|
|
246
|
+
Attempt multiple tactics on a line and return goal state and diagnostics for each.
|
|
247
|
+
Useful to screen different proof attempts before committing to one.
|
|
248
|
+
|
|
249
|
+
When `LEAN_REPL=true`, uses the REPL tactic mode for up to 5x faster execution (see [Environment Variables](#environment-variables)).
|
|
271
250
|
|
|
272
251
|
<details>
|
|
273
252
|
<summary>Example output (attempting `rw [Nat.pow_sub (Fintype.card_pos_of_nonempty S)]` and `by_contra h_neq`)</summary>
|
|
@@ -483,7 +462,12 @@ This MCP server works out-of-the-box without any configuration. However, a few o
|
|
|
483
462
|
### Environment Variables
|
|
484
463
|
|
|
485
464
|
- `LEAN_LOG_LEVEL`: Log level for the server. Options are "INFO", "WARNING", "ERROR", "NONE". Defaults to "INFO".
|
|
465
|
+
- `LEAN_LOG_FILE_CONFIG`: Config file path for logging, with priority over `LEAN_LOG_LEVEL`. If not set, logs are printed to stdout.
|
|
486
466
|
- `LEAN_PROJECT_PATH`: Path to your Lean project root. Set this if the server cannot automatically detect your project.
|
|
467
|
+
- `LEAN_REPL`: Set to `true`, `1`, or `yes` to enable fast REPL-based `lean_multi_attempt` (~5x faster, see [REPL Setup](#repl-setup)).
|
|
468
|
+
- `LEAN_REPL_PATH`: Path to the `repl` binary. Auto-detected from `.lake/packages/repl/` if not set.
|
|
469
|
+
- `LEAN_REPL_TIMEOUT`: Per-command timeout in seconds (default: 60).
|
|
470
|
+
- `LEAN_REPL_MEM_MB`: Max memory per REPL in MB (default: 8192). Only enforced on Linux/macOS.
|
|
487
471
|
- `LEAN_LSP_MCP_TOKEN`: Secret token for bearer authentication when using `streamable-http` or `sse` transport.
|
|
488
472
|
- `LEAN_STATE_SEARCH_URL`: URL for a self-hosted [premise-search.com](https://premise-search.com) instance.
|
|
489
473
|
- `LEAN_HAMMER_URL`: URL for a self-hosted [Lean Hammer Premise Search](https://github.com/hanwenzhu/lean-premise-server) instance.
|
|
@@ -544,6 +528,36 @@ uvx lean-lsp-mcp --transport streamable-http
|
|
|
544
528
|
|
|
545
529
|
Clients should then include the token in the `Authorization` header.
|
|
546
530
|
|
|
531
|
+
### REPL Setup
|
|
532
|
+
|
|
533
|
+
Enable fast REPL-based `lean_multi_attempt` (~5x faster). Uses [leanprover-community/repl](https://github.com/leanprover-community/repl) tactic mode.
|
|
534
|
+
|
|
535
|
+
**1. Add REPL to your Lean project's `lakefile.toml`:**
|
|
536
|
+
|
|
537
|
+
```toml
|
|
538
|
+
[[require]]
|
|
539
|
+
name = "repl"
|
|
540
|
+
git = "https://github.com/leanprover-community/repl"
|
|
541
|
+
rev = "v4.25.0" # Match your Lean version
|
|
542
|
+
```
|
|
543
|
+
|
|
544
|
+
**2. Build it:**
|
|
545
|
+
|
|
546
|
+
```bash
|
|
547
|
+
lake build repl
|
|
548
|
+
```
|
|
549
|
+
|
|
550
|
+
**3. Enable via CLI or environment variable:**
|
|
551
|
+
|
|
552
|
+
```bash
|
|
553
|
+
uvx lean-lsp-mcp --repl
|
|
554
|
+
|
|
555
|
+
# Or via environment variable
|
|
556
|
+
export LEAN_REPL=true
|
|
557
|
+
```
|
|
558
|
+
|
|
559
|
+
The REPL binary is auto-detected from `.lake/packages/repl/`. Falls back to LSP if not found.
|
|
560
|
+
|
|
547
561
|
### Local Loogle
|
|
548
562
|
|
|
549
563
|
Run loogle locally to avoid the remote API's rate limit (3 req/30s). First run takes ~5-10 minutes to build; subsequent runs start in seconds.
|
|
@@ -590,9 +604,15 @@ uv sync --all-extras
|
|
|
590
604
|
uv run pytest tests
|
|
591
605
|
```
|
|
592
606
|
|
|
593
|
-
## Publications using lean-lsp-mcp
|
|
607
|
+
## Publications and Formalization Projects using lean-lsp-mcp
|
|
594
608
|
|
|
595
609
|
- Ax-Prover: A Deep Reasoning Agentic Framework for Theorem Proving in Mathematics and Quantum Physics [arxiv](https://arxiv.org/abs/2510.12787)
|
|
610
|
+
- Numina-Lean-Agent: An Open and General Agentic Reasoning System for Formal Mathematics [arxiv](https://arxiv.org/abs/2601.14027) [github](https://github.com/project-numina/numina-lean-agent)
|
|
611
|
+
- A Group-Theoretic Approach to Shannon Capacity of Graphs and a Limit Theorem from Lattice Packings [github](https://github.com/jzuiddam/GroupTheoreticShannonCapacity/)
|
|
612
|
+
|
|
613
|
+
## Talks
|
|
614
|
+
|
|
615
|
+
lean-lsp-mcp: Tools for agentic interaction with Lean (Lean Together 2026) [youtube](https://www.youtube.com/watch?v=uttbYaTaF-E)
|
|
596
616
|
|
|
597
617
|
## Related Projects
|
|
598
618
|
|
|
@@ -1,20 +1,27 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "lean-lsp-mcp"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.20.0"
|
|
4
4
|
description = "Lean Theorem Prover MCP"
|
|
5
5
|
authors = [{ name = "Oliver Dressler", email = "hey@oli.show" }]
|
|
6
6
|
readme = "README.md"
|
|
7
7
|
requires-python = ">=3.10"
|
|
8
8
|
license = "MIT"
|
|
9
|
-
dependencies = ["leanclient==0.9.2", "mcp[cli]==1.25.0", "orjson>=3.11.1"]
|
|
9
|
+
dependencies = ["leanclient==0.9.2", "mcp[cli]==1.25.0", "orjson>=3.11.1", "certifi>=2024.0.0"]
|
|
10
10
|
|
|
11
11
|
[project.urls]
|
|
12
12
|
Repository = "https://github.com/oOo0oOo/lean-lsp-mcp"
|
|
13
13
|
|
|
14
14
|
[project.optional-dependencies]
|
|
15
|
+
yaml = ["PyYAML>=6.0"]
|
|
15
16
|
lint = ["ruff>=0.2.0"]
|
|
16
17
|
dev = ["ruff>=0.2.0", "pytest>=8.3", "anyio>=4.4", "pytest-asyncio>=0.23", "pytest-timeout>=2.3"]
|
|
17
18
|
|
|
19
|
+
[tool.ruff]
|
|
20
|
+
exclude = [
|
|
21
|
+
"tests/test_project/.lake",
|
|
22
|
+
"tests/mathlib_project/.lake",
|
|
23
|
+
]
|
|
24
|
+
|
|
18
25
|
[tool.pytest.ini_options]
|
|
19
26
|
asyncio_mode = "auto"
|
|
20
27
|
markers = ["slow: marks tests as slow (deselect with '-m \"not slow\"')"]
|
|
@@ -36,6 +36,16 @@ def main():
|
|
|
36
36
|
type=str,
|
|
37
37
|
help="Override loogle cache location (default: ~/.cache/lean-lsp-mcp/loogle)",
|
|
38
38
|
)
|
|
39
|
+
parser.add_argument(
|
|
40
|
+
"--repl",
|
|
41
|
+
action="store_true",
|
|
42
|
+
help="Enable fast REPL-based multi-attempt (~5x faster). Requires Lean REPL.",
|
|
43
|
+
)
|
|
44
|
+
parser.add_argument(
|
|
45
|
+
"--repl-timeout",
|
|
46
|
+
type=int,
|
|
47
|
+
help="REPL command timeout in seconds (default: 60)",
|
|
48
|
+
)
|
|
39
49
|
args = parser.parse_args()
|
|
40
50
|
|
|
41
51
|
# Set env vars from CLI args (CLI takes precedence over env vars)
|
|
@@ -43,6 +53,10 @@ def main():
|
|
|
43
53
|
os.environ["LEAN_LOOGLE_LOCAL"] = "true"
|
|
44
54
|
if args.loogle_cache_dir:
|
|
45
55
|
os.environ["LEAN_LOOGLE_CACHE_DIR"] = args.loogle_cache_dir
|
|
56
|
+
if args.repl:
|
|
57
|
+
os.environ["LEAN_REPL"] = "true"
|
|
58
|
+
if args.repl_timeout:
|
|
59
|
+
os.environ["LEAN_REPL_TIMEOUT"] = str(args.repl_timeout)
|
|
46
60
|
|
|
47
61
|
mcp.settings.host = args.host
|
|
48
62
|
mcp.settings.port = args.port
|
|
@@ -16,19 +16,19 @@ INSTRUCTIONS = """## General Rules
|
|
|
16
16
|
- **lean_profile_proof**: Profile a theorem for performance. Shows tactic hotspots. SLOW!
|
|
17
17
|
|
|
18
18
|
## Search Tools (rate limited)
|
|
19
|
-
- **lean_leansearch** (3/30s): Natural language
|
|
20
|
-
- **lean_loogle** (3/30s): Type pattern
|
|
19
|
+
- **lean_leansearch** (3/30s): Natural language -> mathlib
|
|
20
|
+
- **lean_loogle** (3/30s): Type pattern -> mathlib
|
|
21
21
|
- **lean_leanfinder** (10/30s): Semantic/conceptual search
|
|
22
|
-
- **lean_state_search** (3/30s): Goal
|
|
23
|
-
- **lean_hammer_premise** (3/30s): Goal
|
|
22
|
+
- **lean_state_search** (3/30s): Goal -> closing lemmas
|
|
23
|
+
- **lean_hammer_premise** (3/30s): Goal -> premises for simp/aesop
|
|
24
24
|
|
|
25
25
|
## Search Decision Tree
|
|
26
|
-
1. "Does X exist locally?"
|
|
27
|
-
2. "I need a lemma that says X"
|
|
28
|
-
3. "Find lemma with type pattern"
|
|
29
|
-
4. "What's the Lean name for concept X?"
|
|
30
|
-
5. "What closes this goal?"
|
|
31
|
-
6. "What to feed simp?"
|
|
26
|
+
1. "Does X exist locally?" -> lean_local_search
|
|
27
|
+
2. "I need a lemma that says X" -> lean_leansearch
|
|
28
|
+
3. "Find lemma with type pattern" -> lean_loogle
|
|
29
|
+
4. "What's the Lean name for concept X?" -> lean_leanfinder
|
|
30
|
+
5. "What closes this goal?" -> lean_state_search
|
|
31
|
+
6. "What to feed simp?" -> lean_hammer_premise
|
|
32
32
|
|
|
33
33
|
After finding a name: lean_local_search to verify, lean_hover_info for signature.
|
|
34
34
|
|
|
@@ -37,4 +37,5 @@ List tools return JSON arrays. Empty = `[]`.
|
|
|
37
37
|
|
|
38
38
|
## Error Handling
|
|
39
39
|
Check `isError` in responses: `true` means failure (timeout/LSP error), while `[]` with `isError: false` means no results found.
|
|
40
|
+
|
|
40
41
|
"""
|
|
@@ -8,12 +8,14 @@ import json
|
|
|
8
8
|
import logging
|
|
9
9
|
import os
|
|
10
10
|
import shutil
|
|
11
|
+
import ssl
|
|
11
12
|
import subprocess
|
|
12
13
|
import urllib.parse
|
|
13
14
|
import urllib.request
|
|
14
15
|
from pathlib import Path
|
|
15
16
|
from typing import Any
|
|
16
17
|
|
|
18
|
+
import certifi
|
|
17
19
|
import orjson
|
|
18
20
|
|
|
19
21
|
from lean_lsp_mcp.models import LoogleResult
|
|
@@ -35,7 +37,8 @@ def loogle_remote(query: str, num_results: int) -> list[LoogleResult] | str:
|
|
|
35
37
|
f"https://loogle.lean-lang.org/json?q={urllib.parse.quote(query)}",
|
|
36
38
|
headers={"User-Agent": "lean-lsp-mcp/0.1"},
|
|
37
39
|
)
|
|
38
|
-
|
|
40
|
+
ssl_ctx = ssl.create_default_context(cafile=certifi.where())
|
|
41
|
+
with urllib.request.urlopen(req, timeout=10, context=ssl_ctx) as response:
|
|
39
42
|
results = orjson.loads(response.read())
|
|
40
43
|
if "hits" not in results:
|
|
41
44
|
return "No results found."
|
|
@@ -139,9 +142,12 @@ class LoogleManager:
|
|
|
139
142
|
self._run(["lake", "exe", "cache", "get"], timeout=600)
|
|
140
143
|
except Exception as e:
|
|
141
144
|
logger.warning(f"Cache download: {e}")
|
|
142
|
-
logger.info("Building loogle...")
|
|
145
|
+
logger.info("Building loogle (this may a few minutes)...")
|
|
143
146
|
try:
|
|
144
|
-
|
|
147
|
+
result = self._run(["lake", "build"], timeout=900)
|
|
148
|
+
if result.returncode != 0:
|
|
149
|
+
logger.error(f"Build failed: {result.stderr[:1000]}")
|
|
150
|
+
return result.returncode == 0
|
|
145
151
|
except Exception as e:
|
|
146
152
|
logger.error(f"Build error: {e}")
|
|
147
153
|
return False
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
"""Lean REPL for fast multi-attempt tactic execution using tactic mode."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
import platform
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
if platform.system() != "Windows":
|
|
13
|
+
import resource
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ReplError(Exception):
|
|
17
|
+
pass
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class SnippetResult:
|
|
22
|
+
goals: list[str] = field(default_factory=list)
|
|
23
|
+
messages: list[dict[str, Any]] = field(default_factory=list)
|
|
24
|
+
proof_status: str | None = None
|
|
25
|
+
error: str | None = None
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def repl_enabled() -> bool:
|
|
29
|
+
return os.environ.get("LEAN_REPL", "").lower() in ("1", "true", "yes")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def find_repl_binary(project_dir: str | None = None) -> str | None:
|
|
33
|
+
"""Find REPL binary: env var > .lake/packages > PATH."""
|
|
34
|
+
import shutil
|
|
35
|
+
from pathlib import Path
|
|
36
|
+
|
|
37
|
+
# 1. Explicit env var
|
|
38
|
+
if path := os.environ.get("LEAN_REPL_PATH"):
|
|
39
|
+
return path if Path(path).exists() or shutil.which(path) else None
|
|
40
|
+
|
|
41
|
+
# 2. Auto-detect from .lake/packages (common location after `lake build`)
|
|
42
|
+
if project_dir:
|
|
43
|
+
candidates = [
|
|
44
|
+
Path(project_dir)
|
|
45
|
+
/ ".lake"
|
|
46
|
+
/ "packages"
|
|
47
|
+
/ "repl"
|
|
48
|
+
/ ".lake"
|
|
49
|
+
/ "build"
|
|
50
|
+
/ "bin"
|
|
51
|
+
/ "repl",
|
|
52
|
+
Path(project_dir) / ".lake" / "build" / "bin" / "repl",
|
|
53
|
+
]
|
|
54
|
+
for p in candidates:
|
|
55
|
+
if p.exists():
|
|
56
|
+
return str(p)
|
|
57
|
+
|
|
58
|
+
# 3. Fall back to PATH
|
|
59
|
+
if found := shutil.which("repl"):
|
|
60
|
+
return found
|
|
61
|
+
|
|
62
|
+
return None
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _split_imports(code: str) -> tuple[str, str]:
|
|
66
|
+
"""Split code into (header with imports, body)."""
|
|
67
|
+
lines = code.splitlines()
|
|
68
|
+
i = 0
|
|
69
|
+
while i < len(lines) and (not lines[i].strip() or lines[i].startswith("import ")):
|
|
70
|
+
i += 1
|
|
71
|
+
|
|
72
|
+
# Deduplicate imports while preserving order
|
|
73
|
+
imports = [ln.strip() for ln in lines[:i] if ln.startswith("import ")]
|
|
74
|
+
header = "\n".join(dict.fromkeys(imports))
|
|
75
|
+
return header, "\n".join(lines[i:])
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class Repl:
|
|
79
|
+
"""Lean REPL using tactic mode for fast multi-attempt."""
|
|
80
|
+
|
|
81
|
+
def __init__(self, project_dir: str, repl_path: str | None = None):
|
|
82
|
+
self.project_dir = project_dir
|
|
83
|
+
self.repl_path = repl_path or find_repl_binary(project_dir) or "repl"
|
|
84
|
+
self.timeout = int(os.environ.get("LEAN_REPL_TIMEOUT", "60"))
|
|
85
|
+
self.mem_mb = int(os.environ.get("LEAN_REPL_MEM_MB", "8192"))
|
|
86
|
+
self._proc: asyncio.subprocess.Process | None = None
|
|
87
|
+
self._header: str | None = None
|
|
88
|
+
self._header_env: int | None = None
|
|
89
|
+
self._lock = asyncio.Lock()
|
|
90
|
+
|
|
91
|
+
async def _start(self) -> None:
|
|
92
|
+
"""Start REPL subprocess."""
|
|
93
|
+
kwargs: dict[str, Any] = {
|
|
94
|
+
"cwd": self.project_dir,
|
|
95
|
+
"stdin": asyncio.subprocess.PIPE,
|
|
96
|
+
"stdout": asyncio.subprocess.PIPE,
|
|
97
|
+
"stderr": asyncio.subprocess.PIPE,
|
|
98
|
+
}
|
|
99
|
+
if platform.system() != "Windows":
|
|
100
|
+
kwargs["start_new_session"] = True
|
|
101
|
+
# Memory limit on Unix systems
|
|
102
|
+
if platform.system() == "Linux":
|
|
103
|
+
limit = resource.RLIMIT_AS # Virtual memory
|
|
104
|
+
else: # macOS
|
|
105
|
+
limit = resource.RLIMIT_RSS # Resident set size
|
|
106
|
+
mem = self.mem_mb * 1024 * 1024
|
|
107
|
+
kwargs["preexec_fn"] = lambda: resource.setrlimit(limit, (mem, mem))
|
|
108
|
+
self._proc = await asyncio.create_subprocess_exec(
|
|
109
|
+
"lake", "env", self.repl_path, **kwargs
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
async def _send(self, cmd: dict[str, Any]) -> dict[str, Any]:
|
|
113
|
+
"""Send command and return response."""
|
|
114
|
+
if not self._proc or not self._proc.stdin or not self._proc.stdout:
|
|
115
|
+
raise ReplError("REPL not running")
|
|
116
|
+
|
|
117
|
+
self._proc.stdin.write((json.dumps(cmd) + "\n\n").encode())
|
|
118
|
+
await self._proc.stdin.drain()
|
|
119
|
+
|
|
120
|
+
lines = []
|
|
121
|
+
while True:
|
|
122
|
+
line = await self._proc.stdout.readline()
|
|
123
|
+
if not line or not line.strip():
|
|
124
|
+
break
|
|
125
|
+
lines.append(line)
|
|
126
|
+
|
|
127
|
+
if not lines:
|
|
128
|
+
raise ReplError("No response from REPL")
|
|
129
|
+
return json.loads(b"".join(lines))
|
|
130
|
+
|
|
131
|
+
async def _send_cmd(self, code: str, env: int | None = None) -> dict[str, Any]:
|
|
132
|
+
"""Send a command (code) to the REPL."""
|
|
133
|
+
cmd: dict[str, Any] = {"cmd": code}
|
|
134
|
+
if env is not None:
|
|
135
|
+
cmd["env"] = env
|
|
136
|
+
return await self._send(cmd)
|
|
137
|
+
|
|
138
|
+
async def _send_tactic(self, tactic: str, proof_state: int) -> dict[str, Any]:
|
|
139
|
+
"""Send a tactic to run in a proof state."""
|
|
140
|
+
return await self._send({"tactic": tactic, "proofState": proof_state})
|
|
141
|
+
|
|
142
|
+
async def _ensure_header(self, header: str) -> int | None:
|
|
143
|
+
"""Ensure REPL is running with given header, return header env."""
|
|
144
|
+
if self._header != header:
|
|
145
|
+
await self.close()
|
|
146
|
+
self._header = header
|
|
147
|
+
self._header_env = None
|
|
148
|
+
|
|
149
|
+
if not self._proc or self._proc.returncode is not None:
|
|
150
|
+
await self._start()
|
|
151
|
+
if header:
|
|
152
|
+
resp = await self._send_cmd(header, env=None)
|
|
153
|
+
if "error" in resp:
|
|
154
|
+
raise ReplError(f"Failed to load imports: {resp['error']}")
|
|
155
|
+
self._header_env = resp.get("env")
|
|
156
|
+
|
|
157
|
+
return self._header_env
|
|
158
|
+
|
|
159
|
+
async def run_snippets(
|
|
160
|
+
self, base_code: str, snippets: list[str]
|
|
161
|
+
) -> list[SnippetResult]:
|
|
162
|
+
"""Run multiple tactic snippets using tactic mode.
|
|
163
|
+
|
|
164
|
+
1. Load header (imports) - cached across calls
|
|
165
|
+
2. Send body + sorry to get proofState
|
|
166
|
+
3. Run each tactic via tactic mode (very fast)
|
|
167
|
+
"""
|
|
168
|
+
header, body = _split_imports(base_code)
|
|
169
|
+
|
|
170
|
+
async with self._lock:
|
|
171
|
+
try:
|
|
172
|
+
# Load imports (cached)
|
|
173
|
+
header_env = await asyncio.wait_for(
|
|
174
|
+
self._ensure_header(header), timeout=self.timeout
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
# Send body with sorry to get proof state
|
|
178
|
+
if not body.strip():
|
|
179
|
+
return [SnippetResult(error="No proof body") for _ in snippets]
|
|
180
|
+
|
|
181
|
+
# Ensure proper whitespace before sorry
|
|
182
|
+
body_with_sorry = body.rstrip() + "\n sorry"
|
|
183
|
+
resp = await asyncio.wait_for(
|
|
184
|
+
self._send_cmd(body_with_sorry, env=header_env),
|
|
185
|
+
timeout=self.timeout,
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
if "error" in resp:
|
|
189
|
+
return [SnippetResult(error=resp["error"]) for _ in snippets]
|
|
190
|
+
|
|
191
|
+
# Get proof state from the sorry
|
|
192
|
+
sorries = resp.get("sorries", [])
|
|
193
|
+
if not sorries:
|
|
194
|
+
# No sorry = no proof goal, check messages for errors
|
|
195
|
+
msgs = resp.get("messages", [])
|
|
196
|
+
err = "; ".join(
|
|
197
|
+
m.get("data", "") for m in msgs if m.get("severity") == "error"
|
|
198
|
+
)
|
|
199
|
+
return [
|
|
200
|
+
SnippetResult(error=err or "No proof goal found")
|
|
201
|
+
for _ in snippets
|
|
202
|
+
]
|
|
203
|
+
|
|
204
|
+
proof_state = sorries[0].get("proofState")
|
|
205
|
+
if proof_state is None:
|
|
206
|
+
return [
|
|
207
|
+
SnippetResult(error="No proofState returned") for _ in snippets
|
|
208
|
+
]
|
|
209
|
+
|
|
210
|
+
# Run each tactic in tactic mode
|
|
211
|
+
results = []
|
|
212
|
+
for snippet in snippets:
|
|
213
|
+
try:
|
|
214
|
+
resp = await asyncio.wait_for(
|
|
215
|
+
self._send_tactic(snippet.strip(), proof_state),
|
|
216
|
+
timeout=self.timeout,
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
if "error" in resp:
|
|
220
|
+
# Lean error (tactic failed)
|
|
221
|
+
results.append(SnippetResult(error=resp["error"]))
|
|
222
|
+
else:
|
|
223
|
+
goals = resp.get("goals", [])
|
|
224
|
+
messages = resp.get("messages", [])
|
|
225
|
+
proof_status = resp.get("proofStatus")
|
|
226
|
+
results.append(
|
|
227
|
+
SnippetResult(
|
|
228
|
+
goals=goals,
|
|
229
|
+
messages=messages,
|
|
230
|
+
proof_status=proof_status,
|
|
231
|
+
)
|
|
232
|
+
)
|
|
233
|
+
except Exception as e:
|
|
234
|
+
results.append(SnippetResult(error=str(e)))
|
|
235
|
+
|
|
236
|
+
return results
|
|
237
|
+
|
|
238
|
+
except Exception as e:
|
|
239
|
+
await self.close()
|
|
240
|
+
raise ReplError(str(e)) from e
|
|
241
|
+
|
|
242
|
+
async def close(self) -> None:
|
|
243
|
+
if not self._proc:
|
|
244
|
+
return
|
|
245
|
+
proc, self._proc = self._proc, None
|
|
246
|
+
self._header = None
|
|
247
|
+
self._header_env = None
|
|
248
|
+
try:
|
|
249
|
+
if platform.system() != "Windows":
|
|
250
|
+
os.killpg(os.getpgid(proc.pid), 9)
|
|
251
|
+
else:
|
|
252
|
+
proc.kill()
|
|
253
|
+
except (ProcessLookupError, OSError):
|
|
254
|
+
pass
|
|
255
|
+
try:
|
|
256
|
+
await asyncio.wait_for(proc.wait(), timeout=1.0)
|
|
257
|
+
except asyncio.TimeoutError:
|
|
258
|
+
pass
|
|
259
|
+
if hasattr(proc, "_transport") and proc._transport:
|
|
260
|
+
proc._transport.close()
|