SSD Nodes Learn 🎉 VPS từ $4.99/tháng
Hướng dẫn Matt ConnorBởi Matt Connor

MCP stateless: revision 2026-07-28 thay đổi gì

Revision MCP 2026-07-28 bỏ session và handshake initialize. Xem tác động cụ thể đến reverse proxy, health check, timeout, auth và load balancer.

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 mang theo protocol version, client capabilities 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 này đã loại bỏ handshake initialize và HTTP session bên dưới handshake đó.

Đó là mục đích vận hành chính. Server không lưu gì riêng cho từng 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 bốn process giống nhau thay vì chỉ một process. Server hướng session không làm được những 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 để xử lý một request đều nằm trong chính request đó. Server xử lý độc lập từng request; không được suy ra state từ các request trước đó, kể cả các request trên cùng 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 tồn tại. Điều đó có nghĩa là protocol không mang state trên connection. Vì vậy, server không được xem một connection, một process hoặc một socket đang mở là đại diện cho “client này đang ở giữa một cuộc hội thoại”.

Những gì bản revision 2026-07-28 đã loại bỏ

2026-07-28 là revision hiện tại của specification 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 initialize và notification notifications/initialized. Hoàn toàn không còn handshake (SEP-2575).
  • Header Mcp-Session-Id và việc kết thúc session bằng HTTP DELETE (SEP-2567).
  • HTTP stream độc lập GET mà server dùng để đẩy notification. Thành phần này được thay bằng subscriptions/listen, một POST thông thường có response là một stream tồn tại lâu.
  • Khả năng tiếp tục SSE (server-sent events) stream. Header Last-Event-ID và ID của từng event không còn, nên khi stream bị ngắt, request đang xử lý sẽ bị mất và client phải gửi lại dưới dạng một request mới với request ID mới.
  • ping, logging/setLevelnotifications/roots/list_changed. Log level hiện là một field của từng request, io.modelcontextprotocol/logLevel trong _meta.

Một method mới đã được thêm vào và mọi server đều phải implement method này. server/discover trả về các protocol version, capability và identity mà server hỗ trợ trong một lần gọi. Đây là thành phần gần nhất với handshake còn lại, và client không bắt buộc phải gọi method này.

Vì sao session transport khó chạy 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 rồi trả ID này trong header Mcp-Session-Id trên InitializeResult. Sau đó, client phải gửi header đó 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 bộ nhớ của server, với session ID làm khóa. Mỗi lựa chọn này đều có chi phí vận hành.

  • Khi restart, server xóa toàn bộ session table. Specification yêu cầu server trả về 404 Not Found cho mọi request chứa session ID không còn tồn tại, đồng thời yêu cầu client bắt đầu lại bằng InitializeRequest mới. Mỗi lần deploy đều khiến mọi client đang kết nối phải reconnect.
  • Replica thứ hai không biết các session của replica thứ nhất. Scale out khi đó cần sticky routing tại load balancer hoặc một shared session store để mọi replica đọc dữ liệu trong mỗi request.
  • Session table chiếm bộ nhớ và tăng theo số client không hoạt động. DELETE là tùy chọn, nên các client đóng kết nối mà không gửi nó sẽ để lại entry.
  • Kết quả 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.

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.

Nội dung mỗi request hiện mang theo

Mỗi POST đến MCP endpoint là một request độc lập. Phiên bản protocol và capabilities của client được gửi trong request body dưới _meta. Một số trường được phản chiếu vào HTTP header để intermediary có thể định tuyến dựa trên các trường này 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/protocolVersionio.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 trường này. Request thiếu trường bắt buộc là malformed. Vì vậy, server phải từ chối request bằng lỗi JSON-RPC -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/readprompts/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 dựa trên header còn server thực thi dựa trên 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, trước tiên hãy kiểm tra MCP-Protocol-Version. Các bản sửa đổi trước đây không xác thực header với body, nên trên những phiên bản đó, không thể tin cậy giá trị của header.

