lean-lsp-mcp 0.16.0__py3-none-any.whl → 0.16.2__py3-none-any.whl
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/models.py +77 -0
- lean_lsp_mcp/search_utils.py +121 -27
- lean_lsp_mcp/server.py +74 -65
- {lean_lsp_mcp-0.16.0.dist-info → lean_lsp_mcp-0.16.2.dist-info}/METADATA +3 -4
- {lean_lsp_mcp-0.16.0.dist-info → lean_lsp_mcp-0.16.2.dist-info}/RECORD +9 -9
- {lean_lsp_mcp-0.16.0.dist-info → lean_lsp_mcp-0.16.2.dist-info}/WHEEL +0 -0
- {lean_lsp_mcp-0.16.0.dist-info → lean_lsp_mcp-0.16.2.dist-info}/entry_points.txt +0 -0
- {lean_lsp_mcp-0.16.0.dist-info → lean_lsp_mcp-0.16.2.dist-info}/licenses/LICENSE +0 -0
- {lean_lsp_mcp-0.16.0.dist-info → lean_lsp_mcp-0.16.2.dist-info}/top_level.txt +0 -0
lean_lsp_mcp/models.py
CHANGED
|
@@ -118,3 +118,80 @@ class RunResult(BaseModel):
|
|
|
118
118
|
class DeclarationInfo(BaseModel):
|
|
119
119
|
file_path: str = Field(description="Path to declaration file")
|
|
120
120
|
content: str = Field(description="File content")
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
# Wrapper models for list-returning tools
|
|
124
|
+
# FastMCP flattens bare lists into separate TextContent blocks, causing serialization issues.
|
|
125
|
+
# Wrapping in a model ensures proper JSON serialization.
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
class DiagnosticsResult(BaseModel):
|
|
129
|
+
"""Wrapper for diagnostic messages list."""
|
|
130
|
+
|
|
131
|
+
items: List[DiagnosticMessage] = Field(
|
|
132
|
+
default_factory=list, description="List of diagnostic messages"
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
class CompletionsResult(BaseModel):
|
|
137
|
+
"""Wrapper for completions list."""
|
|
138
|
+
|
|
139
|
+
items: List[CompletionItem] = Field(
|
|
140
|
+
default_factory=list, description="List of completion items"
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
class MultiAttemptResult(BaseModel):
|
|
145
|
+
"""Wrapper for multi-attempt results list."""
|
|
146
|
+
|
|
147
|
+
items: List[AttemptResult] = Field(
|
|
148
|
+
default_factory=list, description="List of attempt results"
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
class LocalSearchResults(BaseModel):
|
|
153
|
+
"""Wrapper for local search results list."""
|
|
154
|
+
|
|
155
|
+
items: List[LocalSearchResult] = Field(
|
|
156
|
+
default_factory=list, description="List of local search results"
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
class LeanSearchResults(BaseModel):
|
|
161
|
+
"""Wrapper for LeanSearch results list."""
|
|
162
|
+
|
|
163
|
+
items: List[LeanSearchResult] = Field(
|
|
164
|
+
default_factory=list, description="List of LeanSearch results"
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
class LoogleResults(BaseModel):
|
|
169
|
+
"""Wrapper for Loogle results list."""
|
|
170
|
+
|
|
171
|
+
items: List[LoogleResult] = Field(
|
|
172
|
+
default_factory=list, description="List of Loogle results"
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
class LeanFinderResults(BaseModel):
|
|
177
|
+
"""Wrapper for Lean Finder results list."""
|
|
178
|
+
|
|
179
|
+
items: List[LeanFinderResult] = Field(
|
|
180
|
+
default_factory=list, description="List of Lean Finder results"
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
class StateSearchResults(BaseModel):
|
|
185
|
+
"""Wrapper for state search results list."""
|
|
186
|
+
|
|
187
|
+
items: List[StateSearchResult] = Field(
|
|
188
|
+
default_factory=list, description="List of state search results"
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
class PremiseResults(BaseModel):
|
|
193
|
+
"""Wrapper for premise results list."""
|
|
194
|
+
|
|
195
|
+
items: List[PremiseResult] = Field(
|
|
196
|
+
default_factory=list, description="List of premise results"
|
|
197
|
+
)
|
lean_lsp_mcp/search_utils.py
CHANGED
|
@@ -8,6 +8,7 @@ import platform
|
|
|
8
8
|
import re
|
|
9
9
|
import shutil
|
|
10
10
|
import subprocess
|
|
11
|
+
import threading
|
|
11
12
|
from orjson import loads as _json_loads
|
|
12
13
|
from pathlib import Path
|
|
13
14
|
|
|
@@ -27,6 +28,21 @@ _PLATFORM_INSTRUCTIONS: dict[str, Iterable[str]] = {
|
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
|
|
31
|
+
def _create_ripgrep_process(command: list[str], *, cwd: str) -> subprocess.Popen[str]:
|
|
32
|
+
"""Spawn ripgrep and return a process with line-streaming stdout.
|
|
33
|
+
|
|
34
|
+
Separated for test monkeypatching and to allow early termination once we
|
|
35
|
+
have enough matches.
|
|
36
|
+
"""
|
|
37
|
+
return subprocess.Popen(
|
|
38
|
+
command,
|
|
39
|
+
stdout=subprocess.PIPE,
|
|
40
|
+
stderr=subprocess.PIPE,
|
|
41
|
+
text=True,
|
|
42
|
+
cwd=cwd,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
|
|
30
46
|
def check_ripgrep_status() -> tuple[bool, str]:
|
|
31
47
|
"""Check whether ``rg`` is available on PATH and return status + message."""
|
|
32
48
|
|
|
@@ -84,38 +100,116 @@ def lean_local_search(
|
|
|
84
100
|
if lean_src := _get_lean_src_search_path():
|
|
85
101
|
command.append(lean_src)
|
|
86
102
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
matches = []
|
|
90
|
-
for line in result.stdout.splitlines():
|
|
91
|
-
if not line or (event := _json_loads(line)).get("type") != "match":
|
|
92
|
-
continue
|
|
103
|
+
process = _create_ripgrep_process(command, cwd=str(root))
|
|
93
104
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
abs_path = (
|
|
102
|
-
file_path if file_path.is_absolute() else (root / file_path).resolve()
|
|
103
|
-
)
|
|
105
|
+
matches: list[dict[str, str]] = []
|
|
106
|
+
stderr_text = ""
|
|
107
|
+
terminated_early = False
|
|
108
|
+
stderr_chunks: list[str] = []
|
|
109
|
+
stderr_chars = 0
|
|
110
|
+
stderr_truncated = False
|
|
111
|
+
max_stderr_chars = 100_000
|
|
104
112
|
|
|
113
|
+
def _drain_stderr(pipe) -> None:
|
|
114
|
+
nonlocal stderr_chars, stderr_truncated
|
|
105
115
|
try:
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
116
|
+
for err_line in pipe:
|
|
117
|
+
if stderr_chars < max_stderr_chars:
|
|
118
|
+
stderr_chunks.append(err_line)
|
|
119
|
+
stderr_chars += len(err_line)
|
|
120
|
+
else:
|
|
121
|
+
stderr_truncated = True
|
|
122
|
+
except Exception:
|
|
123
|
+
return
|
|
124
|
+
|
|
125
|
+
stderr_thread: threading.Thread | None = None
|
|
126
|
+
if process.stderr is not None:
|
|
127
|
+
stderr_thread = threading.Thread(
|
|
128
|
+
target=_drain_stderr,
|
|
129
|
+
args=(process.stderr,),
|
|
130
|
+
name="lean-local-search-rg-stderr",
|
|
131
|
+
daemon=True,
|
|
132
|
+
)
|
|
133
|
+
stderr_thread.start()
|
|
111
134
|
|
|
112
|
-
|
|
113
|
-
|
|
135
|
+
try:
|
|
136
|
+
stdout = process.stdout
|
|
137
|
+
if stdout is None:
|
|
138
|
+
raise RuntimeError("ripgrep did not provide stdout pipe")
|
|
139
|
+
|
|
140
|
+
for line in stdout:
|
|
141
|
+
if not line or (event := _json_loads(line)).get("type") != "match":
|
|
142
|
+
continue
|
|
143
|
+
|
|
144
|
+
data = event["data"]
|
|
145
|
+
parts = data["lines"]["text"].lstrip().split(maxsplit=2)
|
|
146
|
+
if len(parts) < 2:
|
|
147
|
+
continue
|
|
148
|
+
|
|
149
|
+
decl_kind, decl_name = parts[0], parts[1].rstrip(":")
|
|
150
|
+
file_path = Path(data["path"]["text"])
|
|
151
|
+
abs_path = (
|
|
152
|
+
file_path if file_path.is_absolute() else (root / file_path).resolve()
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
try:
|
|
156
|
+
display_path = str(abs_path.relative_to(root))
|
|
157
|
+
except ValueError:
|
|
158
|
+
display_path = str(file_path)
|
|
159
|
+
|
|
160
|
+
matches.append({"name": decl_name, "kind": decl_kind, "file": display_path})
|
|
161
|
+
|
|
162
|
+
if len(matches) >= limit:
|
|
163
|
+
terminated_early = True
|
|
164
|
+
try:
|
|
165
|
+
process.terminate()
|
|
166
|
+
except Exception:
|
|
167
|
+
pass
|
|
168
|
+
break
|
|
114
169
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
170
|
+
try:
|
|
171
|
+
if terminated_early:
|
|
172
|
+
process.wait(timeout=5)
|
|
173
|
+
else:
|
|
174
|
+
process.wait()
|
|
175
|
+
except subprocess.TimeoutExpired:
|
|
176
|
+
process.kill()
|
|
177
|
+
process.wait()
|
|
178
|
+
finally:
|
|
179
|
+
if process.returncode is None:
|
|
180
|
+
try:
|
|
181
|
+
process.terminate()
|
|
182
|
+
except Exception:
|
|
183
|
+
pass
|
|
184
|
+
try:
|
|
185
|
+
process.wait(timeout=5)
|
|
186
|
+
except Exception:
|
|
187
|
+
try:
|
|
188
|
+
process.kill()
|
|
189
|
+
except Exception:
|
|
190
|
+
pass
|
|
191
|
+
try:
|
|
192
|
+
process.wait(timeout=5)
|
|
193
|
+
except Exception:
|
|
194
|
+
pass
|
|
195
|
+
if stderr_thread is not None:
|
|
196
|
+
stderr_thread.join(timeout=1)
|
|
197
|
+
if process.stdout is not None:
|
|
198
|
+
process.stdout.close()
|
|
199
|
+
if process.stderr is not None:
|
|
200
|
+
process.stderr.close()
|
|
201
|
+
|
|
202
|
+
if stderr_chunks:
|
|
203
|
+
stderr_text = "".join(stderr_chunks)
|
|
204
|
+
if stderr_truncated:
|
|
205
|
+
stderr_text += "\n[stderr truncated]"
|
|
206
|
+
|
|
207
|
+
returncode = process.returncode if process.returncode is not None else 0
|
|
208
|
+
|
|
209
|
+
if returncode not in (0, 1) and not matches:
|
|
210
|
+
error_msg = f"ripgrep exited with code {returncode}"
|
|
211
|
+
if stderr_text:
|
|
212
|
+
error_msg += f"\n{stderr_text}"
|
|
119
213
|
raise RuntimeError(error_msg)
|
|
120
214
|
|
|
121
215
|
return matches
|
lean_lsp_mcp/server.py
CHANGED
|
@@ -9,11 +9,10 @@ from dataclasses import dataclass
|
|
|
9
9
|
import urllib
|
|
10
10
|
import orjson
|
|
11
11
|
import functools
|
|
12
|
-
import subprocess
|
|
13
12
|
import uuid
|
|
14
13
|
from pathlib import Path
|
|
15
14
|
|
|
16
|
-
from pydantic import
|
|
15
|
+
from pydantic import Field
|
|
17
16
|
from mcp.server.fastmcp import Context, FastMCP
|
|
18
17
|
from mcp.server.fastmcp.utilities.logging import get_logger, configure_logging
|
|
19
18
|
from mcp.server.auth.settings import AuthSettings
|
|
@@ -47,6 +46,16 @@ from lean_lsp_mcp.models import (
|
|
|
47
46
|
BuildResult,
|
|
48
47
|
RunResult,
|
|
49
48
|
DeclarationInfo,
|
|
49
|
+
# Wrapper models for list-returning tools
|
|
50
|
+
DiagnosticsResult,
|
|
51
|
+
CompletionsResult,
|
|
52
|
+
MultiAttemptResult,
|
|
53
|
+
LocalSearchResults,
|
|
54
|
+
LeanSearchResults,
|
|
55
|
+
LoogleResults,
|
|
56
|
+
LeanFinderResults,
|
|
57
|
+
StateSearchResults,
|
|
58
|
+
PremiseResults,
|
|
50
59
|
)
|
|
51
60
|
from lean_lsp_mcp.utils import (
|
|
52
61
|
COMPLETION_KIND,
|
|
@@ -68,13 +77,6 @@ class LeanToolError(Exception):
|
|
|
68
77
|
pass
|
|
69
78
|
|
|
70
79
|
|
|
71
|
-
def _to_json_array(items: List[BaseModel]) -> str:
|
|
72
|
-
"""Serialize list of models as JSON array (avoids FastMCP list flattening)."""
|
|
73
|
-
return orjson.dumps(
|
|
74
|
-
[item.model_dump() for item in items], option=orjson.OPT_INDENT_2
|
|
75
|
-
).decode()
|
|
76
|
-
|
|
77
|
-
|
|
78
80
|
_LOG_LEVEL = os.environ.get("LEAN_LOG_LEVEL", "INFO")
|
|
79
81
|
configure_logging("CRITICAL" if _LOG_LEVEL == "NONE" else _LOG_LEVEL)
|
|
80
82
|
logger = get_logger(__name__)
|
|
@@ -196,7 +198,7 @@ def rate_limited(category: str, max_requests: int, per_seconds: int):
|
|
|
196
198
|
annotations=ToolAnnotations(
|
|
197
199
|
title="Build Project",
|
|
198
200
|
readOnlyHint=False,
|
|
199
|
-
destructiveHint=
|
|
201
|
+
destructiveHint=True,
|
|
200
202
|
idempotentHint=True,
|
|
201
203
|
openWorldHint=False,
|
|
202
204
|
),
|
|
@@ -207,6 +209,9 @@ async def lsp_build(
|
|
|
207
209
|
Optional[str], Field(description="Path to Lean project")
|
|
208
210
|
] = None,
|
|
209
211
|
clean: Annotated[bool, Field(description="Run lake clean first (slow)")] = False,
|
|
212
|
+
output_lines: Annotated[
|
|
213
|
+
int, Field(description="Return last N lines of build log (0=none)")
|
|
214
|
+
] = 20,
|
|
210
215
|
) -> BuildResult:
|
|
211
216
|
"""Build the Lean project and restart LSP. Use only if needed (e.g. new imports)."""
|
|
212
217
|
if not lean_project_path:
|
|
@@ -220,7 +225,7 @@ async def lsp_build(
|
|
|
220
225
|
"Lean project path not known yet. Provide `lean_project_path` explicitly or call another tool first."
|
|
221
226
|
)
|
|
222
227
|
|
|
223
|
-
|
|
228
|
+
log_lines: List[str] = []
|
|
224
229
|
errors: List[str] = []
|
|
225
230
|
|
|
226
231
|
try:
|
|
@@ -230,13 +235,21 @@ async def lsp_build(
|
|
|
230
235
|
client.close()
|
|
231
236
|
|
|
232
237
|
if clean:
|
|
233
|
-
|
|
234
|
-
|
|
238
|
+
await ctx.report_progress(
|
|
239
|
+
progress=1, total=16, message="Running `lake clean`"
|
|
240
|
+
)
|
|
241
|
+
clean_proc = await asyncio.create_subprocess_exec(
|
|
242
|
+
"lake", "clean", cwd=lean_project_path_obj
|
|
243
|
+
)
|
|
244
|
+
await clean_proc.wait()
|
|
235
245
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
["lake", "exe", "cache", "get"], cwd=lean_project_path_obj, check=False
|
|
246
|
+
await ctx.report_progress(
|
|
247
|
+
progress=2, total=16, message="Running `lake exe cache get`"
|
|
239
248
|
)
|
|
249
|
+
cache_proc = await asyncio.create_subprocess_exec(
|
|
250
|
+
"lake", "exe", "cache", "get", cwd=lean_project_path_obj
|
|
251
|
+
)
|
|
252
|
+
await cache_proc.wait()
|
|
240
253
|
|
|
241
254
|
# Run build with progress reporting
|
|
242
255
|
process = await asyncio.create_subprocess_exec(
|
|
@@ -248,32 +261,24 @@ async def lsp_build(
|
|
|
248
261
|
stderr=asyncio.subprocess.STDOUT,
|
|
249
262
|
)
|
|
250
263
|
|
|
251
|
-
while
|
|
252
|
-
line = await process.stdout.readline()
|
|
253
|
-
if not line:
|
|
254
|
-
break
|
|
255
|
-
|
|
264
|
+
while line := await process.stdout.readline():
|
|
256
265
|
line_str = line.decode("utf-8", errors="replace").rstrip()
|
|
257
|
-
output_lines.append(line_str)
|
|
258
266
|
|
|
259
|
-
|
|
267
|
+
if line_str.startswith("trace:") or "LEAN_PATH=" in line_str:
|
|
268
|
+
continue
|
|
269
|
+
|
|
270
|
+
log_lines.append(line_str)
|
|
260
271
|
if "error" in line_str.lower():
|
|
261
272
|
errors.append(line_str)
|
|
262
273
|
|
|
263
|
-
# Parse progress:
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
total_jobs = int(match.group(2))
|
|
268
|
-
|
|
269
|
-
# Extract what's being built
|
|
270
|
-
desc_match = re.search(
|
|
271
|
-
r"\[\d+/\d+\]\s+(.+?)(?:\s+\(\d+\.?\d*[ms]+\))?$", line_str
|
|
272
|
-
)
|
|
273
|
-
description = desc_match.group(1) if desc_match else "Building"
|
|
274
|
-
|
|
274
|
+
# Parse progress: "[2/8] Building Foo (1.2s)" -> (2, 8, "Building Foo")
|
|
275
|
+
if m := re.search(
|
|
276
|
+
r"\[(\d+)/(\d+)\]\s*(.+?)(?:\s+\(\d+\.?\d*[ms]+\))?$", line_str
|
|
277
|
+
):
|
|
275
278
|
await ctx.report_progress(
|
|
276
|
-
progress=
|
|
279
|
+
progress=int(m.group(1)),
|
|
280
|
+
total=int(m.group(2)),
|
|
281
|
+
message=m.group(3) or "Building",
|
|
277
282
|
)
|
|
278
283
|
|
|
279
284
|
await process.wait()
|
|
@@ -281,7 +286,7 @@ async def lsp_build(
|
|
|
281
286
|
if process.returncode != 0:
|
|
282
287
|
return BuildResult(
|
|
283
288
|
success=False,
|
|
284
|
-
output="\n".join(output_lines),
|
|
289
|
+
output="\n".join(log_lines[-output_lines:]) if output_lines else "",
|
|
285
290
|
errors=errors
|
|
286
291
|
or [f"Build failed with return code {process.returncode}"],
|
|
287
292
|
)
|
|
@@ -295,12 +300,16 @@ async def lsp_build(
|
|
|
295
300
|
logger.info("Built project and re-started LSP client")
|
|
296
301
|
ctx.request_context.lifespan_context.client = client
|
|
297
302
|
|
|
298
|
-
return BuildResult(
|
|
303
|
+
return BuildResult(
|
|
304
|
+
success=True,
|
|
305
|
+
output="\n".join(log_lines[-output_lines:]) if output_lines else "",
|
|
306
|
+
errors=[],
|
|
307
|
+
)
|
|
299
308
|
|
|
300
309
|
except Exception as e:
|
|
301
310
|
return BuildResult(
|
|
302
311
|
success=False,
|
|
303
|
-
output="\n".join(output_lines),
|
|
312
|
+
output="\n".join(log_lines[-output_lines:]) if output_lines else "",
|
|
304
313
|
errors=[str(e)],
|
|
305
314
|
)
|
|
306
315
|
|
|
@@ -408,7 +417,7 @@ def diagnostic_messages(
|
|
|
408
417
|
declaration_name: Annotated[
|
|
409
418
|
Optional[str], Field(description="Filter to declaration (slow)")
|
|
410
419
|
] = None,
|
|
411
|
-
) ->
|
|
420
|
+
) -> DiagnosticsResult:
|
|
412
421
|
"""Get compiler diagnostics (errors, warnings, infos) for a Lean file."""
|
|
413
422
|
rel_path = setup_client_for_file(ctx, file_path)
|
|
414
423
|
if not rel_path:
|
|
@@ -437,7 +446,7 @@ def diagnostic_messages(
|
|
|
437
446
|
inactivity_timeout=15.0,
|
|
438
447
|
)
|
|
439
448
|
|
|
440
|
-
return
|
|
449
|
+
return DiagnosticsResult(items=_to_diagnostic_messages(diagnostics))
|
|
441
450
|
|
|
442
451
|
|
|
443
452
|
@mcp.tool(
|
|
@@ -604,7 +613,7 @@ def completions(
|
|
|
604
613
|
line: Annotated[int, Field(description="Line number (1-indexed)", ge=1)],
|
|
605
614
|
column: Annotated[int, Field(description="Column number (1-indexed)", ge=1)],
|
|
606
615
|
max_completions: Annotated[int, Field(description="Max completions", ge=1)] = 32,
|
|
607
|
-
) ->
|
|
616
|
+
) -> CompletionsResult:
|
|
608
617
|
"""Get IDE autocompletions. Use on INCOMPLETE code (after `.` or partial name)."""
|
|
609
618
|
rel_path = setup_client_for_file(ctx, file_path)
|
|
610
619
|
if not rel_path:
|
|
@@ -633,7 +642,7 @@ def completions(
|
|
|
633
642
|
)
|
|
634
643
|
|
|
635
644
|
if not items:
|
|
636
|
-
return
|
|
645
|
+
return CompletionsResult(items=[])
|
|
637
646
|
|
|
638
647
|
# Find the sort term: The last word/identifier before the cursor
|
|
639
648
|
lines = content.splitlines()
|
|
@@ -660,7 +669,7 @@ def completions(
|
|
|
660
669
|
items.sort(key=lambda x: x.label.lower())
|
|
661
670
|
|
|
662
671
|
# Truncate if too many results
|
|
663
|
-
return
|
|
672
|
+
return CompletionsResult(items=items[:max_completions])
|
|
664
673
|
|
|
665
674
|
|
|
666
675
|
@mcp.tool(
|
|
@@ -735,7 +744,7 @@ def multi_attempt(
|
|
|
735
744
|
snippets: Annotated[
|
|
736
745
|
List[str], Field(description="Tactics to try (3+ recommended)")
|
|
737
746
|
],
|
|
738
|
-
) ->
|
|
747
|
+
) -> MultiAttemptResult:
|
|
739
748
|
"""Try multiple tactics without modifying file. Returns goal state for each."""
|
|
740
749
|
rel_path = setup_client_for_file(ctx, file_path)
|
|
741
750
|
if not rel_path:
|
|
@@ -747,8 +756,6 @@ def multi_attempt(
|
|
|
747
756
|
client.open_file(rel_path)
|
|
748
757
|
|
|
749
758
|
try:
|
|
750
|
-
client.open_file(rel_path)
|
|
751
|
-
|
|
752
759
|
results: List[AttemptResult] = []
|
|
753
760
|
# Avoid mutating caller-provided snippets; normalize locally per attempt
|
|
754
761
|
for snippet in snippets:
|
|
@@ -775,7 +782,7 @@ def multi_attempt(
|
|
|
775
782
|
)
|
|
776
783
|
)
|
|
777
784
|
|
|
778
|
-
return
|
|
785
|
+
return MultiAttemptResult(items=results)
|
|
779
786
|
finally:
|
|
780
787
|
try:
|
|
781
788
|
client.close_files([rel_path])
|
|
@@ -872,7 +879,7 @@ def local_search(
|
|
|
872
879
|
project_root: Annotated[
|
|
873
880
|
Optional[str], Field(description="Project root (inferred if omitted)")
|
|
874
881
|
] = None,
|
|
875
|
-
) ->
|
|
882
|
+
) -> LocalSearchResults:
|
|
876
883
|
"""Fast local search to verify declarations exist. Use BEFORE trying a lemma name."""
|
|
877
884
|
if not _RG_AVAILABLE:
|
|
878
885
|
raise LocalSearchError(_RG_MESSAGE)
|
|
@@ -904,7 +911,7 @@ def local_search(
|
|
|
904
911
|
LocalSearchResult(name=r["name"], kind=r["kind"], file=r["file"])
|
|
905
912
|
for r in raw_results
|
|
906
913
|
]
|
|
907
|
-
return
|
|
914
|
+
return LocalSearchResults(items=results)
|
|
908
915
|
except RuntimeError as exc:
|
|
909
916
|
raise LocalSearchError(f"Search failed: {exc}")
|
|
910
917
|
|
|
@@ -923,7 +930,7 @@ def leansearch(
|
|
|
923
930
|
ctx: Context,
|
|
924
931
|
query: Annotated[str, Field(description="Natural language or Lean term query")],
|
|
925
932
|
num_results: Annotated[int, Field(description="Max results", ge=1)] = 5,
|
|
926
|
-
) ->
|
|
933
|
+
) -> LeanSearchResults:
|
|
927
934
|
"""Search Mathlib via leansearch.net using natural language.
|
|
928
935
|
|
|
929
936
|
Examples: "sum of two even numbers is even", "Cauchy-Schwarz inequality",
|
|
@@ -943,7 +950,7 @@ def leansearch(
|
|
|
943
950
|
results = orjson.loads(response.read())
|
|
944
951
|
|
|
945
952
|
if not results or not results[0]:
|
|
946
|
-
return
|
|
953
|
+
return LeanSearchResults(items=[])
|
|
947
954
|
|
|
948
955
|
raw_results = [r["result"] for r in results[0][:num_results]]
|
|
949
956
|
items = [
|
|
@@ -955,7 +962,7 @@ def leansearch(
|
|
|
955
962
|
)
|
|
956
963
|
for r in raw_results
|
|
957
964
|
]
|
|
958
|
-
return
|
|
965
|
+
return LeanSearchResults(items=items)
|
|
959
966
|
|
|
960
967
|
|
|
961
968
|
@mcp.tool(
|
|
@@ -973,7 +980,7 @@ async def loogle(
|
|
|
973
980
|
str, Field(description="Type pattern, constant, or name substring")
|
|
974
981
|
],
|
|
975
982
|
num_results: Annotated[int, Field(description="Max results", ge=1)] = 8,
|
|
976
|
-
) ->
|
|
983
|
+
) -> LoogleResults:
|
|
977
984
|
"""Search Mathlib by type signature via loogle.lean-lang.org.
|
|
978
985
|
|
|
979
986
|
Examples: `Real.sin`, `"comm"`, `(?a → ?b) → List ?a → List ?b`,
|
|
@@ -986,7 +993,7 @@ async def loogle(
|
|
|
986
993
|
try:
|
|
987
994
|
results = await app_ctx.loogle_manager.query(query, num_results)
|
|
988
995
|
if not results:
|
|
989
|
-
return
|
|
996
|
+
return LoogleResults(items=[])
|
|
990
997
|
items = [
|
|
991
998
|
LoogleResult(
|
|
992
999
|
name=r.get("name", ""),
|
|
@@ -995,7 +1002,7 @@ async def loogle(
|
|
|
995
1002
|
)
|
|
996
1003
|
for r in results
|
|
997
1004
|
]
|
|
998
|
-
return
|
|
1005
|
+
return LoogleResults(items=items)
|
|
999
1006
|
except Exception as e:
|
|
1000
1007
|
logger.warning(f"Local loogle failed: {e}, falling back to remote")
|
|
1001
1008
|
|
|
@@ -1004,13 +1011,15 @@ async def loogle(
|
|
|
1004
1011
|
now = int(time.time())
|
|
1005
1012
|
rate_limit[:] = [t for t in rate_limit if now - t < 30]
|
|
1006
1013
|
if len(rate_limit) >= 3:
|
|
1007
|
-
|
|
1014
|
+
raise LeanToolError(
|
|
1015
|
+
"Rate limit exceeded: 3 requests per 30s. Use --loogle-local to avoid limits."
|
|
1016
|
+
)
|
|
1008
1017
|
rate_limit.append(now)
|
|
1009
1018
|
|
|
1010
1019
|
result = loogle_remote(query, num_results)
|
|
1011
1020
|
if isinstance(result, str):
|
|
1012
|
-
|
|
1013
|
-
return
|
|
1021
|
+
raise LeanToolError(result) # Error message from remote
|
|
1022
|
+
return LoogleResults(items=result)
|
|
1014
1023
|
|
|
1015
1024
|
|
|
1016
1025
|
@mcp.tool(
|
|
@@ -1027,7 +1036,7 @@ def leanfinder(
|
|
|
1027
1036
|
ctx: Context,
|
|
1028
1037
|
query: Annotated[str, Field(description="Mathematical concept or proof state")],
|
|
1029
1038
|
num_results: Annotated[int, Field(description="Max results", ge=1)] = 5,
|
|
1030
|
-
) ->
|
|
1039
|
+
) -> LeanFinderResults:
|
|
1031
1040
|
"""Semantic search by mathematical meaning via Lean Finder.
|
|
1032
1041
|
|
|
1033
1042
|
Examples: "commutativity of addition on natural numbers",
|
|
@@ -1059,7 +1068,7 @@ def leanfinder(
|
|
|
1059
1068
|
)
|
|
1060
1069
|
)
|
|
1061
1070
|
|
|
1062
|
-
return
|
|
1071
|
+
return LeanFinderResults(items=results)
|
|
1063
1072
|
|
|
1064
1073
|
|
|
1065
1074
|
@mcp.tool(
|
|
@@ -1078,7 +1087,7 @@ def state_search(
|
|
|
1078
1087
|
line: Annotated[int, Field(description="Line number (1-indexed)", ge=1)],
|
|
1079
1088
|
column: Annotated[int, Field(description="Column number (1-indexed)", ge=1)],
|
|
1080
1089
|
num_results: Annotated[int, Field(description="Max results", ge=1)] = 5,
|
|
1081
|
-
) ->
|
|
1090
|
+
) -> StateSearchResults:
|
|
1082
1091
|
"""Find lemmas to close the goal at a position. Searches premise-search.com."""
|
|
1083
1092
|
rel_path = setup_client_for_file(ctx, file_path)
|
|
1084
1093
|
if not rel_path:
|
|
@@ -1108,7 +1117,7 @@ def state_search(
|
|
|
1108
1117
|
results = orjson.loads(response.read())
|
|
1109
1118
|
|
|
1110
1119
|
items = [StateSearchResult(name=r["name"]) for r in results]
|
|
1111
|
-
return
|
|
1120
|
+
return StateSearchResults(items=items)
|
|
1112
1121
|
|
|
1113
1122
|
|
|
1114
1123
|
@mcp.tool(
|
|
@@ -1127,7 +1136,7 @@ def hammer_premise(
|
|
|
1127
1136
|
line: Annotated[int, Field(description="Line number (1-indexed)", ge=1)],
|
|
1128
1137
|
column: Annotated[int, Field(description="Column number (1-indexed)", ge=1)],
|
|
1129
1138
|
num_results: Annotated[int, Field(description="Max results", ge=1)] = 32,
|
|
1130
|
-
) ->
|
|
1139
|
+
) -> PremiseResults:
|
|
1131
1140
|
"""Get premise suggestions for automation tactics at a goal position.
|
|
1132
1141
|
|
|
1133
1142
|
Returns lemma names to try with `simp only [...]`, `aesop`, or as hints.
|
|
@@ -1168,7 +1177,7 @@ def hammer_premise(
|
|
|
1168
1177
|
results = orjson.loads(response.read())
|
|
1169
1178
|
|
|
1170
1179
|
items = [PremiseResult(name=r["name"]) for r in results]
|
|
1171
|
-
return
|
|
1180
|
+
return PremiseResults(items=items)
|
|
1172
1181
|
|
|
1173
1182
|
|
|
1174
1183
|
if __name__ == "__main__":
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: lean-lsp-mcp
|
|
3
|
-
Version: 0.16.
|
|
3
|
+
Version: 0.16.2
|
|
4
4
|
Summary: Lean Theorem Prover MCP
|
|
5
5
|
Author-email: Oliver Dressler <hey@oli.show>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -8,9 +8,8 @@ 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.6.
|
|
12
|
-
Requires-Dist: mcp[cli]==1.
|
|
13
|
-
Requires-Dist: mcp[cli]>=1.22.0
|
|
11
|
+
Requires-Dist: leanclient==0.6.2
|
|
12
|
+
Requires-Dist: mcp[cli]==1.24.0
|
|
14
13
|
Requires-Dist: orjson>=3.11.1
|
|
15
14
|
Provides-Extra: lint
|
|
16
15
|
Requires-Dist: ruff>=0.2.0; extra == "lint"
|
|
@@ -4,14 +4,14 @@ lean_lsp_mcp/client_utils.py,sha256=HgPuB35rMitn2Xm8SCAErsFLq15trB6VMz3FDFgmPd8,
|
|
|
4
4
|
lean_lsp_mcp/file_utils.py,sha256=kCTYQSfmV-R2cm_NCi_L8W5Dcsm0_rTOPpTtpyAin78,1365
|
|
5
5
|
lean_lsp_mcp/instructions.py,sha256=S1y834V8v-SFSYJlxxy6Dj-Z0szMyEBT5SkEyM6Npr8,1756
|
|
6
6
|
lean_lsp_mcp/loogle.py,sha256=ChybtPM8jOxP8s28358yNqcLiYvGlQqkAEFFLzR87Zw,11971
|
|
7
|
-
lean_lsp_mcp/models.py,sha256=
|
|
7
|
+
lean_lsp_mcp/models.py,sha256=gDfyAX09YzKtjpKzuo6JtA2mNDc9pRWJ7iT44nHwi94,6326
|
|
8
8
|
lean_lsp_mcp/outline_utils.py,sha256=-eoZNbx2eaKaYmuyFJnwUMWP8I9YXNWusue_2OYpDBM,10981
|
|
9
|
-
lean_lsp_mcp/search_utils.py,sha256=
|
|
10
|
-
lean_lsp_mcp/server.py,sha256=
|
|
9
|
+
lean_lsp_mcp/search_utils.py,sha256=MLqKGe4bhEvyfFLIBCmiDxkbcH4O5J3vl9mWnRSb_v0,6801
|
|
10
|
+
lean_lsp_mcp/server.py,sha256=AvjzoS8lwomUtIP2wrBln4z28-cXzCf1hNgXd9O1w4E,39749
|
|
11
11
|
lean_lsp_mcp/utils.py,sha256=355kzyB3dkwU7_4Mfcg--JXEorFaE2gtqs6-HbH5rRE,11722
|
|
12
|
-
lean_lsp_mcp-0.16.
|
|
13
|
-
lean_lsp_mcp-0.16.
|
|
14
|
-
lean_lsp_mcp-0.16.
|
|
15
|
-
lean_lsp_mcp-0.16.
|
|
16
|
-
lean_lsp_mcp-0.16.
|
|
17
|
-
lean_lsp_mcp-0.16.
|
|
12
|
+
lean_lsp_mcp-0.16.2.dist-info/licenses/LICENSE,sha256=CQlxnf0tQyoVrBE93JYvAUYxv6Z5Yg6sX0pwogOkFvo,1071
|
|
13
|
+
lean_lsp_mcp-0.16.2.dist-info/METADATA,sha256=Wrhb1l5m-Up77bQyegUdesd0k1ryWhe0C6dIHIWJ5mM,20787
|
|
14
|
+
lean_lsp_mcp-0.16.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
15
|
+
lean_lsp_mcp-0.16.2.dist-info/entry_points.txt,sha256=nQbvwctWkWD7I-2f4VrdVQBZYGUw8CnUnFC6QjXxOSE,51
|
|
16
|
+
lean_lsp_mcp-0.16.2.dist-info/top_level.txt,sha256=LGEK0lgMSNPIQ6mG8EO-adaZEGPi_0daDs004epOTF0,13
|
|
17
|
+
lean_lsp_mcp-0.16.2.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|