llm_kernel/install/
wizard.rs1use serde::{Deserialize, Serialize};
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
7pub enum AgentKind {
8 ClaudeDesktop,
10 Cursor,
12 Copilot,
14 OpenCode,
16 Cline,
18 Windsurf,
20 RooCode,
22}
23
24impl AgentKind {
25 pub fn all() -> &'static [AgentKind] {
27 &[
28 AgentKind::ClaudeDesktop,
29 AgentKind::Cursor,
30 AgentKind::Copilot,
31 AgentKind::OpenCode,
32 AgentKind::Cline,
33 AgentKind::Windsurf,
34 AgentKind::RooCode,
35 ]
36 }
37
38 pub fn display_name(&self) -> &'static str {
40 match self {
41 Self::ClaudeDesktop => "Claude Desktop",
42 Self::Cursor => "Cursor",
43 Self::Copilot => "GitHub Copilot",
44 Self::OpenCode => "OpenCode",
45 Self::Cline => "Cline",
46 Self::Windsurf => "Windsurf",
47 Self::RooCode => "Roo Code",
48 }
49 }
50
51 pub fn config_path(&self) -> &'static str {
53 match self {
54 Self::ClaudeDesktop => ".claude.json",
55 Self::Cursor => ".cursor/mcp.json",
56 Self::Copilot => ".copilot/mcp.json",
57 Self::OpenCode => ".opencode.json",
58 Self::Cline => ".cline/mcp.json",
59 Self::Windsurf => ".windsurf/mcp.json",
60 Self::RooCode => ".roo/mcp.json",
61 }
62 }
63}
64
65#[derive(Debug, Clone, Serialize, Deserialize)]
67pub struct McpConfig {
68 pub server_name: String,
70 pub command: String,
72 pub args: Vec<String>,
74 pub env: Vec<(String, String)>,
77}
78
79pub fn generate_mcp_config(_agent: &AgentKind, config: &McpConfig) -> String {
87 let entry = mcp_server_entry(config);
88 make_mcp_json(&config.server_name, entry)
89}
90
91fn mcp_server_entry(config: &McpConfig) -> serde_json::Value {
93 let mut server = serde_json::json!({
94 "command": config.command,
95 "args": config.args,
96 });
97
98 if !config.env.is_empty() {
99 let env_obj: serde_json::Map<String, serde_json::Value> = config
100 .env
101 .iter()
102 .map(|(k, v)| (k.clone(), serde_json::Value::String(v.clone())))
103 .collect();
104 server
105 .as_object_mut()
106 .unwrap()
107 .insert("env".into(), serde_json::Value::Object(env_obj));
108 }
109
110 server
111}
112
113fn make_mcp_json(server_name: &str, entry: serde_json::Value) -> String {
114 let mut map = serde_json::Map::new();
115 map.insert(server_name.to_string(), entry);
116 let json = serde_json::json!({ "mcpServers": map });
117 serde_json::to_string_pretty(&json).unwrap_or_default()
118}
119
120#[cfg(test)]
121mod tests {
122 use super::*;
123
124 fn test_config() -> McpConfig {
125 McpConfig {
126 server_name: "test-server".into(),
127 command: "test-server".into(),
128 args: vec!["--stdio".into()],
129 env: vec![("TEST_API_KEY".into(), "${TEST_API_KEY}".into())],
130 }
131 }
132
133 #[test]
134 fn all_agents_covered() {
135 assert_eq!(AgentKind::all().len(), 7);
136 for agent in AgentKind::all() {
137 let config = test_config();
138 let json = generate_mcp_config(agent, &config);
139 assert!(
140 json.contains("test-server"),
141 "missing server name for {:?}",
142 agent
143 );
144 assert!(json.contains("TEST_API_KEY"), "missing env for {:?}", agent);
145 }
146 }
147
148 #[test]
149 fn claude_desktop_format() {
150 let json = generate_mcp_config(&AgentKind::ClaudeDesktop, &test_config());
151 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
152 assert!(parsed["mcpServers"]["test-server"].is_object());
153 assert_eq!(
154 parsed["mcpServers"]["test-server"]["command"],
155 "test-server"
156 );
157 }
158
159 #[test]
160 fn no_env_when_empty() {
161 let config = McpConfig {
162 server_name: "bare".into(),
163 command: "bare".into(),
164 args: vec![],
165 env: vec![],
166 };
167 let json = generate_mcp_config(&AgentKind::Cursor, &config);
168 assert!(!json.contains("env"), "env should be absent when empty");
169 }
170
171 #[test]
172 fn agent_display_names() {
173 assert_eq!(AgentKind::ClaudeDesktop.display_name(), "Claude Desktop");
174 assert_eq!(AgentKind::Cursor.display_name(), "Cursor");
175 }
176}