Files

203 lines
6.6 KiB
Python

"""MCP context provider."""
from __future__ import annotations
import logging
from pathlib import Path
from typing import Optional, List, TYPE_CHECKING
from codexlens.mcp.schema import (
MCPContext,
SymbolInfo,
ReferenceInfo,
RelatedSymbol,
)
if TYPE_CHECKING:
from codexlens.storage.global_index import GlobalSymbolIndex
from codexlens.storage.registry import RegistryStore
from codexlens.search.chain_search import ChainSearchEngine
logger = logging.getLogger(__name__)
class MCPProvider:
"""Builds MCP context objects from codex-lens data."""
def __init__(
self,
global_index: "GlobalSymbolIndex",
search_engine: "ChainSearchEngine",
registry: "RegistryStore",
) -> None:
self.global_index = global_index
self.search_engine = search_engine
self.registry = registry
def build_context(
self,
symbol_name: str,
context_type: str = "symbol_explanation",
include_references: bool = True,
include_related: bool = True,
max_references: int = 10,
) -> Optional[MCPContext]:
"""Build comprehensive context for a symbol.
Args:
symbol_name: Name of the symbol to contextualize
context_type: Type of context being requested
include_references: Whether to include reference locations
include_related: Whether to include related symbols
max_references: Maximum number of references to include
Returns:
MCPContext object or None if symbol not found
"""
# Look up symbol
symbols = self.global_index.search(symbol_name, prefix_mode=False, limit=1)
if not symbols:
logger.debug(f"Symbol not found for MCP context: {symbol_name}")
return None
symbol = symbols[0]
# Build SymbolInfo
symbol_info = SymbolInfo(
name=symbol.name,
kind=symbol.kind,
file_path=symbol.file or "",
line_start=symbol.range[0],
line_end=symbol.range[1],
signature=None, # Symbol entity doesn't have signature
documentation=None, # Symbol entity doesn't have docstring
)
# Extract definition source code
definition = self._extract_definition(symbol)
# Get references
references = []
if include_references:
refs = self.search_engine.search_references(
symbol_name,
limit=max_references,
)
references = [
ReferenceInfo(
file_path=r.file_path,
line=r.line,
column=r.column,
context=r.context,
relationship_type=r.relationship_type,
)
for r in refs
]
# Get related symbols
related_symbols = []
if include_related:
related_symbols = self._get_related_symbols(symbol)
return MCPContext(
context_type=context_type,
symbol=symbol_info,
definition=definition,
references=references,
related_symbols=related_symbols,
metadata={
"source": "codex-lens",
},
)
def _extract_definition(self, symbol) -> Optional[str]:
"""Extract source code for symbol definition."""
try:
file_path = Path(symbol.file) if symbol.file else None
if not file_path or not file_path.exists():
return None
content = file_path.read_text(encoding='utf-8', errors='ignore')
lines = content.split("\n")
start = symbol.range[0] - 1
end = symbol.range[1]
if start >= len(lines):
return None
return "\n".join(lines[start:end])
except Exception as e:
logger.debug(f"Failed to extract definition: {e}")
return None
def _get_related_symbols(self, symbol) -> List[RelatedSymbol]:
"""Get symbols related to the given symbol."""
related = []
try:
# Search for symbols that might be related by name patterns
# This is a simplified implementation - could be enhanced with relationship data
# Look for imports/callers via reference search
refs = self.search_engine.search_references(symbol.name, limit=20)
seen_names = set()
for ref in refs:
# Extract potential symbol name from context
if ref.relationship_type and ref.relationship_type not in seen_names:
related.append(RelatedSymbol(
name=f"{Path(ref.file_path).stem}",
kind="module",
relationship=ref.relationship_type,
file_path=ref.file_path,
))
seen_names.add(ref.relationship_type)
if len(related) >= 10:
break
except Exception as e:
logger.debug(f"Failed to get related symbols: {e}")
return related
def build_context_for_file(
self,
file_path: Path,
context_type: str = "file_overview",
) -> MCPContext:
"""Build context for an entire file."""
# Try to get symbols by searching with file path
# Note: GlobalSymbolIndex doesn't have search_by_file, so we use a different approach
symbols = []
# Search for common symbols that might be in this file
# This is a simplified approach - a full implementation would query by file path
try:
# Use the global index to search for symbols from this file
file_str = str(file_path.resolve())
# Get all symbols and filter by file path (not efficient but works)
all_symbols = self.global_index.search("", prefix_mode=True, limit=1000)
symbols = [s for s in all_symbols if s.file and str(Path(s.file).resolve()) == file_str]
except Exception as e:
logger.debug(f"Failed to get file symbols: {e}")
related = [
RelatedSymbol(
name=s.name,
kind=s.kind,
relationship="defines",
)
for s in symbols
]
return MCPContext(
context_type=context_type,
related_symbols=related,
metadata={
"file_path": str(file_path),
"symbol_count": len(symbols),
},
)