use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::Arc;
use crate::error::Error;
use crate::tool::Tool;
use crate::tool::mcp::McpClient;
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct McpPreset {
pub name: String,
pub description: String,
pub command: String,
pub args: Vec<String>,
pub env_keys: Vec<String>,
}
static PRESETS: &[(&str, &str)] = &[
("github", include_str!("../../mcp-presets/github.json")),
("gitlab", include_str!("../../mcp-presets/gitlab.json")),
("slack", include_str!("../../mcp-presets/slack.json")),
("notion", include_str!("../../mcp-presets/notion.json")),
(
"postgresql",
include_str!("../../mcp-presets/postgresql.json"),
),
(
"brave-search",
include_str!("../../mcp-presets/brave-search.json"),
),
("sentry", include_str!("../../mcp-presets/sentry.json")),
("linear", include_str!("../../mcp-presets/linear.json")),
(
"google-calendar",
include_str!("../../mcp-presets/google-calendar.json"),
),
("jira", include_str!("../../mcp-presets/jira.json")),
(
"chrome-devtools",
include_str!("../../mcp-presets/chrome-devtools.json"),
),
];
pub fn resolve_preset(name: &str) -> Result<McpPreset, Error> {
for (key, json) in PRESETS {
if *key == name {
let preset: McpPreset = serde_json::from_str(json)
.map_err(|e| Error::Config(format!("failed to parse preset '{name}': {e}")))?;
return Ok(preset);
}
}
Err(Error::Config(format!(
"unknown MCP preset '{name}'. Available presets: {}",
known_presets().join(", ")
)))
}
pub fn known_presets() -> Vec<&'static str> {
PRESETS.iter().map(|(k, _)| *k).collect()
}
pub fn check_preset_env(preset: &McpPreset) -> HashMap<String, bool> {
preset
.env_keys
.iter()
.map(|key| (key.clone(), std::env::var(key).is_ok()))
.collect()
}
pub async fn connect_preset(name: &str) -> Result<Vec<Arc<dyn Tool>>, Error> {
connect_preset_with_args(name, &[]).await
}
pub async fn connect_preset_with_args(
name: &str,
extra_args: &[String],
) -> Result<Vec<Arc<dyn Tool>>, Error> {
let preset = resolve_preset(name)?;
let env: HashMap<String, String> = preset
.env_keys
.iter()
.filter_map(|key| std::env::var(key).ok().map(|val| (key.clone(), val)))
.collect();
let mut args = preset.args.clone();
args.extend(extra_args.iter().cloned());
let client = McpClient::connect_stdio(&preset.command, &args, &env).await?;
Ok(client.into_tools())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn resolve_github_preset() {
let preset = resolve_preset("github").expect("should resolve github");
assert_eq!(preset.name, "GitHub");
assert_eq!(preset.command, "npx");
assert!(!preset.env_keys.is_empty());
}
#[test]
fn resolve_all_presets() {
for name in known_presets() {
let preset = resolve_preset(name).unwrap_or_else(|e| {
panic!("failed to resolve preset '{name}': {e}");
});
assert!(!preset.name.is_empty(), "preset '{name}' has empty name");
assert!(
!preset.command.is_empty(),
"preset '{name}' has empty command"
);
}
}
#[test]
fn resolve_unknown_preset_returns_error() {
let err = resolve_preset("nonexistent").unwrap_err();
assert!(err.to_string().contains("unknown MCP preset"));
assert!(err.to_string().contains("github")); }
#[tokio::test]
async fn connect_preset_with_args_unknown_is_err() {
let result = connect_preset_with_args("nonexistent", &["--flag".to_string()]).await;
match result {
Err(e) => assert!(e.to_string().contains("unknown MCP preset")),
Ok(_) => panic!("unknown preset must error, not spawn a server"),
}
}
#[test]
fn known_presets_returns_all() {
let presets = known_presets();
assert_eq!(presets.len(), 11);
assert!(presets.contains(&"github"));
assert!(presets.contains(&"jira"));
assert!(presets.contains(&"chrome-devtools"));
}
#[test]
fn chrome_devtools_preset_is_bundled() {
let p = resolve_preset("chrome-devtools").expect("chrome-devtools preset exists");
assert_eq!(p.command, "npx");
assert!(
p.args.iter().any(|a| a.contains("chrome-devtools-mcp")),
"args must launch chrome-devtools-mcp: {:?}",
p.args
);
assert!(
p.args.iter().any(|a| a == "--headless"),
"expected --headless"
);
assert!(
p.args.iter().any(|a| a == "--isolated"),
"expected --isolated"
);
assert!(p.env_keys.is_empty());
}
#[test]
fn check_preset_env_returns_status() {
let preset = resolve_preset("github").expect("should resolve github");
let status = check_preset_env(&preset);
assert!(status.contains_key("GITHUB_PERSONAL_ACCESS_TOKEN"));
}
#[test]
fn preset_json_round_trip() {
for name in known_presets() {
let preset = resolve_preset(name).expect("should resolve");
let json = serde_json::to_string(&preset);
assert!(json.is_ok(), "preset '{name}' failed to serialize");
}
}
}