Skip to main content

LSP multiplexer letting several language servers share one file extension

Project description

pylspmux

An LSP multiplexer: presents several language servers to one client as if they were a single server.

It exists because Claude Code maps a file extension to exactly one language server. Registration keeps every matching server in an array, but routing always takes index 0 and warns about the rest:

LSP: extension .py already handled by "pyright"; "ruff" will not be used for .py files

Registering pylspmux for .py instead lets pyright, ruff and pyrefly all see the same files, with their diagnostics merged into one stream.

Nothing about the design is Python-specific — the same config runs typescript-language-server alongside eslint for .ts.

Install

pip install pylspmux

No runtime dependencies. The client spawns this process at every session start, and each dependency is another way for it to fail to start; the whole thing runs on a bare /usr/bin/python3.

Use it

Once installed, pylspmux is just a command on PATH. Point your client's LSP config at it instead of at the real server, and give it a servers.json (see Configuration) telling it which real servers to run.

For Claude Code, drop both in the project root — it reads a .lsp.json there directly, no plugin needed:

.lsp.json:

{
  "pylspmux": {
    "command": "pylspmux",
    "args": ["servers.json"],
    "extensionToLanguage": {
      ".py": "python",
      ".pyi": "python"
    }
  }
}

servers.json:

{
  "servers": [
    {
      "name": "pyright",
      "command": "pyright-langserver",
      "args": [
        "--stdio"
      ]
    },
    {
      "name": "ruff",
      "command": "ruff",
      "args": [
        "server"
      ]
    },
    {
      "name": "pyrefly",
      "command": "pyrefly",
      "args": [
        "lsp"
      ],
      "suppressCodes": [
        "unused-import",
        "unused-variable"
      ]
    }
  ]
}

Restart the client, then check that all three servers came up:

PYLSPMUX_LOG=/tmp/mux.log claude
grep 'live servers' /tmp/mux.log

For any other LSP client the mechanism is the same: pylspmux replaces the single server as the spawned command, with the path to servers.json as its first argument. The key names in the client's own config format will differ.

Configuration

{
  "servers": [
    {
      "name": "pyright",
      "command": "pyright-langserver",
      "args": ["--stdio"],
      "enabled": true,
      "settings": {
        "python": { "analysis": { "typeCheckingMode": "standard" } }
      },
      "suppressCodes": []
    }
  ]
}
Field Meaning
name identifies the server, and becomes the diagnostic source if it sets none
command, args, env how to spawn it
settings answers the server's workspace/configuration requests, and is pushed via workspace/didChangeConfiguration after initialized
initializationOptions passed through in initialize
suppressCodes diagnostic codes to drop from this server
enabled keep an entry without running it

An invalid entry fails the whole load rather than being skipped: a silently dropped server looks exactly like a server that found no problems, which is the worst possible failure mode for a diagnostics tool. A server that fails to spawn is different — that degrades gracefully, and the rest keep working.

pylspmux takes the path to this file as its one required argument.

How it works

Notifications (didOpen, didChange, didSave, didClose) broadcast to every child. Requests go only to children advertising the matching capability, and the answers are merged:

Request Merge
hover every server's answer, each labelled with its name
definition, typeDefinition, implementation, references concatenated, de-duplicated by target
documentSymbol, workspace/symbol concatenated, de-duplicated by name/kind/range
prepareCallHierarchy concatenated; each item remembers its owner so incomingCalls/outgoingCalls route back to the right server

Requests coming from a child (workspace/configuration, client/registerCapability, window/workDoneProgress/create) are answered here rather than forwarded, so the client's connection looks like an ordinary single language server. workspace/applyEdit is refused — the client edits files itself.

A child that fails to spawn, or dies later, is dropped: its diagnostics are cleared and the remaining servers carry on. A child that stops answering hits a 20-second timeout and is left out of that one merge.

Two client quirks worth knowing

Both were read out of the Claude Code 2.1.215 bundle, and both shape the design:

  • A publishDiagnostics payload whose version trails the document the client holds is dropped. A merged payload is only as fresh as its slowest contributor, so republished diagnostics carry no version field at all — that skips the staleness check entirely.
  • A payload with an empty diagnostics array is ignored, so "all problems cleared" never propagates. That is the client's own behaviour and applies equally without the multiplexer.

pyrefly needs per-project config

At its default basic preset pyrefly reports almost nothing — it missed a plain result: str = add(1, 2). Add a pyrefly.toml to each project:

project-includes = ["**/*.py"]
preset = "strict"

Valid presets: off, basic, legacy, default, strict, all. Without one, pyrefly is dead weight in the union.

Because pyrefly and ruff both flag unused imports, the shipped config suppresses pyrefly's unused-import and unused-variable.

Tests

.venv/bin/pytest                    # unit + fake-server end-to-end
.venv/bin/pytest -m integration     # spawns the real pyright/ruff/pyrefly

tests/fake_server.py is a scriptable language server driven by environment variables, so the full pipeline can be tested deterministically without waiting on a real type checker.

Debugging

PYLSPMUX_LOG=/tmp/mux.log claude

Logs each child's stderr, its advertised capabilities, timeouts, and any unhandled message. Nothing may go to stdout — that is the LSP stream — and stderr is swallowed by the client, hence the file.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

pylspmux-0.0.1.tar.gz (25.9 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

pylspmux-0.0.1-py3-none-any.whl (16.4 kB view details)

Uploaded Python 3

File details

Details for the file pylspmux-0.0.1.tar.gz.

File metadata

  • Download URL: pylspmux-0.0.1.tar.gz
  • Upload date:
  • Size: 25.9 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.14

File hashes

Hashes for pylspmux-0.0.1.tar.gz
Algorithm Hash digest
SHA256 07141005d064df953d78e51d801993e9532a0e0b7b3c8b633eb23b7928b627e4
MD5 d8e3a580adc1bc791d68607b550c8763
BLAKE2b-256 fbb695b17d0cdb0b6d87a92405a68e9af874129359a6e713325dad7638715144

See more details on using hashes here.

Provenance

The following attestation bundles were made for pylspmux-0.0.1.tar.gz:

Publisher: publish.yml on MyrikLD/pylspmux

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file pylspmux-0.0.1-py3-none-any.whl.

File metadata

  • Download URL: pylspmux-0.0.1-py3-none-any.whl
  • Upload date:
  • Size: 16.4 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.14

File hashes

Hashes for pylspmux-0.0.1-py3-none-any.whl
Algorithm Hash digest
SHA256 37868bec5ef9ff394657305e1c09d93c184515ea7903ec30fed4f6eb628f41ee
MD5 1ac27280b16a7381ca837cbdd0d70806
BLAKE2b-256 fe6db333751f6e9b7b2be1095f2c5c9828c5d39eba1bee83665c1a804f695171

See more details on using hashes here.

Provenance

The following attestation bundles were made for pylspmux-0.0.1-py3-none-any.whl:

Publisher: publish.yml on MyrikLD/pylspmux

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page