第 6 部

連接性

代理的觸及範圍超越了 localhost。

第十五章:MCP —— 通用工具協定

為何 MCP 的意義超越 Claude Code

本書其他每一章都在討論 Claude Code 的內部機制。這一章不同。模型上下文協定(Model Context Protocol)是一個開放規格,任何代理都能實作,而 Claude Code 的 MCP 子系統是現存最完整的生產環境客戶端之一。如果你正在建構一個需要呼叫外部工具的代理——任何代理、任何語言、任何模型——本章的模式可以直接套用。

核心命題很直接:MCP 定義了一個 JSON-RPC 2.0 協定,用於客戶端(代理)與伺服器(工具提供者)之間的工具發現與調用。客戶端發送 tools/list 來發現伺服器提供了什麼,然後用 tools/call 來執行。伺服器以名稱、描述和 JSON Schema 輸入參數來描述每個工具。這就是全部的契約。其他一切——傳輸層選擇、認證、設定載入、工具名稱正規化——都是將乾淨的規格變成能在現實世界中存活之物的實作工程。

Claude Code 的 MCP 實作橫跨四個核心檔案:types.tsclient.tsauth.tsInProcessTransport.ts。它們共同支援八種傳輸類型、七個設定範圍、跨兩個 RFC 的 OAuth 發現機制,以及一個讓 MCP 工具與內建工具無法區分的工具封裝層——與第六章介紹的 Tool 介面完全相同。本章將逐層講解。


八種傳輸類型

任何 MCP 整合的第一個設計決策是客戶端如何與伺服器通訊。Claude Code 支援八種傳輸層配置:

有三個設計選擇值得關注。第一,stdio 是預設值——當 type 被省略時,系統假設是本地子行程。這向下相容最早期的 MCP 設定。第二,fetch 包裝器是堆疊式的:逾時包裝在最外層,步進偵測在中間,基礎 fetch 在最內層。每個包裝器只處理一個關注點。第三,ws-ide 分支有 Bun/Node 執行時期的分歧——Bun 的 WebSocket 原生支援 proxy 和 TLS 選項,而 Node 需要 ws 套件。

何時使用哪種。 對於本地工具(檔案系統、資料庫、自訂腳本),用 stdio——沒有網路、無需認證,只有管道。對於遠端服務,http(串流式 HTTP)是現行規格的建議。sse 是舊版但部署廣泛。sdk、IDE 和 claudeai-proxy 類型是各自生態系統的內部實作。


設定載入與範圍劃定

MCP 伺服器設定從七個範圍載入,合併後去重:

範圍來源信任等級
local工作目錄中的 .mcp.json需要使用者核准
user~/.claude.json 的 mcpServers 欄位使用者自行管理
project專案層級設定共享的專案設定
enterprise受管企業設定由組織預先核准
managed外掛提供的伺服器自動發現
claudeaiClaude.ai 網頁介面透過網頁預先授權
dynamic執行時期注入(SDK)以程式方式加入

去重是基於內容的,而非基於名稱。 兩個名稱不同但命令或 URL 相同的伺服器會被識別為同一個伺服器。getMcpServerSignature() 函式計算出一個正規鍵值:本地伺服器為 stdio:["command","arg1"],遠端伺服器為 url:https://example.com/mcp。外掛提供的伺服器若其簽名與手動設定匹配,則會被抑制。


工具封裝:從 MCP 到 Claude Code

連線成功後,客戶端呼叫 tools/list。每個工具定義被轉換為 Claude Code 的內部 Tool 介面——與內建工具使用的介面完全相同。封裝完成後,模型無法區分內建工具和 MCP 工具。

封裝過程有四個階段:

1. 名稱正規化。 normalizeNameForMCP() 將無效字元替換為底線。完整限定名稱遵循 mcp__{serverName}__{toolName} 格式。

2. 描述截斷。 上限為 2,048 個字元。OpenAPI 產生的伺服器曾被觀察到將 15-60KB 傾倒進 tool.description——單一工具每回合大約 15,000 個 token。

3. Schema 直通。 工具的 inputSchema 直接傳遞給 API。封裝時不做轉換、不做驗證。Schema 錯誤在呼叫時才會浮現,而非註冊時。

4. 註解映射。 MCP 註解映射到行為旗標:readOnlyHint 將工具標記為可安全並行執行(如第七章串流執行器中所討論的),destructiveHint 觸發額外的權限審查。這些註解來自 MCP 伺服器——惡意伺服器可能將破壞性工具標記為唯讀。這是一個被接受的信任邊界,但值得理解:使用者選擇加入了該伺服器,而惡意伺服器將破壞性工具標記為唯讀確實是一個真實的攻擊向量。系統接受這個取捨,因為替代方案——完全忽略註解——將阻止合法伺服器改善使用者體驗。


MCP 伺服器的 OAuth

遠端 MCP 伺服器通常需要認證。Claude Code 實作了完整的 OAuth 2.0 + PKCE 流程,包含基於 RFC 的發現機制、跨應用程式存取(Cross-App Access)和錯誤回應正規化。

發現鏈

authServerMetadataUrl 這個逃生口的存在是因為某些 OAuth 伺服器兩個 RFC 都沒有實作。

跨應用程式存取(XAA)

當 MCP 伺服器設定中有 oauth.xaa: true 時,系統透過身分提供者(Identity Provider)執行聯合 token 交換——一次 IdP 登入即可解鎖多個 MCP 伺服器。

錯誤回應正規化

