【2026 最新】Stateless MCP 是什麼?從 Session 遷移到 Serverless/Edge

最後更新: ·
Stateless MCP 遷移教學首圖:從 Session 搬上 Serverless 與 Edge

Stateless MCP 不是「MCP Server 從此完全沒有狀態」,而是把協定版本、Client 能力,以及受保護資源所需的身分證明放回每一次請求,不再把它們藏在前一次握手或連線 Session 裡。對同步、唯讀、短時間工具而言,只要所需資料與金鑰在共享或可重建的位置,任一個 Serverless/Edge instance 都能接手請求,不必黏回原本那台機器。

截至 2026 年 8 月 5 日,MCP 2026-07-28正式規格已上線,相關 Hacker News 討論也已超過百點、累積數十則留言。早期 RC 與 Simon Willison 的實作筆記仍值得讀,但封包欄位與錯誤碼不能直接複製;下文全部以 GA 規格及實跑結果為準。

先記住一句話:Stateless MCP = 每個 request 自帶協定上下文,受保護資源逐次驗證身分;跨 request 的業務狀態,改用明確 ID 傳遞。它移除的是協定層黏性,不是資料庫、身份、佇列或長任務。

Stateless MCP 與舊版 Session 差在哪裡?

舊版是 handshake-based:Client 必須先送 initialize,收到結果後再送 notifications/initialized。Streamable HTTP Server 可以在初始化結果建立 MCP-Session-Id;只有它真的建立 Session,後續請求才必須帶回。因此「舊版一定有 Session ID」並不精確,但依賴 Session 的服務確實常需要 sticky routing 或共享 Session store。

# Legacy 2025-era,簡化後是三段 lifecycle
POST /mcp  → initialize
POST /mcp  → notifications/initialized
POST /mcp  → tools/call
MCP-Session-Id: abc…   # 僅在 server 先建立 Session 時帶回

2026-07-28 移除了 initializeinitialized 與協定層 Session。一般 tools/call 可以直接成為第一個 RPC;server/discover 是 Server 必須提供、Client 可選擇呼叫的能力查詢,不是新的強制握手。

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: lookup_product

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "lookup_product",
    "arguments": { "sku": "KB-75" },
    "_meta": {
      "io.modelcontextprotocol/protocolVersion": "2026-07-28",
      "io.modelcontextprotocol/clientCapabilities": {},
      "io.modelcontextprotocol/clientInfo": {
        "name": "curl-lab", "version": "1.0.0"
      }
    }
  }
}

protocolVersionclientCapabilities 是必填;GA 把 clientInfo 改為 SHOULD。完成結果必須有 resultType: "complete",Server 也 SHOULD 在結果的 _meta 回傳 serverInfo。這正是 RC 範例最容易漏掉的地方。

Legacy MCP 與 Stateless MCP 的 lifecycle、Session 與單次請求部署比較圖
舊版一定先握手、Session 則是可選;新版每個 POST 自帶協定上下文,但業務狀態仍要另外設計。

Mcp-Method、Mcp-Name 到底是什麼?

Mcp-Method 是所有新版 Streamable HTTP request 都必須帶的標準 header,值要等於 JSON body 的 method。在 core 中,Mcp-Name 只在 tools/callresources/readprompts/get 必填,分別對應工具名、URI 或 prompt 名稱;Tasks extension 另要求 tasks/gettasks/updatetasks/canceltaskId 作為值。Gateway 可針對 allowlist 內的工具/prompt 名做 routing、限流與統計;URI 與 taskId 可能是高基數或敏感值,metric label 應正規化或捨棄。Header 仍是不可信輸入,Server 必須比對 body,不能只靠它授權。

我刻意把 header 寫成 Mcp-Name: wrong_tool、body 保留 lookup_product,SDK 實際回 HTTP 400 與 HeaderMismatch -32020。GA 的另外兩個新錯誤碼是缺少必要 Client capability 的 -32021,以及不支援協定版本的 -32022;完整規則見正式版 Streamable HTTP

