mirror of
https://github.com/openai/codex.git
synced 2026-10-19 16:17:49 +00:00
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:
committed by
copyberry
parent
9f22ba8039
commit
b23324986d
+7
@@ -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"
|
||||
},
|
||||
|
||||
+7
@@ -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"
|
||||
},
|
||||
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -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(),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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")]
|
||||
|
||||
Reference in New Issue
Block a user