lean-lsp-mcp 0.11.2__py3-none-any.whl → 0.11.3__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.
@@ -64,59 +64,75 @@ def valid_lean_project_path(path: Path | str) -> bool:
64
64
  return (path_obj / "lean-toolchain").is_file()
65
65
 
66
66
 
67
- def setup_client_for_file(ctx: Context, file_path: str) -> str | None:
68
- """Ensure the LSP client matches the file's Lean project and return its relative path."""
67
+ def infer_project_path(ctx: Context, file_path: str) -> Path | None:
68
+ """Infer and cache the Lean project path for a file WITHOUT starting the client.
69
+
70
+ Walks up the directory tree to find a lean-toolchain file, caches the result.
71
+ Sets ctx.request_context.lifespan_context.lean_project_path if found.
72
+
73
+ Side effects when path changes:
74
+ - Next LSP tool will restart the client for the new project
75
+ - File content hashes will be cleared
69
76
 
77
+ Args:
78
+ ctx (Context): Context object
79
+ file_path (str): Absolute or relative path to a Lean file
80
+
81
+ Returns:
82
+ Path | None: The resolved project path if found, None otherwise
83
+ """
70
84
  lifespan = ctx.request_context.lifespan_context
71
- project_cache = getattr(lifespan, "project_cache", {})
72
85
  if not hasattr(lifespan, "project_cache"):
73
- lifespan.project_cache = project_cache
86
+ lifespan.project_cache = {}
74
87
 
75
88
  abs_file_path = os.path.abspath(file_path)
76
89
  file_dir = os.path.dirname(abs_file_path)
77
90
 
78
- def activate_project(project_path: Path, cache_dirs: list[str]) -> str | None:
79
- project_path_obj = project_path
80
- rel = get_relative_file_path(project_path_obj, file_path)
81
- if rel is None:
91
+ def set_project_path(project_path: Path, cache_dirs: list[str]) -> Path | None:
92
+ """Validate file is in project, set path, update cache."""
93
+ if get_relative_file_path(project_path, file_path) is None:
82
94
  return None
83
95
 
84
- project_path_obj = project_path_obj.resolve()
85
- lifespan.lean_project_path = project_path_obj
96
+ project_path = project_path.resolve()
97
+ lifespan.lean_project_path = project_path
86
98
 
87
- cache_targets: list[str] = []
88
- for directory in cache_dirs + [str(project_path_obj)]:
89
- if directory and directory not in cache_targets:
90
- cache_targets.append(directory)
99
+ # Update all relevant directories in cache
100
+ for directory in set(cache_dirs + [str(project_path)]):
101
+ if directory:
102
+ lifespan.project_cache[directory] = project_path
91
103
 
92
- for directory in cache_targets:
93
- project_cache[directory] = project_path_obj
104
+ return project_path
94
105
 
95
- startup_client(ctx)
96
- return rel
106
+ # Fast path: current project already valid for this file
107
+ if lifespan.lean_project_path and set_project_path(
108
+ lifespan.lean_project_path, [file_dir]
109
+ ):
110
+ return lifespan.lean_project_path
97
111
 
98
- # Fast path: current Lean project already valid for this file
99
- if lifespan.lean_project_path is not None:
100
- rel_path = activate_project(lifespan.lean_project_path, [file_dir])
101
- if rel_path is not None:
102
- return rel_path
103
-
104
- # Walk up from file directory to root, using cache hits or lean-toolchain
105
- prev_dir = None
112
+ # Walk up directory tree using cache and lean-toolchain detection
106
113
  current_dir = file_dir
107
- while current_dir and current_dir != prev_dir:
108
- cached_root = project_cache.get(current_dir)
114
+ while current_dir and current_dir != os.path.dirname(current_dir):
115
+ cached_root = lifespan.project_cache.get(current_dir)
116
+
109
117
  if cached_root:
110
- rel_path = activate_project(Path(cached_root), [current_dir])
111
- if rel_path is not None:
112
- return rel_path
118
+ if result := set_project_path(Path(cached_root), [current_dir]):
119
+ return result
113
120
  elif valid_lean_project_path(current_dir):
114
- rel_path = activate_project(Path(current_dir), [current_dir])
115
- if rel_path is not None:
116
- return rel_path
121
+ if result := set_project_path(Path(current_dir), [current_dir]):
122
+ return result
117
123
  else:
118
- project_cache[current_dir] = ""
119
- prev_dir = current_dir
124
+ lifespan.project_cache[current_dir] = "" # Mark as checked
125
+
120
126
  current_dir = os.path.dirname(current_dir)
121
127
 
122
128
  return None
129
+
130
+
131
+ def setup_client_for_file(ctx: Context, file_path: str) -> str | None:
132
+ """Ensure the LSP client matches the file's Lean project and return its relative path."""
133
+ project_path = infer_project_path(ctx, file_path)
134
+ if project_path is None:
135
+ return None
136
+
137
+ startup_client(ctx)
138
+ return get_relative_file_path(project_path, file_path)
lean_lsp_mcp/server.py CHANGED
@@ -18,7 +18,11 @@ from mcp.server.fastmcp.utilities.logging import get_logger, configure_logging
18
18
  from mcp.server.auth.settings import AuthSettings
19
19
  from leanclient import LeanLSPClient, DocumentContentChange
20
20
 
21
- from lean_lsp_mcp.client_utils import setup_client_for_file, startup_client
21
+ from lean_lsp_mcp.client_utils import (
22
+ setup_client_for_file,
23
+ startup_client,
24
+ infer_project_path,
25
+ )
22
26
  from lean_lsp_mcp.file_utils import get_file_contents, update_file
