mirror of
https://github.com/openai/codex.git
synced 2026-04-24 22:54:54 +00:00
This PR adds the following: * A getAuthStatus method on the mcp server. This returns the auth method currently in use (chatgpt or apikey) or none if the user is not authenticated. It also returns the "preferred auth method" which reflects the `preferred_auth_method` value in the config. * A logout method on the mcp server. If called, it logs out the user and deletes the `auth.json` file — the same behavior in the cli's `/logout` command. * An `authStatusChange` event notification that is sent when the auth status changes due to successful login or logout operations. * Logic to pass command-line config overrides to the mcp server at startup time. This allows use cases like `codex mcp -c preferred_auth_method=apikey`.
157 lines
5.7 KiB
Rust
157 lines
5.7 KiB
Rust
//! Prototype MCP server.
|
||
#![deny(clippy::print_stdout, clippy::print_stderr)]
|
||
|
||
use std::io::ErrorKind;
|
||
use std::io::Result as IoResult;
|
||
use std::path::PathBuf;
|
||
|
||
use codex_common::CliConfigOverrides;
|
||
use codex_core::config::Config;
|
||
use codex_core::config::ConfigOverrides;
|
||
|
||
use mcp_types::JSONRPCMessage;
|
||
use tokio::io::AsyncBufReadExt;
|
||
use tokio::io::AsyncWriteExt;
|
||
use tokio::io::BufReader;
|
||
use tokio::io::{self};
|
||
use tokio::sync::mpsc;
|
||
use tracing::debug;
|
||
use tracing::error;
|
||
use tracing::info;
|
||
use tracing_subscriber::EnvFilter;
|
||
|
||
mod codex_message_processor;
|
||
mod codex_tool_config;
|
||
mod codex_tool_runner;
|
||
mod error_code;
|
||
mod exec_approval;
|
||
mod json_to_toml;
|
||
pub(crate) mod message_processor;
|
||
mod outgoing_message;
|
||
mod patch_approval;
|
||
|
||
use crate::message_processor::MessageProcessor;
|
||
use crate::outgoing_message::OutgoingMessage;
|
||
use crate::outgoing_message::OutgoingMessageSender;
|
||
|
||
pub use crate::codex_tool_config::CodexToolCallParam;
|
||
pub use crate::codex_tool_config::CodexToolCallReplyParam;
|
||
pub use crate::exec_approval::ExecApprovalElicitRequestParams;
|
||
pub use crate::exec_approval::ExecApprovalResponse;
|
||
pub use crate::patch_approval::PatchApprovalElicitRequestParams;
|
||
pub use crate::patch_approval::PatchApprovalResponse;
|
||
|
||
/// Size of the bounded channels used to communicate between tasks. The value
|
||
/// is a balance between throughput and memory usage – 128 messages should be
|
||
/// plenty for an interactive CLI.
|
||
const CHANNEL_CAPACITY: usize = 128;
|
||
|
||
pub async fn run_main(
|
||
codex_linux_sandbox_exe: Option<PathBuf>,
|
||
cli_config_overrides: CliConfigOverrides,
|
||
) -> IoResult<()> {
|
||
// Install a simple subscriber so `tracing` output is visible. Users can
|
||
// control the log level with `RUST_LOG`.
|
||
tracing_subscriber::fmt()
|
||
.with_writer(std::io::stderr)
|
||
.with_env_filter(EnvFilter::from_default_env())
|
||
.init();
|
||
|
||
// Set up channels.
|
||
let (incoming_tx, mut incoming_rx) = mpsc::channel::<JSONRPCMessage>(CHANNEL_CAPACITY);
|
||
let (outgoing_tx, mut outgoing_rx) = mpsc::channel::<OutgoingMessage>(CHANNEL_CAPACITY);
|
||
|
||
// Task: read from stdin, push to `incoming_tx`.
|
||
let stdin_reader_handle = tokio::spawn({
|
||
let incoming_tx = incoming_tx.clone();
|
||
async move {
|
||
let stdin = io::stdin();
|
||
let reader = BufReader::new(stdin);
|
||
let mut lines = reader.lines();
|
||
|
||
while let Some(line) = lines.next_line().await.unwrap_or_default() {
|
||
match serde_json::from_str::<JSONRPCMessage>(&line) {
|
||
Ok(msg) => {
|
||
if incoming_tx.send(msg).await.is_err() {
|
||
// Receiver gone – nothing left to do.
|
||
break;
|
||
}
|
||
}
|
||
Err(e) => error!("Failed to deserialize JSONRPCMessage: {e}"),
|
||
}
|
||
}
|
||
|
||
debug!("stdin reader finished (EOF)");
|
||
}
|
||
});
|
||
|
||
// Parse CLI overrides once and derive the base Config eagerly so later
|
||
// components do not need to work with raw TOML values.
|
||
let cli_kv_overrides = cli_config_overrides.parse_overrides().map_err(|e| {
|
||
std::io::Error::new(
|
||
ErrorKind::InvalidInput,
|
||
format!("error parsing -c overrides: {e}"),
|
||
)
|
||
})?;
|
||
let config = Config::load_with_cli_overrides(cli_kv_overrides, ConfigOverrides::default())
|
||
.map_err(|e| {
|
||
std::io::Error::new(ErrorKind::InvalidData, format!("error loading config: {e}"))
|
||
})?;
|
||
|
||
// Task: process incoming messages.
|
||
let processor_handle = tokio::spawn({
|
||
let outgoing_message_sender = OutgoingMessageSender::new(outgoing_tx);
|
||
let mut processor = MessageProcessor::new(
|
||
outgoing_message_sender,
|
||
codex_linux_sandbox_exe,
|
||
std::sync::Arc::new(config),
|
||
);
|
||
async move {
|
||
while let Some(msg) = incoming_rx.recv().await {
|
||
match msg {
|
||
JSONRPCMessage::Request(r) => processor.process_request(r).await,
|
||
JSONRPCMessage::Response(r) => processor.process_response(r).await,
|
||
JSONRPCMessage::Notification(n) => processor.process_notification(n).await,
|
||
JSONRPCMessage::Error(e) => processor.process_error(e),
|
||
}
|
||
}
|
||
|
||
info!("processor task exited (channel closed)");
|
||
}
|
||
});
|
||
|
||
// Task: write outgoing messages to stdout.
|
||
let stdout_writer_handle = tokio::spawn(async move {
|
||
let mut stdout = io::stdout();
|
||
while let Some(outgoing_message) = outgoing_rx.recv().await {
|
||
let msg: JSONRPCMessage = outgoing_message.into();
|
||
match serde_json::to_string(&msg) {
|
||
Ok(json) => {
|
||
if let Err(e) = stdout.write_all(json.as_bytes()).await {
|
||
error!("Failed to write to stdout: {e}");
|
||
break;
|
||
}
|
||
if let Err(e) = stdout.write_all(b"\n").await {
|
||
error!("Failed to write newline to stdout: {e}");
|
||
break;
|
||
}
|
||
if let Err(e) = stdout.flush().await {
|
||
error!("Failed to flush stdout: {e}");
|
||
break;
|
||
}
|
||
}
|
||
Err(e) => error!("Failed to serialize JSONRPCMessage: {e}"),
|
||
}
|
||
}
|
||
|
||
info!("stdout writer exited (channel closed)");
|
||
});
|
||
|
||
// Wait for all tasks to finish. The typical exit path is the stdin reader
|
||
// hitting EOF which, once it drops `incoming_tx`, propagates shutdown to
|
||
// the processor and then to the stdout task.
|
||
let _ = tokio::join!(stdin_reader_handle, processor_handle, stdout_writer_handle);
|
||
|
||
Ok(())
|
||
}
|