Skip to content

NDF の利用者が Claude Code と Codex で Python / PHP / TypeScript の言語サーバ(定義・参照・診断)を使えるようにする #818

Description

@takemi-ohama

何をしたいか

NDF を入れた Claude Code と Codex.py / .php / .ts.js)を扱うとき、定義・参照・診断(型エラー・未解決の import)と、コードを読む量を抑えた編集ができるようにする。

方針(実測にもとづく結論)

ランタイム 診断 シンボルの把握・参照・編集
Claude Code plugin LSP servers Serena
Codex Serena Serena

役割を分け、同じ言語で機能を重複させない。Serena 側の #858 も、重複したツールが並ぶとコンテキストの無駄になると指摘している。

具体例

plugins/ndf/scripts/lib/monitor.py(1,247 行)の _safe_size を、例外のときに 0 ではなく -1 を返すよう変え、呼び出し側への影響を調べる。今は Read でファイル全体(48,847 文字)が入り、型の食い違いは pytest を流すまで分からない。

調べて分かったこと

1. Serena はほぼ使われていない(手元のセッション記録で実測)

2026-06-24〜09-22 のセッション記録から、tool_use を集計した。

ランタイム 記録数 Serena の呼び出し 参考
Claude Code(サブエージェントを含む) 約 7,500 ファイル 10 回(4 セッション)replace_in_files 3 / initial_instructions 3 / get_symbols_overview 2 / activate_project 1 Bash 98,660 / Read 8,262 / Edit 8,523 / Grep 173
Codex 約 4,000 ファイル 0 回。ただし codex plugin listmcp-serena@ai-pluginsnot installed だった。使われなかったのではなく、入っていなかった bigquery 100 / dbhub 96

本来の用途である find_symbol / find_referencing_symbols は一度も呼ばれていない。

2. なぜ使われなかったか

呼んだ 10 回のうち 7 回が失敗していた。 作業として成功したのは Markdown の文字列置換 1 回だけで、sed でも済む作業だった。

日時・場所 呼び出し 結果
06-27 carmo-predict-contract(サブエージェント) get_symbols_overview ×2 No active project で失敗し、以後は呼ばれなかった
08-07 ai-plugins replace_in_filesactivate_projectreplace_in_files ×2 1 回目は No active project、2 回目は expected_count の食い違いで失敗し、3 回目で成功
08-13 検証用リポジトリ initial_instructions ×3 権限が許可されず、最後は拒否

