Skip to content

LSP Integration Guide

Overview

The LSP (Language Server Protocol) integration provides enhanced semantic analysis capabilities by communicating with language servers like Pyright (Python) and typescript-language-server (TypeScript/JavaScript). This enables type-aware vulnerability detection and precise cross-file analysis.

Architecture

┌─────────────────────────────────────────────────────────────────┐
│                      mcp-scan                                    │
│  ┌─────────────────────────────────────────────────────────────┐│
│  │                    LSP Manager                               ││
│  │  ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌────────┐││
│  │  │   Python    │ │ TypeScript  │ │ JavaScript  │ │   Go   │││
│  │  │   Client    │ │   Client    │ │   Client    │ │ Client │││
│  │  └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ └───┬────┘││
│  └─────────┼───────────────┼───────────────┼────────────┼──────┘│
└────────────┼───────────────┼───────────────┼────────────┼───────┘
             │               │               │            │
    ┌────────▼────────┐ ┌────▼────┐ ┌────────▼────┐ ┌────▼────┐
    │     Pyright     │ │tsserver │ │   tsserver  │ │  gopls  │
    │  (subprocess)   │ │(subproc)│ │ (subprocess)│ │(subproc)│
    └─────────────────┘ └─────────┘ └─────────────┘ └─────────┘

Components

LSP Client (internal/lsp/client.go)

The core client manages JSON-RPC 2.0 communication with language servers.

// Create a new LSP client
client, err := lsp.NewClient(ctx, "python", "/path/to/project")
if err != nil {
    log.Fatal(err)
}
defer client.Close()

// Get type information at a position
typeInfo, err := client.GetTypeInfo(ctx, "file:///path/to/file.py", 10, 5)
if err != nil {
    log.Printf("Failed to get type info: %v", err)
}

Supported Operations

Operation Method Description
Hover GetHover() Get hover information (types, docs)
Definition GetDefinition() Go to definition location
References GetReferences() Find all references to a symbol
Document Symbols GetDocumentSymbols() List all symbols in a file
Call Hierarchy PrepareCallHierarchy() Get call hierarchy for a function
Incoming Calls GetIncomingCalls() Find callers of a function
Outgoing Calls GetOutgoingCalls() Find functions called by a function

LSP Manager (internal/lsp/manager.go)

The manager handles multiple language clients and provides a unified interface.

// Create manager for a project
manager := lsp.NewManager("/path/to/project")

// Get client for a specific language (starts server if needed)
client, err := manager.GetClient(ctx, "python")
if err != nil {
    log.Printf("Python LSP not available: %v", err)
}

// Close all clients when done
defer manager.CloseAll()

LSP Types (internal/lsp/types.go)

Complete LSP protocol type definitions:

// Position in a document
type Position struct {
    Line      uint32 `json:"line"`
    Character uint32 `json:"character"`
}

// Range in a document
type Range struct {
    Start Position `json:"start"`
    End   Position `json:"end"`
}

// Location in a document
type Location struct {
    URI   string `json:"uri"`
    Range Range  `json:"range"`
}

// Hover result
type Hover struct {
    Contents MarkupContent `json:"contents"`
    Range    *Range        `json:"range,omitempty"`
}

// Call hierarchy item
type CallHierarchyItem struct {
    Name           string     `json:"name"`
    Kind           SymbolKind `json:"kind"`
    URI            string     `json:"uri"`
    Range          Range      `json:"range"`
    SelectionRange Range      `json:"selectionRange"`
}

LSP Operations (internal/lsp/operations.go)

High-level operations built on the LSP protocol:

// TypeInfo extracted from hover
type TypeInfo struct {
    Signature   string   // Full type signature
    ReturnType  string   // Return type (for functions)
    Parameters  []string // Parameter types
    Description string   // Documentation
    Kind        string   // function, variable, class, etc.
}

// Definition location
type Definition struct {
    URI         string
    StartLine   uint32
    StartColumn uint32
    EndLine     uint32
    EndColumn   uint32
}

// Call graph node
type CallGraphNode struct {
    Name    string
    URI     string
    Kind    SymbolKind
    Range   Range
    Callers []*CallGraphEdge
    Callees []*CallGraphEdge
}

Language Server Installation

Supported Servers

Language Server Command
Python Pyright pyright-langserver --stdio
TypeScript typescript-language-server typescript-language-server --stdio
JavaScript typescript-language-server typescript-language-server --stdio
Go gopls gopls serve

Python (Pyright)

Installation:

# Install with npm (recommended)
npm install -g pyright

# Verify installation
pyright-langserver --version

Note: Pyright requires Node.js 14+.

Server command:

pyright-langserver --stdio

