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}