Claude-skill-registry language-server-protocol

Language Server Protocol (LSP) - Microsoft's open standard for IDE-language server communication. Use for building language servers, implementing LSP clients, understanding protocol architecture, and integrating code intelligence features.

install
source · Clone the upstream repo
git clone https://github.com/majiayu000/claude-skill-registry
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/language-server-protocol" ~/.claude/skills/majiayu000-claude-skill-registry-language-server-protocol && rm -rf "$T"
manifest: skills/data/language-server-protocol/SKILL.md
source content

Language Server Protocol Skill

The Language Server Protocol (LSP) is an open standard that defines the protocol used between an editor or IDE and a language server that provides language features like auto-complete, go to definition, find all references, and more. Think of LSP as "USB-C for code intelligence" - a standardized interface that allows any language server to work with any compatible editor.

Core Value Proposition: Build once, integrate everywhere. A single language server implementation works across VS Code, Neovim, Emacs, Sublime Text, and dozens of other editors without modification.

When to Use This Skill

This skill should be triggered when:

  • Building language servers for programming languages or DSLs
  • Implementing LSP client support in editors or IDEs
  • Understanding LSP protocol architecture and message flow
  • Adding code intelligence features (completion, diagnostics, navigation)
  • Debugging LSP communication issues
  • Integrating existing language servers into tools
  • Extending language server capabilities

Protocol Overview

The Problem LSP Solves

Before LSP:

  • Each editor implemented language features independently
  • Every language needed N integrations for N editors
  • M languages × N editors = M×N implementations

After LSP:

  • One protocol specification
  • M + N implementations needed
  • Any server works with any client

The USB-C Analogy

Just as USB-C provides a universal connector:

  • LSP Client = Device (editor like VS Code, Neovim)
  • LSP Server = Peripheral (language analyzer like rust-analyzer, pyright)
  • LSP Protocol = USB-C specification (JSON-RPC over stdio/TCP)

Architecture

Core Components

┌─────────────────────────────────────────────────────────────┐
│                    LSP ARCHITECTURE                          │
└─────────────────────────────────────────────────────────────┘

┌──────────────────────────────────────────────────────────────┐
│                    EDITOR / IDE (Client)                      │
│  ┌────────────────────────────────────────────────────────┐  │
│  │                    LSP CLIENT                           │  │
│  │  • Sends document events (open, change, save)          │  │
│  │  • Requests language features (completion, definition) │  │
│  │  • Displays diagnostics and suggestions                │  │
│  └───────────┬────────────────────────────────────────────┘  │
└──────────────┼───────────────────────────────────────────────┘
               │ JSON-RPC (stdio / TCP / WebSocket)
               ▼
┌──────────────────────────────────────────────────────────────┐
│                    LANGUAGE SERVER                            │
│                                                              │
│  • Parses and analyzes source code                          │
│  • Maintains project model and symbol tables                │
│  • Responds to feature requests                             │
│  • Publishes diagnostics (errors, warnings)                 │
└──────────────────────────────────────────────────────────────┘

Communication Protocol

LSP uses JSON-RPC 2.0 with a specific message format:

Content-Length: 123\r\n
Content-Type: application/vscode-jsonrpc; charset=utf-8\r\n
\r\n
{"jsonrpc":"2.0","id":1,"method":"textDocument/definition","params":{...}}

Message Types:

TypeHas IDExpects ResponseExample
RequestYesYes
textDocument/completion
ResponseYes (matches request)N/AResult or error
NotificationNoNo
textDocument/didOpen

Lifecycle

┌─────────────────────────────────────────────────────────────┐
│                    LSP LIFECYCLE                             │
└─────────────────────────────────────────────────────────────┘

1. INITIALIZATION
   Client ──initialize──────────► Server
     └─ capabilities, rootUri, clientInfo

   Client ◄──result────────────── Server
     └─ capabilities, serverInfo

   Client ──initialized─────────► Server (notification)

2. DOCUMENT SYNCHRONIZATION
   Client ──didOpen─────────────► Server (file opened)
   Client ──didChange───────────► Server (content changed)
   Client ◄──publishDiagnostics── Server (errors/warnings)
   Client ──didSave─────────────► Server (file saved)
   Client ──didClose────────────► Server (file closed)

