Skip to main content

codex_wrapper/command/
mcp.rs

1use crate::Codex;
2use crate::command::CodexCommand;
3use crate::error::{Error, Result};
4use crate::exec::{self, CommandOutput};
5
6#[derive(Debug, Clone, Default)]
7pub struct McpListCommand {
8    config_overrides: Vec<String>,
9    enabled_features: Vec<String>,
10    disabled_features: Vec<String>,
11    json: bool,
12}
13
14impl McpListCommand {
15    #[must_use]
16    pub fn new() -> Self {
17        Self::default()
18    }
19
20    #[must_use]
21    pub fn json(mut self) -> Self {
22        self.json = true;
23        self
24    }
25
26    #[cfg(feature = "json")]
27    pub async fn execute_json(&self, codex: &Codex) -> Result<serde_json::Value> {
28        let mut args = self.args();
29        if !self.json {
30            args.push("--json".into());
31        }
32
33        let output = exec::run_codex(codex, args).await?;
34        serde_json::from_str(&output.stdout).map_err(|source| Error::Json {
35            message: "failed to parse MCP list output".into(),
36            source,
37        })
38    }
39}
40
41impl CodexCommand for McpListCommand {
42    type Output = CommandOutput;
43
44    fn args(&self) -> Vec<String> {
45        let mut args = base_args(
46            "list",
47            &self.config_overrides,
48            &self.enabled_features,
49            &self.disabled_features,
50        );
51        if self.json {
52            args.push("--json".into());
53        }
54        args
55    }
56
57    async fn execute(&self, codex: &Codex) -> Result<CommandOutput> {
58        exec::run_codex(codex, self.args()).await
59    }
60}
61
62#[derive(Debug, Clone, Default)]
63pub struct McpGetCommand {
64    name: String,
65    config_overrides: Vec<String>,
66    enabled_features: Vec<String>,
67    disabled_features: Vec<String>,
68    json: bool,
69}
70
71impl McpGetCommand {
72    #[must_use]
73    pub fn new(name: impl Into<String>) -> Self {
74        Self {
75            name: name.into(),
76            ..Default::default()
77        }
78    }
79
80    #[must_use]
81    pub fn json(mut self) -> Self {
82        self.json = true;
83        self
84    }
85
86    #[cfg(feature = "json")]
87    pub async fn execute_json(&self, codex: &Codex) -> Result<serde_json::Value> {
88        let mut args = self.args();
89        if !self.json {
90            args.push("--json".into());
91        }
92        let output = exec::run_codex(codex, args).await?;
93        serde_json::from_str(&output.stdout).map_err(|source| Error::Json {
94            message: "failed to parse MCP server output".into(),
95            source,
96        })
97    }
98}
99
100impl CodexCommand for McpGetCommand {
101    type Output = CommandOutput;
102
103    fn args(&self) -> Vec<String> {
104        let mut args = base_args(
105            "get",
106            &self.config_overrides,
107            &self.enabled_features,
108            &self.disabled_features,
109        );
110        if self.json {
111            args.push("--json".into());
112        }
113        args.push(self.name.clone());
114        args
115    }
116
117    async fn execute(&self, codex: &Codex) -> Result<CommandOutput> {
118        exec::run_codex(codex, self.args()).await
119    }
120}
121
122#[derive(Debug, Clone)]
123enum McpAddTransport {
124    Stdio {
125        command: String,
126        args: Vec<String>,
127        env: Vec<String>,
128    },
129    Http {
130        url: String,
131        bearer_token_env_var: Option<String>,
132    },
133}
134
135#[derive(Debug, Clone)]
136pub struct McpAddCommand {
137    name: String,
138    config_overrides: Vec<String>,
139    enabled_features: Vec<String>,
140    disabled_features: Vec<String>,
141    transport: McpAddTransport,
142}
143
144impl McpAddCommand {
145    #[must_use]
146    pub fn stdio(name: impl Into<String>, command: impl Into<String>) -> Self {
147        Self {
148            name: name.into(),
149            config_overrides: Vec::new(),
150            enabled_features: Vec::new(),
151            disabled_features: Vec::new(),
152            transport: McpAddTransport::Stdio {
153                command: command.into(),
154                args: Vec::new(),
155                env: Vec::new(),
156            },
157        }
158    }
159
160    #[must_use]
161    pub fn http(name: impl Into<String>, url: impl Into<String>) -> Self {
162        Self {
163            name: name.into(),
164            config_overrides: Vec::new(),
165            enabled_features: Vec::new(),
166            disabled_features: Vec::new(),
167            transport: McpAddTransport::Http {
168                url: url.into(),
169                bearer_token_env_var: None,
170            },
171        }
172    }
173
174    #[must_use]
175    pub fn arg(mut self, value: impl Into<String>) -> Self {
176        if let McpAddTransport::Stdio { args, .. } = &mut self.transport {
177            args.push(value.into());
178        }
179        self
180    }
181
182    #[must_use]
183    pub fn env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
184        if let McpAddTransport::Stdio { env, .. } = &mut self.transport {
185            env.push(format!("{}={}", key.into(), value.into()));
186        }
187        self
188    }
189
190    #[must_use]
191    pub fn bearer_token_env_var(mut self, env_var: impl Into<String>) -> Self {
192        if let McpAddTransport::Http {
193            bearer_token_env_var,
194            ..
195        } = &mut self.transport
196        {
197            *bearer_token_env_var = Some(env_var.into());
198        }
199        self
200    }
201}
202
203impl CodexCommand for McpAddCommand {
204    type Output = CommandOutput;
205
206    fn args(&self) -> Vec<String> {
207        let mut args = base_args(
208            "add",
209            &self.config_overrides,
210            &self.enabled_features,
211            &self.disabled_features,
212        );
213        args.push(self.name.clone());
214        match &self.transport {
215            McpAddTransport::Stdio {
216                command,
217                args: command_args,
218                env,
219            } => {
220                for entry in env {
221                    args.push("--env".into());
222                    args.push(entry.clone());
223                }
224                args.push("--".into());
225                args.push(command.clone());
226                args.extend(command_args.clone());
227            }
228            McpAddTransport::Http {
229                url,
230                bearer_token_env_var,
231            } => {
232                args.push("--url".into());
233                args.push(url.clone());
234                if let Some(env_var) = bearer_token_env_var {
235                    args.push("--bearer-token-env-var".into());
236                    args.push(env_var.clone());
237                }
238            }
239        }
240        args
241    }
242
243    async fn execute(&self, codex: &Codex) -> Result<CommandOutput> {
244        exec::run_codex(codex, self.args()).await
245    }
246}
247
248#[derive(Debug, Clone)]
249pub struct McpRemoveCommand {
250    name: String,
251}
252
253impl McpRemoveCommand {
254    #[must_use]
255    pub fn new(name: impl Into<String>) -> Self {
256        Self { name: name.into() }
257    }
258}
259
260impl CodexCommand for McpRemoveCommand {
261    type Output = CommandOutput;
262
263    fn args(&self) -> Vec<String> {
264        vec!["mcp".into(), "remove".into(), self.name.clone()]
265    }
266
267    async fn execute(&self, codex: &Codex) -> Result<CommandOutput> {
268        exec::run_codex(codex, self.args()).await
269    }
270}
271
272#[derive(Debug, Clone)]
273pub struct McpLoginCommand {
274    name: String,
275    scopes: Option<String>,
276}
277
278impl McpLoginCommand {
279    #[must_use]
280    pub fn new(name: impl Into<String>) -> Self {
281        Self {
282            name: name.into(),
283            scopes: None,
284        }
285    }
286
287    #[must_use]
288    pub fn scopes(mut self, scopes: impl Into<String>) -> Self {
289        self.scopes = Some(scopes.into());
290        self
291    }
292}
293
294impl CodexCommand for McpLoginCommand {
295    type Output = CommandOutput;
296
297    fn args(&self) -> Vec<String> {
298        let mut args = vec!["mcp".into(), "login".into()];
299        if let Some(scopes) = &self.scopes {
300            args.push("--scopes".into());
301            args.push(scopes.clone());
302        }
303        args.push(self.name.clone());
304        args
305    }
306
307    async fn execute(&self, codex: &Codex) -> Result<CommandOutput> {
308        exec::run_codex(codex, self.args()).await
309    }
310}
311
312#[derive(Debug, Clone)]
313pub struct McpLogoutCommand {
314    name: String,
315}
316
317impl McpLogoutCommand {
318    #[must_use]
319    pub fn new(name: impl Into<String>) -> Self {
320        Self { name: name.into() }
321    }
322}
323
324impl CodexCommand for McpLogoutCommand {
325    type Output = CommandOutput;
326
327    fn args(&self) -> Vec<String> {
328        vec!["mcp".into(), "logout".into(), self.name.clone()]
329    }
330
331    async fn execute(&self, codex: &Codex) -> Result<CommandOutput> {
332        exec::run_codex(codex, self.args()).await
333    }
334}
335
336fn base_args(
337    subcommand: &str,
338    configs: &[String],
339    enabled: &[String],
340    disabled: &[String],
341) -> Vec<String> {
342    let mut args = vec!["mcp".into(), subcommand.into()];
343    for value in configs {
344        args.push("-c".into());
345        args.push(value.clone());
346    }
347    for value in enabled {
348        args.push("--enable".into());
349        args.push(value.clone());
350    }
351    for value in disabled {
352        args.push("--disable".into());
353        args.push(value.clone());
354    }
355    args
356}
357
358#[cfg(test)]
359mod tests {
360    use super::*;
361
362    #[test]
363    fn mcp_list_args() {
364        assert_eq!(
365            McpListCommand::new().json().args(),
366            vec!["mcp", "list", "--json"]
367        );
368    }
369
370    #[test]
371    fn mcp_stdio_add_args() {
372        let args = McpAddCommand::stdio("server", "uvx")
373            .arg("my-server")
374            .env("API_KEY", "secret")
375            .args();
376        assert_eq!(
377            args,
378            vec![
379                "mcp",
380                "add",
381                "server",
382                "--env",
383                "API_KEY=secret",
384                "--",
385                "uvx",
386                "my-server",
387            ]
388        );
389    }
390
391    #[test]
392    fn mcp_http_add_args() {
393        let args = McpAddCommand::http("server", "https://example.com/mcp")
394            .bearer_token_env_var("TOKEN")
395            .args();
396        assert_eq!(
397            args,
398            vec![
399                "mcp",
400                "add",
401                "server",
402                "--url",
403                "https://example.com/mcp",
404                "--bearer-token-env-var",
405                "TOKEN",
406            ]
407        );
408    }
409}