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, Transport};
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    /// Creates a new MCP list command.
26    #[must_use]
27    pub fn new() -> Self {
28        Self
29    }
30}
31
32impl ClaudeCommand for McpListCommand {
33    type Output = CommandOutput;
34
35    fn args(&self) -> Vec<String> {
36        vec!["mcp".to_string(), "list".to_string()]
37    }
38
39    async fn execute(&self, claude: &Claude) -> Result<CommandOutput> {
40        exec::run_claude(claude, self.args()).await
41    }
42}
43
44/// Get details about a specific MCP server.
45#[derive(Debug, Clone)]
46pub struct McpGetCommand {
47    name: String,
48}
49
50impl McpGetCommand {
51    /// Creates a command to get details for a named MCP server.
52    #[must_use]
53    pub fn new(name: impl Into<String>) -> Self {
54        Self { name: name.into() }
55    }
56}
57
58impl ClaudeCommand for McpGetCommand {
59    type Output = CommandOutput;
60
61    fn args(&self) -> Vec<String> {
62        vec!["mcp".to_string(), "get".to_string(), self.name.clone()]
63    }
64
65    async fn execute(&self, claude: &Claude) -> Result<CommandOutput> {
66        exec::run_claude(claude, self.args()).await
67    }
68}
69
70/// Add an MCP server.
71///
72/// # Example
73///
74/// ```no_run
75/// use claude_wrapper::{Claude, ClaudeCommand, McpAddCommand, Scope};
76///
77/// # async fn example() -> claude_wrapper::Result<()> {
78/// let claude = Claude::builder().build()?;
79///
80/// // Add an HTTP MCP server
81/// McpAddCommand::new("sentry", "https://mcp.sentry.dev/mcp")
82///     .transport("http")
83///     .scope(Scope::User)
84///     .execute(&claude)
85///     .await?;
86///
87/// // Add a stdio MCP server with env vars
88/// McpAddCommand::new("my-server", "npx")
89///     .server_args(["my-mcp-server"])
90///     .env("API_KEY", "xxx")
91///     .scope(Scope::Local)
92///     .execute(&claude)
93///     .await?;
94/// # Ok(())
95/// # }
96/// ```
97#[derive(Debug, Clone)]
98pub struct McpAddCommand {
99    name: String,
100    command_or_url: String,
101    server_args: Vec<String>,
102    transport: Option<Transport>,
103    scope: Option<Scope>,
104    env: Vec<(String, String)>,
105    headers: Vec<String>,
106    callback_port: Option<u16>,
107    client_id: Option<String>,
108    client_secret: bool,
109}
110
111impl McpAddCommand {
112    /// Create a new MCP add command.
113    #[must_use]
114    pub fn new(name: impl Into<String>, command_or_url: impl Into<String>) -> Self {
115        Self {
116            name: name.into(),
117            command_or_url: command_or_url.into(),
118            server_args: Vec::new(),
119            transport: None,
120            scope: None,
121            env: Vec::new(),
122            headers: Vec::new(),
123            callback_port: None,
124            client_id: None,
125            client_secret: false,
126        }
127    }
128
129    /// Set the transport type.
130    #[must_use]
131    pub fn transport(mut self, transport: impl Into<Transport>) -> Self {
132        self.transport = Some(transport.into());
133        self
134    }
135
136    /// Set the configuration scope.
137    #[must_use]
138    pub fn scope(mut self, scope: Scope) -> Self {
139        self.scope = Some(scope);
140        self
141    }
142
143    /// Add an environment variable.
144    #[must_use]
145    pub fn env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
146        self.env.push((key.into(), value.into()));
147        self
148    }
149
150    /// Add a header (for HTTP/SSE transport).
151    #[must_use]
152    pub fn header(mut self, header: impl Into<String>) -> Self {
153        self.headers.push(header.into());
154        self
155    }
156
157    /// Set additional arguments for the server command.
158    #[must_use]
159    pub fn server_args(mut self, args: impl IntoIterator<Item = impl Into<String>>) -> Self {
160        self.server_args.extend(args.into_iter().map(Into::into));
161        self
162    }
163
164    /// Set a fixed port for OAuth callback.
165    #[must_use]
166    pub fn callback_port(mut self, port: u16) -> Self {
167        self.callback_port = Some(port);
168        self
169    }
170
171    /// Set the OAuth client ID for HTTP/SSE servers.
172    #[must_use]
173    pub fn client_id(mut self, id: impl Into<String>) -> Self {
174        self.client_id = Some(id.into());
175        self
176    }
177
178    /// Enable prompting for OAuth client secret.
179    #[must_use]
180    pub fn client_secret(mut self) -> Self {
181        self.client_secret = true;
182        self
183    }
184}
185
186impl ClaudeCommand for McpAddCommand {
187    type Output = CommandOutput;
188
189    fn args(&self) -> Vec<String> {
190        let mut args = vec!["mcp".to_string(), "add".to_string()];
191
192        if let Some(transport) = self.transport {
193            args.push("--transport".to_string());
194            args.push(transport.to_string());
195        }
196
197        if let Some(ref scope) = self.scope {
198            args.push("--scope".to_string());
199            args.push(scope.as_arg().to_string());
200        }
201
202        for (key, value) in &self.env {
203            args.push("-e".to_string());
204            args.push(format!("{key}={value}"));
205        }
206
207        for header in &self.headers {
208            args.push("-H".to_string());
209            args.push(header.clone());
210        }
211
212        if let Some(port) = self.callback_port {
213            args.push("--callback-port".to_string());
214            args.push(port.to_string());
215        }
216
217        if let Some(ref id) = self.client_id {
218            args.push("--client-id".to_string());
219            args.push(id.clone());
220        }
221
222        if self.client_secret {
223            args.push("--client-secret".to_string());
224        }
225
226        args.push(self.name.clone());
227        args.push(self.command_or_url.clone());
228
229        if !self.server_args.is_empty() {
230            args.push("--".to_string());
231            args.extend(self.server_args.clone());
232        }
233
234        args
235    }
236
237    async fn execute(&self, claude: &Claude) -> Result<CommandOutput> {
238        exec::run_claude(claude, self.args()).await
239    }
240}
241
242/// Add an MCP server using raw JSON configuration.
243#[derive(Debug, Clone)]
244pub struct McpAddJsonCommand {
245    name: String,
246    json: String,
247    scope: Option<Scope>,
248}
249
250impl McpAddJsonCommand {
251    /// Create a new MCP add-json command.
252    #[must_use]
253    pub fn new(name: impl Into<String>, json: impl Into<String>) -> Self {
254        Self {
255            name: name.into(),
256            json: json.into(),
257            scope: None,
258        }
259    }
260
261    /// Set the configuration scope.
262    #[must_use]
263    pub fn scope(mut self, scope: Scope) -> Self {
264        self.scope = Some(scope);
265        self
266    }
267}
268
269impl ClaudeCommand for McpAddJsonCommand {
270    type Output = CommandOutput;
271
272    fn args(&self) -> Vec<String> {
273        let mut args = vec!["mcp".to_string(), "add-json".to_string()];
274
275        if let Some(ref scope) = self.scope {
276            args.push("--scope".to_string());
277            args.push(scope.as_arg().to_string());
278        }
279
280        args.push(self.name.clone());
281        args.push(self.json.clone());
282
283        args
284    }
285
286    async fn execute(&self, claude: &Claude) -> Result<CommandOutput> {
287        exec::run_claude(claude, self.args()).await
288    }
289}
290
291/// Remove an MCP server.
292#[derive(Debug, Clone)]
293pub struct McpRemoveCommand {
294    name: String,
295    scope: Option<Scope>,
296}
297
298impl McpRemoveCommand {
299    /// Creates a command to remove a named MCP server.
300    #[must_use]
301    pub fn new(name: impl Into<String>) -> Self {
302        Self {
303            name: name.into(),
304            scope: None,
305        }
306    }
307
308    /// Set the configuration scope.
309    #[must_use]
310    pub fn scope(mut self, scope: Scope) -> Self {
311        self.scope = Some(scope);
312        self
313    }
314}
315
316impl ClaudeCommand for McpRemoveCommand {
317    type Output = CommandOutput;
318
319    fn args(&self) -> Vec<String> {
320        let mut args = vec!["mcp".to_string(), "remove".to_string()];
321
322        if let Some(ref scope) = self.scope {
323            args.push("--scope".to_string());
324            args.push(scope.as_arg().to_string());
325        }
326
327        args.push(self.name.clone());
328
329        args
330    }
331
332    async fn execute(&self, claude: &Claude) -> Result<CommandOutput> {
333        exec::run_claude(claude, self.args()).await
334    }
335}
336
337/// Import MCP servers from Claude Desktop (Mac and WSL only).
338#[derive(Debug, Clone, Default)]
339pub struct McpAddFromDesktopCommand {
340    scope: Option<Scope>,
341}
342
343impl McpAddFromDesktopCommand {
344    /// Creates a command to import MCP servers from the Claude Desktop config.
345    #[must_use]
346    pub fn new() -> Self {
347        Self::default()
348    }
349
350    /// Set the configuration scope.
351    #[must_use]
352    pub fn scope(mut self, scope: Scope) -> Self {
353        self.scope = Some(scope);
354        self
355    }
356}
357
358impl ClaudeCommand for McpAddFromDesktopCommand {
359    type Output = CommandOutput;
360
361    fn args(&self) -> Vec<String> {
362        let mut args = vec!["mcp".to_string(), "add-from-claude-desktop".to_string()];
363        if let Some(ref scope) = self.scope {
364            args.push("--scope".to_string());
365            args.push(scope.as_arg().to_string());
366        }
367        args
368    }
369
370    async fn execute(&self, claude: &Claude) -> Result<CommandOutput> {
371        exec::run_claude(claude, self.args()).await
372    }
373}
374
375/// Start the Claude Code MCP server.
376///
377/// # Example
378///
379/// ```no_run
380/// use claude_wrapper::{Claude, ClaudeCommand, McpServeCommand};
381///
382/// # async fn example() -> claude_wrapper::Result<()> {
383/// let claude = Claude::builder().build()?;
384/// McpServeCommand::new()
385///     .verbose()
386///     .execute(&claude)
387///     .await?;
388/// # Ok(())
389/// # }
390/// ```
391#[derive(Debug, Clone, Default)]
392pub struct McpServeCommand {
393    debug: bool,
394    verbose: bool,
395}
396
397impl McpServeCommand {
398    /// Create a new MCP serve command.
399    #[must_use]
400    pub fn new() -> Self {
401        Self::default()
402    }
403
404    /// Enable debug output.
405    #[must_use]
406    pub fn debug(mut self) -> Self {
407        self.debug = true;
408        self
409    }
410
411    /// Enable verbose output.
412    #[must_use]
413    pub fn verbose(mut self) -> Self {
414        self.verbose = true;
415        self
416    }
417}
418
419impl ClaudeCommand for McpServeCommand {
420    type Output = CommandOutput;
421
422    fn args(&self) -> Vec<String> {
423        let mut args = vec!["mcp".to_string(), "serve".to_string()];
424        if self.debug {
425            args.push("--debug".to_string());
426        }
427        if self.verbose {
428            args.push("--verbose".to_string());
429        }
430        args
431    }
432
433    async fn execute(&self, claude: &Claude) -> Result<CommandOutput> {
434        exec::run_claude(claude, self.args()).await
435    }
436}
437
438/// Reset all approved and rejected project-scoped MCP servers.
439#[derive(Debug, Clone, Default)]
440pub struct McpResetProjectChoicesCommand;
441
442impl McpResetProjectChoicesCommand {
443    #[must_use]
444    pub fn new() -> Self {
445        Self
446    }
447}
448
449impl ClaudeCommand for McpResetProjectChoicesCommand {
450    type Output = CommandOutput;
451
452    fn args(&self) -> Vec<String> {
453        vec!["mcp".to_string(), "reset-project-choices".to_string()]
454    }
455
456    async fn execute(&self, claude: &Claude) -> Result<CommandOutput> {
457        exec::run_claude(claude, self.args()).await
458    }
459}
460
461#[cfg(test)]
462mod tests {
463    use super::*;
464
465    #[test]
466    fn test_mcp_list_args() {
467        let cmd = McpListCommand::new();
468        assert_eq!(cmd.args(), vec!["mcp", "list"]);
469    }
470
471    #[test]
472    fn test_mcp_get_args() {
473        let cmd = McpGetCommand::new("my-server");
474        assert_eq!(cmd.args(), vec!["mcp", "get", "my-server"]);
475    }
476
477    #[test]
478    fn test_mcp_add_http() {
479        let cmd = McpAddCommand::new("sentry", "https://mcp.sentry.dev/mcp")
480            .transport("http")
481            .scope(Scope::User);
482
483        let args = cmd.args();
484        assert_eq!(
485            args,
486            vec![
487                "mcp",
488                "add",
489                "--transport",
490                "http",
491                "--scope",
492                "user",
493                "sentry",
494                "https://mcp.sentry.dev/mcp"
495            ]
496        );
497    }
498
499    #[test]
500    fn test_mcp_add_stdio_with_env() {
501        let cmd = McpAddCommand::new("my-server", "npx")
502            .env("API_KEY", "xxx")
503            .server_args(["my-mcp-server"]);
504
505        let args = cmd.args();
506        assert_eq!(
507            args,
508            vec![
509                "mcp",
510                "add",
511                "-e",
512                "API_KEY=xxx",
513                "my-server",
514                "npx",
515                "--",
516                "my-mcp-server"
517            ]
518        );
519    }
520
521    #[test]
522    fn test_mcp_add_oauth_flags() {
523        let cmd = McpAddCommand::new("my-server", "https://example.com/mcp")
524            .transport("http")
525            .callback_port(8080)
526            .client_id("my-app-id")
527            .client_secret();
528
529        let args = cmd.args();
530        assert_eq!(
531            args,
532            vec![
533                "mcp",
534                "add",
535                "--transport",
536                "http",
537                "--callback-port",
538                "8080",
539                "--client-id",
540                "my-app-id",
541                "--client-secret",
542                "my-server",
543                "https://example.com/mcp"
544            ]
545        );
546    }
547
548    #[test]
549    fn test_mcp_remove_args() {
550        let cmd = McpRemoveCommand::new("old-server").scope(Scope::Project);
551        assert_eq!(
552            cmd.args(),
553            vec!["mcp", "remove", "--scope", "project", "old-server"]
554        );
555    }
556
557    #[test]
558    fn test_mcp_add_from_desktop() {
559        let cmd = McpAddFromDesktopCommand::new().scope(Scope::User);
560        assert_eq!(
561            cmd.args(),
562            vec!["mcp", "add-from-claude-desktop", "--scope", "user"]
563        );
564    }
565
566    #[test]
567    fn test_mcp_reset_project_choices() {
568        let cmd = McpResetProjectChoicesCommand::new();
569        assert_eq!(cmd.args(), vec!["mcp", "reset-project-choices"]);
570    }
571
572    #[test]
573    fn test_mcp_serve_default() {
574        let cmd = McpServeCommand::new();
575        assert_eq!(cmd.args(), vec!["mcp", "serve"]);
576    }
577
578    #[test]
579    fn test_mcp_serve_with_flags() {
580        let cmd = McpServeCommand::new().debug().verbose();
581        assert_eq!(cmd.args(), vec!["mcp", "serve", "--debug", "--verbose"]);
582    }
583}