Claude CodeにLSPツールが追加
数ヶ月前の「Serena MCPはClaude Codeを救うのか?」で「Claude Code本体にLanguageServer統合を追加する可能性も考えられます」と書きましたが、その機能が2.0.74で有効になりました。
導入方法は「Claude Code の LSP サポート」が詳しいので参照ください。

まとめると/plugin コマンドからanthropics/claude-plugins-official 経由でインストールするか、.claude-plugin/plugin.json にLSPサーバのコマンドを記述して自作プラグインとして起動します。つまりプラグインの一部として”mcpServers”などとど同列に”lspServers”を定義する仕組みです[1]。
[1]: https://code.claude.com/docs/en/plugins
注意点としては最新版2.0.74ではコネクション確立に失敗する[2]ので安定版2.0.67をフィーチャーフラグ付きで起動して使うことです(まぁ明日には直っていることでしょう)。筆者は以下のコマンドから起動しました。
[2]: No LSP server available と出力されます
$ ENABLE_LSP_TOOL=1 npx @anthropic-ai/claude-code@stable検証のため筆者がカスタムプラグインにセットしたのは以下のようなJSONファイルです。typescript-language-serverコマンドが実行できる必要があります。
% npm install -g typescript-language-server typescript
% typescript-language-server -V
5.1.3
% cat .claude-plugin/plugin.json
{
"name": "my-plugin",
"lspServers": {
"typescript": {
"command": "typescript-language-server",
"args": [
"--stdio"
],
"extensionToLanguage": {
".ts": "typescript",
".tsx": "typescriptreact",
".js": "javascript",
".jsx": "javascriptreact",
".mts": "typescript",
".cts": "typescript",
".mjs": "javascript",
".cjs": "javascript"
}
}
}
}%現在ディレクトリから以下のように起動すると。プラグインが有効になります。/plugin コマンドでInstalledになっていれば成功です。JSONの記述ミスなどがある場合はErrorsタブにバリデーション結果が表示されます。
$ ENABLE_LSP_TOOL=1 npx @anthropic-ai/claude-code@stable --plugin-dir .
lspServers の有効な設定は https://github.com/anthropics/claude-plugins-official/blob/main/.claude-plugin/marketplace.json から借用するのが良いです。メジャーな言語ならすでに揃っています。筆者はPHP(intelephense, phpactor), Go(gopls), Rust(rust-analyzer),Python(pyright)が動作することを確認しました。
LSPツールの基本動作
タイトルで「LSPツール」という呼び方をしたのはこれが「Readツール」や「Searchツール」などのClaude CodeコアのMCPツールの1つだからです。現在のLSPツールはシンボル探索などの読み込み操作のみでSerenaのような書き込み系の機能は提供していません。またPlanモードのExploreツールや別サブエージェントを経由して透過的に使われることもないので、ある程度明示的に「LSPで検索してください」とプロンプトベースで指示する必要があります。
そして万能ではありません。例えば以下の「LSPで全てのClassをリストアップしてください」というタスクではworkspaceSymbolを呼び出したのちに普通にGrepに切り替えました。
> LSPで全てのClassをリストアップしてください
⏺ LSPのworkspaceSymbol操作を使用して、プロジェクト内のクラスを検索します。まず、適切なTypeScript/JavaScriptファイルを見つけてLSP操作を実行します。
⏺ Search(pattern: "src/**/*.ts")
⎿ Found 100 files (ctrl+o to expand)
⏺ LSP(operation: "workspaceSymbol", file: "src/CONFIG.ts")
⎿ Found 21 symbols (ctrl+o to expand)
⏺ workspaceSymbol操作では限定的な結果しか得られませんでした。LSPのworkspaceSymbolはクエリ文字列を指定できないため、全シンボルを取得するのは難しいです。
代わりに、Grepを使用してプロジェクト内のすべてのクラス定義を検索します:
⏺ Search(pattern: "^(export\s+)?(abstract\s+)?class\s+\w+", glob: "*.{ts,tsx,js,jsx}", output_mode: "content")
⎿ Found 355 lines (ctrl+o to expand)
⎿ Found 6 new diagnostic issues in 1 file (ctrl+o to expand)次に特定のクラスの関数のリストアップをしてもらいます。これは期待どおりに素早く探索できました。
> BaseArrowKeyFocusManagerの関数をリストアップしてください。
⏺ LSPのdocumentSymbolを使用してクラス内の関数をリストアップします。
⏺ LSP(operation: "documentSymbol", file: "src/components/ArrowKeyFocusManager.js")
⎿ Found 31 symbols (ctrl+o to expand)
⏺ BaseArrowKeyFocusManager クラスの関数一覧
src/components/ArrowKeyFocusManager.js:39
| メソッド名 | 行番号 | 説明 |
|----------------------|--------|------------------------------------------------------------------------------------|
| componentDidMount | 40 | コンポーネントマウント時のライフサイクルメソッド(キーボードショートカットの登録) |
| componentDidUpdate | 64 | コンポーネント更新時のライフサイクルメソッド |
| componentWillUnmount | 73 | コンポーネントアンマウント時のライフサイクルメソッド |
| onArrowUpKey | 83 | 上矢印キー押下時のハンドラー |
| onArrowDownKey | 102 | 下矢印キー押下時のハンドラー |
| render | 121 | レンダリングメソッド |
---
同ファイル内にはArrowKeyFocusManagerというHOC(Higher-Order Component)ラッパークラスも定義されています(Line 126)。次はIDEでよく行うシンボル検索を試してみます。こちらも通常のファイル検索より高速に結果を返してくれました。
> BankAccountを参照しているコンポーネントを全て教えてください
⏺ LSPのfindReferencesを使用してBankAccountクラスの参照を検索します。
⏺ LSP(operation: "findReferences", symbol: "BankAccount", in: "src/libs/models/BankAccount.ts")
⎿ Found 33 references across 10 files (ctrl+o to expand)
⏺ BankAccount を参照しているファイル一覧
LSPのfindReferencesで10ファイル、33箇所の参照が見つかりました。
コンポーネント / ページ
| ファイル | 参照行 |
|--------------------------------------------------------------------------------------|------------------------|
| src/pages/ReimbursementAccount/ReimbursementAccountPage.js | 19, 224, 235, 404, 406 |
| src/pages/ReimbursementAccount/ConnectBankAccount/ConnectBankAccount.tsx | 11, 46, 56, 57 |
| src/pages/ReimbursementAccount/BeneficialOwnerInfo/substeps/CompanyOwnersListUBO.tsx | 16, 27 |
| src/pages/workspace/WorkspacePageWithSections.tsx | 16, 126 |
| src/pages/workspace/WorkspaceResetBankAccountModal.js | 9, 35 |
| src/pages/workspace/reimburse/WorkspaceReimburseSection.tsx | 14, 32 |
ライブラリ / ユーティリティ
| ファイル | 参照行 |
|------------------------------------------------|-------------------------------------------|
| src/libs/models/BankAccount.ts | 7, 20, 78, 92, 99, 106, 110, 233 (定義元) |
| src/libs/actions/ReimbursementAccount/store.ts | 3, 25 |
| src/libs/PaymentUtils.ts | 8, 17 |
PropTypes定義
| ファイル | 参照行 |
|-----------------------------------------------------------------|--------|
| src/pages/ReimbursementAccount/reimbursementAccountPropTypes.js | 2, 38 |
---実践:巨大リポジトリの探索タスクで効果を検証する
さて基本的なLSP機能は確認できたので、実際にこれがどのような効果をもたらすのかを確認します。気になるのはやはりトークン削減効果、そして探索の精度、実行速度です。
巨大なソースコードということで題材には「Kiroとコンテキストエンジニアリングの時流」で紹介したSWE-Lancerベンチーマークの開発対象であるExpensify/Appを使います。これは数十万行規模のReact Nativeアプリです。
タスクは「Kimi K2とLLMのベンチマークスコア」でも取り上げた、同じくSWE-LancerのManager タスクを1つ選びます。これはExpensifyの実際のIssueに存在する議論を元にソースコードを探索してコメントに紐づいた提案番号(解決策)を選ぶという、アーキテクチュアリングのクイズのようなものです。これは実世界のプロジェクトで意思決定された結果です。つまり正解といっても絶対的な正しさというよりN=1のプロジェクトに合致するというだけですが「特定のコンテキストで人間と同じような判断をするのか」というモデルの判断の参考にはなります。
まずExpensify/Appのリポジトリをチェックアウトして該当タスクのコミットに切り替えます。SWE-Lancerリポジトリにall_swelancer_tasks.csvというファイルがありそこにIssueとそのコメントと正解番号が記録されているので、スクリプトで抜き出してClaude Codeに与えるpromt.txtに保存しました。全文は長いので要約するとこのような指示になります:
問題: Workflowsで銀行口座未登録時に「Connect bank account」ボタンが表示されるべきなのに、「Bank account」と「Authorized payer」ボタンが表示される。
根本原因: 銀行口座未登録時にも、トグル操作時にachAccount: {reimburser: reimburserEmail}が楽観的に設定されるため、UI判定条件(hasVBA = !!policy?.achAccount)が真になってしまう。
提案の要点:
提案0/1/2: 条件チェック方法の改善(bankAccountIDの有無で判定、hasVBAの改善)
提案3: ローディング状態の追加と条件判定の完全な修正
提案4: reimburser設定の代替案
提案5: その他の解決策を求めるこの「提案番号0-5」の中から1つを選ぶというタスクをClaude Codeに依頼します。その際にソースコードをどのように探索してほしいのかを2パターン作りました。ひとつは「SerachとGrepで検索してください」という追加プロンプトで、もう一つが「LSPツールで検索してください」という文章です。この2つを先ほどのpromt.txtに結合して、2回のセッションで同じタスクを試して比較します。
最初は「SerachとGrepで検索してください」=Grepケースです。Claude Codeの探索はReadツールで3ファイル100−2000行ほど読み込み。数十秒で思考を終え”45991 tokens”を消費しました。「提案1」を選びました。
⏺ コードを確認しました。分析結果を報告します。
1
理由:
問題の根本原因:
WorkspaceWorkflowsPage.tsxの87行目で hasVBA が以下のように定義されています:
const hasVBA = !!policy?.achAccount;
一方、Policy.tsのsetWorkspaceReimbursement関数(722-765行目)では、「Make or track payments」をトグルすると、optimisticDataとして以下が設定されます:
achAccount: {reimburser: reimburserEmail}
これにより、実際の銀行口座が接続されていなくても policy?.achAccount がtruthyになり、hasVBA がtrueになってしまいます。
Proposal 1が最適な理由:
1. シンプルで直接的な修正: !!policy?.achAccount を !!policy?.achAccount?.bankAccountID に変更するだけで、実際に銀行口座が接続されているかを正確に判定できます。
2. ACHAccount型の定義を見ると (Policy.ts 187-194行目)、bankAccountIDは実際の銀行口座が連携された時のみ設定されるフィールドです。reimburserだけが設定されている状態では銀行口座は未接続とみなすべきです。
3. 最小限の変更: Proposal 3のようにローディング状態を追加したり、複数箇所を修正する必要がなく、1行の変更で問題を解決できます。
4. Proposal 0は曖昧: 「すべてのフィールドが定義されている場合のみ」という提案は具体的なコードがなく、過度に厳しい条件になる可能性があります。
5. Proposal 2, 4はoptimisticDataを変更: 既存のオフライン対応機能(PR #39017で追加)を壊す可能性があります。次に「LSPツールで検索してください」を試します。想定どうりLSPツールを使った調査が開始されます。
⏺ 問題を正しく理解するために、実際のソースコードを調査します。
Phase 1: シンボル抽出
調査対象のシンボル:
- hasVBA
- achAccount
- bankAccountID
- setWorkspaceReimbursement
- reimburser
Phase 2: LSP調査
⏺ LSP(operation: "documentSymbol", file: "src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx")
⎿ Found 75 symbols
Document symbols:
default (Constant) - Line 312
betas (Property) - Line 314
key (Property) - Line 315
WorkspaceWorkflowsPage (Class) - Line 45
constructor (Constructor) - Line 45
canUseDelayedSubmission (Constant) - Line 53
confirmCurrencyChangeAndHideModal (Constant) - Line 67
useCallback() callback (Function) - Line 67次の行程ではReadでファイル全文を読み取ります。しかしここで”✘ [Line 1:30] Cannot find module”のようなLSPの参照結果を使った情報が一緒に表示されました。これはLSPツールを組み合わせたとき特有の動作です。
⏺ Search(pattern: "src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx")
⎿ Found 1 file
/work/laiso/sandbox/repos/expensify-app/src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx
∴ Thinking…
良いです。ファイルが見つかりました。次に、hasVBAの定義とその使用箇所を調査します。
⏺ Read(src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx)
⎿ Read 319 lines
⎿ src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx (/Users/kstg/work/laiso/sandbox/repos/expensify-app/src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx):
⎿ ✘ [Line 1:30] Cannot find module '@react-navigation/native' or its corresponding type declarations. [2307] (typescript)
⎿ ✘ [Line 2:37] Cannot find module '@react-navigation/stack' or its corresponding type declarations. [2307] (typescript)
⎿ ✘ [Line 3:53] Cannot find module 'react' or its corresponding type declarations. [2307] (typescript)状況に応じてシンボル名からLSPツールを呼び出します
⏺ 次に、setWorkspaceReimbursement関数を確認します:
⏺ Search(pattern: "setWorkspaceReimbursement", path: "src/libs/actions")
⎿ Found 1 file
src/libs/actions/Policy.ts
⏺ LSP(operation: "goToDefinition", symbol: "Policy", in: "src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx")
⎿ Found 1 definition
Defined in src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx:27:13結果はGrepケースと同じく「提案1」を選択しました。しかしレポートがかなり詳細になりました。
⏺ 分析結果まとめ
問題の根本原因
WorkspaceWorkflowsPage.tsx:87でhasVBAが以下のように定義されています:
const hasVBA = !!policy?.achAccount;
Policy.ts:731のsetWorkspaceReimbursement関数では、トグルをオン/オフするたびにoptimisticDataとして以下が設定されます:
achAccount: {reimburser: reimburserEmail}
これにより、実際の銀行口座が追加されていなくてもachAccountオブジェクトが存在し、hasVBAがtrueになってしまいます。
ACHAccount型の定義(Policy.ts:187-194):
type ACHAccount = {
bankAccountID: number;
accountNumber: string;
routingNumber: string;
addressName: string;
bankName: string;
reimburser: string;
};
銀行口座が実際に追加されると、bankAccountIDなどのフィールドがバックエンドから返されます。しかし、トグル操作だけではreimburserのみが設定されます。
各提案の評価
| Proposal | 評価 |
|----------|----------------------------------------------------------------------------|
| 0 | 根本原因は正しいが、具体的なコード変更が示されていない |
| 1 | シンプルで正確な修正:!!policy?.achAccount?.bankAccountID |
| 2 | 過度に複雑な代替案 |
| 3 | 正しいアプローチだがオーバーエンジニアリング(ローディング状態の追加など) |
| 4 | 補足的なコメントのみ |
| 5 | 提案ではなくプロセスに関するコメント |
---
1
Proposal 1が最適な解決策です。
理由:
1. 最小限の変更で問題を解決: const hasVBA = !!policy?.achAccount?.bankAccountID; という1行の変更のみで、問題が根本的に解決されます。
2. 正確な条件チェック: bankAccountIDは銀行口座が実際にバックエンドで登録された場合にのみ設定されます。reimburserだけが設定されている(トグル操作のみ)場合はbankAccountIDはundefinedのため、hasVBAは正しくfalseになります。
3. 既存のロジックとの整合性: 行168、171、180、183、200で使用されているhasVBAのすべての用途において、「実際に銀行口座が存在するか」を確認することが意図されているため、bankAccountIDの存在チェックは論理的に正しいです。
4. シンプルさ: Proposal 3のようにローディング状態の追加や複数のチェックの変更は、このバグ修正には不要です。問題はシンプルで、解決策もシンプルであるべきです。消費したトークンは”39150 tokens”とGrepケースより削減されました。レポートが詳細になってトークンコストが減るならLSPツールの恩恵があると言えそうです。なお実行時間については2つのケースで差はありませんでした。
ちなみにデータセット上の正解は「提案5: その他の解決策を求める」でした。現実は非情である。
まとめ
Claude Codeに導入されたLSPツールは、巨大なコードベース探索において 精度向上とトークン削減効果 をもたらす有効な手段であることが今回は確認できました。従来のGrep検索と比較すると、結果の詳細さやレポート品質が向上し、消費リソースも抑えられるのはメリットです。
ただし現状は「読み込み系」機能に限定されており、万能ではありません。LSPツールの実行になる条件もまだ明確でなく、控えめに設定されている印象です。また、workspaceSymbolのように制約がある場面では、依然としてGrepにフォールバックされ協調します。今後のアップデートや書き込み系機能の追加も期待されます。
