lean-lsp-mcp 0.16.0__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
@@ -9,11 +9,10 @@ from dataclasses import dataclass
9
9
  import urllib
10
10
  import orjson
11
11
  import functools
12
- import subprocess
13
12
  import uuid
14
13
  from pathlib import Path
15
14
 
16
- from pydantic import BaseModel, Field
15
+ from pydantic import Field
17
16
  from mcp.server.fastmcp import Context, FastMCP
18
17
  from mcp.server.fastmcp.utilities.logging import get_logger, configure_logging
19
18
  from mcp.server.auth.settings import AuthSettings
@@ -47,6 +46,16 @@ from lean_lsp_mcp.models import (
47
46
  BuildResult,
48
47
  RunResult,
49
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,
50
59
  )
51
60
  from lean_lsp_mcp.utils import (
52
61
  COMPLETION_KIND,
@@ -68,13 +77,6 @@ class LeanToolError(Exception):
68
77
  pass
69
78
 
70
79
 
71
- def _to_json_array(items: List[BaseModel]) -> str:
72
- """Serialize list of models as JSON array (avoids FastMCP list flattening)."""
73
- return orjson.dumps(
74
- [item.model_dump() for item in items], option=orjson.OPT_INDENT_2
75
- ).decode()
76
-
77
-
78
80
  _LOG_LEVEL = os.environ.get("LEAN_LOG_LEVEL", "INFO")
79
81
  configure_logging("CRITICAL" if _LOG_LEVEL == "NONE" else _LOG_LEVEL)
80
82
  logger = get_logger(__name__)
@@ -196,7 +198,7 @@ def rate_limited(category: str, max_requests: int, per_seconds: int):
196
198
  annotations=ToolAnnotations(
197
199
  title="Build Project",
198
200
  readOnlyHint=False,
199
- destructiveHint=False,
201
+ destructiveHint=True,
200
202
  idempotentHint=True,
201
203
  openWorldHint=False,
202
204
  ),
@@ -207,6 +209,9 @@ async def lsp_build(
207
209
  Optional[str], Field(description="Path to Lean project")
208
210
  ] = None,
209
211
  clean: Annotated[bool, Field(description="Run lake clean first (slow)")] = False,
212
+ output_lines: Annotated[
213
+ int, Field(description="Return last N lines of build log (0=none)")
214
+ ] = 20,
210
215
  ) -> BuildResult:
211
216
  """Build the Lean project and restart LSP. Use only if needed (e.g. new imports)."""
212
217
  if not lean_project_path:
@@ -220,7 +225,7 @@ async def lsp_build(
220
225
  "Lean project path not known yet. Provide `lean_project_path` explicitly or call another tool first."
221
226
  )
222
227
 
223
- output_lines: List[str] = []
228
+ log_lines: List[str] = []
224
229
  errors: List[str] = []
225
230
 
226
231
  try:
