MCP Tools 通知機制:與 Resources 的差異解析
MCP Tools 通知機制:與 Resources 的差異解析
深入理解 Tools 的
list_changed通知與 Resources 訂閱機制的本質差異,以及如何設計跨元件的更新流程。
核心差異總覽
Tools 只有 list_changed,沒有「某個 tool updated」這種細粒度事件;而且這些通知都是「server 發給 client」,不是 Tool 自己直接發。
| 特性 | Tools | Resources |
|---|---|---|
| 清單變更通知 | ✅ notifications/tools/list_changed |
✅ notifications/resources/list_changed |
| 單一項目更新通知 | ❌ 無 | ✅ notifications/resources/updated |
| 訂閱機制 | ❌ 無 | ✅ resources/subscribe |
| 通知發送者 | Server runtime | Server runtime |
Tools 可以發什麼通知?
notifications/tools/list_changed
這是 Tools 唯一支援的通知類型。
使用前提: Server 在 capabilities 宣告:
{
"capabilities": {
"tools": { "listChanged": true }
}
}
表示「這個 server 會在 tools 清單有變時,對 client 發 list_changed 通知」。
觸發時機: 當工具集合有變化時(新增 / 移除 / 變更定義)
通知格式:
{ "jsonrpc": "2.0", "method": "notifications/tools/list_changed" }
Client 反應: 收到通知後,通常會再呼叫一次 tools/list 拿最新清單。
[!IMPORTANT]
這個通知是 「工具清單有變化」,而不是「某個工具被執行了一次」或「工具內部 state 改變」。
為什麼 Tools 沒有 updated 事件?
設計哲學的差異
| 概念 | Resources | Tools |
|---|---|---|
| 本質 | 資料 / 上下文 | 動作 / 函式 |
| 狀態 | 有內容狀態,會變化 | 無狀態,只有定義 |
| 用途 | 被讀取、被消費 | 被執行、產生結果 |
Resources 代表的是「資料」,資料內容會隨時間變化,所以需要 updated 來通知「內容變了」。
Tools 代表的是「能力」,工具本身的定義(名稱、參數、描述)很少變動,變動時用 list_changed 足矣。
如果你想表達「Tool 執行後產生了新 data」
這是常見的需求,但 MCP 的設計是讓你透過 Resources 來處理:
Tool 執行完
↓
寫入/更新一個 Resource
↓
Server 發 notifications/resources/updated
↓
Client 在 notification handler 決定要不要再叫一次 model
這種分離設計讓職責更清晰:Tools 負責做事,Resources 負責儲存結果。
「Tool 自己發通知」這件事怎麼理解?
在協議層,所有訊息都是「MCP server ↔ MCP client」之間的 JSON-RPC:
┌─────────────────────────────────────────────────────────────┐
│ 訊息流向 │
├─────────────────────────────────────────────────────────────┤
│ │
│ Tool Handler (callback) │
│ │ │
│ │ 執行完畢,呼叫 sendToolListChanged() │
│ ↓ │
│ Server Runtime │
│ │ │
│ │ 送出 JSON-RPC notification │
│ ↓ │
│ MCP Client │
│ │
└─────────────────────────────────────────────────────────────┘
重點:
- Tool 實作只是一段 callback / handler
- 真正送
notifications/tools/list_changed的是 server runtime - 實務上你會在 server 程式碼裡呼叫類似
sendToolListChanged()的 helper
實務設計 Pattern
Pattern 1:Tool 執行後觸發 Resource 更新
這是最推薦的做法,符合 MCP 的設計哲學。
// Tool handler
async function executeAnalysisTool(params) {
const result = await runAnalysis(params);
// 把結果寫入 resource
await updateResource('analysis://latest-result', result);
// 通知 client resource 已更新
server.notifyResourceUpdated('analysis://latest-result');
return { success: true };
}
Pattern 2:動態工具(需要 list_changed)
某些場景下,工具清單會動態變化:
// 使用者連接新的 database 後,新增對應的 query tools
async function onDatabaseConnected(dbName) {
registerTool({
name: `query_${dbName}`,
description: `Query the ${dbName} database`,
// ...
});
// 通知 client 工具清單變了
server.notifyToolListChanged();
}
適用場景:
- 根據使用者權限動態啟用/停用工具
- 連接外部系統後新增相關工具
- 根據環境(dev/staging/prod)提供不同工具集
總結對照表
| 問題 | 答案 |
|---|---|
| Tools 可以發通知嗎? | ✅ 可以,但只有 list_changed |
Tools 有 per-tool 的 updated 嗎? |
❌ 沒有 |
| Tool 執行後想推送更新怎麼辦? | 把結果寫成 Resource,用 resources/updated |
| 通知是誰發的? | Server runtime,不是 Tool handler 自己 |
我的補充意見
為什麼這個設計是合理的
MCP 的設計遵循了一個清晰的關注點分離原則:
-
Tools = 動詞(Actions)
工具是「能做什麼」,本身是無狀態的。執行一次就結束,結果要嘛直接回傳,要嘛寫到某個地方。 -
Resources = 名詞(Data)
資源是「有什麼」,代表可被觀察的狀態。狀態會變,所以需要訂閱和更新通知。 -
Prompts = 模板(Templates)
給人類用的快捷方式,不參與運行時的事件流。
這種分離讓架構更容易理解:
想知道「發生了什麼變化」→ 訂閱 Resources
想「讓系統做點事」→ 呼叫 Tools
設計建議
如果你在設計 MCP Server,考慮以下原則:
| 原則 | 說明 |
|---|---|
| Tool 產生的持久結果應寫入 Resource | 讓 client 可以訂閱後續變化 |
只在真正需要時才用 list_changed |
頻繁變動的工具清單會增加 client 負擔 |
| 善用 Resource Templates | 搭配 Tools 使用,讓 LLM 可以先瀏覽再操作 |
這樣的設計不僅符合 MCP 規格,也讓你的 server 更容易被 AI 理解和使用。