[BUG] Desktop app's --plugin-dir copy of a strict:false LSP plugin drops its lspServers, so 0 servers load and the LSP tool never appears (bundled CLI resolves it)

Status Open
Reported on v2.1.251
Maintainer reply None cached
Activity 0 comments · opened Oct 3, 2026

Summary

In a Desktop app session no plugin LSP server is registered, so the LSP tool is never offered. On the same host, the bundled CLI started without the Desktop app's plugin copy loads the same server and offers the tool.

The cause is the plugin copy the Desktop app passes with --plugin-dir:

  • The copy overrides the installed plugin of the same name.
  • The copy's plugin.json lacks the lspServers that the official *-lsp plugins declare only in their strict: false marketplace entry.
  • With 0 servers registered, the LSP tool is not offered at all: not loaded, not deferred, and ToolSearch select:LSP finds nothing.

This is the Desktop-specific failure #90926 originally described. Its reporter closed it as a duplicate of #90114 after the terminal CLI 2.1.251 failed too. On current versions the CLI works, so the Desktop path is what remains broken.

Environment

  • Claude Desktop 2.9939.4, Code tab, session running on a Linux x64 host over SSH
  • Bundled CLI (~/.claude/remote/ccd-cli/2.1.284); terminal CLI 2.1.288 on the same host for comparison
  • rust-analyzer-lsp@claude-plugins-official 1.0.0, enabled; rust-analyzer 1.95.0 on PATH

What happens

  1. The Desktop app starts the session as ~/.claude/remote/ccd-cli/2.1.284 … --plugin-dir ~/.claude/remote/plugins/<id>/<hash> …, one --plugin-dir per plugin it manages, rust-analyzer-lsp among them.
  2. That directory holds README.md, .synced and a .claude-plugin/plugin.json with only name, description, version and author: no lspServers, no .lsp.json. This matches the "Synthesized plugin.json … (strict:false marketplace plugin)" log line quoted in #90926.
  3. The session's debug log:

``
Plugin "rust-analyzer-lsp" from --plugin-dir overrides installed version
[LSP MANAGER] initializeLspServerManager() called
Total LSP servers loaded: 0
LSP server manager initialized successfully
``

  1. In the bundled code, the LSP tool's isEnabled() is the manager's "has ever connected" check: at least one registered server not in an error state. With 0 servers it is false, and the tool never reaches the model. No error is surfaced anywhere.

A/B repro

Same binary (ccd-cli/2.1.284) and same headless --output-format stream-json mode, with CLAUDE_CODE_ENTRYPOINT=claude-desktop inherited, --setting-sources=user,project,local and --debug-file. The only difference is the Desktop app's --plugin-dir for rust-analyzer-lsp:

  • With it: Total LSP servers loaded: 0; the system/init message's tools has no LSP.
  • Without it: Loaded 1 LSP server(s) from plugin: rust-analyzer-lsp; tools contains LSP.

So the entrypoint is not the gate. The CLI resolves the marketplace entry's lspServers for the installed plugin, and the Desktop app's same-name copy replaces it with one that has none.

Impact

All 12 official *-lsp plugins declare lspServers only in their marketplace entry (strict: false), so all of them silently do nothing in Desktop sessions, with no error anywhere to tell the user.

Workaround

A local plugin under a different name that declares the server in its own .claude-plugin/plugin.json:

"lspServers": {
  "rust-analyzer": { "command": "rust-analyzer", "extensionToLanguage": { ".rs": "rust" } }
}

With it, the same Desktop-style launch, still passing the rust-analyzer-lsp copy, logs Loaded 1 LSP server(s) from plugin: rust-lsp and starts the server. The LSP tool then answers a workspaceSymbol query correctly.

Patching the official plugin's installed copy can't fix this: the Desktop app's --plugin-dir copy overrides it by name, and per #90926 the synthesized plugin.json is rewritten on every marketplace refresh.

Expected

The Desktop app's plugin copy of a strict: false marketplace plugin carries the entry's component declarations (lspServers, and likewise mcpServers, …). Alternatively, the --plugin-dir copy does not shadow components the installed plugin resolves from its marketplace entry.

Related: #90926, #90114, #93474, #78604.

View original on GitHub ↗