Capabilities: - Type inference - Import resolution - Call hierarchy - Find references - Go to definition

TypeScript/JavaScript (typescript-language-server)

Installation:

# Install both typescript and the language server
npm install -g typescript typescript-language-server

# Verify installation
typescript-language-server --version

Server command:

typescript-language-server --stdio

Capabilities: - Type inference - Module resolution - Call hierarchy - Find references - Go to definition

Go (gopls)

Installation:

# Install gopls
go install golang.org/x/tools/gopls@latest

# Add ~/go/bin to PATH if not already
export PATH=$PATH:~/go/bin

# Verify installation
gopls version

Server command:

gopls serve

Capabilities: - Type inference - Module resolution - Call hierarchy - Find references - Go to definition

Search Paths

mcp-scan searches for language servers in these locations:

  1. System $PATH
  2. ~/go/bin (Go binaries)
  3. ~/.local/bin
  4. /usr/local/bin
  5. /opt/homebrew/bin (macOS ARM)
  6. ~/.npm-global/bin
  7. ~/node_modules/.bin

CLI Usage

Enable LSP in Scan

# Scan with LSP enabled
mcp-scan scan --lsp ./my-project

# Scan with debug mode to see LSP activity
LSP_DEBUG=1 mcp-scan scan --lsp ./my-project

YAML Configuration

# .mcp-scan.yaml
lsp:
  enabled: true
  languages:
    - python
    - typescript
    - javascript
    - go

Progress Output

Normal mode:

[*] Scanning ./my-project...
[*] LSP: enabled
[*] Discovered 5 files
[*] Running pattern analysis...
[+] Scan completed in 2.3s
[+] Found 3 findings (2 high, 1 medium)

Debug mode (LSP_DEBUG=1):

[LSP] Found 5 symbols, 4 functions in server.py
[LSP] Analyzing tool get_company_data at server.py:31:4
[LSP]   type: (function) def get_company_data(data_type: str) -> str
[LSP]   call graph: get_company_data -> 1 callees


LSP Detector Integration

The LSPDetector uses LSP capabilities for enhanced vulnerability detection.

Configuration

cfg := pattern.LSPDetectorConfig{
    RootPath:  "/path/to/project",
    Languages: []string{"python", "typescript", "javascript"},
}

detector, err := pattern.NewLSPDetector(cfg)
if err != nil {
    log.Fatal(err)
}
defer detector.Close()

Detection Flow

1. Tool Handler Identified
2. Query LSP for Type Info
3. Check for Dangerous Types (Any, object, unknown)
4. Build Call Graph from Handler
5. Traverse Graph for Dangerous Sinks
6. Generate Findings with Type Context

Detection Rules

LSP-TYPE-001: Dangerous Type Pattern

Detects when a tool parameter has a dangerous type like Any, object, or unknown.

# Detected: parameter with Any type
@tool
def process_data(data: Any):  # LSP-TYPE-001
    eval(data)

LSP-SINK-001: Tool Input to Dangerous Sink

Detects when a tool handler can reach a dangerous sink via call graph analysis.

@tool
def execute_command(cmd: str):
    # LSP builds call graph and finds path to os.system
    helper(cmd)

def helper(command):
    run_shell(command)

def run_shell(cmd):
    os.system(cmd)  # LSP-SINK-001: Dangerous sink reached

Call Graph Building

Algorithm

func (c *Client) BuildCallGraph(ctx context.Context, uri string,
    line, character uint32, maxDepth int) (*CallGraphNode, error) {

    // 1. Prepare call hierarchy at position
    items, err := c.PrepareCallHierarchy(ctx, uri, line, character)

    // 2. Create root node
    root := &CallGraphNode{
        Name:  items[0].Name,
        URI:   items[0].URI,
        Kind:  items[0].Kind,
        Range: items[0].Range,
    }

    // 3. Recursively build graph
    visited := make(map[string]bool)
    c.buildCallGraphRecursive(ctx, items[0], root, visited, 0, maxDepth)

    return root, nil
}

Depth Limiting

The maxDepth parameter controls how deep the call graph exploration goes:

Depth Use Case
1-2 Quick checks, direct calls only
3-5 Standard analysis, most vulnerabilities
5-10 Deep analysis, complex call chains
10+ Exhaustive analysis, performance impact

Type Information Extraction

Hover Parsing

The LSP hover response contains type information in markdown format:

```python
(function) def process(data: str) -> None

Process user input data.

Parsed into:
```go
TypeInfo{
    Signature:   "def process(data: str) -> None",
    ReturnType:  "None",
    Parameters:  []string{"data: str"},
    Kind:        "function",
    Description: "Process user input data.",
}

