claude-code - 💡(How to fix) Fix [FEATURE] Multi-LSP per file extension: let linting servers coexist with type servers — or tell us it won't happen

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…

Error Message

Claude Code today: one server wins, the rest never start. No error. No log. The losing server's process never appears in ps. The user gets no diagnostics and no indication why.

Root Cause

  • #32912 — same root cause, two third-party plugins conflicting (open, 0 official responses)
  • #27692 — same request, closed by stale bot after 0 official responses

Fix Action

Fix / Workaround

The decompiled source of claude 2.1.139 shows the flat-map merge in PI7():

// All plugin LSP servers — from every plugin — are merged into ONE flat Map.
// The routing key is the file extension → language ID.
// First match wins. Others are registered but never started.
Object.assign(H, O)  // H = global server registry

Code Example

# Only one server appears as a child process of claude (PID 13851)
89339 13851  node  typescript-language-server --stdio   ✓ started
# eslint-server.mjs — never spawned, zero processes

---

50884 13851  node  eslint-server.mjs --stdio   ✓ started

---

arrayUtils.ts:
[Line 2] Prefer .at() over […length - index]  [unicorn/prefer-at]
[Line 7] Prefer structuredClone()              [unicorn/prefer-structured-clone]
[Line 11] Expect newline after if               [antfu/if-newline]

---

// All plugin LSP servers — from every plugin — are merged into ONE flat Map.
// The routing key is the file extension → language ID.
// First match wins. Others are registered but never started.
Object.assign(H, O)  // H = global server registry

---

3 plugin LSP servers  ← typescript + clangd + eslint, all counted
RAW_BUFFERClick to expand / collapse

The Problem in One Sentence

Claude Code's LSP client routes each file extension to exactly one language server. This silently kills every third-party diagnostic plugin the moment any official language-intelligence plugin is installed.

Why This Blocks an Entire Class of Plugins

The split between type intelligence and linting/diagnostics is not an edge case — it is the standard architecture used by every modern editor:

ConcernServerProtocol mechanism
Type checking, completions, go-to-def, hovertypescript-language-server, pyright, rust-analyzertextDocument/definition, textDocument/hover, textDocument/completion
Linting, style, real-time ESLint/Ruff/oxlint diagnosticseslint-server, ruff server, biometextDocument/publishDiagnostics (push)

VS Code, Neovim, Helix, Zed — every serious editor sends textDocument/didChange to all matching servers simultaneously. The results are merged. This is not a feature; it is the baseline expectation of the LSP ecosystem.

Claude Code today: one server wins, the rest never start. No error. No log. The losing server's process never appears in ps. The user gets no diagnostics and no indication why.

Reproduction (100% Confirmed, Not Theoretical)

Setup:

  • typescript-lsp@claude-plugins-official enabled ← official plugin, maps .ts → typescript
  • eslint-lsp@darian-deng/agent-plugins installed ← third-party plugin, also maps .ts → typescript

After editing a TypeScript file:

# Only one server appears as a child process of claude (PID 13851)
89339 13851  node  typescript-language-server --stdio   ✓ started
# eslint-server.mjs — never spawned, zero processes

After disabling typescript-lsp and repeating the same edit:

50884 13851  node  eslint-server.mjs --stdio   ✓ started

And <new-diagnostics> immediately injected in the same turn:

arrayUtils.ts:
  ✘ [Line 2] Prefer .at(…) over […length - index]  [unicorn/prefer-at]
  ✘ [Line 7] Prefer structuredClone(…)              [unicorn/prefer-structured-clone]
  ✘ [Line 11] Expect newline after if               [antfu/if-newline]

The feature works perfectly. The routing is the only blocker.

Binary Evidence (not speculation)

The decompiled source of claude 2.1.139 shows the flat-map merge in PI7():

// All plugin LSP servers — from every plugin — are merged into ONE flat Map.
// The routing key is the file extension → language ID.
// First match wins. Others are registered but never started.
Object.assign(H, O)  // H = global server registry

/reload-plugins confirms all 3 servers ARE registered:

3 plugin LSP servers  ← typescript + clangd + eslint, all counted

Only typescript-language-server starts for .ts files. The ESLint server is permanently starved.

Impact on the Plugin Ecosystem

This is not about one plugin. It is a structural ceiling on what third-party plugin authors can build:

  • ESLint / oxlint / Biome diagnostics for TypeScript projects → blocked by typescript-lsp
  • Ruff linting for Python projects → blocked by pyright-lsp
  • Spring Boot LSP for Java projects → blocked by jdtls-lsp (confirmed in #32912 comments)
  • Any specialized diagnostic server for a language that already has a type server → blocked

The pattern is identical for every language. Installing the official language-intelligence plugin permanently excludes linting plugins from that language's files.

What We Are Asking For

Option A (preferred): Send textDocument/didChange to all registered servers that match a file extension, exactly as VS Code, Neovim, and Helix do. Merge publishDiagnostics results. No new protocol invention required.

Option B (acceptable): Expose a priority / coexistence mechanism. Let a server declare "diagnosticsOnly": true to opt into a secondary slot that runs alongside the primary intelligence server.

Option C (at minimum): If multi-server routing is an intentional architectural decision and will not be implemented, please say so explicitly here. Plugin authors need to know whether to build around this constraint or wait for it to be fixed. A won't fix with a rationale is far more useful than silence and a stale-bot close.

Prior Art

  • #32912 — same root cause, two third-party plugins conflicting (open, 0 official responses)
  • #27692 — same request, closed by stale bot after 0 official responses

Three reports, zero official engagement. The community is not complaining about an obscure edge case. We are describing a structural limitation that prevents an entire category of Claude Code plugins from functioning.

Environment

  • Claude Code: 2.1.139
  • macOS Darwin 24.6.0 arm64
  • Confirmed reproducible across multiple sessions and file types

Labels to help others find this:
lsp language-server-protocol multi-lsp plugin eslint typescript-lsp diagnostics publishDiagnostics extensionToLanguage linting ruff pyright biome oxlint extension-conflict plugin-ecosystem third-party-plugin routing

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING