1#[cfg(feature = "async")]
2use crate::Claude;
3use crate::command::ClaudeCommand;
4#[cfg(feature = "async")]
5use crate::error::Result;
6#[cfg(feature = "async")]
7use crate::exec;
8use crate::exec::CommandOutput;
9use crate::types::{Scope, Transport};
10
11#[derive(Debug, Clone, Default)]
26pub struct McpListCommand;
27
28impl McpListCommand {
29 #[must_use]
31 pub fn new() -> Self {
32 Self
33 }
34}
35
36impl ClaudeCommand for McpListCommand {
37 type Output = CommandOutput;
38
39 fn args(&self) -> Vec<String> {
40 vec!["mcp".to_string(), "list".to_string()]
41 }
42
43 #[cfg(feature = "async")]
44 async fn execute(&self, claude: &Claude) -> Result<CommandOutput> {
45 exec::run_claude(claude, self.args()).await
46 }
47}
48
49#[derive(Debug, Clone)]
51pub struct McpGetCommand {
52 name: String,
53}
54
55impl McpGetCommand {
56 #[must_use]
58 pub fn new(name: impl Into<String>) -> Self {
59 Self { name: name.into() }
60 }
61}
62
63impl ClaudeCommand for McpGetCommand {
64 type Output = CommandOutput;
65
66 fn args(&self) -> Vec<String> {
67 vec!["mcp".to_string(), "get".to_string(), self.name.clone()]
68 }
69
70 #[cfg(feature = "async")]
71 async fn execute(&self, claude: &Claude) -> Result<CommandOutput> {
72 exec::run_claude(claude, self.args()).await
73 }
74}
75
76#[derive(Debug, Clone)]
104pub struct McpAddCommand {
105 name: String,
106 command_or_url: String,
107 server_args: Vec<String>,
108 transport: Option<Transport>,
109 scope: Option<Scope>,
110 env: Vec<(String, String)>,
111 headers: Vec<String>,
112 callback_port: Option<u16>,
113 client_id: Option<String>,
114 client_secret: bool,
115}
116
117impl McpAddCommand {
118 #[must_use]
120 pub fn new(name: impl Into<String>, command_or_url: impl Into<String>) -> Self {
121 Self {
122 name: name.into(),
123 command_or_url: command_or_url.into(),
124 server_args: Vec::new(),
125 transport: None,
126 scope: None,
127 env: Vec::new(),
128 headers: Vec::new(),
129 callback_port: None,
130 client_id: None,
131 client_secret: false,
132 }
133 }
134
135 #[must_use]
137 pub fn transport(mut self, transport: Transport) -> Self {
138 self.transport = Some(transport);
139 self
140 }
141
142 #[must_use]
144 pub fn scope(mut self, scope: Scope) -> Self {
145 self.scope = Some(scope);
146 self
147 }
148
149 #[must_use]
151 pub fn env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
152 self.env.push((key.into(), value.into()));
153 self
154 }
155
156 #[must_use]
158 pub fn header(mut self, header: impl Into<String>) -> Self {
159 self.headers.push(header.into());
160 self
161 }
162
163 #[must_use]
165 pub fn server_args(mut self, args: impl IntoIterator<Item = impl Into<String>>) -> Self {
166 self.server_args.extend(args.into_iter().map(Into::into));
167 self
168 }
169
170 #[must_use]
172 pub fn callback_port(mut self, port: u16) -> Self {
173 self.callback_port = Some(port);
174 self
175 }
176
177 #[must_use]
179 pub fn client_id(mut self, id: impl Into<String>) -> Self {
180 self.client_id = Some(id.into());
181 self
182 }
183
184 #[must_use]
186 pub fn client_secret(mut self) -> Self {
187 self.client_secret = true;
188 self
189 }
190}
191
192impl ClaudeCommand for McpAddCommand {
193 type Output = CommandOutput;
194
195 fn args(&self) -> Vec<String> {
196 let mut args = vec!["mcp".to_string(), "add".to_string()];
197
198 if let Some(transport) = self.transport {
199 args.push("--transport".to_string());
200 args.push(transport.to_string());
201 }
202
203 if let Some(ref scope) = self.scope {
204 args.push("--scope".to_string());
205 args.push(scope.as_arg().to_string());
206 }
207
208 for (key, value) in &self.env {
209 args.push("-e".to_string());
210 args.push(format!("{key}={value}"));
211 }
212
213 for header in &self.headers {
214 args.push("-H".to_string());
215 args.push(header.clone());
216 }
217
218 if let Some(port) = self.callback_port {
219 args.push("--callback-port".to_string());
220 args.push(port.to_string());
221 }
222
223 if let Some(ref id) = self.client_id {
224 args.push("--client-id".to_string());
225 args.push(id.clone());
226 }
227
228 if self.client_secret {
229 args.push("--client-secret".to_string());
230 }
231
232 args.push(self.name.clone());
233 args.push(self.command_or_url.clone());
234
235 if !self.server_args.is_empty() {
236 args.push("--".to_string());
237 args.extend(self.server_args.clone());
238 }
239
240 args
241 }
242
243 #[cfg(feature = "async")]
244 async fn execute(&self, claude: &Claude) -> Result<CommandOutput> {
245 exec::run_claude(claude, self.args()).await
246 }
247}
248
249#[derive(Debug, Clone)]
251pub struct McpAddJsonCommand {
252 name: String,
253 json: String,
254 scope: Option<Scope>,
255 client_secret: bool,
256}
257
258impl McpAddJsonCommand {
259 #[must_use]
261 pub fn new(name: impl Into<String>, json: impl Into<String>) -> Self {
262 Self {
263 name: name.into(),
264 json: json.into(),
265 scope: None,
266 client_secret: false,
267 }
268 }
269
270 #[must_use]
272 pub fn scope(mut self, scope: Scope) -> Self {
273 self.scope = Some(scope);
274 self
275 }
276
277 #[must_use]
282 pub fn client_secret(mut self) -> Self {
283 self.client_secret = true;
284 self
285 }
286}
287
288impl ClaudeCommand for McpAddJsonCommand {
289 type Output = CommandOutput;
290
291 fn args(&self) -> Vec<String> {
292 let mut args = vec!["mcp".to_string(), "add-json".to_string()];
293
294 if let Some(ref scope) = self.scope {
295 args.push("--scope".to_string());
296 args.push(scope.as_arg().to_string());
297 }
298
299 if self.client_secret {
300 args.push("--client-secret".to_string());
301 }
302
303 args.push(self.name.clone());
304 args.push(self.json.clone());
305
306 args
307 }
308
309 #[cfg(feature = "async")]
310 async fn execute(&self, claude: &Claude) -> Result<CommandOutput> {
311 exec::run_claude(claude, self.args()).await
312 }
313}
314
315#[derive(Debug, Clone)]
317pub struct McpRemoveCommand {
318 name: String,
319 scope: Option<Scope>,
320}
321
322impl McpRemoveCommand {
323 #[must_use]
325 pub fn new(name: impl Into<String>) -> Self {
326 Self {
327 name: name.into(),
328 scope: None,
329 }
330 }
331
332 #[must_use]
334 pub fn scope(mut self, scope: Scope) -> Self {
335 self.scope = Some(scope);
336 self
337 }
338}
339
340impl ClaudeCommand for McpRemoveCommand {
341 type Output = CommandOutput;
342
343 fn args(&self) -> Vec<String> {
344 let mut args = vec!["mcp".to_string(), "remove".to_string()];
345
346 if let Some(ref scope) = self.scope {
347 args.push("--scope".to_string());
348 args.push(scope.as_arg().to_string());
349 }
350
351 args.push(self.name.clone());
352
353 args
354 }
355
356 #[cfg(feature = "async")]
357 async fn execute(&self, claude: &Claude) -> Result<CommandOutput> {
358 exec::run_claude(claude, self.args()).await
359 }
360}
361
362#[derive(Debug, Clone, Default)]
364pub struct McpAddFromDesktopCommand {
365 scope: Option<Scope>,
366}
367
368impl McpAddFromDesktopCommand {
369 #[must_use]
371 pub fn new() -> Self {
372 Self::default()
373 }
374
375 #[must_use]
377 pub fn scope(mut self, scope: Scope) -> Self {
378 self.scope = Some(scope);
379 self
380 }
381}
382
383impl ClaudeCommand for McpAddFromDesktopCommand {
384 type Output = CommandOutput;
385
386 fn args(&self) -> Vec<String> {
387 let mut args = vec!["mcp".to_string(), "add-from-claude-desktop".to_string()];
388 if let Some(ref scope) = self.scope {
389 args.push("--scope".to_string());
390 args.push(scope.as_arg().to_string());
391 }
392 args
393 }
394
395 #[cfg(feature = "async")]
396 async fn execute(&self, claude: &Claude) -> Result<CommandOutput> {
397 exec::run_claude(claude, self.args()).await
398 }
399}
400
401#[derive(Debug, Clone, Default)]
418pub struct McpServeCommand {
419 debug: bool,
420 verbose: bool,
421}
422
423impl McpServeCommand {
424 #[must_use]
426 pub fn new() -> Self {
427 Self::default()
428 }
429
430 #[must_use]
432 pub fn debug(mut self) -> Self {
433 self.debug = true;
434 self
435 }
436
437 #[must_use]
439 pub fn verbose(mut self) -> Self {
440 self.verbose = true;
441 self
442 }
443}
444
445impl ClaudeCommand for McpServeCommand {
446 type Output = CommandOutput;
447
448 fn args(&self) -> Vec<String> {
449 let mut args = vec!["mcp".to_string(), "serve".to_string()];
450 if self.debug {
451 args.push("--debug".to_string());
452 }
453 if self.verbose {
454 args.push("--verbose".to_string());
455 }
456 args
457 }
458
459 #[cfg(feature = "async")]
460 async fn execute(&self, claude: &Claude) -> Result<CommandOutput> {
461 exec::run_claude(claude, self.args()).await
462 }
463}
464
465#[derive(Debug, Clone, Default)]
467pub struct McpResetProjectChoicesCommand;
468
469impl McpResetProjectChoicesCommand {
470 #[must_use]
471 pub fn new() -> Self {
472 Self
473 }
474}
475
476impl ClaudeCommand for McpResetProjectChoicesCommand {
477 type Output = CommandOutput;
478
479 fn args(&self) -> Vec<String> {
480 vec!["mcp".to_string(), "reset-project-choices".to_string()]
481 }
482
483 #[cfg(feature = "async")]
484 async fn execute(&self, claude: &Claude) -> Result<CommandOutput> {
485 exec::run_claude(claude, self.args()).await
486 }
487}
488
489#[derive(Debug, Clone)]
496pub struct McpLoginCommand {
497 name: String,
498 no_browser: bool,
499}
500
501impl McpLoginCommand {
502 #[must_use]
504 pub fn new(name: impl Into<String>) -> Self {
505 Self {
506 name: name.into(),
507 no_browser: false,
508 }
509 }
510
511 #[must_use]
514 pub fn no_browser(mut self) -> Self {
515 self.no_browser = true;
516 self
517 }
518}
519
520impl ClaudeCommand for McpLoginCommand {
521 type Output = CommandOutput;
522
523 fn args(&self) -> Vec<String> {
524 let mut args = vec!["mcp".to_string(), "login".to_string()];
525 if self.no_browser {
526 args.push("--no-browser".to_string());
527 }
528 args.push(self.name.clone());
529 args
530 }
531
532 #[cfg(feature = "async")]
533 async fn execute(&self, claude: &Claude) -> Result<CommandOutput> {
534 exec::run_claude(claude, self.args()).await
535 }
536}
537
538#[derive(Debug, Clone)]
540pub struct McpLogoutCommand {
541 name: String,
542}
543
544impl McpLogoutCommand {
545 #[must_use]
547 pub fn new(name: impl Into<String>) -> Self {
548 Self { name: name.into() }
549 }
550}
551
552impl ClaudeCommand for McpLogoutCommand {
553 type Output = CommandOutput;
554
555 fn args(&self) -> Vec<String> {
556 vec!["mcp".to_string(), "logout".to_string(), self.name.clone()]
557 }
558
559 #[cfg(feature = "async")]
560 async fn execute(&self, claude: &Claude) -> Result<CommandOutput> {
561 exec::run_claude(claude, self.args()).await
562 }
563}
564
565#[cfg(test)]
566mod tests {
567 use super::*;
568
569 #[test]
570 fn test_mcp_list_args() {
571 let cmd = McpListCommand::new();
572 assert_eq!(cmd.args(), vec!["mcp", "list"]);
573 }
574
575 #[test]
576 fn test_mcp_get_args() {
577 let cmd = McpGetCommand::new("my-server");
578 assert_eq!(cmd.args(), vec!["mcp", "get", "my-server"]);
579 }
580
581 #[test]
582 fn test_mcp_add_http() {
583 let cmd = McpAddCommand::new("sentry", "https://mcp.sentry.dev/mcp")
584 .transport(Transport::Http)
585 .scope(Scope::User);
586
587 let args = cmd.args();
588 assert_eq!(
589 args,
590 vec![
591 "mcp",
592 "add",
593 "--transport",
594 "http",
595 "--scope",
596 "user",
597 "sentry",
598 "https://mcp.sentry.dev/mcp"
599 ]
600 );
601 }
602
603 #[test]
604 fn test_mcp_add_stdio_with_env() {
605 let cmd = McpAddCommand::new("my-server", "npx")
606 .env("API_KEY", "xxx")
607 .server_args(["my-mcp-server"]);
608
609 let args = cmd.args();
610 assert_eq!(
611 args,
612 vec![
613 "mcp",
614 "add",
615 "-e",
616 "API_KEY=xxx",
617 "my-server",
618 "npx",
619 "--",
620 "my-mcp-server"
621 ]
622 );
623 }
624
625 #[test]
626 fn test_mcp_add_oauth_flags() {
627 let cmd = McpAddCommand::new("my-server", "https://example.com/mcp")
628 .transport(Transport::Http)
629 .callback_port(8080)
630 .client_id("my-app-id")
631 .client_secret();
632
633 let args = cmd.args();
634 assert_eq!(
635 args,
636 vec![
637 "mcp",
638 "add",
639 "--transport",
640 "http",
641 "--callback-port",
642 "8080",
643 "--client-id",
644 "my-app-id",
645 "--client-secret",
646 "my-server",
647 "https://example.com/mcp"
648 ]
649 );
650 }
651
652 #[test]
653 fn test_mcp_add_json_basic() {
654 let cmd = McpAddJsonCommand::new("srv", r#"{"command":"npx"}"#).scope(Scope::User);
655 assert_eq!(
656 cmd.args(),
657 vec![
658 "mcp",
659 "add-json",
660 "--scope",
661 "user",
662 "srv",
663 r#"{"command":"npx"}"#
664 ]
665 );
666 }
667
668 #[test]
669 fn test_mcp_add_json_client_secret() {
670 let cmd = McpAddJsonCommand::new("srv", "{}").client_secret();
672 assert_eq!(
673 cmd.args(),
674 vec!["mcp", "add-json", "--client-secret", "srv", "{}"]
675 );
676 }
677
678 #[test]
679 fn test_mcp_add_json_no_client_secret_by_default() {
680 let cmd = McpAddJsonCommand::new("srv", "{}");
681 assert!(!cmd.args().contains(&"--client-secret".to_string()));
682 }
683
684 #[test]
685 fn test_mcp_remove_args() {
686 let cmd = McpRemoveCommand::new("old-server").scope(Scope::Project);
687 assert_eq!(
688 cmd.args(),
689 vec!["mcp", "remove", "--scope", "project", "old-server"]
690 );
691 }
692
693 #[test]
694 fn test_mcp_add_from_desktop() {
695 let cmd = McpAddFromDesktopCommand::new().scope(Scope::User);
696 assert_eq!(
697 cmd.args(),
698 vec!["mcp", "add-from-claude-desktop", "--scope", "user"]
699 );
700 }
701
702 #[test]
703 fn test_mcp_reset_project_choices() {
704 let cmd = McpResetProjectChoicesCommand::new();
705 assert_eq!(cmd.args(), vec!["mcp", "reset-project-choices"]);
706 }
707
708 #[test]
709 fn test_mcp_serve_default() {
710 let cmd = McpServeCommand::new();
711 assert_eq!(cmd.args(), vec!["mcp", "serve"]);
712 }
713
714 #[test]
715 fn test_mcp_serve_with_flags() {
716 let cmd = McpServeCommand::new().debug().verbose();
717 assert_eq!(cmd.args(), vec!["mcp", "serve", "--debug", "--verbose"]);
718 }
719
720 #[test]
721 fn test_mcp_login_args() {
722 let cmd = McpLoginCommand::new("sentry");
723 assert_eq!(cmd.args(), vec!["mcp", "login", "sentry"]);
724 }
725
726 #[test]
727 fn test_mcp_login_no_browser() {
728 let cmd = McpLoginCommand::new("sentry").no_browser();
729 assert_eq!(cmd.args(), vec!["mcp", "login", "--no-browser", "sentry"]);
730 }
731
732 #[test]
733 fn test_mcp_logout_args() {
734 let cmd = McpLogoutCommand::new("sentry");
735 assert_eq!(cmd.args(), vec!["mcp", "logout", "sentry"]);
736 }
737}