[codex] preserve runtime MCP servers across client capability refresh

This commit is contained in:
Brian Brunner
2026-06-24 17:47:38 +00:00
parent 68a359353a
commit d0ca4a9b63
5 changed files with 65 additions and 4 deletions

View File

@@ -232,7 +232,7 @@ Example with notification opt-out:
- `mcpServer/oauth/login` — start an OAuth login for a configured MCP server; returns an `authorization_url` and later emits `mcpServer/oauthLogin/completed` once the browser flow finishes.
- `tool/requestUserInput` — prompt the user with 13 short questions for a tool call and return their answers (experimental).
- `config/mcpServer/reload` — reload MCP server config from disk and queue a refresh for loaded threads (applied on each thread's next active turn); returns `{}`. Use this after editing `config.toml` without restarting the server.
- `mcpServerStatus/list` — enumerate configured MCP servers with their tools, auth status, server info, plus resources/resource templates for `full` detail; supports optional `threadId` and cursor+limit pagination. If `threadId` is omitted, the server reads from the latest global config directly. If `detail` is omitted, the server defaults to `full`.
- `mcpServerStatus/list` — enumerate configured MCP servers with their tools, auth status, server info, plus resources/resource templates for `full` detail; supports optional `threadId` and cursor+limit pagination. With `threadId`, the response is a point-in-time snapshot of the MCP manager generation currently installed for that thread. If `threadId` is omitted, the server reads from the latest global config directly. If `detail` is omitted, the server defaults to `full`.
- `mcpServer/resource/read` — read a resource from a configured MCP server by optional `threadId`, `server`, and `uri`, returning text/blob resource `contents`. If `threadId` is omitted, the server reads from the latest MCP config directly.
- `mcpServer/tool/call` — call a tool on a thread's configured MCP server by `threadId`, `server`, `tool`, optional `arguments`, and optional `_meta`, returning the MCP tool result.
- `windowsSandbox/setupStart` — start Windows sandbox setup for the selected mode (`elevated` or `unelevated`); accepts an optional absolute `cwd` to target setup for a specific workspace, returns `{ started: true }` immediately, and later emits `windowsSandbox/setupCompleted`.

View File

@@ -214,7 +214,7 @@ url = "{mcp_server_url}/mcp"
}
#[tokio::test]
async fn mcp_server_status_list_never_advertises_uninstalled_codex_apps() -> Result<()> {
async fn mcp_server_status_list_reflects_installed_manager_generation() -> Result<()> {
let server = create_mock_responses_server_sequence_unchecked(Vec::new()).await;
let (apps_server_url, apps_server_handle) = start_apps_mcp_server("calendar_lookup").await?;
let codex_home = TempDir::new()?;

View File

@@ -593,7 +593,7 @@ impl CodexThread {
self.codex.session.runtime_mcp_config(config).await
}
/// Returns the MCP inventory installed for this thread.
/// Returns a point-in-time snapshot of the MCP inventory installed for this thread.
pub async fn mcp_server_status_snapshot(
&self,
config: &crate::config::Config,

View File

@@ -444,8 +444,9 @@ impl Session {
}
let config = self.get_config().await;
let mcp_servers = self.runtime_mcp_servers(config.as_ref()).await;
let refresh_config = McpServerRefreshConfig {
mcp_servers: serde_json::to_value(config.mcp_servers.get())?,
mcp_servers: serde_json::to_value(mcp_servers)?,
mcp_oauth_credentials_store_mode: serde_json::to_value(
config.mcp_oauth_credentials_store_mode,
)?,

View File

@@ -6,6 +6,10 @@ use std::time::Duration;
use codex_config::DEFAULT_MCP_SERVER_ENVIRONMENT_ID;
use codex_config::types::McpServerConfig;
use codex_config::types::McpServerTransportConfig;
use codex_mcp::CODEX_APPS_MCP_SERVER_NAME;
use codex_protocol::mcp::CallToolResult;
use core_test_support::apps_test_server::AppsTestServer;
use core_test_support::apps_test_server::apps_enabled_builder;
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;
@@ -14,6 +18,62 @@ use core_test_support::skip_if_no_network;
use core_test_support::stdio_server_bin;
use core_test_support::test_codex::test_codex;
use core_test_support::wait_for_mcp_server;
use pretty_assertions::assert_eq;
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn elicitation_capability_refresh_preserves_host_owned_codex_apps() -> anyhow::Result<()> {
skip_if_no_network!(Ok(()));
let server = responses::start_mock_server().await;
let apps_server = AppsTestServer::mount(&server).await?;
let fixture = apps_enabled_builder(apps_server.chatgpt_base_url)
.build(&server)
.await?;
wait_for_mcp_server(&fixture.codex, CODEX_APPS_MCP_SERVER_NAME).await?;
responses::mount_sse_once(
&server,
responses::sse(vec![
responses::ev_response_created("resp-1"),
responses::ev_assistant_message("msg-1", "done"),
responses::ev_completed("resp-1"),
]),
)
.await;
fixture
.codex
.set_openai_form_elicitation_support(/*supported*/ true)
.await?;
fixture.submit_turn("refresh MCP servers").await?;
let result = fixture
.codex
.call_mcp_tool(
CODEX_APPS_MCP_SERVER_NAME,
"calendar_create_event",
Some(serde_json::json!({
"title": "Team sync",
"starts_at": "2026-06-24T17:00:00Z",
})),
/*meta*/ None,
)
.await?;
assert_eq!(
result,
CallToolResult {
content: vec![serde_json::json!({
"type": "text",
"text": "called calendar_create_event for Team sync at 2026-06-24T17:00:00Z with ",
})],
structured_content: Some(serde_json::json!({ "_codex_apps": null })),
is_error: Some(false),
meta: None,
}
);
fixture.codex.shutdown_and_wait().await?;
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn refresh_shuts_down_superseded_mcp_stdio_server() -> anyhow::Result<()> {