LSP/JSONRPC multiplexer for connecting one LSP client to multiple servers
Project description
rassumfrassum
Connects one LSP client to multiple LSP servers.
It spawns one or more stdio-enabled LSP server subprocesses,
communicates with them via pipes, and handles a client connected to
its own stdio. rass behaves like an LSP server, so clients think
they are talking to single LSP server, even though they are secretly
talking to many.
An LSP client like Emacs's Eglot can find a python file in
some project and invoke it like so (C-u M-x eglot probably helps):
rass -- basedpyright-langserver --stdio -- ruff server
This should start managing Python files within a project with two
servers instead of one. The -- separate rass's options from
basedpyright's from ruff's.
To set up other clients, check their documentation.
Issues?
Read this first, please.
Installation
I hope to have made pip install rassumfrassum do the right thing by
now. If I haven't, you can probably clone this repo and call the
top-level rass wrapper script directly, since this doesn't have any
dependencies.
Features
- Merges and synchronizes diagnostics from multiple servers into a
single
textDocument/publishDiagnosticsevent. - Requests
textDocument/codeActionsfrom all servers supporting it; other requests go to the first server that supports the corresponding capability. - Tries its best to merge server capabilities announcements and to track which inferior server supports which capability.
- Zero dependencies beyond Python standard library (3.10+)
Under the hood
Message Routing
JSONRPC has requests, responses, and notifications. Here's how they're routed:
From client to servers:
-
All notifications go unchanged directly to all servers
-
Some requests go only to one server, and that server's response is forwarded to the client
-
Other requests go to multiple servers, and their responses are merged if they arrive in time
From servers to client:
-
Most notifications go directly through, but some like
textDocument/publishDiagnosticswait for all servers to send theirs, then the results are merged before forwarding to the client -
All server requests go to the client. ID tweaking is necessary because servers don't know about each other and they could clash.
Architecture
The codebase lives in src/rassumfrassum/ and is split into several modules:
-
rassum.pyis the main entry point with command-line processing.run_multiplexerstarts a bunch of async tasks to read from the clients and servers, and waits for all of them. The local lexical state inrun_multiplexertracks JSONRPC requests, responses, and notifications, and crucially the progress of ongoing aggregation attempts. In as much as possible,rassum.pyshould be just a JSONRPC-aggregator and not know anything about particular custom handling of LSP message types. There are a few violations of this principle, but whenever it needs to know what to do, it asks/informs the upper layer infrassum.pyabout in-transit messages. -
frassum.pycontains the business logic used byrassum.pyfacilities. This one fully knows about LSP. So it knows, for example, how to mergeinitializeandshutdownresponses, when to reject a staletextDocument/publishDiagnosticsand how to do the actual work for aggregation. -
lolo.pyprovides logging utilities for debugging and monitoring the multiplexer's operation. -
tete.pycontains test utilities used by both client and server test scripts. -
jaja.pyhandles bare JSON-over-stdio logistics and is completely ignorant of LSP. It deals with protocol framing and I/O operations.
Testing
There are tests under test/. Each test is a subdir, usually with a
client.py, a server.py (of which instances are spawned to emulate
multiple servers) and a run.sh, which creates a FIFO special file to
wire up the stdio connections and launches client.py connected to
rass. client.py has the test assertions. Both client.py and
server.py use common utils from src/rassumfrassum/tete.py.
To run all tests, use test/run-all.sh.
Logging
The stderr output of rass is useful for peeking into the
conversation between all entities and understanding how the
multiplexer operates.
FAQ
(...not really, noone's really asked anything yet...)
Related projects
There's [lspx][lspx]! Never tried it, but some people are using it. Development started in this Eglot discussion thread: https://github.com/joaotavora/eglot/discussions/1429
There's also this defunct [lsplex][lsplex] thing by myself in C++ that went nowhere.
Project name?
I'm tired of fretting about names. Kudos if you can guess where I stole this one from. Used to be called dada, btw.
Bugs?
Probably a million. The LSP flora is hard enough to navigate, and maintaining the Eglot client is hard enough because of that. So this is fun and potentially useful but adds another failure point. A pretty big one at that, since of the hundreds (thousands?) of LSP servers out there, there are uncountable combinations of them, and some will definitely trip you up.
Issue reports
Read the preceding section. If you use this and want to report
something, you can start discussions or create issues at will. If you
create an issue, I might just close it with a cantmakesenseofthis
label which just means I can't make sense of it just yet. Also I have
very little time for OSS these days, so this is a totally NO WARRANTY,
YMMV thing. If I close your issue just like that, doesn't mean you're
a bad person, so don't fret. If you can provide an easy, simple, 100%
idiot-proof recipe demonstrating the bug the chances that I'll address
it are slightly higher. Else, just fork this repo, this is just
Python and you're probably a programmer right?
Did you vibe code this junk?
Yeah, a bit, with some heavy coaching, then I took over. The boring bits are definitely an LLM's.
Future/roadmap?
I might rewrite this in Rust or C++ if it makes sense. Having an LSP middleware opens up some possibilities for making JSON communication more efficient.
Options to rass
Use --help to see all options.
The --delay-ms N option delays all JSONRPC messages sent to the
client by N milliseconds. Each message gets its own independent timer,
so if two messages arrive at t=0.5s and t=1.5s with a 3000ms
delay, they'll be dispatched at t=3.5s and t=4.5s
respectively. Useful for diagnostics and testing.
The --drop-tardy option controls an aspect of the "aggregation". If
it's true and a server takes too long to respond to a request, or send
a mergeworthy notification, any messages that arrive too late are
simply dropped and the client sees whatever it got when the timeout
expired. If it's false, the most up-to-date state of the aggregation
is simply retransmitted to the client. The default is false.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file rassumfrassum-0.1.1.tar.gz.
File metadata
- Download URL: rassumfrassum-0.1.1.tar.gz
- Upload date:
- Size: 31.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1bbb8f7259ad29b0024b371745faa2511e4d96a1e6eefedec523ab1ba9b8e2b3
|
|
| MD5 |
d111c16e5df27c4a96a0f00f14433356
|
|
| BLAKE2b-256 |
3145265d7d0ae9dc56bc16d66bf9288654ad9d7abf004ac3c24decdecf0066b0
|
Provenance
The following attestation bundles were made for rassumfrassum-0.1.1.tar.gz:
Publisher:
publish.yml on joaotavora/rassumfrassum
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
rassumfrassum-0.1.1.tar.gz -
Subject digest:
1bbb8f7259ad29b0024b371745faa2511e4d96a1e6eefedec523ab1ba9b8e2b3 - Sigstore transparency entry: 740681331
- Sigstore integration time:
-
Permalink:
joaotavora/rassumfrassum@0a41e7d9a374e321a9d58941072182d6c48e0d37 -
Branch / Tag:
refs/tags/v0.1.1 - Owner: https://github.com/joaotavora
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@0a41e7d9a374e321a9d58941072182d6c48e0d37 -
Trigger Event:
release
-
Statement type:
File details
Details for the file rassumfrassum-0.1.1-py3-none-any.whl.
File metadata
- Download URL: rassumfrassum-0.1.1-py3-none-any.whl
- Upload date:
- Size: 29.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
5ba38427cbeed3124dd6593d98480bc38375c6bc73c05d06744f8151f5ea7e48
|
|
| MD5 |
c299e20b35cac9936b670803327425ce
|
|
| BLAKE2b-256 |
add7c4da33c668da29ca5152563c9f38df045595a0ba1570005167b4b079bbfd
|
Provenance
The following attestation bundles were made for rassumfrassum-0.1.1-py3-none-any.whl:
Publisher:
publish.yml on joaotavora/rassumfrassum
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
rassumfrassum-0.1.1-py3-none-any.whl -
Subject digest:
5ba38427cbeed3124dd6593d98480bc38375c6bc73c05d06744f8151f5ea7e48 - Sigstore transparency entry: 740681352
- Sigstore integration time:
-
Permalink:
joaotavora/rassumfrassum@0a41e7d9a374e321a9d58941072182d6c48e0d37 -
Branch / Tag:
refs/tags/v0.1.1 - Owner: https://github.com/joaotavora
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@0a41e7d9a374e321a9d58941072182d6c48e0d37 -
Trigger Event:
release
-
Statement type: