Skip to main content

codex/commands/
mcp.rs

1use std::ffi::OsString;
2
3use tokio::process::Command;
4
5use crate::{
6    builder::{apply_cli_overrides, resolve_cli_overrides},
7    process::spawn_with_retry,
8    ApplyDiffArtifacts, CodexClient, CodexError, McpAddRequest, McpAddTransport, McpGetRequest,
9    McpListOutput, McpListRequest, McpLogoutRequest, McpOauthLoginRequest, McpOverviewRequest,
10    McpRemoveRequest,
11};
12
13impl CodexClient {
14    /// Runs `codex mcp --help` and returns captured output.
15    pub async fn mcp_overview(
16        &self,
17        request: McpOverviewRequest,
18    ) -> Result<ApplyDiffArtifacts, CodexError> {
19        self.run_simple_command_with_overrides(
20            vec![OsString::from("mcp"), OsString::from("--help")],
21            request.overrides,
22        )
23        .await
24    }
25
26    /// Lists configured MCP servers via `codex mcp list`.
27    pub async fn mcp_list(&self, request: McpListRequest) -> Result<McpListOutput, CodexError> {
28        let McpListRequest { json, overrides } = request;
29        let mut args = vec![OsString::from("mcp"), OsString::from("list")];
30        if json {
31            args.push(OsString::from("--json"));
32        }
33
34        let artifacts = self
35            .run_simple_command_with_overrides(args, overrides)
36            .await?;
37        let parsed = if json {
38            Some(serde_json::from_str(&artifacts.stdout).map_err(|source| {
39                CodexError::JsonParse {
40                    context: "mcp list",
41                    stdout: artifacts.stdout.clone(),
42                    source,
43                }
44            })?)
45        } else {
46            None
47        };
48
49        Ok(McpListOutput {
50            status: artifacts.status,
51            stdout: artifacts.stdout,
52            stderr: artifacts.stderr,
53            json: parsed,
54        })
55    }
56
57    /// Gets a configured MCP server entry via `codex mcp get <NAME>`.
58    pub async fn mcp_get(&self, request: McpGetRequest) -> Result<McpListOutput, CodexError> {
59        let name = request.name.trim();
60        if name.is_empty() {
61            return Err(CodexError::EmptyMcpServerName);
62        }
63
64        let mut args = vec![OsString::from("mcp"), OsString::from("get")];
65        if request.json {
66            args.push(OsString::from("--json"));
67        }
68        args.push(OsString::from(name));
69
70        let artifacts = self
71            .run_simple_command_with_overrides(args, request.overrides)
72            .await?;
73        let parsed = if request.json {
74            Some(serde_json::from_str(&artifacts.stdout).map_err(|source| {
75                CodexError::JsonParse {
76                    context: "mcp get",
77                    stdout: artifacts.stdout.clone(),
78                    source,
79                }
80            })?)
81        } else {
82            None
83        };
84
85        Ok(McpListOutput {
86            status: artifacts.status,
87            stdout: artifacts.stdout,
88            stderr: artifacts.stderr,
89            json: parsed,
90        })
91    }
92
93    /// Adds an MCP server configuration entry via `codex mcp add`.
94    pub async fn mcp_add(&self, request: McpAddRequest) -> Result<ApplyDiffArtifacts, CodexError> {
95        let name = request.name.trim();
96        if name.is_empty() {
97            return Err(CodexError::EmptyMcpServerName);
98        }
99
100        let mut args = vec![
101            OsString::from("mcp"),
102            OsString::from("add"),
103            OsString::from(name),
104        ];
105        match request.transport {
106            McpAddTransport::StreamableHttp {
107                url,
108                bearer_token_env_var,
109            } => {
110                let url = url.trim();
111                if url.is_empty() {
112                    return Err(CodexError::EmptyMcpUrl);
113                }
114                args.push(OsString::from("--url"));
115                args.push(OsString::from(url));
116                if let Some(env_var) = bearer_token_env_var {
117                    if !env_var.trim().is_empty() {
118                        args.push(OsString::from("--bearer-token-env-var"));
119                        args.push(OsString::from(env_var));
120                    }
121                }
122            }
123            McpAddTransport::Stdio { env, command } => {
124                if command.is_empty() {
125                    return Err(CodexError::EmptyMcpCommand);
126                }
127                for (key, value) in env {
128                    let key = key.trim();
129                    if key.is_empty() {
130                        continue;
131                    }
132                    args.push(OsString::from("--env"));
133                    args.push(OsString::from(format!("{key}={value}")));
134                }
135                args.push(OsString::from("--"));
136                args.extend(command);
137            }
138        }
139
140        self.run_simple_command_with_overrides(args, request.overrides)
141            .await
142    }
143
144    /// Removes an MCP server configuration entry via `codex mcp remove <NAME>`.
145    pub async fn mcp_remove(
146        &self,
147        request: McpRemoveRequest,
148    ) -> Result<ApplyDiffArtifacts, CodexError> {
149        let name = request.name.trim();
150        if name.is_empty() {
151            return Err(CodexError::EmptyMcpServerName);
152        }
153
154        self.run_simple_command_with_overrides(
155            vec![
156                OsString::from("mcp"),
157                OsString::from("remove"),
158                OsString::from(name),
159            ],
160            request.overrides,
161        )
162        .await
163    }
164
165    /// Deauthenticates from an MCP server via `codex mcp logout <NAME>`.
166    pub async fn mcp_logout(
167        &self,
168        request: McpLogoutRequest,
169    ) -> Result<ApplyDiffArtifacts, CodexError> {
170        let name = request.name.trim();
171        if name.is_empty() {
172            return Err(CodexError::EmptyMcpServerName);
173        }
174
175        self.run_simple_command_with_overrides(
176            vec![
177                OsString::from("mcp"),
178                OsString::from("logout"),
179                OsString::from(name),
180            ],
181            request.overrides,
182        )
183        .await
184    }
185
186    /// Spawns `codex mcp login <NAME> [--scopes ...]`.
187    pub fn spawn_mcp_oauth_login_process(
188        &self,
189        request: McpOauthLoginRequest,
190    ) -> Result<tokio::process::Child, CodexError> {
191        let name = request.name.trim();
192        if name.is_empty() {
193            return Err(CodexError::EmptyMcpServerName);
194        }
195
196        let resolved_overrides = resolve_cli_overrides(
197            &self.cli_overrides,
198            &request.overrides,
199            self.model.as_deref(),
200        );
201
202        let mut command = Command::new(self.command_env.binary_path());
203        command
204            .stdout(std::process::Stdio::piped())
205            .stderr(std::process::Stdio::piped())
206            .kill_on_drop(true);
207
208        apply_cli_overrides(&mut command, &resolved_overrides, true);
209        command.arg("mcp").arg("login").arg(name);
210
211        if !request.scopes.is_empty() {
212            command.arg("--scopes").arg(request.scopes.join(","));
213        }
214
215        self.command_env.apply(&mut command)?;
216
217        spawn_with_retry(&mut command, self.command_env.binary_path())
218    }
219}