package services import ( "encoding/json" "os" "sproutclaw-web/internal/models" ) // mcp.json is shared with pi-mcp-adapter, which only reads the top-level // "mcpServers", "imports" and "settings" keys. To disable a server we move its // full definition into the adapter-ignored "mcpServersDisabled" object, so the // adapter never starts it; enabling moves it back. All operations work on the // raw JSON map so unrecognized keys (settings, imports, ...) are preserved. // ReadMCPServers loads mcp.json and returns the list of servers with their tools. // Servers under "mcpServers" are reported enabled; those parked under // "mcpServersDisabled" are reported disabled. func ReadMCPServers(mcpConfigPath, mcpCachePath string) []models.MCPServer { raw := loadRawMCP(mcpConfigPath) cache := loadMCPCache(mcpCachePath) excludeSet := map[string]bool{} for _, t := range toStringSlice(raw["excludeTools"]) { excludeSet[t] = true } var servers []models.MCPServer appendServer := func(name string, enabled bool) { srv := models.MCPServer{Name: name, Configured: true, Enabled: enabled} for _, t := range cache[name] { toolEnabled := !(excludeSet[name+"/"+t] || excludeSet[t]) srv.Tools = append(srv.Tools, models.MCPTool{Server: name, Name: t, Enabled: toolEnabled}) srv.ToolCount++ if toolEnabled { srv.EnabledToolCount++ } } servers = append(servers, srv) } for name := range asMap(raw["mcpServers"]) { appendServer(name, true) } for name := range asMap(raw["mcpServersDisabled"]) { appendServer(name, false) } return servers } // ToggleMCPServer enables/disables a server by moving its full definition between // "mcpServers" (adapter reads it -> server can start) and "mcpServersDisabled" // (adapter ignores it -> server never starts). func ToggleMCPServer(mcpConfigPath, serverName string, enable bool) error { raw := loadRawMCP(mcpConfigPath) servers := asMap(raw["mcpServers"]) disabled := asMap(raw["mcpServersDisabled"]) if enable { if def, ok := disabled[serverName]; ok { servers[serverName] = def delete(disabled, serverName) } } else { if def, ok := servers[serverName]; ok { disabled[serverName] = def delete(servers, serverName) } } raw["mcpServers"] = servers setOrDeleteMap(raw, "mcpServersDisabled", disabled) return saveRawMCP(mcpConfigPath, raw) } // ToggleMCPTool enables or disables a specific tool via the top-level // "excludeTools" list (server/tool keys). func ToggleMCPTool(mcpConfigPath, serverName, toolName string, enable bool) error { raw := loadRawMCP(mcpConfigPath) key := serverName + "/" + toolName set := map[string]bool{} for _, t := range toStringSlice(raw["excludeTools"]) { set[t] = true } if enable { delete(set, key) } else { set[key] = true } list := make([]any, 0, len(set)) for k := range set { list = append(list, k) } if len(list) > 0 { raw["excludeTools"] = list } else { delete(raw, "excludeTools") } return saveRawMCP(mcpConfigPath, raw) } func loadRawMCP(path string) map[string]any { data, err := os.ReadFile(path) if err != nil { return map[string]any{} } var raw map[string]any if err := json.Unmarshal(data, &raw); err != nil || raw == nil { return map[string]any{} } return raw } func saveRawMCP(path string, raw map[string]any) error { data, err := json.MarshalIndent(raw, "", " ") if err != nil { return err } return os.WriteFile(path, append(data, '\n'), 0o644) } func asMap(v any) map[string]any { if m, ok := v.(map[string]any); ok { return m } return map[string]any{} } func setOrDeleteMap(raw map[string]any, key string, m map[string]any) { if len(m) > 0 { raw[key] = m } else { delete(raw, key) } } func toStringSlice(v any) []string { arr, ok := v.([]any) if !ok { return nil } out := make([]string, 0, len(arr)) for _, e := range arr { if s, ok := e.(string); ok { out = append(out, s) } } return out } func loadMCPCache(path string) map[string][]string { data, err := os.ReadFile(path) if err != nil { return nil } // cache format: { "serverName": { "tools": [{ "name": "..." }] } } var raw map[string]struct { Tools []struct { Name string `json:"name"` } `json:"tools"` } if err := json.Unmarshal(data, &raw); err != nil { return nil } result := map[string][]string{} for srv, info := range raw { for _, t := range info.Tools { result[srv] = append(result[srv], t.Name) } } return result }