State handle 與 Tasks:無狀態不等於沒狀態

State handle 像寄物櫃號碼牌:Server 仍保管真正資料,第一次工具呼叫只回傳一個 opaque string,下一次再把它當普通參數帶回。它不是新的 MCP 型別,也沒有 handles/* RPC。Server 應在工具說明中寫清楚 handle 的生命週期、持久性與到期規則;有登入身分時,每次以 (handle, auth context) 重新授權。若未登入者能把它當 bearer token 使用,規格建議至少有 128-bit 密碼學隨機性及有限生命週期。

MRTR 會先回 input_required,Client 收集輸入後以新的 JSON-RPC request ID 重送原方法;requestState 是可選欄位,若 Server 有回傳,Client 必須逐字帶回。長時間、可斷線恢復的工作可採 Tasks extension;目前只有 tools/call 可被 task-augment,且由 Server 決定同步完成或建立 task,再以 tasks/get 取得狀態與最終結果、tasks/update 補輸入、tasks/cancel 表示取消意圖。舊版 tasks/resulttasks/list 與每次呼叫的 task flag 都已移除。但 Tasks 是 io.modelcontextprotocol/tasks opt-in extension;截至本文驗證日,官方 TypeScript SDK 2.0.0 支援文件明列尚未實作最終版 Tasks wire,不能把概念範例當成已可執行的 SDK 程式。

實作:建立唯讀 Stateless MCP Server

以下完整 lab 使用 Node.js 22.19+、@modelcontextprotocol/server@2.0.0、Node adapter 2.0.0 與 Zod 4;SDK Server 本身的最低需求較低,但本文同時涵蓋 Wrangler 與 Inspector 2.0,因此以較高版本為準。工具只讀取記憶體商品表,適合先驗證 transport;若你還不熟工具定義,可先看AI Agent Harness 是什麼如何打造 AI Agent Harness

mkdir readonly-mcp && cd readonly-mcp
npm init -y
npm pkg set type=module
npm i @modelcontextprotocol/server@2.0.0 \
  @modelcontextprotocol/node@2.0.0 zod@4.4.3
npm i -D typescript@5.9.3 tsx@4.23.7 @types/node@22.20.1

同日執行 npm audit 會看到 @modelcontextprotocol/node 的 transitive dependency @hono/node-server 被標記一項 Windows serve-static path traversal 中等風險公告;目前 adapter 2.0.0 尚無可用修正。本 lab 沒有使用 static serving;正式環境應避開受影響的 Windows static-serving 路徑、部署當天重跑 audit,並追蹤上游更新。

建立 tsconfig.json,同時涵蓋 Node 與 Worker 使用的型別:

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "Bundler",
    "strict": true,
    "skipLibCheck": true,
    "lib": ["ES2022", "WebWorker"],
    "types": ["node"]
  },
  "include": ["src/**/*.ts"]
}

先把 runtime-neutral 的工具定義放在 src/mcp.ts,Node HTTP adapter 另放 src/node.ts。官方 createMcpHandler(buildServer) 會替每個 HTTP request 建立 fresh Server instance:

// src/mcp.ts
import { McpServer } from "@modelcontextprotocol/server";
import { z } from "zod";

const products = [
  { sku: "KB-75", name: "75% mechanical keyboard", stock: 18 }
];

export function buildServer() {
  const server = new McpServer({ name: "readonly-catalog", version: "1.0.0" });
  server.registerTool("lookup_product", {
    description: "Read one product from the demo catalog by SKU.",
    inputSchema: z.object({ sku: z.string().min(1) }),
    outputSchema: z.object({ sku: z.string(), name: z.string(), stock: z.number().int() }),
    annotations: {
      readOnlyHint: true, destructiveHint: false,
      idempotentHint: true, openWorldHint: false
    }
  }, async ({ sku }) => {
    const item = products.find(p => p.sku === sku.toUpperCase());
    if (!item) return { isError: true, content: [{ type: "text", text: `Unknown SKU: ${sku}` }] };
    return {
      content: [{ type: "text", text: JSON.stringify(item) }],
      structuredContent: item
    };
  });
  return server;
}
// src/node.ts
import { createServer } from "node:http";
import { createMcpHandler } from "@modelcontextprotocol/server";
import {
  localhostHostValidation, localhostOriginValidation, toNodeHandler
} from "@modelcontextprotocol/node";
import { buildServer } from "./mcp.js";

