1. MCP 與 JSON‑RPC:一次就該弄懂的「乏味」基礎
在上一堂課我們談了 MCP 存在的意義,以及它如何融入 Apps SDK 的堆疊。這一課我們把焦點縮到最「枯燥」的一層——MCP 訊息格式,讓你能自信地閱讀原始 JSON 日誌,明白 ChatGPT 寄給你的伺服器的是什麼、伺服器又回了什麼。
MCP 使用 JSON‑RPC 2.0 作為資料傳輸:所有請求、回應與通知都是結構可預期的 JSON 物件。
也就是說,與其「每個服務各自發明格式」,不如有一份基本合約:
- 請求具有必填欄位 jsonrpc(通常是 "2.0")、唯一的 id、字串型的 method 名稱,以及承載參數的 params 物件;
- 回應透過 id 與請求關聯,且只包含 result 或 error;
- 通知(notifications)看起來像請求,但沒有 id,且不會有回應。
看起來大致如下:
{
"jsonrpc": "2.0",
"id": 42,
"method": "tools/list",
"params": {
"cursor": null
}
}
這是 request。成功時的回應:
{
"jsonrpc": "2.0",
"id": 42,
"result": {
"tools": [],
"nextCursor": null
}
}
如果你心想「這不就是一般的 RPC 嗎?」——沒錯。MCP 只是進一步固定,有哪些方法(tools/list、tools/call、resources/list、prompts/list 等)以及它們的格式(期待哪些參數、回傳哪些資料)。
要抓住重點:JSON‑RPC 是「請求–回應–通知」的骨架;MCP 則是「具體有哪些請求,以及它們的內部內容」。
2. Request:MCP 如何請求執行某件事
先從請求開始。它永遠代表「某方想做點事」。通常是用戶端 → 伺服器(ChatGPT → 你的 MCP 伺服器),但 MCP 也允許反向請求,當伺服器請求用戶端做 sampling 或 elicitation。這一課我們主要關注典型情況:用戶端請求伺服器。
任何 MCP‑request 都有三個關鍵欄位:
- jsonrpc —— JSON‑RPC 協議版本,通常是 "2.0"。
- id —— 請求識別碼;可為任意 JSON 類型,但實務常見為數字或字串。重點是對於活躍中的請求,id 必須唯一。
- method —— 形如 "tools/list" 或 "tools/call" 的字串。MCP 規範可用的方法集合。
此外還有 params 物件,其中包含具體方法的參數。
範例:請求工具清單
想像 ChatGPT 剛連上你的 MCP 伺服器,想知道可呼叫哪些工具。它會送出類似下面的請求:
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/list",
"params": {
"cursor": null
}
}
欄位 cursor 用於分頁——若工具很多,伺服器可以分批回傳。
就我們的教學應用(挑選禮物)而言,這裡會很單純:只有一兩個工具,但協議一樣不變。先把它當直觀範例;正式結構我們稍後在 tools 章節再看。
範例:呼叫工具(tools/call)
接下來更有趣一些。假設我們已經有一個 MCP 工具 suggest_gifts,你會在講解 MCP 伺服器的課程中實作它。它期望的參數:
- occasion —— 場合(Birthday、Wedding、…),
- budget —— 美元金額(數值),
- recipient —— 描述送給誰的字串。
當 ChatGPT 決定使用這個工具時,會形成一個 MCP 請求:
{
"jsonrpc": "2.0",
"id": 7,
"method": "tools/call",
"params": {
"name": "suggest_gifts",
"arguments": {
"occasion": "birthday",
"budget": 100,
"recipient": "friend who loves board games"
}
}
}
請注意幾個細節。
首先,工具名稱取自你在伺服器端宣告的內容(server.registerTool("suggest_gifts", …))。其次,arguments 物件必須符合你在工具描述中附帶的 JSON Schema。
如果 GPT 嘗試送出不符合 Schema 的引數(例如,budget:"一百美元"),伺服器可依實作選擇在協議層或業務邏輯層回傳錯誤。此刻只要抓住這類請求的大致形狀;在下方的 tools 章節我們會更系統地重新檢視同樣的訊息。
資源與提示的請求
對資源與提示的請求也很相似。MCP 規範了以下方法:
- resources/list —— 列出可用資源;
- resources/read(或 resources/get)—— 依 URI 讀取特定資源;
- prompts/list —— 取得可用提示(prompts)的清單;
- prompts/get —— 取得特定提示的內容。
讀取包含禮物目錄的資源之請求範例:
{
"jsonrpc": "2.0",
"id": 15,
"method": "resources/read",
"params": {
"uri": "mcp://gift-server/resources/gift_catalog"
}
}
先記住兩件事。其一,對每個原語通常都有 */list 與 */get/*/read。其二,方法名總是在字串欄位 method,而所有內容都在物件 params。
3. Reply:MCP 如何回應——result 與 error
回應(reply)永遠透過 id 與請求關聯。這就像許多分散式系統中的 correlationId:你在日誌裡看到 id=7 的請求收到 id=7 的回應,便知道它們是一對。
JSON‑RPC 設定了一條簡單規則:回應要麼包含 result,要麼包含 error,但絕不會同時出現。MCP 在此之上,針對不同方法(tools/list、tools/call 等)細化 result 的結構,並建議錯誤代碼。
成功回應(result)
來看我們的 suggest_gifts 在 tools/call 成功時的回應範例。伺服器完成處理,找到合適的禮物,並在 result 欄位回傳清單:
{
"jsonrpc": "2.0",
"id": 7,
"result": {
"content": [
{
"type": "text",
"text": "Here are some gift ideas for your friend..."
}
],
"structuredContent": {
"gifts": [
{ "name": "Board game: Catan", "price": 45 },
{ "name": "Dice set", "price": 20 }
]
},
"isError": false
}
}
這裡有幾個重點。
- 首先,content 與 structuredContent 是你在 Apps SDK 中看過的 MCP‑tools 回應部分。模型會使用 content 的文字,而你的元件會將 structuredContent 的資料妥善渲染。
- 其次,isError 標誌屬於業務結果。從協議角度來看,一切都成功:JSON 合法、方法存在、參數已解析。但業務邏輯可能判定:「我沒有找到任何禮物點子,從 UX 角度這算錯誤。」此時你會設置 isError:true,並在 content 中描述問題。
- 第三,對不同方法(tools/list、tools/call、*/list、*/get),MCP 規範會詳細描述 result 應包含哪些欄位。舉例來說,對 tools/list,伺服器會回傳工具描述陣列,包含名稱、標題、說明以及輸入引數的 JSON Schema。
錯誤回應(error)
若協議或伺服器層級出現問題,則以 error 物件取代 result。它通常包含:
- code —— 錯誤代碼(數值);
- message —— 可讀的錯誤描述;
- data —— 選填的附加資料(stack trace、細節等)。
範例:模型呼叫了不存在的方法:
{
"jsonrpc": "2.0",
"id": 99,
"error": {
"code": -32601,
"message": "Method not found: tools/col"
}
}
代碼 -32601 是 JSON‑RPC 經典的「method not found」。
有一條細但重要的界線,區分兩種錯誤。
協議層錯誤 —— 當違反 MCP/JSON‑RPC 規則:未知方法、params 欄位型別不正確、JSON 無效。此時適合在最上層回傳 error。
業務錯誤 —— 當協議遵守,但操作因領域理由失敗:目錄為空、對特定資源無權限、業務識別碼錯誤。此時 MCP 通常建議回傳合法的 result,但標記為 isError:true,並在內容中描述問題。
這種區分能大幅幫助 ChatGPT 與除錯工具:從日誌就能看出,這是技術性故障,還是業務邏輯的有意拒絕。
4. Notifications:單向訊息
通知(notification)就是「不期待回應的信件」。在 JSON‑RPC 中,通知看起來像一般請求,但沒有 id。用戶端不應回覆它們。
在 MCP 中,通知用於事件:tools/resources/prompts 清單變更、長作業進度、日誌訊息等。
最常見且你一定會遇到的例子——工具清單變更通知。MCP 的 tools 規範描述了 capability listChanged 與通知 tools/list_changed,當可用工具集合變更時由伺服器發送。
通知可能長這樣:
{
"jsonrpc": "2.0",
"method": "tools/list_changed",
"params": {
"reason": "New tool 'suggest_gift_cards' was added"
}
}
不需要回應它。用戶端在收到這樣的通知後,可能會判斷:「啊,該再呼叫一次 tools/list 並更新工具快取。」
其他常見的 MCP 通知(我們會在處理串流與事件的模組中詳談):
- 長任務進度事件(notifications/progress);
- 伺服器日誌(notifications/logging/message);
- 資源變更(resources/list_changed)與提示變更(prompts/list_changed)。
目前只要記住:通知 = 沒有 id 的請求,且不期待回應。如果你在日誌裡看到沒有 id 的 JSON,它很可能就是 notification。
Insight
據實驗觀察,ChatGPT App 目前會忽略送給它的訊息(MCP‑notification)。不過,考慮到 ChatGPT Apps 還在發展初期,未來在不久的將來很有可能完整支援 MCP 協議的各個面向。因此仍建議你把這一側的 MCP 協議學起來。
5. tools/resources/prompts 在訊息中的樣貌
接下來是重點:在 MCP 訊息內部,那些我們常提到的 tools、resources 與 prompts 究竟如何描述。
Tools:描述與呼叫
在協議層,tools 有兩個主要流程:
- discovery —— 用戶端了解有哪些工具;
- invocation —— 用戶端呼叫特定工具。
我們已經略看過 tools/list 與 tools/call。現在更系統地看看:它們覆蓋哪些流程,以及 result 會回什麼。
5.1.1. 工具清單 —— tools/list
我們已經看到過 tools/list 的 request。來看回應結構。MCP 規範說:在 result.tools 應回傳一個物件陣列,每個物件描述一個工具。工具必須包含:
- name —— 唯一名稱,之後以此呼叫 tools/call;
- title —— 簡短標題(人與模型都會看到);
- description —— 更詳盡的說明(就像向同事解釋該工具在做什麼);
- inputSchema —— 工具引數的 JSON Schema。
以我們的 suggest_gifts 為例,tools/list 的回應可能(大幅簡化)如下:
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"tools": [
{
"name": "suggest_gifts",
"title": "Gift ideas generator",
"description": "Suggests gift ideas for a given occasion and budget.",
"inputSchema": {
"type": "object",
"properties": {
"occasion": { "type": "string" },
"budget": { "type": "number" },
"recipient": { "type": "string" }
},
"required": ["occasion", "budget"]
}
}
],
"nextCursor": null
}
}
如果你曾在 Apps SDK 註冊工具時寫過 inputSchema,你幾乎已經看過這個物件,只是「從上層」以 TypeScript 物件呈現。MCP 只是把它透過協議傳給用戶端。
5.1.2. 呼叫工具 —— tools/call
我們已經碰過呼叫的格式。MCP 規範描述 params 應包含:
- name —— 工具名稱;
- arguments —— 需符合 inputSchema 的物件。
例如:
{
"jsonrpc": "2.0",
"id": 7,
"method": "tools/call",
"params": {
"name": "suggest_gifts",
"arguments": {
"occasion": "wedding",
"budget": 150,
"recipient": "coworker from marketing"
}
}
}
回應中伺服器會回傳 result,包含 content、structuredContent,以及選填的 _meta(例如指定 openai/outputTemplate,若你想把此工具綁定到特定小工具)。
這組 tools/list → tools/call 是 MCP‑tools 的基本循環:先 discovery,再使用。
Resources:有地址的資料
在 MCP 中,Resources 指的是任何可讓用戶端以 URI 存取的資料:檔案、資料庫紀錄、設定檔、目錄等。
它們有一組標準操作:
- resources/list —— 了解有哪些資源;
- resources/read —— 讀取特定資源(或其部分)。
想像一個資源 gift_catalog,描述基本的禮物目錄:類別、品牌、最低與最高價格。伺服器可用 URI "mcp://gift-server/resources/gift_catalog" 宣告它。
resources/list 的回應可能(簡化)如下:
{
"jsonrpc": "2.0",
"id": 3,
"result": {
"resources": [
{
"uri": "mcp://gift-server/resources/gift_catalog", // 只是唯一的字串。mcp 不是協議。
"name": "gift_catalog",
"description": "Base catalog of gifts with categories and prices",
"mimeType": "application/json"
}
],
"nextCursor": null
}
}
而讀取資源 —— resources/read:
{
"jsonrpc": "2.0",
"id": 4,
"method": "resources/read",
"params": {
"uri": "mcp://gift-server/resources/gift_catalog"
}
}
回應可包含內容本身與中繼資料:
{
"jsonrpc": "2.0",
"id": 4,
"result": {
"contents": [
{
"uri": "mcp://gift-server/resources/gift_catalog",
"mimeType": "application/json",
"text": "{\"categories\":[\"boardgames\",\"books\"]}"
}
]
}
}
核心概念:資源是可定址的資料,而 tools 是操作。MCP 在協議中將兩者都明確化。
Prompts:可重用的模板
Prompts 是伺服器可提供給用戶端的「預先準備的提示」或模板,具有:
- 名稱;
- 人類可讀的標題/描述;
- 內容(常是 system‑prompt 模板或 few‑shot 範例)。
而且不意外地有兩個方法:
- prompts/list —— 了解有哪些提示;
- prompts/get —— 取得某個提示的內容。
例如,你想為禮物搭配的祝賀訊息設定特別風格。那麼在 MCP 伺服器中可以宣告 prompt gift_congrats_style。
prompts/list 的回應可能長這樣:
{
"jsonrpc": "2.0",
"id": 10,
"result": {
"prompts": [
{
"name": "gift_congrats_style",
"description": "Style guide for birthday congratulations in a friendly tone"
}
]
}
}
而 prompts/get 會回傳實際文字(或結構化內容),用戶端接著可把它作為 system‑prompt 的一部分餵給 LLM。以下是請求與回應範例:
{
"jsonrpc": "2.0",
"id": 11,
"method": "prompts/get",
"params": {
"name": "gift_congrats_style"
}
}
{
"jsonrpc": "2.0",
"id": 11,
"result": {
"prompt": {
"name": "gift_congrats_style",
"messages": [
{
"role": "system",
"content": [
{
"type": "text",
"text": "You are a friendly assistant that writes short, warm birthday congratulations..."
}
]
}
]
}
}
}
6. 這與 Apps SDK 與我們的小工具如何關聯
此刻 MCP‑JSON 看起來也許還有點繁瑣。讓我們把它與你已透過 Apps SDK 做過的事串起來。
提醒一下,在你的小工具前端可能有這段程式碼:
// 在 ChatGPT 沙盒中的 React 元件內
async function fetchGifts() {
const result = await window.openai.callTool("suggest_gifts", {
occasion: "birthday",
budget: 50,
recipient: "friend who loves sci-fi"
});
console.log(result);
}
在 Apps SDK 層,這是一個便利函式,它會:
- 知道 MCP 伺服器的 URL(來自應用程式設定);
- 能依名稱 suggest_gifts 找到工具描述;
- 把你的呼叫封裝成 MCP‑request tools/call;
- 透過選定的傳輸(HTTP/SSE)送出;
- 等待 MCP‑reply,解出 result,並在 JavaScript 中回給你作為 result。
若把流程畫成圖,大概如下:
sequenceDiagram
participant Widget
participant AppsSDK as Apps SDK
participant MCP as MCP 伺服器
Widget->>AppsSDK: window.openai.callTool("suggest_gifts", {...})
AppsSDK->>MCP: JSON { id:7, method:"tools/call", params:{...} }
MCP-->>AppsSDK: JSON { id:7, result:{ content, structuredContent } }
AppsSDK-->>Widget: result (ToolOutput)
Widget->>Widget: setState(toolOutput)
理解 MCP 格式會帶來兩個很棒的能力。
首先,你能看懂原始 MCP 日誌(例如 MCP Inspector,之後會有專門一課),清楚看到送出了哪個 tools/call、有哪些引數、回了 result 還是 error。
其次,在設計工具與資源時,你不僅能用 TypeScript 型別思考,更能用 MCP 結構思考:它在 JSON 中會長什麼樣?對其他客戶端(例如也能連到你 MCP 伺服器的代理)是否好用?
7. 小練習:閱讀並「修復」 MCP‑JSON
要讓 MCP 格式變成自己的工具,最好親手把幾則訊息拆解一次。讓我們看一段完整對話:tools/list → tools/call → 結果。
用戶端想要工具清單
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/list",
"params": {}
}
我們觀察到:
- 這是 request(有 id);
- 方法是 tools/list,表示要進行工具 discovery;
- 參數為空,沒有分頁。
伺服器回覆:
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"tools": [
{
"name": "suggest_gifts",
"title": "Gift ideas generator",
"description": "Suggests gift ideas",
"inputSchema": { "type": "object", "properties": { "occasion": { "type": "string" } } }
}
]
}
}
很明顯這是對剛才請求的回覆(同一個 id:1),協議層成功(有 result,沒有 error),而用戶端現在知道存在工具 suggest_gifts。
用戶端呼叫工具
接著用戶端發出 tools/call:
{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/call",
"params": {
"name": "suggest_gifts",
"arguments": {
"occasion": "anniversary"
}
}
}
若伺服器還期望 budget,但模型未提供,它可以:
- 回傳協議層錯誤(例如,最上層的 error,代碼為「invalid params」);
- 或採用預設決策(例如使用平均預算)並回傳正常的 result。
以我們先前的術語來說,第一種是協議層錯誤(上層 error),第二種屬於業務邏輯:你仍回傳合法的 result,並決定是否將其視為業務錯誤(isError:true),或視為正常行為。
引數錯誤時的回應可能如下:
{
"jsonrpc": "2.0",
"id": 2,
"error": {
"code": -32602,
"message": "Missing required property 'budget' in arguments"
}
}
再強調一次,要區分這與業務錯誤:這裡是違反了協議(引數不符合 Schema),因此適合回傳 error。
壞掉的範例:找出 bug
以下是新手有時會出現的 JSON:
{
"jsonrpc": "2.0",
"id": 3,
"method": "tools/call",
"params": {
"tool": "suggest_gifts",
"args": {
"occasion": "birthday",
"budget": 100
}
}
}
乍看似乎合理,但若與 MCP 規範對照,你會發現欄位 tool 與 args 不符合預期應為 name 與 arguments。
MCP‑SDK 的用戶端/伺服器大多不會生成這樣的 JSON,但如果你在不熟規範的情況下手動整合,這種錯誤很可能發生。這正是為什麼課程要把協議「原原本本」地拆開講清楚,而不只看 SDK 的包裝。
8. 使用 MCP 訊息的常見錯誤
錯誤 1:混淆協議層錯誤與業務錯誤。
開發者常把所有「出狀況」的情形都包成上層 error——包括資源不存在、引數錯誤、資料庫崩潰等。在 MCP 的情境下,區分會更有幫助:若 JSON 結構與呼叫 Schema 違規(方法不對、欄位不對、型別不對),適合回 error。若工具只是無法完成領域操作(此預算下沒有禮物、找不到使用者),更好的做法是回傳合法的 result,設置 isError:true,並在 content 中給出明確訊息。如此一來,ChatGPT 模型與除錯工具都能正確區分「通道壞掉」與「伺服器有意拒絕」。
錯誤 2:忽略欄位 id 與請求關聯。
有時你會在 MCP 伺服器的日誌中看到手動輸出的訊息缺少 id,或在多個活躍請求上重複使用相同的 id。在單執行緒的 hello‑world 也許勉強可行,但一旦有平行呼叫或重試,立刻會變得難以判斷哪個回應屬於哪個請求。JSON‑RPC 要求請求存活期間 id 唯一,而 MCP 依賴這條規則。若你使用官方 SDK,通常不用操心 id;但只要你自行實作傳輸或記錄,請務必保存與輸出 id——這會是你除錯奇怪問題時的第一條線索。
錯誤 3:同一方法的 result 結構不穩定。
有時會想「稍微」依情況變動回應格式:有時回傳禮物陣列、有時回傳只含一行字的物件、有時只回 text 而不含 structuredContent。模型也許能勉強接受,但你的元件與其他 MCP 客戶端多半無法。MCP 規範對每個方法都描述了可預期的 result 結構;請盡量遵循。如果需要不同格式,寧可宣告另一個工具或版本,也不要在運行中改變 Schema。
錯誤 4:在 params 中多餘或缺少欄位。
自訂實作的典型問題——在 params 加入 MCP 不預期的欄位,或遺漏必填欄位。例如在 tools/call 中送出 toolName 而不是 name;或在 resources/read 中用 resourceId 取代 uri。MCP‑SDK 通常會驗證並丟出清楚的例外,但如果你較貼近協議在操作,可能會花很多時間摸不著頭緒。好方法是:在處理器旁準備一份規範中的正確 JSON 請求範例,或從運作中的客戶端日誌擷取,拿來比對你送出的內容。
錯誤 5:想把 notifications 當作「第二條回應通道」。
有些開發者看到 notifications 後,開始用通知來傳遞作業結果,取代一般的 replies:「反正我們在 MCP,還有 SSE,就用通知推送吧」。問題在於 JSON‑RPC 通知先天上不綁定特定 id,不會被用戶端視為某個請求的回應。結果就是更難除錯,也無法知道哪個工具呼叫對應到哪則訊息。通知非常適合事件(tools/resources/prompts 清單變更、新的進度、日誌訊息),但不適合取代對 tools/call 等請求的正常回覆。
錯誤 6:不看 MCP 日誌與檢測工具。
最人性的錯誤——只透過 ChatGPT UI 來除錯整合:「按了按鈕,什麼都沒來,哪天再說吧」。在你看不到原始 MCP 訊息(requests、replies、notifications)之前,很難知道問題出在哪個層級:模型沒呼叫工具、Apps SDK 沒到 MCP 伺服器、伺服器回了不對的 JSON,還是最後在元件渲染時才壞掉。MCP Inspector / Jam 與結構化記錄 MCP 訊息是你的好朋友。當你親眼在日誌中看到活生生的 tools/call 與 tools/list,MCP 訊息格式就不再是「魔法」,而是日常工程例行事務。
GO TO FULL VERSION