lsp-builder — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited lsp-builder (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
A step-by-step guide for building Language Server Protocol servers. Reference: LSP 3.17 specification.
Use whenever a user asks to:
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:
Content-Length headersRead 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:
\r\nContent-Length is mandatory; Content-Type defaults to application/vscode-jsonrpc; charset=utf-8Implementation steps:
\r\n\r\n is foundContent-Length valueMessage types:
| Type | Has id? | Has method? | Requires response? |
|---|---|---|---|
| Request | yes | yes | yes |
| Response | yes | no | no |
| Notification | no | yes | no |
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()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)Method: initialize
initializeInitializeParams (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" }
}Method: initialized
window/showMessage, etc.shutdown: Server returns null result. Must stop processing new requests.exit: Server exits with code 0 (success) or 1 (if shutdown wasn't received).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})Read references/specification.md sections on Text Document Synchronization.
The server MUST implement all three or none:
textDocument/didOpen — Notification from client: document openedtextDocument/didChange — Notification from client: content changedtextDocument/didClose — Notification from client: document closedThese 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 sync1 (Full): Send entire document content on each change2 (Incremental): Send only changed rangesinterface 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;
}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"].
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"
}]
}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.
Each feature requires:
initialize responsetextDocument/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
}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):
| Value | Kind | Value | Kind |
|---|---|---|---|
| 1 | Text | 7 | Interface |
| 2 | Method | 8 | Function |
| 3 | Function | 10 | Module |
| 5 | Class | 13 | Value |
| 6 | Variable | 14 | Keyword |
| 15 | Snippet |
textDocument/definition)Capability: "definitionProvider": true Request: DefinitionParams Response: Location, Location[], or LocationLink[]
interface Location {
uri: DocumentUri;
range: Range;
}textDocument/references)Capability: "referencesProvider": true Request: ReferenceParams with context.includeDeclaration Response: Location[]
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;
}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):
| Value | Kind | Value | Kind |
|---|---|---|---|
| 5 | Class | 12 | Function |
| 6 | Method | 13 | Variable |
| 7 | Property | 14 | Constant |
| 8 | Field | 23 | Struct |
| 10 | Enum | 26 | TypeParameter |
| 11 | Interface |
textDocument/formatting)Capability: "documentFormattingProvider": true Request: DocumentFormattingParams with options (tabSize, insertSpaces, etc.) Response: TextEdit[]
interface TextEdit {
range: Range;
newText: string;
}textDocument/codeAction)Capability:
"codeActionProvider": {
"codeActionKinds": ["quickfix", "refactor"]
}Request: CodeActionParams with textDocument, range, context.diagnostics Response: (Command | CodeAction)[]
| Feature | Method | Capability Key |
|---|---|---|
| Signature Help | textDocument/signatureHelp | signatureHelpProvider |
| Rename | textDocument/rename | renameProvider |
| Code Lens | textDocument/codeLens | codeLensProvider (has resolve) |
| Document Highlights | textDocument/documentHighlight | documentHighlightProvider |
| Document Links | textDocument/documentLink | documentLinkProvider (has resolve) |
| Folding Ranges | textDocument/foldingRange | foldingRangeProvider |
| Selection Ranges | textDocument/selectionRange | selectionRangeProvider |
| Semantic Tokens | textDocument/semanticTokens/* | semanticTokensProvider |
| Inlay Hints | textDocument/inlayHint | inlayHintProvider (has resolve) |
| Type Hierarchy | textDocument/typeHierarchy/* | typeHierarchyProvider |
| Call Hierarchy | textDocument/callHierarchy/* | callHierarchyProvider |
| Feature | Method | Capability Key |
|---|---|---|
| Symbol Search | workspace/symbol | workspaceSymbolProvider |
| Execute Command | workspace/executeCommand | executeCommandProvider |
| Configuration | workspace/configuration | N/A (server requests config) |
| Did Change Config | workspace/didChangeConfiguration | textDocumentSync options |
| File Operations | workspace/didCreateFiles etc. | workspace.fileOperations |
These are notifications the server sends TO the client:
| Feature | Method | Direction |
|---|---|---|
| Show Message | window/showMessage | Server → Client |
| Log Message | window/logMessage | Server → Client |
| Progress | $/progress | Bidirectional |
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 FailedWhen building a server, work through this order:
Content-Length based message framing)initialize handler with capabilitiesinitialized handlershutdown and exit handlersdidOpen, didChange, didClose)textDocument/publishDiagnostics)Read references/specification.md for:
publishDiagnostics after document syncpositionEncodingnullid field. didOpen, didChange, didClose, initialized, exit are all notificationsRequestCancelled error code~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.