mirror of
https://github.com/openai/codex.git
synced 2026-10-19 17:28:46 +00:00
## Why App-server clients need to advertise structured MCP extension settings, including supported MCP App UI MIME types, rather than only opting into OpenAI form elicitation with a boolean. ## What changed - Add an `extensions` map to initialize capabilities and preserve `mcpServerOpenaiFormElicitation` as a legacy alias for `openai/form`. - Capture the declared extension profile when a thread is started, resumed, or forked, propagate it to subagents, and advertise it to downstream MCP servers during initialization. - Keep the profile stable for the lifetime of the loaded session instead of changing it on later turns or direct tool calls. ## Testing - Cover extension conversion, downstream MCP initialization, session isolation, legacy form support, and subagent inheritance. GitOrigin-RevId: fbcedbb74ce788e574b0f884a4c45c4cedb9de54
193 lines
7.3 KiB
Rust
193 lines
7.3 KiB
Rust
//! Thread MCP runtime projection and publication.
|
|
//!
|
|
//! This module owns the small correctness boundary between immutable session
|
|
//! inputs and the mutable [`codex_mcp::McpRuntime`]. Background scheduling
|
|
//! belongs elsewhere.
|
|
|
|
use super::session::SessionConfiguration;
|
|
use super::*;
|
|
use crate::mcp::McpRuntimeProjection;
|
|
use codex_mcp::ElicitationReviewerHandle;
|
|
use codex_mcp::PreparedMcpCall;
|
|
use codex_protocol::capabilities::SelectedCapabilityRoot;
|
|
|
|
pub(super) struct McpDesiredState {
|
|
pub(super) config: Arc<Config>,
|
|
pub(super) auth: Option<CodexAuth>,
|
|
pub(super) submit_id: String,
|
|
pub(super) originator: String,
|
|
pub(super) environments: TurnEnvironmentSnapshot,
|
|
pub(super) windows_sandbox_level: WindowsSandboxLevel,
|
|
}
|
|
|
|
impl McpDesiredState {
|
|
pub(super) fn local_stdio_fallback_cwd(&self) -> PathBuf {
|
|
self.environments
|
|
.primary()
|
|
.and_then(|environment| environment.cwd().to_abs_path().ok())
|
|
.map(|cwd| cwd.to_path_buf())
|
|
.unwrap_or_else(|| self.config.cwd.to_path_buf())
|
|
}
|
|
}
|
|
|
|
impl Session {
|
|
/// Waits on this session's refreshed server before tool execution is admitted.
|
|
pub(crate) async fn wait_for_mcp_server(self: &Arc<Self>, server: &str) {
|
|
self.refresh_mcp_if_dirty().await;
|
|
self.services
|
|
.mcp_runtime
|
|
.wait_for_server_startup(server)
|
|
.await;
|
|
}
|
|
|
|
/// Captures this session's current MCP client and catalog for one tool call.
|
|
pub(crate) async fn prepare_mcp_call(
|
|
self: &Arc<Self>,
|
|
server: &str,
|
|
tool: &str,
|
|
) -> Option<PreparedMcpCall> {
|
|
self.refresh_mcp_if_dirty().await;
|
|
self.services
|
|
.mcp_runtime
|
|
.current_binding_for_call(server)
|
|
.await?
|
|
.prepare_call(server, tool)
|
|
}
|
|
|
|
pub(super) async fn latest_mcp_desired_state(
|
|
&self,
|
|
auth: Option<CodexAuth>,
|
|
) -> McpDesiredState {
|
|
let session_configuration = {
|
|
let state = self.state.lock().await;
|
|
state.session_configuration.clone()
|
|
};
|
|
let environments = self.services.turn_environments.snapshot().await;
|
|
let cwd = environments
|
|
.primary()
|
|
.and_then(|environment| environment.cwd().to_abs_path().ok())
|
|
.unwrap_or_else(|| session_configuration.cwd().clone());
|
|
let config = Self::build_per_turn_config(&session_configuration, cwd);
|
|
|
|
McpDesiredState {
|
|
config: Arc::new(config),
|
|
auth,
|
|
submit_id: self.next_internal_sub_id(),
|
|
originator: session_configuration.originator.clone(),
|
|
environments,
|
|
windows_sandbox_level: session_configuration.windows_sandbox_level,
|
|
}
|
|
}
|
|
|
|
pub(super) async fn install_initial_mcp_runtime(
|
|
self: &Arc<Self>,
|
|
session_configuration: &SessionConfiguration,
|
|
auth: Option<CodexAuth>,
|
|
mcp_projection: McpRuntimeProjection,
|
|
resolved_environments: &TurnEnvironmentSnapshot,
|
|
local_stdio_fallback_cwd: PathBuf,
|
|
) -> anyhow::Result<()> {
|
|
let cwd = AbsolutePathBuf::from_absolute_path(local_stdio_fallback_cwd)
|
|
.unwrap_or_else(|_| session_configuration.cwd().clone());
|
|
let config = Self::build_per_turn_config(session_configuration, cwd);
|
|
let desired = McpDesiredState {
|
|
config: Arc::new(config),
|
|
auth,
|
|
submit_id: INITIAL_SUBMIT_ID.to_owned(),
|
|
originator: session_configuration.originator.clone(),
|
|
environments: resolved_environments.clone(),
|
|
windows_sandbox_level: session_configuration.windows_sandbox_level,
|
|
};
|
|
self.publish_mcp_runtime(
|
|
&desired,
|
|
mcp_projection,
|
|
/*ready_selected_capability_roots*/ &[],
|
|
Some(self.mcp_elicitation_reviewer()),
|
|
)
|
|
.instrument(info_span!(
|
|
"session_init.mcp_manager_init",
|
|
otel.name = "session_init.mcp_manager_init",
|
|
))
|
|
.await;
|
|
|
|
self.services.mcp_runtime.validate_required_servers().await
|
|
}
|
|
|
|
#[tracing::instrument(name = "mcp.runtime.refresh", skip_all)]
|
|
pub(super) async fn publish_mcp_runtime(
|
|
&self,
|
|
desired: &McpDesiredState,
|
|
mcp_projection: McpRuntimeProjection,
|
|
ready_selected_capability_roots: &[SelectedCapabilityRoot],
|
|
elicitation_reviewer: Option<ElicitationReviewerHandle>,
|
|
) {
|
|
let input = self.build_mcp_runtime_input(
|
|
desired,
|
|
mcp_projection,
|
|
ready_selected_capability_roots,
|
|
elicitation_reviewer,
|
|
);
|
|
self.services.mcp_runtime.replace(input).await;
|
|
}
|
|
|
|
pub(super) fn build_mcp_runtime_input(
|
|
&self,
|
|
desired: &McpDesiredState,
|
|
mcp_projection: McpRuntimeProjection,
|
|
ready_selected_capability_roots: &[SelectedCapabilityRoot],
|
|
elicitation_reviewer: Option<ElicitationReviewerHandle>,
|
|
) -> McpRuntimeInput {
|
|
let auth = desired.auth.clone();
|
|
let McpRuntimeProjection {
|
|
mut config,
|
|
plugins_available,
|
|
} = mcp_projection;
|
|
config.approval_policy = desired.config.permissions.approval_policy.clone();
|
|
config.permission_profile = desired.config.permissions.effective_permission_profile();
|
|
config.approvals_reviewer = desired.config.approvals_reviewer;
|
|
config.environment_cwds = desired
|
|
.environments
|
|
.turn_environments()
|
|
.map(|environment| {
|
|
(
|
|
environment.environment_id.clone(),
|
|
environment.cwd().clone(),
|
|
)
|
|
})
|
|
.collect();
|
|
config
|
|
.environment_cwds
|
|
.entry(codex_config::DEFAULT_MCP_SERVER_ENVIRONMENT_ID.to_string())
|
|
.or_insert_with(|| PathUri::from_abs_path(&desired.config.cwd));
|
|
let mcp_config = Arc::new(config);
|
|
let mcp_servers = effective_mcp_servers(&mcp_config, auth.as_ref());
|
|
let local_stdio_fallback_cwd = desired.local_stdio_fallback_cwd();
|
|
let runtime_context = McpRuntimeContext::new(
|
|
self.services.turn_environments.environment_manager(),
|
|
local_stdio_fallback_cwd,
|
|
);
|
|
let codex_apps_auth_manager =
|
|
codex_mcp::host_owned_codex_apps_enabled(&mcp_config, auth.as_ref())
|
|
.then(|| Arc::clone(&self.services.auth_manager));
|
|
|
|
McpRuntimeInput {
|
|
config: mcp_config,
|
|
plugins_available,
|
|
ready_selected_capability_roots: ready_selected_capability_roots.to_vec(),
|
|
mcp_servers,
|
|
submit_id: desired.submit_id.clone(),
|
|
tx_event: Some(self.get_tx_event()),
|
|
startup_cancellation_token: CancellationToken::new(),
|
|
runtime_context,
|
|
codex_apps_tools_cache: self.services.mcp_manager.codex_apps_tools_cache(),
|
|
tool_catalog_cache: self.services.mcp_manager.tool_catalog_cache(),
|
|
codex_apps_tools_cache_key: connector_runtime_context_key(auth.as_ref()),
|
|
client_mcp_extensions: self.services.client_mcp_extensions.clone(),
|
|
auth,
|
|
codex_apps_auth_manager,
|
|
elicitation_reviewer,
|
|
elicitation_lifecycle: Some(self.mcp_elicitation_lifecycle()),
|
|
}
|
|
}
|
|
}
|