mirror of
https://github.com/openai/codex.git
synced 2026-11-13 14:46:14 +00:00
Give hosted Codex Apps an independent MCP protocol opt-in (#44318)
## Why Hosted Codex Apps shared the `mcp_2026_07_28` protocol setting with other MCP servers, preventing independent protocol selection. ## What changed - Add `features.codex_apps_mcp_2026_07_28`, disabled by default, for the host-owned HTTP `codex_apps` server. Enabling it attempts 2026-07-28 discovery with Legacy fallback. - Keep other servers, including third-party registrations with Apps-like names or URLs and local stdio servers, governed by `mcp_2026_07_28`. - Support the new flag through `experimentalFeature/enablement/set` and refresh protocol settings in existing sessions. Track protocol mode per server when deciding whether to reuse connections. ## Testing Add coverage for independent feature and protocol selection, hosted event-stream protocol selection, standalone Legacy defaults and discovery fallback, and runtime enablement refreshing an existing app-server thread. GitOrigin-RevId: 640e8e251f0c45e5f4829cc2b73c3082cf42f715
This commit is contained in:
1 parent
e1b23086ac
commit
eb680c0558
18 files changed
+431
-19
No files matched your search
@@ -0,0 +1,128 @@
|
||||
//! Verifies that a hosted Apps protocol opt-in refreshes an existing app-server thread.
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use anyhow::Result;
|
||||
use app_test_support::ChatGptAuthFixture;
|
||||
use app_test_support::MockResponsesConfig;
|
||||
use app_test_support::TestAppServer;
|
||||
use app_test_support::write_chatgpt_auth;
|
||||
use codex_app_server_protocol::ClientRequest;
|
||||
use codex_app_server_protocol::ExperimentalFeatureEnablementSetParams;
|
||||
use codex_app_server_protocol::ExperimentalFeatureEnablementSetResponse;
|
||||
use codex_app_server_protocol::McpServerToolCallParams;
|
||||
use codex_app_server_protocol::McpServerToolCallResponse;
|
||||
use codex_app_server_protocol::ThreadStartParams;
|
||||
use codex_config::types::AuthCredentialsStoreMode;
|
||||
use codex_features::Feature;
|
||||
use core_test_support::apps_test_server::AppsTestServer;
|
||||
use core_test_support::responses;
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde_json::Value;
|
||||
use serde_json::json;
|
||||
use tempfile::TempDir;
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn enabling_hosted_apps_protocol_refreshes_an_existing_thread() -> Result<()> {
|
||||
let server = responses::start_mock_server().await;
|
||||
let apps = AppsTestServer::mount(&server).await?;
|
||||
let codex_home = TempDir::new()?;
|
||||
MockResponsesConfig::new(&server.uri())
|
||||
.with_root_config(&format!("chatgpt_base_url = \"{}\"", apps.chatgpt_base_url))
|
||||
.enable_feature(Feature::Apps)
|
||||
.write(codex_home.path())?;
|
||||
write_chatgpt_auth(
|
||||
codex_home.path(),
|
||||
ChatGptAuthFixture::new("chatgpt-token")
|
||||
.account_id("account-123")
|
||||
.chatgpt_user_id("user-123")
|
||||
.chatgpt_account_id("account-123"),
|
||||
AuthCredentialsStoreMode::File,
|
||||
)?;
|
||||
let mut app_server = TestAppServer::builder()
|
||||
.with_codex_home(codex_home.path())
|
||||
.build_initialized()
|
||||
.await?;
|
||||
let thread_id = app_server
|
||||
.start_thread(ThreadStartParams::default())
|
||||
.await?
|
||||
.thread
|
||||
.id;
|
||||
|
||||
let call_tool = |thread_id: String| McpServerToolCallParams {
|
||||
thread_id,
|
||||
server: "codex_apps".to_string(),
|
||||
tool: "calendar_list_events".to_string(),
|
||||
arguments: Some(json!({ "query": "hello" })),
|
||||
meta: None,
|
||||
};
|
||||
let _: McpServerToolCallResponse = app_server
|
||||
.request(|request_id| ClientRequest::McpServerToolCall {
|
||||
request_id,
|
||||
params: call_tool(thread_id.clone()),
|
||||
})
|
||||
.await?;
|
||||
let methods = server
|
||||
.received_requests()
|
||||
.await
|
||||
.expect("mock server should capture Apps MCP startup requests")
|
||||
.into_iter()
|
||||
.filter(|request| request.url.path() == "/api/codex/ps/mcp")
|
||||
.filter_map(|request| {
|
||||
let body: Value = serde_json::from_slice(&request.body).ok()?;
|
||||
body.get("method")?.as_str().map(str::to_string)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(
|
||||
methods,
|
||||
[
|
||||
"initialize",
|
||||
"notifications/initialized",
|
||||
"tools/list",
|
||||
"tools/call"
|
||||
]
|
||||
);
|
||||
|
||||
let enablement = BTreeMap::from([("codex_apps_mcp_2026_07_28".to_string(), true)]);
|
||||
let request_id = app_server
|
||||
.send_experimental_feature_enablement_set_request(ExperimentalFeatureEnablementSetParams {
|
||||
enablement: enablement.clone(),
|
||||
})
|
||||
.await?;
|
||||
let response: ExperimentalFeatureEnablementSetResponse =
|
||||
app_server.read_response(request_id).await?;
|
||||
assert_eq!(response.enablement, enablement);
|
||||
|
||||
let _: McpServerToolCallResponse = app_server
|
||||
.request(|request_id| ClientRequest::McpServerToolCall {
|
||||
request_id,
|
||||
params: call_tool(thread_id),
|
||||
})
|
||||
.await?;
|
||||
let methods = server
|
||||
.received_requests()
|
||||
.await
|
||||
.expect("mock server should capture Apps MCP startup requests")
|
||||
.into_iter()
|
||||
.filter(|request| request.url.path() == "/api/codex/ps/mcp")
|
||||
.filter_map(|request| {
|
||||
let body: Value = serde_json::from_slice(&request.body).ok()?;
|
||||
body.get("method")?.as_str().map(str::to_string)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(
|
||||
methods,
|
||||
[
|
||||
"initialize",
|
||||
"notifications/initialized",
|
||||
"tools/list",
|
||||
"tools/call",
|
||||
"server/discover",
|
||||
"initialize",
|
||||
"notifications/initialized",
|
||||
"tools/list",
|
||||
"tools/call",
|
||||
]
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
@@ -1341,9 +1341,9 @@ impl ServerHandler for ToolAppsMcpServer {
|
||||
.get("threadId")
|
||||
.and_then(|value| value.as_str())
|
||||
.unwrap_or_default();
|
||||
let client_capabilities = context.peer.peer_info().map(|request| {
|
||||
let client_capabilities = context.client_capabilities().map(|capabilities| {
|
||||
json!({
|
||||
"extensions": request.capabilities.extensions.clone().unwrap_or_default(),
|
||||
"extensions": capabilities.extensions.unwrap_or_default(),
|
||||
})
|
||||
});
|
||||
|
||||
|
||||
@@ -53,6 +53,7 @@ mod marketplace_add;
|
||||
mod marketplace_remove;
|
||||
mod marketplace_upgrade;
|
||||
mod mcp_event_stream;
|
||||
mod mcp_protocol_default;
|
||||
mod mcp_resource;
|
||||
mod mcp_resource_origin;
|
||||
mod mcp_server_elicitation;
|
||||
|
||||
@@ -83,9 +83,8 @@ impl ServerHandler for VerificationServer {
|
||||
) -> Result<CallToolResponse, rmcp::ErrorData> {
|
||||
assert_eq!(
|
||||
context
|
||||
.peer
|
||||
.peer_info()
|
||||
.and_then(|info| info.capabilities.extensions.clone())
|
||||
.client_capabilities()
|
||||
.and_then(|capabilities| capabilities.extensions)
|
||||
.and_then(|extensions| extensions.get(OPENAI_ELICITATION_EXTENSION_ID).cloned())
|
||||
.map(Value::Object),
|
||||
Some(json!({"userVerification": {}})),
|
||||
|
||||
Reference in new issue
Block a user