lean-lsp-mcp 0.16.1__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 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
+ )
@@ -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
- result = subprocess.run(command, capture_output=True, text=True, cwd=str(root))
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
- data = event["data"]
95
- parts = data["lines"]["text"].lstrip().split(maxsplit=2)
96
- if len(parts) < 2:
97
- continue
98
-
99
- decl_kind, decl_name = parts[0], parts[1].rstrip(":")
100
- file_path = Path(data["path"]["text"])
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
- display_path = str(abs_path.relative_to(root))
107
- except ValueError:
108
- display_path = str(file_path)
109
-
110
- matches.append({"name": decl_name, "kind": decl_kind, "file": display_path})
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
- if len(matches) >= limit:
113
- break
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
- if result.returncode not in (0, 1) and not matches:
116
- error_msg = f"ripgrep exited with code {result.returncode}"
117
- if result.stderr:
118
- error_msg += f"\n{result.stderr}"
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
@@ -12,7 +12,7 @@ import functools
12
12
  import uuid
13
13
  from pathlib import Path
14
14
 
15
- from pydantic import BaseModel, Field
15
+ from pydantic import Field
16
16
  from mcp.server.fastmcp import Context, FastMCP
17
17
  from mcp.server.fastmcp.utilities.logging import get_logger, configure_logging
18
18
  from mcp.server.auth.settings import AuthSettings
@@ -46,6 +46,16 @@ from lean_lsp_mcp.models import (
46
46
  BuildResult,
47
47
  RunResult,
48
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,
49
59
  )
50
60
  from lean_lsp_mcp.utils import (
51
61
  COMPLETION_KIND,
@@ -67,13 +77,6 @@ class LeanToolError(Exception):
67
77
  pass
68
78
 
69
79
 
70
- def _to_json_array(items: List[BaseModel]) -> str:
71
- """Serialize list of models as JSON array (avoids FastMCP list flattening)."""
72
- return orjson.dumps(
73
- [item.model_dump() for item in items], option=orjson.OPT_INDENT_2
74
- ).decode()
75
-
76
-
77
80
  _LOG_LEVEL = os.environ.get("LEAN_LOG_LEVEL", "INFO")
78
81
  configure_logging("CRITICAL" if _LOG_LEVEL == "NONE" else _LOG_LEVEL)
79
82
  logger = get_logger(__name__)
@@ -414,7 +417,7 @@ def diagnostic_messages(
414
417
  declaration_name: Annotated[
415
418
  Optional[str], Field(description="Filter to declaration (slow)")
416
419
  ] = None,
417
- ) -> str:
420
+ ) -> DiagnosticsResult:
418
421
  """Get compiler diagnostics (errors, warnings, infos) for a Lean file."""
419
422
  rel_path = setup_client_for_file(ctx, file_path)
420
423
  if not rel_path:
@@ -443,7 +446,7 @@ def diagnostic_messages(
443
446
  inactivity_timeout=15.0,
444
447
  )
445
448
 
446
- return _to_json_array(_to_diagnostic_messages(diagnostics))
449
+ return DiagnosticsResult(items=_to_diagnostic_messages(diagnostics))
447
450
 
448
451
 
449
452
  @mcp.tool(
@@ -610,7 +613,7 @@ def completions(
610
613
  line: Annotated[int, Field(description="Line number (1-indexed)", ge=1)],
611
614
  column: Annotated[int, Field(description="Column number (1-indexed)", ge=1)],
612
615
  max_completions: Annotated[int, Field(description="Max completions", ge=1)] = 32,
613
- ) -> str:
616
+ ) -> CompletionsResult:
614
617
  """Get IDE autocompletions. Use on INCOMPLETE code (after `.` or partial name)."""
615
618
  rel_path = setup_client_for_file(ctx, file_path)
616
619
  if not rel_path:
@@ -639,7 +642,7 @@ def completions(
639
642
  )
640
643
 
641
644
  if not items:
642
- return "[]"
645
+ return CompletionsResult(items=[])
643
646
 
644
647
  # Find the sort term: The last word/identifier before the cursor
645
648
  lines = content.splitlines()
