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 oauth_client_id: Option<String>,
135 oauth_resource: Option<String>,
136 },
137}
138
139#[derive(Debug, Clone)]
140pub struct McpAddCommand {
141 name: String,
142 config_overrides: Vec<String>,
143 enabled_features: Vec<String>,
144 disabled_features: Vec<String>,
145 transport: McpAddTransport,
146}
147
148impl McpAddCommand {
149 #[must_use]
150 pub fn stdio(name: impl Into<String>, command: impl Into<String>) -> Self {
151 Self {
152 name: name.into(),
153 config_overrides: Vec::new(),
154 enabled_features: Vec::new(),
155 disabled_features: Vec::new(),
156 transport: McpAddTransport::Stdio {
157 command: command.into(),
158 args: Vec::new(),
159 env: Vec::new(),
160 },
161 }
162 }
163
164 #[must_use]
165 pub fn http(name: impl Into<String>, url: impl Into<String>) -> Self {
166 Self {
167 name: name.into(),
168 config_overrides: Vec::new(),
169 enabled_features: Vec::new(),
170 disabled_features: Vec::new(),
171 transport: McpAddTransport::Http {
172 url: url.into(),
173 bearer_token_env_var: None,
174 oauth_client_id: None,
175 oauth_resource: None,
176 },
177 }
178 }
179
180 #[must_use]
182 pub fn config(mut self, key_value: impl Into<String>) -> Self {
183 self.config_overrides.push(key_value.into());
184 self
185 }
186
187 #[must_use]
189 pub fn enable(mut self, feature: impl Into<String>) -> Self {
190 self.enabled_features.push(feature.into());
191 self
192 }
193
194 #[must_use]
196 pub fn disable(mut self, feature: impl Into<String>) -> Self {
197 self.disabled_features.push(feature.into());
198 self
199 }
200
201 #[must_use]
202 pub fn arg(mut self, value: impl Into<String>) -> Self {
203 if let McpAddTransport::Stdio { args, .. } = &mut self.transport {
204 args.push(value.into());
205 }
206 self
207 }
208
209 #[must_use]
210 pub fn env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
211 if let McpAddTransport::Stdio { env, .. } = &mut self.transport {
212 env.push(format!("{}={}", key.into(), value.into()));
213 }
214 self
215 }
216
217 #[must_use]
218 pub fn bearer_token_env_var(mut self, env_var: impl Into<String>) -> Self {
219 if let McpAddTransport::Http {
220 bearer_token_env_var,
221 ..
222 } = &mut self.transport
223 {
224 *bearer_token_env_var = Some(env_var.into());
225 }
226 self
227 }
228
229 #[must_use]
233 pub fn oauth_client_id(mut self, client_id: impl Into<String>) -> Self {
234 if let McpAddTransport::Http {
235 oauth_client_id, ..
236 } = &mut self.transport
237 {
238 *oauth_client_id = Some(client_id.into());
239 }
240 self
241 }
242
243 #[must_use]
247 pub fn oauth_resource(mut self, resource: impl Into<String>) -> Self {
248 if let McpAddTransport::Http { oauth_resource, .. } = &mut self.transport {
249 *oauth_resource = Some(resource.into());
250 }
251 self
252 }
253}
254
255impl CodexCommand for McpAddCommand {
256 type Output = CommandOutput;
257
258 fn args(&self) -> Vec<String> {
259 let mut args = base_args(
260 "add",
261 &self.config_overrides,
262 &self.enabled_features,
263 &self.disabled_features,
264 );
265 args.push(self.name.clone());
266 match &self.transport {
267 McpAddTransport::Stdio {
268 command,
269 args: command_args,
270 env,
271 } => {
272 for entry in env {
273 args.push("--env".into());
274 args.push(entry.clone());
275 }
276 args.push("--".into());
277 args.push(command.clone());
278 args.extend(command_args.clone());
279 }
280 McpAddTransport::Http {
281 url,
282 bearer_token_env_var,
283 oauth_client_id,
284 oauth_resource,
285 } => {
286 args.push("--url".into());
287 args.push(url.clone());
288 if let Some(env_var) = bearer_token_env_var {
289 args.push("--bearer-token-env-var".into());
290 args.push(env_var.clone());
291 }
292 if let Some(client_id) = oauth_client_id {
293 args.push("--oauth-client-id".into());
294 args.push(client_id.clone());
295 }
296 if let Some(resource) = oauth_resource {
297 args.push("--oauth-resource".into());
298 args.push(resource.clone());
299 }
300 }
301 }
302 args
303 }
304
305 async fn execute(&self, codex: &Codex) -> Result<CommandOutput> {
306 exec::run_codex(codex, self.args()).await
307 }
308}
309
310#[derive(Debug, Clone)]
311pub struct McpRemoveCommand {
312 name: String,
313}
314
315impl McpRemoveCommand {
316 #[must_use]
317 pub fn new(name: impl Into<String>) -> Self {
318 Self { name: name.into() }
319 }
320}
321
322impl CodexCommand for McpRemoveCommand {
323 type Output = CommandOutput;
324
325 fn args(&self) -> Vec<String> {
326 vec!["mcp".into(), "remove".into(), self.name.clone()]
327 }
328
329 async fn execute(&self, codex: &Codex) -> Result<CommandOutput> {
330 exec::run_codex(codex, self.args()).await
331 }
332}
333
334#[derive(Debug, Clone)]
335pub struct McpLoginCommand {
336 name: String,
337 scopes: Option<String>,
338}
339
340impl McpLoginCommand {
341 #[must_use]
342 pub fn new(name: impl Into<String>) -> Self {
343 Self {
344 name: name.into(),
345 scopes: None,
346 }
347 }
348
349 #[must_use]
350 pub fn scopes(mut self, scopes: impl Into<String>) -> Self {
351 self.scopes = Some(scopes.into());
352 self
353 }
354}
355
356impl CodexCommand for McpLoginCommand {
357 type Output = CommandOutput;
358
359 fn args(&self) -> Vec<String> {
360 let mut args = vec!["mcp".into(), "login".into()];
361 if let Some(scopes) = &self.scopes {
362 args.push("--scopes".into());
363 args.push(scopes.clone());
364 }
365 args.push(self.name.clone());
366 args
367 }
368
369 async fn execute(&self, codex: &Codex) -> Result<CommandOutput> {
370 exec::run_codex(codex, self.args()).await
371 }
372}
373
374#[derive(Debug, Clone)]
375pub struct McpLogoutCommand {
376 name: String,
377}
378
379impl McpLogoutCommand {
380 #[must_use]
381 pub fn new(name: impl Into<String>) -> Self {
382 Self { name: name.into() }
383 }
384}
385
386impl CodexCommand for McpLogoutCommand {
387 type Output = CommandOutput;
388
389 fn args(&self) -> Vec<String> {
390 vec!["mcp".into(), "logout".into(), self.name.clone()]
391 }
392
393 async fn execute(&self, codex: &Codex) -> Result<CommandOutput> {
394 exec::run_codex(codex, self.args()).await
395 }
396}
397
398fn base_args(
399 subcommand: &str,
400 configs: &[String],
401 enabled: &[String],
402 disabled: &[String],
403) -> Vec<String> {
404 let mut args = vec!["mcp".into(), subcommand.into()];
405 for value in configs {
406 args.push("-c".into());
407 args.push(value.clone());
408 }
409 for value in enabled {
410 args.push("--enable".into());
411 args.push(value.clone());
412 }
413 for value in disabled {
414 args.push("--disable".into());
415 args.push(value.clone());
416 }
417 args
418}
419
420#[cfg(test)]
421mod tests {
422 use super::*;
423
424 #[test]
425 fn mcp_list_args() {
426 assert_eq!(
427 McpListCommand::new().json().args(),
428 vec!["mcp", "list", "--json"]
429 );
430 }
431
432 #[test]
433 fn mcp_stdio_add_args() {
434 let args = McpAddCommand::stdio("server", "uvx")
435 .arg("my-server")
436 .env("API_KEY", "secret")
437 .args();
438 assert_eq!(
439 args,
440 vec![
441 "mcp",
442 "add",
443 "server",
444 "--env",
445 "API_KEY=secret",
446 "--",
447 "uvx",
448 "my-server",
449 ]
450 );
451 }
452
453 #[test]
454 fn mcp_http_add_args() {
455 let args = McpAddCommand::http("server", "https://example.com/mcp")
456 .bearer_token_env_var("TOKEN")
457 .args();
458 assert_eq!(
459 args,
460 vec![
461 "mcp",
462 "add",
463 "server",
464 "--url",
465 "https://example.com/mcp",
466 "--bearer-token-env-var",
467 "TOKEN",
468 ]
469 );
470 }
471
472 #[test]
473 fn mcp_http_add_oauth_and_config_args() {
474 let args = McpAddCommand::http("server", "https://example.com/mcp")
475 .config("foo=bar")
476 .enable("beta")
477 .oauth_client_id("client-123")
478 .oauth_resource("https://api.example.com")
479 .args();
480 assert_eq!(
481 args,
482 vec![
483 "mcp",
484 "add",
485 "-c",
486 "foo=bar",
487 "--enable",
488 "beta",
489 "server",
490 "--url",
491 "https://example.com/mcp",
492 "--oauth-client-id",
493 "client-123",
494 "--oauth-resource",
495 "https://api.example.com",
496 ]
497 );
498 }
499
500 #[test]
501 fn mcp_stdio_add_oauth_is_noop() {
502 let args = McpAddCommand::stdio("server", "uvx")
504 .oauth_client_id("ignored")
505 .args();
506 assert_eq!(args, vec!["mcp", "add", "server", "--", "uvx"]);
507 }
508}