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}
256
257impl McpAddJsonCommand {
258    /// Create a new MCP add-json command.
259    #[must_use]
260    pub fn new(name: impl Into<String>, json: impl Into<String>) -> Self {
261        Self {
262            name: name.into(),
263            json: json.into(),
264            scope: None,
265        }
266    }
267
268    /// Set the configuration scope.
269    #[must_use]
270    pub fn scope(mut self, scope: Scope) -> Self {
271        self.scope = Some(scope);
272        self
273    }
274}
275
276impl ClaudeCommand for McpAddJsonCommand {
277    type Output = CommandOutput;
278
279    fn args(&self) -> Vec<String> {
280        let mut args = vec!["mcp".to_string(), "add-json".to_string()];
281
282        if let Some(ref scope) = self.scope {
283            args.push("--scope".to_string());
284            args.push(scope.as_arg().to_string());
285        }
286
287        args.push(self.name.clone());
288        args.push(self.json.clone());
289
290        args
291    }
292
293    #[cfg(feature = "async")]
294    async fn execute(&self, claude: &Claude) -> Result<CommandOutput> {
295        exec::run_claude(claude, self.args()).await
296    }
297}
298
299/// Remove an MCP server.
300#[derive(Debug, Clone)]
301pub struct McpRemoveCommand {
302    name: String,
303    scope: Option<Scope>,
304}
305
306impl McpRemoveCommand {
307    /// Creates a command to remove a named MCP server.
308    #[must_use]
309    pub fn new(name: impl Into<String>) -> Self {
310        Self {
311            name: name.into(),
312            scope: None,
313        }
314    }
315
316    /// Set the configuration scope.
317    #[must_use]
318    pub fn scope(mut self, scope: Scope) -> Self {
319        self.scope = Some(scope);
320        self
321    }
322}
323
324impl ClaudeCommand for McpRemoveCommand {
325    type Output = CommandOutput;
326
327    fn args(&self) -> Vec<String> {
328        let mut args = vec!["mcp".to_string(), "remove".to_string()];
329
330        if let Some(ref scope) = self.scope {
331            args.push("--scope".to_string());
332            args.push(scope.as_arg().to_string());
333        }
334
335        args.push(self.name.clone());
336
337        args
338    }
339
340    #[cfg(feature = "async")]
341    async fn execute(&self, claude: &Claude) -> Result<CommandOutput> {
342        exec::run_claude(claude, self.args()).await
343    }
344}
345
346/// Import MCP servers from Claude Desktop (Mac and WSL only).
347#[derive(Debug, Clone, Default)]
348pub struct McpAddFromDesktopCommand {
349    scope: Option<Scope>,
350}
351
352impl McpAddFromDesktopCommand {
353    /// Creates a command to import MCP servers from the Claude Desktop config.
354    #[must_use]
355    pub fn new() -> Self {
356        Self::default()
357    }
358
359    /// Set the configuration scope.
360    #[must_use]
361    pub fn scope(mut self, scope: Scope) -> Self {
362        self.scope = Some(scope);
363        self
364    }
365}
366
367impl ClaudeCommand for McpAddFromDesktopCommand {
368    type Output = CommandOutput;
369
370    fn args(&self) -> Vec<String> {
371        let mut args = vec!["mcp".to_string(), "add-from-claude-desktop".to_string()];
372        if let Some(ref scope) = self.scope {
373            args.push("--scope".to_string());
374            args.push(scope.as_arg().to_string());
375        }
376        args
377    }
378
379    #[cfg(feature = "async")]
380    async fn execute(&self, claude: &Claude) -> Result<CommandOutput> {
381        exec::run_claude(claude, self.args()).await
382    }
383}
384
385/// Start the Claude Code MCP server.
386///
387/// # Example
388///
389/// ```no_run
390/// use claude_wrapper::{Claude, ClaudeCommand, McpServeCommand};
391///
392/// # async fn example() -> claude_wrapper::Result<()> {
393/// let claude = Claude::builder().build()?;
394/// McpServeCommand::new()
395///     .verbose()
396///     .execute(&claude)
397///     .await?;
398/// # Ok(())
399/// # }
400/// ```
401#[derive(Debug, Clone, Default)]
402pub struct McpServeCommand {
403    debug: bool,
404    verbose: bool,
405}
406
407impl McpServeCommand {
408    /// Create a new MCP serve command.
409    #[must_use]
410    pub fn new() -> Self {
411        Self::default()
412    }
413
414    /// Enable debug output.
415    #[must_use]
416    pub fn debug(mut self) -> Self {
417        self.debug = true;
418        self
419    }
420
421    /// Enable verbose output.
422    #[must_use]
423    pub fn verbose(mut self) -> Self {
424        self.verbose = true;
425        self
426    }
427}
428
429impl ClaudeCommand for McpServeCommand {
430    type Output = CommandOutput;
431
432    fn args(&self) -> Vec<String> {
433        let mut args = vec!["mcp".to_string(), "serve".to_string()];
434        if self.debug {
435            args.push("--debug".to_string());
436        }
437        if self.verbose {
438            args.push("--verbose".to_string());
439        }
440        args
441    }
442
443    #[cfg(feature = "async")]
444    async fn execute(&self, claude: &Claude) -> Result<CommandOutput> {
445        exec::run_claude(claude, self.args()).await
446    }
447}
448
449/// Reset all approved and rejected project-scoped MCP servers.
450#[derive(Debug, Clone, Default)]
451pub struct McpResetProjectChoicesCommand;
452
453impl McpResetProjectChoicesCommand {
454    #[must_use]
455    pub fn new() -> Self {
456        Self
457    }
458}
459
460impl ClaudeCommand for McpResetProjectChoicesCommand {
461    type Output = CommandOutput;
462
463    fn args(&self) -> Vec<String> {
464        vec!["mcp".to_string(), "reset-project-choices".to_string()]
465    }
466
467    #[cfg(feature = "async")]
468    async fn execute(&self, claude: &Claude) -> Result<CommandOutput> {
469        exec::run_claude(claude, self.args()).await
470    }
471}
472
473#[cfg(test)]
474mod tests {
475    use super::*;
476
477    #[test]
478    fn test_mcp_list_args() {
479        let cmd = McpListCommand::new();
480        assert_eq!(cmd.args(), vec!["mcp", "list"]);
481    }
482
483    #[test]
484    fn test_mcp_get_args() {
485        let cmd = McpGetCommand::new("my-server");
486        assert_eq!(cmd.args(), vec!["mcp", "get", "my-server"]);
487    }
488
489    #[test]
490    fn test_mcp_add_http() {
491        let cmd = McpAddCommand::new("sentry", "https://mcp.sentry.dev/mcp")
492            .transport(Transport::Http)
493            .scope(Scope::User);
494
495        let args = cmd.args();
496        assert_eq!(
497            args,
498            vec![
499                "mcp",
500                "add",
501                "--transport",
502                "http",
503                "--scope",
504                "user",
505                "sentry",
506                "https://mcp.sentry.dev/mcp"
507            ]
508        );
509    }
510
511    #[test]
512    fn test_mcp_add_stdio_with_env() {
513        let cmd = McpAddCommand::new("my-server", "npx")
514            .env("API_KEY", "xxx")
515            .server_args(["my-mcp-server"]);
516
517        let args = cmd.args();
518        assert_eq!(
519            args,
520            vec![
521                "mcp",
522                "add",
523                "-e",
524                "API_KEY=xxx",
525                "my-server",
526                "npx",
527                "--",
528                "my-mcp-server"
529            ]
530        );
531    }
532
533    #[test]
534    fn test_mcp_add_oauth_flags() {
535        let cmd = McpAddCommand::new("my-server", "https://example.com/mcp")
536            .transport(Transport::Http)
537            .callback_port(8080)
538            .client_id("my-app-id")
539            .client_secret();
540
541        let args = cmd.args();
542        assert_eq!(
543            args,
544            vec![
545                "mcp",
546                "add",
547                "--transport",
548                "http",
549                "--callback-port",
550                "8080",
551                "--client-id",
552                "my-app-id",
553                "--client-secret",
554                "my-server",
555                "https://example.com/mcp"
556            ]
557        );
558    }
559
560    #[test]
561    fn test_mcp_remove_args() {
562        let cmd = McpRemoveCommand::new("old-server").scope(Scope::Project);
563        assert_eq!(
564            cmd.args(),
565            vec!["mcp", "remove", "--scope", "project", "old-server"]
566        );
567    }
568
569    #[test]
570    fn test_mcp_add_from_desktop() {
571        let cmd = McpAddFromDesktopCommand::new().scope(Scope::User);
572        assert_eq!(
573            cmd.args(),
574            vec!["mcp", "add-from-claude-desktop", "--scope", "user"]
575        );
576    }
577
578    #[test]
579    fn test_mcp_reset_project_choices() {
580        let cmd = McpResetProjectChoicesCommand::new();
581        assert_eq!(cmd.args(), vec!["mcp", "reset-project-choices"]);
582    }
583
584    #[test]
585    fn test_mcp_serve_default() {
586        let cmd = McpServeCommand::new();
587        assert_eq!(cmd.args(), vec!["mcp", "serve"]);
588    }
589
590    #[test]
591    fn test_mcp_serve_with_flags() {
592        let cmd = McpServeCommand::new().debug().verbose();
593        assert_eq!(cmd.args(), vec!["mcp", "serve", "--debug", "--verbose"]);
594    }
595}