Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions mcp/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
27 changes: 20 additions & 7 deletions mcp/streamable.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 (?)
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down
13 changes: 9 additions & 4 deletions mcp/streamable_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
Loading