//! Reference MCP connector presets shared across leading client ecosystems. //! //! These definitions intentionally avoid vendor-specific naming while capturing //! the union of commonly shipped servers: local tooling, automation, retrieval, //! observability, and productivity integrations. use crate::config::McpServerConfig; use crate::tools::tool_identifier_violation; use anyhow::{Result, anyhow}; use std::collections::{HashMap, HashSet}; use std::str::FromStr; /// High-level preset tiers exposed to CLI/TUI. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum PresetTier { Standard, Extended, Full, } impl PresetTier { pub fn as_str(self) -> &'static str { match self { PresetTier::Standard => "standard", PresetTier::Extended => "extended", PresetTier::Full => "full", } } pub fn all() -> &'static [PresetTier] { &[PresetTier::Standard, PresetTier::Extended, PresetTier::Full] } fn description(self) -> &'static str { match self { PresetTier::Standard => { "Core local tooling (filesystem, terminal, git, browser, fetch, python, notebook)." } PresetTier::Extended => { "Standard + retrieval/automation connectors (search, scraping, planning)." } PresetTier::Full => { "Extended + SaaS integrations (observability, productivity, data stores)." } } } } impl FromStr for PresetTier { type Err = anyhow::Error; fn from_str(s: &str) -> Result { let normalized = s.trim().to_ascii_lowercase(); match normalized.as_str() { "standard" | "std" => Ok(PresetTier::Standard), "extended" | "ext" => Ok(PresetTier::Extended), "full" | "all" => Ok(PresetTier::Full), other => Err(anyhow!(format!( "Unknown preset tier '{other}'. Expected one of: standard, extended, full." ))), } } } /// Lightweight description of an MCP connector entry. #[derive(Debug, Clone, Copy)] pub struct PresetConnector { pub name: &'static str, pub command: &'static str, pub args: &'static [&'static str], pub env: &'static [(&'static str, &'static str)], pub description: &'static str, pub capabilities: &'static [&'static str], } impl PresetConnector { pub fn to_config(&self) -> McpServerConfig { if let Some(reason) = tool_identifier_violation(self.name) { panic!("Invalid preset connector '{}': {reason}", self.name); } McpServerConfig { name: self.name.to_string(), command: self.command.to_string(), args: self.args.iter().map(|arg| arg.to_string()).collect(), transport: "stdio".to_string(), env: self .env .iter() .map(|(k, v)| ((*k).to_string(), (*v).to_string())) .collect::>(), oauth: None, rpc_timeout_secs: None, } } } const STANDARD_CONNECTORS: &[PresetConnector] = &[ PresetConnector { name: "filesystem", command: "npx", args: &["-y", "@modelcontextprotocol/server-filesystem"], env: &[], description: "Mount local project directories for read/write operations.", capabilities: &["filesystem", "local"], }, PresetConnector { name: "terminal", command: "npx", args: &["-y", "@modelcontextprotocol/server-shell"], env: &[], description: "Execute shell commands within a sandboxed environment.", capabilities: &["shell", "local"], }, PresetConnector { name: "git", command: "npx", args: &["-y", "@modelcontextprotocol/server-git"], env: &[], description: "Interact with Git repositories for status, diffs, commits.", capabilities: &["git", "local"], }, PresetConnector { name: "browser", command: "npx", args: &["-y", "@modelcontextprotocol/server-browser"], env: &[], description: "Perform scripted browser automation via headless Chromium.", capabilities: &["browser", "automation"], }, PresetConnector { name: "fetch", command: "npx", args: &["-y", "@modelcontextprotocol/server-fetch"], env: &[], description: "Issue structured HTTP requests for REST/JSON APIs.", capabilities: &["network"], }, PresetConnector { name: "python", command: "npx", args: &["-y", "@modelcontextprotocol/server-python"], env: &[], description: "Run Python snippets in an isolated interpreter.", capabilities: &["compute", "python"], }, PresetConnector { name: "notebook", command: "npx", args: &["-y", "@modelcontextprotocol/server-notebook"], env: &[], description: "Evaluate notebook cells and manage Jupyter sessions.", capabilities: &["compute", "notebook"], }, PresetConnector { name: "sequential_thinking", command: "npx", args: &["-y", "@modelcontextprotocol/server-sequential-thinking"], env: &[], description: "Structured reasoning helper with planning support.", capabilities: &["planning"], }, PresetConnector { name: "puppeteer", command: "npx", args: &["-y", "@modelcontextprotocol/server-puppeteer"], env: &[], description: "Full-browser automation via Puppeteer.", capabilities: &["browser", "automation"], }, ]; const EXTENDED_CONNECTORS: &[PresetConnector] = &[ PresetConnector { name: "brave_search", command: "npx", args: &["-y", "@modelcontextprotocol/server-brave-search"], env: &[("BRAVE_API_KEY", "")], description: "Search the web using Brave Search APIs.", capabilities: &["search", "network"], }, PresetConnector { name: "tavily", command: "npx", args: &["-y", "@tavily/mcp-server"], env: &[("TAVILY_API_KEY", "")], description: "General-purpose research with Tavily's search/reasoning API.", capabilities: &["search", "network"], }, PresetConnector { name: "perplexity", command: "npx", args: &["-y", "@perplexity-ai/mcp-server"], env: &[("PPLX_API_KEY", "")], description: "Ask questions against Perplexity's API.", capabilities: &["qa", "network"], }, PresetConnector { name: "firecrawl", command: "npx", args: &["-y", "@firecrawl/mcp-server"], env: &[("FIRECRAWL_TOKEN", "")], description: "Crawl and scrape webpages for summarisation.", capabilities: &["scrape", "network"], }, PresetConnector { name: "memory_bank", command: "npx", args: &["-y", "@modelcontextprotocol/server-memory"], env: &[], description: "Persist structured memories for long-lived tasks.", capabilities: &["memory"], }, ]; const FULL_CONNECTORS: &[PresetConnector] = &[ PresetConnector { name: "sentry", command: "npx", args: &["-y", "@sentry/mcp-server"], env: &[("SENTRY_AUTH_TOKEN", "")], description: "Query issues and alerts from Sentry.", capabilities: &["observability", "network"], }, PresetConnector { name: "notion", command: "npx", args: &["-y", "@notionhq/mcp-server"], env: &[("NOTION_API_KEY", "")], description: "Access Notion databases and pages.", capabilities: &["productivity", "network"], }, PresetConnector { name: "slack", command: "npx", args: &["-y", "@slack/mcp-server"], env: &[("SLACK_BOT_TOKEN", "")], description: "Send messages and search channels in Slack.", capabilities: &["communication", "network"], }, PresetConnector { name: "stripe", command: "npx", args: &["-y", "@stripe/mcp-server"], env: &[("STRIPE_API_KEY", "")], description: "Inspect customers, invoices, and payment intents.", capabilities: &["payments", "network"], }, PresetConnector { name: "google_drive", command: "npx", args: &["-y", "@modelcontextprotocol/server-google-drive"], env: &[("GOOGLE_DRIVE_CREDENTIALS", "")], description: "Browse and fetch Google Drive documents.", capabilities: &["storage", "network"], }, PresetConnector { name: "zapier", command: "npx", args: &["-y", "@zapier/mcp-server"], env: &[("ZAPIER_NLA_API_KEY", "")], description: "Trigger Zapier actions and workflows.", capabilities: &["automation", "network"], }, PresetConnector { name: "postgresql", command: "npx", args: &["-y", "@modelcontextprotocol/server-postgresql"], env: &[], description: "Run SQL against a PostgreSQL database.", capabilities: &["database"], }, PresetConnector { name: "sqlite", command: "npx", args: &["-y", "@modelcontextprotocol/server-sqlite"], env: &[], description: "Run SQL against local SQLite databases.", capabilities: &["database"], }, PresetConnector { name: "redis", command: "npx", args: &["-y", "@modelcontextprotocol/server-redis"], env: &[], description: "Inspect Redis keys and run commands.", capabilities: &["cache", "database"], }, PresetConnector { name: "qdrant", command: "npx", args: &["-y", "@modelcontextprotocol/server-qdrant"], env: &[], description: "Interact with Qdrant vector collections.", capabilities: &["vector", "database"], }, ]; fn connectors_for_tier_internal(tier: PresetTier) -> Vec { let mut result = Vec::new(); result.extend_from_slice(STANDARD_CONNECTORS); if matches!(tier, PresetTier::Extended | PresetTier::Full) { result.extend_from_slice(EXTENDED_CONNECTORS); } if matches!(tier, PresetTier::Full) { result.extend_from_slice(FULL_CONNECTORS); } result } /// Return connectors for the given tier (including lower tiers). pub fn connectors_for_tier(tier: PresetTier) -> Vec { connectors_for_tier_internal(tier) } /// Describe the preset tiers for help output. pub fn tier_descriptions() -> Vec<(PresetTier, &'static str)> { PresetTier::all() .iter() .map(|tier| (*tier, tier.description())) .collect() } /// Details about changes performed when applying a preset. #[derive(Debug)] pub struct PresetApplyReport { pub tier: PresetTier, pub added: Vec, pub updated: Vec, pub removed: Vec, } impl PresetApplyReport { fn new(tier: PresetTier) -> Self { Self { tier, added: Vec::new(), updated: Vec::new(), removed: Vec::new(), } } } /// Details discovered during audit. #[derive(Debug)] pub struct PresetAuditReport { pub tier: PresetTier, pub missing: Vec, pub mismatched: Vec<(PresetConnector, McpServerConfig)>, pub extra: Vec, } impl PresetAuditReport { fn new(tier: PresetTier) -> Self { Self { tier, missing: Vec::new(), mismatched: Vec::new(), extra: Vec::new(), } } } /// Apply the requested preset to the given configuration. pub fn apply_preset( config: &mut crate::config::Config, tier: PresetTier, prune: bool, ) -> Result { let mut report = PresetApplyReport::new(tier); let connectors = connectors_for_tier_internal(tier); let expected_names: HashSet<&str> = connectors.iter().map(|c| c.name).collect(); if prune { config.mcp_servers.retain(|existing| { if expected_names.contains(existing.name.as_str()) { true } else { report.removed.push(existing.name.clone()); false } }); } for connector in connectors { match config .mcp_servers .iter_mut() .find(|srv| srv.name == connector.name) { Some(existing) => { let candidate = connector.to_config(); if existing.command != candidate.command || existing.args != candidate.args || existing.env != candidate.env { *existing = candidate; report.updated.push(connector.name.to_string()); } } None => { config.mcp_servers.push(connector.to_config()); report.added.push(connector.name.to_string()); } } } config.refresh_mcp_servers(None)?; Ok(report) } /// Audit the configuration against a preset without mutating it. pub fn audit_preset(config: &crate::config::Config, tier: PresetTier) -> PresetAuditReport { let mut report = PresetAuditReport::new(tier); let connectors = connectors_for_tier_internal(tier); let expected: HashMap<&str, &PresetConnector> = connectors.iter().map(|c| (c.name, c)).collect(); let mut seen = HashSet::new(); for server in &config.mcp_servers { if let Some(expected_connector) = expected.get(server.name.as_str()) { seen.insert(server.name.as_str()); let expected_config = expected_connector.to_config(); if expected_config.command != server.command || expected_config.args != server.args || expected_config.env != server.env { report .mismatched .push((**expected_connector, server.clone())); } } else { report.extra.push(server.clone()); } } for connector in connectors { if !seen.contains(connector.name) { report.missing.push(connector); } } report }