CodeGym /課程 /ChatGPT Apps /MCP 訊息格式:requests, replies, notifications, tools/resource...

MCP 訊息格式:requests, replies, notifications, tools/resources/prompts

ChatGPT Apps
等級 6 , 課堂 1
開放

1. MCP 與 JSON‑RPC:一次就該弄懂的「乏味」基礎

在上一堂課我們談了 MCP 存在的意義,以及它如何融入 Apps SDK 的堆疊。這一課我們把焦點縮到最「枯燥」的一層——MCP 訊息格式,讓你能自信地閱讀原始 JSON 日誌,明白 ChatGPT 寄給你的伺服器的是什麼、伺服器又回了什麼。

MCP 使用 JSON‑RPC 2.0 作為資料傳輸:所有請求、回應與通知都是結構可預期的 JSON 物件。

也就是說,與其「每個服務各自發明格式」,不如有一份基本合約:

  • 請求具有必填欄位 jsonrpc(通常是 "2.0")、唯一的 id、字串型的 method 名稱,以及承載參數的 params 物件;
  • 回應透過 id 與請求關聯,且只包含 resulterror
  • 通知(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/listtools/callresources/listprompts/list 等)以及它們的格式(期待哪些參數、回傳哪些資料)。

要抓住重點:JSON‑RPC 是「請求–回應–通知」的骨架;MCP 則是「具體有哪些請求,以及它們的內部內容」。

2. Request:MCP 如何請求執行某件事

先從請求開始。它永遠代表「某方想做點事」。通常是用戶端 → 伺服器(ChatGPT → 你的 MCP 伺服器),但 MCP 也允許反向請求,當伺服器請求用戶端做 sampling 或 elicitation。這一課我們主要關注典型情況:用戶端請求伺服器。

任何 MCP‑request 都有三個關鍵欄位:

  1. jsonrpc —— JSON‑RPC 協議版本,通常是 "2.0"
  2. id —— 請求識別碼;可為任意 JSON 類型,但實務常見為數字或字串。重點是對於活躍中的請求,id 必須唯一。
  3. 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 如何回應——resulterror

回應(reply)永遠透過 id 與請求關聯。這就像許多分散式系統中的 correlationId:你在日誌裡看到 id=7 的請求收到 id=7 的回應,便知道它們是一對。

JSON‑RPC 設定了一條簡單規則:回應要麼包含 result要麼包含 error但絕不會同時出現。MCP 在此之上,針對不同方法(tools/listtools/call 等)細化 result 的結構,並建議錯誤代碼。

成功回應(result

來看我們的 suggest_giftstools/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
  }
}

這裡有幾個重點。

  • 首先,contentstructuredContent 是你在 Apps SDK 中看過的 MCP‑tools 回應部分。模型會使用 content 的文字,而你的元件會將 structuredContent 的資料妥善渲染。
  • 其次,isError 標誌屬於業務結果。從協議角度來看,一切都成功:JSON 合法、方法存在、參數已解析。但業務邏輯可能判定:「我沒有找到任何禮物點子,從 UX 角度這算錯誤。」此時你會設置 isErrortrue,並在 content 中描述問題。
  • 第三,對不同方法(tools/listtools/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,但標記為 isErrortrue,並在內容中描述問題。

這種區分能大幅幫助 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 有兩個主要流程:

  1. discovery —— 用戶端了解有哪些工具;
  2. invocation —— 用戶端呼叫特定工具。

我們已經略看過 tools/listtools/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,包含 contentstructuredContent,以及選填的 _meta(例如指定 openai/outputTemplate,若你想把此工具綁定到特定小工具)。

這組 tools/listtools/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 層,這是一個便利函式,它會:

  1. 知道 MCP 伺服器的 URL(來自應用程式設定);
  2. 能依名稱 suggest_gifts 找到工具描述;
  3. 把你的呼叫封裝成 MCP‑request tools/call
  4. 透過選定的傳輸(HTTP/SSE)送出;
  5. 等待 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/listtools/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" } } }
      }
    ]
  }
}

很明顯這是對剛才請求的回覆(同一個 id1),協議層成功(有 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,並決定是否將其視為業務錯誤(isErrortrue),或視為正常行為。

引數錯誤時的回應可能如下:

{
  "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 規範對照,你會發現欄位 toolargs 不符合預期應為 namearguments

MCP‑SDK 的用戶端/伺服器大多不會生成這樣的 JSON,但如果你在不熟規範的情況下手動整合,這種錯誤很可能發生。這正是為什麼課程要把協議「原原本本」地拆開講清楚,而不只看 SDK 的包裝。

8. 使用 MCP 訊息的常見錯誤

錯誤 1:混淆協議層錯誤與業務錯誤。
開發者常把所有「出狀況」的情形都包成上層 error——包括資源不存在、引數錯誤、資料庫崩潰等。在 MCP 的情境下,區分會更有幫助:若 JSON 結構與呼叫 Schema 違規(方法不對、欄位不對、型別不對),適合回 error。若工具只是無法完成領域操作(此預算下沒有禮物、找不到使用者),更好的做法是回傳合法的 result,設置 isErrortrue,並在 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/calltools/list,MCP 訊息格式就不再是「魔法」,而是日常工程例行事務。

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