lean-lsp-mcp 0.11.1__tar.gz → 0.11.3__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.3}/PKG-INFO +5 -1
  2. {lean_lsp_mcp-0.11.1 → lean_lsp_mcp-0.11.3}/README.md +4 -0
  3. {lean_lsp_mcp-0.11.1 → lean_lsp_mcp-0.11.3}/pyproject.toml +1 -1
  4. {lean_lsp_mcp-0.11.1 → lean_lsp_mcp-0.11.3}/src/lean_lsp_mcp/client_utils.py +52 -36
  5. {lean_lsp_mcp-0.11.1 → lean_lsp_mcp-0.11.3}/src/lean_lsp_mcp/search_utils.py +26 -39
  6. {lean_lsp_mcp-0.11.1 → lean_lsp_mcp-0.11.3}/src/lean_lsp_mcp/server.py +51 -70
  7. {lean_lsp_mcp-0.11.1 → lean_lsp_mcp-0.11.3/src/lean_lsp_mcp.egg-info}/PKG-INFO +5 -1
  8. {lean_lsp_mcp-0.11.1 → lean_lsp_mcp-0.11.3}/tests/test_search_tools.py +4 -6
  9. {lean_lsp_mcp-0.11.1 → lean_lsp_mcp-0.11.3}/LICENSE +0 -0
  10. {lean_lsp_mcp-0.11.1 → lean_lsp_mcp-0.11.3}/setup.cfg +0 -0
  11. {lean_lsp_mcp-0.11.1 → lean_lsp_mcp-0.11.3}/src/lean_lsp_mcp/__init__.py +0 -0
  12. {lean_lsp_mcp-0.11.1 → lean_lsp_mcp-0.11.3}/src/lean_lsp_mcp/__main__.py +0 -0
  13. {lean_lsp_mcp-0.11.1 → lean_lsp_mcp-0.11.3}/src/lean_lsp_mcp/file_utils.py +0 -0
  14. {lean_lsp_mcp-0.11.1 → lean_lsp_mcp-0.11.3}/src/lean_lsp_mcp/instructions.py +0 -0
  15. {lean_lsp_mcp-0.11.1 → lean_lsp_mcp-0.11.3}/src/lean_lsp_mcp/utils.py +0 -0
  16. {lean_lsp_mcp-0.11.1 → lean_lsp_mcp-0.11.3}/src/lean_lsp_mcp.egg-info/SOURCES.txt +0 -0
  17. {lean_lsp_mcp-0.11.1 → lean_lsp_mcp-0.11.3}/src/lean_lsp_mcp.egg-info/dependency_links.txt +0 -0
  18. {lean_lsp_mcp-0.11.1 → lean_lsp_mcp-0.11.3}/src/lean_lsp_mcp.egg-info/entry_points.txt +0 -0
  19. {lean_lsp_mcp-0.11.1 → lean_lsp_mcp-0.11.3}/src/lean_lsp_mcp.egg-info/requires.txt +0 -0
  20. {lean_lsp_mcp-0.11.1 → lean_lsp_mcp-0.11.3}/src/lean_lsp_mcp.egg-info/top_level.txt +0 -0
  21. {lean_lsp_mcp-0.11.1 → lean_lsp_mcp-0.11.3}/tests/test_editor_tools.py +0 -0
  22. {lean_lsp_mcp-0.11.1 → lean_lsp_mcp-0.11.3}/tests/test_logging.py +0 -0
  23. {lean_lsp_mcp-0.11.1 → lean_lsp_mcp-0.11.3}/tests/test_misc_tools.py +0 -0
  24. {lean_lsp_mcp-0.11.1 → lean_lsp_mcp-0.11.3}/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.3
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.3"
4
4
  description = "Lean Theorem Prover MCP"
5
5
  authors = [{name="Oliver Dressler", email="hey@oli.show"}]
6
6
  readme = "README.md"