@@ -666,7 +669,7 @@ def completions(
666
669
  items.sort(key=lambda x: x.label.lower())
667
670
 
668
671
  # Truncate if too many results
669
- return _to_json_array(items[:max_completions])
672
+ return CompletionsResult(items=items[:max_completions])
670
673
 
671
674
 
672
675
  @mcp.tool(
@@ -741,7 +744,7 @@ def multi_attempt(
741
744
  snippets: Annotated[
742
745
  List[str], Field(description="Tactics to try (3+ recommended)")
743
746
  ],
744
- ) -> str:
747
+ ) -> MultiAttemptResult:
745
748
  """Try multiple tactics without modifying file. Returns goal state for each."""
746
749
  rel_path = setup_client_for_file(ctx, file_path)
747
750
  if not rel_path:
@@ -753,8 +756,6 @@ def multi_attempt(
753
756
  client.open_file(rel_path)
754
757
 
755
758
  try:
756
- client.open_file(rel_path)
757
-
758
759
  results: List[AttemptResult] = []
759
760
  # Avoid mutating caller-provided snippets; normalize locally per attempt
760
761
  for snippet in snippets:
@@ -781,7 +782,7 @@ def multi_attempt(
781
782
  )
782
783
  )
783
784
 
784
- return _to_json_array(results)
785
+ return MultiAttemptResult(items=results)
785
786
  finally:
786
787
  try:
787
788
  client.close_files([rel_path])
@@ -878,7 +879,7 @@ def local_search(
878
879
  project_root: Annotated[
879
880
  Optional[str], Field(description="Project root (inferred if omitted)")
880
881
  ] = None,
881
- ) -> str:
882
+ ) -> LocalSearchResults:
882
883
  """Fast local search to verify declarations exist. Use BEFORE trying a lemma name."""
883
884
  if not _RG_AVAILABLE:
884
885
  raise LocalSearchError(_RG_MESSAGE)
@@ -910,7 +911,7 @@ def local_search(
910
911
  LocalSearchResult(name=r["name"], kind=r["kind"], file=r["file"])
911
912
  for r in raw_results
912
913
  ]
913
- return _to_json_array(results)
914
+ return LocalSearchResults(items=results)
914
915
  except RuntimeError as exc:
915
916
  raise LocalSearchError(f"Search failed: {exc}")
916
917
 
@@ -929,7 +930,7 @@ def leansearch(
929
930
  ctx: Context,
930
931
  query: Annotated[str, Field(description="Natural language or Lean term query")],
931
932
  num_results: Annotated[int, Field(description="Max results", ge=1)] = 5,
932
- ) -> str:
933
+ ) -> LeanSearchResults:
933
934
  """Search Mathlib via leansearch.net using natural language.
934
935
 
935
936
  Examples: "sum of two even numbers is even", "Cauchy-Schwarz inequality",
@@ -949,7 +950,7 @@ def leansearch(
949
950
  results = orjson.loads(response.read())
950
951
 
951
952
  if not results or not results[0]:
952
- return "[]"
953
+ return LeanSearchResults(items=[])
953
954
 
954
955
  raw_results = [r["result"] for r in results[0][:num_results]]
955
956
  items = [
@@ -961,7 +962,7 @@ def leansearch(
961
962
  )
962
963
  for r in raw_results
963
964
  ]
964
- return _to_json_array(items)
965
+ return LeanSearchResults(items=items)
965
966
 
966
967
 
967
968
  @mcp.tool(
@@ -979,7 +980,7 @@ async def loogle(
979
980
  str, Field(description="Type pattern, constant, or name substring")
980
981
  ],
981
982
  num_results: Annotated[int, Field(description="Max results", ge=1)] = 8,
982
- ) -> str:
983
+ ) -> LoogleResults:
983
984
  """Search Mathlib by type signature via loogle.lean-lang.org.
984
985
 
985
986
  Examples: `Real.sin`, `"comm"`, `(?a → ?b) → List ?a → List ?b`,
