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

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

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

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

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

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

1 change: 1 addition & 0 deletions codex-rs/app-server-protocol/src/protocol/v2/mcp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,7 @@ pub enum McpServerStartupState {
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct McpServerStatusUpdatedNotification {
pub thread_id: Option<String>,
pub name: String,
pub status: McpServerStartupState,
pub error: Option<String>,
Expand Down
27 changes: 27 additions & 0 deletions codex-rs/app-server-protocol/src/protocol/v2/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2039,6 +2039,33 @@ fn mcp_server_status_serializes_absent_server_info_as_null() {
);
}

#[test]
fn mcp_server_status_updated_accepts_missing_thread_id() {
let notification: McpServerStatusUpdatedNotification = serde_json::from_value(json!({
"name": "optional_broken",
"status": "failed",
"error": "handshake failed",
}))
.expect("notification without threadId should deserialize");

let expected = McpServerStatusUpdatedNotification {
thread_id: None,
name: "optional_broken".to_string(),
status: McpServerStartupState::Failed,
error: Some("handshake failed".to_string()),
};
assert_eq!(notification, expected);
assert_eq!(
serde_json::to_value(notification).expect("notification should serialize"),
json!({
"threadId": null,
"name": "optional_broken",
"status": "failed",
"error": "handshake failed",
})
);
}

#[test]
fn mcp_server_status_serializes_absent_server_info_metadata_as_null() {
let response = ListMcpServerStatusResponse {
Expand Down
4 changes: 2 additions & 2 deletions codex-rs/app-server/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1232,7 +1232,7 @@ Because audio is intentionally separate from `ThreadItem`, clients can opt out o

### MCP server startup events

- `mcpServer/startupStatus/updated` — `{ name, status, error }` when app-server observes an MCP server startup transition. `status` is one of `starting`, `ready`, `failed`, or `cancelled`. `error` is `null` except for `failed`.
- `mcpServer/startupStatus/updated` — `{ threadId, name, status, error }` when app-server observes an MCP server startup transition. `threadId` identifies the owning thread when startup is thread-scoped and is `null` when startup is app-scoped. `status` is one of `starting`, `ready`, `failed`, or `cancelled`. `error` is `null` except for `failed`.

### Turn events

Expand Down Expand Up @@ -1782,7 +1782,7 @@ Codex supports these authentication modes. The current mode is surfaced in `acco
- `account/rateLimits/updated` (notify) — emitted whenever a user's ChatGPT rate limits change. This is a sparse rolling update; merge available values into the most recent `account/rateLimits/read` response or refetch that snapshot.
- `account/sendAddCreditsNudgeEmail` — ask ChatGPT to email the workspace owner about depleted credits or a reached usage limit.
- `mcpServer/oauthLogin/completed` (notify) — emitted after a `mcpServer/oauth/login` flow finishes for a server; payload includes `{ name, success, error? }`.
- `mcpServer/startupStatus/updated` (notify) — emitted when a configured MCP server's startup status changes for a loaded thread; payload includes `{ name, status, error }` where `status` is `starting`, `ready`, `failed`, or `cancelled`.
- `mcpServer/startupStatus/updated` (notify) — emitted when a configured MCP server's startup status changes; payload includes `{ threadId, name, status, error }`, where `threadId` is the owning thread when startup is thread-scoped and `null` when it is app-scoped, and `status` is `starting`, `ready`, `failed`, or `cancelled`.

### 1) Check auth state

Expand Down
1 change: 1 addition & 0 deletions codex-rs/app-server/src/bespoke_event_handling.rs
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,7 @@ pub(crate) async fn apply_bespoke_event_handling(
}
};
let notification = McpServerStatusUpdatedNotification {
thread_id: Some(conversation_id.to_string()),
name: update.server,
status,
error,
Expand Down
4 changes: 3 additions & 1 deletion codex-rs/app-server/tests/suite/v2/thread_start.rs
Original file line number Diff line number Diff line change
Expand Up @@ -711,7 +711,7 @@ async fn thread_start_emits_mcp_server_status_updated_notifications() -> Result<
.send_thread_start_request(ThreadStartParams::default())
.await?;

let _: ThreadStartResponse = to_response(
let start_response: ThreadStartResponse = to_response(
timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_response_message(RequestId::Integer(req_id)),
Expand Down Expand Up @@ -748,6 +748,7 @@ async fn thread_start_emits_mcp_server_status_updated_notifications() -> Result<
assert_eq!(
starting,
McpServerStatusUpdatedNotification {
thread_id: Some(start_response.thread.id.clone()),
name: "optional_broken".to_string(),
status: McpServerStartupState::Starting,
error: None,
Expand Down Expand Up @@ -780,6 +781,7 @@ async fn thread_start_emits_mcp_server_status_updated_notifications() -> Result<
let ServerNotification::McpServerStatusUpdated(failed) = failed else {
anyhow::bail!("unexpected notification variant");
};
assert_eq!(failed.thread_id, Some(start_response.thread.id));
assert_eq!(failed.name, "optional_broken");
assert_eq!(failed.status, McpServerStartupState::Failed);
assert!(
Expand Down
41 changes: 40 additions & 1 deletion codex-rs/tui/src/app/app_server_event_targets.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ pub(super) fn server_request_thread_id(request: &ServerRequest) -> Option<Thread
pub(super) enum ServerNotificationThreadTarget {
Thread(ThreadId),
InvalidThreadId(String),
AppScoped,
Global,
}

Expand Down Expand Up @@ -147,8 +148,13 @@ pub(super) fn server_notification_thread_target(
}
ServerNotification::Warning(notification) => notification.thread_id.as_deref(),
ServerNotification::GuardianWarning(notification) => Some(notification.thread_id.as_str()),
ServerNotification::McpServerStatusUpdated(notification) => {
match notification.thread_id.as_deref() {
Some(thread_id) => Some(thread_id),
None => return ServerNotificationThreadTarget::AppScoped,
}
}
Comment thread
fcoury-oai marked this conversation as resolved.
ServerNotification::SkillsChanged(_)
| ServerNotification::McpServerStatusUpdated(_)
| ServerNotification::McpServerOauthLoginCompleted(_)
| ServerNotification::AccountUpdated(_)
| ServerNotification::AccountRateLimitsUpdated(_)
Expand Down Expand Up @@ -184,6 +190,8 @@ mod tests {
use crate::test_support::PathBufExt;
use crate::test_support::test_path_buf;
use codex_app_server_protocol::GuardianWarningNotification;
use codex_app_server_protocol::McpServerStartupState;
use codex_app_server_protocol::McpServerStatusUpdatedNotification;
use codex_app_server_protocol::ServerNotification;
use codex_app_server_protocol::ThreadSettings;
use codex_app_server_protocol::ThreadSettingsUpdatedNotification;
Expand Down Expand Up @@ -259,6 +267,37 @@ mod tests {
assert_eq!(target, ServerNotificationThreadTarget::Thread(thread_id));
}

#[test]
fn mcp_startup_notifications_route_to_threads() {
Comment thread
fcoury-oai marked this conversation as resolved.
let thread_id = ThreadId::new();
let notification =
ServerNotification::McpServerStatusUpdated(McpServerStatusUpdatedNotification {
thread_id: Some(thread_id.to_string()),
name: "sentry".to_string(),
status: McpServerStartupState::Failed,
error: Some("sentry is not logged in".to_string()),
});

let target = server_notification_thread_target(&notification);

assert_eq!(target, ServerNotificationThreadTarget::Thread(thread_id));
}

#[test]
fn mcp_startup_notifications_without_threads_are_app_scoped() {
let notification =
ServerNotification::McpServerStatusUpdated(McpServerStatusUpdatedNotification {
thread_id: None,
name: "sentry".to_string(),
status: McpServerStartupState::Failed,
error: Some("sentry is not logged in".to_string()),
});

let target = server_notification_thread_target(&notification);

assert_eq!(target, ServerNotificationThreadTarget::AppScoped);
}

#[test]
fn thread_settings_updated_notifications_route_to_threads() {
let thread_id = ThreadId::new();
Expand Down
11 changes: 8 additions & 3 deletions codex-rs/tui/src/app/app_server_events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,9 @@ use codex_app_server_protocol::ServerNotification;
use codex_app_server_protocol::ServerRequest;

impl App {
fn refresh_mcp_startup_expected_servers_from_config(&mut self) {
pub(super) fn refresh_mcp_startup_expected_servers_from_config(&mut self) {
let enabled_config_mcp_servers: Vec<String> = self
.chat_widget
.config_ref()
.config
.mcp_servers
.get()
.iter()
Expand Down Expand Up @@ -142,6 +141,12 @@ impl App {
);
return;
}
ServerNotificationThreadTarget::AppScoped => {
tracing::debug!(
"ignoring app-scoped MCP startup notification without a TUI app-level target"
);
return;
}
ServerNotificationThreadTarget::Global => {}
}

Expand Down
Loading
Loading