Skip to content
Merged
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
6 changes: 4 additions & 2 deletions codex-rs/core/src/mcp_tool_call.rs
Original file line number Diff line number Diff line change
Expand Up @@ -661,7 +661,8 @@ async fn maybe_request_codex_apps_auth_elicitation(
};
let response = sess
.request_mcp_server_elicitation(turn_context, request_id, params)
.await;
.await
.response;
if !response
.as_ref()
.is_some_and(|response| response.action == ElicitationAction::Accept)
Expand Down Expand Up @@ -1325,7 +1326,8 @@ async fn maybe_request_mcp_tool_approval(
);
let decision = parse_mcp_tool_approval_elicitation_response(
sess.request_mcp_server_elicitation(turn_context.as_ref(), request_id, params)
.await,
.await
.response,
&question_id,
);
let decision = normalize_approval_decision_for_mode(decision, approval_mode);
Expand Down
79 changes: 71 additions & 8 deletions codex-rs/core/src/session/mcp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use codex_mcp::ElicitationReviewerHandle;
use codex_protocol::config_types::ApprovalsReviewer;
use codex_protocol::mcp_approval_meta::APPROVAL_KIND_KEY as MCP_ELICITATION_APPROVAL_KIND_KEY;
use codex_protocol::mcp_approval_meta::APPROVAL_KIND_MCP_TOOL_CALL as MCP_ELICITATION_APPROVAL_KIND_MCP_TOOL_CALL;
use codex_protocol::mcp_approval_meta::APPROVAL_KIND_TOOL_SUGGESTION as MCP_ELICITATION_APPROVAL_KIND_TOOL_SUGGESTION;
use codex_protocol::mcp_approval_meta::APPROVALS_REVIEWER_KEY as MCP_ELICITATION_APPROVALS_REVIEWER_KEY;
use codex_protocol::mcp_approval_meta::CONNECTOR_DESCRIPTION_KEY as MCP_ELICITATION_CONNECTOR_DESCRIPTION_KEY;
use codex_protocol::mcp_approval_meta::CONNECTOR_ID_KEY as MCP_ELICITATION_CONNECTOR_ID_KEY;
Expand All @@ -21,6 +22,10 @@ use rmcp::model::Meta;
use serde_json::Map;

const MCP_ELICITATION_DECLINE_MESSAGE_KEY: &str = "message";
const TOOL_SUGGESTION_ACTION_INSTALL: &str = "install";
const TOOL_SUGGESTION_ACTION_KEY: &str = "suggest_type";
const TOOL_SUGGESTION_TOOL_ID_KEY: &str = "tool_id";
const TOOL_SUGGESTION_TOOL_TYPE_KEY: &str = "tool_type";

