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.
- {leanback-0.1.1 → leanback-0.1.3}/PKG-INFO +2 -2
- {leanback-0.1.1 → leanback-0.1.3}/pyproject.toml +10 -7
- {leanback-0.1.1 → leanback-0.1.3}/src/leanback/__init__.py +1 -1
- {leanback-0.1.1 → leanback-0.1.3}/src/leanback/check.py +2 -2
- {leanback-0.1.1 → leanback-0.1.3}/src/leanback/common/env.py +25 -0
- {leanback-0.1.1 → leanback-0.1.3}/src/leanback/mcp_server.py +67 -9
- {leanback-0.1.1 → leanback-0.1.3}/.gitignore +0 -0
- {leanback-0.1.1 → leanback-0.1.3}/LICENSE +0 -0
- {leanback-0.1.1 → leanback-0.1.3}/README.md +0 -0
- {leanback-0.1.1 → leanback-0.1.3}/src/leanback/README.md +0 -0
- {leanback-0.1.1 → leanback-0.1.3}/src/leanback/common/__init__.py +0 -0
- {leanback-0.1.1 → leanback-0.1.3}/src/leanback/common/errors.py +0 -0
- {leanback-0.1.1 → leanback-0.1.3}/src/leanback/common/path_manager.py +0 -0
- {leanback-0.1.1 → leanback-0.1.3}/src/leanback/common/project_context.py +0 -0
- {leanback-0.1.1 → leanback-0.1.3}/src/leanback/common/util.py +0 -0
- {leanback-0.1.1 → leanback-0.1.3}/src/leanback/goals.py +0 -0
- {leanback-0.1.1 → leanback-0.1.3}/src/leanback/mathlib_search.py +0 -0
- {leanback-0.1.1 → leanback-0.1.3}/src/leanback/mathlib_search_simple.py +0 -0
- {leanback-0.1.1 → leanback-0.1.3}/src/leanback/mcp_test.py +0 -0
- {leanback-0.1.1 → leanback-0.1.3}/src/leanback/prove.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: leanback
|
|
3
|
-
Version: 0.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.
|
|
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.
|
|
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.
|
|
24
|
+
"mcp>=1.26.0",
|
|
25
25
|
]
|
|
26
26
|
|
|
27
27
|
[dependency-groups]
|
|
28
28
|
dev = [
|
|
29
|
-
"pytest>=
|
|
30
|
-
"ruff>=0.
|
|
31
|
-
"mypy>=1.
|
|
32
|
-
"coverage>=7.
|
|
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]
|
|
@@ -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(
|
|
174
|
-
|
|
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":
|
|
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(
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|