跳到主要內容
技術

探索 MCP 協定:Server、Client、Transport 與身份驗證的設計邏輯

在設定裡加幾行 JSON,AI 就能讀你的資料庫——這背後是 MCP 協定。本文拆解 Host/Client/Server 架構、stdio 與 Streamable HTTP 兩種 Transport,以及 OAuth 2.1 身份驗證的完整流程。

在 Claude Desktop 的設定檔 (Settings > Developer > Edit Config) 裡加這段 JSON:

{
  "mcpServers": {
    "filesystem": {
      "command": "npx",
      "args": ["-y", "@modelcontextprotocol/server-filesystem", "/Users/albert"]
    }
  }
}

重啟之後,Claude 就能讀你的本機檔案了。

這件事有點奇怪。Claude 是一個語言模型,訓練資料有截止日期,跑在 Anthropic 的伺服器上——它為什麼突然能看到你電腦裡的文件?中間發生了什麼?

那段 JSON 建立的,是一條符合 MCP(Model Context Protocol) 規範的連線。MCP 是 Anthropic 在 2024 年開源的協定,定義了 AI 應用要怎麼跟外部工具溝通。


Host、Client、Server:三個角色

MCP 不是一個函式庫,是一套協定——定義了 AI 應用與外部工具之間的溝通形式。

協定裡有三個角色:

Host 是擁有 AI 的應用本身。Claude Desktop、Claude Code、Cursor、VS Code 的 AI 外掛——凡是能對話的那個介面,就是 Host。Host 負責接收使用者指令、呼叫 AI 模型、協調所有連線。

Server 是暴露能力的獨立程式。一個 MCP Server 提供三種東西:

  • Tools:可以被呼叫的函式(讀檔、查 DB、呼叫 API)
  • Resources:可以被讀取的資料(檔案內容、資料庫記錄)
  • Prompts:預先定義好的提示範本

Client 是 Host 內部的連線代理——每個 Server 對應一個 Client 實體。它負責建立連線、序列化訊息、處理回應,讓 Host 可以用統一介面呼叫任意 Server,不用管底層怎麼傳輸。

使用者指令

  Host(Claude Desktop / Claude Code)

  Client(每個 Server 一個實體)

 Transport(stdio / HTTP)

  Server(filesystem / GitHub / DB...)

Client 像電話機,Server 像服務台,Transport 像電話線路——這個比喻不完美,但夠用。


Transport 方式一:stdio

最簡單的 Transport,不涉及網路。

Host 直接用作業系統的 spawn 指令把 Server 啟動成一個子行程,透過 stdin(標準輸入)送訊息、從 stdout(標準輸出)讀回應。訊息格式是 JSON-RPC 2.0,每筆訊息佔一行(newline-delimited)。

連線建立後的初始交握長這樣:

// Host → Server(寫入 stdin)
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "initialize",
  "params": {
    "protocolVersion": "2024-11-05",
    "capabilities": {},
    "clientInfo": { "name": "claude-desktop", "version": "1.0" }
  }
}

// Server → Host(寫入 stdout)
{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "protocolVersion": "2024-11-05",
    "capabilities": { "tools": {}, "resources": {} },
    "serverInfo": { "name": "filesystem", "version": "1.0" }
  }
}

// Host 確認完成交握
{ "jsonrpc": "2.0", "method": "notifications/initialized" }

交握完成後,Host 呼叫 tools/list 取得工具清單,之後用 tools/call 實際執行。

stdio 不涉及網路,就沒有埠號和憑證設定的問題。行程隔離也是現成的——Server 掛掉不會拉倒整個 Host。

限制也很直接:Server 與 Host 必須在同一台機器上。想連遠端服務,stdio 就用不了了。


Transport 方式二:網路傳輸

網路傳輸讓 Server 可以跑在遠端——專用伺服器、雲端函式、或公司的內部服務。MCP 規範在這部分走過一次大改。

HTTP + SSE(2024 舊規範)

最早的網路 Transport 用兩條獨立連線:

  • Client → Server:每筆訊息送一個 HTTP POST,路徑 /messages
  • Server → Client:維持一條 SSE(Server-Sent Events) 長連線,路徑 /sse,Server 主動推送的通知走這裡

問題在於兩條連線完全獨立。Client 得同時管理 POST 端和 SSE 端,斷線重連邏輯複雜,部署時還要處理兩個 endpoint 的 CORS 和 Proxy 設定。

Streamable HTTP(2025 新規範)

2025 年 3 月,MCP 規範改掉了這個設計。新的 Transport 叫 Streamable HTTP,核心想法是:一個 endpoint 搞定一切。

