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
8 changes: 8 additions & 0 deletions codex-rs/Cargo.lock

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/core-plugins/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,4 @@ pub use marketplace_upgrade::ConfiguredMarketplaceUpgradeError as PluginMarketpl
pub use marketplace_upgrade::ConfiguredMarketplaceUpgradeOutcome as PluginMarketplaceUpgradeOutcome;
pub use provider::ExecutorPluginProvider;
pub use provider::ExecutorPluginProviderError;
pub use provider::ResolvedExecutorPlugin;
9 changes: 1 addition & 8 deletions codex-rs/core-plugins/src/loader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -662,14 +662,7 @@ async fn load_plugin(
restriction_product,
skill_config_rules,
} => {
loaded_plugin.manifest_name = manifest
.interface
.as_ref()
.and_then(|interface| interface.display_name.as_deref())
.map(str::trim)
.filter(|display_name| !display_name.is_empty())
.map(str::to_string)
.or_else(|| Some(manifest.name.clone()));
loaded_plugin.manifest_name = Some(manifest.display_name().to_string());
loaded_plugin.manifest_description = manifest.description.clone();
loaded_plugin.skill_roots = plugin_skill_roots(&plugin_root, manifest_paths);
let resolved_skills = load_plugin_skills(
Expand Down
47 changes: 40 additions & 7 deletions codex-rs/core-plugins/src/provider.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,22 +77,38 @@ pub struct ExecutorPluginProvider {
environment_manager: Arc<EnvironmentManager>,
}

/// A resolved plugin paired with the concrete filesystem used to read it.
#[derive(Clone)]
pub struct ResolvedExecutorPlugin {
plugin: ResolvedPlugin,
file_system: Arc<dyn ExecutorFileSystem>,
}

impl ResolvedExecutorPlugin {
/// Returns the source-neutral plugin descriptor.
pub fn plugin(&self) -> &ResolvedPlugin {
&self.plugin
}

/// Returns the concrete filesystem that resolved the descriptor.
pub fn file_system(&self) -> &dyn ExecutorFileSystem {
self.file_system.as_ref()
}
}

impl ExecutorPluginProvider {
/// Creates a provider backed by the active execution environments.
pub fn new(environment_manager: Arc<EnvironmentManager>) -> Self {
Self {
environment_manager,
}
}
}

impl PluginProvider for ExecutorPluginProvider {
type Error = ExecutorPluginProviderError;

async fn resolve(
/// Resolves a plugin and retains the exact filesystem used for package access.
pub async fn resolve_bound(
&self,
selected_root: &SelectedCapabilityRoot,
) -> Result<Option<ResolvedPlugin>, Self::Error> {
) -> Result<Option<ResolvedExecutorPlugin>, ExecutorPluginProviderError> {
let root_id = &selected_root.id;
let plugin_root = selected_plugin_root(selected_root)?;
let CapabilityRootLocation::Environment { environment_id, .. } = &selected_root.location;
Expand All @@ -104,8 +120,25 @@ impl PluginProvider for ExecutorPluginProvider {
environment_id: environment_id.clone(),
})?;
let file_system = environment.get_filesystem();
let plugin = resolve_plugin_root(selected_root, plugin_root, file_system.as_ref()).await?;

Ok(plugin.map(|plugin| ResolvedExecutorPlugin {
plugin,
file_system,
}))
}
}

impl PluginProvider for ExecutorPluginProvider {
type Error = ExecutorPluginProviderError;

resolve_plugin_root(selected_root, plugin_root, file_system.as_ref()).await
async fn resolve(
&self,
selected_root: &SelectedCapabilityRoot,
) -> Result<Option<ResolvedPlugin>, Self::Error> {
self.resolve_bound(selected_root)
.await
.map(|plugin| plugin.map(|plugin| plugin.plugin))
}
}

Expand Down
14 changes: 11 additions & 3 deletions codex-rs/ext/mcp/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,29 @@ version.workspace = true
[lib]
name = "codex_mcp_extension"
path = "src/lib.rs"
test = false
doctest = false

[lints]
workspace = true

[dependencies]
codex-core = { workspace = true }
codex-core-plugins = { workspace = true }
Comment thread
jif-oai marked this conversation as resolved.
Comment thread
jif-oai marked this conversation as resolved.
codex-config = { workspace = true }
codex-exec-server = { workspace = true }
Comment thread
jif-oai marked this conversation as resolved.
codex-extension-api = { workspace = true }
codex-features = { workspace = true }
codex-mcp = { workspace = true }
codex-plugin = { workspace = true }
Comment thread
jif-oai marked this conversation as resolved.
codex-protocol = { workspace = true }
codex-utils-absolute-path = { workspace = true }
codex-utils-path-uri = { workspace = true }
serde_json = { workspace = true }
thiserror = { workspace = true }
tracing = { workspace = true }
tokio = { workspace = true, features = ["sync"] }

[dev-dependencies]
codex-config = { workspace = true }
codex-core-plugins = { workspace = true }
codex-login = { workspace = true }
pretty_assertions = { workspace = true }
tempfile = { workspace = true }
Expand Down
139 changes: 139 additions & 0 deletions codex-rs/ext/mcp/src/executor_plugin.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
use codex_core::config::Config;
use codex_core_plugins::ExecutorPluginProvider;
use codex_exec_server::EnvironmentManager;
use codex_extension_api::ExtensionDataInit;
use codex_extension_api::ExtensionFuture;
use codex_extension_api::McpServerContribution;
use codex_extension_api::McpServerContributionContext;
use codex_extension_api::McpServerContributor;
use codex_protocol::capabilities::SelectedCapabilityRoot;
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::OnceCell;

use self::provider::ExecutorPluginMcpProvider;

mod provider;

/// Frozen MCP declarations for one selected package.
///
/// Each server config retains the stable logical environment ID. Reconnection may replace the
/// concrete environment instance without changing that authority.
#[derive(Clone)]
struct SelectedPluginMcpServers {
plugin_id: String,
plugin_display_name: String,
selection_order: usize,
servers: Vec<(String, codex_config::McpServerConfig)>,
}

#[derive(Default)]
pub(crate) struct SelectedExecutorPluginMcpState {
snapshot: OnceCell<Vec<SelectedPluginMcpServers>>,
}

pub(crate) fn seed_thread_state(thread_init: &mut ExtensionDataInit) {
thread_init.insert(SelectedExecutorPluginMcpState::default());
}

pub(crate) struct SelectedExecutorPluginMcpContributor {
plugin_provider: ExecutorPluginProvider,
mcp_provider: ExecutorPluginMcpProvider,
}

impl SelectedExecutorPluginMcpContributor {
pub(crate) fn new(environment_manager: Arc<EnvironmentManager>) -> Self {
Self {
plugin_provider: ExecutorPluginProvider::new(Arc::clone(&environment_manager)),
mcp_provider: ExecutorPluginMcpProvider,
}
}

async fn resolve_snapshot(
&self,
selected_roots: &[SelectedCapabilityRoot],
) -> Vec<SelectedPluginMcpServers> {
let mut snapshot = Vec::new();

for (selection_order, selected_root) in selected_roots.iter().enumerate() {
let plugin = match self.plugin_provider.resolve_bound(selected_root).await {
Ok(Some(plugin)) => plugin,
Ok(None) => continue,
Err(err) => {
tracing::warn!(
selected_root = selected_root.id,
error = %err,
"failed to resolve selected executor plugin for MCP discovery"
);
continue;
}
};
match self.mcp_provider.load(&plugin).await {
Ok(servers) => snapshot.push(SelectedPluginMcpServers {
plugin_id: plugin.plugin().selected_root_id().to_string(),
plugin_display_name: plugin.plugin().manifest().display_name().to_string(),
selection_order,
servers,
}),
Err(err) => {
tracing::warn!(
selected_root = selected_root.id,
error = %err,
"failed to load selected executor plugin MCP servers"
);
}
}
}

snapshot
}
}

impl McpServerContributor<Config> for SelectedExecutorPluginMcpContributor {
fn id(&self) -> &'static str {
"selected_executor_plugin_mcp"
}

fn contribute<'a>(
&'a self,
context: McpServerContributionContext<'a, Config>,
) -> ExtensionFuture<'a, Vec<McpServerContribution>> {
Box::pin(async move {
let Some(thread_init) = context.thread_init() else {
return Vec::new();
};
let Some(selected_roots) = thread_init.get::<Vec<SelectedCapabilityRoot>>() else {
return Vec::new();
};
let Some(state) = thread_init.get::<SelectedExecutorPluginMcpState>() else {
tracing::warn!("selected executor plugin MCP state was not initialized");
return Vec::new();
};
let snapshot = state
.snapshot
.get_or_init(|| self.resolve_snapshot(selected_roots.as_ref()))
.await;
let mut contributions = Vec::new();

for plugin in snapshot {
let mut servers = plugin.servers.iter().cloned().collect::<HashMap<_, _>>();
context
.config()
.apply_plugin_mcp_server_requirements(&plugin.plugin_id, &mut servers);
let mut servers = servers.into_iter().collect::<Vec<_>>();
servers.sort_unstable_by(|left, right| left.0.cmp(&right.0));
contributions.extend(servers.into_iter().map(|(name, config)| {
McpServerContribution::SelectedPlugin {
name,
plugin_id: plugin.plugin_id.clone(),
plugin_display_name: plugin.plugin_display_name.clone(),
selection_order: plugin.selection_order,
config: Box::new(config),
}
}));
}

contributions
})
}
}
Loading
Loading