Files
Claude-Code-Workflow/codex-lens/docs/LSP_INTEGRATION_PLAN.md

84 KiB
Raw Permalink Blame History

codex-lens LSP Integration - Complete Execution Plan

Version: 1.0 Created: 2026-01-15 Based on: Gemini Multi-Round Deep Analysis Status: Ready for Execution


Table of Contents

  1. Executive Summary
  2. Claude Code LSP Implementation Reference
  3. Architecture Overview
  4. Phase 1: LSP Server Foundation
  5. Phase 2: Find References
  6. Phase 3: Hover Information
  7. Phase 4: MCP Bridge
  8. Phase 5: Advanced Features
  9. Testing Strategy
  10. Deployment Guide
  11. Risk Mitigation

1. Executive Summary

1.1 Project Goal

将 codex-lens 的代码索引和搜索能力通过 LSP (Language Server Protocol) 暴露,使其能够:

  • 为 IDE/编辑器提供代码导航功能
  • 与 Claude Code 的 hook 系统集成
  • 通过 MCP (Model Context Protocol) 为 LLM 提供结构化代码上下文

1.2 Value Proposition

Capability Before After
Code Navigation CLI only IDE integration via LSP
Context for LLM Manual copy-paste Automated MCP injection
Real-time Updates Batch re-index Incremental on save
Cross-project Search Per-directory Unified global index

1.3 Success Criteria

  • All 5 core LSP methods implemented and tested
  • Query latency < 100ms for 95th percentile
  • MCP context generation working with Claude Code hooks
  • Documentation and examples complete

2. Claude Code LSP Implementation Reference

本章节记录 Claude Code 当前 LSP 实现方式,作为 codex-lens 集成的技术参考。

2.1 Claude Code LSP 实现方式概览

Claude Code 实现 LSP 功能有 三种方式

方式 描述 适用场景
内置 LSP 工具 v2.0.74+ 原生支持 快速启用,基础功能
MCP Server (cclsp) 第三方 MCP 桥接 高级功能,位置容错
Plugin Marketplace 插件市场安装 多语言扩展支持

2.2 方式一:内置 LSP 工具 (v2.0.74+)

Claude Code 从 v2.0.74 版本开始内置 LSP 支持。

启用方式

# 设置环境变量启用 LSP
export ENABLE_LSP_TOOL=1
claude

# 永久启用 (添加到 shell 配置)
echo 'export ENABLE_LSP_TOOL=1' >> ~/.bashrc

内置 LSP 工具清单

工具名 功能 对应 LSP 方法 性能
goToDefinition 跳转到符号定义 textDocument/definition ~50ms
findReferences 查找所有引用 textDocument/references ~100ms
documentSymbol 获取文件符号结构 textDocument/documentSymbol ~30ms
hover 显示类型签名和文档 textDocument/hover ~50ms
getDiagnostics 获取诊断信息 textDocument/diagnostic ~100ms

性能对比

传统文本搜索: ~45,000ms (45秒)
LSP 语义搜索: ~50ms
性能提升: 约 900 倍

当前限制

  • 部分语言返回 "No LSP server available"
  • 需要额外安装语言服务器插件
  • 不支持重命名等高级操作

2.3 方式二MCP Server 方式 (cclsp)

cclsp 是一个 MCP Server将 LSP 能力暴露给 Claude Code。

架构图

┌─────────────────────────────────────────────────────────────────┐
│                        Claude Code                              │
│                    (MCP Client)                                 │
└───────────────────────────┬─────────────────────────────────────┘
                            │
                            │ MCP Protocol (JSON-RPC over stdio)
                            │
┌───────────────────────────▼─────────────────────────────────────┐
│                         cclsp                                   │
│                    (MCP Server)                                 │
│  ┌─────────────────────────────────────────────────────────┐   │
│  │              Position Tolerance Layer                    │   │
│  │   (自动尝试多个位置组合,解决 AI 行号不精确问题)           │   │
│  └─────────────────────────────────────────────────────────┘   │
│                            │                                    │
│         ┌──────────────────┼──────────────────┐                │
│         │                  │                  │                │
│         ▼                  ▼                  ▼                │
│  ┌─────────────┐   ┌─────────────┐   ┌─────────────┐          │
│  │   pylsp     │   │   gopls     │   │rust-analyzer│          │
│  │  (Python)   │   │    (Go)     │   │   (Rust)    │          │
│  └─────────────┘   └─────────────┘   └─────────────┘          │
└─────────────────────────────────────────────────────────────────┘

安装与配置

# 一次性运行 (无需安装)
npx cclsp@latest setup

# 用户级配置
npx cclsp@latest setup --user

配置文件格式

位置: .claude/cclsp.json~/.config/claude/cclsp.json

{
  "servers": [
    {
      "extensions": ["py", "pyi"],
      "command": ["pylsp"],
      "rootDir": ".",
      "restartInterval": 5,
      "initializationOptions": {}
    },
    {
      "extensions": ["ts", "tsx", "js", "jsx"],
      "command": ["typescript-language-server", "--stdio"],
      "rootDir": "."
    },
    {
      "extensions": ["go"],
      "command": ["gopls"],
      "rootDir": "."
    },
    {
      "extensions": ["rs"],
      "command": ["rust-analyzer"],
      "rootDir": "."
    }
  ]
}

cclsp 暴露的 MCP 工具

MCP 工具 功能 特性
find_definition 按名称和类型查找定义 支持模糊匹配
find_references 查找所有引用位置 跨文件搜索
rename_symbol 重命名符号 创建 .bak 备份
rename_symbol_strict 精确位置重命名 处理同名歧义
get_diagnostics 获取诊断信息 错误/警告/提示
restart_server 重启 LSP 服务器 解决内存泄漏

核心特性:位置容错

# AI 生成的代码位置常有偏差
# cclsp 自动尝试多个位置组合

positions_to_try = [
    (line, column),           # 原始位置
    (line - 1, column),       # 上一行
    (line + 1, column),       # 下一行
    (line, 0),                # 行首
    (line, len(line_content)) # 行尾
]

for pos in positions_to_try:
    result = lsp_server.definition(pos)
    if result:
        return result

支持的语言服务器

语言 服务器 安装命令
Python pylsp pip install python-lsp-server
TypeScript typescript-language-server npm i -g typescript-language-server
Go gopls go install golang.org/x/tools/gopls@latest
Rust rust-analyzer rustup component add rust-analyzer
C/C++ clangd apt install clangd
Ruby solargraph gem install solargraph
PHP intelephense npm i -g intelephense
Java jdtls Eclipse JDT Language Server

2.4 方式三Plugin Marketplace 插件

Claude Code 官方插件市场提供语言支持扩展。

添加插件市场

/plugin marketplace add boostvolt/claude-code-lsps

安装语言支持

# Python (Pyright)
/plugin install pyright@claude-code-lsps

# TypeScript/JavaScript
/plugin install vtsls@claude-code-lsps

# Go
/plugin install gopls@claude-code-lsps

# Rust
/plugin install rust-analyzer@claude-code-lsps

# Java
/plugin install jdtls@claude-code-lsps

# C/C++
/plugin install clangd@claude-code-lsps

# C#
/plugin install omnisharp@claude-code-lsps

# PHP
/plugin install intelephense@claude-code-lsps

# Kotlin
/plugin install kotlin-language-server@claude-code-lsps

# Ruby
/plugin install solargraph@claude-code-lsps

支持的 11 种语言

Python, TypeScript, Go, Rust, Java, C/C++, C#, PHP, Kotlin, Ruby, HTML/CSS

2.5 三种方式对比

特性 内置 LSP cclsp (MCP) Plugin Marketplace
安装复杂度 低 (环境变量) 中 (npx) 低 (/plugin)
功能完整性 基础 5 个操作 完整 + 重命名 完整
位置容错
重命名支持
自定义配置 完整 JSON 有限
多语言支持 需插件 任意 LSP 11 种
生产稳定性

2.6 codex-lens 集成策略

基于 Claude Code LSP 实现方式分析,推荐以下集成策略:

策略 A作为 MCP Server (推荐)

┌─────────────────────────────────────────────────────────────────┐
│                        Claude Code                              │
└───────────────────────────┬─────────────────────────────────────┘
                            │ MCP Protocol
                            ▼
┌─────────────────────────────────────────────────────────────────┐
│                    codex-lens MCP Server                        │
│  ┌─────────────────────────────────────────────────────────┐   │
│  │                    MCP Tools                             │   │
│  │  • find_definition  → GlobalSymbolIndex.search()         │   │
│  │  • find_references  → ChainSearchEngine.search_refs()    │   │
│  │  • get_context      → MCPProvider.build_context()        │   │
│  │  • hybrid_search    → HybridSearchEngine.search()        │   │
│  └─────────────────────────────────────────────────────────┘   │
│                            │                                    │
│  ┌─────────────────────────▼───────────────────────────────┐   │
│  │                 codex-lens Core                          │   │
│  │   GlobalSymbolIndex │ SQLiteStore │ WatcherManager       │   │
│  └─────────────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────────────┘

优势

  • 直接复用 codex-lens 索引
  • 无需启动额外 LSP 进程
  • 支持 MCP 上下文注入

实现文件: src/codexlens/mcp/server.py

"""codex-lens MCP Server for Claude Code integration."""

import json
import sys
from typing import Any, Dict

from codexlens.mcp.provider import MCPProvider
from codexlens.search.chain_search import ChainSearchEngine
from codexlens.storage.global_index import GlobalSymbolIndex


class CodexLensMCPServer:
    """MCP Server exposing codex-lens capabilities."""

    def __init__(self, workspace_path: str):
        self.global_index = GlobalSymbolIndex(workspace_path)
        self.search_engine = ChainSearchEngine(...)
        self.mcp_provider = MCPProvider(...)

    def handle_request(self, request: Dict[str, Any]) -> Dict[str, Any]:
        """Handle MCP tool call."""
        method = request.get("method")
        params = request.get("params", {})

        handlers = {
            "find_definition": self._find_definition,
            "find_references": self._find_references,
            "get_context": self._get_context,
            "hybrid_search": self._hybrid_search,
        }

        handler = handlers.get(method)
        if handler:
            return handler(params)
        return {"error": f"Unknown method: {method}"}

    def _find_definition(self, params: Dict) -> Dict:
        """Find symbol definition."""
        symbol_name = params.get("symbol")
        symbols = self.global_index.search(symbol_name, exact=True, limit=1)
        if symbols:
            s = symbols[0]
            return {
                "file": s.file_path,
                "line": s.range[0],
                "column": 0,
                "kind": s.kind,
            }
        return {"error": "Symbol not found"}

    def _find_references(self, params: Dict) -> Dict:
        """Find all references."""
        symbol_name = params.get("symbol")
        refs = self.search_engine.search_references(symbol_name)
        return {
            "references": [
                {"file": r.file_path, "line": r.line, "context": r.context}
                for r in refs
            ]
        }

    def _get_context(self, params: Dict) -> Dict:
        """Get MCP context for LLM."""
        symbol_name = params.get("symbol")
        context = self.mcp_provider.build_context(symbol_name)
        return context.to_dict() if context else {"error": "Context not found"}

    def _hybrid_search(self, params: Dict) -> Dict:
        """Execute hybrid search."""
        query = params.get("query")
        # ... implementation

策略 B作为独立 LSP Server

通过 cclsp 配置接入 codex-lens LSP Server。

cclsp 配置 (.claude/cclsp.json)

{
  "servers": [
    {
      "extensions": ["py", "ts", "go", "rs", "java"],
      "command": ["codexlens-lsp", "--stdio"],
      "rootDir": ".",
      "restartInterval": 0
    }
  ]
}

优势

  • 兼容标准 LSP 协议
  • 可被任意 LSP 客户端使用
  • cclsp 提供位置容错

策略 C混合模式 (最佳实践)

┌───────────────────────────────────────────────────────────────────┐
│                         Claude Code                               │
│  ┌──────────────────┐            ┌──────────────────────────┐    │
│  │  内置 LSP 工具   │            │     MCP Client           │    │
│  │  (基础导航)      │            │   (上下文注入)            │    │
│  └────────┬─────────┘            └────────────┬─────────────┘    │
└───────────┼───────────────────────────────────┼──────────────────┘
            │                                   │
            │ LSP Protocol                      │ MCP Protocol
            │                                   │
┌───────────▼───────────────────────────────────▼──────────────────┐
│                    codex-lens Unified Server                      │
│  ┌─────────────────────────┐  ┌─────────────────────────────┐   │
│  │     LSP Handlers        │  │      MCP Handlers           │   │
│  │  • definition           │  │  • get_context              │   │
│  │  • references           │  │  • enrich_prompt            │   │
│  │  • hover                │  │  • hybrid_search            │   │
│  │  • completion           │  │  • semantic_query           │   │
│  └────────────┬────────────┘  └──────────────┬──────────────┘   │
│               │                               │                  │
│               └───────────────┬───────────────┘                  │
│                               ▼                                  │
│  ┌─────────────────────────────────────────────────────────────┐ │
│  │                    codex-lens Core                          │ │
│  │  GlobalSymbolIndex │ HybridSearch │ VectorStore │ Watcher  │ │
│  └─────────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────────┘

优势

  • LSP 提供标准代码导航
  • MCP 提供 LLM 上下文增强
  • 统一索引,避免重复计算

2.7 参考资源

资源 链接
Claude Code LSP 设置指南 https://www.aifreeapi.com/en/posts/claude-code-lsp
cclsp GitHub https://github.com/ktnyt/cclsp
Claude Code Plugins https://code.claude.com/docs/en/plugins-reference
claude-code-lsps 市场 https://github.com/Piebald-AI/claude-code-lsps
LSP 规范 https://microsoft.github.io/language-server-protocol/
MCP 规范 https://modelcontextprotocol.io/

3. Architecture Overview

3.1 Target Architecture

┌─────────────────────────────────────────────────────────────────────────────┐
│                              Client Layer                                   │
├─────────────────────────────────────────────────────────────────────────────┤
│  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐  ┌─────────────────────┐ │
│  │   VS Code   │  │   Neovim    │  │   Sublime   │  │    Claude Code      │ │
│  │  (LSP Client)│  │ (LSP Client)│  │ (LSP Client)│  │ (Hook + MCP Client) │ │
│  └──────┬──────┘  └──────┬──────┘  └──────┬──────┘  └──────────┬──────────┘ │
│         │                │                │                     │           │
└─────────┼────────────────┼────────────────┼─────────────────────┼───────────┘
          │                │                │                     │
          └────────────────┴────────────────┴──────────┬──────────┘
                                                       │
                                              (JSON-RPC / stdio)
                                                       │
┌──────────────────────────────────────────────────────┴──────────────────────┐
│                         codex-lens LSP Server                               │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│  ┌─────────────────────────────────────────────────────────────────────┐   │
│  │                        LSP Layer (NEW)                              │   │
│  │  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐              │   │
│  │  │   Handlers   │  │   Providers  │  │   Protocol   │              │   │
│  │  │  definition  │  │    hover     │  │   messages   │              │   │
│  │  │  references  │  │  completion  │  │   lifecycle  │              │   │
│  │  │   symbols    │  │              │  │              │              │   │
│  │  └──────┬───────┘  └──────┬───────┘  └──────┬───────┘              │   │
│  └─────────┼─────────────────┼─────────────────┼───────────────────────┘   │
│            │                 │                 │                           │
│  ┌─────────┴─────────────────┴─────────────────┴───────────────────────┐   │
│  │                        MCP Layer (NEW)                              │   │
│  │  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐              │   │
│  │  │    Schema    │  │   Provider   │  │    Hooks     │              │   │
│  │  │  MCPContext  │  │ buildContext │  │  pre-tool    │              │   │
│  │  │  SymbolInfo  │  │ enrichPrompt │  │  post-tool   │              │   │
│  │  └──────────────┘  └──────────────┘  └──────────────┘              │   │
│  └─────────────────────────────────────────────────────────────────────┘   │
│                                    │                                       │
│  ┌─────────────────────────────────┴───────────────────────────────────┐   │
│  │                     Existing codex-lens Core                        │   │
│  │                                                                     │   │
│  │  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐  ┌────────────┐ │   │
│  │  │   Search    │  │   Storage   │  │   Watcher   │  │   Parser   │ │   │
│  │  │ ChainSearch │  │ GlobalIndex │  │   Manager   │  │ TreeSitter │ │   │
│  │  │ HybridSearch│  │ SQLiteStore │  │ Incremental │  │  Symbols   │ │   │
│  │  └─────────────┘  └─────────────┘  └─────────────┘  └────────────┘ │   │
│  └─────────────────────────────────────────────────────────────────────┘   │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘

