lean-explore 0.3.0__py3-none-any.whl → 1.0.1__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.
Files changed (55) hide show
  1. lean_explore/__init__.py +14 -1
  2. lean_explore/api/__init__.py +12 -1
  3. lean_explore/api/client.py +64 -176
  4. lean_explore/cli/__init__.py +10 -1
  5. lean_explore/cli/data_commands.py +184 -489
  6. lean_explore/cli/display.py +171 -0
  7. lean_explore/cli/main.py +51 -608
  8. lean_explore/config.py +244 -0
  9. lean_explore/extract/__init__.py +5 -0
  10. lean_explore/extract/__main__.py +368 -0
  11. lean_explore/extract/doc_gen4.py +200 -0
  12. lean_explore/extract/doc_parser.py +499 -0
  13. lean_explore/extract/embeddings.py +369 -0
  14. lean_explore/extract/github.py +110 -0
  15. lean_explore/extract/index.py +316 -0
  16. lean_explore/extract/informalize.py +653 -0
  17. lean_explore/extract/package_config.py +59 -0
  18. lean_explore/extract/package_registry.py +45 -0
  19. lean_explore/extract/package_utils.py +105 -0
  20. lean_explore/extract/types.py +25 -0
  21. lean_explore/mcp/__init__.py +11 -1
  22. lean_explore/mcp/app.py +14 -46
  23. lean_explore/mcp/server.py +20 -35
  24. lean_explore/mcp/tools.py +71 -205
  25. lean_explore/models/__init__.py +9 -0
  26. lean_explore/models/search_db.py +76 -0
  27. lean_explore/models/search_types.py +53 -0
  28. lean_explore/search/__init__.py +32 -0
  29. lean_explore/search/engine.py +651 -0
  30. lean_explore/search/scoring.py +156 -0
  31. lean_explore/search/service.py +68 -0
  32. lean_explore/search/tokenization.py +71 -0
  33. lean_explore/util/__init__.py +28 -0
  34. lean_explore/util/embedding_client.py +92 -0
  35. lean_explore/util/logging.py +22 -0
  36. lean_explore/util/openrouter_client.py +63 -0
  37. lean_explore/util/reranker_client.py +187 -0
  38. {lean_explore-0.3.0.dist-info → lean_explore-1.0.1.dist-info}/METADATA +32 -9
  39. lean_explore-1.0.1.dist-info/RECORD +43 -0
  40. {lean_explore-0.3.0.dist-info → lean_explore-1.0.1.dist-info}/WHEEL +1 -1
  41. lean_explore-1.0.1.dist-info/entry_points.txt +2 -0
  42. lean_explore/cli/agent.py +0 -788
  43. lean_explore/cli/config_utils.py +0 -481
  44. lean_explore/defaults.py +0 -114
  45. lean_explore/local/__init__.py +0 -1
  46. lean_explore/local/search.py +0 -1050
  47. lean_explore/local/service.py +0 -479
  48. lean_explore/shared/__init__.py +0 -1
  49. lean_explore/shared/models/__init__.py +0 -1
  50. lean_explore/shared/models/api.py +0 -117
  51. lean_explore/shared/models/db.py +0 -396
  52. lean_explore-0.3.0.dist-info/RECORD +0 -26
  53. lean_explore-0.3.0.dist-info/entry_points.txt +0 -2
  54. {lean_explore-0.3.0.dist-info → lean_explore-1.0.1.dist-info}/licenses/LICENSE +0 -0
  55. {lean_explore-0.3.0.dist-info → lean_explore-1.0.1.dist-info}/top_level.txt +0 -0
lean_explore/cli/main.py CHANGED
@@ -1,599 +1,87 @@
1
- # src/lean_explore/cli/main.py
2
-
3
1
  """Command-Line Interface for Lean Explore.
4
2
 
5
- Provides commands to configure the CLI, search for Lean statement groups
6
- via the remote API, interact with AI agents, manage local data, and other utilities.
3
+ Provides commands to search for Lean declarations via the remote API,
4
+ interact with AI agents, and manage local data.
7
5
  """
8
6
 
9
- import subprocess # For running the MCP server
10
- import sys # For sys.executable
11
- import textwrap
12
- from typing import List, Optional
7
+ import asyncio
8
+ import logging
9
+ import os
10
+ import subprocess
11
+ import sys
13
12
 
14
- import httpx
15
13
  import typer
