One port. Any language. Monaco-ready LSP bridge.
@ridit/relay multiplexes any number of language servers over a single WebSocket port using path-based routing. Drop it into any Monaco editor project and get completions, hover, diagnostics, go-to-definition, references, signature help, formatting, and code lenses — wired automatically.
ws://127.0.0.1:9721/python → pylsp / pyright
ws://127.0.0.1:9721/typescript → typescript-language-server
ws://127.0.0.1:9721/rust → rust-analyzer
The standard approach to Monaco + LSP is monaco-languageclient — but it pulls in vscode-languageclient and the full VSCode extension host model just to wire up completions. That's hundreds of kilobytes of VSCode abstractions built for a different runtime, bolted onto Monaco as an afterthought.
Relay uses only the primitives it actually needs — vscode-ws-jsonrpc for WebSocket framing and vscode-languageserver-protocol for LSP message types — then talks directly to Monaco's provider APIs. No extension host, no VSCode shims, no adapter layers.
It was extracted from Meridia — a from-scratch code editor — where we needed LSP that fit the stack instead of fighting it. If you're building an editor or any Monaco-based tool, relay gives you full LSP integration without the overhead.
npm install @ridit/relay
# or
bun add @ridit/relayMonaco is a peer dependency — install it separately if you haven't:
npm install monaco-editorimport { Server } from "@ridit/relay";
const server = new Server();
server.register({
command: "pyright-langserver",
args: ["--stdio"],
languageId: "python",
});
server.register({
command: "typescript-language-server",
args: ["--stdio"],
languageId: "typescript",
});
await server.start(9721);import * as monaco from "monaco-editor";
import { Client } from "@ridit/relay";
const editor = monaco.editor.create(document.getElementById("app")!, {
automaticLayout: true,
});
const model = monaco.editor.createModel(
"print('hello')",
"python",
monaco.Uri.file("/workspace/main.py"), // real path required
);
editor.setModel(model);
const client = new Client(monaco);
client.register({ languageId: "python" });
await client.start("/workspace");Windows: pass the workspace path with backslashes —
"C:\\workspace".
Once client.start() is called, relay registers the following Monaco providers for each language:
| Feature | Provider |
|---|---|
| Completions | registerCompletionItemProvider |
| Hover | registerHoverProvider |
| Diagnostics | setModelMarkers via publishDiagnostics |
| Go to Definition | registerDefinitionProvider |
| Find References | registerReferenceProvider |
| Signature Help | registerSignatureHelpProvider |
| Formatting | registerDocumentFormattingEditProvider |
| Code Lenses | registerCodeLensProvider (with usage counts) |
server.register(def: LspServerDefinition): this
server.start(port?: number): Promise<void> // default: 9721LspServerDefinition
{
command: string; // binary name or full path
languageId: string; // e.g. "python", "typescript"
args?: string[]; // e.g. ["--stdio"]
cwd?: string; // working directory override
}client.register(def: LspClientDefinition): this
client.start(workspacePath?: string, port?: number): Promise<void>
client.format_model(model: monaco.editor.ITextModel): Promise<boolean>
client.updateWorkspaceRoot(folderPath: string): Promise<void>
client.dispose(): Promise<void>LspClientDefinition
{
languageId: string; // must match server registration
extensions?: string[]; // file extensions to match, e.g. ["py", "pyw"]
}On Windows, npm-installed language servers are .cmd wrappers. Relay resolves these automatically — pass the bare binary name and it will find the right executable:
server.register({
command: "pyright-langserver",
languageId: "python",
args: ["--stdio"],
});MIT