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.
Files changed (24) hide show
  1. {lean_lsp_mcp-0.11.1/src/lean_lsp_mcp.egg-info → lean_lsp_mcp-0.11.2}/PKG-INFO +5 -1
  2. {lean_lsp_mcp-0.11.1 → lean_lsp_mcp-0.11.2}/README.md +4 -0
  3. {lean_lsp_mcp-0.11.1 → lean_lsp_mcp-0.11.2}/pyproject.toml +1 -1
  4. {lean_lsp_mcp-0.11.1 → lean_lsp_mcp-0.11.2}/src/lean_lsp_mcp/search_utils.py +26 -39
  5. {lean_lsp_mcp-0.11.1 → lean_lsp_mcp-0.11.2}/src/lean_lsp_mcp/server.py +40 -64
  6. {lean_lsp_mcp-0.11.1 → lean_lsp_mcp-0.11.2/src/lean_lsp_mcp.egg-info}/PKG-INFO +5 -1
  7. {lean_lsp_mcp-0.11.1 → lean_lsp_mcp-0.11.2}/tests/test_search_tools.py +4 -6
  8. {lean_lsp_mcp-0.11.1 → lean_lsp_mcp-0.11.2}/LICENSE +0 -0
  9. {lean_lsp_mcp-0.11.1 → lean_lsp_mcp-0.11.2}/setup.cfg +0 -0
  10. {lean_lsp_mcp-0.11.1 → lean_lsp_mcp-0.11.2}/src/lean_lsp_mcp/__init__.py +0 -0
  11. {lean_lsp_mcp-0.11.1 → lean_lsp_mcp-0.11.2}/src/lean_lsp_mcp/__main__.py +0 -0
  12. {lean_lsp_mcp-0.11.1 → lean_lsp_mcp-0.11.2}/src/lean_lsp_mcp/client_utils.py +0 -0
  13. {lean_lsp_mcp-0.11.1 → lean_lsp_mcp-0.11.2}/src/lean_lsp_mcp/file_utils.py +0 -0
  14. {lean_lsp_mcp-0.11.1 → lean_lsp_mcp-0.11.2}/src/lean_lsp_mcp/instructions.py +0 -0
  15. {lean_lsp_mcp-0.11.1 → lean_lsp_mcp-0.11.2}/src/lean_lsp_mcp/utils.py +0 -0
  16. {lean_lsp_mcp-0.11.1 → lean_lsp_mcp-0.11.2}/src/lean_lsp_mcp.egg-info/SOURCES.txt +0 -0
  17. {lean_lsp_mcp-0.11.1 → lean_lsp_mcp-0.11.2}/src/lean_lsp_mcp.egg-info/dependency_links.txt +0 -0
  18. {lean_lsp_mcp-0.11.1 → lean_lsp_mcp-0.11.2}/src/lean_lsp_mcp.egg-info/entry_points.txt +0 -0
  19. {lean_lsp_mcp-0.11.1 → lean_lsp_mcp-0.11.2}/src/lean_lsp_mcp.egg-info/requires.txt +0 -0
  20. {lean_lsp_mcp-0.11.1 → lean_lsp_mcp-0.11.2}/src/lean_lsp_mcp.egg-info/top_level.txt +0 -0
  21. {lean_lsp_mcp-0.11.1 → lean_lsp_mcp-0.11.2}/tests/test_editor_tools.py +0 -0
  22. {lean_lsp_mcp-0.11.1 → lean_lsp_mcp-0.11.2}/tests/test_logging.py +0 -0
  23. {lean_lsp_mcp-0.11.1 → lean_lsp_mcp-0.11.2}/tests/test_misc_tools.py +0 -0
  24. {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.1
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)
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "lean-lsp-mcp"
3
- version = "0.11.1"
3
+ version = "0.11.2"
4
4
  description = "Lean Theorem Prover MCP"
5
5
  authors = [{name="Oliver Dressler", email="hey@oli.show"}]
6
6
  readme = "README.md"