Không khớp phiên bản hiện là lỗi thông thường trên từng request, thay vì làm handshake thất bại. Server không triển khai phiên bản được yêu cầu sẽ trả lời 400 Bad Request kèm lỗi -32022, UnsupportedProtocolVersion, đồng thời liệt kê các phiên bản được hỗ trợ trong data.supported. Client chọn một phiên bản trong danh sách đó rồi retry.

State nằm ở đâu: token, cursor và subscription

State không biến mất. Nó được chuyển sang 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 được gửi kèm mỗi HTTP call và được xác thực mỗi lần. Xem chi tiết trong phần authentication bên dưới.

Cursor phải tự chứa vị trí của nó. Cơ chế phân trang trên tools/list, resources/list, prompts/listresources/templates/list sử dụng một chuỗi cursor opaque, và 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à liên kết offset đó với session. Khi không có session, cursor phải đủ để mọi replica tiếp tục listing, vì vậy hãy mã hóa vị trí bên trong cursor rồi ký cursor, 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 giải mã và tin cậy.

Subscription thuộc về request, không thuộc về connection. Client cần nhận thông báo thay đổi sẽ gửi subscriptions/listen cùng một filter nêu các type mà client muốn nhận: toolsListChanged, promptsListChanged, resourcesListChangedresourceSubscriptions. 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 tường minh. Khi server thực sự phải ghi nhớ một giá trị giữa các call, specification quy định dùng một identifier do server tạo và gửi lại dưới dạng 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 gắn với connection. Server có dữ liệu thực theo từng user ở phía sau, 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ể tiếp tục xử lý call tiếp theo.

Triển khai: reverse proxy, timeout và 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à response dạng streaming, trong đó các giá trị mặc định của proxy gây bất 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 buffer response được proxy, khiến các SSE event bị giữ 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 response SSE, và nginx tôn trọng header đó, nên server triển khai đúng sẽ tự báo cho proxy biết cách xử lý. 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 im lặng lâu hơn khoảng thời gian này sẽ bị nginx đóng, không phải server của bạn, nên log cho thấy process vẫn khỏe còn client lại thấy stream bị ngắt. Chỉ tăng giá trị này trên MCP location, không áp dụ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 thời gian im lặng. Cách này ngăn intermediary timeout stream.

Caddy cần ít cấu hình hơn. Mặc định, Caddy buffer một phần để tăng hiệu quả truyền qua wire và flush ngay khi response có Content-Type: text/event-stream, nên 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 ý health check này đang kiểm tra path nào. Không được active check MCP endpoint bằng GET, vì server chỉ triển khai revision này sẽ trả về 405 Method Not Allowed cho GETDELETE, trong khi health method mặc định của Caddy là GET. Khi đó proxy sẽ đánh dấu 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 có nghĩa là process đang chạy và giao tiếp đúng protocol. Một 404 có JSON-RPC error -32601 có nghĩa là process đang chạy nhưng không hỗ trợ server/discover, thành phần mà mọi server 2026-07-28 đều phải triển khai. Một 400-32022 có 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 cần phát hiện sau khi nâng cấp dependency. nginx bản open source không có active health check, nên hãy dùng max_failsfail_timeout 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ị fail. Thứ duy nhất vẫn bị ngắt là stream subscriptions/listen đang mở, vì stream đó là một connection đang hoạt động tới 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 có routing rule nào khắc phục được điều đó. Client có thể phân biệt hai trường hợp: stream kết thúc bằng result subscriptions/listen rỗng là đã đóng graceful; stream kết thúc mà không có result này là bị ngắt, và client có thể xem đó là lý do để reconnect.

Lần đầu tiên caching trở nên khả thi. Kết quả từ các list method giờ có ttlMscacheScope, còn cacheScope: "public" cho biết intermediary dùng chung có thể cache response. Điều này an toàn chỉ vì kết quả list 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.

