2026-07-28 這天的 MCP 規格拿掉了 initialize/initialized handshake 與
Mcp-Session-Id header——這兩個曾經逼著每個 MCP gateway 團隊寫 sticky routing、
養 Redis session store 的協定核心,被整組刪掉了。
MCP 把 handshake 拿掉之後
MCP 規格從 2025-11-25 的版本走到 2026-07-28 的
release candidate,Google 團隊在官方部落格裡把這次改動說得很直白:「The handshake is gone.」
initialize/initialized handshake(SEP-2575)與
Mcp-Session-Id header(SEP-2567)「have been removed entirely」。
舊模型底下,要橫向擴展的 remote MCP server 部署都得處理
sticky session affinity、Redis session store、或是 gateway 層的封包檢查;
新模型把這整層基礎設施需求直接拿掉,換成每個 request 自己攜帶身分資訊。
要理解這個代價值不值得,得把兩種模型攤開來看。
執筆這篇部落格的是設計 MCP 傳輸層的 Google 團隊本身,文中直接寫 「we helped design SEP-2243」,等於規格作者親自解釋自己主刀的手術。要弄清楚拿掉 session 這件事划不划算,得先把兩套模型的每個維度攤開來看。
這個版本已經在生產環境裡跑著:Go SDK 團隊在規格發布當天就把 v1.7.0
放出來,GitHub MCP Server 也已經照著新規格把 Redis session storage 拿掉。
對還在用舊模型的團隊來說,這已經是有生產系統在跑新模型的既成事實。
click column header to sort · 3 columns × 7 rows
| 維度 | 舊 stateful 模型 | 新 stateless 模型 |
|---|---|---|
| 連線建立 | initialize/initialized handshake + Mcp-Session-Id |
每個 request 自帶 _meta(protocolVersion/clientInfo/clientCapabilities) |
| 路由需求 | sticky session affinity(LB 層級) | Mcp-Protocol-Version/Mcp-Method/Mcp-Name header,不用拆封包 |
| pod 重新啟動 | session state 立即遺失 | 沒有 session 可遺失 |
| 常見錯誤 | 400 Session Not Found |
不適用——沒有 session 這個概念 |
| 多輪對話 | 連線需 hold 住等使用者輸入 | InputRequiredResult + requestState,任何 server instance 都能續接 |
| 長工作 | 阻塞等待或自建輪詢機制 | 立即回 taskId,用 tasks/get/tasks/update 輪詢 |
| 常駐基礎設施 | 需要:Redis session store 或 gateway packet inspection | 不需要:ttlMs/cacheScope 取代監控用的長連線 SSE |
路由與狀態:session 消失之後,request 落在哪個 pod
舊模型的問題,官方部落格寫得很直接:「Standard round-robin load balancers do not know which
container holds which in-memory session.」round-robin 不認得哪台 pod 手上握著哪個 session,
於是團隊只能退回「sticky session affinity rules at the load balancer level, which prevents
even distribution of traffic」——犧牲負載平衡去換 session 黏著。
更麻煩的是 pod 本身不穩:「If a pod restarts or crashes, the session state is instantly
lost」,重新啟動一次,session 就沒了,前端看到的結果是一顆
400 Session Not Found。撐住這套機制,團隊得養「shared Redis session stores
or complex gateway-level packet inspection」——多一層儲存、多一層要維運的基礎設施。
「gateway-level packet inspection」這個做法的成本更隱性:gateway 得認得 MCP 的 wire format,才有辦法從封包裡榨出 session id,這代表 gateway 這一層本身 也綁死了協定版本——規格一改,gateway 的封包解析邏輯就得跟著改。
這個問題有個乾淨的數學形狀:假設 gateway 前面掛了 N 台 pod,round-robin
隨機分配下一個 request,命中原本持有該 session 那台 pod 的機率是 1/N,
也就是說打到「不是那台」、觸發 400 Session Not Found 的機率是 (N-1)/N。
拖動下面的滑桿改變 pod 數量,可以看到這條曲線怎麼隨著擴容爬升——而新的
stateless 模型底下,因為根本沒有 session 這個東西可以丟,這條線永遠貼著 0。這條
曲線只描述 round-robin 隨機路由這一種情境;實際部署若疊加了 sticky routing,
命中率會被人為拉高,但那正是舊模型得額外花錢維持的東西。
這條曲線解釋了為什麼舊模型在小規模部署時感覺不出問題——N 小的時候命中原 pod 的機率還不算低——
但只要團隊開始用 autoscaling 把 pod 數量往上拉,落錯 pod 的機率會很快逼近 100%,
400 Session Not Found 從邊緣情境變成常態。新模型讓「打到哪台 pod」這件事
本身不再重要——因為判斷身分所需的資訊,request 自己就帶著。widget 預設的 20 台 pod,
對照公式已經是 95% 的落錯機率;新模型底下同樣 20 台 pod,落錯機率是 0%。
曲線兩端的數字更清楚:N=2 時機率是 50%,N=50 時已經逼近 98%。小規模部署感覺不出 問題,是因為 N 還沒推上 autoscaling 會用到的規模;一旦 pod 數量隨流量高峰衝上 兩位數,落錯 pod 就從偶發變成常態。
這條曲線在真實環境裡的觸發點通常是 autoscaling:流量一高, orchestrator 就會加開新 pod;流量一降,又會把多餘的 pod 收掉。收掉的那一刻, 「If a pod restarts or crashes, the session state is instantly lost」直接應驗—— 原本握著某個 session 的 pod 消失,那個 session 就跟著消失,不管使用者當下 是不是正在等一個回應。
這篇部落格的標題是「Scaling AI Agent Infrastructure with the MCP Stateless
updates」——對照這幾個問題就看得出為什麼作者選這個框架:AI agent 常常要連續打很多次工具呼叫才能完成一個任務,
一旦中間有一次撞上 400 Session Not Found,整條任務鏈要嘛重新握手,
要嘛整段失敗。session 模型底下,這個風險隨著 pod 數量的成長而成長,剛好卡在
「agent 基礎設施要規模化」這個時間點上。
這也是為什麼「拿掉 handshake」聽起來像是簡化,實際上是把一整類基礎設施決策從 「要不要上 sticky routing、要不要養 Redis、gateway 要不要做深度封包檢查」, 直接變成不存在的問題。舊模型底下,團隊得在這三個選項裡挑一個、承擔各自的維運代價; 新模型讓這道選擇題本身直接消失。
自報家門:_meta 與三個新 header 取代 session
拿掉 session 之後,協定得靠別的方式讓每個 request 自己說清楚「我是誰、我要什麼版本、
我具備什麼能力」。新模型把這些資訊塞進每個 request 的 _meta 欄位:
io.modelcontextprotocol/protocolVersion(值就是規格版本字串,例如
2026-07-28)、io.modelcontextprotocol/clientCapabilities、
io.modelcontextprotocol/clientInfo。body 不再是「已經跟 server 對過話」
之後才有意義的訊息,而是每一次都自帶完整上下文。
但 body 是要拆開才讀得到的東西,對 L7 路由器來說成本不低。這也是 SEP-2243
(HTTP Standardization)要解決的問題——這篇部落格的作者提到自己「helped design
SEP-2243」,把三個關鍵欄位搬到標準 HTTP header 層級:Mcp-Protocol-Version、
Mcp-Method(例如 tools/call)、Mcp-Name。
路由器讀 header 就能決定往哪裡送,不必解 body、不必認得 session。
SEP-2243 掛在 Transports Working Group 底下,這也解釋了它處理的問題層級:
判斷「這個 request 該送到哪裡」,是傳輸層該管的事,不是應用層該管的事——這也是為什麼三個
新 header 都用 Mcp- 前綴,直接掛在 HTTP 標準欄位旁邊。
這件事對 MCP server 本身以外的角色也有影響。任何站在 request 路徑上的中介層—— L7 負載平衡器、WAF、或是做流量觀測的 middleware——只要讀三個固定位置的 header 就能做路由或紀錄決策,不需要理解 MCP 的 JSON body 格式,也不用反序列化整包 payload 才能知道「這是什麼類型的請求」。
hover or tap each header name for what it carries · 3 headers
Mcp-Protocol-Version
規格版本字串,跟 _meta 裡的 protocolVersion 是同一個值,例如 2026-07-28。
、
Mcp-Method
請求類型,例如 tools/call——路由器讀這個就知道往哪類 handler 送,不用拆 body。
、
Mcp-Name
更細一層,標出實際要呼叫的 tool 或 resource 名稱。
三個 header 合起來,讓 L7 路由完全不需要認得任何 session。
把「連線建立」這件事的兩種版本並排放在一起看,差異會更直接。拖動下面的分隔線,
左邊是舊模型的 handshake 序列,右邊是新模型的單次 self-describing request——
左邊照 handshake 定義(initialize → initialized →
tools/call)推算,要三次往返才能發出第一個真正的工具呼叫;右邊第一個
request 就是工具呼叫本身。
這一層 header 化的設計還帶來一個附帶效果:SEP-2549 引進的快取欄位,要解決的是同一種
浪費——原文說得直接,tool 與 resource 的回傳結果現在可以帶「a ttlMs
(Time-to-Live in milliseconds) and a cacheScope」,用來取代只是為了
監控 tool 或 prompt list 有沒有變而開的長連線 SSE。顧名思義,ttlMs
是這個結果還能被快取多久(毫秒),cacheScope 是這個快取的生效
範圍——client 讀這兩個欄位就能自己判斷要不要重新打一次,不必為了監控變化另外
開一條連線。
header 化跟 _meta 化這兩件事合起來,把 MCP 的協定語意從
「需要事先握手才看得懂」改成「每個 request 單獨看都是完整的」。
這裡有一段推論,原文沒有明講,得誠實寫出來:舊模型的 handshake 只在
initialize/initialized 那幾次往返交換 protocolVersion、
clientInfo、clientCapabilities,照這個握手設計推算,之後的 request 應該不必重複帶;
新模型則明確把這些資訊搬進每個 request 的 _meta,代表每一個 request
都要重新帶一次同樣的欄位。這個代價——換來不挑 pod 的自由,付出的是每個 request
body 都變重一點,成本從「維運基礎設施」搬到「每次 request 多帶幾個欄位」——是本文
根據兩個模型的欄位設計推算出來的比較,原文並未討論 payload 大小或新模型的任何成本。
多輪對話與長工作:無狀態怎麼做非同步
session 拿掉之後,剩下真正棘手的是那些天生需要「等一下」的操作:server 要問使用者一個問題才能繼續,或是一個工具呼叫本來就要跑很久。舊模型的直覺解法是把連線 hold 住等答案,但這正是 sticky routing 存在的理由——連線握在哪台 pod 手上,答案就只能回那台。
SEP-2322(Multi Round-Trip Requests)把這個問題拆解掉的方式,官方形容是「restructuring
the interaction lifecycle into self-contained steps」。server 需要使用者輸入時,
直接回一個 InputRequiredResult,裡面帶著序列化過的
requestState。使用者答完之後,client 用 inputResponses
加上原封不動的 requestState 重新發一個 request——官方原文寫得很直接:
「Because the requestState contains everything needed to resume the
task, any server instance behind your load balancer can pick up the retry
request!」這個 request 落到哪台 pod 都無所謂,不需要哪台機器記得上一輪發生過什麼。
這段被原封不動送回的 requestState,順帶解決了另一個問題:伺服器
不需要另外開一張表去記錄「這個 client 現在問到第幾輪」,state 本身就是驗證用的
憑證——收到 request 時,只要能解出 requestState,就代表這是同一輪
對話的延續,不需要查任何伺服器端的紀錄。這跟 SEP-2243 的 header 化是同一個原則:
request 本身帶得動它需要的全部資訊,就不必挑 pod。
真正跑得久的工作走另一條路:Tasks Extension(SEP-2663)「graduates from an
experimental feature to a robust, first-class protocol extension」。長工作不再讓
client 卡著等,server 立刻回一個 taskId,工作在背景跑,client
改用 tasks/get 與 tasks/update 輪詢進度。輪詢本身也是無狀態
request,一樣不挑 pod。這兩個 SEP 合起來的效果是:「等待」這個動作從「佔住一條連線」
變成「client 自己決定什麼時候再問一次」,伺服器端不需要為了等待這件事保留任何專屬資源。
這一整個機制的名稱本身就寫得很白:Tasks Extension 的章節標題是 「Async Work Without Blocking」。blocking 在這裡指的不只是使用者體驗上的等待, 也包括系統資源上的佔用——一個被 hold 住的連線,背後往往牽動一個 worker 或一個事件迴圈的 callback 佇列。
這個設計把「等待」這件事的所有權從伺服器搬到了用戶端手上。舊模型裡,一個長工作
要嘛把連線握住,要嘛得自己刻一套輪詢機制,兩者都預設「發起工作的那個 request,
跟後續查詢工作狀態的 request,必須打到同一台伺服器」。新模型下,taskId
是唯一需要保留的狀態,而它活在 client 手上,不活在任何一台 pod 的記憶體裡。這跟
前面拿掉的 Mcp-Session-Id 是同一個模式的另一次應用:狀態不再留在
某台 pod 的記憶體裡,而是整包放進 request 或 taskId 這種輕量識別碼裡。
代價:12 個月棄用窗與遷移路徑
這次改版有代價。SEP-2577 是 MCP 第一次有正式的棄用政策——官方部落格寫「For the first time, MCP now has a formal deprecation policy」——功能會走過「a structured Active -> Deprecated -> Removed lifecycle with a minimum 12-month transition window」。隨著 2026-07-28 這個版本,三個功能直接進入 deprecated:roots、sampling、 logging,官方原文是「Three features enter deprecation today」。這代表現在還在用 roots/sampling/logging 的整合,時鐘已經開始走。
把規格演進的時間軸與遷移可用的時間點放在一起看更清楚——拖動下面的滑桿, 可以看到從舊規格生效到 12 個月棄用窗滿的整段路徑,以及各語言 SDK 分別在哪個時間點可用。
各語言的 SDK 升級路徑,2026-07-28 當天就把大部分都攤開了。Python 這邊裝
mcp[cli]==2.0.0b1 就能用新版;TypeScript 把套件拆成
@modelcontextprotocol/server@beta 與 @modelcontextprotocol/client@beta
兩包,並提供自動 codemod:npx @modelcontextprotocol/codemod@beta v1-to-v2 .
把現有專案的呼叫方式改寫成新版;維護 Go MCP SDK 的 Google 團隊則是把
v1.7.0 直接在 7 月 28 日當天發出,同一天規格與一個主要語言的 SDK
一起發布。實例層面,GitHub MCP Server 已經升級到新規格,
「completely removed Redis session storage, eliminating database writes and
reads on every single call」——拿掉了每一次呼叫都要多付一次的資料庫讀寫成本。
官方特別用「Major production servers, such as the GitHub MCP Server」這樣的 措辭——「例如」兩個字暗示 GitHub MCP Server 不是唯一一個,只是被挑出來當範例 的那一個。這代表拿掉 Redis session storage 這件事,是已經有不只一個生產系統 做過的既定路徑。
官方直接提供 codemod 這件事本身透露了一個訊息:這是需要自動化改寫既有呼叫方式的
breaking change——codemod 指令裡的 v1-to-v2 命名,說明這是一次
major version 等級的躍遷。TypeScript 這邊乾脆把套件拆成 server 與
client 兩包分開發版,意味著兩端可以各自升級,不必綁死同一個版本號;Go SDK 選擇在
規格發布當天就把 v1.7.0 放出來,對比 Python 先放出 beta 版
2.0.0b1,兩種節奏都算合理,差別只在於團隊願不願意讓早期採用者先去試錯。
三個 SDK 的版本號本身就是一個訊號:Go 直接跳到 v1.7.0 這種穩定版號,
Python 停在 b1,TypeScript 兩個套件都掛 @beta。同一天
發布的規格,底下的 SDK 成熟度並不整齊——這對「現在要不要跳」這個決定本身就是
一項輸入:挑 Go 的團隊今天就能上生產,挑 Python 或 TypeScript 的團隊等於在
beta 版上先賭一把。
這裡有一個容易被忽略的細節:官方文中用的字是「release candidate」,不是 「正式版」。規格文字理論上還可能有最後一輪修訂,但 Go SDK 已經對著它出了穩定 版號,GitHub MCP Server 也已經照著它上線——對實務團隊來說,「等正式版」跟 「現在就開始準備」之間的落差,可能比字面上的 RC 標籤還要小。
現在要不要跳
把前面七個維度攤開來看,真正的判斷其實只落在兩個問題上:部署會不會遇到 pod 重新啟動就掉對話的情況,以及整合有沒有用到 roots、sampling、logging 這三個 已進入 deprecated 的功能。前者決定要不要現在排遷移,後者決定遷移的時程有多急。
這次改版真正的分界線不是「規格好不好」,是「你的部署形狀吃不吃 session 這個包袱」。 如果 MCP server 跑在會自動擴縮的 pod 群裡,或是已經在為 Redis session store、 gateway packet inspection 這類基礎設施買單,前面那條落錯 pod 機率曲線就是真實成本—— pod 數量越多,代價越明顯,這個版本值得現在排進遷移計畫。 如果只是單機或固定小規模部署,沒有 sticky routing 的痛,SEP-2577 給的 12 個月窗口 足夠緩著等下一個穩定版 SDK 出來再動手;但隨著 2026-07-28 這個版本,roots、sampling、 logging 這三個功能已經進入 deprecated,拖到窗口快關才動,遷移只會比現在急。
前面提過的 metadata 重複成本,也要放進這筆帳:拿掉 session 基礎設施省下的錢, 得跟每個 request 變重一點的頻寬成本放在天平兩端一起衡量。
如果你的整合正好用到 roots、sampling 或 logging 這三個功能,不管部署規模大小, 都值得現在就去讀 SEP-2577 的細節——原文寫的是「a minimum 12-month transition window」,12 個月只是最短保證。
這個決定不需要複雜的容量規劃就能下——問自己一句話就夠:你的 MCP server 有沒有「pod 重新啟動一次就可能弄丟使用者當下對話」這種情境?有,就是該遷移的 訊號;沒有,SEP-2577 的窗口就是拿來緩衝的,不用搶頭香。前面的曲線給了一個更具體的 自我檢測方式:把你部署的 pod 數量代進 (N-1)/N,算出來的百分比越高,遷移的急迫性 就越高。
The call:跑在 autoscaling pod 群或已經在養 Redis session store 的團隊,現在就該排新規格的升級;單機小型部署可以緩,但別緩過 SEP-2577 那 12 個月的棄用窗。