A Go library for building Language Server Protocol servers. It handles JSON-RPC framing, message dispatch, and LSP type definitions so you can focus on your language logic.
This library targets LSP 3.17. The table below shows which parts of the specification are currently supported.
| Category | Feature | Supported |
|---|---|---|
| Lifecycle | initialize / shutdown / exit | Yes |
| $/cancelRequest | Yes | |
| $/setTrace | Yes | |
| Text Document Sync | didOpen / didChange / didClose | Yes |
| didSave | Yes | |
| willSave / willSaveWaitUntil | Yes | |
| Language Features | completion | Yes |
| completionItem/resolve | Yes | |
| hover | Yes | |
| signatureHelp | Yes | |
| declaration | Yes | |
| definition | Yes | |
| typeDefinition | Yes | |
| implementation | Yes | |
| references | Yes | |
| documentHighlight | Yes | |
| documentSymbol | Yes | |
| codeAction | Yes | |
| codeAction/resolve | Yes | |
| codeLens | Yes | |
| codeLens/resolve | Yes | |
| documentLink | Yes | |
| documentLink/resolve | Yes | |
| documentColor / colorPresentation | Yes | |
| formatting | Yes | |
| rangeFormatting | Yes | |
| onTypeFormatting | Yes | |
| rename | Yes | |
| prepareRename | Yes | |
| foldingRange | Yes | |
| selectionRange | Yes | |
| callHierarchy | Yes | |
| semanticTokens (full / delta / range) | Yes | |
| linkedEditingRange | Yes | |
| moniker | Yes | |
| Workspace | workspaceSymbol | Yes |
| executeCommand | Yes | |
| didChangeWorkspaceFolders | Yes | |
| didChangeConfiguration | Yes | |
| didChangeWatchedFiles | Yes | |
| workspace/willCreateFiles | Yes | |
| workspace/willRenameFiles | Yes | |
| workspace/willDeleteFiles | Yes | |
| Window | showMessage (server-to-client) | Yes |
| showMessageRequest | Yes | |
| logMessage | Yes | |
| progress | Yes | |
| showDocument | Yes | |
| Diagnostics | publishDiagnostics (server-to-client) | Yes |
| LSP 3.17 | ||
| Language Features | typeHierarchy (prepare / supertypes / subtypes) | Yes |
| inlayHint | Yes | |
| inlayHint/resolve | Yes | |
| inlineValue | Yes | |
| textDocument/diagnostic (pull) | Yes | |
| Workspace | workspace/diagnostic | Yes |
| workspace/codeLens/refresh | Yes | |
| workspace/semanticTokens/refresh | Yes | |
| workspace/inlayHint/refresh | Yes | |
| workspace/inlineValue/refresh | Yes | |
| workspace/diagnostic/refresh | Yes |
go get github.com/owenrumney/go-lsp
Define a handler struct that implements server.LifecycleHandler and any optional interfaces you need, then pass it to NewServer:
package main
import (
"context"
"os"
"github.com/owenrumney/go-lsp/lsp"
"github.com/owenrumney/go-lsp/server"
)
type Handler struct{}
func (h *Handler) Initialize(ctx context.Context, params *lsp.InitializeParams) (*lsp.InitializeResult, error) {
return &lsp.InitializeResult{
Capabilities: lsp.ServerCapabilities{
HoverProvider: &lsp.HoverOptions{},
},
}, nil
}
func (h *Handler) Shutdown(ctx context.Context) error {
return nil
}
func (h *Handler) Hover(ctx context.Context, params *lsp.HoverParams) (*lsp.Hover, error) {
return &lsp.Hover{
Contents: lsp.MarkupContent{
Kind: lsp.MarkupKindMarkdown,
Value: "Hello from the server",
},
}, nil
}
func main() {
srv := server.NewServer(&Handler{})
if err := srv.Run(context.Background(), server.RunStdio()); err != nil {
os.Exit(1)
}
}The server accepts any io.ReadWriteCloser, so you can use it with stdio, TCP, WebSockets, or anything else:
// Stdio (most common for editors)
srv.Run(ctx, stdRWC{})
// TCP
ln, _ := net.Listen("tcp", ":9999")
conn, _ := ln.Accept()
srv.Run(ctx, conn) // net.Conn implements io.ReadWriteCloser
// WebSocket (using nhooyr.io/websocket)
ws, _ := websocket.Accept(w, r, nil)
srv.Run(ctx, websocket.NetConn(ctx, ws, websocket.MessageText))The server automatically advertises capabilities based on which interfaces your handler implements. If your handler satisfies HoverHandler, the server tells the client it supports hover -- you don't need to wire that up yourself. You can still set capabilities explicitly in Initialize if you need finer control; explicit values take precedence.
LifecycleHandler is the only required interface. Everything else is opt-in: implement the interface on your handler struct and the server registers the corresponding LSP method automatically.
| Interface | Methods |
|---|---|
LifecycleHandler |
Initialize, Shutdown |
SetTraceHandler |
SetTrace |
| Interface | Methods |
|---|---|
TextDocumentSyncHandler |
DidOpen, DidChange, DidClose |
TextDocumentSaveHandler |
DidSave |
TextDocumentWillSaveHandler |
WillSave |
TextDocumentWillSaveWaitUntilHandler |
WillSaveWaitUntil |
| Interface | Methods |
|---|---|
CompletionHandler |
Completion |
CompletionResolveHandler |
ResolveCompletionItem |
HoverHandler |
Hover |
SignatureHelpHandler |
SignatureHelp |
DeclarationHandler |
Declaration |
DefinitionHandler |
Definition |
TypeDefinitionHandler |
TypeDefinition |
ImplementationHandler |
Implementation |
ReferencesHandler |
References |
DocumentHighlightHandler |
DocumentHighlight |
DocumentSymbolHandler |
DocumentSymbol |
CodeActionHandler |
CodeAction |
CodeActionResolveHandler |
ResolveCodeAction |
CodeLensHandler |
CodeLens |
CodeLensResolveHandler |
ResolveCodeLens |
DocumentLinkHandler |
DocumentLink |
DocumentLinkResolveHandler |
ResolveDocumentLink |
DocumentColorHandler |
DocumentColor |
ColorPresentationHandler |
ColorPresentation |
DocumentFormattingHandler |
Formatting |
DocumentRangeFormattingHandler |
RangeFormatting |
DocumentOnTypeFormattingHandler |
OnTypeFormatting |
RenameHandler |
Rename |
PrepareRenameHandler |
PrepareRename |
FoldingRangeHandler |
FoldingRange |
SelectionRangeHandler |
SelectionRange |
CallHierarchyHandler |
PrepareCallHierarchy, IncomingCalls, OutgoingCalls |
SemanticTokensFullHandler |
SemanticTokensFull |
SemanticTokensDeltaHandler |
SemanticTokensDelta |
SemanticTokensRangeHandler |
SemanticTokensRange |
LinkedEditingRangeHandler |
LinkedEditingRange |
MonikerHandler |
Moniker |
TypeHierarchyHandler |
PrepareTypeHierarchy, Supertypes, Subtypes |
InlayHintHandler |
InlayHint |
InlayHintResolveHandler |
ResolveInlayHint |
InlineValueHandler |
InlineValue |
DocumentDiagnosticHandler |
DocumentDiagnostic |
| Interface | Methods |
|---|---|
WorkspaceSymbolHandler |
WorkspaceSymbol |
ExecuteCommandHandler |
ExecuteCommand |
WorkspaceFoldersHandler |
DidChangeWorkspaceFolders |
DidChangeConfigurationHandler |
DidChangeConfiguration |
DidChangeWatchedFilesHandler |
DidChangeWatchedFiles |
WillCreateFilesHandler |
WillCreateFiles |
WillRenameFilesHandler |
WillRenameFiles |
WillDeleteFilesHandler |
WillDeleteFiles |
WorkspaceDiagnosticHandler |
WorkspaceDiagnostic |
After the server starts, srv.Client is available for sending notifications and requests back to the editor:
// Publish diagnostics for a file
srv.Client.PublishDiagnostics(ctx, &lsp.PublishDiagnosticsParams{
URI: "file:///path/to/file.go",
Diagnostics: diagnostics,
})
// Show a message popup in the editor
srv.Client.ShowMessage(ctx, &lsp.ShowMessageParams{
Type: lsp.MessageTypeInfo,
Message: "Indexing complete",
})
// Write to the editor's log output
srv.Client.LogMessage(ctx, &lsp.LogMessageParams{
Type: lsp.MessageTypeLog,
Message: "processed 42 files",
})
// Report progress
srv.Client.Progress(ctx, &lsp.ProgressParams{
Token: "indexing",
Value: progressValue,
})
// Show a message with action buttons (request/response)
item, err := srv.Client.ShowMessageRequest(ctx, &lsp.ShowMessageRequestParams{
Type: lsp.MessageTypeInfo,
Message: "Restart server?",
Actions: []lsp.MessageActionItem{{Title: "Yes"}, {Title: "No"}},
})
// Ask the editor to show a document
result, err := srv.Client.ShowDocument(ctx, &lsp.ShowDocumentParams{
URI: "file:///path/to/file.go",
})You can register custom JSON-RPC methods and notifications for server-specific extensions:
srv := server.NewServer(&handler{})
// Custom request method
srv.HandleMethod("custom/myMethod", func(ctx context.Context, params json.RawMessage) (any, error) {
var p MyParams
if err := json.Unmarshal(params, &p); err != nil {
return nil, err
}
return MyResult{Value: "hello"}, nil
})
// Custom notification
srv.HandleNotification("custom/myNotification", func(ctx context.Context, params json.RawMessage) error {
// handle notification
return nil
})
srv.Run(ctx, rwc)Custom handlers must be registered before calling Run.
The library includes an optional debug UI that captures all LSP traffic and displays it in a web interface. This is useful for inspecting the messages flowing between client and server during development.
Enable it with the WithDebugUI option:
srv := server.NewServer(&Handler{}, server.WithDebugUI(":7100"))
srv.Run(ctx, server.RunStdio())Then open http://localhost:7100 in a browser. The tap is transparent -- it intercepts LSP frames for display without modifying them.
Real-time view of all JSON-RPC traffic between client and server:
- Request/response pairing -- responses are matched to their originating request with latency displayed inline
- Direction and type filters -- narrow the list to client-to-server, server-to-client, requests, or notifications
- Full-text search across methods and JSON bodies (including paired responses)
- Pretty-printed detail pane -- click any message to see the full JSON with syntax formatting
Aggregated log output with level filtering (error, warning, info, log). window/logMessage notifications are automatically cross-posted here. Supports search and CSV export.
Waterfall view of request latency, grouped by method:
- Each method gets a single row with all its request bars laid out on a time track
- Bars are color-coded: green (<100ms), orange (100ms--1s), red (>1s), animated blue stripes (pending)
- Minimap header with time axis ticks -- click or drag to scroll to a point in time
- Zoom in/out to adjust the time scale
- Click any bar to jump to that message in the Messages tab
Runtime metrics updated every 2 seconds: uptime, heap usage, goroutine count, GC runs, request/response/notification counts, and average latency.
Method sparklines appear below the stats bar once latency data is available, showing an inline SVG chart of the last 50 latency samples per method (top 10 by volume).
- Capabilities panel -- slide-out panel showing which LSP capabilities the server advertised during initialization
- Dark / light theme -- toggle between Catppuccin Mocha (dark) and Catppuccin Latte (light) themes, persisted in
localStorage
The library is split into three packages:
jsonrpc-- JSON-RPC 2.0 framing over anio.ReadWriteCloser, with method and notification dispatch.lsp-- Go types for LSP structures (capabilities, params, results, enums). No logic, just data definitions.server-- TheServerthat ties it together: accepts a handler, registers LSP methods based on the interfaces it implements, and manages the connection lifecycle.