3. FEATURE REQUESTS
   Client ──completion──────────► Server
   Client ◄──completionItems───── Server

   Client ──definition──────────► Server
   Client ◄──location───────────── Server

4. SHUTDOWN
   Client ──shutdown────────────► Server
   Client ◄──result (null)─────── Server
   Client ──exit────────────────► Server (notification)

Language Features

Navigation Features

MethodPurposeReturns
textDocument/definition
Go to symbol definitionLocation(s)
textDocument/declaration
Go to symbol declarationLocation(s)
textDocument/typeDefinition
Go to type definitionLocation(s)
textDocument/implementation
Go to implementationsLocation(s)
textDocument/references
Find all referencesLocation[]

Example Request (Go to Definition):

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "textDocument/definition",
  "params": {
    "textDocument": {
      "uri": "file:///project/src/main.ts"
    },
    "position": {
      "line": 10,
      "character": 15
    }
  }
}

Example Response:

{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "uri": "file:///project/src/utils.ts",
    "range": {
      "start": { "line": 5, "character": 0 },
      "end": { "line": 5, "character": 20 }
    }
  }
}

Completion

Request:

{
  "jsonrpc": "2.0",
  "id": 2,
  "method": "textDocument/completion",
  "params": {
    "textDocument": { "uri": "file:///project/src/main.ts" },
    "position": { "line": 12, "character": 8 },
    "context": {
      "triggerKind": 1,
      "triggerCharacter": "."
    }
  }
}

Response:

{
  "jsonrpc": "2.0",
  "id": 2,
  "result": {
    "isIncomplete": false,
    "items": [
      {
        "label": "toString",
        "kind": 2,
        "detail": "(): string",
        "documentation": "Returns a string representation",
        "insertText": "toString()"
      },
      {
        "label": "valueOf",
        "kind": 2,
        "detail": "(): number",
        "insertText": "valueOf()"
      }
    ]
  }
}

Completion Item Kinds:

KindValueKindValue
Text1Method2
Function3Constructor4
Field5Variable6
Class7Interface8
Module9Property10
Snippet15Keyword14

Diagnostics

Servers push diagnostics via notification:

{
  "jsonrpc": "2.0",
  "method": "textDocument/publishDiagnostics",
  "params": {
    "uri": "file:///project/src/main.ts",
    "version": 3,
    "diagnostics": [
      {
        "range": {
          "start": { "line": 10, "character": 0 },
          "end": { "line": 10, "character": 10 }
        },
        "severity": 1,
        "code": "TS2322",
        "source": "typescript",
        "message": "Type 'string' is not assignable to type 'number'",
        "relatedInformation": [
          {
            "location": {
              "uri": "file:///project/src/types.ts",
              "range": { "start": { "line": 5, "character": 2 }, "end": { "line": 5, "character": 8 } }
            },
            "message": "The expected type comes from property 'count'"
          }
        ]
      }
    ]
  }
}

Diagnostic Severities:

ValueSeverity
1Error
2Warning
3Information
4Hint

Hover Information

// Request
{
  "method": "textDocument/hover",
  "params": {
    "textDocument": { "uri": "file:///project/src/main.ts" },
    "position": { "line": 8, "character": 10 }
  }
}

// Response
{
  "result": {
    "contents": {
      "kind": "markdown",
      "value": "```typescript\nfunction calculateSum(a: number, b: number): number\n```\n\nCalculates the sum of two numbers."
    },
    "range": {
      "start": { "line": 8, "character": 4 },
      "end": { "line": 8, "character": 16 }
    }
  }
}

Code Actions

Request quick fixes and refactorings:

// Request
{
  "method": "textDocument/codeAction",
  "params": {
    "textDocument": { "uri": "file:///project/src/main.ts" },
    "range": {
      "start": { "line": 10, "character": 0 },
      "end": { "line": 10, "character": 20 }
    },
    "context": {
      "diagnostics": [...],
      "only": ["quickfix"]
    }
  }
}

// Response
{
  "result": [
    {
      "title": "Convert to template literal",
      "kind": "refactor.rewrite",
      "edit": {
        "changes": {
          "file:///project/src/main.ts": [
            {
              "range": { "start": { "line": 10, "character": 0 }, "end": { "line": 10, "character": 25 } },
              "newText": "`Hello, ${name}!`"
            }
          ]
        }
      }
    }
  ]
}

Document Symbols

// Request
{
  "method": "textDocument/documentSymbol",
  "params": {
    "textDocument": { "uri": "file:///project/src/main.ts" }
  }
}

// Response (hierarchical)
{
  "result": [
    {
      "name": "Calculator",
      "kind": 5,
      "range": { "start": { "line": 0, "character": 0 }, "end": { "line": 20, "character": 1 } },
      "selectionRange": { "start": { "line": 0, "character": 6 }, "end": { "line": 0, "character": 16 } },
      "children": [
        {
          "name": "add",
          "kind": 6,
          "range": { "start": { "line": 2, "character": 2 }, "end": { "line": 4, "character": 3 } },
          "selectionRange": { "start": { "line": 2, "character": 2 }, "end": { "line": 2, "character": 5 } }
        }
      ]
    }
  ]
}

Symbol Kinds:

KindValueKindValue
File1Module2
Namespace3Package4
Class5Method6
Property7Field8
Constructor9Enum10
Interface11Function12
Variable13Constant14

Capabilities Negotiation

Client Capabilities (sent in
initialize
)

{
  "capabilities": {
    "textDocument": {
      "synchronization": {
        "dynamicRegistration": true,
        "willSave": true,
        "willSaveWaitUntil": true,
        "didSave": true
      },
      "completion": {
        "completionItem": {
          "snippetSupport": true,
          "commitCharactersSupport": true,
          "documentationFormat": ["markdown", "plaintext"],
          "resolveSupport": {
            "properties": ["documentation", "detail"]
          }
        },
        "contextSupport": true
      },
      "hover": {
        "contentFormat": ["markdown", "plaintext"]
      },
      "definition": {
        "linkSupport": true
      },
      "codeAction": {
        "codeActionLiteralSupport": {
          "codeActionKind": {
            "valueSet": ["quickfix", "refactor", "source"]
          }
        }
      }
    },
    "workspace": {
      "workspaceFolders": true,
      "configuration": true,
      "didChangeConfiguration": {
        "dynamicRegistration": true
      }
    }
  }
}

Server Capabilities (returned in
initialize
response)

{
  "capabilities": {
    "textDocumentSync": {
      "openClose": true,
      "change": 2,
      "save": { "includeText": false }
    },
    "completionProvider": {
      "triggerCharacters": [".", ":", "<"],
      "resolveProvider": true
    },
    "hoverProvider": true,
    "definitionProvider": true,
    "referencesProvider": true,
    "documentSymbolProvider": true,
    "workspaceSymbolProvider": true,
    "codeActionProvider": {
      "codeActionKinds": ["quickfix", "refactor.extract", "source.organizeImports"]
    },
    "documentFormattingProvider": true,
    "renameProvider": {
      "prepareProvider": true
    },
    "diagnosticProvider": {
      "interFileDependencies": true,
      "workspaceDiagnostics": true
    }
  }
}

Text Document Sync Kinds

ValueModeDescription
0NoneNo synchronization
1FullFull document on every change
2IncrementalOnly send changes (preferred)

Building a Language Server

TypeScript (vscode-languageserver)

import {
  createConnection,
  TextDocuments,
  ProposedFeatures,
  InitializeParams,
  TextDocumentSyncKind,
  InitializeResult,
  CompletionItem,
  CompletionItemKind,
  TextDocumentPositionParams,
  Diagnostic,
  DiagnosticSeverity
} from 'vscode-languageserver/node';

import { TextDocument } from 'vscode-languageserver-textdocument';

// Create connection using all proposed features
const connection = createConnection(ProposedFeatures.all);

// Create document manager
const documents: TextDocuments<TextDocument> = new TextDocuments(TextDocument);

connection.onInitialize((params: InitializeParams): InitializeResult => {
  return {
    capabilities: {
      textDocumentSync: TextDocumentSyncKind.Incremental,
      completionProvider: {
        resolveProvider: true,
        triggerCharacters: ['.']
      },
      hoverProvider: true,
      definitionProvider: true,
      referencesProvider: true
    }
  };
});

// Validate documents on change
documents.onDidChangeContent(change => {
  validateTextDocument(change.document);
});

