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
81 changes: 71 additions & 10 deletions internal/executor/executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -876,6 +876,16 @@ func (e *Executor) executeTask(ctx context.Context, task *db.Task) {
}
e.events.EmitTaskWorktreeReady(task)

// Setup TaskYou MCP server so all executors can use taskyou_* tools
// This writes to both ~/.claude.json (for Claude Code, no approval needed)
// and .mcp.json in the worktree (for Codex, Gemini, and other CLI tools)
if err := writeWorkflowMCPConfig(workDir, task.ID); err != nil {
e.logger.Warn("could not setup TaskYou MCP config in claude.json", "error", err)
}
if err := writeWorktreeMCPConfig(workDir, task.ID); err != nil {
e.logger.Warn("could not setup TaskYou MCP config in .mcp.json", "error", err)
}

// Prepare attachments (write to .claude/attachments for seamless access)
attachmentPaths, cleanupAttachments := e.prepareAttachments(task.ID, workDir)
defer cleanupAttachments()
Expand Down Expand Up @@ -1969,11 +1979,7 @@ func (e *Executor) runClaude(ctx context.Context, task *db.Task, workDir, prompt
e.logger.Warn("could not setup Claude hooks", "error", err)
}
// Note: we don't clean up hooks config immediately - it needs to persist for the session

// Setup TaskYou MCP server in ~/.claude.json so Claude can use taskyou_* tools
if err := writeWorkflowMCPConfig(workDir, task.ID); err != nil {
e.logger.Warn("could not setup TaskYou MCP config", "error", err)
}
// Note: TaskYou MCP config is already set up by executeTask() before calling this method

// Create a temp file for the prompt (avoids quoting issues)
promptFile, err := os.CreateTemp("", "task-prompt-*.txt")
Expand Down Expand Up @@ -2145,11 +2151,7 @@ func (e *Executor) runClaudeResume(ctx context.Context, task *db.Task, workDir,
if err != nil {
e.logger.Warn("could not setup Claude hooks", "error", err)
}

// Setup TaskYou MCP server in ~/.claude.json so Claude can use taskyou_* tools
if err := writeWorkflowMCPConfig(workDir, task.ID); err != nil {
e.logger.Warn("could not setup TaskYou MCP config", "error", err)
}
// Note: TaskYou MCP config is already set up by executeTask() before calling this method

// Create a temp file for the feedback (avoids quoting issues)
feedbackFile, err := os.CreateTemp("", "task-feedback-*.txt")
Expand Down Expand Up @@ -3748,6 +3750,65 @@ func writeWorkflowMCPConfig(worktreePath string, taskID int64) error {
return nil
}

// writeWorktreeMCPConfig writes the TaskYou MCP server configuration to .mcp.json in the worktree.
// This enables non-Claude executors (Codex, Gemini, etc.) to use TaskYou tools, since they read
// MCP server config from .mcp.json rather than ~/.claude.json.
// If .mcp.json already exists (e.g., symlinked from the project), it is resolved to a regular file
// and the taskyou server is merged in. The task ID is always updated to match the current task.
func writeWorktreeMCPConfig(worktreePath string, taskID int64) error {
mcpFile := filepath.Join(worktreePath, ".mcp.json")

// Get the path to the task executable
taskExecutable, err := os.Executable()
if err != nil {
taskExecutable = "task"
}

// Build the TaskYou MCP server config (same format as writeWorkflowMCPConfig)
taskyouServer := map[string]interface{}{
"type": "stdio",
"command": taskExecutable,
"args": []string{"mcp-server", "--task-id", fmt.Sprintf("%d", taskID)},
}

// Read existing .mcp.json (resolving symlinks)
var config map[string]interface{}
if data, err := os.ReadFile(mcpFile); err == nil {
if err := json.Unmarshal(data, &config); err != nil {
config = make(map[string]interface{})
}
} else {
config = make(map[string]interface{})
}

// Get or create mcpServers map
mcpServers, ok := config["mcpServers"].(map[string]interface{})
if !ok {
mcpServers = make(map[string]interface{})
}

// Add/update the taskyou server
mcpServers["taskyou"] = taskyouServer
config["mcpServers"] = mcpServers

// If the file is a symlink, remove it first so we write a regular file
// (we don't want to modify the project's shared .mcp.json)
if target, err := os.Readlink(mcpFile); err == nil && target != "" {
os.Remove(mcpFile)
}

data, err := json.MarshalIndent(config, "", " ")
if err != nil {
return fmt.Errorf("marshal .mcp.json: %w", err)
}

if err := os.WriteFile(mcpFile, data, 0644); err != nil {
return fmt.Errorf("write .mcp.json: %w", err)
}

return nil
}

// copyMCPConfig copies the MCP server configuration from the source project to the worktree
// in the claude.json file so that Claude Code in the worktree has the same MCP servers available.
func copyMCPConfig(configPath, srcDir, dstDir string) error {
Expand Down
184 changes: 184 additions & 0 deletions internal/executor/executor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1672,3 +1672,187 @@ func TestWriteWorkflowMCPConfig(t *testing.T) {
}
})
}

func TestWriteWorktreeMCPConfig(t *testing.T) {
// Helper to read .mcp.json and return the taskyou server config
readWorktreeMCP := func(t *testing.T, worktreePath string) map[string]interface{} {
t.Helper()
data, err := os.ReadFile(filepath.Join(worktreePath, ".mcp.json"))
if err != nil {
t.Fatalf("failed to read .mcp.json: %v", err)
}
var config map[string]interface{}
if err := json.Unmarshal(data, &config); err != nil {
t.Fatalf("failed to parse .mcp.json: %v", err)
}
mcpServers, ok := config["mcpServers"].(map[string]interface{})
if !ok {
t.Fatal("expected mcpServers key in .mcp.json")
}
taskyou, ok := mcpServers["taskyou"].(map[string]interface{})
if !ok {
t.Fatal("expected taskyou key in mcpServers")
}
return taskyou
}

t.Run("creates .mcp.json with taskyou config", func(t *testing.T) {
worktreePath := t.TempDir()
taskID := int64(42)

err := writeWorktreeMCPConfig(worktreePath, taskID)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}

taskyou := readWorktreeMCP(t, worktreePath)

if taskyou["type"] != "stdio" {
t.Errorf("taskyou type = %v, want stdio", taskyou["type"])
}

args, ok := taskyou["args"].([]interface{})
if !ok {
t.Fatal("expected args array in taskyou config")
}
if len(args) != 3 || args[0] != "mcp-server" || args[1] != "--task-id" || args[2] != "42" {
t.Errorf("taskyou args = %v, want [mcp-server --task-id 42]", args)
}
})

t.Run("merges into existing .mcp.json", func(t *testing.T) {
worktreePath := t.TempDir()
taskID := int64(99)

// Create existing .mcp.json with another server
existingConfig := map[string]interface{}{
"mcpServers": map[string]interface{}{
"github": map[string]interface{}{
"type": "stdio",
"command": "gh-mcp",
},
},
}
existingData, _ := json.MarshalIndent(existingConfig, "", " ")
if err := os.WriteFile(filepath.Join(worktreePath, ".mcp.json"), existingData, 0644); err != nil {
t.Fatal(err)
}

err := writeWorktreeMCPConfig(worktreePath, taskID)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}

// Verify both servers exist
data, err := os.ReadFile(filepath.Join(worktreePath, ".mcp.json"))
if err != nil {
t.Fatalf("failed to read .mcp.json: %v", err)
}
var config map[string]interface{}
if err := json.Unmarshal(data, &config); err != nil {
t.Fatalf("failed to parse .mcp.json: %v", err)
}
mcpServers := config["mcpServers"].(map[string]interface{})

// Check github server preserved
github, ok := mcpServers["github"].(map[string]interface{})
if !ok {
t.Fatal("expected github server to be preserved")
}
if github["command"] != "gh-mcp" {
t.Errorf("github command = %v, want gh-mcp", github["command"])
}

// Check taskyou added
if _, ok := mcpServers["taskyou"].(map[string]interface{}); !ok {
t.Fatal("expected taskyou server to be added")
}
})

t.Run("replaces symlink with regular file", func(t *testing.T) {
worktreePath := t.TempDir()
projectDir := t.TempDir()
taskID := int64(55)

// Create .mcp.json in project directory
projectMCP := map[string]interface{}{
"mcpServers": map[string]interface{}{
"existing": map[string]interface{}{
"type": "stdio",
"command": "existing-cmd",
},
},
}
projectData, _ := json.MarshalIndent(projectMCP, "", " ")
projectMCPFile := filepath.Join(projectDir, ".mcp.json")
if err := os.WriteFile(projectMCPFile, projectData, 0644); err != nil {
t.Fatal(err)
}

// Create symlink in worktree -> project
worktreeMCPFile := filepath.Join(worktreePath, ".mcp.json")
if err := os.Symlink(projectMCPFile, worktreeMCPFile); err != nil {
t.Fatal(err)
}

err := writeWorktreeMCPConfig(worktreePath, taskID)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}

// Verify it's no longer a symlink
info, err := os.Lstat(worktreeMCPFile)
if err != nil {
t.Fatal(err)
}
if info.Mode()&os.ModeSymlink != 0 {
t.Error("expected .mcp.json to be a regular file, not a symlink")
}

// Verify existing server was preserved and taskyou was added
data, err := os.ReadFile(worktreeMCPFile)
if err != nil {
t.Fatal(err)
}
var config map[string]interface{}
if err := json.Unmarshal(data, &config); err != nil {
t.Fatal(err)
}
mcpServers := config["mcpServers"].(map[string]interface{})
if _, ok := mcpServers["existing"]; !ok {
t.Error("expected existing server to be preserved")
}
if _, ok := mcpServers["taskyou"]; !ok {
t.Error("expected taskyou server to be added")
}

// Verify the project's original file was NOT modified
projectData2, _ := os.ReadFile(projectMCPFile)
var projectConfig map[string]interface{}
json.Unmarshal(projectData2, &projectConfig)
projectServers := projectConfig["mcpServers"].(map[string]interface{})
if _, ok := projectServers["taskyou"]; ok {
t.Error("project .mcp.json should NOT have taskyou - symlink should have been broken")
}
})

t.Run("updates task ID on subsequent calls", func(t *testing.T) {
worktreePath := t.TempDir()

err := writeWorktreeMCPConfig(worktreePath, 100)
if err != nil {
t.Fatalf("first call failed: %v", err)
}

err = writeWorktreeMCPConfig(worktreePath, 200)
if err != nil {
t.Fatalf("second call failed: %v", err)
}

taskyou := readWorktreeMCP(t, worktreePath)
args := taskyou["args"].([]interface{})
if args[2] != "200" {
t.Errorf("task ID = %v, want 200", args[2])
}
})
}