CodeGym /課程 /ChatGPT Apps /Handshake 與 capabilities:用戶端如何得知伺服器能做什麼

Handshake 與 capabilities:用戶端如何得知伺服器能做什麼

ChatGPT Apps
等級 6 , 課堂 2
開放

1. 為什麼需要 handshake

如果把 REST 端點想成是很多扇可以用 URL 逐一叩門的獨立大門,那 MCP 更像是透過單一管道進行的長連線對話。用戶端不只是零散地發送請求,它會先建立一個工作階段。Handshake 就是在這個階段開始時的初次認親。

在 MCP 中,這個時刻由一個特殊請求 initialize 來實現:用戶端在建立傳輸層(STDIO、HTTP/stream、WebSocket —— 都可以)後立刻送出。在請求裡它會說:「我說的是某版本的 MCP,這是我會的能力,還有,我是誰。」伺服器則回覆:「我支援這個版本與這些功能,很高興認識你。」

成功交換後,用戶端會送出通知 notifications/initialized,然後才開始進入工作流程:tools/listresources/listtools/call 等等有用的操作。

打個比方,MCP 的 handshake 就像把伺服器搬進資料中心前先簽好租約。只要雙方還沒談定規則(協定格式、資料中心提供哪些服務、費用由誰負擔)—— 搬伺服器都是白做工。

從實務角度看,handshake 解決三件事:

  1. 檢查協定版本的相容性。
  2. 宣告 MCP 伺服器到底支援哪些「基本原語」:tools、resources、prompts、記錄、通知等。
  3. 提供用戶端與伺服器的中繼資訊 —— 名稱與實作版本。

2. MCP 連線生命週期:handshake 位於哪裡

為了不要太抽象,我們來看一個高度簡化的典型連線流程(flow):

sequenceDiagram
    participant C as 用戶端 (ChatGPT/Inspector)
    participant S as MCP 伺服器

    C->>S: (1) 建立傳輸 (STDIO/HTTP-stream)
    C->>S: (2) Request: "initialize"
    S-->>C: (3) Result: "initialize" (capabilities, serverInfo)
    C->>S: (4) Notification: "notifications/initialized"
    C->>S: (5) Request: "tools/list" / "resources/list"
    S-->>C: (6) Result: 工具/資源清單
    C->>S: (7) Request: "tools/call" 等

從技術面看,步驟大致如下:

  1. 傳輸層已建立:例如 ChatGPT 將你的伺服器作為子行程啟動並連到 STDIO,或 Inspector 對 /mcp 發出 HTTP/stream 請求。
  2. 用戶端送出 JSON-RPC 請求 initialize
  3. 伺服器以 JSON-RPC 結果回覆,內含欄位 protocolVersioncapabilitiesserverInfo
  4. 用戶端送出 notification notifications/initialized —— 訊號:「我已經讀完,可以開始工作」。
  5. 用戶端依伺服器 capabilities 的宣告,呼叫 discovery 方法(tools/listresources/listprompts/list)。
  6. 伺服器回傳工具/資源/提示詞(prompts)的中繼資料。
  7. 接著進入「正式」請求:tools/callresources/read 等。

重要的是,handshake 其實就是一般的 JSON-RPC 呼叫 initialize。沒有魔法。學完 MCP 訊息格式後,你已經會解析這種請求;唯一的差異是這裡的方法只有一個而且「特別」,並且一定要最先執行。

3. 用戶端在 initialize 裡送什麼

我們把 initialize 請求拆開來看。下面是一個最小化(為課程而簡化)的請求範例:

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "initialize",
  "params": {
    "protocolVersion": "2025-06-18",
    "capabilities": {
      "elicitation": {}
    },
    "clientInfo": {
      "name": "chatgpt-gift-client",
      "version": "2.3.0"
    }
  }
}

這個例子與 MCP 官方文件中的內容很接近。params 內的主要欄位:

protocolVersion

MCP 規格版本字串,通常是日期格式,例如 "2025-06-18"。這不是你的應用程式版本,而是協定本身的版本。用戶端表示:「我預期使用這個版本的 MCP。」伺服器在回覆中必須確認,或在不認得該版本時回傳錯誤。

