1use crate::Claude;
2use crate::command::ClaudeCommand;
3use crate::error::Result;
4use crate::exec::{self, CommandOutput};
5use crate::types::{Scope, Transport};
6
7#[derive(Debug, Clone, Default)]
22pub struct McpListCommand;
23
24impl McpListCommand {
25 #[must_use]
27 pub fn new() -> Self {
28 Self
29 }
30}
31
32impl ClaudeCommand for McpListCommand {
33 type Output = CommandOutput;
34
35 fn args(&self) -> Vec<String> {
36 vec!["mcp".to_string(), "list".to_string()]
37 }
38
39 async fn execute(&self, claude: &Claude) -> Result<CommandOutput> {
40 exec::run_claude(claude, self.args()).await
41 }
42}
43
44#[derive(Debug, Clone)]
46pub struct McpGetCommand {
47 name: String,
48}
49
50impl McpGetCommand {
51 #[must_use]
53 pub fn new(name: impl Into<String>) -> Self {
54 Self { name: name.into() }
55 }
56}
57
58impl ClaudeCommand for McpGetCommand {
59 type Output = CommandOutput;
60
61 fn args(&self) -> Vec<String> {
62 vec!["mcp".to_string(), "get".to_string(), self.name.clone()]
63 }
64
65 async fn execute(&self, claude: &Claude) -> Result<CommandOutput> {
66 exec::run_claude(claude, self.args()).await
67 }
68}
69
70#[derive(Debug, Clone)]
98pub struct McpAddCommand {
99 name: String,
100 command_or_url: String,
101 server_args: Vec<String>,
102 transport: Option<Transport>,
103 scope: Option<Scope>,
104 env: Vec<(String, String)>,
105 headers: Vec<String>,
106 callback_port: Option<u16>,
107 client_id: Option<String>,
108 client_secret: bool,
109}
110
111impl McpAddCommand {
112 #[must_use]
114 pub fn new(name: impl Into<String>, command_or_url: impl Into<String>) -> Self {
115 Self {
116 name: name.into(),
117 command_or_url: command_or_url.into(),
118 server_args: Vec::new(),
119 transport: None,
120 scope: None,
121 env: Vec::new(),
122 headers: Vec::new(),
123 callback_port: None,
124 client_id: None,
125 client_secret: false,
126 }
127 }
128
129 #[must_use]
131 pub fn transport(mut self, transport: impl Into<Transport>) -> Self {
132 self.transport = Some(transport.into());
133 self
134 }
135
136 #[must_use]
138 pub fn scope(mut self, scope: Scope) -> Self {
139 self.scope = Some(scope);
140 self
141 }
142
143 #[must_use]
145 pub fn env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
146 self.env.push((key.into(), value.into()));
147 self
148 }
149
150 #[must_use]
152 pub fn header(mut self, header: impl Into<String>) -> Self {
153 self.headers.push(header.into());
154 self
155 }
156
157 #[must_use]
159 pub fn server_args(mut self, args: impl IntoIterator<Item = impl Into<String>>) -> Self {
160 self.server_args.extend(args.into_iter().map(Into::into));
161 self
162 }
163
164 #[must_use]
166 pub fn callback_port(mut self, port: u16) -> Self {
167 self.callback_port = Some(port);
168 self
169 }
170
171 #[must_use]
173 pub fn client_id(mut self, id: impl Into<String>) -> Self {
174 self.client_id = Some(id.into());
175 self
176 }
177
178 #[must_use]
180 pub fn client_secret(mut self) -> Self {
181 self.client_secret = true;
182 self
183 }
184}
185
186impl ClaudeCommand for McpAddCommand {
187 type Output = CommandOutput;
188
189 fn args(&self) -> Vec<String> {
190 let mut args = vec!["mcp".to_string(), "add".to_string()];
191
192 if let Some(transport) = self.transport {
193 args.push("--transport".to_string());
194 args.push(transport.to_string());
195 }
196
197 if let Some(ref scope) = self.scope {
198 args.push("--scope".to_string());
199 args.push(scope.as_arg().to_string());
200 }
201
202 for (key, value) in &self.env {
203 args.push("-e".to_string());
204 args.push(format!("{key}={value}"));
205 }
206
207 for header in &self.headers {
208 args.push("-H".to_string());
209 args.push(header.clone());
210 }
211
212 if let Some(port) = self.callback_port {
213 args.push("--callback-port".to_string());
214 args.push(port.to_string());
215 }
216
217 if let Some(ref id) = self.client_id {
218 args.push("--client-id".to_string());
219 args.push(id.clone());
220 }
221
222 if self.client_secret {
223 args.push("--client-secret".to_string());
224 }
225
226 args.push(self.name.clone());
227 args.push(self.command_or_url.clone());
228
229 if !self.server_args.is_empty() {
230 args.push("--".to_string());
231 args.extend(self.server_args.clone());
232 }
233
234 args
235 }
236
237 async fn execute(&self, claude: &Claude) -> Result<CommandOutput> {
238 exec::run_claude(claude, self.args()).await
239 }
240}
241
242#[derive(Debug, Clone)]
244pub struct McpAddJsonCommand {
245 name: String,
246 json: String,
247 scope: Option<Scope>,
248}
249
250impl McpAddJsonCommand {
251 #[must_use]
253 pub fn new(name: impl Into<String>, json: impl Into<String>) -> Self {
254 Self {
255 name: name.into(),
256 json: json.into(),
257 scope: None,
258 }
259 }
260
261 #[must_use]
263 pub fn scope(mut self, scope: Scope) -> Self {
264 self.scope = Some(scope);
265 self
266 }
267}
268
269impl ClaudeCommand for McpAddJsonCommand {
270 type Output = CommandOutput;
271
272 fn args(&self) -> Vec<String> {
273 let mut args = vec!["mcp".to_string(), "add-json".to_string()];
274
275 if let Some(ref scope) = self.scope {
276 args.push("--scope".to_string());
277 args.push(scope.as_arg().to_string());
278 }
279
280 args.push(self.name.clone());
281 args.push(self.json.clone());
282
283 args
284 }
285
286 async fn execute(&self, claude: &Claude) -> Result<CommandOutput> {
287 exec::run_claude(claude, self.args()).await
288 }
289}
290
291#[derive(Debug, Clone)]
293pub struct McpRemoveCommand {
294 name: String,
295 scope: Option<Scope>,
296}
297
298impl McpRemoveCommand {
299 #[must_use]
301 pub fn new(name: impl Into<String>) -> Self {
302 Self {
303 name: name.into(),
304 scope: None,
305 }
306 }
307
308 #[must_use]
310 pub fn scope(mut self, scope: Scope) -> Self {
311 self.scope = Some(scope);
312 self
313 }
314}
315
316impl ClaudeCommand for McpRemoveCommand {
317 type Output = CommandOutput;
318
319 fn args(&self) -> Vec<String> {
320 let mut args = vec!["mcp".to_string(), "remove".to_string()];
321
322 if let Some(ref scope) = self.scope {
323 args.push("--scope".to_string());
324 args.push(scope.as_arg().to_string());
325 }
326
327 args.push(self.name.clone());
328
329 args
330 }
331
332 async fn execute(&self, claude: &Claude) -> Result<CommandOutput> {
333 exec::run_claude(claude, self.args()).await
334 }
335}
336
337#[derive(Debug, Clone, Default)]
339pub struct McpAddFromDesktopCommand {
340 scope: Option<Scope>,
341}
342
343impl McpAddFromDesktopCommand {
344 #[must_use]
346 pub fn new() -> Self {
347 Self::default()
348 }
349
350 #[must_use]
352 pub fn scope(mut self, scope: Scope) -> Self {
353 self.scope = Some(scope);
354 self
355 }
356}
357
358impl ClaudeCommand for McpAddFromDesktopCommand {
359 type Output = CommandOutput;
360
361 fn args(&self) -> Vec<String> {
362 let mut args = vec!["mcp".to_string(), "add-from-claude-desktop".to_string()];
363 if let Some(ref scope) = self.scope {
364 args.push("--scope".to_string());
365 args.push(scope.as_arg().to_string());
366 }
367 args
368 }
369
370 async fn execute(&self, claude: &Claude) -> Result<CommandOutput> {
371 exec::run_claude(claude, self.args()).await
372 }
373}
374
375#[derive(Debug, Clone, Default)]
392pub struct McpServeCommand {
393 debug: bool,
394 verbose: bool,
395}
396
397impl McpServeCommand {
398 #[must_use]
400 pub fn new() -> Self {
401 Self::default()
402 }
403
404 #[must_use]
406 pub fn debug(mut self) -> Self {
407 self.debug = true;
408 self
409 }
410
411 #[must_use]
413 pub fn verbose(mut self) -> Self {
414 self.verbose = true;
415 self
416 }
417}
418
419impl ClaudeCommand for McpServeCommand {
420 type Output = CommandOutput;
421
422 fn args(&self) -> Vec<String> {
423 let mut args = vec!["mcp".to_string(), "serve".to_string()];
424 if self.debug {
425 args.push("--debug".to_string());
426 }
427 if self.verbose {
428 args.push("--verbose".to_string());
429 }
430 args
431 }
432
433 async fn execute(&self, claude: &Claude) -> Result<CommandOutput> {
434 exec::run_claude(claude, self.args()).await
435 }
436}
437
438#[derive(Debug, Clone, Default)]
440pub struct McpResetProjectChoicesCommand;
441
442impl McpResetProjectChoicesCommand {
443 #[must_use]
444 pub fn new() -> Self {
445 Self
446 }
447}
448
449impl ClaudeCommand for McpResetProjectChoicesCommand {
450 type Output = CommandOutput;
451
452 fn args(&self) -> Vec<String> {
453 vec!["mcp".to_string(), "reset-project-choices".to_string()]
454 }
455
456 async fn execute(&self, claude: &Claude) -> Result<CommandOutput> {
457 exec::run_claude(claude, self.args()).await
458 }
459}
460
461#[cfg(test)]
462mod tests {
463 use super::*;
464
465 #[test]
466 fn test_mcp_list_args() {
467 let cmd = McpListCommand::new();
468 assert_eq!(cmd.args(), vec!["mcp", "list"]);
469 }
470
471 #[test]
472 fn test_mcp_get_args() {
473 let cmd = McpGetCommand::new("my-server");
474 assert_eq!(cmd.args(), vec!["mcp", "get", "my-server"]);
475 }
476
477 #[test]
478 fn test_mcp_add_http() {
479 let cmd = McpAddCommand::new("sentry", "https://mcp.sentry.dev/mcp")
480 .transport("http")
481 .scope(Scope::User);
482
483 let args = cmd.args();
484 assert_eq!(
485 args,
486 vec![
487 "mcp",
488 "add",
489 "--transport",
490 "http",
491 "--scope",
492 "user",
493 "sentry",
494 "https://mcp.sentry.dev/mcp"
495 ]
496 );
497 }
498
499 #[test]
500 fn test_mcp_add_stdio_with_env() {
501 let cmd = McpAddCommand::new("my-server", "npx")
502 .env("API_KEY", "xxx")
503 .server_args(["my-mcp-server"]);
504
505 let args = cmd.args();
506 assert_eq!(
507 args,
508 vec![
509 "mcp",
510 "add",
511 "-e",
512 "API_KEY=xxx",
513 "my-server",
514 "npx",
515 "--",
516 "my-mcp-server"
517 ]
518 );
519 }
520
521 #[test]
522 fn test_mcp_add_oauth_flags() {
523 let cmd = McpAddCommand::new("my-server", "https://example.com/mcp")
524 .transport("http")
525 .callback_port(8080)
526 .client_id("my-app-id")
527 .client_secret();
528
529 let args = cmd.args();
530 assert_eq!(
531 args,
532 vec![
533 "mcp",
534 "add",
535 "--transport",
536 "http",
537 "--callback-port",
538 "8080",
539 "--client-id",
540 "my-app-id",
541 "--client-secret",
542 "my-server",
543 "https://example.com/mcp"
544 ]
545 );
546 }
547
548 #[test]
549 fn test_mcp_remove_args() {
550 let cmd = McpRemoveCommand::new("old-server").scope(Scope::Project);
551 assert_eq!(
552 cmd.args(),
553 vec!["mcp", "remove", "--scope", "project", "old-server"]
554 );
555 }
556
557 #[test]
558 fn test_mcp_add_from_desktop() {
559 let cmd = McpAddFromDesktopCommand::new().scope(Scope::User);
560 assert_eq!(
561 cmd.args(),
562 vec!["mcp", "add-from-claude-desktop", "--scope", "user"]
563 );
564 }
565
566 #[test]
567 fn test_mcp_reset_project_choices() {
568 let cmd = McpResetProjectChoicesCommand::new();
569 assert_eq!(cmd.args(), vec!["mcp", "reset-project-choices"]);
570 }
571
572 #[test]
573 fn test_mcp_serve_default() {
574 let cmd = McpServeCommand::new();
575 assert_eq!(cmd.args(), vec!["mcp", "serve"]);
576 }
577
578 #[test]
579 fn test_mcp_serve_with_flags() {
580 let cmd = McpServeCommand::new().debug().verbose();
581 assert_eq!(cmd.args(), vec!["mcp", "serve", "--debug", "--verbose"]);
582 }
583}