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