Dangerous Type Detection

Types considered dangerous for security analysis:

Type Language Risk
Any Python No type checking
object Python/TS Generic, no constraints
unknown TypeScript Unsafe type
any TypeScript No type checking
void* C/C++ Raw pointer

Performance Considerations

Server Startup

LSP servers have startup overhead:

Server Cold Start Warm
Pyright 2-5s <100ms
tsserver 1-3s <100ms

Optimization: Keep servers running between analyses.

Memory Usage

Server Base Memory Per 1000 Files
Pyright ~150MB +50MB
tsserver ~200MB +80MB

Caching

The manager caches active clients:

type Manager struct {
    rootPath string
    clients  map[string]*Client  // Cached by language
    mu       sync.Mutex
}

Error Handling

Server Not Available

client, err := manager.GetClient(ctx, "python")
if err != nil {
    // Fallback to non-LSP analysis
    log.Printf("LSP not available, using basic analysis: %v", err)
    return basicAnalysis(file)
}

Request Timeout

ctx, cancel := context.WithTimeout(ctx, 30*time.Second)
defer cancel()

result, err := client.GetHover(ctx, uri, line, col)
if err != nil {
    if errors.Is(err, context.DeadlineExceeded) {
        log.Printf("LSP request timed out")
    }
}

Server Crash Recovery

// Client automatically restarts server on failure
func (c *Client) Call(ctx context.Context, method string,
    params, result interface{}) error {

    err := c.sendRequest(ctx, method, params, result)
    if err != nil && isServerDead(err) {
        // Attempt restart
        if restartErr := c.restart(ctx); restartErr != nil {
            return fmt.Errorf("server restart failed: %w", restartErr)
        }
        // Retry request
        return c.sendRequest(ctx, method, params, result)
    }
    return err
}

Integration Examples

With Pattern Engine

func NewEnhancedEngine(rootPath string) (*pattern.Engine, error) {
    engine := pattern.New()

    // Add LSP detector
    lspDetector, err := pattern.NewLSPDetector(pattern.LSPDetectorConfig{
        RootPath:  rootPath,
        Languages: []string{"python", "typescript"},
    })
    if err == nil && lspDetector.IsEnabled() {
        engine.AddRule(&pattern.Rule{
            ID:       "LSP-ENHANCED",
            Detector: lspDetector,
        })
    }

    return engine, nil
}

With Taint Engine

func EnhancedTaintAnalysis(file *ast.File, lspClient *lsp.Client) {
    engine := taint.NewEngine()

    for _, source := range engine.GetSources(file) {
        // Get precise type from LSP
        typeInfo, _ := lspClient.GetTypeInfo(ctx,
            "file://"+file.Path,
            uint32(source.Location.StartLine),
            uint32(source.Location.StartCol))

        if typeInfo != nil {
            // Use type info to improve taint precision
            source.TypeInfo = typeInfo
        }
    }
}

Troubleshooting

Common Issues

Issue: Server fails to start

Error: spawn pyright-langserver ENOENT
Solution: Install the language server globally:
npm install -g pyright

Issue: No type information returned

TypeInfo is nil for position
Solution: Ensure the file is part of a valid project with proper configuration (pyproject.toml, tsconfig.json).

Issue: Slow analysis Solution: 1. Reduce maxDepth in call graph building 2. Limit number of files analyzed 3. Use warm server (don't restart between files)

Debug Logging

Enable LSP debug logging:

client, err := lsp.NewClientWithOptions(ctx, "python", rootPath,
    lsp.WithDebugLog(os.Stderr))

API Reference

Client Methods

Method Parameters Returns Description
NewClient ctx, language, rootPath *Client, error Create new client
Close - error Shutdown server
GetHover ctx, uri, line, char *Hover, error Get hover info
GetTypeInfo ctx, uri, line, char *TypeInfo, error Get structured type info
GetDefinition ctx, uri, line, char []Definition, error Go to definition
GetReferences ctx, uri, line, char, includeDecl []Definition, error Find references
GetDocumentSymbols ctx, uri []Symbol, error List symbols
PrepareCallHierarchy ctx, uri, line, char []CallHierarchyItem, error Prepare call hierarchy
GetIncomingCalls ctx, item []CallHierarchyIncomingCall, error Get callers
GetOutgoingCalls ctx, item []CallHierarchyOutgoingCall, error Get callees
BuildCallGraph ctx, uri, line, char, maxDepth *CallGraphNode, error Build full call graph

Manager Methods

Method Parameters Returns Description
NewManager rootPath *Manager Create manager
GetClient ctx, language *Client, error Get or create client
CloseAll - error Close all clients

See Also