這能避免「用戶端以為是 A、伺服器實作的是 B」的情境。如果無法達成共同版本,與其交換不相容的訊息,不如老實中斷連線。

capabilities(用戶端)

一個物件,用來宣告用戶端本身支援哪些 MCP 能力。例如,ChatGPT 用戶端常宣告 elicitation,表示可以面向使用者發出提問(追加輸入、確認等)。

範例:

"capabilities": {
  "elicitation": {},
  "sampling": {}
}

伺服器可以利用這些資訊來決定有哪些進階能力值得使用。例如,elicitation 表示用戶端(ChatGPT)可以向使用者提出釐清性的問題或要求額外資料。

clientInfo

簡單的中繼資訊:用戶端名稱與版本。

"clientInfo": {
  "name": "ChatGPT",
  "version": "2.0.0"
}

對伺服器開發者來說,這是記錄的黃金資料:你可以隨時知道目前接上來的是哪個用戶端 —— ChatGPT、MCP Inspector、你自己的測試用戶端,以及其版本號。

4. 伺服器如何回應:initialize result

initialize 的回覆是一個普通的 JSON-RPC 結果,id 相同,但在 result 欄位裡放入伺服器的能力描述。

在請求端我們看了用戶端的 capabilities —— 它自己支援什麼。現在來看回覆中的鏡像物件:伺服器的 capabilities,也就是它會什麼。概略如下:

{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "protocolVersion": "2025-06-18",
    "capabilities": {
      "tools": {
        "listChanged": true
      },
      "resources": {},
      "prompts": {},
      "logging": {}
    },
    "serverInfo": {
      "name": "gift-genius-backend",
      "version": "0.1.0"
    }
  }
}

在協定與/或 SDK 的官方說明中,你會看到類似的結構。重點部分:

protocolVersion(回覆)

伺服器會重複用戶端提出的版本,或(理論上)在多個版本都支援時選一個共同版本。典型實作中,如果伺服器支援,就直接確認用戶端的版本;若不支援,則回傳錯誤並結束對話。

serverInfo

伺服器的中繼資訊:名稱與版本。

"serverInfo": {
  "name": "gift-genius-backend",
  "version": "0.1.0"
}

聽起來平淡,但你之後在記錄中要過濾與查找問題時會非常依賴它們,例如:「為什麼版本為 X 的 ChatGPT 與我們版本為 Y 的伺服器協商失敗?」

capabilities(伺服器)

