mcp: keep elicitation requests below app wire types (#29724)

## Why

Core and tools need to request MCP elicitation without constructing
app-server wire payloads. The request should remain a neutral protocol
concept until app-server serializes it for a client.

## What changed

- Switched core and tools to
`codex_protocol::approvals::ElicitationRequest`.
- Derived turn and server context inside core instead of carrying
app-server request types through lower layers.
- Kept the app-server payload unchanged through an explicit boundary
conversion.
- Removed the remaining production app-server-protocol dependency from
tools.

## Stack

This is PR 5 of 6, stacked on [PR
#29723](https://github.com/openai/codex/pull/29723). Review only the
delta from `codex/split-connector-metadata-types`. Next: [PR
#29725](https://github.com/openai/codex/pull/29725).

## Validation

- `codex-core` MCP coverage passed: 87 tests.
- Tools elicitation and app-server round-trip coverage passed.
This commit is contained in:
Adam Perry @ OpenAI
2026-06-24 13:53:27 -07:00
committed by GitHub
Unverified
parent a33ad93996
commit df1ee09ec5
10 changed files with 173 additions and 288 deletions
-1
View File
@@ -4020,7 +4020,6 @@ dependencies = [
name = "codex-tools"
version = "0.0.0"
dependencies = [
"codex-app-server-protocol",
"codex-code-mode",
"codex-connectors",
"codex-features",
+37 -48
View File
@@ -1,4 +1,3 @@
use std::collections::BTreeMap;
use std::collections::HashMap;
use std::time::Duration;
use std::time::Instant;
@@ -26,10 +25,6 @@ use crate::turn_metadata::McpTurnMetadataContext;
use codex_analytics::AppInvocation;
use codex_analytics::InvocationType;
use codex_analytics::build_track_events_context;
use codex_app_server_protocol::McpElicitationObjectType;
use codex_app_server_protocol::McpElicitationSchema;
use codex_app_server_protocol::McpServerElicitationRequest;
use codex_app_server_protocol::McpServerElicitationRequestParams;
use codex_config::ConfigLayerSource;
use codex_config::types::AppToolApproval;
use codex_config::types::ApprovalsReviewer;
@@ -46,6 +41,7 @@ use codex_mcp::auth_elicitation_completed_result;
use codex_mcp::build_auth_elicitation_plan;
use codex_mcp::declared_openai_file_input_param_names;
use codex_mcp::mcp_permission_prompt_is_auto_approved;
use codex_protocol::approvals::ElicitationRequest;
use codex_protocol::items::McpToolCallError;
use codex_protocol::items::McpToolCallItem;
use codex_protocol::items::McpToolCallStatus;
@@ -653,19 +649,19 @@ async fn maybe_request_codex_apps_auth_elicitation(
};
let request_id = rmcp::model::RequestId::String(plan.elicitation.elicitation_id.clone().into());
let params = McpServerElicitationRequestParams {
thread_id: sess.thread_id.to_string(),
turn_id: Some(turn_context.sub_id.clone()),
server_name: CODEX_APPS_MCP_SERVER_NAME.to_string(),
request: McpServerElicitationRequest::Url {
meta: Some(plan.elicitation.meta),
message: plan.elicitation.message,
url: plan.elicitation.url,
elicitation_id: plan.elicitation.elicitation_id,
},
let request = ElicitationRequest::Url {
meta: Some(plan.elicitation.meta),
message: plan.elicitation.message,
url: plan.elicitation.url,
elicitation_id: plan.elicitation.elicitation_id,
};
let response = sess
.request_mcp_server_elicitation(turn_context, request_id, params)
.request_mcp_server_elicitation(
turn_context,
CODEX_APPS_MCP_SERVER_NAME.to_string(),
request_id,
request,
)
.await
.response;
if !response
@@ -1309,10 +1305,8 @@ async fn maybe_request_mcp_tool_approval(
let request_id = rmcp::model::RequestId::String(
format!("{MCP_TOOL_APPROVAL_QUESTION_ID_PREFIX}_{call_id}").into(),
);
let params = build_mcp_tool_approval_elicitation_request(
sess.as_ref(),
turn_context.as_ref(),
McpToolApprovalElicitationRequest {
let request =
build_mcp_tool_approval_elicitation_request(McpToolApprovalElicitationRequest {
server: &invocation.server,
metadata,
tool_params: rendered_template
@@ -1325,12 +1319,16 @@ async fn maybe_request_mcp_tool_approval(
.as_ref()
.map(|rendered_template| rendered_template.elicitation_message.as_str()),
prompt_options,
},
);
});
let decision = parse_mcp_tool_approval_elicitation_response(
sess.request_mcp_server_elicitation(turn_context.as_ref(), request_id, params)
.await
.response,
sess.request_mcp_server_elicitation(
turn_context.as_ref(),
invocation.server.clone(),
request_id,
request,
)
.await
.response,
&question_id,
);
let decision = normalize_approval_decision_for_mode(decision, approval_mode);
@@ -1658,35 +1656,26 @@ fn build_mcp_tool_approval_fallback_message(
}
fn build_mcp_tool_approval_elicitation_request(
sess: &Session,
turn_context: &TurnContext,
request: McpToolApprovalElicitationRequest<'_>,
) -> McpServerElicitationRequestParams {
) -> ElicitationRequest {
let message = request
.message_override
.map(ToString::to_string)
.unwrap_or_else(|| request.question.question.clone());
McpServerElicitationRequestParams {
thread_id: sess.thread_id.to_string(),
turn_id: Some(turn_context.sub_id.clone()),
server_name: request.server.to_string(),
request: McpServerElicitationRequest::Form {
meta: build_mcp_tool_approval_elicitation_meta(
request.server,
request.metadata,
request.tool_params,
request.tool_params_display,
request.prompt_options,
),
message,
requested_schema: McpElicitationSchema {
schema_uri: None,
type_: McpElicitationObjectType::Object,
properties: BTreeMap::new(),
required: None,
},
},
ElicitationRequest::Form {
meta: build_mcp_tool_approval_elicitation_meta(
request.server,
request.metadata,
request.tool_params,
request.tool_params_display,
request.prompt_options,
),
message,
requested_schema: serde_json::json!({
"type": "object",
"properties": {},
}),
}
}
+67 -79
View File
@@ -653,9 +653,8 @@ fn truncates_strings_on_char_boundaries() {
);
}
#[tokio::test]
async fn approval_elicitation_request_uses_message_override_and_preserves_tool_params_keys() {
let (session, turn_context) = make_session_and_context().await;
#[test]
fn approval_elicitation_request_uses_message_override_and_preserves_tool_params_keys() {
let question = build_mcp_tool_approval_question(
"q".to_string(),
CODEX_APPS_MCP_SERVER_NAME,
@@ -667,86 +666,75 @@ async fn approval_elicitation_request_uses_message_override_and_preserves_tool_p
Some("Allow Calendar to create an event?"),
);
let request = build_mcp_tool_approval_elicitation_request(
&session,
&turn_context,
McpToolApprovalElicitationRequest {
server: CODEX_APPS_MCP_SERVER_NAME,
metadata: Some(&approval_metadata(
Some("calendar"),
Some("Calendar"),
Some("Manage events and schedules."),
Some("Create Event"),
Some("Create a calendar event."),
)),
tool_params: Some(&serde_json::json!({
"calendar_id": "primary",
"title": "Roadmap review",
})),
tool_params_display: Some(&[
RenderedMcpToolApprovalParam {
name: "calendar_id".to_string(),
value: serde_json::json!("primary"),
display_name: "Calendar".to_string(),
},
RenderedMcpToolApprovalParam {
name: "title".to_string(),
value: serde_json::json!("Roadmap review"),
display_name: "Title".to_string(),
},
]),
question,
message_override: Some("Allow Calendar to create an event?"),
prompt_options: prompt_options(
/*allow_session_remember*/ true, /*allow_persistent_approval*/ true,
),
},
);
let request = build_mcp_tool_approval_elicitation_request(McpToolApprovalElicitationRequest {
server: CODEX_APPS_MCP_SERVER_NAME,
metadata: Some(&approval_metadata(
Some("calendar"),
Some("Calendar"),
Some("Manage events and schedules."),
Some("Create Event"),
Some("Create a calendar event."),
)),
tool_params: Some(&serde_json::json!({
"calendar_id": "primary",
"title": "Roadmap review",
})),
tool_params_display: Some(&[
RenderedMcpToolApprovalParam {
name: "calendar_id".to_string(),
value: serde_json::json!("primary"),
display_name: "Calendar".to_string(),
},
RenderedMcpToolApprovalParam {
name: "title".to_string(),
value: serde_json::json!("Roadmap review"),
display_name: "Title".to_string(),
},
]),
question,
message_override: Some("Allow Calendar to create an event?"),
prompt_options: prompt_options(
/*allow_session_remember*/ true, /*allow_persistent_approval*/ true,
),
});
assert_eq!(
request,
McpServerElicitationRequestParams {
thread_id: session.thread_id.to_string(),
turn_id: Some(turn_context.sub_id),
server_name: CODEX_APPS_MCP_SERVER_NAME.to_string(),
request: McpServerElicitationRequest::Form {
meta: Some(serde_json::json!({
MCP_TOOL_APPROVAL_KIND_KEY: MCP_TOOL_APPROVAL_KIND_MCP_TOOL_CALL,
MCP_TOOL_APPROVAL_PERSIST_KEY: [
MCP_TOOL_APPROVAL_PERSIST_SESSION,
MCP_TOOL_APPROVAL_PERSIST_ALWAYS,
],
MCP_TOOL_APPROVAL_SOURCE_KEY: MCP_TOOL_APPROVAL_SOURCE_CONNECTOR,
MCP_TOOL_APPROVAL_CONNECTOR_ID_KEY: "calendar",
MCP_TOOL_APPROVAL_CONNECTOR_NAME_KEY: "Calendar",
MCP_TOOL_APPROVAL_CONNECTOR_DESCRIPTION_KEY: "Manage events and schedules.",
MCP_TOOL_APPROVAL_TOOL_TITLE_KEY: "Create Event",
MCP_TOOL_APPROVAL_TOOL_DESCRIPTION_KEY: "Create a calendar event.",
MCP_TOOL_APPROVAL_TOOL_PARAMS_KEY: {
"calendar_id": "primary",
"title": "Roadmap review",
},
MCP_TOOL_APPROVAL_TOOL_PARAMS_DISPLAY_KEY: [
{
"name": "calendar_id",
"value": "primary",
"display_name": "Calendar",
},
{
"name": "title",
"value": "Roadmap review",
"display_name": "Title",
},
],
})),
message: "Allow Calendar to create an event?".to_string(),
requested_schema: McpElicitationSchema {
schema_uri: None,
type_: McpElicitationObjectType::Object,
properties: BTreeMap::new(),
required: None,
ElicitationRequest::Form {
meta: Some(serde_json::json!({
MCP_TOOL_APPROVAL_KIND_KEY: MCP_TOOL_APPROVAL_KIND_MCP_TOOL_CALL,
MCP_TOOL_APPROVAL_PERSIST_KEY: [
MCP_TOOL_APPROVAL_PERSIST_SESSION,
MCP_TOOL_APPROVAL_PERSIST_ALWAYS,
],
MCP_TOOL_APPROVAL_SOURCE_KEY: MCP_TOOL_APPROVAL_SOURCE_CONNECTOR,
MCP_TOOL_APPROVAL_CONNECTOR_ID_KEY: "calendar",
MCP_TOOL_APPROVAL_CONNECTOR_NAME_KEY: "Calendar",
MCP_TOOL_APPROVAL_CONNECTOR_DESCRIPTION_KEY: "Manage events and schedules.",
MCP_TOOL_APPROVAL_TOOL_TITLE_KEY: "Create Event",
MCP_TOOL_APPROVAL_TOOL_DESCRIPTION_KEY: "Create a calendar event.",
MCP_TOOL_APPROVAL_TOOL_PARAMS_KEY: {
"calendar_id": "primary",
"title": "Roadmap review",
},
},
MCP_TOOL_APPROVAL_TOOL_PARAMS_DISPLAY_KEY: [
{
"name": "calendar_id",
"value": "primary",
"display_name": "Calendar",
},
{
"name": "title",
"value": "Roadmap review",
"display_name": "Title",
},
],
})),
message: "Allow Calendar to create an event?".to_string(),
requested_schema: serde_json::json!({
"type": "object",
"properties": {},
}),
}
);
}
+3 -49
View File
@@ -99,8 +99,9 @@ impl Session {
pub async fn request_mcp_server_elicitation(
&self,
turn_context: &TurnContext,
server_name: String,
request_id: RequestId,
params: McpServerElicitationRequestParams,
request: ElicitationRequest,
) -> McpServerElicitationOutcome {
if self
.services
@@ -118,53 +119,6 @@ impl Session {
};
}
let server_name = params.server_name.clone();
let request = match params.request {
McpServerElicitationRequest::Form {
meta,
message,
requested_schema,
} => {
let requested_schema = match serde_json::to_value(requested_schema) {
Ok(requested_schema) => requested_schema,
Err(err) => {
warn!(
"failed to serialize MCP elicitation schema for server_name: {server_name}, request_id: {request_id}: {err:#}"
);
return McpServerElicitationOutcome {
response: None,
sent: false,
};
}
};
codex_protocol::approvals::ElicitationRequest::Form {
meta,
message,
requested_schema,
}
}
McpServerElicitationRequest::OpenAiForm {
meta,
message,
requested_schema,
} => codex_protocol::approvals::ElicitationRequest::OpenAiForm {
meta,
message,
requested_schema,
},
McpServerElicitationRequest::Url {
meta,
message,
url,
elicitation_id,
} => codex_protocol::approvals::ElicitationRequest::Url {
meta,
message,
url,
elicitation_id,
},
};
let (tx_response, rx_response) = oneshot::channel();
let prev_entry = {
let mut active = self.active_turn.lock().await;
@@ -194,7 +148,7 @@ impl Session {
}
};
let event = EventMsg::ElicitationRequest(ElicitationRequestEvent {
turn_id: params.turn_id,
turn_id: Some(turn_context.sub_id.clone()),
server_name,
id,
request,
+1 -2
View File
@@ -54,8 +54,6 @@ use chrono::Utc;
use codex_analytics::AnalyticsEventsClient;
use codex_analytics::SubAgentThreadStartedInput;
use codex_analytics::TurnCodexErrorFact;
use codex_app_server_protocol::McpServerElicitationRequest;
use codex_app_server_protocol::McpServerElicitationRequestParams;
use codex_config::types::AuthKeyringBackendKind;
use codex_config::types::OAuthCredentialsStoreMode;
use codex_exec_server::Environment;
@@ -87,6 +85,7 @@ use codex_otel::current_span_w3c_trace_context;
use codex_otel::set_parent_from_w3c_trace_context;
use codex_protocol::SessionId;
use codex_protocol::ThreadId;
use codex_protocol::approvals::ElicitationRequest;
use codex_protocol::approvals::ElicitationRequestEvent;
use codex_protocol::approvals::ExecPolicyAmendment;
use codex_protocol::approvals::NetworkPolicyAmendment;
+8 -15
View File
@@ -84,7 +84,6 @@ use crate::tools::handlers::ShellCommandHandler;
use crate::tools::registry::ToolExecutor;
use crate::tools::router::ToolCallSource;
use crate::turn_diff_tracker::TurnDiffTracker;
use codex_app_server_protocol::McpElicitationSchema;
use codex_config::config_toml::ConfigToml;
use codex_config::config_toml::ProjectConfig;
use codex_config::permissions_toml::FilesystemPermissionToml;
@@ -377,24 +376,18 @@ async fn request_mcp_server_elicitation_auto_accepts_when_auto_deny_is_enabled()
.load_full()
.set_elicitations_auto_deny(/*auto_deny*/ true);
let requested_schema: McpElicitationSchema = serde_json::from_value(json!({
"type": "object",
"properties": {},
}))
.expect("schema should deserialize");
let response = session
.request_mcp_server_elicitation(
turn_context.as_ref(),
"codex_apps".to_string(),
RequestId::String("request-1".into()),
McpServerElicitationRequestParams {
thread_id: session.thread_id.to_string(),
turn_id: Some(turn_context.sub_id.clone()),
server_name: "codex_apps".to_string(),
request: McpServerElicitationRequest::Form {
meta: None,
message: "Allow this request?".to_string(),
requested_schema,
},
ElicitationRequest::Form {
meta: None,
message: "Allow this request?".to_string(),
requested_schema: json!({
"type": "object",
"properties": {},
}),
},
)
.await;
@@ -173,15 +173,14 @@ impl RequestPluginInstallHandler {
let tool_type = tool.tool_type();
let request_id = RequestId::String(format!("request_plugin_install_{call_id}").into());
let params = build_request_plugin_install_elicitation_request(
CODEX_APPS_MCP_SERVER_NAME,
session.thread_id.to_string(),
turn.sub_id.clone(),
suggest_reason,
&tool,
);
let request = build_request_plugin_install_elicitation_request(suggest_reason, &tool);
let elicitation = session
.request_mcp_server_elicitation(turn.as_ref(), request_id, params)
.request_mcp_server_elicitation(
turn.as_ref(),
CODEX_APPS_MCP_SERVER_NAME.to_string(),
request_id,
request,
)
.await;
let response = elicitation.response;
if let Some(response) = response.as_ref() {
-1
View File
@@ -8,7 +8,6 @@ version.workspace = true
workspace = true
[dependencies]
codex-app-server-protocol = { workspace = true }
codex-code-mode = { workspace = true }
codex-connectors = { workspace = true }
codex-features = { workspace = true }
+12 -27
View File
@@ -1,10 +1,5 @@
use std::collections::BTreeMap;
use codex_app_server_protocol::McpElicitationObjectType;
use codex_app_server_protocol::McpElicitationSchema;
use codex_app_server_protocol::McpServerElicitationRequest;
use codex_app_server_protocol::McpServerElicitationRequestParams;
use codex_connectors::AppInfo;
use codex_protocol::approvals::ElicitationRequest;
use serde::Deserialize;
use serde::Serialize;
use serde_json::json;
@@ -54,31 +49,21 @@ pub struct RequestPluginInstallMeta<'a> {
}
pub fn build_request_plugin_install_elicitation_request(
server_name: &str,
thread_id: String,
turn_id: String,
suggest_reason: &str,
tool: &DiscoverableTool,
) -> McpServerElicitationRequestParams {
) -> ElicitationRequest {
let message = suggest_reason.to_string();
McpServerElicitationRequestParams {
thread_id,
turn_id: Some(turn_id),
server_name: server_name.to_string(),
request: McpServerElicitationRequest::Form {
meta: Some(json!(build_request_plugin_install_meta(
suggest_reason,
tool,
))),
message,
requested_schema: McpElicitationSchema {
schema_uri: None,
type_: McpElicitationObjectType::Object,
properties: BTreeMap::new(),
required: None,
},
},
ElicitationRequest::Form {
meta: Some(json!(build_request_plugin_install_meta(
suggest_reason,
tool,
))),
message,
requested_schema: json!({
"type": "object",
"properties": {},
}),
}
}
@@ -27,42 +27,32 @@ fn build_request_plugin_install_elicitation_request_uses_expected_shape() {
}));
let request = build_request_plugin_install_elicitation_request(
"codex-apps",
"thread-1".to_string(),
"turn-1".to_string(),
"Plan and reference events from your calendar",
&connector,
);
assert_eq!(
request,
McpServerElicitationRequestParams {
thread_id: "thread-1".to_string(),
turn_id: Some("turn-1".to_string()),
server_name: "codex-apps".to_string(),
request: McpServerElicitationRequest::Form {
meta: Some(json!(RequestPluginInstallMeta {
codex_approval_kind: REQUEST_PLUGIN_INSTALL_APPROVAL_KIND_VALUE,
persist: REQUEST_PLUGIN_INSTALL_PERSIST_ALWAYS_VALUE,
tool_type: DiscoverableToolType::Connector,
suggest_type: DiscoverableToolAction::Install,
suggest_reason: "Plan and reference events from your calendar",
tool_id: "connector_2128aebfecb84f64a069897515042a44",
tool_name: "Google Calendar",
install_url: Some(
"https://chatgpt.com/apps/google-calendar/connector_2128aebfecb84f64a069897515042a44"
),
remote_plugin_id: None,
app_connector_ids: None,
})),
message: "Plan and reference events from your calendar".to_string(),
requested_schema: McpElicitationSchema {
schema_uri: None,
type_: McpElicitationObjectType::Object,
properties: BTreeMap::new(),
required: None,
},
},
ElicitationRequest::Form {
meta: Some(json!(RequestPluginInstallMeta {
codex_approval_kind: REQUEST_PLUGIN_INSTALL_APPROVAL_KIND_VALUE,
persist: REQUEST_PLUGIN_INSTALL_PERSIST_ALWAYS_VALUE,
tool_type: DiscoverableToolType::Connector,
suggest_type: DiscoverableToolAction::Install,
suggest_reason: "Plan and reference events from your calendar",
tool_id: "connector_2128aebfecb84f64a069897515042a44",
tool_name: "Google Calendar",
install_url: Some(
"https://chatgpt.com/apps/google-calendar/connector_2128aebfecb84f64a069897515042a44"
),
remote_plugin_id: None,
app_connector_ids: None,
})),
message: "Plan and reference events from your calendar".to_string(),
requested_schema: json!({
"type": "object",
"properties": {},
}),
},
);
}
@@ -80,40 +70,30 @@ fn build_request_plugin_install_elicitation_request_injects_plugin_metadata() {
}));
let request = build_request_plugin_install_elicitation_request(
"codex-apps",
"thread-1".to_string(),
"turn-1".to_string(),
"Use the sample plugin's skills and MCP server",
&plugin,
);
assert_eq!(
request,
McpServerElicitationRequestParams {
thread_id: "thread-1".to_string(),
turn_id: Some("turn-1".to_string()),
server_name: "codex-apps".to_string(),
request: McpServerElicitationRequest::Form {
meta: Some(json!(RequestPluginInstallMeta {
codex_approval_kind: REQUEST_PLUGIN_INSTALL_APPROVAL_KIND_VALUE,
persist: REQUEST_PLUGIN_INSTALL_PERSIST_ALWAYS_VALUE,
tool_type: DiscoverableToolType::Plugin,
suggest_type: DiscoverableToolAction::Install,
suggest_reason: "Use the sample plugin's skills and MCP server",
tool_id: "sample@openai-curated-remote",
tool_name: "Sample Plugin",
install_url: None,
remote_plugin_id: Some("plugins~Plugin_sample"),
app_connector_ids: Some(&["connector_calendar".to_string()]),
})),
message: "Use the sample plugin's skills and MCP server".to_string(),
requested_schema: McpElicitationSchema {
schema_uri: None,
type_: McpElicitationObjectType::Object,
properties: BTreeMap::new(),
required: None,
},
},
ElicitationRequest::Form {
meta: Some(json!(RequestPluginInstallMeta {
codex_approval_kind: REQUEST_PLUGIN_INSTALL_APPROVAL_KIND_VALUE,
persist: REQUEST_PLUGIN_INSTALL_PERSIST_ALWAYS_VALUE,
tool_type: DiscoverableToolType::Plugin,
suggest_type: DiscoverableToolAction::Install,
suggest_reason: "Use the sample plugin's skills and MCP server",
tool_id: "sample@openai-curated-remote",
tool_name: "Sample Plugin",
install_url: None,
remote_plugin_id: Some("plugins~Plugin_sample"),
app_connector_ids: Some(&["connector_calendar".to_string()]),
})),
message: "Use the sample plugin's skills and MCP server".to_string(),
requested_schema: json!({
"type": "object",
"properties": {},
}),
},
);
}