|
4 | 4 | "context" |
5 | 5 | "encoding/json" |
6 | 6 | "errors" |
| 7 | +"maps" |
7 | 8 | "sync" |
8 | 9 | "sync/atomic" |
9 | 10 | "testing" |
@@ -100,6 +101,60 @@ func (f *sessionTestClientWithTools) SetSessionTools(tools map[string]ServerTool |
100 | 101 | f.sessionTools = toolsCopy |
101 | 102 | } |
102 | 103 |
|
| 104 | +// sessionTestClientWithResources implements the SessionWithResources interface for testing |
| 105 | +type sessionTestClientWithResources struct { |
| 106 | +sessionID string |
| 107 | +notificationChannel chan mcp.JSONRPCNotification |
| 108 | +initialized bool |
| 109 | +sessionResources map[string]ServerResource |
| 110 | +mu sync.RWMutex // Mutex to protect concurrent access to sessionResources |
| 111 | +} |
| 112 | + |
| 113 | +func (f *sessionTestClientWithResources) SessionID() string { |
| 114 | +return f.sessionID |
| 115 | +} |
| 116 | + |
| 117 | +func (f *sessionTestClientWithResources) NotificationChannel() chan<- mcp.JSONRPCNotification { |
| 118 | +return f.notificationChannel |
| 119 | +} |
| 120 | + |
| 121 | +func (f *sessionTestClientWithResources) Initialize() { |
| 122 | +f.initialized = true |
| 123 | +} |
| 124 | + |
| 125 | +func (f *sessionTestClientWithResources) Initialized() bool { |
| 126 | +return f.initialized |
| 127 | +} |
| 128 | + |
| 129 | +func (f *sessionTestClientWithResources) GetSessionResources() map[string]ServerResource { |
| 130 | +f.mu.RLock() |
| 131 | +defer f.mu.RUnlock() |
| 132 | + |
| 133 | +if f.sessionResources == nil { |
| 134 | +return nil |
| 135 | +} |
| 136 | + |
| 137 | +// Return a copy of the map to prevent concurrent modification |
| 138 | +resourcesCopy := make(map[string]ServerResource, len(f.sessionResources)) |
| 139 | +maps.Copy(resourcesCopy, f.sessionResources) |
| 140 | +return resourcesCopy |
| 141 | +} |
| 142 | + |
| 143 | +func (f *sessionTestClientWithResources) SetSessionResources(resources map[string]ServerResource) { |
| 144 | +f.mu.Lock() |
| 145 | +defer f.mu.Unlock() |
| 146 | + |
| 147 | +if resources == nil { |
| 148 | +f.sessionResources = nil |
| 149 | +return |
| 150 | +} |
| 151 | + |
| 152 | +// Create a copy of the map to prevent concurrent modification |
| 153 | +resourcesCopy := make(map[string]ServerResource, len(resources)) |
| 154 | +maps.Copy(resourcesCopy, resources) |
| 155 | +f.sessionResources = resourcesCopy |
| 156 | +} |
| 157 | + |
103 | 158 | // sessionTestClientWithClientInfo implements the SessionWithClientInfo interface for testing |
104 | 159 | type sessionTestClientWithClientInfo struct { |
105 | 160 | sessionID string |
@@ -151,7 +206,7 @@ func (f *sessionTestClientWithClientInfo) SetClientCapabilities(clientCapabiliti |
151 | 206 | f.clientCapabilities.Store(clientCapabilities) |
152 | 207 | } |
153 | 208 |
|
154 | | -// sessionTestClientWithTools implements the SessionWithLogging interface for testing |
| 209 | +// sessionTestClientWithLogging implements the SessionWithLogging interface for testing |
155 | 210 | type sessionTestClientWithLogging struct { |
156 | 211 | sessionID string |
157 | 212 | notificationChannel chan mcp.JSONRPCNotification |
@@ -190,6 +245,7 @@ func (f *sessionTestClientWithLogging) GetLogLevel() mcp.LoggingLevel { |
190 | 245 | var ( |
191 | 246 | _ ClientSession = (*sessionTestClient)(nil) |
192 | 247 | _ SessionWithTools = (*sessionTestClientWithTools)(nil) |
| 248 | +_ SessionWithResources = (*sessionTestClientWithResources)(nil) |
193 | 249 | _ SessionWithLogging = (*sessionTestClientWithLogging)(nil) |
194 | 250 | _ SessionWithClientInfo = (*sessionTestClientWithClientInfo)(nil) |
195 | 251 | ) |
@@ -260,6 +316,75 @@ func TestSessionWithTools_Integration(t *testing.T) { |
260 | 316 | }) |
261 | 317 | } |
262 | 318 |
|
| 319 | +func TestSessionWithResources_Integration(t *testing.T) { |
| 320 | +server := NewMCPServer("test-server", "1.0.0") |
| 321 | + |
| 322 | +// Create session-specific resources |
| 323 | +sessionResource := ServerResource{ |
| 324 | +Resource: mcp.NewResource("ui://resource", "session-resource"), |
| 325 | +Handler: func(ctx context.Context, request mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) { |
| 326 | +return []mcp.ResourceContents{mcp.TextResourceContents{ |
| 327 | +URI: "ui://resource", |
| 328 | +Text: "session-resource result", |
| 329 | +}}, nil |
| 330 | +}, |
| 331 | +} |
| 332 | + |
| 333 | +// Create a session with resources |
| 334 | +session := &sessionTestClientWithResources{ |
| 335 | +sessionID: "session-1", |
| 336 | +notificationChannel: make(chan mcp.JSONRPCNotification, 10), |
| 337 | +initialized: true, |
| 338 | +sessionResources: map[string]ServerResource{ |
| 339 | +"ui://resource": sessionResource, |
| 340 | +}, |
| 341 | +} |
| 342 | + |
| 343 | +// Register the session |
| 344 | +err := server.RegisterSession(context.Background(), session) |
| 345 | +require.NoError(t, err) |
| 346 | + |
| 347 | +// Test that we can access the session-specific resource |
| 348 | +testReq := mcp.ReadResourceRequest{} |
| 349 | +testReq.Params.URI = "ui://resource" |
| 350 | +testReq.Params.Arguments = map[string]any{} |
| 351 | + |
| 352 | +// Call using session context |
| 353 | +sessionCtx := server.WithContext(context.Background(), session) |
| 354 | + |
| 355 | +// Check if the session was stored in the context correctly |
| 356 | +s := ClientSessionFromContext(sessionCtx) |
| 357 | +require.NotNil(t, s, "Session should be available from context") |
| 358 | +assert.Equal(t, session.SessionID(), s.SessionID(), "Session ID should match") |
| 359 | + |
| 360 | +// Check if the session can be cast to SessionWithResources |
| 361 | +swr, ok := s.(SessionWithResources) |
| 362 | +require.True(t, ok, "Session should implement SessionWithResources") |
| 363 | + |
| 364 | +// Check if the resources are accessible |
| 365 | +resources := swr.GetSessionResources() |
| 366 | +require.NotNil(t, resources, "Session resources should be available") |
| 367 | +require.Contains(t, resources, "ui://resource", "Session should have ui://resource") |
| 368 | + |
| 369 | +// Test session resource access with session context |
| 370 | +t.Run("test session resource access", func(t *testing.T) { |
| 371 | +// First test directly getting the resource from session resources |
| 372 | +resource, exists := resources["ui://resource"] |
| 373 | +require.True(t, exists, "Session resource should exist in the map") |
| 374 | +require.NotNil(t, resource, "Session resource should not be nil") |
| 375 | + |
| 376 | +// Now test calling directly with the handler |
| 377 | +result, err := resource.Handler(sessionCtx, testReq) |
| 378 | +require.NoError(t, err, "No error calling session resource handler directly") |
| 379 | +require.NotNil(t, result, "Result should not be nil") |
| 380 | +require.Len(t, result, 1, "Result should have one content item") |
| 381 | + |
| 382 | +textContent, ok := result[0].(mcp.TextResourceContents) |
| 383 | +require.True(t, ok, "Content should be TextResourceContents") |
| 384 | +assert.Equal(t, "session-resource result", textContent.Text, "Result text should match") |
| 385 | +}) |
| 386 | +} |
| 387 | + |
263 | 388 | func TestMCPServer_ToolsWithSessionTools(t *testing.T) { |
264 | 389 | // Basic test to verify that session-specific tools are returned correctly in a tools list |
265 | 390 | server := NewMCPServer("test-server", "1.0.0", WithToolCapabilities(true)) |
|
0 commit comments