Skip to content

masterkeysrd/lspx

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

32 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

lspx

One LSP client. Any number of language servers.

lspx is a Go library and CLI tool that acts as an LSP client multiplexer. Instead of writing one client per language server, you connect a single lspx client to as many language servers as you need. lspx transparently fans out requests, aggregates responses, and routes notifications — your code (or your editor) sees exactly one unified LSP interface.

                         ┌─────────────────────────────────────────────┐
                         │                   lspx                      │
  ┌──────────────┐       │  ┌─────────┐    ┌──────────────────────┐   │
  │  Your code   │       │  │         │───►│  gopls               │   │
  │  (or editor) │◄─────►│  │  Mux    │    └──────────────────────┘   │
  └──────────────┘       │  │         │───►│  rust-analyzer       │   │
   one client            │  │         │    └──────────────────────┘   │
                         │  │         │───►│  typescript-language- │   │
                         │  └─────────┘    │  server               │   │
                         │                 └──────────────────────┘   │
                         └─────────────────────────────────────────────┘
                                         many servers

Why lspx?

The LSP ecosystem is rich but fragmented. In a real project you might need:

  • gopls for Go files
  • rust-analyzer for Rust files
  • typescript-language-server for TypeScript/JavaScript
  • eslint-lsp for linting on top of TypeScript
  • A custom internal server for your proprietary language

Without a multiplexer, every client (editor plugin, build tool, CI script) must manage connections to each of those servers independently. That means duplicated lifecycle code, no result merging, and no shared sessions.

lspx moves that complexity into a single, reusable layer:

Without lspx With lspx
Client manages N server connections Client manages 1 lspx connection
Results per-server, no aggregation Unified results merged across servers
Each editor session starts fresh servers Servers are long-lived and shared
Raw JSON-RPC is opaque Full traffic logging built-in

Features

Feature Description
🔀 Client multiplexing One client interface, any number of language servers
🗂️ Smart routing Requests routed to the right server(s) based on file type or language ID
🔗 Result aggregation Diagnostics, completions, and more merged transparently across servers
🤝 Session sharing Multiple clients share a single long-lived server instance
🪵 Traffic logging Full JSON-RPC stream logging (NDJSON) for debugging and testing
⚙️ Flexible config Declarative TOML config file with optional --auto detection mode
🚀 stdio transport Standard LSP stdio transport — works as a drop-in with any LSP-aware editor
📦 Go library Embed the multiplexer directly in your Go programs via a clean, idiomatic API

Go Library

The lspx package is the primary interface. The CLI is just a thin wrapper around it.

Install

go get github.com/masterkeysrd/lspx

Create a multiplexed client

Configure a single client backed by multiple language servers. lspx handles all routing and aggregation transparently.

package main

import (
	"context"
	"fmt"
	"log"

	"github.com/masterkeysrd/lspx"
)

func main() {
	ctx := context.Background()

	// One client — three servers underneath
	client, err := lspx.NewClient(ctx, lspx.ClientOptions{
		Servers: []lspx.ServerConfig{
			{
				Name:      "gopls",
				Command:   []string{"gopls", "serve"},
				FileTypes: []string{"go"},
			},
			{
				Name:      "rust-analyzer",
				Command:   []string{"rust-analyzer"},
				FileTypes: []string{"rust"},
			},
			{
				Name:      "tsserver",
				Command:   []string{"typescript-language-server", "--stdio"},
				FileTypes: []string{"typescript", "javascript"},
			},
		},
		RootURI: "file:///path/to/your/project",
	})
	if err != nil {
		log.Fatal(err)
	}
	defer client.Close()

	// Open a Go file — lspx routes to gopls automatically
	client.DidOpen(ctx, &lspx.DidOpenTextDocumentParams{
		TextDocument: lspx.TextDocumentItem{
			URI:        "file:///path/to/your/project/main.go",
			LanguageID: "go",
			Version:    1,
		},
	})

	// Hover — lspx sends to gopls and returns its response
	hover, err := client.Hover(ctx, &lspx.HoverParams{
		TextDocumentPositionParams: lspx.TextDocumentPositionParams{
			TextDocument: lspx.TextDocumentIdentifier{
				URI: "file:///path/to/your/project/main.go",
			},
			Position: lspx.Position{Line: 10, Character: 5},
		},
	})
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(hover.Contents.Value)
}

