[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)
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.jsonlacks thelspServersthat the official*-lspplugins declare only in theirstrict: falsemarketplace entry. - With 0 servers registered, the LSP tool is not offered at all: not loaded, not deferred, and
ToolSearch select:LSPfinds 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-official1.0.0, enabled;rust-analyzer1.95.0 onPATH
What happens
- The Desktop app starts the session as
~/.claude/remote/ccd-cli/2.1.284 … --plugin-dir ~/.claude/remote/plugins/<id>/<hash> …, one--plugin-dirper plugin it manages,rust-analyzer-lspamong them. - That directory holds
README.md,.syncedand a.claude-plugin/plugin.jsonwith onlyname,description,versionandauthor: nolspServers, no.lsp.json. This matches the "Synthesized plugin.json … (strict:false marketplace plugin)" log line quoted in #90926. - 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
- 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; thesystem/initmessage'stoolshas noLSP. - Without it:
Loaded 1 LSP server(s) from plugin: rust-analyzer-lsp;toolscontainsLSP.
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.