Skip to content

Commit 350b00d

Browse files
Added MCP server command to enable authentication using ChatGPT (#2373)
This PR adds two new APIs for the MCP server: 1) loginChatGpt, and 2) cancelLoginChatGpt. The first starts a login server and returns a local URL that allows for browser-based authentication, and the second provides a way to cancel the login attempt. If the login attempt succeeds, a notification (in the form of an event) is sent to a subscriber. I also added a timeout mechanism for the existing login server. The loginChatGpt code path uses a 10-minute timeout by default, so if the user fails to complete the login flow in that timeframe, the login server automatically shuts down. I tested the timeout code by manually setting the timeout to a much lower number and confirming that it works as expected when used e2e.
1 parent 1930ee7 commit 350b00d

File tree

7 files changed

+290
-5
lines changed

7 files changed

+290
-5
lines changed

codex-rs/Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

codex-rs/login/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ use std::time::Duration;
1818

1919
pub use crate::server::LoginServer;
2020
pub use crate::server::ServerOptions;
21+
pub use crate::server::ShutdownHandle;
2122
pub use crate::server::run_login_server;
2223
pub use crate::token_data::TokenData;
2324
use crate::token_data::parse_id_token;

codex-rs/login/src/server.rs

Lines changed: 93 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@ use std::path::PathBuf;
44
use std::sync::Arc;
55
use std::sync::atomic::AtomicBool;
66
use std::sync::atomic::Ordering;
7+
use std::sync::mpsc;
78
use std::thread;
9+
use std::time::Duration;
810

911
use crate::AuthDotJson;
1012
use crate::get_auth_file;
@@ -27,6 +29,7 @@ pub struct ServerOptions {
2729
pub port: u16,
2830
pub open_browser: bool,
2931
pub force_state: Option<String>,
32+
pub login_timeout: Option<Duration>,
3033
}
3134

3235
impl ServerOptions {
@@ -38,16 +41,17 @@ impl ServerOptions {
3841
port: DEFAULT_PORT,
3942
open_browser: true,
4043
force_state: None,
44+
login_timeout: None,
4145
}
4246
}
4347
}
4448

45-
#[derive(Debug)]
4649
pub struct LoginServer {
4750
pub auth_url: String,
4851
pub actual_port: u16,
4952
pub server_handle: thread::JoinHandle<io::Result<()>>,
5053
pub shutdown_flag: Arc<AtomicBool>,
54+
pub server: Arc<Server>,
5155
}
5256

5357
impl LoginServer {
@@ -59,10 +63,34 @@ impl LoginServer {
5963
}
6064

6165
pub fn cancel(&self) {
62-
self.shutdown_flag.store(true, Ordering::SeqCst);
66+
shutdown(&self.shutdown_flag, &self.server);
67+
}
68+
69+
pub fn cancel_handle(&self) -> ShutdownHandle {
70+
ShutdownHandle {
71+
shutdown_flag: self.shutdown_flag.clone(),
72+
server: self.server.clone(),
73+
}
74+
}
75+
}
76+
77+
#[derive(Clone)]
78+
pub struct ShutdownHandle {
79+
shutdown_flag: Arc<AtomicBool>,
80+
server: Arc<Server>,
81+
}
82+
83+
impl ShutdownHandle {
84+
pub fn cancel(&self) {
85+
shutdown(&self.shutdown_flag, &self.server);
6386
}
6487
}
6588

89+
pub fn shutdown(shutdown_flag: &AtomicBool, server: &Server) {
90+
shutdown_flag.store(true, Ordering::SeqCst);
91+
server.unblock();
92+
}
93+
6694
pub fn run_login_server(
6795
opts: ServerOptions,
6896
shutdown_flag: Option<Arc<AtomicBool>>,
@@ -80,6 +108,7 @@ pub fn run_login_server(
80108
));
81109
}
82110
};
111+
let server = Arc::new(server);
83112

