1use crate::Claude;
2use crate::command::ClaudeCommand;
3use crate::error::Result;
4use crate::exec::{self, CommandOutput};
5use crate::types::Scope;
6
7#[derive(Debug, Clone, Default)]
22pub struct McpListCommand;
23
24impl McpListCommand {
25 #[must_use]
26 pub fn new() -> Self {
27 Self
28 }
29}
30
31impl ClaudeCommand for McpListCommand {
32 type Output = CommandOutput;
33
34 fn args(&self) -> Vec<String> {
35 vec!["mcp".to_string(), "list".to_string()]
36 }
37
38 async fn execute(&self, claude: &Claude) -> Result<CommandOutput> {
39 exec::run_claude(claude, self.args()).await
40 }
41}
42
43#[derive(Debug, Clone)]
45pub struct McpGetCommand {
46 name: String,
47}
48
49impl McpGetCommand {
50 #[must_use]
51 pub fn new(name: impl Into<String>) -> Self {
52 Self { name: name.into() }
53 }
54}
55
56impl ClaudeCommand for McpGetCommand {
57 type Output = CommandOutput;
58
59 fn args(&self) -> Vec<String> {
60 vec!["mcp".to_string(), "get".to_string(), self.name.clone()]
61 }
62
63 async fn execute(&self, claude: &Claude) -> Result<CommandOutput> {
64 exec::run_claude(claude, self.args()).await
65 }
66}
67
68#[derive(Debug, Clone)]
96pub struct McpAddCommand {
97 name: String,
98 command_or_url: String,
99 server_args: Vec<String>,
100 transport: Option<String>,
101 scope: Option<Scope>,
102 env: Vec<(String, String)>,
103 headers: Vec<String>,
104}
105
106impl McpAddCommand {
107 #[must_use]
109 pub fn new(name: impl Into<String>, command_or_url: impl Into<String>) -> Self {
110 Self {
111 name: name.into(),
112 command_or_url: command_or_url.into(),
113 server_args: Vec::new(),
114 transport: None,
115 scope: None,
116 env: Vec::new(),
117 headers: Vec::new(),
118 }
119 }
120
121 #[must_use]
123 pub fn transport(mut self, transport: impl Into<String>) -> Self {
124 self.transport = Some(transport.into());
125 self
126 }
127
128 #[must_use]
130 pub fn scope(mut self, scope: Scope) -> Self {
131 self.scope = Some(scope);
132 self
133 }
134
135 #[must_use]
137 pub fn env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
138 self.env.push((key.into(), value.into()));
139 self
140 }
141
142 #[must_use]
144 pub fn header(mut self, header: impl Into<String>) -> Self {
145 self.headers.push(header.into());
146 self
147 }
148
149 #[must_use]
151 pub fn server_args(mut self, args: impl IntoIterator<Item = impl Into<String>>) -> Self {
152 self.server_args.extend(args.into_iter().map(Into::into));
153 self
154 }
155}
156
157impl ClaudeCommand for McpAddCommand {
158 type Output = CommandOutput;
159
160 fn args(&self) -> Vec<String> {
161 let mut args = vec!["mcp".to_string(), "add".to_string()];
162
163 if let Some(ref transport) = self.transport {
164 args.push("--transport".to_string());
165 args.push(transport.clone());
166 }
167
168 if let Some(ref scope) = self.scope {
169 args.push("--scope".to_string());
170 args.push(scope.as_arg().to_string());
171 }
172
173 for (key, value) in &self.env {
174 args.push("-e".to_string());
175 args.push(format!("{key}={value}"));
176 }
177
178 for header in &self.headers {
179 args.push("-H".to_string());
180 args.push(header.clone());
181 }
182
183 args.push(self.name.clone());
184 args.push(self.command_or_url.clone());
185
186 if !self.server_args.is_empty() {
187 args.push("--".to_string());
188 args.extend(self.server_args.clone());
189 }
190
191 args
192 }
193
194 async fn execute(&self, claude: &Claude) -> Result<CommandOutput> {
195 exec::run_claude(claude, self.args()).await
196 }
197}
198
199#[derive(Debug, Clone)]
201pub struct McpAddJsonCommand {
202 name: String,
203 json: String,
204 scope: Option<Scope>,
205}
206
207impl McpAddJsonCommand {
208 #[must_use]
210 pub fn new(name: impl Into<String>, json: impl Into<String>) -> Self {
211 Self {
212 name: name.into(),
213 json: json.into(),
214 scope: None,
215 }
216 }
217
218 #[must_use]
220 pub fn scope(mut self, scope: Scope) -> Self {
221 self.scope = Some(scope);
222 self
223 }
224}
225
226impl ClaudeCommand for McpAddJsonCommand {
227 type Output = CommandOutput;
228
229 fn args(&self) -> Vec<String> {
230 let mut args = vec!["mcp".to_string(), "add-json".to_string()];
231
232 if let Some(ref scope) = self.scope {
233 args.push("--scope".to_string());
234 args.push(scope.as_arg().to_string());
235 }
236
237 args.push(self.name.clone());
238 args.push(self.json.clone());
239
240 args
241 }
242
243 async fn execute(&self, claude: &Claude) -> Result<CommandOutput> {
244 exec::run_claude(claude, self.args()).await
245 }
246}
247
248#[derive(Debug, Clone)]
250pub struct McpRemoveCommand {
251 name: String,
252 scope: Option<Scope>,
253}
254
255impl McpRemoveCommand {
256 #[must_use]
257 pub fn new(name: impl Into<String>) -> Self {
258 Self {
259 name: name.into(),
260 scope: None,
261 }
262 }
263
264 #[must_use]
266 pub fn scope(mut self, scope: Scope) -> Self {
267 self.scope = Some(scope);
268 self
269 }
270}
271
272impl ClaudeCommand for McpRemoveCommand {
273 type Output = CommandOutput;
274
275 fn args(&self) -> Vec<String> {
276 let mut args = vec!["mcp".to_string(), "remove".to_string()];
277
278 if let Some(ref scope) = self.scope {
279 args.push("--scope".to_string());
280 args.push(scope.as_arg().to_string());
281 }
282
283 args.push(self.name.clone());
284
285 args
286 }
287
288 async fn execute(&self, claude: &Claude) -> Result<CommandOutput> {
289 exec::run_claude(claude, self.args()).await
290 }
291}
292
293#[derive(Debug, Clone, Default)]
295pub struct McpAddFromDesktopCommand {
296 scope: Option<Scope>,
297}
298
299impl McpAddFromDesktopCommand {
300 #[must_use]
301 pub fn new() -> Self {
302 Self::default()
303 }
304
305 #[must_use]
307 pub fn scope(mut self, scope: Scope) -> Self {
308 self.scope = Some(scope);
309 self
310 }
311}
312
313impl ClaudeCommand for McpAddFromDesktopCommand {
314 type Output = CommandOutput;
315
316 fn args(&self) -> Vec<String> {
317 let mut args = vec!["mcp".to_string(), "add-from-claude-desktop".to_string()];
318 if let Some(ref scope) = self.scope {
319 args.push("--scope".to_string());
320 args.push(scope.as_arg().to_string());
321 }
322 args
323 }
324
325 async fn execute(&self, claude: &Claude) -> Result<CommandOutput> {
326 exec::run_claude(claude, self.args()).await
327 }
328}
329
330#[derive(Debug, Clone, Default)]
332pub struct McpResetProjectChoicesCommand;
333
334impl McpResetProjectChoicesCommand {
335 #[must_use]
336 pub fn new() -> Self {
337 Self
338 }
339}
340
341impl ClaudeCommand for McpResetProjectChoicesCommand {
342 type Output = CommandOutput;
343
344 fn args(&self) -> Vec<String> {
345 vec!["mcp".to_string(), "reset-project-choices".to_string()]
346 }
347
348 async fn execute(&self, claude: &Claude) -> Result<CommandOutput> {
349 exec::run_claude(claude, self.args()).await
350 }
351}
352
353#[cfg(test)]
354mod tests {
355 use super::*;
356
357 #[test]
358 fn test_mcp_list_args() {
359 let cmd = McpListCommand::new();
360 assert_eq!(cmd.args(), vec!["mcp", "list"]);
361 }
362
363 #[test]
364 fn test_mcp_get_args() {
365 let cmd = McpGetCommand::new("my-server");
366 assert_eq!(cmd.args(), vec!["mcp", "get", "my-server"]);
367 }
368
369 #[test]
370 fn test_mcp_add_http() {
371 let cmd = McpAddCommand::new("sentry", "https://mcp.sentry.dev/mcp")
372 .transport("http")
373 .scope(Scope::User);
374
375 let args = cmd.args();
376 assert_eq!(
377 args,
378 vec![
379 "mcp",
380 "add",
381 "--transport",
382 "http",
383 "--scope",
384 "user",
385 "sentry",
386 "https://mcp.sentry.dev/mcp"
387 ]
388 );
389 }
390
391 #[test]
392 fn test_mcp_add_stdio_with_env() {
393 let cmd = McpAddCommand::new("my-server", "npx")
394 .env("API_KEY", "xxx")
395 .server_args(["my-mcp-server"]);
396
397 let args = cmd.args();
398 assert_eq!(
399 args,
400 vec![
401 "mcp",
402 "add",
403 "-e",
404 "API_KEY=xxx",
405 "my-server",
406 "npx",
407 "--",
408 "my-mcp-server"
409 ]
410 );
411 }
412
413 #[test]
414 fn test_mcp_remove_args() {
415 let cmd = McpRemoveCommand::new("old-server").scope(Scope::Project);
416 assert_eq!(
417 cmd.args(),
418 vec!["mcp", "remove", "--scope", "project", "old-server"]
419 );
420 }
421
422 #[test]
423 fn test_mcp_add_from_desktop() {
424 let cmd = McpAddFromDesktopCommand::new().scope(Scope::User);
425 assert_eq!(
426 cmd.args(),
427 vec!["mcp", "add-from-claude-desktop", "--scope", "user"]
428 );
429 }
430
431 #[test]
432 fn test_mcp_reset_project_choices() {
433 let cmd = McpResetProjectChoicesCommand::new();
434 assert_eq!(cmd.args(), vec!["mcp", "reset-project-choices"]);
435 }
436}