#[derive(Debug, PartialEq)]
enum GuardianElicitationReview {
Expand All @@ -33,6 +38,18 @@ struct GuardianMcpElicitationReviewer {
session: std::sync::Weak<Session>,
}

pub(crate) struct McpServerElicitationOutcome {
pub(crate) response: Option<ElicitationResponse>,
pub(crate) sent: bool,
}

#[derive(Debug, PartialEq, Eq)]
struct PluginInstallElicitationTelemetryMetadata {
tool_type: String,
tool_id: String,
tool_name: String,
}

impl GuardianMcpElicitationReviewer {
fn new(session: &Arc<Session>) -> Self {
Self {
Expand Down Expand Up @@ -70,19 +87,22 @@ impl Session {
turn_context: &TurnContext,
request_id: RequestId,
params: McpServerElicitationRequestParams,
) -> Option<ElicitationResponse> {
) -> McpServerElicitationOutcome {
if self
.services
.mcp_connection_manager
.read()
.await
.elicitations_auto_deny()
{
return Some(ElicitationResponse {
action: codex_rmcp_client::ElicitationAction::Accept,
content: Some(serde_json::json!({})),
meta: None,
});
return McpServerElicitationOutcome {
response: Some(ElicitationResponse {
action: codex_rmcp_client::ElicitationAction::Accept,
content: Some(serde_json::json!({})),
meta: None,
}),
sent: false,
};
}

let server_name = params.server_name.clone();
Expand All @@ -98,7 +118,10 @@ impl Session {
warn!(
"failed to serialize MCP elicitation schema for server_name: {server_name}, request_id: {request_id}: {err:#}"
);
return None;
return McpServerElicitationOutcome {
response: None,
sent: false,
};
}
};
codex_protocol::approvals::ElicitationRequest::Form {
Expand Down Expand Up @@ -154,11 +177,24 @@ impl Session {
id,
request,
});
let plugin_install_telemetry = plugin_install_elicitation_telemetry_metadata(&event);
turn_context
.turn_metadata_state
.mark_user_input_requested_during_turn();
self.send_event(turn_context, event).await;
rx_response.await.ok()
if let Some(plugin_install_telemetry) = plugin_install_telemetry {
turn_context
.session_telemetry
.record_plugin_install_elicitation_sent(
plugin_install_telemetry.tool_type.as_str(),
plugin_install_telemetry.tool_id.as_str(),
plugin_install_telemetry.tool_name.as_str(),
);
}
McpServerElicitationOutcome {
response: rx_response.await.ok(),
sent: true,
}
}

#[expect(
Expand Down Expand Up @@ -550,6 +586,33 @@ fn metadata_owned_string(meta: &Map<String, Value>, key: &str) -> Option<String>
.map(ToOwned::to_owned)
}

fn plugin_install_elicitation_telemetry_metadata(
event: &EventMsg,
) -> Option<PluginInstallElicitationTelemetryMetadata> {
let EventMsg::ElicitationRequest(ElicitationRequestEvent { request, .. }) = event else {
return None;
};
let codex_protocol::approvals::ElicitationRequest::Form {
meta: Some(Value::Object(meta)),
..
} = request
else {
return None;
};
if metadata_str(meta, MCP_ELICITATION_APPROVAL_KIND_KEY)
!= Some(MCP_ELICITATION_APPROVAL_KIND_TOOL_SUGGESTION)
|| metadata_str(meta, TOOL_SUGGESTION_ACTION_KEY) != Some(TOOL_SUGGESTION_ACTION_INSTALL)
{
return None;
}

Some(PluginInstallElicitationTelemetryMetadata {
tool_type: metadata_owned_string(meta, TOOL_SUGGESTION_TOOL_TYPE_KEY)?,
tool_id: metadata_owned_string(meta, TOOL_SUGGESTION_TOOL_ID_KEY)?,
tool_name: metadata_owned_string(meta, MCP_ELICITATION_TOOL_NAME_KEY)?,
})
}

fn mcp_elicitation_request_id(id: &RequestId) -> String {
match id {
rmcp::model::NumberOrString::String(value) => value.to_string(),
Expand Down
57 changes: 57 additions & 0 deletions codex-rs/core/src/session/mcp_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,63 @@ fn guardian_elicitation_review_request_defaults_missing_tool_params() {
assert_eq!(arguments, Some(json!({})));
}

#[test]
fn plugin_install_elicitation_telemetry_metadata_requires_install_tool_suggestion() {
let event = EventMsg::ElicitationRequest(ElicitationRequestEvent {
turn_id: Some("turn-1".to_string()),
server_name: "codex_apps".to_string(),
id: codex_protocol::mcp::RequestId::String("request-1".to_string()),
request: codex_protocol::approvals::ElicitationRequest::Form {
meta: Some(json!({
"codex_approval_kind": "tool_suggestion",
"suggest_type": "install",
"tool_type": "plugin",
"tool_id": "slack@openai-curated",
"tool_name": "Slack",
})),
message: "Install Slack?".to_string(),
requested_schema: json!({
"type": "object",
"properties": {},
}),
},
});

assert_eq!(
plugin_install_elicitation_telemetry_metadata(&event),
Some(PluginInstallElicitationTelemetryMetadata {
tool_type: "plugin".to_string(),
tool_id: "slack@openai-curated".to_string(),
tool_name: "Slack".to_string(),
})
);

let enable_event = EventMsg::ElicitationRequest(ElicitationRequestEvent {
turn_id: Some("turn-1".to_string()),
server_name: "codex_apps".to_string(),
id: codex_protocol::mcp::RequestId::String("request-2".to_string()),
request: codex_protocol::approvals::ElicitationRequest::Form {
meta: Some(json!({
"codex_approval_kind": "tool_suggestion",
"suggest_type": "enable",
"tool_type": "plugin",
"tool_id": "slack@openai-curated",
"tool_name": "Slack",
})),
message: "Enable Slack?".to_string(),
requested_schema: json!({
"type": "object",
"properties": {},
}),
},
});

assert_eq!(
plugin_install_elicitation_telemetry_metadata(&enable_event),
None
);
}

#[test]
fn guardian_elicitation_review_request_requires_opt_in() {
let request = form_request(meta(json!({
Expand Down
3 changes: 2 additions & 1 deletion codex-rs/core/src/session/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -328,13 +328,14 @@ async fn request_mcp_server_elicitation_auto_accepts_when_auto_deny_is_enabled()
.await;

assert_eq!(
response,
response.response,
Some(ElicitationResponse {
action: ElicitationAction::Accept,
content: Some(json!({})),
meta: None,
})
);
assert!(!response.sent);
assert!(rx.try_recv().is_err());
}

Expand Down
Loading
Loading