<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>工具評估 on Tarragon</title><link>https://tarrragon.github.io/blog/tags/%E5%B7%A5%E5%85%B7%E8%A9%95%E4%BC%B0/</link><description>Recent content in 工具評估 on Tarragon</description><generator>Hugo -- gohugo.io</generator><language>zh-TW</language><copyright>Tarragon (CC BY 4.0)</copyright><lastBuildDate>Mon, 25 May 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/tags/%E5%B7%A5%E5%85%B7%E8%A9%95%E4%BC%B0/index.xml" rel="self" type="application/rss+xml"/><item><title>codebase-memory-mcp：155 語言 tree-sitter 知識圖譜 MCP 的能力與邊界</title><link>https://tarrragon.github.io/blog/record/codebase-memory-mcp155-%E8%AA%9E%E8%A8%80-tree-sitter-%E7%9F%A5%E8%AD%98%E5%9C%96%E8%AD%9C-mcp-%E7%9A%84%E8%83%BD%E5%8A%9B%E8%88%87%E9%82%8A%E7%95%8C/</link><pubDate>Mon, 25 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/record/codebase-memory-mcp155-%E8%AA%9E%E8%A8%80-tree-sitter-%E7%9F%A5%E8%AD%98%E5%9C%96%E8%AD%9C-mcp-%E7%9A%84%E8%83%BD%E5%8A%9B%E8%88%87%E9%82%8A%E7%95%8C/</guid><description>&lt;h2 id="這個-mcp-解什麼問題">這個 MCP 解什麼問題&lt;/h2>
&lt;p>codebase-memory-mcp（下稱 cbm）的核心定位是「&lt;strong>把整個 codebase 預先解析成可被 LLM 廉價查詢的知識圖譜&lt;/strong>」。它要替代的是 agent 在不熟悉的 codebase 上「拿 grep / glob / read 連環翻檔」的探索 pattern——人類用 IDE 編輯、agent 用 cbm 探索、兩者服務不同的工作流。&lt;/p>
&lt;p>設計上跟其他「graph + LLM」工具的關鍵分野，在於它&lt;strong>不內嵌任何 LLM 做自然語言 → 查詢轉換&lt;/strong>：&lt;/p>
&lt;blockquote>
&lt;p>Other code graph tools embed an LLM for natural language → graph query translation. This means extra API keys, extra cost, and another model to configure. With MCP, the agent you&amp;rsquo;re already talking to &lt;em>is&lt;/em> the query translator.&lt;/p>&lt;/blockquote>
&lt;p>所以 cbm 自己只是個提供高品質 graph 查詢 API 的 server，「翻譯自然語言」這件事直接讓呼叫端的 agent 做。這個取捨對 Claude Code 這類 host 是理想的，因為 host 端已經有一顆夠強的模型在跑。&lt;/p>
&lt;h2 id="部署形態決定它的甜蜜點">部署形態決定它的甜蜜點&lt;/h2>
&lt;p>cbm 是&lt;strong>單一靜態 binary&lt;/strong>，所有依賴（155 種 tree-sitter grammar、SQLite、tokenizer）都在 binary 內，安裝後沒有外部 runtime 依賴。&lt;/p>
&lt;p>這個取捨直接影響它的甜蜜點：&lt;/p>
&lt;ul>
&lt;li>跨平台分發成本低，CI 上跑也方便&lt;/li>
&lt;li>不需要為個別語言裝 toolchain（不像 LSP 路線要對應 language server）&lt;/li>
&lt;li>但代價是「能力上限」被 binary 內附的 grammar 跟自寫的 type resolution 算法綁住，無法靠 IDE 生態的成熟度借力&lt;/li>
&lt;/ul>
&lt;p>知道這個取捨之後，後面所有能力差異都解釋得通：能做的事多半是「靜態可推導」的，需要 query 外部 toolchain（如 IDE language server）的場景多半要靠別的工具補。&lt;/p>
&lt;h2 id="索引架構多-pass--ram-first">索引架構：多 pass + RAM-first&lt;/h2>
&lt;p>cbm 的索引流程是 &lt;strong>RAM-first 的多 pass pipeline&lt;/strong>，pass 之間有明確的責任分工：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Pass&lt;/th>
 &lt;th>責任&lt;/th>
 &lt;th>抽出的 edge / node（為主）&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>structure&lt;/td>
 &lt;td>tree-sitter 解 AST，建初始 node&lt;/td>
 &lt;td>Project / Package / Folder / File / Module&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>definitions&lt;/td>
 &lt;td>抽函式 / 類別 / 介面 / 型別定義&lt;/td>
 &lt;td>Class / Function / Method / Interface / Enum / Type&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>calls&lt;/td>
 &lt;td>解析 function call、結合 import 與型別&lt;/td>
 &lt;td>CALLS / ASYNC_CALLS / USAGE / USES_TYPE / IMPLEMENTS&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>HTTP links&lt;/td>
 &lt;td>偵測 REST / gRPC / GraphQL route&lt;/td>
 &lt;td>Route、HTTP_CALLS、HANDLES&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>configuration&lt;/td>
 &lt;td>掃 Docker / Kubernetes / Kustomize&lt;/td>
 &lt;td>Resource、CONFIGURES、WRITES&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>tests&lt;/td>
 &lt;td>偵測測試函式與被測對象關係&lt;/td>
 &lt;td>TESTS、FILE_CHANGES_WITH&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>執行期用 LZ4 壓縮的記憶體 SQLite 加速，所有 pass 跑完一次性 dump 成持久化 DB（路徑 &lt;code>~/.cache/codebase-memory-mcp/&lt;/code>，WAL mode）。team 共享情境下可加跑 zstd 壓縮（best tier 用 &lt;code>zstd -9&lt;/code> + index strip、fast tier 用 &lt;code>zstd -3&lt;/code> 走 watcher 增量），匯出成 &lt;code>.codebase-memory/graph.db.zst&lt;/code> artifact 給 CI / 隊友共用。&lt;/p></description><content:encoded><![CDATA[<h2 id="這個-mcp-解什麼問題">這個 MCP 解什麼問題</h2>
<p>codebase-memory-mcp（下稱 cbm）的核心定位是「<strong>把整個 codebase 預先解析成可被 LLM 廉價查詢的知識圖譜</strong>」。它要替代的是 agent 在不熟悉的 codebase 上「拿 grep / glob / read 連環翻檔」的探索 pattern——人類用 IDE 編輯、agent 用 cbm 探索、兩者服務不同的工作流。</p>
<p>設計上跟其他「graph + LLM」工具的關鍵分野，在於它<strong>不內嵌任何 LLM 做自然語言 → 查詢轉換</strong>：</p>
<blockquote>
<p>Other code graph tools embed an LLM for natural language → graph query translation. This means extra API keys, extra cost, and another model to configure. With MCP, the agent you&rsquo;re already talking to <em>is</em> the query translator.</p></blockquote>
<p>所以 cbm 自己只是個提供高品質 graph 查詢 API 的 server，「翻譯自然語言」這件事直接讓呼叫端的 agent 做。這個取捨對 Claude Code 這類 host 是理想的，因為 host 端已經有一顆夠強的模型在跑。</p>
<h2 id="部署形態決定它的甜蜜點">部署形態決定它的甜蜜點</h2>
<p>cbm 是<strong>單一靜態 binary</strong>，所有依賴（155 種 tree-sitter grammar、SQLite、tokenizer）都在 binary 內，安裝後沒有外部 runtime 依賴。</p>
<p>這個取捨直接影響它的甜蜜點：</p>
<ul>
<li>跨平台分發成本低，CI 上跑也方便</li>
<li>不需要為個別語言裝 toolchain（不像 LSP 路線要對應 language server）</li>
<li>但代價是「能力上限」被 binary 內附的 grammar 跟自寫的 type resolution 算法綁住，無法靠 IDE 生態的成熟度借力</li>
</ul>
<p>知道這個取捨之後，後面所有能力差異都解釋得通：能做的事多半是「靜態可推導」的，需要 query 外部 toolchain（如 IDE language server）的場景多半要靠別的工具補。</p>
<h2 id="索引架構多-pass--ram-first">索引架構：多 pass + RAM-first</h2>
<p>cbm 的索引流程是 <strong>RAM-first 的多 pass pipeline</strong>，pass 之間有明確的責任分工：</p>
<table>
  <thead>
      <tr>
          <th>Pass</th>
          <th>責任</th>
          <th>抽出的 edge / node（為主）</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>structure</td>
          <td>tree-sitter 解 AST，建初始 node</td>
          <td>Project / Package / Folder / File / Module</td>
      </tr>
      <tr>
          <td>definitions</td>
          <td>抽函式 / 類別 / 介面 / 型別定義</td>
          <td>Class / Function / Method / Interface / Enum / Type</td>
      </tr>
      <tr>
          <td>calls</td>
          <td>解析 function call、結合 import 與型別</td>
          <td>CALLS / ASYNC_CALLS / USAGE / USES_TYPE / IMPLEMENTS</td>
      </tr>
      <tr>
          <td>HTTP links</td>
          <td>偵測 REST / gRPC / GraphQL route</td>
          <td>Route、HTTP_CALLS、HANDLES</td>
      </tr>
      <tr>
          <td>configuration</td>
          <td>掃 Docker / Kubernetes / Kustomize</td>
          <td>Resource、CONFIGURES、WRITES</td>
      </tr>
      <tr>
          <td>tests</td>
          <td>偵測測試函式與被測對象關係</td>
          <td>TESTS、FILE_CHANGES_WITH</td>
      </tr>
  </tbody>
</table>
<p>執行期用 LZ4 壓縮的記憶體 SQLite 加速，所有 pass 跑完一次性 dump 成持久化 DB（路徑 <code>~/.cache/codebase-memory-mcp/</code>，WAL mode）。team 共享情境下可加跑 zstd 壓縮（best tier 用 <code>zstd -9</code> + index strip、fast tier 用 <code>zstd -3</code> 走 watcher 增量），匯出成 <code>.codebase-memory/graph.db.zst</code> artifact 給 CI / 隊友共用。</p>
<p>Pass 排序遵循明確的依賴關係：calls 一定在 definitions 之後（因為 call edge 要連到已被建出來的 function / method node）、HTTP links 一定在 calls 之後（需要先有 call edge 才能比對 route 跟 handler）、configuration / tests 是 cross-cutting 的最終層（前面的結構與 call graph 都齊備、它們才能掛上 CONFIGURES / TESTS edge）。實務影響：HTTP links pass 在「單 service repo」上等於 no-op、configuration pass 在「缺 IaC manifest」的 repo 上也是 no-op、這兩個 pass 的價值高度依賴 repo 結構。</p>
<p>這個架構的副作用是：<strong>單次完整 index 速度快</strong>（README 聲稱 Linux kernel 3 分鐘），但<strong>增量更新採背景 git polling</strong>（IDE-style file watcher 是即時觸發、cbm 是定期掃描），對「邊改邊查」的工作流會有秒級延遲。</p>
<h2 id="11-signal-語意搜尋cbm-最強的差異化">11-signal 語意搜尋：cbm 最強的差異化</h2>
<p>如果只看 README 寫的「BM25 全文搜尋」，會嚴重低估 cbm 的搜尋能力。實際上 <code>search_graph</code> 的 ranking 是 <strong>11 個 signal 的加權組合</strong>：</p>
<table>
  <thead>
      <tr>
          <th>Signal</th>
          <th>角色</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>TF-IDF</td>
          <td>詞頻 / 逆文檔頻率，傳統文字相關性</td>
      </tr>
      <tr>
          <td>RRI</td>
          <td>Reverse rank importance，符號在 graph 中的重要性</td>
      </tr>
      <tr>
          <td>API / Type / Decorator signature</td>
          <td>函式簽章、型別標註、decorator 是高權重訊號</td>
      </tr>
      <tr>
          <td>AST profile</td>
          <td>AST 結構相似性</td>
      </tr>
      <tr>
          <td>Data flow</td>
          <td>變數與參數依賴鏈</td>
      </tr>
      <tr>
          <td>Halstead-lite</td>
          <td>簡化的程式複雜度指標</td>
      </tr>
      <tr>
          <td>MinHash</td>
          <td>近重複偵測（找變體 / 複製貼上）</td>
      </tr>
      <tr>
          <td>Module proximity</td>
          <td>符號在依賴 graph 上的距離</td>
      </tr>
      <tr>
          <td>Graph diffusion</td>
          <td>在 graph 上做 spreading activation</td>
      </tr>
  </tbody>
</table>
<p>表格列了 9 個明確 signal、README 另說有 11 個（剩 2 個是 implementation detail 沒公開細節）。實務上 11-signal 的價值在於<strong>幾個高權重 signal 各自負責不同 query 類型</strong>——權重分配有明顯的高低差：</p>
<ul>
<li><strong>RRI</strong> 是 cbm 對「重要符號優先」的 graph 結構 prior。一個被大量檔案 import 的核心 class、即使在 query 字串裡只有間接匹配、RRI 也會把它往上推。這層對「找這個 codebase 的入口 / 主要抽象」類 query 特別重要。</li>
<li><strong>Data flow</strong> 是 cbm 對「概念上接近、但符號名沒共字」的 query 的關鍵 signal。例如查「金額顯示」、<code>formatAmount</code> 跟 <code>_buildPriceDisplay</code> 在符號名上沒共字、但 data flow 能捕捉「<code>formatAmount</code> 的回傳值流入了 <code>_buildPriceDisplay</code> 的 widget」這層連結。</li>
<li><strong>Graph diffusion</strong> 是 cbm 對「擴散式相關性」的最終 boost——已經被前面 signal 推到高分的符號，會把分數擴散到 graph 上鄰近的符號。實務影響：monorepo 上效果最強（跨 module 鄰近性有意義）、單一檔案的小專案上幾乎沒效果。</li>
</ul>
<p>加上一層 <strong><code>cbm_camel_split</code> tokenizer</strong>：對 <code>getMoneyField</code> 這類 identifier 做 camelCase / snake_case 切詞，所以查 <code>money field display</code> 能命中 <code>getMoneyField</code>、<code>MoneyFieldRenderer</code> 之類符號。</p>
<p>這套組合的判讀價值在於：<strong>對「我不知道精確符號名」的概念性查詢，cbm 是少數能給出合理 top-N 的工具</strong>。例如查「金額顯示相關」、結果裡會出現 <code>formatAmount</code> 實作 + <code>_buildPriceDisplay</code> + <code>getBalanceDisplay</code>，這些都跟「金額顯示」業務概念相關、不會被 <code>displayName</code> / <code>displayTags</code> 這種只共享 <code>display</code> 子字串的雜訊淹沒。</p>
<p>下一步路由：要看實測案例，見 <a href="/blog/record/%E4%B8%89-mcp-%E5%B7%A5%E4%BD%9C%E6%B5%81%E8%88%87-dart-%E5%AF%A6%E6%B8%ACcbm-/-codegraph-/-serena-%E7%9A%84%E8%81%B7%E8%B2%AC%E5%88%86%E5%B7%A5%E8%88%87%E4%B8%89%E5%88%80%E6%B5%81/">三 MCP 工作流與 Dart 實測</a>。</p>
<h2 id="hybrid-type-resolution只給五個語言的特殊待遇">Hybrid type resolution：只給五個語言的特殊待遇</h2>
<p>cbm 對 <strong>Go / C / C++ / TypeScript / JavaScript</strong>（JS 含 JSX、TS 含 TSX）額外跑一層 type resolution，README 描述是：</p>
<blockquote>
<p>Clean-room reimplementation of tsserver / typescript-go&rsquo;s type resolution algorithms — parameter binding, return-type inference, generic substitution, JSX component dispatch, JSDoc inference for plain JS files.</p></blockquote>
<p>換言之，這幾個語言的 <code>CALLS</code> edge 在 syntactic match 之上多了一層 type-aware dispatch resolution，效果接近 LSP。其他 149 個語言只跑純 tree-sitter pass，能力會降到「<strong>結構抽得到、call edge 抽得很有限</strong>」。</p>
<p>實測對照（在某 Dart 商業專案上）：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">cbm 索引完成統計：3,038 nodes、6,355 edges
</span></span><span class="line"><span class="ln">2</span><span class="cl">其中 CALLS edge 總數：2（整個專案僅 2 條）</span></span></code></pre></div><p>這個數字反映 cbm 的設計選擇——<strong>hybrid resolution 名單只涵蓋 5 個語言、Dart 在名單外</strong>——所以 <code>trace_call_path</code> 對 Dart symbol 一律回 0 caller，這個 0 屬於 by design 行為。對 Go / TS 主力專案，這個能力上限會完全不一樣。</p>
<p>判讀訊號：開發前先確認自己的主力語言在不在那五個語言內。在的話 cbm 是準 LSP；不在的話它只是個「結構 + 全文搜尋」工具，呼叫鏈相關問題要靠別的 MCP 補。</p>
<h2 id="跨-service-鏈接first-class-http_calls-edge">跨 service 鏈接：first-class HTTP_CALLS edge</h2>
<p>cbm 的另一個差異化能力是把 <strong>REST / gRPC / GraphQL / tRPC route 當 first-class node</strong>，建立跨 service 的 <code>HTTP_CALLS</code> edge：</p>
<ul>
<li>Route 偵測：對應主流 web framework（Express / NestJS / FastAPI / Gin / Rails 等）的 route 定義語法</li>
<li>Call site 比對：以 route pattern 比對 client 端的 URL 字面值或變數，附 confidence score</li>
<li>額外的 channel edge：Socket.IO / EventEmitter / 各種 pub-sub 的 <code>EMITS</code> / <code>LISTENS_ON</code></li>
</ul>
<p>這層能力對單一 monorepo 內的多 service 架構（microservice repo / BFF / API gateway pattern）特別有價值——可以查「這個前端 API call 對應哪個後端 handler」這種跨 service 問題。對單一 service 的單體 repo，這層能力派不上用場。</p>
<p>實際使用前提：要 index 的 repo 必須<strong>同時包含 client 跟 server 端</strong>，分散在多 repo 的話 cbm 不會自動跨 repo 連邊。</p>
<h2 id="cypher-子集支援的查詢與邊界">Cypher 子集：支援的查詢與邊界</h2>
<p>cbm 提供的 <code>query_graph</code> 是 Cypher 的<strong>真子集</strong>——覆蓋大部分 read-only query 語法、省略 mutation 與部分 aggregation 語法：</p>
<p><strong>支援</strong>：</p>
<ul>
<li><code>MATCH</code> 含 label / relationship type / 變長路徑</li>
<li><code>WHERE</code> 含比較 / regex / <code>CONTAINS</code></li>
<li><code>RETURN</code> 含 property access、<code>COUNT</code>、<code>DISTINCT</code></li>
<li><code>ORDER BY</code>、<code>LIMIT</code></li>
</ul>
<p><strong>不支援</strong>：</p>
<ul>
<li><code>WITH</code>（不能多階段 pipeline）</li>
<li><code>COLLECT</code>（不能 aggregate 成 list）</li>
<li><code>OPTIONAL MATCH</code>（不能 left-join）</li>
<li><code>labels(n)</code> / <code>type(r)</code> 等函數呼叫</li>
<li><code>AS</code> 別名</li>
<li>任何 mutation（純讀）</li>
</ul>
<p>幾個限制各自踩到的事故型態：</p>
<ul>
<li><strong><code>WITH</code> 缺席</strong>：所有需要「先 match 一組、再 filter / aggregate」的二階段 query 都寫不出來。例如「列出每個 module 內最常被呼叫的 function」這種 Top-K per group 的 query、在 Cypher 是 <code>MATCH ... WITH module, COUNT(*) AS c ORDER BY c LIMIT 1</code>、在 cbm 要拆成「先 list modules、再對每個 module 跑一次 callers query、外層排序」。</li>
<li><strong><code>OPTIONAL MATCH</code> 缺席</strong>：left-join 場景做不到。例如「列出所有 class、附上它的 supertype（若有）」這種「主結果不該因為某個關係缺失就丟掉」的 query 寫不出來。cbm 上的做法是先抓全部 class、再對每個 class 跑一次 supertype query、在 client 端合併。</li>
<li><strong><code>labels(n)</code> 缺席</strong>：拿不到 graph 內所有 node label 種類的清單。想做「我的 graph 裡有哪幾類 node」這種 schema 探索類 query、得退回 <code>get_graph_schema</code> 拿固定的 schema 介紹、看不到 instance 層真實分布。</li>
<li><strong><code>AS</code> 別名缺席</strong>：query 結果直接是 node / relationship object、沒法 rename 欄位給 downstream consumer。</li>
</ul>
<p>這些限制的共通實際影響：<strong>想做 group-by-count 類的 graph stats 查詢得退回 <code>search_graph</code> 逐 label 抽</strong>。例如「列出每個 file 有幾個 method」這種一行 Cypher 在標準 Neo4j 能寫的、在 cbm 上要拆成多次 query 在外層彙整。</p>
<p>判讀訊號：若 query 需要 <code>WITH ... COLLECT(...) AS xs</code> 這類二階段語法，先別硬寫 Cypher，改用 <code>search_graph</code> 加 client 端聚合。</p>
<h2 id="安裝行為與兩個要注意的小坑">安裝行為與兩個要注意的小坑</h2>
<p>cbm 的 <code>install.sh</code> 對 <code>~/.claude/settings.json</code> 動的範圍比 README 寫得多。實際安裝會：</p>
<ul>
<li>下載對應平台 binary、剝 macOS quarantine、ad-hoc sign</li>
<li>自動偵測 11 種 coding agent，逐一注入 MCP server config</li>
<li>對 Claude Code 寫入 <code>.claude/.mcp.json</code>、4 個 Skill、PreToolUse hook</li>
<li>Hook 名稱：<code>cbm-code-discovery-gate</code>，攔截 Grep / Glob 注入結構化 context</li>
</ul>
<p>兩個實際踩過的小坑：</p>
<p><strong>Hook matcher 與 README 不一致</strong>。README 描述「intercepts Grep/Glob — never Read」，實際安裝版本 matcher 是 <code>&quot;Grep|Glob|Read|Search&quot;</code>，連 Read 也被擋。修法：手動把 matcher 改成 <code>&quot;Grep|Glob|Search&quot;</code>。注意 <code>codebase-memory-mcp update</code> 可能會把這行改回原樣，每次升級要重新確認。</p>
<p><strong>uninstall 不清 hook</strong>。卸載 binary 不會主動把 <code>~/.claude/settings.json</code> 裡的 hook 條目移除。決定不再用 cbm 時要手動清掉 <code>PreToolUse</code> 下的 <code>cbm-code-discovery-gate</code> 條目，否則之後安裝其他工具或除錯時會看到神祕的 BLOCKED 訊息。</p>
<h2 id="14-個-mcp-tool-的分類">14 個 MCP tool 的分類</h2>
<table>
  <thead>
      <tr>
          <th>類別</th>
          <th>Tool</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>索引</td>
          <td><code>index_repository</code>、<code>list_projects</code>、<code>delete_project</code>、<code>index_status</code></td>
      </tr>
      <tr>
          <td>查詢</td>
          <td><code>search_graph</code>、<code>trace_call_path</code>、<code>detect_changes</code>、<code>query_graph</code>、<code>get_graph_schema</code>、<code>get_code_snippet</code>、<code>get_architecture</code>、<code>search_code</code></td>
      </tr>
      <tr>
          <td>管理</td>
          <td><code>manage_adr</code>（架構決策紀錄 CRUD）、<code>ingest_traces</code>（runtime trace 驗證 HTTP_CALLS）</td>
      </tr>
  </tbody>
</table>
<p>特別值得提的兩個：</p>
<ul>
<li><code>manage_adr</code>：把 Architecture Decision Records 當持久化資源管理。對長期專案有累積架構決策需求的場景有用，但若團隊已用 ADR-tools 或 Notion 管 ADR，這層會重複。</li>
<li><code>ingest_traces</code>：餵 runtime trace 進來驗證 <code>HTTP_CALLS</code> edge 是否反映實際的 runtime 調用。可以把靜態推測的 cross-service edge 與真實 runtime 行為對齊。實務上要先有 distributed tracing 基礎建設才開得了，門檻偏高。</li>
</ul>
<h2 id="適用--不適用情境的判讀">適用 / 不適用情境的判讀</h2>
<p><strong>適用情境</strong>：</p>
<ul>
<li><strong>主力語言在 Go / C / C++ / TS / JS 名單內</strong> → 享受 hybrid type resolution。判讀方法：對 5 個熱門 class 跑 <code>trace_call_path</code>、若 caller 數跟 IDE「Find Usages」結果對得上、表示 hybrid 正常工作。</li>
<li><strong>概念性 / 自然語言搜尋需求高</strong> → 11-signal scoring 是少數能勝任的 MCP。判讀方法：對「我只記得功能類別、不記得名字」的 query 跑 cbm 跟其他工具的 search、若 cbm top-10 命中率明顯高、值得當主要入口。</li>
<li><strong>跨 service 的 monorepo</strong> → first-class HTTP_CALLS edge 抽得到 cross-service 鏈。判讀方法：repo 內若有多個 service 用 HTTP / gRPC / GraphQL 互相呼叫、又分散在同一個 git tree 內、cbm 能跨 service 連邊；若只是單 service repo 這條沒效。</li>
<li><strong>偏好單 binary 部署</strong> → 不想為個別語言裝 toolchain、cbm 是少數零外部依賴的選項。</li>
</ul>
<p><strong>不適用情境</strong>：</p>
<ul>
<li><strong>主力語言不在 hybrid resolution 名單</strong>（如 Dart / Swift / Kotlin）且核心需求是 caller / blast radius 追蹤。判讀方法：在自己 repo 跑 cbm <code>trace_call_path</code> 對 5 個熱門 class、若 caller 數明顯偏低或 0、表示 cbm 在這語言只剩結構抽取、要靠 LSP 工具補。</li>
<li><strong>要 symbol-level 編輯</strong>（rename / replace_symbol_body）— cbm 純讀、沒這層。判讀方法：要做「rename method 並更新所有 reference」這類 atomic refactor 時、cbm 完全幫不上忙、要走 LSP 工具。</li>
<li><strong>要編譯 diagnostic 整合</strong> — cbm 不接 LSP、沒法把 type error / unused import 拋給 agent。</li>
</ul>
<p><strong>搭配建議</strong>：在不在 hybrid resolution 名單的語言上，cbm 通常需要配合一個 LSP-based MCP（如 <a href="/blog/record/serena%E6%8A%8A-lsp-%E5%8C%85%E6%88%90-agent-first-mcp-%E7%9A%84-symbol-level-%E7%B7%A8%E8%BC%AF%E6%96%B9%E6%A1%88/">serena</a>）做 caller / impact 補位，加上一個 tree-sitter call graph 工具（如 <a href="/blog/record/codegraph%E7%94%A8-tree-sitter-per-language-query-%E6%92%90%E8%B5%B7-19-%E8%AA%9E%E8%A8%80-call-graph-%E7%9A%84-mcp/">codegraph</a>）做日常結構查詢。三者怎麼分工見 <a href="/blog/record/%E4%B8%89-mcp-%E5%B7%A5%E4%BD%9C%E6%B5%81%E8%88%87-dart-%E5%AF%A6%E6%B8%ACcbm-/-codegraph-/-serena-%E7%9A%84%E8%81%B7%E8%B2%AC%E5%88%86%E5%B7%A5%E8%88%87%E4%B8%89%E5%88%80%E6%B5%81/">三 MCP 工作流與 Dart 實測</a>。</p>
<h2 id="結論">結論</h2>
<p>cbm 的核心價值在三件事：<strong>單 binary 部署</strong>、<strong>11-signal 語意搜尋</strong>、<strong>跨 service HTTP/RPC 鏈接</strong>。前兩件對任何語言都成立，第三件對微服務 monorepo 特別有意義。</p>
<p>它的能力上限被 hybrid type resolution 的語言名單卡死——名單內等於準 LSP，名單外只是個結構抽取器。評估時第一個要問的問題是：「我的主力語言在不在那五個（Go / C / C++ / TS / JS）？」答案決定 cbm 是主刀還是輔刀。</p>
]]></content:encoded></item><item><title>codegraph：用 tree-sitter per-language query 撐起 19+ 語言 call graph 的 MCP</title><link>https://tarrragon.github.io/blog/record/codegraph%E7%94%A8-tree-sitter-per-language-query-%E6%92%90%E8%B5%B7-19-%E8%AA%9E%E8%A8%80-call-graph-%E7%9A%84-mcp/</link><pubDate>Mon, 25 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/record/codegraph%E7%94%A8-tree-sitter-per-language-query-%E6%92%90%E8%B5%B7-19-%E8%AA%9E%E8%A8%80-call-graph-%E7%9A%84-mcp/</guid><description>&lt;h2 id="這個-mcp-解什麼問題">這個 MCP 解什麼問題&lt;/h2>
&lt;p>codegraph 的設計動機很具體：&lt;strong>Claude Code 探索 codebase 時 spawn 的 Explore agent 會用 grep / glob / read 連續刷檔，每個 tool call 都吃 token&lt;/strong>。codegraph 把這層探索預先做好，agent 直接查預建好的 graph。&lt;/p>
&lt;blockquote>
&lt;p>When Claude Code explores a codebase, it spawns Explore agents that scan files with grep, glob, and Read — consuming tokens on every tool call. CodeGraph gives those agents a pre-indexed knowledge graph — symbol relationships, call graphs, and code structure.&lt;/p>&lt;/blockquote>
&lt;p>跟 &lt;a href="https://tarrragon.github.io/blog/record/codebase-memory-mcp155-%E8%AA%9E%E8%A8%80-tree-sitter-%E7%9F%A5%E8%AD%98%E5%9C%96%E8%AD%9C-mcp-%E7%9A%84%E8%83%BD%E5%8A%9B%E8%88%87%E9%82%8A%E7%95%8C/">cbm&lt;/a> 比，codegraph 的 scope 更窄、更專注：不做跨 service 鏈接、不做 ADR / runtime trace 管理、不做 11-signal 語意搜尋，&lt;strong>只把 call graph 跟 symbol relationship 做好&lt;/strong>。這個取捨讓它的 MCP tool 只有 10 個、每個責任都很單一。&lt;/p>
&lt;h2 id="技術架構tree-sitter--per-language-query--fts5">技術架構：tree-sitter + per-language query + FTS5&lt;/h2>
&lt;p>codegraph 的核心 pipeline：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">tree-sitter parse → per-language query 抽 nodes/edges
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> → 解析 reference（import / extends / implements / calls）
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl"> → 寫進 SQLite + FTS5&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>關鍵設計：&lt;strong>對每個語言寫專屬的 tree-sitter query&lt;/strong>——比起通用 AST visitor 路線、這個設計能對特定語言的 dispatch pattern 抽到更精確的 node 跟 edge。&lt;/p>
&lt;blockquote>
&lt;p>Language-specific queries extract nodes (functions, classes, methods) and edges (calls, imports, extends, implements).&lt;/p>&lt;/blockquote>
&lt;p>這個設計選擇直接決定了 codegraph 對非主流語言（如 Dart / Svelte / Liquid）的支援深度——因為每個語言都有專屬 query，所以 19+ 語言裡的 Dart 真的有 working call graph，不像純 tree-sitter wrapper 那樣只能抽結構。&lt;/p>
&lt;p>實際支援的 19+ 語言：&lt;/p>
&lt;p>TypeScript、JavaScript、Python、Go、Rust、Java、C#、PHP、Ruby、C、C++、Swift、Kotlin、Scala、Dart、Svelte、Vue、Liquid、Lua、Luau、Pascal/Delphi。&lt;/p>
&lt;p>過濾規則：「&lt;strong>Files larger than 1 MB are skipped&lt;/strong>」（generated bundle / minified JS / vendored blob 自動忽略）。&lt;/p>
&lt;h2 id="auto-syncnative-os-file-watcher--2s-debounce">Auto-sync：native OS file watcher + 2s debounce&lt;/h2>
&lt;p>codegraph 預設啟用 file watcher、用 native OS 事件（macOS FSEvents / Linux inotify / Windows ReadDirectoryChanges）：&lt;/p></description><content:encoded><![CDATA[<h2 id="這個-mcp-解什麼問題">這個 MCP 解什麼問題</h2>
<p>codegraph 的設計動機很具體：<strong>Claude Code 探索 codebase 時 spawn 的 Explore agent 會用 grep / glob / read 連續刷檔，每個 tool call 都吃 token</strong>。codegraph 把這層探索預先做好，agent 直接查預建好的 graph。</p>
<blockquote>
<p>When Claude Code explores a codebase, it spawns Explore agents that scan files with grep, glob, and Read — consuming tokens on every tool call. CodeGraph gives those agents a pre-indexed knowledge graph — symbol relationships, call graphs, and code structure.</p></blockquote>
<p>跟 <a href="/blog/record/codebase-memory-mcp155-%E8%AA%9E%E8%A8%80-tree-sitter-%E7%9F%A5%E8%AD%98%E5%9C%96%E8%AD%9C-mcp-%E7%9A%84%E8%83%BD%E5%8A%9B%E8%88%87%E9%82%8A%E7%95%8C/">cbm</a> 比，codegraph 的 scope 更窄、更專注：不做跨 service 鏈接、不做 ADR / runtime trace 管理、不做 11-signal 語意搜尋，<strong>只把 call graph 跟 symbol relationship 做好</strong>。這個取捨讓它的 MCP tool 只有 10 個、每個責任都很單一。</p>
<h2 id="技術架構tree-sitter--per-language-query--fts5">技術架構：tree-sitter + per-language query + FTS5</h2>
<p>codegraph 的核心 pipeline：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">tree-sitter parse → per-language query 抽 nodes/edges
</span></span><span class="line"><span class="ln">2</span><span class="cl">                  → 解析 reference（import / extends / implements / calls）
</span></span><span class="line"><span class="ln">3</span><span class="cl">                  → 寫進 SQLite + FTS5</span></span></code></pre></div><p>關鍵設計：<strong>對每個語言寫專屬的 tree-sitter query</strong>——比起通用 AST visitor 路線、這個設計能對特定語言的 dispatch pattern 抽到更精確的 node 跟 edge。</p>
<blockquote>
<p>Language-specific queries extract nodes (functions, classes, methods) and edges (calls, imports, extends, implements).</p></blockquote>
<p>這個設計選擇直接決定了 codegraph 對非主流語言（如 Dart / Svelte / Liquid）的支援深度——因為每個語言都有專屬 query，所以 19+ 語言裡的 Dart 真的有 working call graph，不像純 tree-sitter wrapper 那樣只能抽結構。</p>
<p>實際支援的 19+ 語言：</p>
<p>TypeScript、JavaScript、Python、Go、Rust、Java、C#、PHP、Ruby、C、C++、Swift、Kotlin、Scala、Dart、Svelte、Vue、Liquid、Lua、Luau、Pascal/Delphi。</p>
<p>過濾規則：「<strong>Files larger than 1 MB are skipped</strong>」（generated bundle / minified JS / vendored blob 自動忽略）。</p>
<h2 id="auto-syncnative-os-file-watcher--2s-debounce">Auto-sync：native OS file watcher + 2s debounce</h2>
<p>codegraph 預設啟用 file watcher、用 native OS 事件（macOS FSEvents / Linux inotify / Windows ReadDirectoryChanges）：</p>
<ul>
<li>Debounce window：2 秒（避免快速連續存檔重複觸發）</li>
<li>過濾範圍：只看 source 檔案（按副檔名）</li>
<li>行為描述：「<strong>Incremental sync. The graph stays current as you code — no configuration needed</strong>」</li>
</ul>
<p>這層比 <a href="/blog/record/codebase-memory-mcp155-%E8%AA%9E%E8%A8%80-tree-sitter-%E7%9F%A5%E8%AD%98%E5%9C%96%E8%AD%9C-mcp-%E7%9A%84%E8%83%BD%E5%8A%9B%E8%88%87%E9%82%8A%E7%95%8C/">cbm</a> 的「背景 git polling」更貼近 IDE — 改完檔案 2 秒內 graph 就同步好，「邊改邊問」工作流更順。</p>
<p>判讀訊號：剛存完檔立刻問 caller 還是漏，等 3 秒再試一次；持續漏的話跑 <code>codegraph status</code> 看 indexed 數字對不對得上預期。</p>
<h2 id="call-graph-抽取的能力與聲稱">Call graph 抽取的能力與聲稱</h2>
<p>codegraph 對 caller / callee / impact / trace 這四個查詢的覆蓋是它的主賣點。README 對 <code>codegraph_trace</code> 的聲稱是：</p>
<blockquote>
<p>Follow dynamic-dispatch hops (callbacks, React re-render, interface→impl) that grep can&rsquo;t.</p></blockquote>
<p>實際機制 README 沒詳細寫，從 source 推測是「<strong>對某些常見動態 dispatch pattern 寫了專屬 query</strong>」——比如 React component 的 JSX → component definition 解析、interface method → implementation 對應這類。</p>
<p>這個 claim 在實測上<strong>有但有限</strong>——對 type-inferred receiver 仍會漏。例如 Dart 上（<code>Money</code> 在該專案是 extension type）：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-dart" data-lang="dart"><span class="line"><span class="ln">1</span><span class="cl"><span class="kd">final</span> <span class="n">Money</span> <span class="n">samplePrice</span> <span class="o">=</span> <span class="p">...;</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="n">samplePrice</span><span class="p">.</span><span class="n">multiplyByRate</span><span class="p">(</span><span class="n">rate</span><span class="p">);</span>   <span class="o">//</span> <span class="err">←</span> <span class="n">codegraph</span> <span class="err">抽不到這條</span> <span class="n">edge</span></span></span></code></pre></div><p><code>samplePrice</code> 是 local variable，要做型別推斷才知道 receiver 是 <code>Money</code>。tree-sitter 看到的只是 <code>&lt;identifier&gt;.multiplyByRate(...)</code>、不知道 <code>samplePrice</code> 的型別、無法 dispatch 到 <code>Money.multiplyByRate</code>。</p>
<p>判讀訊號：<strong>對「靠型別解析才能找到的 callsite」會漏</strong>。如果專案大量使用 generics、type aliasing、factory pattern 隱藏型別、duck typing，codegraph 的 caller 數字會系統性偏低。重要 refactor 別只看它的數字決策。</p>
<p>下一步路由：實測對照數字見 <a href="/blog/record/%E4%B8%89-mcp-%E5%B7%A5%E4%BD%9C%E6%B5%81%E8%88%87-dart-%E5%AF%A6%E6%B8%ACcbm-/-codegraph-/-serena-%E7%9A%84%E8%81%B7%E8%B2%AC%E5%88%86%E5%B7%A5%E8%88%87%E4%B8%89%E5%88%80%E6%B5%81/">三 MCP 工作流與 Dart 實測</a>。</p>
<h2 id="caller-跟-callsite-的計數單位差異">Caller 跟 callsite 的計數單位差異</h2>
<p>codegraph 的 <code>codegraph_callers</code> 採用的計數單位是「<strong>caller symbol 數</strong>」（同一個 method 內呼叫目標兩次仍然只算 1 個 caller）——跟「callsite 數」屬於兩種不同的統計方式。</p>
<p>這個設計的影響：跟 LSP-based 工具（如 <a href="/blog/record/serena%E6%8A%8A-lsp-%E5%8C%85%E6%88%90-agent-first-mcp-%E7%9A%84-symbol-level-%E7%B7%A8%E8%BC%AF%E6%96%B9%E6%A1%88/">serena</a>）對比時，數字會看起來「少」，但這是計數規則的差異、跟精度差距屬於兩個不同議題。寫實測 baseline 時要把這個單位寫死，避免「codegraph 回 3、serena 回 9」被誤判成「codegraph 漏 6 個」。</p>
<p>實際上這 3 vs 9 的差距要分兩段看：codegraph 抓到的 3 個 caller symbol 對應 6 個 callsite（同一個 method 內有多處呼叫、被計數規則合併成 1 caller）、剩下的 3 個 callsite 在第 4 個檔案（<code>product.dart</code>）、是真的漏（type-inferred dispatch）。算術：6 callsite（codegraph 算 3 caller）+ 3 callsite（真的漏）= serena 的 9。要拆開看才知道哪部分是計數差異、哪部分是能力差距。</p>
<h2 id="14-web-framework-的-route-識別">14 web framework 的 route 識別</h2>
<p>codegraph 內建對 web framework 的 route 識別：</p>
<p>Django、Flask、FastAPI、Express、NestJS、Laravel、Drupal、Rails、Spring、Gin / chi / gorilla / mux、Axum / actix / Rocket、ASP.NET、Vapor、React Router、SvelteKit。</p>
<p>README 標稱「14 個」、實際展開後是 15 條（Gin / chi / gorilla / mux 跟 Axum / actix / Rocket 各算一組路由生態）。這個小落差源自分組計數方式、不影響功能。</p>
<p>這層的角色是讓 <code>codegraph_search</code> 能用 URL pattern 找到對應 handler，不必去猜 handler 函式名。但跟 <a href="/blog/record/codebase-memory-mcp155-%E8%AA%9E%E8%A8%80-tree-sitter-%E7%9F%A5%E8%AD%98%E5%9C%96%E8%AD%9C-mcp-%E7%9A%84%E8%83%BD%E5%8A%9B%E8%88%87%E9%82%8A%E7%95%8C/">cbm</a> 的 first-class HTTP_CALLS edge 不一樣，codegraph 沒做「client URL 字面值 → server route 比對」，所以<strong>單一 service 內找 handler 可以、跨 service 鏈接做不到</strong>。</p>
<p>判讀訊號：純前端 / 純後端 repo 上這層夠用；要跨 service 追 cross-service call 仍要靠 cbm 或別的工具。</p>
<h2 id="10-個-mcp-tool-的責任分工">10 個 MCP tool 的責任分工</h2>
<table>
  <thead>
      <tr>
          <th>Tool</th>
          <th>責任</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>codegraph_search</code></td>
          <td>用名稱 / pattern 找 symbol</td>
      </tr>
      <tr>
          <td><code>codegraph_context</code></td>
          <td>給定 task，組出相關 code context</td>
      </tr>
      <tr>
          <td><code>codegraph_trace</code></td>
          <td>兩個 symbol 之間的 call path、每跳含 body</td>
      </tr>
      <tr>
          <td><code>codegraph_callers</code></td>
          <td>找誰呼叫了 X（一跳）</td>
      </tr>
      <tr>
          <td><code>codegraph_callees</code></td>
          <td>找 X 呼叫了誰（一跳）</td>
      </tr>
      <tr>
          <td><code>codegraph_impact</code></td>
          <td>改 X 會影響什麼（blast radius）</td>
      </tr>
      <tr>
          <td><code>codegraph_node</code></td>
          <td>取 symbol 詳情 + 原始碼</td>
      </tr>
      <tr>
          <td><code>codegraph_explore</code></td>
          <td>一次回多個相關 symbol 的原始碼</td>
      </tr>
      <tr>
          <td><code>codegraph_files</code></td>
          <td>已索引的檔案結構</td>
      </tr>
      <tr>
          <td><code>codegraph_status</code></td>
          <td>索引健康度跟統計</td>
      </tr>
  </tbody>
</table>
<p>設計上有四個值得單獨展開的 tool：</p>
<p><strong><code>codegraph_explore</code></strong> 是為了<strong>省 tool call</strong> — 不用對 N 個 symbol 各呼叫一次 <code>codegraph_node</code>、一次拿到所有 source。這直接呼應 codegraph 整體「省 token / 省 tool call」的設計目標。</p>
<p><strong><code>codegraph_trace</code></strong> <strong>單一 call 涵蓋整個路徑</strong>、每一跳的 function body 直接 inline 在結果裡。對「X 怎麼影響到 Y」這種多跳問題，傳統做法要 N 次 <code>codegraph_callers</code> + N 次 <code>codegraph_node</code>，trace 把這壓成 1 次。代價是若兩個 symbol 之間沒有 static-resolvable 路徑（如 type-inferred dispatch 中斷），會直接回「No direct path」、不會主動找替代解釋。</p>
<p><strong><code>codegraph_context</code></strong> 跟 <code>codegraph_explore</code> 的責任差別常被搞混。<code>codegraph_explore</code> 是「我已經知道要看哪幾個 symbol」、一次拿原始碼；<code>codegraph_context</code> 是「我有個 task description、不知道相關 symbol 是哪些」、由它從 task 內容拉出可能相關的 graph 鄰域。前者是「精確檢索」、後者是「概念性彙整」。實務上 task agent 開新任務時用 <code>codegraph_context</code>、debug 細節時用 <code>codegraph_explore</code>。</p>
<p><strong><code>codegraph_impact</code></strong> 是 blast radius 工具、但<strong>它的精度被 tree-sitter syntactic 限制卡住</strong>——跟 caller / callee 同源、type-inferred dispatch 的影響範圍會漏。實務影響：對「rename method 會影響什麼」這類重要 refactor 不能單看它的數字、要走 LSP 工具 cross-check。判讀訊號：<code>codegraph_impact X</code> 回的 affected symbol 數明顯少於預期、且 X 是被廣泛使用的 type / method 時、blast radius 多半有漏、要補 LSP 驗證。</p>
<h2 id="token-efficiency-benchmark方法論與限制">Token efficiency benchmark：方法論與限制</h2>
<p>README 聲稱「<strong>~35% cheaper · ~70% fewer tool calls · 100% local</strong>」、median 跨 7 codebase：</p>
<ul>
<li>Cost: 35% reduction</li>
<li>Tokens: 57% fewer</li>
<li>Time: 46% faster</li>
<li>Tool calls: 71% fewer</li>
</ul>
<p>方法論：</p>
<blockquote>
<p>Claude Opus 4.7 run headlessly. WITH = CodeGraph&rsquo;s MCP server enabled, WITHOUT = empty MCP config. Same question per repo, 4 runs per arm, median reported.</p></blockquote>
<p>7 個 benchmark codebase：</p>
<table>
  <thead>
      <tr>
          <th>Repo</th>
          <th>語言</th>
          <th>規模</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>VS Code</td>
          <td>TypeScript</td>
          <td>~10k 檔</td>
      </tr>
      <tr>
          <td>Excalidraw</td>
          <td>TypeScript</td>
          <td>~640 檔</td>
      </tr>
      <tr>
          <td>Django</td>
          <td>Python</td>
          <td>~3k 檔</td>
      </tr>
      <tr>
          <td>Tokio</td>
          <td>Rust</td>
          <td>~790 檔</td>
      </tr>
      <tr>
          <td>OkHttp</td>
          <td>Java</td>
          <td>~645 檔</td>
      </tr>
      <tr>
          <td>Gin</td>
          <td>Go</td>
          <td>~110 檔</td>
      </tr>
      <tr>
          <td>Alamofire</td>
          <td>Swift</td>
          <td>~110 檔</td>
      </tr>
  </tbody>
</table>
<p>幾個要注意的解讀偏差：</p>
<p><strong>Benchmark 集中在 codegraph 強項語言</strong>。VS Code / Django / Tokio 都是 codegraph 的核心支援語言、且 LSP 生態成熟。Dart / Svelte / Liquid 這類 long-tail 語言沒列在 benchmark 內，token 節省效果在那些語言上是否成立不知道。</p>
<p><strong>Empty MCP config 的對照組不一定貼近實務</strong>。沒裝任何 MCP 時 agent 的 baseline 探索行為跟「裝了其他 MCP」不同。實務 stack 通常多個 MCP 並用，這個 35% 對「加裝 codegraph 進已有 MCP stack」的邊際效益會打折。</p>
<p>判讀訊號：benchmark 數字當「值得試」的參考、不當「裝了就省 35%」的硬保證。實際省多少要在自己的 stack 上跑同樣 question set 才知。</p>
<h2 id="安裝行為">安裝行為</h2>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">npm i -g @colbymchenry/codegraph
</span></span><span class="line"><span class="ln">2</span><span class="cl">codegraph install --target claude --location global -y
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="nb">cd</span> your-project <span class="o">&amp;&amp;</span> codegraph init -i</span></span></code></pre></div><p><code>codegraph install</code> 會把 MCP server 條目寫進 <code>~/.claude.json</code> 的 <code>mcpServers</code>、<code>codegraph init -i</code> 在當前 repo 建 <code>.codegraph/codegraph.db</code>、啟動 watcher。</p>
<p>跟 <a href="/blog/record/codebase-memory-mcp155-%E8%AA%9E%E8%A8%80-tree-sitter-%E7%9F%A5%E8%AD%98%E5%9C%96%E8%AD%9C-mcp-%E7%9A%84%E8%83%BD%E5%8A%9B%E8%88%87%E9%82%8A%E7%95%8C/">cbm</a> 不一樣：codegraph <strong>不寫 PreToolUse hook</strong>、不攔截 Grep/Glob。它純粹當 MCP server 提供 tool、決策權留給 agent，對既有工作流的干擾較小。</p>
<p>CLI mode 是另一個方便點：所有 MCP tool 在 CLI 都有對應指令（<code>codegraph callers X</code> / <code>codegraph trace X Y</code>），不必等 Claude Code 重啟載入 MCP 就能先在 terminal 驗證效果。</p>
<h2 id="適用--不適用情境的判讀">適用 / 不適用情境的判讀</h2>
<p><strong>適用情境</strong>：</p>
<ul>
<li>主力語言在 19+ 支援列表內，且需要可靠的 caller / impact / trace 查詢</li>
<li>「邊改邊問」工作流（auto-sync 2s debounce 比較貼近 IDE）</li>
<li>希望 MCP 保持原生 grep / glob 行為、把決策權留給 agent 自主判斷（避開 hook 層強制介入）</li>
<li>要 CLI 跟 MCP 雙管道使用（CLI 可先試、MCP 給 agent 用）</li>
</ul>
<p><strong>不適用情境</strong>：</p>
<ul>
<li>語言不在支援列表（codegraph 不像 cbm 一次 vendor 155 個 grammar）</li>
<li>需要跨 service 的 client URL → server route 鏈接（codegraph 只認 route definition）</li>
<li>需要 symbol-level atomic edit（codegraph 純讀、沒 rename / replace_symbol_body）</li>
<li>重要 refactor 要保證不漏 callsite（tree-sitter syntactic 上限會漏 type-inferred dispatch）</li>
</ul>
<p><strong>搭配建議</strong>：對 type-inferred dispatch 漏的部分，可以靠 LSP-based 工具（如 <a href="/blog/record/serena%E6%8A%8A-lsp-%E5%8C%85%E6%88%90-agent-first-mcp-%E7%9A%84-symbol-level-%E7%B7%A8%E8%BC%AF%E6%96%B9%E6%A1%88/">serena</a>）補位。對概念性自然語言搜尋，<a href="/blog/record/codebase-memory-mcp155-%E8%AA%9E%E8%A8%80-tree-sitter-%E7%9F%A5%E8%AD%98%E5%9C%96%E8%AD%9C-mcp-%E7%9A%84%E8%83%BD%E5%8A%9B%E8%88%87%E9%82%8A%E7%95%8C/">cbm</a> 的 11-signal scoring 比 codegraph 的 symbol pattern match 更強。三者怎麼分工見 <a href="/blog/record/%E4%B8%89-mcp-%E5%B7%A5%E4%BD%9C%E6%B5%81%E8%88%87-dart-%E5%AF%A6%E6%B8%ACcbm-/-codegraph-/-serena-%E7%9A%84%E8%81%B7%E8%B2%AC%E5%88%86%E5%B7%A5%E8%88%87%E4%B8%89%E5%88%80%E6%B5%81/">三 MCP 工作流與 Dart 實測</a>。</p>
<h2 id="結論">結論</h2>
<p>codegraph 的核心價值是<strong>用 per-language tree-sitter query 把 call graph 做成 19+ 語言通用的 MCP 服務</strong>，加上 auto-sync 跟 CLI 雙管道。它的 scope 聚焦在 call graph、比 cbm 窄很多、但聚焦範圍內品質很高。</p>
<p>它的型別解析靠 tree-sitter syntactic：<strong>receiver 是顯式型別宣告或 literal 的 callsite 解得好、receiver 要靠型別推斷的 callsite 會漏</strong>。判斷 codegraph 在自己專案上的可信度，先估專案有多少比例的 call 是 type-inferred receiver——比例高就要配 LSP 工具補位、比例低就放心用。</p>
]]></content:encoded></item><item><title>serena：把 LSP 包成 agent-first MCP 的 symbol-level 編輯方案</title><link>https://tarrragon.github.io/blog/record/serena%E6%8A%8A-lsp-%E5%8C%85%E6%88%90-agent-first-mcp-%E7%9A%84-symbol-level-%E7%B7%A8%E8%BC%AF%E6%96%B9%E6%A1%88/</link><pubDate>Mon, 25 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/record/serena%E6%8A%8A-lsp-%E5%8C%85%E6%88%90-agent-first-mcp-%E7%9A%84-symbol-level-%E7%B7%A8%E8%BC%AF%E6%96%B9%E6%A1%88/</guid><description>&lt;h2 id="這個-mcp-解什麼問題">這個 MCP 解什麼問題&lt;/h2>
&lt;p>serena 的核心定位是「&lt;strong>把現成 LSP 生態包成適合 agent 用的高階抽象&lt;/strong>」。它不自建 type system、不自寫 parser，直接 spawn 各語言對應的 language server（Dart 用 &lt;code>dart analysis_server&lt;/code>、TS 用 &lt;code>tsserver&lt;/code>、Rust 用 &lt;code>rust-analyzer&lt;/code> 等），把 LSP 的能力轉成 MCP tool。&lt;/p>
&lt;p>設計哲學是 README 自己歸納的「agent-first tool design」：&lt;/p>
&lt;blockquote>
&lt;p>Involves robust high-level abstractions, distinguishing it from approaches that rely on low-level concepts like line numbers or primitive search patterns.&lt;/p>&lt;/blockquote>
&lt;p>換言之，serena 的所有編輯都是 &lt;strong>symbol-level&lt;/strong>——讓 agent 直接用 symbol 語意操作（「把 X function 的 body 整個換掉」、「在 Y class 後面插一段」、「rename Z」），跳過 line number 跟 text patch 這層 raw text 處理。對應的是 LSP 路線本來就有的 symbol 結構與 reference 追蹤。&lt;/p>
&lt;p>跟 tree-sitter 路線的本質分野：tree-sitter 只給結構、不給型別；LSP 給的是「IDE 等級的真型別系統」。代價是 LSP 要每個語言裝對應 language server、執行期 spawn process、per-session 維護狀態。&lt;/p>
&lt;h2 id="部署形態兩個-backend執行期-spawn-lsp">部署形態：兩個 backend、執行期 spawn LSP&lt;/h2>
&lt;p>serena 提供兩個 backend：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Backend&lt;/th>
 &lt;th>適用情境&lt;/th>
 &lt;th>取捨&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Language Server&lt;/td>
 &lt;td>預設、開源、跨平台&lt;/td>
 &lt;td>要對應語言的 language server 在環境內&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>JetBrains Plugin&lt;/td>
 &lt;td>已用 JetBrains IDE 的 paid 使用者&lt;/td>
 &lt;td>借用 IDE 完整能力（debug / breakpoint）&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Language Server backend 是 OSS 用戶會接觸的路線。serena 透過 LSP 抽象涵蓋 40+ 語言、實際能力依各語言 LSP 成熟度而定——Python / TypeScript / Go / Rust / Java / C# / Dart 等主流語言由 serena 內建 bootstrap 自動下載 server、冷門語言（如 Liquid / Pascal）需要使用者自己準備 server binary、無 server 的語言視同 fallback 到純文字工具。判讀訊號：跑 &lt;code>activate_project&lt;/code> 後若 serena 沒在背景 spawn 對應 LSP、表示該語言走 fallback 路線、&lt;code>find_referencing_symbols&lt;/code> 等型別敏感 tool 不可用。&lt;/p>