const mcp = createMcpHandler(buildServer); // 預設也提供 legacy stateless fallback
const nodeHandler = toNodeHandler(mcp);
const validHost = localhostHostValidation();
const validOrigin = localhostOriginValidation();

createServer(async (req, res) => {
  const path = new URL(req.url ?? "/", "http://localhost").pathname;
  if (path !== "/mcp") {
    res.writeHead(404).end("Not found");
    return;
  }
  if (!validHost(req, res) || !validOrigin(req, res)) return;
  await nodeHandler(req, res);
}).listen(8787, "127.0.0.1");
npx tsx src/node.ts

CLI 實測:doctor、list、inspect、call

本文用 mcp-explorer 0.3。它需要 Python 3.10+;以下用 uvx建立隔離環境並鎖定 PyPI 版本:

run_mcp() {
  uvx --from mcp-explorer==0.3 mcp-explorer "$@"
}

run_mcp doctor http://127.0.0.1:8787/mcp
run_mcp list http://127.0.0.1:8787/mcp
run_mcp inspect http://127.0.0.1:8787/mcp lookup_product
run_mcp call http://127.0.0.1:8787/mcp \
  lookup_product '{"sku":"KB-75"}' --json

實跑結果是新版與 legacy fallback 都健康;list 只列出 lookup_product(sku: string)inspect 顯示 JSON Schema 2020-12 與四個 annotations,call 回傳 {"sku":"KB-75","name":"75% mechanical keyboard","stock":18}。若改用官方 Inspector 2.0,請依官方設定mcp.json 明確設 "protocolEra":"modern";其 ad-hoc URL 模式目前預設 legacy,否則會在不知情下測錯世代。

把 Stateless MCP 部署到 Serverless/Edge

官方 handler 是 Web-standard { fetch } 介面,可直接用於 Cloudflare Workers、Deno 或 Bun;只有接 plain node:httpreqres 介面時,才需要 @modelcontextprotocol/node adapter。Worker 入口可重用同一個 buildServer,並在公開端點前加 Host/Origin allowlist:

import {
  createMcpHandler,
  hostHeaderValidationResponse,
  originValidationResponse
} from "@modelcontextprotocol/server";
import { buildServer } from "./mcp";

const mcp = createMcpHandler(buildServer);

export default {
  async fetch(request: Request): Promise<Response> {
    if (new URL(request.url).pathname !== "/mcp")
      return new Response("Not found", { status: 404 });

    const rejected =
      hostHeaderValidationResponse(request, ["mcp.example.com"]) ??
      originValidationResponse(request, ["app.example.com"]);
    return rejected ?? mcp.fetch(request);
  }
};

部署前把 mcp.example.com 換成實際 Worker hostname,app.example.com 換成允許的瀏覽器來源 hostname;否則 workers.dev 預覽或 localhost 會被擋下。Helpers 只比對 hostname、忽略 scheme/port,而沒有 Origin 的非瀏覽器請求會通過 Origin 檢查;它們是 DNS rebinding/Origin 防護,不是 authentication,也不會替你設定 CORS。

安裝 wrangler@4.118.0,再建立最小設定。2026-08-05 的乾淨安裝會由 Miniflare 帶入 undici@7.28.0,命中一項 高風險安全公告與數項中等風險公告;在 Wrangler 上游更新前,可用 npm override 鎖到已修正的 7.29.0

npm i -D wrangler@4.118.0
npm pkg set overrides.undici=7.29.0
npm install
npm audit

