diff --git a/mcp/server.go b/mcp/server.go index d25c7922..183226d1 100644 --- a/mcp/server.go +++ b/mcp/server.go @@ -143,6 +143,9 @@ type ServerOptions struct { // // As a special case, if GetSessionID returns the empty string, the // 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 } diff --git a/mcp/streamable.go b/mcp/streamable.go index 84beff3a..d3f3f4fa 100644 --- a/mcp/streamable.go +++ b/mcp/streamable.go @@ -128,13 +128,21 @@ func (i *sessionInfo) stopTimer() { type StreamableHTTPOptions struct { // Stateless controls whether the session is 'stateless'. // - // A stateless server ignores the Mcp-Session-Id header, and uses a - // temporary session with default initialization parameters. Any + // A stateless server does not read or set the Mcp-Session-Id header, and + // 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 // 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 // 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 // TODO(#148): support session retention (?) @@ -1689,11 +1697,13 @@ func init() { // Connect implements the [Transport] interface. // // The resulting [Connection] writes messages via POST requests to the -// transport URL with the Mcp-Session-Id header set, and reads messages from -// hanging requests. +// transport URL, and reads messages from hanging requests. If the server +// 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 -// session. +// When closed, the connection issues a DELETE request to terminate the +// session, unless no session was established. func (t *StreamableClientTransport) Connect(ctx context.Context) (Connection, error) { client := t.HTTPClient if client == nil { @@ -2370,6 +2380,9 @@ func (c *streamableClientConn) Close() error { c.closeOnce.Do(func() { if errors.Is(c.failure(), ErrSessionMissing) { // 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 { req, err := http.NewRequestWithContext(c.ctx, http.MethodDelete, c.url, nil) if err != nil { diff --git a/mcp/streamable_client.go b/mcp/streamable_client.go index c2cc25b8..74940b3a 100644 --- a/mcp/streamable_client.go +++ b/mcp/streamable_client.go @@ -40,8 +40,12 @@ components: # Sessions -The client maintains a session with the server, identified by a session ID -(Mcp-Session-Id header): +The client optionally maintains a session with the server, identified by a +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 - 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 - Used by [streamableClientConn.Close] - Skipped if session is already known to be gone ([ErrSessionMissing]) + or if no session was established (sessionless server) # Error Handling @@ -176,7 +181,7 @@ Errors are categorized and handled differently: - 404 Not Found: Session terminated by server ([ErrSessionMissing]) - Message decode errors: Protocol violation - 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. Terminal errors are stored via [streamableClientConn.fail] and returned by @@ -211,7 +216,7 @@ This header (set by [streamableClientConn.setMCPHeaders]): State management: - [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.failed]: Channel closed on terminal error - [streamableClientConn.done]: Channel closed on graceful shutdown