diff --git a/codex-rs/app-server/src/message_processor.rs b/codex-rs/app-server/src/message_processor.rs index 0697fb6a4e..a138b83245 100644 --- a/codex-rs/app-server/src/message_processor.rs +++ b/codex-rs/app-server/src/message_processor.rs @@ -1218,7 +1218,9 @@ impl MessageProcessor { self.catalog_processor.skills_config_write(params).await } ClientRequest::PluginInstall { params, .. } => { - self.plugin_processor.plugin_install(params).await + self.plugin_processor + .plugin_install(params, mcp_client_capabilities.clone()) + .await } ClientRequest::PluginUninstall { params, .. } => { self.plugin_processor.plugin_uninstall(params).await @@ -1316,7 +1318,7 @@ impl MessageProcessor { } ClientRequest::McpServerToolCall { params, .. } => { self.mcp_processor - .mcp_server_tool_call(&request_id, params) + .mcp_server_tool_call(&request_id, params, mcp_client_capabilities.clone()) .await } ClientRequest::WindowsSandboxSetupStart { params, .. } => { diff --git a/codex-rs/app-server/src/request_processors/initialize_processor.rs b/codex-rs/app-server/src/request_processors/initialize_processor.rs index 0b8157e202..a1280edabe 100644 --- a/codex-rs/app-server/src/request_processors/initialize_processor.rs +++ b/codex-rs/app-server/src/request_processors/initialize_processor.rs @@ -3,6 +3,7 @@ use std::sync::atomic::Ordering; use axum::http::HeaderValue; use codex_analytics::AppServerRpcTransport; +use codex_app_server_protocol::InitializeCapabilities; use codex_login::default_client::SetOriginatorError; use codex_login::default_client::USER_AGENT_SUFFIX; use codex_login::default_client::get_codex_user_agent; @@ -67,22 +68,13 @@ impl InitializeRequestProcessor { // experimental API). Proposed direction is instance-global first-write-wins // with initialize-time mismatch rejection. let analytics_initialize_params = params.clone(); - let ( - experimental_api_enabled, + let InitializeCapabilities { + experimental_api: experimental_api_enabled, request_attestation, opt_out_notification_methods, mcp_client_capabilities, - ) = match params.capabilities { - Some(capabilities) => ( - capabilities.experimental_api, - capabilities.request_attestation, - capabilities - .opt_out_notification_methods - .unwrap_or_default(), - capabilities.mcp_client_capabilities, - ), - None => (false, false, Vec::new(), None), - }; + } = params.capabilities.unwrap_or_default(); + let opt_out_notification_methods = opt_out_notification_methods.unwrap_or_default(); let ClientInfo { name, title: _title, diff --git a/codex-rs/app-server/src/request_processors/mcp_processor.rs b/codex-rs/app-server/src/request_processors/mcp_processor.rs index 1d7b9c12b4..4f5f900002 100644 --- a/codex-rs/app-server/src/request_processors/mcp_processor.rs +++ b/codex-rs/app-server/src/request_processors/mcp_processor.rs @@ -70,8 +70,9 @@ impl McpRequestProcessor { &self, request_id: &ConnectionRequestId, params: McpServerToolCallParams, + mcp_client_capabilities: Option, ) -> Result, JSONRPCErrorError> { - self.call_mcp_server_tool(request_id, params) + self.call_mcp_server_tool(request_id, params, mcp_client_capabilities) .await .map(|()| None) } @@ -364,7 +365,9 @@ impl McpRequestProcessor { let request_id = request_id.clone(); tokio::spawn(async move { - let result = thread.read_mcp_resource(&server, &uri).await; + let result = thread + .read_mcp_resource(&server, &uri, mcp_client_capabilities.as_ref()) + .await; Self::send_mcp_resource_read_response(outgoing, request_id, result).await; }); return Ok(()); @@ -420,6 +423,7 @@ impl McpRequestProcessor { &self, request_id: &ConnectionRequestId, params: McpServerToolCallParams, + mcp_client_capabilities: Option, ) -> Result<(), JSONRPCErrorError> { let outgoing = Arc::clone(&self.outgoing); let thread_id = params.thread_id.clone(); @@ -429,7 +433,13 @@ impl McpRequestProcessor { tokio::spawn(async move { let result = thread - .call_mcp_tool(¶ms.server, ¶ms.tool, params.arguments, meta) + .call_mcp_tool( + ¶ms.server, + ¶ms.tool, + params.arguments, + meta, + mcp_client_capabilities.as_ref(), + ) .await .map(McpServerToolCallResponse::from) .map_err(|error| internal_error(format!("{error:#}"))); diff --git a/codex-rs/app-server/src/request_processors/plugins.rs b/codex-rs/app-server/src/request_processors/plugins.rs index 360b41d047..847fc281c8 100644 --- a/codex-rs/app-server/src/request_processors/plugins.rs +++ b/codex-rs/app-server/src/request_processors/plugins.rs @@ -428,8 +428,9 @@ impl PluginRequestProcessor { pub(crate) async fn plugin_install( &self, params: PluginInstallParams, + mcp_client_capabilities: Option, ) -> Result, JSONRPCErrorError> { - self.plugin_install_response(params) + self.plugin_install_response(params, mcp_client_capabilities) .await .map(|response| Some(response.into())) } @@ -1364,6 +1365,7 @@ impl PluginRequestProcessor { async fn plugin_install_response( &self, params: PluginInstallParams, + mcp_client_capabilities: Option, ) -> Result { let PluginInstallParams { marketplace_path, @@ -1374,7 +1376,11 @@ impl PluginRequestProcessor { (Some(marketplace_path), None) => marketplace_path, (None, Some(remote_marketplace_name)) => { return self - .remote_plugin_install_response(remote_marketplace_name, plugin_name) + .remote_plugin_install_response( + remote_marketplace_name, + plugin_name, + mcp_client_capabilities, + ) .await; } (Some(_), Some(_)) | (None, None) => { @@ -1432,6 +1438,7 @@ impl PluginRequestProcessor { auth.as_ref().is_some_and(CodexAuth::is_chatgpt_auth), &result.plugin_id.as_key(), &plugin_apps, + mcp_client_capabilities, ) .await; @@ -1445,6 +1452,7 @@ impl PluginRequestProcessor { &self, remote_marketplace_name: String, remote_plugin_id: String, + mcp_client_capabilities: Option, ) -> Result { let config = self.load_latest_config(/*fallback_cwd*/ None).await?; if !config.features.enabled(Feature::Plugins) { @@ -1547,6 +1555,7 @@ impl PluginRequestProcessor { auth.as_ref().is_some_and(CodexAuth::is_chatgpt_auth), &result.plugin_id.as_key(), &plugin_apps, + mcp_client_capabilities, ) .await; @@ -1562,6 +1571,7 @@ impl PluginRequestProcessor { is_chatgpt_auth: bool, plugin_id: &str, plugin_apps: &[codex_plugin::AppConnectorId], + mcp_client_capabilities: Option, ) -> Vec { if plugin_apps.is_empty() || !config.features.apps_enabled_for_auth(is_chatgpt_auth) { return Vec::new(); @@ -1574,7 +1584,7 @@ impl PluginRequestProcessor { config, /*force_refetch*/ true, Arc::clone(&environment_manager), - None, + mcp_client_capabilities.clone(), ), ); @@ -1599,9 +1609,12 @@ impl PluginRequestProcessor { "failed to load accessible apps after plugin install: {err:#}" ); ( - connectors::list_cached_accessible_connectors_from_mcp_tools(config, None) - .await - .unwrap_or_default(), + connectors::list_cached_accessible_connectors_from_mcp_tools( + config, + mcp_client_capabilities.as_ref(), + ) + .await + .unwrap_or_default(), false, ) } diff --git a/codex-rs/app-server/tests/suite/v2/mcp_tool.rs b/codex-rs/app-server/tests/suite/v2/mcp_tool.rs index 10c8a6ef04..744bc74555 100644 --- a/codex-rs/app-server/tests/suite/v2/mcp_tool.rs +++ b/codex-rs/app-server/tests/suite/v2/mcp_tool.rs @@ -10,9 +10,13 @@ use app_test_support::create_mock_responses_server_sequence; use app_test_support::to_response; use app_test_support::write_mock_responses_config_toml; use axum::Router; +use codex_app_server_protocol::ClientInfo; +use codex_app_server_protocol::InitializeCapabilities; use codex_app_server_protocol::ItemCompletedNotification; use codex_app_server_protocol::JSONRPCError; use codex_app_server_protocol::JSONRPCResponse; +use codex_app_server_protocol::McpAppUiCapability; +use codex_app_server_protocol::McpClientCapabilities; use codex_app_server_protocol::McpElicitationSchema; use codex_app_server_protocol::McpServerElicitationAction; use codex_app_server_protocol::McpServerElicitationRequest; @@ -95,7 +99,24 @@ url = "{mcp_server_url}/mcp" std::fs::write(config_path, config_toml)?; let mut mcp = TestAppServer::new(codex_home.path()).await?; - timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; + timeout( + DEFAULT_READ_TIMEOUT, + mcp.initialize_with_capabilities( + ClientInfo { + name: "mcp-tool-capability-test".to_string(), + title: None, + version: "1.0.0".to_string(), + }, + Some(InitializeCapabilities { + experimental_api: true, + mcp_client_capabilities: Some(McpClientCapabilities { + app_ui: [McpAppUiCapability::WebView].into_iter().collect(), + }), + ..Default::default() + }), + ), + ) + .await??; let thread_start_id = mcp .send_thread_start_request(ThreadStartParams { @@ -577,6 +598,7 @@ impl ServerHandler for ToolAppsMcpServer { context: RequestContext, ) -> Result { assert_eq!(request.name.as_ref(), TEST_TOOL_NAME); + assert!(!context.meta.0.contains_key("openai/clientCapabilities")); let message = request .arguments .as_ref() diff --git a/codex-rs/chatgpt/src/connectors.rs b/codex-rs/chatgpt/src/connectors.rs index 6c33ec9661..63b34e7355 100644 --- a/codex-rs/chatgpt/src/connectors.rs +++ b/codex-rs/chatgpt/src/connectors.rs @@ -13,7 +13,6 @@ use codex_connectors::merge::merge_connectors; use codex_connectors::merge::merge_plugin_connectors; use codex_core::config::Config; pub use codex_core::connectors::list_accessible_connectors_from_mcp_tools; -pub use codex_core::connectors::list_accessible_connectors_from_mcp_tools_with_client_capabilities; pub use codex_core::connectors::list_accessible_connectors_from_mcp_tools_with_environment_manager; pub use codex_core::connectors::list_accessible_connectors_from_mcp_tools_with_options; pub use codex_core::connectors::list_accessible_connectors_from_mcp_tools_with_options_and_status; diff --git a/codex-rs/codex-mcp/src/connection_manager.rs b/codex-rs/codex-mcp/src/connection_manager.rs index 59178286b6..39c172b83f 100644 --- a/codex-rs/codex-mcp/src/connection_manager.rs +++ b/codex-rs/codex-mcp/src/connection_manager.rs @@ -268,11 +268,15 @@ impl McpConnectionManager { }, ) .await; + let effective_client_capabilities = (host_owned_codex_apps_enabled + && server_name == CODEX_APPS_MCP_SERVER_NAME) + .then(|| codex_apps_client_capabilities.clone()) + .flatten(); let codex_apps_tools_cache_context = if server_name == CODEX_APPS_MCP_SERVER_NAME { Some(CodexAppsToolsCacheContext { codex_home: codex_home.clone(), user_key: codex_apps_tools_cache_key.clone(), - client_capabilities_fingerprint: codex_apps_client_capabilities + client_capabilities_fingerprint: effective_client_capabilities .as_ref() .map(client_capabilities::fingerprint), }) @@ -303,9 +307,7 @@ impl McpConnectionManager { tx_event.clone(), elicitation_requests.clone(), codex_apps_tools_cache_context, - (host_owned_codex_apps_enabled && server_name == CODEX_APPS_MCP_SERVER_NAME) - .then(|| codex_apps_client_capabilities.clone()) - .flatten(), + effective_client_capabilities, Arc::clone(&tool_plugin_provenance), runtime_context.clone(), runtime_auth_provider, @@ -616,7 +618,53 @@ impl McpConnectionManager { .client() .await .context("failed to get client")?; + let client_capabilities = managed_client.client_capabilities.clone(); + self.hard_refresh_codex_apps_tools_cache_for_client( + &managed_client, + client_capabilities.as_ref(), + managed_client.codex_apps_tools_cache_context.clone(), + ) + .await + } + pub async fn hard_refresh_codex_apps_tools_cache_with_client_capabilities( + &self, + client_capabilities: Option<&McpClientCapabilities>, + ) -> Result> { + let client_capabilities = self + .is_host_owned_codex_apps_server(CODEX_APPS_MCP_SERVER_NAME) + .then_some(client_capabilities) + .flatten(); + let managed_client = self + .clients + .get(CODEX_APPS_MCP_SERVER_NAME) + .ok_or_else(|| anyhow!("unknown MCP server '{CODEX_APPS_MCP_SERVER_NAME}'"))? + .client() + .await + .context("failed to get client")?; + let cache_context = + managed_client + .codex_apps_tools_cache_context + .clone() + .map(|mut context| { + context.client_capabilities_fingerprint = + client_capabilities.map(client_capabilities::fingerprint); + context + }); + self.hard_refresh_codex_apps_tools_cache_for_client( + &managed_client, + client_capabilities, + cache_context, + ) + .await + } + + async fn hard_refresh_codex_apps_tools_cache_for_client( + &self, + managed_client: &ManagedClient, + client_capabilities: Option<&McpClientCapabilities>, + cache_context: Option, + ) -> Result> { let list_start = Instant::now(); let fetch_start = Instant::now(); let tools = list_tools_for_client_uncached( @@ -624,7 +672,7 @@ impl McpConnectionManager { &managed_client.client, managed_client.tool_timeout, managed_client.server_instructions.as_deref(), - managed_client.client_capabilities.as_ref(), + client_capabilities, ) .await .with_context(|| { @@ -638,7 +686,7 @@ impl McpConnectionManager { write_cached_codex_apps_tools_if_needed( CODEX_APPS_MCP_SERVER_NAME, - managed_client.codex_apps_tools_cache_context.as_ref(), + cache_context.as_ref(), &managed_client.server_info, &tools, ); @@ -818,16 +866,53 @@ impl McpConnectionManager { server: &str, tool: &str, arguments: Option, - mut meta: Option, + meta: Option, ) -> Result { let client = self.client_by_name(server).await?; + let client_capabilities = client.client_capabilities.clone(); + Self::call_tool_for_client( + &client, + server, + tool, + arguments, + meta, + client_capabilities.as_ref(), + ) + .await + } + + pub async fn call_tool_with_client_capabilities( + &self, + server: &str, + tool: &str, + arguments: Option, + meta: Option, + client_capabilities: Option<&McpClientCapabilities>, + ) -> Result { + let client_capabilities = self + .is_host_owned_codex_apps_server(server) + .then_some(client_capabilities) + .flatten(); + let client = self.client_by_name(server).await?; + Self::call_tool_for_client(&client, server, tool, arguments, meta, client_capabilities) + .await + } + + async fn call_tool_for_client( + client: &ManagedClient, + server: &str, + tool: &str, + arguments: Option, + mut meta: Option, + client_capabilities: Option<&McpClientCapabilities>, + ) -> Result { if !client.tool_filter.allows(tool) { return Err(anyhow!( "tool '{tool}' is disabled for MCP server '{server}'" )); } - client_capabilities::add_json_meta(&mut meta, client.client_capabilities.as_ref()); + client_capabilities::add_json_meta(&mut meta, client_capabilities); let result: rmcp::model::CallToolResult = client .client .call_tool(tool.to_string(), arguments, meta, client.tool_timeout) @@ -870,9 +955,9 @@ impl McpConnectionManager { let managed = self.client_by_name(server).await?; let timeout = managed.tool_timeout; - if managed.client_capabilities.is_some() { + if let Some(capabilities) = managed.client_capabilities.as_ref() { let params = params.get_or_insert_with(PaginatedRequestParams::default); - client_capabilities::add_meta(&mut params.meta, managed.client_capabilities.as_ref()); + client_capabilities::add_meta(&mut params.meta, Some(capabilities)); } managed .client @@ -891,9 +976,9 @@ impl McpConnectionManager { let client = managed.client.clone(); let timeout = managed.tool_timeout; - if managed.client_capabilities.is_some() { + if let Some(capabilities) = managed.client_capabilities.as_ref() { let params = params.get_or_insert_with(PaginatedRequestParams::default); - client_capabilities::add_meta(&mut params.meta, managed.client_capabilities.as_ref()); + client_capabilities::add_meta(&mut params.meta, Some(capabilities)); } client .list_resource_templates(params, timeout) @@ -905,13 +990,37 @@ impl McpConnectionManager { pub async fn read_resource( &self, server: &str, - mut params: ReadResourceRequestParams, + params: ReadResourceRequestParams, ) -> Result { let managed = self.client_by_name(server).await?; + let client_capabilities = managed.client_capabilities.clone(); + Self::read_resource_for_client(&managed, server, params, client_capabilities.as_ref()).await + } + + pub async fn read_resource_with_client_capabilities( + &self, + server: &str, + params: ReadResourceRequestParams, + client_capabilities: Option<&McpClientCapabilities>, + ) -> Result { + let client_capabilities = self + .is_host_owned_codex_apps_server(server) + .then_some(client_capabilities) + .flatten(); + let managed = self.client_by_name(server).await?; + Self::read_resource_for_client(&managed, server, params, client_capabilities).await + } + + async fn read_resource_for_client( + managed: &ManagedClient, + server: &str, + mut params: ReadResourceRequestParams, + client_capabilities: Option<&McpClientCapabilities>, + ) -> Result { let client = managed.client.clone(); let timeout = managed.tool_timeout; let uri = params.uri.clone(); - client_capabilities::add_meta(&mut params.meta, managed.client_capabilities.as_ref()); + client_capabilities::add_meta(&mut params.meta, client_capabilities); client .read_resource(params, timeout) diff --git a/codex-rs/core/src/codex_thread.rs b/codex-rs/core/src/codex_thread.rs index 4159669227..809c74cdb9 100644 --- a/codex-rs/core/src/codex_thread.rs +++ b/codex-rs/core/src/codex_thread.rs @@ -553,11 +553,16 @@ impl CodexThread { &self, server: &str, uri: &str, + client_capabilities: Option<&McpClientCapabilities>, ) -> anyhow::Result { let result = self .codex .session - .read_resource(server, ReadResourceRequestParams::new(uri)) + .read_resource_with_client_capabilities( + server, + ReadResourceRequestParams::new(uri), + client_capabilities, + ) .await?; Ok(serde_json::to_value(result)?) @@ -569,10 +574,11 @@ impl CodexThread { tool: &str, arguments: Option, meta: Option, + client_capabilities: Option<&McpClientCapabilities>, ) -> anyhow::Result { self.codex .session - .call_tool(server, tool, arguments, meta) + .call_tool_with_client_capabilities(server, tool, arguments, meta, client_capabilities) .await } diff --git a/codex-rs/core/src/connectors.rs b/codex-rs/core/src/connectors.rs index e063d97758..ac94d25eb6 100644 --- a/codex-rs/core/src/connectors.rs +++ b/codex-rs/core/src/connectors.rs @@ -203,22 +203,6 @@ pub async fn list_accessible_connectors_from_mcp_tools_with_options( ) } -pub async fn list_accessible_connectors_from_mcp_tools_with_client_capabilities( - config: &Config, - force_refetch: bool, - mcp_client_capabilities: Option, -) -> anyhow::Result> { - Ok( - list_accessible_connectors_from_mcp_tools_with_options_and_status( - config, - force_refetch, - mcp_client_capabilities, - ) - .await? - .connectors, - ) -} - pub async fn list_accessible_connectors_from_mcp_tools_with_options_and_status( config: &Config, force_refetch: bool, diff --git a/codex-rs/core/src/mcp_tool_call.rs b/codex-rs/core/src/mcp_tool_call.rs index dab05490c4..50c42f1879 100644 --- a/codex-rs/core/src/mcp_tool_call.rs +++ b/codex-rs/core/src/mcp_tool_call.rs @@ -569,11 +569,12 @@ async fn execute_mcp_tool_call( .start_mcp_call_trace(call_id); let request_meta = mcp_call_trace.add_request_meta(request_meta); let result = sess - .call_tool( + .call_tool_with_client_capabilities( &invocation.server, &invocation.tool, rewritten_arguments, request_meta, + turn_context.mcp_client_capabilities.as_ref(), ) .await .map_err(|e| format!("tool call error: {e:?}"))?; @@ -676,7 +677,11 @@ async fn maybe_request_codex_apps_auth_elicitation( async fn refresh_codex_apps_after_connector_auth(sess: &Session, turn_context: &TurnContext) { let mcp_tools_result = { let manager = sess.services.mcp_connection_manager.read().await; - manager.hard_refresh_codex_apps_tools_cache().await + manager + .hard_refresh_codex_apps_tools_cache_with_client_capabilities( + turn_context.mcp_client_capabilities.as_ref(), + ) + .await }; match mcp_tools_result { @@ -1441,13 +1446,14 @@ pub(crate) async fn lookup_mcp_tool_metadata( .await { Some(connectors) => Some(connectors), - None => connectors::list_accessible_connectors_from_mcp_tools_with_client_capabilities( + None => connectors::list_accessible_connectors_from_mcp_tools_with_options_and_status( turn_context.config.as_ref(), false, turn_context.mcp_client_capabilities.clone(), ) .await - .ok(), + .ok() + .map(|status| status.connectors), }; connectors.and_then(|connectors| { let connector_id = tool_info.connector_id.as_deref()?; diff --git a/codex-rs/core/src/session/mcp.rs b/codex-rs/core/src/session/mcp.rs index c5f1b7c76d..ba41b78f92 100644 --- a/codex-rs/core/src/session/mcp.rs +++ b/codex-rs/core/src/session/mcp.rs @@ -270,16 +270,17 @@ impl Session { clippy::await_holding_invalid_type, reason = "MCP resource calls are serialized through the session-owned manager guard" )] - pub async fn read_resource( + pub async fn read_resource_with_client_capabilities( &self, server: &str, params: ReadResourceRequestParams, + client_capabilities: Option<&McpClientCapabilities>, ) -> anyhow::Result { self.services .mcp_connection_manager .read() .await - .read_resource(server, params) + .read_resource_with_client_capabilities(server, params, client_capabilities) .await } @@ -287,18 +288,19 @@ impl Session { clippy::await_holding_invalid_type, reason = "MCP tool calls are serialized through the session-owned manager guard" )] - pub async fn call_tool( + pub async fn call_tool_with_client_capabilities( &self, server: &str, tool: &str, arguments: Option, meta: Option, + client_capabilities: Option<&McpClientCapabilities>, ) -> anyhow::Result { self.services .mcp_connection_manager .read() .await - .call_tool(server, tool, arguments, meta) + .call_tool_with_client_capabilities(server, tool, arguments, meta, client_capabilities) .await } diff --git a/codex-rs/core/src/tools/handlers/mcp_resource/read_mcp_resource.rs b/codex-rs/core/src/tools/handlers/mcp_resource/read_mcp_resource.rs index 126c5d85e8..6d871ae79e 100644 --- a/codex-rs/core/src/tools/handlers/mcp_resource/read_mcp_resource.rs +++ b/codex-rs/core/src/tools/handlers/mcp_resource/read_mcp_resource.rs @@ -78,7 +78,11 @@ impl ToolExecutor for ReadMcpResourceHandler { let payload_result: Result = async { let result = session - .read_resource(&server, ReadResourceRequestParams::new(uri.clone())) + .read_resource_with_client_capabilities( + &server, + ReadResourceRequestParams::new(uri.clone()), + turn.mcp_client_capabilities.as_ref(), + ) .await .map_err(|err| { FunctionCallError::RespondToModel(format!("resources/read failed: {err:#}")) diff --git a/codex-rs/core/tests/suite/mcp_turn_metadata.rs b/codex-rs/core/tests/suite/mcp_turn_metadata.rs index 49d3c0f2a6..c74a2dc59c 100644 --- a/codex-rs/core/tests/suite/mcp_turn_metadata.rs +++ b/codex-rs/core/tests/suite/mcp_turn_metadata.rs @@ -8,6 +8,8 @@ use codex_features::Feature; use codex_protocol::config_types::CollaborationMode; use codex_protocol::config_types::ModeKind; use codex_protocol::config_types::Settings; +use codex_protocol::mcp::McpAppUiCapability; +use codex_protocol::mcp::McpClientCapabilities; use codex_protocol::models::PermissionProfile; use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::ElicitationAction; @@ -142,6 +144,17 @@ async fn approved_mcp_tool_call_metadata_records_prior_user_input_request() -> R }); let test = builder.build(&server).await?; + test.codex + .set_app_server_client_info( + Some("turn-client".to_string()), + Some("1.0.0".to_string()), + false, + Some(McpClientCapabilities { + app_ui: [McpAppUiCapability::WebView].into_iter().collect(), + }), + ) + .await?; + submit_user_turn( &test, "Use [$calendar](app://calendar) to create a calendar event.", @@ -167,6 +180,15 @@ async fn approved_mcp_tool_call_metadata_records_prior_user_input_request() -> R unreachable!("event guard guarantees ElicitationRequest"); }; + test.codex + .set_app_server_client_info( + Some("resumed-client".to_string()), + Some("1.0.0".to_string()), + false, + Some(McpClientCapabilities::default()), + ) + .await?; + test.codex .submit(Op::ResolveElicitation { server_name: request.server_name, @@ -190,6 +212,12 @@ async fn approved_mcp_tool_call_metadata_records_prior_user_input_request() -> R .pointer("/params/_meta/x-codex-turn-metadata/user_input_requested_during_turn"), Some(&json!(true)) ); + assert_eq!( + apps_tool_call.pointer( + "/params/_meta/openai~1clientCapabilities/extensions/io.modelcontextprotocol~1ui/mimeTypes" + ), + Some(&json!(["text/html;profile=mcp-app"])) + ); Ok(()) }