lean-explore 1.0.2__tar.gz → 1.1.0__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.
- {lean_explore-1.0.2 → lean_explore-1.1.0}/PKG-INFO +1 -1
- {lean_explore-1.0.2 → lean_explore-1.1.0}/pyproject.toml +1 -1
- lean_explore-1.1.0/src/lean_explore/mcp/tools.py +244 -0
- lean_explore-1.1.0/src/lean_explore/models/__init__.py +23 -0
- lean_explore-1.1.0/src/lean_explore/models/search_types.py +107 -0
- {lean_explore-1.0.2 → lean_explore-1.1.0}/src/lean_explore.egg-info/PKG-INFO +1 -1
- lean_explore-1.0.2/src/lean_explore/mcp/tools.py +0 -136
- lean_explore-1.0.2/src/lean_explore/models/__init__.py +0 -9
- lean_explore-1.0.2/src/lean_explore/models/search_types.py +0 -53
- {lean_explore-1.0.2 → lean_explore-1.1.0}/LICENSE +0 -0
- {lean_explore-1.0.2 → lean_explore-1.1.0}/README.md +0 -0
- {lean_explore-1.0.2 → lean_explore-1.1.0}/setup.cfg +0 -0
- {lean_explore-1.0.2 → lean_explore-1.1.0}/src/lean_explore/__init__.py +0 -0
- {lean_explore-1.0.2 → lean_explore-1.1.0}/src/lean_explore/api/__init__.py +0 -0
- {lean_explore-1.0.2 → lean_explore-1.1.0}/src/lean_explore/api/client.py +0 -0
- {lean_explore-1.0.2 → lean_explore-1.1.0}/src/lean_explore/cli/__init__.py +0 -0
- {lean_explore-1.0.2 → lean_explore-1.1.0}/src/lean_explore/cli/data_commands.py +0 -0
- {lean_explore-1.0.2 → lean_explore-1.1.0}/src/lean_explore/cli/display.py +0 -0
- {lean_explore-1.0.2 → lean_explore-1.1.0}/src/lean_explore/cli/main.py +0 -0
- {lean_explore-1.0.2 → lean_explore-1.1.0}/src/lean_explore/config.py +0 -0
- {lean_explore-1.0.2 → lean_explore-1.1.0}/src/lean_explore/extract/__init__.py +0 -0
- {lean_explore-1.0.2 → lean_explore-1.1.0}/src/lean_explore/extract/__main__.py +0 -0
- {lean_explore-1.0.2 → lean_explore-1.1.0}/src/lean_explore/extract/doc_gen4.py +0 -0
- {lean_explore-1.0.2 → lean_explore-1.1.0}/src/lean_explore/extract/doc_parser.py +0 -0
- {lean_explore-1.0.2 → lean_explore-1.1.0}/src/lean_explore/extract/embeddings.py +0 -0
- {lean_explore-1.0.2 → lean_explore-1.1.0}/src/lean_explore/extract/github.py +0 -0
- {lean_explore-1.0.2 → lean_explore-1.1.0}/src/lean_explore/extract/index.py +0 -0
- {lean_explore-1.0.2 → lean_explore-1.1.0}/src/lean_explore/extract/informalize.py +0 -0
- {lean_explore-1.0.2 → lean_explore-1.1.0}/src/lean_explore/extract/package_config.py +0 -0
- {lean_explore-1.0.2 → lean_explore-1.1.0}/src/lean_explore/extract/package_registry.py +0 -0
- {lean_explore-1.0.2 → lean_explore-1.1.0}/src/lean_explore/extract/package_utils.py +0 -0
- {lean_explore-1.0.2 → lean_explore-1.1.0}/src/lean_explore/extract/types.py +0 -0
- {lean_explore-1.0.2 → lean_explore-1.1.0}/src/lean_explore/mcp/__init__.py +0 -0
- {lean_explore-1.0.2 → lean_explore-1.1.0}/src/lean_explore/mcp/app.py +0 -0
- {lean_explore-1.0.2 → lean_explore-1.1.0}/src/lean_explore/mcp/server.py +0 -0
- {lean_explore-1.0.2 → lean_explore-1.1.0}/src/lean_explore/models/search_db.py +0 -0
- {lean_explore-1.0.2 → lean_explore-1.1.0}/src/lean_explore/search/__init__.py +0 -0
- {lean_explore-1.0.2 → lean_explore-1.1.0}/src/lean_explore/search/engine.py +0 -0
- {lean_explore-1.0.2 → lean_explore-1.1.0}/src/lean_explore/search/scoring.py +0 -0
- {lean_explore-1.0.2 → lean_explore-1.1.0}/src/lean_explore/search/service.py +0 -0
- {lean_explore-1.0.2 → lean_explore-1.1.0}/src/lean_explore/search/tokenization.py +0 -0
- {lean_explore-1.0.2 → lean_explore-1.1.0}/src/lean_explore/util/__init__.py +0 -0
- {lean_explore-1.0.2 → lean_explore-1.1.0}/src/lean_explore/util/embedding_client.py +0 -0
- {lean_explore-1.0.2 → lean_explore-1.1.0}/src/lean_explore/util/logging.py +0 -0
- {lean_explore-1.0.2 → lean_explore-1.1.0}/src/lean_explore/util/openrouter_client.py +0 -0
- {lean_explore-1.0.2 → lean_explore-1.1.0}/src/lean_explore/util/reranker_client.py +0 -0
- {lean_explore-1.0.2 → lean_explore-1.1.0}/src/lean_explore.egg-info/SOURCES.txt +0 -0
- {lean_explore-1.0.2 → lean_explore-1.1.0}/src/lean_explore.egg-info/dependency_links.txt +0 -0
- {lean_explore-1.0.2 → lean_explore-1.1.0}/src/lean_explore.egg-info/entry_points.txt +0 -0
- {lean_explore-1.0.2 → lean_explore-1.1.0}/src/lean_explore.egg-info/requires.txt +0 -0
- {lean_explore-1.0.2 → lean_explore-1.1.0}/src/lean_explore.egg-info/top_level.txt +0 -0
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
"""Defines MCP tools for interacting with the Lean Explore search engine."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import logging
|
|
5
|
+
from typing import TypedDict
|
|
6
|
+
|
|
7
|
+
from mcp.server.fastmcp import Context as MCPContext
|
|
8
|
+
|
|
9
|
+
from lean_explore.mcp.app import AppContext, BackendServiceType, mcp_app
|
|
10
|
+
from lean_explore.models import SearchResponse, SearchResult
|
|
11
|
+
from lean_explore.models.search_types import (
|
|
12
|
+
SearchResultSummary,
|
|
13
|
+
SearchSummaryResponse,
|
|
14
|
+
extract_bold_description,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class SearchResultSummaryDict(TypedDict, total=False):
|
|
19
|
+
"""Serialized SearchResultSummary for slim MCP search responses."""
|
|
20
|
+
|
|
21
|
+
id: int
|
|
22
|
+
name: str
|
|
23
|
+
description: str | None
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class SearchSummaryResponseDict(TypedDict, total=False):
|
|
27
|
+
"""Serialized SearchSummaryResponse for slim MCP search responses."""
|
|
28
|
+
|
|
29
|
+
query: str
|
|
30
|
+
results: list[SearchResultSummaryDict]
|
|
31
|
+
count: int
|
|
32
|
+
processing_time_ms: int | None
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class SearchResultDict(TypedDict, total=False):
|
|
36
|
+
"""Serialized SearchResult for verbose MCP tool responses."""
|
|
37
|
+
|
|
38
|
+
id: int
|
|
39
|
+
name: str
|
|
40
|
+
module: str
|
|
41
|
+
docstring: str | None
|
|
42
|
+
source_text: str
|
|
43
|
+
source_link: str
|
|
44
|
+
dependencies: str | None
|
|
45
|
+
informalization: str | None
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class SearchResponseDict(TypedDict, total=False):
|
|
49
|
+
"""Serialized SearchResponse for verbose MCP tool responses."""
|
|
50
|
+
|
|
51
|
+
query: str
|
|
52
|
+
results: list[SearchResultDict]
|
|
53
|
+
count: int
|
|
54
|
+
processing_time_ms: int | None
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
logger = logging.getLogger(__name__)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
async def _get_backend_from_context(ctx: MCPContext) -> BackendServiceType:
|
|
61
|
+
"""Retrieves the backend service from the MCP context.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
ctx: The MCP context provided to the tool.
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
The configured backend service (ApiClient or Service).
|
|
68
|
+
|
|
69
|
+
Raises:
|
|
70
|
+
RuntimeError: If the backend service is not available in the context.
|
|
71
|
+
"""
|
|
72
|
+
app_ctx: AppContext = ctx.request_context.lifespan_context
|
|
73
|
+
backend = app_ctx.backend_service
|
|
74
|
+
if not backend:
|
|
75
|
+
logger.error("MCP Tool Error: Backend service is not available.")
|
|
76
|
+
raise RuntimeError("Backend service not configured or available for MCP tool.")
|
|
77
|
+
return backend
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
async def _execute_backend_search(
|
|
81
|
+
backend: BackendServiceType,
|
|
82
|
+
query: str,
|
|
83
|
+
limit: int,
|
|
84
|
+
rerank_top: int | None,
|
|
85
|
+
packages: list[str] | None,
|
|
86
|
+
) -> SearchResponse:
|
|
87
|
+
"""Execute a search on the backend, handling both async and sync backends.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
backend: The backend service (ApiClient or Service).
|
|
91
|
+
query: The search query string.
|
|
92
|
+
limit: Maximum number of results.
|
|
93
|
+
rerank_top: Number of candidates to rerank with cross-encoder.
|
|
94
|
+
packages: Optional package filter.
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
The search response from the backend.
|
|
98
|
+
|
|
99
|
+
Raises:
|
|
100
|
+
RuntimeError: If the backend does not support search.
|
|
101
|
+
"""
|
|
102
|
+
if not hasattr(backend, "search"):
|
|
103
|
+
logger.error("Backend service does not have a 'search' method.")
|
|
104
|
+
raise RuntimeError("Search functionality not available on configured backend.")
|
|
105
|
+
|
|
106
|
+
if asyncio.iscoroutinefunction(backend.search):
|
|
107
|
+
return await backend.search(
|
|
108
|
+
query=query, limit=limit, rerank_top=rerank_top, packages=packages
|
|
109
|
+
)
|
|
110
|
+
return backend.search(
|
|
111
|
+
query=query, limit=limit, rerank_top=rerank_top, packages=packages
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
@mcp_app.tool()
|
|
116
|
+
async def search(
|
|
117
|
+
ctx: MCPContext,
|
|
118
|
+
query: str,
|
|
119
|
+
limit: int = 10,
|
|
120
|
+
rerank_top: int | None = 50,
|
|
121
|
+
packages: list[str] | None = None,
|
|
122
|
+
) -> SearchSummaryResponseDict:
|
|
123
|
+
"""Searches Lean declarations and returns concise results.
|
|
124
|
+
|
|
125
|
+
Returns slim results (id, name, short description) to minimize token usage.
|
|
126
|
+
Use get_by_id to retrieve full details for specific declarations, or
|
|
127
|
+
search_verbose to get all fields upfront.
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
ctx: The MCP context, providing access to the backend service.
|
|
131
|
+
query: A search query string, e.g., "continuous function".
|
|
132
|
+
limit: The maximum number of search results to return. Defaults to 10.
|
|
133
|
+
rerank_top: Number of candidates to rerank with cross-encoder. Set to 0 or
|
|
134
|
+
None to skip reranking. Defaults to 50. Only used with local backend.
|
|
135
|
+
packages: Filter results to specific packages (e.g., ["Mathlib", "Std"]).
|
|
136
|
+
Defaults to None (all packages).
|
|
137
|
+
|
|
138
|
+
Returns:
|
|
139
|
+
A dictionary containing slim search results with id, name, and description.
|
|
140
|
+
"""
|
|
141
|
+
backend = await _get_backend_from_context(ctx)
|
|
142
|
+
logger.info(
|
|
143
|
+
f"MCP Tool 'search' called with query: '{query}', limit: {limit}, "
|
|
144
|
+
f"rerank_top: {rerank_top}, packages: {packages}"
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
response = await _execute_backend_search(
|
|
148
|
+
backend, query, limit, rerank_top, packages
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
# Convert full results to slim summaries
|
|
152
|
+
summary_results = [
|
|
153
|
+
SearchResultSummary(
|
|
154
|
+
id=result.id,
|
|
155
|
+
name=result.name,
|
|
156
|
+
description=extract_bold_description(result.informalization),
|
|
157
|
+
)
|
|
158
|
+
for result in response.results
|
|
159
|
+
]
|
|
160
|
+
summary_response = SearchSummaryResponse(
|
|
161
|
+
query=response.query,
|
|
162
|
+
results=summary_results,
|
|
163
|
+
count=response.count,
|
|
164
|
+
processing_time_ms=response.processing_time_ms,
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
return summary_response.model_dump(exclude_none=True)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
@mcp_app.tool()
|
|
171
|
+
async def search_verbose(
|
|
172
|
+
ctx: MCPContext,
|
|
173
|
+
query: str,
|
|
174
|
+
limit: int = 10,
|
|
175
|
+
rerank_top: int | None = 50,
|
|
176
|
+
packages: list[str] | None = None,
|
|
177
|
+
) -> SearchResponseDict:
|
|
178
|
+
"""Searches Lean declarations and returns full results with all fields.
|
|
179
|
+
|
|
180
|
+
Returns complete results including source code, dependencies, module info,
|
|
181
|
+
and full informalization. Use this when you need all details upfront. For
|
|
182
|
+
a more concise overview, use search instead.
|
|
183
|
+
|
|
184
|
+
Args:
|
|
185
|
+
ctx: The MCP context, providing access to the backend service.
|
|
186
|
+
query: A search query string, e.g., "continuous function".
|
|
187
|
+
limit: The maximum number of search results to return. Defaults to 10.
|
|
188
|
+
rerank_top: Number of candidates to rerank with cross-encoder. Set to 0 or
|
|
189
|
+
None to skip reranking. Defaults to 50. Only used with local backend.
|
|
190
|
+
packages: Filter results to specific packages (e.g., ["Mathlib", "Std"]).
|
|
191
|
+
Defaults to None (all packages).
|
|
192
|
+
|
|
193
|
+
Returns:
|
|
194
|
+
A dictionary containing the full search response with all fields.
|
|
195
|
+
"""
|
|
196
|
+
backend = await _get_backend_from_context(ctx)
|
|
197
|
+
logger.info(
|
|
198
|
+
f"MCP Tool 'search_verbose' called with query: '{query}', limit: {limit}, "
|
|
199
|
+
f"rerank_top: {rerank_top}, packages: {packages}"
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
response = await _execute_backend_search(
|
|
203
|
+
backend, query, limit, rerank_top, packages
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
return response.model_dump(exclude_none=True)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
@mcp_app.tool()
|
|
210
|
+
async def get_by_id(
|
|
211
|
+
ctx: MCPContext,
|
|
212
|
+
declaration_id: int,
|
|
213
|
+
) -> SearchResultDict | None:
|
|
214
|
+
"""Retrieves a specific declaration by its unique identifier.
|
|
215
|
+
|
|
216
|
+
Returns the full declaration including source code, dependencies, module
|
|
217
|
+
info, and informalization. Use this to expand results from the search tool.
|
|
218
|
+
|
|
219
|
+
Args:
|
|
220
|
+
ctx: The MCP context, providing access to the backend service.
|
|
221
|
+
declaration_id: The unique integer identifier of the declaration.
|
|
222
|
+
|
|
223
|
+
Returns:
|
|
224
|
+
A dictionary representing the SearchResult, or None if not found.
|
|
225
|
+
"""
|
|
226
|
+
backend = await _get_backend_from_context(ctx)
|
|
227
|
+
logger.info(f"MCP Tool 'get_by_id' called for declaration_id: {declaration_id}")
|
|
228
|
+
|
|
229
|
+
if not hasattr(backend, "get_by_id"):
|
|
230
|
+
logger.error("Backend service does not have a 'get_by_id' method.")
|
|
231
|
+
raise RuntimeError(
|
|
232
|
+
"Get by ID functionality not available on configured backend."
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
# Call backend get_by_id (handle both async and sync)
|
|
236
|
+
if asyncio.iscoroutinefunction(backend.get_by_id):
|
|
237
|
+
result: SearchResult | None = await backend.get_by_id(
|
|
238
|
+
declaration_id=declaration_id
|
|
239
|
+
)
|
|
240
|
+
else:
|
|
241
|
+
result: SearchResult | None = backend.get_by_id(declaration_id=declaration_id)
|
|
242
|
+
|
|
243
|
+
# Return as dict for MCP, or None
|
|
244
|
+
return result.model_dump(exclude_none=True) if result else None
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Data models for lean_explore.
|
|
2
|
+
|
|
3
|
+
This package contains database models and type definitions for search results.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from lean_explore.models.search_db import Base, Declaration
|
|
7
|
+
from lean_explore.models.search_types import (
|
|
8
|
+
SearchResponse,
|
|
9
|
+
SearchResult,
|
|
10
|
+
SearchResultSummary,
|
|
11
|
+
SearchSummaryResponse,
|
|
12
|
+
extract_bold_description,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"Base",
|
|
17
|
+
"Declaration",
|
|
18
|
+
"SearchResult",
|
|
19
|
+
"SearchResponse",
|
|
20
|
+
"SearchResultSummary",
|
|
21
|
+
"SearchSummaryResponse",
|
|
22
|
+
"extract_bold_description",
|
|
23
|
+
]
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""Type definitions for search results and related data structures."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, ConfigDict
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def extract_bold_description(informalization: str | None) -> str | None:
|
|
9
|
+
"""Extract the bold header text from an informalization string.
|
|
10
|
+
|
|
11
|
+
Informalizations follow the pattern: **Bold Title.** Rest of description...
|
|
12
|
+
This function extracts just the bold title portion.
|
|
13
|
+
|
|
14
|
+
Args:
|
|
15
|
+
informalization: The full informalization text, or None.
|
|
16
|
+
|
|
17
|
+
Returns:
|
|
18
|
+
The bold header text (without ** markers), or None if no bold
|
|
19
|
+
header is found or input is None.
|
|
20
|
+
"""
|
|
21
|
+
if not informalization:
|
|
22
|
+
return None
|
|
23
|
+
match = re.match(r"\*\*(.+?)\*\*", informalization)
|
|
24
|
+
return match.group(1) if match else None
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class SearchResultSummary(BaseModel):
|
|
28
|
+
"""A slim search result containing only identification and description.
|
|
29
|
+
|
|
30
|
+
Used by the MCP search tool to return concise results that minimize
|
|
31
|
+
token usage. Consumers can use the id to fetch full details via get_by_id.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
id: int
|
|
35
|
+
"""Primary key identifier."""
|
|
36
|
+
|
|
37
|
+
name: str
|
|
38
|
+
"""Fully qualified Lean name (e.g., 'Nat.add')."""
|
|
39
|
+
|
|
40
|
+
description: str | None
|
|
41
|
+
"""Short description extracted from the informalization bold header."""
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class SearchSummaryResponse(BaseModel):
|
|
45
|
+
"""Response from a slim search operation containing summary results."""
|
|
46
|
+
|
|
47
|
+
query: str
|
|
48
|
+
"""The original search query string."""
|
|
49
|
+
|
|
50
|
+
results: list[SearchResultSummary]
|
|
51
|
+
"""List of slim search results."""
|
|
52
|
+
|
|
53
|
+
count: int
|
|
54
|
+
"""Number of results returned."""
|
|
55
|
+
|
|
56
|
+
processing_time_ms: int | None = None
|
|
57
|
+
"""Processing time in milliseconds, if available."""
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class SearchResult(BaseModel):
|
|
61
|
+
"""A search result representing a Lean declaration.
|
|
62
|
+
|
|
63
|
+
This model represents the core information returned from a search query,
|
|
64
|
+
mirroring the essential fields from the database Declaration model.
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
id: int
|
|
68
|
+
"""Primary key identifier."""
|
|
69
|
+
|
|
70
|
+
name: str
|
|
71
|
+
"""Fully qualified Lean name (e.g., 'Nat.add')."""
|
|
72
|
+
|
|
73
|
+
module: str
|
|
74
|
+
"""Module name (e.g., 'Mathlib.Data.List.Basic')."""
|
|
75
|
+
|
|
76
|
+
docstring: str | None
|
|
77
|
+
"""Documentation string from the source code, if available."""
|
|
78
|
+
|
|
79
|
+
source_text: str
|
|
80
|
+
"""The actual Lean source code for this declaration."""
|
|
81
|
+
|
|
82
|
+
source_link: str
|
|
83
|
+
"""GitHub URL to the declaration source code."""
|
|
84
|
+
|
|
85
|
+
dependencies: str | None
|
|
86
|
+
"""JSON array of declaration names this declaration depends on."""
|
|
87
|
+
|
|
88
|
+
informalization: str | None
|
|
89
|
+
"""Natural language description of the declaration."""
|
|
90
|
+
|
|
91
|
+
model_config = ConfigDict(from_attributes=True)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class SearchResponse(BaseModel):
|
|
95
|
+
"""Response from a search operation containing results and metadata."""
|
|
96
|
+
|
|
97
|
+
query: str
|
|
98
|
+
"""The original search query string."""
|
|
99
|
+
|
|
100
|
+
results: list[SearchResult]
|
|
101
|
+
"""List of search results."""
|
|
102
|
+
|
|
103
|
+
count: int
|
|
104
|
+
"""Number of results returned."""
|
|
105
|
+
|
|
106
|
+
processing_time_ms: int | None = None
|
|
107
|
+
"""Processing time in milliseconds, if available."""
|
|
@@ -1,136 +0,0 @@
|
|
|
1
|
-
"""Defines MCP tools for interacting with the Lean Explore search engine."""
|
|
2
|
-
|
|
3
|
-
import asyncio
|
|
4
|
-
import logging
|
|
5
|
-
from typing import TypedDict
|
|
6
|
-
|
|
7
|
-
from mcp.server.fastmcp import Context as MCPContext
|
|
8
|
-
|
|
9
|
-
from lean_explore.mcp.app import AppContext, BackendServiceType, mcp_app
|
|
10
|
-
from lean_explore.models import SearchResponse, SearchResult
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
class SearchResultDict(TypedDict, total=False):
|
|
14
|
-
"""Serialized SearchResult for MCP tool responses."""
|
|
15
|
-
|
|
16
|
-
id: int
|
|
17
|
-
name: str
|
|
18
|
-
module: str
|
|
19
|
-
docstring: str | None
|
|
20
|
-
source_text: str
|
|
21
|
-
source_link: str
|
|
22
|
-
dependencies: str | None
|
|
23
|
-
informalization: str | None
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
class SearchResponseDict(TypedDict, total=False):
|
|
27
|
-
"""Serialized SearchResponse for MCP tool responses."""
|
|
28
|
-
|
|
29
|
-
query: str
|
|
30
|
-
results: list[SearchResultDict]
|
|
31
|
-
count: int
|
|
32
|
-
processing_time_ms: int | None
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
logger = logging.getLogger(__name__)
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
async def _get_backend_from_context(ctx: MCPContext) -> BackendServiceType:
|
|
39
|
-
"""Retrieves the backend service from the MCP context.
|
|
40
|
-
|
|
41
|
-
Args:
|
|
42
|
-
ctx: The MCP context provided to the tool.
|
|
43
|
-
|
|
44
|
-
Returns:
|
|
45
|
-
The configured backend service (ApiClient or Service).
|
|
46
|
-
|
|
47
|
-
Raises:
|
|
48
|
-
RuntimeError: If the backend service is not available in the context.
|
|
49
|
-
"""
|
|
50
|
-
app_ctx: AppContext = ctx.request_context.lifespan_context
|
|
51
|
-
backend = app_ctx.backend_service
|
|
52
|
-
if not backend:
|
|
53
|
-
logger.error("MCP Tool Error: Backend service is not available.")
|
|
54
|
-
raise RuntimeError("Backend service not configured or available for MCP tool.")
|
|
55
|
-
return backend
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
@mcp_app.tool()
|
|
59
|
-
async def search(
|
|
60
|
-
ctx: MCPContext,
|
|
61
|
-
query: str,
|
|
62
|
-
limit: int = 10,
|
|
63
|
-
rerank_top: int | None = 50,
|
|
64
|
-
packages: list[str] | None = None,
|
|
65
|
-
) -> SearchResponseDict:
|
|
66
|
-
"""Searches Lean declarations by a query string.
|
|
67
|
-
|
|
68
|
-
Args:
|
|
69
|
-
ctx: The MCP context, providing access to the backend service.
|
|
70
|
-
query: A search query string, e.g., "continuous function".
|
|
71
|
-
limit: The maximum number of search results to return. Defaults to 10.
|
|
72
|
-
rerank_top: Number of candidates to rerank with cross-encoder. Set to 0 or
|
|
73
|
-
None to skip reranking. Defaults to 50. Only used with local backend.
|
|
74
|
-
packages: Filter results to specific packages (e.g., ["Mathlib", "Std"]).
|
|
75
|
-
Defaults to None (all packages).
|
|
76
|
-
|
|
77
|
-
Returns:
|
|
78
|
-
A dictionary containing the search response with results.
|
|
79
|
-
"""
|
|
80
|
-
backend = await _get_backend_from_context(ctx)
|
|
81
|
-
logger.info(
|
|
82
|
-
f"MCP Tool 'search' called with query: '{query}', limit: {limit}, "
|
|
83
|
-
f"rerank_top: {rerank_top}, packages: {packages}"
|
|
84
|
-
)
|
|
85
|
-
|
|
86
|
-
if not hasattr(backend, "search"):
|
|
87
|
-
logger.error("Backend service does not have a 'search' method.")
|
|
88
|
-
raise RuntimeError("Search functionality not available on configured backend.")
|
|
89
|
-
|
|
90
|
-
# Call backend search (handle both async and sync)
|
|
91
|
-
if asyncio.iscoroutinefunction(backend.search):
|
|
92
|
-
response: SearchResponse = await backend.search(
|
|
93
|
-
query=query, limit=limit, rerank_top=rerank_top, packages=packages
|
|
94
|
-
)
|
|
95
|
-
else:
|
|
96
|
-
response: SearchResponse = backend.search(
|
|
97
|
-
query=query, limit=limit, rerank_top=rerank_top, packages=packages
|
|
98
|
-
)
|
|
99
|
-
|
|
100
|
-
# Return as dict for MCP
|
|
101
|
-
return response.model_dump(exclude_none=True)
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
@mcp_app.tool()
|
|
105
|
-
async def get_by_id(
|
|
106
|
-
ctx: MCPContext,
|
|
107
|
-
declaration_id: int,
|
|
108
|
-
) -> SearchResultDict | None:
|
|
109
|
-
"""Retrieves a specific declaration by its unique identifier.
|
|
110
|
-
|
|
111
|
-
Args:
|
|
112
|
-
ctx: The MCP context, providing access to the backend service.
|
|
113
|
-
declaration_id: The unique integer identifier of the declaration.
|
|
114
|
-
|
|
115
|
-
Returns:
|
|
116
|
-
A dictionary representing the SearchResult, or None if not found.
|
|
117
|
-
"""
|
|
118
|
-
backend = await _get_backend_from_context(ctx)
|
|
119
|
-
logger.info(f"MCP Tool 'get_by_id' called for declaration_id: {declaration_id}")
|
|
120
|
-
|
|
121
|
-
if not hasattr(backend, "get_by_id"):
|
|
122
|
-
logger.error("Backend service does not have a 'get_by_id' method.")
|
|
123
|
-
raise RuntimeError(
|
|
124
|
-
"Get by ID functionality not available on configured backend."
|
|
125
|
-
)
|
|
126
|
-
|
|
127
|
-
# Call backend get_by_id (handle both async and sync)
|
|
128
|
-
if asyncio.iscoroutinefunction(backend.get_by_id):
|
|
129
|
-
result: SearchResult | None = await backend.get_by_id(
|
|
130
|
-
declaration_id=declaration_id
|
|
131
|
-
)
|
|
132
|
-
else:
|
|
133
|
-
result: SearchResult | None = backend.get_by_id(declaration_id=declaration_id)
|
|
134
|
-
|
|
135
|
-
# Return as dict for MCP, or None
|
|
136
|
-
return result.model_dump(exclude_none=True) if result else None
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
"""Data models for lean_explore.
|
|
2
|
-
|
|
3
|
-
This package contains database models and type definitions for search results.
|
|
4
|
-
"""
|
|
5
|
-
|
|
6
|
-
from lean_explore.models.search_db import Base, Declaration
|
|
7
|
-
from lean_explore.models.search_types import SearchResponse, SearchResult
|
|
8
|
-
|
|
9
|
-
__all__ = ["Base", "Declaration", "SearchResult", "SearchResponse"]
|
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
"""Type definitions for search results and related data structures."""
|
|
2
|
-
|
|
3
|
-
from pydantic import BaseModel, ConfigDict
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
class SearchResult(BaseModel):
|
|
7
|
-
"""A search result representing a Lean declaration.
|
|
8
|
-
|
|
9
|
-
This model represents the core information returned from a search query,
|
|
10
|
-
mirroring the essential fields from the database Declaration model.
|
|
11
|
-
"""
|
|
12
|
-
|
|
13
|
-
id: int
|
|
14
|
-
"""Primary key identifier."""
|
|
15
|
-
|
|
16
|
-
name: str
|
|
17
|
-
"""Fully qualified Lean name (e.g., 'Nat.add')."""
|
|
18
|
-
|
|
19
|
-
module: str
|
|
20
|
-
"""Module name (e.g., 'Mathlib.Data.List.Basic')."""
|
|
21
|
-
|
|
22
|
-
docstring: str | None
|
|
23
|
-
"""Documentation string from the source code, if available."""
|
|
24
|
-
|
|
25
|
-
source_text: str
|
|
26
|
-
"""The actual Lean source code for this declaration."""
|
|
27
|
-
|
|
28
|
-
source_link: str
|
|
29
|
-
"""GitHub URL to the declaration source code."""
|
|
30
|
-
|
|
31
|
-
dependencies: str | None
|
|
32
|
-
"""JSON array of declaration names this declaration depends on."""
|
|
33
|
-
|
|
34
|
-
informalization: str | None
|
|
35
|
-
"""Natural language description of the declaration."""
|
|
36
|
-
|
|
37
|
-
model_config = ConfigDict(from_attributes=True)
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
class SearchResponse(BaseModel):
|
|
41
|
-
"""Response from a search operation containing results and metadata."""
|
|
42
|
-
|
|
43
|
-
query: str
|
|
44
|
-
"""The original search query string."""
|
|
45
|
-
|
|
46
|
-
results: list[SearchResult]
|
|
47
|
-
"""List of search results."""
|
|
48
|
-
|
|
49
|
-
count: int
|
|
50
|
-
"""Number of results returned."""
|
|
51
|
-
|
|
52
|
-
processing_time_ms: int | None = None
|
|
53
|
-
"""Processing time in milliseconds, if available."""
|
|
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
|
|
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
|
|
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
|