16
14
  from rich.console import Console
17
- from rich.panel import Panel
18
- from rich.table import Table
19
15
 
20
- from lean_explore.api.client import Client as APIClient
21
- from lean_explore.cli import (
22
- config_utils,
23
- data_commands, # For data management subcommands
24
- )
25
- from lean_explore.shared.models.api import APISearchResponse
16
+ from lean_explore.api import ApiClient
17
+ from lean_explore.cli import data_commands
18
+ from lean_explore.cli.display import display_search_results
26
19
 
27
- # Import the specific command function and its async wrapper from agent.py
28
- from .agent import agent_chat_command
29
- from .agent import typer_async as agent_typer_async
20
+ logger = logging.getLogger(__name__)
30
21
 
31
- # Initialize Typer app and Rich console
32
22
  app = typer.Typer(
33
- name="leanexplore",
23
+ name="lean-explore",
34
24
  help="A CLI tool to explore and search Lean mathematical libraries.",
35
25
  add_completion=False,
36
- rich_markup_mode="markdown", # Enables rich markup in help text
26
+ rich_markup_mode="markdown",
37
27
  )
38
- configure_app = typer.Typer(
39
- name="configure", help="Configure leanexplore CLI settings."
40
- )
41
- app.add_typer(configure_app)
42
28
 
43
29
  mcp_app = typer.Typer(
44
30
  name="mcp", help="Manage and run the Model Context Protocol (MCP) server."
45
31
  )
46
32
  app.add_typer(mcp_app)
47
33
 
48
- # Add the data_commands.app as a subcommand group named "data"
49
34
  app.add_typer(
50
35
  data_commands.app,
51
36
  name="data",
52
37
  help="Manage local data toolchains.",
53
38
  )
54
39
 
55
- # Register the agent_chat_command directly on the main app as "chat"
56
- # The agent_chat_command is already decorated with @typer_async in agent.py
57
- app.command("chat", help="Interact with an AI agent using Lean Explore tools.")(
58
- agent_chat_command
59
- )
60
-
61
-
62
- console = Console()
63
- error_console = Console(stderr=True)
64
-
65
- # Content width for panels.
66
- PANEL_CONTENT_WIDTH = 80
67
-
68
-
69
- @configure_app.command("api-key")
70
- def configure_lean_explore_api_key(
71
- api_key: Optional[str] = typer.Option(
72
- None,
73
- prompt="Please enter your Lean Explore API key",
74
- help="Your personal API key for accessing Lean Explore services.",
75
- hide_input=True,
76
- confirmation_prompt=True,
77
- ),
78
- ):
79
- """Configure and save your Lean Explore API key.
80
-
81
- Args:
82
- api_key: The API key string to save. Prompts if not provided.
83
- """
84
- if not api_key:
85
- error_console.print(
86
- "[bold red]Lean Explore API key cannot be empty.[/bold red]"
87
- )
88
- raise typer.Abort()
89
-
90
- if config_utils.save_api_key(api_key):
91
- config_path = config_utils.get_config_file_path()
92
- console.print(
93
- f"[bold green]Lean Explore API key saved successfully to: "
94
- f"{config_path}[/bold green]"
95
- )
96
- else:
97
- error_console.print(
98
- "[bold red]Failed to save Lean Explore API key. "
99
- "Check logs or permissions.[/bold red]"
100
- )
101
- raise typer.Abort()
102
-
103
-
104
- @configure_app.command("openai-key")
105
- def configure_openai_api_key(
106
- api_key: Optional[str] = typer.Option(
107
- None,
108
- prompt="Please enter your OpenAI API key",
109
- help="Your personal API key for OpenAI services (e.g., GPT-4).",
110
- hide_input=True,
111
- confirmation_prompt=True,
112
- ),
113
- ):
114
- """Configure and save your OpenAI API key.
115
-
116
- This key is used by agent functionalities that leverage OpenAI models.
117
-
118
- Args:
119
- api_key: The OpenAI API key string to save. Prompts if not provided.
120
- """
121
- if not api_key:
122
- error_console.print("[bold red]OpenAI API key cannot be empty.[/bold red]")
123
- raise typer.Abort()
124
-
125
- if config_utils.save_openai_api_key(api_key):
126
- config_path = config_utils.get_config_file_path()
127
- console.print(
128
- f"[bold green]OpenAI API key saved successfully to: "
129
- f"{config_path}[/bold green]"
130
- )
131
- else:
132
- error_console.print(
133
- "[bold red]Failed to save OpenAI API key. "
134
- "Check logs or permissions.[/bold red]"
135
- )
136
- raise typer.Abort()
137
-
138
-
139
- def _get_api_client() -> Optional[APIClient]:
140
- """Loads Lean Explore API key and initializes the APIClient.
141
-
142
- Returns:
143
- Optional[APIClient]: APIClient instance if key is found, None otherwise.
144
- """
145
- api_key = config_utils.load_api_key()
146
- if not api_key:
147
- config_path = config_utils.get_config_file_path()
148
- error_console.print(
149
- "[bold yellow]Lean Explore API key not configured. Please run:"
150
- "[/bold yellow]\n"
151
- f" `leanexplore configure api-key`\n"
152
- f"Your API key will be stored in: {config_path}"
153
- )
154
- return None
155
- return APIClient(api_key=api_key)
156
-
157
40
 