async function validateTextDocument(document: TextDocument): Promise<void> {
  const diagnostics: Diagnostic[] = [];
  const text = document.getText();

  // Example: Find TODO comments
  const todoPattern = /\bTODO\b/g;
  let match;
  while ((match = todoPattern.exec(text))) {
    diagnostics.push({
      severity: DiagnosticSeverity.Information,
      range: {
        start: document.positionAt(match.index),
        end: document.positionAt(match.index + match[0].length)
      },
      message: 'TODO comment found',
      source: 'my-language-server'
    });
  }

  connection.sendDiagnostics({ uri: document.uri, diagnostics });
}

// Provide completions
connection.onCompletion((params: TextDocumentPositionParams): CompletionItem[] => {
  return [
    {
      label: 'console',
      kind: CompletionItemKind.Module,
      detail: 'Console object',
      documentation: 'The console object provides access to debugging console'
    },
    {
      label: 'console.log',
      kind: CompletionItemKind.Function,
      detail: '(message: any): void',
      insertText: 'console.log($1)',
      insertTextFormat: 2 // Snippet
    }
  ];
});

// Provide hover information
connection.onHover((params) => {
  const document = documents.get(params.textDocument.uri);
  if (!document) return null;

  // Get word at position and return hover info
  return {
    contents: {
      kind: 'markdown',
      value: '**Symbol Info**\n\nDocumentation here'
    }
  };
});

// Listen for document events
documents.listen(connection);

// Start the connection
connection.listen();

Python (pygls)

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

server = LanguageServer("my-language-server", "v1.0")

@server.feature(lsp.INITIALIZE)
def initialize(params: lsp.InitializeParams) -> lsp.InitializeResult:
    return lsp.InitializeResult(
        capabilities=lsp.ServerCapabilities(
            text_document_sync=lsp.TextDocumentSyncOptions(
                open_close=True,
                change=lsp.TextDocumentSyncKind.Incremental,
            ),
            completion_provider=lsp.CompletionOptions(
                trigger_characters=["."],
                resolve_provider=True,
            ),
            hover_provider=True,
            definition_provider=True,
        )
    )

@server.feature(lsp.TEXT_DOCUMENT_DID_OPEN)
def did_open(params: lsp.DidOpenTextDocumentParams):
    """Handle document open."""
    validate_document(params.text_document.uri)

@server.feature(lsp.TEXT_DOCUMENT_DID_CHANGE)
def did_change(params: lsp.DidChangeTextDocumentParams):
    """Handle document changes."""
    validate_document(params.text_document.uri)

def validate_document(uri: str):
    """Validate document and publish diagnostics."""
    document = server.workspace.get_text_document(uri)
    diagnostics = []

    # Example: Find syntax issues
    for i, line in enumerate(document.lines):
        if "TODO" in line:
            diagnostics.append(lsp.Diagnostic(
                range=lsp.Range(
                    start=lsp.Position(line=i, character=line.index("TODO")),
                    end=lsp.Position(line=i, character=line.index("TODO") + 4),
                ),
                message="TODO comment found",
                severity=lsp.DiagnosticSeverity.Information,
                source="my-language-server",
            ))

    server.publish_diagnostics(uri, diagnostics)

@server.feature(lsp.TEXT_DOCUMENT_COMPLETION)
def completions(params: lsp.CompletionParams) -> lsp.CompletionList:
    """Provide completion items."""
    return lsp.CompletionList(
        is_incomplete=False,
        items=[
            lsp.CompletionItem(
                label="print",
                kind=lsp.CompletionItemKind.Function,
                detail="print(*args, **kwargs)",
                documentation="Print to stdout",
            ),
            lsp.CompletionItem(
                label="len",
                kind=lsp.CompletionItemKind.Function,
                detail="len(obj) -> int",
                documentation="Return the length of an object",
            ),
        ],
    )

@server.feature(lsp.TEXT_DOCUMENT_HOVER)
def hover(params: lsp.HoverParams) -> lsp.Hover | None:
    """Provide hover information."""
    document = server.workspace.get_text_document(params.text_document.uri)
    # Get word at position and return info
    return lsp.Hover(
        contents=lsp.MarkupContent(
            kind=lsp.MarkupKind.Markdown,
            value="**Symbol Info**\n\nDocumentation here",
        )
    )

if __name__ == "__main__":
    server.start_io()

Building an LSP Client

Node.js Client Example

import * as cp from 'child_process';
import * as rpc from 'vscode-jsonrpc/node';

// Spawn the language server
const serverProcess = cp.spawn('node', ['path/to/server.js']);

