Skip to content

Conversation

@cnnrznn
Copy link

@cnnrznn cnnrznn commented Dec 12, 2025

Description

This PR adds two missing session cleanup calls to the streamable http transport's handleDelete method.

  • Removes the session from the StreamableHTTPServer 'activeSession' map
  • Invokes the MCP server's UnregisterSession() method

Fixes #651

Type of Change

  • Bug fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)
  • MCP spec compatibility implementation
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Documentation update
  • Code refactoring (no functional changes)
  • Performance improvement
  • Tests only (no functional changes)
  • Other (please describe):

Checklist

  • My code follows the code style of this project
  • I have performed a self-review of my own code
  • I have added tests that prove my fix is effective or that my feature works
  • I have updated the documentation accordingly

Additional Information

When writing my test, I noticed an inconsistency between the existing assertion style and AGENTS.md.
AGENTS.md tells agents to use assert/require. The existing tests use t.Error().

Summary by CodeRabbit

Release Notes

  • Bug Fixes

    • Sessions are now properly cleaned up and removed from the server when DELETE requests are processed, ensuring complete and explicit resource management.
  • Tests

    • Added comprehensive test coverage for session deletion to verify correct cleanup behavior, proper session removal from active registry, and appropriate notifications upon session termination.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 12, 2025

Walkthrough

The pull request adds session cleanup logic to the StreamableHTTPServer's DELETE handler, which now removes the session from the activeSessions map and unregisters it from the MCP server. A corresponding test validates this cleanup behavior and hook invocation.

Changes

Cohort / File(s) Change Summary
Session cleanup on DELETE
server/streamable_http.go
Adds session removal from activeSessions map and MCP server unregistration when handling DELETE requests
Test coverage for session cleanup
server/streamable_http_test.go
Adds TestStreamableHTTP_Delete test that verifies HTTP 200 response, session removal from activeSessions and MCP sessions map, and OnUnregisterSession hook invocation

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

  • Single method modification with straightforward cleanup logic
  • New test follows existing patterns using established test infrastructure
  • Clear, focused change addressing a specific bug

Possibly related PRs

  • PR #365: Modifies handleDelete to improve session cleanup and management in the same code path
  • PR #626: Refactors session ID management in handleDelete, affecting the same DELETE request handling
  • PR #652: Modifies session lifecycle cleanup in handleDelete affecting the activeSessions map and session unregistration

