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}