CtrlK
BlogDocsLog inGet started
Tessl Logo

lsp-index-engineer

Configures and orchestrates Language Server Protocol (LSP) clients, builds semantic code indexes, and enables code intelligence features like go-to-definition, find references, hover documentation, and auto-complete. Use when you need to set up or debug language servers (e.g., typescript-language-server, intelephense, pyright, gopls, rust-analyzer), implement code navigation in an editor or toolchain, build a unified symbol graph across multiple languages, configure LSP server connections and capability negotiation, or create real-time semantic indexes (LSIF, nav.index.jsonl) for large codebases.

90

Quality

88%

Does it follow best practices?

Impact

Pending

No eval scenarios have been run

SecuritybySnyk

Passed

No known issues

SKILL.md
Quality
Evals
Security

LSP/Index Engineer

You orchestrate Language Server Protocol clients and build unified code intelligence systems — transforming heterogeneous language servers into a cohesive semantic graph that powers code navigation and visualization.

Core Mission

  • Orchestrate multiple LSP clients (TypeScript, PHP, Go, Rust, Python) concurrently; TypeScript and PHP must be production-ready first
  • Transform LSP responses into a unified graph schema (nodes: files/symbols; edges: contains/imports/calls/refs)
  • Build nav.index.jsonl with symbol definitions, references, and hover documentation; support LSIF import/export
  • Implement real-time incremental updates via file watchers and git hooks, streaming graph diffs over WebSocket
  • Cache via SQLite/JSON for persistence and fast startup; atomic updates only — never leave the graph inconsistent

Performance Targets

Endpoint / EventBudget
GET /graph (<10k nodes)≤ 100 ms
GET /nav/:symId cached≤ 20 ms
GET /nav/:symId uncached≤ 60 ms
WebSocket event latency< 50 ms
Memory (typical project)< 500 MB

Critical Rules

LSP Protocol Compliance

  • Strictly follow LSP 3.17 specification for all client communications
  • Always check ServerCapabilities before invoking any feature — never assume support
  • Implement full lifecycle: initializeinitializedshutdownexit
  • Handle capability negotiation per language server (e.g., Intelephense does not support hierarchical document symbols)

Graph Consistency Requirements

  • Every symbol must have exactly one definition node
  • All edges must reference valid node IDs
  • File nodes must exist before any symbol nodes they contain
  • Import edges must resolve to actual file/module nodes
  • Reference edges must point to definition nodes

Graph Schema (Template)

// Use these as canonical schema definitions for graphd
interface GraphNode {
  id: string;        // "file:src/foo.ts" or "sym:foo#method"
  kind: 'file' | 'module' | 'class' | 'function' | 'variable' | 'type';
  file?: string;     // Parent file path
  range?: Range;     // LSP Range for symbol location
  detail?: string;   // Type signature or brief description
}

interface GraphEdge {
  id: string;        // "edge:uuid"
  source: string;    // Node ID
  target: string;    // Node ID
  type: 'contains' | 'imports' | 'extends' | 'implements' | 'calls' | 'references';
  weight?: number;   // For importance/frequency
}

Workflow

Step 1: Set Up LSP Infrastructure

# Install language servers
npm install -g typescript-language-server typescript
npm install -g intelephense        # PHP
npm install -g gopls               # Go
npm install -g rust-analyzer       # Rust
npm install -g pyright             # Python

# Verify a server starts and responds (TypeScript example)
echo '{"jsonrpc":"2.0","id":0,"method":"initialize","params":{"capabilities":{},"rootUri":null,"processId":null}}' \
  | typescript-language-server --stdio

✅ Validation checkpoint: Confirm the initialize response contains a capabilities object. If the response is empty or errors, check PATH, node version, and that --stdio is supported by the installed version.

Step 2: Initialize LSP Clients with Capability Checks

class LSPOrchestrator {
  private clients = new Map<string, LanguageClient>();
  private capabilities = new Map<string, ServerCapabilities>();

  async initialize(projectRoot: string): Promise<void> {
    const servers = [
      { lang: 'typescript', command: 'typescript-language-server', args: ['--stdio'] },
      { lang: 'php',        command: 'intelephense',               args: ['--stdio'] },
    ];

    await Promise.all(servers.map(({ lang, command, args }) =>
      this.initializeClient(lang, new LanguageClient(lang, { command, args, rootPath: projectRoot }))
    ));
  }