@@ -230,13 +235,21 @@ async def lsp_build(
230
235
  client.close()
231
236
 
232
237
  if clean:
233
- subprocess.run(["lake", "clean"], cwd=lean_project_path_obj, check=False)
234
- logger.info("Ran `lake clean`")
238
+ await ctx.report_progress(
239
+ progress=1, total=16, message="Running `lake clean`"
240
+ )
241
+ clean_proc = await asyncio.create_subprocess_exec(
242
+ "lake", "clean", cwd=lean_project_path_obj
243
+ )
244
+ await clean_proc.wait()
235
245
 
236
- # Fetch cache
237
- subprocess.run(
238
- ["lake", "exe", "cache", "get"], cwd=lean_project_path_obj, check=False
246
+ await ctx.report_progress(
247
+ progress=2, total=16, message="Running `lake exe cache get`"
239
248
  )
249
+ cache_proc = await asyncio.create_subprocess_exec(
250
+ "lake", "exe", "cache", "get", cwd=lean_project_path_obj
251
+ )
252
+ await cache_proc.wait()
240
253
 
241
254
  # Run build with progress reporting
242
255
  process = await asyncio.create_subprocess_exec(
@@ -248,32 +261,24 @@ async def lsp_build(
248
261
  stderr=asyncio.subprocess.STDOUT,
249
262
  )
250
263
 
251
- while True:
252
- line = await process.stdout.readline()
253
- if not line:
254
- break
255
-
264
+ while line := await process.stdout.readline():
256
265
  line_str = line.decode("utf-8", errors="replace").rstrip()
257
- output_lines.append(line_str)
258
266
 
259
- # Collect error lines
267
+ if line_str.startswith("trace:") or "LEAN_PATH=" in line_str:
268
+ continue
269
+
270
+ log_lines.append(line_str)
260
271
  if "error" in line_str.lower():
261
272
  errors.append(line_str)
262
273
 
263
- # Parse progress: look for pattern like "[2/8]" or "[10/100]"
264
- match = re.search(r"\[(\d+)/(\d+)\]", line_str)
265
- if match:
266
- current_job = int(match.group(1))
267
- total_jobs = int(match.group(2))
268
-
269
- # Extract what's being built
270
- desc_match = re.search(
271
- r"\[\d+/\d+\]\s+(.+?)(?:\s+\(\d+\.?\d*[ms]+\))?$", line_str
272
- )
273
- description = desc_match.group(1) if desc_match else "Building"
274
-
274
+ # Parse progress: "[2/8] Building Foo (1.2s)" -> (2, 8, "Building Foo")
275
+ if m := re.search(
276
+ r"\[(\d+)/(\d+)\]\s*(.+?)(?:\s+\(\d+\.?\d*[ms]+\))?$", line_str
277
+ ):
275
278
  await ctx.report_progress(
276
- progress=current_job, total=total_jobs, message=description
279
+ progress=int(m.group(1)),
280
+ total=int(m.group(2)),
281
+ message=m.group(3) or "Building",
277
282
  )
278
283
 
279
284
  await process.wait()
@@ -281,7 +286,7 @@ async def lsp_build(
281
286
  if process.returncode != 0:
282
287
  return BuildResult(
283
288
  success=False,
284
- output="\n".join(output_lines),
289
+ output="\n".join(log_lines[-output_lines:]) if output_lines else "",
285
290
  errors=errors
286
291
  or [f"Build failed with return code {process.returncode}"],
287
292
  )
@@ -295,12 +300,16 @@ async def lsp_build(
295
300
  logger.info("Built project and re-started LSP client")
296
301
  ctx.request_context.lifespan_context.client = client
297
302
 
298
- return BuildResult(success=True, output="\n".join(output_lines), errors=[])
303
+ return BuildResult(
304
+ success=True,
305
+ output="\n".join(log_lines[-output_lines:]) if output_lines else "",
306
+ errors=[],
307
+ )
299
308
 
300
309
  except Exception as e:
301
310
  return BuildResult(
302
311
  success=False,
303
- output="\n".join(output_lines),
312
+ output="\n".join(log_lines[-output_lines:]) if output_lines else "",
304
313
  errors=[str(e)],
305
314
  )
306
315
 
@@ -408,7 +417,7 @@ def diagnostic_messages(
408
417
  declaration_name: Annotated[
409
418
  Optional[str], Field(description="Filter to declaration (slow)")
410
419
  ] = None,
411
- ) -> str:
420
+ ) -> DiagnosticsResult:
412
421
  """Get compiler diagnostics (errors, warnings, infos) for a Lean file."""
413
422
  rel_path = setup_client_for_file(ctx, file_path)
414
423
  if not rel_path:
@@ -437,7 +446,7 @@ def diagnostic_messages(
437
446
  inactivity_timeout=15.0,
438
447
  )
439
448
 
440
- return _to_json_array(_to_diagnostic_messages(diagnostics))
449
+ return DiagnosticsResult(items=_to_diagnostic_messages(diagnostics))
441
450
 
442
451
 
443
452
  @mcp.tool(
@@ -604,7 +613,7 @@ def completions(
604
613
  line: Annotated[int, Field(description="Line number (1-indexed)", ge=1)],
605
614
  column: Annotated[int, Field(description="Column number (1-indexed)", ge=1)],
606
615
  max_completions: Annotated[int, Field(description="Max completions", ge=1)] = 32,
607
- ) -> str:
616
+ ) -> CompletionsResult:
608
617
  """Get IDE autocompletions. Use on INCOMPLETE code (after `.` or partial name)."""
609
618
  rel_path = setup_client_for_file(ctx, file_path)
610
619
  if not rel_path:
@@ -633,7 +642,7 @@ def completions(
633
642
  )
634
643
 
635
644
  if not items:
636
- return "[]"
645
+ return CompletionsResult(items=[])
637
646
 
638
647
  # Find the sort term: The last word/identifier before the cursor
639
648
  lines = content.splitlines()
@@ -660,7 +669,7 @@ def completions(
660
669
  items.sort(key=lambda x: x.label.lower())
661
670
 
662
671
  # Truncate if too many results
663
- return _to_json_array(items[:max_completions])
672
+ return CompletionsResult(items=items[:max_completions])
664
673
 
665
674
 
666
675
  @mcp.tool(
@@ -735,7 +744,7 @@ def multi_attempt(
735
744
  snippets: Annotated[
736
745
  List[str], Field(description="Tactics to try (3+ recommended)")
737
746
  ],
738
- ) -> str:
747
+ ) -> MultiAttemptResult:
739
748
  """Try multiple tactics without modifying file. Returns goal state for each."""
740
749
  rel_path = setup_client_for_file(ctx, file_path)
741
750
  if not rel_path:
@@ -747,8 +756,6 @@ def multi_attempt(
747
756
  client.open_file(rel_path)
748
757
 
749
758
  try:
750
- client.open_file(rel_path)
751
-
752
759
  results: List[AttemptResult] = []
753
760
  # Avoid mutating caller-provided snippets; normalize locally per attempt
754
761
  for snippet in snippets:
@@ -775,7 +782,7 @@ def multi_attempt(
775
782
  )
776
783
  )
777
784
 
778
- return _to_json_array(results)
785
+ return MultiAttemptResult(items=results)
779
786
  finally:
780
787
  try:
781
788
  client.close_files([rel_path])
@@ -872,7 +879,7 @@ def local_search(
872
879
  project_root: Annotated[
873
880
  Optional[str], Field(description="Project root (inferred if omitted)")
874
881
  ] = None,
875
- ) -> str:
882
+ ) -> LocalSearchResults:
876
883
  """Fast local search to verify declarations exist. Use BEFORE trying a lemma name."""
877
884
  if not _RG_AVAILABLE:
878
885
  raise LocalSearchError(_RG_MESSAGE)
@@ -904,7 +911,7 @@ def local_search(
904
911
  LocalSearchResult(name=r["name"], kind=r["kind"], file=r["file"])
905
912
  for r in raw_results
906
913
  ]
907
- return _to_json_array(results)
914
+ return LocalSearchResults(items=results)
908
915
  except RuntimeError as exc:
909
916
  raise LocalSearchError(f"Search failed: {exc}")
910
917
 
@@ -923,7 +930,7 @@ def leansearch(
923
930
  ctx: Context,
924
931
  query: Annotated[str, Field(description="Natural language or Lean term query")],
925
932
  num_results: Annotated[int, Field(description="Max results", ge=1)] = 5,
926
- ) -> str:
933
+ ) -> LeanSearchResults:
927
934
  """Search Mathlib via leansearch.net using natural language.
928
935
 
929
936
  Examples: "sum of two even numbers is even", "Cauchy-Schwarz inequality",
@@ -943,7 +950,7 @@ def leansearch(
943
950
  results = orjson.loads(response.read())
944
951
 
945
952
  if not results or not results[0]:
946
- return "[]"
953
+ return LeanSearchResults(items=[])
947
954
 
948
955
  raw_results = [r["result"] for r in results[0][:num_results]]
949
956
  items = [
@@ -955,7 +962,7 @@ def leansearch(
955
962
  )
956
963
  for r in raw_results
957
964
  ]
958
- return _to_json_array(items)
965
+ return LeanSearchResults(items=items)
959
966
 
960
967
 
961
968
  @mcp.tool(
@@ -973,7 +980,7 @@ async def loogle(
973
980
  str, Field(description="Type pattern, constant, or name substring")
974
981
  ],
975
982
  num_results: Annotated[int, Field(description="Max results", ge=1)] = 8,
976
- ) -> str:
983
+ ) -> LoogleResults:
977
984
  """Search Mathlib by type signature via loogle.lean-lang.org.
978
985
 
979
986
  Examples: `Real.sin`, `"comm"`, `(?a → ?b) → List ?a → List ?b`,
@@ -986,7 +993,7 @@ async def loogle(
986
993
  try:
987
994
  results = await app_ctx.loogle_manager.query(query, num_results)
988
995
  if not results:
989
- return "No results found."
996
+ return LoogleResults(items=[])
990
997
  items = [
991
998
  LoogleResult(
992
999
  name=r.get("name", ""),
@@ -995,7 +1002,7 @@ async def loogle(
995
1002
  )
996
1003
  for r in results
997
1004
  ]
998
- return _to_json_array(items)
1005
+ return LoogleResults(items=items)
999
1006
  except Exception as e:
1000
1007
  logger.warning(f"Local loogle failed: {e}, falling back to remote")
1001
1008
 
@@ -1004,13 +1011,15 @@ async def loogle(
1004
1011
  now = int(time.time())
1005
1012
  rate_limit[:] = [t for t in rate_limit if now - t < 30]
1006
1013
  if len(rate_limit) >= 3:
1007
- 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
+ )
1008
1017
  rate_limit.append(now)
1009
1018
 
1010
1019
  result = loogle_remote(query, num_results)
1011
1020
  if isinstance(result, str):
1012
- return result # Error message
1013
- return _to_json_array(result)
1021
+ raise LeanToolError(result) # Error message from remote
1022
+ return LoogleResults(items=result)
1014
1023
 
1015
1024
 
1016
1025
  @mcp.tool(
@@ -1027,7 +1036,7 @@ def leanfinder(
1027
1036
  ctx: Context,
1028
1037
  query: Annotated[str, Field(description="Mathematical concept or proof state")],
1029
1038
  num_results: Annotated[int, Field(description="Max results", ge=1)] = 5,
1030
- ) -> str:
1039
+ ) -> LeanFinderResults:
1031
1040
  """Semantic search by mathematical meaning via Lean Finder.
1032
1041
 
1033
1042
  Examples: "commutativity of addition on natural numbers",
@@ -1059,7 +1068,7 @@ def leanfinder(
1059
1068
  )
1060
1069
  )
1061
1070
 
1062
- return _to_json_array(results)
1071
+ return LeanFinderResults(items=results)
1063
1072
 
1064
1073
 
1065
1074
  @mcp.tool(
@@ -1078,7 +1087,7 @@ def state_search(
1078
1087
  line: Annotated[int, Field(description="Line number (1-indexed)", ge=1)],
1079
1088
  column: Annotated[int, Field(description="Column number (1-indexed)", ge=1)],
1080
1089
  num_results: Annotated[int, Field(description="Max results", ge=1)] = 5,
1081
- ) -> str:
1090
+ ) -> StateSearchResults:
1082
1091
  """Find lemmas to close the goal at a position. Searches premise-search.com."""
1083
1092
  rel_path = setup_client_for_file(ctx, file_path)
1084
1093
  if not rel_path:
@@ -1108,7 +1117,7 @@ def state_search(
1108
1117
  results = orjson.loads(response.read())
1109
1118
 
1110
1119
  items = [StateSearchResult(name=r["name"]) for r in results]
1111
- return _to_json_array(items)
1120
+ return StateSearchResults(items=items)
1112
1121
 
1113
1122
 
1114
1123
  @mcp.tool(
@@ -1127,7 +1136,7 @@ def hammer_premise(
1127
1136
  line: Annotated[int, Field(description="Line number (1-indexed)", ge=1)],
1128
1137
  column: Annotated[int, Field(description="Column number (1-indexed)", ge=1)],
1129
1138
  num_results: Annotated[int, Field(description="Max results", ge=1)] = 32,
1130
- ) -> str:
1139
+ ) -> PremiseResults:
1131
1140
  """Get premise suggestions for automation tactics at a goal position.
1132
1141
 
1133
1142
  Returns lemma names to try with `simp only [...]`, `aesop`, or as hints.
@@ -1168,7 +1177,7 @@ def hammer_premise(
1168
1177
  results = orjson.loads(response.read())
1169
1178
 
1170
1179
  items = [PremiseResult(name=r["name"]) for r in results]
1171
- return _to_json_array(items)
1180
+ return PremiseResults(items=items)
1172
1181
 
1173
1182
 
1174
1183
  if __name__ == "__main__":
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lean-lsp-mcp
3
- Version: 0.16.0
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=9Lr7uqG7uei1ygOS_k2h0yEu-4TGkHpIT9-UuDrYtJo,39164
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.0.dist-info/licenses/LICENSE,sha256=CQlxnf0tQyoVrBE93JYvAUYxv6Z5Yg6sX0pwogOkFvo,1071
13
- lean_lsp_mcp-0.16.0.dist-info/METADATA,sha256=EYEoi9FZTPAiuVqcmfdAsFtSX5I4UeaJWPJoTfYh9_A,20819
14
- lean_lsp_mcp-0.16.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
15
- lean_lsp_mcp-0.16.0.dist-info/entry_points.txt,sha256=nQbvwctWkWD7I-2f4VrdVQBZYGUw8CnUnFC6QjXxOSE,51
16
- lean_lsp_mcp-0.16.0.dist-info/top_level.txt,sha256=LGEK0lgMSNPIQ6mG8EO-adaZEGPi_0daDs004epOTF0,13
17
- lean_lsp_mcp-0.16.0.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,,