Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions codex-rs/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

66 changes: 57 additions & 9 deletions codex-rs/cli/src/mcp_cmd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ use codex_core::config::load_global_mcp_servers;
use codex_core::config::write_global_mcp_servers;
use codex_core::config_types::McpServerConfig;
use codex_core::config_types::McpServerTransportConfig;
use codex_core::mcp::auth::compute_auth_statuses;
use codex_core::protocol::McpAuthStatus;
use codex_rmcp_client::delete_oauth_tokens;
use codex_rmcp_client::perform_oauth_login;

Expand Down Expand Up @@ -339,11 +341,20 @@ async fn run_list(config_overrides: &CliConfigOverrides, list_args: ListArgs) ->

let mut entries: Vec<_> = config.mcp_servers.iter().collect();
entries.sort_by(|(a, _), (b, _)| a.cmp(b));
let auth_statuses = compute_auth_statuses(
config.mcp_servers.iter(),
config.mcp_oauth_credentials_store_mode,
)
.await;

if list_args.json {
let json_entries: Vec<_> = entries
.into_iter()
.map(|(name, cfg)| {
let auth_status = auth_statuses
.get(name.as_str())
.copied()
.unwrap_or(McpAuthStatus::Unsupported);
let transport = match &cfg.transport {
McpServerTransportConfig::Stdio { command, args, env } => serde_json::json!({
"type": "stdio",
Expand Down Expand Up @@ -372,6 +383,7 @@ async fn run_list(config_overrides: &CliConfigOverrides, list_args: ListArgs) ->
"tool_timeout_sec": cfg
.tool_timeout_sec
.map(|timeout| timeout.as_secs_f64()),
"auth_status": auth_status,
})
})
.collect();
Expand All @@ -385,8 +397,8 @@ async fn run_list(config_overrides: &CliConfigOverrides, list_args: ListArgs) ->
return Ok(());
}

let mut stdio_rows: Vec<[String; 4]> = Vec::new();
let mut http_rows: Vec<[String; 3]> = Vec::new();
let mut stdio_rows: Vec<[String; 5]> = Vec::new();
let mut http_rows: Vec<[String; 4]> = Vec::new();

for (name, cfg) in entries {
match &cfg.transport {
Expand All @@ -409,52 +421,79 @@ async fn run_list(config_overrides: &CliConfigOverrides, list_args: ListArgs) ->
.join(", ")
}
};
stdio_rows.push([name.clone(), command.clone(), args_display, env_display]);
let status = auth_statuses
.get(name.as_str())
.copied()
.unwrap_or(McpAuthStatus::Unsupported)
.to_string();
stdio_rows.push([
name.clone(),
command.clone(),
args_display,
env_display,
status,
]);
}
McpServerTransportConfig::StreamableHttp {
url,
bearer_token_env_var,
} => {
let status = auth_statuses
.get(name.as_str())
.copied()
.unwrap_or(McpAuthStatus::Unsupported)
.to_string();
http_rows.push([
name.clone(),
url.clone(),
bearer_token_env_var.clone().unwrap_or("-".to_string()),
status,
]);
}
}
}

if !stdio_rows.is_empty() {
let mut widths = ["Name".len(), "Command".len(), "Args".len(), "Env".len()];
let mut widths = [
"Name".len(),
"Command".len(),
"Args".len(),
"Env".len(),
"Auth".len(),
];
for row in &stdio_rows {
for (i, cell) in row.iter().enumerate() {
widths[i] = widths[i].max(cell.len());
}
}

println!(
"{:<name_w$} {:<cmd_w$} {:<args_w$} {:<env_w$}",
"{:<name_w$} {:<cmd_w$} {:<args_w$} {:<env_w$} {:<auth_w$}",
"Name",
"Command",
"Args",
"Env",
"Auth",
name_w = widths[0],
cmd_w = widths[1],
args_w = widths[2],
env_w = widths[3],
auth_w = widths[4],
);

for row in &stdio_rows {
println!(
"{:<name_w$} {:<cmd_w$} {:<args_w$} {:<env_w$}",
"{:<name_w$} {:<cmd_w$} {:<args_w$} {:<env_w$} {:<auth_w$}",
row[0],
row[1],
row[2],
row[3],
row[4],
name_w = widths[0],
cmd_w = widths[1],
args_w = widths[2],
env_w = widths[3],
auth_w = widths[4],
);
}
}
Expand All @@ -464,32 +503,41 @@ async fn run_list(config_overrides: &CliConfigOverrides, list_args: ListArgs) ->
}

