Expose HTTP origins in MCP server status (#47159)

## What changed

Add nullable `httpOrigin` to `McpServerStatus`, derived from the effective configured endpoint for both configured and plugin-provided servers. Return only the HTTP(S) origin, excluding credentials, path, query, and fragment; return `null` for stdio transports, invalid URLs, and non-HTTP(S) schemes. Update the generated protocol schemas and TypeScript and Python types.

## Testing

Extend status integration tests to cover configured and plugin-provided HTTP servers, verify that endpoint paths and query parameters are omitted, and assert `null` for a stdio server. Update protocol tests to check null serialization and compatibility with older inventory responses.

GitOrigin-RevId: 033ebdeefe829e3a48b2d4ab7d8866dac4cda5c5
This commit is contained in:
Alex Kotliarskyi
2026-09-22 02:24:25 +00:00
committed by copyberry
parent 9f22ba8039
commit b23324986d
15 changed files with 114 additions and 7 deletions
@@ -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"
},
@@ -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"
},
@@ -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"
},
@@ -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.
*/
@@ -78,6 +78,9 @@ pub struct McpServerStatus {
/// Current thread-runtime connection state; null when unavailable or the configuration changed.
pub runtime_status: Option<McpServerConnectionStatus>,
pub plugin_id: Option<String>,
/// 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<String>,
pub server_info: Option<McpServerInfo>,
/// Capabilities advertised by the initialized MCP server; null when unavailable.
pub server_capabilities: Option<serde_json::Value>,
@@ -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",
@@ -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(),
@@ -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<_>>(),
BTreeSet::from(["look-up.raw".to_string()])
@@ -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(),
+1
View File
@@ -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(),
@@ -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,
+2
View File
@@ -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(),
@@ -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")]