lean-lsp-mcp 0.19.1__py3-none-any.whl → 0.20.0__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/__init__.py +14 -0
- lean_lsp_mcp/instructions.py +11 -10
- lean_lsp_mcp/loogle.py +9 -3
- lean_lsp_mcp/repl.py +260 -0
- lean_lsp_mcp/server.py +161 -28
- {lean_lsp_mcp-0.19.1.dist-info → lean_lsp_mcp-0.20.0.dist-info}/METADATA +50 -4
- lean_lsp_mcp-0.20.0.dist-info/RECORD +19 -0
- {lean_lsp_mcp-0.19.1.dist-info → lean_lsp_mcp-0.20.0.dist-info}/WHEEL +1 -1
- lean_lsp_mcp-0.19.1.dist-info/RECORD +0 -18
- {lean_lsp_mcp-0.19.1.dist-info → lean_lsp_mcp-0.20.0.dist-info}/entry_points.txt +0 -0
- {lean_lsp_mcp-0.19.1.dist-info → lean_lsp_mcp-0.20.0.dist-info}/licenses/LICENSE +0 -0
- {lean_lsp_mcp-0.19.1.dist-info → lean_lsp_mcp-0.20.0.dist-info}/top_level.txt +0 -0
lean_lsp_mcp/__init__.py
CHANGED
|
@@ -36,6 +36,16 @@ def main():
|
|
|
36
36
|
type=str,
|
|
37
37
|
help="Override loogle cache location (default: ~/.cache/lean-lsp-mcp/loogle)",
|
|
38
38
|
)
|
|
39
|
+
parser.add_argument(
|
|
40
|
+
"--repl",
|
|
41
|
+
action="store_true",
|
|
42
|
+
help="Enable fast REPL-based multi-attempt (~5x faster). Requires Lean REPL.",
|
|
43
|
+
)
|
|
44
|
+
parser.add_argument(
|
|
45
|
+
"--repl-timeout",
|
|
46
|
+
type=int,
|
|
47
|
+
help="REPL command timeout in seconds (default: 60)",
|
|
48
|
+
)
|
|
39
49
|
args = parser.parse_args()
|
|
40
50
|
|
|
41
51
|
# Set env vars from CLI args (CLI takes precedence over env vars)
|
|
@@ -43,6 +53,10 @@ def main():
|
|
|
43
53
|
os.environ["LEAN_LOOGLE_LOCAL"] = "true"
|
|
44
54
|
if args.loogle_cache_dir:
|
|
45
55
|
os.environ["LEAN_LOOGLE_CACHE_DIR"] = args.loogle_cache_dir
|
|
56
|
+
if args.repl:
|
|
57
|
+
os.environ["LEAN_REPL"] = "true"
|
|
58
|
+
if args.repl_timeout:
|
|
59
|
+
os.environ["LEAN_REPL_TIMEOUT"] = str(args.repl_timeout)
|
|
46
60
|
|
|
47
61
|
mcp.settings.host = args.host
|
|
48
62
|
mcp.settings.port = args.port
|
lean_lsp_mcp/instructions.py
CHANGED
|
@@ -16,19 +16,19 @@ INSTRUCTIONS = """## General Rules
|
|
|
16
16
|
- **lean_profile_proof**: Profile a theorem for performance. Shows tactic hotspots. SLOW!
|
|
17
17
|
|
|
18
18
|
## Search Tools (rate limited)
|
|
19
|
-
- **lean_leansearch** (3/30s): Natural language
|
|
20
|
-
- **lean_loogle** (3/30s): Type pattern
|
|
19
|
+
- **lean_leansearch** (3/30s): Natural language -> mathlib
|
|
20
|
+
- **lean_loogle** (3/30s): Type pattern -> mathlib
|
|
21
21
|
- **lean_leanfinder** (10/30s): Semantic/conceptual search
|
|
22
|
-
- **lean_state_search** (3/30s): Goal
|
|
23
|
-
- **lean_hammer_premise** (3/30s): Goal
|
|
22
|
+
- **lean_state_search** (3/30s): Goal -> closing lemmas
|
|
23
|
+
- **lean_hammer_premise** (3/30s): Goal -> premises for simp/aesop
|
|
24
24
|
|
|
25
25
|
## Search Decision Tree
|
|
26
|
-
1. "Does X exist locally?"
|
|
27
|
-
2. "I need a lemma that says X"
|
|
28
|
-
3. "Find lemma with type pattern"
|
|
29
|
-
4. "What's the Lean name for concept X?"
|
|
30
|
-
5. "What closes this goal?"
|
|
31
|
-
6. "What to feed simp?"
|
|
26
|
+
1. "Does X exist locally?" -> lean_local_search
|
|
27
|
+
2. "I need a lemma that says X" -> lean_leansearch
|
|
28
|
+
3. "Find lemma with type pattern" -> lean_loogle
|
|
29
|
+
4. "What's the Lean name for concept X?" -> lean_leanfinder
|
|
30
|
+
5. "What closes this goal?" -> lean_state_search
|
|
31
|
+
6. "What to feed simp?" -> lean_hammer_premise
|
|
32
32
|
|
|
33
33
|
After finding a name: lean_local_search to verify, lean_hover_info for signature.
|
|
34
34
|
|
|
@@ -37,4 +37,5 @@ List tools return JSON arrays. Empty = `[]`.
|
|
|
37
37
|
|
|
38
38
|
## Error Handling
|
|
39
39
|
Check `isError` in responses: `true` means failure (timeout/LSP error), while `[]` with `isError: false` means no results found.
|
|
40
|
+
|
|
40
41
|
"""
|
lean_lsp_mcp/loogle.py
CHANGED
|
@@ -8,12 +8,14 @@ import json
|
|
|
8
8
|
import logging
|
|
9
9
|
import os
|
|
10
10
|
import shutil
|
|
11
|
+
import ssl
|
|
11
12
|
import subprocess
|
|
12
13
|
import urllib.parse
|
|
13
14
|
import urllib.request
|
|
14
15
|
from pathlib import Path
|
|
15
16
|
from typing import Any
|
|
16
17
|
|
|
18
|
+
import certifi
|
|
17
19
|
import orjson
|
|
18
20
|
|
|
19
21
|
from lean_lsp_mcp.models import LoogleResult
|
|
@@ -35,7 +37,8 @@ def loogle_remote(query: str, num_results: int) -> list[LoogleResult] | str:
|
|
|
35
37
|
f"https://loogle.lean-lang.org/json?q={urllib.parse.quote(query)}",
|
|
36
38
|
headers={"User-Agent": "lean-lsp-mcp/0.1"},
|
|
37
39
|
)
|
|
38
|
-
|
|
40
|
+
ssl_ctx = ssl.create_default_context(cafile=certifi.where())
|
|
41
|
+
with urllib.request.urlopen(req, timeout=10, context=ssl_ctx) as response:
|
|
39
42
|
results = orjson.loads(response.read())
|
|
40
43
|
if "hits" not in results:
|
|
41
44
|
return "No results found."
|
|
@@ -139,9 +142,12 @@ class LoogleManager:
|
|
|
139
142
|
self._run(["lake", "exe", "cache", "get"], timeout=600)
|
|
140
143
|
except Exception as e:
|
|
141
144
|
logger.warning(f"Cache download: {e}")
|
|
142
|
-
logger.info("Building loogle...")
|
|
145
|
+
logger.info("Building loogle (this may a few minutes)...")
|
|
143
146
|
try:
|
|
144
|
-
|
|
147
|
+
result = self._run(["lake", "build"], timeout=900)
|
|
148
|
+
if result.returncode != 0:
|
|
149
|
+
logger.error(f"Build failed: {result.stderr[:1000]}")
|
|
150
|
+
return result.returncode == 0
|
|
145
151
|
except Exception as e:
|
|
146
152
|
logger.error(f"Build error: {e}")
|
|
147
153
|
return False
|
lean_lsp_mcp/repl.py
ADDED
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
"""Lean REPL for fast multi-attempt tactic execution using tactic mode."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
import platform
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
if platform.system() != "Windows":
|
|
13
|
+
import resource
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ReplError(Exception):
|
|
17
|
+
pass
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class SnippetResult:
|
|
22
|
+
goals: list[str] = field(default_factory=list)
|
|
23
|
+
messages: list[dict[str, Any]] = field(default_factory=list)
|
|
24
|
+
proof_status: str | None = None
|
|
25
|
+
error: str | None = None
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def repl_enabled() -> bool:
|
|
29
|
+
return os.environ.get("LEAN_REPL", "").lower() in ("1", "true", "yes")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def find_repl_binary(project_dir: str | None = None) -> str | None:
|
|
33
|
+
"""Find REPL binary: env var > .lake/packages > PATH."""
|
|
34
|
+
import shutil
|
|
35
|
+
from pathlib import Path
|
|
36
|
+
|
|
37
|
+
# 1. Explicit env var
|
|
38
|
+
if path := os.environ.get("LEAN_REPL_PATH"):
|
|
39
|
+
return path if Path(path).exists() or shutil.which(path) else None
|
|
40
|
+
|
|
41
|
+
# 2. Auto-detect from .lake/packages (common location after `lake build`)
|
|
42
|
+
if project_dir:
|
|
43
|
+
candidates = [
|
|
44
|
+
Path(project_dir)
|
|
45
|
+
/ ".lake"
|
|
46
|
+
/ "packages"
|
|
47
|
+
/ "repl"
|
|
48
|
+
/ ".lake"
|
|
49
|
+
/ "build"
|
|
50
|
+
/ "bin"
|
|
51
|
+
/ "repl",
|
|
52
|
+
Path(project_dir) / ".lake" / "build" / "bin" / "repl",
|
|
53
|
+
]
|
|
54
|
+
for p in candidates:
|
|
55
|
+
if p.exists():
|
|
56
|
+
return str(p)
|
|
57
|
+
|
|
58
|
+
# 3. Fall back to PATH
|
|
59
|
+
if found := shutil.which("repl"):
|
|
60
|
+
return found
|
|
61
|
+
|
|
62
|
+
return None
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _split_imports(code: str) -> tuple[str, str]:
|
|
66
|
+
"""Split code into (header with imports, body)."""
|
|
67
|
+
lines = code.splitlines()
|
|
68
|
+
i = 0
|
|
69
|
+
while i < len(lines) and (not lines[i].strip() or lines[i].startswith("import ")):
|
|
70
|
+
i += 1
|
|
71
|
+
|
|
72
|
+
# Deduplicate imports while preserving order
|
|
73
|
+
imports = [ln.strip() for ln in lines[:i] if ln.startswith("import ")]
|
|
74
|
+
header = "\n".join(dict.fromkeys(imports))
|
|
75
|
+
return header, "\n".join(lines[i:])
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class Repl:
|
|
79
|
+
"""Lean REPL using tactic mode for fast multi-attempt."""
|
|
80
|
+
|
|
81
|
+
def __init__(self, project_dir: str, repl_path: str | None = None):
|
|
82
|
+
self.project_dir = project_dir
|
|
83
|
+
self.repl_path = repl_path or find_repl_binary(project_dir) or "repl"
|
|
84
|
+
self.timeout = int(os.environ.get("LEAN_REPL_TIMEOUT", "60"))
|
|
85
|
+
self.mem_mb = int(os.environ.get("LEAN_REPL_MEM_MB", "8192"))
|
|
86
|
+
self._proc: asyncio.subprocess.Process | None = None
|
|
87
|
+
self._header: str | None = None
|
|
88
|
+
self._header_env: int | None = None
|
|
89
|
+
self._lock = asyncio.Lock()
|
|
90
|
+
|
|
91
|
+
async def _start(self) -> None:
|
|
92
|
+
"""Start REPL subprocess."""
|
|
93
|
+
kwargs: dict[str, Any] = {
|
|
94
|
+
"cwd": self.project_dir,
|
|
95
|
+
"stdin": asyncio.subprocess.PIPE,
|
|
96
|
+
"stdout": asyncio.subprocess.PIPE,
|
|
97
|
+
"stderr": asyncio.subprocess.PIPE,
|
|
98
|
+
}
|
|
99
|
+
if platform.system() != "Windows":
|
|
100
|
+
kwargs["start_new_session"] = True
|
|
101
|
+
# Memory limit on Unix systems
|
|
102
|
+
if platform.system() == "Linux":
|
|
103
|
+
limit = resource.RLIMIT_AS # Virtual memory
|
|
104
|
+
else: # macOS
|
|
105
|
+
limit = resource.RLIMIT_RSS # Resident set size
|
|
106
|
+
mem = self.mem_mb * 1024 * 1024
|
|
107
|
+
kwargs["preexec_fn"] = lambda: resource.setrlimit(limit, (mem, mem))
|
|
108
|
+
self._proc = await asyncio.create_subprocess_exec(
|
|
109
|
+
"lake", "env", self.repl_path, **kwargs
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
async def _send(self, cmd: dict[str, Any]) -> dict[str, Any]:
|
|
113
|
+
"""Send command and return response."""
|
|
114
|
+
if not self._proc or not self._proc.stdin or not self._proc.stdout:
|
|
115
|
+
raise ReplError("REPL not running")
|
|
116
|
+
|
|
117
|
+
self._proc.stdin.write((json.dumps(cmd) + "\n\n").encode())
|
|
118
|
+
await self._proc.stdin.drain()
|
|
119
|
+
|
|
120
|
+
lines = []
|
|
121
|
+
while True:
|
|
122
|
+
line = await self._proc.stdout.readline()
|
|
123
|
+
if not line or not line.strip():
|
|
124
|
+
break
|
|
125
|
+
lines.append(line)
|
|
126
|
+
|
|
127
|
+
if not lines:
|
|
128
|
+
raise ReplError("No response from REPL")
|
|
129
|
+
return json.loads(b"".join(lines))
|
|
130
|
+
|
|
131
|
+
async def _send_cmd(self, code: str, env: int | None = None) -> dict[str, Any]:
|
|
132
|
+
"""Send a command (code) to the REPL."""
|
|
133
|
+
cmd: dict[str, Any] = {"cmd": code}
|
|
134
|
+
if env is not None:
|
|
135
|
+
cmd["env"] = env
|
|
136
|
+
return await self._send(cmd)
|
|
137
|
+
|
|
138
|
+
async def _send_tactic(self, tactic: str, proof_state: int) -> dict[str, Any]:
|
|
139
|
+
"""Send a tactic to run in a proof state."""
|
|
140
|
+
return await self._send({"tactic": tactic, "proofState": proof_state})
|
|
141
|
+
|
|
142
|
+
async def _ensure_header(self, header: str) -> int | None:
|
|
143
|
+
"""Ensure REPL is running with given header, return header env."""
|
|
144
|
+
if self._header != header:
|
|
145
|
+
await self.close()
|
|
146
|
+
self._header = header
|
|
147
|
+
self._header_env = None
|
|
148
|
+
|
|
149
|
+
if not self._proc or self._proc.returncode is not None:
|
|
150
|
+
await self._start()
|
|
151
|
+
if header:
|
|
152
|
+
resp = await self._send_cmd(header, env=None)
|
|
153
|
+
if "error" in resp:
|
|
154
|
+
raise ReplError(f"Failed to load imports: {resp['error']}")
|
|
155
|
+
self._header_env = resp.get("env")
|
|
156
|
+
|
|
157
|
+
return self._header_env
|
|
158
|
+
|
|
159
|
+
async def run_snippets(
|
|
160
|
+
self, base_code: str, snippets: list[str]
|
|
161
|
+
) -> list[SnippetResult]:
|
|
162
|
+
"""Run multiple tactic snippets using tactic mode.
|
|
163
|
+
|
|
164
|
+
1. Load header (imports) - cached across calls
|
|
165
|
+
2. Send body + sorry to get proofState
|
|
166
|
+
3. Run each tactic via tactic mode (very fast)
|
|
167
|
+
"""
|
|
168
|
+
header, body = _split_imports(base_code)
|
|
169
|
+
|
|
170
|
+
async with self._lock:
|
|
171
|
+
try:
|
|
172
|
+
# Load imports (cached)
|
|
173
|
+
header_env = await asyncio.wait_for(
|
|
174
|
+
self._ensure_header(header), timeout=self.timeout
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
# Send body with sorry to get proof state
|
|
178
|
+
if not body.strip():
|
|
179
|
+
return [SnippetResult(error="No proof body") for _ in snippets]
|
|
180
|
+
|
|
181
|
+
# Ensure proper whitespace before sorry
|
|
182
|
+
body_with_sorry = body.rstrip() + "\n sorry"
|
|
183
|
+
resp = await asyncio.wait_for(
|
|
184
|
+
self._send_cmd(body_with_sorry, env=header_env),
|
|
185
|
+
timeout=self.timeout,
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
if "error" in resp:
|
|
189
|
+
return [SnippetResult(error=resp["error"]) for _ in snippets]
|
|
190
|
+
|
|
191
|
+
# Get proof state from the sorry
|
|
192
|
+
sorries = resp.get("sorries", [])
|
|
193
|
+
if not sorries:
|
|
194
|
+
# No sorry = no proof goal, check messages for errors
|
|
195
|
+
msgs = resp.get("messages", [])
|
|
196
|
+
err = "; ".join(
|
|
197
|
+
m.get("data", "") for m in msgs if m.get("severity") == "error"
|
|
198
|
+
)
|
|
199
|
+
return [
|
|
200
|
+
SnippetResult(error=err or "No proof goal found")
|
|
201
|
+
for _ in snippets
|
|
202
|
+
]
|
|
203
|
+
|
|
204
|
+
proof_state = sorries[0].get("proofState")
|
|
205
|
+
if proof_state is None:
|
|
206
|
+
return [
|
|
207
|
+
SnippetResult(error="No proofState returned") for _ in snippets
|
|
208
|
+
]
|
|
209
|
+
|
|
210
|
+
# Run each tactic in tactic mode
|
|
211
|
+
results = []
|
|
212
|
+
for snippet in snippets:
|
|
213
|
+
try:
|
|
214
|
+
resp = await asyncio.wait_for(
|
|
215
|
+
self._send_tactic(snippet.strip(), proof_state),
|
|
216
|
+
timeout=self.timeout,
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
if "error" in resp:
|
|
220
|
+
# Lean error (tactic failed)
|
|
221
|
+
results.append(SnippetResult(error=resp["error"]))
|
|
222
|
+
else:
|
|
223
|
+
goals = resp.get("goals", [])
|
|
224
|
+
messages = resp.get("messages", [])
|
|
225
|
+
proof_status = resp.get("proofStatus")
|
|
226
|
+
results.append(
|
|
227
|
+
SnippetResult(
|
|
228
|
+
goals=goals,
|
|
229
|
+
messages=messages,
|
|
230
|
+
proof_status=proof_status,
|
|
231
|
+
)
|
|
232
|
+
)
|
|
233
|
+
except Exception as e:
|
|
234
|
+
results.append(SnippetResult(error=str(e)))
|
|
235
|
+
|
|
236
|
+
return results
|
|
237
|
+
|
|
238
|
+
except Exception as e:
|
|
239
|
+
await self.close()
|
|
240
|
+
raise ReplError(str(e)) from e
|
|
241
|
+
|
|
242
|
+
async def close(self) -> None:
|
|
243
|
+
if not self._proc:
|
|
244
|
+
return
|
|
245
|
+
proc, self._proc = self._proc, None
|
|
246
|
+
self._header = None
|
|
247
|
+
self._header_env = None
|
|
248
|
+
try:
|
|
249
|
+
if platform.system() != "Windows":
|
|
250
|
+
os.killpg(os.getpgid(proc.pid), 9)
|
|
251
|
+
else:
|
|
252
|
+
proc.kill()
|
|
253
|
+
except (ProcessLookupError, OSError):
|
|
254
|
+
pass
|
|
255
|
+
try:
|
|
256
|
+
await asyncio.wait_for(proc.wait(), timeout=1.0)
|
|
257
|
+
except asyncio.TimeoutError:
|
|
258
|
+
pass
|
|
259
|
+
if hasattr(proc, "_transport") and proc._transport:
|
|
260
|
+
proc._transport.close()
|
lean_lsp_mcp/server.py
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import functools
|
|
3
|
+
import logging.config
|
|
3
4
|
import os
|
|
4
5
|
import re
|
|
6
|
+
import ssl
|
|
5
7
|
import time
|
|
6
8
|
import urllib
|
|
7
9
|
import uuid
|
|
@@ -11,6 +13,7 @@ from dataclasses import dataclass
|
|
|
11
13
|
from pathlib import Path
|
|
12
14
|
from typing import Annotated, Dict, List, Optional
|
|
13
15
|
|
|
16
|
+
import certifi
|
|
14
17
|
import orjson
|
|
15
18
|
from leanclient import DocumentContentChange, LeanLSPClient
|
|
16
19
|
from mcp.server.auth.settings import AuthSettings
|
|
@@ -27,6 +30,7 @@ from lean_lsp_mcp.client_utils import (
|
|
|
27
30
|
from lean_lsp_mcp.file_utils import get_file_contents
|
|
28
31
|
from lean_lsp_mcp.instructions import INSTRUCTIONS
|
|
29
32
|
from lean_lsp_mcp.loogle import LoogleManager, loogle_remote
|
|
33
|
+
from lean_lsp_mcp.repl import Repl, repl_enabled
|
|
30
34
|
from lean_lsp_mcp.models import (
|
|
31
35
|
AttemptResult,
|
|
32
36
|
BuildResult,
|
|
@@ -56,6 +60,9 @@ from lean_lsp_mcp.models import (
|
|
|
56
60
|
StateSearchResults,
|
|
57
61
|
TermGoalState,
|
|
58
62
|
)
|
|
63
|
+
|
|
64
|
+
# REPL models not imported - low-level REPL tools not exposed to keep API simple.
|
|
65
|
+
# The model uses lean_multi_attempt which handles REPL internally.
|
|
59
66
|
from lean_lsp_mcp.outline_utils import generate_outline_data
|
|
60
67
|
from lean_lsp_mcp.search_utils import check_ripgrep_status, lean_local_search
|
|
61
68
|
from lean_lsp_mcp.utils import (
|
|
@@ -80,9 +87,10 @@ DIAGNOSTIC_SEVERITY: Dict[int, str] = {1: "error", 2: "warning", 3: "info", 4: "
|
|
|
80
87
|
|
|
81
88
|
async def _urlopen_json(req: urllib.request.Request, timeout: float):
|
|
82
89
|
"""Run urllib.request.urlopen in a worker thread to avoid blocking the event loop."""
|
|
90
|
+
ssl_ctx = ssl.create_default_context(cafile=certifi.where())
|
|
83
91
|
|
|
84
92
|
def _do_request():
|
|
85
|
-
with urllib.request.urlopen(req, timeout=timeout) as response:
|
|
93
|
+
with urllib.request.urlopen(req, timeout=timeout, context=ssl_ctx) as response:
|
|
86
94
|
return orjson.loads(response.read())
|
|
87
95
|
|
|
88
96
|
return await asyncio.to_thread(_do_request)
|
|
@@ -97,8 +105,36 @@ async def _safe_report_progress(
|
|
|
97
105
|
return
|
|
98
106
|
|
|
99
107
|
|
|
108
|
+
_LOG_FILE_CONFIG = os.environ.get("LEAN_LOG_FILE_CONFIG", None)
|
|
100
109
|
_LOG_LEVEL = os.environ.get("LEAN_LOG_LEVEL", "INFO")
|
|
101
|
-
|
|
110
|
+
if _LOG_FILE_CONFIG:
|
|
111
|
+
try:
|
|
112
|
+
if _LOG_FILE_CONFIG.endswith((".yaml", ".yml")):
|
|
113
|
+
import yaml
|
|
114
|
+
|
|
115
|
+
with open(_LOG_FILE_CONFIG, "r", encoding="utf-8") as f:
|
|
116
|
+
cfg = yaml.safe_load(f)
|
|
117
|
+
logging.config.dictConfig(cfg)
|
|
118
|
+
elif _LOG_FILE_CONFIG.endswith(".json"):
|
|
119
|
+
with open(_LOG_FILE_CONFIG, "r", encoding="utf-8") as f:
|
|
120
|
+
cfg = orjson.loads(f.read())
|
|
121
|
+
logging.config.dictConfig(cfg)
|
|
122
|
+
else:
|
|
123
|
+
# .ini / fileConfig
|
|
124
|
+
logging.config.fileConfig(_LOG_FILE_CONFIG, disable_existing_loggers=False)
|
|
125
|
+
except Exception as e:
|
|
126
|
+
# fallback to LEAN_LOG_LEVEL so server still runs
|
|
127
|
+
# use the existing configure_logging helper to set level
|
|
128
|
+
configure_logging("CRITICAL" if _LOG_LEVEL == "NONE" else _LOG_LEVEL)
|
|
129
|
+
logger = get_logger(__name__) # temporary to emit the warning
|
|
130
|
+
logger.warning(
|
|
131
|
+
"Failed to load logging config %s: %s. Falling back to LEAN_LOG_LEVEL.",
|
|
132
|
+
_LOG_FILE_CONFIG,
|
|
133
|
+
e,
|
|
134
|
+
)
|
|
135
|
+
else:
|
|
136
|
+
configure_logging("CRITICAL" if _LOG_LEVEL == "NONE" else _LOG_LEVEL)
|
|
137
|
+
|
|
102
138
|
logger = get_logger(__name__)
|
|
103
139
|
|
|
104
140
|
|
|
@@ -113,12 +149,17 @@ class AppContext:
|
|
|
113
149
|
lean_search_available: bool
|
|
114
150
|
loogle_manager: LoogleManager | None = None
|
|
115
151
|
loogle_local_available: bool = False
|
|
152
|
+
# REPL for efficient multi-attempt execution
|
|
153
|
+
repl: Repl | None = None
|
|
154
|
+
repl_enabled: bool = False
|
|
116
155
|
|
|
117
156
|
|
|
118
157
|
@asynccontextmanager
|
|
119
158
|
async def app_lifespan(server: FastMCP) -> AsyncIterator[AppContext]:
|
|
120
159
|
loogle_manager: LoogleManager | None = None
|
|
121
160
|
loogle_local_available = False
|
|
161
|
+
repl: Repl | None = None
|
|
162
|
+
repl_on = False
|
|
122
163
|
|
|
123
164
|
try:
|
|
124
165
|
lean_project_path_str = os.environ.get("LEAN_PROJECT_PATH", "").strip()
|
|
@@ -140,6 +181,26 @@ async def app_lifespan(server: FastMCP) -> AsyncIterator[AppContext]:
|
|
|
140
181
|
else:
|
|
141
182
|
logger.warning("Local loogle installation failed, will use remote API")
|
|
142
183
|
|
|
184
|
+
# Initialize REPL if enabled
|
|
185
|
+
if repl_enabled():
|
|
186
|
+
if lean_project_path:
|
|
187
|
+
from lean_lsp_mcp.repl import find_repl_binary
|
|
188
|
+
|
|
189
|
+
repl_bin = find_repl_binary(str(lean_project_path))
|
|
190
|
+
if repl_bin:
|
|
191
|
+
logger.info("REPL enabled, using: %s", repl_bin)
|
|
192
|
+
repl = Repl(project_dir=str(lean_project_path), repl_path=repl_bin)
|
|
193
|
+
repl_on = True
|
|
194
|
+
logger.info("REPL initialized: timeout=%ds", repl.timeout)
|
|
195
|
+
else:
|
|
196
|
+
logger.warning(
|
|
197
|
+
"REPL enabled but binary not found. "
|
|
198
|
+
'Add `require repl from git "https://github.com/leanprover-community/repl"` '
|
|
199
|
+
"to lakefile and run `lake build repl`. Falling back to LSP."
|
|
200
|
+
)
|
|
201
|
+
else:
|
|
202
|
+
logger.warning("REPL requires LEAN_PROJECT_PATH to be set")
|
|
203
|
+
|
|
143
204
|
context = AppContext(
|
|
144
205
|
lean_project_path=lean_project_path,
|
|
145
206
|
client=None,
|
|
@@ -153,6 +214,8 @@ async def app_lifespan(server: FastMCP) -> AsyncIterator[AppContext]:
|
|
|
153
214
|
lean_search_available=_RG_AVAILABLE,
|
|
154
215
|
loogle_manager=loogle_manager,
|
|
155
216
|
loogle_local_available=loogle_local_available,
|
|
217
|
+
repl=repl,
|
|
218
|
+
repl_enabled=repl_on,
|
|
156
219
|
)
|
|
157
220
|
yield context
|
|
158
221
|
finally:
|
|
@@ -164,6 +227,9 @@ async def app_lifespan(server: FastMCP) -> AsyncIterator[AppContext]:
|
|
|
164
227
|
if loogle_manager:
|
|
165
228
|
await loogle_manager.stop()
|
|
166
229
|
|
|
230
|
+
if repl:
|
|
231
|
+
await repl.close()
|
|
232
|
+
|
|
167
233
|
|
|
168
234
|
mcp_kwargs = dict(
|
|
169
235
|
name="Lean LSP",
|
|
@@ -823,24 +889,65 @@ def declaration_file(
|
|
|
823
889
|
return DeclarationInfo(file_path=str(abs_path), content=file_content)
|
|
824
890
|
|
|
825
891
|
|
|
826
|
-
|
|
827
|
-
"lean_multi_attempt",
|
|
828
|
-
annotations=ToolAnnotations(
|
|
829
|
-
title="Multi-Attempt",
|
|
830
|
-
readOnlyHint=True,
|
|
831
|
-
idempotentHint=True,
|
|
832
|
-
openWorldHint=False,
|
|
833
|
-
),
|
|
834
|
-
)
|
|
835
|
-
def multi_attempt(
|
|
892
|
+
async def _multi_attempt_repl(
|
|
836
893
|
ctx: Context,
|
|
837
|
-
file_path:
|
|
838
|
-
line:
|
|
839
|
-
snippets:
|
|
840
|
-
|
|
841
|
-
|
|
894
|
+
file_path: str,
|
|
895
|
+
line: int,
|
|
896
|
+
snippets: List[str],
|
|
897
|
+
) -> MultiAttemptResult | None:
|
|
898
|
+
"""Try tactics using REPL (fast path)."""
|
|
899
|
+
app_ctx: AppContext = ctx.request_context.lifespan_context
|
|
900
|
+
if not app_ctx.repl_enabled or not app_ctx.repl:
|
|
901
|
+
return None
|
|
902
|
+
|
|
903
|
+
try:
|
|
904
|
+
content = get_file_contents(file_path)
|
|
905
|
+
if content is None:
|
|
906
|
+
return None
|
|
907
|
+
lines = content.splitlines()
|
|
908
|
+
if line > len(lines):
|
|
909
|
+
return None
|
|
910
|
+
|
|
911
|
+
base_code = "\n".join(lines[: line - 1])
|
|
912
|
+
repl_results = await app_ctx.repl.run_snippets(base_code, snippets)
|
|
913
|
+
|
|
914
|
+
results = []
|
|
915
|
+
for snippet, pr in zip(snippets, repl_results):
|
|
916
|
+
diagnostics = [
|
|
917
|
+
DiagnosticMessage(
|
|
918
|
+
severity=m.get("severity", "info"),
|
|
919
|
+
message=m.get("data", ""),
|
|
920
|
+
line=m.get("pos", {}).get("line", 0),
|
|
921
|
+
column=m.get("pos", {}).get("column", 0),
|
|
922
|
+
)
|
|
923
|
+
for m in (pr.messages or [])
|
|
924
|
+
]
|
|
925
|
+
if pr.error:
|
|
926
|
+
diagnostics.append(
|
|
927
|
+
DiagnosticMessage(
|
|
928
|
+
severity="error", message=pr.error, line=0, column=0
|
|
929
|
+
)
|
|
930
|
+
)
|
|
931
|
+
results.append(
|
|
932
|
+
AttemptResult(
|
|
933
|
+
snippet=snippet.rstrip("\n"),
|
|
934
|
+
goals=pr.goals or [],
|
|
935
|
+
diagnostics=diagnostics,
|
|
936
|
+
)
|
|
937
|
+
)
|
|
938
|
+
return MultiAttemptResult(items=results)
|
|
939
|
+
except Exception as e:
|
|
940
|
+
logger.debug(f"REPL multi_attempt failed: {e}")
|
|
941
|
+
return None
|
|
942
|
+
|
|
943
|
+
|
|
944
|
+
def _multi_attempt_lsp(
|
|
945
|
+
ctx: Context,
|
|
946
|
+
file_path: str,
|
|
947
|
+
line: int,
|
|
948
|
+
snippets: List[str],
|
|
842
949
|
) -> MultiAttemptResult:
|
|
843
|
-
"""Try
|
|
950
|
+
"""Try tactics using LSP file modifications (fallback)."""
|
|
844
951
|
rel_path = setup_client_for_file(ctx, file_path)
|
|
845
952
|
if not rel_path:
|
|
846
953
|
raise LeanToolError(
|
|
@@ -849,25 +956,22 @@ def multi_attempt(
|
|
|
849
956
|
|
|
850
957
|
client: LeanLSPClient = ctx.request_context.lifespan_context.client
|
|
851
958
|
client.open_file(rel_path)
|
|
959
|
+
original_content = get_file_contents(file_path)
|
|
852
960
|
|
|
853
961
|
try:
|
|
854
962
|
results: List[AttemptResult] = []
|
|
855
|
-
# Avoid mutating caller-provided snippets; normalize locally per attempt
|
|
856
963
|
for snippet in snippets:
|
|
857
964
|
snippet_str = snippet.rstrip("\n")
|
|
858
965
|
payload = f"{snippet_str}\n"
|
|
859
|
-
# Create a DocumentContentChange for the snippet
|
|
860
966
|
change = DocumentContentChange(
|
|
861
967
|
payload,
|
|
862
968
|
[line - 1, 0],
|
|
863
969
|
[line, 0],
|
|
864
970
|
)
|
|
865
|
-
# Apply the change to the file, capture diagnostics and goal state
|
|
866
971
|
client.update_file(rel_path, [change])
|
|
867
972
|
diag = client.get_diagnostics(rel_path)
|
|
868
973
|
check_lsp_response(diag, "get_diagnostics")
|
|
869
974
|
filtered_diag = filter_diagnostics_by_position(diag, line - 1, None)
|
|
870
|
-
# Use the snippet text length without any trailing newline for the column
|
|
871
975
|
goal_result = client.get_goal(rel_path, line - 1, len(snippet_str))
|
|
872
976
|
goals = extract_goals_list(goal_result)
|
|
873
977
|
results.append(
|
|
@@ -880,12 +984,41 @@ def multi_attempt(
|
|
|
880
984
|
|
|
881
985
|
return MultiAttemptResult(items=results)
|
|
882
986
|
finally:
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
987
|
+
if original_content is not None:
|
|
988
|
+
try:
|
|
989
|
+
client.update_file_content(rel_path, original_content)
|
|
990
|
+
except Exception as exc:
|
|
991
|
+
logger.warning(
|
|
992
|
+
"Failed to restore `%s` after multi_attempt: %s", rel_path, exc
|
|
993
|
+
)
|
|
994
|
+
|
|
995
|
+
|
|
996
|
+
@mcp.tool(
|
|
997
|
+
"lean_multi_attempt",
|
|
998
|
+
annotations=ToolAnnotations(
|
|
999
|
+
title="Multi-Attempt",
|
|
1000
|
+
readOnlyHint=True,
|
|
1001
|
+
idempotentHint=True,
|
|
1002
|
+
openWorldHint=False,
|
|
1003
|
+
),
|
|
1004
|
+
)
|
|
1005
|
+
async def multi_attempt(
|
|
1006
|
+
ctx: Context,
|
|
1007
|
+
file_path: Annotated[str, Field(description="Absolute path to Lean file")],
|
|
1008
|
+
line: Annotated[int, Field(description="Line number (1-indexed)", ge=1)],
|
|
1009
|
+
snippets: Annotated[
|
|
1010
|
+
List[str],
|
|
1011
|
+
Field(description="Tactics to try (3+ recommended)"),
|
|
1012
|
+
],
|
|
1013
|
+
) -> MultiAttemptResult:
|
|
1014
|
+
"""Try multiple tactics without modifying file. Returns goal state for each."""
|
|
1015
|
+
# Priority 1: REPL
|
|
1016
|
+
result = await _multi_attempt_repl(ctx, file_path, line, snippets)
|
|
1017
|
+
if result is not None:
|
|
1018
|
+
return result
|
|
1019
|
+
|
|
1020
|
+
# Priority 2: LSP approach (fallback)
|
|
1021
|
+
return _multi_attempt_lsp(ctx, file_path, line, snippets)
|
|
889
1022
|
|
|
890
1023
|
|
|
891
1024
|
@mcp.tool(
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: lean-lsp-mcp
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.20.0
|
|
4
4
|
Summary: Lean Theorem Prover MCP
|
|
5
5
|
Author-email: Oliver Dressler <hey@oli.show>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -11,6 +11,9 @@ License-File: LICENSE
|
|
|
11
11
|
Requires-Dist: leanclient==0.9.2
|
|
12
12
|
Requires-Dist: mcp[cli]==1.25.0
|
|
13
13
|
Requires-Dist: orjson>=3.11.1
|
|
14
|
+
Requires-Dist: certifi>=2024.0.0
|
|
15
|
+
Provides-Extra: yaml
|
|
16
|
+
Requires-Dist: PyYAML>=6.0; extra == "yaml"
|
|
14
17
|
Provides-Extra: lint
|
|
15
18
|
Requires-Dist: ruff>=0.2.0; extra == "lint"
|
|
16
19
|
Provides-Extra: dev
|
|
@@ -266,8 +269,10 @@ l1c1-l1c6, severity: 3
|
|
|
266
269
|
|
|
267
270
|
#### lean_multi_attempt
|
|
268
271
|
|
|
269
|
-
Attempt multiple
|
|
270
|
-
|
|
272
|
+
Attempt multiple tactics on a line and return goal state and diagnostics for each.
|
|
273
|
+
Useful to screen different proof attempts before committing to one.
|
|
274
|
+
|
|
275
|
+
When `LEAN_REPL=true`, uses the REPL tactic mode for up to 5x faster execution (see [Environment Variables](#environment-variables)).
|
|
271
276
|
|
|
272
277
|
<details>
|
|
273
278
|
<summary>Example output (attempting `rw [Nat.pow_sub (Fintype.card_pos_of_nonempty S)]` and `by_contra h_neq`)</summary>
|
|
@@ -483,7 +488,12 @@ This MCP server works out-of-the-box without any configuration. However, a few o
|
|
|
483
488
|
### Environment Variables
|
|
484
489
|
|
|
485
490
|
- `LEAN_LOG_LEVEL`: Log level for the server. Options are "INFO", "WARNING", "ERROR", "NONE". Defaults to "INFO".
|
|
491
|
+
- `LEAN_LOG_FILE_CONFIG`: Config file path for logging, with priority over `LEAN_LOG_LEVEL`. If not set, logs are printed to stdout.
|
|
486
492
|
- `LEAN_PROJECT_PATH`: Path to your Lean project root. Set this if the server cannot automatically detect your project.
|
|
493
|
+
- `LEAN_REPL`: Set to `true`, `1`, or `yes` to enable fast REPL-based `lean_multi_attempt` (~5x faster, see [REPL Setup](#repl-setup)).
|
|
494
|
+
- `LEAN_REPL_PATH`: Path to the `repl` binary. Auto-detected from `.lake/packages/repl/` if not set.
|
|
495
|
+
- `LEAN_REPL_TIMEOUT`: Per-command timeout in seconds (default: 60).
|
|
496
|
+
- `LEAN_REPL_MEM_MB`: Max memory per REPL in MB (default: 8192). Only enforced on Linux/macOS.
|
|
487
497
|
- `LEAN_LSP_MCP_TOKEN`: Secret token for bearer authentication when using `streamable-http` or `sse` transport.
|
|
488
498
|
- `LEAN_STATE_SEARCH_URL`: URL for a self-hosted [premise-search.com](https://premise-search.com) instance.
|
|
489
499
|
- `LEAN_HAMMER_URL`: URL for a self-hosted [Lean Hammer Premise Search](https://github.com/hanwenzhu/lean-premise-server) instance.
|
|
@@ -544,6 +554,36 @@ uvx lean-lsp-mcp --transport streamable-http
|
|
|
544
554
|
|
|
545
555
|
Clients should then include the token in the `Authorization` header.
|
|
546
556
|
|
|
557
|
+
### REPL Setup
|
|
558
|
+
|
|
559
|
+
Enable fast REPL-based `lean_multi_attempt` (~5x faster). Uses [leanprover-community/repl](https://github.com/leanprover-community/repl) tactic mode.
|
|
560
|
+
|
|
561
|
+
**1. Add REPL to your Lean project's `lakefile.toml`:**
|
|
562
|
+
|
|
563
|
+
```toml
|
|
564
|
+
[[require]]
|
|
565
|
+
name = "repl"
|
|
566
|
+
git = "https://github.com/leanprover-community/repl"
|
|
567
|
+
rev = "v4.25.0" # Match your Lean version
|
|
568
|
+
```
|
|
569
|
+
|
|
570
|
+
**2. Build it:**
|
|
571
|
+
|
|
572
|
+
```bash
|
|
573
|
+
lake build repl
|
|
574
|
+
```
|
|
575
|
+
|
|
576
|
+
**3. Enable via CLI or environment variable:**
|
|
577
|
+
|
|
578
|
+
```bash
|
|
579
|
+
uvx lean-lsp-mcp --repl
|
|
580
|
+
|
|
581
|
+
# Or via environment variable
|
|
582
|
+
export LEAN_REPL=true
|
|
583
|
+
```
|
|
584
|
+
|
|
585
|
+
The REPL binary is auto-detected from `.lake/packages/repl/`. Falls back to LSP if not found.
|
|
586
|
+
|
|
547
587
|
### Local Loogle
|
|
548
588
|
|
|
549
589
|
Run loogle locally to avoid the remote API's rate limit (3 req/30s). First run takes ~5-10 minutes to build; subsequent runs start in seconds.
|
|
@@ -590,9 +630,15 @@ uv sync --all-extras
|
|
|
590
630
|
uv run pytest tests
|
|
591
631
|
```
|
|
592
632
|
|
|
593
|
-
## Publications using lean-lsp-mcp
|
|
633
|
+
## Publications and Formalization Projects using lean-lsp-mcp
|
|
594
634
|
|
|
595
635
|
- Ax-Prover: A Deep Reasoning Agentic Framework for Theorem Proving in Mathematics and Quantum Physics [arxiv](https://arxiv.org/abs/2510.12787)
|
|
636
|
+
- Numina-Lean-Agent: An Open and General Agentic Reasoning System for Formal Mathematics [arxiv](https://arxiv.org/abs/2601.14027) [github](https://github.com/project-numina/numina-lean-agent)
|
|
637
|
+
- A Group-Theoretic Approach to Shannon Capacity of Graphs and a Limit Theorem from Lattice Packings [github](https://github.com/jzuiddam/GroupTheoreticShannonCapacity/)
|
|
638
|
+
|
|
639
|
+
## Talks
|
|
640
|
+
|
|
641
|
+
lean-lsp-mcp: Tools for agentic interaction with Lean (Lean Together 2026) [youtube](https://www.youtube.com/watch?v=uttbYaTaF-E)
|
|
596
642
|
|
|
597
643
|
## Related Projects
|
|
598
644
|
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
lean_lsp_mcp/__init__.py,sha256=0D0XJqzMsM_4EFOrfXQsRhn_F5n3z_Dzc7_5urTwehs,1855
|
|
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=pAmgg-Lr3-t0f377SQP4ahtfsHAmAbfZ7dXfcQQv-Zg,1985
|
|
6
|
+
lean_lsp_mcp/loogle.py,sha256=PJ_z9InIk-3yJsWmkhVctPOYIbovD7Z9A3vrGuC1Em0,15774
|
|
7
|
+
lean_lsp_mcp/models.py,sha256=dyjM6m36PscEtBiLjGxDxeM5b0ELpRTw1SUayYDeOwA,7549
|
|
8
|
+
lean_lsp_mcp/outline_utils.py,sha256=i7xL27UO2rTT48IdKXkoMq5FVJNxyA3tPuQREOBf_gU,11105
|
|
9
|
+
lean_lsp_mcp/profile_utils.py,sha256=LwtBmgwrywPiNhWS6xK-_QF-FTy0uWkW6NCK5l5rCQI,8144
|
|
10
|
+
lean_lsp_mcp/repl.py,sha256=5a3JrfwU6lvZaPfsCzdCqTm2LrbGoXOdh9ov6RVkf84,9259
|
|
11
|
+
lean_lsp_mcp/search_utils.py,sha256=MLqKGe4bhEvyfFLIBCmiDxkbcH4O5J3vl9mWnRSb_v0,6801
|
|
12
|
+
lean_lsp_mcp/server.py,sha256=kXzKrmfuoaLwZy8SHQoILhaXbf2e2OoyEJi1G5lswzM,50051
|
|
13
|
+
lean_lsp_mcp/utils.py,sha256=dGv84a4E-szOkQVYtSE-q9GbawpiVk47qvrkTN-Clts,13478
|
|
14
|
+
lean_lsp_mcp-0.20.0.dist-info/licenses/LICENSE,sha256=CQlxnf0tQyoVrBE93JYvAUYxv6Z5Yg6sX0pwogOkFvo,1071
|
|
15
|
+
lean_lsp_mcp-0.20.0.dist-info/METADATA,sha256=apVisDrqIP-TAGv0bJKnssn4gikRt9la2Pcq_-A7Tzg,23168
|
|
16
|
+
lean_lsp_mcp-0.20.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
17
|
+
lean_lsp_mcp-0.20.0.dist-info/entry_points.txt,sha256=nQbvwctWkWD7I-2f4VrdVQBZYGUw8CnUnFC6QjXxOSE,51
|
|
18
|
+
lean_lsp_mcp-0.20.0.dist-info/top_level.txt,sha256=LGEK0lgMSNPIQ6mG8EO-adaZEGPi_0daDs004epOTF0,13
|
|
19
|
+
lean_lsp_mcp-0.20.0.dist-info/RECORD,,
|
|
@@ -1,18 +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=HgENbIe1nwIzKXqf_yYC44f7YQ8j47zJgKBYmmaD_UI,1994
|
|
6
|
-
lean_lsp_mcp/loogle.py,sha256=zUgnDWoTIqa4G6GXStAIxxJUR545YbU8Z-8KMjddKV0,15500
|
|
7
|
-
lean_lsp_mcp/models.py,sha256=dyjM6m36PscEtBiLjGxDxeM5b0ELpRTw1SUayYDeOwA,7549
|
|
8
|
-
lean_lsp_mcp/outline_utils.py,sha256=i7xL27UO2rTT48IdKXkoMq5FVJNxyA3tPuQREOBf_gU,11105
|
|
9
|
-
lean_lsp_mcp/profile_utils.py,sha256=LwtBmgwrywPiNhWS6xK-_QF-FTy0uWkW6NCK5l5rCQI,8144
|
|
10
|
-
lean_lsp_mcp/search_utils.py,sha256=MLqKGe4bhEvyfFLIBCmiDxkbcH4O5J3vl9mWnRSb_v0,6801
|
|
11
|
-
lean_lsp_mcp/server.py,sha256=VFdyRxHJsBM0sehHyvEkdG17HQl78MYgBgH8usBkpUU,45430
|
|
12
|
-
lean_lsp_mcp/utils.py,sha256=dGv84a4E-szOkQVYtSE-q9GbawpiVk47qvrkTN-Clts,13478
|
|
13
|
-
lean_lsp_mcp-0.19.1.dist-info/licenses/LICENSE,sha256=CQlxnf0tQyoVrBE93JYvAUYxv6Z5Yg6sX0pwogOkFvo,1071
|
|
14
|
-
lean_lsp_mcp-0.19.1.dist-info/METADATA,sha256=D2CgfgVE_jKV0Lnz3TDlbCgOdywCk48WXP1Bp9xLZNs,21274
|
|
15
|
-
lean_lsp_mcp-0.19.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
16
|
-
lean_lsp_mcp-0.19.1.dist-info/entry_points.txt,sha256=nQbvwctWkWD7I-2f4VrdVQBZYGUw8CnUnFC6QjXxOSE,51
|
|
17
|
-
lean_lsp_mcp-0.19.1.dist-info/top_level.txt,sha256=LGEK0lgMSNPIQ6mG8EO-adaZEGPi_0daDs004epOTF0,13
|
|
18
|
-
lean_lsp_mcp-0.19.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|