Skip to content

Commit e7d2547

Browse files
feat(tools): implicitly register capabilities (#292)
When users add tools via AddTool or AddSessionTool, implicitly set the tools capability. If the user has not already called WithToolCapabilities, then default listChanged to true, but honor any existing value. This mimics the behavior of the official typescript sdk, which registers `tools.listChanged: true` when the user adds a tool to the MCP server.
1 parent c1e70f3 commit e7d2547

File tree

4 files changed

+113
-3
lines changed

4 files changed

+113
-3
lines changed

server/server.go

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -411,20 +411,29 @@ func (s *MCPServer) AddTool(tool mcp.Tool, handler ToolHandlerFunc) {
411411
s.AddTools(ServerTool{Tool: tool, Handler: handler})
412412
}
413413

414-
// AddTools registers multiple tools at once
415-
func (s *MCPServer) AddTools(tools ...ServerTool) {
414+
// Register tool capabilities due to a tool being added. Default to
415+
// listChanged: true, but don't change the value if we've already explicitly
416+
// registered tools.listChanged false.
417+
func (s *MCPServer) implicitlyRegisterToolCapabilities() {
416418
s.capabilitiesMu.RLock()
417419
if s.capabilities.tools == nil {
418420
s.capabilitiesMu.RUnlock()
419421

420422
s.capabilitiesMu.Lock()
421423
if s.capabilities.tools == nil {
422-
s.capabilities.tools = &toolCapabilities{}
424+
s.capabilities.tools = &toolCapabilities{
425+
listChanged: true,
426+
}
423427
}
424428
s.capabilitiesMu.Unlock()
425429
} else {
426430
s.capabilitiesMu.RUnlock()
427431
}
432+
}
433+
434+
// AddTools registers multiple tools at once
435+
func (s *MCPServer) AddTools(tools ...ServerTool) {
436+
s.implicitlyRegisterToolCapabilities()
428437

429438
s.toolsMu.Lock()
430439
for _, entry := range tools {

server/server_test.go

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1624,3 +1624,46 @@ func BenchmarkMCPServer_PaginationForReflect(b *testing.B) {
16241624
_, _, _ = listByPaginationForReflect[mcp.Tool](ctx, server, "dG9vbDY1NA==", list)
16251625
}
16261626
}
1627+
1628+
func TestMCPServer_ToolCapabilitiesBehavior(t *testing.T) {
1629+
tests := []struct {
1630+
name string
1631+
serverOptions []ServerOption
1632+
validateServer func(t *testing.T, s *MCPServer)
1633+
}{
1634+
{
1635+
name: "no tool capabilities provided",
1636+
serverOptions: []ServerOption{
1637+
// No WithToolCapabilities
1638+
},
1639+
validateServer: func(t *testing.T, s *MCPServer) {
1640+
s.capabilitiesMu.RLock()
1641+
defer s.capabilitiesMu.RUnlock()
1642+
1643+
require.NotNil(t, s.capabilities.tools, "tools capability should be initialized")
1644+
assert.True(t, s.capabilities.tools.listChanged, "listChanged should be true when no capabilities were provided")
1645+
},
1646+
},
1647+
{
1648+
name: "tools.listChanged set to false",
1649+
serverOptions: []ServerOption{
1650+
WithToolCapabilities(false),
1651+
},
1652+
validateServer: func(t *testing.T, s *MCPServer) {
1653+
s.capabilitiesMu.RLock()
1654+
defer s.capabilitiesMu.RUnlock()
1655+
1656+
require.NotNil(t, s.capabilities.tools, "tools capability should be initialized")
1657+
assert.False(t, s.capabilities.tools.listChanged, "listChanged should remain false when explicitly set to false")
1658+
},
1659+
},
1660+
}
1661+
1662+
for _, tt := range tests {
1663+
t.Run(tt.name, func(t *testing.T) {
1664+
server := NewMCPServer("test-server", "1.0.0", tt.serverOptions...)
1665+
server.AddTool(mcp.NewTool("test-tool"), nil)
1666+
tt.validateServer(t, server)
1667+
})
1668+
}
1669+
}

server/session.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,8 @@ func (s *MCPServer) AddSessionTools(sessionID string, tools ...ServerTool) error
224224
return ErrSessionDoesNotSupportTools
225225
}
226226

227+
s.implicitlyRegisterToolCapabilities()
228+
227229
// Get existing tools (this should return a thread-safe copy)
228230
sessionTools := session.GetSessionTools()
229231

server/session_test.go

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -802,3 +802,59 @@ 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_SessionToolCapabilitiesBehavior(t *testing.T) {
807+
tests := []struct {
808+
name string
809+
serverOptions []ServerOption
810+
validateServer func(t *testing.T, s *MCPServer, session *sessionTestClientWithTools)
811+
}{
812+
{
813+
name: "no tool capabilities provided",
814+
serverOptions: []ServerOption{
815+
// No WithToolCapabilities
816+
},
817+
validateServer: func(t *testing.T, s *MCPServer, session *sessionTestClientWithTools) {
818+
s.capabilitiesMu.RLock()
819+
defer s.capabilitiesMu.RUnlock()
820+
821+
require.NotNil(t, s.capabilities.tools, "tools capability should be initialized")
822+
assert.True(t, s.capabilities.tools.listChanged, "listChanged should be true when no capabilities were provided")
823+
},
824+
},
825+
{
826+
name: "tools.listChanged set to false",
827+
serverOptions: []ServerOption{
828+
WithToolCapabilities(false),
829+
},
830+
validateServer: func(t *testing.T, s *MCPServer, session *sessionTestClientWithTools) {
831+
s.capabilitiesMu.RLock()
832+
defer s.capabilitiesMu.RUnlock()
833+
834+
require.NotNil(t, s.capabilities.tools, "tools capability should be initialized")
835+
assert.False(t, s.capabilities.tools.listChanged, "listChanged should remain false when explicitly set to false")
836+
},
837+
},
838+
}
839+
840+
for _, tt := range tests {
841+
t.Run(tt.name, func(t *testing.T) {
842+
server := NewMCPServer("test-server", "1.0.0", tt.serverOptions...)
843+
844+
// Create and register a session
845+
session := &sessionTestClientWithTools{
846+
sessionID: "test-session",
847+
notificationChannel: make(chan mcp.JSONRPCNotification, 10),
848+
initialized: true,
849+
}
850+
err := server.RegisterSession(context.Background(), session)
851+
require.NoError(t, err)
852+
853+
// Add a session tool and verify listChanged remains false
854+
err = server.AddSessionTool(session.SessionID(), mcp.NewTool("test-tool"), nil)
855+
require.NoError(t, err)
856+
857+
tt.validateServer(t, server, session)
858+
})
859+
}
860+
}

0 commit comments

Comments
 (0)