Route selected apps over MCP fallbacks

This commit is contained in:
jif-oai
2026-06-25 00:25:21 +01:00
parent 62c0392bb6
commit f4c2118044
3 changed files with 69 additions and 18 deletions
@@ -11,6 +11,8 @@ use codex_app_server_protocol::AppInfo;
use codex_app_server_protocol::AppsListParams;
use codex_app_server_protocol::AppsListResponse;
use codex_app_server_protocol::CapabilityRootLocation;
use codex_app_server_protocol::ListMcpServerStatusParams;
use codex_app_server_protocol::ListMcpServerStatusResponse;
use codex_app_server_protocol::LoginAccountResponse;
use codex_app_server_protocol::RequestId;
use codex_app_server_protocol::SelectedCapabilityRoot;
@@ -29,6 +31,10 @@ use super::app_list::connector_tool;
use super::app_list::start_apps_server_with_delays_and_access_token;
const DEFAULT_TIMEOUT: Duration = Duration::from_secs(60);
const APP_CONFIG: &[u8] = br#"{"apps":{"calendar_app":{"id":"calendar"}}}"#;
const APP_NAME: &str = "calendar_app";
const CONFLICTING_MCP_CONFIG: &[u8] =
br#"{"mcpServers":{"calendar_app":{"command":"must-not-start","startup_timeout_sec":1}}}"#;
const CONNECTOR_ID: &str = "calendar";
const CONNECTOR_NAME: &str = "Calendar";
const PLUGIN_DISPLAY_NAME: &str = "Executor Calendar";
@@ -92,13 +98,8 @@ async fn selected_executor_connector_is_frozen_thread_scoped_and_hosted() -> Res
.await?;
let app_config_path = plugin_root.join(".app.json")?;
executor_file_system
.write_file(
&app_config_path,
br#"{"apps":{"calendar":{"id":"calendar"}}}"#.to_vec(),
/*sandbox*/ None,
)
.write_file(&app_config_path, APP_CONFIG.to_vec(), /*sandbox*/ None)
.await?;
let mut app_server = TestAppServer::new_with_auto_env(codex_home.path()).await?;
timeout(DEFAULT_TIMEOUT, app_server.initialize()).await??;
let environment_id = app_server.auto_env_params()?.environment_id;
@@ -177,10 +178,13 @@ async fn selected_executor_connector_is_frozen_thread_scoped_and_hosted() -> Res
}
);
executor_file_system
.write_file(&app_config_path, APP_CONFIG.to_vec(), /*sandbox*/ None)
.await?;
executor_file_system
.write_file(
&app_config_path,
br#"{"apps":{"calendar":{"id":"calendar"}}}"#.to_vec(),
&plugin_root.join(".mcp.json")?,
CONFLICTING_MCP_CONFIG.to_vec(),
/*sandbox*/ None,
)
.await?;
@@ -195,6 +199,22 @@ async fn selected_executor_connector_is_frozen_thread_scoped_and_hosted() -> Res
}),
)
.await?;
let request_id = app_server
.send_list_mcp_server_status_request(ListMcpServerStatusParams {
cursor: None,
limit: None,
detail: None,
thread_id: Some(model_selected_thread.clone()),
})
.await?;
let response = timeout(
DEFAULT_TIMEOUT,
app_server.read_stream_until_response_message(RequestId::Integer(request_id)),
)
.await??;
let response: ListMcpServerStatusResponse = to_response(response)?;
assert!(response.data.iter().all(|server| server.name != APP_NAME));
let selected_description =
model_tool_description(&mut app_server, &responses_server, &model_selected_thread).await?;
let unselected_description =
+19 -2
View File
@@ -10,6 +10,7 @@ use codex_plugin::PluginCapabilitySummary;
pub struct PluginConnectorSource {
plugin_id: String,
plugin_display_name: String,
app_names: HashSet<String>,
connector_ids: Vec<AppConnectorId>,
}
@@ -20,13 +21,21 @@ impl PluginConnectorSource {
plugin_display_name: impl Into<String>,
declarations: impl IntoIterator<Item = AppDeclaration>,
) -> Self {
Self::from_connector_ids(
let declarations = declarations.into_iter().collect::<Vec<_>>();
let app_names = declarations
.iter()
.filter(|declaration| !declaration.connector_id.0.trim().is_empty())
.map(|declaration| declaration.name.clone())
.collect();
let mut source = Self::from_connector_ids(
plugin_id,
plugin_display_name,
declarations
.into_iter()
.map(|declaration| declaration.connector_id),
)
);
source.app_names = app_names;
source
}
/// Creates one plugin source from connector IDs that were already parsed.
@@ -44,6 +53,7 @@ impl PluginConnectorSource {
Self {
plugin_id: plugin_id.into(),
plugin_display_name: plugin_display_name.into(),
app_names: HashSet::new(),
connector_ids,
}
}
@@ -125,6 +135,13 @@ impl ConnectorSnapshot {
.unwrap_or_default()
}
/// Returns whether one package declares an app with the given logical name.
pub fn plugin_declares_app(&self, plugin_id: &str, app_name: &str) -> bool {
self.sources
.iter()
.any(|source| source.plugin_id == plugin_id && source.app_names.contains(app_name))
}
/// Combines two snapshots while preserving source order and provenance.
pub fn merged_with(&self, other: &Self) -> Self {
Self::from_plugin_sources(self.sources.iter().chain(&other.sources).cloned())
+22 -8
View File
@@ -5,6 +5,7 @@ use crate::config::Config;
use codex_config::McpServerConfig;
use codex_connectors::ConnectorSnapshot;
use codex_core_plugins::PluginsManager;
use codex_core_plugins::apps_route_available;
use codex_extension_api::ExtensionDataInit;
use codex_extension_api::ExtensionRegistry;
use codex_extension_api::McpServerContribution;
@@ -96,6 +97,8 @@ impl McpManager {
Some(thread_init) => McpServerContributionContext::for_thread(config, thread_init),
None => McpServerContributionContext::global(config),
};
let selected_connector_snapshot =
thread_init.and_then(ExtensionDataInit::get::<ConnectorSnapshot>);
let mut selected_plugin_registrations = Vec::new();
let mut overlays = Vec::new();
// A contributor can emit multiple ordered actions, so order each action globally rather
@@ -118,14 +121,25 @@ impl McpManager {
plugin_display_name,
selection_order,
config,
} => selected_plugin_registrations.push(
McpServerRegistration::from_selected_plugin(
name,
McpPluginAttribution::new(plugin_id, plugin_display_name),
selection_order,
*config,
),
),
} => {
let routed_through_app =
apps_route_available(self.plugins_manager.auth_mode())
&& selected_connector_snapshot
.as_ref()
.is_some_and(|snapshot| {
snapshot.plugin_declares_app(&plugin_id, &name)
});
if !routed_through_app {
selected_plugin_registrations.push(
McpServerRegistration::from_selected_plugin(
name,
McpPluginAttribution::new(plugin_id, plugin_display_name),
selection_order,
*config,
),
);
}
}
McpServerContribution::Remove { name } => {
overlays.push(OrderedMcpOverlay::Remove {
contributor_id: contributor.id(),