Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module github.com/mark3labs/mcp-go

go 1.23
go 1.23.0
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

go failed to run on macos without adding in this patch version specifier 🤷


require (
github.com/google/uuid v1.6.0
Expand Down
83 changes: 75 additions & 8 deletions server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -822,15 +822,56 @@ func (s *MCPServer) handleListResources(
) (*mcp.ListResourcesResult, *requestError) {
s.resourcesMu.RLock()
resources := make([]mcp.Resource, 0, len(s.resources))
for _, entry := range s.resources {
resources = append(resources, entry.resource)

// Get all resource names for consistent ordering
resourceNames := make([]string, 0, len(s.resources))
for name := range s.resources {
resourceNames = append(resourceNames, name)
}

// Sort the resource names for consistent ordering
sort.Strings(resourceNames)

// Add resources in sorted order
for _, name := range resourceNames {
resources = append(resources, s.resources[name].resource)
}
s.resourcesMu.RUnlock()

// Sort the resources by name
sort.Slice(resources, func(i, j int) bool {
return resources[i].Name < resources[j].Name
})
// Check if there are session-specific resources
session := ClientSessionFromContext(ctx)
if session != nil {
if sessionWithResources, ok := session.(SessionWithResources); ok {
if sessionResources := sessionWithResources.GetSessionResources(); sessionResources != nil {
// Override or add session-specific resources
// We need to create a map first to merge the resources properly
resourceMap := make(map[string]mcp.Resource)

// Add global resources first
for _, resource := range resources {
resourceMap[resource.Name] = resource
}

// Then override with session-specific resources
for name, serverResource := range sessionResources {
resourceMap[name] = serverResource.Resource
}

// Convert back to slice
resources = make([]mcp.Resource, 0, len(resourceMap))
for _, resource := range resourceMap {
resources = append(resources, resource)
}

// Sort again to maintain consistent ordering
sort.Slice(resources, func(i, j int) bool {
return resources[i].Name < resources[j].Name
})
}
}
}

// Apply pagination
resourcesToReturn, nextCursor, err := listByPagination(
ctx,
s,
Expand Down Expand Up @@ -895,9 +936,35 @@ func (s *MCPServer) handleReadResource(
request mcp.ReadResourceRequest,
) (*mcp.ReadResourceResult, *requestError) {
s.resourcesMu.RLock()

// First check session-specific resources
var handler ResourceHandlerFunc
var ok bool

session := ClientSessionFromContext(ctx)
if session != nil {
if sessionWithResources, typeAssertOk := session.(SessionWithResources); typeAssertOk {
if sessionResources := sessionWithResources.GetSessionResources(); sessionResources != nil {
resource, sessionOk := sessionResources[request.Params.URI]
if sessionOk {
handler = resource.Handler
ok = true
}
}
}
}

// If not found in session tools, check global tools
if !ok {
globalResource, rok := s.resources[request.Params.URI]
if rok {
handler = globalResource.handler
ok = true
}
}

// First try direct resource handlers
if entry, ok := s.resources[request.Params.URI]; ok {
handler := entry.handler
if ok {
s.resourcesMu.RUnlock()

finalHandler := handler
Expand Down
11 changes: 11 additions & 0 deletions server/session.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,17 @@ type SessionWithTools interface {
SetSessionTools(tools map[string]ServerTool)
}

// SessionWithResources is an extension of ClientSession that can store session-specific resource data
type SessionWithResources interface {
ClientSession
// GetSessionResources returns the resources specific to this session, if any
// This method must be thread-safe for concurrent access
GetSessionResources() map[string]ServerResource
// SetSessionResources sets resources specific to this session
// This method must be thread-safe for concurrent access
SetSessionResources(resources map[string]ServerResource)
}

// SessionWithClientInfo is an extension of ClientSession that can store client info
type SessionWithClientInfo interface {
ClientSession
Expand Down
121 changes: 121 additions & 0 deletions server/session_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"encoding/json"
"errors"
"maps"
"sync"
"sync/atomic"
"testing"
Expand Down Expand Up @@ -100,6 +101,60 @@ func (f *sessionTestClientWithTools) SetSessionTools(tools map[string]ServerTool
f.sessionTools = toolsCopy
}

// sessionTestClientWithTools implements the SessionWithTools interface for testing
type sessionTestClientWithResources struct {
sessionID string
notificationChannel chan mcp.JSONRPCNotification
initialized bool
sessionResources map[string]ServerResource
mu sync.RWMutex // Mutex to protect concurrent access to sessionTools
}

func (f *sessionTestClientWithResources) SessionID() string {
return f.sessionID
}

func (f *sessionTestClientWithResources) NotificationChannel() chan<- mcp.JSONRPCNotification {
return f.notificationChannel
}

func (f *sessionTestClientWithResources) Initialize() {
f.initialized = true
}

func (f *sessionTestClientWithResources) Initialized() bool {
return f.initialized
}

func (f *sessionTestClientWithResources) GetSessionResources() map[string]ServerResource {
f.mu.RLock()
defer f.mu.RUnlock()

if f.sessionResources == nil {
return nil
}

// Return a copy of the map to prevent concurrent modification
resourcesCopy := make(map[string]ServerResource, len(f.sessionResources))
maps.Copy(resourcesCopy, f.sessionResources)
return resourcesCopy
}

func (f *sessionTestClientWithResources) SetSessionResources(resources map[string]ServerResource) {
f.mu.Lock()
defer f.mu.Unlock()

if resources == nil {
f.sessionResources = nil
return
}

// Create a copy of the map to prevent concurrent modification
resourcesCopy := make(map[string]ServerResource, len(resources))
maps.Copy(resourcesCopy, resources)
f.sessionResources = resourcesCopy
}

// sessionTestClientWithClientInfo implements the SessionWithClientInfo interface for testing
type sessionTestClientWithClientInfo struct {
sessionID string
Expand Down Expand Up @@ -260,6 +315,72 @@ func TestSessionWithTools_Integration(t *testing.T) {
})
}

func TestSessionWithResources_Integration(t *testing.T) {
server := NewMCPServer("test-server", "1.0.0", WithToolCapabilities(true))

// Create session-specific resources
sessionResource := ServerResource{
Resource: mcp.NewResource("ui://resource", "session-resource"),
Handler: func(ctx context.Context, request mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) {
return []mcp.ResourceContents{mcp.TextResourceContents{Text: "session-tool result"}}, nil
},
}

// Create a session with resources
session := &sessionTestClientWithResources{
sessionID: "session-1",
notificationChannel: make(chan mcp.JSONRPCNotification, 10),
initialized: true,
sessionResources: map[string]ServerResource{
"session-resource": sessionResource,
},
}

// Register the session
err := server.RegisterSession(context.Background(), session)
require.NoError(t, err)

// Test that we can access the session-specific tool
testReq := mcp.ReadResourceRequest{}
testReq.Params.URI = "ui://resource"
testReq.Params.Arguments = map[string]any{}

// Call using session context
sessionCtx := server.WithContext(context.Background(), session)

// Check if the session was stored in the context correctly
s := ClientSessionFromContext(sessionCtx)
require.NotNil(t, s, "Session should be available from context")
assert.Equal(t, session.SessionID(), s.SessionID(), "Session ID should match")

// Check if the session can be cast to SessionWithResources
swr, ok := s.(SessionWithResources)
require.True(t, ok, "Session should implement SessionWithResources")

// Check if the resources are accessible
resources := swr.GetSessionResources()
require.NotNil(t, resources, "Session resources should be available")
require.Contains(t, resources, "session-resource", "Session should have session-resource")

// Test session resource access with session context
t.Run("test session resource access", func(t *testing.T) {
// First test directly getting the resource from session resources
resource, exists := resources["session-resource"]
require.True(t, exists, "Session resource should exist in the map")
require.NotNil(t, resource, "Session resource should not be nil")

// Now test calling directly with the handler
result, err := resource.Handler(sessionCtx, testReq)
require.NoError(t, err, "No error calling session resource handler directly")
require.NotNil(t, result, "Result should not be nil")
require.Len(t, result, 1, "Result should have one content item")

textContent, ok := result[0].(mcp.TextResourceContents)
require.True(t, ok, "Content should be TextResourceContents")
assert.Equal(t, "session-tool result", textContent.Text, "Result text should match")
})
}

func TestMCPServer_ToolsWithSessionTools(t *testing.T) {
// Basic test to verify that session-specific tools are returned correctly in a tools list
server := NewMCPServer("test-server", "1.0.0", WithToolCapabilities(true))
Expand Down
23 changes: 23 additions & 0 deletions server/sse.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ type sseSession struct {
initialized atomic.Bool
loggingLevel atomic.Value
tools sync.Map // stores session-specific tools
resources sync.Map // stores session-specific resources
clientInfo atomic.Value // stores session-specific client info
clientCapabilities atomic.Value // stores session-specific client capabilities
}
Expand Down Expand Up @@ -75,6 +76,27 @@ func (s *sseSession) GetLogLevel() mcp.LoggingLevel {
return level.(mcp.LoggingLevel)
}

func (s *sseSession) GetSessionResources() map[string]ServerResource {
resources := make(map[string]ServerResource)
s.resources.Range(func(key, value any) bool {
if resource, ok := value.(ServerResource); ok {
resources[key.(string)] = resource
}
return true
})
return resources
}

func (s *sseSession) SetSessionResources(resources map[string]ServerResource) {
// Clear existing resources
s.resources.Clear()

// Set new resources
for name, resource := range resources {
s.resources.Store(name, resource)
}
}

func (s *sseSession) GetSessionTools() map[string]ServerTool {
tools := make(map[string]ServerTool)
s.tools.Range(func(key, value any) bool {
Expand Down Expand Up @@ -125,6 +147,7 @@ func (s *sseSession) GetClientCapabilities() mcp.ClientCapabilities {
var (
_ ClientSession = (*sseSession)(nil)
_ SessionWithTools = (*sseSession)(nil)
_ SessionWithResources = (*sseSession)(nil)
_ SessionWithLogging = (*sseSession)(nil)
_ SessionWithClientInfo = (*sseSession)(nil)
)
Expand Down
32 changes: 32 additions & 0 deletions www/docs/pages/servers/resources.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -543,6 +543,38 @@ func (h *CachedResourceHandler) HandleResource(ctx context.Context, req mcp.Read
}
```

## Advanced Resource Patterns

### Session-specific Resources

You can add resources to a specific client session using the `SessionWithResources` interface.

```go
sseServer := server.NewSSEServer(
s,
server.WithAppendQueryToMessageEndpoint(),
server.WithSSEContextFunc(func(ctx context.Context, r *http.Request) context.Context {
withNewResources := r.URL.Query().Get("withNewResources")
if withNewResources != "1" {
return ctx
}

session := server.ClientSessionFromContext(ctx)
if sessionWithResources, ok := session.(server.SessionWithResources); ok {
// Add the new resources
sessionWithResources.SetSessionResources(map[string]server.ServerResource{
myNewResource.URI: {
Resource: myNewResource,
Handler: myNewResourceHandler,
},
})
}

return ctx
}),
)
```

## Next Steps

- **[Tools](/servers/tools)** - Learn to implement interactive functionality
Expand Down
30 changes: 30 additions & 0 deletions www/docs/pages/servers/tools.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -1047,6 +1047,36 @@ func addConditionalTools(s *server.MCPServer, userRole string) {
}
```

### Session-specific Tools

You can add tools to a specific client session using the `SessionWithTools` interface.

```go
sseServer := server.NewSSEServer(
s,
server.WithAppendQueryToMessageEndpoint(),
server.WithSSEContextFunc(func(ctx context.Context, r *http.Request) context.Context {
withNewTools := r.URL.Query().Get("withNewTools")
if withNewTools != "1" {
return ctx
}

session := server.ClientSessionFromContext(ctx)
if sessionWithTools, ok := session.(server.SessionWithTools); ok {
// Add the new tools
sessionWithTools.SetSessionTools(map[string]server.ServerTool{
myNewTool.Name: {
Tool: myNewTool,
Handler: NewToolHandler(myNewToolHandler),
},
})
}

return ctx
}),
)
```

## Next Steps

- **[Prompts](/servers/prompts)** - Learn to create reusable interaction templates
Expand Down