lean-lsp-mcp 0.11.1__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.1/src/lean_lsp_mcp.egg-info → lean_lsp_mcp-0.11.2}/PKG-INFO +5 -1
- {lean_lsp_mcp-0.11.1 → lean_lsp_mcp-0.11.2}/README.md +4 -0
- {lean_lsp_mcp-0.11.1 → lean_lsp_mcp-0.11.2}/pyproject.toml +1 -1
- {lean_lsp_mcp-0.11.1 → lean_lsp_mcp-0.11.2}/src/lean_lsp_mcp/search_utils.py +26 -39
- {lean_lsp_mcp-0.11.1 → lean_lsp_mcp-0.11.2}/src/lean_lsp_mcp/server.py +40 -64
- {lean_lsp_mcp-0.11.1 → lean_lsp_mcp-0.11.2/src/lean_lsp_mcp.egg-info}/PKG-INFO +5 -1
- {lean_lsp_mcp-0.11.1 → lean_lsp_mcp-0.11.2}/tests/test_search_tools.py +4 -6
- {lean_lsp_mcp-0.11.1 → lean_lsp_mcp-0.11.2}/LICENSE +0 -0
- {lean_lsp_mcp-0.11.1 → lean_lsp_mcp-0.11.2}/setup.cfg +0 -0
- {lean_lsp_mcp-0.11.1 → lean_lsp_mcp-0.11.2}/src/lean_lsp_mcp/__init__.py +0 -0
- {lean_lsp_mcp-0.11.1 → lean_lsp_mcp-0.11.2}/src/lean_lsp_mcp/__main__.py +0 -0
- {lean_lsp_mcp-0.11.1 → lean_lsp_mcp-0.11.2}/src/lean_lsp_mcp/client_utils.py +0 -0
- {lean_lsp_mcp-0.11.1 → lean_lsp_mcp-0.11.2}/src/lean_lsp_mcp/file_utils.py +0 -0
- {lean_lsp_mcp-0.11.1 → lean_lsp_mcp-0.11.2}/src/lean_lsp_mcp/instructions.py +0 -0
- {lean_lsp_mcp-0.11.1 → lean_lsp_mcp-0.11.2}/src/lean_lsp_mcp/utils.py +0 -0
- {lean_lsp_mcp-0.11.1 → lean_lsp_mcp-0.11.2}/src/lean_lsp_mcp.egg-info/SOURCES.txt +0 -0
- {lean_lsp_mcp-0.11.1 → lean_lsp_mcp-0.11.2}/src/lean_lsp_mcp.egg-info/dependency_links.txt +0 -0
- {lean_lsp_mcp-0.11.1 → lean_lsp_mcp-0.11.2}/src/lean_lsp_mcp.egg-info/entry_points.txt +0 -0
- {lean_lsp_mcp-0.11.1 → lean_lsp_mcp-0.11.2}/src/lean_lsp_mcp.egg-info/requires.txt +0 -0
- {lean_lsp_mcp-0.11.1 → lean_lsp_mcp-0.11.2}/src/lean_lsp_mcp.egg-info/top_level.txt +0 -0
- {lean_lsp_mcp-0.11.1 → lean_lsp_mcp-0.11.2}/tests/test_editor_tools.py +0 -0
- {lean_lsp_mcp-0.11.1 → lean_lsp_mcp-0.11.2}/tests/test_logging.py +0 -0
- {lean_lsp_mcp-0.11.1 → lean_lsp_mcp-0.11.2}/tests/test_misc_tools.py +0 -0
- {lean_lsp_mcp-0.11.1 → 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
|
|
@@ -543,6 +543,10 @@ uv sync --all-extras
|
|
|
543
543
|
uv run pytest tests
|
|
544
544
|
```
|
|
545
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
|
+
|
|
546
550
|
## Related Projects
|
|
547
551
|
|
|
548
552
|
- [LeanTool](https://github.com/GasStationManager/LeanTool)
|
|
@@ -521,6 +521,10 @@ uv sync --all-extras
|
|
|
521
521
|
uv run pytest tests
|
|
522
522
|
```
|
|
523
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
|
+
|
|
524
528
|
## Related Projects
|
|
525
529
|
|
|
526
530
|
- [LeanTool](https://github.com/GasStationManager/LeanTool)
|
|
@@ -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)
|
|
@@ -572,9 +572,7 @@ def multi_attempt(
|
|
|
572
572
|
# Apply the change to the file, capture diagnostics and goal state
|
|
573
573
|
client.update_file(rel_path, [change])
|
|
574
574
|
diag = client.get_diagnostics(rel_path)
|
|
575
|
-
formatted_diag = "\n".join(
|
|
576
|
-
format_diagnostics(diag, select_line=line - 1)
|
|
577
|
-
)
|
|
575
|
+
formatted_diag = "\n".join(format_diagnostics(diag, select_line=line - 1))
|
|
578
576
|
# Use the snippet text length without any trailing newline for the column
|
|
579
577
|
goal = client.get_goal(rel_path, line - 1, len(snippet_str))
|
|
580
578
|
formatted_goal = format_goal(goal, "Missing goal")
|
|
@@ -585,7 +583,9 @@ def multi_attempt(
|
|
|
585
583
|
try:
|
|
586
584
|
client.close_files([rel_path])
|
|
587
585
|
except Exception as exc: # pragma: no cover - close failures only logged
|
|
588
|
-
logger.warning(
|
|
586
|
+
logger.warning(
|
|
587
|
+
"Failed to close `%s` after multi_attempt: %s", rel_path, exc
|
|
588
|
+
)
|
|
589
589
|
|
|
590
590
|
|
|
591
591
|
@mcp.tool("lean_run_code")
|
|
@@ -683,11 +683,10 @@ def local_search(
|
|
|
683
683
|
return _RG_MESSAGE
|
|
684
684
|
|
|
685
685
|
stored_root = ctx.request_context.lifespan_context.lean_project_path
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
)
|
|
690
|
-
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)
|
|
691
690
|
|
|
692
691
|
|
|
693
692
|
@mcp.tool("lean_leansearch")
|
|
@@ -711,9 +710,7 @@ def leansearch(ctx: Context, query: str, num_results: int = 5) -> List[Dict] | s
|
|
|
711
710
|
"""
|
|
712
711
|
try:
|
|
713
712
|
headers = {"User-Agent": "lean-lsp-mcp/0.1", "Content-Type": "application/json"}
|
|
714
|
-
payload = orjson.dumps(
|
|
715
|
-
{"num_results": str(num_results), "query": [query]}
|
|
716
|
-
)
|
|
713
|
+
payload = orjson.dumps({"num_results": str(num_results), "query": [query]})
|
|
717
714
|
|
|
718
715
|
req = urllib.request.Request(
|
|
719
716
|
"https://leansearch.net/search",
|
|
@@ -784,73 +781,52 @@ def loogle(ctx: Context, query: str, num_results: int = 8) -> List[dict] | str:
|
|
|
784
781
|
|
|
785
782
|
@mcp.tool("lean_leanfinder")
|
|
786
783
|
@rate_limited("leanfinder", max_requests=10, per_seconds=30)
|
|
787
|
-
def leanfinder(
|
|
788
|
-
|
|
789
|
-
) -> List[tuple] | str:
|
|
790
|
-
"""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.
|
|
791
786
|
|
|
792
787
|
Effective query types:
|
|
793
|
-
-
|
|
794
|
-
-
|
|
795
|
-
-
|
|
796
|
-
-
|
|
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.
|
|
797
792
|
|
|
798
|
-
Tips:
|
|
793
|
+
Tips: Multiple targeted queries beat one complex query.
|
|
799
794
|
|
|
800
795
|
Args:
|
|
801
|
-
query (str): Mathematical
|
|
796
|
+
query (str): Mathematical concept or proof state
|
|
802
797
|
num_results (int, optional): Max results. Defaults to 5.
|
|
803
798
|
|
|
804
799
|
Returns:
|
|
805
|
-
List[
|
|
800
|
+
List[Dict] | str: List of Lean statement objects (full name, formal statement, informal statement) or error msg
|
|
806
801
|
"""
|
|
807
802
|
try:
|
|
808
803
|
headers = {"User-Agent": "lean-lsp-mcp/0.1", "Content-Type": "application/json"}
|
|
809
|
-
|
|
810
|
-
|
|
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)})
|
|
811
808
|
req = urllib.request.Request(
|
|
812
|
-
"
|
|
813
|
-
data=payload,
|
|
814
|
-
headers=headers,
|
|
815
|
-
method="POST",
|
|
809
|
+
request_url, data=payload, headers=headers, method="POST"
|
|
816
810
|
)
|
|
817
811
|
|
|
818
|
-
|
|
819
|
-
event_data = orjson.loads(response.read())
|
|
820
|
-
event_id = event_data.get("event_id")
|
|
821
|
-
|
|
822
|
-
if not event_id:
|
|
823
|
-
return "Lean Finder has timed out or errored. It might be warming up, try a second time in 2 minutes."
|
|
824
|
-
|
|
825
|
-
result_url = f"https://delta-lab-ai-lean-finder.hf.space/gradio_api/call/retrieve/{event_id}"
|
|
826
|
-
req = urllib.request.Request(result_url, headers=headers, method="GET")
|
|
827
|
-
|
|
812
|
+
results = []
|
|
828
813
|
with urllib.request.urlopen(req, timeout=30) as response:
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
if
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
if formal:
|
|
846
|
-
results.append((
|
|
847
|
-
formal.group(1).strip(),
|
|
848
|
-
informal.group(1).strip() if informal else ""
|
|
849
|
-
))
|
|
850
|
-
|
|
851
|
-
return results if results else "Lean Finder: No results parsed"
|
|
852
|
-
|
|
853
|
-
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"
|
|
854
830
|
except Exception as e:
|
|
855
831
|
return f"Lean Finder Error:\n{str(e)}"
|
|
856
832
|
|
|
@@ -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
|
|
@@ -543,6 +543,10 @@ uv sync --all-extras
|
|
|
543
543
|
uv run pytest tests
|
|
544
544
|
```
|
|
545
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
|
+
|
|
546
550
|
## Related Projects
|
|
547
551
|
|
|
548
552
|
- [LeanTool](https://github.com/GasStationManager/LeanTool)
|
|
@@ -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
|
|
File without changes
|
|
File without changes
|