เจาะลึก MCP server แบบ stateless เปลี่ยนไปอย่างไรบ้าง
สรุปการเปลี่ยนแปลงใน MCP revision 2026-07-28 ที่ยกเลิก session และ handshake พร้อมผลกระทบต่อการตั้งค่า reverse proxy, health checks, timeout และระบบยืนยันตัวตน
MCP server แบบ stateless คืออะไร
MCP server แบบ stateless จะไม่เก็บสถานะของไคลเอนต์แต่ละรายไว้ระหว่างการร้องขอ ทุกคำขอจะประกอบด้วยเวอร์ชันของโปรโตคอล, ขีดความสามารถของไคลเอนต์ และข้อมูลรับรองที่เซิร์ฟเวอร์จำเป็นต้องใช้ในการตอบกลับ ดังนั้นกระบวนการใดๆ บนเครื่องใดก็ตามจึงสามารถตอบสนองต่อคำขอใดๆ ได้ MCP (Model Context Protocol ซึ่งเป็นรูปแบบการสื่อสารที่เอเจนต์ใช้เพื่อเข้าถึงเครื่องมือต่างๆ) ได้กำหนดให้สิ่งนี้เป็นกฎใน revision 2026-07-28 ซึ่งได้ยกเลิกการทำ handshake แบบ initialize และ HTTP session ที่เคยอยู่เบื้องล่างออกไป
นั่นคือประเด็นสำคัญทั้งหมดในการปฏิบัติงาน เซิร์ฟเวอร์ที่ไม่เก็บข้อมูลแยกตามไคลเอนต์สามารถวางไว้หลัง load balancer ทั่วไปโดยไม่ต้องพึ่งพา session affinity สามารถรีสตาร์ทระหว่างการ deploy ได้โดยไม่ทำให้ไคลเอนต์หลุด และสามารถรันเป็นกระบวนการที่เหมือนกันสี่กระบวนการแทนที่จะเป็นกระบวนการเดียวได้ ในขณะที่เซิร์ฟเวอร์แบบเน้น session ไม่สามารถทำสิ่งเหล่านี้ได้หากไม่มีกลไกเพิ่มเติม
Model Context Protocol เป็นโปรโตคอลแบบ stateless: ข้อมูลทั้งหมดที่จำเป็นในการประมวลผลคำขอจะถูกบรรจุอยู่ในตัวคำขอนั้นเอง เซิร์ฟเวอร์จะประมวลผลแต่ละคำขออย่างเป็นอิสระต่อกัน ไม่ควรอนุมานสถานะใดๆ จากคำขอก่อนหน้า แม้จะเป็นคำขอที่มาจาก connection หรือ stream เดียวกันก็ตาม
คำว่า stateless ไม่ได้หมายความว่าเซิร์ฟเวอร์ของคุณจะไม่จัดเก็บข้อมูลใดๆ เลย ฐานข้อมูล, คิว และแคชของคุณยังคงอยู่ตามปกติ แต่หมายความว่าตัว โปรโตคอล จะไม่แบกรับสถานะใดๆ ไว้บน connection ดังนั้นเซิร์ฟเวอร์จึงต้องไม่ถือว่า connection, กระบวนการ หรือ socket ที่เปิดอยู่ เป็นตัวแทนของ "ไคลเอนต์รายนี้ที่กำลังอยู่ระหว่างการสนทนา"
สิ่งที่ถูกนำออกใน revision 2026-07-28
2026-07-28 คือ revision ปัจจุบันของข้อกำหนด ณ เดือนสิงหาคม 2026 เมื่อเปรียบเทียบกับ 2025-11-25 พบว่ามีการนำสิ่งที่เคยมีอยู่เพื่อรองรับ session ออกไป 5 รายการ ดังนี้:
- คำขอ
initializeและการแจ้งเตือนnotifications/initializedโดยไม่มีการทำ handshake ใดๆ ทั้งสิ้น (SEP-2575) - ส่วนหัว
Mcp-Session-Idและการยุติ session ด้วย HTTPDELETE(SEP-2567) - สตรีม HTTP
GETแบบแยกส่วนที่เซิร์ฟเวอร์ใช้ส่งการแจ้งเตือน โดยถูกแทนที่ด้วยsubscriptions/listenซึ่งเป็น POST ปกติที่การตอบกลับจะเป็นสตรีมแบบ long-lived - ความสามารถในการกลับมาทำงานต่อของสตรีม SSE (server-sent events) ส่วนหัว
Last-Event-IDและ ID ประจำเหตุการณ์ถูกนำออกไป ดังนั้นหากสตรีมขาดหายไป คำขอที่กำลังดำเนินการอยู่จะสูญหาย และไคลเอนต์ต้องส่งคำขอใหม่พร้อม ID คำขอใหม่ ping,logging/setLevelและnotifications/roots/list_changedโดยปัจจุบันระดับของ log จะเป็นฟิลด์ประจำคำขอ คือio.modelcontextprotocol/logLevelใน_meta
มีการเพิ่มหนึ่งเมธอดเข้ามาซึ่งเซิร์ฟเวอร์ทุกตัวต้องรองรับ โดย server/discover จะส่งกลับเวอร์ชันของโปรโตคอล ความสามารถ และตัวตนของเซิร์ฟเวอร์ในการเรียกครั้งเดียว ซึ่งถือเป็นสิ่งที่ใกล้เคียงกับ handshake มากที่สุดที่ยังคงเหลืออยู่ และไคลเอนต์ไม่จำเป็นต้องเรียกใช้งานเมธอดนี้ก็ได้
เหตุผลที่ session transport ใช้งานใน production ได้ยาก
ใน 2025-11-25 และเวอร์ชันก่อนหน้า เซิร์ฟเวอร์สามารถสร้าง session ID ขึ้นมาในขั้นตอน initialization และส่งกลับไปใน header Mcp-Session-Id ในการตอบกลับ InitializeResult จากนั้นไคลเอนต์จะต้องส่ง header ดังกล่าวในทุกคำขอที่ตามมา เวอร์ชันของโปรโตคอลที่ตกลงกันไว้และความสามารถของไคลเอนต์จะถูกเก็บไว้ในหน่วยความจำของเซิร์ฟเวอร์โดยอ้างอิงจาก ID นั้น ซึ่งทางเลือกแต่ละอย่างล้วนมีต้นทุนในการดำเนินงาน
- การรีสตาร์ทเซิร์ฟเวอร์จะทำให้ตาราง session หายไปทั้งหมด ข้อกำหนดระบุให้เซิร์ฟเวอร์ต้องตอบกลับคำขอใดก็ตามที่ส่ง session ID ที่ไม่มีอยู่จริงมาด้วย
404 Not Foundและกำหนดให้ไคลเอนต์ต้องเริ่มต้นใหม่ด้วยInitializeRequestใหม่ การ deploy แต่ละครั้งจึงกลายเป็นการบังคับให้ไคลเอนต์ที่เชื่อมต่ออยู่ทั้งหมดต้องเชื่อมต่อใหม่ - replica ตัวที่สองจะไม่ทราบข้อมูล session ของ replica ตัวแรก การขยายระบบ (scaling out) จึงหมายถึงการต้องใช้ sticky routing ที่ load balancer หรือต้องมี session store ส่วนกลางที่ทุก replica ต้องอ่านในทุกคำขอ
- ตาราง session คือหน่วยความจำที่เพิ่มขึ้นตามจำนวนไคลเอนต์ที่ไม่ได้ใช้งาน
DELETEเป็นสิ่งที่เลือกทำได้ (optional) และไคลเอนต์ที่ปิดการเชื่อมต่อโดยไม่ส่งข้อมูลนี้จะทิ้งรายการค้างไว้ในหน่วยความจำ - ผลลัพธ์ของรายการอาจแตกต่างกันไปในแต่ละการเชื่อมต่อ ทำให้การทำ caching หน้าเซิร์ฟเวอร์ไม่ปลอดภัย
การยกเลิกการใช้ session ช่วยขจัดปัญหาทั้งสี่ประการนี้ไปพร้อมกัน นี่คือการเปลี่ยนแปลงที่ควรทำความเข้าใจก่อนที่คุณจะปรับแต่งค่าคอนฟิกใดๆ
สิ่งที่ทุกคำขอต้องมีในปัจจุบัน
คำขอ POST แต่ละรายการไปยัง MCP endpoint จะทำงานแยกจากกัน โดยเวอร์ชันของโปรโตคอลและความสามารถของไคลเอนต์จะถูกส่งไปในส่วนเนื้อหาของคำขอภายใต้ _meta และฟิลด์ที่เลือกจะถูกคัดลอกไปยัง HTTP headers เพื่อให้ตัวกลางสามารถทำ routing ได้โดยไม่ต้องวิเคราะห์ JSON
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: get_weather
Authorization: Bearer <access token>
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "get_weather",
"arguments": {"location": "Seattle, WA"},
"_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientInfo": {"name": "ExampleClient", "version": "1.0.0"},
"io.modelcontextprotocol/clientCapabilities": {}
}
}
}io.modelcontextprotocol/protocolVersion และ io.modelcontextprotocol/clientCapabilities เป็นสิ่งที่จำเป็นต้องมีในทุกคำขอ ส่วน clientInfo ไม่จำเป็นต้องมี แต่ไคลเอนต์ควรส่งมาด้วย คำขอที่ขาดฟิลด์บังคับถือว่ามีรูปแบบไม่ถูกต้อง ดังนั้นเซิร์ฟเวอร์ต้องปฏิเสธคำขอดังกล่าวด้วย JSON-RPC error -32602 และ HTTP 400 Bad Request
Header Mcp-Method เป็นสิ่งที่จำเป็นต้องมีในทุกคำขอ และ Mcp-Name จำเป็นต้องมีใน tools/call, resources/read และ prompts/get ค่าใน header ต้องตรงกับข้อมูลในส่วนเนื้อหา และเซิร์ฟเวอร์ที่ประมวลผลเนื้อหาต้องปฏิเสธหากข้อมูลไม่ตรงกันด้วย 400 Bad Request และรหัสข้อผิดพลาด -32020, HeaderMismatch กฎนี้มีไว้เนื่องจาก load balancer ที่ทำ routing โดยดูจาก header และเซิร์ฟเวอร์ที่ประมวลผลข้อมูลในเนื้อหาเป็นแหล่งข้อมูลที่แตกต่างกัน หากคุณทำ routing หรือจำกัดอัตราการเข้าถึง (rate-limit) โดยอ้างอิงจาก header เหล่านี้ ให้ตรวจสอบ MCP-Protocol-Version ก่อน เนื่องจากเวอร์ชันก่อนหน้านี้ไม่เคยมีการตรวจสอบความถูกต้องของ header เทียบกับเนื้อหา ดังนั้นค่าใน header ของเวอร์ชันเหล่านั้นจึงไม่น่าเชื่อถือ
ความไม่สอดคล้องกันของเวอร์ชันในปัจจุบันถือเป็นข้อผิดพลาดปกติในระดับคำขอ แทนที่จะเป็นการล้มเหลวของการทำ handshake เซิร์ฟเวอร์ที่ไม่รองรับเวอร์ชันที่ร้องขอจะตอบกลับด้วย 400 Bad Request พร้อมข้อผิดพลาด -32022, UnsupportedProtocolVersion และระบุเวอร์ชันที่รองรับไว้ใน data.supported โดยไคลเอนต์จะเลือกหนึ่งเวอร์ชันจากรายการนั้นแล้วลองส่งคำใหม่อีกครั้ง
สถานะไปอยู่ที่ไหน: โทเค็น, เคอร์เซอร์ และการสมัครรับข้อมูล
สถานะไม่ได้หายไปไหน แต่ย้ายไปอยู่ในจุดที่คุณสามารถมองเห็นและบันทึก log ได้
ข้อมูลประจำตัวจะถูกส่งไปกับทุกคำขอ เนื่องจากไม่มี session สำหรับผูกตัวตนเข้ากับเซสชัน ดังนั้น access token จึงต้องติดไปกับทุกการเรียก HTTP และถูกตรวจสอบความถูกต้องในทุกครั้ง รายละเอียดอยู่ในส่วนการยืนยันตัวตนด้านล่าง
เคอร์เซอร์ต้องระบุตำแหน่งของตัวเอง การแบ่งหน้า (pagination) บน tools/list, resources/list, prompts/list และ resources/templates/list จะใช้สตริงเคอร์เซอร์แบบ opaque ซึ่งไคลเอนต์ห้ามทำการ parse หรือแก้ไขโดยเด็ดขาด ในเซิร์ฟเวอร์แบบ single-process การเก็บค่า offset ไว้ในหน่วยความจำโดยอ้างอิงจาก session เป็นเรื่องปกติ แต่เมื่อไม่มี session เคอร์เซอร์จะต้องมีข้อมูลเพียงพอให้ replica ใดก็ตามสามารถดำเนินการแสดงรายการต่อได้ ดังนั้นให้เข้ารหัสตำแหน่งไว้ภายในเคอร์เซอร์แล้วทำการเซ็นกำกับ (sign) หรือเก็บไว้ในที่จัดเก็บข้อมูลที่ทุก replica เข้าถึงร่วมกัน หากเคอร์เซอร์ไม่ถูกต้องควรส่งคืน -32602 การเซ็นกำกับมีความจำเป็นเพราะเคอร์เซอร์แบบ opaque ยังคงเป็นอินพุตที่ไคลเอนต์ส่งมา ซึ่งโค้ดของคุณต้องถอดรหัสและเชื่อถือ
การสมัครรับข้อมูล (subscriptions) ขึ้นอยู่กับคำขอ ไม่ใช่การเชื่อมต่อ ไคลเอนต์ที่ต้องการรับการแจ้งเตือนการเปลี่ยนแปลงจะส่ง subscriptions/listen พร้อมตัวกรองระบุประเภทที่ต้องการ: toolsListChanged, promptsListChanged, resourcesListChanged และ resourceSubscriptions เซิร์ฟเวอร์จะตอบกลับด้วย notifications/subscriptions/acknowledged และคง stream ของการตอบกลับนั้นไว้ หาก stream หลุด เซิร์ฟเวอร์จะไม่เก็บสถานะใดๆ ไว้ และไคลเอนต์จะต้องส่ง subscriptions/listen ใหม่อีกครั้งเพื่อรับข้อมูลต่อ
สถานะของแอปพลิเคชันที่ข้ามการเรียก (cross-call) จะกลายเป็น handle ที่ชัดเจน เมื่อเซิร์ฟเวอร์จำเป็นต้องจดจำข้อมูลระหว่างการเรียก คำตอบตามข้อกำหนดคือการใช้ตัวระบุที่เซิร์ฟเวอร์สร้างขึ้นแล้วส่งกลับไปในฐานะอาร์กิวเมนต์ปกติของเครื่องมือ ข้อมูลนี้จะปรากฏใน tool schema สามารถบันทึก log ได้ และไม่ได้ถูกอนุมานจากการเชื่อมต่อ เซิร์ฟเวอร์ที่มีข้อมูลเฉพาะของผู้ใช้จริง เช่น เซิร์ฟเวอร์อีเมล MCP ที่โฮสต์เอง จะใช้รูปแบบนี้แทนการใช้ session โดยตัวระบุกล่องจดหมายหรือฉบับร่างจะเป็นอาร์กิวเมนต์ของเครื่องมือ ทำให้ replica ใดก็ตามสามารถรับช่วงการเรียกถัดไปได้ เครื่องมือจำนวนมากไม่จำเป็นต้องใช้ handle เลย เช่น เครื่องมือค้นหาที่ทำงานบน SearXNG instance ของคุณเอง ซึ่งรับคำค้นหาและส่งผลลัพธ์กลับ โดยไม่มีข้อมูลใดที่ต้องรอการเรียกครั้งถัดไปเพื่อดำเนินการต่อ และไม่จำเป็นต้องสนใจว่า replica ใดเป็นผู้ตอบคำถามนั้น
การปรับใช้: reverse proxy, การตั้งค่า timeout และการตรวจสอบสถานะ (health check)
MCP endpoint เป็น path หนึ่งที่รับคำขอแบบ POST โดยทั่วไปทราฟฟิกส่วนใหญ่จะเป็นคำขอสั้นๆ และการตอบกลับแบบ JSON ซึ่ง proxy ทั่วไปสามารถจัดการได้ แต่ข้อยกเว้นคือการตอบกลับแบบ streaming ซึ่งค่าเริ่มต้นของ proxy มักจะขัดขวางการทำงาน นี่คือส่วนที่จะเปลี่ยนไปเมื่อคุณย้ายจากการทดสอบบนแล็ปท็อปไปสู่ การรัน MCP server บน VPS
location /mcp {
proxy_pass http://127.0.0.1:8080;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_buffering off;
proxy_read_timeout 1h;
proxy_send_timeout 1h;
}proxy_buffering off มีความสำคัญเนื่องจาก Nginx จะทำ buffering การตอบกลับที่ผ่าน proxy โดยค่าเริ่มต้น ซึ่งจะกักเก็บ SSE events ไว้จนกว่า buffer จะเต็มหรือการตอบกลับสิ้นสุดลง ข้อกำหนดระบุให้เซิร์ฟเวอร์ส่ง X-Accel-Buffering: no ในการตอบกลับแบบ SSE ซึ่ง Nginx จะเคารพ header ดังกล่าว ดังนั้นเซิร์ฟเวอร์ที่ถูกต้องจะแจ้งให้ proxy ทราบถึงสิ่งที่ควรทำด้วยตัวเอง อย่างไรก็ตาม คุณควรตั้งค่า directive นี้ไว้ด้วยเพราะเป็นส่วนที่คุณควบคุมได้
proxy_read_timeout มีค่าเริ่มต้นที่ 60 วินาที หากมี subscriptions/listen stream ที่ไม่มีการเคลื่อนไหวนานกว่านั้น Nginx จะเป็นผู้ตัดการเชื่อมต่อ ไม่ใช่เซิร์ฟเวอร์ของคุณ ส่งผลให้ log แสดงว่า process ยังทำงานปกติ แต่ฝั่ง client จะเห็นว่า stream ถูกตัด ให้เพิ่มค่านี้เฉพาะในส่วนของ MCP location เท่านั้น ไม่ใช่ทั้งเซิร์ฟเวอร์ นอกจากนี้ เซิร์ฟเวอร์ควรส่ง SSE comment line (บรรทัดที่ขึ้นต้นด้วยเครื่องหมาย colon) เพื่อเป็น keep-alive ในช่วงที่ไม่มีข้อมูล ซึ่งจะช่วยป้องกันไม่ให้ตัวกลางตัดการเชื่อมต่อ stream
Caddy ต้องการการตั้งค่าน้อยกว่า โดยจะทำ buffering เพียงบางส่วนเพื่อประสิทธิภาพในการส่งข้อมูลและจะ flush ข้อมูลทันทีเมื่อการตอบกลับมี Content-Type: text/event-stream ดังนั้น streaming จึงทำงานได้โดยไม่ต้องเพิ่ม directive พิเศษ
mcp.example.com {
reverse_proxy 127.0.0.1:8080 {
health_uri /healthz
health_interval 10s
}
}โปรดสังเกตว่า health check ชี้ไปที่ใด อย่าชี้การตรวจสอบแบบ active ไปที่ MCP endpoint ด้วย GET เนื่องจากเซิร์ฟเวอร์ที่รองรับเฉพาะ revision นี้จะตอบกลับด้วย 405 Method Not Allowed ต่อ GET และ DELETE ในขณะที่วิธีตรวจสอบสถานะเริ่มต้นของ Caddy คือ GET ซึ่งจะทำให้ proxy ระบุว่า backend ที่ทำงานปกติกลายเป็นสถานะ down ให้สร้าง path ธรรมดาเช่น /healthz สำหรับ proxy และตรวจสอบโปรโตคอลแยกต่างหากด้วย POST
curl -sS https://mcp.example.com/mcp \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-H 'MCP-Protocol-Version: 2026-07-28' \
-H 'Mcp-Method: server/discover' \
-d '{"jsonrpc":"2.0","id":"health-1","method":"server/discover","params":{"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientCapabilities":{}}}}'การได้รับ 200 พร้อมรายการ supportedVersions หมายความว่า process กำลังทำงานและสื่อสารด้วยโปรโตคอลได้ การได้รับ 404 พร้อม JSON-RPC error -32601 หมายความว่า process ทำงานอยู่แต่ไม่รองรับ server/discover ซึ่งเป็นสิ่งที่ 2026-07-28 server ทุกตัวต้องมี การได้รับ 400 พร้อม -32022 หมายความว่าตัวตรวจสอบของคุณร้องขอเวอร์ชันที่ build นี้ไม่รองรับ ซึ่งเป็นสิ่งที่คุณต้องการตรวจพบหลังจากอัปเกรด dependency สำหรับ Nginx รุ่น open source จะไม่มี active health check ให้ใช้ passive max_fails และ fail_timeout บน upstream แทน และรันการตรวจสอบโปรโตคอลผ่านระบบ monitoring ของคุณ
การทำ rolling restart ในตอนนี้จะทำให้คุณเสียเฉพาะคำขอที่กำลังประมวลผลอยู่เท่านั้น ให้ทำการ drain, รอให้ POST ที่ค้างอยู่เสร็จสิ้น, เริ่ม process ใหม่ แล้ว client จะส่งคำขอซ้ำในส่วนที่ล้มเหลว สิ่งเดียวที่คุณยังคงสูญเสียคือ subscriptions/listen stream ที่เปิดค้างไว้ เนื่องจาก stream นั้นเป็นการเชื่อมต่อสดกับ process เฉพาะเจาะจง การทำให้ระบบเป็น stateless ช่วยลด session affinity ได้ แต่ไม่ได้ลด connection affinity สำหรับ stream ที่เปิดอยู่ และไม่มีกฎการ routing ใดแก้ไขปัญหานี้ได้ Client สามารถแยกแยะความแตกต่างได้: stream ที่จบลงด้วยผลลัพธ์ subscriptions/listen ว่างเปล่าถือว่าปิดการเชื่อมต่ออย่างสมบูรณ์ ส่วน stream ที่จบลงโดยไม่มีผลลัพธ์ถือว่าถูกตัด ซึ่ง client อาจใช้เป็นเหตุผลในการเชื่อมต่อใหม่
การทำ caching สามารถทำได้เป็นครั้งแรก ผลลัพธ์จาก list method จะมี ttlMs และ cacheScope และ cacheScope: "public" จะแจ้งให้ตัวกลางที่ใช้ร่วมกันทราบว่าสามารถ cache การตอบกลับได้ ซึ่งปลอดภัยเพราะผลลัพธ์ของ list ไม่แปรผันตามการเชื่อมต่ออีกต่อไป อันเป็นผลโดยตรงจากการยกเลิกการใช้ sessions
เหตุใดการยืนยันตัวตนจึงเปลี่ยนไปเมื่อไม่มี session
เมื่อมี session เรามักจะเผลอทำการยืนยันตัวตนเพียงครั้งเดียวที่ initialize แล้วใช้ session ID เป็นหลักฐานสำหรับทุกอย่างหลังจากนั้น session ID ที่ใช้ในลักษณะนี้เปรียบเสมือน bearer credential ที่ไม่มีการระบุกลุ่มผู้รับ (audience) ไม่มีวันหมดอายุ และไม่มีช่องทางในการเพิกถอนสิทธิ์ ซึ่งเซิร์ฟเวอร์ของคุณเป็นผู้สร้างขึ้นเอง การยกเลิกการใช้ session จะช่วยตัดทางลัดดังกล่าวออกไป และทำให้การทดแทนด้วยวิธีใหม่มีความเข้มงวดมากขึ้น
เซิร์ฟเวอร์ MCP ที่มีการป้องกันจะทำหน้าที่เป็น OAuth 2.1 resource server ทุกคำขอ HTTP จากไคลเอนต์จะต้องแนบ Authorization: Bearer <access token> มาด้วย และเซิร์ฟเวอร์จะตรวจสอบความถูกต้องของโทเค็นในทุกคำขอ การตรวจสอบนี้รวมถึงการตรวจสอบกลุ่มผู้รับ (audience) ด้วย โดยเซิร์ฟเวอร์ต้องยืนยันว่าโทเค็นนั้นถูกออกให้สำหรับเซิร์ฟเวอร์โดยเฉพาะ ตามมาตรฐาน RFC 8707 (Resource Indicators for OAuth 2.0) และต้องไม่ยอมรับหรือส่งต่อโทเค็นที่ตั้งใจให้ใช้กับบริการอื่น ไคลเอนต์จะร้องขอสิทธิ์สำหรับกลุ่มผู้รับที่ถูกต้องโดยการส่งพารามิเตอร์ resource พร้อมกับ canonical URI ของเซิร์ฟเวอร์
กระบวนการค้นหา (discovery) จะเริ่มจาก challenge เมื่อมีคำขอเข้ามาโดยไม่มีโทเค็นที่ใช้งานได้ เซิร์ฟเวอร์จะตอบกลับด้วย 401 Unauthorized
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer resource_metadata="https://mcp.example.com/.well-known/oauth-protected-resource",
scope="files:read"ไคลเอนต์จะอ่าน resource_metadata จากนั้นจึงดึงเอกสารดังกล่าว (RFC 9728, OAuth 2.0 Protected Resource Metadata ซึ่งเซิร์ฟเวอร์ MCP ต้องรองรับ) เพื่อค้นหา authorization server และดำเนินการตาม flow หากโทเค็นที่ถูกต้องมีสิทธิ์ไม่เพียงพอ เซิร์ฟเวอร์จะตอบกลับด้วย 403 Forbidden พร้อมกับ error="insufficient_scope" และขอบเขต (scopes) ที่จำเป็นสำหรับการดำเนินการนั้น
ผลลัพธ์ที่ตามมาสำหรับการใช้งานมีสองประการ ประการแรก การตรวจสอบโทเค็นจะเกิดขึ้นในทุกคำขอแทนที่จะเป็นเพียงครั้งเดียวต่อ session ดังนั้นการส่งข้อมูลไป-กลับผ่านเครือข่ายไปยัง introspection endpoint ในทุกการเรียกใช้จะส่งผลต่อค่า latency ของคุณ: ควรเลือกใช้โทเค็นที่สามารถตรวจสอบได้ในเครื่องผ่านลายเซ็น (signature), กลุ่มผู้รับ (audience) และวันหมดอายุ หรือใช้วิธีแคชผลการตรวจสอบไว้ในช่วงเวลาสั้นๆ โดยใช้โทเค็นเป็นคีย์ และประการที่สอง เนื่องจากไม่มี session ที่คอยเก็บข้อมูลระบุตัวตน การให้สิทธิ์ (authorization) จึงต้องคำนวณจากโทเค็นในทุกการเรียกใช้ วิธีนี้มีความโปร่งใสมากกว่าโมเดลแบบ session และสอดคล้องกับแนวทางปฏิบัติทั่วไปในการเก็บข้อมูลรับรอง (credentials) ไว้นอกกระบวนการทำงานของ agent ซึ่งได้อธิบายไว้ใน การเก็บความลับไว้นอก AI agent
สิ่งที่ถูกต้องเกี่ยวกับรุ่นนี้และสิ่งที่ไม่ถูกต้อง
เนื้อหาทั้งหมดข้างต้นอธิบายถึงรุ่น 2026-07-28 เท่านั้น ไม่ได้อธิบายถึง MCP ตลอดไป และไม่ได้อธิบายถึงเซิร์ฟเวอร์ที่คุณติดตั้งใช้งานเมื่อปีที่แล้ว
ไคลเอนต์และเซิร์ฟเวอร์ที่ใช้รุ่น 2025-11-25 หรือเก่ากว่านั้นยังคงใช้รูปแบบการทำ handshake แบบเดิม ข้อกำหนดเรียกการแก้ไขเหล่านั้นว่ารุ่นเก่า (legacy) และเรียกการแก้ไขแบบ per-request-metadata ว่ารุ่นใหม่ (modern) เซิร์ฟเวอร์ที่รองรับเฉพาะรุ่นนี้เมื่อพบกับไคลเอนต์รุ่นเก่า ควรตอบกลับด้วย 405 Method Not Allowed ไปยัง GET หรือ DELETE ที่ MCP endpoint, เพิกเฉยต่อ header Mcp-Session-Id ใดๆ โดยไม่ต้องสร้างหรือส่งค่ากลับ และเพิกเฉยต่อ Last-Event-ID เนื่องจากสตรีมไม่สามารถกลับมาทำงานต่อได้ (resumable) เซิร์ฟเวอร์ที่รองรับทั้งสองยุคอาจให้บริการทั้งสองแบบบน endpoint เดียวกัน โดยคำขอที่มี _meta แบบใหม่จะถูกให้บริการแบบไร้สถานะ (stateless) และคำขอแบบ initialize จะเลือกใช้รูปแบบเซสชันแบบเดิม
ดังนั้น โปรดตรวจสอบสตริงรุ่นก่อนที่จะเชื่อถือข้อมูลใดๆ ในหน้านี้ หาก SDK ของคุณยังคงส่ง initialize อยู่ เซสชันยังคงมีผลจริงสำหรับการติดตั้งใช้งานของคุณ และปัญหาในรูปแบบเซสชันที่กล่าวถึงข้างต้นยังคงเป็นสิ่งที่คุณต้องจัดการเอง เช่นเดียวกันกับฝั่งไคลเอนต์ กระบวนการ agent บนเครื่องของคุณเอง เช่น การตั้งค่าใน การรัน coding agent บน VPS จะเป็นแบบไร้สถานะในความหมายนี้ก็ต่อเมื่อไลบรารีที่ใช้รองรับรุ่นใหม่เท่านั้น โปรดอ่านเวอร์ชันที่ runtime ของคุณเจรจาต่อรอง จากนั้นอ่านรุ่นของข้อกำหนดที่ตรงกัน และถือว่าหน้านี้อธิบายถึงรุ่นที่ระบุไว้รุ่นหนึ่งเท่านั้น ไม่ใช่โปรโตคอลโดยรวมทั้งหมด
FAQ
การที่ MCP server เป็นแบบ stateless หมายความว่าฉันไม่สามารถจัดเก็บข้อมูลใดๆ ได้เลยใช่หรือไม่?
ไม่จริง คำว่า stateless อธิบายถึงตัวโปรโตคอล ไม่ใช่ตัวแอปพลิเคชันของคุณ ฐานข้อมูล คิว และแคชยังคงทำงานได้ตามปกติเหมือนเดิม สิ่งที่เปลี่ยนไปคือสถานะที่ครอบคลุมการเรียกใช้งานหลายครั้งจะต้องถูกอ้างอิงด้วยตัวระบุที่ชัดเจนซึ่งไคลเอนต์ส่งมาในทุกคำขอ เช่น handle ที่เซิร์ฟเวอร์สร้างขึ้นในอาร์กิวเมนต์ของเครื่องมือ สิ่งที่คุณไม่สามารถทำได้คือการอนุมานบริบทจากการเชื่อมต่อ เพราะข้อกำหนดระบุว่าเซิร์ฟเวอร์ต้องไม่พึ่งพาคำขอก่อนหน้าผ่านการเชื่อมต่อเดิมเพื่อกำหนดความสามารถ เวอร์ชันของโปรโตคอล หรือตัวตนของไคลเอนต์ เนื่องจากทุกคำขอจะระบุข้อมูลเหล่านั้นไว้ใน _meta อยู่แล้ว
ฉันยังจำเป็นต้องใช้ sticky sessions บน load balancer ของฉันหรือไม่?
ไม่จำเป็นสำหรับคำขอทั่วไป ภายใต้การแก้ไข 2026-07-28 คำขอ POST แต่ละรายการจะระบุเวอร์ชันของโปรโตคอล ความสามารถ และข้อมูลรับรองของตนเอง ดังนั้น replica ใดๆ ก็สามารถตอบคำถามได้ และการทำ round-robin ก็เพียงพอแล้ว สิ่งเดียวที่ยังคงอยู่ยาวนานคือสตรีมการตอบกลับ subscriptions/listen ซึ่งเป็นการเชื่อมต่อแบบเปิดเดียวไปยังกระบวนการเดียว มันจะสิ้นสุดลงเมื่อกระบวนการนั้นจบลง และไคลเอนต์จะส่ง subscriptions/listen อีกครั้งเพื่อสร้างการเชื่อมต่อใหม่ นั่นเป็นเรื่องของอายุการเชื่อมต่อมากกว่าการทำ session affinity และไม่มีกฎการกำหนดเส้นทางใดที่ป้องกันเรื่องนี้ได้
เกิดอะไรขึ้นกับ Mcp-Session-Id และ HTTP GET stream?
ทั้งสองอย่างถูกลบออกในการแก้ไข 2026-07-28 ภายใต้ SEP-2567 และ SEP-2575 เซิร์ฟเวอร์ที่ใช้การแก้ไขนี้ควรตอบกลับ 405 Method Not Allowed ต่อ GET และ DELETE ที่ MCP endpoint และควรเพิกเฉยต่อ header Mcp-Session-Id แทนที่จะส่งกลับไป การแจ้งเตือนการเปลี่ยนแปลงที่เซิร์ฟเวอร์เป็นผู้เริ่มจะส่งผ่านสตรีมการตอบกลับของคำขอ subscriptions/listen แทนที่จะเป็นสตรีม GET แบบแยกต่างหาก เซิร์ฟเวอร์ที่ยังคงต้องให้บริการไคลเอนต์รุ่นเก่าสามารถใช้พฤติกรรมของรุ่นก่อนหน้าควบคู่ไปกับรุ่นนี้ได้
ฉันจะตรวจสอบสถานะ (health check) ของ MCP server ที่ไม่มีการทำ handshake ได้อย่างไร?
ให้ใช้การตรวจสอบสองระดับ ชี้การตรวจสอบแบบ active ของ proxy ไปที่ path HTTP ปกติที่แอปพลิเคชันของคุณให้บริการ เพราะการส่ง GET ไปยัง MCP endpoint จะส่งผลให้ได้ 405 ซึ่งจะทำให้ backend ที่ทำงานปกติถูกทำเครื่องหมายว่าใช้งานไม่ได้ จากนั้นให้ตรวจสอบตัวโปรโตคอลเองโดยการส่ง POST ด้วย server/discover ซึ่งเซิร์ฟเวอร์ 2026-07-28 ทุกตัวต้องรองรับ และตรวจสอบว่าการตอบกลับเป็น HTTP 200 และแสดงรายการเวอร์ชันของโปรโตคอลที่ไคลเอนต์ของคุณใช้งานอยู่ หากได้รับ 404 พร้อมกับ JSON-RPC error -32601 หมายความว่ากระบวนการกำลังทำงานอยู่แต่ไม่ได้ให้บริการเมธอดนั้น และหากได้รับ 400 พร้อมกับ -32022 หมายความว่าเวอร์ชันที่คุณร้องขอไม่ได้รับการรองรับโดย build นั้นๆ