leanback 0.1.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.
leanback/mcp_test.py ADDED
@@ -0,0 +1,496 @@
1
+ #!/usr/bin/env python3
2
+ """E2E test client for LeanBack MCP server."""
3
+
4
+ import argparse
5
+ import asyncio
6
+ import json
7
+ import logging
8
+ import sys
9
+ import traceback
10
+ from pathlib import Path
11
+
12
+ from mcp.client.session import ClientSession
13
+ from mcp.client.stdio import StdioServerParameters, stdio_client
14
+
15
+
16
+ # Configure logging
17
+ logging.basicConfig(
18
+ level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s"
19
+ )
20
+ logger = logging.getLogger(__name__)
21
+
22
+
23
+ async def test_check_tool(session: ClientSession, project: str):
24
+ """Test the check tool."""
25
+ logger.info("Testing check tool...")
26
+
27
+ # Test 1: Check entire project
28
+ logger.info(f" Test 1: Checking entire project '{project}'")
29
+ try:
30
+ result = await session.call_tool("check", {"project": project})
31
+ content = result.content[0] if result.content else None
32
+ if content and hasattr(content, "text"):
33
+ data = json.loads(content.text)
34
+ logger.info(f" Result: {json.dumps(data, indent=2)}")
35
+ else:
36
+ logger.info(f" Raw result: {result}")
37
+ except Exception as e:
38
+ logger.error(f" Failed: {e}")
39
+ traceback.print_exc()
40
+
41
+ # Test 2: Check specific file
42
+ logger.info(" Test 2: Checking specific file 'test_simple_proof.lean'")
43
+ try:
44
+ result = await session.call_tool(
45
+ "check", {"project": project, "file": "test_simple_proof.lean"}
46
+ )
47
+ content = result.content[0] if result.content else None
48
+ if content and hasattr(content, "text"):
49
+ data = json.loads(content.text)
50
+ logger.info(f" Result: {json.dumps(data, indent=2)}")
51
+ except Exception as e:
52
+ logger.error(f" Failed: {e}")
53
+
54
+ # Test 3: Check expression
55
+ logger.info(" Test 3: Checking expression '#check 2 + 2 = 4'")
56
+ try:
57
+ result = await session.call_tool(
58
+ "check", {"project": project, "expr": "#check 2 + 2 = 4"}
59
+ )
60
+ content = result.content[0] if result.content else None
61
+ if content and hasattr(content, "text"):
62
+ data = json.loads(content.text)
63
+ logger.info(f" Result: {json.dumps(data, indent=2)}")
64
+ except Exception as e:
65
+ logger.error(f" Failed: {e}")
66
+
67
+ logger.info("✓ Check tool tests completed")
68
+
69
+
70
+ async def test_prove_tool(session: ClientSession, project: str):
71
+ """Test the prove tool."""
72
+ logger.info("Testing prove tool...")
73
+
74
+ # Test 1: Simple theorem with tactic
75
+ logger.info(" Test 1: Proving simple theorem '2 + 2 = 4'")
76
+ try:
77
+ result = await session.call_tool(
78
+ "prove", {"project": project, "theorem": "2 + 2 = 4", "tactics": ["rfl"]}
79
+ )
80
+ content = result.content[0] if result.content else None
81
+ if content and hasattr(content, "text"):
82
+ data = json.loads(content.text)
83
+ logger.info(f" Result: {json.dumps(data, indent=2)}")
84
+ except Exception as e:
85
+ logger.error(f" Failed: {e}")
86
+
87
+ # Test 2: Verify proof file
88
+ logger.info(" Test 2: Verifying proof file 'test_simple_proof.lean'")
89
+ try:
90
+ result = await session.call_tool(
91
+ "prove", {"project": project, "file": "test_simple_proof.lean"}
92
+ )
93
+ content = result.content[0] if result.content else None
94
+ if content and hasattr(content, "text"):
95
+ data = json.loads(content.text)
96
+ logger.info(f" Result: {json.dumps(data, indent=2)}")
97
+ except Exception as e:
98
+ logger.error(f" Failed: {e}")
99
+
100
+ # Test 3: Theorem with mathlib
101
+ logger.info(" Test 3: Proving theorem with mathlib")
102
+ try:
103
+ result = await session.call_tool(
104
+ "prove",
105
+ {
106
+ "project": project,
107
+ "theorem": "∀ n : Nat, n + 0 = n",
108
+ "tactics": ["intro n", "rfl"],
109
+ "mathlib": True,
110
+ },
111
+ )
112
+ content = result.content[0] if result.content else None
113
+ if content and hasattr(content, "text"):
114
+ data = json.loads(content.text)
115
+ logger.info(f" Result: {json.dumps(data, indent=2)}")
116
+ except Exception as e:
117
+ logger.error(f" Failed: {e}")
118
+
119
+ logger.info("✓ Prove tool tests completed")
120
+
121
+
122
+ async def test_eval_tool(session: ClientSession, project: str):
123
+ """Test the eval tool."""
124
+ logger.info("Testing eval tool...")
125
+
126
+ # Test 1: Evaluate expression
127
+ logger.info(" Test 1: Evaluating expression '#eval 2 + 2'")
128
+ try:
129
+ result = await session.call_tool(
130
+ "eval", {"project": project, "code": "#eval 2 + 2"}
131
+ )
132
+ content = result.content[0] if result.content else None
133
+ if content and hasattr(content, "text"):
134
+ data = json.loads(content.text)
135
+ logger.info(f" Result: {json.dumps(data, indent=2)}")
136
+ except Exception as e:
137
+ logger.error(f" Failed: {e}")
138
+
139
+ # Test 2: Check type
140
+ logger.info(" Test 2: Checking type '#check Nat.add'")
141
+ try:
142
+ result = await session.call_tool(
143
+ "eval", {"project": project, "code": "#check Nat.add"}
144
+ )
145
+ content = result.content[0] if result.content else None
146
+ if content and hasattr(content, "text"):
147
+ data = json.loads(content.text)
148
+ logger.info(f" Result: {json.dumps(data, indent=2)}")
149
+ except Exception as e:
150
+ logger.error(f" Failed: {e}")
151
+
152
+ # Test 3: Evaluate with mathlib
153
+ logger.info(" Test 3: Evaluating with mathlib")
154
+ try:
155
+ result = await session.call_tool(
156
+ "eval",
157
+ {
158
+ "project": project,
159
+ "code": "example : ∃ x : Nat, x > 0 := by use 1; simp",
160
+ "mathlib": True,
161
+ },
162
+ )
163
+ content = result.content[0] if result.content else None
164
+ if content and hasattr(content, "text"):
165
+ data = json.loads(content.text)
166
+ logger.info(f" Result: {json.dumps(data, indent=2)}")
167
+ except Exception as e:
168
+ logger.error(f" Failed: {e}")
169
+
170
+ logger.info("✓ Eval tool tests completed")
171
+
172
+
173
+ async def test_search_tool(session: ClientSession, project: str):
174
+ """Test the search tool."""
175
+ logger.info("Testing search tool...")
176
+
177
+ # Test 1: Search for a pattern
178
+ logger.info(" Test 1: Searching for 'add_comm'")
179
+ try:
180
+ result = await session.call_tool(
181
+ "search", {"project": project, "pattern": "add_comm", "max_results": 5}
182
+ )
183
+ content = result.content[0] if result.content else None
184
+ if content and hasattr(content, "text"):
185
+ data = json.loads(content.text)
186
+ logger.info(f" Result: Found {len(data.get('results', []))} matches")
187
+ if data.get("results"):
188
+ logger.info(f" First match: {data['results'][0]['name']}")
189
+ except Exception as e:
190
+ logger.error(f" Failed: {e}")
191
+
192
+ # Test 2: Batch search
193
+ logger.info(" Test 2: Batch searching for 'add_comm,mul_comm,sub_eq_add_neg'")
194
+ try:
195
+ result = await session.call_tool(
196
+ "search",
197
+ {
198
+ "project": project,
199
+ "batch": "add_comm,mul_comm,sub_eq_add_neg",
200
+ "max_results": 3,
201
+ },
202
+ )
203
+ content = result.content[0] if result.content else None
204
+ if content and hasattr(content, "text"):
205
+ data = json.loads(content.text)
206
+ logger.info(f" Result: {json.dumps(data, indent=2)}")
207
+ except Exception as e:
208
+ logger.error(f" Failed: {e}")
209
+
210
+ # Test 3: Filtered search
211
+ logger.info(" Test 3: Searching for 'reverse' in List namespace")
212
+ try:
213
+ result = await session.call_tool(
214
+ "search",
215
+ {
216
+ "project": project,
217
+ "pattern": "reverse",
218
+ "filter_namespace": "List",
219
+ "max_results": 5,
220
+ },
221
+ )
222
+ content = result.content[0] if result.content else None
223
+ if content and hasattr(content, "text"):
224
+ data = json.loads(content.text)
225
+ logger.info(
226
+ f" Result: Found {len(data.get('results', []))} matches in List namespace"
227
+ )
228
+ except Exception as e:
229
+ logger.error(f" Failed: {e}")
230
+
231
+ logger.info("✓ Search tool tests completed")
232
+
233
+
234
+ async def test_goals_tool(session: ClientSession, project: str):
235
+ """Test the goals tool."""
236
+ logger.info("Testing goals tool...")
237
+
238
+ # Test 1: Initial goal (no tactics)
239
+ logger.info(" Test 1: Initial goal for '∀ n : Nat, n + 0 = n'")
240
+ try:
241
+ result = await session.call_tool(
242
+ "goals",
243
+ {"project": project, "theorem": "∀ n : Nat, n + 0 = n"},
244
+ )
245
+ content = result.content[0] if result.content else None
246
+ if content and hasattr(content, "text"):
247
+ data = json.loads(content.text)
248
+ logger.info(f" Result: {json.dumps(data, indent=2)}")
249
+ except Exception as e:
250
+ logger.error(f" Failed: {e}")
251
+
252
+ # Test 2: Partial tactics (goals remain)
253
+ logger.info(" Test 2: After 'intro n'")
254
+ try:
255
+ result = await session.call_tool(
256
+ "goals",
257
+ {
258
+ "project": project,
259
+ "theorem": "∀ n : Nat, n + 0 = n",
260
+ "tactics": ["intro n"],
261
+ },
262
+ )
263
+ content = result.content[0] if result.content else None
264
+ if content and hasattr(content, "text"):
265
+ data = json.loads(content.text)
266
+ logger.info(f" Result: {json.dumps(data, indent=2)}")
267
+ except Exception as e:
268
+ logger.error(f" Failed: {e}")
269
+
270
+ # Test 3: Complete proof
271
+ logger.info(" Test 3: Complete proof with simp")
272
+ try:
273
+ result = await session.call_tool(
274
+ "goals",
275
+ {
276
+ "project": project,
277
+ "theorem": "∀ n : Nat, n + 0 = n",
278
+ "tactics": ["intro n", "simp"],
279
+ "mathlib": True,
280
+ },
281
+ )
282
+ content = result.content[0] if result.content else None
283
+ if content and hasattr(content, "text"):
284
+ data = json.loads(content.text)
285
+ logger.info(f" Result: {json.dumps(data, indent=2)}")
286
+ except Exception as e:
287
+ logger.error(f" Failed: {e}")
288
+
289
+ # Test 4: Tactic failure
290
+ logger.info(" Test 4: Failing tactic (rfl on non-reflexive goal)")
291
+ try:
292
+ result = await session.call_tool(
293
+ "goals",
294
+ {
295
+ "project": project,
296
+ "theorem": "∀ n m : Nat, n + m = m + n",
297
+ "tactics": ["intro n", "intro m", "rfl"],
298
+ },
299
+ )
300
+ content = result.content[0] if result.content else None
301
+ if content and hasattr(content, "text"):
302
+ data = json.loads(content.text)
303
+ logger.info(f" Result: {json.dumps(data, indent=2)}")
304
+ except Exception as e:
305
+ logger.error(f" Failed: {e}")
306
+
307
+ # Test 5: Multiple goals (constructor)
308
+ logger.info(" Test 5: Multiple goals with constructor")
309
+ try:
310
+ result = await session.call_tool(
311
+ "goals",
312
+ {
313
+ "project": project,
314
+ "theorem": "True ∧ True",
315
+ "tactics": ["constructor"],
316
+ },
317
+ )
318
+ content = result.content[0] if result.content else None
319
+ if content and hasattr(content, "text"):
320
+ data = json.loads(content.text)
321
+ logger.info(f" Result: {json.dumps(data, indent=2)}")
322
+ except Exception as e:
323
+ logger.error(f" Failed: {e}")
324
+
325
+ # Test 6: Induction (case labels)
326
+ logger.info(" Test 6: Induction goals")
327
+ try:
328
+ result = await session.call_tool(
329
+ "goals",
330
+ {
331
+ "project": project,
332
+ "theorem": "∀ n : Nat, 0 + n = n",
333
+ "tactics": ["intro n", "induction n"],
334
+ },
335
+ )
336
+ content = result.content[0] if result.content else None
337
+ if content and hasattr(content, "text"):
338
+ data = json.loads(content.text)
339
+ logger.info(f" Result: {json.dumps(data, indent=2)}")
340
+ except Exception as e:
341
+ logger.error(f" Failed: {e}")
342
+
343
+ # Test 7: Sorry
344
+ logger.info(" Test 7: Sorry usage")
345
+ try:
346
+ result = await session.call_tool(
347
+ "goals",
348
+ {
349
+ "project": project,
350
+ "theorem": "∀ n : Nat, n + 0 = n",
351
+ "tactics": ["intro n", "sorry"],
352
+ },
353
+ )
354
+ content = result.content[0] if result.content else None
355
+ if content and hasattr(content, "text"):
356
+ data = json.loads(content.text)
357
+ logger.info(f" Result: {json.dumps(data, indent=2)}")
358
+ except Exception as e:
359
+ logger.error(f" Failed: {e}")
360
+
361
+ logger.info("✓ Goals tool tests completed")
362
+
363
+
364
+ async def show_tool_descriptions():
365
+ """Connect to the MCP server and display tool descriptions."""
366
+ server_params = StdioServerParameters(
367
+ command="uv",
368
+ args=["run", "python", "-m", "leanback.mcp_server"],
369
+ )
370
+
371
+ try:
372
+ logger.info("Connecting to LeanBack MCP server...")
373
+ async with stdio_client(server_params) as (read, write):
374
+ async with ClientSession(read, write) as session:
375
+ await session.initialize()
376
+ logger.info("✓ Connected to server\n")
377
+
378
+ try:
379
+ tools_result = await session.list_tools()
380
+ for tool in tools_result.tools:
381
+ print(f"=== Tool: {tool.name} ===")
382
+ print(f"\n{tool.description}")
383
+ print("\n" + "=" * 50 + "\n")
384
+ except Exception as e:
385
+ logger.error(f"Failed to list tools: {e}")
386
+ raise
387
+
388
+ except Exception as e:
389
+ logger.error(f"Failed to connect: {e}")
390
+ raise
391
+
392
+
393
+ async def run_tests(project: str, tool: str | None):
394
+ """Run tests against the MCP server."""
395
+ server_params = StdioServerParameters(
396
+ command="uv",
397
+ args=["run", "python", "-m", "leanback.mcp_server"],
398
+ )
399
+
400
+ try:
401
+ logger.info("Connecting to LeanBack MCP server...")
402
+ async with stdio_client(server_params) as (read, write):
403
+ async with ClientSession(read, write) as session:
404
+ await session.initialize()
405
+ logger.info("✓ Connected to server")
406
+
407
+ # List available tools
408
+ try:
409
+ tools_result = await session.list_tools()
410
+ tool_names = [t.name for t in tools_result.tools]
411
+ logger.info(f"Available tools: {tool_names}")
412
+ except Exception as e:
413
+ logger.error(f"Failed to list tools: {e}")
414
+ raise
415
+
416
+ # Run tests based on specified tool
417
+ if tool is None or tool == "check":
418
+ await test_check_tool(session, project)
419
+ if tool is None or tool == "prove":
420
+ await test_prove_tool(session, project)
421
+ if tool is None or tool == "goals":
422
+ await test_goals_tool(session, project)
423
+ if tool is None or tool == "eval":
424
+ await test_eval_tool(session, project)
425
+ if tool is None or tool == "search":
426
+ await test_search_tool(session, project)
427
+
428
+ except Exception as e:
429
+ logger.error(f"Test failed: {e}")
430
+ raise
431
+
432
+
433
+ def main():
434
+ """Main entry point for the test client."""
435
+ parser = argparse.ArgumentParser(description="LeanBack MCP Server Test Client")
436
+ parser.add_argument(
437
+ "--project",
438
+ "-p",
439
+ type=str,
440
+ required=False,
441
+ help="Absolute path to a Lean project for testing (required for tool tests)",
442
+ )
443
+ parser.add_argument(
444
+ "--tool",
445
+ type=str,
446
+ choices=["check", "prove", "goals", "eval", "search"],
447
+ help="Test specific tool only",
448
+ )
449
+ parser.add_argument(
450
+ "--debug",
451
+ action="store_true",
452
+ help="Enable debug logging",
453
+ )
454
+ parser.add_argument(
455
+ "--tool-descriptions",
456
+ action="store_true",
457
+ help="Display tool descriptions and exit",
458
+ )
459
+
460
+ args = parser.parse_args()
461
+
462
+ if args.debug:
463
+ logging.getLogger().setLevel(logging.DEBUG)
464
+
465
+ # Show tool descriptions (no project needed)
466
+ if args.tool_descriptions:
467
+ try:
468
+ asyncio.run(show_tool_descriptions())
469
+ except Exception:
470
+ logger.error("Failed to show tool descriptions!")
471
+ sys.exit(1)
472
+ return
473
+
474
+ # For tool tests, project is required
475
+ if not args.project:
476
+ logger.error("--project is required for tool tests")
477
+ parser.print_usage()
478
+ sys.exit(1)
479
+
480
+ # Validate project path
481
+ project_path = Path(args.project).resolve()
482
+ if not project_path.exists():
483
+ logger.error(f"Project directory does not exist: {project_path}")
484
+ sys.exit(1)
485
+
486
+ # Run tests
487
+ try:
488
+ asyncio.run(run_tests(str(project_path), args.tool))
489
+ logger.info("All tests passed!")
490
+ except Exception:
491
+ logger.error("Tests failed!")
492
+ sys.exit(1)
493
+
494
+
495
+ if __name__ == "__main__":
496
+ main()
leanback/prove.py ADDED
@@ -0,0 +1,87 @@
1
+ """
2
+ Implementation of the leanback-prove tool.
3
+
4
+ This module provides functionality for verifying Lean proofs.
5
+ """
6
+
7
+ import os
8
+ from pathlib import Path
9
+
10
+ from rich.console import Console
11
+
12
+ from leanback.common.errors import LeanError
13
+ from leanback.common.util import build_proof_content, create_temp_file
14
+
15
+
16
+ def verify_proof(
17
+ theorem: str,
18
+ tactics: list[str] | None = None,
19
+ proof_file: str | Path | None = None,
20
+ imports: list[str] | None = None,
21
+ project_path: str | Path | None = None,
22
+ console: Console | None = None,
23
+ ) -> tuple[bool, list[LeanError]]:
24
+ """
25
+ Verify a proof without interactive mode.
26
+
27
+ Args:
28
+ theorem: The theorem statement to prove
29
+ tactics: List of tactics to apply
30
+ proof_file: Path to a file containing the proof
31
+ imports: List of modules to import
32
+ project_path: Path to the Lean project
33
+ console: Rich console for output
34
+
35
+ Returns:
36
+ Tuple of (success, errors)
37
+ """
38
+ if console is None:
39
+ console = Console()
40
+
41
+ if project_path and isinstance(project_path, str):
42
+ project_path = Path(project_path)
43
+
44
+ # If proof_file is provided, read the proof from file
45
+ if proof_file:
46
+ proof_file = Path(proof_file) if isinstance(proof_file, str) else proof_file
47
+ if not proof_file.exists():
48
+ return False, [
49
+ LeanError(
50
+ file_name=str(proof_file),
51
+ line=0,
52
+ column=0,
53
+ severity="error",
54
+ message=f"Proof file '{proof_file}' does not exist",
55
+ )
56
+ ]
57
+
58
+ # Run Lean on the file directly
59
+ from leanback.check import check_file
60
+
61
+ return check_file(
62
+ file_path=proof_file, project_path=project_path, console=console
63
+ )
64
+
65
+ # Otherwise, construct a proof from theorem and tactics
66
+ content, _ = build_proof_content(
67
+ theorem=theorem,
68
+ tactics=tactics or [],
69
+ imports=imports or [],
70
+ )
71
+
72
+ # Create a temporary file
73
+ temp_file = create_temp_file(content)
74
+
75
+ try:
76
+ # Check the temporary file
77
+ from leanback.check import check_file
78
+
79
+ return check_file(
80
+ file_path=temp_file, project_path=project_path, console=console
81
+ )
82
+ finally:
83
+ # Clean up
84
+ try:
85
+ os.unlink(temp_file)
86
+ except OSError:
87
+ pass