這是本文已跑過 typecheck、Wrangler local request 與 dry-run 的日期化 workaround,不代表可以永久忽略相依風險;上游發布含修正的 Miniflare/Wrangler 後,先移除 override、重新安裝並重跑測試與 audit。

// wrangler.jsonc
{
  "name": "stateless-mcp-lab",
  "main": "src/worker.ts",
  "compatibility_date": "2026-08-05"
}

Cloudflare Workers 部署流程,先跑 npx tsc --noEmitnpx wrangler deploy --dry-run,再執行 npx wrangler deploy。本文已在本機完成 typecheck、dry-run、Wrangler local request,以及透過官方 Node adapter 的端到端協定驗證;正式上線仍要逐平台確認串流、逾時、地區與 durable storage 限制。

OAuth/OIDC、Cache、Observability 怎麼補?

  • OAuth/OIDC:Auth 在 MCP 協定層是可選;端點一旦啟用,就由 MCP Server 扮演 OAuth resource server,發布 protected resource metadata 與 401 challenge。MCP Client 找到 Authorization Server 後採 PKCE S256,並驗證授權回應的 iss;Resource Server 則逐次檢查 Access Token 的期限、audience/resource 與 scope,禁止 token passthrough。OIDC 是 Authorization Server 可採的身份層,不是 transport 必備;而 createMcpHandler 不會替你驗證 Authorization header。詳見正式授權規格AI Agent 密鑰安全教學
  • Cache:正式快取規格要求 server/discovertools/listprompts/listresources/listresources/templates/listresources/read 的完整結果帶 ttlMscacheScopepublic 才能跨身份共用;private 必須按 authorization context 分區。這是 MCP Client 的 freshness hint,不是 CDN Cache-Control,TTL 也不保證資料絕不改變;含 input_requiredinputResponsesrequestState 的 MRTR 交換不得快取。
  • Observability:從追 Session 改成追 request,記 protocol era、method、resultType、狀態碼、延遲與 region;只記 allowlist 內的 tool/prompt name,URI 則正規化、雜湊或捨棄。用 OpenTelemetry 傳遞 trace context,但不要記 Bearer token、handle、taskId、原始工具輸入或 PII,也不要把高基數 taskId 當 metric label。延伸可讀Agent Observability 實作指南
Stateless MCP 遷移地圖,涵蓋協定、狀態、安全快取與可觀測性四層
遷移不是只刪 Session;協定、狀態、安全、觀測與雙協定上線策略要一起驗收。

Breaking-change audit:現有 Server 要查什麼?

  1. 搜尋 initializeinitializedMCP-Session-Id、記憶體 Session map 與 sticky routing。
  2. 把每次 request 的 _meta、必要 headers,以及每個 result 的 resultType 納入 contract test。
  3. 把跨次狀態改成顯式 handle;依撤銷與更新需求,選共享 durable store,或採可驗證的簽章/加密 self-contained state。具副作用工具另加 idempotency key/deduplication。
  4. resources/subscriberesources/unsubscribe 與清單/資源變更通知改採 subscriptions/listen;request SSE 斷線後不能用 Last-Event-ID resume,重試要送新的 request ID。更早的 HTTP+SSE transport 是 deprecated,不是被這條規則直接刪除。
  5. 把 server-initiated sampling、elicitation、roots 流程改成 MRTR;移除 pinglogging/setLevelnotifications/roots/list_changed。URL elicitation 也移除 elicitationIdnotifications/elicitation/complete,跨 retry 改由可選的 requestState 關聯。Final core 標記 Roots、Sampling、Logging deprecated;Elicitation 沒有被標記 deprecated。
  6. 依 GA 重做 OAuth issuer/audience、JSON Schema 2020-12、cache scope 與三個新錯誤碼測試。
  7. Tasks 先做 capability negotiation;逐一核對 Host 與 SDK 是否真的支援最終 extension。
  8. 先部署 dual-era canary、量測 modern/legacy/fallback,再升級 Clients;legacy 使用量歸零後,才切成 { legacy: "reject" }

