use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum AgentKind {
ClaudeDesktop,
Cursor,
Copilot,
OpenCode,
Cline,
}
impl AgentKind {
pub fn all() -> &'static [AgentKind] {
&[
AgentKind::ClaudeDesktop,
AgentKind::Cursor,
AgentKind::Copilot,
AgentKind::OpenCode,
AgentKind::Cline,
]
}
pub fn display_name(&self) -> &'static str {
match self {
Self::ClaudeDesktop => "Claude Desktop",
Self::Cursor => "Cursor",
Self::Copilot => "GitHub Copilot",
Self::OpenCode => "OpenCode",
Self::Cline => "Cline",
}
}
pub fn config_path(&self) -> &'static str {
match self {
Self::ClaudeDesktop => ".claude.json",
Self::Cursor => ".cursor/mcp.json",
Self::Copilot => ".copilot/mcp.json",
Self::OpenCode => ".opencode.json",
Self::Cline => ".cline/mcp.json",
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct McpConfig {
pub server_name: String,
pub command: String,
pub args: Vec<String>,
pub env: Vec<(String, String)>,
}
pub fn generate_mcp_config(_agent: &AgentKind, config: &McpConfig) -> String {
let entry = mcp_server_entry(config);
make_mcp_json(&config.server_name, entry)
}
fn mcp_server_entry(config: &McpConfig) -> serde_json::Value {
let mut server = serde_json::json!({
"command": config.command,
"args": config.args,
});
if !config.env.is_empty() {
let env_obj: serde_json::Map<String, serde_json::Value> = config
.env
.iter()
.map(|(k, v)| (k.clone(), serde_json::Value::String(v.clone())))
.collect();
server
.as_object_mut()
.unwrap()
.insert("env".into(), serde_json::Value::Object(env_obj));
}
server
}
fn make_mcp_json(server_name: &str, entry: serde_json::Value) -> String {
let mut map = serde_json::Map::new();
map.insert(server_name.to_string(), entry);
let json = serde_json::json!({ "mcpServers": map });
serde_json::to_string_pretty(&json).unwrap_or_default()
}
#[cfg(test)]
mod tests {
use super::*;
fn test_config() -> McpConfig {
McpConfig {
server_name: "test-server".into(),
command: "test-server".into(),
args: vec!["--stdio".into()],
env: vec![("TEST_API_KEY".into(), "${TEST_API_KEY}".into())],
}
}
#[test]
fn all_agents_covered() {
assert_eq!(AgentKind::all().len(), 5);
for agent in AgentKind::all() {
let config = test_config();
let json = generate_mcp_config(agent, &config);
assert!(
json.contains("test-server"),
"missing server name for {:?}",
agent
);
assert!(json.contains("TEST_API_KEY"), "missing env for {:?}", agent);
}
}
#[test]
fn claude_desktop_format() {
let json = generate_mcp_config(&AgentKind::ClaudeDesktop, &test_config());
let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
assert!(parsed["mcpServers"]["test-server"].is_object());
assert_eq!(
parsed["mcpServers"]["test-server"]["command"],
"test-server"
);
}
#[test]
fn no_env_when_empty() {
let config = McpConfig {
server_name: "bare".into(),
command: "bare".into(),
args: vec![],
env: vec![],
};
let json = generate_mcp_config(&AgentKind::Cursor, &config);
assert!(!json.contains("env"), "env should be absent when empty");
}
#[test]
fn agent_display_names() {
assert_eq!(AgentKind::ClaudeDesktop.display_name(), "Claude Desktop");
assert_eq!(AgentKind::Cursor.display_name(), "Cursor");
}
}