Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
102 changes: 102 additions & 0 deletions mcpserver/roots.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
package mcpserver

import (
"context"
"fmt"
"log/slog"
"strings"
"sync"

"github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/server"
)

// clientRoots holds the roots provided by the client
// this assumes a single client, which may go out the window when we add support for streaming http.
var (
clientRoots []mcp.Root
clientRootsMu sync.RWMutex
)

// sendRootsListRequest sends a roots/list request to the client
func sendRootsListRequest(ctx context.Context, session server.ClientSession) {
if session == nil {
return
}

// Send roots/list request as a notification
// Note: In a proper implementation, this would be a request-response
// but for now we'll send as notification and handle response separately
notification := mcp.JSONRPCNotification{
JSONRPC: "2.0",
Notification: mcp.Notification{
Method: "roots/list",
Params: mcp.NotificationParams{},
},
}

select {
case session.NotificationChannel() <- notification:
slog.Info("Requested roots/list from client")
case <-ctx.Done():
return
}
}

// updateClientRoots parses roots from notification params and updates the global clientRoots
func updateClientRoots(notification mcp.JSONRPCNotification) int {
if notification.Params.AdditionalFields == nil {
return 0
}
rootsData, ok := notification.Params.AdditionalFields["roots"].([]any)
if !ok {
return 0
}

newRoots := make([]mcp.Root, 0, len(rootsData))
for _, rootData := range rootsData {
if rootMap, ok := rootData.(map[string]any); ok {
root := mcp.Root{}
if uri, ok := rootMap["uri"].(string); ok {
root.URI = uri
}
if name, ok := rootMap["name"].(string); ok {
root.Name = name
}
newRoots = append(newRoots, root)
}
}

clientRootsMu.Lock()
clientRoots = newRoots
clientRootsMu.Unlock()

return len(newRoots)
}

// repoOpenErrorMessage provides helpful error messages when repository opening fails
func repoOpenErrorMessage(source string, originalErr error) error {
baseMsg := fmt.Sprintf("failed to open repository '%s'", source)

// If we have client roots, suggest them
clientRootsMu.RLock()
defer clientRootsMu.RUnlock()

if len(clientRoots) > 0 {
baseMsg += "\n\nAvailable roots from client:"
for _, root := range clientRoots {
uri := root.URI
uri = strings.TrimPrefix(uri, "file://")
if root.Name != "" {
baseMsg += fmt.Sprintf("\n - %s (%s)", uri, root.Name)
} else {
baseMsg += fmt.Sprintf("\n - %s", uri)
}
}
return fmt.Errorf("%s: %w", baseMsg, originalErr)
}

// Fallback: suggest common patterns
baseMsg += "\n\nTry using:\n - '.' for current directory\n - An absolute path to your git repository"
return fmt.Errorf("%s: %w", baseMsg, originalErr)
}
36 changes: 34 additions & 2 deletions mcpserver/tools.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,12 +57,44 @@ type Tool struct {
}

func RunStdioServer(ctx context.Context, dag *dagger.Client) error {
// Create hooks to handle initialization and roots
hooks := &server.Hooks{}

// Add hook to check for roots capability and request initial roots
hooks.AddAfterInitialize(func(ctx context.Context, id any, message *mcp.InitializeRequest, result *mcp.InitializeResult) {
if message.Params.Capabilities.Roots != nil {
slog.Info("Client supports roots capability", "listChanged", message.Params.Capabilities.Roots.ListChanged)

// Get the client session and send roots/list request
if session := server.ClientSessionFromContext(ctx); session != nil {
sendRootsListRequest(ctx, session)
}
} else {
slog.Info("Client does not support roots capability")
}
})

s := server.NewMCPServer(
"Dagger",
"1.0.0",
server.WithInstructions(rules.AgentRules),
server.WithHooks(hooks),
)

// Add notification handler for roots updates from client
s.AddNotificationHandler("notifications/roots/list_changed", func(ctx context.Context, notification mcp.JSONRPCNotification) {
slog.Info("Received notifications/roots/list_changed from client")
count := updateClientRoots(notification)
slog.Info("Updated client roots", "count", count)
})

// Add response handler for roots/list responses
s.AddNotificationHandler("roots/list", func(ctx context.Context, notification mcp.JSONRPCNotification) {
slog.Info("Received roots/list response from client")
count := updateClientRoots(notification)
slog.Info("Updated client roots from response", "count", count)
})

for _, t := range tools {
s.AddTool(t.Definition, wrapToolWithClient(t, dag).Handler)
}
Expand Down Expand Up @@ -241,7 +273,7 @@ var EnvironmentOpenTool = &Tool{
func GetEnvironmentFromSource(ctx context.Context, dag *dagger.Client, source, envID string) (*environment.Environment, error) {
repo, err := repository.Open(ctx, source)
if err != nil {
return nil, err
return nil, repoOpenErrorMessage(source, err)
}

env, err := repo.Get(ctx, dag, envID)
Expand Down Expand Up @@ -320,7 +352,7 @@ You MUST tell the user: To include these changes in the environment, they need t
func CreateEnvironment(ctx context.Context, dag *dagger.Client, source, title, explanation string) (*repository.Repository, *environment.Environment, error) {
repo, err := repository.Open(ctx, source)
if err != nil {
return nil, nil, err
return nil, nil, repoOpenErrorMessage(source, err)
}

env, err := repo.Create(ctx, dag, title, explanation)
Expand Down