|
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