lean-lsp-mcp 0.11.3__py3-none-any.whl → 0.12.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.
@@ -34,7 +34,6 @@ def startup_client(ctx: Context):
34
34
  return # Client already set up correctly - reuse it!
35
35
  # Different project path - close old client
36
36
  client.close()
37
- ctx.request_context.lifespan_context.file_content_hashes.clear()
38
37
 
39
38
  # Need to create a new client
40
39
  with OutputCapture() as output:
@@ -1,13 +1,6 @@
1
- from typing import Optional, Dict
1
+ from typing import Optional
2
2
  from pathlib import Path
3
3
 
4
- from mcp.server.fastmcp import Context
5
- from mcp.server.fastmcp.utilities.logging import get_logger
6
- from leanclient import LeanLSPClient
7
-
8
-
9
- logger = get_logger(__name__)
10
-
11
4
 
12
5
  def get_relative_file_path(lean_project_path: Path, file_path: str) -> Optional[str]:
13
6
  """Convert path relative to project path.
@@ -21,25 +14,24 @@ def get_relative_file_path(lean_project_path: Path, file_path: str) -> Optional[
21
14
  """
22
15
  file_path_obj = Path(file_path)
23
16
 
24
- # Check if absolute path
17
+ # Absolute path under project
25
18
  if file_path_obj.is_absolute() and file_path_obj.exists():
26
19
  try:
27
20
  return str(file_path_obj.relative_to(lean_project_path))
28
21
  except ValueError:
29
- # File is not in this project
30
22
  return None
31
23
 
32
- # Check if relative to project path
24
+ # Relative to project path
33
25
  path = lean_project_path / file_path
34
26
  if path.exists():
35
27
  return str(path.relative_to(lean_project_path))
36
28
 
37
- # Check if relative to CWD
29
+ # Relative to CWD, but only if inside project root
38
30
  cwd = Path.cwd()
39
31
  path = cwd / file_path
40
32
  if path.exists():
41
33
  try:
42
- return str(path.relative_to(lean_project_path))
34
+ return str(path.resolve().relative_to(lean_project_path))
43
35
  except ValueError:
44
36
  return None
45
37
 
@@ -55,46 +47,3 @@ def get_file_contents(abs_path: str) -> str:
55
47
  continue
56
48
  with open(abs_path, "r", encoding=None) as f:
57
49
  return f.read()
58
-
59
-
60
- def update_file(ctx: Context, rel_path: str) -> str:
61
- """Update the file contents in the context.
62
- Args:
63
- ctx (Context): Context object.
64
- rel_path (str): Relative file path.
65
-
66
- Returns:
67
- str: Updated file contents.
68
- """
69
- # Get file contents and hash
70
- abs_path = ctx.request_context.lifespan_context.lean_project_path / rel_path
71
- file_content = get_file_contents(str(abs_path))
72
- hashed_file = hash(file_content)
73
-
74
- # Check if file_contents have changed
75
- file_content_hashes: Dict[str, str] = (
76
- ctx.request_context.lifespan_context.file_content_hashes
77
- )
78
- if rel_path not in file_content_hashes:
79
- file_content_hashes[rel_path] = hashed_file
80
- return file_content
81
-
82
- elif hashed_file == file_content_hashes[rel_path]:
83
- return file_content
84
-
85
- # Update file_contents
86
- file_content_hashes[rel_path] = hashed_file
87
-
88
- # Reload file in LSP
89
- client: LeanLSPClient = ctx.request_context.lifespan_context.client
90
- try:
91
- client.close_files([rel_path])
92
- except FileNotFoundError as e:
93
- logger.warning(
94
- f"Attempted to close file {rel_path} that wasn't open in LSP client: {e}"
95
- )
96
- except Exception as e:
97
- logger.error(
98
- f"Unexpected error closing file {rel_path}: {type(e).__name__}: {e}"
99
- )
100
- return file_content
lean_lsp_mcp/server.py CHANGED
@@ -23,7 +23,7 @@ from lean_lsp_mcp.client_utils import (
23
23
  startup_client,
24
24
  infer_project_path,
25
25
  )
26
- from lean_lsp_mcp.file_utils import get_file_contents, update_file
26
+ from lean_lsp_mcp.file_utils import get_file_contents
27
27
  from lean_lsp_mcp.instructions import INSTRUCTIONS
28
28
  from lean_lsp_mcp.search_utils import check_ripgrep_status, lean_local_search