Client 把 JSON-RPC 訊息放進 HTTP POST 的 body,打到同一個路徑。Server 根據情況決定怎麼回:

# 情況一:簡單的請求-回應,直接回 JSON
POST /mcp HTTP/1.1
Content-Type: application/json
Accept: application/json, text/event-stream

{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{...}}

---

HTTP/1.1 200 OK
Content-Type: application/json

{"jsonrpc":"2.0","id":1,"result":{"content":[{"type":"text","text":"完成"}]}}
# 情況二:需要串流,升級成 SSE
HTTP/1.1 200 OK
Content-Type: text/event-stream

data: {"jsonrpc":"2.0","method":"notifications/progress","params":{"progress":50}}

data: {"jsonrpc":"2.0","id":1,"result":{"content":[...]}}

狀態維持靠 Mcp-Session-Id header——Client 在初始化後收到 session ID,後續請求帶著它,Server 就能識別是哪條連線。

這比雙通道乾淨很多:一個 endpoint、Server 自己決定要不要串流。

兩種 Transport 對照

stdioStreamable HTTP
傳輸方向雙向(行程管道)HTTP POST + 選擇性 SSE
能否跨機器
設定複雜度中(需要 HTTP 伺服器)
適用場景本機工具、CLI遠端服務、SaaS 整合
身份驗證需求不需要OAuth 2.1

身份驗證

stdio 不需要驗證,但信任問題仍然存在

stdio Server 是 Host 直接啟動的子行程。Host 相信它,所以給它跑起來;一旦跑起來,它就繼承了目前使用者的系統權限——能讀的檔案、能存取的網路,Server 都能碰到。

這裡沒有「身份驗證」的問題,有的是「信任」的問題。

你怎麼確定裝的那個 MCP Server 套件做的是它說的事?MCP 規範的因應方式是把責任放在 Host 上:Host 必須讓使用者明確審核並同意每個 Server 的設定,不能讓 AI 自動安裝或啟動新 Server。但這阻止不了惡意套件——一個偽裝成合法工具的 npm 套件,裝下去就有事。這是 supply chain 問題,不是協定能解的。

網路傳輸:OAuth 2.1

一旦 Server 跑在遠端,就需要真正的身份驗證。2025 年的 MCP 規範規定:網路傳輸的 MCP Server 若需要驗證,必須採用 OAuth 2.1——驗證本身仍是可選的,但一旦要做就只有這條路。

流程分五步:

第一步:發現授權設定

MCP 的 discovery 是兩階段的。Client 先打 MCP Server 的 protected resource metadata,找到實際的授權伺服器:

GET /.well-known/oauth-protected-resource HTTP/1.1
Host: api.example-mcp-server.com

回傳的 JSON 會指向授權伺服器(可能是 MCP Server 自架、也可能是第三方,如 GitHub OAuth)。Client 接著打授權伺服器的 metadata endpoint:

GET /.well-known/oauth-authorization-server HTTP/1.1
Host: auth.example.com

這次拿到的是授權端點位置、token endpoint、支援的 grant type 等細節。

第二步:產生 PKCE 驗證碼

OAuth 2.1 強制要求 PKCE(Proof Key for Code Exchange),防止授權碼被中間人攔截偽造。

Client 在本地產生一個隨機字串(code_verifier),然後用 SHA-256 做 hash,得到 code_challenge

code_verifier  = "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"
code_challenge = BASE64URL(SHA256(code_verifier))
               = "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM"

code_challenge 送給授權伺服器,code_verifier 留在本地。

第三步:導向授權頁面

https://auth.example.com/authorize
  ?response_type=code
  &client_id=mcp-client-xyz
  &redirect_uri=http://localhost:8080/callback
  &scope=read:issues write:comments
  &code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
  &code_challenge_method=S256

使用者在瀏覽器裡登入、同意授權,伺服器把 code 帶回 redirect URI。

第四步:交換 Access Token

POST /token HTTP/1.1
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code
&code=SplxlOBeZQQYbYS6WxSbIA
&redirect_uri=http://localhost:8080/callback
&code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk

伺服器自己算 SHA-256,跟之前收到的 code_challenge 比對——一致才換 token。攔截到授權碼但不知道 code_verifier 的攻擊者,換不到 token,這就是 PKCE 的防護點。

第五步:後續請求帶 Bearer Token

POST /mcp HTTP/1.1
Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...
Content-Type: application/json

{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{...}}

Dynamic Client Registration

回頭看第三步——導向授權頁面的 URL 帶了個 client_id,這 ID 哪來的?

