Discovery, Versions, and Statelessness
version คือวันที่
หัวข้อที่มีชื่อว่า “version คือวันที่”MCP กำหนด version ของ protocol ด้วย string รูปแบบ YYYY-MM-DD ซึ่งระบุวันที่ล่าสุดที่มีการเปลี่ยนแปลงแบบ backwards-incompatible การเพิ่มที่ compatible ไม่ทำให้ version ขยับ
| Revision | จุดเด่น |
|---|---|
2026-07-28 | ปัจจุบัน Stateless: ไม่มี initialize, ไม่มี session, _meta ต่อ request, server/discover, multi round-trip request, subscriptions/listen |
2025-11-25 | Legacy elicitation แบบ URL, tasks (ตอนนั้นยัง experimental), session ผ่าน Mcp-Session-Id |
2025-06-18 | Legacy header MCP-Protocol-Version, structured tool output, OAuth resource indicator |
2025-03-26 | Legacy เปิดตัว Streamable HTTP, deprecate HTTP+SSE |
2024-11-05 | Legacy รุ่นแรก; transport แบบ HTTP+SSE |
revision จะเป็น Draft (กำลังทำ), Current (พร้อมใช้ อาจเพิ่มของที่ compatible ได้อีก) หรือ Final (แช่แข็งแล้ว) feature แต่ละตัวยังถูกทำเครื่องหมาย Deprecated ได้ตาม lifecycle policy ที่รับประกันเวลาอย่างน้อยสิบสองเดือนก่อนถูกถอดออก
MCP เป็น stateless
หัวข้อที่มีชื่อว่า “MCP เป็น stateless”คุณสมบัติที่นิยาม revision ปัจจุบัน: ทุก request มีทุกอย่างที่จำเป็นในการประมวลผลมัน server ไม่อนุมานอะไรจาก request ก่อนหน้า แม้จะอยู่บน connection เดียวกัน
พูดให้เป็นรูปธรรม:
- ไม่มี handshake
initializerevision ยุค legacy (2025-11-25 และก่อนหน้า) เปิดทุก connection ด้วยinitialize→notifications/initializedแล้วจำ version และ capability ที่ negotiate ได้ ตอนนี้แต่ละ request พกio.modelcontextprotocol/protocolVersionและio.modelcontextprotocol/clientCapabilitiesมาใน_meta - ไม่มี session ระดับ protocol Streamable HTTP server ยุค legacy เคยออก header
Mcp-Session-Idตอนนี้ไม่มีแล้ว list endpoint ห้ามให้ผลต่างกันตาม connection - connection ที่เปิดอยู่ — แม้แต่ subprocess ของ stdio — ไม่ใช่ conversation client อาจสลับ request ที่ไม่เกี่ยวกันบน transport เดียว และ server ห้ามใช้ตัวตนของ process หรือ connection แทนความต่อเนื่อง
- state ที่ข้าม request (งานที่รันนาน, ตะกร้าสินค้า) ต้องอ้างอิงด้วย identifier ที่ชัดเจน ซึ่ง client ส่งมาทุกครั้ง
sequenceDiagram participant C as Client participant S as Server Note over C,S: legacy (2025-11-25 และก่อนหน้า) C->>S: initialize (version, capabilities) S-->>C: version, capabilities, session id C->>S: notifications/initialized C->>S: tools/call (พึ่ง session ที่จำไว้) Note over C,S: ปัจจุบัน (2026-07-28) C->>S: tools/call + _meta (version, capabilities) S-->>C: result + _meta (serverInfo) C->>S: resources/read + _meta (version, capabilities) S-->>C: result
ทำไมถึงไป stateless? remote server รันหลัง load balancer เป็น replica หลายตัว session affinity, sticky routing และ shared session store คือส่วนที่ยากที่สุดในการดูแล server ยุค legacy เมื่อไม่มี session replica ไหนก็ตอบ request ไหนก็ได้ และ replica ที่ crash เสียแค่ call ที่กำลังค้างอยู่ ซึ่ง client ก็แค่ retry
server/discover
หัวข้อที่มีชื่อว่า “server/discover”client รู้ทุกอย่างเกี่ยวกับ server ได้ใน request เดียว server ต้อง implement มัน; client จะเรียกหรือไม่ก็ได้
{ "jsonrpc": "2.0", "id": "discover-1", "method": "server/discover", "params": { "_meta": { "io.modelcontextprotocol/protocolVersion": "2026-07-28", "io.modelcontextprotocol/clientInfo": { "name": "ExampleClient", "version": "1.0.0" }, "io.modelcontextprotocol/clientCapabilities": {} } }}{ "jsonrpc": "2.0", "id": "discover-1", "result": { "resultType": "complete", "supportedVersions": ["2026-07-28"], "capabilities": { "tools": { "listChanged": true }, "resources": { "subscribe": true } }, "_meta": { "io.modelcontextprotocol/serverInfo": { "name": "ExampleServer", "version": "1.0.0" } }, "instructions": "This server provides weather and resource utilities.", "ttlMs": 3600000, "cacheScope": "public" }}| Field | การใช้ |
|---|---|
supportedVersions | เลือกหนึ่งในนี้สำหรับ request ต่อ ๆ ไป |
capabilities | primitive และ notification ที่ server รองรับ |
_meta.io.modelcontextprotocol/serverInfo | ชื่อและ version สำหรับแสดงผลและ log (ไม่ได้ verify) |
instructions | คำแนะนำแบบ free-text สำหรับ model ว่าจะใช้ server นี้ให้ดีอย่างไร — ที่ดีสำหรับใส่ “เรียก list_projects ก่อน create_issue เสมอ” |
ttlMs, cacheScope | cache result ได้หนึ่งชั่วโมง และ intermediary แชร์ได้ |
เหตุผลสองข้อที่จะเรียกมัน: เพื่อ แสดง ตัวตนและ feature ของ server ในครั้งเดียว แทนที่จะแยก probe tools/list, prompts/list และ resources/list และเพื่อเป็น backward-compatibility probe บน stdio ซึ่งไม่มี HTTP status ให้แยก server รุ่นใหม่ออกจากรุ่น legacy
negotiate version ต่อ request
หัวข้อที่มีชื่อว่า “negotiate version ต่อ request”ไม่มี handshake สำหรับ negotiate client ส่ง request โดยประกาศ version ที่ตัวเองต้องการ ถ้า server ไม่รองรับ server จะตอบด้วย UnsupportedProtocolVersionError:
{ "jsonrpc": "2.0", "id": 1, "error": { "code": -32022, "message": "Unsupported protocol version", "data": { "supported": ["2026-07-28", "2025-11-25"], "requested": "1900-01-01" } }}client เลือก version ที่รองรับร่วมกันจาก data.supported แล้ว retry หรือแสดง error ถ้าไม่มีตัวไหนตรงกัน ทั้งสองฝั่งรองรับหลาย version พร้อมกันได้ บน Streamable HTTP version ยังถูก mirror ไปที่ header MCP-Protocol-Version เพื่อให้ proxy route ตามมันได้โดยไม่ต้อง parse body
Capability
หัวข้อที่มีชื่อว่า “Capability”capability เป็น object ไม่ใช่ boolean เพื่อให้แต่ละตัวพก setting ได้
Server capability (return จาก server/discover):
{ "capabilities": { "tools": { "listChanged": true }, "resources": { "listChanged": true, "subscribe": true }, "prompts": { "listChanged": true }, "extensions": { "io.modelcontextprotocol/tasks": {} } }}tools,resources,prompts— มีอยู่แปลว่ารองรับ primitive นั้นlistChanged: trueแปลว่า server ส่ง list-changed notification ได้;resources.subscribe: trueแปลว่าส่ง update notification ราย resource ได้extensions— map จาก extension identifier (ตั้งชื่อเหมือน key ใน_metaและบังคับมี prefix) ไปยัง setting ของแต่ละ extension
Client capability (ส่งใน _meta ทุก request):
{ "io.modelcontextprotocol/clientCapabilities": { "elicitation": { "form": {}, "url": {} }, "extensions": { "io.modelcontextprotocol/ui": { "mimeTypes": ["text/html;profile=mcp-app"] } } }}elicitation— โหมดที่ client แสดงผลได้ object ว่าง{}หมายถึงเฉพาะโหมด form เพื่อความเข้ากันได้กับของเก่าextensions— รูปร่างเดียวกับฝั่ง server
server ห้าม ใช้ capability ที่ client ไม่ได้ประกาศใน request นี้ เพราะการประกาศเดินทางมากับทุก message กฎนี้จึงบังคับใช้ได้ง่ายมาก: ไม่ต้องจำ ไม่มี race
handle แทน session
หัวข้อที่มีชื่อว่า “handle แทน session”ไม่มี session แล้ว workflow หลายขั้นตอนทำงานอย่างไร? server ออก handle ที่ชัดเจน และ model พกมันต่อไปเป็น argument ธรรมดาของ tool:
{ "name": "create_basket", "arguments": {} }{ "content": [{ "type": "text", "text": "Created basket bsk_a1b2c3" }], "structuredContent": { "basket_id": "bsk_a1b2c3" }}{ "name": "add_item", "arguments": { "basket_id": "bsk_a1b2c3", "sku": "MUG-01" } }จากมุมของ wire basket_id เป็นแค่ string กฎการออกแบบที่ทำให้มันปลอดภัย:
- Authorization — สำหรับ server ที่มี authentication handle เป็น ชื่อ ไม่ใช่ capability: ตรวจว่าผู้เรียกเป็นเจ้าของมันทุก call สำหรับ server ที่ไม่มี authentication handle คือ bearer token ดังนั้นต้อง generate ด้วย entropy จริง (UUIDv4) และมีวันหมดอายุ
- Opacity — อย่า encode โครงสร้างที่ผู้เรียก parse หรือเดาได้
- Lifetime — ระบุใน description ของ tool ที่สร้าง handle ว่ามันอยู่ได้นานแค่ไหน (“ตะกร้าหมดอายุหลัง 24 ชั่วโมง”) เพื่อให้ model เห็น
- Expiry error — call ที่ใช้ handle ที่ไม่รู้จักหรือหมดอายุแล้ว ต้อง return tool execution error ที่บอกชัด เพื่อให้ model สร้างใหม่ได้
ยุคใหม่กับยุค legacy: ดูคร่าว ๆ
หัวข้อที่มีชื่อว่า “ยุคใหม่กับยุค legacy: ดูคร่าว ๆ”revision ปัจจุบันเรียก implementation ที่ใช้ metadata ต่อ request ว่า modern และแบบที่ใช้ handshake (2025-11-25 และก่อนหน้า) ว่า legacy implementation แบบ dual-era รองรับทั้งคู่ กฎง่าย ๆ: client รุ่นใหม่คุยกับ server รุ่น legacy จะล้มเหลว เว้นแต่ client เป็น dual-era; client รุ่น legacy คุยกับ server ที่เป็น modern อย่างเดียวจะล้มเหลวและไม่มีทาง upgrade ตัวเอง วิธีที่ client ตรวจจับยุค — server/discover บน stdio, ตรวจ body ของ 400 บน HTTP — อยู่ใน Transports → Backward Compatibility