Error performing documentSymbol: Command failed with ENOENT: pyright-langserver --stdio
Executable not found in $PATH: "pyright-langserver"
pyright-lspプラグインを有効化した直後にLSP操作を呼ぶと、このエラーが返ってきました。プラグインは言語サーバー本体を同梱しない設計。Claude Code v2.1.258で、この失敗状態から型エラーの自動検出が動くまでを実測します。
プラグインの有効化だけではLSPは動かない
手元の ~/.claude/settings.json はこの状態です。/plugin install pyright-lsp@claude-plugins-official を実行済みで、プラグインとしては有効の状態。
{
"enabledPlugins": {
"pyright-lsp@claude-plugins-official": true
}
}
marketplaceの追加からインストールまでの基本操作はClaude Codeプラグインのインストール手順に書きました。で、この状態でPythonファイルに対してLSPツールを呼ぶと、冒頭のENOENTになります。
プラグインの中身はREADMEとLICENSEだけ
キャッシュディレクトリを見ると、バイナリどころか設定ファイルすらありません。
$ ls ~/.claude/plugins/cache/claude-plugins-official/pyright-lsp/1.0.0/
LICENSE README.md
サーバーの定義は、マーケットプレイスのカタログ ~/.claude/plugins/marketplaces/claude-plugins-official/.claude-plugin/marketplace.json にインラインで書かれていました。
"lspServers": {
"pyright": {
"command": "pyright-langserver",
"args": ["--stdio"],
"extensionToLanguage": { ".py": "python", ".pyi": "python" }
}
}
Claude Codeは command をPATHから解決します。プラグインの仕事は「どのコマンドを、どの拡張子で起動するか」の宣言だけ。pyright本体は利用者が自分で入れる必要があります。同梱のREADME.mdも、npm・pip・pipxの3通りのインストールコマンドを案内しているだけでした。
Claude CodeはプラグインのbinをPATHに足す
Claude CodeのBashツールで echo $PATH を見ると、Claude Codeが ~/.claude/plugins/cache/claude-plugins-official/pyright-lsp/1.0.0/bin を足しています。binディレクトリを持つプラグインなら、実行ファイルをそこへ置くだけで配布が済む造り。pyright-lspはbinを持たないので、この追加は空振りしています。
公式マーケットプレイスのLSPプラグインは12言語
marketplace.jsonを数えると、291プラグイン中12個が lspServers を宣言していました。
| プラグイン名 | 起動コマンド | 対象拡張子 |
|---|---|---|
| pyright-lsp | pyright-langserver | .py .pyi |
| typescript-lsp | typescript-language-server | .ts .tsx .js .jsx など8種 |
| gopls-lsp | gopls | .go |
| rust-analyzer-lsp | rust-analyzer | .rs |
| clangd-lsp | clangd | .c .h .cpp など9種 |
| jdtls-lsp | jdtls | .java |
| kotlin-lsp | kotlin-lsp | .kt .kts |
| csharp-lsp | csharp-ls | .cs |
| ruby-lsp | ruby-lsp | .rb .rake .erb など5種 |
| php-lsp | intelephense | .php |
| lua-lsp | lua-language-server | .lua |
| swift-lsp | sourcekit-lsp | .swift |
構造はどれも同じで、宣言されたコマンドをPATHから探して起動するだけです。バイナリの入手はすべて利用者側の仕事になります。
pyright本体を入れて踏んだ、stdoutの汚染
uv tool installは1.3秒で終わる
$ uv tool install pyright
Installed 3 packages in 48ms
+ nodeenv==1.10.0
+ pyright==1.1.411
+ typing-extensions==4.16.0
Installed 4 executables: pyright, pyright-langserver, pyright-python, pyright-python-langserver
実測1.3秒。pyright 1.1.411 と実行ファイル4つが ~/.local/bin に入ります。依存の nodeenv が示すとおり、PyPI版のpyrightはMicrosoftのnpm版本体を呼び出すラッパーです。Node.jsと本体が見つからなければ、初回実行時に自動でダウンロードが走ります。
初回起動がLSPのヘッダーを壊す
インストール直後にもう一度LSPツールを呼ぶと、今度は別のエラーになりました。
Error performing documentSymbol: LSP server sent non-protocol output
in the header block — its stdout is desynchronized from the base protocol
LSPはstdout上で Content-Length ヘッダー付きのフレームをやり取りします。ラッパーが初回ダウンロードの進捗をstdoutに書き出すため、プロトコルの同期が失われて切断される。対処は、先に一度CLIとして起動しておくことです。
$ pyright --version
pyright 1.1.411
これで本体の取得が済み、以降の pyright-langserver --stdio はstdoutに余計な出力をしなくなります。実際、直後に pyright-langserver --stdio < /dev/null を実行してstdoutを数えると0バイト。npm版(npm i -g pyright)を直接入れる場合、このラッパー起因の問題は起きません。
LSPツールの9操作を実測する
ツール定義には goToDefinition、findReferences、hover、documentSymbol、workspaceSymbol、goToImplementation、prepareCallHierarchy、incomingCalls、outgoingCalls が並びます。diagnosticsは操作に含まれません。診断は後述のとおり、呼ばなくても勝手に届くからです。
documentSymbolとhover
このブログの投稿スクリプト(wordpress.py、約480行)に対するdocumentSymbolの結果の抜粋です。
Document symbols:
_with_retry (Function) - Line 19
_get_auth (Function) - Line 55
get_category_id (Function) - Line 63
get_orphan_posts (Function) - Line 205
limit (Variable) - Line 205
posts (Variable) - Line 215
publish_post (Function) - Line 407
関数だけでなくローカル変数まで階層付きで返ります。hoverはシグネチャとdocstringを返しました。
(function) def get_orphan_posts(limit: int = 15) -> list[dict[Unknown, Unknown]]
---
他の記事の本文から一度もリンクされていない記事を新しい順で返す
戻り値が list[dict[Unknown, Unknown]] になっているのは、型ヒントを省いた自作コードをpyrightがどこまで推論できたかの正直な答えです。
incomingCallsはGrepで代替できない
認証情報を読む内部関数 _get_auth でfindReferencesを実行すると、定義1件と呼び出し3件の計4件が返ります。これは grep -n "_get_auth" でも同じ4行が見つかるので、差はまだ小さい。差が開くのはincomingCallsです。
Found 3 incoming calls:
get_recent_titles (Function) - Line 171 [calls at: 174:24]
get_orphan_posts (Function) - Line 205 [calls at: 213:24]
publish_post (Function) - Line 408 [calls at: 414:20]
「どの行に文字列があるか」ではなく「どの関数の中から呼ばれているか」が返ります。テキスト検索では書けない問い合わせで、同名の別シンボルも区別できるため、リネームや破壊的変更の影響調査で効きます。
編集した直後に型エラーが会話へ注入される
公式ドキュメント plugins-reference の “LSP servers” セクションには、設定フィールド diagnostics があります。定義は「診断の自動注入、デフォルトtrue」。何が起きるのか、わざと型エラーを書いて確かめました。
def add(a: int, b: int) -> int:
return a + b
result = add("1", 2)
result.upper()
この編集を保存した直後、次のツール実行の結果と一緒にこのブロックが会話へ入ってきました。
<new-diagnostics>The following new diagnostic issues were detected:
lsp_scratch.py:
✘ [Line 5:14] Argument of type "Literal['1']" cannot be assigned to
parameter "a" of type "int" in function "add" [reportArgumentType]
✘ [Line 6:8] Cannot access attribute "upper" for class "int"
[reportAttributeAccessIssue]
pytestもmypyも走らせていません。書いた瞬間に2件のエラーが届き、Claudeは同じターン内で修正に入れます。編集→実行→エラーを読んで修正、というループの1周目がまるごと省ける。この自動注入がLSP連携の本体です。VS Codeなら、言語サーバーの診断はエディタが赤線で表示します。CLIのClaude Codeは、自前のLSPクライアントで診断を会話に流し込む方式。エディタ側との関係はClaude Code VS Code拡張の設定で扱いました。
venvを見ていない警告が混ざる
実は最初の診断には、型エラーとは別の警告も混ざっていました。
⚠ [Line 10:8] Import "requests" could not be resolved from source
[reportMissingModuleSource]
requests は .venv にインストール済みなのに、です。pyrightはプロジェクトの仮想環境を自動では探しに行かない。放置すると外部ライブラリ絡みの誤検知が診断に混ざり続けます。リポジトリ直下に pyrightconfig.json を置いて、venvの場所を明示します。
{
"venvPath": ".",
"venv": ".venv"
}
venvPath と venv はpyright公式のConfigurationドキュメントに定義されているキーです。LSPを入れた直後に警告が大量に届いたら、先にこのファイルの有無を確認してください。
.lsp.jsonのフィールドとENABLE_LSP_TOOLの現在
自作プラグインで言語サーバーを繋ぐ場合は、plugin.jsonの lspServers か専用の .lsp.json に書きます。”LSP servers” セクションにあるオプションフィールドの抜粋です。
| フィールド | デフォルト | 役割 |
|---|---|---|
| transport | stdio | 通信方式。socketも選べる |
| diagnostics | true | 診断の自動注入 |
| restartOnCrash | true | クラッシュ後の自動再起動 |
| maxRestarts | – | 再起動の試行上限 |
| startupTimeout | – | 起動待機時間(ミリ秒) |
stdout汚染で切断された後も、呼び直すと新しいサーバープロセスが立ち上がりました。切断が即、手動復旧になるわけではない。
なお2026年3月ごろの海外の解説記事では、settings.jsonの env に ENABLE_LSP_TOOL=1 を足す手順が定番でした。v2.1.258では不要。手元のsettings.jsonに env キー自体が無い状態で、LSPツールは呼べました。
まとめ
- LSPプラグインは
lspServersの宣言だけ。pyright本体はuv tool install pyrightなどで自分で入れる(公式マーケットプレイスの12言語すべて同じ構造) - PyPI版pyrightはラッパーで、初回起動がstdoutを汚してプロトコルを壊す。先に
pyright --versionを1回実行してから繋ぐ - 編集直後の診断注入がデフォルト有効。
pyrightconfig.jsonのvenvPathを書かないとreportMissingModuleSourceの誤検知が混ざる - Grepで代替できないのはincomingCallsなどの呼び出し階層系。影響調査の道具として使い分ける