SSD Nodes Learn 🎉 VPS $5.50/月起
指南 Matt Connor作者: Matt Connor · 已更新 2026-08-13

無狀態 MCP 伺服器規格變更解析

MCP 2026-07-28 版本移除了 initialize 交握與 session 機制。本文說明此變更對反向代理、健康檢查、逾時設定及驗證流程的具體影響,協助您調整架構。

何謂無狀態 MCP 伺服器

無狀態 MCP 伺服器在請求之間不會保留任何客戶端狀態。每個請求皆包含協定版本、客戶端功能以及伺服器回應所需的憑證,因此任何機器上的任何處理程序皆可回應任何請求。MCP (Model Context Protocol,代理程式用來存取工具的傳輸格式) 在 2026-07-28 版本中將此列為規範,並移除了 initialize 交握程序以及底層的 HTTP session。

這正是其運作的核心意義。不保留客戶端狀態的伺服器可部署於一般的負載平衡器後方,無需設定 session affinity;在部署期間重啟也不會中斷客戶端連線,且能以四個相同的處理程序取代單一程序執行。若為連線導向的伺服器,則必須額外建置機制才能達成上述功能。

Model Context Protocol 是一種無狀態協定:處理請求所需的所有資訊皆包含在請求本身。伺服器會獨立處理每個請求;不應從先前的請求推斷任何狀態,即使是來自相同連線或串流的請求亦然。

無狀態並不代表伺服器無法儲存任何資料。您的資料庫、佇列與快取依然存在。其意義在於「協定」本身不在連線中攜帶狀態,因此伺服器不得將連線、處理程序或開啟的 socket 視為「該客戶端正處於對話中」的依據。

2026-07-28 版本移除了哪些內容

2026-07-28 是截至 2026 年 8 月的最新規格版本。與 2025-11-25 相比,它移除了五項為了支援 session 而存在的機制。

  • initialize 請求與 notifications/initialized 通知。現在完全沒有交握(handshake)機制(SEP-2575)。
  • Mcp-Session-Id 標頭,以及透過 HTTP DELETE 終止 session 的機制(SEP-2567)。
  • 伺服器用來推送通知的獨立 HTTP GET 串流。該機制已被 subscriptions/listen 取代,現在改為一般的 POST 請求,其回應即為長連線串流。
  • SSE(server-sent events)串流的續傳能力。Last-Event-ID 標頭與每個事件的 ID 已被移除,因此串流中斷時,傳輸中的請求會遺失,客戶端必須以新的請求 ID 重新發起請求。
  • pinglogging/setLevelnotifications/roots/list_changed。日誌層級現在改為每個請求的欄位,即 _meta 中的 io.modelcontextprotocol/logLevel

規格新增了一個方法,且所有伺服器皆必須實作。server/discover 可在單次呼叫中回傳伺服器支援的協定版本、功能與識別資訊。這是目前僅存最接近交握的機制,但客戶端可選擇是否呼叫。

為何會話傳輸在生產環境中難以執行

2025-11-25 及更早的版本中,伺服器可在初始化時產生一個會話 ID,並透過 InitializeResultMcp-Session-Id 標頭中回傳。客戶端隨後必須在每次後續請求中帶上該標頭。協商後的協定版本與客戶端功能均儲存在伺服器的記憶體中,並以該 ID 作為索引鍵。這些設計選擇每一項都帶來了維運成本。

  • 重新啟動會導致會話表遺失。規格要求伺服器必須以 404 Not Found 回應任何帶有失效會話 ID 的請求,並要求客戶端以新的 InitializeRequest 重新開始。這使得每次部署都成為所有已連線客戶端的重新連線事件。
  • 第二個複本無法得知第一個複本的會話。橫向擴展意味著必須在負載平衡器上進行黏性路由(sticky routing),或是使用每個複本在每次請求時都要讀取的共享會話儲存區。
  • 會話表是會隨閒置客戶端增加而成長的記憶體。DELETE 是選用的,而未發送該訊號即關閉的客戶端會留下殘留項目。
  • 列表結果可能因連線而異,因此在伺服器前端進行快取並不安全。

移除會話機制可同時解決上述四個問題。在修改任何設定之前,務必先理解這項變更。

每個請求現在攜帶的內容

每個發送至 MCP 端點的 POST 請求皆為獨立運作。協定版本與客戶端能力會包含在請求主體的 _meta 欄位中,且選定的欄位會鏡像至 HTTP 標頭,以便中介層無需解析 JSON 即可進行路由。