@@ -64,59 +64,75 @@ def valid_lean_project_path(path: Path | str) -> bool:
64
64
  return (path_obj / "lean-toolchain").is_file()
65
65
 
66
66
 
67
- def setup_client_for_file(ctx: Context, file_path: str) -> str | None:
68
- """Ensure the LSP client matches the file's Lean project and return its relative path."""
67
+ def infer_project_path(ctx: Context, file_path: str) -> Path | None:
68
+ """Infer and cache the Lean project path for a file WITHOUT starting the client.
69
+
70
+ Walks up the directory tree to find a lean-toolchain file, caches the result.
71
+ Sets ctx.request_context.lifespan_context.lean_project_path if found.
72
+
73
+ Side effects when path changes:
74
+ - Next LSP tool will restart the client for the new project
75
+ - File content hashes will be cleared
69
76
 
77
+ Args:
78
+ ctx (Context): Context object
79
+ file_path (str): Absolute or relative path to a Lean file
80
+
81
+ Returns:
82
+ Path | None: The resolved project path if found, None otherwise
83
+ """
70
84
  lifespan = ctx.request_context.lifespan_context
71
- project_cache = getattr(lifespan, "project_cache", {})
72
85
  if not hasattr(lifespan, "project_cache"):
73
- lifespan.project_cache = project_cache
86
+ lifespan.project_cache = {}
74
87
 
75
88
  abs_file_path = os.path.abspath(file_path)
76
89
  file_dir = os.path.dirname(abs_file_path)
77
90
 
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:
91
+ def set_project_path(project_path: Path, cache_dirs: list[str]) -> Path | None:
92
+ """Validate file is in project, set path, update cache."""
93
+ if get_relative_file_path(project_path, file_path) is None:
82
94
  return None
83
95
 
84
- project_path_obj = project_path_obj.resolve()
85
- lifespan.lean_project_path = project_path_obj
96
+ project_path = project_path.resolve()
97
+ lifespan.lean_project_path = project_path
86
98
 
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)
99
+ # Update all relevant directories in cache
100
+ for directory in set(cache_dirs + [str(project_path)]):
101
+ if directory:
102
+ lifespan.project_cache[directory] = project_path
91
103
 
92
- for directory in cache_targets:
93
- project_cache[directory] = project_path_obj
104
+ return project_path
94
105
 
95
- startup_client(ctx)
96
- return rel
106
+ # Fast path: current project already valid for this file
107
+ if lifespan.lean_project_path and set_project_path(
108
+ lifespan.lean_project_path, [file_dir]
109
+ ):
110
+ return lifespan.lean_project_path
97
111
 
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])
101
- if rel_path is not None:
102
- return rel_path
103
-
104
- # Walk up from file directory to root, using cache hits or lean-toolchain
105
- prev_dir = None
112
+ # Walk up directory tree using cache and lean-toolchain detection
106
113
  current_dir = file_dir
107
- while current_dir and current_dir != prev_dir:
108
- cached_root = project_cache.get(current_dir)
114
+ while current_dir and current_dir != os.path.dirname(current_dir):
115
+ cached_root = lifespan.project_cache.get(current_dir)
116
+
109
117
  if cached_root:
110
- rel_path = activate_project(Path(cached_root), [current_dir])
111
- if rel_path is not None:
112
- return rel_path
118
+ if result := set_project_path(Path(cached_root), [current_dir]):
119
+ return result
113
120
  elif valid_lean_project_path(current_dir):
114
- rel_path = activate_project(Path(current_dir), [current_dir])
115
- if rel_path is not None:
116
- return rel_path
121
+ if result := set_project_path(Path(current_dir), [current_dir]):
122
+ return result
117
123
  else:
118
- project_cache[current_dir] = ""
119
- prev_dir = current_dir
124
+ lifespan.project_cache[current_dir] = "" # Mark as checked
125
+
120
126
  current_dir = os.path.dirname(current_dir)
121
127
 
