From c8a570c172a9632df74f155c2cd9057482d440cf Mon Sep 17 00:00:00 2001 From: Nguyen Van Duc D Date: Thu, 29 Jan 2026 09:51:11 +0700 Subject: [PATCH 1/2] feat: add beads mcp intallation --- cmd/init.go | 24 +++++++-- internal/beads/install.go | 104 ++++++++++++++++++++++++++++++++++++ internal/uv/install.go | 108 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 233 insertions(+), 3 deletions(-) create mode 100644 internal/beads/install.go create mode 100644 internal/uv/install.go diff --git a/cmd/init.go b/cmd/init.go index 4f1bf4a..f9e62e1 100644 --- a/cmd/init.go +++ b/cmd/init.go @@ -11,6 +11,7 @@ import ( "github.com/momorph/cli/internal/api" "github.com/momorph/cli/internal/auth" + "github.com/momorph/cli/internal/beads" "github.com/momorph/cli/internal/config" "github.com/momorph/cli/internal/logger" "github.com/momorph/cli/internal/template" @@ -20,8 +21,9 @@ import ( ) var ( - aiTool string - templateTag string + aiTool string + templateTag string + installBeads bool // ErrUserCancelled is returned when the user cancels an operation ErrUserCancelled = errors.New("user cancelled") ) @@ -31,7 +33,8 @@ var initCmd = &cobra.Command{ Short: "Initialize a new MoMorph project from the latest template", Example: ` momorph init my-project --ai=copilot momorph init . --ai=cursor - momorph init my-project`, + momorph init my-project --with-beads + momorph init my-project --ai=claude --tag=stable --with-beads`, Args: cobra.ExactArgs(1), RunE: runInit, } @@ -39,6 +42,7 @@ var initCmd = &cobra.Command{ func init() { initCmd.Flags().StringVar(&aiTool, "ai", "", "AI tool to use (copilot, cursor, claude, windsurf, gemini)") initCmd.Flags().StringVar(&templateTag, "tag", "", "Template version tag (stable, latest, or specific version)") + initCmd.Flags().BoolVar(&installBeads, "with-beads", false, "Install uv and beads-mcp for task management") rootCmd.AddCommand(initCmd) } @@ -194,6 +198,20 @@ func runInit(cmd *cobra.Command, args []string) error { } } + // Install beads-mcp (requires uv) - only if flag is set + if installBeads { + fmt.Println("🔮 Installing beads-mcp...") + beadsResult := beads.EnsureInstalled() + if beadsResult.Error != nil { + logger.Warn("beads-mcp installation failed: %v", beadsResult.Error) + fmt.Printf(" ⚠ %s\n", beadsResult.Message) + } else if beadsResult.Installed { + fmt.Printf(" ✓ %s\n", beadsResult.Message) + } else { + fmt.Printf(" ⚠ %s\n", beadsResult.Message) + } + } + // Install VS Code extension fmt.Println("📦 Installing VS Code extension...") result := vscode.InstallExtension() diff --git a/internal/beads/install.go b/internal/beads/install.go new file mode 100644 index 0000000..19fe5ca --- /dev/null +++ b/internal/beads/install.go @@ -0,0 +1,104 @@ +package beads + +import ( + "bytes" + "fmt" + "os/exec" + "strings" + + "github.com/momorph/cli/internal/logger" + "github.com/momorph/cli/internal/uv" +) + +// InstallResult represents the result of beads-mcp installation +type InstallResult struct { + Installed bool + Message string + Error error +} + +// IsInstalled checks if beads-mcp is installed +func IsInstalled() bool { + // Check if beads-mcp is available via uvx + cmd := exec.Command("uvx", "beads-mcp", "--version") + if err := cmd.Run(); err != nil { + logger.Debug("beads-mcp not found via uvx: %v", err) + return false + } + logger.Debug("beads-mcp is installed") + return true +} + +// Install installs beads-mcp using uv tool install +func Install() InstallResult { + // First ensure uv is installed + if !uv.IsInstalled() { + uvResult := uv.Install() + if uvResult.Error != nil { + return InstallResult{ + Installed: false, + Message: fmt.Sprintf("Cannot install beads-mcp: uv is required. %s", uvResult.Message), + Error: uvResult.Error, + } + } + // If uv was just installed, it might not be in PATH yet + if !uv.IsInstalled() { + return InstallResult{ + Installed: false, + Message: "uv was installed but is not available in PATH. Please restart your terminal and run 'uv tool install beads-mcp' manually", + Error: nil, + } + } + } + + // Install beads-mcp using uv tool install (with packaging dependency) + logger.Debug("Installing beads-mcp with: uv tool install beads-mcp --with packaging") + + cmd := exec.Command("uv", "tool", "install", "beads-mcp", "--with", "packaging") + var stdout, stderr bytes.Buffer + cmd.Stdout = &stdout + cmd.Stderr = &stderr + + if err := cmd.Run(); err != nil { + // Check if it's already installed (uv returns error if already installed) + stderrStr := stderr.String() + if strings.Contains(stderrStr, "already installed") || strings.Contains(stdout.String(), "already installed") { + return InstallResult{ + Installed: true, + Message: "beads-mcp is already installed", + Error: nil, + } + } + + logger.Debug("beads-mcp install stdout: %s", stdout.String()) + logger.Debug("beads-mcp install stderr: %s", stderrStr) + return InstallResult{ + Installed: false, + Message: fmt.Sprintf("Failed to install beads-mcp: %v", err), + Error: err, + } + } + + return InstallResult{ + Installed: true, + Message: "beads-mcp installed successfully", + Error: nil, + } +} + +// EnsureInstalled ensures beads-mcp is installed +func EnsureInstalled() InstallResult { + // First ensure uv is available + if !uv.IsInstalled() { + uvResult := uv.EnsureInstalled() + if uvResult.Error != nil || !uvResult.Installed { + return InstallResult{ + Installed: false, + Message: fmt.Sprintf("Cannot install beads-mcp: %s", uvResult.Message), + Error: uvResult.Error, + } + } + } + + return Install() +} diff --git a/internal/uv/install.go b/internal/uv/install.go new file mode 100644 index 0000000..b3a5429 --- /dev/null +++ b/internal/uv/install.go @@ -0,0 +1,108 @@ +package uv + +import ( + "bytes" + "fmt" + "os/exec" + "runtime" + "strings" + + "github.com/momorph/cli/internal/logger" +) + +// InstallResult represents the result of uv installation +type InstallResult struct { + Installed bool + Message string + Error error +} + +// IsInstalled checks if uv is installed by running "uv --version" +func IsInstalled() bool { + cmd := exec.Command("uv", "--version") + var stdout bytes.Buffer + cmd.Stdout = &stdout + if err := cmd.Run(); err != nil { + logger.Debug("uv not found: %v", err) + return false + } + version := strings.TrimSpace(stdout.String()) + logger.Debug("uv found: %s", version) + return true +} + +// Install attempts to install uv using the official installer script +func Install() InstallResult { + // Check if already installed + if IsInstalled() { + return InstallResult{ + Installed: true, + Message: "uv is already installed", + Error: nil, + } + } + + var cmd *exec.Cmd + var installCmd string + + switch runtime.GOOS { + case "linux", "darwin": + // Use curl to download and execute the installer script + installCmd = "curl -LsSf https://astral.sh/uv/install.sh | sh" + cmd = exec.Command("sh", "-c", installCmd) + case "windows": + // Use PowerShell to download and execute the installer script + installCmd = "irm https://astral.sh/uv/install.ps1 | iex" + cmd = exec.Command("powershell", "-Command", installCmd) + default: + return InstallResult{ + Installed: false, + Message: fmt.Sprintf("Unsupported OS: %s. Please install uv manually: https://docs.astral.sh/uv/getting-started/installation/", runtime.GOOS), + Error: fmt.Errorf("unsupported OS: %s", runtime.GOOS), + } + } + + logger.Debug("Installing uv with command: %s", installCmd) + + var stdout, stderr bytes.Buffer + cmd.Stdout = &stdout + cmd.Stderr = &stderr + + if err := cmd.Run(); err != nil { + logger.Debug("uv install stdout: %s", stdout.String()) + logger.Debug("uv install stderr: %s", stderr.String()) + return InstallResult{ + Installed: false, + Message: fmt.Sprintf("Failed to install uv: %v. Please install manually: https://docs.astral.sh/uv/getting-started/installation/", err), + Error: err, + } + } + + // Verify installation + if !IsInstalled() { + // uv might be installed but not in PATH yet + return InstallResult{ + Installed: true, + Message: "uv installed. You may need to restart your terminal or add ~/.local/bin to PATH", + Error: nil, + } + } + + return InstallResult{ + Installed: true, + Message: "uv installed successfully", + Error: nil, + } +} + +// EnsureInstalled checks if uv is installed, and installs it if not +func EnsureInstalled() InstallResult { + if IsInstalled() { + return InstallResult{ + Installed: true, + Message: "uv is already installed", + Error: nil, + } + } + return Install() +} From 96de0cf64bbfe382efc2dec353cdfa541139250e Mon Sep 17 00:00:00 2001 From: Nguyen Van Duc D Date: Thu, 29 Jan 2026 12:34:45 +0700 Subject: [PATCH 2/2] fix: config mcp --- .gitignore | 9 ++++ internal/template/mcp.go | 111 ++++++++++++++++++++++++++++++++------- 2 files changed, 100 insertions(+), 20 deletions(-) diff --git a/.gitignore b/.gitignore index 82b7993..594acc3 100644 --- a/.gitignore +++ b/.gitignore @@ -59,3 +59,12 @@ __debug_bin # Test cache .cache/ +docs/ + +# Added by MoMorph +.mcp.json +mcp.json +.momorph/ +.claude +CLAUDE.md +.windsurf/ \ No newline at end of file diff --git a/internal/template/mcp.go b/internal/template/mcp.go index ef8592e..799dfa0 100644 --- a/internal/template/mcp.go +++ b/internal/template/mcp.go @@ -122,7 +122,7 @@ func (c *copilotConfigUpdater) ConfigureMCPServer(projectDir, githubToken, mcpSe // cursorConfigUpdater handles Cursor-specific config updates type cursorConfigUpdater struct{} -// ConfigureMCPServer updates Cursor's global mcp.json with MoMorph server +// ConfigureMCPServer updates Cursor's global mcp.json with servers from project's .mcp.json // Config file: ~/.cursor/mcp.json func (c *cursorConfigUpdater) ConfigureMCPServer(projectDir, githubToken, mcpServerEndpoint string) error { // Cursor config is in user's home directory, not project directory @@ -139,7 +139,7 @@ func (c *cursorConfigUpdater) ConfigureMCPServer(projectDir, githubToken, mcpSer return fmt.Errorf("failed to create .cursor directory: %w", err) } - // Read existing config or create new one + // Read existing global config or create new one var mcpConfig map[string]interface{} if data, err := os.ReadFile(mcpFilePath); err == nil { if err := json.Unmarshal(data, &mcpConfig); err != nil { @@ -151,18 +151,32 @@ func (c *cursorConfigUpdater) ConfigureMCPServer(projectDir, githubToken, mcpSer mcpConfig = make(map[string]interface{}) } - // Get or create mcpServers - var servers map[string]interface{} + // Get or create mcpServers in global config + var globalServers map[string]interface{} if serversInterface, exists := mcpConfig["mcpServers"]; exists { - servers, _ = serversInterface.(map[string]interface{}) + globalServers, _ = serversInterface.(map[string]interface{}) } - if servers == nil { - servers = make(map[string]interface{}) - mcpConfig["mcpServers"] = servers + if globalServers == nil { + globalServers = make(map[string]interface{}) + mcpConfig["mcpServers"] = globalServers } - // Add/update momorph server configuration - servers["momorph"] = map[string]interface{}{ + // Read project's .mcp.json and merge servers + projectMCPPath := filepath.Join(projectDir, ".mcp.json") + if projectServers, err := readMCPServers(projectMCPPath); err == nil { + for name, server := range projectServers { + // Skip if server already exists in global config (don't overwrite user's config) + if _, exists := globalServers[name]; !exists { + globalServers[name] = server + logger.Debug("Added server '%s' to Cursor config", name) + } + } + } else { + logger.Debug("Could not read project .mcp.json for Cursor: %v", err) + } + + // Always update momorph server with current token and endpoint + globalServers["momorph"] = map[string]interface{}{ "url": mcpServerEndpoint, "headers": map[string]string{ "x-github-token": githubToken, @@ -186,7 +200,7 @@ func (c *cursorConfigUpdater) ConfigureMCPServer(projectDir, githubToken, mcpSer // windsurfConfigUpdater handles Windsurf-specific config updates type windsurfConfigUpdater struct{} -// ConfigureMCPServer updates Windsurf's global mcp_config.json with MoMorph server +// ConfigureMCPServer updates Windsurf's global mcp_config.json with servers from project's .mcp.json // Config file: ~/.codeium/windsurf/mcp_config.json func (w *windsurfConfigUpdater) ConfigureMCPServer(projectDir, githubToken, mcpServerEndpoint string) error { // Windsurf config is in user's home directory @@ -203,7 +217,7 @@ func (w *windsurfConfigUpdater) ConfigureMCPServer(projectDir, githubToken, mcpS return fmt.Errorf("failed to create windsurf config directory: %w", err) } - // Read existing config or create new one + // Read existing global config or create new one var mcpConfig map[string]interface{} if data, err := os.ReadFile(mcpFilePath); err == nil { if err := json.Unmarshal(data, &mcpConfig); err != nil { @@ -214,19 +228,38 @@ func (w *windsurfConfigUpdater) ConfigureMCPServer(projectDir, githubToken, mcpS mcpConfig = make(map[string]interface{}) } - // Get or create mcpServers - var servers map[string]interface{} + // Get or create mcpServers in global config + var globalServers map[string]interface{} if serversInterface, exists := mcpConfig["mcpServers"]; exists { - servers, _ = serversInterface.(map[string]interface{}) + globalServers, _ = serversInterface.(map[string]interface{}) } - if servers == nil { - servers = make(map[string]interface{}) - mcpConfig["mcpServers"] = servers + if globalServers == nil { + globalServers = make(map[string]interface{}) + mcpConfig["mcpServers"] = globalServers + } + + // Read project's .mcp.json and merge servers (with key transformation) + projectMCPPath := filepath.Join(projectDir, ".mcp.json") + if projectServers, err := readMCPServers(projectMCPPath); err == nil { + for name, server := range projectServers { + // Skip if server already exists in global config (don't overwrite user's config) + if _, exists := globalServers[name]; !exists { + // Transform server config for Windsurf (url -> serverUrl) + if serverMap, ok := server.(map[string]interface{}); ok { + globalServers[name] = transformServerForWindsurf(serverMap) + } else { + globalServers[name] = server + } + logger.Debug("Added server '%s' to Windsurf config", name) + } + } + } else { + logger.Debug("Could not read project .mcp.json for Windsurf: %v", err) } - // Add/update momorph server configuration + // Always update momorph server with current token and endpoint // Windsurf uses "serverUrl" instead of "url" - servers["momorph"] = map[string]interface{}{ + globalServers["momorph"] = map[string]interface{}{ "serverUrl": mcpServerEndpoint, "headers": map[string]string{ "x-github-token": githubToken, @@ -247,6 +280,44 @@ func (w *windsurfConfigUpdater) ConfigureMCPServer(projectDir, githubToken, mcpS return nil } +// readMCPServers reads the mcpServers from a .mcp.json file +func readMCPServers(mcpFilePath string) (map[string]interface{}, error) { + data, err := os.ReadFile(mcpFilePath) + if err != nil { + return nil, fmt.Errorf("failed to read file: %w", err) + } + + var mcpConfig map[string]interface{} + if err := json.Unmarshal(data, &mcpConfig); err != nil { + return nil, fmt.Errorf("failed to parse JSON: %w", err) + } + + serversInterface, exists := mcpConfig["mcpServers"] + if !exists { + return nil, fmt.Errorf("no mcpServers field found") + } + + servers, ok := serversInterface.(map[string]interface{}) + if !ok { + return nil, fmt.Errorf("mcpServers is not a valid object") + } + + return servers, nil +} + +// transformServerForWindsurf converts server config for Windsurf (url -> serverUrl) +func transformServerForWindsurf(server map[string]interface{}) map[string]interface{} { + result := make(map[string]interface{}) + for k, v := range server { + if k == "url" { + result["serverUrl"] = v + } else { + result[k] = v + } + } + return result +} + // GetConfigUpdater returns the appropriate config updater for the given AI tool func GetConfigUpdater(aiTool string) ConfigUpdater { switch aiTool {