這是最有意思的欄位。伺服器在此宣告支援哪些 MCP 原語與擴充:能否處理 tools/*resources/*prompts/*,是否能送出清單變更通知等等。

如果 capabilities 裡沒有 tools 區段,任何正確實作的用戶端都不會呼叫 tools/listtools/call。相同地,若缺少 resources,用戶端就不會送出 resources/listresources/read

因此 capabilities 是一種輕量合約:「對這個伺服器,哪些事情可以做、哪些不行」。

5. 把 capabilities 當作「超能力清單」

接下來我們只關心伺服器的 capabilities —— 也就是在回覆 initialize 時帶回來、用來界定伺服器支援哪些 MCP 原語的那個物件。

我們更仔細看看它的結構。範例(簡化,但接近規格):

 {
"capabilities": {
  "tools": {
    "listChanged": true
  },
  "resources": {
    "subscribe": true,
    "listChanged": true
  },
  "prompts": {
    "listChanged": false
  },
  "logging": {}
}

這樣的例子在 MCP 的官方架構文檔中有詳細說明。我們逐一拆解。

Capabilities.tools

存在 tools 鍵表示:伺服器會回應 tools/listtools/call 方法。 若裡面還有旗標 listChanged: true,則代表未來當工具清單改變時,伺服器可能會送出 tools/list_changed 通知。

對 ChatGPT 來說這很有用:可以快取工具清單,當收到 list_changed 時再更新,而不必整個重新連線。

Capabilities.resources

resources 區段宣告伺服器支援資源操作:resources/listresources/read,有時也包含搜尋。內部旗標:

  • subscribe: true —— 用戶端可以訂閱資源變更(例如 live logs 或檔案更新)。
  • listChanged: true —— 當資源新增或移除時,伺服器可以送出 resources/list_changed 通知。

這對大型目錄或「動態」資料特別重要,因為它們會不斷變化。

Capabilities.prompts

如果伺服器註冊了預先定義的提示詞(例如綁定你網域的模型提示模板),那麼在 capabilities 中會有 prompts 鍵。裡面也可能有 listChanged 旗標。

用戶端看到這個區段,就知道可以使用 prompts/list,甚至 prompts/get

Capabilities.logging 與其他

一些伺服器實作還會宣告 logging —— 代表伺服器能透過 MCP 傳送結構化日誌,便於除錯。

也可能出現其他區段(例如 sampling 或特定擴充)。重點是,協定自始就設計為可擴充:你可以在 capabilities 中加入新鍵,而舊用戶端若不認識它們,會直接忽略。

洞見

實驗證實,ChatGPT App 目前會忽略送給它的 listChanged 訊息。也就是說,撰寫應用時你無法先宣告一組 tools,之後再動態增減工具。雖然 MCP 協定本身允許這麼做。

在撰寫本課時的現況是:在你註冊應用至 ChatGPT Store 的那一刻,ChatGPT 會向你的應用請求 tools 與 resources 清單,並永久快取。2026 年內情況改變的機率很大;但在 2026 年第一季內改變的機率不高。

6. Handshake 之後的 discovery:如何取得工具與資源清單

Handshake 回答的是「伺服器大致能做什麼」。 下一步就是所謂的 discovery:用戶端透過具體方法把細節拉出來 —— 有哪些工具、可用哪些資源、內建了哪些提示詞。

為此會使用 discovery 方法:像是 tools/listresources/listprompts/list。在 MCP 架構文檔中也建議這樣描述:handshake → discovery → 工具呼叫。

範例請求 tools/list

{
  "jsonrpc": "2.0",
  "id": 2,
  "method": "tools/list",
  "params": {}
}

伺服器的回覆包含一個工具陣列:名稱、描述、引數的 JSON Schema,有時還會有分類或圖示等中繼資料。

之後 ChatGPT(或其他用戶端)會快取這份清單,並在對話期間用它來:

  • 為使用者的任務挑選合適的工具;
  • 確認工具名稱是否存在;
  • 在送出 tools/call 前驗證引數。

資源的流程相似,只是 resources/list 常會用游標做分頁,避免一次拉回百萬筆資料。這也在 MCP 規格中有說明,是大型目錄的典型案例。

7. 以我們的 GiftGen 應用為例看 handshake 與 capabilities

在前面的單元中,我們打造了一個幫忙挑選禮物的教學應用。我們已經有小工具(widget)、後端有一個 suggest_gifts 工具,也有某種禮物目錄。現在來想像,MCP 伺服器 gift-genius 的 handshake 會長什麼樣。

GiftGen 的 handshake 範例

用戶端的請求:

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "initialize",
  "params": {
    "protocolVersion": "2025-06-18",
    "capabilities": {
      "elicitation": {}
    },
    "clientInfo": {
      "name": "ChatGPT",
      "version": "2.1.0"
    }
  }
}

我們伺服器的回覆:

{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "protocolVersion": "2025-06-18",
    "capabilities": {
      "tools": { "listChanged": true },
      "resources": { "listChanged": true },
      "prompts": {},
      "logging": {}
    },
    "serverInfo": {
      "name": "gift-genius-backend",
      "version": "0.2.0"
    }
  }
}

本質上我們幾乎重現 MCP 官方架構中的範例,只是把名稱換成我們的應用。

用戶端從這個回覆得知:

  • 有工具(tools),而且清單可能會動態變更(listChanged: true)。
  • 有資源(我們的禮物目錄,可能存於檔案或資料庫)。
  • 有提示詞(例如「為使用者 N 撰寫一段簡短的禮物描述」這類模板)。
  • 伺服器可以送出日誌(對 Inspector 與除錯很方便)。

接著用戶端呼叫 tools/list,會看到例如這樣的工具:

{
  "name": "suggest_gifts",
  "description": "根據收禮者的個人資料挑選禮物點子。",
  "inputSchema": {
    "type": "object",
    "properties": {
      "age": { "type": "integer" },
      "relationship": { "type": "string" },
      "budget": { "type": "number" }
    },
    "required": ["age", "relationship"]
  }
}

現在,當使用者輸入類似:「幫我為妹妹準備禮物,25 歲,預算 50 美元以內」,模型就知道:有個 suggest_gifts 工具,需要哪些引數,並可透過 tools/call 來呼叫它。

8. SDK 如何隱藏 handshake(但為何仍需理解)

在我們下一講會使用的 MCP TypeScript SDK 中,initializenotifications/initialized 的流程都被封裝在 connect 方法裡。大致程式碼如下:

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";

const server = new McpServer({
  name: "gift-genius",
  version: "1.0.0",
});

// 註冊工具 —— SDK 會根據這些設定自動配置 capabilities.tools
server.tool(
  "suggest_gifts",
  {
    description: "挑選禮物點子。",
    inputSchema: {
      type: "object",
      properties: {
        age: { type: "integer" },
        relationship: { type: "string" },
        budget: { type: "number" },
      },
      required: ["age", "relationship"],
    },
  },
  async (input) => {
    // ... 挑選禮物的邏輯 ...
    return { suggestions: [] };
  },
);

const transport = new StdioServerTransport();

// 在這裡 SDK 會:
// 1) 接收用戶端的 initialize
// 2) 回覆 serverInfo 與 capabilities
// 3) 等待 notifications/initialized
// 4) 然後開始處理 tools/* 呼叫
await server.connect(transport);

SDK 會根據你註冊的內容自動組合 capabilities:如果至少有一個 server.tool(...),它就會在 capabilities 中加入 tools 區段。若你註冊了資源或提示詞,則會出現 resourcesprompts

理解 handshake 與 capabilities 並不是叫你手寫 JSON(千萬別這樣做),而是為了:

  • 閱讀 MCP 日誌,理解為何用戶端「看不到」你的工具;
  • 診斷協定版本不相容;
  • 在必要時實作自訂伺服器或非標準傳輸層。

9. 協議版本與能力演進

Handshake 中的 protocolVersion 不是裝飾。MCP 規格明確強調:這是協商相容協定版本的方式;若找不到共同版本,最好結束連線。

典型情境:

  1. 你在正式環境部署了一個 MCP 伺服器,SDK 實作的 MCP 版本是 "2025-06-18"
  2. 過一段時間出現新的 MCP 版本,你更新了用戶端,但伺服器仍是舊版。
  3. 用戶端送出 protocolVersion: "2026-02-01",伺服器不認識該版本並回傳錯誤 invalid protocol version(或類似訊息)。

實務上常見:開發者忽略這個欄位,然後納悶為何連線就是建立不起來。

正確看待版本:

  • 永遠清楚你的 SDK 支援哪個 MCP 版本(通常見於文件或發行說明)。
  • 在更新 SDK 時,刻意調整協定版本。
  • 日誌與監控應清楚顯示因 protocolVersion 不一致導致初始化失敗的錯誤。

透過 capabilities 擴充功能也與演進有關:新的 MCP 功能會做為 capabilities 中的新鍵加入。舊用戶端忽略它們,新用戶端可以利用。官方文件正是以此模式來維持相容性。

10. 從 ChatGPT 與 Inspector 的角度看 handshake

ChatGPT 在連上 MCP 時做了什麼

當你在 Dev Mode 把 MCP 伺服器綁到 ChatGPT 時,平台在背後大致會做:

  1. 開啟傳輸(通常是連到 /mcp 的 HTTP/stream)。
  2. 送出 initialize,包含 protocolVersioncapabilitiesclientInfo(像是「ChatGPT Enterprise、某版本」)。
  3. 收到回覆後,快取伺服器的 capabilities。
  4. 依照看到的 capabilities 呼叫 tools/listresources/listprompts/list
  5. 在對話期間,當模型決定呼叫工具時,會對照這個快取:是否有該工具、其引數結構為何、該如何發起呼叫。

如果伺服器的 capabilities 不包含 tools,ChatGPT 甚至不會嘗試把你的 App 當作可用工具。若 capabilities 有 resources,但沒有 listChanged 旗標,ChatGPT 可能會快取資源清單,不等待變更通知。

Inspector 與 MCP Jam 如何協助除錯

像 MCP Jam / MCP Inspector 這類工具做的事幾乎一樣:建立連線、執行 handshake、把伺服器的 capabilities 呈現給你,並讓你手動呼叫 tools/listtools/call 等等。

對開發者來說幾乎是必備:

  • 看得見伺服器實際回的 protocolVersion
  • 立刻知道 capabilities 是否含有 toolsresourcesprompts
  • 能理解為什麼 ChatGPT 看不到工具(capabilities 未宣告或 handshake 失敗)。

在本模組最後一講你會更深入使用這些工具,但現在就該明白,它們正是基於我們在此解析的 handshake 運作。

11. 使用 handshake 與 capabilities 的常見錯誤

理論上很直觀,但實務上 handshake 與 capabilities 宣告經常成為最基本的錯誤來源 —— 尤其在 Dev Mode 或 MCP Inspector。下面列出幾個你幾乎一定會在自己的程式碼或同事的日誌中遇到的常見錯誤。

錯誤 №1:initialize 請求格式不正確。
在沒有 SDK、手工實作 MCP 伺服器時非常常見 —— 遺漏某個 JSON-RPC 必要欄位。例如忘了 jsonrpc: "2.0",把 method 打成 "init" 而非 "initialize",或是把 capabilities 寫成布林值而不是物件。MCP 規格要求嚴格的格式;任何偏差都會導致解析錯誤與連線中斷。文件與實作指南都強調:先確保 initialize 嚴格符合規格,再看其他問題。

錯誤 №2:忽略 protocolVersion。
有時候開發者直接複製文件中的範例,填上一串隨意的字串而不確認 SDK 的支援情況。結果就是用戶端與伺服器說的是不同版本的 MCP,連線建立不起來。錯誤可能表現為「用戶端完全連不上」。請把 protocolVersion 當作真正的合約:需要由前端/代理平台與撰寫 MCP 伺服器的團隊協調一致。

錯誤 №3:遺漏 capabilities。
經典情境:你在伺服器註冊了工具,但在手動實作 handshake 時忘了在 initialize 回覆的 capabilities 中加入 "tools": {}。在 inspector 裡你看得到工具,但 ChatGPT 顯示「No tools available」—— 因為它忠實相信 capabilities,如果缺少 tools 區段,它就不會呼叫 tools/list。Apps SDK 的疑難排解指南也特別強調:若 ChatGPT 看不到工具,第一步先檢查 capabilities。

錯誤 №4:嘗試使用未在 capabilities 宣告的方法。
有時同學會實驗,對沒有 resources 區段的伺服器送出 resources/list。形式上伺服器可能回 Method not found,但更正確的做法是根本不要呼叫這類方法。MCP 特別引入 capabilities 就是為了防止這種嘗試。用戶端應先檢查 capabilities 是否有相應區段,再決定是否呼叫方法。

錯誤 №5:在收到 notifications/initialized 前伺服器就開始「說話」。
若伺服器在回覆 initialize 後就立刻開始傳送日誌或通知,而沒有等待 notifications/initialized,部分用戶端可能會忽略這些訊息甚至斷線。MCP 官方架構強調:先完成 handshake,並且收到初始化通知後,才開始「工作」階段。

錯誤 №6:變更工具的結構卻未發送清單變更通知。
當你修改工具的 JSON Schema(把欄位改為必填、重新命名引數),但沒有重啟伺服器或發送「工具清單已變更」的通知,用戶端的快取可能仍是舊版結構,導致奇怪的驗證錯誤。規格建議使用 listChanged 旗標與 tools/list_changedresources/list_changed 通知,協助用戶端適時更新快取。

錯誤 №7:過早最佳化,對 capabilities 施展「魔法」。
有時開發者在還沒掌握基礎機制時,就開始設計複雜的動態 capabilities 生成、針對不同用戶端的版本化與各種奇技淫巧。起步時如實宣告伺服器會什麼就好:tools、resources、prompts、logging。隨著真正的需求再擴充 capabilities,不要「為了未來」而先行堆砌。這比較像是組織上的反模式,而非純技術性的錯誤,但在實戰專案中極為常見。

留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION