leanback 0.1.1__tar.gz → 0.1.3__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: leanback
3
- Version: 0.1.1
3
+ Version: 0.1.3
4
4
  Summary: MCP server for Lean 4 theorem proving — check, prove, goals, eval, search
5
5
  Author: Stefan Szeider
6
6
  License: Apache-2.0
@@ -13,7 +13,7 @@ Classifier: Programming Language :: Python :: 3.12
13
13
  Classifier: Programming Language :: Python :: 3.13
14
14
  Classifier: Topic :: Scientific/Engineering :: Mathematics
15
15
  Requires-Python: >=3.11
16
- Requires-Dist: mcp>=1.9.4
16
+ Requires-Dist: mcp>=1.26.0
17
17
  Requires-Dist: rich>=14.0.0
18
18
  Description-Content-Type: text/markdown
19
19
 
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "leanback"
7
- version = "0.1.1"
7
+ version = "0.1.3"
8
8
  description = "MCP server for Lean 4 theorem proving — check, prove, goals, eval, search"
9
9
  readme = "README.md"
10
10
  license = { text = "Apache-2.0" }
@@ -21,15 +21,15 @@ classifiers = [
21
21
  ]
22
22
  dependencies = [
23
23
  "rich>=14.0.0",
24
- "mcp>=1.9.4",
24
+ "mcp>=1.26.0",
25
25
  ]
26
26
 
27
27
  [dependency-groups]
28
28
  dev = [
29
- "pytest>=8.4.0",
30
- "ruff>=0.11.0",
31
- "mypy>=1.16.0",
32
- "coverage>=7.8.0",
29
+ "pytest>=9.0.0",
30
+ "ruff>=0.15.0",
31
+ "mypy>=1.19.0",
32
+ "coverage>=7.13.0",
33
33
  ]
34
34
 
35
35
  [tool.ruff]
@@ -47,6 +47,9 @@ line-length = 88
47
47
  # Auto-fix issues
48
48
  fix = true
49
49
 
50
+ # Exclude non-Python project directories
51
+ exclude = ["projects/"]
52
+
50
53
  [tool.ruff.format]
51
54
  # Black-compatible formatting
52
55
  quote-style = "double"
@@ -87,7 +90,7 @@ lines-after-imports = 2
87
90
 
88
91
  [tool.ruff.lint.per-file-ignores]
89
92
  # Tests can use assert and have unused imports
90
- "tests/*" = ["S101", "F401", "F841"]
93
+ "tests/*" = ["S101", "F401", "F841", "RUF059"]
91
94
  "__init__.py" = ["F401"]
92
95
 
93
96
  [tool.mypy]
@@ -1,3 +1,3 @@
1
1
  """LeanBack - Lean 4 development tools for Claude Code."""
2
2
 
3
- __version__ = "0.3.0"
3
+ __version__ = "0.1.2"
@@ -295,8 +295,8 @@ def check_project(
295
295
  lean_files = list(project_path.glob("**/*.lean"))
296
296
 
297
297
  for file_path in lean_files:
298
- # Skip files in .lake directory
299
- if ".lake" in file_path.parts:
298
+ # Skip files in .lake directory and lakefile.lean (uses Lake DSL, not standard Lean)
299
+ if ".lake" in file_path.parts or file_path.name == "lakefile.lean":
300
300
  continue
301
301
 
302
302
  console.print(f"Checking {file_path.relative_to(project_path)}...")
@@ -6,9 +6,29 @@ This module provides functionality for executing Lean commands in the correct en
6
6
 
7
7
  import os
8
8
  import subprocess
9
+ import sys
9
10
  from pathlib import Path
10
11
 
11
12
 
13
+ def _find_native_dynlibs(project_path: Path) -> list[Path]:
14
+ """Discover native shared libraries in Lake packages.
15
+
16
+ Scans .lake/packages/*/.lake/build/lib/ for .so (Linux) or .dylib (macOS)
17
+ files that need --load-dynlib for the Lean interpreter.
18
+ """
19
+ packages_dir = project_path / ".lake" / "packages"
20
+ if not packages_dir.is_dir():
21
+ return []
22
+
23
+ if sys.platform == "darwin":
24
+ ext = "dylib"
25
+ elif sys.platform == "win32":
26
+ ext = "dll"
27
+ else:
28
+ ext = "so"
29
+ return sorted(packages_dir.glob(f"*/.lake/build/lib/lib*.{ext}"))
30
+
31
+
12
32
  class LeanRunner:
13
33
  """
14
34
  Utility for running Lean commands in the appropriate environment.
@@ -53,6 +73,11 @@ class LeanRunner:
53
73
 
54
74
  # Always use lake env lean for projects - this ensures proper dependency management
55
75
  command = ["lake", "env", "lean"]
76
+
77
+ # Load native shared libraries for FFI-dependent packages (e.g., cvc5)
78
+ for lib in _find_native_dynlibs(project_path):
79
+ command.append(f"--load-dynlib={lib}")
80
+
56
81
  if capture_json:
57
82
  command.append("--json")
58
83
  command.append(str(abs_file_path))
@@ -14,6 +14,8 @@ import time
14
14
  from pathlib import Path
15
15
 
16
16
  from mcp.server.fastmcp import FastMCP
17
+ from mcp.server.fastmcp.exceptions import ToolError
18
+ from mcp.server.fastmcp.tools.tool_manager import ToolManager
17
19
 
18
20
 
19
21
  def _json(obj: object) -> str:
@@ -87,6 +89,25 @@ modify files. Use your own file tools to create/edit .lean files, then use LeanB
87
89
  )
88
90
 
89
91
 
92
+ class _StrictToolManager(ToolManager):
93
+ """Rejects unknown parameters instead of silently dropping them."""
94
+
95
+ async def call_tool(self, name, arguments, context=None, **kwargs):
96
+ tool = self.get_tool(name)
97
+ if tool and arguments:
98
+ known = set(tool.parameters.get("properties", {}).keys())
99
+ unknown = set(arguments.keys()) - known
100
+ if unknown:
101
+ raise ToolError(
102
+ f"Unknown parameter(s) for '{name}': {', '.join(sorted(unknown))}. "
103
+ f"Valid parameters: {', '.join(sorted(known))}"
104
+ )
105
+ return await super().call_tool(name, arguments, context, **kwargs)
106
+
107
+
108
+ mcp._tool_manager = _StrictToolManager()
109
+
110
+
90
111
  def _resolve_project(project: str) -> Path | None:
91
112
  """Resolve and validate a project path. Returns Path or None."""
92
113
  p = Path(project)
@@ -170,11 +191,26 @@ def _clean_lean_message(message: str) -> str:
170
191
  return message
171
192
 
172
193
 
173
- def _serialize_errors(errors: list, sanitize_paths: bool = False) -> list[dict]:
174
- """Serialize LeanError objects to dicts for JSON output."""
194
+ def _serialize_errors(
195
+ errors: list, sanitize_paths: bool = False, source_file: str | None = None
196
+ ) -> list[dict]:
197
+ """Serialize LeanError objects to dicts for JSON output.
198
+
199
+ If source_file is provided, temp file paths are replaced with it
200
+ instead of the generic '<inline>' label.
201
+ """
202
+
203
+ def _resolve_path(file_name: str) -> str:
204
+ if not sanitize_paths:
205
+ return file_name
206
+ sanitized = _sanitize_file_path(file_name)
207
+ if sanitized == "<inline>" and source_file:
208
+ return source_file
209
+ return sanitized
210
+
175
211
  return [
176
212
  {
177
- "file": _sanitize_file_path(e.file_name) if sanitize_paths else e.file_name,
213
+ "file": _resolve_path(e.file_name),
178
214
  "line": e.line,
179
215
  "column": e.column,
180
216
  "end_line": e.end_line,
@@ -413,7 +449,7 @@ def check(
413
449
  - file: Path to a .lean file within the project (e.g., "Basic.lean", "src/Logic.lean")
414
450
  - check_all: Check each file individually for detailed error reporting (slower but more thorough)
415
451
  - expr: A Lean expression or command to check (e.g., "2 + 2 = 4", "Nat.add", "#eval 1 + 1"). Bare expressions are auto-wrapped in #check.
416
- - imports: List of modules to import when checking expressions (e.g., ["Mathlib.Tactic"])
452
+ - imports: List of modules to import when checking expressions (e.g., ["Mathlib.Tactic"]). First Mathlib import takes ~20-30s.
417
453
 
418
454
  Returns JSON with 'success' boolean. The 'messages' array contains objects with:
419
455
  - file: Path to the file (or "<inline>" for expressions)
@@ -565,6 +601,12 @@ def check(
565
601
  "files_checked": 1,
566
602
  },
567
603
  }
604
+ if file_path.name == "lakefile.lean" and not success:
605
+ result["note"] = (
606
+ "lakefile.lean uses Lake DSL which may produce errors "
607
+ "when checked outside the build system. "
608
+ "These errors are typically not actionable."
609
+ )
568
610
  return _json(result)
569
611
 
570
612
  # Case 3: Check entire project
@@ -591,6 +633,9 @@ def check(
591
633
  "total_info": info_count,
592
634
  "files_with_errors": len(files_with_errors),
593
635
  },
636
+ "hint": "This checked the entire project via 'lake build'. "
637
+ "To check specific code, use expr=. "
638
+ "To execute code, use the eval tool.",
594
639
  }
595
640
  return _json(result)
596
641
 
@@ -620,7 +665,7 @@ def prove(
620
665
  - tactics: List of proof tactics to apply in order (e.g., ["intro n", "rfl"])
621
666
  - file: Path to a .lean file containing complete proofs to verify
622
667
  - imports: Additional modules to import (e.g., ["Mathlib.Data.Nat.Basic"])
623
- - mathlib: Import Mathlib.Tactic for all standard tactics (simp, ring, omega, linarith, norm_num, decide, aesop, etc.)
668
+ - mathlib: Import Mathlib.Tactic for all standard tactics (simp, ring, omega, linarith, norm_num, decide, aesop, etc.). First call takes ~8-11s for import.
624
669
 
625
670
  Use either (theorem + tactics) OR (file), not both. When using theorem + tactics,
626
671
  the tool constructs and verifies the proof. When using file, it verifies existing proofs.
@@ -798,7 +843,7 @@ def goals(
798
843
  - theorem: Theorem statement to prove (e.g., "2 + 2 = 4", "∀ n : Nat, n + 0 = n")
799
844
  - tactics: List of tactics applied so far (e.g., ["intro n"]). Omit or pass [] to see the initial goal.
800
845
  - imports: Additional modules to import (e.g., ["Mathlib.Data.Nat.Basic"])
801
- - mathlib: Import Mathlib.Tactic for standard tactics (simp, ring, omega, linarith, norm_num, decide, aesop, etc.)
846
+ - mathlib: Import Mathlib.Tactic for standard tactics (simp, ring, omega, linarith, norm_num, decide, aesop, etc.). First call takes ~8-11s for import.
802
847
 
803
848
  Returns JSON with:
804
849
  - success: Whether the tool ran without internal errors
@@ -884,7 +929,7 @@ def eval(
884
929
  - code: Lean code to execute (e.g., "#eval 2 + 2", "#check Nat.add", "def f := 5")
885
930
  - file: Path to a .lean file to execute instead of inline code
886
931
  - imports: Additional modules to import (e.g., ["Mathlib.Data.Finset.Basic"])
887
- - mathlib: Import Mathlib.Tactic for all standard tactics (simp, ring, omega, linarith, norm_num, decide, aesop, etc.)
932
+ - mathlib: Import Mathlib.Tactic for all standard tactics (simp, ring, omega, linarith, norm_num, decide, aesop, etc.). First call takes ~8-11s for import.
888
933
 
889
934
  Use either 'code' OR 'file', not both. The tool can handle multi-line code.
890
935
 
@@ -984,7 +1029,9 @@ def eval(
984
1029
  "type": "code_execution",
985
1030
  "code": code_to_execute,
986
1031
  "imports": resolved_imports,
987
- "messages": _serialize_errors(errors, sanitize_paths=True),
1032
+ "messages": _serialize_errors(
1033
+ errors, sanitize_paths=True, source_file=file
1034
+ ),
988
1035
  }
989
1036
  )
990
1037
 
@@ -1089,6 +1136,12 @@ def search(
1089
1136
  )
1090
1137
 
1091
1138
  results = batch_search_declarations(patterns, project_path, **search_params)
1139
+ if isinstance(results, dict):
1140
+ results["note"] = (
1141
+ "Search covers Mathlib declarations only, "
1142
+ "not core Lean (e.g., Nat.add_comm, List.map). "
1143
+ "Use eval with #check to inspect core declarations."
1144
+ )
1092
1145
  return _json(results)
1093
1146
  else:
1094
1147
  try:
@@ -1122,6 +1175,11 @@ def search(
1122
1175
  "results": serializable_results,
1123
1176
  "metadata": metadata,
1124
1177
  }
1178
+ result["note"] = (
1179
+ "Search covers Mathlib declarations only, "
1180
+ "not core Lean (e.g., Nat.add_comm, List.map). "
1181
+ "Use eval with #check to inspect core declarations."
1182
+ )
1125
1183
  if len(serializable_results) == 0:
1126
1184
  if filter_namespace and total_found > 0:
1127
1185
  result["note"] = (
@@ -1133,7 +1191,7 @@ def search(
1133
1191
  result["note"] = (
1134
1192
  "No matches found. This search covers Mathlib declarations only, "
1135
1193
  "not core Lean (e.g., Nat.add_comm, List.map). "
1136
- "Try a broader pattern or check the declaration name."
1194
+ "Try a broader pattern, or use eval with #check to inspect core declarations."
1137
1195
  )
1138
1196
  return _json(result)
1139
1197
  except SearchError as e:
File without changes
File without changes
File without changes
File without changes
File without changes