POST /mcp HTTP/1.1
Content-Type: application/json
Accept: application/json, text/event-stream
MCP-Protocol-Version: 2026-07-28
Mcp-Method: tools/call
Mcp-Name: get_weather
Authorization: Bearer <access token>

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "get_weather",
    "arguments": {"location": "Seattle, WA"},
    "_meta": {
      "io.modelcontextprotocol/protocolVersion": "2026-07-28",
      "io.modelcontextprotocol/clientInfo": {"name": "ExampleClient", "version": "1.0.0"},
      "io.modelcontextprotocol/clientCapabilities": {}
    }
  }
}

io.modelcontextprotocol/protocolVersionio.modelcontextprotocol/clientCapabilities 在每個請求中皆為必填。clientInfo 雖非強制,但建議客戶端發送。若請求缺少必填欄位則視為格式錯誤,伺服器必須拒絕該請求,並回傳 JSON-RPC 錯誤 -32602 與 HTTP 400 Bad Request

Mcp-Method 標頭在每個請求中皆為必填。Mcp-Name 則在 tools/callresources/readprompts/get 中為必填。標頭值必須與主體內容一致;若伺服器處理了主體內容但發現不符,必須以 400 Bad Request 及錯誤代碼 -32020HeaderMismatch 拒絕請求。此規則的存在是因為依據標頭進行路由的負載平衡器,與依據主體內容執行任務的伺服器,屬於兩個不同的信任來源。若您依據這些標頭進行路由或速率限制,請務必先檢查 MCP-Protocol-Version:早期的修訂版本並未驗證標頭與主體的一致性,因此在這些版本中,標頭值不可作為信任依據。

版本不一致現在被視為一般的單次請求錯誤,而非交握失敗。若伺服器未實作請求的版本,應回應 400 Bad Request 並附帶錯誤 -32022UnsupportedProtocolVersion,並在 data.supported 中列出其支援的版本。客戶端應從該列表中選擇一個版本並重試。

狀態的去向:權杖、游標與訂閱

狀態並未消失,而是移至可見且可記錄的位置。

憑證會隨每個請求傳遞。 由於沒有可供附加身分的 Session,存取權杖(access token)會隨每次 HTTP 呼叫傳送並進行驗證。詳細資訊請見下方的身分驗證章節。

游標必須攜帶自身位置。 tools/listresources/listprompts/listresources/templates/list 的分頁功能使用不透明的游標字串,客戶端不得解析或修改該字串。在單一處理程序的伺服器上,將偏移量(offset)儲存在記憶體並以 Session 為鍵值是常見做法。但在無 Session 的架構下,游標必須包含足夠資訊,讓任何複本(replica)都能恢復列表作業,因此需將位置編碼於游標內並進行簽章,或將其儲存在所有複本共用的儲存空間中。無效的游標應回傳 -32602。請務必進行簽章,因為不透明游標仍屬於客戶端提供的輸入,您的程式碼會對其進行解碼並予以信任。

訂閱屬於請求,而非連線。 若客戶端需要變更通知,需發送 subscriptions/listen 並附上篩選條件,指定所需的類型:toolsListChangedpromptsListChangedresourcesListChangedresourceSubscriptions。伺服器會以 notifications/subscriptions/acknowledged 回應並保持該回應串流開啟。若串流中斷,伺服器不會保留任何狀態,客戶端需重新發送 subscriptions/listen 以恢復訂閱。

跨呼叫的應用程式狀態轉變為明確的控制代碼(handle)。 當伺服器確實需要在呼叫之間記憶資訊時,規格的解決方案是使用伺服器產生的識別碼,並將其作為一般的工具參數傳回。該識別碼會出現在工具架構(tool schema)中,可被記錄,且絕不會隱含於連線中。若伺服器後端存有真實的用戶資料(例如 自架的 MCP 電子郵件伺服器),則會使用此模式取代 Session:信箱或草稿識別碼作為工具參數,確保任何複本都能接續處理下一次呼叫。許多工具根本不需要控制代碼:由您自架 SearXNG 實例支援的搜尋工具 僅接收查詢並回傳結果,無需為下一次呼叫恢復任何狀態,也不需在意是由哪一個複本進行回應。

Deployment: reverse proxy, timeouts, health checks

The MCP endpoint is one path that accepts POST. Most traffic is a short request and a JSON response, which any proxy handles. The exception is the streaming response, where proxy defaults work against you. This is the part that changes when you move from a laptop demo to an MCP server running on a VPS.

