主題: AI agents

MCP Tool Result:給決策,不要把 API 整包倒進 context

MCP tool 的價值不在把 REST JSON 原封不動搬給模型,而在交付可判斷的摘要、可驗證欄位與下一步查詢入口。

MCP server 最容易犯的錯,不是 transport 寫錯,也不是 schema 少一個 required,而是把既有 API 的 response 原封不動包進 tool result。

這看起來很忠實,實際上常常很懶。模型想判斷「哪三筆資料需要處理」,你卻回傳十萬字的 nested JSON;模型想確認一個狀態,卻得先猜哪個欄位才是最新版本。tool 能呼叫成功,不代表 agent 因此更可靠。

我會用一句話判斷:tool result 應該交付決策所需的 context,不是把後端資料庫的抽屜整個拉開。

動態迷因(展開/收合)
結果要補上判斷所需脈絡與可深入的線索;不代表把沒切邊的原始 response 一次倒進來。 · 來源:GIPHY

API response 不是 agent 的工作單位

傳統 API 常為人類前端或另一個程式服務而設計。它可能合理地回傳完整 object、內嵌關聯、presentation 欄位、所有 audit metadata,或給 UI 做下一頁用的資料。

但模型通常不是要「擁有整個 object」。它要做的是下一個判斷:

  • 找出需要人工確認的三筆異常;
  • 比較兩個設定的差異;
  • 判斷能不能重試;
  • 告訴使用者還缺什麼資訊。

所以別把 GET /orders/:id 直接改名成 get_order 就結案。先寫清楚 agent 的 job,再讓 tool 回傳剛好支持那個 job 的資料。最近的 Reddit 實作討論也點出同一件事:模型只要一個欄位時,整包 JSON 只是在污染後續 context。

好 result 先回答,再留下可追查的把手

我偏好把讀取型 tool 的 result 切成四層:

  1. 決策摘要:答案、計數、篩選條件與 observedAt
  2. 少量證據:可辨識的 id、狀態、時間與最小必要欄位;不是把私密或無關欄位一併送出。
  3. 明確邊界:有沒有截斷、下一頁是否存在、結果是否過期或權限不足。
  4. 下一步:可用的 cursor、精確的 follow-up tool,或需要使用者確認的動作。

例如,agent 的任務是找出要先處理的 incidents。它需要的是排好序的少量候選、各自的理由、查詢時間與 nextCursor,不是 500 筆完整事件紀錄:

const result = {
  summary: {
    message: "找到 3 筆需要人工確認的 incident",
    observedAt: "2026-08-17T11:00:00Z",
    totalMatches: 27,
  },
  incidents: [
    { id: "inc_102", severity: "high", state: "open", updatedAt: "2026-08-17T10:42:00Z" },
    { id: "inc_081", severity: "high", state: "open", updatedAt: "2026-08-17T10:19:00Z" },
    { id: "inc_077", severity: "medium", state: "triage", updatedAt: "2026-08-17T09:58:00Z" },
  ],
  nextCursor: "opaque-cursor",
};

return {
  content: [{ type: "text", text: JSON.stringify(result) }],
  structuredContent: result,
  isError: false,
};

MCP Tools specification定義了 structuredContent 與可選的 outputSchema:有 schema 時,server 必須回傳符合它的 structured result,client 應驗證它。這很適合把「summary 一定有時間」、「每筆一定有 id 與 state」變成可檢查的契約,而不是留給 prompt 猜。

cursor 不是裝飾;它是拒絕資訊洪水的方法

資料多時,別只做一個 limit: 1000 然後祈禱模型看得懂。要讓 result 明說「這是前幾筆,還有更多」,並把 opaque cursor 帶回下一次查詢的 arguments。

MCP 的 pagination 文件明確把 cursor 用在列舉型操作,像 tools/listresources/listprompts/list;cursor 不該被 client 解析或改寫。這表示你的業務查詢 tool 也不會自動得到「萬用分頁」:要在自己的 input schema 設計 cursorlimit,並把 result 的截斷語意說清楚。

動態迷因(展開/收合)
一次把所有資料送進模型,通常不是透明,而是讓真正要判斷的訊號被資訊過載淹沒。 · 來源:GIPHY

實務上,query tool 可以有三個很平凡但很有用的參數:

{
  query: "failed deploys since yesterday",
  limit: 10,
  cursor: "opaque-cursor-or-omitted"
}

limit 是上限,不是承諾;cursor 是 server 所發的能力票,不是 page number。若需要細節,讓 agent 用穩定 id 再呼叫 get_incident。這會多一次 tool call,但少了把不相干資料塞進每一輪 context 的成本。

error 也必須能讓 agent 下一步正確

讀取失敗時,最糟的 response 是 "Error"。模型無法知道該縮小查詢、等待、要求權限,還是停止。

MCP 區分 protocol error 與 tool execution error;後者應放在 result 裡並設 isError: true,讓模型看得到、能修正。官方規格也要求server 驗證輸入、實作 access control、rate limit 與 sanitize output,client 在交給 LLM 前驗證 result。

因此,error result 至少應保留:

  • 可公開的原因碼,例如 RATE_LIMITEDSCOPE_REQUIREDCURSOR_EXPIRED
  • 是否可安全重試,以及建議等待多久;
  • 可採取的安全下一步,而不是內部 stack trace;
  • 不能自動執行的敏感操作,仍要由使用者明確確認。

這不是多寫一層文案。它是讓 agent 從失敗中恢復,且不把內部資料誤丟進 context 的最短路徑。

什麼時候才該把中間資料留在模型外?

小型、單一步驟 lookup 不必為了「context engineering」多建一個 execution sandbox。直接回傳小而完整的 result,比再加一層 orchestration 更可靠。

但任務需要大量 filter、aggregate、sort,或三個以上相依 tool call 時,應該在受控程式環境處理中間結果,只把最終、可解釋的結論交回模型。Anthropic 的 advanced tool use 說明給了一個很好的界線:大資料集的中間結果、重複查詢與平行操作適合交給程式;簡單 lookup 或模型本來就必須審閱每筆中間資料時,則不值得增加這層。

這不等於每個 MCP server 都要模仿某個 vendor 的 feature。原則更簡單:資料處理交給可驗證的程式;模型只看它需要推理與說明的部分。

發布前,我會用這五題檢查 result 契約

  1. agent 需要做的下一個判斷是什麼?result 有沒有直接支持它?
  2. result 能否被 outputSchema 驗證,並在 TextContent 保留 serialized JSON 的 compatibility text?
  3. 大量結果是否有明確 limit、截斷訊息與 opaque cursor?
  4. 每筆 evidence 是否有穩定 id、時間與來源,而不是看起來很完整卻無法追查?
  5. 錯誤、權限與副作用是否讓 agent 知道該停止、重試或要求確認?

如果五題都答得出來,這個 tool 才是在替 agent 降低猜測。否則它只是把一個 API endpoint 搬進對話視窗,然後把資料量、權限和錯誤處理都交給模型賭運氣。


外部參考連結