diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json index 23b6ff042c..c3c8a756b9 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json @@ -14721,6 +14721,13 @@ "authStatus": { "$ref": "#/definitions/v2/McpAuthStatus" }, + "httpOrigin": { + "description": "HTTP origin of the effective configured endpoint, including plugin servers. Excludes credentials, path, query, and fragment; null for non-HTTP transports.", + "type": [ + "string", + "null" + ] + }, "name": { "type": "string" }, diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json index c1f737a88e..76f4348dd8 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json @@ -10773,6 +10773,13 @@ "authStatus": { "$ref": "#/definitions/McpAuthStatus" }, + "httpOrigin": { + "description": "HTTP origin of the effective configured endpoint, including plugin servers. Excludes credentials, path, query, and fragment; null for non-HTTP transports.", + "type": [ + "string", + "null" + ] + }, "name": { "type": "string" }, diff --git a/codex-rs/app-server-protocol/schema/json/v2/ListMcpServerStatusResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ListMcpServerStatusResponse.json index 61c0dd6886..da96c50354 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ListMcpServerStatusResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ListMcpServerStatusResponse.json @@ -69,6 +69,13 @@ "authStatus": { "$ref": "#/definitions/McpAuthStatus" }, + "httpOrigin": { + "description": "HTTP origin of the effective configured endpoint, including plugin servers. Excludes credentials, path, query, and fragment; null for non-HTTP transports.", + "type": [ + "string", + "null" + ] + }, "name": { "type": "string" }, diff --git a/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-experimental.json.zst b/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-experimental.json.zst index 9bcc74d804..01951041d8 100644 Binary files a/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-experimental.json.zst and b/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-experimental.json.zst differ diff --git a/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-stable.json.zst b/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-stable.json.zst index 172ace1368..21284ac61f 100644 Binary files a/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-stable.json.zst and b/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-stable.json.zst differ diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/McpServerStatus.ts b/codex-rs/app-server-protocol/schema/typescript/v2/McpServerStatus.ts index f33da54a43..0b7d5449d9 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/McpServerStatus.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/McpServerStatus.ts @@ -13,7 +13,12 @@ export type McpServerStatus = { name: string, /** * Current thread-runtime connection state; null when unavailable or the configuration changed. */ -runtimeStatus: McpServerConnectionStatus | null, pluginId: string | null, serverInfo: McpServerInfo | null, +runtimeStatus: McpServerConnectionStatus | null, pluginId: string | null, +/** + * HTTP origin of the effective configured endpoint, including plugin servers. + * Excludes credentials, path, query, and fragment; null for non-HTTP transports. + */ +httpOrigin: string | null, serverInfo: McpServerInfo | null, /** * Capabilities advertised by the initialized MCP server; null when unavailable. */ diff --git a/codex-rs/app-server-protocol/src/protocol/v2/mcp.rs b/codex-rs/app-server-protocol/src/protocol/v2/mcp.rs index 44b217b4c6..d1d920ddbd 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/mcp.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/mcp.rs @@ -78,6 +78,9 @@ pub struct McpServerStatus { /// Current thread-runtime connection state; null when unavailable or the configuration changed. pub runtime_status: Option, pub plugin_id: Option, + /// HTTP origin of the effective configured endpoint, including plugin servers. + /// Excludes credentials, path, query, and fragment; null for non-HTTP transports. + pub http_origin: Option, pub server_info: Option, /// Capabilities advertised by the initialized MCP server; null when unavailable. pub server_capabilities: Option, diff --git a/codex-rs/app-server-protocol/src/protocol/v2/tests.rs b/codex-rs/app-server-protocol/src/protocol/v2/tests.rs index 4cbe82ab4b..b24eb2412d 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/tests.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/tests.rs @@ -2639,6 +2639,7 @@ fn mcp_server_status_serializes_absent_server_info_as_null() { name: "not-ready".to_string(), runtime_status: None, plugin_id: None, + http_origin: None, server_info: None, tools: HashMap::new(), resources: Vec::new(), @@ -2655,6 +2656,7 @@ fn mcp_server_status_serializes_absent_server_info_as_null() { "name": "not-ready", "runtimeStatus": null, "pluginId": null, + "httpOrigin": null, "serverInfo": null, "serverCapabilities": null, "tools": {}, @@ -2688,6 +2690,7 @@ fn mcp_server_status_accepts_older_inventory_without_runtime_status() { name: "older-server".to_string(), runtime_status: None, plugin_id: None, + http_origin: None, server_info: None, tools: HashMap::new(), resources: Vec::new(), @@ -2761,6 +2764,7 @@ fn mcp_server_status_serializes_absent_server_info_metadata_as_null() { name: "initialized".to_string(), runtime_status: None, plugin_id: Some("lookup@test".to_string()), + http_origin: None, server_info: Some(McpServerInfo { name: "lookup-server".to_string(), title: None, @@ -2784,6 +2788,7 @@ fn mcp_server_status_serializes_absent_server_info_metadata_as_null() { "name": "initialized", "runtimeStatus": null, "pluginId": "lookup@test", + "httpOrigin": null, "serverCapabilities": null, "serverInfo": { "name": "lookup-server", diff --git a/codex-rs/app-server/src/request_processors/mcp_processor.rs b/codex-rs/app-server/src/request_processors/mcp_processor.rs index 9c22739de9..9312b57beb 100644 --- a/codex-rs/app-server/src/request_processors/mcp_processor.rs +++ b/codex-rs/app-server/src/request_processors/mcp_processor.rs @@ -404,6 +404,18 @@ impl McpRequestProcessor { | McpServerSource::Extension { .. } => None, }, ), + http_origin: mcp_config + .mcp_server_catalog + .server(name) + .and_then(|server| match &server.config().transport { + McpServerTransportConfig::StreamableHttp { url, .. } => { + url::Url::parse(url).ok().and_then(|url| { + matches!(url.scheme(), "http" | "https") + .then(|| url.origin().ascii_serialization()) + }) + } + McpServerTransportConfig::Stdio { .. } => None, + }), server_info: server_infos.remove(name), server_capabilities: server_capabilities.remove(name), tools: tools_by_server.remove(name).unwrap_or_default(), diff --git a/codex-rs/app-server/tests/suite/v2/mcp_server_status.rs b/codex-rs/app-server/tests/suite/v2/mcp_server_status.rs index 8947a99c49..c9f6a3f132 100644 --- a/codex-rs/app-server/tests/suite/v2/mcp_server_status.rs +++ b/codex-rs/app-server/tests/suite/v2/mcp_server_status.rs @@ -25,6 +25,8 @@ use codex_app_server_protocol::McpServerConnectionStatus; use codex_app_server_protocol::McpServerOauthLoginCompletedNotification; use codex_app_server_protocol::McpServerOauthLoginResponse; use codex_app_server_protocol::McpServerStatusDetail; +use codex_app_server_protocol::PluginInstallParams; +use codex_app_server_protocol::PluginInstallResponse; use codex_app_server_protocol::RequestId; use codex_app_server_protocol::ThreadStartParams; use codex_app_server_protocol::ThreadStartResponse; @@ -33,6 +35,7 @@ use codex_http_client::HttpClientBuilder; use codex_protocol::config_types::TrustLevel; use codex_rmcp_client::McpOAuthCallbackMode; use codex_rmcp_client::resolve_mcp_oauth_callback_url; +use codex_utils_absolute_path::AbsolutePathBuf; use core_test_support::skip_if_remote; use core_test_support::stdio_server_bin; use pretty_assertions::assert_eq; @@ -525,16 +528,23 @@ async fn oauth_login_automatically_selects_callback_specific_cimd_without_metada Ok(()) } +#[test_case(false; "configured server")] +#[test_case(true; "plugin server")] #[tokio::test] -async fn mcp_server_status_list_returns_raw_server_and_tool_names() -> Result<()> { +async fn mcp_server_status_list_returns_raw_server_and_tool_names(plugin: bool) -> Result<()> { let server = create_mock_responses_server_sequence_unchecked(Vec::new()).await; let (mcp_server_url, mcp_server_handle) = start_mcp_server("look-up.raw", /*tools_error*/ None).await?; let codex_home = TempDir::new()?; + let endpoint = format!("{mcp_server_url}/mcp?secret=not-for-clients"); + let http_config = if plugin { + "[features]\nplugins = true\n".to_string() + } else { + format!("[mcp_servers.some-server]\nurl = {endpoint:?}\n") + }; mock_responses_config(&server.uri()) .with_extra_config(&format!( - "[mcp_servers.some-server]\nurl = \"{mcp_server_url}/mcp\"\n\ - [mcp_servers.broken-server]\ncommand = {}", + "{http_config}[mcp_servers.broken-server]\ncommand = {}", toml::Value::String( codex_home .path() @@ -550,6 +560,39 @@ async fn mcp_server_status_list_returns_raw_server_and_tool_names() -> Result<() .without_auto_env() .build_initialized() .await?; + let plugin_root = TempDir::new()?; + if plugin { + std::fs::create_dir_all(plugin_root.path().join(".git"))?; + std::fs::create_dir_all(plugin_root.path().join(".agents/plugins"))?; + std::fs::create_dir_all(plugin_root.path().join("sample/.codex-plugin"))?; + std::fs::write( + plugin_root.path().join("sample/.codex-plugin/plugin.json"), + serde_json::to_vec(&json!({"name": "sample"}))?, + )?; + std::fs::write( + plugin_root.path().join("sample/.mcp.json"), + serde_json::to_vec( + &json!({"mcpServers": {"some-server": {"type": "http", "url": endpoint}}}), + )?, + )?; + let marketplace_path = plugin_root.path().join(".agents/plugins/marketplace.json"); + std::fs::write( + &marketplace_path, + serde_json::to_vec(&json!({ + "name": "debug", "plugins": [{"name": "sample", "source": {"source": "local", "path": "./sample"}}] + }))?, + )?; + let request_id = mcp + .send_plugin_install_request(PluginInstallParams { + marketplace_path: Some(AbsolutePathBuf::try_from(marketplace_path)?), + remote_marketplace_name: None, + install_attempt_id: None, + plugin_name: "sample".to_string(), + }) + .await?; + let _: PluginInstallResponse = + timeout(DEFAULT_READ_TIMEOUT, mcp.read_response(request_id)).await??; + } let response: ListMcpServerStatusResponse = mcp .request(|request_id| ClientRequest::McpServerStatusList { request_id, @@ -568,9 +611,10 @@ async fn mcp_server_status_list_returns_raw_server_and_tool_names() -> Result<() .data .iter() .find(|status| status.name == "broken-server") - .unwrap(); + .expect("broken server status"); assert!(failed.tools.is_empty()); assert_eq!(failed.server_capabilities, None); + assert_eq!(failed.http_origin, None); assert!( failed .tools_error @@ -581,7 +625,7 @@ async fn mcp_server_status_list_returns_raw_server_and_tool_names() -> Result<() .data .iter() .find(|status| status.name == "some-server") - .unwrap(); + .expect("configured server status"); assert_eq!( status .server_capabilities @@ -594,7 +638,11 @@ async fn mcp_server_status_list_returns_raw_server_and_tool_names() -> Result<() assert_eq!(status.tools_error, None); assert_eq!(status.name, "some-server"); assert_eq!(status.runtime_status, None); - assert_eq!(status.plugin_id, None); + assert_eq!( + status.plugin_id.as_deref(), + plugin.then_some("sample@debug") + ); + assert_eq!(status.http_origin.as_deref(), Some(mcp_server_url.as_str())); assert_eq!( status.tools.keys().cloned().collect::>(), BTreeSet::from(["look-up.raw".to_string()]) diff --git a/codex-rs/tui/src/app/background_requests.rs b/codex-rs/tui/src/app/background_requests.rs index 4cdde21b77..c3b9646a5c 100644 --- a/codex-rs/tui/src/app/background_requests.rs +++ b/codex-rs/tui/src/app/background_requests.rs @@ -1564,6 +1564,7 @@ mod tests { name: "docs".to_string(), runtime_status: None, plugin_id: None, + http_origin: None, server_info: None, tools: HashMap::from([( "list".to_string(), @@ -1588,6 +1589,7 @@ mod tests { name: "disabled".to_string(), runtime_status: None, plugin_id: None, + http_origin: None, server_info: None, tools: HashMap::new(), resources: Vec::new(), diff --git a/codex-rs/tui/src/app/tests.rs b/codex-rs/tui/src/app/tests.rs index b111d7e068..4f8109fe67 100644 --- a/codex-rs/tui/src/app/tests.rs +++ b/codex-rs/tui/src/app/tests.rs @@ -430,6 +430,7 @@ async fn handle_mcp_inventory_result_respects_origin_thread() { name: "docs".to_string(), runtime_status: None, plugin_id: None, + http_origin: None, server_info: None, tools: HashMap::new(), resources: Vec::new(), diff --git a/codex-rs/tui/src/history_cell/mcp_tests.rs b/codex-rs/tui/src/history_cell/mcp_tests.rs index dcf651fd30..7a35659f17 100644 --- a/codex-rs/tui/src/history_cell/mcp_tests.rs +++ b/codex-rs/tui/src/history_cell/mcp_tests.rs @@ -28,6 +28,7 @@ fn mcp_inventory_connection_states() { name: name.to_string(), runtime_status, plugin_id: None, + http_origin: None, server_info: None, tools: HashMap::new(), tools_error: None, diff --git a/codex-rs/tui/src/history_cell/tests.rs b/codex-rs/tui/src/history_cell/tests.rs index 0062cd5b2c..5ce4b76560 100644 --- a/codex-rs/tui/src/history_cell/tests.rs +++ b/codex-rs/tui/src/history_cell/tests.rs @@ -1118,6 +1118,7 @@ fn mcp_tools_output_from_statuses_renders_status_only_servers() { name: "plugin_docs".to_string(), runtime_status: None, plugin_id: None, + http_origin: None, server_info: None, tools: HashMap::from([( "lookup".to_string(), @@ -1152,6 +1153,7 @@ fn mcp_tools_output_from_statuses_renders_verbose_inventory() { name: "plugin_docs".to_string(), runtime_status: None, plugin_id: None, + http_origin: None, server_info: None, tools: HashMap::from([( "lookup".to_string(), diff --git a/sdk/python/src/openai_codex/generated/v2_all.py b/sdk/python/src/openai_codex/generated/v2_all.py index 278cbabada..f1c6b5b05e 100644 --- a/sdk/python/src/openai_codex/generated/v2_all.py +++ b/sdk/python/src/openai_codex/generated/v2_all.py @@ -8413,6 +8413,13 @@ class McpServerStatus(BaseModel): populate_by_name=True, ) auth_status: Annotated[McpAuthStatus, Field(alias="authStatus")] + http_origin: Annotated[ + str | None, + Field( + alias="httpOrigin", + description="HTTP origin of the effective configured endpoint, including plugin servers. Excludes credentials, path, query, and fragment; null for non-HTTP transports.", + ), + ] = None name: str plugin_id: Annotated[str | None, Field(alias="pluginId")] = None resource_templates: Annotated[list[ResourceTemplate], Field(alias="resourceTemplates")]