go-lsp-bridge

A pure-Go WebSocket↔stdio bridge for JSON-RPC LSP traffic — drop a real language server behind a web editor, no cgo.

pure Go · zero cgo WebSocket ↔ stdio Content-Length framing CodeMirror-compatible per-connection subprocess concurrency caps LSPBRIDGE_* overrides 100% coverage 6 arches
Documentation GitHub
Documentation (MkDocs Material + mike) License: BSD-3-Clause Go 1.26.4+ Coverage 100%

go-lsp-bridge relays JSON-RPC Language Server Protocol traffic between a WebSocket and a language-server subprocess's stdio. It owns the Content-Length framing on the stdio side so each WS text frame carries one bare JSON object — exactly what browser LSP clients like codemirror-languageserver and @open-rpc/codemirror-lsp expect. A web editor gets real completions, hovers, diagnostics and go-to-definition from an actual server (texlab, gopls, pyright, typescript-language-server, rust-analyzer, …) behind a single HTTP handler — pure Go, no cgo, with per-connection subprocesses and both global and per-user concurrency caps.

server → client : one JSON-RPC object per WS text frame
client → server : one JSON-RPC object per WS text frame

Transport framing bridge ready

Relays JSON-RPC untouched between a WebSocket and a language server’s stdio, adding + stripping the Content-Length framing so each WS text frame carries one bare JSON object — exactly what codemirror-languageserver / @open-rpc/codemirror-lsp expect.

Per-connection subprocesses ready

Every WebSocket spawns its own language server (exec.CommandContext) and kills it on close. A missing binary or a failed spawn is reported back as a JSON-RPC error frame, so the editor surfaces one visible failure instead of silently dropping completions.

Configurable server registry ready

DefaultServers() ships launchers for latex / go / python / typescript / javascript / rust, each overridable per-deployment via a neutral LSPBRIDGE_* env var. The registry is a plain map — bring your own, or extend the defaults in place; adding a language is one line.

Concurrency caps ready

Two layers keep the host safe: a process-wide ceiling so it can’t be saturated, and a per-subject ceiling (WithSubject) so one runaway editor with a reconnect loop can’t starve everyone else. The per-user counter keys by authenticated identity, not raw token.

Coverage & six arches ready

A hermetic WS → subprocess → WS round-trip against a bundled fake-lsp stub, plus unit tests for every framing / registry / concurrency / error branch — 100% coverage, gofmt + go vet clean, green across all six 64-bit Go arches (exec/WS tests gated off the qemu lanes).

Server pooling & metrics planned

Future work: optional warm-subprocess pooling to amortise language-server startup, and Prometheus-style counters for spawns, rejections and frame throughput. Not required for the core bridge, which stays dependency-light.

A bridge, not a client: it transports JSON-RPC untouched, translating only the transport framing (WebSocket text frames ↔ stdio Content-Length). The launcher registry is a plain map with neutral LSPBRIDGE_* environment overrides — no host-application naming baked in — so it is reusable by any Go program. Validated by a hermetic WS→subprocess→WS round-trip against a bundled stub, at 100% coverage across github.com/go-lsp-bridge.