From e669acd745cf169c9bb70c9d872c9eefe0633a4f Mon Sep 17 00:00:00 2001 From: agent-sh Date: Sun, 26 Apr 2026 03:25:44 +0300 Subject: [PATCH] =?UTF-8?q?fix:=20restore=20.lsp.json=20=E2=80=94=20empiri?= =?UTF-8?q?cally=20required=20for=20third-party=20LSP=20plugins?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous round of changes deleted .lsp.json based on the (correct) observation that official LSP plugins ship only README+LICENSE in their source. The (incorrect) inference: marketplace.json's lspServers must be the universal mechanism. Empirical testing on v2.1.119 disproved this — without .lsp.json in the cached plugin dir, the loader silently skips the entry. Two-path picture (verified by adding .lsp.json directly to the cached install dir at ~/.claude/plugins/cache/agent-sh/zig-lsp/0.1.0/.lsp.json and confirming `Loaded 1 LSP server(s) from plugin: zig-lsp` in claude --debug): - Official LSP plugins (typescript-lsp etc.) — hardcoded path inside the binary, gated on marketplace name 'claude-plugins-official'. Plugin source ships only README+LICENSE. - Third-party LSP plugins — must ship .lsp.json at plugin root in the source repo, which gets copied to the user's cache on install. The runtime loader reads from the cache path; that's what registers the server. Keep lspServers in marketplace.json too — identical to .lsp.json content. Matches the official-plugin shape and is the right metadata target if/when Anthropic wires the marketplace-side loader as the docs describe. Files: - plugins/zig-lsp/.lsp.json — restored - CLAUDE.md / AGENTS.md / CONTRIBUTING.md — non-negotiables updated to require both files, kept byte-equivalent on server config - README.md — layout reflects .lsp.json - agent-knowledge/claude-code-lsp-plugin-contribution.md — correction block expanded with the two-path picture + empirical evidence table - CHANGELOG.md — Unreleased entry rewritten --- .gitignore | 1 + AGENTS.md | 4 +-- CHANGELOG.md | 8 ++--- CLAUDE.md | 4 +-- CONTRIBUTING.md | 5 +-- README.md | 3 ++ .../claude-code-lsp-plugin-contribution.md | 33 +++++++++++-------- plugins/zig-lsp/.lsp.json | 22 +++++++++++++ 8 files changed, 56 insertions(+), 24 deletions(-) create mode 100644 plugins/zig-lsp/.lsp.json diff --git a/.gitignore b/.gitignore index 9da9a89..cd69dcf 100644 --- a/.gitignore +++ b/.gitignore @@ -3,5 +3,6 @@ Thumbs.db *.log .idea/ .vscode/ +.claude/settings.local.json zig-cache/ zig-out/ diff --git a/AGENTS.md b/AGENTS.md index ed7eb65..5e8a94e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -27,10 +27,10 @@ None. This is a config-only plugin - no slash commands, no agents, no skills. ## Plugin shape - non-negotiables -1. **LSP server config lives in `marketplace.json`'s `lspServers` field**, inline on the plugin entry. NOT in a separate `.lsp.json` file at the plugin root - that mechanism does not exist in Claude Code's loader. Verified by inspecting the 12 official LSP plugins under `claude-plugins-official/.claude-plugin/marketplace.json`; their plugin source dirs ship only README + LICENSE. +1. **For third-party LSP plugins, ship `.lsp.json` at the plugin root** (`plugins/zig-lsp/.lsp.json`). This is the file the runtime loader reads from the user's cached install. We also keep `lspServers` inline in `marketplace.json` for marketplace-UI metadata and as a backstop; both are required because official LSP plugins (typescript-lsp, rust-analyzer-lsp, etc.) load via a *separate* hardcoded path that doesn't apply to third parties. Empirically verified on Claude Code v2.1.119: with `.lsp.json` present in the cached plugin dir, `zig-lsp` registers as `Total LSP servers loaded: 3`; without it, the loader silently skips the entry even when `marketplace.json` has identical content. 2. The `command` field is `"zls"` - a PATH-resolved binary name. Never embed an absolute path; users install the binary themselves. 3. `extensionToLanguage` maps both `.zig` and `.zon` to language ID `"zig"`. Don't drop `.zon` - the package manifest needs LSP support. -4. Bumping `zls` defaults in `lspServers` is a user-facing change - update CHANGELOG and bump the plugin `version` in `plugin.json`. +4. Bumping `zls` defaults: keep `.lsp.json` and the `lspServers` block in `marketplace.json` byte-equal. Update CHANGELOG and bump the plugin `version` in `plugin.json`. ## Validation diff --git a/CHANGELOG.md b/CHANGELOG.md index 8c9c771..6e81a7d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,12 +8,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Fixed -- **LSP server config now actually registers with Claude Code.** Empirical inspection of the 12 working official LSP plugins (typescript-lsp, rust-analyzer-lsp, etc.) showed the LSP loader reads `lspServers` *inline on the marketplace.json plugin entry* — not from a separate `.lsp.json` at the plugin root, as the research doc claimed. Moved the ZLS server config into `.claude-plugin/marketplace.json`'s zig-lsp entry; deleted `plugins/zig-lsp/.lsp.json`. Same content, correct location. +- **`.lsp.json` restored at plugin root.** Empirical testing on Claude Code v2.1.119 showed third-party LSP plugins need both: an `lspServers` block in `marketplace.json` (matches official-plugin shape) AND a `.lsp.json` file at the plugin root (this is what the runtime loader actually reads from the cached install directory). The previous round of changes deleted `.lsp.json` based on an incorrect inference from the official LSP plugins' minimal source layout — those plugins use a separate hardcoded loader path that doesn't apply to third parties. With `.lsp.json` shipped, `claude --debug` logs `Loaded 1 LSP server(s) from plugin: zig-lsp`. Without it, the loader silently skips the entry. ### Changed -- `agent-knowledge/claude-code-lsp-plugin-contribution.md` carries a prominent correction block at the top documenting the loader-mechanism discovery; original sections are kept as historical record. -- `CLAUDE.md` / `AGENTS.md` / `CONTRIBUTING.md` "non-negotiables" rewritten to reflect the marketplace-level `lspServers` mechanism. -- Top-level `README.md` repo layout updated. +- `agent-knowledge/claude-code-lsp-plugin-contribution.md` correction block expanded with the two-path picture (official hardcoded vs third-party `.lsp.json`) and the empirical evidence table. +- `CLAUDE.md` / `AGENTS.md` / `CONTRIBUTING.md` "non-negotiables" updated: `.lsp.json` and marketplace `lspServers` are both required, kept byte-equivalent on server config. +- Top-level `README.md` repo layout reflects `.lsp.json` back in the plugin source. ## [0.1.0] - 2026-04-26 diff --git a/CLAUDE.md b/CLAUDE.md index ed7eb65..5e8a94e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -27,10 +27,10 @@ None. This is a config-only plugin - no slash commands, no agents, no skills. ## Plugin shape - non-negotiables -1. **LSP server config lives in `marketplace.json`'s `lspServers` field**, inline on the plugin entry. NOT in a separate `.lsp.json` file at the plugin root - that mechanism does not exist in Claude Code's loader. Verified by inspecting the 12 official LSP plugins under `claude-plugins-official/.claude-plugin/marketplace.json`; their plugin source dirs ship only README + LICENSE. +1. **For third-party LSP plugins, ship `.lsp.json` at the plugin root** (`plugins/zig-lsp/.lsp.json`). This is the file the runtime loader reads from the user's cached install. We also keep `lspServers` inline in `marketplace.json` for marketplace-UI metadata and as a backstop; both are required because official LSP plugins (typescript-lsp, rust-analyzer-lsp, etc.) load via a *separate* hardcoded path that doesn't apply to third parties. Empirically verified on Claude Code v2.1.119: with `.lsp.json` present in the cached plugin dir, `zig-lsp` registers as `Total LSP servers loaded: 3`; without it, the loader silently skips the entry even when `marketplace.json` has identical content. 2. The `command` field is `"zls"` - a PATH-resolved binary name. Never embed an absolute path; users install the binary themselves. 3. `extensionToLanguage` maps both `.zig` and `.zon` to language ID `"zig"`. Don't drop `.zon` - the package manifest needs LSP support. -4. Bumping `zls` defaults in `lspServers` is a user-facing change - update CHANGELOG and bump the plugin `version` in `plugin.json`. +4. Bumping `zls` defaults: keep `.lsp.json` and the `lspServers` block in `marketplace.json` byte-equal. Update CHANGELOG and bump the plugin `version` in `plugin.json`. ## Validation diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 57f60df..59340b4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -44,10 +44,11 @@ Update `CHANGELOG.md` in the same PR. Keep entries under the `## [Unreleased]` h ## Plugin shape - non-negotiables -1. **LSP server config is inline in `marketplace.json`'s `lspServers` field**, on the plugin entry. NOT in a separate `.lsp.json` file - that mechanism doesn't exist in Claude Code's loader. The 12 official LSP plugins (typescript-lsp, rust-analyzer-lsp, etc.) all follow this pattern: their plugin source dirs ship only README + LICENSE; the `lspServers` block lives next to `description`/`version` in the marketplace entry. +1. **Ship `.lsp.json` at the plugin root** (`plugins/zig-lsp/.lsp.json`) AND keep an `lspServers` block inline in `.claude-plugin/marketplace.json`. Both are required for a third-party LSP plugin: the runtime loader reads `.lsp.json` from the user's cached install (this is what makes the LSP register on Claude Code v2.1.119); the marketplace `lspServers` field is metadata for the marketplace UI and matches what official plugins ship. Official LSP plugins (typescript-lsp, rust-analyzer-lsp) load via a separate hardcoded code path inside the Claude Code binary and don't ship `.lsp.json` themselves; that path doesn't apply to third parties. 2. The `command` field is `"zls"` - a PATH-resolved binary name. Never embed an absolute path; users install the binary themselves. 3. `extensionToLanguage` maps both `.zig` and `.zon` to language ID `"zig"`. Don't drop `.zon` - `build.zig.zon` needs LSP support. -4. `CLAUDE.md` and `AGENTS.md` are mirrored byte-identically. When editing one, update the other in the same commit. +4. Keep `.lsp.json` and the `lspServers` block in `marketplace.json` byte-equal on the server config. If they drift, behavior depends on which file the loader picks up first. +5. `CLAUDE.md` and `AGENTS.md` are mirrored byte-identically. When editing one, update the other in the same commit. ## Style diff --git a/README.md b/README.md index b76de7c..7bd289c 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,9 @@ Claude Code's `LSP` tool dispatches to language-specific plugins. There are 11 o │ └── zig-lsp/ │ ├── .claude-plugin/ │ │ └── plugin.json # Plugin identity (name, version, license) +│ ├── .lsp.json # ZLS server config — copied to user's +│ │ cache on install; this is what the +│ │ LSP runtime loader actually reads │ └── README.md # User-facing setup + troubleshooting ├── agent-knowledge/ # Research notes for contributors ├── .agnix.toml # agnix config (target: claude-code) diff --git a/agent-knowledge/claude-code-lsp-plugin-contribution.md b/agent-knowledge/claude-code-lsp-plugin-contribution.md index 7a412a3..d370d69 100644 --- a/agent-knowledge/claude-code-lsp-plugin-contribution.md +++ b/agent-knowledge/claude-code-lsp-plugin-contribution.md @@ -4,27 +4,32 @@ **Sources**: 24 resources analyzed **Depth**: medium -> ## ⚠ CORRECTION — original research doc was wrong about the loader mechanism +> ## ⚠ CORRECTION — empirical investigation reshaped these facts twice > -> The original "Key facts" said the plugin needs `.lsp.json` at the plugin root, citing PR #378. **That is not how Claude Code's LSP loader actually works.** Verified empirically by inspecting the 12 working official LSP plugins under `~/.claude/plugins/marketplaces/claude-plugins-official/`: +> The original "Key facts" said the plugin needs `.lsp.json` at the plugin root, citing PR #378. After empirical testing on Claude Code v2.1.119 we now have the *actual* picture, which differs from both the research doc AND the official Anthropic docs in subtle ways. Two paths exist, and you need both for a third-party LSP plugin to work: > -> - Their plugin source dirs (`plugins/typescript-lsp/`, `plugins/rust-analyzer-lsp/`, etc.) ship **only `README.md` + `LICENSE`** — no `.claude-plugin/`, no `.lsp.json`. -> - The actual LSP server config lives **inline** in `marketplace.json`'s plugin entry, in a top-level `lspServers` field — alongside `name`, `description`, `version`, `source`. -> - The `lspServers` schema is identical to what the original research doc described (server name → `command` / `args` / `extensionToLanguage` / `initializationOptions` / `startupTimeout` / etc.). It just lives in a different file. -> - PR #378 may have been an aspirational change that was never merged or was rolled back. Don't trust the research doc on the `.lsp.json` claim. +> **The official LSP plugins** (typescript-lsp, rust-analyzer-lsp, etc., 12 of them in `claude-plugins-official`): +> - Plugin source dirs ship only `README.md` + `LICENSE` — no `.claude-plugin/plugin.json`, no `.lsp.json`. +> - `marketplace.json`'s plugin entry has `lspServers` inline alongside `name`/`description`/`version`/`source`. +> - These plugins load via a hardcoded code path inside the Claude Code binary that's wired specifically for the official marketplace name. > -> **What this means for a contributor:** +> **Third-party LSP plugins** (anything not under `claude-plugins-official`): +> - Must ship `.lsp.json` at the plugin root in the source repo. This file gets copied into the user's `~/.claude/plugins/cache////` on install. +> - The runtime LSP loader reads `.lsp.json` from that cached path. **That's what actually registers the server.** +> - `lspServers` in marketplace.json is metadata for the marketplace UI / future-proofing — empirically not what triggers loading for non-`claude-plugins-official` plugins. Keep it anyway, identical to the `.lsp.json` content. +> - Open Anthropic issues describing related bugs and dead-code paths: [#15521](https://github.com/anthropics/claude-code/issues/15521), [#31468](https://github.com/anthropics/claude-code/issues/31468). The picture there is incomplete — `.lsp.json` empirically does work for third parties on v2.1.119. > -> | Old (wrong) claim | Reality | +> **Empirical evidence (this repo, this session, v2.1.119):** +> +> | State | Result | > | --- | --- | -> | Ship `.lsp.json` at plugin root | Don't ship `.lsp.json` at all — Claude Code ignores it | -> | Plugin needs `.claude-plugin/plugin.json` | Optional — official LSP plugins don't ship it. We keep it for identity metadata only | -> | LSP config is plugin-local | LSP config is marketplace-local (lives in marketplace.json `lspServers`) | -> | Test with `claude --plugin-dir ` | Test by registering a marketplace and installing — `--plugin-dir` may not exercise the marketplace-side `lspServers` loader | +> | Plugin in `agent-sh` marketplace, only `lspServers` in marketplace.json (no `.lsp.json`) | Silently skipped. `Checking plugin zig-lsp` appears in debug log, no `Loaded ... LSP server(s)` line follows. | +> | Same plugin replicated under `claude-plugins-official` marketplace cache | Loads. Hardcoded path. | +> | Plugin in `agent-sh` marketplace WITH `.lsp.json` at plugin root → cached in install dir | **Loads.** `Loaded 1 LSP server(s) from plugin: zig-lsp`. | > -> **Updated key facts**: A working LSP plugin is 1 marketplace-level file (`.claude-plugin/marketplace.json` with `lspServers` inline on the plugin entry) + at minimum `README.md` in the plugin source dir. Binary name is `zls`. Extensions: `.zig` and `.zon` → language `"zig"`. Official marketplace submission is form-gated (not PR-based). +> **Updated key facts (canonical):** A working third-party LSP plugin ships `.lsp.json` at the plugin root + an `lspServers` block in `.claude-plugin/marketplace.json` (kept byte-equivalent on server config) + a `plugin.json` for identity metadata + `README.md`. Binary name `zls`. Extensions `.zig` and `.zon` → language `"zig"`. Submission to the official marketplace is form-gated at . > -> The sections below are kept verbatim with the original `.lsp.json` framing as historical record. Refer to `CONTRIBUTING.md` and `CLAUDE.md` in this repo for the corrected, currently-accurate guidance. +> The sections below are kept verbatim with the original `.lsp.json`-only framing as historical record. Refer to `CONTRIBUTING.md` and `CLAUDE.md` in this repo for the corrected, currently-accurate guidance. --- diff --git a/plugins/zig-lsp/.lsp.json b/plugins/zig-lsp/.lsp.json new file mode 100644 index 0000000..e0176ef --- /dev/null +++ b/plugins/zig-lsp/.lsp.json @@ -0,0 +1,22 @@ +{ + "zig": { + "command": "zls", + "extensionToLanguage": { + ".zig": "zig", + ".zon": "zig" + }, + "initializationOptions": { + "enable_snippets": true, + "enable_argument_placeholders": true, + "enable_build_on_save": true, + "build_on_save_args": [], + "semantic_tokens": "full", + "inlay_hints_show_variable_type_hints": true, + "inlay_hints_show_parameter_name": true, + "prefer_ast_check_as_child_process": true + }, + "startupTimeout": 30000, + "restartOnCrash": true, + "maxRestarts": 3 + } +}