&lt;p>對 Dart 而言：serena 啟動時 spawn &lt;code>dart analysis_server&lt;/code>、跟 Flutter SDK 內附的同一隻。所以 serena 對 Dart 的能力等同 &lt;code>dart analysis_server&lt;/code> 暴露的能力——比 tree-sitter 路線高一個量級。&lt;/p>
&lt;h2 id="per-session-模型與-activate_project">Per-session 模型與 activate_project&lt;/h2>
&lt;p>serena 的 LSP backend 是 &lt;strong>per-session&lt;/strong> 的：&lt;/p></description><content:encoded><![CDATA[<h2 id="這個-mcp-解什麼問題">這個 MCP 解什麼問題</h2>
<p>serena 的核心定位是「<strong>把現成 LSP 生態包成適合 agent 用的高階抽象</strong>」。它不自建 type system、不自寫 parser，直接 spawn 各語言對應的 language server（Dart 用 <code>dart analysis_server</code>、TS 用 <code>tsserver</code>、Rust 用 <code>rust-analyzer</code> 等），把 LSP 的能力轉成 MCP tool。</p>
<p>設計哲學是 README 自己歸納的「agent-first tool design」：</p>
<blockquote>
<p>Involves robust high-level abstractions, distinguishing it from approaches that rely on low-level concepts like line numbers or primitive search patterns.</p></blockquote>
<p>換言之，serena 的所有編輯都是 <strong>symbol-level</strong>——讓 agent 直接用 symbol 語意操作（「把 X function 的 body 整個換掉」、「在 Y class 後面插一段」、「rename Z」），跳過 line number 跟 text patch 這層 raw text 處理。對應的是 LSP 路線本來就有的 symbol 結構與 reference 追蹤。</p>
<p>跟 tree-sitter 路線的本質分野：tree-sitter 只給結構、不給型別；LSP 給的是「IDE 等級的真型別系統」。代價是 LSP 要每個語言裝對應 language server、執行期 spawn process、per-session 維護狀態。</p>
<h2 id="部署形態兩個-backend執行期-spawn-lsp">部署形態：兩個 backend、執行期 spawn LSP</h2>
<p>serena 提供兩個 backend：</p>
<table>
  <thead>
      <tr>
          <th>Backend</th>
          <th>適用情境</th>
          <th>取捨</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Language Server</td>
          <td>預設、開源、跨平台</td>
          <td>要對應語言的 language server 在環境內</td>
      </tr>
      <tr>
          <td>JetBrains Plugin</td>
          <td>已用 JetBrains IDE 的 paid 使用者</td>
          <td>借用 IDE 完整能力（debug / breakpoint）</td>
      </tr>
  </tbody>
</table>
<p>Language Server backend 是 OSS 用戶會接觸的路線。serena 透過 LSP 抽象涵蓋 40+ 語言、實際能力依各語言 LSP 成熟度而定——Python / TypeScript / Go / Rust / Java / C# / Dart 等主流語言由 serena 內建 bootstrap 自動下載 server、冷門語言（如 Liquid / Pascal）需要使用者自己準備 server binary、無 server 的語言視同 fallback 到純文字工具。判讀訊號：跑 <code>activate_project</code> 後若 serena 沒在背景 spawn 對應 LSP、表示該語言走 fallback 路線、<code>find_referencing_symbols</code> 等型別敏感 tool 不可用。</p>
<p>對 Dart 而言：serena 啟動時 spawn <code>dart analysis_server</code>、跟 Flutter SDK 內附的同一隻。所以 serena 對 Dart 的能力等同 <code>dart analysis_server</code> 暴露的能力——比 tree-sitter 路線高一個量級。</p>
<h2 id="per-session-模型與-activate_project">Per-session 模型與 activate_project</h2>
<p>serena 的 LSP backend 是 <strong>per-session</strong> 的：</p>
<ul>
<li>沒有持久化 graph DB（不像 <a href="/blog/record/codebase-memory-mcp155-%E8%AA%9E%E8%A8%80-tree-sitter-%E7%9F%A5%E8%AD%98%E5%9C%96%E8%AD%9C-mcp-%E7%9A%84%E8%83%BD%E5%8A%9B%E8%88%87%E9%82%8A%E7%95%8C/">cbm</a> / <a href="/blog/record/codegraph%E7%94%A8-tree-sitter-per-language-query-%E6%92%90%E8%B5%B7-19-%E8%AA%9E%E8%A8%80-call-graph-%E7%9A%84-mcp/">codegraph</a> 把結果寫進 SQLite）</li>
<li>每個 session 啟動時要 <code>activate_project</code>、spawn 對應 language server、warm up index</li>
<li>Session 結束 server 也跟著 terminate，下次重來</li>
</ul>
<p><code>activate_project</code> 的角色是告訴 serena「這個 session 接下來要分析哪個 project root」，serena 才知道要 spawn 哪幾個 language server、index 哪個 workspace。一個 session 內可以切多次 project，但同時只 active 一個。</p>
<p>這個模型的取捨很清楚：</p>
<ul>
<li><strong>好處</strong>：永遠拿到當下最新狀態（不會有 stale index 問題）、不必管 watcher / debounce</li>
<li><strong>代價</strong>：每次 session warm-up 有秒級至分鐘級延遲（大專案 LSP indexing 慢）、跨 session 不能重用結果</li>
</ul>
<p>判讀訊號：第一次查詢回得慢、之後快——這是 LSP indexing warm-up。若每次查都慢、檢查 LSP 是否因記憶體不足重啟。</p>
<h2 id="symbol-level-atomic-edit-的價值">Symbol-level atomic edit 的價值</h2>
<p>serena 的 editing tool 都是 symbol-level：</p>
<ul>
<li><code>replace_symbol_body</code>：取代某個 function / method / class 的 body</li>
<li><code>insert_after_symbol</code> / <code>insert_before_symbol</code>：在指定 symbol 前後插入內容</li>
<li><code>safe_delete_symbol</code>：刪除 symbol 並檢查 reference</li>
<li><code>rename_symbol</code>：rename symbol、自動更新所有 reference（LS backend 限 symbol 範圍、JetBrains backend 額外支援 file / directory 層級重命名）</li>
</ul>
<p>對比 <code>Edit</code> tool 用「old_string / new_string」做 text-level patch：</p>
<table>
  <thead>
      <tr>
          <th>操作</th>
          <th>text-level edit</th>
          <th>symbol-level edit</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>改 method body</td>
          <td>要 match 整個 body 含縮排與空白</td>
          <td>指定 method 名、給新 body</td>
      </tr>
      <tr>
          <td>Method body 內某行有特殊字元</td>
          <td>容易 escape 錯、match fail</td>
          <td>不受影響、agent 不處理 raw text</td>
      </tr>
      <tr>
          <td>同名 method 在多個 class</td>
          <td>要 match 含 class 名上下文</td>
          <td>用 <code>ClassName/methodName</code> 路徑唯一定位</td>
      </tr>
      <tr>
          <td>Rename 跨檔</td>
          <td>要全 repo grep + 逐檔 patch</td>
          <td>一次 call 完成 + LSP 保證 reference 全更新</td>
      </tr>
  </tbody>
</table>
<p>實務上的價值：<strong>type-sensitive refactor 的事故率大幅降低</strong>。改 method 不會手抖把 indentation 改錯、rename 不會漏改 reference。代價是 symbol 路徑必須寫成包含父層的完整形式（<code>ClassName/methodName</code>）。</p>
<p>判讀訊號：寫 <code>replace_symbol_body</code> 後若 LSP 報 syntax error、先 <code>get_diagnostics_for_file</code> 看具體錯在哪、別直接 retry 同個 patch。</p>
<h2 id="find_referencing_symbolslsp-路線的型別精確-caller-來源">find_referencing_symbols：LSP 路線的型別精確 caller 來源</h2>
<p>對 Dart / Swift / Kotlin 這類 tree-sitter 工具支援薄弱的語言，<code>find_referencing_symbols</code> 是少數能拿到「<strong>型別精確的 caller 清單</strong>」的 MCP tool。</p>
<p>實測對 Dart <code>Money.multiplyByRate</code>（某商業專案、<code>Money</code> 是 extension type）：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">serena find_referencing_symbols → 4 個檔案、9 個 callsite
</span></span><span class="line"><span class="ln">2</span><span class="cl">codegraph callers              → 3 個 caller symbol（漏 3 個 callsite）
</span></span><span class="line"><span class="ln">3</span><span class="cl">cbm trace_call_path            → 0 callers（Dart 不在 hybrid resolution 名單）</span></span></code></pre></div><p>差距來源就是型別解析：<code>samplePrice.multiplyByRate(...)</code> 這種 receiver 是 local variable 的 callsite，要知道 <code>samplePrice</code> 的型別是 <code>Money</code> 才能 dispatch 到正確 method。LSP 走 <code>dart analysis_server</code> 拿到完整型別資訊，所以這層 dispatch 是精確的。</p>
<p>下一步路由：對照數字與 5 個實測實驗見 <a href="/blog/record/%E4%B8%89-mcp-%E5%B7%A5%E4%BD%9C%E6%B5%81%E8%88%87-dart-%E5%AF%A6%E6%B8%ACcbm-/-codegraph-/-serena-%E7%9A%84%E8%81%B7%E8%B2%AC%E5%88%86%E5%B7%A5%E8%88%87%E4%B8%89%E5%88%80%E6%B5%81/">三 MCP 工作流與 Dart 實測</a>。</p>
<h2 id="30-mcp-tool-的分類">30+ MCP tool 的分類</h2>
<p>serena 的 tool 數量比 cbm / codegraph 都多、覆蓋更廣的工作流：</p>
<table>
  <thead>
      <tr>
          <th>類別</th>
          <th>Tool</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>檢索</td>
          <td><code>find_symbol</code>、<code>get_symbols_overview</code>、<code>find_referencing_symbols</code>、<code>find_declaration</code>、<code>find_implementations</code>、<code>get_diagnostics_for_file</code></td>
      </tr>
      <tr>
          <td>編輯（symbol）</td>
          <td><code>replace_symbol_body</code>、<code>insert_after_symbol</code>、<code>insert_before_symbol</code>、<code>safe_delete_symbol</code>、<code>rename_symbol</code></td>
      </tr>
      <tr>
          <td>編輯（text）</td>
          <td><code>replace_content</code>、<code>search_for_pattern</code></td>
      </tr>
      <tr>
          <td>檔案 / 目錄</td>
          <td><code>list_dir</code>、<code>find_file</code>、<code>read_file</code>、<code>create_text_file</code></td>
      </tr>
      <tr>
          <td>執行</td>
          <td><code>execute_shell_command</code></td>
      </tr>
      <tr>
          <td>Memory</td>
          <td><code>write_memory</code>、<code>read_memory</code>、<code>list_memories</code>、<code>delete_memory</code>、<code>rename_memory</code>、<code>edit_memory</code></td>
      </tr>
      <tr>
          <td>Project</td>
          <td><code>activate_project</code>、<code>get_current_config</code>、<code>onboarding</code>、<code>initial_instructions</code></td>
      </tr>
      <tr>
          <td>Debug</td>
          <td>（僅 JetBrains backend）breakpoint、variable inspection、expression eval</td>
      </tr>
  </tbody>
</table>
<p>幾個值得單獨展開的類別：</p>
<p><strong>檢索類</strong>是 serena 跟 LSP 黏最緊的入口——<code>find_symbol</code> / <code>find_declaration</code> / <code>find_implementations</code> 走 LSP 的 textDocument 命令、<code>find_referencing_symbols</code> 是 LSP <code>references</code> 的 wrapper。這層是 serena 不可替代的核心、所有需要型別精確的查詢都從這走。</p>
<p><strong><code>get_diagnostics_for_file</code></strong> 是把 LSP 的編譯診斷直接暴露給 agent。改完 code 不必跑 build 就能知道有沒有 type error / unused import / missing await。對 type-sensitive refactor 是必備。</p>
<p><strong>Symbol-level edit vs text-level edit 的選用</strong>：symbol-level（<code>replace_symbol_body</code> / <code>insert_after_symbol</code> / <code>safe_delete_symbol</code> / <code>rename_symbol</code>）對「有明確 symbol 邊界的修改」最穩、不會踩到 indentation 或 escape 問題；text-level（<code>replace_content</code> / <code>search_for_pattern</code>）保留給「跨 symbol 邊界、或非 code 內容」的場合（如改 markdown、config、log 字串）。判讀訊號：要動的內容能不能用「ClassName/methodName」這種 symbol path 定位？能就走 symbol-level、不能就 text-level。</p>
<p><strong><code>execute_shell_command</code></strong> 是 LSP-only 工具裡的「逃生門」——LSP 本身不執行命令、但實務上 agent 需要跑 test / build / git status / 任意 CLI 工具來驗證自己的修改。這條等於把 LSP-based 工具補成「能 query 又能執行」的完整 workflow 工具。安全考量：因為它能跑任意 shell command、Claude Code 對 serena 的 trust level 要跟 Bash tool 對齊看待、不要假設它「只是讀取工具」。</p>
<p><strong>Memory system</strong> 採用「跨 session 的 markdown 筆記檔」形式、屬於自由格式存儲。用途接近 agent 的本地長期記憶——存「這個專案的 setup 注意事項」、「上次 refactor 的決策紀錄」、「常用的 codebase pattern」。跟 <a href="/blog/record/codebase-memory-mcp155-%E8%AA%9E%E8%A8%80-tree-sitter-%E7%9F%A5%E8%AD%98%E5%9C%96%E8%AD%9C-mcp-%E7%9A%84%E8%83%BD%E5%8A%9B%E8%88%87%E9%82%8A%E7%95%8C/">cbm</a> 的 <code>manage_adr</code>（結構化 ADR）走相反取向：serena 把 schema 留給使用者自定、manage_adr 給定 ADR 欄位結構。</p>
<p><strong>Project 類</strong>（<code>activate_project</code> / <code>get_current_config</code> / <code>onboarding</code> / <code>initial_instructions</code>）是 serena 對「agent 第一次接觸新專案要先讀什麼」的明確協議。<code>onboarding</code> 讓 agent 主動 read 專案 onboarding doc、<code>initial_instructions</code> 給 agent 一份 serena 自己的使用手冊、<code>activate_project</code> 切 project root、<code>get_current_config</code> 暴露當前 session 的配置給 agent debug。這層降低盲目探索成本、是把 serena 從「LSP wrapper」抬升到「agent-first」的關鍵。</p>
<h2 id="per-session-與持久化-graph-的搭配問題">Per-session 與持久化 graph 的搭配問題</h2>
<p>serena 的 per-session 模型在「<strong>單純查 caller / refactor</strong>」工作流很合適，但對「<strong>自然語言搜尋 / 跨 session 累積 graph context</strong>」就不夠。</p>
<p>實際差距：</p>
<ul>
<li>想用「金額顯示相關」這種概念性 query 找 symbol → serena 沒有 BM25 / 11-signal scoring、只有 <code>search_for_pattern</code>（regex / literal）跟 <code>find_symbol</code>（exact name match）</li>
<li>想跨 session 累積「這個 codebase 有哪些 module」的整體 inventory → serena 每次重 index、沒有持久化的 graph 可查</li>
<li>想做跨 service HTTP_CALLS 鏈接 → serena 沒有這層</li>
</ul>
<p>判讀訊號：搜尋需求若是「我知道某個 symbol 的精確名稱、要找它的 references」就用 serena；若是「我不知道精確名稱、用概念找」要配合 cbm。</p>
<h2 id="安裝行為">安裝行為</h2>
<p>serena 在 Claude Code 是 plugin 形式：在 plugin marketplace enable 即可，不需要單獨 <code>npm i</code>。Plugin 啟動時 serena 會 spawn LSP，第一次 activate 某個 project 時 indexing 完成才能跑 query。</p>
<p>跟 <a href="/blog/record/codebase-memory-mcp155-%E8%AA%9E%E8%A8%80-tree-sitter-%E7%9F%A5%E8%AD%98%E5%9C%96%E8%AD%9C-mcp-%E7%9A%84%E8%83%BD%E5%8A%9B%E8%88%87%E9%82%8A%E7%95%8C/">cbm</a> / <a href="/blog/record/codegraph%E7%94%A8-tree-sitter-per-language-query-%E6%92%90%E8%B5%B7-19-%E8%AA%9E%E8%A8%80-call-graph-%E7%9A%84-mcp/">codegraph</a> 的差異：</p>
<ul>
<li><strong>不寫 PreToolUse hook</strong>、不攔截既有 grep / glob 行為</li>
<li><strong>不在 <code>~/.claude.json</code> 直接加 mcpServers</strong>（plugin 機制管理）</li>
<li><strong>每個 project 要顯式 activate</strong>——第一次 session 進新 project 時 agent 要主動跑 <code>activate_project</code> 或在 plugin config 預設 project root</li>
</ul>
<p>要注意的點：</p>
<p><strong>Language server 缺失時的失敗模式</strong>。對冷門語言（如 Liquid / Pascal）若環境沒裝 language server、<code>activate_project</code> 會回失敗但不會主動裝。需要使用者自己準備 server binary。Dart / TS / Python / Go / Rust 等主流語言 serena 會 bootstrap 處理。</p>
<p><strong>JetBrains backend 是付費</strong>。OSS 用戶只能用 LS backend、得不到 debug 整合那組能力。</p>
<h2 id="適用--不適用情境的判讀">適用 / 不適用情境的判讀</h2>
<p><strong>適用情境</strong>：</p>
<ul>
<li>主力語言有成熟 LSP（Dart / TS / Python / Go / Rust / Java / C# 等）</li>
<li>型別敏感的 refactor 場景（rename / extract method / 跨檔 reference 更新）</li>
<li>要編譯 diagnostic 即時反饋（取代 build / test cycle 的部分功能）</li>
<li>Symbol-level atomic edit 的可靠性比 graph 持久化重要</li>
</ul>
<p><strong>不適用情境</strong>：</p>
<ul>
<li>主力語言 LSP 不成熟或不存在（serena 沒得借力）</li>
<li>需要概念性 / 自然語言搜尋（用 <a href="/blog/record/codebase-memory-mcp155-%E8%AA%9E%E8%A8%80-tree-sitter-%E7%9F%A5%E8%AD%98%E5%9C%96%E8%AD%9C-mcp-%E7%9A%84%E8%83%BD%E5%8A%9B%E8%88%87%E9%82%8A%E7%95%8C/">cbm</a> 的 11-signal scoring）</li>
<li>需要跨 session 累積的 graph context（serena per-session、不持久化）</li>
<li>需要跨 service HTTP/RPC 鏈接（serena 沒這層）</li>
</ul>
<p><strong>搭配建議</strong>：serena 是「<strong>型別精確 + 編輯出口</strong>」的角色。在它擅長的語言上做 caller 追蹤 / refactor、把概念性搜尋讓給 cbm、把日常結構查詢讓給 codegraph。三者怎麼分工見 <a href="/blog/record/%E4%B8%89-mcp-%E5%B7%A5%E4%BD%9C%E6%B5%81%E8%88%87-dart-%E5%AF%A6%E6%B8%ACcbm-/-codegraph-/-serena-%E7%9A%84%E8%81%B7%E8%B2%AC%E5%88%86%E5%B7%A5%E8%88%87%E4%B8%89%E5%88%80%E6%B5%81/">三 MCP 工作流與 Dart 實測</a>。</p>
<h2 id="結論">結論</h2>
<p>serena 的核心價值在三件事：<strong>直接借 LSP 拿型別精確的 reference</strong>、<strong>symbol-level atomic edit 的可靠性</strong>、<strong>編譯 diagnostic 即時整合</strong>。前兩件對任何成熟 LSP 語言都成立，第三件對「改完 code 想立刻驗 type error」的工作流特別重要。</p>
<p>它的能力上限取決於「<strong>目標語言 LSP 成熟度</strong>」——LSP 強的語言上 serena 是強工具、LSP 弱的語言上 serena 也跟著弱。它的能力下限取決於「<strong>持久化 graph 與自然語言搜尋</strong>」這兩層空白——這兩層要靠別的 MCP 補齊。</p>
]]></content:encoded></item><item><title>三 MCP 工作流與 Dart 實測：cbm / codegraph / serena 的職責分工與三刀流</title><link>https://tarrragon.github.io/blog/record/%E4%B8%89-mcp-%E5%B7%A5%E4%BD%9C%E6%B5%81%E8%88%87-dart-%E5%AF%A6%E6%B8%ACcbm-/-codegraph-/-serena-%E7%9A%84%E8%81%B7%E8%B2%AC%E5%88%86%E5%B7%A5%E8%88%87%E4%B8%89%E5%88%80%E6%B5%81/</link><pubDate>Mon, 25 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/record/%E4%B8%89-mcp-%E5%B7%A5%E4%BD%9C%E6%B5%81%E8%88%87-dart-%E5%AF%A6%E6%B8%ACcbm-/-codegraph-/-serena-%E7%9A%84%E8%81%B7%E8%B2%AC%E5%88%86%E5%B7%A5%E8%88%87%E4%B8%89%E5%88%80%E6%B5%81/</guid><description>&lt;h2 id="為什麼需要對照為什麼選-dart">為什麼需要對照、為什麼選 Dart&lt;/h2>
&lt;p>評估 code intelligence MCP 不能只看 README benchmark：每個工具的 benchmark 都選自己擅長的 codebase 跟語言，readme 數字只能參考、不能直接套到自家 stack。&lt;/p>
&lt;p>這次選一個 Dart 商業專案做對照場域有兩個理由：&lt;/p>
&lt;ul>
&lt;li>Dart 是三個工具的「中間地帶」——&lt;a href="https://tarrragon.github.io/blog/record/codebase-memory-mcp155-%E8%AA%9E%E8%A8%80-tree-sitter-%E7%9F%A5%E8%AD%98%E5%9C%96%E8%AD%9C-mcp-%E7%9A%84%E8%83%BD%E5%8A%9B%E8%88%87%E9%82%8A%E7%95%8C/">cbm&lt;/a> 不在 hybrid resolution 名單、&lt;a href="https://tarrragon.github.io/blog/record/codegraph%E7%94%A8-tree-sitter-per-language-query-%E6%92%90%E8%B5%B7-19-%E8%AA%9E%E8%A8%80-call-graph-%E7%9A%84-mcp/">codegraph&lt;/a> 列為 full support、&lt;a href="https://tarrragon.github.io/blog/record/serena%E6%8A%8A-lsp-%E5%8C%85%E6%88%90-agent-first-mcp-%E7%9A%84-symbol-level-%E7%B7%A8%E8%BC%AF%E6%96%B9%E6%A1%88/">serena&lt;/a> 借 &lt;code>dart analysis_server&lt;/code> 有完整 LSP。三條技術路線在同一語言上的能力差距會被最大化。&lt;/li>
&lt;li>Dart 大量用 extension type、generic、factory pattern，這些是 type-inferred dispatch 的高發場景，能逼出每個工具的真實精度差。&lt;/li>
&lt;/ul>
&lt;p>在 Go / TypeScript 上跑同樣對照，結論會反過來——cbm 的 hybrid resolution 在那裡會接近 LSP 精度，三刀流的必要性會降低。所以這篇結論限定「LSP 成熟但 cbm 不在 hybrid resolution 名單」的語言。&lt;/p>
&lt;h2 id="本質差異tree-sitter-syntactic-vs-lsp-type-aware">本質差異：tree-sitter syntactic vs LSP type-aware&lt;/h2>
&lt;p>三個工具在 Dart 上的能力差距，根源是兩條技術路線的本質落差：&lt;/p>
&lt;p>&lt;strong>tree-sitter syntactic&lt;/strong>：只看語法結構。看到 &lt;code>a.b()&lt;/code> 知道有個 method call、不知道 &lt;code>a&lt;/code> 是什麼型別、不知道 &lt;code>b()&lt;/code> 連到哪個 declaration。對 receiver 是 literal 或顯式型別宣告的 callsite 可以解、對 local variable / parameter / 推斷型別的 callsite 會漏。&lt;/p>
&lt;p>&lt;strong>LSP type-aware&lt;/strong>：走 language server 內建的型別推斷引擎。跟 IDE 用同一套後端、能解出 &lt;code>a&lt;/code> 的真實型別、再從 type declaration 找到對應的 method。所以 reference 是型別精確的。&lt;/p>
&lt;p>cbm 的 hybrid type resolution（限 Go / C / C++ / TS / JS）是把 LSP 的型別解析算法 clean-room 重寫進 binary、所以那幾個語言上 cbm 等於有 LSP 級精度但沒 LSP 依賴。Dart 沒得到這個待遇，所以 cbm 在 Dart 上只剩純 syntactic 結構抽取。&lt;/p>
&lt;p>判讀訊號：看一個工具對某語言的能力強弱，問「&lt;strong>它在這語言上做型別解析嗎？&lt;/strong>」——做的話接近 LSP，不做的話只是個結構抽取器。&lt;/p>
&lt;p>這個 framework 建立後、下節展開到 9 個維度的設計對照。&lt;/p>
&lt;h2 id="三個工具的設計差異對照">三個工具的設計差異對照&lt;/h2>
&lt;p>三個工具雖然都是「code intelligence MCP」，設計取向互補：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>維度&lt;/th>
 &lt;th>&lt;a href="https://tarrragon.github.io/blog/record/codebase-memory-mcp155-%E8%AA%9E%E8%A8%80-tree-sitter-%E7%9F%A5%E8%AD%98%E5%9C%96%E8%AD%9C-mcp-%E7%9A%84%E8%83%BD%E5%8A%9B%E8%88%87%E9%82%8A%E7%95%8C/">cbm&lt;/a>&lt;/th>
 &lt;th>&lt;a href="https://tarrragon.github.io/blog/record/codegraph%E7%94%A8-tree-sitter-per-language-query-%E6%92%90%E8%B5%B7-19-%E8%AA%9E%E8%A8%80-call-graph-%E7%9A%84-mcp/">codegraph&lt;/a>&lt;/th>
 &lt;th>&lt;a href="https://tarrragon.github.io/blog/record/serena%E6%8A%8A-lsp-%E5%8C%85%E6%88%90-agent-first-mcp-%E7%9A%84-symbol-level-%E7%B7%A8%E8%BC%AF%E6%96%B9%E6%A1%88/">serena&lt;/a>&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>解析後端&lt;/td>
 &lt;td>tree-sitter + 自寫 type resolver&lt;/td>
 &lt;td>tree-sitter + per-language query&lt;/td>
 &lt;td>LSP（per-language server）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>語言覆蓋&lt;/td>
 &lt;td>155（vendored grammar）&lt;/td>
 &lt;td>19+（每語言寫 query）&lt;/td>
 &lt;td>視 LSP 支援度（40+）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>持久化&lt;/td>
 &lt;td>SQLite + WAL（可 zstd 匯出為 team artifact）&lt;/td>
 &lt;td>SQLite + FTS5&lt;/td>
 &lt;td>per-session、不持久化&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Sync 機制&lt;/td>
 &lt;td>背景 git polling&lt;/td>
 &lt;td>native OS file watcher 2s debounce&lt;/td>
 &lt;td>session warm-up&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Type resolution&lt;/td>
 &lt;td>Go / C / C++ / TS / JS 有 hybrid、其他語言只有 syntactic&lt;/td>
 &lt;td>tree-sitter syntactic 為主、聲稱對部分 dynamic dispatch 有解&lt;/td>
 &lt;td>完整 LSP 型別解析&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>跨 service&lt;/td>
 &lt;td>first-class HTTP_CALLS edge + channel&lt;/td>
 &lt;td>route definition 識別、不做 client URL → server route 比對&lt;/td>
 &lt;td>無&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>概念性自然語言搜尋&lt;/td>
 &lt;td>11-signal scoring + camel split&lt;/td>
 &lt;td>symbol pattern match&lt;/td>
 &lt;td>無&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Symbol-level 編輯&lt;/td>
 &lt;td>無（純讀）&lt;/td>
 &lt;td>無（純讀）&lt;/td>
 &lt;td>完整（replace_symbol_body / rename）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>編譯 diagnostic&lt;/td>
 &lt;td>無&lt;/td>
 &lt;td>無&lt;/td>
 &lt;td>有（&lt;code>get_diagnostics_for_file&lt;/code>）&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>這張表的判讀重點：&lt;strong>三者擅長的事不重疊&lt;/strong>。cbm 強在「找東西」、codegraph 強在「日常 call graph + auto-sync」、serena 強在「型別精確 reference + 編輯出口」。&lt;/p></description><content:encoded><![CDATA[<h2 id="為什麼需要對照為什麼選-dart">為什麼需要對照、為什麼選 Dart</h2>
<p>評估 code intelligence MCP 不能只看 README benchmark：每個工具的 benchmark 都選自己擅長的 codebase 跟語言，readme 數字只能參考、不能直接套到自家 stack。</p>
<p>這次選一個 Dart 商業專案做對照場域有兩個理由：</p>
<ul>
<li>Dart 是三個工具的「中間地帶」——<a href="/blog/record/codebase-memory-mcp155-%E8%AA%9E%E8%A8%80-tree-sitter-%E7%9F%A5%E8%AD%98%E5%9C%96%E8%AD%9C-mcp-%E7%9A%84%E8%83%BD%E5%8A%9B%E8%88%87%E9%82%8A%E7%95%8C/">cbm</a> 不在 hybrid resolution 名單、<a href="/blog/record/codegraph%E7%94%A8-tree-sitter-per-language-query-%E6%92%90%E8%B5%B7-19-%E8%AA%9E%E8%A8%80-call-graph-%E7%9A%84-mcp/">codegraph</a> 列為 full support、<a href="/blog/record/serena%E6%8A%8A-lsp-%E5%8C%85%E6%88%90-agent-first-mcp-%E7%9A%84-symbol-level-%E7%B7%A8%E8%BC%AF%E6%96%B9%E6%A1%88/">serena</a> 借 <code>dart analysis_server</code> 有完整 LSP。三條技術路線在同一語言上的能力差距會被最大化。</li>
<li>Dart 大量用 extension type、generic、factory pattern，這些是 type-inferred dispatch 的高發場景，能逼出每個工具的真實精度差。</li>
</ul>
<p>在 Go / TypeScript 上跑同樣對照，結論會反過來——cbm 的 hybrid resolution 在那裡會接近 LSP 精度，三刀流的必要性會降低。所以這篇結論限定「LSP 成熟但 cbm 不在 hybrid resolution 名單」的語言。</p>
<h2 id="本質差異tree-sitter-syntactic-vs-lsp-type-aware">本質差異：tree-sitter syntactic vs LSP type-aware</h2>
<p>三個工具在 Dart 上的能力差距，根源是兩條技術路線的本質落差：</p>
<p><strong>tree-sitter syntactic</strong>：只看語法結構。看到 <code>a.b()</code> 知道有個 method call、不知道 <code>a</code> 是什麼型別、不知道 <code>b()</code> 連到哪個 declaration。對 receiver 是 literal 或顯式型別宣告的 callsite 可以解、對 local variable / parameter / 推斷型別的 callsite 會漏。</p>
<p><strong>LSP type-aware</strong>：走 language server 內建的型別推斷引擎。跟 IDE 用同一套後端、能解出 <code>a</code> 的真實型別、再從 type declaration 找到對應的 method。所以 reference 是型別精確的。</p>
<p>cbm 的 hybrid type resolution（限 Go / C / C++ / TS / JS）是把 LSP 的型別解析算法 clean-room 重寫進 binary、所以那幾個語言上 cbm 等於有 LSP 級精度但沒 LSP 依賴。Dart 沒得到這個待遇，所以 cbm 在 Dart 上只剩純 syntactic 結構抽取。</p>
<p>判讀訊號：看一個工具對某語言的能力強弱，問「<strong>它在這語言上做型別解析嗎？</strong>」——做的話接近 LSP，不做的話只是個結構抽取器。</p>
<p>這個 framework 建立後、下節展開到 9 個維度的設計對照。</p>
<h2 id="三個工具的設計差異對照">三個工具的設計差異對照</h2>
<p>三個工具雖然都是「code intelligence MCP」，設計取向互補：</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th><a href="/blog/record/codebase-memory-mcp155-%E8%AA%9E%E8%A8%80-tree-sitter-%E7%9F%A5%E8%AD%98%E5%9C%96%E8%AD%9C-mcp-%E7%9A%84%E8%83%BD%E5%8A%9B%E8%88%87%E9%82%8A%E7%95%8C/">cbm</a></th>
          <th><a href="/blog/record/codegraph%E7%94%A8-tree-sitter-per-language-query-%E6%92%90%E8%B5%B7-19-%E8%AA%9E%E8%A8%80-call-graph-%E7%9A%84-mcp/">codegraph</a></th>
          <th><a href="/blog/record/serena%E6%8A%8A-lsp-%E5%8C%85%E6%88%90-agent-first-mcp-%E7%9A%84-symbol-level-%E7%B7%A8%E8%BC%AF%E6%96%B9%E6%A1%88/">serena</a></th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>解析後端</td>
          <td>tree-sitter + 自寫 type resolver</td>
          <td>tree-sitter + per-language query</td>
          <td>LSP（per-language server）</td>
      </tr>
      <tr>
          <td>語言覆蓋</td>
          <td>155（vendored grammar）</td>
          <td>19+（每語言寫 query）</td>
          <td>視 LSP 支援度（40+）</td>
      </tr>
      <tr>
          <td>持久化</td>
          <td>SQLite + WAL（可 zstd 匯出為 team artifact）</td>
          <td>SQLite + FTS5</td>
          <td>per-session、不持久化</td>
      </tr>
      <tr>
          <td>Sync 機制</td>
          <td>背景 git polling</td>
          <td>native OS file watcher 2s debounce</td>
          <td>session warm-up</td>
      </tr>
      <tr>
          <td>Type resolution</td>
          <td>Go / C / C++ / TS / JS 有 hybrid、其他語言只有 syntactic</td>
          <td>tree-sitter syntactic 為主、聲稱對部分 dynamic dispatch 有解</td>
          <td>完整 LSP 型別解析</td>
      </tr>
      <tr>
          <td>跨 service</td>
          <td>first-class HTTP_CALLS edge + channel</td>
          <td>route definition 識別、不做 client URL → server route 比對</td>
          <td>無</td>
      </tr>
      <tr>
          <td>概念性自然語言搜尋</td>
          <td>11-signal scoring + camel split</td>
          <td>symbol pattern match</td>
          <td>無</td>
      </tr>
      <tr>
          <td>Symbol-level 編輯</td>
          <td>無（純讀）</td>
          <td>無（純讀）</td>
          <td>完整（replace_symbol_body / rename）</td>
      </tr>
      <tr>
          <td>編譯 diagnostic</td>
          <td>無</td>
          <td>無</td>
          <td>有（<code>get_diagnostics_for_file</code>）</td>
      </tr>
  </tbody>
</table>
<p>這張表的判讀重點：<strong>三者擅長的事不重疊</strong>。cbm 強在「找東西」、codegraph 強在「日常 call graph + auto-sync」、serena 強在「型別精確 reference + 編輯出口」。</p>
<p>對照表的維度很多、但實務上踩到事故的多半集中在三個維度，把它們各自展開：</p>
<p><strong>Type resolution 決定 caller 數字的可信度</strong>。Dart / Swift / Kotlin 這類「LSP 完整、但 cbm 走純 syntactic 路線」的語言上、tree-sitter 工具回的 caller 數字是 lower bound（實際值通常更高）。<code>samplePrice.multiplyByRate(...)</code> 這種 type-inferred receiver 是這層差距的主戰場。判讀訊號：對熱門 class 跑同一 query、若 tree-sitter 工具 caller 數比 LSP 工具低過半、type-inferred dispatch 在這語言是主流模式、tree-sitter 結果只能當 starting point。</p>
<p><strong>Sync 機制決定「邊改邊問」是否可用</strong>。codegraph 的 native OS file watcher + 2s debounce 最貼近 IDE、cbm 的背景 git polling 有秒級至分級延遲、serena 的 session warm-up 是「啟動時等一次、之後即時」。事故型態：在 codegraph 改完檔案立刻問 caller 多半 OK、在 cbm 立刻問會拿到 stale graph。判讀訊號：問完 query 對結果存疑時、先檢查工具的 sync 狀態（cbm 跑 <code>index_status</code>、codegraph 跑 <code>codegraph_status</code>、serena 直接重 query）。</p>
<p><strong>持久化模式決定跨 session 的累積成本</strong>。cbm / codegraph 寫 SQLite、跨 session 重用；serena per-session、每次 spawn LSP warm up。對「短任務反覆 ad-hoc 查詢」cbm / codegraph 邊際成本更低、對「會做 symbol-level edit 跟 diagnostic」serena 的 per-session warm up 是必要 cost。判讀訊號：第一次 query 慢、之後快——LSP indexing warm up、正常；每次 query 都慢——LSP 可能因記憶體不足重啟、需排查。</p>
<p>下面的實測是這張表在 Dart 上的數字驗證。</p>
<h2 id="dart-實測對照同題不同工具">Dart 實測對照：同題不同工具</h2>
<p>實測環境：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">專案類型：Dart 商業專案（POS / 零售領域）
</span></span><span class="line"><span class="ln">2</span><span class="cl">Branch：refactor/money-value-object
</span></span><span class="line"><span class="ln">3</span><span class="cl">索引規模：
</span></span><span class="line"><span class="ln">4</span><span class="cl">  cbm:        3,038 nodes,  6,355 edges（Dart 沒 CALLS edge）
</span></span><span class="line"><span class="ln">5</span><span class="cl">  codegraph:  6,244 nodes, 12,223 edges（含 CALLS edge）
</span></span><span class="line"><span class="ln">6</span><span class="cl">  serena:     per-session、無索引統計</span></span></code></pre></div><p>cbm 跟 codegraph 的 nodes 約 2x、edges 約 2x，差異關鍵不在 nodes（cbm 缺 import / enum_member 等次要 node）、而在「<strong>有沒有 CALLS edge</strong>」——這直接決定 caller / impact 類查詢能不能用。</p>
<blockquote>
<p><strong>實測數字的適用範圍</strong>：本節的所有 callsite / caller / impact 數字（含查詢 1-5）都是<strong>單一 Dart 商業專案的內部 baseline</strong>、不保證跨專案重現。Dart 上 type-inferred receiver 比例高的專案會放大三個工具的差距、比例低的專案會縮小差距。換到 Swift / Kotlin / Rust 等語言上、絕對數字會不同但「tree-sitter syntactic vs LSP type-aware」的差距方向通常一致。讀者要套用結論時、先在自家 repo 跑一遍同題對照、看自己的數字落差。</p></blockquote>
<h3 id="查詢-1誰呼叫了-moneymultiplybyrate">查詢 1：誰呼叫了 <code>Money.multiplyByRate</code></h3>
<table>
  <thead>
      <tr>
          <th>工具</th>
          <th>結果</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>cbm</td>
          <td>0（hybrid resolution 不含 Dart）</td>
      </tr>
      <tr>
          <td>codegraph</td>
          <td>3 caller symbols（4 個檔案中漏 product.dart 的 3 個 callsite）</td>
      </tr>
      <tr>
          <td>serena</td>
          <td>4 個檔案、9 個 callsite</td>
      </tr>
  </tbody>
</table>
<p>codegraph 漏掉的 3 個 callsite 共同特徵：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-dart" data-lang="dart"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">// lib/data/models/product/product.dart
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="kd">final</span> <span class="n">Money</span> <span class="n">samplePrice</span> <span class="o">=</span> <span class="p">...;</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="n">samplePrice</span><span class="p">.</span><span class="n">multiplyByRate</span><span class="p">(</span><span class="n">Decimal</span><span class="p">.</span><span class="n">parse</span><span class="p">(</span><span class="s1">&#39;0.9&#39;</span><span class="p">));</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="n">samplePrice</span><span class="p">.</span><span class="n">multiplyByRate</span><span class="p">(</span><span class="n">Decimal</span><span class="p">.</span><span class="n">parse</span><span class="p">(</span><span class="s1">&#39;0.6&#39;</span><span class="p">));</span></span></span></code></pre></div><p><code>samplePrice</code> 是 local variable、要型別推斷才知道是 <code>Money</code>。tree-sitter 看到的只是 <code>&lt;identifier&gt;.multiplyByRate(...)</code>、解不出 dispatch target。</p>
<p>serena 透過 <code>dart analysis_server</code> 拿到完整型別資訊、知道 <code>samplePrice</code> 宣告是 <code>Money</code>、能精確 dispatch。</p>
<h3 id="查詢-2誰呼叫了-localesymbolconfigformatamount">查詢 2：誰呼叫了 <code>LocaleSymbolConfig.formatAmount</code></h3>
<table>
  <thead>
      <tr>
          <th>工具</th>
          <th>結果</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>cbm</td>
          <td>0</td>
      </tr>
      <tr>
          <td>codegraph</td>
          <td>30（<code>--limit 30</code>，預設 20 截斷）</td>
      </tr>
      <tr>
          <td>serena</td>
          <td>5 個檔案、21 個 callsite</td>
      </tr>
  </tbody>
</table>
<p>這題 codegraph 跟 serena 的差距比較小——<code>formatAmount</code> 在很多地方是用顯式 receiver 呼叫（如 <code>LocaleSymbolConfig.cny.formatAmount(...)</code>），tree-sitter 對顯式 receiver 解得到。</p>
<p>兩邊數字的差異主因是 <strong>caller symbol 數 vs callsite 數</strong>的計數單位差：</p>
<ul>
<li>codegraph 算 caller symbol（一個 method 內呼叫幾次都算 1）</li>
<li>serena 算 callsite</li>
</ul>
<p>寫實測 baseline 時這個單位要寫死、否則 3 vs 9 看起來像精度差距、實際上一部分只是計數規則不同。</p>
<h3 id="查詢-3money-符號的內部結構">查詢 3：<code>Money</code> 符號的內部結構</h3>
<table>
  <thead>
      <tr>
          <th>工具</th>
          <th>結果</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>cbm</td>
          <td>只認得 File / Module、extension type 子結構抽不到</td>
      </tr>
      <tr>
          <td>codegraph</td>
          <td>認得 class 但 extension type 支援度未驗證</td>
      </tr>
      <tr>
          <td>serena</td>
          <td>Namespace kind、3 個 Field、16 個 Method、3 個 Property 都附行號</td>
      </tr>
  </tbody>
</table>
<p>Dart <code>extension type</code> 是相對新的特性、tree-sitter grammar 對它的支援深度不一。serena 走 LSP 直接拿到 <code>dart analysis_server</code> 對 extension type 的完整解析。</p>
<p>對需要「列出某 class / extension 所有 member」的場景、serena 是 Dart 上 LSP 級精度最可信的選項（其他 MCP 在 Dart extension type 上做不到完整 member 列舉）。</p>
<h3 id="查詢-4概念性搜尋金額顯示相關函式">查詢 4：概念性搜尋「金額顯示」相關函式</h3>
<p>對「我不知道精確名稱、只記得功能類別」這種 query：</p>
<table>
  <thead>
      <tr>
          <th>名次</th>
          <th>cbm（11-signal scoring）</th>
          <th>codegraph_search</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>1-4</td>
          <td>4 個 <code>formatAmount</code> 實作（兩邊一致）</td>
          <td>4 個 <code>formatAmount</code> 實作（兩邊一致）</td>
      </tr>
      <tr>
          <td>5</td>
          <td><code>externalDisplayMain</code></td>
          <td><code>displayCategories</code></td>
      </tr>
      <tr>
          <td>6</td>
          <td><code>connectExternalDisplay</code></td>
          <td><code>displayTags</code></td>
      </tr>
      <tr>
          <td>7</td>
          <td><code>_buildQuantityDisplay</code></td>
          <td><code>displayName</code></td>
      </tr>
      <tr>
          <td>8</td>
          <td><code>connectExternalDisplay</code>（另一個）</td>
          <td><code>displayCover</code></td>
      </tr>
      <tr>
          <td>9</td>
          <td><code>getBalanceDisplay</code></td>
          <td><code>displayName</code>（另一個）</td>
      </tr>
      <tr>
          <td>10</td>
          <td><code>_buildPriceDisplay</code></td>
          <td><code>displayName</code>（另一個）</td>
      </tr>
  </tbody>
</table>
<p>前 4 名兩邊都抓到核心 <code>formatAmount</code> 實作，第 5 名後分歧明顯：</p>
<ul>
<li>cbm 補進的 <code>getBalanceDisplay</code> / <code>_buildPriceDisplay</code> / <code>connectExternalDisplay</code> 都跟「金額顯示」概念相關（顯示金額 / 顯示餘額 / 外接顯示器）</li>
<li>codegraph 補進的 <code>displayName</code> / <code>displayTags</code> 只是符號名含 &ldquo;display&rdquo; 子字串、跟金額無關</li>
</ul>
<p>差異來源是 cbm 的 11-signal scoring + <code>cbm_camel_split</code> 對 camelCase 切詞做語意切分（<code>getMoneyField</code> → <code>get</code> + <code>money</code> + <code>field</code>）。codegraph 的 search 是 symbol pattern match、沒對自然語言 query 做語意處理。</p>
<p>這題的判讀很關鍵——<strong>cbm 在「找東西」的角色不能被 codegraph 取代</strong>。即使 codegraph 在 Dart 上有可用的 call graph、它的 search 仍然贏不了 cbm 的概念性 query。</p>
<h3 id="查詢-5money-的-impact-範圍--cross-symbol-trace">查詢 5：<code>Money</code> 的 impact 範圍 / cross-symbol trace</h3>
<table>
  <thead>
      <tr>
          <th>工具</th>
          <th>結果</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>cbm</td>
          <td>無 impact 概念、回不出</td>
      </tr>
      <tr>
          <td>codegraph</td>
          <td>5 個 affected symbol、全在 MoneyFieldRenderer 一檔</td>
      </tr>
      <tr>
          <td>serena</td>
          <td>走 <code>find_referencing_symbols</code> 跨 4 個檔案找完整 reference</td>
      </tr>
  </tbody>
</table>
<p>Money 是該專案大量使用的 value object、實際被使用的檔案橫跨 receipt_data 實作、settlement、cart_item、order_dto 等業務模組。codegraph 只回 1 個檔案 5 個 symbol、嚴重低估 blast radius。</p>
<p>漏掉的原因跟查詢 1 同源——<code>something.multiplyByRate(...)</code>、<code>Money</code> 在 factory 內被隱式構造這些都不在 tree-sitter 能解的範圍。MoneyFieldRenderer 之所以被抓到、是因為它的 field 顯式宣告為 <code>Money</code>，這是少數 tree-sitter syntactic 能抓的場合。</p>
<p>對 cross-symbol trace：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">codegraph_trace(from: &#34;Money/multiplyByRate&#34;, to: &#34;ProductSpecification&#34;)
</span></span><span class="line"><span class="ln">2</span><span class="cl">→ &#34;No direct path&#34;、建議跳到 dynamic dispatch</span></span></code></pre></div><p>graph 上根本沒這條 edge（漏掉的 product.dart 那 3 個 callsite 正是這條 trace 的關鍵跳）、所以 trace 直接失敗。</p>
<p>判讀訊號：<strong>重要 refactor 不能單看 codegraph 的 impact 數字</strong>。要走 serena <code>find_referencing_symbols</code> 二次確認；對 cbm 不在 hybrid resolution 名單的語言、blast radius 必須用 LSP 工具驗證。</p>
<h2 id="三刀流工作流">三刀流工作流</h2>
<p>實測結論：cbm / codegraph / serena 各有不可替代的角色，組合使用才是 Dart 主力專案的合理 stack。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln"> 1</span><span class="cl">找東西（不知道精確名稱、概念性 query）
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">  → cbm search_graph(query=&#34;...&#34;)           ← 11-signal scoring 對概念性 query 最強
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">知道精確名稱、找 caller / callee
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">  → codegraph_callers / codegraph_callees   ← auto-sync 2s 反應最快
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">  ↓
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">  發現結果可能不完整（type-inferred dispatch 多的場合）
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">  → serena find_referencing_symbols         ← LSP 完整精度補位
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">
</span></span><span class="line"><span class="ln">10</span><span class="cl">重要 refactor 確認 blast radius
</span></span><span class="line"><span class="ln">11</span><span class="cl">  → serena find_referencing_symbols         ← 不能單靠 codegraph_impact
</span></span><span class="line"><span class="ln">12</span><span class="cl">
</span></span><span class="line"><span class="ln">13</span><span class="cl">符號層級的編輯
</span></span><span class="line"><span class="ln">14</span><span class="cl">  → serena replace_symbol_body / rename     ← symbol-level atomic edit
</span></span><span class="line"><span class="ln">15</span><span class="cl">
</span></span><span class="line"><span class="ln">16</span><span class="cl">跨 service HTTP/RPC 鏈接（若 monorepo 含 client + server）
</span></span><span class="line"><span class="ln">17</span><span class="cl">  → cbm HTTP_CALLS edge                     ← 三個工具中只有 cbm 有這層</span></span></code></pre></div><p>幾個關鍵的判讀原則：</p>
<p><strong>入口跟出口要分清楚</strong>：cbm 是「廣度索引 + 模糊搜尋」的入口、拿到 qualified name 後轉給 serena 做精確查詢與編輯。codegraph 補在中間、做日常結構查詢。</p>
<p><strong>重要 refactor 必走 serena 補位</strong>：codegraph 的 caller / impact 在 Dart 上系統性偏低、不能單看數字判斷影響範圍。決定 rename 或大幅修改 method 之前、用 serena 跑一次 <code>find_referencing_symbols</code> 對齊。</p>
<p><strong>Hook 不要打架</strong>：cbm 會寫 PreToolUse hook 攔截 Grep / Glob / Read / Search（README 描述只擋前兩者、實裝版本含 Read / Search）、codegraph / serena 都不寫 hook。同時用三個工具時、注意 cbm hook 是否誤判把正常的 markdown grep 也擋掉（實測有 false positive）。</p>
<h2 id="對其他語言-stack-怎麼變化">對其他語言 stack 怎麼變化</h2>
<p>這個三刀流結論限定 Dart。不同語言 stack 的真實壓力不一樣、推薦組合也跟著變——把幾個常見 stack 各自展開。</p>
<h3 id="go--typescript--c--c-主力">Go / TypeScript / C / C++ 主力</h3>
<p>這層是 cbm 的甜蜜點：hybrid type resolution 涵蓋這四個語族、CALLS edge 抽得到、cbm 的 caller / blast radius 精度接近 LSP。實務影響是「cbm 在 Dart 上需要 codegraph + serena 補位」的場景大幅縮小——cbm 自己就能處理 caller / impact、加上它原本就強的 11-signal 概念搜尋跟跨 service HTTP_CALLS，等於一個工具撐住「找東西」「caller / impact」「cross-service」三層。</p>
<p>serena 在這個 stack 仍是 symbol-level edit 跟 compile diagnostic 的關鍵來源——cbm 純讀、沒 rename / replace_symbol_body、沒 LSP 診斷整合。所以合理組合是「cbm + serena 雙刀流」、codegraph 的角色被 cbm 取代掉。判讀訊號：在自家 repo 跑 cbm <code>trace_call_path</code> 對 5 個熱門 class、若 caller 數跟 serena 的 <code>find_referencing_symbols</code> 對得上、codegraph 確實可以省下。</p>
<h3 id="swift--kotlin--rust-主力">Swift / Kotlin / Rust 主力</h3>
<p>這層跟 Dart 場景結構接近：serena 透過 sourcekit-lsp / kotlin-language-server / rust-analyzer 能拿到完整型別解析、cbm 不在 hybrid resolution 名單只剩純 syntactic。所以「三刀流」的論證仍適用。</p>
<p>但 codegraph 在這三個語言的 query 品質要實測——19+ 列表內這幾個都列為 supported、實際解析深度因語言成熟度而異。Swift 特別容易踩坑的點是 Objective-C interop（dispatch table 跨語言）跟 protocol extension 的型別推斷、Kotlin 則是 reified generics 跟 inline function、Rust 是 trait method 跟 macro 展開後的 callsite。判讀訊號：對自家專案最常用的 dispatch pattern 寫一個 minimal example、跑 codegraph callers、看抓不抓得到。</p>
<h3 id="python-主力">Python 主力</h3>
<p>三個工具的 Python 支援都成熟、但著力點不同：cbm 對 Python 有完整 hybrid resolution、codegraph 對 Python 是核心支援語言之一（VS Code benchmark 在它的 7 codebase 列表內）、serena 透過 pyright / pylsp 拿型別資訊。</p>
<p>Python 的特殊壓力是 dynamic dispatch（duck typing / monkey patching / metaclass / <strong>getattr</strong>）——這層任何 static 工具都會漏。判讀訊號：對自家 codebase 跑「找 X class 的所有 method 呼叫」、若大量真實 callsite 在 type annotation 缺失的位置、所有工具都只能給 lower bound。實務組合多半雙刀（codegraph + serena）夠用、cbm 對 Python 的不可替代價值在 cross-service HTTP_CALLS（Django / FastAPI 跨 service 場景）。</p>
<h3 id="冷門語言--dslliquid--pascal--svelte-template-等">冷門語言 / DSL（Liquid / Pascal / Svelte template 等）</h3>
<p>這層 serena 多半沒 LSP 可借（除非自備 server）、cbm 純 syntactic（hybrid 名單外）、codegraph 是少數仍有 query 的工具——但 query 品質要看 codegraph 對該語言投入多深、Pascal / Delphi / Liquid 這類列表末段的支援度可能只到 symbol 抽取、callsite 不一定有。</p>
<p>實務上對這層語言、退回 <code>grep + codegraph</code> 比強推三刀流合理——caller / impact 用 codegraph 試、不夠就 grep 補、別期待 LSP 級精度。判讀訊號：若 codegraph status 顯示 indexed file 多但 edges 數明顯偏低（&lt; 1 條 edge per file）、call graph 多半沒抽起來、視同純 syntactic 工具用。</p>
<h3 id="共通的評估方法">共通的評估方法</h3>
<p>無論哪個 stack、第一次裝 MCP 前在自家 repo 跑「找重要 class / function 的所有 caller」這個基準題、把不同工具的數字並列比較、再決定組合。README benchmark 是行銷數字、自家 stack 跑出的數字才是真實 baseline。</p>
<h2 id="評估新-mcp-工具的-checklist">評估新 MCP 工具的 checklist</h2>
<p>從這次踩三個（含一個跳過實裝的 GitNexus）的經驗回推、未來評估新 code intelligence MCP 要先確認：</p>
<p><strong>License</strong>：商業專案要 MIT / Apache 2.0 / BSD。PolyForm Noncommercial 之類限制商業使用的 license 直接刷掉。這條最便宜、最早做、最少人記得做。</p>
<p><strong>目標語言的 call graph 支援</strong>：README 寫「full support」要實測。tree-sitter wrapper 通常只到「結構抽得到」、沒到「call edge 抽得到」。同樣是「有 CALLS edge」、有 type-inferred dispatch 的 syntactic 工具跟有完整 LSP 的差距可能 2-3x callsite 數。</p>
<p><strong>MCP tool 數量不等於能力</strong>：14 個 tool 不一定贏過 10 個。看 caller / impact / find_referencing_symbols 這類核心功能有沒有、品質好不好、勝過 tool 多寡。</p>
<p><strong>是否會自動改 <code>~/.claude/</code> 設定</strong>：大多會。先看 install script 動了哪些檔案、能不能還原、uninstall 是否徹底（cbm uninstall 不清 hook 是踩過的坑）。</p>
<p><strong>是否有 CLI 模式</strong>：有的話本 session 就能實測、不必等 Claude Code 重啟載入 MCP。CLI mode 對「驗證 baseline」特別重要——拿 CLI 結果當 ground truth、再對 MCP 結果做差異比對。</p>
<p><strong>Auto-sync 機制</strong>：file watcher / git polling / 純手動 reindex 差異很大。「邊改邊問」工作流對 sync 延遲很敏感、選錯會踩到 stale graph 的事故。</p>
<h2 id="結論">結論</h2>
<p>對 Dart 主力專案：<strong>三刀流（cbm + codegraph + serena）是合理 stack</strong>。三者擅長的事不重疊、互相補位有明確角色：</p>
<ul>
<li><a href="/blog/record/codebase-memory-mcp155-%E8%AA%9E%E8%A8%80-tree-sitter-%E7%9F%A5%E8%AD%98%E5%9C%96%E8%AD%9C-mcp-%E7%9A%84%E8%83%BD%E5%8A%9B%E8%88%87%E9%82%8A%E7%95%8C/">cbm</a>：概念性搜尋入口、跨 service HTTP/RPC 鏈接</li>
<li><a href="/blog/record/codegraph%E7%94%A8-tree-sitter-per-language-query-%E6%92%90%E8%B5%B7-19-%E8%AA%9E%E8%A8%80-call-graph-%E7%9A%84-mcp/">codegraph</a>：日常 80% 的結構查詢、auto-sync 反應最快</li>
<li><a href="/blog/record/serena%E6%8A%8A-lsp-%E5%8C%85%E6%88%90-agent-first-mcp-%E7%9A%84-symbol-level-%E7%B7%A8%E8%BC%AF%E6%96%B9%E6%A1%88/">serena</a>：型別精確 reference、symbol-level atomic edit、編譯 diagnostic</li>
</ul>
<p>對其他語言 stack、cbm 進入 hybrid resolution 名單後組合會收斂、但 serena 的 symbol edit 跟 diagnostic 角色仍不可取代。</p>
<p>評估方法的更普遍結論：<strong>README benchmark 只是起點、要在自己的 stack 上跑同樣的基準題才算數</strong>。每個工具的 benchmark 都選自己擅長的語言跟 codebase、跨語言遷移結論需要重新驗證。用 5 個查詢做 baseline、把 CLI 數字當 ground truth、再對 MCP 結果做差異對比、是現階段最低成本的工具評估法。</p>
]]></content:encoded></item></channel></rss>