// Create JSON-RPC connection
const connection = rpc.createMessageConnection(
  new rpc.StreamMessageReader(serverProcess.stdout),
  new rpc.StreamMessageWriter(serverProcess.stdin)
);

// Listen for notifications from server
connection.onNotification('textDocument/publishDiagnostics', (params) => {
  console.log('Diagnostics:', params.diagnostics);
});

// Start connection
connection.listen();

// Send initialize request
const initResult = await connection.sendRequest('initialize', {
  processId: process.pid,
  rootUri: 'file:///path/to/workspace',
  capabilities: {
    textDocument: {
      completion: { completionItem: { snippetSupport: true } },
      hover: { contentFormat: ['markdown'] }
    }
  }
});

console.log('Server capabilities:', initResult.capabilities);

// Send initialized notification
connection.sendNotification('initialized', {});

// Open a document
connection.sendNotification('textDocument/didOpen', {
  textDocument: {
    uri: 'file:///path/to/file.ts',
    languageId: 'typescript',
    version: 1,
    text: 'const x = 1;\nconsole.log(x);'
  }
});

// Request completion
const completions = await connection.sendRequest('textDocument/completion', {
  textDocument: { uri: 'file:///path/to/file.ts' },
  position: { line: 1, character: 8 }
});

console.log('Completions:', completions);

// Shutdown
await connection.sendRequest('shutdown');
connection.sendNotification('exit');

SDK Reference

Official and Popular SDKs

LanguageSDKRepository
TypeScriptvscode-languageservermicrosoft/vscode-languageserver-node
Pythonpyglsopenlawlibrary/pygls
JavaLSP4Jeclipse/lsp4j
Rusttower-lsptower-rs/tower-lsp
C#OmniSharpOmniSharp/csharp-language-server-protocol
Gogo-lspsourcegraph/go-lsp
Haskelllsphaskell/lsp

Popular Language Servers

LanguageServerNotes
TypeScript/JavaScripttypescript-language-serverUses tsserver
Pythonpyright, pylspStatic typing / general
Rustrust-analyzerOfficial Rust analyzer
GogoplsOfficial Go team
C/C++clangdLLVM-based
JavaEclipse JDT LSUsed by VS Code Java
C#OmniSharp.NET ecosystem

Debugging LSP

Enable Tracing

Most clients support logging LSP messages:

VS Code (

settings.json
):

{
  "myExtension.trace.server": "verbose"
}

Neovim (Lua):

vim.lsp.set_log_level("debug")
-- Logs at: ~/.local/state/nvim/lsp.log

Manual Testing with JSON-RPC

# Start server and send messages manually
echo 'Content-Length: 108\r\n\r\n{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"processId":null,"rootUri":null,"capabilities":{}}}' | node server.js

Common Issues

Server doesn't start:

  • Check executable path and permissions
  • Verify runtime (Node.js, Python) is installed
  • Check stderr for error messages

No completions:

  • Verify
    completionProvider
    capability is advertised
  • Check trigger characters match
  • Ensure document is open (
    didOpen
    sent)

Diagnostics not showing:

  • Check
    textDocumentSync
    capability
  • Verify
    publishDiagnostics
    notifications are being sent
  • Check diagnostic severity levels

Best Practices

For Server Developers

  1. Use incremental sync - Full sync is expensive for large files
  2. Debounce validation - Don't validate on every keystroke
  3. Support cancellation - Long operations should check
    $/cancelRequest
  4. Provide resolve - Return minimal completion items, resolve on demand
  5. Include code actions - Quick fixes improve user experience
  6. Report progress - Use
    $/progress
    for long operations

For Client Developers

  1. Cache capabilities - Don't re-check server capabilities repeatedly
  2. Batch requests - Combine related requests when possible
  3. Handle partial results - Some servers support streaming results
  4. Implement timeout - Don't wait forever for responses
  5. Support dynamic registration - Allow servers to register/unregister capabilities

Performance Tips

// Debounce document changes
let validationTimeout: NodeJS.Timeout;
documents.onDidChangeContent(change => {
  clearTimeout(validationTimeout);
  validationTimeout = setTimeout(() => {
    validateDocument(change.document);
  }, 500);
});

