mirror of
https://github.com/openai/codex.git
synced 2026-11-05 16:29:31 +00:00
Negotiate MCP extensions per app-server session (#36910)
## 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
This commit is contained in:
1 parent
bab7c2dcc1
commit
ee46c5ba0e
61 files changed
+729
-284
No files matched your search
@@ -17,6 +17,7 @@ use codex_core::spawn::CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR;
|
||||
use codex_protocol::config_types::CollaborationMode;
|
||||
use codex_protocol::config_types::ModeKind;
|
||||
use codex_protocol::config_types::Settings;
|
||||
use codex_protocol::mcp::ClientMcpExtensions;
|
||||
use codex_protocol::protocol::EventMsg;
|
||||
use codex_protocol::protocol::Op;
|
||||
use codex_protocol::protocol::WarningEvent;
|
||||
@@ -830,7 +831,7 @@ async fn resume_conversation(
|
||||
path,
|
||||
auth_manager,
|
||||
/*parent_trace*/ None,
|
||||
/*supports_openai_form_elicitation*/ false,
|
||||
ClientMcpExtensions::default(),
|
||||
))
|
||||
.await
|
||||
.expect("resume conversation")
|
||||
|
||||
@@ -5,6 +5,7 @@ use codex_core::NewThread;
|
||||
use codex_core::ThreadConfigSnapshot;
|
||||
use codex_core::parse_turn_item;
|
||||
use codex_protocol::items::TurnItem;
|
||||
use codex_protocol::mcp::ClientMcpExtensions;
|
||||
use codex_protocol::protocol::EventMsg;
|
||||
use codex_protocol::protocol::InitialHistory;
|
||||
use codex_protocol::protocol::Op;
|
||||
@@ -238,7 +239,7 @@ async fn assert_copied_fork_persists_inherited_history(history_mode: ThreadHisto
|
||||
}),
|
||||
/*thread_source*/ None,
|
||||
/*parent_trace*/ None,
|
||||
/*supports_openai_form_elicitation*/ false,
|
||||
ClientMcpExtensions::default(),
|
||||
)
|
||||
.await
|
||||
.expect("fork from stored history");
|
||||
@@ -274,7 +275,7 @@ async fn assert_copied_fork_persists_inherited_history(history_mode: ThreadHisto
|
||||
codex_login::CodexAuth::from_api_key("dummy"),
|
||||
),
|
||||
/*parent_trace*/ None,
|
||||
/*supports_openai_form_elicitation*/ false,
|
||||
ClientMcpExtensions::default(),
|
||||
)
|
||||
.await
|
||||
.expect("resume copied paginated fork")
|
||||
|
||||
@@ -17,6 +17,7 @@ use codex_mcp::McpRuntime;
|
||||
use codex_mcp::McpRuntimeContext;
|
||||
use codex_mcp::McpRuntimeInput;
|
||||
use codex_mcp::McpToolCatalogCache;
|
||||
use codex_protocol::mcp::ClientMcpExtensions;
|
||||
use codex_protocol::protocol::AskForApproval;
|
||||
use core_test_support::apps_test_server::AppsTestServer;
|
||||
use core_test_support::responses::start_mock_server;
|
||||
@@ -102,7 +103,7 @@ async fn hosted_plugin_runtime_ps_mcp_tool_calls_use_current_auth_manager_token(
|
||||
codex_apps_tools_cache: CodexAppsToolsCache::default(),
|
||||
tool_catalog_cache: McpToolCatalogCache::default(),
|
||||
codex_apps_tools_cache_key: codex_mcp::codex_apps_tools_cache_key(Some(&expected_auth)),
|
||||
supports_openai_form_elicitation: false,
|
||||
client_mcp_extensions: ClientMcpExtensions::default(),
|
||||
auth: Some(expected_auth.clone()),
|
||||
codex_apps_auth_manager: Some(Arc::clone(&auth_manager)),
|
||||
elicitation_reviewer: None,
|
||||
|
||||
@@ -6,6 +6,7 @@ use std::time::Duration;
|
||||
use codex_config::DEFAULT_MCP_SERVER_ENVIRONMENT_ID;
|
||||
use codex_config::types::McpServerConfig;
|
||||
use codex_config::types::McpServerTransportConfig;
|
||||
use codex_protocol::protocol::Op;
|
||||
use core_test_support::process::process_is_alive;
|
||||
use core_test_support::process::wait_for_pid_file;
|
||||
use core_test_support::process::wait_for_process_exit;
|
||||
@@ -112,10 +113,7 @@ async fn refresh_keeps_superseded_mcp_server_alive_for_in_flight_calls() -> anyh
|
||||
]),
|
||||
)
|
||||
.await;
|
||||
fixture
|
||||
.codex
|
||||
.set_openai_form_elicitation_support(/*supported*/ true)
|
||||
.await?;
|
||||
fixture.codex.submit(Op::RefreshMcpServers).await?;
|
||||
fixture.submit_turn("refresh MCP servers").await?;
|
||||
|
||||
let replacement_pid = wait_for_pid_file(&pid_file).await?;
|
||||
|
||||
@@ -6,6 +6,7 @@ use codex_core::test_support::auth_manager_from_auth;
|
||||
use codex_login::CodexAuth;
|
||||
use codex_login::OPENAI_API_KEY_ENV_VAR;
|
||||
use codex_protocol::ThreadId;
|
||||
use codex_protocol::mcp::ClientMcpExtensions;
|
||||
use codex_protocol::models::ContentItem;
|
||||
use codex_protocol::models::ResponseItem;
|
||||
use codex_protocol::protocol::CodexErrorInfo;
|
||||
@@ -2726,7 +2727,7 @@ async fn conversation_startup_context_current_thread_selects_many_turns_by_budge
|
||||
InitialHistory::Forked(history),
|
||||
auth_manager_from_auth(CodexAuth::from_api_key("dummy")),
|
||||
/*parent_trace*/ None,
|
||||
/*supports_openai_form_elicitation*/ false,
|
||||
ClientMcpExtensions::default(),
|
||||
)
|
||||
.await?;
|
||||
let codex = resumed_thread.thread;
|
||||
|
||||
@@ -7,6 +7,7 @@ use codex_login::CodexAuth;
|
||||
use codex_protocol::ThreadId;
|
||||
use codex_protocol::config_types::ModeKind;
|
||||
use codex_protocol::config_types::ReasoningSummary;
|
||||
use codex_protocol::mcp::ClientMcpExtensions;
|
||||
use codex_protocol::protocol::EventMsg;
|
||||
use codex_protocol::protocol::InitialHistory;
|
||||
use codex_protocol::protocol::ResumedHistory;
|
||||
@@ -116,7 +117,7 @@ async fn emits_warning_when_resumed_model_differs() {
|
||||
initial_history,
|
||||
auth_manager,
|
||||
/*parent_trace*/ None,
|
||||
/*supports_openai_form_elicitation*/ false,
|
||||
ClientMcpExtensions::default(),
|
||||
)
|
||||
.await
|
||||
.expect("resume conversation");
|
||||
|
||||
@@ -426,58 +426,6 @@ async fn openai_form_capability_is_not_advertised_by_default() -> anyhow::Result
|
||||
assert_openai_form_capability_advertisement(/*expected*/ false).await
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
|
||||
async fn openai_form_capability_updates_for_loaded_thread() -> anyhow::Result<()> {
|
||||
skip_if_wine_exec!(
|
||||
Ok(()),
|
||||
"requires a Windows test_stdio_server in the Wine-exec environment"
|
||||
);
|
||||
|
||||
let server = start_mock_server().await;
|
||||
let server_name = "capabilities";
|
||||
let command = stdio_server_bin()?;
|
||||
let fixture = test_codex()
|
||||
.with_config(move |config| {
|
||||
insert_mcp_server(
|
||||
config,
|
||||
server_name,
|
||||
stdio_transport(command, /*env*/ None, Vec::new()),
|
||||
TestMcpServerOptions::default(),
|
||||
);
|
||||
})
|
||||
.build(&server)
|
||||
.await?;
|
||||
wait_for_mcp_server(&fixture.codex, server_name).await?;
|
||||
|
||||
let unsupported = call_structured_tool(
|
||||
&server,
|
||||
&fixture,
|
||||
server_name,
|
||||
"client_capabilities",
|
||||
"call-client-capabilities-unsupported",
|
||||
)
|
||||
.await?;
|
||||
assert_eq!(
|
||||
unsupported,
|
||||
json!({ "supportsOpenaiFormElicitation": false })
|
||||
);
|
||||
|
||||
fixture
|
||||
.codex
|
||||
.set_openai_form_elicitation_support(/*supported*/ true)
|
||||
.await?;
|
||||
let supported = call_structured_tool(
|
||||
&server,
|
||||
&fixture,
|
||||
server_name,
|
||||
"client_capabilities",
|
||||
"call-client-capabilities-supported",
|
||||
)
|
||||
.await?;
|
||||
assert_eq!(supported, json!({ "supportsOpenaiFormElicitation": true }));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn assert_openai_form_capability_advertisement(expected: bool) -> anyhow::Result<()> {
|
||||
skip_if_wine_exec!(
|
||||
Ok(()),
|
||||
|
||||
@@ -4,6 +4,7 @@ use codex_config::CONFIG_TOML_FILE;
|
||||
use codex_core::NewThread;
|
||||
use codex_features::Feature;
|
||||
use codex_login::CodexAuth;
|
||||
use codex_protocol::mcp::ClientMcpExtensions;
|
||||
use codex_protocol::protocol::EventMsg;
|
||||
use codex_protocol::protocol::InitialHistory;
|
||||
use codex_protocol::protocol::WarningEvent;
|
||||
@@ -50,7 +51,7 @@ async fn emits_warning_when_unstable_features_enabled_via_config() {
|
||||
InitialHistory::New,
|
||||
auth_manager,
|
||||
/*parent_trace*/ None,
|
||||
/*supports_openai_form_elicitation*/ false,
|
||||
ClientMcpExtensions::default(),
|
||||
)
|
||||
.await
|
||||
.expect("spawn conversation");
|
||||
@@ -100,7 +101,7 @@ async fn suppresses_warning_when_configured() {
|
||||
InitialHistory::New,
|
||||
auth_manager,
|
||||
/*parent_trace*/ None,
|
||||
/*supports_openai_form_elicitation*/ false,
|
||||
ClientMcpExtensions::default(),
|
||||
)
|
||||
.await
|
||||
.expect("spawn conversation");
|
||||
|
||||
Reference in new issue
Block a user