スキル一覧に戻る
enuno

language-server-protocol

by enuno

Claude command & AI agent creation best practices and templates

6🍴 0📅 2026年1月24日
GitHubで見るManusで実行

SKILL.md


name: language-server-protocol version: "1.2.0" description: 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. specification_version: "3.17"

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
RequestYesYestextDocument/completion
ResponseYes (matches request)N/AResult or error
NotificationNoNotextDocument/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

MethodPurposeReturns
textDocument/definitionGo to symbol definitionLocation(s)
textDocument/declarationGo to symbol declarationLocation(s)
textDocument/typeDefinitionGo to type definitionLocation(s)
textDocument/implementationGo to implementationsLocation(s)
textDocument/referencesFind 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

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
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
StructuralfromPosition, toPosition, fromRange, toRange, fromLocation, toLocation
DiagnosticsfromMarkerData, toMarkerData, fromMarkerSeverity, toMarkerSeverity
CompletionfromCompletionItem, toCompletionItem, toCompletionList, fromCompletionItemKind
Code ActionsfromCodeAction, toCodeAction, fromCodeActionContext
NavigationfromDefinition, toDefinition, fromDocumentHighlight, toDocumentHighlight
SymbolsfromDocumentSymbol, toDocumentSymbol, fromSymbolKind, toSymbolKind
FormattingfromTextEdit, toTextEdit, fromFormattingOptions
SemanticfromSemanticTokens, toSemanticTokens, fromInlayHint, toInlayHint
WorkspacefromWorkspaceEdit, 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

スコア

総合スコア

50/100

リポジトリの品質指標に基づく評価

SKILL.md

SKILL.mdファイルが含まれている

+20
LICENSE

ライセンスが設定されている

0/10
説明文

100文字以上の説明がある

0/10
人気

GitHub Stars 100以上

0/15
最近の活動

3ヶ月以内に更新がある

0/10
フォーク

10回以上フォークされている

0/5
Issue管理

オープンIssueが50未満

+5
言語

プログラミング言語が設定されている

+5
タグ

1つ以上のタグが設定されている

0/5

レビュー

💬

レビュー機能は近日公開予定です