// Support cancellation
connection.onDefinition(async (params, token) => {
  // Check cancellation periodically
  if (token.isCancellationRequested) {
    return null;
  }

  const result = await findDefinition(params);

  if (token.isCancellationRequested) {
    return null;
  }

  return result;
});

LSP 3.17 Features

Inlay Hints

Display inline parameter names, type annotations:

{
  "method": "textDocument/inlayHint",
  "params": {
    "textDocument": { "uri": "file:///project/src/main.ts" },
    "range": { "start": { "line": 0, "character": 0 }, "end": { "line": 50, "character": 0 } }
  }
}

// Response
{
  "result": [
    {
      "position": { "line": 10, "character": 15 },
      "label": ": number",
      "kind": 1,
      "paddingLeft": true
    }
  ]
}

Type Hierarchy

Navigate type relationships:

// Prepare
{ "method": "textDocument/prepareTypeHierarchy", "params": { "textDocument": {...}, "position": {...} } }

// Get supertypes
{ "method": "typeHierarchy/supertypes", "params": { "item": {...} } }

// Get subtypes
{ "method": "typeHierarchy/subtypes", "params": { "item": {...} } }

Diagnostic Pull Model

Client-initiated diagnostics (alternative to push):

// Request diagnostics for a document
{ "method": "textDocument/diagnostic", "params": { "textDocument": { "uri": "..." } } }

// Request workspace-wide diagnostics
{ "method": "workspace/diagnostic", "params": { "previousResultIds": [...] } }

Development Tools

LSP DevTools

LSP DevTools is a collection of CLI utilities for inspecting and visualizing language server interactions. Essential for debugging protocol issues.

Installation:

pipx install lsp-devtools

Architecture: The LSP Agent acts as a proxy between client and server, forwarding messages while copying them to a monitoring application.

┌────────────────┐      ┌─────────────┐      ┌─────────────────┐
│ Language Client│◄────►│  LSP Agent  │◄────►│ Language Server │
└────────────────┘      └──────┬──────┘      └─────────────────┘
                               │
                               ▼ TCP (localhost:8765)
                        ┌──────────────┐
                        │ lsp-devtools │
                        │   record /   │
                        │   inspect    │
                        └──────────────┘

Agent Command

Wrap your language server to enable inspection:

# Basic usage - wrap server command
lsp-devtools agent -- <server-cmd>

# Custom host/port
lsp-devtools agent --host 127.0.0.1 --port 1234 -- python -m my_server

# Example: wrap esbonio server
lsp-devtools agent -- esbonio

Neovim Configuration:

-- In nvim-lspconfig setup
require('lspconfig').esbonio.setup {
  cmd = { "lsp-devtools", "agent", "--", "esbonio" }
}

VS Code Configuration:

{
  "myLanguage.server.path": "lsp-devtools",
  "myLanguage.server.args": ["agent", "--", "my-server"]
}

Record Command

Capture LSP sessions to various destinations:

# Record to console (pretty-printed JSON)
lsp-devtools record

# Record to file (JSON-RPC, one message per line)
lsp-devtools record --to-file session.jsonl

# Record to SQLite database (for analysis)
lsp-devtools record --to-sqlite session.db

# Save console output as HTML/SVG
lsp-devtools record --save-output session.html

Filtering Options:

# Filter by source
lsp-devtools record --message-source client
lsp-devtools record --message-source server

# Filter by message type
lsp-devtools record --include-message-type request
lsp-devtools record --include-message-type notification

# Filter by method
lsp-devtools record --include-method textDocument/completion
lsp-devtools record --exclude-method textDocument/didChange

# Custom format
lsp-devtools record -f "{message.method}: {message.params.position.line}"

Inspect Command

Interactive TUI for real-time LSP message inspection:

# Launch inspector
lsp-devtools inspect

# Custom port
lsp-devtools inspect --port 1234

Features:

  • Browse all messages between client and server
  • Hierarchical capability display (30+ capability types)
  • Real-time message flow visualization
  • Detailed message content inspection

pytest-lsp

End-to-end testing framework for language servers, built on pygls.

Installation:

pip install pytest-lsp

Key Features:

  • Run language servers in subprocesses
  • Communicate via stdio (mimics real clients)
  • Language-agnostic (test servers in any language)
  • Async test support

Basic Test Setup

import pytest
import pytest_lsp
from pytest_lsp import ClientServerConfig, LanguageClient
from lsprotocol.types import (
    InitializeParams,
    CompletionParams,
    TextDocumentIdentifier,
    Position,
)