預設 legacy: "stateless" 只會替每個舊請求建 fresh instance,不會復活原本的 sessionful 狀態。若舊服務真的依賴 Session,應用 isLegacyRequest 暫時分流到原 handler,新請求走 modern handler;依正式相容性矩陣,modern-only 與 legacy-only 本身不能直接互通。想把它放進完整 Agent 架構,可再讀Hermes Agent 教學

Dual-era Client 也不能看到錯誤就盲目降級:Streamable HTTP 只有在 400 且 body 為空、或不是已知 modern error 時才 fallback;探測更早的 deprecated HTTP+SSE transport 時另處理 4044054014035xx 與可辨識的 modern error 都不得觸發降級。stdio 則先送 server/discover,遇到非 modern error 或合理逾時才 fallback,不能只判斷 -32601

Stateless MCP 常見問題

1. Stateless MCP 代表不需要資料庫嗎?

不是。它移除協定層隱藏 Session;使用者資料、工作進度、handle 對應內容仍可放在耐久儲存。

2. 每次呼叫前都要 server/discover 嗎?

不用。Server 必須實作,Client 可以選擇是否呼叫,也可以用仍有效的 discovery cache;它不是新版 handshake。

3. 一個工具操作永遠只送一個 HTTP request 嗎?

不一定。工具 POST 本身可獨立處理,但 CLI 可能先 discovery/list;MRTR、Tasks、OAuth 與 subscriptions 也會增加 round trips。

4. 新版已經沒有 SSE 嗎?

仍然有。一般回應可用該 request 專屬 SSE,subscriptions/listen 也是長連線;只是舊版 standalone GET stream 與 resume 機制不再相同。

5. Mcp-Name 可以當授權依據嗎?

不能單獨使用。它適合 routing 與限流,但 Server 必須先驗證 header/body 一致,再依 token、scope 與實際參數做授權;Tasks 等 extension 也可定義自己的 Mcp-Name 規則。

6. State handle 可以做成 JWT 嗎?

可以,但要綁定用途、principal 與 expiry。若需即時撤銷或頻繁更新,Server-side KV 加 opaque handle 通常更好管理。

7. 新版 Client 都支援 Tasks 嗎?

不能假設。Tasks 是 opt-in extension,且官方文件與實作仍在收斂;必須逐一核對雙方 capabilities 與 SDK 版本。

8. 搬到 Edge 一定更快、更便宜嗎?

不一定。它省掉協定 Session 黏性,但冷啟動、token 驗證、遠端資料庫、長 SSE 與跨區流量仍可能主導成本。

新手先做這 5 步

  1. 挑一個同步、唯讀、無副作用工具作 canary。
  2. 升級 SDK,以 factory 建立 per-request Server。
  3. 用 modern CLI 跑 doctor、list、inspect、call 與 header mismatch。
  4. 補上每次授權、handle ownership、cache scope 與 request trace。
  5. 以 dual-era 觀察真實流量,確認 legacy 歸零後再關舊路徑。

結論:把隱藏狀態改成看得見的契約

Stateless MCP 最有價值的地方,不只是「比較容易丟上 Edge」,而是迫使系統清楚回答:這次請求用哪個版本、Client 具備什麼能力、狀態由誰保存、憑什麼存取、失敗後能不能安全重試。當這些答案都進入可測試的契約,Serverless 才是結果,不是口號。

想繼續把 MCP 接進完整工作流,可閱讀從零打造 AI Agent Harness;更多新手實作整理在 AlphaLab AI 專區免費課程

ALPHALAB 社群

有問題?來 Telegram 聊

和 Terry、編輯、其他網友一起討論這篇文章。提問、分享觀點,回覆更即時。

加入 Telegram 討論

📩 訂閱 AlphaLab 電子報

每週一封,第一時間收到新文章與投資觀察。

我們不會 spam,隨時可退訂。