if !http_rows.is_empty() {
let mut widths = ["Name".len(), "Url".len(), "Bearer Token Env Var".len()];
let mut widths = [
"Name".len(),
"Url".len(),
"Bearer Token Env Var".len(),
"Auth".len(),
];
for row in &http_rows {
for (i, cell) in row.iter().enumerate() {
widths[i] = widths[i].max(cell.len());
}
}

println!(
"{:<name_w$} {:<url_w$} {:<token_w$}",
"{:<name_w$} {:<url_w$} {:<token_w$} {:<auth_w$}",
"Name",
"Url",
"Bearer Token Env Var",
"Auth",
name_w = widths[0],
url_w = widths[1],
token_w = widths[2],
auth_w = widths[3],
);

for row in &http_rows {
println!(
"{:<name_w$} {:<url_w$} {:<token_w$}",
"{:<name_w$} {:<url_w$} {:<token_w$} {:<auth_w$}",
row[0],
row[1],
row[2],
row[3],
name_w = widths[0],
url_w = widths[1],
token_w = widths[2],
auth_w = widths[3],
);
}
}
Expand Down
5 changes: 4 additions & 1 deletion codex-rs/cli/tests/mcp_list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ fn list_and_get_render_expected_output() -> Result<()> {
assert!(stdout.contains("docs"));
assert!(stdout.contains("docs-server"));
assert!(stdout.contains("TOKEN=secret"));
assert!(stdout.contains("Auth"));
assert!(stdout.contains("Unsupported"));

let mut list_json_cmd = codex_command(codex_home.path())?;
let json_output = list_json_cmd.args(["mcp", "list", "--json"]).output()?;
Expand All @@ -76,7 +78,8 @@ fn list_and_get_render_expected_output() -> Result<()> {
}
},
"startup_timeout_sec": null,
"tool_timeout_sec": null
"tool_timeout_sec": null,
"auth_status": "unsupported"
}
]
)
Expand Down
11 changes: 10 additions & 1 deletion codex-rs/core/src/codex.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ use crate::exec_command::WriteStdinParams;
use crate::executor::Executor;
use crate::executor::ExecutorConfig;
use crate::executor::normalize_exec_result;
use crate::mcp::auth::compute_auth_statuses;
use crate::mcp_connection_manager::McpConnectionManager;
use crate::model_family::find_family_for_model;
use crate::openai_model_info::get_model_info;
Expand Down Expand Up @@ -1403,10 +1404,18 @@ async fn submission_loop(

// This is a cheap lookup from the connection manager's cache.
let tools = sess.services.mcp_connection_manager.list_all_tools();
let auth_statuses = compute_auth_statuses(
config.mcp_servers.iter(),
config.mcp_oauth_credentials_store_mode,
)
.await;
let event = Event {
id: sub_id,
msg: EventMsg::McpListToolsResponse(
crate::protocol::McpListToolsResponseEvent { tools },
crate::protocol::McpListToolsResponseEvent {
tools,
auth_statuses,
},
),
};
sess.send_event(event).await;
Expand Down
1 change: 1 addition & 0 deletions codex-rs/core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ pub mod executor;
mod flags;
pub mod git_info;
pub mod landlock;
pub mod mcp;
mod mcp_connection_manager;
mod mcp_tool_call;
mod message_history;
Expand Down
58 changes: 58 additions & 0 deletions codex-rs/core/src/mcp/auth.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
use std::collections::HashMap;

use anyhow::Result;
use codex_protocol::protocol::McpAuthStatus;
use codex_rmcp_client::OAuthCredentialsStoreMode;
use codex_rmcp_client::determine_streamable_http_auth_status;
use futures::future::join_all;
use tracing::warn;

use crate::config_types::McpServerConfig;
use crate::config_types::McpServerTransportConfig;

pub async fn compute_auth_statuses<'a, I>(
servers: I,
store_mode: OAuthCredentialsStoreMode,
) -> HashMap<String, McpAuthStatus>
where
I: IntoIterator<Item = (&'a String, &'a McpServerConfig)>,
{
let futures = servers.into_iter().map(|(name, config)| {
let name = name.clone();
let config = config.clone();
async move {
let status = match compute_auth_status(&name, &config, store_mode).await {
Ok(status) => status,
Err(error) => {
warn!("failed to determine auth status for MCP server `{name}`: {error:?}");
McpAuthStatus::Unsupported
}
};
(name, status)
}
});

join_all(futures).await.into_iter().collect()
}

async fn compute_auth_status(
server_name: &str,
config: &McpServerConfig,
store_mode: OAuthCredentialsStoreMode,
) -> Result<McpAuthStatus> {
match &config.transport {
McpServerTransportConfig::Stdio { .. } => Ok(McpAuthStatus::Unsupported),
McpServerTransportConfig::StreamableHttp {
url,
bearer_token_env_var,
} => {
determine_streamable_http_auth_status(
server_name,
url,
bearer_token_env_var.as_deref(),
store_mode,
)
.await
}
}
}
1 change: 1 addition & 0 deletions codex-rs/core/src/mcp/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pub mod auth;
24 changes: 24 additions & 0 deletions codex-rs/protocol/src/protocol.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1243,6 +1243,30 @@ pub struct GetHistoryEntryResponseEvent {
pub struct McpListToolsResponseEvent {
/// Fully qualified tool name -> tool definition.
pub tools: std::collections::HashMap<String, McpTool>,
/// Authentication status for each configured MCP server.
pub auth_statuses: std::collections::HashMap<String, McpAuthStatus>,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, TS)]
#[serde(rename_all = "snake_case")]
#[ts(rename_all = "snake_case")]
pub enum McpAuthStatus {
Unsupported,
NotLoggedIn,
BearerToken,
OAuth,
}

impl fmt::Display for McpAuthStatus {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let text = match self {
McpAuthStatus::Unsupported => "Unsupported",
McpAuthStatus::NotLoggedIn => "Not logged in",
McpAuthStatus::BearerToken => "Bearer token",
McpAuthStatus::OAuth => "OAuth",
};
f.write_str(text)
}
}

/// Response payload for `Op::ListCustomPrompts`.
Expand Down
1 change: 1 addition & 0 deletions codex-rs/rmcp-client/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ axum = { workspace = true, default-features = false, features = [
"http1",
"tokio",
] }
codex-protocol = { workspace = true }
keyring = { workspace = true, features = [
"apple-native",
"crypto-rust",
Expand Down
Loading
Loading