158
- def _format_text_for_fixed_panel(text_content: Optional[str], width: int) -> str:
159
- """Wraps text and pads lines to ensure fixed content width for a Panel.
41
+ def _get_console(use_stderr: bool = False) -> Console:
42
+ """Create a Rich console instance for output.
160
43
 
161
44
  Args:
162
- text_content: The text content to wrap and pad.
163
- width: The target width for text wrapping and padding.
45
+ use_stderr: If True, output to stderr instead of stdout.
164
46
 
165
47
  Returns:
166
- A string with wrapped and padded text suitable for fixed-width display.
48
+ A configured Console instance.
167
49
  """
168
- if not text_content:
169
- return " " * width
170
-
171
- final_output_lines = []
172
- paragraphs = text_content.split("\n\n")
173
-
174
- for i, paragraph in enumerate(paragraphs):
175
- if not paragraph.strip() and i < len(paragraphs) - 1:
176
- final_output_lines.append(" " * width)
177
- continue
178
-
179
- lines_in_paragraph = paragraph.splitlines()
180
- if not lines_in_paragraph and paragraph.strip() == "":
181
- final_output_lines.append(" " * width)
182
- continue
183
- if not lines_in_paragraph and not paragraph:
184
- final_output_lines.append(" " * width)
185
- continue
186
-
187
- for line in lines_in_paragraph:
188
- if not line.strip():
189
- final_output_lines.append(" " * width)
190
- continue
191
-
192
- wrapped_segments = textwrap.wrap(
193
- line,
194
- width=width,
195
- replace_whitespace=True,
196
- drop_whitespace=True,
197
- break_long_words=True,
198
- break_on_hyphens=True,
199
- )
200
- if not wrapped_segments:
201
- final_output_lines.append(" " * width)
202
- else:
203
- for segment in wrapped_segments:
204
- final_output_lines.append(segment.ljust(width))
205
-
206
- if i < len(paragraphs) - 1 and (
207
- paragraph.strip() or (not paragraph.strip() and not lines_in_paragraph)
208
- ):
209
- # Add a blank padded line between paragraphs
210
- final_output_lines.append(" " * width)
211
-
212
- if not final_output_lines and text_content.strip():
213
- # Fallback for content that becomes empty after processing but was not initially
214
- return " " * width
215
-
216
- return "\n".join(final_output_lines)
217
-
218
-
219
- def _display_search_results(response: APISearchResponse, display_limit: int = 5):
220
- """Displays search results using fixed-width Panels for each item.
221
-
222
- Args:
223
- response: The APISearchResponse object from the backend.
224
- display_limit: The maximum number of individual results to display in detail.
225
- """
226
- console.print(
227
- Panel(
228
- f"[bold cyan]Search Query:[/bold cyan] {response.query}",
229
- expand=False,
230
- border_style="dim",
231
- )
232
- )
233
- if response.packages_applied:
234
- console.print(
235
- f"[bold cyan]Package Filters:[/bold cyan] "
236
- f"{', '.join(response.packages_applied)}"
237
- )
238
-
239
- num_results_to_show = min(len(response.results), display_limit)
240
- console.print(
241
- f"Showing {num_results_to_show} of {response.count} "
242
- f"(out of {response.total_candidates_considered} candidates considered by "
243
- f"server). Time: {response.processing_time_ms}ms"
244
- )
245
-
246
- if not response.results:
247
- console.print("[yellow]No results found.[/yellow]")
248
- return
249
-
250
- console.print("") # Adds a blank line for spacing
251
-
252
- for i, item in enumerate(response.results):
253
- if i >= display_limit:
254
- break
255
-
256
- lean_name = (
257
- item.primary_declaration.lean_name if item.primary_declaration else "N/A"
258
- )
259
-
260
- console.rule(f"[bold]Result {i + 1}[/bold]", style="dim")
261
- console.print(f"[bold cyan]ID:[/bold cyan] [dim]{item.id}[/dim]")
262
- console.print(f"[bold cyan]Name:[/bold cyan] {lean_name}")
263
- console.print(
264
- f"[bold cyan]File:[/bold cyan] [green]{item.source_file}[/green]:"
265
- f"[dim]{item.range_start_line}[/dim]"
266
- )
267
-
268
- code_to_display = item.display_statement_text or item.statement_text
269
- if code_to_display:
270
- formatted_code = _format_text_for_fixed_panel(
271
- code_to_display, PANEL_CONTENT_WIDTH
272
- )
273
- console.print(
274
- Panel(
275
- formatted_code,
276
- title="[bold green]Code[/bold green]",
277
- border_style="green",
278
- expand=False,
279
- padding=(0, 1),
280
- )
281
- )
282
-
283
- if item.docstring:
284
- formatted_doc = _format_text_for_fixed_panel(
285
- item.docstring, PANEL_CONTENT_WIDTH
286
- )
287
- console.print(
288
- Panel(
289
- formatted_doc,
290
- title="[bold blue]Docstring[/bold blue]",
291
- border_style="blue",
292
- expand=False,
293
- padding=(0, 1),
294
- )
295
- )
296
-
297
- if item.informal_description:
298
- formatted_informal = _format_text_for_fixed_panel(
299
- item.informal_description, PANEL_CONTENT_WIDTH
300
- )
301
- console.print(
302
- Panel(
303
- formatted_informal,
304
- title="[bold magenta]Informal Description[/bold magenta]",
305
- border_style="magenta",
306
- expand=False,
307
- padding=(0, 1),
308
- )
309
- )
310
- elif not item.docstring and not code_to_display:
311
- console.print(
312
- "[dim]No further textual details (docstring, informal description, "
313
- "code) available for this item.[/dim]"
314
- )
315
-
316
- if i < num_results_to_show - 1: # Add spacing between items
317
- console.print("")
318
-
319
- console.rule(style="dim")
320
- if len(response.results) > num_results_to_show:
321
- console.print(
322
- f"...and {len(response.results) - num_results_to_show} more results "
323
- "received from server but not shown due to limit."
324
- )
325
- elif response.count > len(
326
- response.results
327
- ): # Should be total_candidates_considered
328
- console.print(
329
- f"...and {response.total_candidates_considered - len(response.results)} "
330
- "more results available "
331
- "on server."
332
- )
50
+ return Console(stderr=use_stderr)
333
51
 
334
52
 
335
53
  @app.command("search")
336
- @agent_typer_async
337
- async def search_command(
54
+ def search_command(
338
55
  query_string: str = typer.Argument(..., help="The search query string."),
339
- package: Optional[List[str]] = typer.Option(
340
- None,
341
- "--package",
342
- "-p",
343
- help="Filter by package name(s). Can be used multiple times.",
344
- ),
345
56
  limit: int = typer.Option(
346
57
  5, "--limit", "-n", help="Number of search results to display."
347
58
  ),
348
- ):
349
- """Search for Lean statement groups using the Lean Explore API.
350
-
351
- Args:
352
- query_string: The natural language query to search for.
353
- package: An optional list of package names to filter results by.
354
- limit: The maximum number of search results to display to the user.
355
- """
356
- client = _get_api_client()
357
- if not client:
358
- raise typer.Exit(code=1)
359
-
360
- console.print(f"Searching for: '{query_string}'...")
361
- try:
362
- response = await client.search(query=query_string, package_filters=package)
363
- _display_search_results(response, display_limit=limit)
364
- except httpx.HTTPStatusError as e:
365
- if e.response.status_code == 401:
366
- error_console.print(
367
- f"[bold red]API Error {e.response.status_code}: Unauthorized. "
368
- "Your API key might be invalid or expired.[/bold red]"
369
- )
370
- error_console.print(
371
- "Please reconfigure your API key using: `leanexplore configure api-key`"
372
- )
373
- else:
374
- try:
375
- error_detail = e.response.json().get("detail", e.response.text)
376
- error_console.print(
377
- f"[bold red]API Error {e.response.status_code}: "
378
- f"{error_detail}[/bold red]"
379
- )
380
- except Exception:
381
- error_console.print(
382
- f"[bold red]API Error {e.response.status_code}: "
383
- f"{e.response.text}[/bold red]"
384
- )
385
- raise typer.Exit(code=1)
386
- except httpx.RequestError as e:
387
- error_console.print(
388
- f"[bold red]Network Error: Could not connect to the API. {e}[/bold red]"
389
- )
390
- raise typer.Exit(code=1)
391
- except Exception as e:
392
- error_console.print(f"[bold red]An unexpected error occurred: {e}[/bold red]")
393
- raise typer.Exit(code=1)
394
-
395
-
396
- @app.command("get")
397
- @agent_typer_async
398
- async def get_by_id_command(
399
- group_id: int = typer.Argument(
400
- ..., help="The ID of the statement group to retrieve."
59
+ packages: list[str] | None = typer.Option(
60
+ None, "--package", "-p", help="Filter by package (e.g., -p Mathlib -p Std)."
401
61
  ),
402
62
  ):
403
- """Get detailed information about a specific statement group by its ID.
404
-
405
- Args:
406
- group_id: The unique integer identifier of the statement group.
407
- """
408
- client = _get_api_client()
409
- if not client:
410
- raise typer.Exit(code=1)
63
+ """Search for Lean declarations using the Lean Explore API."""
64
+ asyncio.run(_search_async(query_string, limit, packages))
411
65
 
412
- console.print(f"Fetching statement group ID: {group_id}...")
413
- try:
414
- item = await client.get_by_id(group_id)
415
- if item:
416
- console.print(
417
- Panel(
418
- f"[bold green]Statement Group ID: {item.id}[/bold green]",
419
- expand=False,
420
- border_style="dim",
421
- )
422
- )
423
- lean_name = (
424
- item.primary_declaration.lean_name
425
- if item.primary_declaration
426
- else "N/A"
427
- )
428
- console.print(f" [bold cyan]Lean Name:[/bold cyan] {lean_name}")
429
- console.print(
430
- f" [bold cyan]Source File:[/bold cyan] "
431
- f"[green]{item.source_file}[/green]:"
432
- f"[dim]{item.range_start_line}[/dim]"
433
- )
434
66
 
435
- if item.statement_text:
436
- formatted_stmt_text = _format_text_for_fixed_panel(
437
- item.statement_text, PANEL_CONTENT_WIDTH
438
- )
439
- console.print(
440
- Panel(
441
- formatted_stmt_text,
442
- title="[bold green]Code[/bold green]",
443
- border_style="green",
444
- expand=False,
445
- padding=(0, 1),
446
- )
447
- )
448
-
449
- if item.docstring:
450
- formatted_docstring = _format_text_for_fixed_panel(
451
- item.docstring, PANEL_CONTENT_WIDTH
452
- )
453
- console.print(
454
- Panel(
455
- formatted_docstring,
456
- title="[bold blue]Docstring[/bold blue]",
457
- border_style="blue",
458
- expand=False,
459
- padding=(0, 1),
460
- )
461
- )
462
-
463
- if item.informal_description:
464
- formatted_informal = _format_text_for_fixed_panel(
465
- item.informal_description, PANEL_CONTENT_WIDTH
466
- )
467
- console.print(
468
- Panel(
469
- formatted_informal,
470
- title="[bold magenta]Informal Description[/bold magenta]",
471
- border_style="magenta",
472
- expand=False,
473
- padding=(0, 1),
474
- )
475
- )
476
-
477
- else:
478
- error_console.print( # Changed to error_console for error/warning message
479
- f"[yellow]Statement group with ID {group_id} not found.[/yellow]"
480
- )
67
+ async def _search_async(
68
+ query_string: str, limit: int, packages: list[str] | None
69
+ ) -> None:
70
+ """Async implementation of search command."""
71
+ console = _get_console()
72
+ error_console = _get_console(use_stderr=True)
481
73
 
482
- except httpx.HTTPStatusError as e:
483
- if e.response.status_code == 401:
484
- error_console.print(
485
- f"[bold red]API Error {e.response.status_code}: Unauthorized."
486
- " Your API key might be invalid or expired.[/bold red]"
487
- )
488
- else:
489
- try:
490
- error_detail = e.response.json().get("detail", e.response.text)
491
- error_console.print(
492
- f"[bold red]API Error {e.response.status_code}: "
493
- f"{error_detail}[/bold red]"
494
- )
495
- except Exception:
496
- error_console.print(
497
- f"[bold red]API Error {e.response.status_code}: "
498
- f"{e.response.text}[/bold red]"
499
- )
500
- raise typer.Exit(code=1)
501
- except httpx.RequestError as e:
502
- error_console.print(
503
- f"[bold red]Network Error: Could not connect to the API. {e}[/bold red]"
504
- )
505
- raise typer.Exit(code=1)
506
- except Exception as e:
507
- error_console.print(f"[bold red]An unexpected error occurred: {e}[/bold red]")
508
- raise typer.Exit(code=1)
509
-
510
-
511
- @app.command("dependencies")
512
- @agent_typer_async
513
- async def get_dependencies_command(
514
- group_id: int = typer.Argument(
515
- ..., help="The ID of the statement group to get dependencies for."
516
- ),
517
- ):
518
- """Get dependencies (citations) for a specific statement group by its ID.
519
-
520
- Args:
521
- group_id: The unique integer identifier of the statement group.
522
- """
523
- client = _get_api_client()
524
- if not client:
525
- raise typer.Exit(code=1)
526
-
527
- console.print(f"Fetching dependencies for statement group ID: {group_id}...")
528
74
  try:
529
- response = await client.get_dependencies(group_id)
530
- if response:
531
- console.print(
532
- Panel(
533
- f"[bold green]Citations for Statement Group ID: "
534
- f"{response.source_group_id}[/bold green]",
535
- expand=False,
536
- border_style="dim",
537
- )
538
- )
539
- console.print(f"Found {response.count} direct citations.")
540
-
541
- if response.citations:
542
- citation_table = Table(show_header=True, header_style="bold magenta")
543
- citation_table.add_column("ID", style="dim", width=6)
544
- citation_table.add_column("Cited Lean Name", width=40)
545
- citation_table.add_column("File", style="green")
546
- citation_table.add_column("Line", style="dim")
547
-
548
- for item in response.citations:
549
- lean_name = (
550
- item.primary_declaration.lean_name
551
- if item.primary_declaration
552
- else "N/A"
553
- )
554
- citation_table.add_row(
555
- str(item.id),
556
- lean_name,
557
- item.source_file,
558
- str(item.range_start_line),
559
- )
560
- console.print(citation_table)
561
- else:
562
- console.print("[yellow]No citations found for this group.[/yellow]")
563
- else:
564
- error_console.print( # Changed to error_console for error/warning message
565
- f"[yellow]Statement group with ID {group_id} not found or no "
566
- "citations data available.[/yellow]"
567
- )
568
-
569
- except httpx.HTTPStatusError as e:
570
- if e.response.status_code == 401:
571
- error_console.print(
572
- f"[bold red]API Error {e.response.status_code}: Unauthorized."
573
- " Your API key might be invalid or expired.[/bold red]"
574
- )
575
- else:
576
- try:
577
- error_detail = e.response.json().get("detail", e.response.text)
578
- error_console.print(
579
- f"[bold red]API Error {e.response.status_code}: "
580
- f"{error_detail}[/bold red]"
581
- )
582
- except Exception:
583
- error_console.print(
584
- f"[bold red]API Error {e.response.status_code}: "
585
- f"{e.response.text}[/bold red]"
586
- )
587
- raise typer.Exit(code=1)
588
- except httpx.RequestError as e:
589
- error_console.print(
590
- f"[bold red]Network Error: Could not connect to the API. {e}[/bold red]"
591
- )
592
- raise typer.Exit(code=1)
593
- except Exception as e:
594
- error_console.print(f"[bold red]An unexpected error occurred: {e}[/bold red]")
75
+ client = ApiClient()
76
+ except ValueError as error:
77
+ logger.error("Failed to initialize API client: %s", error)
78
+ error_console.print(f"[bold red]Error: {error}[/bold red]")
595
79
  raise typer.Exit(code=1)
596
80
 
81
+ console.print(f"Searching for: '{query_string}'...")
82
+ response = await client.search(query=query_string, limit=limit, packages=packages)
83
+ display_search_results(response, display_limit=limit, console=console)
84
+
597
85
 
598
86
  @mcp_app.command("serve")
599
87
  def mcp_serve_command(
@@ -605,23 +93,15 @@ def mcp_serve_command(
605
93
  case_sensitive=False,
606
94
  show_choices=True,
607
95
  ),
608
- api_key_override: Optional[str] = typer.Option(
96
+ api_key_override: str | None = typer.Option(
609
97
  None,
610
98
  "--api-key",
611
- help="API key to use if backend is 'api'. Overrides stored key. "
612
- "Not used for 'local' backend.",
99
+ help="API key to use if backend is 'api'. Overrides env var.",
613
100
  ),
614
101
  ):
615
- """Launch the Lean Explore MCP (Model Context Protocol) server.
616
-
617
- The server communicates via stdio and provides Lean search functionalities
618
- as MCP tools. The actual checks for local data presence or API key validity
619
- are handled by the 'lean_explore.mcp.server' module when it starts.
102
+ """Launch the Lean Explore MCP (Model Context Protocol) server."""
103
+ error_console = _get_console(use_stderr=True)
620
104
 