location /mcp {
    proxy_pass http://127.0.0.1:8080;
    proxy_http_version 1.1;
    proxy_set_header Connection "";
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_buffering off;
    proxy_read_timeout 1h;
    proxy_send_timeout 1h;
}

proxy_buffering off matters because nginx buffers proxied responses by default, which holds SSE events until a buffer fills or the response ends. The specification also asks servers to send X-Accel-Buffering: no on SSE responses, and nginx honours that header, so a correct server tells your proxy the right thing on its own. Set the directive too, because that is the half you control.

proxy_read_timeout defaults to 60 seconds. A subscriptions/listen stream that sits quiet for longer than that is closed by nginx, not by your server, so your logs show a healthy process and your client shows a dropped stream. Raise it on the MCP location only, not on the whole server. Servers are also encouraged to send an SSE comment line (a line beginning with a colon) as a keep-alive during quiet periods, which stops intermediaries from timing the stream out at all.

Caddy needs less. It buffers partially by default for wire efficiency and flushes immediately when the response carries Content-Type: text/event-stream, so streaming works without extra directives.

mcp.example.com {
	reverse_proxy 127.0.0.1:8080 {
		health_uri /healthz
		health_interval 10s
	}
}

Note what that health check points at. Do not aim an active check at the MCP endpoint with GET, because a server implementing only this revision answers 405 Method Not Allowed to GET and DELETE, and Caddy's default health method is GET. The proxy would then mark a perfectly healthy backend as down. Serve a plain path such as /healthz for the proxy, and check the protocol separately with a POST.

curl -sS https://mcp.example.com/mcp \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json, text/event-stream' \
  -H 'MCP-Protocol-Version: 2026-07-28' \
  -H 'Mcp-Method: server/discover' \
  -d '{"jsonrpc":"2.0","id":"health-1","method":"server/discover","params":{"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientCapabilities":{}}}}'

A 200 carrying a supportedVersions list means the process is up and speaking the protocol. A 404 with JSON-RPC error -32601 means the process is up but does not serve server/discover, which every 2026-07-28 server must implement. A 400 with -32022 means your checker asked for a version this build does not support, which is exactly what you want to catch after a dependency upgrade. Open source nginx has no active health checks, so use passive max_fails and fail_timeout on the upstream and run the protocol check from your monitoring instead.

A rolling restart now costs you the requests in flight and nothing else. Drain, let open POSTs finish, start the new process, and clients re-issue whatever failed. The one thing you still drop is any open subscriptions/listen stream, because that stream is a live connection to one specific process. Statelessness removed session affinity. It did not remove connection affinity for a stream that is open right now, and no routing rule fixes that. A client can tell the difference: a stream that ends with the empty subscriptions/listen result closed gracefully, and a stream that ends without one dropped, which the client may treat as a reason to reconnect.

Caching becomes possible for the first time. Results from the list methods now carry ttlMs and cacheScope, and cacheScope: "public" tells shared intermediaries they may cache the response. That is only safe because list results no longer vary per connection, which is a direct consequence of removing sessions.

為何在沒有 session 的情況下驗證方式會改變

在使用 session 時,我們很容易在 initialize 進行一次驗證,隨後將 session ID 視為後續所有操作的憑證。以這種方式使用的 session ID 是一種不具備受眾限制(audience)、過期時間或撤銷機制的持有者憑證(bearer credential),且由您自己的伺服器所核發。移除 session 後,這種捷徑便不復存在,取而代之的是更嚴格的機制。

受保護的 MCP server 會扮演 OAuth 2.1 資源伺服器的角色。客戶端發出的每一筆 HTTP 請求都必須攜帶 Authorization: Bearer <access token>,且伺服器會在每次請求時驗證該 token。驗證過程包含受眾檢查:根據 RFC 8707(OAuth 2.0 的資源指示器),伺服器必須確認該 token 是專門為其核發的,且不得接受或轉發用於其他用途的 token。客戶端會透過發送 resource 參數並指定伺服器的標準 URI 來請求正確的受眾。

探索(Discovery)機制是基於挑戰(challenge)運作的。當請求抵達且未攜帶可用的 token 時,伺服器會回應 401 Unauthorized

HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer resource_metadata="https://mcp.example.com/.well-known/oauth-protected-resource",
                         scope="files:read"

客戶端讀取 resource_metadata,取得該文件(RFC 9728,OAuth 2.0 受保護資源元數據,MCP server 必須實作此規範),找到授權伺服器並執行流程。若有效的 token 權限不足,則會收到 403 Forbidden,其中包含 error="insufficient_scope" 以及該操作所需的範圍(scopes)。

