Skip to content

Commit 1253d17

Browse files
mcp: do not call DELETE for sessionless & doc adjustments (#960)
1 parent 8eb5463 commit 1253d17

3 files changed

Lines changed: 32 additions & 11 deletions

File tree

mcp/server.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,9 @@ type ServerOptions struct {
143143
//
144144
// As a special case, if GetSessionID returns the empty string, the
145145
// Mcp-Session-Id header will not be set.
146+
//
147+
// GetSessionID is not consulted when [StreamableHTTPOptions.Stateless] is
148+
// true, since stateless servers do not maintain sessions.
146149
GetSessionID func() string
147150
}
148151

mcp/streamable.go

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -128,13 +128,21 @@ func (i *sessionInfo) stopTimer() {
128128
type StreamableHTTPOptions struct {
129129
// Stateless controls whether the session is 'stateless'.
130130
//
131-
// A stateless server ignores the Mcp-Session-Id header, and uses a
132-
// temporary session with default initialization parameters. Any
131+
// A stateless server does not read or set the Mcp-Session-Id header, and
132+
// uses a temporary session with default initialization parameters for each
133+
// request. [ServerOptions.GetSessionID] is not consulted. Any
133134
// server->client request is rejected immediately as there's no way for the
134135
// client to respond. Server->Client notifications may reach the client if
135136
// they are made in the context of an incoming request, as described in the
136137
// documentation for [StreamableServerTransport].
137-
// In Stateless mode DELETE requests will return 405 Method Not Allowed.
138+
// In Stateless mode, GET and DELETE requests return 405 Method Not Allowed.
139+
//
140+
// This mode aligns with the sessionless direction of the MCP spec; see
141+
// [SEP-2567]. The previous behavior, in which stateless servers still
142+
// honored Mcp-Session-Id, can be restored temporarily via the
143+
// MCPGODEBUG compatibility parameter "allowsessionsinstateless=1".
144+
//
145+
// [SEP-2567]: https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2567
138146
Stateless bool
139147

140148
// TODO(#148): support session retention (?)
@@ -1689,11 +1697,13 @@ func init() {
16891697
// Connect implements the [Transport] interface.
16901698
//
16911699
// The resulting [Connection] writes messages via POST requests to the
1692-
// transport URL with the Mcp-Session-Id header set, and reads messages from
1693-
// hanging requests.
1700+
// transport URL, and reads messages from hanging requests. If the server
1701+
// provides a session ID via the Mcp-Session-Id response header, subsequent
1702+
// requests include it; sessionless servers that omit the header are fully
1703+
// supported.
16941704
//
1695-
// When closed, the connection issues a DELETE request to terminate the logical
1696-
// session.
1705+
// When closed, the connection issues a DELETE request to terminate the
1706+
// session, unless no session was established.
16971707
func (t *StreamableClientTransport) Connect(ctx context.Context) (Connection, error) {
16981708
client := t.HTTPClient
16991709
if client == nil {
@@ -2370,6 +2380,9 @@ func (c *streamableClientConn) Close() error {
23702380
c.closeOnce.Do(func() {
23712381
if errors.Is(c.failure(), ErrSessionMissing) {
23722382
// If the session is missing, no need to delete it.
2383+
} else if c.SessionID() == "" {
2384+
// No session was established (e.g. the server is stateless),
2385+
// so there is nothing to delete.
23732386
} else {
23742387
req, err := http.NewRequestWithContext(c.ctx, http.MethodDelete, c.url, nil)
23752388
if err != nil {

mcp/streamable_client.go

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,12 @@ components:
4040
4141
# Sessions
4242
43-
The client maintains a session with the server, identified by a session ID
44-
(Mcp-Session-Id header):
43+
The client optionally maintains a session with the server, identified by a
44+
session ID (Mcp-Session-Id header). Sessionless servers (those that never
45+
send Mcp-Session-Id) are fully supported; the client simply omits the
46+
header from subsequent requests and skips the DELETE on close.
47+
48+
When a session is present:
4549
4650
- Session ID is received from the server after initialization
4751
- Client includes the session ID in all subsequent requests
@@ -162,6 +166,7 @@ The client must handle two response formats from POST requests:
162166
- DELETE: Terminate the session
163167
- Used by [streamableClientConn.Close]
164168
- Skipped if session is already known to be gone ([ErrSessionMissing])
169+
or if no session was established (sessionless server)
165170
166171
# Error Handling
167172
@@ -176,7 +181,7 @@ Errors are categorized and handled differently:
176181
- 404 Not Found: Session terminated by server ([ErrSessionMissing])
177182
- Message decode errors: Protocol violation
178183
- Context cancellation: Client closed connection
179-
- Mismatched session IDs: Protocol error
184+
- Mismatched session IDs: Protocol error (only relevant for servers that use sessions)
180185
- See issue #683: our terminal errors are too strict.
181186
182187
Terminal errors are stored via [streamableClientConn.fail] and returned by
@@ -211,7 +216,7 @@ This header (set by [streamableClientConn.setMCPHeaders]):
211216
212217
State management:
213218
- [streamableClientConn.incoming]: Buffered channel for received messages
214-
- [streamableClientConn.sessionID]: Server-assigned session identifier
219+
- [streamableClientConn.sessionID]: Server-assigned session identifier (empty for sessionless servers)
215220
- [streamableClientConn.initializedResult]: Cached for protocol version header
216221
- [streamableClientConn.failed]: Channel closed on terminal error
217222
- [streamableClientConn.done]: Channel closed on graceful shutdown

0 commit comments

Comments
 (0)