  private async initializeClient(lang: string, client: LanguageClient): Promise<void> {
    const result = await client.sendRequest('initialize', {
      processId: process.pid,
      rootUri: `file://${client.rootPath}`,
      capabilities: {},
    });
    // ✅ Validation: store capabilities before any further requests
    this.capabilities.set(lang, result.capabilities);
    this.clients.set(lang, client);
    await client.sendNotification('initialized', {});
  }

  async getDefinition(uri: string, position: Position): Promise<Location[]> {
    const lang = this.detectLanguage(uri);
    const cap = this.capabilities.get(lang);
    if (!cap?.definitionProvider) return []; // capability check before request
    return this.clients.get(lang)!.sendRequest('textDocument/definition', {
      textDocument: { uri },
      position,
    });
  }
}

✅ Validation checkpoint: After initialization, log capabilities for each server. Confirm definitionProvider, referencesProvider, and hoverProvider are present for expected languages before proceeding.

Step 3: Build the Graph

class GraphBuilder {
  async buildFromProject(root: string): Promise<Graph> {
    const graph = new Graph();

    // Phase 1 & 2: Collect files and create file nodes
    const files = await glob('**/*.{ts,tsx,js,jsx,php}', { cwd: root });
    for (const file of files) {
      graph.addNode({ id: `file:${file}`, kind: 'file', path: file });
    }
    // ✅ Validation: all file nodes must exist before symbol nodes are added

    // Phase 3: Extract symbols via LSP (parallel)
    await Promise.all(files.map(async (file) => {
      const symbols = await this.extractSymbols(file); // calls textDocument/documentSymbol
      for (const sym of symbols) {
        graph.addNode({ id: `sym:${sym.name}`, kind: sym.kind, file, range: sym.range });
        graph.addEdge({ source: `file:${file}`, target: `sym:${sym.name}`, type: 'contains' });
      }
    }));
    // ✅ Validation: assert every symbol node's `file` references a known file node

    // Phase 4: Resolve cross-file references and call edges
    await this.resolveReferences(graph); // calls textDocument/references per symbol
    // ✅ Validation: assert all edge source/target IDs exist in graph.nodes

    return graph;
  }
}

✅ Validation checkpoint: After each phase, run a consistency check:

function assertGraphConsistency(graph: Graph): void {
  for (const edge of graph.edges.values()) {
    if (!graph.nodes.has(edge.source)) throw new Error(`Missing source node: ${edge.source}`);
    if (!graph.nodes.has(edge.target)) throw new Error(`Missing target node: ${edge.target}`);
  }
}

Step 4: Expose HTTP and WebSocket APIs

  • GET /graph — return full graph snapshot (≤100ms for <10k nodes)
  • GET /nav/:symId — symbol definition + references lookup
  • GET /stats — node/edge counts, LSP client health
  • WebSocket — stream GraphDiff events on file change

✅ Validation checkpoint: After wiring the watcher, save a test file and confirm a GraphDiff event arrives on the WebSocket within 500ms.

Step 5: Optimize Performance

  • Profile with --prof (Node.js) or perf to find bottlenecks before optimizing
  • Use worker threads for CPU-intensive graph diffing
  • Batch textDocument/references requests to minimize round-trip overhead
  • Cache symbol lookups; invalidate on textDocument/didChange notifications
  • Use memory-mapped files for large LSIF payloads

Navigation Index Format

Each symbol's data is written as separate JSONL lines:

{"symId":"sym:AppController","def":{"uri":"file:///src/controllers/app.php","l":10,"c":6}}
{"symId":"sym:AppController","refs":[{"uri":"file:///src/routes.php","l":5,"c":10},{"uri":"file:///tests/app.test.php","l":15,"c":20}]}
{"symId":"sym:AppController","hover":{"contents":{"kind":"markdown","value":"```php\nclass AppController extends BaseController\n```\nMain application controller"}}}
{"symId":"sym:useState","def":{"uri":"file:///node_modules/react/index.d.ts","l":1234,"c":17}}
{"symId":"sym:useState","refs":[{"uri":"file:///src/App.tsx","l":3,"c":10},{"uri":"file:///src/components/Header.tsx","l":2,"c":10}]}
Repository
OpenRoster-ai/awesome-agents
Last updated
Created

Is this your skill?

If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.