Aggregate results from multiple servers

When more than one server handles a file type, lspx merges their responses.

// Two servers handle TypeScript: tsserver for completions, eslint-lsp for linting
client, _ := lspx.NewClient(ctx, lspx.ClientOptions{
	Servers: []lspx.ServerConfig{
		{
			Name:      "tsserver",
			Command:   []string{"typescript-language-server", "--stdio"},
			FileTypes: []string{"typescript"},
		},
		{
			Name:      "eslint",
			Command:   []string{"vscode-eslint-language-server", "--stdio"},
			FileTypes: []string{"typescript"},
			// Only provide diagnostics; skip completions
			Capabilities: lspx.CapabilityDiagnosticProvider,
		},
	},
	Aggregate: lspx.AggregateOptions{
		Diagnostics: true, // merge from both servers
		Completions: false, // only from first matching server
	},
	RootURI: "file:///path/to/your/project",
})

Subscribe to server-push notifications

All servers' notifications are funneled through a single handler:

// Diagnostics from ALL servers arrive here, already merged
client.OnNotification(lspx.MethodTextDocumentPublishDiagnostics, func(params *lspx.PublishDiagnosticsParams) {
	for _, diag := range params.Diagnostics {
		fmt.Printf("[%s] %s line %d: %s\n",
			diag.Source, diag.Severity, diag.Range.Start.Line, diag.Message)
	}
})

Core API

Method LSP Method Routing
client.Initialize initialize Sent to all servers
client.DidOpen textDocument/didOpen Sent to servers matching the file's language ID
client.DidChange textDocument/didChange Sent to servers matching the file's language ID
client.DidClose textDocument/didClose Sent to servers matching the file's language ID
client.Hover textDocument/hover First matching server
client.Completion textDocument/completion Aggregated or first, configurable
client.Definition textDocument/definition First matching server
client.Declaration textDocument/declaration First matching server
client.TypeDefinition textDocument/typeDefinition First matching server
client.Implementation textDocument/implementation First matching server
client.PrepareCallHierarchy textDocument/prepareCallHierarchy First matching server
client.IncomingCalls callHierarchy/incomingCalls First matching server (routes using item URI)
client.OutgoingCalls callHierarchy/outgoingCalls First matching server (routes using item URI)
client.PrepareTypeHierarchy textDocument/prepareTypeHierarchy First matching server
client.Supertypes typeHierarchy/supertypes First matching server (routes using item URI)
client.Subtypes typeHierarchy/subtypes First matching server (routes using item URI)
client.DocumentHighlight textDocument/documentHighlight First matching server
client.DocumentLink textDocument/documentLink First matching server
client.ResolveDocumentLink documentLink/resolve First matching server (routes using wrapped server metadata)
client.CodeLens textDocument/codeLens First matching server
client.ResolveCodeLens codeLens/resolve First matching server (routes using wrapped server metadata)
client.FoldingRange textDocument/foldingRange First matching server
client.References textDocument/references Aggregated across matching servers
client.Diagnostics textDocument/diagnostic Aggregated across matching servers
client.Formatting textDocument/formatting First matching server
client.OnNotification Receives notifications from all servers

Full API reference: go doc github.com/masterkeysrd/lspx or pkg.go.dev


CLI

The lspx CLI exposes the same multiplexer over stdio, making it a drop-in LSP server for any editor. The editor connects to lspx and gets unified results from all configured servers.

Installation

From source (requires Go 1.21+):

git clone https://github.com/masterkeysrd/lspx.git
cd lspx
go build -o lspx ./cmd/lspx

Via go install:

go install github.com/masterkeysrd/lspx/cmd/lspx@latest

