Skip to main content

claude_wrapper/command/
mcp.rs

1use crate::Claude;
2use crate::command::ClaudeCommand;
3use crate::error::Result;
4use crate::exec::{self, CommandOutput};
5use crate::types::Scope;
6
7/// List configured MCP servers.
8///
9/// # Example
10///
11/// ```no_run
12/// use claude_wrapper::{Claude, ClaudeCommand, McpListCommand};
13///
14/// # async fn example() -> claude_wrapper::Result<()> {
15/// let claude = Claude::builder().build()?;
16/// let output = McpListCommand::new().execute(&claude).await?;
17/// println!("{}", output.stdout);
18/// # Ok(())
19/// # }
20/// ```
21#[derive(Debug, Clone, Default)]
22pub struct McpListCommand;
23
24impl McpListCommand {
25    #[must_use]
26    pub fn new() -> Self {
27        Self
28    }
29}
30
31impl ClaudeCommand for McpListCommand {
32    type Output = CommandOutput;
33
34    fn args(&self) -> Vec<String> {
35        vec!["mcp".to_string(), "list".to_string()]
36    }
37
38    async fn execute(&self, claude: &Claude) -> Result<CommandOutput> {
39        exec::run_claude(claude, self.args()).await
40    }
41}
42
43/// Get details about a specific MCP server.
44#[derive(Debug, Clone)]
45pub struct McpGetCommand {
46    name: String,
47}
48
49impl McpGetCommand {
50    #[must_use]
51    pub fn new(name: impl Into<String>) -> Self {
52        Self { name: name.into() }
53    }
54}
55
56impl ClaudeCommand for McpGetCommand {
57    type Output = CommandOutput;
58
59    fn args(&self) -> Vec<String> {
60        vec!["mcp".to_string(), "get".to_string(), self.name.clone()]
61    }
62
63    async fn execute(&self, claude: &Claude) -> Result<CommandOutput> {
64        exec::run_claude(claude, self.args()).await
65    }
66}
67
68/// Add an MCP server.
69///
70/// # Example
71///
72/// ```no_run
73/// use claude_wrapper::{Claude, ClaudeCommand, McpAddCommand, Scope};
74///
75/// # async fn example() -> claude_wrapper::Result<()> {
76/// let claude = Claude::builder().build()?;
77///
78/// // Add an HTTP MCP server
79/// McpAddCommand::new("sentry", "https://mcp.sentry.dev/mcp")
80///     .transport("http")
81///     .scope(Scope::User)
82///     .execute(&claude)
83///     .await?;
84///
85/// // Add a stdio MCP server with env vars
86/// McpAddCommand::new("my-server", "npx")
87///     .server_args(["my-mcp-server"])
88///     .env("API_KEY", "xxx")
89///     .scope(Scope::Local)
90///     .execute(&claude)
91///     .await?;
92/// # Ok(())
93/// # }
94/// ```
95#[derive(Debug, Clone)]
96pub struct McpAddCommand {
97    name: String,
98    command_or_url: String,
99    server_args: Vec<String>,
100    transport: Option<String>,
101    scope: Option<Scope>,
102    env: Vec<(String, String)>,
103    headers: Vec<String>,
104}
105
106impl McpAddCommand {
107    /// Create a new MCP add command.
108    #[must_use]
109    pub fn new(name: impl Into<String>, command_or_url: impl Into<String>) -> Self {
110        Self {
111            name: name.into(),
112            command_or_url: command_or_url.into(),
113            server_args: Vec::new(),
114            transport: None,
115            scope: None,
116            env: Vec::new(),
117            headers: Vec::new(),
118        }
119    }
120
121    /// Set the transport type (stdio, sse, http).
122    #[must_use]
123    pub fn transport(mut self, transport: impl Into<String>) -> Self {
124        self.transport = Some(transport.into());
125        self
126    }
127
128    /// Set the configuration scope.
129    #[must_use]
130    pub fn scope(mut self, scope: Scope) -> Self {
131        self.scope = Some(scope);
132        self
133    }
134
135    /// Add an environment variable.
136    #[must_use]
137    pub fn env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
138        self.env.push((key.into(), value.into()));
139        self
140    }
141
142    /// Add a header (for HTTP/SSE transport).
143    #[must_use]
144    pub fn header(mut self, header: impl Into<String>) -> Self {
145        self.headers.push(header.into());
146        self
147    }
148
149    /// Set additional arguments for the server command.
150    #[must_use]
151    pub fn server_args(mut self, args: impl IntoIterator<Item = impl Into<String>>) -> Self {
152        self.server_args.extend(args.into_iter().map(Into::into));
153        self
154    }
155}
156
157impl ClaudeCommand for McpAddCommand {
158    type Output = CommandOutput;
159
160    fn args(&self) -> Vec<String> {
161        let mut args = vec!["mcp".to_string(), "add".to_string()];
162
163        if let Some(ref transport) = self.transport {
164            args.push("--transport".to_string());
165            args.push(transport.clone());
166        }
167
168        if let Some(ref scope) = self.scope {
169            args.push("--scope".to_string());
170            args.push(scope.as_arg().to_string());
171        }
172
173        for (key, value) in &self.env {
174            args.push("-e".to_string());
175            args.push(format!("{key}={value}"));
176        }
177
178        for header in &self.headers {
179            args.push("-H".to_string());
180            args.push(header.clone());
181        }
182
183        args.push(self.name.clone());
184        args.push(self.command_or_url.clone());
185
186        if !self.server_args.is_empty() {
187            args.push("--".to_string());
188            args.extend(self.server_args.clone());
189        }
190
191        args
192    }
193
194    async fn execute(&self, claude: &Claude) -> Result<CommandOutput> {
195        exec::run_claude(claude, self.args()).await
196    }
197}
198
199/// Add an MCP server using raw JSON configuration.
200#[derive(Debug, Clone)]
201pub struct McpAddJsonCommand {
202    name: String,
203    json: String,
204    scope: Option<Scope>,
205}
206
207impl McpAddJsonCommand {
208    /// Create a new MCP add-json command.
209    #[must_use]
210    pub fn new(name: impl Into<String>, json: impl Into<String>) -> Self {
211        Self {
212            name: name.into(),
213            json: json.into(),
214            scope: None,
215        }
216    }
217
218    /// Set the configuration scope.
219    #[must_use]
220    pub fn scope(mut self, scope: Scope) -> Self {
221        self.scope = Some(scope);
222        self
223    }
224}
225
226impl ClaudeCommand for McpAddJsonCommand {
227    type Output = CommandOutput;
228
229    fn args(&self) -> Vec<String> {
230        let mut args = vec!["mcp".to_string(), "add-json".to_string()];
231
232        if let Some(ref scope) = self.scope {
233            args.push("--scope".to_string());
234            args.push(scope.as_arg().to_string());
235        }
236
237        args.push(self.name.clone());
238        args.push(self.json.clone());
239
240        args
241    }
242
243    async fn execute(&self, claude: &Claude) -> Result<CommandOutput> {
244        exec::run_claude(claude, self.args()).await
245    }
246}
247
248/// Remove an MCP server.
249#[derive(Debug, Clone)]
250pub struct McpRemoveCommand {
251    name: String,
252    scope: Option<Scope>,
253}
254
255impl McpRemoveCommand {
256    #[must_use]
257    pub fn new(name: impl Into<String>) -> Self {
258        Self {
259            name: name.into(),
260            scope: None,
261        }
262    }
263
264    /// Set the configuration scope.
265    #[must_use]
266    pub fn scope(mut self, scope: Scope) -> Self {
267        self.scope = Some(scope);
268        self
269    }
270}
271
272impl ClaudeCommand for McpRemoveCommand {
273    type Output = CommandOutput;
274
275    fn args(&self) -> Vec<String> {
276        let mut args = vec!["mcp".to_string(), "remove".to_string()];
277
278        if let Some(ref scope) = self.scope {
279            args.push("--scope".to_string());
280            args.push(scope.as_arg().to_string());
281        }
282
283        args.push(self.name.clone());
284
285        args
286    }
287
288    async fn execute(&self, claude: &Claude) -> Result<CommandOutput> {
289        exec::run_claude(claude, self.args()).await
290    }
291}
292
293/// Import MCP servers from Claude Desktop (Mac and WSL only).
294#[derive(Debug, Clone, Default)]
295pub struct McpAddFromDesktopCommand {
296    scope: Option<Scope>,
297}
298
299impl McpAddFromDesktopCommand {
300    #[must_use]
301    pub fn new() -> Self {
302        Self::default()
303    }
304
305    /// Set the configuration scope.
306    #[must_use]
307    pub fn scope(mut self, scope: Scope) -> Self {
308        self.scope = Some(scope);
309        self
310    }
311}
312
313impl ClaudeCommand for McpAddFromDesktopCommand {
314    type Output = CommandOutput;
315
316    fn args(&self) -> Vec<String> {
317        let mut args = vec!["mcp".to_string(), "add-from-claude-desktop".to_string()];
318        if let Some(ref scope) = self.scope {
319            args.push("--scope".to_string());
320            args.push(scope.as_arg().to_string());
321        }
322        args
323    }
324
325    async fn execute(&self, claude: &Claude) -> Result<CommandOutput> {
326        exec::run_claude(claude, self.args()).await
327    }
328}
329
330/// Reset all approved and rejected project-scoped MCP servers.
331#[derive(Debug, Clone, Default)]
332pub struct McpResetProjectChoicesCommand;
333
334impl McpResetProjectChoicesCommand {
335    #[must_use]
336    pub fn new() -> Self {
337        Self
338    }
339}
340
341impl ClaudeCommand for McpResetProjectChoicesCommand {
342    type Output = CommandOutput;
343
344    fn args(&self) -> Vec<String> {
345        vec!["mcp".to_string(), "reset-project-choices".to_string()]
346    }
347
348    async fn execute(&self, claude: &Claude) -> Result<CommandOutput> {
349        exec::run_claude(claude, self.args()).await
350    }
351}
352
353#[cfg(test)]
354mod tests {
355    use super::*;
356
357    #[test]
358    fn test_mcp_list_args() {
359        let cmd = McpListCommand::new();
360        assert_eq!(cmd.args(), vec!["mcp", "list"]);
361    }
362
363    #[test]
364    fn test_mcp_get_args() {
365        let cmd = McpGetCommand::new("my-server");
366        assert_eq!(cmd.args(), vec!["mcp", "get", "my-server"]);
367    }
368
369    #[test]
370    fn test_mcp_add_http() {
371        let cmd = McpAddCommand::new("sentry", "https://mcp.sentry.dev/mcp")
372            .transport("http")
373            .scope(Scope::User);
374
375        let args = cmd.args();
376        assert_eq!(
377            args,
378            vec![
379                "mcp",
380                "add",
381                "--transport",
382                "http",
383                "--scope",
384                "user",
385                "sentry",
386                "https://mcp.sentry.dev/mcp"
387            ]
388        );
389    }
390
391    #[test]
392    fn test_mcp_add_stdio_with_env() {
393        let cmd = McpAddCommand::new("my-server", "npx")
394            .env("API_KEY", "xxx")
395            .server_args(["my-mcp-server"]);
396
397        let args = cmd.args();
398        assert_eq!(
399            args,
400            vec![
401                "mcp",
402                "add",
403                "-e",
404                "API_KEY=xxx",
405                "my-server",
406                "npx",
407                "--",
408                "my-mcp-server"
409            ]
410        );
411    }
412
413    #[test]
414    fn test_mcp_remove_args() {
415        let cmd = McpRemoveCommand::new("old-server").scope(Scope::Project);
416        assert_eq!(
417            cmd.args(),
418            vec!["mcp", "remove", "--scope", "project", "old-server"]
419        );
420    }
421
422    #[test]
423    fn test_mcp_add_from_desktop() {
424        let cmd = McpAddFromDesktopCommand::new().scope(Scope::User);
425        assert_eq!(
426            cmd.args(),
427            vec!["mcp", "add-from-claude-desktop", "--scope", "user"]
428        );
429    }
430
431    #[test]
432    fn test_mcp_reset_project_choices() {
433        let cmd = McpResetProjectChoicesCommand::new();
434        assert_eq!(cmd.args(), vec!["mcp", "reset-project-choices"]);
435    }
436}