3.2 Data Flow

                           LSP Request Flow
                           ================

[Client] ─── textDocument/definition ───> [LSP Server]
                                              │
                                              v
                                    ┌─────────────────┐
                                    │  Parse Request  │
                                    │  Extract symbol │
                                    └────────┬────────┘
                                              │
                                              v
                                    ┌─────────────────┐
                                    │ GlobalSymbolIdx │
                                    │    .search()    │
                                    └────────┬────────┘
                                              │
                                              v
                                    ┌─────────────────┐
                                    │  Format Result  │
                                    │   as Location   │
                                    └────────┬────────┘
                                              │
[Client] <─── Location Response ────────────────┘


                           MCP Context Flow
                           ================

[Claude Code] ─── pre-tool hook ───> [MCP Provider]
                                          │
                    ┌─────────────────────┴─────────────────────┐
                    v                     v                     v
            ┌─────────────┐       ┌─────────────┐       ┌─────────────┐
            │  Definition │       │  References │       │   Related   │
            │   Lookup    │       │   Lookup    │       │   Symbols   │
            └──────┬──────┘       └──────┬──────┘       └──────┬──────┘
                    │                     │                     │
                    └─────────────────────┴─────────────────────┘
                                          │
                                          v
                                  ┌───────────────┐
                                  │  MCPContext   │
                                  │    Object     │
                                  └───────┬───────┘
                                          │
[Claude Code] <─── JSON Context ──────────┘
                                          │
                                          v
                              ┌───────────────────────┐
                              │ Inject into LLM Prompt│
                              └───────────────────────┘

3.3 Module Dependencies

                    ┌─────────────────────┐
                    │   lsp/server.py     │
                    │   (Entry Point)     │
                    └──────────┬──────────┘
                               │
           ┌───────────────────┼───────────────────┐
           │                   │                   │
           v                   v                   v
    ┌─────────────┐    ┌─────────────┐    ┌─────────────┐
    │lsp/handlers │    │lsp/providers│    │ mcp/provider│
    │  .py        │    │  .py        │    │  .py        │
    └──────┬──────┘    └──────┬──────┘    └──────┬──────┘
           │                   │                   │
           └───────────────────┼───────────────────┘
                               │
                               v
                    ┌─────────────────────┐
                    │ search/chain_search │
                    │        .py          │
                    └──────────┬──────────┘
                               │
           ┌───────────────────┼───────────────────┐
           │                   │                   │
           v                   v                   v
    ┌─────────────┐    ┌─────────────┐    ┌─────────────┐
    │storage/     │    │storage/     │    │watcher/     │
    │global_index │    │sqlite_store │    │manager.py   │
    └─────────────┘    └─────────────┘    └─────────────┘

4. Phase 1: LSP Server Foundation

4.1 Overview

Attribute Value
Priority HIGH
Complexity Medium
Dependencies pygls library
Deliverables Working LSP server with 3 core handlers

4.2 Task Breakdown

Task 1.1: Project Setup

File: pyproject.toml (MODIFY)

[project.optional-dependencies]
lsp = [
    "pygls>=1.3.0",
]

[project.scripts]
codexlens-lsp = "codexlens.lsp:main"

Acceptance Criteria:

  • pip install -e ".[lsp]" succeeds
  • codexlens-lsp --help shows usage

Task 1.2: LSP Server Core

File: src/codexlens/lsp/__init__.py (NEW)

"""codex-lens Language Server Protocol implementation."""

from codexlens.lsp.server import CodexLensLanguageServer, main

__all__ = ["CodexLensLanguageServer", "main"]

File: src/codexlens/lsp/server.py (NEW)

"""Main LSP server implementation using pygls."""

import logging
from pathlib import Path
from typing import Optional

from lsprotocol import types as lsp
from pygls.server import LanguageServer

from codexlens.search.chain_search import ChainSearchEngine
from codexlens.storage.global_index import GlobalSymbolIndex
from codexlens.storage.registry import RegistryStore
from codexlens.storage.path_mapper import PathMapper
from codexlens.watcher.manager import WatcherManager

logger = logging.getLogger(__name__)


class CodexLensLanguageServer(LanguageServer):
    """Language Server powered by codex-lens indexing."""

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.workspace_path: Optional[Path] = None
        self.registry: Optional[RegistryStore] = None
        self.search_engine: Optional[ChainSearchEngine] = None
        self.global_index: Optional[GlobalSymbolIndex] = None
        self.watcher: Optional[WatcherManager] = None

    def initialize_codexlens(self, workspace_path: Path) -> None:
        """Initialize codex-lens components for the workspace."""
        self.workspace_path = workspace_path

        # Initialize registry and search engine
        self.registry = RegistryStore()
        self.registry.initialize()

        mapper = PathMapper()
        self.search_engine = ChainSearchEngine(self.registry, mapper)

        # Initialize global symbol index
        self.global_index = GlobalSymbolIndex(workspace_path)

        # Start file watcher for incremental updates
        self.watcher = WatcherManager(
            root_path=workspace_path,
            on_indexed=self._on_file_indexed
        )
        self.watcher.start()

        logger.info(f"Initialized codex-lens for workspace: {workspace_path}")

    def _on_file_indexed(self, file_path: Path) -> None:
        """Callback when a file is indexed."""
        logger.debug(f"File indexed: {file_path}")

    def shutdown_codexlens(self) -> None:
        """Cleanup codex-lens components."""
        if self.watcher:
            self.watcher.stop()
            self.watcher = None
        logger.info("codex-lens shutdown complete")


# Create server instance
server = CodexLensLanguageServer(
    name="codex-lens",
    version="0.1.0"
)


@server.feature(lsp.INITIALIZE)
def on_initialize(params: lsp.InitializeParams) -> lsp.InitializeResult:
    """Handle LSP initialize request."""
    if params.root_uri:
        workspace_path = Path(params.root_uri.replace("file://", ""))
        server.initialize_codexlens(workspace_path)

    return lsp.InitializeResult(
        capabilities=lsp.ServerCapabilities(
            text_document_sync=lsp.TextDocumentSyncOptions(
                open_close=True,
                change=lsp.TextDocumentSyncKind.Incremental,
                save=lsp.SaveOptions(include_text=False),
            ),
            definition_provider=True,
            references_provider=True,
            completion_provider=lsp.CompletionOptions(
                trigger_characters=[".", "_"],
            ),
            hover_provider=True,
            workspace_symbol_provider=True,
        ),
        server_info=lsp.ServerInfo(
            name="codex-lens",
            version="0.1.0",
        ),
    )


@server.feature(lsp.SHUTDOWN)
def on_shutdown(params: None) -> None:
    """Handle LSP shutdown request."""
    server.shutdown_codexlens()


def main():
    """Entry point for the LSP server."""
    import argparse

    parser = argparse.ArgumentParser(description="codex-lens Language Server")
    parser.add_argument("--stdio", action="store_true", help="Use stdio transport")
    parser.add_argument("--tcp", action="store_true", help="Use TCP transport")
    parser.add_argument("--host", default="127.0.0.1", help="TCP host")
    parser.add_argument("--port", type=int, default=2087, help="TCP port")

    args = parser.parse_args()

    if args.tcp:
        server.start_tcp(args.host, args.port)
    else:
        server.start_io()


if __name__ == "__main__":
    main()

Acceptance Criteria:

  • Server starts without errors
  • Handles initialize/shutdown lifecycle
  • WatcherManager starts on workspace open

Task 1.3: Definition Handler

File: src/codexlens/lsp/handlers.py (NEW)

"""LSP request handlers."""

import logging
from pathlib import Path
from typing import List, Optional, Union

