amperion note

Serena MCPを使ってgrepのトークン浪費を66%減らせた話

はじめに

Claude Code や Cursor でコーディングエージェントを使っていると、grep(ripgrep)の検索結果がノイズだらけでトークンを浪費する場面に遭遇します。Serena MCP という LSP ベースの MCP サーバーを導入してみたところ、シンボルレベルの検索で grep との差がかなり出たので、比較結果を共有します。

想定読者

  • Claude Code / Cursor を日常的に使っているエンジニア
  • MCP サーバーの導入を検討している方
  • コーディングエージェントのトークン効率や精度を改善したい方

参考:公式ドキュメント

https://github.com/oraios/serena

https://oraios.github.io/serena/

grep で困ること

Claude Code のビルトインツール(Grep, Glob, Read, Edit)は小規模プロジェクトなら十分に使えます。ripgrep は速いですし、正規表現も効きます。

ただ、コードベースが大きくなってくると困ることがあります。TypeScript プロジェクトで User を検索するとこうなります。

src/models/user.ts:3:  export interface User {
src/models/user.ts:15: export interface UserProfile extends User {
src/services/userService.ts:8:  async getUser(id: string): Promise<User> {
src/services/userService.ts:22: // User validation logic
src/controllers/auth.ts:5:  import { User } from '../models/user';
src/controllers/auth.ts:31: const user: User = await userService.getUser(req.params.id);
src/tests/user.test.ts:12: const mockUser: User = { id: '1', name: 'Test User' };
src/tests/user.test.ts:15: // Test User creation
src/config/strings.ts:8:  USER_NOT_FOUND: 'User not found',

9件ヒット。でも LLM が欲しいのは User インターフェースの定義だけだったりします。残りの8件はノイズで、それぞれがトークンを消費します。

grep はテキストを検索するツールなので、「どれが定義で、どれが参照で、どれがコメントか」の区別はできません。LLM がそこを推測するわけですが、推測にはトークンが要りますし、間違えることもあります。

Serena MCP とは

この問題に対して「LSP を使えばいい」というアプローチを取った MCP サーバーです。MIT ライセンスで完全無料、GitHub Stars は2026年3月時点で約21,900。

Microsoft の multilspy をベースに独自開発した Solid-LSP ライブラリで言語サーバーを統合し、MCP プロトコル経由でシンボル操作を LLM に公開しています。対応言語は30以上。

公式の設計思想はこう書かれています。

エージェントはファイル全体を読んだり、grep ライクな検索や基本的な文字列置換をする必要がなくなる

中核となるツールはこの4つです。

  • find_symbol: シンボル名パスで検索。型情報・hover 情報付き
  • find_referencing_symbols: 指定シンボルを参照している全シンボルを追跡
  • get_symbols_overview: ファイル内のシンボル構造を俯瞰
  • rename_symbol: LSP rename でコードベース全体をリネーム

特徴的なのが name_path というアドレッシング方式です。ファイルパスでもクラス名でもなく、シンボルツリー上のパスでコード要素を特定します。

"UserService/getUser"     → UserService クラスの getUser メソッド
"User"                    → User という名前の全シンボル
"/models/User"            → models 配下の User(完全パス指定)

grep と Serena を比較してみる

TypeScript プロジェクトを想定した具体例で、grep と Serena の違いを見ていきます。以下の MCP 呼び出し例は Serena のツール仕様に基づく想定シナリオです(実際のレスポンス形式はプロジェクトや言語サーバーによって若干異なります)。

ケース1: User インターフェースの定義を探す

grep の場合:

rg "User" --type ts

先ほどの9件がヒットします。コメント、文字列リテラル、変数名、型参照が全部混ざっています。

Serena の場合:

Claude Code で「User インターフェースの定義を探して」と指示すると、Serena は内部でこういう MCP 呼び出しを実行します。

{
  "name": "find_symbol",
  "arguments": {
    "name_path_pattern": "User",
    "include_kinds": [11],
    "depth": 1,
    "include_body": false,
    "include_info": true
  }
}

include_kinds=[11] は LSP の SymbolKind で Interface を指定しています(Class=5, Method=6, Property=7, Interface=11 — LSP 仕様参照)。レスポンスはこんな感じ。

{
  "symbol": "User",
  "kind": "Interface",
  "location": "src/models/user.ts:3-7",
  "info": "interface User { id: string; name: string; email: string }",
  "children": [
    {"name": "id", "kind": "Property", "type": "string"},
    {"name": "name", "kind": "Property", "type": "string"},
    {"name": "email", "kind": "Property", "type": "string"}
  ]
}

grep が9件返すところを1件で済んでいます。トークン消費の差はかなりのものです。

ケース2: getUser の呼び出し元を追跡する

grep の場合:

rg "getUser\(" --type ts

getUser( を含む行が返ります。ここで問題になるのが同名メソッドの存在です。UserService.getUserAdminService.getUser が両方ヒットしますが、grep ではどちらの呼び出しか区別できません。

Serena の場合:

Claude Code に「UserServicegetUser を呼んでいる箇所を全部見せて」と指示するとこうなります。

{
  "name": "find_referencing_symbols",
  "arguments": {
    "name_path": "UserService/getUser"
  }
}
{
  "referencing_symbols": [
    {"name": "AuthController/login", "location": "src/controllers/auth.ts:31"},
    {"name": "UserController/getProfile", "location": "src/controllers/user.ts:15"}
  ]
}

LSP の参照解析が走って、型解決済みの参照だけが返ります。AdminService.getUser の呼び出し元は含まれません。これは grep では原理的に不可能な操作です。

ケース3: getUserfetchUser にリネームする

grep + sed の場合:

rg "getUser" --type ts -l | xargs sed -i 's/getUser/fetchUser/g'

getUser という文字列を全ファイルで置換します。コメント内の // getUser is deprecated も変わりますし、getUserResult のような部分一致も壊れます。

Serena の場合:

rename_symbol("UserService/getUser", new_name="fetchUser")

LSP の rename 機能が全参照箇所を正確に特定して、意味的に正しいリネームを実行します。コメントや文字列リテラルは変えません。部分一致も壊しません。

リファクタリングに関しては差が大きいです。grep + sed は壊れない保証がないので。

トークン効率はどれくらい変わるのか

正直なところ、Serena 公式は定量ベンチマークを公開していません。トークン消費量・正答率・所要時間の3軸で自前計測するのが理想ですが、公開データはトークン消費量の参考値に限られます。正答率と所要時間はプロジェクトの性質に大きく依存するので、各自の環境で計測するのがよさそうです。

参考になるデータを並べておきます。

計測元 手法 結果
azukiazusa.dev 実測 リファクタリングタスクで Serena あり/なし比較 27,453 → 約9,151トークン(約66%削減
jCodeMunch ベンチマーク(※tree-sitter ベース、参考値) 50回 A/B テスト 成功率 72%→80%、コスト6%削減、トークン10.5%削減

ただし、見落としがちなトレードオフがあります。

Serena のツール定義自体が約23,878トークンを消費します。 29個のツールスキーマがコンテキストウィンドウを占有するんですよね。serena-slim というラッパーを使えば50.3%削減(約12,000トークン)できますが、無視はできない量です。

つまりこういう構図です。

  • 大規模プロジェクト: 検索結果の圧縮効果がツール定義のオーバーヘッドを上回る → Serena が有利
  • 小規模プロジェクト: ファイル全体を Read しても大したトークン量にならない → grep で十分

個人的には、境界線は「ファイル数50〜100」あたりかなという感覚です。

Claude Code へのセットアップ

前提条件は uv(Astral 製の Python パッケージマネージャー)だけです。未導入の場合は以下でインストールできます。

curl -LsSf https://astral.sh/uv/install.sh | sh

なお、Serena は Python 3.11 が必要ですが(3.12 以上は非対応)、uvx 経由で実行すれば自動的にハンドリングされます。

プロジェクト単位で追加:

claude mcp add serena -- uvx --from git+https://github.com/oraios/serena serena start-mcp-server --context claude-code --project "$(pwd)"

グローバルに追加する場合:

claude mcp add --scope user serena -- uvx --from git+https://github.com/oraios/serena serena start-mcp-server --context=claude-code --project-from-cwd

--context claude-code は必ず付けてください。 これを指定すると、Claude Code の組み込みツールと重複する Serena のツール5つ(read_file, create_text_file, execute_shell_command, replace_content, prepare_for_new_conversation)が自動的に除外され、LSP シンボルツールに特化した構成になります。

さらに、このコンテキストには LLM への行動指示も埋め込まれています。

ファイル全体を読むことは厳に避けよ。コードの探索と読み取りには Serena のシンボルツールを優先せよ。

これ結構大事で、--context claude-code を付けないと重複ツールが有効化されて、LLM が Read と read_file のどっちを使えばいいか迷って非効率になったりします。

接続確認は claude mcp list で。serena の Status が connected になっていれば OK です。

接続できたら、Claude Code でこう指示してみてください。

> このプロジェクトの主要なクラスを find_symbol で一覧して

Serena がシンボル一覧を返してくれれば、セットアップ完了です。

初回起動時に Serena がプロジェクト構造を解析し、.serena/project.yml を自動生成します。TypeScript や Python のプロジェクトであれば、基本的にデフォルト設定のまま使えます。言語サーバーの個別設定が必要な場合は 公式の設定ドキュメント を参照してください。

ハマりポイント

実際に使ってみて気づいた注意点をいくつか挙げておきます。

コンテキストコンパクション問題。 Claude Code は長いセッションでコンテキストを自動圧縮しますが、この過程で Serena ツールの存在を「忘れる」ことがあります。突然 grep に戻り始めたら、「Serena の find_symbol を使って」と明示的に指示するか、セッションを区切るのがよいです。

stdout が ERROR 表示される。 MCP の仕様上、サーバーの stdout 出力がすべて ERROR レベルで表示されます。初めて見ると焦りますが、実際にはエラーではないケースがほとんどです。

find_referencing_symbols の信頼性。 言語サーバーの実装品質に依存します。Java(Eclipse LSP)で参照が見つからないケースが報告されています。TypeScript(tsserver)や Python(Pylsp)は比較的安定している印象です。

静的解析の限界。 動的な型生成、リフレクション、メタプログラミングで生成されるシンボルは追跡できません。ここは grep の方が強いです。

じゃあいつ Serena を使うのか

Serena は grep の上位互換ではなく、補完です。Serena 自身も search_for_pattern という grep 相当のツールを内蔵しています。テキスト検索が要らなくなるわけではありません。

使い分けの目安はこんな感じです。

grep で十分なケース: * ファイル数50以下、新規開発 * 設定値・ログメッセージの検索 * 正規表現でのパターンマッチ

Serena が向いているケース: * 大規模コードベースのリファクタリング * 同名シンボルの区別が必要な場面 * 参照追跡・依存関係分析 * API 課金でトークンコスト最適化が重要な場合

Serena の開発チームが lessons_learned.md にこう書いています。

LLM は行カウントが苦手で、編集後に行番号が変わることも理解しにくい。我々はシンボル名ベースの編集に転換した。

grep が返す「行」ではなく、LSP が返す「シンボル」で考える。興味がある方は、まず手元のプロジェクトで試してみてください。

手順をまとめるとこうなります。

  1. uv をインストール
  2. claude mcp add で Serena を追加(--context claude-code を忘れずに)
  3. claude mcp list で接続確認
  4. find_symbol で主要クラスを検索してみる
  5. 同じ検索を grep でもやって、ヒット数やトークン消費の差を確認する

自分のプロジェクトでトークン削減効果を実測してみると、導入するかどうかの判断がしやすくなります。

関連情報