一個 Tool 準備刪掉三個檔案,做到最後一步才需要問使用者:「確定嗎?」

另一個 Tool 啟動一個部署工作,可能要跑 12 分鐘。Client 中途斷線,回來後還要能繼續看進度。

同一時間,Gateway 想知道這個 Request 呼叫的是哪個 Tool,決定要送到哪個 Backend、套哪個 Rate Limit;Client 又不想每次都重新抓一次 tools/list;Tracing 系統則希望同一條 Trace 可以一路穿過 Host、MCP Client、Gateway、MCP Server,再進下游 API。

如果把這些需求全部塞回一個長駐 Session,MCP 就只是換了一個地方繼續背 Session 的成本。

2026-07-28 採用的是另一種做法:把原本容易黏在 Session 上的責任拆開,各自給一個比較明確的生命週期。[1]

先用 30 秒記住這篇:

  • MRTR: server 缺一個答案。
  • Tasks: 工作本身還沒做完,而且要能跨斷線繼續。
  • Headers: 讓 gateway 在不先拆 JSON body 的情況下,看懂這是哪種 request。
  • Caching: 告訴 client 這份結果還能不能繼續用,以及能不能和別的使用者共用。
  • Trace Context: 讓不同服務知道「這些步驟屬於同一次操作」。

先記住這五句。下面的 requestStatetaskIdttlMs_meta.traceparent,只是把這些責任落到 protocol 裡的具體做法。

這篇關心的是每種機制到底負責哪一種問題,以及選錯 lifecycle 時,Stateless 怎麼又悄悄長回 Session。

版本範圍:本文以 MCP 2026-07-28 與 2026-08-18 可取得的 current specification / extension documentation 為準。Client / Server 如何同時兼容舊 protocol era、Authorization hardening 與 deprecated features 的退場方式留到 Part 03。Tasks 是 Extension,規格可以獨立演進,因此實作前仍應再次核對 current extension docs。

先把 Session 原本黏在一起的責任分開

Part 01 處理的是 State 到哪裡去。Part 02 往下一層看:拿掉 protocol session 之後,原本靠一條長連線或一段 session context 串在一起的互動,怎麼重新接起來?

可以先用這張表定位:

問題2026-07-28 的主要機制State / lifetime
Tool 做到一半需要使用者確認或補參數MRTR短暫 continuation;必要資訊透過 requestState 回傳
工作可能跑幾分鐘、幾小時,甚至 Client 會重連Tasks ExtensionDurable task state + taskId
Gateway 想知道 RPC method / tool / URIHTTP request metadata每個 Request 都重新帶
Tool catalog / resource read 不想每次重抓ttlMs + cacheScopeClient-side freshness / cache scope
一個 Tool Call 想跨 Client、Server、Downstream 串成同一條 Trace_meta.traceparent / tracestate / baggageDistributed trace context

這五個機制看起來都和「延續 Context」有關,但它們的責任差很多。

最容易混在一起的是前兩個:MRTR 和 Tasks

Stateless MCP 的兩種續接方式:MRTR 處理短暫 round trip;Tasks 處理 Durable Async Work

Figure 2-1|MRTR 讓 Client 重新送出一個完整 Request;Tasks 則讓原本的長時間工作成為可查詢、可恢復的 Durable Task。兩者都可能出現 input_required,但生命週期不同。

MRTR:需要一個答案,就結束這一輪 Request

先看一個最適合 MRTR 的場景。

假設有一個:

delete_files(["a.txt", "b.txt", "c.txt"])

Tool 已經檢查完檔案、權限和影響範圍,最後只差一個使用者確認。

舊的 bidirectional 模式可以讓 Server 在原本的 SSE stream 上主動送出 elicitation request,再等待 Client 回覆。問題是這條互動會把兩個方向的 Message 綁在同一段 Session / Stream lifecycle 裡。[3]

MRTR 改成:Server 先結束這一次 Request。

它可以回一個 resultType: "input_required" 的結果,裡面帶 inputRequests,以及必要時的 requestState。[1][3]

示意:

{
  "resultType": "input_required",
  "inputRequests": {
    "confirm_delete": {
      "method": "elicitation/create",
      "params": {
        "message": "Delete these three files?"
      }
    }
  },
  "requestState": "opaque-server-state"
}

Client 把問題交給使用者或其他能回答的 Handler。拿到答案後,重新送出原本的 tools/call,並附上:

{
  "inputResponses": {
    "confirm_delete": {
      "action": "accept"
    }
  },
  "requestState": "opaque-server-state"
}

第一個 HTTP request 到這裡已經結束;後面是新的 call。

新的 Call 有新的 JSON-RPC ID,Load Balancer 也可以把它送到另一台 Server Instance。只要這個 Request 本身帶齊完成下一步需要的資訊,接手的 Instance 不需要偷偷讀前一台機器的 Session Memory。[2][3]

目前這套 InputRequiredResult 主要套在 tools/callprompts/getresources/read 這類需要在執行途中補 Client input 的操作。[2][3]

requestState 的角色很窄,別把它養成另一個 Session

requestState 很容易讓人產生一個直覺:

既然我可以把 State 丟給 Client,再讓它帶回來,那我是不是把整個 Session 塞進這個欄位就好了?

技術上,Server 可以自行決定怎麼 encode requestState。SEP-2322 甚至列出 plain JSON、Base64、JWT 或其他格式作為例子。[3]

但它的用途有明確邊界:續接同一個 logical request 的 multi-round workflow。

Client 對 requestState 沒有解讀權。收到之後必須把它當 opaque value,retry 時原樣 echo 回去,不能 parse、修改或猜裡面代表什麼。[3]

Server 則不能因為這串資料「看起來是自己發的」就直接信任。Client 是不可信的中介者,所以每次收到 requestState 都必須驗證。如果 State 包含和原使用者有關的資料,SEP 進一步要求 Server 用某種方式把資料 cryptographically bind 到原使用者,並確認回來的 State 和目前 authenticated user 相符。[3]

這條規則很實際。

如果一段 requestState 裡寫著:

files = [a.txt, b.txt, c.txt]
approved_scope = project-123

另一個登入者不應該只靠拿到這串 State,就能把它塞進自己的 Request 繼續執行。

MRTR 適合的是 Ephemeral Continuation

MRTR 很適合:

  • 確認刪除
  • 補一個缺少的參數
  • 取得一次性 Elicitation
  • 一個短流程需要兩三輪 Input 才能完成
  • Load Shedding 時把必要 continuation state 帶到下一個 Request

如果 Server 已經跑了 20 分鐘、建立很多 Durable State,還要在 Client 斷線後繼續工作,requestState 就不是最好的容器。

這種 workload 需要的是另一條生命週期:Task

Tasks:工作本身要活得比 HTTP Request 久

想像一個 deploy_service Tool。

它要:

  1. 建 image
  2. 跑 tests
  3. 上傳 artifact
  4. 等 deployment rollout
  5. 等人工 approval
  6. 驗證 health check

整個流程可能超過一般 HTTP timeout,也可能遇到 Client 關閉 App、Wi-Fi 切換或 Worker 被回收。

這時如果還堅持「原本那一個 Request 必須活著」,你只是在和 timeout 賭運氣。

MCP Tasks 把這種工作移到 io.modelcontextprotocol/tasks Extension。Client 在這一個 Request 的 capabilities 宣告自己支援 Tasks;Server 也透過 server/discover 宣告 Extension support。只要 Client 有 opt in,Server 可以依這一次工作的特性,決定回普通 Tool Result,或回 CreateTaskResult。[5][6]

目前 Tasks Extension 支援 task-augment tools/call。這一點很容易跟早期 proposal 或 2025 experimental Tasks 混在一起,所以不要把舊範例裡的 wire shape 直接搬過來。[5][6]

Task 建立時會有一個 durable taskId,以及像 statusttlMspollIntervalMs 這類 operational metadata。Server 必須先把 Task durable-create 完,才能把 Task Result 回給 Client。[5][6]

接著 Client 不需要維持原本的 Tool Call:

tools/call

CreateTaskResult(taskId)

tasks/get

working / input_required / completed / failed / cancelled

如果工作需要中途確認,Task 可以進到 input_required。這時 Client 從 tasks/get 讀到 inputRequests,再用 tasks/updateinputResponses。[5][6]

注意這裡和 MRTR 的差異。

MRTR:Client retry 原本的 Method。

Task:Client 繼續操作同一個 Durable Task。

兩邊都有 input_required,卻不是同一套 continuation lifecycle。

Client 也可以送 tasks/cancel,但 cancellation 是 cooperative。Server 必須 acknowledge 取消意圖,不代表 Backend 一定能在那一毫秒停住;Task 最後甚至可能在取消請求送出後完成。[5][6]

狀態更新預設可以靠 tasks/get polling。Server 也可以送 notifications/tasks,Client 透過 subscriptions/listen 訂閱這些更新。[5][6]