84113
let redirect_uri = format!("http://localhost:{actual_port}/auth/callback");
85114
let auth_url = build_authorize_url(&opts.issuer, &opts.client_id, &redirect_uri, &pkce, &state);
@@ -89,11 +118,35 @@ pub fn run_login_server(
89118
}
90119
let shutdown_flag = shutdown_flag.unwrap_or_else(|| Arc::new(AtomicBool::new(false)));
91120
let shutdown_flag_clone = shutdown_flag.clone();
121+
let timeout_flag = Arc::new(AtomicBool::new(false));
122+
123+
// Channel used to signal completion to timeout watcher.
124+
let (done_tx, done_rx) = mpsc::channel::<()>();
125+
126+
if let Some(timeout) = opts.login_timeout {
127+
spawn_timeout_watcher(
128+
done_rx,
129+
timeout,
130+
shutdown_flag.clone(),
131+
timeout_flag.clone(),
132+
server.clone(),
133+
);
134+
}
135+
136+
let server_for_thread = server.clone();
92137
let server_handle = thread::spawn(move || {
93138
while !shutdown_flag.load(Ordering::SeqCst) {
94-
let req = match server.recv() {
139+
let req = match server_for_thread.recv() {
95140
Ok(r) => r,
96-
Err(e) => return Err(io::Error::other(e)),
141+
Err(e) => {
142+
// If we've been asked to shut down, break gracefully so that
143+
// we can report timeout or cancellation status uniformly.
144+
if shutdown_flag.load(Ordering::SeqCst) {
145+
break;
146+
} else {
147+
return Err(io::Error::other(e));
148+
}
149+
}
97150
};
98151

99152
let url_raw = req.url().to_string();
@@ -198,24 +251,59 @@ pub fn run_login_server(
198251
}
199252
let _ = req.respond(resp);
200253
shutdown_flag.store(true, Ordering::SeqCst);
254+
255+
// Login has succeeded, so disarm the timeout watcher.
256+
let _ = done_tx.send(());
201257
return Ok(());
202258
}
203259
_ => {
204260
let _ = req.respond(Response::from_string("Not Found").with_status_code(404));
205261
}
206262
}
207263
}
208-
Err(io::Error::other("Login flow was not completed"))
264+
265+
// Login has failed or timed out, so disarm the timeout watcher.
266+
let _ = done_tx.send(());
267+
268+
if timeout_flag.load(Ordering::SeqCst) {
269+
Err(io::Error::other("Login timed out"))
270+
} else {
271+
Err(io::Error::other("Login was not completed"))
272+
}
209273
});
210274

211275
Ok(LoginServer {
212276
auth_url: auth_url.clone(),
213277
actual_port,
214278
server_handle,
215279
shutdown_flag: shutdown_flag_clone,
280+
server,
216281
})
217282
}
218283

284+
/// Spawns a detached thread that waits for either a completion signal on `done_rx`
285+
/// or the specified `timeout` to elapse. If the timeout elapses first it marks
286+
/// the `shutdown_flag`, records `timeout_flag`, and unblocks the HTTP server so
287+
/// that the main server loop can exit promptly.
288+
fn spawn_timeout_watcher(
289+
done_rx: mpsc::Receiver<()>,
290+
timeout: Duration,
291+
shutdown_flag: Arc<AtomicBool>,
292+
timeout_flag: Arc<AtomicBool>,
293+
server: Arc<Server>,
294+
) {
295+
thread::spawn(move || {
296+
if done_rx.recv_timeout(timeout).is_err()
297+
&& shutdown_flag
298+
.compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst)
299+
.is_ok()
300+
{
301+
timeout_flag.store(true, Ordering::SeqCst);
302+
server.unblock();
303+
}
304+
});
305+
}
306+
219307
fn build_authorize_url(
220308
issuer: &str,
221309
client_id: &str,

codex-rs/login/tests/login_server_e2e.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ fn end_to_end_login_flow_persists_auth_json() {
100100
port: 0,
101101
open_browser: false,
102102
force_state: Some(state),
103+
login_timeout: None,
103104
};
104105
let server = run_login_server(opts, None).unwrap();
105106
let login_port = server.actual_port;
@@ -158,6 +159,7 @@ fn creates_missing_codex_home_dir() {
158159
port: 0,
159160
open_browser: false,
160161
force_state: Some(state),
162+
login_timeout: None,
161163
};
162164
let server = run_login_server(opts, None).unwrap();
163165
let login_port = server.actual_port;

codex-rs/mcp-server/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ workspace = true
1818
anyhow = "1"
1919
codex-arg0 = { path = "../arg0" }
2020
codex-core = { path = "../core" }
21+
codex-login = { path = "../login" }
2122
mcp-types = { path = "../mcp-types" }
2223
schemars = "0.8.22"
2324
serde = { version = "1", features = ["derive"] }

0 commit comments

Comments
 (0)