From e473d9751bcd98b4b174de3d5d3581743ddae4a6 Mon Sep 17 00:00:00 2001 From: Connor Braa Date: Wed, 9 Jul 2025 14:55:30 -0700 Subject: [PATCH 1/2] add roots support, currently only facilitating repo open error messages Signed-off-by: Connor Braa --- mcpserver/roots.go | 100 +++++++++++++++++++++++++++++++++++++++++++++ mcpserver/tools.go | 36 +++++++++++++++- 2 files changed, 134 insertions(+), 2 deletions(-) create mode 100644 mcpserver/roots.go diff --git a/mcpserver/roots.go b/mcpserver/roots.go new file mode 100644 index 00000000..3f3786a0 --- /dev/null +++ b/mcpserver/roots.go @@ -0,0 +1,100 @@ +package mcpserver + +import ( + "context" + "errors" + "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 { + if rootsData, ok := notification.Params.AdditionalFields["roots"].([]any); ok { + 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) + } + } + return 0 +} + +// repoOpenErrorMessage provides helpful error messages when repository opening fails +func repoOpenErrorMessage(source string, originalErr error) error { + baseMsg := fmt.Sprintf("failed to open repository '%s': %v", source, originalErr) + + // 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 errors.New(baseMsg) + } + + // Fallback: suggest common patterns + baseMsg += "\n\nTry using:\n - '.' for current directory\n - An absolute path to your git repository" + return errors.New(baseMsg) +} diff --git a/mcpserver/tools.go b/mcpserver/tools.go index d7331429..9401d7cb 100644 --- a/mcpserver/tools.go +++ b/mcpserver/tools.go @@ -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) } @@ -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) @@ -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) From 569471b4ad93f6a09ff1325caf120c438c97648d Mon Sep 17 00:00:00 2001 From: Connor Braa Date: Fri, 11 Jul 2025 10:40:05 -0700 Subject: [PATCH 2/2] address comments Signed-off-by: Connor Braa --- mcpserver/roots.go | 52 ++++++++++++++++++++++++---------------------- 1 file changed, 27 insertions(+), 25 deletions(-) diff --git a/mcpserver/roots.go b/mcpserver/roots.go index 3f3786a0..f292a4e3 100644 --- a/mcpserver/roots.go +++ b/mcpserver/roots.go @@ -2,7 +2,6 @@ package mcpserver import ( "context" - "errors" "fmt" "log/slog" "strings" @@ -46,35 +45,38 @@ func sendRootsListRequest(ctx context.Context, session server.ClientSession) { // updateClientRoots parses roots from notification params and updates the global clientRoots func updateClientRoots(notification mcp.JSONRPCNotification) int { - if notification.Params.AdditionalFields != nil { - if rootsData, ok := notification.Params.AdditionalFields["roots"].([]any); ok { - 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() + if notification.Params.AdditionalFields == nil { + return 0 + } + rootsData, ok := notification.Params.AdditionalFields["roots"].([]any) + if !ok { + return 0 + } - return len(newRoots) + 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) } } - return 0 + + 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': %v", source, originalErr) + baseMsg := fmt.Sprintf("failed to open repository '%s'", source) // If we have client roots, suggest them clientRootsMu.RLock() @@ -91,10 +93,10 @@ func repoOpenErrorMessage(source string, originalErr error) error { baseMsg += fmt.Sprintf("\n - %s", uri) } } - return errors.New(baseMsg) + 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 errors.New(baseMsg) + return fmt.Errorf("%s: %w", baseMsg, originalErr) }