agentsclimarketplace

Lsp builder

Skill woyxiang/skills/skills/lsp-builder

Agent Skills

Install
npx -y skills add woyxiang/skills --skill lsp-builder

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

One thing to look at

  • 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

What its author says it does

Copied from the file, not written here

Guides building Language Server Protocol (LSP) servers from scratch based on the LSP 3.17 specification. Use this skill when the user wants to create, debug, or extend an LSP server in any language. Trigger on mentions of "language server", "LSP", "language server protocol", "textDocument", "completion provider", "hover provider", "diagnostics", or any LSP method name. This skill covers the full server lifecycle: base protocol transport, capabilities negotiation, text document synchronization, language features, workspace features, and window features.

SKILL.md

18.3 KB, as published. Nobody here has run it

LSP Builder Skill

A step-by-step guide for building Language Server Protocol servers. Reference: LSP 3.17 specification.

When to Use This Skill

Use whenever a user asks to:

  • Build a new language server or LSP implementation
  • Add language features (hover, completion, go-to-definition, etc.) to an editor via LSP
  • Debug or extend an existing LSP server
  • Understand how LSP transport, capabilities, or features work

Architecture Overview

An LSP server is a process that communicates with a client (editor) using JSON-RPC messages over a transport layer. The protocol has three layers:

  1. Transport — How bytes move between client and server (stdio, sockets, pipes)
  2. Base Protocol — JSON-RPC message framing with Content-Length headers
  3. Language Protocol — Semantic requests/responses (hover, completion, diagnostics, etc.)

Phase 1: Base Protocol Transport

Read references/specification.md sections on Base Protocol and Header Part.

The wire format is:

Content-Length: <byte-length-of-json>\r\n
\r\n
<json-rpc-message>

Requirements:

  • Header is ASCII-encoded, terminated by \r\n
  • Content is UTF-8 JSON-RPC 2.0
  • Content-Length is mandatory; Content-Type defaults to application/vscode-jsonrpc; charset=utf-8

Implementation steps:

  1. Open stdin/stdout (or socket) for bidirectional communication
  2. Read headers until \r\n\r\n is found
  3. Parse Content-Length value
  4. Read exactly that many bytes as the JSON content
  5. Parse JSON to determine message type (request, response, or notification)
  6. Repeat

Message types:

TypeHas id?Has method?Requires response?
Requestyesyesyes
Responseyesnono
Notificationnoyesno

Reference: Message Transport (Python)

import json
import sys

def read_message():
    """Read one JSON-RPC message from stdin using Content-Length framing."""
    headers = {}
    while True:
        line = sys.stdin.buffer.readline()
        if not line:
            return None  # EOF
        line = line.decode("ascii")
        if line == "\r\n":
            break
        name, value = line.strip().split(": ", 1)
        headers[name.lower()] = value
    content_length = int(headers["content-length"])
    body = sys.stdin.buffer.read(content_length)
    return json.loads(body.decode("utf-8"))

def write_message(msg):
    """Write one JSON-RPC message to stdout using Content-Length framing."""
    body = json.dumps(msg, separators=(",", ":")).encode("utf-8")
    header = f"Content-Length: {len(body)}\r\n\r\n"
    sys.stdout.buffer.write(header.encode("ascii"))
    sys.stdout.buffer.write(body)
    sys.stdout.buffer.flush()

Phase 2: Server Lifecycle

Read references/specification.md sections on Server Lifecycle.

The lifecycle sequence is strictly defined:

Client                          Server
  |                                |
  |--- initialize (request) ----->|
  |<-- initialize (response) -----|
  |--- initialized (notify) ----->|
  |                                |  (server is now ready)
  |                                |
  |  ... normal operation ...      |
  |                                |
  |--- shutdown (request) ------->|
  |<-- shutdown (response) -------|
  |--- exit (notify) ------------>|
  |                                |  (server exits)

initialize Request

Method: initialize

  • Client sends capabilities and workspace info
  • Server responds with its capabilities
  • Must not send other requests/notifications before receiving initialize

InitializeParams (from client):

{
  "processId": 12345,
  "rootUri": "file:///path/to/workspace",
  "capabilities": { ... },
  "clientInfo": { "name": "VSCode", "version": "1.74.0" }
}