from lsprotocol import types as lsp

from codexlens.lsp.server import server
from codexlens.entities import Symbol

logger = logging.getLogger(__name__)


def symbol_to_location(symbol: Symbol) -> lsp.Location:
    """Convert codex-lens Symbol to LSP Location."""
    return lsp.Location(
        uri=f"file://{symbol.file_path}",
        range=lsp.Range(
            start=lsp.Position(
                line=symbol.range[0] - 1,  # LSP is 0-indexed
                character=0,
            ),
            end=lsp.Position(
                line=symbol.range[1] - 1,
                character=0,
            ),
        ),
    )


@server.feature(lsp.TEXT_DOCUMENT_DEFINITION)
def on_definition(
    params: lsp.DefinitionParams,
) -> Optional[Union[lsp.Location, List[lsp.Location]]]:
    """Handle textDocument/definition request."""
    if not server.global_index:
        return None

    # Get the word at cursor position
    document = server.workspace.get_text_document(params.text_document.uri)
    word = _get_word_at_position(document, params.position)

    if not word:
        return None

    logger.debug(f"Definition lookup for: {word}")

    # Search in global symbol index
    symbols = server.global_index.search(word, exact=True, limit=10)

    if not symbols:
        return None

    if len(symbols) == 1:
        return symbol_to_location(symbols[0])

    return [symbol_to_location(s) for s in symbols]


def _get_word_at_position(document, position: lsp.Position) -> Optional[str]:
    """Extract the word at the given position."""
    try:
        lines = document.source.split("\n")
        if position.line >= len(lines):
            return None

        line = lines[position.line]

        # Find word boundaries
        start = position.character
        end = position.character

        # Expand left
        while start > 0 and _is_identifier_char(line[start - 1]):
            start -= 1

        # Expand right
        while end < len(line) and _is_identifier_char(line[end]):
            end += 1

        word = line[start:end]
        return word if word else None
    except Exception as e:
        logger.error(f"Error extracting word: {e}")
        return None


def _is_identifier_char(char: str) -> bool:
    """Check if character is valid in an identifier."""
    return char.isalnum() or char == "_"

Acceptance Criteria:

  • Returns Location for known symbols
  • Returns None for unknown symbols
  • Handles multiple definitions (overloads)

Task 1.4: Completion Handler

File: src/codexlens/lsp/handlers.py (APPEND)

@server.feature(lsp.TEXT_DOCUMENT_COMPLETION)
def on_completion(
    params: lsp.CompletionParams,
) -> Optional[lsp.CompletionList]:
    """Handle textDocument/completion request."""
    if not server.global_index:
        return None

    # Get partial word at cursor
    document = server.workspace.get_text_document(params.text_document.uri)
    prefix = _get_prefix_at_position(document, params.position)

    if not prefix or len(prefix) < 2:
        return None

    logger.debug(f"Completion lookup for prefix: {prefix}")

    # Search with prefix mode
    symbols = server.global_index.search(prefix, prefix_mode=True, limit=50)

    if not symbols:
        return None

    items = []
    for symbol in symbols:
        kind = _symbol_kind_to_completion_kind(symbol.kind)
        items.append(
            lsp.CompletionItem(
                label=symbol.name,
                kind=kind,
                detail=f"{symbol.kind} in {Path(symbol.file_path).name}",
                documentation=lsp.MarkupContent(
                    kind=lsp.MarkupKind.Markdown,
                    value=f"Defined at line {symbol.range[0]}",
                ),
            )
        )

    return lsp.CompletionList(is_incomplete=len(items) >= 50, items=items)


def _get_prefix_at_position(document, position: lsp.Position) -> Optional[str]:
    """Extract the incomplete word prefix at position."""
    try:
        lines = document.source.split("\n")
        if position.line >= len(lines):
            return None

        line = lines[position.line]

        # Find prefix start
        start = position.character
        while start > 0 and _is_identifier_char(line[start - 1]):
            start -= 1

        return line[start:position.character] if start < position.character else None
    except Exception:
        return None


def _symbol_kind_to_completion_kind(kind: str) -> lsp.CompletionItemKind:
    """Map symbol kind to LSP completion kind."""
    mapping = {
        "function": lsp.CompletionItemKind.Function,
        "method": lsp.CompletionItemKind.Method,
        "class": lsp.CompletionItemKind.Class,
        "variable": lsp.CompletionItemKind.Variable,
        "constant": lsp.CompletionItemKind.Constant,
        "module": lsp.CompletionItemKind.Module,
        "property": lsp.CompletionItemKind.Property,
        "interface": lsp.CompletionItemKind.Interface,
        "enum": lsp.CompletionItemKind.Enum,
    }
    return mapping.get(kind.lower(), lsp.CompletionItemKind.Text)

Acceptance Criteria:

  • Returns completion items for valid prefixes
  • Respects minimum prefix length (2 chars)
  • Maps symbol kinds correctly

Task 1.5: Workspace Symbol Handler

File: src/codexlens/lsp/handlers.py (APPEND)

@server.feature(lsp.WORKSPACE_SYMBOL)
def on_workspace_symbol(
    params: lsp.WorkspaceSymbolParams,
) -> Optional[List[lsp.SymbolInformation]]:
    """Handle workspace/symbol request."""
    if not server.search_engine or not server.workspace_path:
        return None

    query = params.query
    if not query or len(query) < 2:
        return None

    logger.debug(f"Workspace symbol search: {query}")

    # Use chain search engine's symbol search
    result = server.search_engine.search_symbols(
        query=query,
        source_path=server.workspace_path,
        limit=100,
    )

    if not result:
        return None

    items = []
    for symbol in result:
        kind = _symbol_kind_to_symbol_kind(symbol.kind)
        items.append(
            lsp.SymbolInformation(
                name=symbol.name,
                kind=kind,
                location=symbol_to_location(symbol),
                container_name=Path(symbol.file_path).parent.name,
            )
        )

    return items


def _symbol_kind_to_symbol_kind(kind: str) -> lsp.SymbolKind:
    """Map symbol kind string to LSP SymbolKind."""
    mapping = {
        "function": lsp.SymbolKind.Function,
        "method": lsp.SymbolKind.Method,
        "class": lsp.SymbolKind.Class,
        "variable": lsp.SymbolKind.Variable,
        "constant": lsp.SymbolKind.Constant,
        "module": lsp.SymbolKind.Module,
        "property": lsp.SymbolKind.Property,
        "interface": lsp.SymbolKind.Interface,
        "enum": lsp.SymbolKind.Enum,
        "struct": lsp.SymbolKind.Struct,
        "namespace": lsp.SymbolKind.Namespace,
    }
    return mapping.get(kind.lower(), lsp.SymbolKind.Variable)

Acceptance Criteria:

  • Returns symbols matching query
  • Respects result limit
  • Includes container information

Task 1.6: File Watcher Integration

File: src/codexlens/lsp/handlers.py (APPEND)

@server.feature(lsp.TEXT_DOCUMENT_DID_SAVE)
def on_did_save(params: lsp.DidSaveTextDocumentParams) -> None:
    """Handle textDocument/didSave notification."""
    if not server.watcher:
        return

    file_path = Path(params.text_document.uri.replace("file://", ""))
    logger.debug(f"File saved: {file_path}")

    # Trigger incremental indexing
    server.watcher.trigger_index(file_path)


@server.feature(lsp.TEXT_DOCUMENT_DID_OPEN)
def on_did_open(params: lsp.DidOpenTextDocumentParams) -> None:
    """Handle textDocument/didOpen notification."""
    logger.debug(f"File opened: {params.text_document.uri}")


@server.feature(lsp.TEXT_DOCUMENT_DID_CLOSE)
def on_did_close(params: lsp.DidCloseTextDocumentParams) -> None:
    """Handle textDocument/didClose notification."""
    logger.debug(f"File closed: {params.text_document.uri}")

Acceptance Criteria:

  • didSave triggers incremental index
  • No blocking on save
  • Proper logging

4.3 Phase 1 Test Plan

File: tests/lsp/test_server.py (NEW)

"""Tests for LSP server."""

import pytest
from pathlib import Path
from unittest.mock import Mock, patch

