Skip to content

Commit cf00332

Browse files
fix: Gate notifications on capabilities
Servers may report their [tools.listChanged][] capability as false, in which case they indicate that they will not send notifications when available tools change. Honor the spec by not sending notifications/tools/list_changed notifications when capabilities.tools.listChanged is false. [tools.listChanged]: https://modelcontextprotocol.io/specification/2025-03-26/server/tools#capabilities
1 parent c1e70f3 commit cf00332

File tree

2 files changed

+71
-4
lines changed

2 files changed

+71
-4
lines changed

server/session.go

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -245,8 +245,12 @@ func (s *MCPServer) AddSessionTools(sessionID string, tools ...ServerTool) error
245245

246246
// It only makes sense to send tool notifications to initialized sessions --
247247
// if we're not initialized yet the client can't possibly have sent their
248-
// initial tools/list message
249-
if session.Initialized() {
248+
// initial tools/list message.
249+
//
250+
// For initialized sessions, honor tools.listChanged, which is specifically
251+
// about whether notifications will be sent or not.
252+
// see <https://modelcontextprotocol.io/specification/2025-03-26/server/tools#capabilities>
253+
if session.Initialized() && s.capabilities.tools != nil && s.capabilities.tools.listChanged {
250254
// Send notification only to this session
251255
if err := s.SendNotificationToSpecificClient(sessionID, "notifications/tools/list_changed", nil); err != nil {
252256
// Log the error but don't fail the operation
@@ -303,8 +307,12 @@ func (s *MCPServer) DeleteSessionTools(sessionID string, names ...string) error
303307

304308
// It only makes sense to send tool notifications to initialized sessions --
305309
// if we're not initialized yet the client can't possibly have sent their
306-
// initial tools/list message
307-
if session.Initialized() {
310+
// initial tools/list message.
311+
//
312+
// For initialized sessions, honor tools.listChanged, which is specifically
313+
// about whether notifications will be sent or not.
314+
// see <https://modelcontextprotocol.io/specification/2025-03-26/server/tools#capabilities>
315+
if session.Initialized() && s.capabilities.tools != nil && s.capabilities.tools.listChanged {
308316
// Send notification only to this session
309317
if err := s.SendNotificationToSpecificClient(sessionID, "notifications/tools/list_changed", nil); err != nil {
310318
// Log the error but don't fail the operation

server/session_test.go

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -802,3 +802,62 @@ func TestMCPServer_NotificationChannelBlocked(t *testing.T) {
802802
assert.Equal(t, "blocked-session", localErrorSessionID, "Session ID should be captured in the error hook")
803803
assert.Equal(t, "broadcast-message", localErrorMethod, "Method should be captured in the error hook")
804804
}
805+
806+
func TestMCPServer_ToolNotificationsDisabled(t *testing.T) {
807+
// This test verifies that when tool capabilities are disabled, we still
808+
// add/delete tools correctly but don't send notifications about it.
809+
//
810+
// This is important because:
811+
// 1. Tools should still work even if notifications are disabled
812+
// 2. We shouldn't waste resources sending notifications that won't be used
813+
// 3. The client might not be ready to handle tool notifications yet
814+
815+
// Create a server WITHOUT tool capabilities
816+
server := NewMCPServer("test-server", "1.0.0", WithToolCapabilities(false))
817+
ctx := context.Background()
818+
819+
// Create an initialized session
820+
sessionChan := make(chan mcp.JSONRPCNotification, 1)
821+
session := &sessionTestClientWithTools{
822+
sessionID: "session-1",
823+
notificationChannel: sessionChan,
824+
initialized: true,
825+
}
826+
827+
// Register the session
828+
err := server.RegisterSession(ctx, session)
829+
require.NoError(t, err)
830+
831+
// Add a tool
832+
err = server.AddSessionTools(session.SessionID(),
833+
ServerTool{Tool: mcp.NewTool("test-tool")},
834+
)
835+
require.NoError(t, err)
836+
837+
// Verify no notification was sent
838+
select {
839+
case <-sessionChan:
840+
t.Error("Expected no notification to be sent when capabilities.tools.listChanged is false")
841+
default:
842+
// This is the expected case - no notification should be sent
843+
}
844+
845+
// Verify tool was added to session
846+
assert.Len(t, session.GetSessionTools(), 1)
847+
assert.Contains(t, session.GetSessionTools(), "test-tool")
848+
849+
// Delete the tool
850+
err = server.DeleteSessionTools(session.SessionID(), "test-tool")
851+
require.NoError(t, err)
852+
853+
// Verify no notification was sent
854+
select {
855+
case <-sessionChan:
856+
t.Error("Expected no notification to be sent when capabilities.tools.listChanged is false")
857+
default:
858+
// This is the expected case - no notification should be sent
859+
}
860+
861+
// Verify tool was deleted from session
862+
assert.Len(t, session.GetSessionTools(), 0)
863+
}

0 commit comments

Comments
 (0)