122
128
  return None
129
+
130
+
131
+ def setup_client_for_file(ctx: Context, file_path: str) -> str | None:
132
+ """Ensure the LSP client matches the file's Lean project and return its relative path."""
133
+ project_path = infer_project_path(ctx, file_path)
134
+ if project_path is None:
135
+ return None
136
+
137
+ startup_client(ctx)
138
+ return get_relative_file_path(project_path, file_path)
@@ -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)
@@ -18,7 +18,11 @@ from mcp.server.fastmcp.utilities.logging import get_logger, configure_logging
18
18
  from mcp.server.auth.settings import AuthSettings
19
19
  from leanclient import LeanLSPClient, DocumentContentChange
20
20
 
21
- from lean_lsp_mcp.client_utils import setup_client_for_file, startup_client
21
+ from lean_lsp_mcp.client_utils import (
22
+ setup_client_for_file,
23
+ startup_client,
24
+ infer_project_path,
25
+ )
22
26
  from lean_lsp_mcp.file_utils import get_file_contents, update_file
23
27
  from lean_lsp_mcp.instructions import INSTRUCTIONS
24
28
  from lean_lsp_mcp.search_utils import check_ripgrep_status, lean_local_search
@@ -154,10 +158,7 @@ async def lsp_build(
154
158
  ctx.request_context.lifespan_context.lean_project_path = lean_project_path_obj
155
159
 
156
160
  if lean_project_path_obj is None:
157
- return (
158
- "Lean project path not known yet. Provide `lean_project_path` explicitly or call a "
159
- "tool that infers it (e.g. `lean_goal`) before running `lean_build`."
160
- )
161
+ return "Lean project path not known yet. Provide `lean_project_path` explicitly or call a tool that infers it (e.g. `lean_file_contents`) before running `lean_build`."
161
162
 
162
163
  build_output = ""
163
164
  try:
@@ -247,6 +248,10 @@ def file_contents(ctx: Context, file_path: str, annotate_lines: bool = True) ->
247
248
  Returns:
248
249
  str: File content or error msg
249
250
  """
251
+ # Infer project path but do not start a client
252
+ if file_path.endswith(".lean"):
253
+ infer_project_path(ctx, file_path) # Silently fails for non-project files
254
+
250
255
  try:
251
256
  data = get_file_contents(file_path)
252
257
  except FileNotFoundError:
@@ -572,9 +577,7 @@ def multi_attempt(
572
577
  # Apply the change to the file, capture diagnostics and goal state
573
578
  client.update_file(rel_path, [change])
574
579
  diag = client.get_diagnostics(rel_path)
575
- formatted_diag = "\n".join(
576
- format_diagnostics(diag, select_line=line - 1)
577
- )
580
+ formatted_diag = "\n".join(format_diagnostics(diag, select_line=line - 1))
578
581
  # Use the snippet text length without any trailing newline for the column
579
582
  goal = client.get_goal(rel_path, line - 1, len(snippet_str))
580
583
  formatted_goal = format_goal(goal, "Missing goal")
@@ -585,7 +588,9 @@ def multi_attempt(
585
588
  try:
586
589
  client.close_files([rel_path])
587
590
  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)
591
+ logger.warning(
592
+ "Failed to close `%s` after multi_attempt: %s", rel_path, exc
593
+ )
589
594
 
590
595
 
591
596
  @mcp.tool("lean_run_code")
@@ -604,7 +609,7 @@ def run_code(ctx: Context, code: str) -> List[str] | str:
604
609
  lifespan_context = ctx.request_context.lifespan_context
605
610
  lean_project_path = lifespan_context.lean_project_path
606
611
  if lean_project_path is None:
607
- return "No valid Lean project path found. Run another tool (e.g. `lean_diagnostic_messages`) first to set it up or set the LEAN_PROJECT_PATH environment variable."
612
+ return "No valid Lean project path found. Run another tool (e.g. `lean_file_contents`) first to set it up."
608
613
 
609
614
  # Use a unique snippet filename to avoid collisions under concurrency
610
615
  rel_path = f"_mcp_snippet_{uuid.uuid4().hex}.lean"
@@ -683,11 +688,10 @@ def local_search(
683
688
  return _RG_MESSAGE
684
689
 
685
690
  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
691
+ if stored_root is None:
692
+ return "Lean project path not set. Call a file-based tool (like lean_file_contents) first to set the project path."
693
+
694
+ return lean_local_search(query=query.strip(), limit=limit, project_root=stored_root)
691
695
 
692
696
 
693
697
  @mcp.tool("lean_leansearch")
@@ -711,9 +715,7 @@ def leansearch(ctx: Context, query: str, num_results: int = 5) -> List[Dict] | s
711
715
  """
712
716
  try:
713
717
  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
- )
718
+ payload = orjson.dumps({"num_results": str(num_results), "query": [query]})
717
719
 