from lsprotocol import types as lsp

from codexlens.lsp.server import CodexLensLanguageServer, on_initialize


class TestServerInitialization:
    """Test server lifecycle."""

    def test_initialize_creates_components(self, tmp_path):
        """Server creates all components on initialize."""
        server = CodexLensLanguageServer("test", "0.1.0")

        params = lsp.InitializeParams(
            root_uri=f"file://{tmp_path}",
            capabilities=lsp.ClientCapabilities(),
        )

        result = on_initialize(params)

        assert result.capabilities.definition_provider
        assert result.capabilities.completion_provider
        assert result.capabilities.workspace_symbol_provider


class TestDefinitionHandler:
    """Test textDocument/definition handler."""

    def test_definition_returns_location(self):
        """Definition returns valid Location."""
        # Setup mock global index
        mock_symbol = Mock()
        mock_symbol.file_path = "/test/file.py"
        mock_symbol.range = (10, 15)

        with patch.object(server, 'global_index') as mock_index:
            mock_index.search.return_value = [mock_symbol]

            # Call handler
            result = on_definition(Mock(
                text_document=Mock(uri="file:///test/file.py"),
                position=lsp.Position(line=5, character=10),
            ))

            assert isinstance(result, lsp.Location)
            assert result.uri == "file:///test/file.py"


class TestCompletionHandler:
    """Test textDocument/completion handler."""

    def test_completion_returns_items(self):
        """Completion returns CompletionList."""
        # Test implementation
        pass

Acceptance Criteria:

  • All unit tests pass
  • Coverage > 80% for LSP module
  • Integration test with real workspace

5. Phase 2: Find References

5.1 Overview

Attribute Value
Priority MEDIUM
Complexity High
Dependencies Phase 1 complete
Deliverables search_references() method + LSP handler

5.2 Task Breakdown

Task 2.1: Add search_references to ChainSearchEngine

File: src/codexlens/search/chain_search.py (MODIFY)

# Add to ChainSearchEngine class

from dataclasses import dataclass
from typing import List
from concurrent.futures import ThreadPoolExecutor, as_completed


@dataclass
class ReferenceResult:
    """Result from reference search."""
    file_path: str
    line: int
    column: int
    context: str  # Surrounding code snippet
    relationship_type: str  # "call", "import", "inheritance", etc.


def search_references(
    self,
    symbol_name: str,
    source_path: Optional[Path] = None,
    depth: int = -1,
    limit: int = 100,
) -> List[ReferenceResult]:
    """Find all references to a symbol across the project.

    Args:
        symbol_name: Fully qualified or simple name of the symbol
        source_path: Starting path for search (default: workspace root)
        depth: Search depth (-1 = unlimited)
        limit: Maximum results to return

    Returns:
        List of ReferenceResult objects sorted by file path and line
    """
    source = source_path or self._workspace_path

    # Collect all index paths
    index_paths = self._collect_index_paths(source, depth)

    if not index_paths:
        logger.warning(f"No indexes found for reference search: {source}")
        return []

    # Parallel query across all indexes
    all_results: List[ReferenceResult] = []

    with ThreadPoolExecutor(max_workers=self._options.max_workers) as executor:
        futures = {
            executor.submit(
                self._search_references_single,
                idx_path,
                symbol_name,
            ): idx_path
            for idx_path in index_paths
        }

        for future in as_completed(futures):
            try:
                results = future.result(timeout=10)
                all_results.extend(results)
            except Exception as e:
                logger.error(f"Reference search failed: {e}")

    # Sort and limit
    all_results.sort(key=lambda r: (r.file_path, r.line))
    return all_results[:limit]


def _search_references_single(
    self,
    index_path: Path,
    symbol_name: str,
) -> List[ReferenceResult]:
    """Search for references in a single index."""
    results = []

    try:
        store = DirIndexStore(index_path.parent)

        # Query code_relationships table
        query = """
            SELECT
                cr.source_file,
                cr.source_line,
                cr.source_column,
                cr.relationship_type,
                f.content
            FROM code_relationships cr
            JOIN files f ON f.full_path = cr.source_file
            WHERE cr.target_qualified_name LIKE ?
               OR cr.target_name = ?
            ORDER BY cr.source_file, cr.source_line
        """

        rows = store.execute_query(
            query,
            (f"%{symbol_name}", symbol_name),
        )

        for row in rows:
            # Extract context (3 lines around reference)
            content_lines = row["content"].split("\n")
            line_idx = row["source_line"] - 1
            start = max(0, line_idx - 1)
            end = min(len(content_lines), line_idx + 2)
            context = "\n".join(content_lines[start:end])

            results.append(ReferenceResult(
                file_path=row["source_file"],
                line=row["source_line"],
                column=row["source_column"] or 0,
                context=context,
                relationship_type=row["relationship_type"],
            ))
    except Exception as e:
        logger.error(f"Failed to search references in {index_path}: {e}")

    return results

Acceptance Criteria:

  • Searches all index files in parallel
  • Returns properly formatted ReferenceResult
  • Handles missing indexes gracefully

Task 2.2: LSP References Handler

File: src/codexlens/lsp/handlers.py (APPEND)

@server.feature(lsp.TEXT_DOCUMENT_REFERENCES)
def on_references(
    params: lsp.ReferenceParams,
) -> Optional[List[lsp.Location]]:
    """Handle textDocument/references request."""
    if not server.search_engine or not server.workspace_path:
        return None

    # Get the word at cursor
    document = server.workspace.get_text_document(params.text_document.uri)
    word = _get_word_at_position(document, params.position)

    if not word:
        return None

    logger.debug(f"References lookup for: {word}")

    # Search for references
    references = server.search_engine.search_references(
        symbol_name=word,
        source_path=server.workspace_path,
        limit=200,
    )

    if not references:
        return None

    # Convert to LSP Locations
    locations = []
    for ref in references:
        locations.append(
            lsp.Location(
                uri=f"file://{ref.file_path}",
                range=lsp.Range(
                    start=lsp.Position(line=ref.line - 1, character=ref.column),
                    end=lsp.Position(line=ref.line - 1, character=ref.column + len(word)),
                ),
            )
        )

    return locations

Acceptance Criteria:

  • Returns all references across project
  • Includes definition if params.context.include_declaration
  • Performance < 200ms for typical project

5.3 Phase 2 Test Plan

class TestReferencesSearch:
    """Test reference search functionality."""

    def test_finds_function_calls(self, indexed_project):
        """Finds all calls to a function."""
        results = search_engine.search_references("my_function")
        assert len(results) > 0
        assert all(r.relationship_type == "call" for r in results)

    def test_finds_imports(self, indexed_project):
        """Finds all imports of a module."""
        results = search_engine.search_references("my_module")
        assert any(r.relationship_type == "import" for r in results)

    def test_parallel_search_performance(self, large_project):
        """Parallel search completes within time limit."""
        import time
        start = time.time()
        results = search_engine.search_references("common_symbol")
        elapsed = time.time() - start
        assert elapsed < 0.2  # 200ms

6. Phase 3: Hover Information

6.1 Overview

Attribute Value
Priority MEDIUM
Complexity Low
Dependencies Phase 1 complete
Deliverables Hover provider + LSP handler

6.2 Task Breakdown

Task 3.1: Hover Provider

File: src/codexlens/lsp/providers.py (NEW)

"""LSP feature providers."""

import logging
from dataclasses import dataclass
from pathlib import Path
from typing import Optional

from codexlens.entities import Symbol
from codexlens.storage.sqlite_store import SQLiteStore

logger = logging.getLogger(__name__)


@dataclass
class HoverInfo:
    """Hover information for a symbol."""
    name: str
    kind: str
    signature: str
    documentation: Optional[str]
    file_path: str
    line_range: tuple


