mirror of
https://github.com/openai/codex.git
synced 2026-11-12 21:20:34 +00:00
feat(mcp): propagate client UI capabilities
This commit is contained in:
@@ -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(),
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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": {
|
||||
|
||||
+35
@@ -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"
|
||||
|
||||
+35
@@ -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>, };
|
||||
@@ -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";
|
||||
|
||||
@@ -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,
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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(¶ms.thread_id)
|
||||
&& self
|
||||
@@ -2425,6 +2439,7 @@ impl ThreadRequestProcessor {
|
||||
¶ms,
|
||||
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(¶ms.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(¶ms.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,
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -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": []},
|
||||
},
|
||||
},
|
||||
}))
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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!(
|
||||
|
||||
@@ -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()?;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user