Skip to content

Commit 5e2f369

Browse files
committed
feat: add session resource helper functions
- Add ErrSessionDoesNotSupportResources error constant - Implement AddSessionResource() helper for adding single resource - Implement AddSessionResources() helper for batch resource operations - Implement DeleteSessionResources() helper for removing resources - Auto-register resource capabilities with listChanged=true for session resources - Send notifications/resources/list_changed when resources are modified - Match session tools helper pattern for consistency
1 parent caa59b1 commit 5e2f369

File tree

2 files changed

+138
-5
lines changed

2 files changed

+138
-5
lines changed

server/errors.go

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,12 @@ var (
1313
ErrToolNotFound = errors.New("tool not found")
1414

1515
// Session-related errors
16-
ErrSessionNotFound = errors.New("session not found")
17-
ErrSessionExists = errors.New("session already exists")
18-
ErrSessionNotInitialized = errors.New("session not properly initialized")
19-
ErrSessionDoesNotSupportTools = errors.New("session does not support per-session tools")
20-
ErrSessionDoesNotSupportLogging = errors.New("session does not support setting logging level")
16+
ErrSessionNotFound = errors.New("session not found")
17+
ErrSessionExists = errors.New("session already exists")
18+
ErrSessionNotInitialized = errors.New("session not properly initialized")
19+
ErrSessionDoesNotSupportTools = errors.New("session does not support per-session tools")
20+
ErrSessionDoesNotSupportResources = errors.New("session does not support per-session resources")
21+
ErrSessionDoesNotSupportLogging = errors.New("session does not support setting logging level")
2122

2223
// Notification-related errors
2324
ErrNotificationNotInitialized = errors.New("notification channel not initialized")

server/session.go

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -460,3 +460,135 @@ func (s *MCPServer) DeleteSessionTools(sessionID string, names ...string) error
460460

461461
return nil
462462
}
463+
464+
// AddSessionResource adds a resource for a specific session
465+
func (s *MCPServer) AddSessionResource(sessionID string, resource mcp.Resource, handler ResourceHandlerFunc) error {
466+
return s.AddSessionResources(sessionID, ServerResource{Resource: resource, Handler: handler})
467+
}
468+
469+
// AddSessionResources adds resources for a specific session
470+
func (s *MCPServer) AddSessionResources(sessionID string, resources ...ServerResource) error {
471+
sessionValue, ok := s.sessions.Load(sessionID)
472+
if !ok {
473+
return ErrSessionNotFound
474+
}
475+
476+
session, ok := sessionValue.(SessionWithResources)
477+
if !ok {
478+
return ErrSessionDoesNotSupportResources
479+
}
480+
481+
// For session resources, we want listChanged enabled by default
482+
s.implicitlyRegisterCapabilities(
483+
func() bool { return s.capabilities.resources != nil },
484+
func() { s.capabilities.resources = &resourceCapabilities{listChanged: true} },
485+
)
486+
487+
// Get existing resources (this should return a thread-safe copy)
488+
sessionResources := session.GetSessionResources()
489+
490+
// Create a new map to avoid concurrent modification issues
491+
newSessionResources := make(map[string]ServerResource, len(sessionResources)+len(resources))
492+
493+
// Copy existing resources
494+
for k, v := range sessionResources {
495+
newSessionResources[k] = v
496+
}
497+
498+
// Add new resources
499+
for _, resource := range resources {
500+
newSessionResources[resource.Resource.URI] = resource
501+
}
502+
503+
// Set the resources (this should be thread-safe)
504+
session.SetSessionResources(newSessionResources)
505+
506+
// It only makes sense to send resource notifications to initialized sessions --
507+
// if we're not initialized yet the client can't possibly have sent their
508+
// initial resources/list message.
509+
//
510+
// For initialized sessions, honor resources.listChanged, which is specifically
511+
// about whether notifications will be sent or not.
512+
// see <https://modelcontextprotocol.io/specification/2025-03-26/server/resources#capabilities>
513+
if session.Initialized() && s.capabilities.resources != nil && s.capabilities.resources.listChanged {
514+
// Send notification only to this session
515+
if err := s.SendNotificationToSpecificClient(sessionID, "notifications/resources/list_changed", nil); err != nil {
516+
// Log the error but don't fail the operation
517+
// The resources were successfully added, but notification failed
518+
if s.hooks != nil && len(s.hooks.OnError) > 0 {
519+
hooks := s.hooks
520+
go func(sID string, hooks *Hooks) {
521+
ctx := context.Background()
522+
hooks.onError(ctx, nil, "notification", map[string]any{
523+
"method": "notifications/resources/list_changed",
524+
"sessionID": sID,
525+
}, fmt.Errorf("failed to send notification after adding resources: %w", err))
526+
}(sessionID, hooks)
527+
}
528+
}
529+
}
530+
531+
return nil
532+
}
533+
534+
// DeleteSessionResources removes resources from a specific session
535+
func (s *MCPServer) DeleteSessionResources(sessionID string, uris ...string) error {
536+
sessionValue, ok := s.sessions.Load(sessionID)
537+
if !ok {
538+
return ErrSessionNotFound
539+
}
540+
541+
session, ok := sessionValue.(SessionWithResources)
542+
if !ok {
543+
return ErrSessionDoesNotSupportResources
544+
}
545+
546+
// Get existing resources (this should return a thread-safe copy)
547+
sessionResources := session.GetSessionResources()
548+
if sessionResources == nil {
549+
return nil
550+
}
551+
552+
// Create a new map to avoid concurrent modification issues
553+
newSessionResources := make(map[string]ServerResource, len(sessionResources))
554+
555+
// Copy existing resources except those being deleted
556+
for k, v := range sessionResources {
557+
newSessionResources[k] = v
558+
}
559+
560+
// Remove specified resources
561+
for _, uri := range uris {
562+
delete(newSessionResources, uri)
563+
}
564+
565+
// Set the resources (this should be thread-safe)
566+
session.SetSessionResources(newSessionResources)
567+
568+
// It only makes sense to send resource notifications to initialized sessions --
569+
// if we're not initialized yet the client can't possibly have sent their
570+
// initial resources/list message.
571+
//
572+
// For initialized sessions, honor resources.listChanged, which is specifically
573+
// about whether notifications will be sent or not.
574+
// see <https://modelcontextprotocol.io/specification/2025-03-26/server/resources#capabilities>
575+
if session.Initialized() && s.capabilities.resources != nil && s.capabilities.resources.listChanged {
576+
// Send notification only to this session
577+
if err := s.SendNotificationToSpecificClient(sessionID, "notifications/resources/list_changed", nil); err != nil {
578+
// Log the error but don't fail the operation
579+
// The resources were successfully deleted, but notification failed
580+
if s.hooks != nil && len(s.hooks.OnError) > 0 {
581+
hooks := s.hooks
582+
go func(sID string, hooks *Hooks) {
583+
ctx := context.Background()
584+
hooks.onError(ctx, nil, "notification", map[string]any{
585+
"method": "notifications/resources/list_changed",
586+
"sessionID": sID,
587+
}, fmt.Errorf("failed to send notification after deleting resources: %w", err))
588+
}(sessionID, hooks)
589+
}
590+
}
591+
}
592+
593+
return nil
594+
}

0 commit comments

Comments
 (0)