621
- Args:
622
- backend: The backend choice ('api' or 'local').
623
- api_key_override: Optional API key to override any stored key.
624
- """
625
105
  command_parts = [
626
106
  sys.executable,
627
107
  "-m",
@@ -631,60 +111,23 @@ def mcp_serve_command(
631
111
  ]
632
112
 
633
113
  if backend.lower() == "api":
634
- effective_lean_explore_api_key = api_key_override or config_utils.load_api_key()
635
- if not effective_lean_explore_api_key:
114
+ effective_api_key = api_key_override or os.getenv("LEANEXPLORE_API_KEY")
115
+ if not effective_api_key:
116
+ logger.error("API key required for 'api' backend but not provided")
636
117
  error_console.print(
637
- "[bold red]Lean Explore API key is required for 'api' backend."
638
- "[/bold red]\n"
639
- "Please configure it using `leanexplore configure api-key` "
640
- "or provide it with the `--api-key` option for this command."
118
+ "[bold red]API key required for 'api' backend.[/bold red]\n"
119
+ "Set LEANEXPLORE_API_KEY or use --api-key option."
641
120
  )
642
121
  raise typer.Abort()
643
122
  if api_key_override:
644
123
  command_parts.extend(["--api-key", api_key_override])
645
- elif backend.lower() == "local":
646
- error_console.print( # Changed to error_console for consistency
647
- "[dim]Attempting to start MCP server with 'local' backend. "
648
- "The server will verify local data availability.[/dim]"
649
- )
650
- else:
651
- error_console.print(
652
- f"[bold red]Invalid backend: '{backend}'. Must be 'api' or 'local'."
653
- "[/bold red]"
654
- )
655
- raise typer.Abort()
656
124
 
657
- error_console.print(
658
- f"[green]Launching MCP server subprocess with '{backend}' backend...[/green]"
659
- )
660
- error_console.print(
661
- "[dim]The server will now take over stdio. To stop it, the connected MCP "
662
- "client should disconnect, or you may need to manually terminate this process "
663
- "(e.g., Ctrl+C if no client is managing it).[/dim]"
664
- )
125
+ logger.info("Starting MCP server with backend: %s", backend.lower())
126
+ result = subprocess.run(command_parts, check=False)
665
127
 
666
- try:
667
- process_result = subprocess.run(command_parts, check=False)
668
- if process_result.returncode != 0:
669
- error_console.print(
670
- f"[bold red]MCP server subprocess exited with code: "
671
- f"{process_result.returncode}. Check server logs above for "
672
- f"details.[/bold red]"
673
- )
674
- except FileNotFoundError:
675
- error_console.print(
676
- f"[bold red]Error: Could not find Python interpreter '{sys.executable}' "
677
- f"or the MCP server module 'lean_explore.mcp.server'.[/bold red]"
678
- )
679
- error_console.print(
680
- "Please ensure the package is installed correctly and "
681
- "`python -m lean_explore.mcp.server` is runnable."
682
- )
683
- except Exception as e:
684
- error_console.print(
685
- f"[bold red]An error occurred while trying to launch or run the MCP "
686
- f"server: {e}[/bold red]"
687
- )
128
+ if result.returncode != 0:
129
+ logger.error("MCP server exited with code %d", result.returncode)
130
+ raise typer.Exit(code=result.returncode)
688
131
 
689
132
 
690
133
  if __name__ == "__main__":