Skip to main content

claude_wrapper/command/
mcp.rs

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/// List configured MCP servers.
12///
13/// # Example
14///
15/// ```no_run
16/// use claude_wrapper::{Claude, ClaudeCommand, McpListCommand};
17///
18/// # async fn example() -> claude_wrapper::Result<()> {
19/// let claude = Claude::builder().build()?;
20/// let output = McpListCommand::new().execute(&claude).await?;
21/// println!("{}", output.stdout);
22/// # Ok(())
23/// # }
24/// ```
25#[derive(Debug, Clone, Default)]
26pub struct McpListCommand;
27
28impl McpListCommand {
29    /// Creates a new MCP list command.
30    #[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/// Get details about a specific MCP server.
50#[derive(Debug, Clone)]
51pub struct McpGetCommand {
52    name: String,
53}
54
55impl McpGetCommand {
56    /// Creates a command to get details for a named MCP server.
57    #[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/// Add an MCP server.
77///
78/// # Example
79///
80/// ```no_run
81/// use claude_wrapper::{Claude, ClaudeCommand, McpAddCommand, Scope, Transport};
82///
83/// # async fn example() -> claude_wrapper::Result<()> {
84/// let claude = Claude::builder().build()?;
85///
86/// // Add an HTTP MCP server
87/// McpAddCommand::new("sentry", "https://mcp.sentry.dev/mcp")
88///     .transport(Transport::Http)
89///     .scope(Scope::User)
90///     .execute(&claude)
91///     .await?;
92///
93/// // Add a stdio MCP server with env vars
94/// McpAddCommand::new("my-server", "npx")
95///     .server_args(["my-mcp-server"])
96///     .env("API_KEY", "xxx")
97///     .scope(Scope::Local)
98///     .execute(&claude)
99///     .await?;
100/// # Ok(())
101/// # }
102/// ```
103#[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    /// Create a new MCP add command.
119    #[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    /// Set the transport type.
136    #[must_use]
137    pub fn transport(mut self, transport: Transport) -> Self {
138        self.transport = Some(transport);
139        self
140    }
141
142    /// Set the configuration scope.
143    #[must_use]
144    pub fn scope(mut self, scope: Scope) -> Self {
145        self.scope = Some(scope);
146        self
147    }
148
149    /// Add an environment variable.
150    #[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    /// Add a header (for HTTP/SSE transport).
157    #[must_use]
158    pub fn header(mut self, header: impl Into<String>) -> Self {
159        self.headers.push(header.into());
160        self
161    }
162
163    /// Set additional arguments for the server command.
164    #[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    /// Set a fixed port for OAuth callback.
171    #[must_use]
172    pub fn callback_port(mut self, port: u16) -> Self {
173        self.callback_port = Some(port);
174        self
175    }
176
177    /// Set the OAuth client ID for HTTP/SSE servers.
178    #[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    /// Enable prompting for OAuth client secret.
185    #[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/// Add an MCP server using raw JSON configuration.
250#[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    /// Create a new MCP add-json command.
260    #[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    /// Set the configuration scope.
271    #[must_use]
272    pub fn scope(mut self, scope: Scope) -> Self {
273        self.scope = Some(scope);
274        self
275    }
276
277    /// Prompt for the OAuth client secret (`--client-secret`; or set
278    /// it via the `MCP_CLIENT_SECRET` env var).
279    ///
280    /// Parity with [`McpAddCommand::client_secret`].
281    #[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/// Remove an MCP server.
316#[derive(Debug, Clone)]
317pub struct McpRemoveCommand {
318    name: String,
319    scope: Option<Scope>,
320}
321
322impl McpRemoveCommand {
323    /// Creates a command to remove a named MCP server.
324    #[must_use]
325    pub fn new(name: impl Into<String>) -> Self {
326        Self {
327            name: name.into(),
328            scope: None,
329        }
330    }
331
332    /// Set the configuration scope.
333    #[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/// Import MCP servers from Claude Desktop (Mac and WSL only).
363#[derive(Debug, Clone, Default)]
364pub struct McpAddFromDesktopCommand {
365    scope: Option<Scope>,
366}
367
368impl McpAddFromDesktopCommand {
369    /// Creates a command to import MCP servers from the Claude Desktop config.
370    #[must_use]
371    pub fn new() -> Self {
372        Self::default()
373    }
374
375    /// Set the configuration scope.
376    #[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/// Start the Claude Code MCP server.
402///
403/// # Example
404///
405/// ```no_run
406/// use claude_wrapper::{Claude, ClaudeCommand, McpServeCommand};
407///
408/// # async fn example() -> claude_wrapper::Result<()> {
409/// let claude = Claude::builder().build()?;
410/// McpServeCommand::new()
411///     .verbose()
412///     .execute(&claude)
413///     .await?;
414/// # Ok(())
415/// # }
416/// ```
417#[derive(Debug, Clone, Default)]
418pub struct McpServeCommand {
419    debug: bool,
420    verbose: bool,
421}
422
423impl McpServeCommand {
424    /// Create a new MCP serve command.
425    #[must_use]
426    pub fn new() -> Self {
427        Self::default()
428    }
429
430    /// Enable debug output.
431    #[must_use]
432    pub fn debug(mut self) -> Self {
433        self.debug = true;
434        self
435    }
436
437    /// Enable verbose output.
438    #[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/// Reset all approved and rejected project-scoped MCP servers.
466#[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#[cfg(test)]
490mod tests {
491    use super::*;
492
493    #[test]
494    fn test_mcp_list_args() {
495        let cmd = McpListCommand::new();
496        assert_eq!(cmd.args(), vec!["mcp", "list"]);
497    }
498
499    #[test]
500    fn test_mcp_get_args() {
501        let cmd = McpGetCommand::new("my-server");
502        assert_eq!(cmd.args(), vec!["mcp", "get", "my-server"]);
503    }
504
505    #[test]
506    fn test_mcp_add_http() {
507        let cmd = McpAddCommand::new("sentry", "https://mcp.sentry.dev/mcp")
508            .transport(Transport::Http)
509            .scope(Scope::User);
510
511        let args = cmd.args();
512        assert_eq!(
513            args,
514            vec![
515                "mcp",
516                "add",
517                "--transport",
518                "http",
519                "--scope",
520                "user",
521                "sentry",
522                "https://mcp.sentry.dev/mcp"
523            ]
524        );
525    }
526
527    #[test]
528    fn test_mcp_add_stdio_with_env() {
529        let cmd = McpAddCommand::new("my-server", "npx")
530            .env("API_KEY", "xxx")
531            .server_args(["my-mcp-server"]);
532
533        let args = cmd.args();
534        assert_eq!(
535            args,
536            vec![
537                "mcp",
538                "add",
539                "-e",
540                "API_KEY=xxx",
541                "my-server",
542                "npx",
543                "--",
544                "my-mcp-server"
545            ]
546        );
547    }
548
549    #[test]
550    fn test_mcp_add_oauth_flags() {
551        let cmd = McpAddCommand::new("my-server", "https://example.com/mcp")
552            .transport(Transport::Http)
553            .callback_port(8080)
554            .client_id("my-app-id")
555            .client_secret();
556
557        let args = cmd.args();
558        assert_eq!(
559            args,
560            vec![
561                "mcp",
562                "add",
563                "--transport",
564                "http",
565                "--callback-port",
566                "8080",
567                "--client-id",
568                "my-app-id",
569                "--client-secret",
570                "my-server",
571                "https://example.com/mcp"
572            ]
573        );
574    }
575
576    #[test]
577    fn test_mcp_add_json_basic() {
578        let cmd = McpAddJsonCommand::new("srv", r#"{"command":"npx"}"#).scope(Scope::User);
579        assert_eq!(
580            cmd.args(),
581            vec![
582                "mcp",
583                "add-json",
584                "--scope",
585                "user",
586                "srv",
587                r#"{"command":"npx"}"#
588            ]
589        );
590    }
591
592    #[test]
593    fn test_mcp_add_json_client_secret() {
594        // #601 parity: add-json accepts --client-secret like add does.
595        let cmd = McpAddJsonCommand::new("srv", "{}").client_secret();
596        assert_eq!(
597            cmd.args(),
598            vec!["mcp", "add-json", "--client-secret", "srv", "{}"]
599        );
600    }
601
602    #[test]
603    fn test_mcp_add_json_no_client_secret_by_default() {
604        let cmd = McpAddJsonCommand::new("srv", "{}");
605        assert!(!cmd.args().contains(&"--client-secret".to_string()));
606    }
607
608    #[test]
609    fn test_mcp_remove_args() {
610        let cmd = McpRemoveCommand::new("old-server").scope(Scope::Project);
611        assert_eq!(
612            cmd.args(),
613            vec!["mcp", "remove", "--scope", "project", "old-server"]
614        );
615    }
616
617    #[test]
618    fn test_mcp_add_from_desktop() {
619        let cmd = McpAddFromDesktopCommand::new().scope(Scope::User);
620        assert_eq!(
621            cmd.args(),
622            vec!["mcp", "add-from-claude-desktop", "--scope", "user"]
623        );
624    }
625
626    #[test]
627    fn test_mcp_reset_project_choices() {
628        let cmd = McpResetProjectChoicesCommand::new();
629        assert_eq!(cmd.args(), vec!["mcp", "reset-project-choices"]);
630    }
631
632    #[test]
633    fn test_mcp_serve_default() {
634        let cmd = McpServeCommand::new();
635        assert_eq!(cmd.args(), vec!["mcp", "serve"]);
636    }
637
638    #[test]
639    fn test_mcp_serve_with_flags() {
640        let cmd = McpServeCommand::new().debug().verbose();
641        assert_eq!(cmd.args(), vec!["mcp", "serve", "--debug", "--verbose"]);
642    }
643}