@pytest_lsp.fixture(
    config=ClientServerConfig(
        server_command=["python", "-m", "my_server"],
    ),
)
async def client(lsp_client: LanguageClient):
    # Setup: Initialize the LSP session
    await lsp_client.initialize_session(
        InitializeParams(
            capabilities={},
            root_uri="file:///workspace",
        )
    )

    yield

    # Teardown: Shutdown gracefully
    await lsp_client.shutdown_session()

@pytest.mark.asyncio
async def test_completions(client: LanguageClient):
    """Test that completion returns expected items."""
    result = await client.text_document_completion_async(
        CompletionParams(
            text_document=TextDocumentIdentifier(uri="file:///test.py"),
            position=Position(line=0, character=0),
        )
    )

    labels = [item.label for item in result.items]
    assert "hello" in labels
    assert "world" in labels

Parameterized Client Testing

Test against multiple client configurations:

@pytest.fixture(params=["neovim", "vscode", "emacs"])
def client_name(request):
    return request.param

@pytest_lsp.fixture(
    config=ClientServerConfig(
        server_command=["python", "-m", "my_server"],
    ),
)
async def client(lsp_client: LanguageClient, client_name: str):
    # Get capabilities for specific client
    capabilities = client_capabilities(client_name)

    await lsp_client.initialize_session(
        InitializeParams(
            capabilities=capabilities,
            client_info={"name": client_name},
        )
    )
    yield
    await lsp_client.shutdown_session()

Testing Diagnostics

@pytest.mark.asyncio
async def test_diagnostics(client: LanguageClient):
    """Test diagnostic publishing."""
    # Open a document with errors
    client.text_document_did_open(
        TextDocumentItem(
            uri="file:///test.py",
            language_id="python",
            version=1,
            text="def foo(\n  invalid syntax",
        )
    )

    # Wait for diagnostics
    await client.wait_for_notification("textDocument/publishDiagnostics")

    # Check diagnostics were received
    diagnostics = client.diagnostics["file:///test.py"]
    assert len(diagnostics) > 0
    assert diagnostics[0].severity == DiagnosticSeverity.Error

Common Pitfall

Servers must explicitly start I/O handling:

# In your server's __main__.py
if __name__ == "__main__":
    server = MyLanguageServer()
    server.start_io()  # Don't forget this!

Monaco Editor Integration

When building browser-based LSP clients with Monaco Editor, use

monaco-languageserver-types
for bidirectional type conversion.

Installation:

npm install monaco-languageserver-types

Key Concept: Monaco Editor and LSP use different type representations. This library provides

from*
and
to*
functions for seamless conversion:

  • from*
    - Convert Monaco types → LSP types
  • to*
    - Convert LSP types → Monaco types

Type Conversion Examples

Position & Range:

import { fromRange, toRange, fromPosition, toPosition } from 'monaco-languageserver-types';

// Monaco uses 1-based lines, LSP uses 0-based
const monacoRange = {
  startLineNumber: 1,
  startColumn: 2,
  endLineNumber: 3,
  endColumn: 4
};

// Convert to LSP format
const lspRange = fromRange(monacoRange);
// { start: { line: 0, character: 1 }, end: { line: 2, character: 3 } }

// Convert back to Monaco format
const backToMonaco = toRange(lspRange);
// { startLineNumber: 1, startColumn: 2, endLineNumber: 3, endColumn: 4 }

Diagnostics:

import { fromMarkerData, toMarkerData, fromMarkerSeverity } from 'monaco-languageserver-types';

// Convert Monaco marker to LSP diagnostic
const monacoMarker = {
  severity: monaco.MarkerSeverity.Error,
  message: "Unexpected token",
  startLineNumber: 5,
  startColumn: 10,
  endLineNumber: 5,
  endColumn: 15
};

const lspDiagnostic = fromMarkerData(monacoMarker);
// Ready to send to language server

// Convert LSP diagnostic to Monaco marker
const marker = toMarkerData(lspDiagnostic);
monaco.editor.setModelMarkers(model, 'lsp', [marker]);

Completion Items:

import { fromCompletionItem, toCompletionItem, toCompletionList } from 'monaco-languageserver-types';

