McpHub:MCP 伺服器生命週期管理
職責
McpHub 是 Cline 接入外部 MCP 伺服器的中央樞紐。它負責讀取 cline_mcp_settings.json 裡設定的所有伺服器,為每個伺服器建立一條獨立的 client + transport 連線,然後把暴露出來的工具 (tool)、資源 (resource)、提示 (prompt) 快取到記憶體裡供主 agent 呼叫。整條 MCP 工具鏈——從設定變更、檔案監聽、連線/重連、能力發現、到 tools/call 呼叫——都收斂到這一個類,主 Task 不需要直接碰 MCP SDK。
它的位置在「主 agent loop」和「具體 MCP 協議」之間。想呼叫一個 MCP 工具時,Task 透過 McpHub.callTool (callTool:1233) 走出去;UI 想看當前伺服器列表,透過 getServers() 拿到一個排序後的快照 (getServers:112)。一個 Extension 實例裡只有一個 McpHub,但 connections: McpConnection[] 陣列裡每條都是獨立 transport,掛了不影響別的。
設計動機
- 每伺服器一個 client:不同 MCP 伺服器有不同 capabilities、不同設定、不同錯誤處理,獨立 client 讓重連和作用域隔離都簡單 (
per-server client:363)。 - 統一三種 transport:
stdio跑本地行程、sse走 Server-Sent Events、streamableHttp用新版 HTTP 流,在connectToServer的switch裡分發 (transport switch:382)。 - **檔案監聽 + 內部更新雙通道
watchMcpSettingsFile監聽外部編輯器改設定,isUpdatingClineSettings標記內部自己寫時跳過觸發 (isUpdatingClineSettings:76),避免回環。 - OAuth 按需懶載入:只有
sse/streamableHttp才找McpOAuthManager要 authProvider,stdio不走 (authProvider setup:377)。 - 企業策略前置於連線:企業部署可以禁 marketplace、設白名單,在 stdio 連線前就攔截掉 (
enterprise allowlist:310)。 - UID 縮短工具名:每個伺服器分配一個
c+ nanoid(5) 的短 uid,避免 MCP 工具名拼成長串 (getMcpServerKey:130)。
關鍵檔案
McpHub class:51— 單例類,持有 connections、檔案監聽器、OAuth manager 和通知回呼。constructor:108— 啟動時立刻watchMcpSettingsFile+initializeMcpServers。watchMcpSettingsFile:213— 用 chokidar 監聽設定檔改動,diff 出增/刪/改觸發重連。connectToServer:286— 核心連線方法,校驗、選 transport、建 client、發現能力。transport switch:382— stdio / sse / streamableHttp 三分支。stdio stderr pipe:415— stdio 伺服器把 stderr 拉出來當 info/error 日誌流。fetchToolsList:677— 發tools/list,並給每個 tool 標autoApprove標記。fetchResourcesList:713— 發resources/list,disabled 伺服器直接回空。notification handler:611— 註冊notifications/message回呼,把伺服器日誌塞給當前 Task 或暫存。deleteConnection:784— 關 transport + client,從 connections 裡過濾掉。restartConnection:1047— 重新連一個伺服器,帶 500ms 人為延遲讓 UI 看到「connecting」狀態。callTool:1233— 發tools/call,按伺服器設定取逾時,全程埋 telemetry。
資料流
connectToServer 進來先做三件事:校驗企業策略、構造 disabled 占位或真實 client、選 transport。transport 選完建 McpConnection 推進陣列,然後 client.connect(transport) 真正握手。握手成功後才註冊通知回呼並拉四份能力清單:
// apps/vscode/src/services/mcp/McpHub.ts
connection.server.status = "connected"
connection.server.error = ""
// Register notification handler for real-time messages
try {
// ...
connection.client.setNotificationHandler(NotificationMessageSchema as any, async (notification: any) => {
// ...
if (this.notificationCallback) {
this.notificationCallback(name, level, message)
} else {
this.pendingNotifications.push({ serverName: name, level, message, timestamp: Date.now() })
}
})
} catch (error) {
Logger.error(`[MCP Debug] Error setting notification handlers for ${name}:`, error)
}
// Initial fetch of tools, resources, and prompts
connection.server.tools = await this.fetchToolsList(name)
connection.server.resources = await this.fetchResourcesList(name)
connection.server.resourceTemplates = await this.fetchResourceTemplatesList(name)
connection.server.prompts = await this.fetchPromptsList(name)這段在 post-connect setup:584 附近。連線建好後,外部工具呼叫都走 callTool——查連線、按 config.timeout 算逾時、發 tools/call (tools/call request:1270)。資源讀類似,走 resources/read (resources/read:1183)。
邊界與失敗
- OAuth 缺失即降級:
client.connect拋UnauthorizedError時,不報錯而是把 connection 標成oauthRequired: true,等使用者在前端授權 (UnauthorizedError branch:556)。 - disabled 伺服器占位但不連線:disabled 設定仍進 connections 陣列以便 UI 顯示,但 client/transport 都是
null(disabled placeholder:339)。 - stdio stderr 分類:stderr 裡出現
error字樣才記 error,否則當 info log (stderr classification:420)。 - streamableHttp 404 當 405:很多伺服器把 SSE 不支援時該回 405 錯回 404,這裡在 fetch 層把 404 歸一成 405 讓 SDK 接受 (
404 to 405:499)。 - SSE token 動態刷新:sse 分支用自訂 fetch 每次現取
authProvider.tokens(),否則 token 過期後重連會一直 401 (dynamic token fetch:459)。 - streamableHttp 重連交給 handler:
StreamableHttpReconnectHandler拿到connectToServer閉包,自己決定重連節奏 (reconnect handler:519)。 - 通知暫存:沒有活動 Task 時,伺服器來的 notification 先存
pendingNotifications,等下一個 Task 起來再取 (pendingNotifications:631)。
小結
McpHub 把 MCP 協議細節全收進自己,對主 agent 暴露 callTool/readResource/getServers 三個口子。想看它如何被工具層包裝成 use_mcp_tool 可以讀 cap-tools/use-mcp-tool;想看 subagent 如何隔離自己的上下文視窗可以讀 subagent;想看主 Task 怎麼把 MCP 伺服器列表塞進系統提示可以讀 agent-loop/task-class。