MCP stateless: revision 2026-07-28 đã đổi gì?
MCP revision 2026-07-28 bỏ session và handshake initialize. Xem tác động cụ thể lên reverse proxy, health check, timeout và auth của server.
MCP server stateless là gì
MCP server stateless không lưu state riêng của từng client giữa các request. Mỗi request đều chứa phiên bản protocol, capabilities của client và credentials mà server cần để xử lý request. Vì vậy, bất kỳ process nào trên bất kỳ máy nào cũng có thể xử lý bất kỳ request nào. MCP (Model Context Protocol, wire format mà agent dùng để gọi tool) đưa quy tắc này vào revision 2026-07-28. Revision đó loại bỏ handshake initialize và HTTP session bên dưới handshake này. Nội dung ở đây chỉ nói về phía server của wire protocol. Nếu bạn chưa quen với phía agent, lộ trình học AI agent theo từng bước giải thích vòng lặp quyết định gọi tool trước khi đi vào các chi tiết HTTP này.
Đó là mục đích vận hành chính. Server không lưu state riêng theo client có thể chạy sau một load balancer thông thường mà không cần session affinity, có thể restart trong lúc deploy mà không làm client bị gián đoạn, và có thể chạy thành 4 process giống hệt nhau thay vì chỉ 1 process. Server hướng session không làm được các việc này nếu không có thêm cơ chế hỗ trợ.
Model Context Protocol là một protocol stateless: mọi thông tin cần thiết để xử lý request đều nằm trong chính request đó. Server xử lý từng request độc lập. Không được suy ra state từ các request trước đó, kể cả khi các request đó dùng cùng một connection hoặc stream.
Stateless không có nghĩa là server không lưu gì. Database, queue và cache của bạn vẫn ở đó. Điều đó có nghĩa là protocol không mang state trên connection, vì vậy server không được coi một connection, một process hoặc một socket đang mở là đại diện cho “client này đang ở giữa cuộc hội thoại”. Cách phân tách này dễ thấy nhất ở một app đã tự quản lý dữ liệu: MCP server chỉ đọc của openGym trả lời các câu hỏi về lịch sử tập luyện được lưu trong database riêng của app, và không phần nào trong storage đó phụ thuộc vào connection mà request cụ thể sử dụng để đến server.
Những gì bản sửa đổi 2026-07-28 đã loại bỏ
2026-07-28 là bản sửa đổi hiện tại của đặc tả tính đến tháng 8 năm 2026. So với 2025-11-25, bản này loại bỏ 5 thành phần từng được dùng để hỗ trợ session.
- Request
initializevà notificationnotifications/initialized. Hoàn toàn không còn handshake (SEP-2575). - Header
Mcp-Session-Idvà việc kết thúc session bằng HTTPDELETE(SEP-2567). - Stream HTTP
GETđộc lập, nơi server đẩy notification. Thành phần này được thay bằngsubscriptions/listen, một POST thông thường có response là stream tồn tại lâu dài. - Khả năng tiếp tục stream SSE (server-sent events). Header
Last-Event-IDvà ID của từng event bị loại bỏ. Vì vậy, khi stream bị ngắt, request đang xử lý sẽ mất và client phải gửi lại dưới dạng request mới với request ID mới. ping,logging/setLevelvànotifications/roots/list_changed. Log level hiện là một field của từng request,io.modelcontextprotocol/logLeveltrong_meta.
Một method được bổ sung và mọi server đều phải triển khai method này. server/discover trả về các phiên bản protocol, capability và identity mà server hỗ trợ trong một lần gọi. Đây là thành phần gần với handshake nhất còn lại, nhưng client không bắt buộc phải gọi nó.
Vì sao session transport khó vận hành trong môi trường production
Trong 2025-11-25 và các phiên bản trước đó, server có thể tạo session ID khi khởi tạo và trả ID này trong header Mcp-Session-Id trên InitializeResult. Sau đó, client phải gửi header này trong mọi request tiếp theo. Version của protocol đã thương lượng và các capability của client được lưu trong memory của server, với session ID làm khóa. Mỗi lựa chọn này đều tạo ra chi phí vận hành.
- Khi restart, server sẽ xóa toàn bộ session table. Specification yêu cầu server trả về
404 Not Foundcho mọi request chứa session ID không còn hiệu lực, đồng thời yêu cầu client bắt đầu lại bằngInitializeRequestmới. Mỗi lần deploy trở thành một sự kiện reconnect đối với mọi client đang kết nối. - Replica thứ hai không biết các session của replica thứ nhất. Scale out yêu cầu load balancer định tuyến sticky, hoặc dùng shared session store để mọi replica đọc dữ liệu này trong mỗi request.
- Session table chiếm memory và tăng theo số client đang idle.
DELETElà tùy chọn, nên các client đóng kết nối mà không gửi nó sẽ để lại các entry trong table. - Kết quả của list có thể khác nhau giữa các connection, nên không an toàn khi đặt cache phía trước server.
Loại bỏ session sẽ giải quyết cả 4 vấn đề cùng lúc. Đây là thay đổi cần hiểu rõ trước khi chỉnh bất kỳ config nào.
Mỗi request hiện mang theo những gì
Mỗi POST đến MCP endpoint là một request độc lập. Protocol version và client capabilities được truyền trong request body tại _meta. Một số field được chọn cũng được sao chép vào HTTP header để intermediary có thể định tuyến dựa trên chúng mà không cần parse 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 và io.modelcontextprotocol/clientCapabilities là bắt buộc trong mọi request. clientInfo không bắt buộc, nhưng client nên gửi field này. Request thiếu field bắt buộc là request không hợp lệ. Server phải từ chối request đó bằng JSON-RPC error -32602 và HTTP 400 Bad Request.
Header Mcp-Method là bắt buộc trong mọi request. Mcp-Name là bắt buộc trên tools/call, resources/read và prompts/get. Giá trị của header phải khớp với body. Server đã xử lý body phải từ chối trường hợp không khớp bằng 400 Bad Request và error code -32020, HeaderMismatch. Quy tắc này cần thiết vì load balancer định tuyến theo header và server thực thi theo body là hai nguồn dữ liệu khác nhau. Nếu bạn định tuyến hoặc rate-limit dựa trên các header này, hãy kiểm tra MCP-Protocol-Version trước. Các revision trước đây không xác thực header với body, nên trên những version đó, giá trị của header không đáng tin cậy.
Version không khớp hiện là lỗi thông thường ở từng request, thay vì làm handshake thất bại. Server không implement version được yêu cầu sẽ trả lời 400 Bad Request kèm error -32022, UnsupportedProtocolVersion và liệt kê các version mà server hỗ trợ trong data.supported. Client chọn một version trong danh sách đó rồi gửi lại request.
Nơi state được lưu: token, cursor, subscription
State không biến mất. Nó chuyển vào những nơi bạn có thể xem và ghi log.
Credential được đưa vào mọi request. Không có session để gắn identity, nên access token đi kèm từng HTTP call và được kiểm tra mỗi lần. Xem phần authentication bên dưới để biết chi tiết.
Cursor phải tự chứa vị trí của nó. Phân trang trên tools/list, resources/list, prompts/list và resources/templates/list sử dụng một chuỗi cursor opaque, client không được parse hoặc sửa đổi chuỗi này. Trên server chạy một process, cách phổ biến là lưu offset trong memory và lập khóa theo session. Khi không có session, cursor phải đủ thông tin để bất kỳ replica nào cũng có thể tiếp tục listing. Vì vậy, hãy encode vị trí vào cursor rồi ký nó, hoặc lưu vị trí trong storage dùng chung cho mọi replica. Cursor không hợp lệ phải trả về -32602. Hãy ký cursor vì cursor opaque vẫn là input do client cung cấp, được code của bạn decode và tin cậy.
Subscription thuộc về request, không thuộc về connection. Client muốn nhận thông báo thay đổi sẽ gửi subscriptions/listen kèm một filter chỉ rõ các type cần theo dõi: toolsListChanged, promptsListChanged, resourcesListChanged và resourceSubscriptions. Server trả về notifications/subscriptions/acknowledged và giữ response stream đó mở. Nếu stream bị ngắt, server không lưu gì cả, còn client sẽ gửi lại subscriptions/listen để khôi phục stream.
Application state giữa các call trở thành một handle rõ ràng. Khi server thực sự phải ghi nhớ một thứ giữa các call, specification quy định dùng một identifier do server tạo và trả về để client truyền lại như một tool argument thông thường. Identifier này xuất hiện trong tool schema, có thể được ghi log và không bao giờ được ngầm định từ connection. Server có dữ liệu thực tế theo từng user, chẳng hạn một MCP email server tự host, sẽ dùng pattern này thay cho session: identifier của mailbox hoặc draft là một tool argument, nên bất kỳ replica nào cũng có thể xử lý call tiếp theo. Nhiều tool hoàn toàn không cần handle: một search tool dùng SearXNG instance của bạn làm backend nhận query rồi trả về kết quả, không có gì cần dùng để tiếp tục call sau và cũng không cần quan tâm replica nào đã trả lời.
Triển khai: reverse proxy, timeout, health check
MCP endpoint là một path nhận POST. Phần lớn traffic là request ngắn và response JSON, proxy nào cũng xử lý được. Ngoại lệ là streaming response, vì các giá trị mặc định của proxy có thể gây lỗi. Đây là phần thay đổi khi bạn chuyển từ bản demo trên laptop sang MCP server chạy trên 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 quan trọng vì nginx mặc định sẽ buffer response được proxy chuyển tiếp. Việc này giữ các SSE event lại cho đến khi buffer đầy hoặc response kết thúc. Specification cũng yêu cầu server gửi X-Accel-Buffering: no trong SSE response, và nginx tôn trọng header đó. Vì vậy, server đúng chuẩn có thể tự báo cho proxy cách xử lý phù hợp. Bạn vẫn nên đặt directive này, vì đó là phần bạn kiểm soát.
proxy_read_timeout mặc định là 60 giây. Một stream subscriptions/listen không có dữ liệu trong thời gian lâu hơn mức này sẽ bị nginx đóng, không phải server của bạn. Vì vậy, log vẫn cho thấy process khỏe mạnh nhưng client lại thấy stream bị ngắt. Chỉ tăng giá trị này trên MCP location, không tăng cho toàn bộ server. Server cũng nên gửi một dòng comment SSE (dòng bắt đầu bằng dấu hai chấm) làm keep-alive trong các khoảng yên lặng. Cách này ngăn các intermediary timeout stream.
Caddy cần ít cấu hình hơn. Mặc định, Caddy buffer một phần response để tối ưu hiệu quả truyền dữ liệu và flush ngay khi response chứa Content-Type: text/event-stream. Vì vậy, streaming hoạt động mà không cần thêm directive.
mcp.example.com {
reverse_proxy 127.0.0.1:8080 {
health_uri /healthz
health_interval 10s
}
}Lưu ý endpoint mà health check đó kiểm tra. Không dùng GET để chạy active check vào MCP endpoint, vì server chỉ triển khai revision này sẽ trả về 405 Method Not Allowed cho GET và DELETE, trong khi health method mặc định của Caddy là GET. Khi đó, proxy sẽ đánh dấu một backend hoàn toàn khỏe mạnh là down. Hãy cung cấp một path đơn giản như /healthz cho proxy, rồi kiểm tra protocol riêng bằng 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":{}}}}'Một 200 chứa danh sách supportedVersions nghĩa là process đang chạy và đang giao tiếp đúng protocol. Một 404 có JSON-RPC error -32601 nghĩa là process đang chạy nhưng không cung cấp server/discover, trong khi mọi server 2026-07-28 đều phải triển khai method này. Một 400 có -32022 nghĩa là checker đã yêu cầu một version mà bản build này không hỗ trợ. Đây chính xác là lỗi bạn cần phát hiện sau khi nâng cấp dependency. nginx mã nguồn mở không có active health check, nên hãy dùng max_fails và fail_timeout dạng passive trên upstream, đồng thời chạy protocol check từ hệ thống monitoring.
Rolling restart giờ chỉ làm mất các request đang xử lý, không ảnh hưởng gì khác. Hãy drain, chờ các POST đang mở hoàn tất, khởi động process mới, rồi để client gửi lại các request bị lỗi. Thứ duy nhất bạn vẫn làm rớt là stream subscriptions/listen đang mở, vì stream đó là một connection trực tiếp đến một process cụ thể. Statelessness đã loại bỏ session affinity. Nó không loại bỏ connection affinity đối với stream đang mở, và không routing rule nào giải quyết được việc này. Client có thể phân biệt hai trường hợp: stream kết thúc bằng kết quả subscriptions/listen rỗng là stream đã đóng một cách graceful; stream kết thúc mà không có kết quả này là stream bị rớt, và client có thể xem đó là lý do để reconnect.
Caching giờ đây lần đầu tiên trở nên khả thi. Kết quả từ các list method hiện chứa ttlMs và cacheScope, còn cacheScope: "public" cho biết shared intermediary có thể cache response. Việc này chỉ an toàn vì list result không còn thay đổi theo từng connection, là hệ quả trực tiếp của việc loại bỏ session.
Cách authentication thay đổi khi không có session
Khi có session, bạn dễ chọn cách authentication một lần tại initialize rồi coi session ID là bằng chứng cho mọi yêu cầu sau đó. Session ID được dùng theo cách này là một bearer credential không có audience, thời hạn hết hạn hoặc cơ chế thu hồi, và do chính server của bạn tạo ra. Xóa session sẽ loại bỏ lối tắt đó, nên cơ chế thay thế phải chặt chẽ hơn.
MCP server được bảo vệ hoạt động như một OAuth 2.1 resource server. Mọi HTTP request từ client phải mang theo Authorization: Bearer <access token>, và server phải validate token trong từng request. Việc validation bao gồm cả audience: server phải xác nhận token được cấp riêng cho chính server đó, theo RFC 8707 (Resource Indicators for OAuth 2.0), đồng thời không được chấp nhận hoặc chuyển tiếp token dành cho bất kỳ dịch vụ nào khác. Client yêu cầu đúng audience bằng cách gửi tham số resource với canonical URI của server.
Discovery bắt đầu từ một challenge. Khi nhận request không có token dùng được, server trả về 401 Unauthorized.
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer resource_metadata="https://mcp.example.com/.well-known/oauth-protected-resource",
scope="files:read"Client đọc resource_metadata, fetch document đó (RFC 9728, OAuth 2.0 Protected Resource Metadata, mà MCP server bắt buộc phải implement), tìm authorization server rồi chạy flow. Token hợp lệ nhưng có quá ít permission sẽ nhận 403 Forbidden kèm error="insufficient_scope" và các scope cần cho thao tác đó.
Điều này dẫn đến 2 hệ quả khi vận hành. Token validation hiện diễn ra trong từng request thay vì một lần cho mỗi session. Vì vậy, nếu mỗi call đều phải round trip đến introspection endpoint, độ trễ sẽ tăng rõ rệt. Hãy ưu tiên token có thể verify cục bộ bằng signature, audience và thời hạn hết hạn, hoặc cache kết quả validation trong một khoảng thời gian ngắn với token làm key. Ngoài ra, vì không còn session lưu identity, authorization phải được tính từ token trong từng call. Cách này phản ánh đúng hơn so với session model, đồng thời phù hợp với thực tế phổ biến là giữ credential bên ngoài agent process, như mô tả trong giữ secret bên ngoài AI agent. Scope chỉ giới hạn những gì token được phép thực hiện sau khi request đến server của bạn. Trên máy chạy agent, harness plugin thêm rule về quyền dùng tool và giới hạn ngân sách quyết định những call nào được thực hiện ngay từ đầu.
Điều gì đúng với revision này và điều gì không
Toàn bộ nội dung trên mô tả revision 2026-07-28. Nội dung này không mô tả MCP mãi mãi, cũng không mô tả server bạn đã triển khai vào năm ngoái.
Client và server dùng 2025-11-25 trở về trước vẫn sử dụng mô hình handshake. Specification gọi các revision đó là legacy, còn các revision có metadata theo từng request là modern. Một server chỉ hỗ trợ revision này, khi gặp client cũ hơn, nên trả về 405 Method Not Allowed cho GET hoặc DELETE trên MCP endpoint, bỏ qua mọi header Mcp-Session-Id mà không tự tạo hoặc phản hồi header đó, đồng thời bỏ qua Last-Event-ID vì stream không thể resume. Một server hỗ trợ cả hai thế hệ có thể phục vụ cả hai trên cùng một endpoint: request có _meta được xử lý stateless, còn request initialize sẽ chọn semantics session kiểu cũ.
Vì vậy, hãy kiểm tra revision string trước khi tin rằng nội dung này áp dụng được. Nếu SDK của bạn vẫn gửi initialize, session vẫn là thành phần thực tế trong deployment của bạn và bạn vẫn phải quản lý các vấn đề liên quan đến session nêu trên. Điều tương tự cũng áp dụng ở phía client: một agent process chạy trên chính máy của bạn, chẳng hạn setup trong chạy coding agent trên VPS, chỉ stateless theo nghĩa này nếu library mà nó dùng hỗ trợ một revision modern. Hãy đọc version mà runtime của bạn negotiate, sau đó đọc revision tương ứng của specification, và xem trang này là mô tả một revision có tên cụ thể thay vì mô tả protocol nói chung.
FAQ
Một MCP server stateless có nghĩa là tôi không thể lưu trữ gì sao?
Không. Stateless mô tả protocol, không mô tả ứng dụng của bạn. Database, queue và cache vẫn hoạt động như trước. Điểm thay đổi là state kéo dài qua nhiều lần gọi phải được tham chiếu bằng một identifier rõ ràng do client gửi trong mỗi request, chẳng hạn một handle do server tạo trong tool argument. Bạn không được suy ra context từ connection: specification quy định server không được dựa vào các request trước đó trên cùng connection để xác định capability, protocol version hoặc client identity, vì mỗi request đều cung cấp các thông tin này trong _meta.
Tôi vẫn cần sticky session trên load balancer chứ?
Không cần cho các request thông thường. Theo revision 2026-07-28, mỗi POST tự chứa protocol version, capability và credential, nên replica nào cũng có thể trả lời mọi request và round-robin là đủ. Thành phần duy nhất còn duy trì lâu dài là response stream subscriptions/listen, vốn là một connection mở duy nhất đến một process duy nhất. Stream này kết thúc khi process đó kết thúc, rồi client gửi lại subscriptions/listen để thiết lập lại. Đây là vòng đời của connection, không phải session affinity, nên không có routing rule nào ngăn được việc này.
Mcp-Session-Id và HTTP GET stream đã được thay đổi thế nào?
Cả hai đã bị loại bỏ trong revision 2026-07-28, theo SEP-2567 và SEP-2575. Server chỉ triển khai revision này phải trả lời 405 Method Not Allowed cho GET và DELETE trên MCP endpoint, đồng thời phải bỏ qua header Mcp-Session-Id thay vì gửi lại header đó. Các thông báo thay đổi do server khởi tạo hiện được gửi trên response stream của request subscriptions/listen thay vì một stream GET độc lập. Các server vẫn phải phục vụ client cũ sẽ triển khai behavior của revision trước song song với revision này.
Làm thế nào để health check một MCP server không có handshake?
Hãy dùng 2 cấp kiểm tra. Trỏ active check của proxy đến một HTTP path thông thường do ứng dụng phục vụ, vì GET đến MCP endpoint sẽ trả về 405 đúng theo protocol và khiến proxy đánh dấu backend đang hoạt động là down. Sau đó kiểm tra chính protocol bằng cách gửi POST server/discover. Mọi server 2026-07-28 đều phải triển khai phương thức này. Xác nhận response có HTTP 200 và liệt kê một protocol version mà client của bạn sử dụng. Một 404 kèm JSON-RPC error -32601 nghĩa là process đang chạy nhưng không phục vụ method đó. Một 400 kèm -32022 nghĩa là build này không hỗ trợ version bạn yêu cầu.