lean-lsp-mcp 0.10.2__tar.gz → 0.11.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.10.2/src/lean_lsp_mcp.egg-info → lean_lsp_mcp-0.11.0}/PKG-INFO +35 -9
- {lean_lsp_mcp-0.10.2 → lean_lsp_mcp-0.11.0}/README.md +33 -7
- {lean_lsp_mcp-0.10.2 → lean_lsp_mcp-0.11.0}/pyproject.toml +2 -2
- {lean_lsp_mcp-0.10.2 → lean_lsp_mcp-0.11.0}/src/lean_lsp_mcp/client_utils.py +31 -28
- {lean_lsp_mcp-0.10.2 → lean_lsp_mcp-0.11.0}/src/lean_lsp_mcp/file_utils.py +0 -1
- {lean_lsp_mcp-0.10.2 → lean_lsp_mcp-0.11.0}/src/lean_lsp_mcp/instructions.py +1 -0
- {lean_lsp_mcp-0.10.2 → lean_lsp_mcp-0.11.0}/src/lean_lsp_mcp/server.py +190 -14
- {lean_lsp_mcp-0.10.2 → lean_lsp_mcp-0.11.0}/src/lean_lsp_mcp/utils.py +53 -11
- {lean_lsp_mcp-0.10.2 → lean_lsp_mcp-0.11.0/src/lean_lsp_mcp.egg-info}/PKG-INFO +35 -9
- {lean_lsp_mcp-0.10.2 → lean_lsp_mcp-0.11.0}/src/lean_lsp_mcp.egg-info/requires.txt +1 -1
- {lean_lsp_mcp-0.10.2 → lean_lsp_mcp-0.11.0}/tests/test_search_tools.py +20 -0
- {lean_lsp_mcp-0.10.2 → lean_lsp_mcp-0.11.0}/LICENSE +0 -0
- {lean_lsp_mcp-0.10.2 → lean_lsp_mcp-0.11.0}/setup.cfg +0 -0
- {lean_lsp_mcp-0.10.2 → lean_lsp_mcp-0.11.0}/src/lean_lsp_mcp/__init__.py +0 -0
- {lean_lsp_mcp-0.10.2 → lean_lsp_mcp-0.11.0}/src/lean_lsp_mcp/__main__.py +0 -0
- {lean_lsp_mcp-0.10.2 → lean_lsp_mcp-0.11.0}/src/lean_lsp_mcp/search_utils.py +0 -0
- {lean_lsp_mcp-0.10.2 → lean_lsp_mcp-0.11.0}/src/lean_lsp_mcp.egg-info/SOURCES.txt +0 -0
- {lean_lsp_mcp-0.10.2 → lean_lsp_mcp-0.11.0}/src/lean_lsp_mcp.egg-info/dependency_links.txt +0 -0
- {lean_lsp_mcp-0.10.2 → lean_lsp_mcp-0.11.0}/src/lean_lsp_mcp.egg-info/entry_points.txt +0 -0
- {lean_lsp_mcp-0.10.2 → lean_lsp_mcp-0.11.0}/src/lean_lsp_mcp.egg-info/top_level.txt +0 -0
- {lean_lsp_mcp-0.10.2 → lean_lsp_mcp-0.11.0}/tests/test_editor_tools.py +0 -0
- {lean_lsp_mcp-0.10.2 → lean_lsp_mcp-0.11.0}/tests/test_logging.py +0 -0
- {lean_lsp_mcp-0.10.2 → lean_lsp_mcp-0.11.0}/tests/test_misc_tools.py +0 -0
- {lean_lsp_mcp-0.10.2 → lean_lsp_mcp-0.11.0}/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.
|
|
3
|
+
Version: 0.11.0
|
|
4
4
|
Summary: Lean Theorem Prover MCP
|
|
5
5
|
Author-email: Oliver Dressler <hey@oli.show>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -8,7 +8,7 @@ Project-URL: Repository, https://github.com/oOo0oOo/lean-lsp-mcp
|
|
|
8
8
|
Requires-Python: >=3.10
|
|
9
9
|
Description-Content-Type: text/markdown
|
|
10
10
|
License-File: LICENSE
|
|
11
|
-
Requires-Dist: leanclient==0.
|
|
11
|
+
Requires-Dist: leanclient==0.4.0
|
|
12
12
|
Requires-Dist: mcp[cli]==1.19.0
|
|
13
13
|
Requires-Dist: orjson>=3.11.1
|
|
14
14
|
Provides-Extra: lint
|
|
@@ -43,7 +43,7 @@ MCP server that allows agentic interaction with the [Lean theorem prover](https:
|
|
|
43
43
|
## Key Features
|
|
44
44
|
|
|
45
45
|
* **Rich Lean Interaction**: Access diagnostics, goal states, term information, hover documentation and more.
|
|
46
|
-
* **External Search Tools**: Use `
|
|
46
|
+
* **External Search Tools**: Use `LeanSearch`, `Loogle`, `Lean Finder`, `Lean Hammer` and `Lean State Search` to find relevant theorems and definitions.
|
|
47
47
|
* **Easy Setup**: Simple configuration for various clients, including VSCode, Cursor and Claude Code.
|
|
48
48
|
|
|
49
49
|
## Setup
|
|
@@ -122,11 +122,9 @@ Run one of these commands in the root directory of your Lean project (where `lak
|
|
|
122
122
|
# Local-scoped MCP server
|
|
123
123
|
claude mcp add lean-lsp uvx lean-lsp-mcp
|
|
124
124
|
|
|
125
|
-
# OR project-scoped MCP server
|
|
125
|
+
# OR project-scoped MCP server
|
|
126
|
+
# (creates or updates a .mcp.json file in the current directory)
|
|
126
127
|
claude mcp add lean-lsp -s project uvx lean-lsp-mcp
|
|
127
|
-
|
|
128
|
-
# OR If you run into issues with the project path (e.g. the language server directory cannot be found), you can also set it manually e.g.
|
|
129
|
-
claude mcp add lean-lsp uvx lean-lsp-mcp -e LEAN_PROJECT_PATH=$PWD
|
|
130
128
|
```
|
|
131
129
|
|
|
132
130
|
You can find more details about MCP server configuration for Claude Code [here](https://docs.anthropic.com/en/docs/agents-and-tools/claude-code/tutorials#configure-mcp-servers).
|
|
@@ -134,7 +132,7 @@ You can find more details about MCP server configuration for Claude Code [here](
|
|
|
134
132
|
|
|
135
133
|
#### Claude Skill: Lean4 Theorem Proving
|
|
136
134
|
|
|
137
|
-
If you are using [Claude Desktop](https://modelcontextprotocol.io/quickstart/user) or [Claude Code](https://claude.ai/code), you can also install the [Lean4 Theorem Proving Skill](https://github.com/cameronfreer/lean4-skills/tree/main/lean4-theorem-proving). This skill provides additional prompts and templates for interacting with Lean4 projects and includes a section on interacting with the `lean-lsp-mcp` server.
|
|
135
|
+
If you are using [Claude Desktop](https://modelcontextprotocol.io/quickstart/user) or [Claude Code](https://claude.ai/code), you can also install the [Lean4 Theorem Proving Skill](https://github.com/cameronfreer/lean4-skills/tree/main/plugins/lean4-theorem-proving). This skill provides additional prompts and templates for interacting with Lean4 projects and includes a section on interacting with the `lean-lsp-mcp` server.
|
|
138
136
|
|
|
139
137
|
### 4. Install ripgrep (optional but recommended)
|
|
140
138
|
|
|
@@ -284,7 +282,9 @@ This tool requires [ripgrep](https://github.com/BurntSushi/ripgrep?tab=readme-ov
|
|
|
284
282
|
|
|
285
283
|
### External Search Tools
|
|
286
284
|
|
|
287
|
-
Currently
|
|
285
|
+
Currently most external tools are separately **rate limited to 3 requests per 30 seconds**. Please don't ruin the fun for everyone by overusing these amazing free services!
|
|
286
|
+
|
|
287
|
+
Please cite the original authors of these tools if you use them!
|
|
288
288
|
|
|
289
289
|
#### lean_leansearch
|
|
290
290
|
|
|
@@ -337,6 +337,32 @@ Search for Lean definitions and theorems using [loogle.lean-lang.org](https://lo
|
|
|
337
337
|
```
|
|
338
338
|
</details>
|
|
339
339
|
|
|
340
|
+
#### lean_leanfinder
|
|
341
|
+
|
|
342
|
+
Semantic search for Mathlib theorems using [Lean Finder](https://huggingface.co/spaces/delta-lab-ai/Lean-Finder).
|
|
343
|
+
|
|
344
|
+
[Arxiv Paper](https://arxiv.org/abs/2510.15940)
|
|
345
|
+
|
|
346
|
+
- Supports informal descriptions, user questions, proof states, and statement fragments.
|
|
347
|
+
- Examples: `algebraic elements x,y over K with same minimal polynomial`, `Does y being a root of minpoly(x) imply minpoly(x)=minpoly(y)?`, `⊢ |re z| ≤ ‖z‖` + `transform to squared norm inequality`, `theorem restrict Ioi: restrict Ioi e = restrict Ici e`
|
|
348
|
+
|
|
349
|
+
<details>
|
|
350
|
+
<summary>Example output</summary>
|
|
351
|
+
|
|
352
|
+
Query: `Does y being a root of minpoly(x) imply minpoly(x)=minpoly(y)?`
|
|
353
|
+
|
|
354
|
+
```json
|
|
355
|
+
[
|
|
356
|
+
[
|
|
357
|
+
"/-- If `y : L` is a root of `minpoly K x`, then `minpoly K y = minpoly K x`. -/\ntheorem eq_of_root {x y : L} (hx : IsAlgebraic K x)\n (h_ev : Polynomial.aeval y (minpoly K x) = 0) : minpoly K y = minpoly K x :=\n ((eq_iff_aeval_minpoly_eq_zero hx.isIntegral).mpr h_ev).symm",
|
|
358
|
+
|
|
359
|
+
"Let $L/K$ be a field extension, and let $x, y \\in L$ be elements such that $y$ is a root of the minimal polynomial of $x$ over $K$. If $x$ is algebraic over $K$, then the minimal polynomial of $y$ over $K$ is equal to the minimal polynomial of $x$ over $K$, i.e., $\\text{minpoly}_K(y) = \\text{minpoly}_K(x)$. This means that if $y$ satisfies the polynomial equation defined by $x$, then $y$ shares the same minimal polynomial as $x$."
|
|
360
|
+
],
|
|
361
|
+
...
|
|
362
|
+
]
|
|
363
|
+
```
|
|
364
|
+
</details>
|
|
365
|
+
|
|
340
366
|
#### lean_state_search
|
|
341
367
|
|
|
342
368
|
Search for applicable theorems for the current proof goal using [premise-search.com](https://premise-search.com/).
|
|
@@ -21,7 +21,7 @@ MCP server that allows agentic interaction with the [Lean theorem prover](https:
|
|
|
21
21
|
## Key Features
|
|
22
22
|
|
|
23
23
|
* **Rich Lean Interaction**: Access diagnostics, goal states, term information, hover documentation and more.
|
|
24
|
-
* **External Search Tools**: Use `
|
|
24
|
+
* **External Search Tools**: Use `LeanSearch`, `Loogle`, `Lean Finder`, `Lean Hammer` and `Lean State Search` to find relevant theorems and definitions.
|
|
25
25
|
* **Easy Setup**: Simple configuration for various clients, including VSCode, Cursor and Claude Code.
|
|
26
26
|
|
|
27
27
|
## Setup
|
|
@@ -100,11 +100,9 @@ Run one of these commands in the root directory of your Lean project (where `lak
|
|
|
100
100
|
# Local-scoped MCP server
|
|
101
101
|
claude mcp add lean-lsp uvx lean-lsp-mcp
|
|
102
102
|
|
|
103
|
-
# OR project-scoped MCP server
|
|
103
|
+
# OR project-scoped MCP server
|
|
104
|
+
# (creates or updates a .mcp.json file in the current directory)
|
|
104
105
|
claude mcp add lean-lsp -s project uvx lean-lsp-mcp
|
|
105
|
-
|
|
106
|
-
# OR If you run into issues with the project path (e.g. the language server directory cannot be found), you can also set it manually e.g.
|
|
107
|
-
claude mcp add lean-lsp uvx lean-lsp-mcp -e LEAN_PROJECT_PATH=$PWD
|
|
108
106
|
```
|
|
109
107
|
|
|
110
108
|
You can find more details about MCP server configuration for Claude Code [here](https://docs.anthropic.com/en/docs/agents-and-tools/claude-code/tutorials#configure-mcp-servers).
|
|
@@ -112,7 +110,7 @@ You can find more details about MCP server configuration for Claude Code [here](
|
|
|
112
110
|
|
|
113
111
|
#### Claude Skill: Lean4 Theorem Proving
|
|
114
112
|
|
|
115
|
-
If you are using [Claude Desktop](https://modelcontextprotocol.io/quickstart/user) or [Claude Code](https://claude.ai/code), you can also install the [Lean4 Theorem Proving Skill](https://github.com/cameronfreer/lean4-skills/tree/main/lean4-theorem-proving). This skill provides additional prompts and templates for interacting with Lean4 projects and includes a section on interacting with the `lean-lsp-mcp` server.
|
|
113
|
+
If you are using [Claude Desktop](https://modelcontextprotocol.io/quickstart/user) or [Claude Code](https://claude.ai/code), you can also install the [Lean4 Theorem Proving Skill](https://github.com/cameronfreer/lean4-skills/tree/main/plugins/lean4-theorem-proving). This skill provides additional prompts and templates for interacting with Lean4 projects and includes a section on interacting with the `lean-lsp-mcp` server.
|
|
116
114
|
|
|
117
115
|
### 4. Install ripgrep (optional but recommended)
|
|
118
116
|
|
|
@@ -262,7 +260,9 @@ This tool requires [ripgrep](https://github.com/BurntSushi/ripgrep?tab=readme-ov
|
|
|
262
260
|
|
|
263
261
|
### External Search Tools
|
|
264
262
|
|
|
265
|
-
Currently
|
|
263
|
+
Currently most external tools are separately **rate limited to 3 requests per 30 seconds**. Please don't ruin the fun for everyone by overusing these amazing free services!
|
|
264
|
+
|
|
265
|
+
Please cite the original authors of these tools if you use them!
|
|
266
266
|
|
|
267
267
|
#### lean_leansearch
|
|
268
268
|
|
|
@@ -315,6 +315,32 @@ Search for Lean definitions and theorems using [loogle.lean-lang.org](https://lo
|
|
|
315
315
|
```
|
|
316
316
|
</details>
|
|
317
317
|
|
|
318
|
+
#### lean_leanfinder
|
|
319
|
+
|
|
320
|
+
Semantic search for Mathlib theorems using [Lean Finder](https://huggingface.co/spaces/delta-lab-ai/Lean-Finder).
|
|
321
|
+
|
|
322
|
+
[Arxiv Paper](https://arxiv.org/abs/2510.15940)
|
|
323
|
+
|
|
324
|
+
- Supports informal descriptions, user questions, proof states, and statement fragments.
|
|
325
|
+
- Examples: `algebraic elements x,y over K with same minimal polynomial`, `Does y being a root of minpoly(x) imply minpoly(x)=minpoly(y)?`, `⊢ |re z| ≤ ‖z‖` + `transform to squared norm inequality`, `theorem restrict Ioi: restrict Ioi e = restrict Ici e`
|
|
326
|
+
|
|
327
|
+
<details>
|
|
328
|
+
<summary>Example output</summary>
|
|
329
|
+
|
|
330
|
+
Query: `Does y being a root of minpoly(x) imply minpoly(x)=minpoly(y)?`
|
|
331
|
+
|
|
332
|
+
```json
|
|
333
|
+
[
|
|
334
|
+
[
|
|
335
|
+
"/-- If `y : L` is a root of `minpoly K x`, then `minpoly K y = minpoly K x`. -/\ntheorem eq_of_root {x y : L} (hx : IsAlgebraic K x)\n (h_ev : Polynomial.aeval y (minpoly K x) = 0) : minpoly K y = minpoly K x :=\n ((eq_iff_aeval_minpoly_eq_zero hx.isIntegral).mpr h_ev).symm",
|
|
336
|
+
|
|
337
|
+
"Let $L/K$ be a field extension, and let $x, y \\in L$ be elements such that $y$ is a root of the minimal polynomial of $x$ over $K$. If $x$ is algebraic over $K$, then the minimal polynomial of $y$ over $K$ is equal to the minimal polynomial of $x$ over $K$, i.e., $\\text{minpoly}_K(y) = \\text{minpoly}_K(x)$. This means that if $y$ satisfies the polynomial equation defined by $x$, then $y$ shares the same minimal polynomial as $x$."
|
|
338
|
+
],
|
|
339
|
+
...
|
|
340
|
+
]
|
|
341
|
+
```
|
|
342
|
+
</details>
|
|
343
|
+
|
|
318
344
|
#### lean_state_search
|
|
319
345
|
|
|
320
346
|
Search for applicable theorems for the current proof goal using [premise-search.com](https://premise-search.com/).
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "lean-lsp-mcp"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.11.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
9
|
dependencies = [
|
|
10
|
-
"leanclient==0.
|
|
10
|
+
"leanclient==0.4.0",
|
|
11
11
|
"mcp[cli]==1.19.0",
|
|
12
12
|
"orjson>=3.11.1",
|
|
13
13
|
]
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
from pathlib import Path
|
|
2
|
+
from threading import Lock
|
|
2
3
|
|
|
3
4
|
from mcp.server.fastmcp import Context
|
|
4
5
|
from mcp.server.fastmcp.utilities.logging import get_logger
|
|
@@ -9,6 +10,7 @@ from lean_lsp_mcp.utils import OutputCapture
|
|
|
9
10
|
|
|
10
11
|
|
|
11
12
|
logger = get_logger(__name__)
|
|
13
|
+
CLIENT_LOCK = Lock()
|
|
12
14
|
|
|
13
15
|
|
|
14
16
|
def startup_client(ctx: Context):
|
|
@@ -17,34 +19,35 @@ def startup_client(ctx: Context):
|
|
|
17
19
|
Args:
|
|
18
20
|
ctx (Context): Context object.
|
|
19
21
|
"""
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
22
|
+
with CLIENT_LOCK:
|
|
23
|
+
lean_project_path = ctx.request_context.lifespan_context.lean_project_path
|
|
24
|
+
if lean_project_path is None:
|
|
25
|
+
raise ValueError("lean project path is not set.")
|
|
26
|
+
|
|
27
|
+
# Check if already correct client
|
|
28
|
+
client: LeanLSPClient | None = ctx.request_context.lifespan_context.client
|
|
29
|
+
|
|
30
|
+
if client is not None:
|
|
31
|
+
# Both are Path objects now, direct comparison works
|
|
32
|
+
if client.project_path == lean_project_path:
|
|
33
|
+
return # Client already set up correctly - reuse it!
|
|
34
|
+
# Different project path - close old client
|
|
35
|
+
client.close()
|
|
36
|
+
ctx.request_context.lifespan_context.file_content_hashes.clear()
|
|
37
|
+
|
|
38
|
+
# Need to create a new client
|
|
39
|
+
with OutputCapture() as output:
|
|
40
|
+
try:
|
|
41
|
+
client = LeanLSPClient(lean_project_path)
|
|
42
|
+
logger.info(f"Connected to Lean language server at {lean_project_path}")
|
|
43
|
+
except Exception as e:
|
|
44
|
+
logger.warning(f"Initial connection failed, trying with build: {e}")
|
|
45
|
+
client = LeanLSPClient(lean_project_path, initial_build=True)
|
|
46
|
+
logger.info(f"Connected with initial build to {lean_project_path}")
|
|
47
|
+
build_output = output.get_output()
|
|
48
|
+
if build_output:
|
|
49
|
+
logger.debug(f"Build output: {build_output}")
|
|
50
|
+
ctx.request_context.lifespan_context.client = client
|
|
48
51
|
|
|
49
52
|
|
|
50
53
|
def valid_lean_project_path(path: Path | str) -> bool:
|
|
@@ -11,5 +11,6 @@ INSTRUCTIONS = """## General Rules
|
|
|
11
11
|
- lean_hover_info: Documentation about terms and lean syntax.
|
|
12
12
|
- lean_leansearch: Search theorems using natural language or Lean terms.
|
|
13
13
|
- lean_loogle: Search definitions and theorems by name, type, or subexpression.
|
|
14
|
+
- lean_leanfinder: Semantic search for theorems using Lean Finder.
|
|
14
15
|
- lean_state_search: Search theorems using goal-based search.
|
|
15
16
|
"""
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import asyncio
|
|
1
2
|
import os
|
|
2
3
|
import re
|
|
3
4
|
import time
|
|
@@ -9,6 +10,7 @@ import urllib
|
|
|
9
10
|
import json
|
|
10
11
|
import functools
|
|
11
12
|
import subprocess
|
|
13
|
+
import uuid
|
|
12
14
|
from pathlib import Path
|
|
13
15
|
|
|
14
16
|
from mcp.server.fastmcp import Context, FastMCP
|
|
@@ -16,7 +18,7 @@ from mcp.server.fastmcp.utilities.logging import get_logger, configure_logging
|
|
|
16
18
|
from mcp.server.auth.settings import AuthSettings
|
|
17
19
|
from leanclient import LeanLSPClient, DocumentContentChange
|
|
18
20
|
|
|
19
|
-
from lean_lsp_mcp.client_utils import setup_client_for_file
|
|
21
|
+
from lean_lsp_mcp.client_utils import setup_client_for_file, startup_client
|
|
20
22
|
from lean_lsp_mcp.file_utils import get_file_contents, update_file
|
|
21
23
|
from lean_lsp_mcp.instructions import INSTRUCTIONS
|
|
22
24
|
from lean_lsp_mcp.search_utils import check_ripgrep_status, lean_local_search
|
|
@@ -66,6 +68,7 @@ async def app_lifespan(server: FastMCP) -> AsyncIterator[AppContext]:
|
|
|
66
68
|
rate_limit={
|
|
67
69
|
"leansearch": [],
|
|
68
70
|
"loogle": [],
|
|
71
|
+
"leanfinder": [],
|
|
69
72
|
"lean_state_search": [],
|
|
70
73
|
"hammer_premise": [],
|
|
71
74
|
},
|
|
@@ -103,7 +106,14 @@ def rate_limited(category: str, max_requests: int, per_seconds: int):
|
|
|
103
106
|
def decorator(func):
|
|
104
107
|
@functools.wraps(func)
|
|
105
108
|
def wrapper(*args, **kwargs):
|
|
106
|
-
|
|
109
|
+
ctx = kwargs.get("ctx")
|
|
110
|
+
if ctx is None:
|
|
111
|
+
if not args:
|
|
112
|
+
raise KeyError(
|
|
113
|
+
"rate_limited wrapper requires ctx as a keyword argument or the first positional argument"
|
|
114
|
+
)
|
|
115
|
+
ctx = args[0]
|
|
116
|
+
rate_limit = ctx.request_context.lifespan_context.rate_limit
|
|
107
117
|
current_time = int(time.time())
|
|
108
118
|
rate_limit[category] = [
|
|
109
119
|
timestamp
|
|
@@ -123,7 +133,9 @@ def rate_limited(category: str, max_requests: int, per_seconds: int):
|
|
|
123
133
|
|
|
124
134
|
# Project level tools
|
|
125
135
|
@mcp.tool("lean_build")
|
|
126
|
-
def lsp_build(
|
|
136
|
+
async def lsp_build(
|
|
137
|
+
ctx: Context, lean_project_path: str = None, clean: bool = False
|
|
138
|
+
) -> str:
|
|
127
139
|
"""Build the Lean project and restart the LSP Server.
|
|
128
140
|
|
|
129
141
|
Use only if needed (e.g. new imports).
|
|
@@ -141,10 +153,17 @@ def lsp_build(ctx: Context, lean_project_path: str = None, clean: bool = False)
|
|
|
141
153
|
lean_project_path_obj = Path(lean_project_path).resolve()
|
|
142
154
|
ctx.request_context.lifespan_context.lean_project_path = lean_project_path_obj
|
|
143
155
|
|
|
156
|
+
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
|
+
|
|
144
162
|
build_output = ""
|
|
145
163
|
try:
|
|
146
164
|
client: LeanLSPClient = ctx.request_context.lifespan_context.client
|
|
147
165
|
if client:
|
|
166
|
+
ctx.request_context.lifespan_context.client = None
|
|
148
167
|
client.close()
|
|
149
168
|
ctx.request_context.lifespan_context.file_content_hashes.clear()
|
|
150
169
|
|
|
@@ -152,13 +171,65 @@ def lsp_build(ctx: Context, lean_project_path: str = None, clean: bool = False)
|
|
|
152
171
|
subprocess.run(["lake", "clean"], cwd=lean_project_path_obj, check=False)
|
|
153
172
|
logger.info("Ran `lake clean`")
|
|
154
173
|
|
|
155
|
-
|
|
156
|
-
|
|
174
|
+
# Fetch cache
|
|
175
|
+
subprocess.run(
|
|
176
|
+
["lake", "exe", "cache", "get"], cwd=lean_project_path_obj, check=False
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
# Run build with progress reporting
|
|
180
|
+
process = await asyncio.create_subprocess_exec(
|
|
181
|
+
"lake",
|
|
182
|
+
"build",
|
|
183
|
+
"--verbose",
|
|
184
|
+
cwd=lean_project_path_obj,
|
|
185
|
+
stdout=asyncio.subprocess.PIPE,
|
|
186
|
+
stderr=asyncio.subprocess.STDOUT,
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
output_lines = []
|
|
190
|
+
|
|
191
|
+
while True:
|
|
192
|
+
line = await process.stdout.readline()
|
|
193
|
+
if not line:
|
|
194
|
+
break
|
|
195
|
+
|
|
196
|
+
line_str = line.decode("utf-8", errors="replace").rstrip()
|
|
197
|
+
output_lines.append(line_str)
|
|
198
|
+
|
|
199
|
+
# Parse progress: look for pattern like "[2/8]" or "[10/100]"
|
|
200
|
+
match = re.search(r"\[(\d+)/(\d+)\]", line_str)
|
|
201
|
+
if match:
|
|
202
|
+
current_job = int(match.group(1))
|
|
203
|
+
total_jobs = int(match.group(2))
|
|
204
|
+
|
|
205
|
+
# Extract what's being built
|
|
206
|
+
# Line format: "ℹ [2/8] Built TestLeanBuild.Basic (1.6s)"
|
|
207
|
+
desc_match = re.search(
|
|
208
|
+
r"\[\d+/\d+\]\s+(.+?)(?:\s+\(\d+\.?\d*[ms]+\))?$", line_str
|
|
209
|
+
)
|
|
210
|
+
description = desc_match.group(1) if desc_match else "Building"
|
|
211
|
+
|
|
212
|
+
# Report progress using dynamic totals from Lake
|
|
213
|
+
await ctx.report_progress(
|
|
214
|
+
progress=current_job, total=total_jobs, message=description
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
await process.wait()
|
|
218
|
+
|
|
219
|
+
if process.returncode != 0:
|
|
220
|
+
build_output = "\n".join(output_lines)
|
|
221
|
+
raise Exception(f"Build failed with return code {process.returncode}")
|
|
222
|
+
|
|
223
|
+
# Start LSP client (without initial build since we just did it)
|
|
224
|
+
with OutputCapture():
|
|
225
|
+
client = LeanLSPClient(
|
|
226
|
+
lean_project_path_obj, initial_build=False, prevent_cache_get=True
|
|
227
|
+
)
|
|
157
228
|
|
|
158
229
|
logger.info("Built project and re-started LSP client")
|
|
159
230
|
|
|
160
231
|
ctx.request_context.lifespan_context.client = client
|
|
161
|
-
build_output =
|
|
232
|
+
build_output = "\n".join(output_lines)
|
|
162
233
|
return build_output
|
|
163
234
|
except Exception as e:
|
|
164
235
|
return f"Error during build:\n{str(e)}\n{build_output}"
|
|
@@ -521,11 +592,13 @@ def run_code(ctx: Context, code: str) -> List[str] | str:
|
|
|
521
592
|
Returns:
|
|
522
593
|
List[str] | str: Diagnostics msgs or error msg
|
|
523
594
|
"""
|
|
524
|
-
|
|
595
|
+
lifespan_context = ctx.request_context.lifespan_context
|
|
596
|
+
lean_project_path = lifespan_context.lean_project_path
|
|
525
597
|
if lean_project_path is None:
|
|
526
598
|
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."
|
|
527
599
|
|
|
528
|
-
|
|
600
|
+
# Use a unique snippet filename to avoid collisions under concurrency
|
|
601
|
+
rel_path = f"_mcp_snippet_{uuid.uuid4().hex}.lean"
|
|
529
602
|
abs_path = lean_project_path / rel_path
|
|
530
603
|
|
|
531
604
|
try:
|
|
@@ -534,14 +607,44 @@ def run_code(ctx: Context, code: str) -> List[str] | str:
|
|
|
534
607
|
except Exception as e:
|
|
535
608
|
return f"Error writing code snippet to file `{abs_path}`:\n{str(e)}"
|
|
536
609
|
|
|
537
|
-
client: LeanLSPClient =
|
|
538
|
-
diagnostics =
|
|
539
|
-
|
|
610
|
+
client: LeanLSPClient | None = lifespan_context.client
|
|
611
|
+
diagnostics: List[str] | str = []
|
|
612
|
+
close_error: str | None = None
|
|
613
|
+
remove_error: str | None = None
|
|
614
|
+
opened_file = False
|
|
540
615
|
|
|
541
616
|
try:
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
617
|
+
if client is None:
|
|
618
|
+
startup_client(ctx)
|
|
619
|
+
client = lifespan_context.client
|
|
620
|
+
if client is None:
|
|
621
|
+
return "Failed to initialize Lean client for run_code."
|
|
622
|
+
|
|
623
|
+
assert client is not None # startup_client guarantees an initialized client
|
|
624
|
+
client.open_file(rel_path)
|
|
625
|
+
opened_file = True
|
|
626
|
+
diagnostics = format_diagnostics(client.get_diagnostics(rel_path))
|
|
627
|
+
finally:
|
|
628
|
+
if opened_file:
|
|
629
|
+
try:
|
|
630
|
+
client.close_files([rel_path])
|
|
631
|
+
except Exception as exc: # pragma: no cover - close failures only logged
|
|
632
|
+
close_error = str(exc)
|
|
633
|
+
logger.warning("Failed to close `%s` after run_code: %s", rel_path, exc)
|
|
634
|
+
try:
|
|
635
|
+
os.remove(abs_path)
|
|
636
|
+
except FileNotFoundError:
|
|
637
|
+
pass
|
|
638
|
+
except Exception as e:
|
|
639
|
+
remove_error = str(e)
|
|
640
|
+
logger.warning(
|
|
641
|
+
"Failed to remove temporary Lean snippet `%s`: %s", abs_path, e
|
|
642
|
+
)
|
|
643
|
+
|
|
644
|
+
if remove_error:
|
|
645
|
+
return f"Error removing temporary file `{abs_path}`:\n{remove_error}"
|
|
646
|
+
if close_error:
|
|
647
|
+
return f"Error closing temporary Lean document `{rel_path}`:\n{close_error}"
|
|
545
648
|
|
|
546
649
|
return (
|
|
547
650
|
diagnostics
|
|
@@ -670,6 +773,79 @@ def loogle(ctx: Context, query: str, num_results: int = 8) -> List[dict] | str:
|
|
|
670
773
|
return f"loogle error:\n{str(e)}"
|
|
671
774
|
|
|
672
775
|
|
|
776
|
+
@mcp.tool("lean_leanfinder")
|
|
777
|
+
@rate_limited("leanfinder", max_requests=10, per_seconds=30)
|
|
778
|
+
def leanfinder(
|
|
779
|
+
ctx: Context, query: str, num_results: int = 5
|
|
780
|
+
) -> List[tuple] | str:
|
|
781
|
+
"""Search Mathlib theorems/definitions semantically by mathematical concept using Lean Finder.
|
|
782
|
+
|
|
783
|
+
Effective query types:
|
|
784
|
+
- Math + API: "setAverage Icc interval", "integral_pow symmetric bounds"
|
|
785
|
+
- Conceptual: "algebraic elements same minimal polynomial", "quadrature nodes"
|
|
786
|
+
- Structure: "Finset expect sum commute", "polynomial degree bounded eval"
|
|
787
|
+
- Natural: "average equals point values", "root implies equal polynomials"
|
|
788
|
+
|
|
789
|
+
Tips: Mix informal math terms with Lean identifiers. Multiple targeted queries beat one complex query.
|
|
790
|
+
|
|
791
|
+
Args:
|
|
792
|
+
query (str): Mathematical concepts combined with Lean terms
|
|
793
|
+
num_results (int, optional): Max results. Defaults to 5.
|
|
794
|
+
|
|
795
|
+
Returns:
|
|
796
|
+
List[tuple] | str: (lean_statement, english_description) pairs or error
|
|
797
|
+
"""
|
|
798
|
+
try:
|
|
799
|
+
headers = {"User-Agent": "lean-lsp-mcp/0.1", "Content-Type": "application/json"}
|
|
800
|
+
payload = json.dumps({"data": [query, num_results, "Normal"]}).encode("utf-8")
|
|
801
|
+
|
|
802
|
+
req = urllib.request.Request(
|
|
803
|
+
"https://delta-lab-ai-lean-finder.hf.space/gradio_api/call/retrieve",
|
|
804
|
+
data=payload,
|
|
805
|
+
headers=headers,
|
|
806
|
+
method="POST",
|
|
807
|
+
)
|
|
808
|
+
|
|
809
|
+
with urllib.request.urlopen(req, timeout=10) as response:
|
|
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
|
+
|
|
819
|
+
with urllib.request.urlopen(req, timeout=30) as response:
|
|
820
|
+
for line in response:
|
|
821
|
+
line = line.decode("utf-8").strip()
|
|
822
|
+
if line.startswith("data: "):
|
|
823
|
+
data = json.loads(line[6:])
|
|
824
|
+
if isinstance(data, list) and len(data) > 0:
|
|
825
|
+
html = data[0] if isinstance(data[0], str) else str(data)
|
|
826
|
+
|
|
827
|
+
# Parse HTML table rows
|
|
828
|
+
rows = re.findall(
|
|
829
|
+
r"<tr><td>\d+</td><td>(.*?)</td><td>(.*?)</td></tr>",
|
|
830
|
+
html, re.DOTALL
|
|
831
|
+
)
|
|
832
|
+
results = []
|
|
833
|
+
for formal_cell, informal_cell in rows:
|
|
834
|
+
formal = re.search(r"<code[^>]*>(.*?)</code>", formal_cell, re.DOTALL)
|
|
835
|
+
informal = re.search(r"<span[^>]*>(.*?)</span>", informal_cell, re.DOTALL)
|
|
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"
|
|
845
|
+
except Exception as e:
|
|
846
|
+
return f"Lean Finder Error:\n{str(e)}"
|
|
847
|
+
|
|
848
|
+
|
|
673
849
|
@mcp.tool("lean_state_search")
|
|
674
850
|
@rate_limited("lean_state_search", max_requests=3, per_seconds=30)
|
|
675
851
|
def state_search(
|
|
@@ -73,6 +73,27 @@ def format_goal(goal, default_msg):
|
|
|
73
73
|
return rendered.replace("```lean\n", "").replace("\n```", "") if rendered else None
|
|
74
74
|
|
|
75
75
|
|
|
76
|
+
def _utf16_index_to_py_index(text: str, utf16_index: int) -> int | None:
|
|
77
|
+
"""Convert an LSP UTF-16 column index into a Python string index."""
|
|
78
|
+
if utf16_index < 0:
|
|
79
|
+
return None
|
|
80
|
+
|
|
81
|
+
units = 0
|
|
82
|
+
for idx, ch in enumerate(text):
|
|
83
|
+
code_point = ord(ch)
|
|
84
|
+
next_units = units + (2 if code_point > 0xFFFF else 1)
|
|
85
|
+
|
|
86
|
+
if utf16_index < next_units:
|
|
87
|
+
return idx
|
|
88
|
+
if utf16_index == next_units:
|
|
89
|
+
return idx + 1
|
|
90
|
+
|
|
91
|
+
units = next_units
|
|
92
|
+
if units >= utf16_index:
|
|
93
|
+
return len(text)
|
|
94
|
+
return None
|
|
95
|
+
|
|
96
|
+
|
|
76
97
|
def extract_range(content: str, range: dict) -> str:
|
|
77
98
|
"""Extract the text from the content based on the range.
|
|
78
99
|
|
|
@@ -88,16 +109,36 @@ def extract_range(content: str, range: dict) -> str:
|
|
|
88
109
|
end_line = range["end"]["line"]
|
|
89
110
|
end_char = range["end"]["character"]
|
|
90
111
|
|
|
91
|
-
lines = content.splitlines()
|
|
92
|
-
if
|
|
112
|
+
lines = content.splitlines(keepends=True)
|
|
113
|
+
if not lines:
|
|
114
|
+
lines = [""]
|
|
115
|
+
|
|
116
|
+
line_offsets: List[int] = []
|
|
117
|
+
offset = 0
|
|
118
|
+
for line in lines:
|
|
119
|
+
line_offsets.append(offset)
|
|
120
|
+
offset += len(line)
|
|
121
|
+
total_length = len(content)
|
|
122
|
+
|
|
123
|
+
def position_to_offset(line: int, character: int) -> int | None:
|
|
124
|
+
if line == len(lines) and character == 0:
|
|
125
|
+
return total_length
|
|
126
|
+
if line < 0 or line >= len(lines):
|
|
127
|
+
return None
|
|
128
|
+
py_index = _utf16_index_to_py_index(lines[line], character)
|
|
129
|
+
if py_index is None:
|
|
130
|
+
return None
|
|
131
|
+
if py_index > len(lines[line]):
|
|
132
|
+
return None
|
|
133
|
+
return line_offsets[line] + py_index
|
|
134
|
+
|
|
135
|
+
start_offset = position_to_offset(start_line, start_char)
|
|
136
|
+
end_offset = position_to_offset(end_line, end_char)
|
|
137
|
+
|
|
138
|
+
if start_offset is None or end_offset is None or start_offset > end_offset:
|
|
93
139
|
return "Range out of bounds"
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
else:
|
|
97
|
-
selected_lines = lines[start_line : end_line + 1]
|
|
98
|
-
selected_lines[0] = selected_lines[0][start_char:]
|
|
99
|
-
selected_lines[-1] = selected_lines[-1][:end_char]
|
|
100
|
-
return "\n".join(selected_lines)
|
|
140
|
+
|
|
141
|
+
return content[start_offset:end_offset]
|
|
101
142
|
|
|
102
143
|
|
|
103
144
|
def find_start_position(content: str, query: str) -> dict | None:
|
|
@@ -136,13 +177,14 @@ def format_line(
|
|
|
136
177
|
"""
|
|
137
178
|
lines = file_content.splitlines()
|
|
138
179
|
line_number -= 1
|
|
139
|
-
if line_number <
|
|
180
|
+
if line_number < 0 or line_number >= len(lines):
|
|
140
181
|
return "Line number out of range"
|
|
141
182
|
line = lines[line_number]
|
|
142
183
|
if column is None:
|
|
143
184
|
return line
|
|
144
185
|
column -= 1
|
|
145
|
-
|
|
186
|
+
# Allow placing the cursor at end-of-line (column == len(line))
|
|
187
|
+
if column < 0 or column > len(line):
|
|
146
188
|
return "Invalid column number"
|
|
147
189
|
return f"{line[:column]}{cursor_tag}{line[column:]}"
|
|
148
190
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: lean-lsp-mcp
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.11.0
|
|
4
4
|
Summary: Lean Theorem Prover MCP
|
|
5
5
|
Author-email: Oliver Dressler <hey@oli.show>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -8,7 +8,7 @@ Project-URL: Repository, https://github.com/oOo0oOo/lean-lsp-mcp
|
|
|
8
8
|
Requires-Python: >=3.10
|
|
9
9
|
Description-Content-Type: text/markdown
|
|
10
10
|
License-File: LICENSE
|
|
11
|
-
Requires-Dist: leanclient==0.
|
|
11
|
+
Requires-Dist: leanclient==0.4.0
|
|
12
12
|
Requires-Dist: mcp[cli]==1.19.0
|
|
13
13
|
Requires-Dist: orjson>=3.11.1
|
|
14
14
|
Provides-Extra: lint
|
|
@@ -43,7 +43,7 @@ MCP server that allows agentic interaction with the [Lean theorem prover](https:
|
|
|
43
43
|
## Key Features
|
|
44
44
|
|
|
45
45
|
* **Rich Lean Interaction**: Access diagnostics, goal states, term information, hover documentation and more.
|
|
46
|
-
* **External Search Tools**: Use `
|
|
46
|
+
* **External Search Tools**: Use `LeanSearch`, `Loogle`, `Lean Finder`, `Lean Hammer` and `Lean State Search` to find relevant theorems and definitions.
|
|
47
47
|
* **Easy Setup**: Simple configuration for various clients, including VSCode, Cursor and Claude Code.
|
|
48
48
|
|
|
49
49
|
## Setup
|
|
@@ -122,11 +122,9 @@ Run one of these commands in the root directory of your Lean project (where `lak
|
|
|
122
122
|
# Local-scoped MCP server
|
|
123
123
|
claude mcp add lean-lsp uvx lean-lsp-mcp
|
|
124
124
|
|
|
125
|
-
# OR project-scoped MCP server
|
|
125
|
+
# OR project-scoped MCP server
|
|
126
|
+
# (creates or updates a .mcp.json file in the current directory)
|
|
126
127
|
claude mcp add lean-lsp -s project uvx lean-lsp-mcp
|
|
127
|
-
|
|
128
|
-
# OR If you run into issues with the project path (e.g. the language server directory cannot be found), you can also set it manually e.g.
|
|
129
|
-
claude mcp add lean-lsp uvx lean-lsp-mcp -e LEAN_PROJECT_PATH=$PWD
|
|
130
128
|
```
|
|
131
129
|
|
|
132
130
|
You can find more details about MCP server configuration for Claude Code [here](https://docs.anthropic.com/en/docs/agents-and-tools/claude-code/tutorials#configure-mcp-servers).
|
|
@@ -134,7 +132,7 @@ You can find more details about MCP server configuration for Claude Code [here](
|
|
|
134
132
|
|
|
135
133
|
#### Claude Skill: Lean4 Theorem Proving
|
|
136
134
|
|
|
137
|
-
If you are using [Claude Desktop](https://modelcontextprotocol.io/quickstart/user) or [Claude Code](https://claude.ai/code), you can also install the [Lean4 Theorem Proving Skill](https://github.com/cameronfreer/lean4-skills/tree/main/lean4-theorem-proving). This skill provides additional prompts and templates for interacting with Lean4 projects and includes a section on interacting with the `lean-lsp-mcp` server.
|
|
135
|
+
If you are using [Claude Desktop](https://modelcontextprotocol.io/quickstart/user) or [Claude Code](https://claude.ai/code), you can also install the [Lean4 Theorem Proving Skill](https://github.com/cameronfreer/lean4-skills/tree/main/plugins/lean4-theorem-proving). This skill provides additional prompts and templates for interacting with Lean4 projects and includes a section on interacting with the `lean-lsp-mcp` server.
|
|
138
136
|
|
|
139
137
|
### 4. Install ripgrep (optional but recommended)
|
|
140
138
|
|
|
@@ -284,7 +282,9 @@ This tool requires [ripgrep](https://github.com/BurntSushi/ripgrep?tab=readme-ov
|
|
|
284
282
|
|
|
285
283
|
### External Search Tools
|
|
286
284
|
|
|
287
|
-
Currently
|
|
285
|
+
Currently most external tools are separately **rate limited to 3 requests per 30 seconds**. Please don't ruin the fun for everyone by overusing these amazing free services!
|
|
286
|
+
|
|
287
|
+
Please cite the original authors of these tools if you use them!
|
|
288
288
|
|
|
289
289
|
#### lean_leansearch
|
|
290
290
|
|
|
@@ -337,6 +337,32 @@ Search for Lean definitions and theorems using [loogle.lean-lang.org](https://lo
|
|
|
337
337
|
```
|
|
338
338
|
</details>
|
|
339
339
|
|
|
340
|
+
#### lean_leanfinder
|
|
341
|
+
|
|
342
|
+
Semantic search for Mathlib theorems using [Lean Finder](https://huggingface.co/spaces/delta-lab-ai/Lean-Finder).
|
|
343
|
+
|
|
344
|
+
[Arxiv Paper](https://arxiv.org/abs/2510.15940)
|
|
345
|
+
|
|
346
|
+
- Supports informal descriptions, user questions, proof states, and statement fragments.
|
|
347
|
+
- Examples: `algebraic elements x,y over K with same minimal polynomial`, `Does y being a root of minpoly(x) imply minpoly(x)=minpoly(y)?`, `⊢ |re z| ≤ ‖z‖` + `transform to squared norm inequality`, `theorem restrict Ioi: restrict Ioi e = restrict Ici e`
|
|
348
|
+
|
|
349
|
+
<details>
|
|
350
|
+
<summary>Example output</summary>
|
|
351
|
+
|
|
352
|
+
Query: `Does y being a root of minpoly(x) imply minpoly(x)=minpoly(y)?`
|
|
353
|
+
|
|
354
|
+
```json
|
|
355
|
+
[
|
|
356
|
+
[
|
|
357
|
+
"/-- If `y : L` is a root of `minpoly K x`, then `minpoly K y = minpoly K x`. -/\ntheorem eq_of_root {x y : L} (hx : IsAlgebraic K x)\n (h_ev : Polynomial.aeval y (minpoly K x) = 0) : minpoly K y = minpoly K x :=\n ((eq_iff_aeval_minpoly_eq_zero hx.isIntegral).mpr h_ev).symm",
|
|
358
|
+
|
|
359
|
+
"Let $L/K$ be a field extension, and let $x, y \\in L$ be elements such that $y$ is a root of the minimal polynomial of $x$ over $K$. If $x$ is algebraic over $K$, then the minimal polynomial of $y$ over $K$ is equal to the minimal polynomial of $x$ over $K$, i.e., $\\text{minpoly}_K(y) = \\text{minpoly}_K(x)$. This means that if $y$ satisfies the polynomial equation defined by $x$, then $y$ shares the same minimal polynomial as $x$."
|
|
360
|
+
],
|
|
361
|
+
...
|
|
362
|
+
]
|
|
363
|
+
```
|
|
364
|
+
</details>
|
|
365
|
+
|
|
340
366
|
#### lean_state_search
|
|
341
367
|
|
|
342
368
|
Search for applicable theorems for the current proof goal using [premise-search.com](https://premise-search.com/).
|
|
@@ -109,3 +109,23 @@ async def test_search_tools(
|
|
|
109
109
|
if entry is None:
|
|
110
110
|
pytest.skip("lean_leansearch did not return JSON content")
|
|
111
111
|
assert {"module_name", "name", "type"} <= set(entry.keys())
|
|
112
|
+
|
|
113
|
+
# Test lean_finder with different query types
|
|
114
|
+
finder_informal = await client.call_tool(
|
|
115
|
+
"lean_leanfinder",
|
|
116
|
+
{
|
|
117
|
+
"query": "If two algebraic elements have the same minimal polynomial, are they related by a field isomorphism?",
|
|
118
|
+
"num_results": 3,
|
|
119
|
+
},
|
|
120
|
+
)
|
|
121
|
+
finder_results = _first_json_block(finder_informal)
|
|
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)
|
|
128
|
+
else:
|
|
129
|
+
finder_text = result_text(finder_informal)
|
|
130
|
+
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
|