718
720
  req = urllib.request.Request(
719
721
  "https://leansearch.net/search",
@@ -784,73 +786,52 @@ def loogle(ctx: Context, query: str, num_results: int = 8) -> List[dict] | str:
784
786
 
785
787
  @mcp.tool("lean_leanfinder")
786
788
  @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.
789
+ def leanfinder(ctx: Context, query: str, num_results: int = 5) -> List[Dict] | str:
790
+ """Search Mathlib theorems/definitions semantically by mathematical concept or proof state using Lean Finder.
791
791
 
792
792
  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"
793
+ - Natural language mathematical statement: "For any natural numbers n and m, the sum n+m is equal to m+n."
794
+ - 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?"
795
+ - Proof state. For better results, enter a proof state followed by how you want to transform the proof state.
796
+ - Statement definition: Fragment or the whole statement definition.
797
797
 
798
- Tips: Mix informal math terms with Lean identifiers. Multiple targeted queries beat one complex query.
798
+ Tips: Multiple targeted queries beat one complex query.
799
799
 
800
800
  Args:
801
- query (str): Mathematical concepts combined with Lean terms
801
+ query (str): Mathematical concept or proof state
802
802
  num_results (int, optional): Max results. Defaults to 5.
803
803
 
804
804
  Returns:
805
- List[tuple] | str: (lean_statement, english_description) pairs or error
805
+ List[Dict] | str: List of Lean statement objects (full name, formal statement, informal statement) or error msg
806
806
  """
807
807
  try:
808
808
  headers = {"User-Agent": "lean-lsp-mcp/0.1", "Content-Type": "application/json"}
809
- payload = orjson.dumps({"data": [query, num_results, "Normal"]})
810
-
809
+ request_url = (
810
+ "https://bxrituxuhpc70w8w.us-east-1.aws.endpoints.huggingface.cloud"
811
+ )
812
+ payload = orjson.dumps({"inputs": query, "top_k": int(num_results)})
811
813
  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",
814
+ request_url, data=payload, headers=headers, method="POST"
816
815
  )
817
816
 
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
-
817
+ results = []
828
818
  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"
819
+ data = orjson.loads(response.read())
820
+ for result in data["results"]:
821
+ if (
822
+ "https://leanprover-community.github.io/mathlib4_docs"
823
+ not in result["url"]
824
+ ): # Do not include results from other sources other than mathlib4, since users might not have imported them
825
+ continue
826
+ full_name = re.search(r"pattern=(.*?)#doc", result["url"]).group(1)
827
+ obj = {
828
+ "full_name": full_name,
829
+ "formal_statement": result["formal_statement"],
830
+ "informal_statement": result["informal_statement"],
831
+ }
832
+ results.append(obj)
833
+
834
+ return results if results else "Lean Finder: No results parsed"
854
835
  except Exception as e:
855
836
  return f"Lean Finder Error:\n{str(e)}"
856
837
 
@@ -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.3
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