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 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 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 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 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 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 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 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 .arg("mcp")
205 .arg("login")
206 .arg(name)
207 .stdout(std::process::Stdio::piped())
208 .stderr(std::process::Stdio::piped())
209 .kill_on_drop(true);
210
211 if !request.scopes.is_empty() {
212 command.arg("--scopes").arg(request.scopes.join(","));
213 }
214
215 apply_cli_overrides(&mut command, &resolved_overrides, true);
216 self.command_env.apply(&mut command)?;
217
218 spawn_with_retry(&mut command, self.command_env.binary_path())
219 }
220}