lean-lsp-mcp 0.11.0__tar.gz → 0.11.2__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.11.0/src/lean_lsp_mcp.egg-info → lean_lsp_mcp-0.11.2}/PKG-INFO +29 -2
- {lean_lsp_mcp-0.11.0 → lean_lsp_mcp-0.11.2}/README.md +29 -2
- {lean_lsp_mcp-0.11.0 → lean_lsp_mcp-0.11.2}/pyproject.toml +1 -1
- {lean_lsp_mcp-0.11.0 → lean_lsp_mcp-0.11.2}/src/lean_lsp_mcp/client_utils.py +49 -25
- {lean_lsp_mcp-0.11.0 → lean_lsp_mcp-0.11.2}/src/lean_lsp_mcp/search_utils.py +26 -39
- {lean_lsp_mcp-0.11.0 → lean_lsp_mcp-0.11.2}/src/lean_lsp_mcp/server.py +72 -87
- {lean_lsp_mcp-0.11.0 → lean_lsp_mcp-0.11.2}/src/lean_lsp_mcp/utils.py +68 -34
- {lean_lsp_mcp-0.11.0 → lean_lsp_mcp-0.11.2/src/lean_lsp_mcp.egg-info}/PKG-INFO +29 -2
- {lean_lsp_mcp-0.11.0 → lean_lsp_mcp-0.11.2}/tests/test_search_tools.py +7 -9
- {lean_lsp_mcp-0.11.0 → lean_lsp_mcp-0.11.2}/LICENSE +0 -0
- {lean_lsp_mcp-0.11.0 → lean_lsp_mcp-0.11.2}/setup.cfg +0 -0
- {lean_lsp_mcp-0.11.0 → lean_lsp_mcp-0.11.2}/src/lean_lsp_mcp/__init__.py +0 -0
- {lean_lsp_mcp-0.11.0 → lean_lsp_mcp-0.11.2}/src/lean_lsp_mcp/__main__.py +0 -0
- {lean_lsp_mcp-0.11.0 → lean_lsp_mcp-0.11.2}/src/lean_lsp_mcp/file_utils.py +0 -0
- {lean_lsp_mcp-0.11.0 → lean_lsp_mcp-0.11.2}/src/lean_lsp_mcp/instructions.py +0 -0
- {lean_lsp_mcp-0.11.0 → lean_lsp_mcp-0.11.2}/src/lean_lsp_mcp.egg-info/SOURCES.txt +0 -0
- {lean_lsp_mcp-0.11.0 → lean_lsp_mcp-0.11.2}/src/lean_lsp_mcp.egg-info/dependency_links.txt +0 -0
- {lean_lsp_mcp-0.11.0 → lean_lsp_mcp-0.11.2}/src/lean_lsp_mcp.egg-info/entry_points.txt +0 -0
- {lean_lsp_mcp-0.11.0 → lean_lsp_mcp-0.11.2}/src/lean_lsp_mcp.egg-info/requires.txt +0 -0
- {lean_lsp_mcp-0.11.0 → lean_lsp_mcp-0.11.2}/src/lean_lsp_mcp.egg-info/top_level.txt +0 -0
- {lean_lsp_mcp-0.11.0 → lean_lsp_mcp-0.11.2}/tests/test_editor_tools.py +0 -0
- {lean_lsp_mcp-0.11.0 → lean_lsp_mcp-0.11.2}/tests/test_logging.py +0 -0
- {lean_lsp_mcp-0.11.0 → lean_lsp_mcp-0.11.2}/tests/test_misc_tools.py +0 -0
- {lean_lsp_mcp-0.11.0 → lean_lsp_mcp-0.11.2}/tests/test_project_tools.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: lean-lsp-mcp
|
|
3
|
-
Version: 0.11.
|
|
3
|
+
Version: 0.11.2
|
|
4
4
|
Summary: Lean Theorem Prover MCP
|
|
5
5
|
Author-email: Oliver Dressler <hey@oli.show>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -77,7 +77,11 @@ OR using the setup wizard:
|
|
|
77
77
|
|
|
78
78
|
Ctrl+Shift+P > "MCP: Add Server..." > "Command (stdio)" > "uvx lean-lsp-mcp" > "lean-lsp" (or any name you like) > Global or Workspace
|
|
79
79
|
|
|
80
|
-
OR manually
|
|
80
|
+
OR manually adding config by opening `mcp.json` with:
|
|
81
|
+
|
|
82
|
+
Ctrl+Shift+P > "MCP: Open User Configuration"
|
|
83
|
+
|
|
84
|
+
and adding the following
|
|
81
85
|
|
|
82
86
|
```jsonc
|
|
83
87
|
{
|
|
@@ -92,6 +96,25 @@ OR manually add config to `mcp.json`:
|
|
|
92
96
|
}
|
|
93
97
|
}
|
|
94
98
|
```
|
|
99
|
+
|
|
100
|
+
If you installed VSCode on Windows and are using WSL2 as your development environment, you may need to use this config instead:
|
|
101
|
+
|
|
102
|
+
```jsonc
|
|
103
|
+
{
|
|
104
|
+
"servers": {
|
|
105
|
+
"lean-lsp": {
|
|
106
|
+
"type": "stdio",
|
|
107
|
+
"command": "wsl.exe",
|
|
108
|
+
"args": [
|
|
109
|
+
"uvx",
|
|
110
|
+
"lean-lsp-mcp"
|
|
111
|
+
]
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
If that doesn't work, you can try cloning this repository and replace `"lean-lsp-mcp"` with `"/path/to/cloned/lean-lsp-mcp"`.
|
|
117
|
+
|
|
95
118
|
</details>
|
|
96
119
|
|
|
97
120
|
<details>
|
|
@@ -520,6 +543,10 @@ uv sync --all-extras
|
|
|
520
543
|
uv run pytest tests
|
|
521
544
|
```
|
|
522
545
|
|
|
546
|
+
## Publications using lean-lsp-mcp
|
|
547
|
+
|
|
548
|
+
- Ax-Prover: A Deep Reasoning Agentic Framework for Theorem Proving in Mathematics and Quantum Physics [arxiv](https://arxiv.org/abs/2510.12787)
|
|
549
|
+
|
|
523
550
|
## Related Projects
|
|
524
551
|
|
|
525
552
|
- [LeanTool](https://github.com/GasStationManager/LeanTool)
|
|
@@ -55,7 +55,11 @@ OR using the setup wizard:
|
|
|
55
55
|
|
|
56
56
|
Ctrl+Shift+P > "MCP: Add Server..." > "Command (stdio)" > "uvx lean-lsp-mcp" > "lean-lsp" (or any name you like) > Global or Workspace
|
|
57
57
|
|
|
58
|
-
OR manually
|
|
58
|
+
OR manually adding config by opening `mcp.json` with:
|
|
59
|
+
|
|
60
|
+
Ctrl+Shift+P > "MCP: Open User Configuration"
|
|
61
|
+
|
|
62
|
+
and adding the following
|
|
59
63
|
|
|
60
64
|
```jsonc
|
|
61
65
|
{
|
|
@@ -70,6 +74,25 @@ OR manually add config to `mcp.json`:
|
|
|
70
74
|
}
|
|
71
75
|
}
|
|
72
76
|
```
|
|
77
|
+
|
|
78
|
+
If you installed VSCode on Windows and are using WSL2 as your development environment, you may need to use this config instead:
|
|
79
|
+
|
|
80
|
+
```jsonc
|
|
81
|
+
{
|
|
82
|
+
"servers": {
|
|
83
|
+
"lean-lsp": {
|
|
84
|
+
"type": "stdio",
|
|
85
|
+
"command": "wsl.exe",
|
|
86
|
+
"args": [
|
|
87
|
+
"uvx",
|
|
88
|
+
"lean-lsp-mcp"
|
|
89
|
+
]
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
If that doesn't work, you can try cloning this repository and replace `"lean-lsp-mcp"` with `"/path/to/cloned/lean-lsp-mcp"`.
|
|
95
|
+
|
|
73
96
|
</details>
|
|
74
97
|
|
|
75
98
|
<details>
|
|
@@ -498,6 +521,10 @@ uv sync --all-extras
|
|
|
498
521
|
uv run pytest tests
|
|
499
522
|
```
|
|
500
523
|
|
|
524
|
+
## Publications using lean-lsp-mcp
|
|
525
|
+
|
|
526
|
+
- Ax-Prover: A Deep Reasoning Agentic Framework for Theorem Proving in Mathematics and Quantum Physics [arxiv](https://arxiv.org/abs/2510.12787)
|
|
527
|
+
|
|
501
528
|
## Related Projects
|
|
502
529
|
|
|
503
530
|
- [LeanTool](https://github.com/GasStationManager/LeanTool)
|
|
@@ -517,4 +544,4 @@ Citing this repository is highly appreciated but not required by the license.
|
|
|
517
544
|
month = {3},
|
|
518
545
|
year = {2025}
|
|
519
546
|
}
|
|
520
|
-
```
|
|
547
|
+
```
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import os
|
|
1
2
|
from pathlib import Path
|
|
2
3
|
from threading import Lock
|
|
3
4
|
|
|
@@ -64,35 +65,58 @@ def valid_lean_project_path(path: Path | str) -> bool:
|
|
|
64
65
|
|
|
65
66
|
|
|
66
67
|
def setup_client_for_file(ctx: Context, file_path: str) -> str | None:
|
|
67
|
-
"""
|
|
68
|
+
"""Ensure the LSP client matches the file's Lean project and return its relative path."""
|
|
68
69
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
70
|
+
lifespan = ctx.request_context.lifespan_context
|
|
71
|
+
project_cache = getattr(lifespan, "project_cache", {})
|
|
72
|
+
if not hasattr(lifespan, "project_cache"):
|
|
73
|
+
lifespan.project_cache = project_cache
|
|
72
74
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
75
|
+
abs_file_path = os.path.abspath(file_path)
|
|
76
|
+
file_dir = os.path.dirname(abs_file_path)
|
|
77
|
+
|
|
78
|
+
def activate_project(project_path: Path, cache_dirs: list[str]) -> str | None:
|
|
79
|
+
project_path_obj = project_path
|
|
80
|
+
rel = get_relative_file_path(project_path_obj, file_path)
|
|
81
|
+
if rel is None:
|
|
82
|
+
return None
|
|
83
|
+
|
|
84
|
+
project_path_obj = project_path_obj.resolve()
|
|
85
|
+
lifespan.lean_project_path = project_path_obj
|
|
86
|
+
|
|
87
|
+
cache_targets: list[str] = []
|
|
88
|
+
for directory in cache_dirs + [str(project_path_obj)]:
|
|
89
|
+
if directory and directory not in cache_targets:
|
|
90
|
+
cache_targets.append(directory)
|
|
91
|
+
|
|
92
|
+
for directory in cache_targets:
|
|
93
|
+
project_cache[directory] = project_path_obj
|
|
94
|
+
|
|
95
|
+
startup_client(ctx)
|
|
96
|
+
return rel
|
|
97
|
+
|
|
98
|
+
# Fast path: current Lean project already valid for this file
|
|
99
|
+
if lifespan.lean_project_path is not None:
|
|
100
|
+
rel_path = activate_project(lifespan.lean_project_path, [file_dir])
|
|
80
101
|
if rel_path is not None:
|
|
81
|
-
startup_client(ctx)
|
|
82
102
|
return rel_path
|
|
83
103
|
|
|
84
|
-
#
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
rel_path =
|
|
104
|
+
# Walk up from file directory to root, using cache hits or lean-toolchain
|
|
105
|
+
prev_dir = None
|
|
106
|
+
current_dir = file_dir
|
|
107
|
+
while current_dir and current_dir != prev_dir:
|
|
108
|
+
cached_root = project_cache.get(current_dir)
|
|
109
|
+
if cached_root:
|
|
110
|
+
rel_path = activate_project(Path(cached_root), [current_dir])
|
|
111
|
+
if rel_path is not None:
|
|
112
|
+
return rel_path
|
|
113
|
+
elif valid_lean_project_path(current_dir):
|
|
114
|
+
rel_path = activate_project(Path(current_dir), [current_dir])
|
|
91
115
|
if rel_path is not None:
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
116
|
+
return rel_path
|
|
117
|
+
else:
|
|
118
|
+
project_cache[current_dir] = ""
|
|
119
|
+
prev_dir = current_dir
|
|
120
|
+
current_dir = os.path.dirname(current_dir)
|
|
97
121
|
|
|
98
|
-
return
|
|
122
|
+
return None
|
|
@@ -56,10 +56,10 @@ def lean_local_search(
|
|
|
56
56
|
) -> list[dict[str, str]]:
|
|
57
57
|
"""Search Lean declarations matching ``query`` using ripgrep; results include theorems, lemmas, defs, classes, instances, structures, inductives, abbrevs, and opaque decls."""
|
|
58
58
|
root = (project_root or Path.cwd()).resolve()
|
|
59
|
-
|
|
60
|
-
|
|
59
|
+
|
|
60
|
+
pattern = (
|
|
61
61
|
rf"^\s*(?:theorem|lemma|def|axiom|class|instance|structure|inductive|abbrev|opaque)\s+"
|
|
62
|
-
rf"{
|
|
62
|
+
rf"(?:[A-Za-z0-9_'.]+\.)*{re.escape(query)}[A-Za-z0-9_'.]*(?:\s|:)"
|
|
63
63
|
)
|
|
64
64
|
|
|
65
65
|
command = [
|
|
@@ -70,68 +70,55 @@ def lean_local_search(
|
|
|
70
70
|
"--hidden",
|
|
71
71
|
"--color",
|
|
72
72
|
"never",
|
|
73
|
+
"--no-messages",
|
|
73
74
|
"-g",
|
|
74
75
|
"*.lean",
|
|
75
76
|
"-g",
|
|
76
77
|
"!.git/**",
|
|
77
78
|
"-g",
|
|
78
79
|
"!.lake/build/**",
|
|
79
|
-
|
|
80
|
-
|
|
80
|
+
pattern,
|
|
81
|
+
str(root),
|
|
81
82
|
]
|
|
82
83
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
command.append(lean_src_path)
|
|
86
|
-
|
|
87
|
-
completed = subprocess.run(
|
|
88
|
-
command,
|
|
89
|
-
capture_output=True,
|
|
90
|
-
text=True,
|
|
91
|
-
cwd=str(root),
|
|
92
|
-
)
|
|
84
|
+
if lean_src := _get_lean_src_search_path():
|
|
85
|
+
command.append(lean_src)
|
|
93
86
|
|
|
94
|
-
|
|
87
|
+
result = subprocess.run(command, capture_output=True, text=True, cwd=str(root))
|
|
95
88
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
event = _json_loads(raw_line)
|
|
101
|
-
|
|
102
|
-
if event.get("type") != "match":
|
|
89
|
+
matches = []
|
|
90
|
+
for line in result.stdout.splitlines():
|
|
91
|
+
if not line or (event := _json_loads(line)).get("type") != "match":
|
|
103
92
|
continue
|
|
104
93
|
|
|
105
94
|
data = event["data"]
|
|
106
|
-
|
|
107
|
-
parts = line_text.lstrip().split(maxsplit=2)
|
|
95
|
+
parts = data["lines"]["text"].lstrip().split(maxsplit=2)
|
|
108
96
|
if len(parts) < 2:
|
|
109
97
|
continue
|
|
110
98
|
|
|
111
|
-
decl_kind,
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
path_text = data["path"]["text"]
|
|
115
|
-
file_path = Path(path_text)
|
|
116
|
-
absolute_path = (
|
|
99
|
+
decl_kind, decl_name = parts[0], parts[1].rstrip(":")
|
|
100
|
+
file_path = Path(data["path"]["text"])
|
|
101
|
+
abs_path = (
|
|
117
102
|
file_path if file_path.is_absolute() else (root / file_path).resolve()
|
|
118
103
|
)
|
|
104
|
+
|
|
119
105
|
try:
|
|
120
|
-
display_path = str(
|
|
106
|
+
display_path = str(abs_path.relative_to(root))
|
|
121
107
|
except ValueError:
|
|
122
108
|
display_path = str(file_path)
|
|
123
109
|
|
|
124
|
-
|
|
110
|
+
matches.append({"name": decl_name, "kind": decl_kind, "file": display_path})
|
|
125
111
|
|
|
126
|
-
if len(
|
|
112
|
+
if len(matches) >= limit:
|
|
127
113
|
break
|
|
128
114
|
|
|
129
|
-
if
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
115
|
+
if result.returncode not in (0, 1) and not matches:
|
|
116
|
+
error_msg = f"ripgrep exited with code {result.returncode}"
|
|
117
|
+
if result.stderr:
|
|
118
|
+
error_msg += f"\n{result.stderr}"
|
|
119
|
+
raise RuntimeError(error_msg)
|
|
133
120
|
|
|
134
|
-
return
|
|
121
|
+
return matches
|
|
135
122
|
|
|
136
123
|
|
|
137
124
|
@lru_cache(maxsize=1)
|
|
@@ -7,7 +7,7 @@ from contextlib import asynccontextmanager
|
|
|
7
7
|
from collections.abc import AsyncIterator
|
|
8
8
|
from dataclasses import dataclass
|
|
9
9
|
import urllib
|
|
10
|
-
import
|
|
10
|
+
import orjson
|
|
11
11
|
import functools
|
|
12
12
|
import subprocess
|
|
13
13
|
import uuid
|
|
@@ -555,28 +555,37 @@ def multi_attempt(
|
|
|
555
555
|
update_file(ctx, rel_path)
|
|
556
556
|
client: LeanLSPClient = ctx.request_context.lifespan_context.client
|
|
557
557
|
|
|
558
|
-
|
|
558
|
+
try:
|
|
559
|
+
client.open_file(rel_path)
|
|
559
560
|
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
561
|
+
results = []
|
|
562
|
+
# Avoid mutating caller-provided snippets; normalize locally per attempt
|
|
563
|
+
for snippet in snippets:
|
|
564
|
+
snippet_str = snippet.rstrip("\n")
|
|
565
|
+
payload = f"{snippet_str}\n"
|
|
566
|
+
# Create a DocumentContentChange for the snippet
|
|
567
|
+
change = DocumentContentChange(
|
|
568
|
+
payload,
|
|
569
|
+
[line - 1, 0],
|
|
570
|
+
[line, 0],
|
|
571
|
+
)
|
|
572
|
+
# Apply the change to the file, capture diagnostics and goal state
|
|
573
|
+
client.update_file(rel_path, [change])
|
|
574
|
+
diag = client.get_diagnostics(rel_path)
|
|
575
|
+
formatted_diag = "\n".join(format_diagnostics(diag, select_line=line - 1))
|
|
576
|
+
# Use the snippet text length without any trailing newline for the column
|
|
577
|
+
goal = client.get_goal(rel_path, line - 1, len(snippet_str))
|
|
578
|
+
formatted_goal = format_goal(goal, "Missing goal")
|
|
579
|
+
results.append(f"{snippet_str}:\n {formatted_goal}\n\n{formatted_diag}")
|
|
576
580
|
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
581
|
+
return results
|
|
582
|
+
finally:
|
|
583
|
+
try:
|
|
584
|
+
client.close_files([rel_path])
|
|
585
|
+
except Exception as exc: # pragma: no cover - close failures only logged
|
|
586
|
+
logger.warning(
|
|
587
|
+
"Failed to close `%s` after multi_attempt: %s", rel_path, exc
|
|
588
|
+
)
|
|
580
589
|
|
|
581
590
|
|
|
582
591
|
@mcp.tool("lean_run_code")
|
|
@@ -674,11 +683,10 @@ def local_search(
|
|
|
674
683
|
return _RG_MESSAGE
|
|
675
684
|
|
|
676
685
|
stored_root = ctx.request_context.lifespan_context.lean_project_path
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
)
|
|
681
|
-
return results
|
|
686
|
+
if stored_root is None:
|
|
687
|
+
return "Lean project path not set. Call a file-based tool (like lean_goal) first to set the project path."
|
|
688
|
+
|
|
689
|
+
return lean_local_search(query=query.strip(), limit=limit, project_root=stored_root)
|
|
682
690
|
|
|
683
691
|
|
|
684
692
|
@mcp.tool("lean_leansearch")
|
|
@@ -702,9 +710,7 @@ def leansearch(ctx: Context, query: str, num_results: int = 5) -> List[Dict] | s
|
|
|
702
710
|
"""
|
|
703
711
|
try:
|
|
704
712
|
headers = {"User-Agent": "lean-lsp-mcp/0.1", "Content-Type": "application/json"}
|
|
705
|
-
payload =
|
|
706
|
-
{"num_results": str(num_results), "query": [query]}
|
|
707
|
-
).encode("utf-8")
|
|
713
|
+
payload = orjson.dumps({"num_results": str(num_results), "query": [query]})
|
|
708
714
|
|
|
709
715
|
req = urllib.request.Request(
|
|
710
716
|
"https://leansearch.net/search",
|
|
@@ -714,7 +720,7 @@ def leansearch(ctx: Context, query: str, num_results: int = 5) -> List[Dict] | s
|
|
|
714
720
|
)
|
|
715
721
|
|
|
716
722
|
with urllib.request.urlopen(req, timeout=20) as response:
|
|
717
|
-
results =
|
|
723
|
+
results = orjson.loads(response.read())
|
|
718
724
|
|
|
719
725
|
if not results or not results[0]:
|
|
720
726
|
return "No results found."
|
|
@@ -760,14 +766,14 @@ def loogle(ctx: Context, query: str, num_results: int = 8) -> List[dict] | str:
|
|
|
760
766
|
)
|
|
761
767
|
|
|
762
768
|
with urllib.request.urlopen(req, timeout=20) as response:
|
|
763
|
-
results =
|
|
769
|
+
results = orjson.loads(response.read())
|
|
764
770
|
|
|
765
771
|
if "hits" not in results:
|
|
766
772
|
return "No results found."
|
|
767
773
|
|
|
768
774
|
results = results["hits"][:num_results]
|
|
769
775
|
for result in results:
|
|
770
|
-
result.pop("doc")
|
|
776
|
+
result.pop("doc", None)
|
|
771
777
|
return results
|
|
772
778
|
except Exception as e:
|
|
773
779
|
return f"loogle error:\n{str(e)}"
|
|
@@ -775,73 +781,52 @@ def loogle(ctx: Context, query: str, num_results: int = 8) -> List[dict] | str:
|
|
|
775
781
|
|
|
776
782
|
@mcp.tool("lean_leanfinder")
|
|
777
783
|
@rate_limited("leanfinder", max_requests=10, per_seconds=30)
|
|
778
|
-
def leanfinder(
|
|
779
|
-
|
|
780
|
-
) -> List[tuple] | str:
|
|
781
|
-
"""Search Mathlib theorems/definitions semantically by mathematical concept using Lean Finder.
|
|
784
|
+
def leanfinder(ctx: Context, query: str, num_results: int = 5) -> List[Dict] | str:
|
|
785
|
+
"""Search Mathlib theorems/definitions semantically by mathematical concept or proof state using Lean Finder.
|
|
782
786
|
|
|
783
787
|
Effective query types:
|
|
784
|
-
-
|
|
785
|
-
-
|
|
786
|
-
-
|
|
787
|
-
-
|
|
788
|
+
- Natural language mathematical statement: "For any natural numbers n and m, the sum n+m is equal to m+n."
|
|
789
|
+
- Natural language questions: "I'm working with algebraic elements over a field extension … Does this imply that the minimal polynomials of x and y are equal?"
|
|
790
|
+
- Proof state. For better results, enter a proof state followed by how you want to transform the proof state.
|
|
791
|
+
- Statement definition: Fragment or the whole statement definition.
|
|
788
792
|
|
|
789
|
-
Tips:
|
|
793
|
+
Tips: Multiple targeted queries beat one complex query.
|
|
790
794
|
|
|
791
795
|
Args:
|
|
792
|
-
query (str): Mathematical
|
|
796
|
+
query (str): Mathematical concept or proof state
|
|
793
797
|
num_results (int, optional): Max results. Defaults to 5.
|
|
794
798
|
|
|
795
799
|
Returns:
|
|
796
|
-
List[
|
|
800
|
+
List[Dict] | str: List of Lean statement objects (full name, formal statement, informal statement) or error msg
|
|
797
801
|
"""
|
|
798
802
|
try:
|
|
799
803
|
headers = {"User-Agent": "lean-lsp-mcp/0.1", "Content-Type": "application/json"}
|
|
800
|
-
|
|
801
|
-
|
|
804
|
+
request_url = (
|
|
805
|
+
"https://bxrituxuhpc70w8w.us-east-1.aws.endpoints.huggingface.cloud"
|
|
806
|
+
)
|
|
807
|
+
payload = orjson.dumps({"inputs": query, "top_k": int(num_results)})
|
|
802
808
|
req = urllib.request.Request(
|
|
803
|
-
"
|
|
804
|
-
data=payload,
|
|
805
|
-
headers=headers,
|
|
806
|
-
method="POST",
|
|
809
|
+
request_url, data=payload, headers=headers, method="POST"
|
|
807
810
|
)
|
|
808
811
|
|
|
809
|
-
|
|
810
|
-
event_data = json.loads(response.read().decode("utf-8"))
|
|
811
|
-
event_id = event_data.get("event_id")
|
|
812
|
-
|
|
813
|
-
if not event_id:
|
|
814
|
-
return "Lean Finder has timed out or errored. It might be warming up, try a second time in 2 minutes."
|
|
815
|
-
|
|
816
|
-
result_url = f"https://delta-lab-ai-lean-finder.hf.space/gradio_api/call/retrieve/{event_id}"
|
|
817
|
-
req = urllib.request.Request(result_url, headers=headers, method="GET")
|
|
818
|
-
|
|
812
|
+
results = []
|
|
819
813
|
with urllib.request.urlopen(req, timeout=30) as response:
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
if
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
if formal:
|
|
837
|
-
results.append((
|
|
838
|
-
formal.group(1).strip(),
|
|
839
|
-
informal.group(1).strip() if informal else ""
|
|
840
|
-
))
|
|
841
|
-
|
|
842
|
-
return results if results else "Lean Finder: No results parsed"
|
|
843
|
-
|
|
844
|
-
return "Lean Finder: No results received"
|
|
814
|
+
data = orjson.loads(response.read())
|
|
815
|
+
for result in data["results"]:
|
|
816
|
+
if (
|
|
817
|
+
"https://leanprover-community.github.io/mathlib4_docs"
|
|
818
|
+
not in result["url"]
|
|
819
|
+
): # Do not include results from other sources other than mathlib4, since users might not have imported them
|
|
820
|
+
continue
|
|
821
|
+
full_name = re.search(r"pattern=(.*?)#doc", result["url"]).group(1)
|
|
822
|
+
obj = {
|
|
823
|
+
"full_name": full_name,
|
|
824
|
+
"formal_statement": result["formal_statement"],
|
|
825
|
+
"informal_statement": result["informal_statement"],
|
|
826
|
+
}
|
|
827
|
+
results.append(obj)
|
|
828
|
+
|
|
829
|
+
return results if results else "Lean Finder: No results parsed"
|
|
845
830
|
except Exception as e:
|
|
846
831
|
return f"Lean Finder Error:\n{str(e)}"
|
|
847
832
|
|
|
@@ -887,7 +872,7 @@ def state_search(
|
|
|
887
872
|
)
|
|
888
873
|
|
|
889
874
|
with urllib.request.urlopen(req, timeout=20) as response:
|
|
890
|
-
results =
|
|
875
|
+
results = orjson.loads(response.read())
|
|
891
876
|
|
|
892
877
|
for result in results:
|
|
893
878
|
result.pop("rev")
|
|
@@ -941,11 +926,11 @@ def hammer_premise(
|
|
|
941
926
|
"Content-Type": "application/json",
|
|
942
927
|
},
|
|
943
928
|
method="POST",
|
|
944
|
-
data=
|
|
929
|
+
data=orjson.dumps(data),
|
|
945
930
|
)
|
|
946
931
|
|
|
947
932
|
with urllib.request.urlopen(req, timeout=20) as response:
|
|
948
|
-
results =
|
|
933
|
+
results = orjson.loads(response.read())
|
|
949
934
|
|
|
950
935
|
results = [result["name"] for result in results]
|
|
951
936
|
results.insert(0, f"Results for line:\n{f_line}")
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import os
|
|
2
|
+
import secrets
|
|
2
3
|
import sys
|
|
3
4
|
import tempfile
|
|
4
5
|
from typing import List, Dict, Optional
|
|
@@ -41,6 +42,19 @@ class OutputCapture:
|
|
|
41
42
|
return self.captured_output
|
|
42
43
|
|
|
43
44
|
|
|
45
|
+
class OptionalTokenVerifier(TokenVerifier):
|
|
46
|
+
"""Minimal verifier that accepts a single pre-shared token."""
|
|
47
|
+
|
|
48
|
+
def __init__(self, expected_token: str):
|
|
49
|
+
self._expected_token = expected_token
|
|
50
|
+
|
|
51
|
+
async def verify_token(self, token: str | None) -> AccessToken | None:
|
|
52
|
+
if token is None or not secrets.compare_digest(token, self._expected_token):
|
|
53
|
+
return None
|
|
54
|
+
# AccessToken requires both client_id and scopes parameters to be provided.
|
|
55
|
+
return AccessToken(token=token, client_id="lean-lsp-mcp-optional", scopes=[])
|
|
56
|
+
|
|
57
|
+
|
|
44
58
|
def format_diagnostics(diagnostics: List[Dict], select_line: int = -1) -> List[str]:
|
|
45
59
|
"""Format the diagnostics messages.
|
|
46
60
|
|
|
@@ -190,38 +204,58 @@ def format_line(
|
|
|
190
204
|
|
|
191
205
|
|
|
192
206
|
def filter_diagnostics_by_position(
|
|
193
|
-
diagnostics: List[Dict], line: int, column: Optional[int]
|
|
207
|
+
diagnostics: List[Dict], line: Optional[int], column: Optional[int]
|
|
194
208
|
) -> List[Dict]:
|
|
195
|
-
"""
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
209
|
+
"""Return diagnostics that intersect the requested (0-indexed) position."""
|
|
210
|
+
|
|
211
|
+
if line is None:
|
|
212
|
+
return list(diagnostics)
|
|
213
|
+
|
|
214
|
+
matches: List[Dict] = []
|
|
215
|
+
for diagnostic in diagnostics:
|
|
216
|
+
diagnostic_range = diagnostic.get("range") or diagnostic.get("fullRange")
|
|
217
|
+
if not diagnostic_range:
|
|
218
|
+
continue
|
|
219
|
+
|
|
220
|
+
start = diagnostic_range.get("start", {})
|
|
221
|
+
end = diagnostic_range.get("end", {})
|
|
222
|
+
start_line = start.get("line")
|
|
223
|
+
end_line = end.get("line")
|
|
224
|
+
|
|
225
|
+
if start_line is None or end_line is None:
|
|
226
|
+
continue
|
|
227
|
+
if line < start_line or line > end_line:
|
|
228
|
+
continue
|
|
229
|
+
|
|
230
|
+
start_char = start.get("character")
|
|
231
|
+
end_char = end.get("character")
|
|
232
|
+
|
|
233
|
+
if column is None:
|
|
234
|
+
if (
|
|
235
|
+
line == end_line
|
|
236
|
+
and line != start_line
|
|
237
|
+
and end_char is not None
|
|
238
|
+
and end_char == 0
|
|
239
|
+
):
|
|
240
|
+
continue
|
|
241
|
+
matches.append(diagnostic)
|
|
242
|
+
continue
|
|
243
|
+
|
|
244
|
+
if start_char is None:
|
|
245
|
+
start_char = 0
|
|
246
|
+
if end_char is None:
|
|
247
|
+
end_char = column + 1
|
|
248
|
+
|
|
249
|
+
if start_line == end_line and start_char == end_char:
|
|
250
|
+
if column == start_char:
|
|
251
|
+
matches.append(diagnostic)
|
|
252
|
+
continue
|
|
253
|
+
|
|
254
|
+
if line == start_line and column < start_char:
|
|
255
|
+
continue
|
|
256
|
+
if line == end_line and column >= end_char:
|
|
257
|
+
continue
|
|
258
|
+
|
|
259
|
+
matches.append(diagnostic)
|
|
260
|
+
|
|
261
|
+
return matches
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: lean-lsp-mcp
|
|
3
|
-
Version: 0.11.
|
|
3
|
+
Version: 0.11.2
|
|
4
4
|
Summary: Lean Theorem Prover MCP
|
|
5
5
|
Author-email: Oliver Dressler <hey@oli.show>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -77,7 +77,11 @@ OR using the setup wizard:
|
|
|
77
77
|
|
|
78
78
|
Ctrl+Shift+P > "MCP: Add Server..." > "Command (stdio)" > "uvx lean-lsp-mcp" > "lean-lsp" (or any name you like) > Global or Workspace
|
|
79
79
|
|
|
80
|
-
OR manually
|
|
80
|
+
OR manually adding config by opening `mcp.json` with:
|
|
81
|
+
|
|
82
|
+
Ctrl+Shift+P > "MCP: Open User Configuration"
|
|
83
|
+
|
|
84
|
+
and adding the following
|
|
81
85
|
|
|
82
86
|
```jsonc
|
|
83
87
|
{
|
|
@@ -92,6 +96,25 @@ OR manually add config to `mcp.json`:
|
|
|
92
96
|
}
|
|
93
97
|
}
|
|
94
98
|
```
|
|
99
|
+
|
|
100
|
+
If you installed VSCode on Windows and are using WSL2 as your development environment, you may need to use this config instead:
|
|
101
|
+
|
|
102
|
+
```jsonc
|
|
103
|
+
{
|
|
104
|
+
"servers": {
|
|
105
|
+
"lean-lsp": {
|
|
106
|
+
"type": "stdio",
|
|
107
|
+
"command": "wsl.exe",
|
|
108
|
+
"args": [
|
|
109
|
+
"uvx",
|
|
110
|
+
"lean-lsp-mcp"
|
|
111
|
+
]
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
If that doesn't work, you can try cloning this repository and replace `"lean-lsp-mcp"` with `"/path/to/cloned/lean-lsp-mcp"`.
|
|
117
|
+
|
|
95
118
|
</details>
|
|
96
119
|
|
|
97
120
|
<details>
|
|
@@ -520,6 +543,10 @@ uv sync --all-extras
|
|
|
520
543
|
uv run pytest tests
|
|
521
544
|
```
|
|
522
545
|
|
|
546
|
+
## Publications using lean-lsp-mcp
|
|
547
|
+
|
|
548
|
+
- Ax-Prover: A Deep Reasoning Agentic Framework for Theorem Proving in Mathematics and Quantum Physics [arxiv](https://arxiv.org/abs/2510.12787)
|
|
549
|
+
|
|
523
550
|
## Related Projects
|
|
524
551
|
|
|
525
552
|
- [LeanTool](https://github.com/GasStationManager/LeanTool)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
import
|
|
3
|
+
import orjson
|
|
4
4
|
from collections.abc import Callable
|
|
5
5
|
from pathlib import Path
|
|
6
6
|
from typing import AsyncContextManager
|
|
@@ -16,8 +16,8 @@ def _first_json_block(result) -> dict[str, str] | None:
|
|
|
16
16
|
if not text:
|
|
17
17
|
continue
|
|
18
18
|
try:
|
|
19
|
-
return
|
|
20
|
-
except
|
|
19
|
+
return orjson.loads(text)
|
|
20
|
+
except orjson.JSONDecodeError:
|
|
21
21
|
continue
|
|
22
22
|
return None
|
|
23
23
|
|
|
@@ -120,12 +120,10 @@ async def test_search_tools(
|
|
|
120
120
|
)
|
|
121
121
|
finder_results = _first_json_block(finder_informal)
|
|
122
122
|
if finder_results:
|
|
123
|
-
assert isinstance(finder_results,
|
|
124
|
-
assert
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
assert isinstance(informal, str)
|
|
123
|
+
assert isinstance(finder_results, dict) and len(finder_results.keys()) == 3
|
|
124
|
+
assert {"full_name", "formal_statement", "informal_statement"} <= set(
|
|
125
|
+
finder_results.keys()
|
|
126
|
+
)
|
|
128
127
|
else:
|
|
129
128
|
finder_text = result_text(finder_informal)
|
|
130
129
|
assert finder_text and len(finder_text) > 0
|
|
131
|
-
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|