// Handle LSP completion response for Monaco
connection.onCompletion(async (params) => {
  const lspCompletions = await languageServer.getCompletions(params);
  return lspCompletions;
});

// In Monaco provider
const monacoProvider: monaco.languages.CompletionItemProvider = {
  provideCompletionItems: async (model, position) => {
    const lspPosition = fromPosition(position);
    const lspResult = await client.sendRequest('textDocument/completion', {
      textDocument: { uri: model.uri.toString() },
      position: lspPosition
    });

    return toCompletionList(lspResult);
  }
};

Hover:

import { fromPosition, toHover } from 'monaco-languageserver-types';

const hoverProvider: monaco.languages.HoverProvider = {
  provideHover: async (model, position) => {
    const lspHover = await client.sendRequest('textDocument/hover', {
      textDocument: { uri: model.uri.toString() },
      position: fromPosition(position)
    });

    return lspHover ? toHover(lspHover) : null;
  }
};

Available Conversions

CategoryFunctions
Structural
fromPosition
,
toPosition
,
fromRange
,
toRange
,
fromLocation
,
toLocation
Diagnostics
fromMarkerData
,
toMarkerData
,
fromMarkerSeverity
,
toMarkerSeverity
Completion
fromCompletionItem
,
toCompletionItem
,
toCompletionList
,
fromCompletionItemKind
Code Actions
fromCodeAction
,
toCodeAction
,
fromCodeActionContext
Navigation
fromDefinition
,
toDefinition
,
fromDocumentHighlight
,
toDocumentHighlight
Symbols
fromDocumentSymbol
,
toDocumentSymbol
,
fromSymbolKind
,
toSymbolKind
Formatting
fromTextEdit
,
toTextEdit
,
fromFormattingOptions
Semantic
fromSemanticTokens
,
toSemanticTokens
,
fromInlayHint
,
toInlayHint
Workspace
fromWorkspaceEdit
,
toWorkspaceEdit

Full Monaco LSP Client Example

import * as monaco from 'monaco-editor';
import {
  fromPosition, fromRange, toCompletionList, toHover,
  toMarkerData, toDocumentHighlight, toCodeAction
} from 'monaco-languageserver-types';
import { createLanguageClient } from './lsp-client';

// Create LSP client connection
const client = createLanguageClient('ws://localhost:3000/lsp');

// Register Monaco providers that bridge to LSP
monaco.languages.registerCompletionItemProvider('typescript', {
  triggerCharacters: ['.', '"', "'", '/'],
  provideCompletionItems: async (model, position) => {
    const result = await client.textDocumentCompletion({
      textDocument: { uri: model.uri.toString() },
      position: fromPosition(position)
    });
    return toCompletionList(result);
  }
});

monaco.languages.registerHoverProvider('typescript', {
  provideHover: async (model, position) => {
    const result = await client.textDocumentHover({
      textDocument: { uri: model.uri.toString() },
      position: fromPosition(position)
    });
    return result ? toHover(result) : null;
  }
});

// Handle diagnostics from server
client.onNotification('textDocument/publishDiagnostics', (params) => {
  const model = monaco.editor.getModel(monaco.Uri.parse(params.uri));
  if (model) {
    const markers = params.diagnostics.map(toMarkerData);
    monaco.editor.setModelMarkers(model, 'lsp', markers);
  }
});

Resources

Official Documentation

Implementations

Development Tools

Tutorials


Version History

  • 1.2.0 (2026-01-11): Added Monaco Editor integration

    • monaco-languageserver-types library documentation
    • Bidirectional type conversion (from*/to* functions)
    • Position, Range, Diagnostic conversions
    • Completion, Hover, Code Action examples
    • Full Monaco LSP client example
    • Available conversions reference table
  • 1.1.0 (2026-01-11): Added Development Tools section

    • LSP DevTools integration (agent, record, inspect commands)
    • pytest-lsp testing framework with examples
    • Architecture diagrams for debugging workflow
    • Parameterized client testing patterns
    • Diagnostic testing examples
  • 1.0.0 (2026-01-10): Initial skill release

    • Complete protocol overview (architecture, lifecycle, capabilities)
    • All language features documented (completion, diagnostics, navigation, etc.)
    • Server development guides (TypeScript, Python)
    • Client development guide
    • SDK reference table
    • LSP 3.17 features (inlay hints, type hierarchy, diagnostic pull)
    • Debugging and best practices