Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
5 changes: 5 additions & 0 deletions codex-rs/app-server-protocol/src/protocol/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,11 @@ client_request_definitions! {
response: v2::McpServerOauthLoginResponse,
},

McpServerRefresh => "config/mcpServer/reload" {
params: #[ts(type = "undefined")] #[serde(skip_serializing_if = "Option::is_none")] Option<()>,
response: v2::McpServerRefreshResponse,
},

McpServerStatusList => "mcpServerStatus/list" {
params: v2::ListMcpServerStatusParams,
response: v2::ListMcpServerStatusResponse,
Expand Down
10 changes: 10 additions & 0 deletions codex-rs/app-server-protocol/src/protocol/v2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -940,6 +940,16 @@ pub struct ListMcpServerStatusResponse {
pub next_cursor: Option<String>,
}

#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct McpServerRefreshParams {}

#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct McpServerRefreshResponse {}

#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
Expand Down
1 change: 1 addition & 0 deletions codex-rs/app-server/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ Example (from OpenAI's official VSCode extension):
- `model/list` — list available models (with reasoning effort options).
- `skills/list` — list skills for one or more `cwd` values (optional `forceReload`).
- `mcpServer/oauth/login` — start an OAuth login for a configured MCP server; returns an `authorization_url` and later emits `mcpServer/oauthLogin/completed` once the browser flow finishes.
- `config/mcpServer/reload` — reload MCP server config from disk and queue a refresh for loaded threads (applied on each thread's next active turn); returns `{}`. Use this after editing `config.toml` without restarting the server.
- `mcpServerStatus/list` — enumerate configured MCP servers with their tools, resources, resource templates, and auth status; supports cursor+limit pagination.
- `feedback/upload` — submit a feedback report (classification + optional reason/logs and conversation_id); returns the tracking thread id.
- `command/exec` — run a single command under the server sandbox without starting a thread/turn (handy for utilities and validation).
Expand Down
56 changes: 56 additions & 0 deletions codex-rs/app-server/src/codex_message_processor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ use codex_app_server_protocol::LogoutChatGptResponse;
use codex_app_server_protocol::McpServerOauthLoginCompletedNotification;
use codex_app_server_protocol::McpServerOauthLoginParams;
use codex_app_server_protocol::McpServerOauthLoginResponse;
use codex_app_server_protocol::McpServerRefreshResponse;
use codex_app_server_protocol::McpServerStatus;
use codex_app_server_protocol::ModelListParams;
use codex_app_server_protocol::ModelListResponse;
Expand Down Expand Up @@ -157,6 +158,7 @@ use codex_protocol::items::TurnItem;
use codex_protocol::models::ResponseItem;
use codex_protocol::protocol::GitInfo as CoreGitInfo;
use codex_protocol::protocol::McpAuthStatus as CoreMcpAuthStatus;
use codex_protocol::protocol::McpServerRefreshConfig;
use codex_protocol::protocol::RateLimitSnapshot as CoreRateLimitSnapshot;
use codex_protocol::protocol::RolloutItem;
use codex_protocol::protocol::SessionMetaLine;
Expand Down Expand Up @@ -425,6 +427,9 @@ impl CodexMessageProcessor {
ClientRequest::McpServerOauthLogin { request_id, params } => {
self.mcp_server_oauth_login(request_id, params).await;
}
ClientRequest::McpServerRefresh { request_id, params } => {
self.mcp_server_refresh(request_id, params).await;
}
ClientRequest::McpServerStatusList { request_id, params } => {
self.list_mcp_server_status(request_id, params).await;
}
Expand Down Expand Up @@ -2302,6 +2307,57 @@ impl CodexMessageProcessor {
outgoing.send_response(request_id, response).await;
}

async fn mcp_server_refresh(&self, request_id: RequestId, _params: Option<()>) {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This does not refresh the mcp connection manager for all active threads for inform them there is an update with the latest mcp config. It is up to the thread to refresh the connection on next active turn. This should help with performance and avoid excessive refresh if there is a lot of MCP changes at once.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe put this in a comment?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"It is up to the thread to refresh the connection on next active turn."

What does this mean?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All active threads in the thread manager would be aware of a new mcp config but we are not triggering an actual reinitialization yet. On a future turn (i.e. user submit a new prompt), if the thread detects a pending_mcp_config, then it would first reinitialize and then move onto the actual turn. The benefit here is that we are not doing any extra leg work - for example you started a thread and then we change the mcp servers but never reengage with that thread, then that thread would not refresh mcp server until called upon.

let config = match self.load_latest_config().await {
Ok(config) => config,
Comment on lines +2311 to +2312
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Honor loader overrides when reloading MCP config

This new mcpServer/refresh path reloads config via load_latest_config(), which only applies CLI overrides. The app server itself is initialized with ConfigBuilder::loader_overrides(...) (see app-server/src/lib.rs), so setups that rely on a managed config path (e.g., CODEX_APP_SERVER_MANAGED_CONFIG_PATH) will have refresh read the default config instead of the managed file. In that case, threads refresh with stale/incorrect MCP definitions even after editing the managed config. Consider threading LoaderOverrides into CodexMessageProcessor and using them in the reload path so the same config source is used.

Useful? React with 👍 / 👎.

Err(error) => {
self.outgoing.send_error(request_id, error).await;
return;
}
};

let mcp_servers = match serde_json::to_value(&config.mcp_servers) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why serialize with serde_json::to_value? these types become Value which feels less useful/descriptive than the real types.

i think self.load_latest_config() will already throw an error if it fails to read the config on disk

Ok(value) => value,
Err(err) => {
let error = JSONRPCErrorError {
code: INTERNAL_ERROR_CODE,
message: format!("failed to serialize MCP servers: {err}"),
data: None,
};
self.outgoing.send_error(request_id, error).await;
return;
}
};

let mcp_oauth_credentials_store_mode =
match serde_json::to_value(config.mcp_oauth_credentials_store_mode) {
Ok(value) => value,
Err(err) => {
let error = JSONRPCErrorError {
code: INTERNAL_ERROR_CODE,
message: format!(
"failed to serialize MCP OAuth credentials store mode: {err}"
),
data: None,
};
self.outgoing.send_error(request_id, error).await;
return;
}
};

let refresh_config = McpServerRefreshConfig {
mcp_servers,
mcp_oauth_credentials_store_mode,
};

// Refresh requests are queued per thread; each thread rebuilds MCP connections on its next
// active turn to avoid work for threads that never resume.
let thread_manager = Arc::clone(&self.thread_manager);
thread_manager.refresh_mcp_servers(refresh_config).await;
let response = McpServerRefreshResponse {};
self.outgoing.send_response(request_id, response).await;
}

async fn mcp_server_oauth_login(
&self,
request_id: RequestId,
Expand Down
Loading
Loading