議新版解讀:無狀態(tài)架構遷移與舊教程淘汰指南)
MCP 最近這波重寫動靜比很多人想象中大。過去你看到的大多數(shù)教程還在教你建立 Session、維護會話狀態(tài)、通過 Sampling 讓服務端反向調(diào)用客戶端的大模型生成內(nèi)容可上個月新規(guī)范一出來Session 被移出協(xié)議核心Sampling 被明確廢棄。我最早是在公司內(nèi)部筆記本上發(fā)現(xiàn)的——按老教程寫的 demo 突然連不上翻 spec 才發(fā)現(xiàn)不是 bug是規(guī)范變了。今天這篇不是新聞復述而是把我改代碼、遷移服務、踩坑的過程拆開給你看你手里的舊教程哪些還能用、哪些必須扔讀完就知道了。1. 這次改版不是小修小補是把協(xié)議“扁平化”了先別急著罵。MCP 從誕生到現(xiàn)在一直處于快速演進期2024 年底的 snapshot、2025 年早期的版本和現(xiàn)在的規(guī)范相比已經(jīng)像兩代協(xié)議。很多開發(fā)者看到Session 沒了、Sampling 廢了第一反應是功能縮水但如果你把新舊規(guī)范并排看會發(fā)現(xiàn)這其實是一次目標明確的架構收斂把服務端有狀態(tài)的長連接協(xié)議改造成客戶端無狀態(tài)的短請求協(xié)議。1.1 Session 被砍掉的真正原因老版本里Session 是協(xié)議的一等公民??蛻舳送ㄟ^ initialize 握手建立會話服務端保存會話狀態(tài)包括 roots 列表、資源訂閱關系、能力協(xié)商結果甚至還有 session id后面的請求都要帶著這個 id 走。這套模型在桌面端單用戶場景下沒問題但一旦上生產(chǎn)就非常難受。首先是水平擴展的問題。Session 意味著服務端的每個實例都要維護內(nèi)存狀態(tài)負載均衡器必須開啟 sticky session否則請求打到另一個實例就找不到上下文。其次是長連接的心跳和超時處理HTTP 場景下跨網(wǎng)關、代理非常容易斷斷一個 session 就要重新初始化客戶端和服務端都要寫一堆重連邏輯。最后是安全和權限模型老協(xié)議的 session 一旦建立狀態(tài)就一直有效權限變更不能實時生效。新版直接把 Session 從核心規(guī)范里摘出去改成每次請求獨立、狀態(tài)自己帶或存服務端外部存儲的模型。initialize 仍然保留但它的作用不再是建立長期會話而是做一次單向的能力協(xié)商和版本確認。之后每個 JSON-RPC 請求都是自包含的服務端不需要記住你是誰來決定怎么回復而是根據(jù)請求里攜帶的憑證和上下文直接響應。這個改動過后服務端可以隨便橫向擴容網(wǎng)關也不需要做會話粘滯架構上輕了一大截。1.2 Sampling 為什么成為棄子Sampling 在舊規(guī)范里是少數(shù)讓我一直覺得不舒服的設計。它的本意很好讓服務端在需要調(diào)模型時通過協(xié)議請求客戶端幫忙調(diào)用本地的模型避免服務端直接持有模型 API key。但在實際使用中它帶來的問題遠大于價值。從安全角度說Sampling 是一個天然的濫用面。服務端發(fā)起 createMessage 請求客戶端必須幫它執(zhí)行一次模型推理那 tokens 算誰的用戶會為服務端的每次請求悄悄買單。更嚴重的是如果服務端被惡意控制它可以構造大量的模型請求來耗光客戶端額度甚至通過精心構造的 prompt 對模型做注入。從架構職責說Sampling 也讓邊界變模糊。一個服務端本來應該返回結構化數(shù)據(jù)和工具執(zhí)行結果卻反過來去指導客戶端如何生成自然語言這種服務端越權在現(xiàn)代 LLM 應用里會導致調(diào)試困難——服務端代碼在跑但真正消耗的模型調(diào)用卻發(fā)生在用戶本地客戶端里日志不完整成本不可控。新版把 Sampling 從協(xié)議能力里移除邏輯就清爽了服務端把數(shù)據(jù)準備好呈現(xiàn)和解釋是客戶端模型的工作。你需要摘要、轉化、渲染那就在客戶端處理服務端需要模型能力那就通過工具把數(shù)據(jù)交給外部模型服務而不是回手跟客戶端要。這只是一種職責重新分配不是能力后退。2. 舊版 vs 新版一張表看懂核心差異與其看長篇 breaking changes 說明不如直接對照新舊兩版的協(xié)議行為。我把自己遷移過程中最關注的幾個點整理成了下面的表方便你有事沒事拿出來對照。對比維度舊版Session Sampling 時代新版無狀態(tài)、無 Sampling連接生命周期initialize 建立長期 Session之后復用initialize 只做能力協(xié)商請求之間無綁定關系會話標識服務端分配 session id后續(xù)請求攜帶無 session id靠請求自身憑證token、簽名識別服務端狀態(tài)內(nèi)存里保存 roots、訂閱關系、能力結果不保存協(xié)議級狀態(tài)狀態(tài)放數(shù)據(jù)庫/緩存或由客戶端傳入資源訂閱服務端通過 Session 推送資源變更無長連接推送改為客戶端拉取或服務端主動回調(diào)接口Sampling服務端通過 sampling/createMessage 反向請求客戶端推理移除該能力服務端只返回結構化數(shù)據(jù)模型調(diào)用由客戶端側完成錯誤處理會話級錯誤如 session terminated請求級錯誤一次請求的失敗不影響下一個請求鑒權方式依賴會話內(nèi)共享的上下文每次請求攜帶 token、JWT 或元數(shù)據(jù)標準 HTTP 鑒權模式流式輸出依賴長連接 SSE 持續(xù)推送單次請求內(nèi)部仍然支持流式響應但不是持久通道這張表的信息量其實很大。最核心的是服務端狀態(tài)那一行——新規(guī)范不是讓你不要有狀態(tài)而是狀態(tài)不能再藏在協(xié)議會話里。你原來的狀態(tài)得換地方放比如放 Redis、放數(shù)據(jù)庫或者干脆讓客戶端在每次請求里把必要上下文傳過來。2.1 連接模型的變化對工具的影響MCP 的三大核心能力是 Tools、Resources、Prompts。這次改版里Tools 受影響最小因為工具調(diào)用天然就是你給參數(shù)、我返回結果的請求響應模型跟有沒有 Session 不沖突。Resources 受影響最大老規(guī)范里資源訂閱靠 Session 推送更新現(xiàn)在沒有了長期會話服務端無法主動往客戶端推資源變更只能改成客戶端定時拉取或者服務端在外部回調(diào)里通知客戶端刷新。Prompts 本身是模板不涉及狀態(tài)基本只是能力聲明里的格式微調(diào)。如果你正在寫的是純工具類 MCP server那遷移成本其實很低如果你的 server 依賴資源訂閱推送就要重新設計數(shù)據(jù)同步機制常見做法是給客戶端暴露一個 refresh 方法或者用消息隊列驅動的 webhook 讓客戶端知道什么時候該重新拉。2.2 能力聲明的變化舊版 initialize 返回的 serverCapabilities 里有 tools、resources、prompts、sampling、roots 這幾項能力聲明新版里 sampling 字段被移除roots 的語義也從會話級根目錄授權變成請求級根目錄提示。客戶端在握手后要檢查新版能力字段老代碼里capabilities.sampling會直接返回 undefined不報錯但邏輯會不知不覺失效。我建議你在收到服務端能力聲明后加一段顯式校驗把不認識的 capability 字段打 log而不是默默忽略。遷移期最怕這種不報錯但不干活的隱性故障。3. 實操遷移把你的 MCP server 從舊版搬到新版說再多概念不如直接改代碼。下面這部分是我把自己一個舊版 sqlite 查詢 server 遷移到新版的全過程按步驟拆開你可以照著對照你自己的項目。3.1 先檢查你的 SDK 版本卡住的第一步永遠是版本。老教程里寫的是pip install mcp或npm install modelcontextprotocol/sdk但包名一樣版本天差地別。新版 SDK 發(fā)布后舊版本并不會自動升級你的 lockfile 可能還鎖著老版本。檢查你自己項目里實際用的 SDK 版本比看教程簡單得多。Python 看import mcp后mcp.__version__TypeScript 看package.json里modelcontextprotocol/sdk的版本號。如果版本號早于新版規(guī)范發(fā)布的里程碑直接升級。升級后第一件事就是把服務端能力和客戶端能力打印出來看有沒有你預期的字段。我的習慣是在配置文件里加一個顯式的min_api_version字段服務端和客戶端在握手后互相比較版本不滿足就直接拒絕啟動而不是等調(diào)用某個功能時才炸出來。3.2 移除 Session 依賴的三種寫法改法下面是用 Python 演示遷移思路具體 API 以你使用的新版 SDK 為準。舊版里常見寫法是這樣from mcp.server import Server server Server(legacy-demo) server.session_opened async def on_session(session): session.state[user_id] parse_token(session.auth_token) server.tool(query_user) async def query_user(session, user_id: str): # 從 session 狀態(tài)里拿當前用戶 current_user session.state[user_id] return db.query(user_id, current_user)新版里沒有 session.state也沒有 session 回調(diào)。改造后是這樣from mcp.server import Server server Server(stateless-demo) server.tool(query_user) async def query_user(user_id: str, auth_token: str): # 每次請求都從參數(shù)里拿身份信息 current_user parse_token(auth_token) return db.query(user_id, current_user)核心思路就一條原來存在 session 里的東西現(xiàn)在要么顯式作為請求參數(shù)傳入要么在服務端外部存儲里按 token 查。我實際項目里遇到最多的是從 session 里拿用戶上下文這個改起來最機械但也最容易漏——所有工具函數(shù)都要加一個參數(shù)所有測試都要跟著改。3.3 Sampling 調(diào)用改造成“返回結構化數(shù)據(jù)”舊版服務端調(diào)用 Sampling 最常見的用途是讓客戶端模型幫我把這個查詢結果總結一下。舊代碼類似server.tool(summarize_table) async def summarize_table(sql_result: str): reply await session.sampling.create_message( messages[{role: user, content: fSummarize: {sql_result}}] ) return reply.content[0].text新版不能再這么寫。我的改造方向是服務端只做關我屁事的決策——把原始結果、統(tǒng)計指標、候選摘要文本全都返回由客戶端模型決定怎么用server.tool(analyze_table) async def analyze_table(sql_result: str) - dict: return { raw: sql_result, row_count: sql_result.count(\n), columns: extract_columns(sql_result), top_50: sql_result[:2000], summary_candidate: generate_local_gist(sql_result) }這樣客戶端拿到的是一份完整的結構化數(shù)據(jù)模型可以自己決定是生成摘要、畫圖表還是做下一步推理。服務端還可以額外提供一個本地規(guī)則生成的摘要候選給客戶端模型當素材但不再強制客戶端執(zhí)行推理。還有一個容易忽略的點Sampling 移除后服務端想去調(diào)外部大模型應該怎么辦。我的方案是把模型調(diào)用封成普通工具比如call_llm由客戶端控制何時調(diào)用、用什么 key。服務端需要智能時就調(diào)用這個工具把數(shù)據(jù)轉給獨立的模型服務返回結果再走正常工具鏈路。這是一種更干凈的分層。3.4 服務端狀態(tài)往外挪用 Token 和外部存儲代替 Session替換 Session 最標準的方案就是 JWT??蛻舳嗽诿看握埱蟮?Authorization 頭里帶 token服務端解析 token 拿到用戶身份和上下文這就是無狀態(tài)鑒權。和廣為人知的 Web 會話機制類似MCP 的新模型也回到了每次請求自證身份的思路上。需要復雜狀態(tài)時把狀態(tài)挪到 Redis。比如一個服務端要記住用戶的查詢歷史、偏好設置老邏輯是存在 session.state新邏輯是在 token 里附帶一個 state_id服務端啟動時根據(jù) state_id 從 Redis 加載狀態(tài)用完后寫回。注意這種狀態(tài)不屬于協(xié)議本身服務端完全可以用任何存儲實現(xiàn)協(xié)議不管也不該管。這里有個注意事項Token 不要塞太多信息。JWT 雖然可以塞自定義 claims但請求頭體積和 token 刷新復雜度都會上升。我一般只在 token 里放用戶 id、租戶 id、過期時間其他上下文走 Redis。3.5 資源訂閱推送的替代方案如果你老項目用了 resources 訂閱改造時要多想一步。舊版服務端可以在資源變化時通過 session 主動 push 通知新版沒有 session 通道就必須把通知機制從協(xié)議層挪到業(yè)務層。一種我驗證過可行的做法是服務端提供list_resources和read_resource兩個工具客戶端在有需要時主動拉取服務端檢測到外部數(shù)據(jù)變化后不直接推給客戶端而是記錄一個resource_version字段客戶端在每次請求后返回最新的 version與本地不一致就重新拉取。類似樂觀鎖的思路成本低也能覆蓋絕大多數(shù)場景。如果要更實時的推送可以在 MCP 之外單獨搭一條 webhook 或者 WebSocket 通道但這已經(jīng)不屬于 MCP 協(xié)議層的職責了。協(xié)議瘦身后這種協(xié)議外補充是正常的架構選擇不用覺得不標準。4. 常見問題排查與踩坑實錄遷移過程不會一次順利。下面幾條是我在真實環(huán)境里遇到的報錯和排查思路直接整理成速查表方便你直接抄?,F(xiàn)象原因解決方法第一次請求成功第二次返回 401服務端還在按舊邏輯查找 session找不到就拒絕改為每請求解析 token移除 session id 依賴initialize 返回后客戶端沒有收到 serverCapabilities.sampling新版規(guī)范已移除 sampling 能力聲明客戶端不要依賴 sampling 字段服務端把模型需求改造成工具調(diào)用服務端內(nèi)存里的 roots 列表不生效roots 不再是會話級狀態(tài)而是請求級提示信息將 roots 信息放到客戶端請求上下文中或通過配置接口下發(fā)流式輸出中斷舊版長連接被網(wǎng)關切掉改成分塊響應內(nèi)流式返回不依賴持久通道后續(xù)請求找不到上一次會話里的變量狀態(tài)沒有外部化把狀態(tài)寫入 Redis/數(shù)據(jù)庫或要求客戶端在下一次請求時顯式帶上 key資源訂閱永遠收不到更新服務端還在用 session 推送但通道已不存在改成客戶端定時拉取 服務端提供 version 字段做增量判斷老教程里的 sampling 鉤子代碼不報錯但也不生效SDK 兼容層吞掉了舊方法調(diào)用全局搜索 sampling / create_message全部重寫成結構化返回4.1 調(diào)試技巧把協(xié)議交互錄下來排查 MCP 問題最有效的工具不是斷點調(diào)試而是把 JSON-RPC 消息完整記錄下來。我遷移時會在客戶端和服務端之間加一個簡單的日志中間件把每個請求、響應、錯誤碼打出來重點看method和params里有沒有 session 相關字段。新版規(guī)范下一次正常的工具調(diào)用應該是三段式日志initialize/request、tools/call、response。如果中間混入了session/close、sampling/createMessage之類的遺留請求那說明有舊代碼沒清干凈順著調(diào)用棧找就行。4.2 本地生產(chǎn)環(huán)境不一致的坑本地跑得通、上生產(chǎn)就掛這種問題在新版 MCP 里尤其常見。本地工具進程是短命的、單實例的服務器上如果有負載均衡和網(wǎng)關舊 session 模型的問題才會暴露。新版無狀態(tài)模型對生產(chǎn)環(huán)境更友好但前提是服務端真的不保存協(xié)議狀態(tài)。我遇到過一個案例服務端代碼已經(jīng)改干凈了但數(shù)據(jù)庫連接池還保存在全局單例里配合長連接顯得像有狀態(tài)最后靠強制回收連接解決。記住無狀態(tài)指的是協(xié)議層不保存會話狀態(tài)不是業(yè)務數(shù)據(jù)不能保存。業(yè)務數(shù)據(jù)存數(shù)據(jù)庫、緩存都沒問題關鍵是每一次請求都要能被任意實例獨立處理。5. 別急著刪教程先學會“按版本讀書”MCP 社區(qū)目前最大的混亂來自教程過期。很多熱門教程寫于 2025 年初那套內(nèi)容在舊版規(guī)范下是準確的但放到新版就錯得很離譜。你的任務不是把所有舊教程拉黑而是學會識別它的版本氣息。5.1 怎么判斷教程是哪一代的給你幾個快速判斷標準教程里如果出現(xiàn)了 Session 作為核心概念講建立會話會話生命周期session id大概率是舊版。教程里出現(xiàn) Sampling 或者 sampling/createMessage幾乎可以確定是舊版新版已經(jīng)移除了這個能力。教程里描述 transport 時提到 SSE 長連接 與 POST /messages 這種兩頭傳消息的結構是舊式 transport新版 transport 更接近單請求流式響應的模型。教程發(fā)布日期如果是新規(guī)范發(fā)布之后并且明確寫了無狀態(tài)無 session移除 sampling字樣才是可參考的新版資料。我建議你在看任何 MCP 教程時先翻到目錄搜索這三個詞session、sampling、sse。任何一項出現(xiàn)都要帶著這可能過時了的心態(tài)去讀。5.2 我推薦的入門路徑如果你現(xiàn)在完全是 MCP 新手直接按新規(guī)范學別碰舊教程。先理解三個問題什么是工具調(diào)用、什么是資源、什么是能力協(xié)商。把這三個概念吃透再用當前版 SDK 寫一個最簡單的 echo server。然后加上一個真實工具比如查詢數(shù)據(jù)庫、讀寫文件、調(diào)用外部 API。最后再考慮鑒權、部署、流式輸出這些工程問題。進階一點去讀規(guī)范里 capability negotiation 的部分理解方 service 如何聲明能力、客戶端如何選擇能力這比背 API 名有用得多。MCP 更新還會繼續(xù)但底層 JSON-RPC 加能力協(xié)商的骨架短期內(nèi)不會變抓住主干枝葉變了也不慌。5.3 給團隊內(nèi)部資料打版本標簽如果你所在團隊也在寫 MCP 代碼我強烈建議像管理 API 文檔一樣管理協(xié)議教程開頭標注 適用于 2025-XX 新版規(guī)范并用一個簡單的字段記錄規(guī)范版本號。這次改版讓我最痛苦的就是內(nèi)部 Wiki 里的教程沒寫版本遷移時根本不知道哪篇能信。加一個版本標簽看起來只是一個小動作卻能讓后來的人少走很多彎路。6. 遷移完之后的真實體感最后說點實際的感受。我把自己的所有 MCP 工具全部遷到新版之后最明顯的變化是服務端部署簡單多了不用再做會話粘滯多個副本隨便起擴容就是加實例網(wǎng)關層直接把 sticky 配置刪了。其次是安全性更清晰每次請求都帶 token權限模型跟普通 Web API 一致審計日志也能精確到單次請求不像以前哪個 session 干了什么要靠查內(nèi)存。代價是客戶端和工具函數(shù)的代碼量變大了。身份信息要從 token 里解析傳參資源變更要主動拉取模型生成邏輯要明確放在客戶端側。這不能叫缺點準確說是協(xié)議變笨了但架構變聰明了。如果你正在做批量遷移我的建議是一次性改完不要留過渡分支。新舊協(xié)議混合運行帶來的兼容代碼比你想的難維護得多。我踩過的坑是給舊代碼留了一個兼容開關結果兩周后自己都忘了哪些分支走的是舊邏輯。如果你剛接觸 MCP不用擔心現(xiàn)在學的教程會立刻過期。協(xié)議的核心——工具、資源、能力協(xié)商——并沒有變變的只是連接模型和職責邊界。抓住主干跟上版本剩下的都是工程細節(jié)。