MRTR 和 Tasks 到底怎麼選?

這兩個機制放在一起看,比各背一次 API 有用。

判斷MRTRTasks
主要用途同一 logical request 的短暫多輪互動長時間、可恢復的 async work
Server 回什麼resultType: "input_required"resultType: "task"
Client 下一步retry 原本的 requesttasks/get / tasks/update
Continuation handle可選 requestStatetaskId
State 性質可以完全由 Request 帶著走Server 持有 Durable Task State
Client 重連後繼續不適合拿來當長期 Job Registry是 Tasks 的主要價值之一
中途需要 InputinputRequests → retry with inputResponsestasks/gettasks/update
目前支援範圍core MRTR 適用的指定 Request 類型Current extension 目前 task-augment tools/call

一個簡單判斷方式是看「工作」本身需不需要活著。

如果 Server 只是暫停一下,等你補一個答案,然後下一個 Request 可以重新接手,MRTR 很合適。

如果工作已經變成一個需要持續執行、保存進度、跨斷線恢復的實體,就給它 taskId,不要讓 requestState 硬扛 Job System 的責任。

HTTP Headers:Gateway 終於不用先拆 JSON 才知道在做什麼

把互動 lifecycle 拆開之後,還有另一個 production 問題:Gateway 怎麼知道 Request 是什麼?

JSON-RPC Method 和 Tool Name 原本都在 body 裡。Load Balancer、WAF、Rate Limiter 或 Gateway 如果想做:

  • tools/callresources/read 走不同 policy
  • execute_sql 走特定 Backend
  • 某個 Tool 每分鐘最多 20 次
  • 某個 Region 的 Query 導到指定 Cluster

最直覺的做法是解析 JSON body。

2026-07-28 把部分 Request metadata mirror 到 HTTP headers,讓 Intermediary 可以在 HTTP 邊界先理解這個 Request。[2]

以一個 tools/call 為例:

POST /mcp HTTP/1.1
MCP-Protocol-Version: 2026-07-28
Mcp-Method: tools/call
Mcp-Name: execute_sql

Mcp-Method 用來 mirror RPC method。Mcp-Name 則只在有 name / URI 的幾個指定 Request 上要求,目前是 tools/callresources/readprompts/get。[2]

Tool Definition 還可以透過 x-mcp-header 指定某些 primitive argument 要 mirror 成 Mcp-Param-*。例如 region 可以變成:

Mcp-Param-Region: us-west1

這讓 routing 或 policy layer 不一定要先 parse 整份 JSON-RPC body。[2]

Stateless MCP 的 HTTP operability:Headers 讓流量可路由,Cache Hints 管 Freshness,Trace Context 串起 Distributed Trace

Figure 2-2|Headers、Caching 與 Trace Context 解的是三個不同的 infrastructure 問題。Gateway 可以看 Request metadata;Client 管 Cache Freshness;Trace Context 則跨服務傳遞 correlation。

Header 只是鏡像訊號,body 仍然要驗證

把同一份資料放在 Header 和 Body,馬上會出現一個安全問題:

如果 Gateway 看到:

Mcp-Name: safe_tool

但 MCP Server 打開 body 後看到:

{"name": "dangerous_tool"}

那 Gateway 和 Server 就活在兩個不同世界。

因此 current Streamable HTTP spec 要求:任何會處理 Message Body 的 Server,都要驗證 mirrored header 和 body 是否一致。Required header 缺失、格式錯誤或值對不上,要回 400 Bad Request,並使用 MCP 的 HeaderMismatch JSON-RPC error -32020。[2]

這條 validation 才是 Header-based routing 可以被信任的前提。

所以比較精確的架構是:

Header → 讓 Intermediary 看得懂 Request
Body   → MCP Server 真正執行的 Message
Validation → 確認兩邊沒有分裂

Header 可以成為 routing / rate-limit / policy signal。它不會因為存在,就自動讓 Authorization 變安全。Authorization 本身的 issuer、token audience 與 client registration 問題留到 Part 03。

Caching:ttlMs 是 Freshness Hint,不是背景計時器

Tool catalog 常常沒有必要每次重抓。

如果 tools/list 十分鐘內都不會變,Client 一直向 Server 重新取同一份結果,只會增加 round trip,也可能讓上游 Prompt Cache 不穩定。

Current MCP caching spec 讓特定 resultType: "complete" 結果帶兩個欄位:[4]

ttlMs
cacheScope

目前必須提供 cache hints 的 complete results 包含:

  • server/discover
  • tools/list
  • prompts/list
  • resources/list
  • resources/templates/list
  • resources/read

ttlMs 表示 Client 可以把結果視為 fresh 多久。它的語義接近 HTTP Cache-Control: max-age,但只是 freshness hint,不是「到了 30 秒就自動在背景重新抓一次」。TTL 過期後,Client 在下一次需要這份資料時再判斷是否 refetch。[4]

cacheScope 則處理另一條邊界:這份結果能不能跨使用者共用。

  • public:內容沒有 user-specific data,可以被 shared cache 使用。
  • private:只能在同一 authorization context 內重用,不能把 A 使用者的結果送給 B。[4]

另外兩條很容易漏:

第一,input_required 這種 MRTR 中間結果不能 cache。

第二,帶 inputResponsesrequestState 的 MRTR retry 也不能拿一般 cache key 重用,因為結果依賴的 input 不完整地反映在普通 method / params cache key 裡。[4]

public cache scope 還要看實際的 cache infrastructure

MCP 定義的是 Protocol-level cache semantics。

某個 CDN、Reverse Proxy 或 SDK 會不會真的 cache POST response,還取決於那一層自己的 cache policy、method support、key design 與 security settings。

文章裡如果把這件事寫成「MCP 現在可以直接丟給 CDN 快取」,會把 protocol hint 和 infrastructure implementation 混成一件事。

Trace Context:先把 correlation 串起來,trace 本身還要靠 instrumentation

Stateless Request 可以換 Instance,也可能一路經過:

Host

MCP Client

Gateway

MCP Server

External API / Database / another service

如果每一層都自己生一條孤立 Trace,出問題時還是很難知道一次 Tool Call 到底在哪裡慢、在哪裡 fail。

SEP-414 文件化了 MCP _meta 裡的三個 Trace Context key:[7]

traceparent
tracestate
baggage

當 OpenTelemetry / W3C Trace Context 透過 _meta 傳遞時,這些 key 使用 W3C Trace Context 與 W3C Baggage 的格式。[7]

這三個 key 也是 _meta DNS-prefix 命名慣例的刻意例外。MCP 沿用既有的 W3C / OpenTelemetry 名稱,讓既有與新的 implementation 能互通,也避免有人把 traceparent 改成 io.modelcontextprotocol.traceparent 之類的 namespaced key,結果把 trace 與 log correlation 切斷。[7]

這個 wording 有一個很重要的限制:它定義 propagation convention,不代表 MCP 保證每一個 Request 都有 Trace。

你還是要有 Instrumentation,還是要決定哪些 Span 要建立、哪些 Downstream 要 inject / extract context,以及最後把 Trace 送去哪個 Backend。

baggage 和 correlation metadata 也不是免費的。SEP 特別提醒 Trace Context 可能包含 correlation IDs,實作要遵守自己環境適用的 data-handling policy。[7]

把五個機制放回同一個 Tool Call

用一個示意的 provision_environment Tool 看會比較完整。

Client 先呼叫:

provision_environment(region="sg", size="medium")

HTTP 邊界上:

  1. Mcp-Method / Mcp-Name 讓 Gateway 知道這是 tools/call 和哪個 Tool。
  2. 如果 Tool Schema 把 region 標成 x-mcp-header,Gateway 可以直接看到對應的 Mcp-Param-* metadata。
  3. MCP Server 仍要驗證 Header 和 Body 一致,再執行。[2]
  4. _meta.traceparent 如果存在,就沿著 MCP Server 傳到後面的 Cloud API / Worker,讓 Trace 可以串起來。[7]

Server 判斷 Provisioning 不會在這一個 HTTP Request 內完成,而且 Client 宣告支援 Tasks,因此回一個 taskId。[5][6]

Client 之後用 tasks/get 看進度。如果部署做到 Production 前需要人工批准,Task 進 input_required,Client 再用 tasks/update 回答。[5][6]

同一時間,tools/listserver/discover 可以依自己的 ttlMs / cacheScope 被 Client reuse,不需要因為每個 Task poll 都順手再抓一次 catalog。[4]

把整個 tool call 串起來後,每個責任其實都有自己的 ID、scope 和 lifetime。

對應起來會長這樣:

短暫多輪 continuation → requestState
Durable background work → taskId
Gateway routing signal → HTTP headers
Result freshness → ttlMs / cacheScope
Distributed correlation → trace context

這個分法會讓系統多幾個名詞,卻讓每一層更容易回答兩個問題:這段 State 應該活多久?誰有權相信它?

這些做法看起來合理,實際上會出問題

用 MRTR 撐一個 30 分鐘 job

如果工作需要 Durable Progress、Client 重連與長時間 Backend execution,requestState 很快會變成一個奇怪的 Job Store。

這種 workload 交給 Tasks 比較符合它的 lifecycle。[3][5]

Task 已經 input_required,卻又 retry 原本 tools/call

Current Tasks 的 mid-flight input 是 tasks/getinputRequests,再用 tasks/updateinputResponses。這和 MRTR retry original request 是兩條路。[5][6]

Gateway 信 header,server 不驗 body

這會製造 routing / policy split-brain。Current spec 已經明確要求 processing Server 驗證 mirrored values,不一致就拒絕。[2]

ttlMs 當 polling interval

TTL 只是「目前還可以當 fresh」的時間。它不要求 Client 到點就發背景 Request。[4]

traceparent 就以為 observability 做完了

Trace Context 只處理 propagation。沒有 instrumentation、span design、exporter 與 data policy,最後只會得到一串被傳來傳去的 ID。[7]

實作前,我會沿著這幾條邊界檢查

如果你準備把 Remote MCP Server 升到 2026-07-28,Part 02 這一層可以先查:

  • Interaction lifetime: 互動只是等一個答案,還是工作本身要 durable?前者先看 MRTR,後者先看 Tasks。
  • requestState trust: requestState 有沒有被當不可信輸入驗證?如果含 user-specific state,有沒有綁定 authenticated identity?[3]
  • Tasks opt-in: Tasks client / server 是否真的都宣告 extension support?Server 不應對沒有 opt in 的 client 突然回 CreateTaskResult。[5][6]
  • Header/body agreement: Gateway 如果依 Mcp-Method / Mcp-Name / Mcp-Param-* 做 policy,誰負責驗證 body 一致性?[2]
  • Cache boundary: Cache key 有沒有把 method、影響結果的 params 與 authorization context 算對?Private result 不能跨 context 共用。[4]
  • Freshness: ttlMs 是拿來做 freshness check,還是被誤用成固定 polling timer?[4]
  • Trace pipeline: Trace Context 是真的串進既有 OTel pipeline,還是只把三個 key 放進 _meta?[7]

這七題如果答不清楚,系統表面上可能已經「支援 2026-07-28」,實際上只是把舊 Session 的問題拆散後換了幾個名字。

最後,把每種 lifetime 放回自己的位置

MCP 拿掉長駐 protocol session 後,多輪互動沒有消失,長時間工作也沒有被迫變成同步 Request。

MRTR 讓短暫的互動在 Request 之間續接;Tasks 給 Durable Work 一個獨立 lifecycle;HTTP headers 讓 Intermediary 可以看懂 Request;cache hints 把 freshness 和 scope 交代清楚;Trace Context 則負責把一次操作跨服務串起來。

這些能力放在一起,才讓 Stateless MCP 不只「可以 Round-robin」,還能處理真實 Agent Workflow 裡的確認、等待、長任務、Gateway Policy 與 Observability。

下一篇會從 Mechanism 走到 Migration:

一個已經跑在 2025-11-25、依賴 initializeMcp-Session-Id、server-initiated requests、legacy transport 或舊 Tasks 的系統,要怎麼升級,而不把 production 一次切斷?

Part 03 會拆 protocol-era detection、dual support、Authorization hardening、DCR / CIMD,以及 Roots、Sampling、Logging、legacy HTTP+SSE 的正確 deprecation 路徑。

參考資料

  1. Model Context Protocol, The 2026-07-28 Specification, 28 July 2026. https://blog.modelcontextprotocol.io/posts/2026-07-28/
  2. Model Context Protocol, Streamable HTTP: 2026-07-28 Specification. https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/streamable-http
  3. Model Context Protocol, SEP-2322: Multi Round-Trip Requests. https://modelcontextprotocol.io/seps/2322-MRTR
  4. Model Context Protocol, Caching: 2026-07-28 Specification. https://modelcontextprotocol.io/specification/2026-07-28/server/utilities/caching
  5. Model Context Protocol, MCP Tasks: Current Extension Documentation. https://modelcontextprotocol.io/extensions/tasks/overview
  6. Model Context Protocol, Tasks Extension Specification. https://tasks.extensions.modelcontextprotocol.io/specification/draft/tasks
  7. Model Context Protocol, SEP-414: Document OpenTelemetry Trace Context Propagation Conventions. https://modelcontextprotocol.io/seps/414-request-meta