Suggested reviewers

  • ezynda3
  • pottekkat
  • rwjblue-glean

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the main change: adding missing session cleanup to the StreamableHTTPServer DELETE handler.
Description check ✅ Passed The description covers all required sections from the template including Type of Change, Checklist items, and linked issue reference (#651).
Linked Issues check ✅ Passed The PR directly addresses issue #651 by implementing the proposed solution: calling UnregisterSession() in handleDelete and removing the session from activeSessions map.
Out of Scope Changes check ✅ Passed All changes are directly scoped to fixing the session cleanup issue in the StreamableHTTPServer DELETE handler with appropriate test coverage.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
server/streamable_http.go (1)

657-682: Guard against empty session ID.

If a client sends DELETE without Mcp-Session-Id, the handler will delete session-scoped stores for "" (Line 659). Consider returning 400 Bad Request when sessionID == "" to prevent unintended cleanup of non-existent sessions.

 func (s *StreamableHTTPServer) handleDelete(w http.ResponseWriter, r *http.Request) {
 	// delete request terminate the session
 	sessionID := r.Header.Get(HeaderKeySessionID)
+	if sessionID == "" {
+		http.Error(w, "Missing session ID", http.StatusBadRequest)
+		return
+	}
 	sessionIdManager := s.sessionIdManagerResolver.ResolveSessionIdManager(r)
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 919c4bf and 50551f6.

📒 Files selected for processing (2)
  • server/streamable_http.go (1 hunks)
  • server/streamable_http_test.go (2 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
**/*.go

📄 CodeRabbit inference engine (AGENTS.md)

**/*.go: Order imports: standard library first, then third-party, then local packages (goimports enforces this)
Follow Go naming conventions: exported identifiers in PascalCase; unexported in camelCase; acronyms uppercase (HTTP, JSON, MCP)
Error handling: return sentinel errors, wrap with fmt.Errorf("context: %w", err), and check with errors.Is/As
Prefer explicit types and strongly-typed structs; avoid using any except where protocol flexibility is required (e.g., Arguments any)
All exported types and functions must have GoDoc comments starting with the identifier name; avoid inline comments unless necessary
Functions that are handlers or long-running must accept context.Context as the first parameter
Ensure thread safety for shared state using sync.Mutex and document thread-safety requirements in comments
For JSON: use json struct tags with omitempty for optional fields; use json.RawMessage for flexible/deferred parsing

Files:

  • server/streamable_http.go
  • server/streamable_http_test.go
**/*_test.go

📄 CodeRabbit inference engine (AGENTS.md)

**/*_test.go: Testing: use testify/assert and testify/require
Write table-driven tests using a tests := []struct{ name, ... } pattern
Go test files must end with _test.go

Files:

  • server/streamable_http_test.go
🧠 Learnings (2)
📚 Learning: 2025-10-13T09:35:20.180Z
Learnt from: CR
Repo: mark3labs/mcp-go PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-10-13T09:35:20.180Z
Learning: Applies to **/*_test.go : Testing: use testify/assert and testify/require

Applied to files:

  • server/streamable_http_test.go
📚 Learning: 2025-03-04T07:00:57.111Z
Learnt from: xinwo
Repo: mark3labs/mcp-go PR: 35
File: mcp/tools.go:0-0
Timestamp: 2025-03-04T07:00:57.111Z
Learning: The Tool struct in the mark3labs/mcp-go project should handle both InputSchema and RawInputSchema consistently between MarshalJSON and UnmarshalJSON methods, even though the tools response from MCP server typically doesn't contain rawInputSchema.

Applied to files:

  • server/streamable_http_test.go
🧬 Code graph analysis (1)
server/streamable_http_test.go (4)
server/session.go (1)
  • ClientSession (12-21)
server/hooks.go (1)
  • Hooks (94-121)
client/transport/constants.go (1)
  • HeaderKeySessionID (5-5)
server/constants.go (1)
  • HeaderKeySessionID (5-5)
🔇 Additional comments (1)
server/streamable_http_test.go (1)

18-20: Good move switching new assertions to testify.
Matches AGENTS.md guidance. Based on learnings, ...

Comment on lines +2490 to +2527
func TestStreamableHTTP_Delete(t *testing.T) {
var hookCalled bool
var hookSession ClientSession

hooks := &Hooks{}
hooks.AddOnUnregisterSession(func(ctx context.Context, session ClientSession) {
hookCalled = true
hookSession = session
})

mcpServer := NewMCPServer("test-mcp-server", "1.0", WithHooks(hooks))
sseServer := NewStreamableHTTPServer(mcpServer, WithStateful(true))
testServer := httptest.NewServer(sseServer)
defer testServer.Close()

resp, err := postJSON(testServer.URL, initRequest)
require.NoError(t, err)
resp.Body.Close()
sessionID := resp.Header.Get(HeaderKeySessionID)

req, _ := http.NewRequest(http.MethodDelete, testServer.URL, nil)
req.Header.Set(HeaderKeySessionID, sessionID)

resp, err = testServer.Client().Do(req)
require.NoError(t, err)
resp.Body.Close()

require.Equal(t, http.StatusOK, resp.StatusCode)

_, activeSessionExists := sseServer.activeSessions.Load(sessionID)
assert.False(t, activeSessionExists)

_, serverSessionExists := mcpServer.sessions.Load(sessionID)
assert.False(t, serverSessionExists)

assert.True(t, hookCalled)
assert.Equal(t, sessionID, hookSession.SessionID())
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Fix potential race/panic in the unregister hook assertions.

hookCalled / hookSession are written in the hook and read later without synchronization; because the hook currently sets hookCalled = true before hookSession = session, the final hookSession.SessionID() assertion can panic or flake if hook execution is async or reordered by the scheduler.

Prefer capturing the session via a channel (and also assert sessionID is non-empty):

 func TestStreamableHTTP_Delete(t *testing.T) {
-	var hookCalled bool
-	var hookSession ClientSession
+	hookSessionCh := make(chan ClientSession, 1)
 
 	hooks := &Hooks{}
 	hooks.AddOnUnregisterSession(func(ctx context.Context, session ClientSession) {
-		hookCalled = true
-		hookSession = session
+		select {
+		case hookSessionCh <- session:
+		default:
+		}
 	})
@@
 	resp, err := postJSON(testServer.URL, initRequest)
 	require.NoError(t, err)
 	resp.Body.Close()
 	sessionID := resp.Header.Get(HeaderKeySessionID)
+	require.NotEmpty(t, sessionID)
@@
 	require.Equal(t, http.StatusOK, resp.StatusCode)
@@
-	assert.True(t, hookCalled)
-	assert.Equal(t, sessionID, hookSession.SessionID())
+	var hookSession ClientSession
+	select {
+	case hookSession = <-hookSessionCh:
+	case <-time.After(1 * time.Second):
+		t.Fatal("timeout waiting for OnUnregisterSession hook")
+	}
+	require.NotNil(t, hookSession)
+	assert.Equal(t, sessionID, hookSession.SessionID())
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func TestStreamableHTTP_Delete(t *testing.T) {
var hookCalled bool
var hookSession ClientSession
hooks := &Hooks{}
hooks.AddOnUnregisterSession(func(ctx context.Context, session ClientSession) {
hookCalled = true
hookSession = session
})
mcpServer := NewMCPServer("test-mcp-server", "1.0", WithHooks(hooks))
sseServer := NewStreamableHTTPServer(mcpServer, WithStateful(true))
testServer := httptest.NewServer(sseServer)
defer testServer.Close()
resp, err := postJSON(testServer.URL, initRequest)
require.NoError(t, err)
resp.Body.Close()
sessionID := resp.Header.Get(HeaderKeySessionID)
req, _ := http.NewRequest(http.MethodDelete, testServer.URL, nil)
req.Header.Set(HeaderKeySessionID, sessionID)
resp, err = testServer.Client().Do(req)
require.NoError(t, err)
resp.Body.Close()
require.Equal(t, http.StatusOK, resp.StatusCode)
_, activeSessionExists := sseServer.activeSessions.Load(sessionID)
assert.False(t, activeSessionExists)
_, serverSessionExists := mcpServer.sessions.Load(sessionID)
assert.False(t, serverSessionExists)
assert.True(t, hookCalled)
assert.Equal(t, sessionID, hookSession.SessionID())
}
func TestStreamableHTTP_Delete(t *testing.T) {
hookSessionCh := make(chan ClientSession, 1)
hooks := &Hooks{}
hooks.AddOnUnregisterSession(func(ctx context.Context, session ClientSession) {
select {
case hookSessionCh <- session:
default:
}
})
mcpServer := NewMCPServer("test-mcp-server", "1.0", WithHooks(hooks))
sseServer := NewStreamableHTTPServer(mcpServer, WithStateful(true))
testServer := httptest.NewServer(sseServer)
defer testServer.Close()
resp, err := postJSON(testServer.URL, initRequest)
require.NoError(t, err)
resp.Body.Close()
sessionID := resp.Header.Get(HeaderKeySessionID)
require.NotEmpty(t, sessionID)
req, _ := http.NewRequest(http.MethodDelete, testServer.URL, nil)
req.Header.Set(HeaderKeySessionID, sessionID)
resp, err = testServer.Client().Do(req)
require.NoError(t, err)
resp.Body.Close()
require.Equal(t, http.StatusOK, resp.StatusCode)
_, activeSessionExists := sseServer.activeSessions.Load(sessionID)
assert.False(t, activeSessionExists)
_, serverSessionExists := mcpServer.sessions.Load(sessionID)
assert.False(t, serverSessionExists)
var hookSession ClientSession
select {
case hookSession = <-hookSessionCh:
case <-time.After(1 * time.Second):
t.Fatal("timeout waiting for OnUnregisterSession hook")
}
require.NotNil(t, hookSession)
assert.Equal(t, sessionID, hookSession.SessionID())
}
🤖 Prompt for AI Agents
In server/streamable_http_test.go around lines 2490 to 2527, the test reads
hookCalled/hookSession without synchronization which can race/panic; change the
hook to send the session (or sessionID) over a channel and in the test wait to
receive it (with a timeout) before making assertions, assert the received
sessionID is non-empty, then compare it to the request sessionID; this ensures
ordering and avoids read/write races on shared variables.

@cnnrznn
Copy link
Author

cnnrznn commented Dec 16, 2025

@ezynda3 - thoughts on this approach?

Re: #651

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bug: OnUnregisterSession hooks not called in the StreamableHttp session termination handler

2 participants