class HoverProvider:
    """Provides hover information for symbols."""

    def __init__(self, global_index, registry):
        self.global_index = global_index
        self.registry = registry

    def get_hover_info(self, symbol_name: str) -> Optional[HoverInfo]:
        """Get hover information for a symbol.

        Args:
            symbol_name: Name of the symbol to look up

        Returns:
            HoverInfo or None if symbol not found
        """
        # Look up symbol in global index
        symbols = self.global_index.search(symbol_name, exact=True, limit=1)

        if not symbols:
            return None

        symbol = symbols[0]

        # Extract signature from source
        signature = self._extract_signature(symbol)

        return HoverInfo(
            name=symbol.name,
            kind=symbol.kind,
            signature=signature,
            documentation=symbol.docstring,
            file_path=symbol.file_path,
            line_range=symbol.range,
        )

    def _extract_signature(self, symbol: Symbol) -> str:
        """Extract function/class signature from source."""
        try:
            # Find the index for this file
            index_path = self.registry.find_index_path(
                Path(symbol.file_path).parent
            )

            if not index_path:
                return f"{symbol.kind} {symbol.name}"

            store = SQLiteStore(index_path.parent)

            # Get file content
            rows = store.execute_query(
                "SELECT content FROM files WHERE full_path = ?",
                (symbol.file_path,),
            )

            if not rows:
                return f"{symbol.kind} {symbol.name}"

            content = rows[0]["content"]
            lines = content.split("\n")

            # Extract signature lines
            start_line = symbol.range[0] - 1
            signature_lines = []

            # Get first line (def/class declaration)
            if start_line < len(lines):
                first_line = lines[start_line]
                signature_lines.append(first_line)

                # Continue if line ends with backslash or doesn't have closing paren
                i = start_line + 1
                while i < len(lines) and i < start_line + 5:
                    if "):" in signature_lines[-1] or ":" in signature_lines[-1]:
                        break
                    signature_lines.append(lines[i])
                    i += 1

            return "\n".join(signature_lines)
        except Exception as e:
            logger.error(f"Failed to extract signature: {e}")
            return f"{symbol.kind} {symbol.name}"

    def format_hover_markdown(self, info: HoverInfo) -> str:
        """Format hover info as Markdown."""
        parts = []

        # Code block with signature
        parts.append(f"```python\n{info.signature}\n```")

        # Documentation if available
        if info.documentation:
            parts.append(f"\n---\n\n{info.documentation}")

        # Location info
        parts.append(
            f"\n---\n\n*{info.kind}* defined in "
            f"`{Path(info.file_path).name}` "
            f"(line {info.line_range[0]})"
        )

        return "\n".join(parts)

Task 3.2: LSP Hover Handler

File: src/codexlens/lsp/handlers.py (APPEND)

from codexlens.lsp.providers import HoverProvider


@server.feature(lsp.TEXT_DOCUMENT_HOVER)
def on_hover(params: lsp.HoverParams) -> Optional[lsp.Hover]:
    """Handle textDocument/hover request."""
    if not server.global_index or not server.registry:
        return None

    # Get word at cursor
    document = server.workspace.get_text_document(params.text_document.uri)
    word = _get_word_at_position(document, params.position)

    if not word:
        return None

    logger.debug(f"Hover lookup for: {word}")

    # Get hover info
    provider = HoverProvider(server.global_index, server.registry)
    info = provider.get_hover_info(word)

    if not info:
        return None

    # Format as markdown
    content = provider.format_hover_markdown(info)

    return lsp.Hover(
        contents=lsp.MarkupContent(
            kind=lsp.MarkupKind.Markdown,
            value=content,
        ),
    )

Acceptance Criteria:

  • Shows function signature
  • Shows documentation if available
  • Shows file location

7. Phase 4: MCP Bridge

7.1 Overview

Attribute Value
Priority HIGH VALUE
Complexity Medium
Dependencies Phase 1-2 complete
Deliverables MCP schema + provider + hook interfaces

7.2 Task Breakdown

Task 4.1: MCP Schema Definition

File: src/codexlens/mcp/__init__.py (NEW)

"""Model Context Protocol implementation for Claude Code integration."""

from codexlens.mcp.schema import (
    MCPContext,
    SymbolInfo,
    ReferenceInfo,
    RelatedSymbol,
)
from codexlens.mcp.provider import MCPProvider

__all__ = [
    "MCPContext",
    "SymbolInfo",
    "ReferenceInfo",
    "RelatedSymbol",
    "MCPProvider",
]

File: src/codexlens/mcp/schema.py (NEW)

"""MCP data models."""

from dataclasses import dataclass, field, asdict
from typing import List, Optional
import json


@dataclass
class SymbolInfo:
    """Information about a code symbol."""
    name: str
    kind: str
    file_path: str
    line_start: int
    line_end: int
    signature: Optional[str] = None
    documentation: Optional[str] = None

    def to_dict(self) -> dict:
        return asdict(self)


@dataclass
class ReferenceInfo:
    """Information about a symbol reference."""
    file_path: str
    line: int
    column: int
    context: str
    relationship_type: str

    def to_dict(self) -> dict:
        return asdict(self)


@dataclass
class RelatedSymbol:
    """Related symbol (import, call target, etc.)."""
    name: str
    kind: str
    relationship: str  # "imports", "calls", "inherits", "uses"
    file_path: Optional[str] = None

    def to_dict(self) -> dict:
        return asdict(self)


@dataclass
class MCPContext:
    """Model Context Protocol context object.

    This is the structured context that gets injected into
    LLM prompts to provide code understanding.
    """
    version: str = "1.0"
    context_type: str = "code_context"
    symbol: Optional[SymbolInfo] = None
    definition: Optional[str] = None
    references: List[ReferenceInfo] = field(default_factory=list)
    related_symbols: List[RelatedSymbol] = field(default_factory=list)
    metadata: dict = field(default_factory=dict)

    def to_dict(self) -> dict:
        """Convert to dictionary for JSON serialization."""
        result = {
            "version": self.version,
            "context_type": self.context_type,
            "metadata": self.metadata,
        }

        if self.symbol:
            result["symbol"] = self.symbol.to_dict()

        if self.definition:
            result["definition"] = self.definition

        if self.references:
            result["references"] = [r.to_dict() for r in self.references]

        if self.related_symbols:
            result["related_symbols"] = [s.to_dict() for s in self.related_symbols]

        return result

    def to_json(self, indent: int = 2) -> str:
        """Serialize to JSON string."""
        return json.dumps(self.to_dict(), indent=indent)

    def to_prompt_injection(self) -> str:
        """Format for injection into LLM prompt."""
        parts = ["<code_context>"]

        if self.symbol:
            parts.append(f"## Symbol: {self.symbol.name}")
            parts.append(f"Type: {self.symbol.kind}")
            parts.append(f"Location: {self.symbol.file_path}:{self.symbol.line_start}")

        if self.definition:
            parts.append("\n## Definition")
            parts.append(f"```\n{self.definition}\n```")

        if self.references:
            parts.append(f"\n## References ({len(self.references)} found)")
            for i, ref in enumerate(self.references[:5]):  # Limit to 5
                parts.append(f"- {ref.file_path}:{ref.line} ({ref.relationship_type})")
                parts.append(f"  ```\n  {ref.context}\n  ```")

        if self.related_symbols:
            parts.append("\n## Related Symbols")
            for sym in self.related_symbols[:10]:  # Limit to 10
                parts.append(f"- {sym.name} ({sym.relationship})")

        parts.append("</code_context>")

        return "\n".join(parts)

Task 4.2: MCP Provider

File: src/codexlens/mcp/provider.py (NEW)

"""MCP context provider."""

import logging
from pathlib import Path
from typing import Optional, List

from codexlens.mcp.schema import (
    MCPContext,
    SymbolInfo,
    ReferenceInfo,
    RelatedSymbol,
)
from codexlens.search.chain_search import ChainSearchEngine
from codexlens.storage.global_index import GlobalSymbolIndex
from codexlens.storage.sqlite_store import SQLiteStore
from codexlens.storage.registry import RegistryStore

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,
    ):
        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, exact=True, limit=1)

        if not symbols:
            logger.warning(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_path,
            line_start=symbol.range[0],
            line_end=symbol.range[1],
            signature=getattr(symbol, 'signature', None),
            documentation=getattr(symbol, 'docstring', None),
        )

        # 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",
                "indexed_at": symbol.indexed_at if hasattr(symbol, 'indexed_at') else None,
            },
        )

    def _extract_definition(self, symbol) -> Optional[str]:
        """Extract source code for symbol definition."""
        try:
            index_path = self.registry.find_index_path(
                Path(symbol.file_path).parent
            )

            if not index_path:
                return None

            store = SQLiteStore(index_path.parent)
            rows = store.execute_query(
                "SELECT content FROM files WHERE full_path = ?",
                (symbol.file_path,),
            )

            if not rows:
                return None

            content = rows[0]["content"]
            lines = content.split("\n")

            # Extract symbol lines
            start = symbol.range[0] - 1
            end = symbol.range[1]

            return "\n".join(lines[start:end])
        except Exception as e:
            logger.error(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:
            index_path = self.registry.find_index_path(
                Path(symbol.file_path).parent
            )

            if not index_path:
                return related

            store = SQLiteStore(index_path.parent)

            # Query relationships where this symbol is the source
            rows = store.execute_query(
                """
                SELECT target_name, target_qualified_name, relationship_type
                FROM code_relationships
                WHERE source_qualified_name LIKE ?
                LIMIT 20
                """,
                (f"%{symbol.name}%",),
            )

            for row in rows:
                related.append(RelatedSymbol(
                    name=row["target_name"],
                    kind="unknown",  # Would need another lookup
                    relationship=row["relationship_type"],
                ))
        except Exception as e:
            logger.error(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."""
        # Get all symbols in file
        symbols = self.global_index.search_by_file(str(file_path))

        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),
            },
        )

Task 4.3: Hook Interfaces

File: src/codexlens/mcp/hooks.py (NEW)

"""Hook interfaces for Claude Code integration."""

import logging
from pathlib import Path
from typing import Any, Dict, Optional, Callable

from codexlens.mcp.provider import MCPProvider
from codexlens.mcp.schema import MCPContext

logger = logging.getLogger(__name__)


class HookManager:
    """Manages hook registration and execution."""

    def __init__(self, mcp_provider: MCPProvider):
        self.mcp_provider = mcp_provider
        self._pre_hooks: Dict[str, Callable] = {}
        self._post_hooks: Dict[str, Callable] = {}

        # Register default hooks
        self._register_default_hooks()

    def _register_default_hooks(self):
        """Register built-in hooks."""
        self._pre_hooks["explain"] = self._pre_explain_hook
        self._pre_hooks["refactor"] = self._pre_refactor_hook
        self._pre_hooks["document"] = self._pre_document_hook

    def execute_pre_hook(
        self,
        action: str,
        params: Dict[str, Any],
    ) -> Optional[MCPContext]:
        """Execute pre-tool hook to gather context.

        Args:
            action: The action being performed (e.g., "explain", "refactor")
            params: Parameters for the action

        Returns:
            MCPContext to inject into prompt, or None
        """
        hook = self._pre_hooks.get(action)

        if not hook:
            logger.debug(f"No pre-hook for action: {action}")
            return None

        try:
            return hook(params)
        except Exception as e:
            logger.error(f"Pre-hook failed for {action}: {e}")
            return None

    def execute_post_hook(
        self,
        action: str,
        result: Any,
    ) -> None:
        """Execute post-tool hook for proactive caching.

        Args:
            action: The action that was performed
            result: Result of the action
        """
        hook = self._post_hooks.get(action)

        if not hook:
            return

        try:
            hook(result)
        except Exception as e:
            logger.error(f"Post-hook failed for {action}: {e}")

    def _pre_explain_hook(self, params: Dict[str, Any]) -> Optional[MCPContext]:
        """Pre-hook for 'explain' action."""
        symbol_name = params.get("symbol")

        if not symbol_name:
            return None

        return self.mcp_provider.build_context(
            symbol_name=symbol_name,
            context_type="symbol_explanation",
            include_references=True,
            include_related=True,
        )

    def _pre_refactor_hook(self, params: Dict[str, Any]) -> Optional[MCPContext]:
        """Pre-hook for 'refactor' action."""
        symbol_name = params.get("symbol")

        if not symbol_name:
            return None

        return self.mcp_provider.build_context(
            symbol_name=symbol_name,
            context_type="refactor_context",
            include_references=True,  # Important for refactoring
            include_related=True,
            max_references=20,  # More references for refactoring
        )

    def _pre_document_hook(self, params: Dict[str, Any]) -> Optional[MCPContext]:
        """Pre-hook for 'document' action."""
        symbol_name = params.get("symbol")
        file_path = params.get("file_path")

        if symbol_name:
            return self.mcp_provider.build_context(
                symbol_name=symbol_name,
                context_type="documentation_context",
                include_references=False,
                include_related=True,
            )
        elif file_path:
            return self.mcp_provider.build_context_for_file(
                Path(file_path),
                context_type="file_documentation",
            )

        return None

    def register_pre_hook(
        self,
        action: str,
        hook: Callable[[Dict[str, Any]], Optional[MCPContext]],
    ) -> None:
        """Register a custom pre-tool hook."""
        self._pre_hooks[action] = hook

    def register_post_hook(
        self,
        action: str,
        hook: Callable[[Any], None],
    ) -> None:
        """Register a custom post-tool hook."""
        self._post_hooks[action] = hook


# Convenience function for Claude Code integration
def create_context_for_prompt(
    mcp_provider: MCPProvider,
    action: str,
    params: Dict[str, Any],
) -> str:
    """Create context string for prompt injection.

    This is the main entry point for Claude Code hook integration.

    Args:
        mcp_provider: The MCP provider instance
        action: Action being performed
        params: Action parameters

    Returns:
        Formatted context string for prompt injection
    """
    manager = HookManager(mcp_provider)
    context = manager.execute_pre_hook(action, params)

    if context:
        return context.to_prompt_injection()

    return ""

8. Phase 5: Advanced Features

8.1 Custom LSP Commands

File: src/codexlens/lsp/handlers.py (APPEND)

# Custom commands for advanced features

@server.command("codexlens.hybridSearch")
def cmd_hybrid_search(params: List[Any]) -> dict:
    """Execute hybrid search combining FTS and semantic."""
    if len(params) < 1:
        return {"error": "Query required"}

    query = params[0]
    limit = params[1] if len(params) > 1 else 20

    from codexlens.search.hybrid_search import HybridSearchEngine

    engine = HybridSearchEngine(server.search_engine.store)
    results = engine.search(query, limit=limit)

    return {
        "results": [
            {
                "path": r.path,
                "score": r.score,
                "excerpt": r.excerpt,
            }
            for r in results
        ]
    }


@server.command("codexlens.getMCPContext")
def cmd_get_mcp_context(params: List[Any]) -> dict:
    """Get MCP context for a symbol."""
    if len(params) < 1:
        return {"error": "Symbol name required"}

    symbol_name = params[0]
    context_type = params[1] if len(params) > 1 else "symbol_explanation"

    from codexlens.mcp.provider import MCPProvider

    provider = MCPProvider(
        server.global_index,
        server.search_engine,
        server.registry,
    )

    context = provider.build_context(symbol_name, context_type)

    if context:
        return context.to_dict()

    return {"error": "Symbol not found"}

8.2 Performance Optimizations

File: src/codexlens/lsp/cache.py (NEW)

"""Caching layer for LSP performance."""

import time
from functools import lru_cache
from typing import Any, Dict, Optional
from threading import Lock


class LRUCacheWithTTL:
    """LRU cache with time-to-live expiration."""

    def __init__(self, maxsize: int = 1000, ttl_seconds: int = 300):
        self.maxsize = maxsize
        self.ttl = ttl_seconds
        self._cache: Dict[str, tuple] = {}  # key -> (value, timestamp)
        self._lock = Lock()

    def get(self, key: str) -> Optional[Any]:
        """Get value from cache if not expired."""
        with self._lock:
            if key not in self._cache:
                return None

            value, timestamp = self._cache[key]

            if time.time() - timestamp > self.ttl:
                del self._cache[key]
                return None

            return value

    def set(self, key: str, value: Any) -> None:
        """Set value in cache."""
        with self._lock:
            # Evict oldest if at capacity
            if len(self._cache) >= self.maxsize:
                oldest_key = min(
                    self._cache.keys(),
                    key=lambda k: self._cache[k][1],
                )
                del self._cache[oldest_key]

            self._cache[key] = (value, time.time())

    def invalidate(self, key: str) -> None:
        """Remove key from cache."""
        with self._lock:
            self._cache.pop(key, None)

    def invalidate_prefix(self, prefix: str) -> None:
        """Remove all keys with given prefix."""
        with self._lock:
            keys_to_remove = [
                k for k in self._cache.keys()
                if k.startswith(prefix)
            ]
            for key in keys_to_remove:
                del self._cache[key]

    def clear(self) -> None:
        """Clear all cache entries."""
        with self._lock:
            self._cache.clear()


# Global cache instances
definition_cache = LRUCacheWithTTL(maxsize=500, ttl_seconds=300)
references_cache = LRUCacheWithTTL(maxsize=200, ttl_seconds=60)
completion_cache = LRUCacheWithTTL(maxsize=100, ttl_seconds=30)

9. Testing Strategy

9.1 Test Structure

tests/
├── lsp/
│   ├── __init__.py
│   ├── conftest.py              # Fixtures
│   ├── test_server.py           # Server lifecycle
│   ├── test_definition.py       # Definition handler
│   ├── test_references.py       # References handler
│   ├── test_completion.py       # Completion handler
│   ├── test_hover.py            # Hover handler
│   └── test_workspace_symbol.py # Workspace symbol
│
├── mcp/
│   ├── __init__.py
│   ├── test_schema.py           # MCP schema validation
│   ├── test_provider.py         # Context building
│   └── test_hooks.py            # Hook execution
│
└── integration/
    ├── __init__.py
    ├── test_lsp_client.py       # Full LSP handshake
    └── test_mcp_flow.py         # End-to-end MCP flow

9.2 Fixtures

File: tests/lsp/conftest.py

"""Test fixtures for LSP tests."""

import pytest
from pathlib import Path
import tempfile
import shutil

from codexlens.lsp.server import CodexLensLanguageServer


@pytest.fixture
def temp_workspace():
    """Create temporary workspace with sample files."""
    tmpdir = Path(tempfile.mkdtemp())

    # Create sample Python files
    (tmpdir / "main.py").write_text("""
def main():
    result = helper_function(42)
    print(result)

def helper_function(x):
    return x * 2
""")

    (tmpdir / "utils.py").write_text("""
from main import helper_function

class Calculator:
    def add(self, a, b):
        return a + b

    def multiply(self, a, b):
        return helper_function(a) * b
""")

    yield tmpdir

    shutil.rmtree(tmpdir)


@pytest.fixture
def indexed_workspace(temp_workspace):
    """Workspace with built indexes."""
    from codexlens.cli.commands import index_directory

    index_directory(temp_workspace)

    return temp_workspace


@pytest.fixture
def lsp_server(indexed_workspace):
    """Initialized LSP server."""
    server = CodexLensLanguageServer("test", "0.1.0")
    server.initialize_codexlens(indexed_workspace)

    yield server

    server.shutdown_codexlens()

9.3 Performance Benchmarks

File: tests/benchmarks/test_performance.py

"""Performance benchmarks for LSP operations."""

import pytest
import time


class TestPerformance:
    """Performance benchmark tests."""

    @pytest.mark.benchmark
    def test_definition_latency(self, lsp_server, benchmark):
        """Definition lookup should be < 50ms."""
        def lookup():
            return lsp_server.global_index.search("helper_function", exact=True)

        result = benchmark(lookup)
        assert benchmark.stats.stats.mean < 0.05  # 50ms

    @pytest.mark.benchmark
    def test_completion_latency(self, lsp_server, benchmark):
        """Completion should be < 100ms."""
        def complete():
            return lsp_server.global_index.search("help", prefix_mode=True, limit=50)

        result = benchmark(complete)
        assert benchmark.stats.stats.mean < 0.1  # 100ms

    @pytest.mark.benchmark
    def test_references_latency(self, lsp_server, benchmark):
        """References should be < 200ms."""
        def find_refs():
            return lsp_server.search_engine.search_references("helper_function")

        result = benchmark(find_refs)
        assert benchmark.stats.stats.mean < 0.2  # 200ms

10. Deployment Guide

10.1 Installation

# Install with LSP support
pip install codex-lens[lsp]

# Or from source
git clone https://github.com/your-org/codex-lens.git
cd codex-lens
pip install -e ".[lsp]"

10.2 VS Code Configuration

File: .vscode/settings.json

{
    "codexlens.enable": true,
    "codexlens.serverPath": "codexlens-lsp",
    "codexlens.serverArgs": ["--stdio"],
    "codexlens.trace.server": "verbose"
}

10.3 Neovim Configuration

File: ~/.config/nvim/lua/lsp/codexlens.lua

local lspconfig = require('lspconfig')
local configs = require('lspconfig.configs')

configs.codexlens = {
    default_config = {
        cmd = { 'codexlens-lsp', '--stdio' },
        filetypes = { 'python', 'javascript', 'typescript' },
        root_dir = lspconfig.util.root_pattern('.git', 'pyproject.toml'),
        settings = {},
    },
}

lspconfig.codexlens.setup{}

10.4 Claude Code Integration

File: ~/.claude/hooks/pre-tool.sh

#!/bin/bash
# Pre-tool hook for Claude Code

ACTION="$1"
PARAMS="$2"

# Call codex-lens MCP provider
python -c "
from codexlens.mcp.hooks import create_context_for_prompt
from codexlens.mcp.provider import MCPProvider
from codexlens.storage.global_index import GlobalSymbolIndex
from codexlens.search.chain_search import ChainSearchEngine
from codexlens.storage.registry import RegistryStore
from codexlens.storage.path_mapper import PathMapper
import json

# Initialize components
registry = RegistryStore()
registry.initialize()
mapper = PathMapper()
search = ChainSearchEngine(registry, mapper)
global_idx = GlobalSymbolIndex(Path.cwd())

provider = MCPProvider(global_idx, search, registry)

params = json.loads('$PARAMS')
context = create_context_for_prompt(provider, '$ACTION', params)
print(context)
"

11. Risk Mitigation

11.1 Risk Matrix

Risk Probability Impact Mitigation
pygls compatibility issues Low High Pin version, test on multiple platforms
Performance degradation Medium Medium Implement caching, benchmark tests
Index corruption Low High Use WAL mode, implement recovery
Memory leaks in long sessions Medium Medium Implement connection pooling, periodic cleanup
Hook execution timeout Medium Low Implement timeout limits, async execution

11.2 Fallback Strategies

  1. Index not available: Return empty results, don't block LSP
  2. Search timeout: Return partial results with warning
  3. WatcherManager crash: Auto-restart with exponential backoff
  4. MCP generation failure: Return minimal context, log error

11.3 Monitoring

# Add to server.py

import prometheus_client

# Metrics
DEFINITION_LATENCY = prometheus_client.Histogram(
    'codexlens_definition_latency_seconds',
    'Time to process definition request',
)
REFERENCES_LATENCY = prometheus_client.Histogram(
    'codexlens_references_latency_seconds',
    'Time to process references request',
)
INDEX_SIZE = prometheus_client.Gauge(
    'codexlens_index_symbols_total',
    'Total symbols in index',
)

Appendix: Quick Reference

File Creation Summary

Phase File Type
1 src/codexlens/lsp/__init__.py NEW
1 src/codexlens/lsp/server.py NEW
1 src/codexlens/lsp/handlers.py NEW
2 src/codexlens/search/chain_search.py MODIFY
3 src/codexlens/lsp/providers.py NEW
4 src/codexlens/mcp/__init__.py NEW
4 src/codexlens/mcp/schema.py NEW
4 src/codexlens/mcp/provider.py NEW
4 src/codexlens/mcp/hooks.py NEW
5 src/codexlens/lsp/cache.py NEW

Command Reference

# Start LSP server
codexlens-lsp --stdio

# Start with TCP (for debugging)
codexlens-lsp --tcp --port 2087

# Run tests
pytest tests/lsp/ -v

# Run benchmarks
pytest tests/benchmarks/ --benchmark-only

# Check coverage
pytest tests/lsp/ --cov=codexlens.lsp --cov-report=html

Document End