29
29
  from lean_lsp_mcp.utils import (
@@ -51,7 +51,6 @@ _RG_AVAILABLE, _RG_MESSAGE = check_ripgrep_status()
51
51
  class AppContext:
52
52
  lean_project_path: Path | None
53
53
  client: LeanLSPClient | None
54
- file_content_hashes: Dict[str, str]
55
54
  rate_limit: Dict[str, List[int]]
56
55
  lean_search_available: bool
57
56
 
@@ -68,7 +67,6 @@ async def app_lifespan(server: FastMCP) -> AsyncIterator[AppContext]:
68
67
  context = AppContext(
69
68
  lean_project_path=lean_project_path,
70
69
  client=None,
71
- file_content_hashes={},
72
70
  rate_limit={
73
71
  "leansearch": [],
74
72
  "loogle": [],
@@ -166,7 +164,6 @@ async def lsp_build(
166
164
  if client:
167
165
  ctx.request_context.lifespan_context.client = None
168
166
  client.close()
169
- ctx.request_context.lifespan_context.file_content_hashes.clear()
170
167
 
171
168
  if clean:
172
169
  subprocess.run(["lake", "clean"], cwd=lean_project_path_obj, check=False)
@@ -271,13 +268,20 @@ def file_contents(ctx: Context, file_path: str, annotate_lines: bool = True) ->
271
268
 
272
269
 
273
270
  @mcp.tool("lean_diagnostic_messages")
274
- def diagnostic_messages(ctx: Context, file_path: str) -> List[str] | str:
271
+ def diagnostic_messages(
272
+ ctx: Context,
273
+ file_path: str,
274
+ start_line: Optional[int] = None,
275
+ end_line: Optional[int] = None,
276
+ ) -> List[str] | str:
275
277
  """Get all diagnostic msgs (errors, warnings, infos) for a Lean file.
276
278
 
277
279
  "no goals to be solved" means code may need removal.
278
280
 
279
281
  Args:
280
282
  file_path (str): Abs path to Lean file
283
+ start_line (int, optional): Start line (1-indexed) for filtering diagnostics. If provided, only diagnostics from this line onwards are returned.
284
+ end_line (int, optional): End line (1-indexed) for filtering diagnostics. If provided with start_line, only diagnostics in this range are returned.
281
285
 
282
286
  Returns:
283
287
  List[str] | str: Diagnostic msgs or error msg
@@ -286,10 +290,22 @@ def diagnostic_messages(ctx: Context, file_path: str) -> List[str] | str:
286
290
  if not rel_path:
287
291
  return "Invalid Lean file path: Unable to start LSP server or load file"
288
292
 
289
- update_file(ctx, rel_path)
290
-
291
293
  client: LeanLSPClient = ctx.request_context.lifespan_context.client
292
- diagnostics = client.get_diagnostics(rel_path)
294
+
295
+ # leanclient requires both start_line and end_line to filter
296
+ # Convert 1-indexed to 0-indexed for leanclient
297
+ if start_line is not None or end_line is not None:
298
+ start_line_0 = (start_line - 1) if start_line is not None else 0
299
+ end_line_0 = (end_line - 1) if end_line is not None else 999999
300
+ diagnostics = client.get_diagnostics(
301
+ rel_path,
302
+ start_line=start_line_0,
303
+ end_line=end_line_0,
304
+ inactivity_timeout=10.0,
305
+ )
306
+ else:
307
+ diagnostics = client.get_diagnostics(rel_path, inactivity_timeout=10.0)
308
+
293
309
  return format_diagnostics(diagnostics)
294
310
 
295
311
 
@@ -314,8 +330,9 @@ def goal(ctx: Context, file_path: str, line: int, column: Optional[int] = None)
314
330
  if not rel_path:
315
331
  return "Invalid Lean file path: Unable to start LSP server or load file"
316
332
 
317
- content = update_file(ctx, rel_path)
318
333
  client: LeanLSPClient = ctx.request_context.lifespan_context.client
334
+ client.open_file(rel_path)
335
+ content = client.get_file_content(rel_path)
319
336
 
320
337
  if column is None:
321
338
  lines = content.splitlines()
@@ -360,14 +377,15 @@ def term_goal(
360
377
  if not rel_path:
361
378
  return "Invalid Lean file path: Unable to start LSP server or load file"
362
379
 
363
- content = update_file(ctx, rel_path)
380
+ client: LeanLSPClient = ctx.request_context.lifespan_context.client
381
+ client.open_file(rel_path)
382
+ content = client.get_file_content(rel_path)
364
383
  if column is None:
365
384
  lines = content.splitlines()
366
385
  if line < 1 or line > len(lines):
367
386
  return "Line number out of range. Try elsewhere?"
368
387
  column = len(content.splitlines()[line - 1])
369
388
 
370
- client: LeanLSPClient = ctx.request_context.lifespan_context.client
371
389
  term_goal = client.get_term_goal(rel_path, line - 1, column - 1)
372
390
  f_line = format_line(content, line, column)
373
391
  if term_goal is None:
@@ -394,8 +412,9 @@ def hover(ctx: Context, file_path: str, line: int, column: int) -> str:
394
412
  if not rel_path:
395
413
  return "Invalid Lean file path: Unable to start LSP server or load file"
396
414
 
397
- file_content = update_file(ctx, rel_path)
398
415
  client: LeanLSPClient = ctx.request_context.lifespan_context.client
416
+ client.open_file(rel_path)
417
+ file_content = client.get_file_content(rel_path)
399
418
  hover_info = client.get_hover(rel_path, line - 1, column - 1)
400
419
  if hover_info is None:
401
420
  f_line = format_line(file_content, line, column)
@@ -440,9 +459,10 @@ def completions(
440
459
  rel_path = setup_client_for_file(ctx, file_path)
441
460
  if not rel_path:
442
461
  return "Invalid Lean file path: Unable to start LSP server or load file"
443
- content = update_file(ctx, rel_path)
444
462
 
445
463
  client: LeanLSPClient = ctx.request_context.lifespan_context.client
464
+ client.open_file(rel_path)
465
+ content = client.get_file_content(rel_path)
446
466
  completions = client.get_completions(rel_path, line - 1, column - 1)
447
467
  formatted = [c["label"] for c in completions if "label" in c]
448
468
  f_line = format_line(content, line, column)
@@ -502,14 +522,16 @@ def declaration_file(ctx: Context, file_path: str, symbol: str) -> str:
502
522
  rel_path = setup_client_for_file(ctx, file_path)
503
523
  if not rel_path:
504
524
  return "Invalid Lean file path: Unable to start LSP server or load file"
505
- orig_file_content = update_file(ctx, rel_path)
525
+
526
+ client: LeanLSPClient = ctx.request_context.lifespan_context.client
527
+ client.open_file(rel_path)
528
+ orig_file_content = client.get_file_content(rel_path)
506
529
 
507
530
  # Find the first occurence of the symbol (line and column) in the file,
508
531
  position = find_start_position(orig_file_content, symbol)
509
532
  if not position:
510
533
  return f"Symbol `{symbol}` (case sensitive) not found in file `{rel_path}`. Add it first, then try again."
511
534
 
512
- client: LeanLSPClient = ctx.request_context.lifespan_context.client
513
535
  declaration = client.get_declarations(
514
536
  rel_path, position["line"], position["column"]
515
537
  )
@@ -557,8 +579,9 @@ def multi_attempt(
557
579
  rel_path = setup_client_for_file(ctx, file_path)
558
580
  if not rel_path:
559
581
  return "Invalid Lean file path: Unable to start LSP server or load file"
560
- update_file(ctx, rel_path)
582
+
561
583
  client: LeanLSPClient = ctx.request_context.lifespan_context.client
584
+ client.open_file(rel_path)
562
585
 
563
586
  try:
564
587
  client.open_file(rel_path)
@@ -858,8 +881,9 @@ def state_search(
858
881
  if not rel_path:
859
882
  return "Invalid Lean file path: Unable to start LSP server or load file"
860
883
 
861
- file_contents = update_file(ctx, rel_path)
862
884
  client: LeanLSPClient = ctx.request_context.lifespan_context.client
885
+ client.open_file(rel_path)
886
+ file_contents = client.get_file_content(rel_path)
863
887
  goal = client.get_goal(rel_path, line - 1, column - 1)
864
888
 
865
889
  f_line = format_line(file_contents, line, column)
@@ -908,8 +932,9 @@ def hammer_premise(
908
932
  if not rel_path:
909
933
  return "Invalid Lean file path: Unable to start LSP server or load file"
910
934
 
911
- file_contents = update_file(ctx, rel_path)
912
935
  client: LeanLSPClient = ctx.request_context.lifespan_context.client
936
+ client.open_file(rel_path)
937
+ file_contents = client.get_file_content(rel_path)
913
938
  goal = client.get_goal(rel_path, line - 1, column - 1)
914
939
 
915
940
  f_line = format_line(file_contents, line, column)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lean-lsp-mcp
3
- Version: 0.11.3
3
+ Version: 0.12.0
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.4.0
12
- Requires-Dist: mcp[cli]==1.19.0
11
+ Requires-Dist: leanclient==0.5.0
12
+ Requires-Dist: mcp[cli]==1.21.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,14 @@
1
+ lean_lsp_mcp/__init__.py,sha256=lxqDq0G_sI2iu2Nniy-pTW7BE9Ux7ZXeDoGf0OAWIDc,763
2
+ lean_lsp_mcp/__main__.py,sha256=XnpTzfJc0T-j9tHtdkA8ovTr1c139ffTewcJGhxYDaM,49
3
+ lean_lsp_mcp/client_utils.py,sha256=hF941DEeRE3ICMgMhv9J4vv6bO4hZPJOAcFU03yIDXs,4988
4
+ lean_lsp_mcp/file_utils.py,sha256=kCTYQSfmV-R2cm_NCi_L8W5Dcsm0_rTOPpTtpyAin78,1365
5
+ lean_lsp_mcp/instructions.py,sha256=y_gHlbeJoKnPohmcSVrQQds6mbBO1en-lxnXAfEypZE,892
6
+ lean_lsp_mcp/search_utils.py,sha256=X2LPynDNLi767UDxbxHpMccOkbnfKJKv_HxvRNxIXM4,3984
7
+ lean_lsp_mcp/server.py,sha256=oxuytxr58QLS-lCVRxYgtlLGEW0uHPhw2gKC5oRChqk,35799
8
+ lean_lsp_mcp/utils.py,sha256=zLu2VIhaX4yocY07F3Z94LB2jRGrkH1ID9SjR3poE9A,8255
9
+ lean_lsp_mcp-0.12.0.dist-info/licenses/LICENSE,sha256=CQlxnf0tQyoVrBE93JYvAUYxv6Z5Yg6sX0pwogOkFvo,1071
10
+ lean_lsp_mcp-0.12.0.dist-info/METADATA,sha256=wMGQwNNQxVIt_hxZZgOZDIJ1E5wQmq2h4x9hRJrBgjw,19626
11
+ lean_lsp_mcp-0.12.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
12
+ lean_lsp_mcp-0.12.0.dist-info/entry_points.txt,sha256=nQbvwctWkWD7I-2f4VrdVQBZYGUw8CnUnFC6QjXxOSE,51
13
+ lean_lsp_mcp-0.12.0.dist-info/top_level.txt,sha256=LGEK0lgMSNPIQ6mG8EO-adaZEGPi_0daDs004epOTF0,13
14
+ lean_lsp_mcp-0.12.0.dist-info/RECORD,,
@@ -1,14 +0,0 @@
1
- lean_lsp_mcp/__init__.py,sha256=lxqDq0G_sI2iu2Nniy-pTW7BE9Ux7ZXeDoGf0OAWIDc,763
2
- lean_lsp_mcp/__main__.py,sha256=XnpTzfJc0T-j9tHtdkA8ovTr1c139ffTewcJGhxYDaM,49
3
- lean_lsp_mcp/client_utils.py,sha256=FNfiEWQagRj9tmo2xUDxbSZYVz5_RfaNH6OApgNQ9ZM,5065
4
- lean_lsp_mcp/file_utils.py,sha256=qddegF-T5-egZop8dPe_3Cma-3rRSKsAErVDQLecmbE,2916
5
- lean_lsp_mcp/instructions.py,sha256=y_gHlbeJoKnPohmcSVrQQds6mbBO1en-lxnXAfEypZE,892
6
- lean_lsp_mcp/search_utils.py,sha256=X2LPynDNLi767UDxbxHpMccOkbnfKJKv_HxvRNxIXM4,3984
7
- lean_lsp_mcp/server.py,sha256=zwiHeMlgRpERNIF-E_6OKzI8a-YmAx09EVX3QO2gJAk,34792
8
- lean_lsp_mcp/utils.py,sha256=zLu2VIhaX4yocY07F3Z94LB2jRGrkH1ID9SjR3poE9A,8255
9
- lean_lsp_mcp-0.11.3.dist-info/licenses/LICENSE,sha256=CQlxnf0tQyoVrBE93JYvAUYxv6Z5Yg6sX0pwogOkFvo,1071
10
- lean_lsp_mcp-0.11.3.dist-info/METADATA,sha256=1mf98hyaVVN1lZLWMZEdtSV6-nDZ7KAFyVLPRc7gbx8,19626
11
- lean_lsp_mcp-0.11.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
12
- lean_lsp_mcp-0.11.3.dist-info/entry_points.txt,sha256=nQbvwctWkWD7I-2f4VrdVQBZYGUw8CnUnFC6QjXxOSE,51
13
- lean_lsp_mcp-0.11.3.dist-info/top_level.txt,sha256=LGEK0lgMSNPIQ6mG8EO-adaZEGPi_0daDs004epOTF0,13
14
- lean_lsp_mcp-0.11.3.dist-info/RECORD,,