normalizeOAuthErrorBody() 函式處理違反規格的 OAuth 伺服器。Slack 對錯誤回應返回 HTTP 200,錯誤訊息埋在 JSON 本體中。該函式會窺探 2xx POST 回應的本體,當本體匹配 OAuthErrorResponseSchema 但不匹配 OAuthTokensSchema 時,將回應重寫為 HTTP 400。它還會將 Slack 特有的錯誤碼(invalid_refresh_tokenexpired_refresh_tokentoken_expired)正規化為標準的 invalid_grant


行程內傳輸層

不是每個 MCP 伺服器都需要是獨立行程。InProcessTransport 類別使 MCP 伺服器和客戶端可以在同一行程中執行:

class InProcessTransport implements Transport {
  async send(message: JSONRPCMessage): Promise<void> {
    if (this.closed) throw new Error('Transport is closed')
    queueMicrotask(() => { this.peer?.onmessage?.(message) })
  }
  async close(): Promise<void> {
    if (this.closed) return
    this.closed = true
    this.onclose?.()
    if (this.peer && !this.peer.closed) {
      this.peer.closed = true
      this.peer.onclose?.()
    }
  }
}

整個檔案只有 63 行。兩個設計決策值得關注。第一,send() 透過 queueMicrotask() 傳遞,以防止同步請求/回應循環中的堆疊深度問題。第二,close() 會級聯到對等端,防止半開啟狀態。Chrome MCP 伺服器和 Computer Use MCP 伺服器都使用這個模式。


連線管理

連線狀態

每個 MCP 伺服器連線存在於五種狀態之一:connectedfailedneeds-auth(帶有 15 分鐘的 TTL 快取,防止 30 個伺服器各自獨立發現同一個過期 token)、pendingdisabled

工作階段過期偵測

MCP 的串流式 HTTP 傳輸層使用工作階段 ID。當伺服器重新啟動時,請求會返回 HTTP 404 並帶有 JSON-RPC 錯誤碼 -32001。isMcpSessionExpiredError() 函式檢查這兩個訊號——注意它使用字串包含來偵測錯誤碼,這務實但脆弱:

export function isMcpSessionExpiredError(error: Error): boolean {
  const httpStatus = 'code' in error ? (error as any).code : undefined
  if (httpStatus !== 404) return false
  return error.message.includes('"code":-32001') ||
    error.message.includes('"code": -32001')
}

偵測到後,連線快取清除並重試一次呼叫。

批次連線

本地伺服器以每批 3 個連線(產生行程可能耗盡檔案描述符),遠端伺服器以每批 20 個連線。React 上下文提供者 MCPConnectionManager.tsx 管理生命週期,將當前連線與新設定進行差異比對。


Claude.ai 代理傳輸層

claudeai-proxy 傳輸層展示了一種常見的代理整合模式:透過中介連線。Claude.ai 的訂閱者透過網頁介面設定 MCP「連接器」,而 CLI 透過 Claude.ai 的基礎設施路由,由其處理供應商端的 OAuth。

createClaudeAiProxyFetch() 函式在請求時捕獲 sentToken,而非在 401 後重新讀取。在多個連接器併發 401 的情況下,另一個連接器的重試可能已經刷新了 token。該函式還會在重新整理處理器返回 false 時檢查併發刷新——即「ELOCKED 競爭」的場景,另一個連接器贏得了鎖定檔案的競爭。


逾時架構

MCP 的逾時是分層的,每一層防護不同的失敗模式:

層級持續時間防護目標
連線30 秒無法到達或啟動緩慢的伺服器
每次請求60 秒(每次請求重新計時)過期逾時訊號的程式缺陷
工具呼叫約 27.8 小時合法的長時間操作
認證每次 OAuth 請求 30 秒無法到達的 OAuth 伺服器

每次請求的逾時值得強調。早期的實作在連線時建立單一的 AbortSignal.timeout(60000)。閒置 60 秒後,下一次請求會立即中止——因為訊號已經過期了。修正方式:wrapFetchWithTimeout() 為每次請求建立新的逾時訊號。它還會正規化 Accept 標頭,作為防止執行時期和代理伺服器丟棄它的最後防線。


實踐應用:將 MCP 整合到你自己的代理中

從 stdio 開始,之後再增加複雜度。 StdioClientTransport 處理一切:產生行程、管道、終止。一行設定、一個傳輸類別,你就有了 MCP 工具。

正規化名稱並截斷描述。 名稱必須匹配 ^[a-zA-Z0-9_-]{1,64}$。加上 mcp__{serverName}__ 前綴以避免衝突。描述上限為 2,048 個字元——否則 OpenAPI 產生的伺服器會浪費上下文 token。

延遲處理認證。 在伺服器返回 401 之前不要嘗試 OAuth。大多數 stdio 伺服器不需要認證。

對內建伺服器使用行程內傳輸層。 createLinkedTransportPair() 消除了你所控制之伺服器的子行程開銷。

尊重工具註解並清理輸出。 readOnlyHint 啟用並行執行。對回應進行清理以防禦惡意 Unicode(雙向覆寫字元、零寬度連接符),這些可能誤導模型。

MCP 協定刻意保持極簡——兩個 JSON-RPC 方法。在這些方法與生產環境部署之間的一切都是工程:八種傳輸層、七個設定範圍、兩個 OAuth RFC,以及逾時分層。Claude Code 的實作展示了這種工程在規模化時的樣貌。

下一章將探討當代理超越 localhost 時會發生什麼:遠端執行協定讓 Claude Code 在雲端容器中運行、接受來自網頁瀏覽器的指令,並透過注入憑證的代理伺服器建立 API 流量隧道。