@@ -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
- escaped_query = re.escape(query)
60
- ripgrep_pattern = (
59
+
60
+ pattern = (
61
61
  rf"^\s*(?:theorem|lemma|def|axiom|class|instance|structure|inductive|abbrev|opaque)\s+"
62
- rf"{escaped_query}[A-Za-z0-9_'.]*(?:\s|:)"
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
- ripgrep_pattern,
80
- ".",
80
+ pattern,
81
+ str(root),
81
82
  ]
82
83
 
83
- lean_src_path = _get_lean_src_search_path()
84
- if lean_src_path is not None:
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
- results: list[dict[str, str]] = []
87
+ result = subprocess.run(command, capture_output=True, text=True, cwd=str(root))
95
88
 
96
- for raw_line in completed.stdout.splitlines():
97
- if not raw_line:
98
- continue
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
- line_text = data["lines"]["text"]
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, raw_name = parts[0], parts[1]
112
- decl_name = raw_name.rstrip(":")
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(absolute_path.relative_to(root))
106
+ display_path = str(abs_path.relative_to(root))
121
107
  except ValueError:
122
108
  display_path = str(file_path)
123
109
 
124
- results.append({"name": decl_name, "kind": decl_kind, "file": display_path})
110
+ matches.append({"name": decl_name, "kind": decl_kind, "file": display_path})
125
111
 
126
- if len(results) >= limit:
112
+ if len(matches) >= limit:
127
113
  break
128
114
 
129
- if completed.returncode not in (0, 1):
130
- raise RuntimeError(
131
- f"ripgrep exited with code {completed.returncode}\n{completed.stderr}"
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 results
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("Failed to close `%s` after multi_attempt: %s", rel_path, exc)
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
- project_root = Path(stored_root) if stored_root else None
687
- results = lean_local_search(
688
- query=query.strip(), limit=limit, project_root=project_root
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
- ctx: Context, query: str, num_results: int = 5
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
- - Math + API: "setAverage Icc interval", "integral_pow symmetric bounds"
794
- - Conceptual: "algebraic elements same minimal polynomial", "quadrature nodes"
795
- - Structure: "Finset expect sum commute", "polynomial degree bounded eval"
796
- - Natural: "average equals point values", "root implies equal polynomials"
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: Mix informal math terms with Lean identifiers. Multiple targeted queries beat one complex query.
793
+ Tips: Multiple targeted queries beat one complex query.
799
794
 
800
795
  Args:
801
- query (str): Mathematical concepts combined with Lean terms
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[tuple] | str: (lean_statement, english_description) pairs or error
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
- payload = orjson.dumps({"data": [query, num_results, "Normal"]})
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
- "https://delta-lab-ai-lean-finder.hf.space/gradio_api/call/retrieve",
813
- data=payload,
814
- headers=headers,
815
- method="POST",
809
+ request_url, data=payload, headers=headers, method="POST"
816
810
  )
817
811
 
818
- with urllib.request.urlopen(req, timeout=10) as response:
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
- for line in response:
830
- line = line.decode("utf-8").strip()
831
- if line.startswith("data: "):
832
- data = orjson.loads(line[6:])
833
- if isinstance(data, list) and len(data) > 0:
834
- html = data[0] if isinstance(data[0], str) else str(data)
835
-
836
- # Parse HTML table rows
837
- rows = re.findall(
838
- r"<tr><td>\d+</td><td>(.*?)</td><td>(.*?)</td></tr>",
839
- html, re.DOTALL
840
- )
841
- results = []
842
- for formal_cell, informal_cell in rows:
843
- formal = re.search(r"<code[^>]*>(.*?)</code>", formal_cell, re.DOTALL)
844
- informal = re.search(r"<span[^>]*>(.*?)</span>", informal_cell, re.DOTALL)
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.1
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, list) and len(finder_results) > 0
124
- assert isinstance(finder_results[0], list) and len(finder_results[0]) == 2
125
- formal, informal = finder_results[0]
126
- assert isinstance(formal, str) and len(formal) > 0
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