Thay đổi xác thực khi không có session

Khi có session, bạn có thể xác thực một lần tại initialize rồi dùng session ID làm bằng chứng cho mọi yêu cầu tiếp theo. Session ID được dùng theo cách đó là một bearer credential không có audience, thời hạn hết hạn hoặc cơ chế thu hồi, do chính server của bạn cấp. Khi bỏ session, lối tắt này không còn và 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 Authorization: Bearer <access token>, và server phải validate token trên từng request. Việc validation bao gồm audience: server phải xác nhận token được cấp riêng cho nó, 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 đối tượng khác. Client yêu cầu đúng audience bằng cách gửi tham số resource với URI chuẩn của server.

Discovery được kích hoạt bằng một challenge. Khi request đến mà 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 tài liệu đó (RFC 9728, OAuth 2.0 Protected Resource Metadata, mà MCP server phải implement), tìm authorization server rồi chạy flow. Token hợp lệ nhưng có quá ít quyền sẽ nhận 403 Forbidden cùng với 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 trên từng request thay vì một lần cho mỗi session, nên một network round trip đến introspection endpoint trong mỗi lần gọi sẽ làm tăng latency: ưu tiên token có thể được verify locally dựa trên 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ó session lưu identity, authorization phải được tính từ token trong mỗi lần gọi. Cách này phản ánh đúng bản chất hơn so với session model, đồng thời phù hợp với thực hành rộng hơn là không để credential trong agent process. Nội dung này được trình bày trong không để secret trong AI agent.

Điều gì đúng và không đúng với revision này

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 năm ngoái.

Client và server trên 2025-11-25 trở về trước vẫn 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. Server chỉ hỗ trợ revision này, khi gặp client cũ, nên trả lời 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 echo lại header đó, đồng thời bỏ qua Last-Event-ID vì stream không thể resume. Server hỗ trợ cả hai thời kỳ có thể phục vụ cả hai trên cùng một endpoint: request có _meta hiện đại sẽ được xử lý stateless, còn request initialize sẽ chọn session semantics cũ.

Vì vậy, hãy kiểm tra revision string trước khi tin vào các nội dung này. Nếu SDK của bạn vẫn gửi initialize, session vẫn tồn tại 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 cấu hình trong bài 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ụ thể, không phải toàn bộ protocol.

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 phải ứ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 request phải được tham chiếu bằng một identifier rõ ràng mà client gửi trong mỗi request, chẳng hạn một handle do server cấp 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 sao?

Không cần đối với các request thông thường. Trong revision 2026-07-28, mỗi POST tự mang protocol version, capability và credential, vì vậy replica nào cũng có thể xử lý request và round-robin là đủ. Thành phần duy trì lâu dài duy nhất còn lại là response stream subscriptions/listen, tức một connection đang mở đến một process duy nhất. Stream 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à thời gian tồn tại của connection, không phải session affinity, và không có routing rule nào ngăn việc này.

Mcp-Session-Id và HTTP GET stream đã được xử lý như 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ả về 405 Method Not Allowed cho GETDELETE 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 truyền trên response stream của request subscriptions/listen thay vì một stream GET độc lập. Server vẫn phải phục vụ client cũ cần triển khai hành vi của revision trước song song với revision này.

Làm cách nào để health check MCP server không có handshake?

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ì một GET đến MCP endpoint sẽ trả về 405 đúng theo protocol và khiến proxy đánh dấu backend khỏe là down. Sau đó kiểm tra chính protocol bằng cách POST server/discover; mọi server 2026-07-28 đều phải triển khai method này. Xác nhận reply là HTTP 200 và liệt kê một protocol version mà client của bạn sử dụng. Một 404 kèm lỗi JSON-RPC -32601 có nghĩa là process đang chạy nhưng không phục vụ method đó. Một 400 kèm -32022 có nghĩa là build đó không hỗ trợ version bạn yêu cầu.