package mcp import ( "encoding/json" "fmt" "os" "os/exec" "path/filepath" "github.com/muyue/muyue/internal/config" ) type MCPServer struct { Name string `json:"name"` Command string `json:"command"` Args []string `json:"args"` Env map[string]string `json:"env,omitempty"` Installed bool `json:"installed"` Category string `json:"category"` } type mcpEntry struct { name string cmd string args []string env map[string]string } var knownMCPServers = []MCPServer{ {Name: "filesystem", Command: "npx", Args: []string{"-y", "@modelcontextprotocol/server-filesystem"}, Category: "core"}, {Name: "github", Command: "npx", Args: []string{"-y", "@modelcontextprotocol/server-github"}, Env: map[string]string{"GITHUB_PERSONAL_ACCESS_TOKEN": ""}, Category: "vcs"}, {Name: "git", Command: "npx", Args: []string{"-y", "@modelcontextprotocol/server-git"}, Category: "vcs"}, {Name: "fetch", Command: "npx", Args: []string{"-y", "@modelcontextprotocol/server-fetch"}, Category: "web"}, {Name: "memory", Command: "npx", Args: []string{"-y", "@modelcontextprotocol/server-memory"}, Category: "core"}, {Name: "sequential-thinking", Command: "npx", Args: []string{"-y", "@modelcontextprotocol/server-sequential-thinking"}, Category: "ai"}, {Name: "brave-search", Command: "npx", Args: []string{"-y", "@modelcontextprotocol/server-brave-search"}, Env: map[string]string{"BRAVE_API_KEY": ""}, Category: "web"}, {Name: "sqlite", Command: "npx", Args: []string{"-y", "@modelcontextprotocol/server-sqlite"}, Category: "database"}, {Name: "postgres", Command: "npx", Args: []string{"-y", "@modelcontextprotocol/server-postgres"}, Category: "database"}, {Name: "docker", Command: "npx", Args: []string{"-y", "@modelcontextprotocol/server-docker"}, Category: "devops"}, {Name: "minimax-web-search", Command: "npx", Args: []string{"-y", "@minimax/mcp-web-search"}, Env: map[string]string{"MINIMAX_API_KEY": ""}, Category: "ai"}, {Name: "minimax-image", Command: "npx", Args: []string{"-y", "@minimax/mcp-image-understanding"}, Env: map[string]string{"MINIMAX_API_KEY": ""}, Category: "ai"}, } func ScanServers() []MCPServer { servers := make([]MCPServer, len(knownMCPServers)) for i, s := range knownMCPServers { servers[i] = s _, err := exec.LookPath(s.Command) servers[i].Installed = err == nil } return servers } func getCoreEntries(homeDir string) []mcpEntry { return []mcpEntry{ {"filesystem", "npx", []string{"-y", "@modelcontextprotocol/server-filesystem", filepath.Join(homeDir, "projects")}, nil}, {"fetch", "npx", []string{"-y", "@modelcontextprotocol/server-fetch"}, nil}, {"memory", "npx", []string{"-y", "@modelcontextprotocol/server-memory"}, nil}, } } func withProviderEntries(base []mcpEntry, cfg *config.MuyueConfig, extraEntries []mcpEntry) []mcpEntry { entries := make([]mcpEntry, len(base)) copy(entries, base) entries = append(entries, extraEntries...) if cfg != nil { for _, p := range cfg.AI.Providers { if p.Name == "minimax" && p.APIKey != "" { entries = append(entries, mcpEntry{"minimax-web-search", "npx", []string{"-y", "@minimax/mcp-web-search"}, map[string]string{"MINIMAX_API_KEY": p.APIKey}}, mcpEntry{"minimax-image", "npx", []string{"-y", "@minimax/mcp-image-understanding"}, map[string]string{"MINIMAX_API_KEY": p.APIKey}}, ) } } } return entries } func writeMCPConfig(configPath string, mcpKey string, entries []mcpEntry) error { configDir := filepath.Dir(configPath) if err := os.MkdirAll(configDir, 0700); err != nil { return fmt.Errorf("create config dir: %w", err) } existing := map[string]interface{}{} data, err := os.ReadFile(configPath) if err == nil { if err := json.Unmarshal(data, &existing); err != nil { return fmt.Errorf("parse existing config: %w", err) } } mcpMap := map[string]interface{}{} for _, e := range entries { entry := map[string]interface{}{ "command": e.cmd, "args": e.args, } if len(e.env) > 0 { entry["env"] = e.env } mcpMap[e.name] = entry } existing[mcpKey] = mcpMap out, err := json.MarshalIndent(existing, "", " ") if err != nil { return err } return os.WriteFile(configPath, out, 0600) } func GenerateCrushMCPConfig(cfg *config.MuyueConfig, homeDir string) error { if homeDir == "" { home, _ := os.UserHomeDir() homeDir = home } core := getCoreEntries(homeDir) entries := withProviderEntries(core, cfg, nil) configPath := filepath.Join(homeDir, ".config", "crush", "crush.json") return writeMCPConfig(configPath, "mcps", entries) } func GenerateClaudeMCPConfig(cfg *config.MuyueConfig, homeDir string) error { if homeDir == "" { home, _ := os.UserHomeDir() homeDir = home } core := getCoreEntries(homeDir) extra := []mcpEntry{ {"sequential-thinking", "npx", []string{"-y", "@modelcontextprotocol/server-sequential-thinking"}, nil}, } entries := withProviderEntries(core, cfg, extra) configPath := filepath.Join(homeDir, ".claude.json") return writeMCPConfig(configPath, "mcpServers", entries) } func ConfigureAll(cfg *config.MuyueConfig) error { home, err := os.UserHomeDir() if err != nil { return fmt.Errorf("get home dir: %w", err) } if err := GenerateCrushMCPConfig(cfg, home); err != nil { return fmt.Errorf("crush MCP config: %w", err) } if err := GenerateClaudeMCPConfig(cfg, home); err != nil { return fmt.Errorf("claude MCP config: %w", err) } return nil }