原因は 4 つある。いずれも配布設定の不備で、モデルの性質ではない。

  1. 言語の設定が、実際のコードと合っていない。 /work/ai-plugins/.serena/project.ymllanguage_servers- bash だけで、このリポジトリの .py 322 個(約 9 万行)にシンボル機能が効かない。手元の /work/*/.serena/project.yml 9 個のうち、言語を設定しているのは 3 個だけだった
  2. 使い始めるまでの段階が多い。 ツールは遅延読み込みで ToolSearch が要り、activate_project が要り(--project-from-cwd が無いため)、Serena 自身が先に initial_instructions を読むよう求める
  3. 指示の中での扱いが弱い。 CLAUDE.md やエージェント定義には「Serena を活用」とあるだけで、どの場面で grep ではなく Serena を使うかが書かれていない。qa.md のツール名は古い名前空間 mcp__plugin_ndf_serena__* のままで、今のツールに届かない
  4. Serena に触れているエージェント(corder など)が呼ばれる回数も少ない

3. 読み込む量の実測(Claude Code、sonnet-5、同じタスクを 5 条件)

題材は monitor.py(1,247 行)の _safe_size の変更と、呼び出し側への影響の報告。

条件 使われたツール ツール結果の文字数 最終手番のコンテキスト
A 素の状態 Read(全体)→ Edit 51,631 68,532
B LSP あり・指示なし Read(全体)→ Edit 49,057 66,094
C Serena あり・指示なし Read(全体)→ Edit 49,467 66,985
D Serena あり・使うよう指示 find_symbol / find_referencing_symbols / replace_symbol_body 18,577 51,355
E LSP あり・使うよう指示 LSP(documentSymbol / findReferences)+ 範囲を絞った Read 4 回 13,517 49,466
  • 指示しなければ、どちらも使われずファイル全体が読まれる。 差を決めているのは機能ではなく誘導である
  • 起動時の約 39.5k トークンを除いた増分で比べると、素の状態 29k に対し D は 11.4k、E は 10k。Serena と LSP の節約量はほぼ同じ
  • D の読み込みの半分(9,208 文字)は initial_instructions だった。これを外せば Serena が有利になる
  • Serena を載せても初回のコンテキストはほとんど増えない(D 39,925 / E 39,480 トークン)。ツールが遅延読み込みのため、併用のコストは小さい

4. Claude Code の plugin LSP servers(実測)

--plugin-dir で LSP 定義だけのプラグインを読み込ませて確認した。

  • 編集後の診断は自動で届く。 次の手番の system-reminder<new-diagnostics> として入る(Python の reportOperatorIssue、TypeScript の 2362 を確認)
  • 届くのは編集したファイルの分だけ。 util.py の型を変えて壊れた呼び出し側 a.py の分は届かなかった
  • 呼べる操作は 9 種類(goToDefinition / findReferences / hover / documentSymbol / workspaceSymbol / goToImplementation / prepareCallHierarchy / incomingCalls / outgoingCalls)。診断を取る操作も rename も無い
  • LSP ツールも遅延読み込みで、ToolSearch を 1 回挟む
  • 言語サーバ本体は利用者が入れる。同じ拡張子を複数のプラグインが宣言すると、最初に登録されたものだけが動く
  • 言語サーバ本体が無くても、セッション中には何も表示されない。 実在しないコマンドを lspServers に宣言したプラグインで .rb を編集したが、警告もエラーも届かず、診断が来ないだけだった。「動いているつもりで動いていない」状態に気付けないため、検査は自前で持つ必要がある
  • 公式の typescript-lsp は、案内どおりに入れると動かない。 npm install -g typescript で入る latest は 7.0.2(Go への移植版)で tsserver.js を持たないため、typescript-language-server が初期化に失敗する。しかも失敗は表示されず、診断が届かないだけになる。typescript@5 に固定すると動いた(Serena は ^5.9.3 を自分で入れるため、この問題を踏まない)

5. Codex には LSP の仕組みが無い(実測)

codex --version は 0.154.0。codex features list にも設定キーにも lsp 系が無い。Codex で LSP を使うには MCP でつなぐしかない。

Codex の利用上限のため Codex からは試せず、代わりに MCP クライアントから Serena を直接呼んで確かめた(--context codex --project-from-cwd)。

言語 言語サーバ(Serena が自分で入れる) find_referencing_symbols get_diagnostics_for_file
Python pyright 通った reportArgumentType を返した
TypeScript typescript-language-server 通った 2345 を返した
PHP intelephense 通った (無料版は型の食い違いを診断しないと見ている。要確認)
  • --project-from-cwd を付けると、activate_project を呼ばずに最初の呼び出しから通る。 ただし、この版ではツール一覧から activate_project が消えはしなかった
  • 言語サーバ本体を利用者が入れなくてよい~/.serena/language_servers/static/ へ Serena が入れる)
  • 自動で入れた言語サーバのキャッシュが壊れることがある。手元では TypeScript のものが ENOENT ... node_modules/package.json で初期化に失敗した(symlink が実体のコピーになっていた)。退避して入れ直させると動いた

6. Serena 以外の LSP-MCP は、配って使うには弱い

候補 最近の更新 1 つのサーバで扱える言語 言語サーバ本体
oraios/serena 29,715 毎日。正式版 v1.7.0(2026-08) 複数 自動で入る
isaacphi/mcp-language-server 1,596 実質 2025-05 で停止 1 言語--workspace の絶対パスも要る) 利用者が入れる
ktnyt/cclsp 676 2026-02 が最後 複数 利用者が入れる
jonrad/lsp-mcp、Tritlo/lsp-mcp 191 / 124 2025 で停止 利用者が入れる

7. 公式の hook で「使わせる」仕組みがある(外部調査)

Serena には公式の hook(serena-hooks)がある。

  • PreToolUse で grep や Read が続くと permissionDecision: "deny" を返して Serena の使用を促し、直後にカウンタを戻すため作業は止まらない(閾値は grep 3 回 / コードファイルの Read 3 回 / 混在 4 回、120 秒のクールダウン)
  • SessionStartactivate、Serena ツールの auto-approveSessionEndcleanup もある
  • 公式サンプルの matcher mcp__serena__* は、私たちの配布名 mcp__plugin_mcp-serena_serena__* に一致しない。 書き換えが要る
  • serena-hooksuvx で毎回起動すると、ツール呼び出しのたびに遅延が乗る。プラグインに実装を同梱するか、導入時に uv tool install serena-agent を求めるかを決める(未計測)
  • 公式ドキュメントは、Claude Code の組み込みツールの説明が強いバイアスになると認めており、serena prompts print-cc-system-prompt-override と hook を勧めている
  • Serena 本体を Skill にする案は、上流が #802 で否定している(言語サーバを毎回起動することになるため)。私たちが作るのは「Serena を呼ばせる薄い Skill / hook」である

出典: Connecting Your MCP Client / #802 / #858 / plugins reference

8. プロジェクトに依存しない設計にするための実測

このリポジトリ専用の設定にしない。言語は導入先ごとに違うため、検出して設定する形にする。

  • Serena は約 45 言語に対応しているsolidlsp/ls_config.pyLanguage。python / typescript / php / go / ruby / java / rust / csharp / kotlin / swift / bash / terraform / vue / svelte など)。拡張子との対応も Serena 自身が持つ(Language.get_source_fn_matcher
  • serena project create の言語推定は、最も多い言語だけを自動で選び、残りは対話で確認する。 複数言語のリポジトリで試すと Enable typescript (28.57% of source files)? [y/N] と聞かれ、非対話では Error: EOF when reading a line で失敗した。ai-plugins が bash だけになっているのは、この経路を通ったためと見られる
  • 非対話の手段はある。 serena project create --ls python --ls typescript ... のように --language を複数回渡せる
  • 単一言語のリポジトリでは、自動生成された project.yml は正しく検出していた(python / typescript / php のプローブで確認)
  • 検出は作成時の 1 回だけで、以後は更新されない。 言語が増えたリポジトリでは設定が古いまま残る

不要な言語を足したときの代償(混在リポジトリで実測。Python 3 / TypeScript 2 / PHP 1 / bash 1 ファイル)

python のみ python + typescript + php + bash
初回の find_symbol 0.1 秒 4.2 秒
メモリ(既存プロセス 345MB を差し引いた分) 約 344MB 約 880MB(2.5 倍)
initialize 1.9 秒 1.7 秒(差は無い)

いちばん大きいのは、1 つの失敗が全体を止めることである。 4 言語で試した 1 回目、bash の言語サーバが壊れていた(Cannot find module './server')ため、Python の find_symbol まで The language server manager is not initialized で失敗した。Serena は Failed to start 1 language server(s) で初期化ごと失敗させる。つまり使っていない言語を足すほど、使っている言語の機能が道連れで止まる確率が上がる。手元では TypeScript と bash の 2 つでキャッシュ破損が起きていた。

ディスクも増える(intelephense 162MB、bash 48MB、TypeScript 26MB。初回は自動ダウンロードの待ちも入る)。

9. プロジェクトに依存しない仕様(提案)

言語ごとに変わるのは対応表だけにし、手順と hook は言語に依存させない。

(1) 言語の決め方(共通)

  1. git ls-files の拡張子を数える(追跡対象だけを見る。生成物や node_modules を拾わないため)
  2. 件数の多い順に、対応表で言語へ変換する。しきい値(例: 10 ファイル以上、または全体の 5% 以上)を超えたものだけを採る。しきい値を置くのは、上の代償(メモリ 2.5 倍、初回 42 倍、道連れの失敗)を避けるためである
  3. 確認は取らずに書くserena project create --ls <言語> ...
  4. 書いたあと、1 言語ずつ起動を検証し、失敗した言語は設定から外す。 Serena は 1 つの失敗で全シンボル操作を止めるため、これが確認より効く
  5. 足した言語、外した言語とその理由を報告する

(2) 成果物は 3 つ

成果物 役割 言語への依存
Skill(導入・検査) 検出 → 確認 → project.yml の生成 → 必要な言語サーバの案内 → 動作確認 対応表のみ
hook(SessionStart) 検出結果と project.yml の食い違い、未導入の言語サーバを知らせる 対応表のみ
hook(PreToolUse) grep や Read が続いたときにシンボル単位の手順へ誘導する 無し(拡張子の一覧は設定で持つ)
  • 対応表は 1 箇所に置く(例: plugins/ndf/.../languages.json)。拡張子 → Serena の言語識別子 → 公式 LSP プラグイン名 → 言語サーバ本体の導入コマンド、の 4 列
  • SessionStart の通知は、食い違いがあるときだけ出す。毎回出すと今の echo と同じく読み飛ばされる

(3) Claude Code の LSP は、NDF に定義を持たせない

導入先の言語は分からないため、lspServers を NDF が固定で持つと、使わない言語の定義が全員に載る。代わりに Skill が検出結果に応じて公式プラグイン(pyright-lsp / typescript-lsp / php-lsp / gopls-lsp / ruby-lsp など)の導入を案内する。4 で分かったとおり、本体が無くても何も表示されないため、Skill 側で導入の有無を検査する

(4) 導入先のリポジトリを汚さない

  • .serena/project.yml は導入先に作られる。.gitignore へ入れるか、追跡するかは導入先の判断とし、Skill は選ばせる(このリポジトリでは serena_config.ymlauth_secret が入る問題がある)
  • 作業ツリー(.worktrees/<ブランチ名>)でも動くこと。--project-from-cwd は最も近い境界を選ぶため、作業ツリー側が有効化される
  • 読み取り専用で始められること。検出だけを実行し、設定を書く前に必ず確認を取る

10. ツール名の長さと、起動の仕方(実測)

ツール名は mcp__plugin_<プラグイン名>_<サーバ名>__<ツール名> である。 小さな MCP サーバを作り、名前を変えて確かめた。

プラグイン名 .mcp.json のサーバ名 実際のツール名
mcp-serena serena mcp__plugin_mcp-serena_serena__ping(接頭辞 30 文字)
zq zs mcp__plugin_zq_zs__ping(接頭辞 18 文字)
zq-long-plugin-name serena-server-name mcp__plugin_zq-long-plugin-name_serena-server-name__ping
  • 両方とも私たちが決められる。 ただし mcp__plugin_ の部分は固定で、プラグインとして配る限り公式 hook の matcher mcp__serena__* には一致しない
  • mcp__serena__* にしたいなら、プラグインではなく利用者の MCP 設定(claude mcp add serena ...)として入れることになる。その場合、プラグインの導入・更新の仕組みから外れる
  • プラグイン名は導入済みのプラグインと衝突する。 検証中、serenandf という名前のプラグインを読み込ませたところ、同名の導入済みプラグインがあるため failed になった。公式マーケットプレイスには Oraios 製の serena プラグインがあるため、私たちのプラグインを serena に改名するのは避ける

${CLAUDE_PLUGIN_ROOT}.mcp.json で展開され、ラッパースクリプトを起動できる(実測)。したがって「導入済みの Serena があればそれを使い、無ければ uvx」は実現できる。

#!/usr/bin/env bash
# 例: 導入済みを優先し、無ければ uvx で起動する
if command -v serena >/dev/null 2>&1 && serena --version | grep -q "Serena 1\."; then
  exec serena start-mcp-server "$@"
fi
exec uvx --from serena-agent==1.7.0 serena start-mcp-server "$@"
  • 版の確認を入れるのは、導入済みが 2.0 系だと破壊的変更(initial_instructions の要求)を踏むためである
  • uvx の 2 回目以降の起動は 0.8 秒だった(PyPI の serena-agent==1.7.0git+... は 1.3 秒)。速度のために自動インストールする理由は無い。効くのは初回のダウンロードとオフラインのとき
  • セッション開始時に黙って uv tool install を走らせるのは避ける。 利用者の ~/.local へ書き込み、ネットワークに依存し、失敗しても気付きにくい。導入は Skill の手順(明示的な実行)と、コンテナ側(devbasex/devbase#236)で先に入れておく形にする

11. 公式マーケットプレイスの serena プラグインを使わない理由(実物を確認)

claude-plugins-officialserena(Oraios 製、community-managed)の実体は anthropics/claude-plugins-publicexternal_plugins/serena にあり、中身は .mcp.json 1 つだけである。

{"serena": {"command": "uvx",
  "args": ["--from", "git+https://github.com/oraios/serena", "serena", "start-mcp-server"]}}

.claude-plugin/plugin.json は名前と説明のみで、hook は無い。2025-12-02 以降、更新されていない。

論点 公式版 私たちが直す案
版の固定 無しgit+... の main を追う=現在 2.0.0.dev0)。initial_instructions を要求される破壊的変更を踏む serena-agent==1.7.0 に固定
--project-from-cwd 無し。毎回 activate_project が要る(2 の失敗 3 回の原因) 付ける
--context 無し(既定は desktop-app で、Claude Code 向けのツール除外が効かない) ランタイム別に指定
memory / onboarding の抑止 無し no-memories / no-onboarding
hook(誘導・自動許可) 無し 付ける
導入済み Serena の利用 無し ラッパースクリプトで対応
Codex / Kiro 使えない(Claude Code 専用) 同じ定義を 3 ランタイムへ配れる

公式版は起動引数を変えられないため、hook や Skill を足しても activate_project と開発版の問題が残る。 Codex には、そもそも使えない。よって mcp-serena を維持し、A・B で直す。

併存のリスク(推測): 私たちの mcp-serena と公式の serena は名前が違うため、両方入れると Serena が 2 つ起動し、ツールが重複してメモリも 2 倍になると見ている(実測したのは同名プラグインどうしの衝突までで、この併存は未確認)。どちらか一方を選ぶよう案内に書く。

上流が起動引数を持つようになったら、乗り換えを再検討する。

12. Bash の扱い(2026-09-23 追記)

  • 量: 追跡対象の .sh は 71 個・14,499 行(.py は 325 個・92,101 行)。hook と plugins/ndf/scripts に多い。今後のスクリプト化で大半を Python へ移すため、比率はさらに下がる
  • 効くのは診断で、読む量はあまり減らない。 bash-language-server は shellcheck を内部で呼んで診断を返す(クォート漏れ・未定義変数・終了コードの扱い)。定義・参照の追跡は関数とファイル内の変数くらいまでに留まる
  • shellcheck が無いと診断は出ず、何も表示されない(4 と同じ)。devbase の base コンテナには入っていない(which shellcheck で見つからない)。導入は chore: base イメージに shellcheck を入れる(言語サーバによる Bash の診断に要る) devbase#249 で扱う
  • 方針: bash だけの作業項目は立てない。9 の言語の検出に含め、しきい値を超えれば他の言語と同じく設定する。追加するのは shellcheck の有無の検査だけにする。優先は Python > TypeScript / PHP > Bash

作業項目

A. Serena の配布設定を直す(これをやらないと、他をやっても効かない)

  • .serena/project.ymllanguage_serverspython を足す(順序は python, bash)。ignored_paths.worktrees/** を足す
  • .mcp.json の版を固定する(serena-agent==1.7.0。今は git+... の main を引いており、手元で動いた版は 2.0.0.dev0 だった)
  • .mcp.json--project-from-cwd を足す
  • 起動をラッパースクリプト(${CLAUDE_PLUGIN_ROOT}/scripts/serena-launch.sh)にし、導入済みの Serena があればそれを使い、無ければ uvx で起動する。導入済みの版が想定の範囲かを確かめる
  • プラグイン名とサーバ名を短くするか決める(今の接頭辞は 30 文字)。serena への改名は、公式マーケットプレイスのプラグインと衝突するため避ける
  • --context をランタイムごとに分ける(Claude Code は claude-code、Codex は codex)。今の --context ide-assistant は旧名で、Codex でも claude-code として動いてしまう
  • no-memories / no-onboarding を指定する(CLAUDE.md の「Serena memory は使用禁止」を、文章ではなく設定として実装する)
  • .serena/serena_config.yml を追跡対象から外す。 生成される auth_secret が入るファイルである。コミット 17d5d441(Fix: 利用者ごとの Serena の設定を追跡から外す)で追跡を外し、.gitignore:54 が対象にする。develop で追跡している .serena/ のファイルは .serena/.gitignore.serena/project.yml だけである

B. 使わせる仕組みを作る

C. Claude Code の LSP を用意する(言語は導入先ごとに決める)

  • NDF に lspServers を固定で持たせない。Skill が検出結果に応じて公式プラグインの導入を案内する形にする
  • 対応表(拡張子 → Serena の言語識別子 → 公式 LSP プラグイン名 → 本体の導入コマンド)を 1 箇所に作る
  • 言語サーバ本体の導入を検査する手順を Skill に入れる(本体が無くても何も表示されないため、自前の検査が要る
  • TypeScript を 5 に固定する案内を書く(公式の案内のままでは動かないため)
  • Serena と機能が重複しないようにする(診断は LSP、ナビゲーションと編集は Serena)
  • Bash が検出されたときは、shellcheck の有無も導入の検査に含める(無いと診断が何も表示されずに出ない。12)

D. Codex

  • Codex への導入を案内に入れる(codex plugin add mcp-serena@ai-plugins)。今は入っていない
  • Codex 用の hook([features] codex_hooks = true、PreToolUse の matcher は Bash、PostToolUse でカウンタを戻す)を用意するか決める

E. 他のプロジェクトへ適用できるようにする

  • 言語の検出(git ls-files の拡張子の集計 → しきい値 → --ls を複数渡して設定)を Skill にする。確認は取らない
  • 設定後に 1 言語ずつ起動を検証し、失敗した言語を外す手順を Skill に入れる(1 つの失敗が全シンボル操作を止めるため)
  • SessionStart の hook は、検出結果と project.yml の食い違い、未導入の言語サーバがあるときだけ知らせる
  • PreToolUse の hook は言語に依存させない。対象の拡張子は設定で持つ
  • .serena/project.yml を追跡するかどうかを、導入先で選べるようにする
  • 作業ツリー(.worktrees/<ブランチ名>)で動くことを確かめる
  • ai-plugins 以外のリポジトリで実際に適用して確かめる(Python 以外の言語が主のもの。例: /work/carmo-system-serverside は typescript、/work/carmo-predict-contract は python)

受け入れ条件

  • A を適用したうえで、同じタスク(3 の題材)を再度計測し、指示なしでも Serena が使われるようになったかを数字で示す
  • Claude Code: 3 言語それぞれで、編集後の診断が自動で届くことを実測する(TypeScript は 5 に固定した状態で)
  • Codex: codex exec から、3 言語それぞれで参照検索と診断が使えることを実測する(5 は MCP を直接呼んだ結果で、Codex からは未実測)
  • git ls-files .serenaserena_config.yml が出ないこと
  • claude plugin validate . が終了コード 0 で終わる
  • Bash を検出したリポジトリで shellcheck が無いとき、導入の検査がそれを知らせる
  • 言語構成の違う 2 つ以上のリポジトリで、検出 → 設定 → 動作確認までが通る(少なくとも 1 つは Python 以外が主のもの)
  • 壊れた言語サーバが 1 つある状態で、その言語だけが外れ、残りの言語のシンボル操作が動くことを確かめる

関連

未確認のこと

  • PHP は、Serena / LSP のどちらでも型の食い違いが診断されなかった。intelephense の無料版の限界かどうか
  • Kiro CLI と agy に、同じ仕組みがあるか
  • hook の遅延(uvx 起動を毎回挟む場合の実測)
  • 「小規模・短時間のセッションでは Serena は持ち出しになる」という第三者の報告があり、私たちのリポジトリで得か損かは A の適用後に測る必要がある

🤖 Generated with Claude Code

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area: mcpMCP プラグインenhancementNew feature or requestneeds-decision方針の判断を待っている

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions