Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 4 additions & 24 deletions codex-rs/analytics/src/analytics_client_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -793,12 +793,7 @@ fn sample_initialize_fact(connection_id: u64) -> AnalyticsFact {
title: None,
version: "1.0.0".to_string(),
},
capabilities: Some(InitializeCapabilities {
experimental_api: false,
request_attestation: false,
opt_out_notification_methods: None,
mcp_server_openai_form_elicitation: false,
}),
capabilities: Some(InitializeCapabilities::default()),
},
product_client_id: DEFAULT_ORIGINATOR.to_string(),
runtime: CodexRuntimeMetadata {
Expand Down Expand Up @@ -1674,12 +1669,7 @@ async fn initialize_caches_client_and_thread_lifecycle_publishes_once_initialize
title: None,
version: "1.0.0".to_string(),
},
capabilities: Some(InitializeCapabilities {
experimental_api: false,
request_attestation: false,
opt_out_notification_methods: None,
mcp_server_openai_form_elicitation: false,
}),
capabilities: Some(InitializeCapabilities::default()),
},
product_client_id: DEFAULT_ORIGINATOR.to_string(),
runtime: CodexRuntimeMetadata {
Expand Down Expand Up @@ -1978,12 +1968,7 @@ async fn compaction_event_ingests_custom_fact() {
title: None,
version: "1.0.0".to_string(),
},
capabilities: Some(InitializeCapabilities {
experimental_api: false,
request_attestation: false,
opt_out_notification_methods: None,
mcp_server_openai_form_elicitation: false,
}),
capabilities: Some(InitializeCapabilities::default()),
},
product_client_id: DEFAULT_ORIGINATOR.to_string(),
runtime: sample_runtime_metadata(),
Expand Down Expand Up @@ -2108,12 +2093,7 @@ async fn guardian_review_event_ingests_custom_fact_with_optional_target_item() {
title: None,
version: "1.0.0".to_string(),
},
capabilities: Some(InitializeCapabilities {
experimental_api: false,
request_attestation: false,
opt_out_notification_methods: None,
mcp_server_openai_form_elicitation: false,
}),
capabilities: Some(InitializeCapabilities::default()),
},
product_client_id: DEFAULT_ORIGINATOR.to_string(),
runtime: sample_runtime_metadata(),
Expand Down
1 change: 1 addition & 0 deletions codex-rs/app-server-client/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -374,6 +374,7 @@ impl InProcessClientStartArgs {
Some(self.opt_out_notification_methods.clone())
},
mcp_server_openai_form_elicitation: self.mcp_server_openai_form_elicitation,
extensions: None,
};

InitializeParams {
Expand Down
1 change: 1 addition & 0 deletions codex-rs/app-server-client/src/remote.rs
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ impl RemoteAppServerConnectArgs {
Some(self.opt_out_notification_methods.clone())
},
mcp_server_openai_form_elicitation: self.mcp_server_openai_form_elicitation,
extensions: None,
};

InitializeParams {
Expand Down
8 changes: 8 additions & 0 deletions codex-rs/app-server-protocol/schema/json/ClientRequest.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

26 changes: 24 additions & 2 deletions codex-rs/app-server-protocol/src/protocol/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2207,6 +2207,12 @@ mod tests {
"thread/started".to_string(),
"item/agentMessage/delta".to_string(),
]),
extensions: Some(std::collections::HashMap::from([(
"io.modelcontextprotocol/ui".to_string(),
json!({
"mimeTypes": ["text/html;profile=mcp-app"],
}),
)])),
}),
},
};
Expand All @@ -2228,7 +2234,12 @@ mod tests {
"optOutNotificationMethods": [
"thread/started",
"item/agentMessage/delta"
]
],
"extensions": {
"io.modelcontextprotocol/ui": {
"mimeTypes": ["text/html;profile=mcp-app"]
}
}
}
}
}),
Expand All @@ -2255,7 +2266,12 @@ mod tests {
"optOutNotificationMethods": [
"thread/started",
"item/agentMessage/delta"
]
],
"extensions": {
"io.modelcontextprotocol/ui": {
"mimeTypes": ["text/html;profile=mcp-app"]
}
}
}
}
}))?;
Expand All @@ -2278,6 +2294,12 @@ mod tests {
"thread/started".to_string(),
"item/agentMessage/delta".to_string(),
]),
extensions: Some(std::collections::HashMap::from([(
"io.modelcontextprotocol/ui".to_string(),
json!({
"mimeTypes": ["text/html;profile=mcp-app"],
}),
)])),
}),
},
}
Expand Down
7 changes: 7 additions & 0 deletions codex-rs/app-server-protocol/src/protocol/v1.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,13 @@ pub struct InitializeCapabilities {
/// connection (for example `thread/started`).
#[ts(optional = nullable)]
pub opt_out_notification_methods: Option<Vec<String>>,
/// MCP extension settings declared by the app-server client.
///
/// Codex currently consumes `io.modelcontextprotocol/ui` so widget-capable
/// hosts can advertise the same UI support to downstream MCP servers.
#[serde(skip_serializing_if = "Option::is_none")]
#[ts(optional = nullable)]
pub extensions: Option<HashMap<String, serde_json::Value>>,
}

#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
Expand Down
1 change: 1 addition & 0 deletions codex-rs/app-server-test-client/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1671,6 +1671,7 @@ impl CodexClient {
.collect(),
),
mcp_server_openai_form_elicitation: false,
extensions: None,
}),
},
};
Expand Down
18 changes: 18 additions & 0 deletions codex-rs/app-server/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,24 @@ App-server then advertises the downstream `openai/form` MCP extension for
threads started, resumed, or forked by that connection. Clients that cannot
handle the request envelope omit the field or set it to `false`.

Clients that can render MCP App WebViews advertise the MCP UI extension:

```json
{
"capabilities": {
"extensions": {
"io.modelcontextprotocol/ui": {
"mimeTypes": ["text/html;profile=mcp-app"]
}
}
}
}
```

App-server forwards this capability to the host-owned `codex_apps` MCP
server during initialization and on each `tools/call`. Clients without an
MCP App WebView omit the extension.

Applications building on top of `codex app-server` should identify themselves via the `clientInfo` parameter.

**Important**: `clientInfo.name` is used to identify the client for the OpenAI Compliance Logs Platform. If
Expand Down
23 changes: 20 additions & 3 deletions codex-rs/app-server/src/message_processor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,7 @@ pub(crate) struct InitializedConnectionSessionState {
pub(crate) client_version: String,
pub(crate) request_attestation: bool,
pub(crate) supports_openai_form_elicitation: bool,
pub(crate) supports_mcp_app_ui_webview: bool,
}

impl Default for ConnectionSessionState {
Expand Down Expand Up @@ -281,6 +282,12 @@ impl ConnectionSessionState {
.get()
.is_some_and(|session| session.supports_openai_form_elicitation)
}

pub(crate) fn supports_mcp_app_ui_webview(&self) -> bool {
self.initialized
.get()
.is_some_and(|session| session.supports_mcp_app_ui_webview)
}
pub(crate) fn initialize(&self, session: InitializedConnectionSessionState) -> Result<(), ()> {
self.initialized.set(session).map_err(|_| ())
}
Expand Down Expand Up @@ -897,6 +904,7 @@ impl MessageProcessor {
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 supports_openai_form_elicitation = session.supports_openai_form_elicitation();
let supports_mcp_app_ui_webview = session.supports_mcp_app_ui_webview();
let error_request_id = connection_request_id.clone();
let rpc_gate = Arc::clone(&session.rpc_gate);
let processor = Arc::clone(self);
Expand All @@ -913,6 +921,7 @@ impl MessageProcessor {
app_server_client_name,
client_version,
supports_openai_form_elicitation,
supports_mcp_app_ui_webview,
)
.await;
if let Err(error) = result {
Expand All @@ -935,6 +944,7 @@ impl MessageProcessor {
Ok(())
}

#[allow(clippy::too_many_arguments)]
async fn handle_initialized_client_request(
self: Arc<Self>,
connection_request_id: ConnectionRequestId,
Expand All @@ -943,6 +953,7 @@ impl MessageProcessor {
app_server_client_name: Option<String>,
client_version: Option<String>,
supports_openai_form_elicitation: bool,
supports_mcp_app_ui_webview: bool,
) -> Result<(), JSONRPCErrorError> {
let connection_id = connection_request_id.connection_id;
let request_id = ConnectionRequestId {
Expand Down Expand Up @@ -1099,6 +1110,7 @@ impl MessageProcessor {
app_server_client_name.clone(),
client_version.clone(),
supports_openai_form_elicitation,
supports_mcp_app_ui_webview,
request_context,
)
.await
Expand All @@ -1117,6 +1129,7 @@ impl MessageProcessor {
client_version.clone(),
/*supports_openai_form_elicitation*/
supports_openai_form_elicitation,
supports_mcp_app_ui_webview,
)
.await
}
Expand All @@ -1129,6 +1142,7 @@ impl MessageProcessor {
client_version.clone(),
/*supports_openai_form_elicitation*/
supports_openai_form_elicitation,
supports_mcp_app_ui_webview,
)
.await
}
Expand Down Expand Up @@ -1330,6 +1344,7 @@ impl MessageProcessor {
client_version.clone(),
/*supports_openai_form_elicitation*/
supports_openai_form_elicitation,
supports_mcp_app_ui_webview,
)
.await
}
Expand All @@ -1346,7 +1361,7 @@ impl MessageProcessor {
}
ClientRequest::ThreadRealtimeStart { params, .. } => {
self.turn_processor
.thread_realtime_start(&request_id, params)
.thread_realtime_start(&request_id, params, supports_mcp_app_ui_webview)
.await
}
ClientRequest::ThreadRealtimeAppendAudio { params, .. } => {
Expand All @@ -1373,7 +1388,9 @@ 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, supports_mcp_app_ui_webview)
.await
}
ClientRequest::McpServerOauthLogin { params, .. } => {
self.mcp_processor.mcp_server_oauth_login(params).await
Expand All @@ -1393,7 +1410,7 @@ impl MessageProcessor {
}
ClientRequest::McpServerToolCall { params, .. } => {
self.mcp_processor
.mcp_server_tool_call(&request_id, params)
.mcp_server_tool_call(&request_id, params, supports_mcp_app_ui_webview)
.await
}
ClientRequest::WindowsSandboxSetupStart { params, .. } => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@ impl InitializeRequestProcessor {
let experimental_api_enabled = capabilities.experimental_api;
let request_attestation = capabilities.request_attestation;
let supports_openai_form_elicitation = capabilities.mcp_server_openai_form_elicitation;
let supports_mcp_app_ui_webview =
codex_mcp::supports_mcp_app_ui_webview(capabilities.extensions.as_ref());
let opt_out_notification_methods = capabilities
.opt_out_notification_methods
.unwrap_or_default();
Expand Down Expand Up @@ -98,6 +100,7 @@ impl InitializeRequestProcessor {
client_version: version,
request_attestation,
supports_openai_form_elicitation,
supports_mcp_app_ui_webview,
})
.is_err()
{
Expand Down
Loading
Loading