Backward Compatibility and Protocol Eras
revision 2026-07-28 ทำให้ MCP เป็น stateless และนั่นแบ่งประวัติของ protocol ออกเป็นสองยุค คำที่ spec ใช้เอง:
- Modern — version ที่ส่ง protocol version, ตัวตน และ capability เป็น metadata ต่อ request:
2026-07-28และหลังจากนั้น - Legacy — version ที่สร้าง session ด้วย handshake
initialize:2025-11-25และก่อนหน้า - Dual-era — implementation ที่รองรับทั้งคู่
server ส่วนใหญ่ที่ deploy อยู่ทุกวันนี้ยังเป็น legacy ดังนั้น client ที่พูดได้แค่ modern จะใช้มันไม่ได้ และ legacy client ก็ใช้ server ที่เป็น modern อย่างเดียวไม่ได้ การรู้วิธีตรวจจับและเชื่อมสองยุคจึงเป็นทักษะที่ใช้จริง ไม่ใช่เกร็ดความรู้
ยุค legacy หน้าตาเป็นอย่างไร
หัวข้อที่มีชื่อว่า “ยุค legacy หน้าตาเป็นอย่างไร”| Legacy (2025-11-25 และก่อนหน้า) | Modern (2026-07-28) |
|---|---|
request initialize แล้วตามด้วย notifications/initialized; negotiate version และ capability ครั้งเดียว | version และ capability อยู่ใน _meta ทุก request; server/discover เป็น optional |
header Mcp-Session-Id ที่ server ออกให้ จบด้วย HTTP DELETE | ไม่มี session ระดับ protocol; ใช้ handle ที่ชัดเจนใน argument ของ tool |
HTTP GET เปิด SSE stream แยกสำหรับ message ที่ server เป็นฝ่ายเริ่ม | ไม่มี GET; POST subscriptions/listen return stream ที่เปิดค้างยาว |
server ส่ง sampling/createMessage, elicitation/create, roots/list เป็น request บน SSE ได้ | server ฝังมันไว้ใน InputRequiredResult (MRTR) |
SSE resume ได้ด้วย Last-Event-ID | resume ไม่ได้; ส่ง request ใหม่ |
resources/subscribe / unsubscribe, ping, logging/setLevel, notifications/roots/list_changed | ถูกถอดออก; ใช้ filter resourceSubscriptions และ logLevel ต่อ request ใน _meta |
result ไม่มี resultType | ทุก result มี resultType; client ถือว่าตัวที่ไม่มีเป็น "complete" |
ก่อนหน้านั้น 2024-11-05 ใช้ transport แยกชื่อ HTTP+SSE (GET stream ที่ประกาศ POST endpoint ผ่าน event endpoint) มันถูก deprecate ตั้งแต่ 2025-03-26 และเป็น Deprecated อย่างเป็นทางการภายใต้ feature lifecycle policy — อย่านำมาใช้
ตรวจจับยุคบน stdio
หัวข้อที่มีชื่อว่า “ตรวจจับยุคบน stdio”ไม่มี HTTP status code ให้อิง dual-era client จึง probe ด้วย server/discover โดยใส่ version ยุคใหม่ที่ต้องการใน _meta
sequenceDiagram
participant C as Dual-era client
participant S as Server
C->>S: server/discover (protocolVersion 2026-07-28)
alt DiscoverResult
S-->>C: supportedVersions, capabilities
Note over C: Modern เลือก version ที่รองรับ อยู่ในโหมด modern ต่อ
else Recognized modern error (e.g. -32022)
S-->>C: UnsupportedProtocolVersionError พร้อมรายการ supported
Note over C: Modern retry ด้วย version ในรายการ ห้าม fall back
else Other error or timeout
S-->>C: -32601 / -32602 / เงียบ
Note over C: Legacy ส่ง initialize แล้วทำต่อในโหมด legacy
end การ fallback ต้องไม่ ผูกกับ code ตัวใดตัวหนึ่ง legacy server ตอบ request ที่ไม่รู้จักก่อน initialize ด้วย error แบบ implementation-defined — มักเป็น -32601 หรือ -32602 — หรือไม่ตอบเลย แนะนำให้ probe แม้กับ client ที่เป็น modern อย่างเดียว: legacy server บางตัวไม่ตรวจว่า request มาหลัง initialize หรือไม่ และจะประมวลผล tools/call ที่คลุมเครือระหว่างยุคด้วย semantics แบบ legacy การ probe เปลี่ยนเรื่องนั้นให้เป็นความล้มเหลวที่คาดเดาได้ซึ่งคุณรายงานให้ผู้ใช้ทราบได้
ตรวจจับยุคบน HTTP
หัวข้อที่มีชื่อว่า “ตรวจจับยุคบน HTTP”บน Streamable HTTP client แค่ลองส่ง request แบบ modern ก่อน legacy server ไม่รู้ว่าจะทำอะไรกับมันและมักตอบ 400 Bad Request — แต่ server แบบ modern ก็ตอบ 400 เหมือนกันสำหรับ UnsupportedProtocolVersionError, MissingRequiredClientCapabilityError และความล้มเหลวจากการ validate header ดังนั้นเมื่อได้ 400 client ควร ตรวจ body ก่อนตัดสินใจ:
- body มี modern JSON-RPC error ที่รู้จัก → server เป็น modern retry ด้วย version ที่ประกาศไว้หรือแก้ request ห้าม fall back
- body ว่างหรือไม่ใช่ modern error ที่รู้จัก → fall back ไป
initializeแล้วทำต่อด้วย revision แบบ legacy
ถ้าจะรองรับ transport HTTP+SSE ที่ deprecate แล้วด้วย client ที่ได้ 400, 404 หรือ 405 โดยไม่มี modern error body จะส่ง GET ไปที่ URL; ถ้า SSE stream เปิดขึ้นและ event แรกคือ endpoint แสดงว่าเป็น server รุ่น 2024-11-05
ยุคเป็นคุณสมบัติของ server ไม่ใช่ของ request cache ผลไว้ตลอดอายุของ process (stdio) หรือ origin (HTTP) จะเก็บข้าม restart ของ configuration เดิมก็ได้ และ probe ใหม่ถ้าสมมติฐานที่ cache ไว้ล้มเหลวในภายหลัง
ตาราง compatibility
หัวข้อที่มีชื่อว่า “ตาราง compatibility”| Client | Server | ผลลัพธ์ |
|---|---|---|
| Modern | Modern | ใช้ได้ server/discover เป็น optional; version ไม่ตรงกันโผล่เป็น -32022 แล้ว client retry |
| Modern | Legacy | ล้มเหลว server อาจปฏิเสธ, เงียบ หรือประมวลผล method ที่คลุมเครือด้วย semantics แบบ legacy probe บน stdio เพื่อให้ล้มเหลวแบบคาดเดาได้ |
| Dual-era | Modern | ใช้ได้ probe ได้ DiscoverResult หรือ modern error; client อยู่ในโหมด modern |
| Dual-era | Legacy | ใช้ได้ probe ได้ error ที่ไม่ใช่ modern หรือ timeout; client fall back ไป initialize (และอาจไปถึง HTTP+SSE) |
| Legacy | Modern | ล้มเหลว stdio: initialize เป็น method ที่ไม่รู้จักและไม่มี _meta ถูกปฏิเสธด้วย error แบบ implementation-defined HTTP: ไม่มี header บังคับ → 400 legacy client ไม่มีทาง fall forward |
| Legacy | Dual-era | ใช้ได้ server ตอบ initialize และให้บริการตาม revision legacy ที่ negotiate ได้ |
| Legacy | Legacy | ใช้ได้ตาม revision แบบ legacy |
อ่านมันเป็นกฎการออกแบบ: เพื่อเข้าถึง peer ได้มากที่สุด client ควรเป็น dual-era และ server ควรเป็น dual-era ตราบเท่าที่ legacy client ยังสำคัญกับคุณ
server แบบ modern อย่างเดียวเจอ traffic แบบ legacy
หัวข้อที่มีชื่อว่า “server แบบ modern อย่างเดียวเจอ traffic แบบ legacy”ถ้าคุณเลือกไม่รองรับ legacy client ก็ควรปฏิเสธอย่างสุภาพ:
- HTTP
GETหรือDELETEมาที่ MCP endpoint →405 Method Not Allowed - header
Mcp-Session-Id→ เพิกเฉย; ห้ามออกหรือส่ง session ID กลับ - header
Last-Event-ID→ เพิกเฉย; stream resume ไม่ได้ - request
initializeบน transport ใดก็ตาม → ปฏิเสธ และ ระบุ protocol version ที่คุณรองรับ ใน error message legacy client ไม่มีกลไก fall forward ดังนั้น message นั้นอาจเป็นข้อมูล diagnostic เดียวที่ผู้ใช้จะได้เห็น
server แบบ dual-era
หัวข้อที่มีชื่อว่า “server แบบ dual-era”dual-era server เลือกพฤติกรรมจากวิธีที่ client เปิด: request ที่พก _meta แบบ modern จะได้รับบริการแบบ stateless; request initialize เลือก semantics แบบ legacy ที่ผูกกับ process ของ stdio หรือ session ของ HTTP ทั้งสองยุครันพร้อมกันบน endpoint เดียวได้
ใน TypeScript SDK เรื่องนี้เป็น option เดียวบน handler:
import { createMcpHandler, McpServer } from '@modelcontextprotocol/server';
const buildServer = () => new McpServer({ name: 'notes', version: '1.0.0' });
// default: also serve legacy clients, statelesslyconst handler = createMcpHandler(buildServer);
// modern only: reject legacy trafficconst strict = createMcpHandler(buildServer, { legacy: 'reject' });factory ตัวเดียวกันรองรับทั้งสองยุค คุณจึงลงทะเบียน tool ครั้งเดียว MCP Inspector ก็ negotiate ทั้งสองยุคได้ ทำให้มันเป็นวิธีที่เร็วที่สุดในการยืนยันว่า server รันอยู่ในโหมดไหนจริง ๆ — ดู testing and debugging