傳統 OAuth 服務(GitHub、Google)要求開發者先到後台建立 OAuth App,拿到 client_id/client_secret 才能整合。MCP 套不上:每個 Claude Code 使用者、每個 Cursor 安裝都是獨立 Client——不可能要每個人去 GitHub 後台手動註冊一個 App,再把 ID 貼回設定檔。

DCR(Dynamic Client Registration,RFC 7591) 解這個問題。授權伺服器多開一個註冊 endpoint,Client 第一次連線時自己註冊:

POST /register HTTP/1.1
Host: auth.example.com
Content-Type: application/json

{
  "client_name": "Claude Code",
  "redirect_uris": ["http://localhost:8080/callback"],
  "grant_types": ["authorization_code", "refresh_token"],
  "token_endpoint_auth_method": "none"
}

伺服器回傳一組新的 client_id,Client 拿著它接著走授權流程。

MCP 規範建議 Server 支援 DCR,但不強制。有支援的話,Host 安裝完直接連就能用;沒支援的話,使用者得手動拿 client_id 貼進設定,OAuth 的麻煩又回來了。

實務上 MCP 自架的 Server 通常都會做 DCR;但接到第三方既有的 OAuth 服務(GitHub、Notion)時,DCR 常常沒得用——這些服務不會開放公開註冊。中間的解法是讓 MCP Server 自己當代理,包一層在第三方驗證上面:對 Client 暴露 DCR,對第三方用 Server 自己的 OAuth App 認證。

實務痛點

理論上 OAuth 2.1 設計得嚴謹。實務上有幾個地方會卡。

Token 放在哪? 規範說 Token 由 Host 負責儲存和管理,但沒說怎麼存。桌面應用可以放 keychain,瀏覽器端的 Host 就複雜了——不能放 localStorage(XSS 風險),要用 httpOnly cookie 或 BFF(Backend-for-Frontend)模式把 token 留在後端。每個 Host 各自實作,沒有統一標準。

多個 Server = 多次 OAuth。 你用五個 MCP Server(GitHub、Notion、Linear、Slack、資料庫),就要走五次 OAuth 流程,管理五組 token,各自處理過期和 refresh。目前沒有優雅的解法。

Refresh Token 的可靠性。 Access Token 有時效。規範支援 Refresh Token,但不強制 Server 一定要發,Client 也不一定都正確實作了 token rotation。Token 到期時工具呼叫靜默失敗,使用者得重新授權。

生態系還在跟上。 OAuth 2.1 是 2025 年才納入規範,很多第三方 MCP Server 還停在 API Key header 的自訂方案,或直接沒有驗證。規範方向是對的,實作覆蓋率還在追。


一次完整的工具呼叫

走一遍具體流程——Claude Code 呼叫遠端 GitHub MCP Server,讀取 issue #42:

你:「幫我看一下 #42 這個 issue 在說什麼」

  Claude 判斷需要呼叫 get_issue 工具

  Host(Claude Code)
    └─ 找到 GitHub MCP Server 對應的 Client
    └─ 檢查 Access Token 是否有效
    └─ 沒有 → 走 OAuth 2.1 → 瀏覽器登入 → 拿到 token

  Client 透過 Streamable HTTP 送出請求:

    POST /mcp HTTP/1.1
    Authorization: Bearer eyJ...
    Content-Type: application/json

    {
      "jsonrpc": "2.0",
      "id": 5,
      "method": "tools/call",
      "params": {
        "name": "get_issue",
        "arguments": { "owner": "anthropics", "repo": "claude-code", "issue_number": 42 }
      }
    }

  GitHub MCP Server
    └─ 驗證 Bearer Token
    └─ 呼叫 GitHub REST API
    └─ 回傳 JSON-RPC response

  Host 把結果交給 Claude 模型

Claude:「Issue #42 在討論...」

每一層做自己的事,Transport 隱藏了網路細節,OAuth 確保了 Server 只回應有授權的請求。


結語

MCP 做的事說起來不複雜:替 AI 和外部工具之間定義一套統一的對話方式。

但「統一」沒有看起來那麼簡單。本機工具用 stdio 就夠了,子行程起來、stdin/stdout 接好,乾淨直接。一旦 Server 跑到遠端,傳輸格式要變、驗證要設計、token 過期要處理。Streamable HTTP 是 2025 年才定案的修正,OAuth 2.1 生態系還在跟上規範。

Transport 決定你的部署架構,OAuth 2.1 決定誰能跟誰說話。

下次你貼上那段 JSON 的時候,你知道那幾行在做什麼了。