Skip to main content

codex_wrapper/command/
mcp.rs

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