mcp: do not call DELETE for sessionless & doc adjustments (#960)

This commit is contained in:
Maciej Kisiel 2026-05-15 21:47:34 +02:00 • committed by GitHub
commit 1253d17add
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 32 additions and 11 deletions

View file

@ -143,6 +143,9 @@ type ServerOptions struct {
// //
// As a special case, if GetSessionID returns the empty string, the // As a special case, if GetSessionID returns the empty string, the
// Mcp-Session-Id header will not be set. // Mcp-Session-Id header will not be set.
//
// GetSessionID is not consulted when [StreamableHTTPOptions.Stateless] is
// true, since stateless servers do not maintain sessions.
GetSessionID func() string GetSessionID func() string
} }

View file

@ -128,13 +128,21 @@ func (i *sessionInfo) stopTimer() {
type StreamableHTTPOptions struct { type StreamableHTTPOptions struct {
// Stateless controls whether the session is 'stateless'. // Stateless controls whether the session is 'stateless'.
// //
// A stateless server ignores the Mcp-Session-Id header, and uses a // A stateless server does not read or set the Mcp-Session-Id header, and
// temporary session with default initialization parameters. Any // uses a temporary session with default initialization parameters for each
// request. [ServerOptions.GetSessionID] is not consulted. Any
// server->client request is rejected immediately as there's no way for the // server->client request is rejected immediately as there's no way for the
// client to respond. Server->Client notifications may reach the client if // client to respond. Server->Client notifications may reach the client if
// they are made in the context of an incoming request, as described in the // they are made in the context of an incoming request, as described in the
// documentation for [StreamableServerTransport]. // documentation for [StreamableServerTransport].
// In Stateless mode DELETE requests will return 405 Method Not Allowed. // In Stateless mode, GET and DELETE requests return 405 Method Not Allowed.
//
// This mode aligns with the sessionless direction of the MCP spec; see
// [SEP-2567]. The previous behavior, in which stateless servers still
// honored Mcp-Session-Id, can be restored temporarily via the
// MCPGODEBUG compatibility parameter "allowsessionsinstateless=1".
//
// [SEP-2567]: https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2567
Stateless bool Stateless bool
// TODO(#148): support session retention (?) // TODO(#148): support session retention (?)
@ -1689,11 +1697,13 @@ func init() {
// Connect implements the [Transport] interface. // Connect implements the [Transport] interface.
// //
// The resulting [Connection] writes messages via POST requests to the // The resulting [Connection] writes messages via POST requests to the
// transport URL with the Mcp-Session-Id header set, and reads messages from // transport URL, and reads messages from hanging requests. If the server
// hanging requests. // provides a session ID via the Mcp-Session-Id response header, subsequent
// requests include it; sessionless servers that omit the header are fully
// supported.
// //
// When closed, the connection issues a DELETE request to terminate the logical // When closed, the connection issues a DELETE request to terminate the
// session. // session, unless no session was established.
func (t *StreamableClientTransport) Connect(ctx context.Context) (Connection, error) { func (t *StreamableClientTransport) Connect(ctx context.Context) (Connection, error) {
client := t.HTTPClient client := t.HTTPClient
if client == nil { if client == nil {
@ -2370,6 +2380,9 @@ func (c *streamableClientConn) Close() error {
c.closeOnce.Do(func() { c.closeOnce.Do(func() {
if errors.Is(c.failure(), ErrSessionMissing) { if errors.Is(c.failure(), ErrSessionMissing) {
// If the session is missing, no need to delete it. // If the session is missing, no need to delete it.
} else if c.SessionID() == "" {
// No session was established (e.g. the server is stateless),
// so there is nothing to delete.
} else { } else {
req, err := http.NewRequestWithContext(c.ctx, http.MethodDelete, c.url, nil) req, err := http.NewRequestWithContext(c.ctx, http.MethodDelete, c.url, nil)
if err != nil { if err != nil {

View file

@ -40,8 +40,12 @@ components:
# Sessions # Sessions
The client maintains a session with the server, identified by a session ID The client optionally maintains a session with the server, identified by a
(Mcp-Session-Id header): session ID (Mcp-Session-Id header). Sessionless servers (those that never
send Mcp-Session-Id) are fully supported; the client simply omits the
header from subsequent requests and skips the DELETE on close.
When a session is present:
- Session ID is received from the server after initialization - Session ID is received from the server after initialization
- Client includes the session ID in all subsequent requests - Client includes the session ID in all subsequent requests
@ -162,6 +166,7 @@ The client must handle two response formats from POST requests:
- DELETE: Terminate the session - DELETE: Terminate the session
- Used by [streamableClientConn.Close] - Used by [streamableClientConn.Close]
- Skipped if session is already known to be gone ([ErrSessionMissing]) - Skipped if session is already known to be gone ([ErrSessionMissing])
or if no session was established (sessionless server)
# Error Handling # Error Handling
@ -176,7 +181,7 @@ Errors are categorized and handled differently:
- 404 Not Found: Session terminated by server ([ErrSessionMissing]) - 404 Not Found: Session terminated by server ([ErrSessionMissing])
- Message decode errors: Protocol violation - Message decode errors: Protocol violation
- Context cancellation: Client closed connection - Context cancellation: Client closed connection
- Mismatched session IDs: Protocol error - Mismatched session IDs: Protocol error (only relevant for servers that use sessions)
- See issue #683: our terminal errors are too strict. - See issue #683: our terminal errors are too strict.
Terminal errors are stored via [streamableClientConn.fail] and returned by Terminal errors are stored via [streamableClientConn.fail] and returned by
@ -211,7 +216,7 @@ This header (set by [streamableClientConn.setMCPHeaders]):
State management: State management:
- [streamableClientConn.incoming]: Buffered channel for received messages - [streamableClientConn.incoming]: Buffered channel for received messages
- [streamableClientConn.sessionID]: Server-assigned session identifier - [streamableClientConn.sessionID]: Server-assigned session identifier (empty for sessionless servers)
- [streamableClientConn.initializedResult]: Cached for protocol version header - [streamableClientConn.initializedResult]: Cached for protocol version header
- [streamableClientConn.failed]: Channel closed on terminal error - [streamableClientConn.failed]: Channel closed on terminal error
- [streamableClientConn.done]: Channel closed on graceful shutdown - [streamableClientConn.done]: Channel closed on graceful shutdown