InitializeResult (server response):

{
  "capabilities": {
    "textDocumentSync": 1,
    "completionProvider": { "triggerCharacters": [".", ":"] },
    "hoverProvider": true,
    "definitionProvider": true,
    "referencesProvider": true,
    "documentSymbolProvider": true,
    "codeActionProvider": true,
    "documentFormattingProvider": true
  },
  "serverInfo": { "name": "my-lang-server", "version": "0.1.0" }
}

initialized Notification

Method: initialized

  • Server can now register capabilities, send window/showMessage, etc.

shutdown and exit

  • shutdown: Server returns null result. Must stop processing new requests.
  • exit: Server exits with code 0 (success) or 1 (if shutdown wasn't received).

Reference: Lifecycle Dispatch (Python)

def main():
    shutting_down = False
    while True:
        msg = read_message()
        if msg is None:
            break
        method = msg.get("method")
        msg_id = msg.get("id")    # Notifications have NO id
        params = msg.get("params", {})

        if msg_id is None:
            # This is a NOTIFICATION (no response required)
            if method == "exit":
                return 0 if shutting_down else 1
            # Other notifications: "initialized", "textDocument/didOpen", etc.
            handle_notification(method, params)
            continue

        # This is a REQUEST (must send a response)
        if method == "shutdown":
            shutting_down = True
            write_message({"jsonrpc": "2.0", "id": msg_id, "result": None})
            continue
        result = handle_request(method, params)
        write_message({"jsonrpc": "2.0", "id": msg_id, "result": result})

Phase 3: Text Document Synchronization

Read references/specification.md sections on Text Document Synchronization.

The server MUST implement all three or none:

  • textDocument/didOpenNotification from client: document opened
  • textDocument/didChangeNotification from client: content changed
  • textDocument/didCloseNotification from client: document closed

These are notifications, not requests — they have no id. The server must NOT send responses to them.

Configure via textDocumentSync capability:

{
  "textDocumentSync": {
    "openClose": true,
    "change": 2
  }
}

change values:

  • 0 (None): No sync
  • 1 (Full): Send entire document content on each change
  • 2 (Incremental): Send only changed ranges

Core Types

interface Position {
    line: uinteger;        // 0-indexed
    character: uinteger;   // 0-indexed UTF-16 offset by default
}

interface Range {
    start: Position;
    end: Position;
}

interface TextDocumentIdentifier {
    uri: DocumentUri;      // e.g. "file:///path/to/file.py"
}

interface TextDocumentItem {
    uri: DocumentUri;
    languageId: string;
    version: integer;
    text: string;
}

interface VersionedTextDocumentIdentifier extends TextDocumentIdentifier {
    version: integer | null;
}

Full Sync (change: 1)

The simplest approach. On each didChange, the client sends the entire document text:

{
  "textDocument": { "uri": "file:///foo.py", "version": 2 },
  "contentChanges": [{ "text": "entire document content here" }]
}

Server just stores contentChanges[-1]["text"].

Incremental Sync (change: 2)

The client sends only the changed range. The server must apply these edits to its in-memory copy:

{
  "textDocument": { "uri": "file:///foo.py", "version": 2 },
  "contentChanges": [{
    "range": {
      "start": { "line": 0, "character": 5 },
      "end": { "line": 0, "character": 10 }
    },
    "rangeLength": 5,
    "text": "world"
  }]
}

Reference: Incremental Sync Implementation (Python)

class DocumentStore:
    def __init__(self):
        self._docs = {}  # uri -> text

    def open(self, uri, text):
        self._docs[uri] = text

    def close(self, uri):
        self._docs.pop(uri, None)

    def apply_incremental(self, uri, changes):
        text = self._docs.get(uri, "")
        for change in changes:
            if "range" in change:
                text = self._apply_range(text, change)
            else:
                # Full content replacement
                text = change.get("text", text)
        self._docs[uri] = text

    @staticmethod
    def _apply_range(text, change):
        lines = text.split("\n")
        rng = change["range"]
        s_line = rng["start"]["line"]
        s_char = rng["start"]["character"]
        e_line = rng["end"]["line"]
        e_char = rng["end"]["character"]
        new_text = change.get("text", "")

        before = "\n".join(lines[:s_line])
        if s_line > 0:
            before += "\n"
        prefix = lines[s_line][:s_char] if s_line < len(lines) else ""
        suffix = lines[e_line][e_char:] if e_line < len(lines) else ""
        after = "\n".join(lines[e_line + 1:]) if e_line + 1 < len(lines) else ""

        result = before + prefix + new_text + suffix + after
        return result

    def get(self, uri):
        return self._docs.get(uri)

Position encoding: Lines are 0-indexed. Characters are 0-indexed UTF-16 offsets by default. Since 3.17, you can negotiate UTF-8 or UTF-32 via positionEncoding in capabilities.

Phase 4: Language Features

Each feature requires:

  1. Server advertises the capability in initialize response
  2. Server implements the handler for the corresponding method

Hover (textDocument/hover)

Return documentation for the symbol at a given position.

Capability: "hoverProvider": true Request: HoverParams with textDocument and position Response: Hover with contents (MarkupContent or MarkedString) and optional range

interface Hover {
    contents: MarkedString | MarkedString[] | MarkupContent;
    range?: Range;
}

interface HoverParams extends TextDocumentPositionParams {
    // textDocument: TextDocumentIdentifier
    // position: Position
}

Completion (textDocument/completion)

Provide code completions at a cursor position.

Capability:

"completionProvider": {
  "triggerCharacters": ["."],
  "resolveProvider": true,
  "completionItem": { "labelDetailsSupport": true }
}

Request: CompletionParams with textDocument, position, context Response: CompletionList with items[] and isIncomplete flag, or CompletionItem[]

Important: Always include triggerCharacters when advertising completionProvider — clients use this to know when to automatically request completions.

interface CompletionItem {
    label: string;
    kind?: CompletionItemKind;  // 1=Text, 2=Method, ..., 14=Keyword, 6=Variable, ...
    detail?: string;
    documentation?: string | MarkupContent;
    deprecated?: boolean;
    preselect?: boolean;
    sortText?: string;
    filterText?: string;
    insertText?: string;
    insertTextFormat?: InsertTextFormat;  // 1=PlainText, 2=Snippet
    textEdit?: TextEdit | InsertReplaceEdit;
    additionalTextEdits?: TextEdit[];
    commitCharacters?: string[];
    command?: Command;
    data?: LSPAny;
}

CompletionItemKind values (common):

ValueKindValueKind
1Text7Interface
2Method8Function
3Function10Module
5Class13Value
6Variable14Keyword
15Snippet

Go to Definition (textDocument/definition)

Capability: "definitionProvider": true Request: DefinitionParams Response: Location, Location[], or LocationLink[]

interface Location {
    uri: DocumentUri;
    range: Range;
}

References (textDocument/references)

Capability: "referencesProvider": true Request: ReferenceParams with context.includeDeclaration Response: Location[]

Diagnostics (Push Model)

The server sends diagnostics to the client via textDocument/publishDiagnostics notification. No request is needed — the server pushes whenever diagnostics change.

Notification: PublishDiagnosticsParams with uri, version, and diagnostics[]

interface Diagnostic {
    range: Range;
    severity?: DiagnosticSeverity;  // 1=Error, 2=Warning, 3=Information, 4=Hint
    code?: integer | string;
    codeDescription?: { href: URI };
    source?: string;          // e.g. "my-lang-linter"
    message: string;
    tags?: DiagnosticTag[];   // 1=Unnecessary, 2=Deprecated
    relatedInformation?: DiagnosticRelatedInformation[];
    data?: LSPAny;
}

Document Symbols (textDocument/documentSymbol)

Capability: "documentSymbolProvider": true Response: DocumentSymbol[] (hierarchical) or SymbolInformation[] (flat)

interface DocumentSymbol {
    name: string;
    detail?: string;
    kind: SymbolKind;      // 5=Class, 12=Function, 13=Variable, 14=Constant, ...
    tags?: SymbolTag[];
    deprecated?: boolean;
    range: Range;          // Full range of this symbol (including body)
    selectionRange: Range; // Range that should be selected (the name)
    children?: DocumentSymbol[];
}

SymbolKind values (common):

ValueKindValueKind
5Class12Function
6Method13Variable
7Property14Constant
8Field23Struct
10Enum26TypeParameter
11Interface

Formatting (textDocument/formatting)

Capability: "documentFormattingProvider": true Request: DocumentFormattingParams with options (tabSize, insertSpaces, etc.) Response: TextEdit[]

interface TextEdit {
    range: Range;
    newText: string;
}

Code Actions (textDocument/codeAction)

Capability:

"codeActionProvider": {
  "codeActionKinds": ["quickfix", "refactor"]
}

Request: CodeActionParams with textDocument, range, context.diagnostics Response: (Command | CodeAction)[]

Other Features (implement as needed)

FeatureMethodCapability Key
Signature HelptextDocument/signatureHelpsignatureHelpProvider
RenametextDocument/renamerenameProvider
Code LenstextDocument/codeLenscodeLensProvider (has resolve)
Document HighlightstextDocument/documentHighlightdocumentHighlightProvider
Document LinkstextDocument/documentLinkdocumentLinkProvider (has resolve)
Folding RangestextDocument/foldingRangefoldingRangeProvider
Selection RangestextDocument/selectionRangeselectionRangeProvider
Semantic TokenstextDocument/semanticTokens/*semanticTokensProvider
Inlay HintstextDocument/inlayHintinlayHintProvider (has resolve)
Type HierarchytextDocument/typeHierarchy/*typeHierarchyProvider
Call HierarchytextDocument/callHierarchy/*callHierarchyProvider

Phase 5: Workspace Features

FeatureMethodCapability Key
Symbol Searchworkspace/symbolworkspaceSymbolProvider
Execute Commandworkspace/executeCommandexecuteCommandProvider
Configurationworkspace/configurationN/A (server requests config)
Did Change Configworkspace/didChangeConfigurationtextDocumentSync options
File Operationsworkspace/didCreateFiles etc.workspace.fileOperations

Phase 6: Window Features (Server → Client)

These are notifications the server sends TO the client:

FeatureMethodDirection
Show Messagewindow/showMessageServer → Client
Log Messagewindow/logMessageServer → Client
Progress$/progressBidirectional

Error Handling

Use ResponseError with standard error codes:

-32700  Parse Error
-32600  Invalid Request
-32601  Method Not Found
-32602  Invalid Params
-32603  Internal Error
-32002  Server Not Initialized
-32800  Request Cancelled
-32801  Content Modified
-32802  Server Cancelled
-32803  Request Failed

Implementation Checklist

When building a server, work through this order:

  1. Set up transport (stdio, socket, or pipe)
  2. Implement header parsing (Content-Length based message framing)
  3. Implement JSON-RPC message dispatch (request/notification/response routing)
  4. Implement initialize handler with capabilities
  5. Implement initialized handler
  6. Implement shutdown and exit handlers
  7. Implement text document sync (didOpen, didChange, didClose)
  8. Implement at least one language feature (hover is simplest)
  9. Implement diagnostics (push via textDocument/publishDiagnostics)
  10. Add more language features as needed
  11. Implement workspace features if applicable
  12. Test with an LSP client (VS Code extension, Emacs lsp-mode, Neovim LSP)

Key References

Read references/specification.md for:

  • Complete type definitions for every request/response/notification
  • Detailed capability structures
  • The full list of features with their client/server capability paths
  • Registration options for dynamic capabilities
  • Work done progress and partial result progress mechanisms
  • Notebook document support (3.17)

Common Pitfalls

  • Don't send requests before initialize — Clients will reject them
  • Don't notify without being asked — Only send publishDiagnostics after document sync
  • Positions are 0-indexed — Both line and character start at 0
  • UTF-16 character offsets by default — Unless you negotiate UTF-8/UTF-32 via positionEncoding
  • Every request needs a response — Even if returning null
  • Notifications must NOT get responses — They have no id field. didOpen, didChange, didClose, initialized, exit are all notifications
  • Cancellation returns error, not no response — Use RequestCancelled error code
  • Always use Content-Length framing — Never read raw lines from stdin; use binary readline + exact byte reads
  • Incremental sync needs careful position math — Convert 0-indexed line/character to string offsets before splicing

Keep looking

Skills are one crate of 328,083. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.