Config file

lspx looks for .lspx.toml in the project root, then ~/.config/lspx/config.toml.

# .lspx.toml

[lspx]
log_file  = "~/.local/share/lspx/lspx.log"
log_level = "info"

[aggregate]
diagnostics = true
completions = false

[[server]]
name      = "gopls"
command   = ["gopls", "serve"]
filetypes = ["go"]

[[server]]
name      = "rust-analyzer"
command   = ["rust-analyzer"]
filetypes = ["rust"]

[[server]]
name      = "tsserver"
command   = ["typescript-language-server", "--stdio"]
filetypes = ["typescript", "javascript", "typescriptreact", "javascriptreact"]

[[server]]
name         = "eslint"
command      = ["vscode-eslint-language-server", "--stdio"]
filetypes    = ["typescript", "javascript"]
capabilities = ["diagnostics"]

Editor integration

Neovim (via nvim-lspconfig):

require('lspconfig').lspx.setup({
  cmd = { 'lspx' },
  -- lspx handles all languages; list every filetype you need here
  filetypes = { 'go', 'rust', 'typescript', 'javascript' },
  root_dir = require('lspconfig.util').root_pattern('.lspx.toml', '.git'),
})

VS Code (settings.json):

{
  "lspx.server.command": ["lspx"]
}

Auto-detection mode

Skip the config file entirely — lspx detects installed language servers automatically:

lspx --auto

Explicit config always takes precedence. --auto is a fast way to get started.

CLI flags

Usage:
  lspx [flags]

Flags:
  --auto          Auto-detect installed language servers
  --config PATH   Config file path (default: .lspx.toml, ~/.config/lspx/config.toml)
  --log PATH      Override log file path
  --log-level     Log verbosity: debug, info, warn, error (default: info)
  --version       Print version and exit
  -h, --help      Show this help

Configuration Reference

Server block

Field Type Description
name string Friendly name used in logs
command []string Command to launch the server
filetypes []string Language IDs this server handles
env {string: string} Extra environment variables
capabilities []string Limit which capabilities are used from this server (diagnostics, completions, hover, declaration, definition, typeDefinition, implementation, references, callHierarchy, typeHierarchy, documentHighlight, documentLink, codeLens, foldingRange, …). Omit to use all.
share_sessions bool Share this server across multiple editor clients (default: true)

Aggregation block

Field Type Default Description
diagnostics bool true Merge diagnostics from all matching servers
completions bool false Merge completions (disable to avoid duplicates)
references bool true Merge textDocument/references results

Traffic Logging

When log_file is set, lspx records the full JSON-RPC stream in both directions for every client↔server pair. Log entries are NDJSON with:

Field Description
ts ISO-8601 timestamp
server Server name
direction client→server or server→client
message Raw LSP JSON-RPC message

Useful for debugging misbehaving servers, understanding editor traffic, and building test fixtures from real sessions.


Roadmap

Go Library

  • Project scaffold
  • JSON-RPC client over stdio
  • initialize / shutdown lifecycle
  • textDocument/* request methods
  • Server-push notification handlers
  • Multi-server multiplexer core
  • File-type based routing
  • Result aggregation engine
  • Per-server capability filtering
  • Workspace requests (workspace/symbol, etc.)
  • Type-safe LSP types (generated from the LSP spec)
  • pkg.go.dev documentation

CLI

  • Config file parsing (TOML)
  • stdio server mode (editor integration)
  • Session sharing across clients
  • Traffic logging
  • --auto detection mode
  • VS Code extension
  • Homebrew formula

Contributing

Contributions are welcome! Please open an issue first to discuss significant changes.

  1. Fork the repository
  2. Create your feature branch: git checkout -b feat/my-feature
  3. Commit your changes: git commit -m 'feat: add my feature'
  4. Push and open a Pull Request

Please follow the Conventional Commits specification.


License

This project is licensed under the MIT License.


Made with ☕ and Go

About

One LSP client. Any number of language servers. An LSP multiplexer written in Go.

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages