Files
MuyueWorkspace/internal/mcp/mcp.go
Augustin 3494f6b40d
All checks were successful
CI / build (push) Successful in 2m37s
feat: security hardening, tests, doctor command, CI update, CHANGELOG
- Add AES-256-GCM encryption for API keys (internal/secret)
- Add dangerous command detection in terminal
- Add muyue doctor command for system health checks
- Add scanner TTL cache, orchestrator history mutex, shared HTTP client
- Deduplicate MCP config generation, refactor skills YAML parser
- Add XDG-compliant config dir with legacy migration
- Add cleanup on all TUI quit paths
- Add 8 test files (config, workflow, skills, orchestrator, version,
  platform, scanner, secret)
- Update CI to actions/setup-go@v5
- Add CHANGELOG.md, update README and Makefile

🤖 Generated with Crush

Assisted-by: GLM-5.1 via Crush <crush@charm.land>
2026-04-20 19:56:07 +02:00

157 lines
5.3 KiB
Go

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", 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 {
json.Unmarshal(data, &existing)
}
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
}