Activate selected executor plugin MCPs in app-server (#27893)

## Why

#27870 teaches the MCP extension how to discover stdio MCP servers
declared by a selected executor plugin, but app-server does not yet
install that contributor or initialize its per-thread state. As a
result, `thread/start.selectedCapabilityRoots` can select the plugin
while its MCP servers remain inactive.

This PR closes that app-server wiring gap:

```text
thread/start(selectedCapabilityRoots)
    -> initialize the thread's selected-plugin MCP snapshot
    -> read the selected plugin's .mcp.json through its environment
    -> start declared stdio servers in that environment
    -> expose their tools only on the selected thread
```

## What changed

- Install the selected-executor-plugin MCP contributor in app-server
using the existing shared `EnvironmentManager`.
- Initialize its frozen thread snapshot when `thread/start` includes
selected capability roots.
- Document that selected plugin stdio MCPs are activated in their owning
environment.
- Add an app-server E2E covering the complete selection-to-tool-call
path.

The E2E verifies that:

- the selected MCP process receives an executor-only environment value,
proving the tool runs through the selected environment;
- the MCP tool is advertised to the model and can be called;
- a normal MCP config reload does not discard the thread's frozen
selected-plugin registration;
- another thread without the selected root does not see the MCP server.

## Scope

- Existing sessions without `selectedCapabilityRoots` are unchanged.
- Only stdio MCP declarations are activated. HTTP declarations remain
inactive.
- This does not change selected-root persistence across resume/fork or
add hosted-plugin behavior.

## Verification

- Focused app-server E2E:
`selected_executor_plugin_exposes_its_stdio_mcp_only_to_that_thread`

## Stack

Stacked on #27870.
This commit is contained in:
jif
2026-06-15 15:23:37 +01:00
committed by GitHub
Unverified
parent e5253b97fb
commit c8c78b63a7
10 changed files with 321 additions and 20 deletions
+7 -8
View File
@@ -9,6 +9,7 @@ use codex_core::NewThread;
use codex_core::StartThreadOptions;
use codex_core::ThreadManager;
use codex_core::config::Config;
use codex_exec_server::EnvironmentManager;
use codex_extension_api::AgentSpawnFuture;
use codex_extension_api::AgentSpawner;
use codex_extension_api::ExtensionEventSink;
@@ -27,9 +28,6 @@ use crate::outgoing_message::OutgoingMessageSender;
use crate::thread_state::ThreadListenerCommand;
use crate::thread_state::ThreadStateManager;
// TODO(jif): Enable once /ps/mcp serves complete hosted skill packages.
const ORCHESTRATOR_SKILLS_ENABLED: bool = false;
pub(crate) struct ThreadExtensionDependencies {
pub(crate) event_sink: Arc<dyn ExtensionEventSink>,
pub(crate) auth_manager: Arc<AuthManager>,
@@ -37,6 +35,7 @@ pub(crate) struct ThreadExtensionDependencies {
pub(crate) analytics_events_client: AnalyticsEventsClient,
pub(crate) thread_manager: Weak<ThreadManager>,
pub(crate) goal_service: Arc<GoalService>,
pub(crate) environment_manager: Arc<EnvironmentManager>,
pub(crate) executor_skill_provider: Arc<dyn codex_skills_extension::SkillProvider>,
/// Process-scoped persistence backend for extensions that need stored thread history.
pub(crate) thread_store: Arc<dyn ThreadStore>,
@@ -56,6 +55,7 @@ where
analytics_events_client,
thread_manager,
goal_service,
environment_manager,
executor_skill_provider,
thread_store: _thread_store,
} = dependencies;
@@ -74,15 +74,14 @@ where
codex_guardian::install(&mut builder, guardian_agent_spawner);
codex_memories_extension::install(&mut builder, codex_otel::global());
codex_mcp_extension::install(&mut builder);
codex_mcp_extension::install_executor_plugins(&mut builder, environment_manager);
codex_web_search_extension::install(&mut builder, auth_manager.clone());
codex_image_generation_extension::install(&mut builder, auth_manager);
let mut skill_providers = codex_skills_extension::SkillProviders::new()
.with_executor_provider(executor_skill_provider);
if ORCHESTRATOR_SKILLS_ENABLED {
skill_providers = skill_providers.with_orchestrator_provider(Arc::new(
let skill_providers = codex_skills_extension::SkillProviders::new()
.with_executor_provider(executor_skill_provider)
.with_orchestrator_provider(Arc::new(
codex_skills_extension::OrchestratorSkillProvider::new(),
));
}
codex_skills_extension::install_with_providers(
&mut builder,
skill_providers,
+1
View File
@@ -237,6 +237,7 @@ mod tests {
analytics_events_client: codex_analytics::AnalyticsEventsClient::disabled(),
thread_manager: thread_manager.clone(),
goal_service: Arc::new(codex_goal_extension::GoalService::new()),
environment_manager: Arc::clone(&environment_manager),
executor_skill_provider: Arc::clone(&executor_skill_provider),
thread_store: Arc::clone(&thread_store),
},
+2 -1
View File
@@ -329,7 +329,7 @@ impl MessageProcessor {
let restriction_product = session_source.restriction_product();
let executor_skill_provider: Arc<dyn codex_skills_extension::SkillProvider> = Arc::new(
codex_skills_extension::ExecutorSkillProvider::new_with_restriction_product(
environment_manager_for_extensions,
Arc::clone(&environment_manager_for_extensions),
restriction_product,
),
);
@@ -352,6 +352,7 @@ impl MessageProcessor {
analytics_events_client: analytics_events_client.clone(),
thread_manager: thread_manager.clone(),
goal_service: Arc::clone(&goal_service),
environment_manager: Arc::clone(&environment_manager_for_extensions),
executor_skill_provider: Arc::clone(&executor_skill_provider),
thread_store: Arc::clone(&thread_store),
},
@@ -1096,6 +1096,7 @@ impl ThreadRequestProcessor {
let mut thread_extension_init = ExtensionDataInit::new();
if !selected_capability_roots.is_empty() {
thread_extension_init.insert(selected_capability_roots);
codex_mcp_extension::initialize_executor_plugin_thread_data(&mut thread_extension_init);
}
let create_thread_started_at = std::time::Instant::now();
let NewThread {