這對您的執行方式產生了兩個影響。首先,token 驗證現在發生在每次請求,而非每個 session 一次,因此若每次呼叫都對內省端點(introspection endpoint)進行網路往返,將會反映在延遲上:建議優先使用可透過簽章、受眾與過期時間進行本地驗證的 token,或是以 token 為鍵值,在短時間內快取驗證結果。其次,由於沒有儲存身分的 session,授權必須在每次呼叫時根據 token 計算得出。這比 session 模型更為嚴謹,且符合將憑證排除在代理程式(agent)處理程序之外的廣泛實務,相關內容請參閱 將機密排除在 AI 代理之外

關於此修訂版本的正確與錯誤認知

上述內容僅描述 2026-07-28 修訂版本。它不適用於 MCP 的所有版本,也不適用於您去年部署的伺服器。

2025-11-25 及更早版本的客戶端與伺服器仍使用交握模型。規格書將這些修訂版本稱為舊版(legacy),並將支援單次請求元資料(per-request-metadata)的修訂版本稱為現代版(modern)。若伺服器僅支援此修訂版本,在遇到舊版客戶端時,應於 MCP 端點回應 405 Method Not AllowedGETDELETE;忽略任何 Mcp-Session-Id 標頭且不進行產生或回傳;並忽略 Last-Event-ID,因為串流不可恢復。雙時代伺服器可在同一端點同時提供服務:帶有現代 _meta 的請求將以無狀態方式處理,而 initialize 請求則會選用舊版的連線語意。

因此,在採信上述內容前,請先檢查修訂版本字串。若您的 SDK 仍發送 initialize,則連線對您的部署而言仍具實質意義,且上述與連線相關的問題仍需由您自行管理。客戶端亦同:若您在本地機器上執行代理程式(例如 在 VPS 上執行編碼代理程式 中的設定),唯有在其使用的函式庫支援現代修訂版本時,該代理程式才具備此處所述的無狀態特性。請讀取您的執行環境所協商的版本,接著閱讀對應的規格書修訂版本,並將本頁面視為僅描述單一特定修訂版本,而非通用的協定說明。

FAQ

無狀態 MCP 伺服器是否代表我無法儲存任何資料?

不是。無狀態描述的是協定,而非您的應用程式。資料庫、佇列與快取的使用方式與以往完全相同。改變的是,跨越多個呼叫的狀態必須透過用戶端在每次請求中傳遞的明確識別碼來參照,例如工具參數中由伺服器產生的控制代碼(handle)。您不能做的是從連線中推斷上下文:規格說明伺服器不得依賴同一連線上的先前請求來建立功能、協定版本或用戶端身分,因為每個請求都會在 _meta 中提供這些資訊。

我還需要在負載平衡器上設定 sticky sessions 嗎?

一般的請求不需要。根據 2026-07-28 修訂版,每個 POST 都會攜帶自己的協定版本、功能與憑證,因此任何複本都能回應任何請求,使用輪詢(round-robin)即可。唯一長效存在的是 subscriptions/listen 回應串流,這是一個連線至單一處理序的開放連線。當該處理序結束時,連線即終止,用戶端會重新發送 subscriptions/listen 以重新建立連線。這是連線生命週期而非工作階段親和性(session affinity),任何路由規則都無法避免此情況。

Mcp-Session-Id 和 HTTP GET 串流怎麼了?

兩者皆已在 2026-07-28 修訂版中移除(根據 SEP-2567 與 SEP-2575)。僅實作此修訂版的伺服器應對 MCP 端點上的 405 Method Not AllowedGETDELETE 做出回應,並應忽略 Mcp-Session-Id 標頭,而非將其回傳。伺服器發起的變更通知現在會透過 subscriptions/listen 請求的回應串流傳輸,而非透過獨立的 GET 串流。必須繼續服務舊版用戶端的伺服器,應同時實作舊版行為與此版本行為。

如何在沒有交握(handshake)的情況下對 MCP 伺服器進行健康檢查?

使用兩個層級。將代理伺服器的活動檢查指向您應用程式提供的普通 HTTP 路徑,因為對 MCP 端點發送 GET 會正確回傳 405,這會導致健康的後端被標記為離線。接著,透過 POST server/discover 來檢查協定本身(每個 2026-07-28 伺服器都必須實作此功能),並確認回覆為 HTTP 200 且列出了您的用戶端所使用的協定版本。若收到 404 且包含 JSON-RPC 錯誤 -32601,代表處理序正在執行但未提供該方法;若收到 400 且包含 -32022,則代表該組建不支援您要求的版本。