Third-party LSP plugins: `.lsp.json` at plugin root works on v2.1.119; marketplace.json `lspServers` alone is silently ignored
Summary
Working through the third-party LSP plugin path on Claude Code v2.1.119, I found a gap between the marketplace-side docs and what actually loads, plus two documented config fields that the runtime rejects. Posting the empirical findings as a heads-up for anyone else hitting the same wall.
Setup
- Claude Code 2.1.119 on Windows (also tested under WSL)
- Plugin:
zig-lspdistributed viaagent-sh/zig-lsp— a third-party marketplace, notclaude-plugins-official - Language server:
zls0.16.0 onPATH - Test workspace: a real Zig 0.17 project with ~900 LOC
What worked, what didn't
| Setup | Result |
|---|---|
| lspServers block inline in marketplace.json's plugin entry; no .lsp.json in the plugin source | Plugin shows enabled, but the LSP never registers. Debug log shows Checking plugin zig-lsp and then nothing — no Loaded N LSP server(s) from plugin line follows. |
| Same plugin entry copied into the local claude-plugins-official marketplace cache (marketplace.json + an empty plugin source dir) | Loads fine: Loaded 1 LSP server(s) from plugin: zig-lsp-test. The behavior is gated on the marketplace name, not on schema. |
| .lsp.json at the plugin root in the source repo (which gets copied to ~/.claude/plugins/cache/<marketplace>/<plugin>/<version>/.lsp.json on install) | Loads. Loaded 1 LSP server(s) from plugin: zig-lsp, Total LSP servers loaded: <N>. ZLS spawns and post-edit diagnostics flow as expected. |
| lspServers inline in .claude-plugin/plugin.json (alongside name/version/etc., not in marketplace.json) | Also loads. Third valid path; not in either docs page. |
| Manifest with restartOnCrash: true or maxRestarts: N (in any of the three config locations) | Initialization fails. Loader logs [ERROR] LSP server '<id>': restartOnCrash is not yet implemented. Remove this field from the configuration. The entire LSP server is rejected, not just the field. .lsp.json files get No LSP server available. startupTimeout works fine. |
Two paths the docs describe, three that actually work, two fields that don't
For a third-party LSP plugin on 2.1.119, the loader will pick up its server config from any of:
.lsp.jsonat the plugin root (documented at /plugins#add-lsp-servers-to-your-plugin)lspServersinline in.claude-plugin/plugin.json(undocumented; verified viamichelsciortino/dart-lspand a local test)lspServersinline in.claude-plugin/marketplace.json— only loads forclaude-plugins-official; inert for third-party marketplaces (documented at /plugins-reference#lsp-servers)
The restartOnCrash and maxRestarts fields are listed in the marketplace lspServers schema in the plugins reference, but the runtime rejects them at init with the error above. Either remove them from the docs or implement the runtime side; right now a plugin author copying the documented schema gets a non-loading LSP.
Related / why a fresh issue
- #15521 was closed as completed in v2.1.76 with the claim that LSP plugin loading was fixed. On 2.1.119 the marketplace-level
lspServerspath remains inert for third-party marketplaces. The.lsp.jsonruntime path does work, but that distinction isn't anywhere in the closed issue's comments. - #31468 was closed as NOT_PLANNED with a comment that "no configuration-based workaround exists." Empirically there is a working configuration —
.lsp.jsonat the plugin root, orlspServersinplugin.json— and neither is mentioned in any of the comments on that thread. Opening this issue so the next person searching for "third-party LSP plugin not loading" finds the working answer.
What a new user should do today (Claude Code 2.1.119)
For a third-party LSP plugin to register:
<marketplace-repo>/
├── .claude-plugin/
│ └── marketplace.json # plugin entry can include `lspServers`
│ (matches official-plugin shape;
│ harmless if currently ignored)
└── plugins/
└── <plugin-name>/
├── .claude-plugin/
│ └── plugin.json # identity metadata; lspServers can
│ also be inlined here as the third
│ valid config location
├── .lsp.json # ← THIS file registers the LSP at
│ runtime (recommended path)
└── README.md
Schema is server-name → { command, args, extensionToLanguage, initializationOptions, ... } — exactly as documented at /plugins-reference#lsp-servers. But strip restartOnCrash and maxRestarts — they're documented but rejected at runtime today. startupTimeout is fine.
Verify after install with claude --debug:
- ✓ Working: log contains
Loaded N LSP server(s) from plugin: <name>andTotal LSP servers loaded: M. - ✗ Plugin not picking up config: log contains
Checking plugin <name>with no followingLoadedline —.lsp.jsonisn't reaching the cache. - ✗ Bad field: log contains
Failed to initialize LSP server <id>— usuallyrestartOnCrashor another unimplemented field. Strip it from the manifest and reinstall.
Working reference: agent-sh/zig-lsp v0.1.1 ships .lsp.json + marketplace.json lspServers (no restartOnCrash); verified end-to-end on 2.1.119 (uninstall → reinstall → cold session → Loaded 1 LSP server(s) from plugin: zig-lsp, documentSymbol/hover return real results from a Zig 0.17 codebase).
Asks
- Doc note on the
lspServersmarketplace reference clarifying:
- For third-party marketplaces,
.lsp.jsonat the plugin root is what registers the server today. lspServersinline inplugin.jsonis also accepted as an alternative.restartOnCrashandmaxRestartsare not yet implemented; including them in any config location fails initialization.
- Wire the marketplace
lspServersfield for third-party marketplaces so plugin authors can ship a single source of truth instead of keeping.lsp.jsonand themarketplace.jsonblock byte-equivalent. - Implement
restartOnCrash/maxRestartsor remove them from the schema until they are.
Happy to test fixes against agent-sh/zig-lsp.
This issue has 3 comments on GitHub. Read the full discussion on GitHub ↗