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 移除了 initialize、initialized 與協定層 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"
}
}
}
}
protocolVersion 與 clientCapabilities 是必填;GA 把 clientInfo 改為 SHOULD。完成結果必須有 resultType: "complete",Server 也 SHOULD 在結果的 _meta 回傳 serverInfo。這正是 RC 範例最容易漏掉的地方。

Mcp-Method、Mcp-Name 到底是什麼?
Mcp-Method 是所有新版 Streamable HTTP request 都必須帶的標準 header,值要等於 JSON body 的 method。在 core 中,Mcp-Name 只在 tools/call、resources/read、prompts/get 必填,分別對應工具名、URI 或 prompt 名稱;Tasks extension 另要求 tasks/get、tasks/update、tasks/cancel 以 taskId 作為值。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/result、tasks/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:http 的 req/res 介面時,才需要 @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 --noEmit 與 npx 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/discover、tools/list、prompts/list、resources/list、resources/templates/list、resources/read的完整結果帶ttlMs與cacheScope。public才能跨身份共用;private必須按 authorization context 分區。這是 MCP Client 的 freshness hint,不是 CDNCache-Control,TTL 也不保證資料絕不改變;含input_required、inputResponses或requestState的 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 實作指南。

Breaking-change audit:現有 Server 要查什麼?
- 搜尋
initialize、initialized、MCP-Session-Id、記憶體 Session map 與 sticky routing。 - 把每次 request 的
_meta、必要 headers,以及每個 result 的resultType納入 contract test。 - 把跨次狀態改成顯式 handle;依撤銷與更新需求,選共享 durable store,或採可驗證的簽章/加密 self-contained state。具副作用工具另加 idempotency key/deduplication。
- 把
resources/subscribe、resources/unsubscribe與清單/資源變更通知改採subscriptions/listen;request SSE 斷線後不能用Last-Event-IDresume,重試要送新的 request ID。更早的 HTTP+SSE transport 是 deprecated,不是被這條規則直接刪除。 - 把 server-initiated sampling、elicitation、roots 流程改成 MRTR;移除
ping、logging/setLevel、notifications/roots/list_changed。URL elicitation 也移除elicitationId與notifications/elicitation/complete,跨 retry 改由可選的requestState關聯。Final core 標記 Roots、Sampling、Logging deprecated;Elicitation 沒有被標記 deprecated。 - 依 GA 重做 OAuth issuer/audience、JSON Schema 2020-12、cache scope 與三個新錯誤碼測試。
- Tasks 先做 capability negotiation;逐一核對 Host 與 SDK 是否真的支援最終 extension。
- 先部署 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 時另處理 404/405。401/403、5xx 與可辨識的 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 步
- 挑一個同步、唯讀、無副作用工具作 canary。
- 升級 SDK,以 factory 建立 per-request Server。
- 用 modern CLI 跑 doctor、list、inspect、call 與 header mismatch。
- 補上每次授權、handle ownership、cache scope 與 request trace。
- 以 dual-era 觀察真實流量,確認 legacy 歸零後再關舊路徑。
結論:把隱藏狀態改成看得見的契約
Stateless MCP 最有價值的地方,不只是「比較容易丟上 Edge」,而是迫使系統清楚回答:這次請求用哪個版本、Client 具備什麼能力、狀態由誰保存、憑什麼存取、失敗後能不能安全重試。當這些答案都進入可測試的契約,Serverless 才是結果,不是口號。
想繼續把 MCP 接進完整工作流,可閱讀從零打造 AI Agent Harness;更多新手實作整理在 AlphaLab AI 專區與免費課程。