23
27
  from lean_lsp_mcp.instructions import INSTRUCTIONS
24
28
  from lean_lsp_mcp.search_utils import check_ripgrep_status, lean_local_search
@@ -154,10 +158,7 @@ async def lsp_build(
154
158
  ctx.request_context.lifespan_context.lean_project_path = lean_project_path_obj
155
159
 
156
160
  if lean_project_path_obj is None:
157
- return (
158
- "Lean project path not known yet. Provide `lean_project_path` explicitly or call a "
159
- "tool that infers it (e.g. `lean_goal`) before running `lean_build`."
160
- )
161
+ return "Lean project path not known yet. Provide `lean_project_path` explicitly or call a tool that infers it (e.g. `lean_file_contents`) before running `lean_build`."
161
162
 
162
163
  build_output = ""
163
164
  try:
@@ -247,6 +248,10 @@ def file_contents(ctx: Context, file_path: str, annotate_lines: bool = True) ->
247
248
  Returns:
248
249
  str: File content or error msg
249
250
  """
251
+ # Infer project path but do not start a client
252
+ if file_path.endswith(".lean"):
253
+ infer_project_path(ctx, file_path) # Silently fails for non-project files
254
+
250
255
  try:
251
256
  data = get_file_contents(file_path)
252
257
  except FileNotFoundError:
@@ -604,7 +609,7 @@ def run_code(ctx: Context, code: str) -> List[str] | str:
604
609
  lifespan_context = ctx.request_context.lifespan_context
605
610
  lean_project_path = lifespan_context.lean_project_path
606
611
  if lean_project_path is None:
607
- return "No valid Lean project path found. Run another tool (e.g. `lean_diagnostic_messages`) first to set it up or set the LEAN_PROJECT_PATH environment variable."
612
+ return "No valid Lean project path found. Run another tool (e.g. `lean_file_contents`) first to set it up."
608
613
 
609
614
  # Use a unique snippet filename to avoid collisions under concurrency
610
615
  rel_path = f"_mcp_snippet_{uuid.uuid4().hex}.lean"
@@ -684,7 +689,7 @@ def local_search(
684
689
 
685
690
  stored_root = ctx.request_context.lifespan_context.lean_project_path
686
691
  if stored_root is None:
687
- return "Lean project path not set. Call a file-based tool (like lean_goal) first to set the project path."
692
+ return "Lean project path not set. Call a file-based tool (like lean_file_contents) first to set the project path."
688
693
 
689
694
  return lean_local_search(query=query.strip(), limit=limit, project_root=stored_root)
690
695
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lean-lsp-mcp
3
- Version: 0.11.2
3
+ Version: 0.11.3
4
4
  Summary: Lean Theorem Prover MCP
5
5
  Author-email: Oliver Dressler <hey@oli.show>
6
6
  License-Expression: MIT
@@ -1,14 +1,14 @@
1
1
  lean_lsp_mcp/__init__.py,sha256=lxqDq0G_sI2iu2Nniy-pTW7BE9Ux7ZXeDoGf0OAWIDc,763
2
2
  lean_lsp_mcp/__main__.py,sha256=XnpTzfJc0T-j9tHtdkA8ovTr1c139ffTewcJGhxYDaM,49
3
- lean_lsp_mcp/client_utils.py,sha256=Z-2AoFBA36UO8oeauTNUtquE6Yz_ZRTKz_9n1iapyJ4,4519
3
+ lean_lsp_mcp/client_utils.py,sha256=FNfiEWQagRj9tmo2xUDxbSZYVz5_RfaNH6OApgNQ9ZM,5065
4
4
  lean_lsp_mcp/file_utils.py,sha256=qddegF-T5-egZop8dPe_3Cma-3rRSKsAErVDQLecmbE,2916
5
5
  lean_lsp_mcp/instructions.py,sha256=y_gHlbeJoKnPohmcSVrQQds6mbBO1en-lxnXAfEypZE,892
6
6
  lean_lsp_mcp/search_utils.py,sha256=X2LPynDNLi767UDxbxHpMccOkbnfKJKv_HxvRNxIXM4,3984
7
- lean_lsp_mcp/server.py,sha256=knA8HsJZeUNnMqNdv2rnAnS__Eblr0aPnlwKH1FrLbA,34661
7
+ lean_lsp_mcp/server.py,sha256=zwiHeMlgRpERNIF-E_6OKzI8a-YmAx09EVX3QO2gJAk,34792
8
8
  lean_lsp_mcp/utils.py,sha256=zLu2VIhaX4yocY07F3Z94LB2jRGrkH1ID9SjR3poE9A,8255
9
- lean_lsp_mcp-0.11.2.dist-info/licenses/LICENSE,sha256=CQlxnf0tQyoVrBE93JYvAUYxv6Z5Yg6sX0pwogOkFvo,1071
10
- lean_lsp_mcp-0.11.2.dist-info/METADATA,sha256=2sGVG-wCFYATL2iKhQ7Twt_6jcvPumAk7P74y_2uCto,19626
11
- lean_lsp_mcp-0.11.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
12
- lean_lsp_mcp-0.11.2.dist-info/entry_points.txt,sha256=nQbvwctWkWD7I-2f4VrdVQBZYGUw8CnUnFC6QjXxOSE,51
13
- lean_lsp_mcp-0.11.2.dist-info/top_level.txt,sha256=LGEK0lgMSNPIQ6mG8EO-adaZEGPi_0daDs004epOTF0,13
14
- lean_lsp_mcp-0.11.2.dist-info/RECORD,,
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,,