@@ -992,7 +993,7 @@ async def loogle(
992
993
  try:
993
994
  results = await app_ctx.loogle_manager.query(query, num_results)
994
995
  if not results:
995
- return "No results found."
996
+ return LoogleResults(items=[])
996
997
  items = [
997
998
  LoogleResult(
998
999
  name=r.get("name", ""),
@@ -1001,7 +1002,7 @@ async def loogle(
1001
1002
  )
1002
1003
  for r in results
1003
1004
  ]
1004
- return _to_json_array(items)
1005
+ return LoogleResults(items=items)
1005
1006
  except Exception as e:
1006
1007
  logger.warning(f"Local loogle failed: {e}, falling back to remote")
1007
1008
 
@@ -1010,13 +1011,15 @@ async def loogle(
1010
1011
  now = int(time.time())
1011
1012
  rate_limit[:] = [t for t in rate_limit if now - t < 30]
1012
1013
  if len(rate_limit) >= 3:
1013
- return "Rate limit exceeded: 3 requests per 30s. Use --loogle-local to avoid limits."
1014
+ raise LeanToolError(
1015
+ "Rate limit exceeded: 3 requests per 30s. Use --loogle-local to avoid limits."
1016
+ )
1014
1017
  rate_limit.append(now)
1015
1018
 
1016
1019
  result = loogle_remote(query, num_results)
1017
1020
  if isinstance(result, str):
1018
- return result # Error message
1019
- return _to_json_array(result)
1021
+ raise LeanToolError(result) # Error message from remote
1022
+ return LoogleResults(items=result)
1020
1023
 
1021
1024
 
1022
1025
  @mcp.tool(
@@ -1033,7 +1036,7 @@ def leanfinder(
1033
1036
  ctx: Context,
1034
1037
  query: Annotated[str, Field(description="Mathematical concept or proof state")],
1035
1038
  num_results: Annotated[int, Field(description="Max results", ge=1)] = 5,
1036
- ) -> str:
1039
+ ) -> LeanFinderResults:
1037
1040
  """Semantic search by mathematical meaning via Lean Finder.
1038
1041
 
1039
1042
  Examples: "commutativity of addition on natural numbers",
@@ -1065,7 +1068,7 @@ def leanfinder(
1065
1068
  )
1066
1069
  )
1067
1070
 
1068
- return _to_json_array(results)
1071
+ return LeanFinderResults(items=results)
1069
1072
 
1070
1073
 
1071
1074
  @mcp.tool(
@@ -1084,7 +1087,7 @@ def state_search(
1084
1087
  line: Annotated[int, Field(description="Line number (1-indexed)", ge=1)],
1085
1088
  column: Annotated[int, Field(description="Column number (1-indexed)", ge=1)],
1086
1089
  num_results: Annotated[int, Field(description="Max results", ge=1)] = 5,
1087
- ) -> str:
1090
+ ) -> StateSearchResults:
1088
1091
  """Find lemmas to close the goal at a position. Searches premise-search.com."""
1089
1092
  rel_path = setup_client_for_file(ctx, file_path)
1090
1093
  if not rel_path:
@@ -1114,7 +1117,7 @@ def state_search(
1114
1117
  results = orjson.loads(response.read())
1115
1118
 
1116
1119
  items = [StateSearchResult(name=r["name"]) for r in results]
1117
- return _to_json_array(items)
1120
+ return StateSearchResults(items=items)
1118
1121
 
1119
1122
 
1120
1123
  @mcp.tool(
@@ -1133,7 +1136,7 @@ def hammer_premise(
1133
1136
  line: Annotated[int, Field(description="Line number (1-indexed)", ge=1)],
1134
1137
  column: Annotated[int, Field(description="Column number (1-indexed)", ge=1)],
1135
1138
  num_results: Annotated[int, Field(description="Max results", ge=1)] = 32,
1136
- ) -> str:
1139
+ ) -> PremiseResults:
1137
1140
  """Get premise suggestions for automation tactics at a goal position.
1138
1141
 
1139
1142
  Returns lemma names to try with `simp only [...]`, `aesop`, or as hints.
@@ -1174,7 +1177,7 @@ def hammer_premise(
1174
1177
  results = orjson.loads(response.read())
1175
1178
 
1176
1179
  items = [PremiseResult(name=r["name"]) for r in results]
1177
- return _to_json_array(items)
1180
+ return PremiseResults(items=items)
1178
1181
 
1179
1182
 
1180
1183
  if __name__ == "__main__":
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lean-lsp-mcp
3
- Version: 0.16.1
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.1
12
- Requires-Dist: mcp[cli]==1.23.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=M8CmTg0_NL7KwcQ7UX_Zk7ZG1zXoWLINr41NPs_no2Y,4301
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=X2LPynDNLi767UDxbxHpMccOkbnfKJKv_HxvRNxIXM4,3984
10
- lean_lsp_mcp/server.py,sha256=8QEkRlbLEKif4NfagD2l1min1w1IFF7Jw1TUUOK-xFg,39481
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.1.dist-info/licenses/LICENSE,sha256=CQlxnf0tQyoVrBE93JYvAUYxv6Z5Yg6sX0pwogOkFvo,1071
13
- lean_lsp_mcp-0.16.1.dist-info/METADATA,sha256=nwfjUYlagiUb49IkT3AWoOmJwH4fnwJ5ElyvD9V8zGw,20819
14
- lean_lsp_mcp-0.16.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
15
- lean_lsp_mcp-0.16.1.dist-info/entry_points.txt,sha256=nQbvwctWkWD7I-2f4VrdVQBZYGUw8CnUnFC6QjXxOSE,51
16
- lean_lsp_mcp-0.16.1.dist-info/top_level.txt,sha256=LGEK0lgMSNPIQ6mG8EO-adaZEGPi_0daDs004epOTF0,13
17
- lean_lsp_mcp-0.16.1.dist-info/RECORD,,
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,,