Skip to main content

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.

Release files for pylspmux 0.0.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for pylspmux 0.0.1
File Size Uploaded
pylspmux-0.0.1.tar.gz 25.9 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for pylspmux 0.0.1
File Interpreter ABI Platform
pylspmux-0.0.1-py3-none-any.whl Python 3 none any Details

Total release size: 42.3 kB

Release files / pylspmux-0.0.1.tar.gz

Download URL pylspmux-0.0.1.tar.gz
Size 25.9 kB
Tags Source
SHA-256 checksum
How to use checksums
07141005d064df953d78e51d801993e9532a0e0b7b3c8b633eb23b7928b627e4
BLAKE2b-256 checksum
How to use checksums
fbb695b17d0cdb0b6d87a92405a68e9af874129359a6e713325dad7638715144
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Jul 27, 2026.

Transparency log

Release files / pylspmux-0.0.1-py3-none-any.whl

Download URL pylspmux-0.0.1-py3-none-any.whl
Size 16.4 kB
Tags Python 3
SHA-256 checksum
How to use checksums
37868bec5ef9ff394657305e1c09d93c184515ea7903ec30fed4f6eb628f41ee
BLAKE2b-256 checksum
How to use checksums
fe6db333751f6e9b7b2be1095f2c5c9828c5d39eba1bee83665c1a804f695171
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Jul 27, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.0.1 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page