lean-lsp-mcp 0.16.2__py3-none-any.whl → 0.17.1__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/instructions.py +3 -0
- lean_lsp_mcp/loogle.py +99 -18
- lean_lsp_mcp/models.py +11 -3
- lean_lsp_mcp/server.py +62 -49
- lean_lsp_mcp/utils.py +34 -6
- {lean_lsp_mcp-0.16.2.dist-info → lean_lsp_mcp-0.17.1.dist-info}/METADATA +3 -3
- lean_lsp_mcp-0.17.1.dist-info/RECORD +17 -0
- lean_lsp_mcp-0.16.2.dist-info/RECORD +0 -17
- {lean_lsp_mcp-0.16.2.dist-info → lean_lsp_mcp-0.17.1.dist-info}/WHEEL +0 -0
- {lean_lsp_mcp-0.16.2.dist-info → lean_lsp_mcp-0.17.1.dist-info}/entry_points.txt +0 -0
- {lean_lsp_mcp-0.16.2.dist-info → lean_lsp_mcp-0.17.1.dist-info}/licenses/LICENSE +0 -0
- {lean_lsp_mcp-0.16.2.dist-info → lean_lsp_mcp-0.17.1.dist-info}/top_level.txt +0 -0
lean_lsp_mcp/instructions.py
CHANGED
|
@@ -33,4 +33,7 @@ After finding a name: lean_local_search to verify, lean_hover_info for signature
|
|
|
33
33
|
|
|
34
34
|
## Return Formats
|
|
35
35
|
List tools return JSON arrays. Empty = `[]`.
|
|
36
|
+
|
|
37
|
+
## Error Handling
|
|
38
|
+
Check `isError` in responses: `true` means failure (timeout/LSP error), while `[]` with `isError: false` means no results found.
|
|
36
39
|
"""
|
lean_lsp_mcp/loogle.py
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import asyncio
|
|
6
|
+
import hashlib
|
|
6
7
|
import json
|
|
7
8
|
import logging
|
|
8
9
|
import os
|
|
@@ -34,7 +35,7 @@ def loogle_remote(query: str, num_results: int) -> list[LoogleResult] | str:
|
|
|
34
35
|
f"https://loogle.lean-lang.org/json?q={urllib.parse.quote(query)}",
|
|
35
36
|
headers={"User-Agent": "lean-lsp-mcp/0.1"},
|
|
36
37
|
)
|
|
37
|
-
with urllib.request.urlopen(req, timeout=
|
|
38
|
+
with urllib.request.urlopen(req, timeout=10) as response:
|
|
38
39
|
results = orjson.loads(response.read())
|
|
39
40
|
if "hits" not in results:
|
|
40
41
|
return "No results found."
|
|
@@ -52,18 +53,25 @@ def loogle_remote(query: str, num_results: int) -> list[LoogleResult] | str:
|
|
|
52
53
|
|
|
53
54
|
|
|
54
55
|
class LoogleManager:
|
|
55
|
-
"""Manages local loogle installation and async subprocess.
|
|
56
|
+
"""Manages local loogle installation and async subprocess.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
cache_dir: Directory for loogle repo and indices (default: ~/.cache/lean-lsp-mcp/loogle)
|
|
60
|
+
project_path: Optional Lean project path to index its .lake/packages dependencies
|
|
61
|
+
"""
|
|
56
62
|
|
|
57
63
|
REPO_URL = "https://github.com/nomeata/loogle.git"
|
|
58
64
|
READY_SIGNAL = "Loogle is ready."
|
|
59
65
|
|
|
60
|
-
def __init__(self, cache_dir: Path | None = None):
|
|
66
|
+
def __init__(self, cache_dir: Path | None = None, project_path: Path | None = None):
|
|
61
67
|
self.cache_dir = cache_dir or get_cache_dir()
|
|
62
68
|
self.repo_dir = self.cache_dir / "repo"
|
|
63
69
|
self.index_dir = self.cache_dir / "index"
|
|
70
|
+
self.project_path = project_path
|
|
64
71
|
self.process: asyncio.subprocess.Process | None = None
|
|
65
72
|
self._ready = False
|
|
66
73
|
self._lock = asyncio.Lock()
|
|
74
|
+
self._extra_paths: list[Path] = []
|
|
67
75
|
|
|
68
76
|
@property
|
|
69
77
|
def binary_path(self) -> Path:
|
|
@@ -181,21 +189,54 @@ class LoogleManager:
|
|
|
181
189
|
return False, err
|
|
182
190
|
return True, ""
|
|
183
191
|
|
|
192
|
+
def _discover_project_paths(self) -> list[Path]:
|
|
193
|
+
"""Find .lake/packages lib paths from the user's project."""
|
|
194
|
+
if not self.project_path:
|
|
195
|
+
return []
|
|
196
|
+
paths = []
|
|
197
|
+
# Check packages directory
|
|
198
|
+
lake_packages = self.project_path / ".lake" / "packages"
|
|
199
|
+
if lake_packages.exists():
|
|
200
|
+
for pkg_dir in lake_packages.iterdir():
|
|
201
|
+
if not pkg_dir.is_dir():
|
|
202
|
+
continue
|
|
203
|
+
lib_path = pkg_dir / ".lake" / "build" / "lib" / "lean"
|
|
204
|
+
if lib_path.exists():
|
|
205
|
+
paths.append(lib_path)
|
|
206
|
+
# Also add the project's own build output
|
|
207
|
+
project_lib = self.project_path / ".lake" / "build" / "lib" / "lean"
|
|
208
|
+
if project_lib.exists():
|
|
209
|
+
paths.append(project_lib)
|
|
210
|
+
return sorted(paths)
|
|
211
|
+
|
|
184
212
|
def _get_index_path(self) -> Path:
|
|
185
|
-
|
|
213
|
+
base = f"mathlib-{self._get_mathlib_version()}"
|
|
214
|
+
if self._extra_paths:
|
|
215
|
+
# Include hash of extra paths for project-specific index
|
|
216
|
+
paths_str = ":".join(str(p) for p in sorted(self._extra_paths))
|
|
217
|
+
path_hash = hashlib.sha256(paths_str.encode()).hexdigest()[:8]
|
|
218
|
+
return self.index_dir / f"{base}-{path_hash}.idx"
|
|
219
|
+
return self.index_dir / f"{base}.idx"
|
|
186
220
|
|
|
187
221
|
def _cleanup_old_indices(self) -> None:
|
|
188
|
-
"""Remove old index files from previous mathlib versions.
|
|
222
|
+
"""Remove old index files from previous mathlib versions.
|
|
223
|
+
|
|
224
|
+
Cleans up both mathlib-only indexes (mathlib-<version>.idx) and
|
|
225
|
+
project-specific indexes (mathlib-<version>-<hash>.idx) that don't
|
|
226
|
+
match the current mathlib version.
|
|
227
|
+
"""
|
|
189
228
|
if not self.index_dir.exists():
|
|
190
229
|
return
|
|
191
|
-
|
|
230
|
+
current_mathlib = f"mathlib-{self._get_mathlib_version()}"
|
|
192
231
|
for idx in self.index_dir.glob("*.idx"):
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
232
|
+
# Keep indexes with current mathlib version (both base and project-specific)
|
|
233
|
+
if idx.name.startswith(current_mathlib):
|
|
234
|
+
continue
|
|
235
|
+
try:
|
|
236
|
+
idx.unlink()
|
|
237
|
+
logger.info(f"Removed old index: {idx.name}")
|
|
238
|
+
except Exception:
|
|
239
|
+
pass
|
|
199
240
|
|
|
200
241
|
def _build_index(self) -> Path | None:
|
|
201
242
|
index_path = self._get_index_path()
|
|
@@ -205,17 +246,37 @@ class LoogleManager:
|
|
|
205
246
|
return None
|
|
206
247
|
self.index_dir.mkdir(parents=True, exist_ok=True)
|
|
207
248
|
self._cleanup_old_indices()
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
249
|
+
|
|
250
|
+
# Build command with extra paths
|
|
251
|
+
cmd = [str(self.binary_path), "--write-index", str(index_path), "--json"]
|
|
252
|
+
for path in self._extra_paths:
|
|
253
|
+
cmd.extend(["--path", str(path)])
|
|
254
|
+
cmd.append("") # Empty query for index building
|
|
255
|
+
|
|
256
|
+
if self._extra_paths:
|
|
257
|
+
logger.info(
|
|
258
|
+
f"Building search index with {len(self._extra_paths)} extra paths..."
|
|
213
259
|
)
|
|
260
|
+
else:
|
|
261
|
+
logger.info("Building search index...")
|
|
262
|
+
try:
|
|
263
|
+
self._run(cmd, timeout=600)
|
|
214
264
|
return index_path if index_path.exists() else None
|
|
215
265
|
except Exception as e:
|
|
216
266
|
logger.error(f"Index build error: {e}")
|
|
217
267
|
return None
|
|
218
268
|
|
|
269
|
+
def set_project_path(self, project_path: Path | None) -> bool:
|
|
270
|
+
"""Update project path and rediscover extra paths. Returns True if paths changed."""
|
|
271
|
+
self.project_path = project_path
|
|
272
|
+
new_paths = self._discover_project_paths()
|
|
273
|
+
if new_paths != self._extra_paths:
|
|
274
|
+
self._extra_paths = new_paths
|
|
275
|
+
if new_paths:
|
|
276
|
+
logger.info(f"Discovered {len(new_paths)} project library paths")
|
|
277
|
+
return True
|
|
278
|
+
return False
|
|
279
|
+
|
|
219
280
|
def ensure_installed(self) -> bool:
|
|
220
281
|
ok, err = self._check_prerequisites()
|
|
221
282
|
if not ok:
|
|
@@ -223,6 +284,10 @@ class LoogleManager:
|
|
|
223
284
|
return False
|
|
224
285
|
if not self._clone_repo() or not self._build_loogle():
|
|
225
286
|
return False
|
|
287
|
+
# Discover project paths before building index
|
|
288
|
+
self._extra_paths = self._discover_project_paths()
|
|
289
|
+
if self._extra_paths:
|
|
290
|
+
logger.info(f"Indexing {len(self._extra_paths)} project library paths")
|
|
226
291
|
if not self._build_index():
|
|
227
292
|
logger.warning("Index build failed, loogle will build on startup")
|
|
228
293
|
return self.is_installed
|
|
@@ -234,10 +299,26 @@ class LoogleManager:
|
|
|
234
299
|
if not ok:
|
|
235
300
|
logger.error(f"Loogle environment check failed: {err}")
|
|
236
301
|
return False
|
|
302
|
+
|
|
303
|
+
# Check if project paths changed and we need to rebuild index
|
|
304
|
+
if self.project_path:
|
|
305
|
+
new_paths = self._discover_project_paths()
|
|
306
|
+
if new_paths != self._extra_paths:
|
|
307
|
+
self._extra_paths = new_paths
|
|
308
|
+
# Build new index if paths changed
|
|
309
|
+
self._build_index()
|
|
310
|
+
|
|
237
311
|
cmd = [str(self.binary_path), "--json", "--interactive"]
|
|
238
312
|
if (idx := self._get_index_path()).exists():
|
|
239
313
|
cmd.extend(["--read-index", str(idx)])
|
|
240
|
-
|
|
314
|
+
# Add extra paths for runtime search (in case not all are indexed)
|
|
315
|
+
for path in self._extra_paths:
|
|
316
|
+
cmd.extend(["--path", str(path)])
|
|
317
|
+
|
|
318
|
+
if self._extra_paths:
|
|
319
|
+
logger.info(f"Starting loogle with {len(self._extra_paths)} extra paths...")
|
|
320
|
+
else:
|
|
321
|
+
logger.info("Starting loogle subprocess...")
|
|
241
322
|
try:
|
|
242
323
|
self.process = await asyncio.create_subprocess_exec(
|
|
243
324
|
*cmd,
|
lean_lsp_mcp/models.py
CHANGED
|
@@ -46,7 +46,15 @@ class DiagnosticMessage(BaseModel):
|
|
|
46
46
|
|
|
47
47
|
class GoalState(BaseModel):
|
|
48
48
|
line_context: str = Field(description="Source line where goals were queried")
|
|
49
|
-
goals: str = Field(
|
|
49
|
+
goals: Optional[List[str]] = Field(
|
|
50
|
+
None, description="Goal list at specified column position"
|
|
51
|
+
)
|
|
52
|
+
goals_before: Optional[List[str]] = Field(
|
|
53
|
+
None, description="Goals at line start (when column omitted)"
|
|
54
|
+
)
|
|
55
|
+
goals_after: Optional[List[str]] = Field(
|
|
56
|
+
None, description="Goals at line end (when column omitted)"
|
|
57
|
+
)
|
|
50
58
|
|
|
51
59
|
|
|
52
60
|
class CompletionItem(BaseModel):
|
|
@@ -94,8 +102,8 @@ class FileOutline(BaseModel):
|
|
|
94
102
|
|
|
95
103
|
class AttemptResult(BaseModel):
|
|
96
104
|
snippet: str = Field(description="Code snippet that was tried")
|
|
97
|
-
|
|
98
|
-
|
|
105
|
+
goals: List[str] = Field(
|
|
106
|
+
default_factory=list, description="Goal list after applying snippet"
|
|
99
107
|
)
|
|
100
108
|
diagnostics: List[DiagnosticMessage] = Field(
|
|
101
109
|
default_factory=list, description="Diagnostics for this attempt"
|
lean_lsp_mcp/server.py
CHANGED
|
@@ -1,82 +1,80 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
+
import functools
|
|
2
3
|
import os
|
|
3
4
|
import re
|
|
4
5
|
import time
|
|
5
|
-
from typing import Annotated, List, Optional, Dict
|
|
6
|
-
from contextlib import asynccontextmanager
|
|
7
|
-
from collections.abc import AsyncIterator
|
|
8
|
-
from dataclasses import dataclass
|
|
9
6
|
import urllib
|
|
10
|
-
import orjson
|
|
11
|
-
import functools
|
|
12
7
|
import uuid
|
|
8
|
+
from collections.abc import AsyncIterator
|
|
9
|
+
from contextlib import asynccontextmanager
|
|
10
|
+
from dataclasses import dataclass
|
|
13
11
|
from pathlib import Path
|
|
12
|
+
from typing import Annotated, Dict, List, Optional
|
|
14
13
|
|
|
15
|
-
|
|
16
|
-
from
|
|
17
|
-
from mcp.server.fastmcp.utilities.logging import get_logger, configure_logging
|
|
14
|
+
import orjson
|
|
15
|
+
from leanclient import DocumentContentChange, LeanLSPClient
|
|
18
16
|
from mcp.server.auth.settings import AuthSettings
|
|
17
|
+
from mcp.server.fastmcp import Context, FastMCP
|
|
18
|
+
from mcp.server.fastmcp.utilities.logging import configure_logging, get_logger
|
|
19
19
|
from mcp.types import ToolAnnotations
|
|
20
|
-
from
|
|
20
|
+
from pydantic import Field
|
|
21
21
|
|
|
22
22
|
from lean_lsp_mcp.client_utils import (
|
|
23
|
+
infer_project_path,
|
|
23
24
|
setup_client_for_file,
|
|
24
25
|
startup_client,
|
|
25
|
-
infer_project_path,
|
|
26
26
|
)
|
|
27
27
|
from lean_lsp_mcp.file_utils import get_file_contents
|
|
28
28
|
from lean_lsp_mcp.instructions import INSTRUCTIONS
|
|
29
|
-
from lean_lsp_mcp.search_utils import check_ripgrep_status, lean_local_search
|
|
30
29
|
from lean_lsp_mcp.loogle import LoogleManager, loogle_remote
|
|
31
|
-
from lean_lsp_mcp.outline_utils import generate_outline_data
|
|
32
30
|
from lean_lsp_mcp.models import (
|
|
33
|
-
LocalSearchResult,
|
|
34
|
-
LeanSearchResult,
|
|
35
|
-
LoogleResult,
|
|
36
|
-
LeanFinderResult,
|
|
37
|
-
StateSearchResult,
|
|
38
|
-
PremiseResult,
|
|
39
|
-
DiagnosticMessage,
|
|
40
|
-
GoalState,
|
|
41
|
-
CompletionItem,
|
|
42
|
-
HoverInfo,
|
|
43
|
-
TermGoalState,
|
|
44
|
-
FileOutline,
|
|
45
31
|
AttemptResult,
|
|
46
32
|
BuildResult,
|
|
47
|
-
|
|
33
|
+
CompletionItem,
|
|
34
|
+
CompletionsResult,
|
|
48
35
|
DeclarationInfo,
|
|
36
|
+
DiagnosticMessage,
|
|
49
37
|
# Wrapper models for list-returning tools
|
|
50
38
|
DiagnosticsResult,
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
39
|
+
FileOutline,
|
|
40
|
+
GoalState,
|
|
41
|
+
HoverInfo,
|
|
42
|
+
LeanFinderResult,
|
|
43
|
+
LeanFinderResults,
|
|
44
|
+
LeanSearchResult,
|
|
54
45
|
LeanSearchResults,
|
|
46
|
+
LocalSearchResult,
|
|
47
|
+
LocalSearchResults,
|
|
48
|
+
LoogleResult,
|
|
55
49
|
LoogleResults,
|
|
56
|
-
|
|
57
|
-
|
|
50
|
+
MultiAttemptResult,
|
|
51
|
+
PremiseResult,
|
|
58
52
|
PremiseResults,
|
|
53
|
+
RunResult,
|
|
54
|
+
StateSearchResult,
|
|
55
|
+
StateSearchResults,
|
|
56
|
+
TermGoalState,
|
|
59
57
|
)
|
|
58
|
+
from lean_lsp_mcp.outline_utils import generate_outline_data
|
|
59
|
+
from lean_lsp_mcp.search_utils import check_ripgrep_status, lean_local_search
|
|
60
60
|
from lean_lsp_mcp.utils import (
|
|
61
61
|
COMPLETION_KIND,
|
|
62
|
+
LeanToolError,
|
|
63
|
+
OptionalTokenVerifier,
|
|
62
64
|
OutputCapture,
|
|
65
|
+
check_lsp_response,
|
|
63
66
|
deprecated,
|
|
67
|
+
extract_goals_list,
|
|
64
68
|
extract_range,
|
|
65
69
|
filter_diagnostics_by_position,
|
|
66
70
|
find_start_position,
|
|
67
|
-
format_goal,
|
|
68
71
|
get_declaration_range,
|
|
69
|
-
OptionalTokenVerifier,
|
|
70
72
|
)
|
|
71
73
|
|
|
72
74
|
# LSP Diagnostic severity: 1=error, 2=warning, 3=info, 4=hint
|
|
73
75
|
DIAGNOSTIC_SEVERITY: Dict[int, str] = {1: "error", 2: "warning", 3: "info", 4: "hint"}
|
|
74
76
|
|
|
75
77
|
|
|
76
|
-
class LeanToolError(Exception):
|
|
77
|
-
pass
|
|
78
|
-
|
|
79
|
-
|
|
80
78
|
_LOG_LEVEL = os.environ.get("LEAN_LOG_LEVEL", "INFO")
|
|
81
79
|
configure_logging("CRITICAL" if _LOG_LEVEL == "NONE" else _LOG_LEVEL)
|
|
82
80
|
logger = get_logger(__name__)
|
|
@@ -110,7 +108,7 @@ async def app_lifespan(server: FastMCP) -> AsyncIterator[AppContext]:
|
|
|
110
108
|
# Initialize local loogle if enabled via env var or CLI
|
|
111
109
|
if os.environ.get("LEAN_LOOGLE_LOCAL", "").lower() in ("1", "true", "yes"):
|
|
112
110
|
logger.info("Local loogle enabled, initializing...")
|
|
113
|
-
loogle_manager = LoogleManager()
|
|
111
|
+
loogle_manager = LoogleManager(project_path=lean_project_path)
|
|
114
112
|
if loogle_manager.ensure_installed():
|
|
115
113
|
if await loogle_manager.start():
|
|
116
114
|
loogle_local_available = True
|
|
@@ -445,6 +443,7 @@ def diagnostic_messages(
|
|
|
445
443
|
end_line=end_line_0,
|
|
446
444
|
inactivity_timeout=15.0,
|
|
447
445
|
)
|
|
446
|
+
check_lsp_response(diagnostics, "get_diagnostics")
|
|
448
447
|
|
|
449
448
|
return DiagnosticsResult(items=_to_diagnostic_messages(diagnostics))
|
|
450
449
|
|
|
@@ -494,15 +493,18 @@ def goal(
|
|
|
494
493
|
(i for i, c in enumerate(line_context) if not c.isspace()), 0
|
|
495
494
|
)
|
|
496
495
|
goal_start = client.get_goal(rel_path, line - 1, column_start)
|
|
496
|
+
check_lsp_response(goal_start, "get_goal", allow_none=True)
|
|
497
497
|
goal_end = client.get_goal(rel_path, line - 1, column_end)
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
498
|
+
return GoalState(
|
|
499
|
+
line_context=line_context,
|
|
500
|
+
goals_before=extract_goals_list(goal_start),
|
|
501
|
+
goals_after=extract_goals_list(goal_end),
|
|
502
|
+
)
|
|
502
503
|
else:
|
|
503
504
|
goal_result = client.get_goal(rel_path, line - 1, column - 1)
|
|
505
|
+
check_lsp_response(goal_result, "get_goal", allow_none=True)
|
|
504
506
|
return GoalState(
|
|
505
|
-
line_context=line_context, goals=
|
|
507
|
+
line_context=line_context, goals=extract_goals_list(goal_result)
|
|
506
508
|
)
|
|
507
509
|
|
|
508
510
|
|
|
@@ -543,6 +545,7 @@ def term_goal(
|
|
|
543
545
|
column = len(line_context)
|
|
544
546
|
|
|
545
547
|
term_goal_result = client.get_term_goal(rel_path, line - 1, column - 1)
|
|
548
|
+
check_lsp_response(term_goal_result, "get_term_goal", allow_none=True)
|
|
546
549
|
expected_type = None
|
|
547
550
|
if term_goal_result is not None:
|
|
548
551
|
rendered = term_goal_result.get("goal")
|
|
@@ -578,6 +581,7 @@ def hover(
|
|
|
578
581
|
client.open_file(rel_path)
|
|
579
582
|
file_content = client.get_file_content(rel_path)
|
|
580
583
|
hover_info = client.get_hover(rel_path, line - 1, column - 1)
|
|
584
|
+
check_lsp_response(hover_info, "get_hover", allow_none=True)
|
|
581
585
|
if hover_info is None:
|
|
582
586
|
raise LeanToolError(f"No hover information at line {line}, column {column}")
|
|
583
587
|
|
|
@@ -589,6 +593,7 @@ def hover(
|
|
|
589
593
|
|
|
590
594
|
# Add diagnostics if available
|
|
591
595
|
diagnostics = client.get_diagnostics(rel_path)
|
|
596
|
+
check_lsp_response(diagnostics, "get_diagnostics")
|
|
592
597
|
filtered = filter_diagnostics_by_position(diagnostics, line - 1, column - 1)
|
|
593
598
|
|
|
594
599
|
return HoverInfo(
|
|
@@ -625,6 +630,7 @@ def completions(
|
|
|
625
630
|
client.open_file(rel_path)
|
|
626
631
|
content = client.get_file_content(rel_path)
|
|
627
632
|
raw_completions = client.get_completions(rel_path, line - 1, column - 1)
|
|
633
|
+
check_lsp_response(raw_completions, "get_completions")
|
|
628
634
|
|
|
629
635
|
# Convert to CompletionItem models
|
|
630
636
|
items: List[CompletionItem] = []
|
|
@@ -770,14 +776,15 @@ def multi_attempt(
|
|
|
770
776
|
# Apply the change to the file, capture diagnostics and goal state
|
|
771
777
|
client.update_file(rel_path, [change])
|
|
772
778
|
diag = client.get_diagnostics(rel_path)
|
|
779
|
+
check_lsp_response(diag, "get_diagnostics")
|
|
773
780
|
filtered_diag = filter_diagnostics_by_position(diag, line - 1, None)
|
|
774
781
|
# Use the snippet text length without any trailing newline for the column
|
|
775
782
|
goal_result = client.get_goal(rel_path, line - 1, len(snippet_str))
|
|
776
|
-
|
|
783
|
+
goals = extract_goals_list(goal_result)
|
|
777
784
|
results.append(
|
|
778
785
|
AttemptResult(
|
|
779
786
|
snippet=snippet_str,
|
|
780
|
-
|
|
787
|
+
goals=goals,
|
|
781
788
|
diagnostics=_to_diagnostic_messages(filtered_diag),
|
|
782
789
|
)
|
|
783
790
|
)
|
|
@@ -838,6 +845,7 @@ def run_code(
|
|
|
838
845
|
client.open_file(rel_path)
|
|
839
846
|
opened_file = True
|
|
840
847
|
raw_diagnostics = client.get_diagnostics(rel_path, inactivity_timeout=15.0)
|
|
848
|
+
check_lsp_response(raw_diagnostics, "get_diagnostics")
|
|
841
849
|
finally:
|
|
842
850
|
if opened_file:
|
|
843
851
|
try:
|
|
@@ -946,7 +954,7 @@ def leansearch(
|
|
|
946
954
|
method="POST",
|
|
947
955
|
)
|
|
948
956
|
|
|
949
|
-
with urllib.request.urlopen(req, timeout=
|
|
957
|
+
with urllib.request.urlopen(req, timeout=10) as response:
|
|
950
958
|
results = orjson.loads(response.read())
|
|
951
959
|
|
|
952
960
|
if not results or not results[0]:
|
|
@@ -990,6 +998,11 @@ async def loogle(
|
|
|
990
998
|
|
|
991
999
|
# Try local loogle first if available (no rate limiting)
|
|
992
1000
|
if app_ctx.loogle_local_available and app_ctx.loogle_manager:
|
|
1001
|
+
# Update project path if it changed (adds new library paths)
|
|
1002
|
+
if app_ctx.lean_project_path != app_ctx.loogle_manager.project_path:
|
|
1003
|
+
if app_ctx.loogle_manager.set_project_path(app_ctx.lean_project_path):
|
|
1004
|
+
# Restart to pick up new paths
|
|
1005
|
+
await app_ctx.loogle_manager.stop()
|
|
993
1006
|
try:
|
|
994
1007
|
results = await app_ctx.loogle_manager.query(query, num_results)
|
|
995
1008
|
if not results:
|
|
@@ -1050,7 +1063,7 @@ def leanfinder(
|
|
|
1050
1063
|
)
|
|
1051
1064
|
|
|
1052
1065
|
results: List[LeanFinderResult] = []
|
|
1053
|
-
with urllib.request.urlopen(req, timeout=
|
|
1066
|
+
with urllib.request.urlopen(req, timeout=10) as response:
|
|
1054
1067
|
data = orjson.loads(response.read())
|
|
1055
1068
|
for result in data["results"]:
|
|
1056
1069
|
if (
|
|
@@ -1113,7 +1126,7 @@ def state_search(
|
|
|
1113
1126
|
method="GET",
|
|
1114
1127
|
)
|
|
1115
1128
|
|
|
1116
|
-
with urllib.request.urlopen(req, timeout=
|
|
1129
|
+
with urllib.request.urlopen(req, timeout=10) as response:
|
|
1117
1130
|
results = orjson.loads(response.read())
|
|
1118
1131
|
|
|
1119
1132
|
items = [StateSearchResult(name=r["name"]) for r in results]
|
|
@@ -1173,7 +1186,7 @@ def hammer_premise(
|
|
|
1173
1186
|
data=orjson.dumps(data),
|
|
1174
1187
|
)
|
|
1175
1188
|
|
|
1176
|
-
with urllib.request.urlopen(req, timeout=
|
|
1189
|
+
with urllib.request.urlopen(req, timeout=10) as response:
|
|
1177
1190
|
results = orjson.loads(response.read())
|
|
1178
1191
|
|
|
1179
1192
|
items = [PremiseResult(name=r["name"]) for r in results]
|
lean_lsp_mcp/utils.py
CHANGED
|
@@ -2,11 +2,39 @@ import os
|
|
|
2
2
|
import secrets
|
|
3
3
|
import sys
|
|
4
4
|
import tempfile
|
|
5
|
-
from typing import List, Dict, Optional, Callable
|
|
5
|
+
from typing import Any, List, Dict, Optional, Callable
|
|
6
6
|
|
|
7
7
|
from mcp.server.auth.provider import AccessToken, TokenVerifier
|
|
8
8
|
|
|
9
9
|
|
|
10
|
+
class LeanToolError(Exception):
|
|
11
|
+
"""Exception raised when a Lean MCP tool operation fails."""
|
|
12
|
+
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def check_lsp_response(
|
|
17
|
+
response: Any, operation: str, *, allow_none: bool = False
|
|
18
|
+
) -> Any:
|
|
19
|
+
"""Check an LSP response for error patterns and raise if found.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
response: The response from a leanclient LSP operation
|
|
23
|
+
operation: Human-readable description of the operation
|
|
24
|
+
allow_none: If False (default), None raises LeanToolError (timeout).
|
|
25
|
+
If True, None is allowed (for operations where None is valid).
|
|
26
|
+
|
|
27
|
+
Raises:
|
|
28
|
+
LeanToolError: If response indicates failure
|
|
29
|
+
"""
|
|
30
|
+
if response is None and not allow_none:
|
|
31
|
+
raise LeanToolError(f"LSP timeout during {operation}")
|
|
32
|
+
if isinstance(response, dict) and "error" in response:
|
|
33
|
+
msg = response["error"].get("message", "unknown error")
|
|
34
|
+
raise LeanToolError(f"LSP error during {operation}: {msg}")
|
|
35
|
+
return response
|
|
36
|
+
|
|
37
|
+
|
|
10
38
|
class OutputCapture:
|
|
11
39
|
"""Capture any output to stdout and stderr at the file descriptor level."""
|
|
12
40
|
|
|
@@ -80,11 +108,11 @@ def format_diagnostics(diagnostics: List[Dict], select_line: int = -1) -> List[s
|
|
|
80
108
|
return msgs
|
|
81
109
|
|
|
82
110
|
|
|
83
|
-
def
|
|
84
|
-
if
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
return
|
|
111
|
+
def extract_goals_list(goal_response: dict | None) -> List[str]:
|
|
112
|
+
"""Extract goals list from LSP response, returning empty list if no goals."""
|
|
113
|
+
if goal_response is None:
|
|
114
|
+
return []
|
|
115
|
+
return goal_response.get("goals", [])
|
|
88
116
|
|
|
89
117
|
|
|
90
118
|
def _utf16_index_to_py_index(text: str, utf16_index: int) -> int | None:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: lean-lsp-mcp
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.17.1
|
|
4
4
|
Summary: Lean Theorem Prover MCP
|
|
5
5
|
Author-email: Oliver Dressler <hey@oli.show>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -8,8 +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.
|
|
12
|
-
Requires-Dist: mcp[cli]==1.
|
|
11
|
+
Requires-Dist: leanclient==0.8.0
|
|
12
|
+
Requires-Dist: mcp[cli]==1.25.0
|
|
13
13
|
Requires-Dist: orjson>=3.11.1
|
|
14
14
|
Provides-Extra: lint
|
|
15
15
|
Requires-Dist: ruff>=0.2.0; extra == "lint"
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
lean_lsp_mcp/__init__.py,sha256=MN_bNFyb5-p33JWWGbrlUYBd1UUMQKtZYGC9KCh2mtM,1403
|
|
2
|
+
lean_lsp_mcp/__main__.py,sha256=XnpTzfJc0T-j9tHtdkA8ovTr1c139ffTewcJGhxYDaM,49
|
|
3
|
+
lean_lsp_mcp/client_utils.py,sha256=HgPuB35rMitn2Xm8SCAErsFLq15trB6VMz3FDFgmPd8,4897
|
|
4
|
+
lean_lsp_mcp/file_utils.py,sha256=kCTYQSfmV-R2cm_NCi_L8W5Dcsm0_rTOPpTtpyAin78,1365
|
|
5
|
+
lean_lsp_mcp/instructions.py,sha256=iJk_oD67tqNaC8K5OXEuXafULKSbbiHjZAsSRebOwdw,1904
|
|
6
|
+
lean_lsp_mcp/loogle.py,sha256=zUgnDWoTIqa4G6GXStAIxxJUR545YbU8Z-8KMjddKV0,15500
|
|
7
|
+
lean_lsp_mcp/models.py,sha256=2pLmvNsrMdn4vO1k119Jw8gqYeaeGKewWW0q1TabBCY,6604
|
|
8
|
+
lean_lsp_mcp/outline_utils.py,sha256=-eoZNbx2eaKaYmuyFJnwUMWP8I9YXNWusue_2OYpDBM,10981
|
|
9
|
+
lean_lsp_mcp/search_utils.py,sha256=MLqKGe4bhEvyfFLIBCmiDxkbcH4O5J3vl9mWnRSb_v0,6801
|
|
10
|
+
lean_lsp_mcp/server.py,sha256=1G7-FZz1TNJ7uU36eSTm9yponWyMO3QhQiyOhhTNOH8,40626
|
|
11
|
+
lean_lsp_mcp/utils.py,sha256=MmGgdhrLEvCtRRVNgN_vflO9_A25h76QbJXhBD-OKt0,12721
|
|
12
|
+
lean_lsp_mcp-0.17.1.dist-info/licenses/LICENSE,sha256=CQlxnf0tQyoVrBE93JYvAUYxv6Z5Yg6sX0pwogOkFvo,1071
|
|
13
|
+
lean_lsp_mcp-0.17.1.dist-info/METADATA,sha256=K0Ygues0l2TNEY6nXV4uyEds2QZfMV2DQ0kwId1uBHQ,20787
|
|
14
|
+
lean_lsp_mcp-0.17.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
15
|
+
lean_lsp_mcp-0.17.1.dist-info/entry_points.txt,sha256=nQbvwctWkWD7I-2f4VrdVQBZYGUw8CnUnFC6QjXxOSE,51
|
|
16
|
+
lean_lsp_mcp-0.17.1.dist-info/top_level.txt,sha256=LGEK0lgMSNPIQ6mG8EO-adaZEGPi_0daDs004epOTF0,13
|
|
17
|
+
lean_lsp_mcp-0.17.1.dist-info/RECORD,,
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
lean_lsp_mcp/__init__.py,sha256=MN_bNFyb5-p33JWWGbrlUYBd1UUMQKtZYGC9KCh2mtM,1403
|
|
2
|
-
lean_lsp_mcp/__main__.py,sha256=XnpTzfJc0T-j9tHtdkA8ovTr1c139ffTewcJGhxYDaM,49
|
|
3
|
-
lean_lsp_mcp/client_utils.py,sha256=HgPuB35rMitn2Xm8SCAErsFLq15trB6VMz3FDFgmPd8,4897
|
|
4
|
-
lean_lsp_mcp/file_utils.py,sha256=kCTYQSfmV-R2cm_NCi_L8W5Dcsm0_rTOPpTtpyAin78,1365
|
|
5
|
-
lean_lsp_mcp/instructions.py,sha256=S1y834V8v-SFSYJlxxy6Dj-Z0szMyEBT5SkEyM6Npr8,1756
|
|
6
|
-
lean_lsp_mcp/loogle.py,sha256=ChybtPM8jOxP8s28358yNqcLiYvGlQqkAEFFLzR87Zw,11971
|
|
7
|
-
lean_lsp_mcp/models.py,sha256=gDfyAX09YzKtjpKzuo6JtA2mNDc9pRWJ7iT44nHwi94,6326
|
|
8
|
-
lean_lsp_mcp/outline_utils.py,sha256=-eoZNbx2eaKaYmuyFJnwUMWP8I9YXNWusue_2OYpDBM,10981
|
|
9
|
-
lean_lsp_mcp/search_utils.py,sha256=MLqKGe4bhEvyfFLIBCmiDxkbcH4O5J3vl9mWnRSb_v0,6801
|
|
10
|
-
lean_lsp_mcp/server.py,sha256=AvjzoS8lwomUtIP2wrBln4z28-cXzCf1hNgXd9O1w4E,39749
|
|
11
|
-
lean_lsp_mcp/utils.py,sha256=355kzyB3dkwU7_4Mfcg--JXEorFaE2gtqs6-HbH5rRE,11722
|
|
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
|