feat(mcp): propagate client UI capabilities

This commit is contained in:
Casey Chow
2026-06-08 20:56:52 +00:00
parent f3c1283411
commit 4ab5708c10
50 changed files with 1164 additions and 61 deletions
@@ -1627,6 +1627,7 @@ async fn initialize_caches_client_and_thread_lifecycle_publishes_once_initialize
experimental_api: false,
request_attestation: false,
opt_out_notification_methods: None,
mcp_client_capabilities: None,
}),
},
product_client_id: DEFAULT_ORIGINATOR.to_string(),
@@ -1776,6 +1777,7 @@ async fn compaction_event_ingests_custom_fact() {
experimental_api: false,
request_attestation: false,
opt_out_notification_methods: None,
mcp_client_capabilities: None,
}),
},
product_client_id: DEFAULT_ORIGINATOR.to_string(),
+44
View File
@@ -38,6 +38,7 @@ use codex_app_server_protocol::ConfigWarningNotification;
use codex_app_server_protocol::InitializeCapabilities;
use codex_app_server_protocol::InitializeParams;
use codex_app_server_protocol::JSONRPCErrorError;
use codex_app_server_protocol::McpClientCapabilities;
use codex_app_server_protocol::RequestId;
use codex_app_server_protocol::Result as JsonRpcResult;
use codex_app_server_protocol::ServerNotification;
@@ -363,6 +364,8 @@ pub struct InProcessClientStartArgs {
pub experimental_api: bool,
/// Notification methods this client opts out of receiving.
pub opt_out_notification_methods: Vec<String>,
/// Passive MCP host capabilities advertised by this client.
pub mcp_client_capabilities: Option<McpClientCapabilities>,
/// Queue capacity for command/event channels (clamped to at least 1).
pub channel_capacity: usize,
}
@@ -385,6 +388,7 @@ impl InProcessClientStartArgs {
} else {
Some(self.opt_out_notification_methods.clone())
},
mcp_client_capabilities: self.mcp_client_capabilities.clone(),
};
InitializeParams {
@@ -950,6 +954,8 @@ mod tests {
use codex_app_server_protocol::JSONRPCMessage;
use codex_app_server_protocol::JSONRPCRequest;
use codex_app_server_protocol::JSONRPCResponse;
use codex_app_server_protocol::McpAppUiCapability;
use codex_app_server_protocol::McpClientCapabilities;
use codex_app_server_protocol::ServerNotification;
use codex_app_server_protocol::SessionSource as ApiSessionSource;
use codex_app_server_protocol::ThreadStartParams;
@@ -1047,6 +1053,7 @@ mod tests {
client_version: "0.0.0-test".to_string(),
experimental_api: true,
opt_out_notification_methods: Vec::new(),
mcp_client_capabilities: None,
channel_capacity,
})
.await
@@ -1239,10 +1246,32 @@ mod tests {
client_version: "0.0.0-test".to_string(),
experimental_api: true,
opt_out_notification_methods: Vec::new(),
mcp_client_capabilities: None,
channel_capacity: 8,
}
}
#[test]
fn remote_initialize_params_forward_mcp_client_capabilities() {
let expected = McpClientCapabilities {
app_ui: [
McpAppUiCapability::WebView,
McpAppUiCapability::DeclarativeUi,
]
.into_iter()
.collect(),
};
let mut args = test_remote_connect_args("ws://127.0.0.1:4500".to_string());
args.mcp_client_capabilities = Some(expected.clone());
assert_eq!(
args.initialize_params()
.capabilities
.and_then(|capabilities| capabilities.mcp_client_capabilities),
Some(expected)
);
}
#[tokio::test]
async fn typed_request_roundtrip_works() {
let client = start_test_client(SessionSource::Exec).await;
@@ -1513,6 +1542,7 @@ mod tests {
client_version: "0.0.0-test".to_string(),
experimental_api: true,
opt_out_notification_methods: Vec::new(),
mcp_client_capabilities: None,
channel_capacity: 8,
})
.await
@@ -1601,6 +1631,7 @@ mod tests {
client_version: "0.0.0-test".to_string(),
experimental_api: true,
opt_out_notification_methods: Vec::new(),
mcp_client_capabilities: None,
channel_capacity: 8,
})
.await
@@ -1620,6 +1651,7 @@ mod tests {
client_version: "0.0.0-test".to_string(),
experimental_api: true,
opt_out_notification_methods: Vec::new(),
mcp_client_capabilities: None,
channel_capacity: 8,
})
.await;
@@ -2179,6 +2211,9 @@ mod tests {
#[tokio::test]
async fn runtime_start_args_forward_environment_manager() {
let config = Arc::new(build_test_config().await);
let expected_capabilities = McpClientCapabilities {
app_ui: [McpAppUiCapability::WebView].into_iter().collect(),
};
let environment_manager = Arc::new(
EnvironmentManager::create_for_tests(
Some("ws://127.0.0.1:8765".to_string()),
@@ -2211,11 +2246,19 @@ mod tests {
client_version: "0.0.0-test".to_string(),
experimental_api: true,
opt_out_notification_methods: Vec::new(),
mcp_client_capabilities: Some(expected_capabilities.clone()),
channel_capacity: DEFAULT_IN_PROCESS_CHANNEL_CAPACITY,
}
.into_runtime_start_args();
assert_eq!(runtime_args.config, config);
assert_eq!(
runtime_args
.initialize
.capabilities
.and_then(|capabilities| capabilities.mcp_client_capabilities),
Some(expected_capabilities)
);
assert!(Arc::ptr_eq(
&runtime_args.environment_manager,
&environment_manager
@@ -2252,6 +2295,7 @@ mod tests {
client_version: "0.0.0-test".to_string(),
experimental_api: true,
opt_out_notification_methods: Vec::new(),
mcp_client_capabilities: None,
channel_capacity: DEFAULT_IN_PROCESS_CHANNEL_CAPACITY,
}
.into_runtime_start_args();
+4 -1
View File
@@ -33,6 +33,7 @@ use codex_app_server_protocol::JSONRPCMessage;
use codex_app_server_protocol::JSONRPCNotification;
use codex_app_server_protocol::JSONRPCRequest;
use codex_app_server_protocol::JSONRPCResponse;
use codex_app_server_protocol::McpClientCapabilities;
use codex_app_server_protocol::RequestId;
use codex_app_server_protocol::Result as JsonRpcResult;
use codex_app_server_protocol::ServerNotification;
@@ -87,10 +88,11 @@ pub struct RemoteAppServerConnectArgs {
pub client_version: String,
pub experimental_api: bool,
pub opt_out_notification_methods: Vec<String>,
pub mcp_client_capabilities: Option<McpClientCapabilities>,
pub channel_capacity: usize,
}
impl RemoteAppServerConnectArgs {
fn initialize_params(&self) -> InitializeParams {
pub(crate) fn initialize_params(&self) -> InitializeParams {
let capabilities = InitializeCapabilities {
experimental_api: self.experimental_api,
request_attestation: false,
@@ -99,6 +101,7 @@ impl RemoteAppServerConnectArgs {
} else {
Some(self.opt_out_notification_methods.clone())
},
mcp_client_capabilities: self.mcp_client_capabilities.clone(),
};
InitializeParams {
@@ -1124,6 +1124,17 @@
"description": "Opt into receiving experimental API methods and fields.",
"type": "boolean"
},
"mcpClientCapabilities": {
"anyOf": [
{
"$ref": "#/definitions/McpClientCapabilities"
},
{
"type": "null"
}
],
"description": "Passive MCP capabilities supported by this app-server client."
},
"optOutNotificationMethods": {
"description": "Exact notification method names that should be suppressed for this connection (for example `thread/started`).",
"items": {
@@ -1406,6 +1417,30 @@
},
"type": "object"
},
"McpAppUiCapability": {
"description": "Passive MCP App presentation features supported by the active host.",
"enum": [
"webView",
"declarativeUi"
],
"type": "string"
},
"McpClientCapabilities": {
"description": "Passive MCP capabilities supplied by the active Codex host.",
"properties": {
"appUi": {
"items": {
"$ref": "#/definitions/McpAppUiCapability"
},
"type": "array",
"uniqueItems": true
}
},
"required": [
"appUi"
],
"type": "object"
},
"McpResourceReadParams": {
"properties": {
"server": {
@@ -2821,6 +2821,17 @@
"description": "Opt into receiving experimental API methods and fields.",
"type": "boolean"
},
"mcpClientCapabilities": {
"anyOf": [
{
"$ref": "#/definitions/McpClientCapabilities"
},
{
"type": "null"
}
],
"description": "Passive MCP capabilities supported by this app-server client."
},
"optOutNotificationMethods": {
"description": "Exact notification method names that should be suppressed for this connection (for example `thread/started`).",
"items": {
@@ -3011,6 +3022,30 @@
"title": "JSONRPCResponse",
"type": "object"
},
"McpAppUiCapability": {
"description": "Passive MCP App presentation features supported by the active host.",
"enum": [
"webView",
"declarativeUi"
],
"type": "string"
},
"McpClientCapabilities": {
"description": "Passive MCP capabilities supplied by the active Codex host.",
"properties": {
"appUi": {
"items": {
"$ref": "#/definitions/McpAppUiCapability"
},
"type": "array",
"uniqueItems": true
}
},
"required": [
"appUi"
],
"type": "object"
},
"McpElicitationArrayType": {
"enum": [
"array"
@@ -6928,6 +6928,17 @@
"description": "Opt into receiving experimental API methods and fields.",
"type": "boolean"
},
"mcpClientCapabilities": {
"anyOf": [
{
"$ref": "#/definitions/McpClientCapabilities"
},
{
"type": "null"
}
],
"description": "Passive MCP capabilities supported by this app-server client."
},
"optOutNotificationMethods": {
"description": "Exact notification method names that should be suppressed for this connection (for example `thread/started`).",
"items": {
@@ -7718,6 +7729,14 @@
"title": "MarketplaceUpgradeResponse",
"type": "object"
},
"McpAppUiCapability": {
"description": "Passive MCP App presentation features supported by the active host.",
"enum": [
"webView",
"declarativeUi"
],
"type": "string"
},
"McpAuthStatus": {
"enum": [
"unsupported",
@@ -7727,6 +7746,22 @@
],
"type": "string"
},
"McpClientCapabilities": {
"description": "Passive MCP capabilities supplied by the active Codex host.",
"properties": {
"appUi": {
"items": {
"$ref": "#/definitions/McpAppUiCapability"
},
"type": "array",
"uniqueItems": true
}
},
"required": [
"appUi"
],
"type": "object"
},
"McpResourceReadParams": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
@@ -30,6 +30,17 @@
"description": "Opt into receiving experimental API methods and fields.",
"type": "boolean"
},
"mcpClientCapabilities": {
"anyOf": [
{
"$ref": "#/definitions/McpClientCapabilities"
},
{
"type": "null"
}
],
"description": "Passive MCP capabilities supported by this app-server client."
},
"optOutNotificationMethods": {
"description": "Exact notification method names that should be suppressed for this connection (for example `thread/started`).",
"items": {
@@ -47,6 +58,30 @@
}
},
"type": "object"
},
"McpAppUiCapability": {
"description": "Passive MCP App presentation features supported by the active host.",
"enum": [
"webView",
"declarativeUi"
],
"type": "string"
},
"McpClientCapabilities": {
"description": "Passive MCP capabilities supplied by the active Codex host.",
"properties": {
"appUi": {
"items": {
"$ref": "#/definitions/McpAppUiCapability"
},
"type": "array",
"uniqueItems": true
}
},
"required": [
"appUi"
],
"type": "object"
}
},
"properties": {
@@ -1,6 +1,7 @@
// GENERATED CODE! DO NOT MODIFY BY HAND!
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { McpClientCapabilities } from "./McpClientCapabilities";
/**
* Client-declared capabilities negotiated during initialize.
@@ -18,4 +19,8 @@ requestAttestation: boolean,
* Exact notification method names that should be suppressed for this
* connection (for example `thread/started`).
*/
optOutNotificationMethods?: Array<string> | null, };
optOutNotificationMethods?: Array<string> | null,
/**
* Passive MCP capabilities supported by this app-server client.
*/
mcpClientCapabilities?: McpClientCapabilities | null, };
@@ -0,0 +1,8 @@
// GENERATED CODE! DO NOT MODIFY BY HAND!
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
/**
* Passive MCP App presentation features supported by the active host.
*/
export type McpAppUiCapability = "webView" | "declarativeUi";
@@ -0,0 +1,9 @@
// GENERATED CODE! DO NOT MODIFY BY HAND!
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { McpAppUiCapability } from "./McpAppUiCapability";
/**
* Passive MCP capabilities supplied by the active Codex host.
*/
export type McpClientCapabilities = { appUi: Array<McpAppUiCapability>, };
+2
View File
@@ -43,6 +43,8 @@ export type { InternalSessionSource } from "./InternalSessionSource";
export type { LocalShellAction } from "./LocalShellAction";
export type { LocalShellExecAction } from "./LocalShellExecAction";
export type { LocalShellStatus } from "./LocalShellStatus";
export type { McpAppUiCapability } from "./McpAppUiCapability";
export type { McpClientCapabilities } from "./McpClientCapabilities";
export type { McpServerInfo } from "./McpServerInfo";
export type { MessagePhase } from "./MessagePhase";
export type { ModeKind } from "./ModeKind";
+2
View File
@@ -4,6 +4,8 @@ mod jsonrpc_lite;
mod protocol;
mod schema_fixtures;
pub use codex_protocol::mcp::McpAppUiCapability;
pub use codex_protocol::mcp::McpClientCapabilities;
pub use experimental_api::*;
pub use export::GenerateTsOptions;
pub use export::generate_internal_json_schema;
@@ -1622,6 +1622,8 @@ mod tests {
use anyhow::Result;
use codex_protocol::ThreadId;
use codex_protocol::account::PlanType;
use codex_protocol::mcp::McpAppUiCapability;
use codex_protocol::mcp::McpClientCapabilities;
use codex_protocol::models::BUILT_IN_PERMISSION_PROFILE_READ_ONLY;
use codex_protocol::parse_command::ParsedCommand;
use codex_protocol::protocol::RealtimeConversationVersion;
@@ -2111,6 +2113,7 @@ mod tests {
"thread/started".to_string(),
"item/agentMessage/delta".to_string(),
]),
mcp_client_capabilities: None,
}),
},
};
@@ -2140,6 +2143,58 @@ mod tests {
Ok(())
}
#[test]
fn serialize_initialize_with_mcp_client_capabilities() -> Result<()> {
let capabilities = McpClientCapabilities {
app_ui: [
McpAppUiCapability::WebView,
McpAppUiCapability::DeclarativeUi,
]
.into_iter()
.collect(),
};
let request = ClientRequest::Initialize {
request_id: RequestId::Integer(42),
params: v1::InitializeParams {
client_info: v1::ClientInfo {
name: "codex_app".to_string(),
title: Some("Codex App".to_string()),
version: "0.1.0".to_string(),
},
capabilities: Some(v1::InitializeCapabilities {
experimental_api: false,
request_attestation: false,
opt_out_notification_methods: None,
mcp_client_capabilities: Some(capabilities),
}),
},
};
assert_eq!(
json!({
"method": "initialize",
"id": 42,
"params": {
"clientInfo": {
"name": "codex_app",
"title": "Codex App",
"version": "0.1.0"
},
"capabilities": {
"experimentalApi": false,
"requestAttestation": false,
"optOutNotificationMethods": null,
"mcpClientCapabilities": {
"appUi": ["webView", "declarativeUi"]
}
}
}
}),
serde_json::to_value(&request)?,
);
Ok(())
}
#[test]
fn deserialize_initialize_with_opt_out_notification_methods() -> Result<()> {
let request: ClientRequest = serde_json::from_value(json!({
@@ -2179,6 +2234,7 @@ mod tests {
"thread/started".to_string(),
"item/agentMessage/delta".to_string(),
]),
mcp_client_capabilities: None,
}),
},
}
@@ -6,6 +6,7 @@ use codex_protocol::config_types::ForcedLoginMethod;
use codex_protocol::config_types::ReasoningSummary;
use codex_protocol::config_types::SandboxMode;
use codex_protocol::config_types::Verbosity;
use codex_protocol::mcp::McpClientCapabilities;
use codex_protocol::openai_models::ReasoningEffort;
use codex_protocol::parse_command::ParsedCommand;
use codex_protocol::protocol::AskForApproval;
@@ -54,6 +55,10 @@ pub struct InitializeCapabilities {
/// connection (for example `thread/started`).
#[ts(optional = nullable)]
pub opt_out_notification_methods: Option<Vec<String>>,
/// Passive MCP capabilities supported by this app-server client.
#[serde(skip_serializing_if = "Option::is_none")]
#[ts(optional = nullable)]
pub mcp_client_capabilities: Option<McpClientCapabilities>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
@@ -1564,6 +1564,7 @@ impl CodexClient {
.map(|method| (*method).to_string())
.collect(),
),
mcp_client_capabilities: None,
}),
},
};
+32 -5
View File
@@ -62,6 +62,7 @@ use codex_app_server_protocol::JSONRPCErrorError;
use codex_app_server_protocol::JSONRPCNotification;
use codex_app_server_protocol::JSONRPCRequest;
use codex_app_server_protocol::JSONRPCResponse;
use codex_app_server_protocol::McpClientCapabilities;
use codex_app_server_protocol::ServerRequestPayload;
use codex_app_server_protocol::experimental_required_message;
use codex_arg0::Arg0DispatchPaths;
@@ -201,6 +202,7 @@ pub(crate) struct InitializedConnectionSessionState {
pub(crate) app_server_client_name: String,
pub(crate) client_version: String,
pub(crate) request_attestation: bool,
pub(crate) mcp_client_capabilities: Option<McpClientCapabilities>,
}
impl Default for ConnectionSessionState {
@@ -252,6 +254,12 @@ impl ConnectionSessionState {
.is_some_and(|session| session.request_attestation)
}
pub(crate) fn mcp_client_capabilities(&self) -> Option<&McpClientCapabilities> {
self.initialized
.get()
.and_then(|session| session.mcp_client_capabilities.as_ref())
}
pub(crate) fn initialize(&self, session: InitializedConnectionSessionState) -> Result<(), ()> {
self.initialized.set(session).map_err(|_| ())
}
@@ -838,6 +846,7 @@ impl MessageProcessor {
let serialization_scope = codex_request.serialization_scope();
let app_server_client_name = session.app_server_client_name().map(str::to_string);
let client_version = session.client_version().map(str::to_string);
let mcp_client_capabilities = session.mcp_client_capabilities().cloned();
let error_request_id = connection_request_id.clone();
let rpc_gate = Arc::clone(&session.rpc_gate);
let processor = Arc::clone(self);
@@ -853,6 +862,7 @@ impl MessageProcessor {
request_context,
app_server_client_name,
client_version,
mcp_client_capabilities,
)
.await;
if let Err(error) = result {
@@ -882,6 +892,7 @@ impl MessageProcessor {
request_context: RequestContext,
app_server_client_name: Option<String>,
client_version: Option<String>,
mcp_client_capabilities: Option<McpClientCapabilities>,
) -> Result<(), JSONRPCErrorError> {
let connection_id = connection_request_id.connection_id;
let request_id = ConnectionRequestId {
@@ -1021,6 +1032,7 @@ impl MessageProcessor {
params,
app_server_client_name.clone(),
client_version.clone(),
mcp_client_capabilities.clone(),
request_context,
)
.await
@@ -1037,6 +1049,7 @@ impl MessageProcessor {
params,
app_server_client_name.clone(),
client_version.clone(),
mcp_client_capabilities.clone(),
)
.await
}
@@ -1047,6 +1060,7 @@ impl MessageProcessor {
params,
app_server_client_name.clone(),
client_version.clone(),
mcp_client_capabilities.clone(),
)
.await
}
@@ -1171,7 +1185,9 @@ impl MessageProcessor {
self.plugin_processor.plugin_installed(params).await
}
ClientRequest::PluginRead { params, .. } => {
self.plugin_processor.plugin_read(params).await
self.plugin_processor
.plugin_read(params, mcp_client_capabilities.clone())
.await
}
ClientRequest::PluginSkillRead { params, .. } => {
self.plugin_processor.plugin_skill_read(params).await
@@ -1194,7 +1210,9 @@ impl MessageProcessor {
self.plugin_processor.plugin_share_delete(params).await
}
ClientRequest::AppsList { params, .. } => {
self.apps_processor.apps_list(&request_id, params).await
self.apps_processor
.apps_list(&request_id, params, mcp_client_capabilities.clone())
.await
}
ClientRequest::SkillsConfigWrite { params, .. } => {
self.catalog_processor.skills_config_write(params).await
@@ -1231,6 +1249,7 @@ impl MessageProcessor {
params,
app_server_client_name.clone(),
client_version.clone(),
mcp_client_capabilities.clone(),
)
.await
}
@@ -1269,7 +1288,15 @@ impl MessageProcessor {
self.turn_processor.thread_realtime_list_voices().await
}
ClientRequest::ReviewStart { params, .. } => {
self.turn_processor.review_start(&request_id, params).await
self.turn_processor
.review_start(
&request_id,
params,
app_server_client_name.clone(),
client_version.clone(),
mcp_client_capabilities.clone(),
)
.await
}
ClientRequest::McpServerOauthLogin { params, .. } => {
self.mcp_processor.mcp_server_oauth_login(params).await
@@ -1279,12 +1306,12 @@ impl MessageProcessor {
}
ClientRequest::McpServerStatusList { params, .. } => {
self.mcp_processor
.mcp_server_status_list(&request_id, params)
.mcp_server_status_list(&request_id, params, mcp_client_capabilities.clone())
.await
}
ClientRequest::McpResourceRead { params, .. } => {
self.mcp_processor
.mcp_resource_read(&request_id, params)
.mcp_resource_read(&request_id, params, mcp_client_capabilities.clone())
.await
}
ClientRequest::McpServerToolCall { params, .. } => {
@@ -1,4 +1,5 @@
use super::*;
use codex_app_server_protocol::McpClientCapabilities;
pub(crate) struct AppsRequestProcessor {
auth_manager: Arc<AuthManager>,
@@ -35,8 +36,9 @@ impl AppsRequestProcessor {
&self,
request_id: &ConnectionRequestId,
params: AppsListParams,
mcp_client_capabilities: Option<McpClientCapabilities>,
) -> Result<Option<ClientResponsePayload>, JSONRPCErrorError> {
self.apps_list_inner(request_id, params)
self.apps_list_inner(request_id, params, mcp_client_capabilities)
.await
.map(|response| response.map(Into::into))
}
@@ -45,6 +47,7 @@ impl AppsRequestProcessor {
&self,
request_id: &ConnectionRequestId,
params: AppsListParams,
mcp_client_capabilities: Option<McpClientCapabilities>,
) -> Result<Option<AppsListResponse>, JSONRPCErrorError> {
let thread = if let Some(thread_id) = params.thread_id.as_deref() {
let (_, loaded_thread) = self.load_thread(thread_id).await?;
@@ -92,7 +95,14 @@ impl AppsRequestProcessor {
tokio::spawn(async move {
tokio::select! {
_ = shutdown_token.cancelled() => {}
_ = Self::apps_list_task(outgoing, request, params, config, environment_manager) => {}
_ = Self::apps_list_task(
outgoing,
request,
params,
config,
environment_manager,
mcp_client_capabilities,
) => {}
}
});
Ok(None)
@@ -108,11 +118,19 @@ impl AppsRequestProcessor {
params: AppsListParams,
config: Config,
environment_manager: Arc<EnvironmentManager>,
mcp_client_capabilities: Option<McpClientCapabilities>,
) {
let retry_params = params.clone();
let retry_config = config.clone();
let retry_environment_manager = Arc::clone(&environment_manager);
let result = Self::apps_list_response(&outgoing, params, config, environment_manager).await;
let result = Self::apps_list_response(
&outgoing,
params,
config,
environment_manager,
mcp_client_capabilities.clone(),
)
.await;
let should_retry = result
.as_ref()
.is_ok_and(|(_, codex_apps_ready)| !codex_apps_ready);
@@ -128,6 +146,7 @@ impl AppsRequestProcessor {
retry_params,
retry_config,
retry_environment_manager,
mcp_client_capabilities,
)
.await
{
@@ -141,6 +160,7 @@ impl AppsRequestProcessor {
params: AppsListParams,
config: Config,
environment_manager: Arc<EnvironmentManager>,
mcp_client_capabilities: Option<McpClientCapabilities>,
) -> Result<(AppsListResponse, bool), JSONRPCErrorError> {
let AppsListParams {
cursor,
@@ -157,7 +177,10 @@ impl AppsRequestProcessor {
};
let (mut accessible_connectors, mut all_connectors) = tokio::join!(
connectors::list_cached_accessible_connectors_from_mcp_tools(&config),
connectors::list_cached_accessible_connectors_from_mcp_tools(
&config,
mcp_client_capabilities.as_ref(),
),
connectors::list_cached_all_connectors(&config)
);
let cached_all_connectors = all_connectors.clone();
@@ -172,6 +195,7 @@ impl AppsRequestProcessor {
&accessible_config,
force_refetch,
Arc::clone(&environment_manager),
mcp_client_capabilities,
)
.await
.map_err(|err| format!("failed to load accessible apps: {err}"));
@@ -67,17 +67,22 @@ 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, request_attestation, opt_out_notification_methods) =
match params.capabilities {
Some(capabilities) => (
capabilities.experimental_api,
capabilities.request_attestation,
capabilities
.opt_out_notification_methods
.unwrap_or_default(),
),
None => (false, false, Vec::new()),
};
let (
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),
};
let ClientInfo {
name,
title: _title,
@@ -101,6 +106,7 @@ impl InitializeRequestProcessor {
app_server_client_name: name.clone(),
client_version: version,
request_attestation,
mcp_client_capabilities,
})
.is_err()
{
@@ -1,4 +1,5 @@
use super::*;
use codex_app_server_protocol::McpClientCapabilities;
const MCP_TOOL_THREAD_ID_META_KEY: &str = "threadId";
@@ -47,8 +48,9 @@ impl McpRequestProcessor {
&self,
request_id: &ConnectionRequestId,
params: ListMcpServerStatusParams,
mcp_client_capabilities: Option<McpClientCapabilities>,
) -> Result<Option<ClientResponsePayload>, JSONRPCErrorError> {
self.list_mcp_server_status(request_id, params)
self.list_mcp_server_status(request_id, params, mcp_client_capabilities)
.await
.map(|()| None)
}
@@ -57,8 +59,9 @@ impl McpRequestProcessor {
&self,
request_id: &ConnectionRequestId,
params: McpResourceReadParams,
mcp_client_capabilities: Option<McpClientCapabilities>,
) -> Result<Option<ClientResponsePayload>, JSONRPCErrorError> {
self.read_mcp_resource(request_id, params)
self.read_mcp_resource(request_id, params, mcp_client_capabilities)
.await
.map(|()| None)
}
@@ -195,6 +198,7 @@ impl McpRequestProcessor {
&self,
request_id: &ConnectionRequestId,
params: ListMcpServerStatusParams,
mcp_client_capabilities: Option<McpClientCapabilities>,
) -> Result<(), JSONRPCErrorError> {
let request = request_id.clone();
@@ -229,6 +233,7 @@ impl McpRequestProcessor {
mcp_config,
auth,
runtime_context,
mcp_client_capabilities,
)
.await;
});
@@ -242,6 +247,7 @@ impl McpRequestProcessor {
mcp_config: codex_mcp::McpConfig,
auth: Option<CodexAuth>,
runtime_context: McpRuntimeContext,
mcp_client_capabilities: Option<McpClientCapabilities>,
) {
let result = Self::list_mcp_server_status_response(
request_id.request_id.to_string(),
@@ -249,6 +255,7 @@ impl McpRequestProcessor {
mcp_config,
auth,
runtime_context,
mcp_client_capabilities,
)
.await;
outgoing.send_result(request_id, result).await;
@@ -260,6 +267,7 @@ impl McpRequestProcessor {
mcp_config: codex_mcp::McpConfig,
auth: Option<CodexAuth>,
runtime_context: McpRuntimeContext,
mcp_client_capabilities: Option<McpClientCapabilities>,
) -> Result<ListMcpServerStatusResponse, JSONRPCErrorError> {
let detail = match params.detail.unwrap_or(McpServerStatusDetail::Full) {
McpServerStatusDetail::Full => McpSnapshotDetail::Full,
@@ -272,6 +280,7 @@ impl McpRequestProcessor {
request_id,
runtime_context,
detail,
mcp_client_capabilities,
)
.await;
@@ -341,6 +350,7 @@ impl McpRequestProcessor {
&self,
request_id: &ConnectionRequestId,
params: McpResourceReadParams,
mcp_client_capabilities: Option<McpClientCapabilities>,
) -> Result<(), JSONRPCErrorError> {
let outgoing = Arc::clone(&self.outgoing);
let McpResourceReadParams {
@@ -380,6 +390,7 @@ impl McpRequestProcessor {
runtime_context,
&server,
&uri,
mcp_client_capabilities,
)
.await
.and_then(|result| serde_json::to_value(result).map_err(anyhow::Error::from));
@@ -1,6 +1,7 @@
use super::*;
use crate::error_code::internal_error;
use crate::error_code::invalid_request;
use codex_app_server_protocol::McpClientCapabilities;
use codex_app_server_protocol::PluginAvailability;
use codex_app_server_protocol::PluginInstallPolicy;
use codex_app_server_protocol::PluginSharePrincipalRole;
@@ -363,8 +364,9 @@ impl PluginRequestProcessor {
pub(crate) async fn plugin_read(
&self,
params: PluginReadParams,
mcp_client_capabilities: Option<McpClientCapabilities>,
) -> Result<Option<ClientResponsePayload>, JSONRPCErrorError> {
self.plugin_read_response(params)
self.plugin_read_response(params, mcp_client_capabilities)
.await
.map(|response| Some(response.into()))
}
@@ -925,6 +927,7 @@ impl PluginRequestProcessor {
async fn plugin_read_response(
&self,
params: PluginReadParams,
mcp_client_capabilities: Option<McpClientCapabilities>,
) -> Result<PluginReadResponse, JSONRPCErrorError> {
let plugins_manager = self.thread_manager.plugins_manager();
let PluginReadParams {
@@ -1017,6 +1020,7 @@ impl PluginRequestProcessor {
&config,
&outcome.plugin.apps,
Arc::clone(&environment_manager),
mcp_client_capabilities.clone(),
)
.await;
let visible_skills = outcome
@@ -1099,6 +1103,7 @@ impl PluginRequestProcessor {
&config,
&plugin_apps,
Arc::clone(&environment_manager),
mcp_client_capabilities,
)
.await;
remote_plugin_detail_to_info(remote_detail, app_summaries)
@@ -1568,7 +1573,8 @@ impl PluginRequestProcessor {
connectors::list_accessible_connectors_from_mcp_tools_with_environment_manager(
config,
/*force_refetch*/ true,
Arc::clone(&environment_manager)
Arc::clone(&environment_manager),
None,
),
);
@@ -1593,7 +1599,7 @@ impl PluginRequestProcessor {
"failed to load accessible apps after plugin install: {err:#}"
);
(
connectors::list_cached_accessible_connectors_from_mcp_tools(config)
connectors::list_cached_accessible_connectors_from_mcp_tools(config, None)
.await
.unwrap_or_default(),
false,
@@ -1835,6 +1841,7 @@ async fn load_plugin_app_summaries(
config: &Config,
plugin_apps: &[codex_plugin::AppConnectorId],
environment_manager: Arc<EnvironmentManager>,
mcp_client_capabilities: Option<McpClientCapabilities>,
) -> Vec<AppSummary> {
if plugin_apps.is_empty() {
return Vec::new();
@@ -1858,6 +1865,7 @@ async fn load_plugin_app_summaries(
config,
/*force_refetch*/ false,
environment_manager,
mcp_client_capabilities,
)
.await
{
@@ -1,5 +1,6 @@
use super::*;
use crate::error_code::method_not_found;
use codex_app_server_protocol::McpClientCapabilities;
use codex_protocol::models::BUILT_IN_PERMISSION_PROFILE_DANGER_FULL_ACCESS;
use codex_protocol::models::BUILT_IN_PERMISSION_PROFILE_WORKSPACE;
@@ -370,6 +371,7 @@ impl ThreadRequestProcessor {
params: ThreadStartParams,
app_server_client_name: Option<String>,
app_server_client_version: Option<String>,
mcp_client_capabilities: Option<McpClientCapabilities>,
request_context: RequestContext,
) -> Result<Option<ClientResponsePayload>, JSONRPCErrorError> {
self.thread_start_inner(
@@ -377,6 +379,7 @@ impl ThreadRequestProcessor {
params,
app_server_client_name,
app_server_client_version,
mcp_client_capabilities,
request_context,
)
.await
@@ -399,12 +402,14 @@ impl ThreadRequestProcessor {
params: ThreadResumeParams,
app_server_client_name: Option<String>,
app_server_client_version: Option<String>,
mcp_client_capabilities: Option<McpClientCapabilities>,
) -> Result<Option<ClientResponsePayload>, JSONRPCErrorError> {
self.thread_resume_inner(
request_id,
params,
app_server_client_name,
app_server_client_version,
mcp_client_capabilities,
)
.await
.map(|()| None)
@@ -416,12 +421,14 @@ impl ThreadRequestProcessor {
params: ThreadForkParams,
app_server_client_name: Option<String>,
app_server_client_version: Option<String>,
mcp_client_capabilities: Option<McpClientCapabilities>,
) -> Result<Option<ClientResponsePayload>, JSONRPCErrorError> {
self.thread_fork_inner(
request_id,
params,
app_server_client_name,
app_server_client_version,
mcp_client_capabilities,
)
.await
.map(|()| None)
@@ -680,6 +687,7 @@ impl ThreadRequestProcessor {
thread: &CodexThread,
app_server_client_name: Option<String>,
app_server_client_version: Option<String>,
mcp_client_capabilities: Option<McpClientCapabilities>,
) -> Result<(), JSONRPCErrorError> {
let mcp_elicitations_auto_deny = xcode_26_4_mcp_elicitations_auto_deny(
app_server_client_name.as_deref(),
@@ -690,6 +698,7 @@ impl ThreadRequestProcessor {
app_server_client_name,
app_server_client_version,
mcp_elicitations_auto_deny,
mcp_client_capabilities,
)
.await
.map_err(|err| internal_error(format!("failed to set app server client info: {err}")))
@@ -805,6 +814,7 @@ impl ThreadRequestProcessor {
params: ThreadStartParams,
app_server_client_name: Option<String>,
app_server_client_version: Option<String>,
mcp_client_capabilities: Option<McpClientCapabilities>,
request_context: RequestContext,
) -> Result<(), JSONRPCErrorError> {
let ThreadStartParams {
@@ -874,6 +884,7 @@ impl ThreadRequestProcessor {
request_id,
app_server_client_name,
app_server_client_version,
mcp_client_capabilities,
config,
typesafe_overrides,
dynamic_tools,
@@ -946,6 +957,7 @@ impl ThreadRequestProcessor {
request_id: ConnectionRequestId,
app_server_client_name: Option<String>,
app_server_client_version: Option<String>,
mcp_client_capabilities: Option<McpClientCapabilities>,
config_overrides: Option<HashMap<String, serde_json::Value>>,
typesafe_overrides: ConfigOverrides,
dynamic_tools: Option<Vec<ApiDynamicToolSpec>>,
@@ -1094,6 +1106,7 @@ impl ThreadRequestProcessor {
thread.as_ref(),
app_server_client_name,
app_server_client_version,
mcp_client_capabilities,
)
.await?;
@@ -2381,6 +2394,7 @@ impl ThreadRequestProcessor {
params: ThreadResumeParams,
app_server_client_name: Option<String>,
app_server_client_version: Option<String>,
mcp_client_capabilities: Option<McpClientCapabilities>,
) -> Result<(), JSONRPCErrorError> {
if let Ok(thread_id) = ThreadId::from_string(&params.thread_id)
&& self
@@ -2425,6 +2439,7 @@ impl ThreadRequestProcessor {
&params,
app_server_client_name.clone(),
app_server_client_version.clone(),
mcp_client_capabilities.clone(),
)
.await
{
@@ -2533,6 +2548,7 @@ impl ThreadRequestProcessor {
codex_thread.as_ref(),
app_server_client_name,
app_server_client_version,
mcp_client_capabilities.clone(),
)
.await
{
@@ -2707,6 +2723,7 @@ impl ThreadRequestProcessor {
params: &ThreadResumeParams,
app_server_client_name: Option<String>,
app_server_client_version: Option<String>,
mcp_client_capabilities: Option<McpClientCapabilities>,
) -> Result<bool, JSONRPCErrorError> {
let running_thread = if params.history.is_some() {
if let Ok(existing_thread_id) = ThreadId::from_string(&params.thread_id)
@@ -2829,6 +2846,7 @@ impl ThreadRequestProcessor {
existing_thread.as_ref(),
app_server_client_name,
app_server_client_version,
mcp_client_capabilities,
)
.await?;
@@ -3127,6 +3145,7 @@ impl ThreadRequestProcessor {
params: ThreadForkParams,
app_server_client_name: Option<String>,
app_server_client_version: Option<String>,
mcp_client_capabilities: Option<McpClientCapabilities>,
) -> Result<(), JSONRPCErrorError> {
let ThreadForkParams {
thread_id,
@@ -3250,6 +3269,7 @@ impl ThreadRequestProcessor {
forked_thread.as_ref(),
app_server_client_name,
app_server_client_version,
mcp_client_capabilities,
)
.await?;
if session_configured.rollout_path.is_some()
@@ -1,4 +1,5 @@
use super::*;
use codex_app_server_protocol::McpClientCapabilities;
use codex_protocol::protocol::AdditionalContextEntry as CoreAdditionalContextEntry;
use codex_protocol::protocol::AdditionalContextKind as CoreAdditionalContextKind;
@@ -95,12 +96,14 @@ impl TurnRequestProcessor {
params: TurnStartParams,
app_server_client_name: Option<String>,
app_server_client_version: Option<String>,
mcp_client_capabilities: Option<McpClientCapabilities>,
) -> Result<Option<ClientResponsePayload>, JSONRPCErrorError> {
self.turn_start_inner(
request_id,
params,
app_server_client_name,
app_server_client_version,
mcp_client_capabilities,
)
.await
.map(|response| Some(response.into()))
@@ -200,10 +203,19 @@ impl TurnRequestProcessor {
&self,
request_id: &ConnectionRequestId,
params: ReviewStartParams,
app_server_client_name: Option<String>,
app_server_client_version: Option<String>,
mcp_client_capabilities: Option<McpClientCapabilities>,
) -> Result<Option<ClientResponsePayload>, JSONRPCErrorError> {
self.review_start_inner(request_id, params)
.await
.map(|()| None)
self.review_start_inner(
request_id,
params,
app_server_client_name,
app_server_client_version,
mcp_client_capabilities,
)
.await
.map(|()| None)
}
fn track_error_response(
@@ -369,6 +381,7 @@ impl TurnRequestProcessor {
params: TurnStartParams,
app_server_client_name: Option<String>,
app_server_client_version: Option<String>,
mcp_client_capabilities: Option<McpClientCapabilities>,
) -> Result<TurnStartResponse, JSONRPCErrorError> {
if let Err(error) = Self::validate_v2_input_limit(&params.input) {
self.track_error_response(
@@ -388,6 +401,7 @@ impl TurnRequestProcessor {
thread.as_ref(),
app_server_client_name,
app_server_client_version,
mcp_client_capabilities,
)
.await
.inspect_err(|error| {
@@ -736,6 +750,7 @@ impl TurnRequestProcessor {
thread: &CodexThread,
app_server_client_name: Option<String>,
app_server_client_version: Option<String>,
mcp_client_capabilities: Option<McpClientCapabilities>,
) -> Result<(), JSONRPCErrorError> {
let mcp_elicitations_auto_deny = xcode_26_4_mcp_elicitations_auto_deny(
app_server_client_name.as_deref(),
@@ -746,6 +761,7 @@ impl TurnRequestProcessor {
app_server_client_name,
app_server_client_version,
mcp_elicitations_auto_deny,
mcp_client_capabilities,
)
.await
.map_err(|err| internal_error(format!("failed to set app server client info: {err}")))
@@ -1065,6 +1081,9 @@ impl TurnRequestProcessor {
parent_thread: Arc<CodexThread>,
review_request: ReviewRequest,
display_text: &str,
app_server_client_name: Option<String>,
app_server_client_version: Option<String>,
mcp_client_capabilities: Option<McpClientCapabilities>,
) -> std::result::Result<(), JSONRPCErrorError> {
parent_thread.ensure_rollout_materialized().await;
parent_thread.flush_rollout().await.map_err(|err| {
@@ -1108,6 +1127,14 @@ impl TurnRequestProcessor {
internal_error(format!("error creating detached review thread: {err}"))
})?;
Self::set_app_server_client_info(
review_thread.as_ref(),
app_server_client_name,
app_server_client_version,
mcp_client_capabilities,
)
.await?;
log_listener_attach_result(
self.ensure_conversation_listener(
thread_id,
@@ -1173,6 +1200,9 @@ impl TurnRequestProcessor {
&self,
request_id: &ConnectionRequestId,
params: ReviewStartParams,
app_server_client_name: Option<String>,
app_server_client_version: Option<String>,
mcp_client_capabilities: Option<McpClientCapabilities>,
) -> Result<(), JSONRPCErrorError> {
let ReviewStartParams {
thread_id,
@@ -1181,6 +1211,13 @@ impl TurnRequestProcessor {
} = params;
let (parent_thread_id, parent_thread) = self.load_thread(&thread_id).await?;
Self::set_app_server_client_info(
parent_thread.as_ref(),
app_server_client_name.clone(),
app_server_client_version.clone(),
mcp_client_capabilities.clone(),
)
.await?;
let (review_request, display_text) = Self::review_request_from_target(target)?;
match delivery.unwrap_or(ApiReviewDelivery::Inline).to_core() {
CoreReviewDelivery::Inline => {
@@ -1200,6 +1237,9 @@ impl TurnRequestProcessor {
parent_thread,
review_request,
&display_text,
app_server_client_name,
app_server_client_version,
mcp_client_capabilities,
)
.await?;
}
@@ -90,6 +90,7 @@ async fn attestation_generate_round_trip_adds_header_to_responses_websocket_hand
experimental_api: true,
request_attestation: true,
opt_out_notification_methods: None,
mcp_client_capabilities: None,
}),
),
)
@@ -39,6 +39,7 @@ async fn mock_experimental_method_requires_experimental_api_capability() -> Resu
experimental_api: false,
request_attestation: false,
opt_out_notification_methods: None,
mcp_client_capabilities: None,
}),
)
.await?;
@@ -70,6 +71,7 @@ async fn realtime_conversation_start_requires_experimental_api_capability() -> R
experimental_api: false,
request_attestation: false,
opt_out_notification_methods: None,
mcp_client_capabilities: None,
}),
)
.await?;
@@ -108,6 +110,7 @@ async fn thread_memory_mode_set_requires_experimental_api_capability() -> Result
experimental_api: false,
request_attestation: false,
opt_out_notification_methods: None,
mcp_client_capabilities: None,
}),
)
.await?;
@@ -142,6 +145,7 @@ async fn thread_settings_update_requires_experimental_api_capability() -> Result
experimental_api: false,
request_attestation: false,
opt_out_notification_methods: None,
mcp_client_capabilities: None,
}),
)
.await?;
@@ -176,6 +180,7 @@ async fn realtime_webrtc_start_requires_experimental_api_capability() -> Result<
experimental_api: false,
request_attestation: false,
opt_out_notification_methods: None,
mcp_client_capabilities: None,
}),
)
.await?;
@@ -218,6 +223,7 @@ async fn thread_start_mock_field_requires_experimental_api_capability() -> Resul
experimental_api: false,
request_attestation: false,
opt_out_notification_methods: None,
mcp_client_capabilities: None,
}),
)
.await?;
@@ -256,6 +262,7 @@ async fn thread_start_without_dynamic_tools_allows_without_experimental_api_capa
experimental_api: false,
request_attestation: false,
opt_out_notification_methods: None,
mcp_client_capabilities: None,
}),
)
.await?;
@@ -293,6 +300,7 @@ async fn thread_start_granular_approval_policy_requires_experimental_api_capabil
experimental_api: false,
request_attestation: false,
opt_out_notification_methods: None,
mcp_client_capabilities: None,
}),
)
.await?;
@@ -214,6 +214,7 @@ async fn initialize_opt_out_notification_methods_filters_notifications() -> Resu
experimental_api: true,
request_attestation: false,
opt_out_notification_methods: Some(vec!["thread/started".to_string()]),
mcp_client_capabilities: None,
}),
),
)
@@ -148,6 +148,7 @@ async fn thread_status_changed_can_be_opted_out() -> Result<()> {
experimental_api: true,
request_attestation: false,
opt_out_notification_methods: Some(vec!["thread/status/changed".to_string()]),
mcp_client_capabilities: None,
}),
),
)
+1
View File
@@ -13,6 +13,7 @@ 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;
@@ -0,0 +1,199 @@
use codex_protocol::mcp::McpAppUiCapability;
use codex_protocol::mcp::McpClientCapabilities;
use rmcp::model::ExtensionCapabilities;
use rmcp::model::Meta;
use rmcp::model::PaginatedRequestParams;
use serde_json::Map;
use serde_json::Value as JsonValue;
use sha1::Digest;
use sha1::Sha1;
const MCP_APPS_UI_EXTENSION: &str = "io.modelcontextprotocol/ui";
const MCP_CLIENT_CAPABILITIES_META: &str = "openai/clientCapabilities";
const MCP_FUTURE_CLIENT_CAPABILITIES_META: &str = "io.modelcontextprotocol/clientCapabilities";
const MCP_APP_WEB_VIEW_MIME_TYPE: &str = "text/html;profile=mcp-app";
const MCP_APP_DECLARATIVE_UI_MIME_TYPE: &str = "text/x-dil;profile=mcp-app";
pub(crate) fn fingerprint(capabilities: &McpClientCapabilities) -> String {
let canonical = serde_json::to_string(capabilities).unwrap_or_default();
let digest = Sha1::digest(format!("v1:{canonical}").as_bytes());
format!("{digest:x}")
}
fn app_ui_mime_types(capabilities: &McpClientCapabilities) -> Vec<&'static str> {
capabilities
.app_ui
.iter()
.map(|capability| match capability {
McpAppUiCapability::WebView => MCP_APP_WEB_VIEW_MIME_TYPE,
McpAppUiCapability::DeclarativeUi => MCP_APP_DECLARATIVE_UI_MIME_TYPE,
})
.collect()
}
fn as_json(capabilities: &McpClientCapabilities) -> JsonValue {
serde_json::json!({
"extensions": {
MCP_APPS_UI_EXTENSION: {
"mimeTypes": app_ui_mime_types(capabilities),
}
}
})
}
pub(crate) fn as_extensions(capabilities: &McpClientCapabilities) -> ExtensionCapabilities {
let settings = [(
"mimeTypes".to_string(),
JsonValue::Array(
app_ui_mime_types(capabilities)
.into_iter()
.map(|mime_type| JsonValue::String(mime_type.to_string()))
.collect(),
),
)]
.into_iter()
.collect();
[(MCP_APPS_UI_EXTENSION.to_string(), settings)]
.into_iter()
.collect()
}
pub(crate) fn add_meta(meta: &mut Option<Meta>, capabilities: Option<&McpClientCapabilities>) {
if let Some(meta) = meta.as_mut() {
meta.remove(MCP_CLIENT_CAPABILITIES_META);
meta.remove(MCP_FUTURE_CLIENT_CAPABILITIES_META);
}
let Some(capabilities) = capabilities else {
return;
};
meta.get_or_insert_with(Meta::new).insert(
MCP_CLIENT_CAPABILITIES_META.to_string(),
as_json(capabilities),
);
}
pub(crate) fn add_json_meta(
meta: &mut Option<JsonValue>,
capabilities: Option<&McpClientCapabilities>,
) {
if let Some(object) = meta.as_mut().and_then(JsonValue::as_object_mut) {
object.remove(MCP_CLIENT_CAPABILITIES_META);
object.remove(MCP_FUTURE_CLIENT_CAPABILITIES_META);
}
let Some(capabilities) = capabilities else {
return;
};
if !meta.as_ref().is_some_and(JsonValue::is_object) {
*meta = Some(JsonValue::Object(Map::new()));
}
meta.as_mut()
.and_then(JsonValue::as_object_mut)
.expect("metadata was normalized to an object")
.insert(
MCP_CLIENT_CAPABILITIES_META.to_string(),
as_json(capabilities),
);
}
pub(crate) fn paginated_params(
cursor: Option<String>,
capabilities: Option<&McpClientCapabilities>,
) -> Option<PaginatedRequestParams> {
if cursor.is_none() && capabilities.is_none() {
return None;
}
let mut params = PaginatedRequestParams::default().with_cursor(cursor);
add_meta(&mut params.meta, capabilities);
Some(params)
}
#[cfg(test)]
mod tests {
use super::*;
fn combined_capabilities() -> McpClientCapabilities {
McpClientCapabilities {
app_ui: [
McpAppUiCapability::DeclarativeUi,
McpAppUiCapability::WebView,
]
.into_iter()
.collect(),
}
}
#[test]
fn semantic_capabilities_map_to_mcp_mime_types() {
assert_eq!(
as_json(&combined_capabilities()),
serde_json::json!({
"extensions": {
"io.modelcontextprotocol/ui": {
"mimeTypes": [
"text/html;profile=mcp-app",
"text/x-dil;profile=mcp-app",
],
},
},
})
);
}
#[test]
fn fingerprint_is_stable_for_sorted_enum_set() {
let first = combined_capabilities();
let second = McpClientCapabilities {
app_ui: [
McpAppUiCapability::WebView,
McpAppUiCapability::DeclarativeUi,
]
.into_iter()
.collect(),
};
assert_eq!(fingerprint(&first), fingerprint(&second));
assert_ne!(
fingerprint(&first),
fingerprint(&McpClientCapabilities::default())
);
}
#[test]
fn request_metadata_removes_both_reserved_forms() {
let mut meta = Some(serde_json::json!({
MCP_CLIENT_CAPABILITIES_META: {"spoofed": true},
MCP_FUTURE_CLIENT_CAPABILITIES_META: {"spoofed": true},
"caller": "preserved",
}));
add_json_meta(&mut meta, Some(&McpClientCapabilities::default()));
assert_eq!(
meta,
Some(serde_json::json!({
"caller": "preserved",
MCP_CLIENT_CAPABILITIES_META: {
"extensions": {
MCP_APPS_UI_EXTENSION: {"mimeTypes": []},
},
},
}))
);
}
#[test]
fn request_metadata_replaces_non_object_values() {
let mut meta = Some(serde_json::json!("invalid"));
add_json_meta(&mut meta, Some(&McpClientCapabilities::default()));
assert_eq!(
meta,
Some(serde_json::json!({
MCP_CLIENT_CAPABILITIES_META: {
"extensions": {
MCP_APPS_UI_EXTENSION: {"mimeTypes": []},
},
},
}))
);
}
}
+6 -3
View File
@@ -41,6 +41,7 @@ pub fn codex_apps_tools_cache_key(auth: Option<&CodexAuth>) -> CodexAppsToolsCac
pub(crate) struct CodexAppsToolsCacheContext {
pub(crate) codex_home: PathBuf,
pub(crate) user_key: CodexAppsToolsCacheKey,
pub(crate) client_capabilities_fingerprint: Option<String>,
}
impl CodexAppsToolsCacheContext {
@@ -55,9 +56,11 @@ impl CodexAppsToolsCacheContext {
fn cache_path_in(&self, cache_dir: &str) -> PathBuf {
let user_key_json = serde_json::to_string(&self.user_key).unwrap_or_default();
let user_key_hash = sha1_hex(&user_key_json);
self.codex_home
.join(cache_dir)
.join(format!("{user_key_hash}.json"))
let filename = self.client_capabilities_fingerprint.as_ref().map_or_else(
|| format!("{user_key_hash}.json"),
|fingerprint| format!("{user_key_hash}-{fingerprint}.json"),
);
self.codex_home.join(cache_dir).join(filename)
}
}
+151 -10
View File
@@ -14,6 +14,7 @@ use std::time::Duration;
use std::time::Instant;
use crate::McpAuthStatusEntry;
use crate::client_capabilities;
use crate::codex_apps::CodexAppsToolsCacheContext;
use crate::codex_apps::CodexAppsToolsCacheKey;
use crate::codex_apps::write_cached_codex_apps_tools_if_needed;
@@ -40,11 +41,13 @@ use anyhow::Context;
use anyhow::Result;
use anyhow::anyhow;
use async_channel::Sender;
use codex_api::SharedAuthProvider;
use codex_config::Constrained;
use codex_config::McpServerTransportConfig;
use codex_config::types::OAuthCredentialsStoreMode;
use codex_login::CodexAuth;
use codex_protocol::mcp::CallToolResult;
use codex_protocol::mcp::McpClientCapabilities;
use codex_protocol::mcp::McpServerInfo;
use codex_protocol::models::PermissionProfile;
use codex_protocol::protocol::AskForApproval;
@@ -110,6 +113,7 @@ pub struct McpConnectionManager {
prefix_mcp_tool_names: bool,
elicitation_requests: ElicitationRequestManager,
startup_cancellation_token: CancellationToken,
codex_apps_client_capabilities: Option<McpClientCapabilities>,
}
impl McpConnectionManager {
@@ -142,6 +146,7 @@ impl McpConnectionManager {
/*reviewer*/ None,
),
startup_cancellation_token: CancellationToken::new(),
codex_apps_client_capabilities: None,
}
}
@@ -189,6 +194,10 @@ impl McpConnectionManager {
self.host_owned_codex_apps_enabled && server_name == CODEX_APPS_MCP_SERVER_NAME
}
pub fn codex_apps_client_capabilities(&self) -> Option<&McpClientCapabilities> {
self.codex_apps_client_capabilities.as_ref()
}
pub fn set_approval_policy(&self, approval_policy: &Constrained<AskForApproval>) {
if let Ok(mut policy) = self.elicitation_requests.approval_policy.lock() {
*policy = approval_policy.value();
@@ -227,6 +236,7 @@ impl McpConnectionManager {
tool_plugin_provenance: ToolPluginProvenance,
auth: Option<&CodexAuth>,
elicitation_reviewer: Option<ElicitationReviewerHandle>,
codex_apps_client_capabilities: Option<McpClientCapabilities>,
) -> (Self, CancellationToken) {
let cancel_token = CancellationToken::new();
let mut clients = HashMap::new();
@@ -262,6 +272,9 @@ impl McpConnectionManager {
Some(CodexAppsToolsCacheContext {
codex_home: codex_home.clone(),
user_key: codex_apps_tools_cache_key.clone(),
client_capabilities_fingerprint: codex_apps_client_capabilities
.as_ref()
.map(client_capabilities::fingerprint),
})
} else {
None
@@ -290,6 +303,9 @@ 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(),
Arc::clone(&tool_plugin_provenance),
runtime_context.clone(),
runtime_auth_provider,
@@ -338,6 +354,7 @@ impl McpConnectionManager {
prefix_mcp_tool_names,
elicitation_requests: elicitation_requests.clone(),
startup_cancellation_token: cancel_token.clone(),
codex_apps_client_capabilities,
};
tokio::spawn(async move {
let outcomes = join_set.join_all().await;
@@ -364,6 +381,115 @@ impl McpConnectionManager {
(manager, cancel_token)
}
#[allow(clippy::too_many_arguments)]
pub async fn refresh_codex_apps_client(
&mut self,
server: Option<EffectiveMcpServer>,
store_mode: OAuthCredentialsStoreMode,
auth_entry: Option<McpAuthStatusEntry>,
submit_id: String,
tx_event: Sender<Event>,
codex_home: PathBuf,
codex_apps_tools_cache_key: CodexAppsToolsCacheKey,
client_capabilities: Option<McpClientCapabilities>,
runtime_context: McpRuntimeContext,
runtime_auth_provider: Option<SharedAuthProvider>,
client_elicitation_capability: ElicitationCapability,
) {
if let Some(client) = self.clients.remove(CODEX_APPS_MCP_SERVER_NAME) {
client.shutdown().await;
}
self.server_metadata.remove(CODEX_APPS_MCP_SERVER_NAME);
self.codex_apps_client_capabilities = client_capabilities.clone();
let Some(server) = server.filter(EffectiveMcpServer::enabled) else {
return;
};
self.server_metadata.insert(
CODEX_APPS_MCP_SERVER_NAME.to_string(),
McpServerMetadata::from(&server),
);
let _ = emit_update(
submit_id.as_str(),
&tx_event,
McpStartupUpdateEvent {
server: CODEX_APPS_MCP_SERVER_NAME.to_string(),
status: McpStartupStatus::Starting,
},
)
.await;
let cancel_token = self.startup_cancellation_token.child_token();
let client = AsyncManagedClient::new(
CODEX_APPS_MCP_SERVER_NAME.to_string(),
server,
store_mode,
cancel_token.clone(),
tx_event.clone(),
self.elicitation_requests.clone(),
Some(CodexAppsToolsCacheContext {
codex_home,
user_key: codex_apps_tools_cache_key,
client_capabilities_fingerprint: client_capabilities
.as_ref()
.map(client_capabilities::fingerprint),
}),
client_capabilities,
Arc::clone(&self.tool_plugin_provenance),
runtime_context,
runtime_auth_provider,
client_elicitation_capability,
);
self.clients
.insert(CODEX_APPS_MCP_SERVER_NAME.to_string(), client.clone());
tokio::spawn(async move {
let mut outcome = client.client().await;
if cancel_token.is_cancelled() {
outcome = Err(StartupOutcomeError::Cancelled);
}
let status = match &outcome {
Ok(_) => McpStartupStatus::Ready,
Err(StartupOutcomeError::Cancelled) => McpStartupStatus::Cancelled,
Err(error) => McpStartupStatus::Failed {
error: mcp_init_error_display(
CODEX_APPS_MCP_SERVER_NAME,
auth_entry.as_ref(),
error,
),
},
};
let _ = emit_update(
submit_id.as_str(),
&tx_event,
McpStartupUpdateEvent {
server: CODEX_APPS_MCP_SERVER_NAME.to_string(),
status,
},
)
.await;
let mut summary = McpStartupCompleteEvent::default();
match outcome {
Ok(_) => summary.ready.push(CODEX_APPS_MCP_SERVER_NAME.to_string()),
Err(StartupOutcomeError::Cancelled) => summary
.cancelled
.push(CODEX_APPS_MCP_SERVER_NAME.to_string()),
Err(StartupOutcomeError::Failed { error }) => {
summary.failed.push(McpStartupFailure {
server: CODEX_APPS_MCP_SERVER_NAME.to_string(),
error,
});
}
}
let _ = tx_event
.send(Event {
id: submit_id,
msg: EventMsg::McpStartupComplete(summary),
})
.await;
});
}
pub async fn resolve_elicitation(
&self,
server_name: String,
@@ -498,6 +624,7 @@ impl McpConnectionManager {
&managed_client.client,
managed_client.tool_timeout,
managed_client.server_instructions.as_deref(),
managed_client.client_capabilities.as_ref(),
)
.await
.with_context(|| {
@@ -561,15 +688,17 @@ impl McpConnectionManager {
};
let timeout = managed_client.tool_timeout;
let client = managed_client.client.clone();
let client_capabilities = managed_client.client_capabilities.clone();
join_set.spawn(async move {
let mut collected: Vec<Resource> = Vec::new();
let mut cursor: Option<String> = None;
loop {
let params = cursor.as_ref().map(|next| {
PaginatedRequestParams::default().with_cursor(Some(next.clone()))
});
let params = client_capabilities::paginated_params(
cursor.clone(),
client_capabilities.as_ref(),
);
let response = match client.list_resources(params, timeout).await {
Ok(result) => result,
Err(err) => return (server_name, Err(err)),
@@ -626,15 +755,17 @@ impl McpConnectionManager {
};
let client = managed_client.client.clone();
let timeout = managed_client.tool_timeout;
let client_capabilities = managed_client.client_capabilities.clone();
join_set.spawn(async move {
let mut collected: Vec<ResourceTemplate> = Vec::new();
let mut cursor: Option<String> = None;
loop {
let params = cursor.as_ref().map(|next| {
PaginatedRequestParams::default().with_cursor(Some(next.clone()))
});
let params = client_capabilities::paginated_params(
cursor.clone(),
client_capabilities.as_ref(),
);
let response = match client.list_resource_templates(params, timeout).await {
Ok(result) => result,
Err(err) => return (server_name_cloned, Err(err)),
@@ -687,7 +818,7 @@ impl McpConnectionManager {
server: &str,
tool: &str,
arguments: Option<serde_json::Value>,
meta: Option<serde_json::Value>,
mut meta: Option<serde_json::Value>,
) -> Result<CallToolResult> {
let client = self.client_by_name(server).await?;
if !client.tool_filter.allows(tool) {
@@ -696,6 +827,7 @@ impl McpConnectionManager {
));
}
client_capabilities::add_json_meta(&mut meta, client.client_capabilities.as_ref());
let result: rmcp::model::CallToolResult = client
.client
.call_tool(tool.to_string(), arguments, meta, client.tool_timeout)
@@ -733,11 +865,15 @@ impl McpConnectionManager {
pub async fn list_resources(
&self,
server: &str,
params: Option<PaginatedRequestParams>,
mut params: Option<PaginatedRequestParams>,
) -> Result<ListResourcesResult> {
let managed = self.client_by_name(server).await?;
let timeout = managed.tool_timeout;
if managed.client_capabilities.is_some() {
let params = params.get_or_insert_with(PaginatedRequestParams::default);
client_capabilities::add_meta(&mut params.meta, managed.client_capabilities.as_ref());
}
managed
.client
.list_resources(params, timeout)
@@ -749,12 +885,16 @@ impl McpConnectionManager {
pub async fn list_resource_templates(
&self,
server: &str,
params: Option<PaginatedRequestParams>,
mut params: Option<PaginatedRequestParams>,
) -> Result<ListResourceTemplatesResult> {
let managed = self.client_by_name(server).await?;
let client = managed.client.clone();
let timeout = managed.tool_timeout;
if managed.client_capabilities.is_some() {
let params = params.get_or_insert_with(PaginatedRequestParams::default);
client_capabilities::add_meta(&mut params.meta, managed.client_capabilities.as_ref());
}
client
.list_resource_templates(params, timeout)
.await
@@ -765,12 +905,13 @@ impl McpConnectionManager {
pub async fn read_resource(
&self,
server: &str,
params: ReadResourceRequestParams,
mut params: ReadResourceRequestParams,
) -> Result<ReadResourceResult> {
let managed = self.client_by_name(server).await?;
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
.read_resource(params, timeout)
@@ -82,9 +82,110 @@ fn create_codex_apps_tools_cache_context(
chatgpt_user_id: chatgpt_user_id.map(ToOwned::to_owned),
is_workspace_account: false,
},
client_capabilities_fingerprint: None,
}
}
#[test]
fn capability_profiles_partition_codex_apps_cache_paths() {
let legacy = create_codex_apps_tools_cache_context(PathBuf::from("/tmp/codex"), None, None);
let mut explicit_empty = legacy.clone();
explicit_empty.client_capabilities_fingerprint = Some(crate::client_capabilities::fingerprint(
&codex_protocol::mcp::McpClientCapabilities::default(),
));
assert_ne!(legacy.tools_cache_path(), explicit_empty.tools_cache_path());
assert_ne!(
legacy.server_info_cache_path(),
explicit_empty.server_info_cache_path()
);
assert!(
legacy
.tools_cache_path()
.to_string_lossy()
.ends_with(".json")
);
}
#[tokio::test]
async fn refreshing_codex_apps_capabilities_preserves_unrelated_clients() {
let approval_policy = Constrained::allow_any(AskForApproval::OnFailure);
let permission_profile = Constrained::allow_any(PermissionProfile::default());
let mut manager = McpConnectionManager::new_uninitialized(
&approval_policy,
&permission_profile,
/*prefix_mcp_tool_names*/ true,
);
let unrelated_cancel_token = CancellationToken::new();
let old_codex_apps_cancel_token = CancellationToken::new();
let pending_client = futures::future::pending::<Result<ManagedClient, StartupOutcomeError>>()
.boxed()
.shared();
let cancelled_client = futures::future::ready(Err(StartupOutcomeError::Cancelled))
.boxed()
.shared();
manager.clients.insert(
"local".to_string(),
AsyncManagedClient {
client: pending_client,
cached_tool_info_snapshot: None,
cached_server_info: None,
startup_complete: Arc::new(std::sync::atomic::AtomicBool::new(false)),
tool_plugin_provenance: Arc::new(ToolPluginProvenance::default()),
cancel_token: unrelated_cancel_token.clone(),
},
);
manager.clients.insert(
CODEX_APPS_MCP_SERVER_NAME.to_string(),
AsyncManagedClient {
client: cancelled_client,
cached_tool_info_snapshot: None,
cached_server_info: None,
startup_complete: Arc::new(std::sync::atomic::AtomicBool::new(true)),
tool_plugin_provenance: Arc::new(ToolPluginProvenance::default()),
cancel_token: old_codex_apps_cancel_token.clone(),
},
);
let capabilities = codex_protocol::mcp::McpClientCapabilities {
app_ui: [codex_protocol::mcp::McpAppUiCapability::WebView]
.into_iter()
.collect(),
};
let (tx_event, _rx_event) = async_channel::unbounded();
manager
.refresh_codex_apps_client(
None,
OAuthCredentialsStoreMode::default(),
None,
"test".to_string(),
tx_event,
tempdir().expect("tempdir").path().to_path_buf(),
CodexAppsToolsCacheKey {
account_id: None,
chatgpt_user_id: None,
is_workspace_account: false,
},
Some(capabilities.clone()),
McpRuntimeContext::new(
Arc::new(EnvironmentManager::without_environments()),
PathBuf::from("/tmp"),
),
None,
ElicitationCapability::default(),
)
.await;
assert!(old_codex_apps_cancel_token.is_cancelled());
assert!(!unrelated_cancel_token.is_cancelled());
assert!(manager.clients.contains_key("local"));
assert!(!manager.clients.contains_key(CODEX_APPS_MCP_SERVER_NAME));
assert_eq!(
manager.codex_apps_client_capabilities(),
Some(&capabilities)
);
}
fn create_test_server_info(title: &str) -> McpServerInfo {
McpServerInfo {
name: "codex-apps".to_string(),
@@ -1191,6 +1292,7 @@ async fn no_local_runtime_fails_local_stdio_but_keeps_local_http_server() {
ToolPluginProvenance::default(),
/*auth*/ None,
/*elicitation_reviewer*/ None,
/*codex_apps_client_capabilities*/ None,
)
.await;
+1
View File
@@ -53,6 +53,7 @@ pub use mcp::qualified_mcp_tool_name_prefix;
pub use tools::declared_openai_file_input_param_names;
pub(crate) mod auth_elicitation;
pub(crate) mod client_capabilities;
pub(crate) mod codex_apps;
pub(crate) mod connection_manager;
pub(crate) mod elicitation;
+5
View File
@@ -24,6 +24,7 @@ use codex_config::types::AppToolApproval;
use codex_config::types::OAuthCredentialsStoreMode;
use codex_login::CodexAuth;
use codex_plugin::PluginCapabilitySummary;
use codex_protocol::mcp::McpClientCapabilities;
use codex_protocol::mcp::McpServerInfo;
use codex_protocol::mcp::Resource;
use codex_protocol::mcp::ResourceTemplate;
@@ -266,6 +267,7 @@ pub async fn read_mcp_resource(
runtime_context: McpRuntimeContext,
server: &str,
uri: &str,
mcp_client_capabilities: Option<McpClientCapabilities>,
) -> anyhow::Result<ReadResourceResult> {
let mut mcp_servers = effective_mcp_servers(config, auth);
let host_owned_codex_apps_enabled = host_owned_codex_apps_enabled(config, auth);
@@ -295,6 +297,7 @@ pub async fn read_mcp_resource(
tool_plugin_provenance(config),
auth,
/*elicitation_reviewer*/ None,
mcp_client_capabilities,
)
.await;
@@ -321,6 +324,7 @@ pub async fn collect_mcp_server_status_snapshot_with_detail(
submit_id: String,
runtime_context: McpRuntimeContext,
detail: McpSnapshotDetail,
mcp_client_capabilities: Option<McpClientCapabilities>,
) -> McpServerStatusSnapshot {
let mcp_servers = effective_mcp_servers(config, auth);
let host_owned_codex_apps_enabled = host_owned_codex_apps_enabled(config, auth);
@@ -365,6 +369,7 @@ pub async fn collect_mcp_server_status_snapshot_with_detail(
tool_plugin_provenance,
auth,
/*elicitation_reviewer*/ None,
mcp_client_capabilities,
)
.await;
+15 -1
View File
@@ -16,6 +16,7 @@ use std::sync::atomic::Ordering;
use std::time::Duration;
use std::time::Instant;
use crate::client_capabilities;
use crate::codex_apps::CachedCodexAppsToolsLoad;
use crate::codex_apps::CodexAppsToolsCacheContext;
use crate::codex_apps::filter_disallowed_codex_apps_tools;
@@ -48,6 +49,7 @@ use codex_config::McpServerTransportConfig;
use codex_config::types::OAuthCredentialsStoreMode;
use codex_exec_server::HttpClient;
use codex_exec_server::ReqwestHttpClient;
use codex_protocol::mcp::McpClientCapabilities;
use codex_protocol::mcp::McpServerInfo;
use codex_protocol::protocol::Event;
use codex_rmcp_client::ExecutorStdioServerLauncher;
@@ -94,6 +96,7 @@ pub(crate) struct ManagedClient {
pub(crate) server_instructions: Option<String>,
pub(crate) server_supports_sandbox_state_meta_capability: bool,
pub(crate) codex_apps_tools_cache_context: Option<CodexAppsToolsCacheContext>,
pub(crate) client_capabilities: Option<McpClientCapabilities>,
}
impl ManagedClient {
@@ -145,6 +148,7 @@ impl AsyncManagedClient {
tx_event: Sender<Event>,
elicitation_requests: ElicitationRequestManager,
codex_apps_tools_cache_context: Option<CodexAppsToolsCacheContext>,
client_capabilities: Option<McpClientCapabilities>,
tool_plugin_provenance: Arc<ToolPluginProvenance>,
runtime_context: McpRuntimeContext,
runtime_auth_provider: Option<SharedAuthProvider>,
@@ -200,6 +204,7 @@ impl AsyncManagedClient {
tx_event,
elicitation_requests,
codex_apps_tools_cache_context,
client_capabilities,
client_elicitation_capability,
},
)
@@ -344,9 +349,11 @@ pub(crate) async fn list_tools_for_client_uncached(
client: &Arc<RmcpClient>,
timeout: Option<Duration>,
server_instructions: Option<&str>,
client_capabilities: Option<&McpClientCapabilities>,
) -> Result<Vec<ToolInfo>> {
let params = client_capabilities::paginated_params(None, client_capabilities);
let resp = client
.list_tools_with_connector_ids(/*params*/ None, timeout)
.list_tools_with_connector_ids(params, timeout)
.await?;
let tools = resp
.tools
@@ -479,10 +486,14 @@ async fn start_server_task(
tx_event,
elicitation_requests,
codex_apps_tools_cache_context,
client_capabilities,
client_elicitation_capability,
} = params;
let mut capabilities = ClientCapabilities::default();
capabilities.elicitation = Some(client_elicitation_capability);
capabilities.extensions = client_capabilities
.as_ref()
.map(client_capabilities::as_extensions);
let params = InitializeRequestParams::new(
capabilities,
Implementation::new("codex-mcp-client", env!("CARGO_PKG_VERSION")).with_title("Codex"),
@@ -509,6 +520,7 @@ async fn start_server_task(
&client,
startup_timeout,
initialize_result.instructions.as_deref(),
client_capabilities.as_ref(),
)
.await
.map_err(StartupOutcomeError::from)?;
@@ -542,6 +554,7 @@ async fn start_server_task(
server_instructions: initialize_result.instructions,
server_supports_sandbox_state_meta_capability,
codex_apps_tools_cache_context,
client_capabilities,
};
Ok(managed)
@@ -570,6 +583,7 @@ struct StartServerTaskParams {
tx_event: Sender<Event>,
elicitation_requests: ElicitationRequestManager,
codex_apps_tools_cache_context: Option<CodexAppsToolsCacheContext>,
client_capabilities: Option<McpClientCapabilities>,
client_elicitation_capability: ElicitationCapability,
}
+3
View File
@@ -14,6 +14,7 @@ use codex_protocol::config_types::WindowsSandboxLevel;
use codex_protocol::error::CodexErr;
use codex_protocol::error::Result as CodexResult;
use codex_protocol::mcp::CallToolResult;
use codex_protocol::mcp::McpClientCapabilities;
use codex_protocol::models::ActivePermissionProfile;
use codex_protocol::models::ContentItem;
use codex_protocol::models::PermissionProfile;
@@ -315,12 +316,14 @@ impl CodexThread {
app_server_client_name: Option<String>,
app_server_client_version: Option<String>,
mcp_elicitations_auto_deny: bool,
mcp_client_capabilities: Option<McpClientCapabilities>,
) -> ConstraintResult<()> {
self.codex
.set_app_server_client_info(
app_server_client_name,
app_server_client_version,
mcp_elicitations_auto_deny,
mcp_client_capabilities,
)
.await
}
+39 -7
View File
@@ -14,6 +14,7 @@ use codex_connectors::ConnectorDirectoryCacheContext;
use codex_connectors::ConnectorDirectoryCacheKey;
use codex_exec_server::EnvironmentManager;
use codex_exec_server::ExecServerRuntimePaths;
use codex_protocol::mcp::McpClientCapabilities;
use codex_protocol::models::PermissionProfile;
use codex_tools::DiscoverableTool;
use rmcp::model::ToolAnnotations;
@@ -67,6 +68,7 @@ struct AccessibleConnectorsCacheKey {
account_id: Option<String>,
chatgpt_user_id: Option<String>,
is_workspace_account: bool,
mcp_client_capabilities: Option<McpClientCapabilities>,
}
#[derive(Clone)]
@@ -90,7 +92,7 @@ pub async fn list_accessible_connectors_from_mcp_tools(
) -> anyhow::Result<Vec<AppInfo>> {
Ok(
list_accessible_connectors_from_mcp_tools_with_options_and_status(
config, /*force_refetch*/ false,
config, /*force_refetch*/ false, None,
)
.await?
.connectors,
@@ -147,6 +149,7 @@ pub(crate) async fn list_tool_suggest_discoverable_tools_with_auth(
pub async fn list_cached_accessible_connectors_from_mcp_tools(
config: &Config,
mcp_client_capabilities: Option<&McpClientCapabilities>,
) -> Option<Vec<AppInfo>> {
let auth_manager =
AuthManager::shared_from_config(config, /*enable_codex_api_key_env*/ false).await;
@@ -157,7 +160,8 @@ pub async fn list_cached_accessible_connectors_from_mcp_tools(
{
return Some(Vec::new());
}
let cache_key = accessible_connectors_cache_key(config, auth.as_ref());
let cache_key =
accessible_connectors_cache_key(config, auth.as_ref(), mcp_client_capabilities.cloned());
read_cached_accessible_connectors(&cache_key).map(|connectors| {
codex_connectors::filter::filter_disallowed_connectors(
connectors,
@@ -170,12 +174,13 @@ pub(crate) fn refresh_accessible_connectors_cache_from_mcp_tools(
config: &Config,
auth: Option<&CodexAuth>,
mcp_tools: &[ToolInfo],
mcp_client_capabilities: Option<&McpClientCapabilities>,
) {
if !config.features.enabled(Feature::Apps) {
return;
}
let cache_key = accessible_connectors_cache_key(config, auth);
let cache_key = accessible_connectors_cache_key(config, auth, mcp_client_capabilities.cloned());
let accessible_connectors = codex_connectors::filter::filter_disallowed_connectors(
accessible_connectors_from_mcp_tools(mcp_tools),
originator().value.as_str(),
@@ -188,15 +193,36 @@ pub async fn list_accessible_connectors_from_mcp_tools_with_options(
force_refetch: bool,
) -> anyhow::Result<Vec<AppInfo>> {
Ok(
list_accessible_connectors_from_mcp_tools_with_options_and_status(config, force_refetch)
.await?
.connectors,
list_accessible_connectors_from_mcp_tools_with_options_and_status(
config,
force_refetch,
None,
)
.await?
.connectors,
)
}
pub async fn list_accessible_connectors_from_mcp_tools_with_client_capabilities(
config: &Config,
force_refetch: bool,
mcp_client_capabilities: Option<McpClientCapabilities>,
) -> anyhow::Result<Vec<AppInfo>> {
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,
mcp_client_capabilities: Option<McpClientCapabilities>,
) -> anyhow::Result<AccessibleConnectorsStatus> {
// TODO: Wire callers that already own an EnvironmentManager into
// list_accessible_connectors_from_mcp_tools_with_environment_manager instead
@@ -212,6 +238,7 @@ pub async fn list_accessible_connectors_from_mcp_tools_with_options_and_status(
config,
force_refetch,
Arc::new(environment_manager),
mcp_client_capabilities,
)
.await
}
@@ -220,6 +247,7 @@ pub async fn list_accessible_connectors_from_mcp_tools_with_environment_manager(
config: &Config,
force_refetch: bool,
environment_manager: Arc<EnvironmentManager>,
mcp_client_capabilities: Option<McpClientCapabilities>,
) -> anyhow::Result<AccessibleConnectorsStatus> {
let auth_manager =
AuthManager::shared_from_config(config, /*enable_codex_api_key_env*/ false).await;
@@ -233,7 +261,8 @@ pub async fn list_accessible_connectors_from_mcp_tools_with_environment_manager(
codex_apps_ready: true,
});
}
let cache_key = accessible_connectors_cache_key(config, auth.as_ref());
let cache_key =
accessible_connectors_cache_key(config, auth.as_ref(), mcp_client_capabilities.clone());
let plugins_manager = Arc::new(PluginsManager::new(config.codex_home.to_path_buf()));
let mcp_manager = McpManager::new(Arc::clone(&plugins_manager));
let tool_plugin_provenance = mcp_manager.tool_plugin_provenance(config).await;
@@ -289,6 +318,7 @@ pub async fn list_accessible_connectors_from_mcp_tools_with_environment_manager(
ToolPluginProvenance::default(),
auth.as_ref(),
/*elicitation_reviewer*/ None,
mcp_client_capabilities,
)
.await;
@@ -366,6 +396,7 @@ pub async fn list_accessible_connectors_from_mcp_tools_with_environment_manager(
fn accessible_connectors_cache_key(
config: &Config,
auth: Option<&CodexAuth>,
mcp_client_capabilities: Option<McpClientCapabilities>,
) -> AccessibleConnectorsCacheKey {
let account_id = auth.and_then(CodexAuth::get_account_id);
let chatgpt_user_id = auth.and_then(CodexAuth::get_chatgpt_user_id);
@@ -375,6 +406,7 @@ fn accessible_connectors_cache_key(
account_id,
chatgpt_user_id,
is_workspace_account,
mcp_client_capabilities,
}
}
+51 -3
View File
@@ -21,6 +21,8 @@ use codex_features::Feature;
use codex_login::CodexAuth;
use codex_mcp::CODEX_APPS_MCP_SERVER_NAME;
use codex_mcp::ToolInfo;
use codex_protocol::mcp::McpAppUiCapability;
use codex_protocol::mcp::McpClientCapabilities;
use codex_utils_absolute_path::AbsolutePathBuf;
use pretty_assertions::assert_eq;
use rmcp::model::JsonObject;
@@ -167,7 +169,43 @@ async fn refresh_accessible_connectors_cache_from_mcp_tools_writes_latest_instal
.await
.expect("config should load");
let _ = config.features.set_enabled(Feature::Apps, /*enabled*/ true);
let cache_key = accessible_connectors_cache_key(&config, /*auth*/ None);
let explicit_empty = McpClientCapabilities::default();
let web_view = McpClientCapabilities {
app_ui: [McpAppUiCapability::WebView].into_iter().collect(),
};
let declarative_ui = McpClientCapabilities {
app_ui: [McpAppUiCapability::DeclarativeUi].into_iter().collect(),
};
let combined = McpClientCapabilities {
app_ui: [
McpAppUiCapability::WebView,
McpAppUiCapability::DeclarativeUi,
]
.into_iter()
.collect(),
};
let legacy_cache_key = accessible_connectors_cache_key(&config, /*auth*/ None, None);
let explicit_empty_cache_key =
accessible_connectors_cache_key(&config, /*auth*/ None, Some(explicit_empty.clone()));
let web_view_cache_key =
accessible_connectors_cache_key(&config, /*auth*/ None, Some(web_view));
let declarative_ui_cache_key =
accessible_connectors_cache_key(&config, /*auth*/ None, Some(declarative_ui));
let combined_cache_key =
accessible_connectors_cache_key(&config, /*auth*/ None, Some(combined));
let cache_keys = [
&legacy_cache_key,
&explicit_empty_cache_key,
&web_view_cache_key,
&declarative_ui_cache_key,
&combined_cache_key,
];
for (index, cache_key) in cache_keys.iter().enumerate() {
for other_cache_key in &cache_keys[index + 1..] {
assert!(cache_key != other_cache_key);
}
}
let tools = vec![
codex_app_tool(
"calendar_list_events",
@@ -184,8 +222,18 @@ async fn refresh_accessible_connectors_cache_from_mcp_tools_writes_latest_instal
];
let cached = with_accessible_connectors_cache_cleared(|| {
refresh_accessible_connectors_cache_from_mcp_tools(&config, /*auth*/ None, &tools);
read_cached_accessible_connectors(&cache_key).expect("cache should be populated")
refresh_accessible_connectors_cache_from_mcp_tools(
&config,
/*auth*/ None,
&tools,
Some(&explicit_empty),
);
assert!(read_cached_accessible_connectors(&legacy_cache_key).is_none());
assert!(read_cached_accessible_connectors(&web_view_cache_key).is_none());
assert!(read_cached_accessible_connectors(&declarative_ui_cache_key).is_none());
assert!(read_cached_accessible_connectors(&combined_cache_key).is_none());
read_cached_accessible_connectors(&explicit_empty_cache_key)
.expect("explicit-empty capability cache should be populated")
});
assert_eq!(
+9 -5
View File
@@ -686,6 +686,7 @@ async fn refresh_codex_apps_after_connector_auth(sess: &Session, turn_context: &
&turn_context.config,
auth.as_ref(),
&mcp_tools,
turn_context.mcp_client_capabilities.as_ref(),
);
}
Err(err) => {
@@ -1435,15 +1436,18 @@ pub(crate) async fn lookup_mcp_tool_metadata(
let connector_description = if server == CODEX_APPS_MCP_SERVER_NAME {
let connectors = match connectors::list_cached_accessible_connectors_from_mcp_tools(
turn_context.config.as_ref(),
turn_context.mcp_client_capabilities.as_ref(),
)
.await
{
Some(connectors) => Some(connectors),
None => {
connectors::list_accessible_connectors_from_mcp_tools(turn_context.config.as_ref())
.await
.ok()
}
None => connectors::list_accessible_connectors_from_mcp_tools_with_client_capabilities(
turn_context.config.as_ref(),
false,
turn_context.mcp_client_capabilities.clone(),
)
.await
.ok(),
};
connectors.and_then(|connectors| {
let connector_id = tool_info.connector_id.as_deref()?;
+1
View File
@@ -1276,6 +1276,7 @@ async fn install_host_owned_codex_apps_manager(session: &Session, turn_context:
codex_mcp::ToolPluginProvenance::default(),
auth.as_ref(),
/*elicitation_reviewer*/ None,
turn_context.mcp_client_capabilities.clone(),
)
.await;
*session.services.mcp_connection_manager.write().await = manager;
+49
View File
@@ -358,6 +358,7 @@ impl Session {
tool_plugin_provenance,
auth.as_ref(),
elicitation_reviewer,
turn_context.mcp_client_capabilities.clone(),
)
.await;
{
@@ -379,6 +380,54 @@ impl Session {
old_manager.shutdown().await;
}
pub(crate) async fn refresh_codex_apps_client(
&self,
client_capabilities: Option<McpClientCapabilities>,
submit_id: String,
) {
let auth = self.services.auth_manager.auth().await;
let config = self.get_config().await;
let mcp_config = config
.to_mcp_config(self.services.plugins_manager.as_ref())
.await;
let mut effective = with_codex_apps_mcp(HashMap::new(), auth.as_ref(), &mcp_config);
let auth_statuses = compute_auth_statuses(
effective.iter(),
config.mcp_oauth_credentials_store_mode,
auth.as_ref(),
)
.await;
let server = effective.remove(codex_mcp::CODEX_APPS_MCP_SERVER_NAME);
let auth_entry = auth_statuses
.get(codex_mcp::CODEX_APPS_MCP_SERVER_NAME)
.cloned();
let runtime_auth_provider = auth
.as_ref()
.filter(|auth| auth.uses_codex_backend())
.map(codex_model_provider::auth_provider_from_auth);
self.services
.mcp_connection_manager
.write()
.await
.refresh_codex_apps_client(
server,
config.mcp_oauth_credentials_store_mode,
auth_entry,
submit_id,
self.get_tx_event(),
config.codex_home.to_path_buf(),
codex_apps_tools_cache_key(auth.as_ref()),
client_capabilities,
McpRuntimeContext::new(
Arc::clone(&self.services.environment_manager),
config.cwd.to_path_buf(),
),
runtime_auth_provider,
mcp_config.client_elicitation_capability,
)
.await;
}
pub(crate) async fn refresh_mcp_servers_if_requested(
&self,
turn_context: &TurnContext,
+29 -2
View File
@@ -89,6 +89,7 @@ use codex_protocol::dynamic_tools::DynamicToolSpec;
use codex_protocol::items::TurnItem;
use codex_protocol::items::UserMessageItem;
use codex_protocol::mcp::CallToolResult;
use codex_protocol::mcp::McpClientCapabilities;
use codex_protocol::models::ActivePermissionProfile;
use codex_protocol::models::AdditionalPermissionProfile;
use codex_protocol::models::BaseInstructions;
@@ -319,6 +320,7 @@ use codex_git_utils::get_git_repo_root;
use codex_mcp::compute_auth_statuses;
use codex_mcp::effective_mcp_servers_from_configured;
use codex_mcp::host_owned_codex_apps_enabled;
use codex_mcp::with_codex_apps_mcp;
use codex_otel::SessionTelemetry;
use codex_otel::THREAD_STARTED_METRIC;
use codex_otel::TelemetryAuthMode;
@@ -614,6 +616,8 @@ impl Codex {
metrics_service_name,
app_server_client_name: None,
app_server_client_version: None,
mcp_client_capabilities: matches!(&session_source, SessionSource::Cli)
.then(McpClientCapabilities::default),
session_source,
forked_from_thread_id,
parent_thread_id,
@@ -782,16 +786,39 @@ impl Codex {
app_server_client_name: Option<String>,
app_server_client_version: Option<String>,
mcp_elicitations_auto_deny: bool,
mcp_client_capabilities: Option<McpClientCapabilities>,
) -> ConstraintResult<()> {
let _profile_update_guard = self
.session
.app_server_client_profile_update_lock
.lock()
.await;
let capabilities_changed = {
let manager = self.session.services.mcp_connection_manager.read().await;
manager.codex_apps_client_capabilities() != mcp_client_capabilities.as_ref()
};
self.session
.update_settings(SessionSettingsUpdate {
app_server_client_name,
app_server_client_version,
mcp_client_capabilities: Some(mcp_client_capabilities.clone()),
..Default::default()
})
.await?;
let mcp_connection_manager = self.session.services.mcp_connection_manager.read().await;
mcp_connection_manager.set_elicitations_auto_deny(mcp_elicitations_auto_deny);
self.session
.services
.mcp_connection_manager
.read()
.await
.set_elicitations_auto_deny(mcp_elicitations_auto_deny);
if capabilities_changed {
self.session
.refresh_codex_apps_client(
mcp_client_capabilities,
"app-server-client-profile".to_string(),
)
.await;
}
Ok(())
}
+1
View File
@@ -130,6 +130,7 @@ pub(super) async fn spawn_review_thread(
current_date: parent_turn_context.current_date.clone(),
timezone: parent_turn_context.timezone.clone(),
app_server_client_name: parent_turn_context.app_server_client_name.clone(),
mcp_client_capabilities: parent_turn_context.mcp_client_capabilities.clone(),
developer_instructions: None,
user_instructions: None,
compact_prompt: parent_turn_context.compact_prompt.clone(),
+10
View File
@@ -29,6 +29,9 @@ pub(crate) struct Session {
/// Serializes rebuild/apply cycles for the running proxy; each cycle
/// rebuilds from the current SessionState while holding this lock.
pub(super) managed_network_proxy_refresh_lock: Semaphore,
/// Keeps the active app-server client profile and the host-owned Apps MCP
/// client in sync when different clients resume the same thread.
pub(super) app_server_client_profile_update_lock: Mutex<()>,
/// The set of enabled features should be invariant for the lifetime of the
/// session.
pub(super) features: ManagedFeatures,
@@ -93,6 +96,7 @@ pub(crate) struct SessionConfiguration {
pub(super) metrics_service_name: Option<String>,
pub(super) app_server_client_name: Option<String>,
pub(super) app_server_client_version: Option<String>,
pub(super) mcp_client_capabilities: Option<McpClientCapabilities>,
/// Source of the session (cli, vscode, exec, mcp, ...)
pub(super) session_source: SessionSource,
/// Immediate history source copied into this thread, when this thread was forked.
@@ -367,6 +371,9 @@ impl SessionConfiguration {
if let Some(app_server_client_version) = updates.app_server_client_version.clone() {
next_configuration.app_server_client_version = Some(app_server_client_version);
}
if let Some(mcp_client_capabilities) = updates.mcp_client_capabilities.clone() {
next_configuration.mcp_client_capabilities = mcp_client_capabilities;
}
Ok(next_configuration)
}
@@ -425,6 +432,7 @@ pub(crate) struct SessionSettingsUpdate {
pub(crate) personality: Option<Personality>,
pub(crate) app_server_client_name: Option<String>,
pub(crate) app_server_client_version: Option<String>,
pub(crate) mcp_client_capabilities: Option<Option<McpClientCapabilities>>,
}
pub(crate) struct AppServerClientMetadata {
@@ -1060,6 +1068,7 @@ impl Session {
out_of_band_elicitation_paused,
state: Mutex::new(state),
managed_network_proxy_refresh_lock: Semaphore::new(/*permits*/ 1),
app_server_client_profile_update_lock: Mutex::new(()),
features: config.features.clone(),
multi_agent_version,
pending_mcp_server_refresh_config: Mutex::new(None),
@@ -1176,6 +1185,7 @@ impl Session {
tool_plugin_provenance,
auth,
Some(sess.mcp_elicitation_reviewer()),
session_configuration.mcp_client_capabilities.clone(),
)
.instrument(info_span!(
"session_init.mcp_manager_init",
+11
View File
@@ -3167,6 +3167,7 @@ async fn set_rate_limits_retains_previous_credits() {
metrics_service_name: None,
app_server_client_name: None,
app_server_client_version: None,
mcp_client_capabilities: None,
session_source: SessionSource::Exec,
forked_from_thread_id: None,
parent_thread_id: None,
@@ -3274,6 +3275,7 @@ async fn set_rate_limits_updates_plan_type_when_present() {
metrics_service_name: None,
app_server_client_name: None,
app_server_client_version: None,
mcp_client_capabilities: None,
session_source: SessionSource::Exec,
forked_from_thread_id: None,
parent_thread_id: None,
@@ -3805,6 +3807,7 @@ pub(crate) async fn make_session_configuration_for_tests() -> SessionConfigurati
metrics_service_name: None,
app_server_client_name: None,
app_server_client_version: None,
mcp_client_capabilities: None,
session_source: SessionSource::Exec,
forked_from_thread_id: None,
parent_thread_id: None,
@@ -4657,6 +4660,7 @@ async fn session_new_fails_when_zsh_fork_enabled_without_packaged_zsh() {
metrics_service_name: None,
app_server_client_name: None,
app_server_client_version: None,
mcp_client_capabilities: None,
session_source: SessionSource::Exec,
forked_from_thread_id: None,
parent_thread_id: None,
@@ -4764,6 +4768,7 @@ pub(crate) async fn make_session_and_context() -> (Session, TurnContext) {
metrics_service_name: None,
app_server_client_name: None,
app_server_client_version: None,
mcp_client_capabilities: None,
session_source: SessionSource::Exec,
forked_from_thread_id: None,
parent_thread_id: None,
@@ -4915,6 +4920,7 @@ pub(crate) async fn make_session_and_context() -> (Session, TurnContext) {
out_of_band_elicitation_paused: watch::channel(false).0,
state: Mutex::new(state),
managed_network_proxy_refresh_lock: Semaphore::new(/*permits*/ 1),
app_server_client_profile_update_lock: Mutex::new(()),
features: config.features.clone(),
multi_agent_version: OnceLock::from(config.multi_agent_version_from_features()),
pending_mcp_server_refresh_config: Mutex::new(None),
@@ -4996,6 +5002,7 @@ async fn make_session_with_config_and_rx(
metrics_service_name: None,
app_server_client_name: None,
app_server_client_version: None,
mcp_client_capabilities: None,
session_source: SessionSource::Exec,
forked_from_thread_id: None,
parent_thread_id: None,
@@ -5097,6 +5104,8 @@ async fn make_session_with_history_source_and_agent_control_and_rx(
metrics_service_name: None,
app_server_client_name: None,
app_server_client_version: None,
mcp_client_capabilities: matches!(&session_source, SessionSource::Cli)
.then(McpClientCapabilities::default),
session_source: session_source.clone(),
forked_from_thread_id: None,
parent_thread_id: None,
@@ -6827,6 +6836,7 @@ where
metrics_service_name: None,
app_server_client_name: None,
app_server_client_version: None,
mcp_client_capabilities: None,
session_source: SessionSource::Exec,
forked_from_thread_id: None,
parent_thread_id: None,
@@ -6978,6 +6988,7 @@ where
out_of_band_elicitation_paused: watch::channel(false).0,
state: Mutex::new(state),
managed_network_proxy_refresh_lock: Semaphore::new(/*permits*/ 1),
app_server_client_profile_update_lock: Mutex::new(()),
features: config.features.clone(),
multi_agent_version: OnceLock::from(config.multi_agent_version_from_features()),
pending_mcp_server_refresh_config: Mutex::new(None),
@@ -78,6 +78,7 @@ pub struct TurnContext {
pub(crate) current_date: Option<String>,
pub(crate) timezone: Option<String>,
pub(crate) app_server_client_name: Option<String>,
pub(crate) mcp_client_capabilities: Option<McpClientCapabilities>,
pub(crate) developer_instructions: Option<String>,
pub(crate) compact_prompt: Option<String>,
pub(crate) user_instructions: Option<String>,
@@ -246,6 +247,7 @@ impl TurnContext {
current_date: self.current_date.clone(),
timezone: self.timezone.clone(),
app_server_client_name: self.app_server_client_name.clone(),
mcp_client_capabilities: self.mcp_client_capabilities.clone(),
developer_instructions: self.developer_instructions.clone(),
compact_prompt: self.compact_prompt.clone(),
user_instructions: self.user_instructions.clone(),
@@ -545,6 +547,7 @@ impl Session {
current_date: Some(current_date),
timezone: Some(timezone),
app_server_client_name: session_configuration.app_server_client_name.clone(),
mcp_client_capabilities: session_configuration.mcp_client_capabilities.clone(),
developer_instructions: session_configuration.developer_instructions.clone(),
compact_prompt: session_configuration.compact_prompt.clone(),
user_instructions: session_configuration
@@ -339,6 +339,7 @@ async fn refresh_missing_requested_connectors(
&turn.config,
auth,
&mcp_tools,
turn.mcp_client_capabilities.as_ref(),
);
Some(accessible_connectors)
}
+1
View File
@@ -550,6 +550,7 @@ pub async fn run_main(cli: Cli, arg0_paths: Arg0DispatchPaths) -> anyhow::Result
client_version: env!("CARGO_PKG_VERSION").to_string(),
experimental_api: true,
opt_out_notification_methods: Vec::new(),
mcp_client_capabilities: None,
channel_capacity: DEFAULT_IN_PROCESS_CHANNEL_CAPACITY,
};
run_exec_session(ExecRunArgs {
+19
View File
@@ -6,8 +6,27 @@
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use std::collections::BTreeSet;
use ts_rs::TS;
/// Passive MCP App presentation features supported by the active host.
#[derive(
Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, JsonSchema, TS,
)]
#[serde(rename_all = "camelCase")]
#[ts(rename_all = "camelCase")]
pub enum McpAppUiCapability {
WebView,
DeclarativeUi,
}
/// Passive MCP capabilities supplied by the active Codex host.
#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
pub struct McpClientCapabilities {
pub app_ui: BTreeSet<McpAppUiCapability>,
}
/// ID of a request, which can be either a string or an integer.
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema, TS)]
#[serde(untagged)]
+2
View File
@@ -464,6 +464,7 @@ async fn connect_remote_app_server(
client_version: env!("CARGO_PKG_VERSION").to_string(),
experimental_api: true,
opt_out_notification_methods: Vec::new(),
mcp_client_capabilities: Some(Default::default()),
channel_capacity: DEFAULT_IN_PROCESS_CHANNEL_CAPACITY,
})
.await
@@ -627,6 +628,7 @@ where
client_version: env!("CARGO_PKG_VERSION").to_string(),
experimental_api: true,
opt_out_notification_methods: Vec::new(),
mcp_client_capabilities: Some(Default::default()),
channel_capacity: DEFAULT_IN_PROCESS_CHANNEL_CAPACITY,
})
.await
+1
View File
@@ -1054,6 +1054,7 @@ mod tests {
client_version: "test".to_string(),
experimental_api: true,
opt_out_notification_methods: Vec::new(),
mcp_client_capabilities: Some(Default::default()),
channel_capacity: DEFAULT_IN_PROCESS_CHANNEL_CAPACITY,
})
.await