diff --git a/codex-rs/app-server-protocol/schema/json/McpServerElicitationRequestParams.json b/codex-rs/app-server-protocol/schema/json/McpServerElicitationRequestParams.json index 3fc6971330..7fbc7e988a 100644 --- a/codex-rs/app-server-protocol/schema/json/McpServerElicitationRequestParams.json +++ b/codex-rs/app-server-protocol/schema/json/McpServerElicitationRequestParams.json @@ -578,6 +578,27 @@ ], "type": "object" }, + { + "properties": { + "_meta": true, + "message": { + "type": "string" + }, + "mode": { + "enum": [ + "openaiForm" + ], + "type": "string" + }, + "requestedSchema": true + }, + "required": [ + "message", + "mode", + "requestedSchema" + ], + "type": "object" + }, { "properties": { "_meta": true, diff --git a/codex-rs/app-server-protocol/schema/json/ServerRequest.json b/codex-rs/app-server-protocol/schema/json/ServerRequest.json index e701e96481..242ee4c3b0 100644 --- a/codex-rs/app-server-protocol/schema/json/ServerRequest.json +++ b/codex-rs/app-server-protocol/schema/json/ServerRequest.json @@ -1436,6 +1436,27 @@ ], "type": "object" }, + { + "properties": { + "_meta": true, + "message": { + "type": "string" + }, + "mode": { + "enum": [ + "openaiForm" + ], + "type": "string" + }, + "requestedSchema": true + }, + "required": [ + "message", + "mode", + "requestedSchema" + ], + "type": "object" + }, { "properties": { "_meta": true, diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json index cc28083823..f1c4329b43 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json @@ -3999,6 +3999,27 @@ ], "type": "object" }, + { + "properties": { + "_meta": true, + "message": { + "type": "string" + }, + "mode": { + "enum": [ + "openaiForm" + ], + "type": "string" + }, + "requestedSchema": true + }, + "required": [ + "message", + "mode", + "requestedSchema" + ], + "type": "object" + }, { "properties": { "_meta": true, diff --git a/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-experimental.json.zst b/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-experimental.json.zst index 702e92d386..1415cb301a 100644 Binary files a/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-experimental.json.zst and b/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-experimental.json.zst differ diff --git a/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-stable.json.zst b/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-stable.json.zst index bec977fae9..dc68057798 100644 Binary files a/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-stable.json.zst and b/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-stable.json.zst differ diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/McpServerElicitationRequestParams.ts b/codex-rs/app-server-protocol/schema/typescript/v2/McpServerElicitationRequestParams.ts index a4f1e732c6..3c86739977 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/McpServerElicitationRequestParams.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/McpServerElicitationRequestParams.ts @@ -13,4 +13,4 @@ export type McpServerElicitationRequestParams = { threadId: string, * context is app-server correlation rather than part of the protocol identity of the * elicitation itself. */ -turnId: string | null, serverName: string, } & ({ "mode": "form", _meta: JsonValue | null, message: string, requestedSchema: McpElicitationSchema, } | { "mode": "openai/form", _meta: JsonValue | null, message: string, requestedSchema: JsonValue, } | { "mode": "url", _meta: JsonValue | null, message: string, url: string, elicitationId: string, }); +turnId: string | null, serverName: string, } & ({ "mode": "form", _meta: JsonValue | null, message: string, requestedSchema: McpElicitationSchema, } | { "mode": "openai/form", _meta: JsonValue | null, message: string, requestedSchema: JsonValue, } | { "mode": "openaiForm", _meta: JsonValue | null, message: string, requestedSchema: JsonValue, } | { "mode": "url", _meta: JsonValue | null, message: string, url: string, elicitationId: string, }); diff --git a/codex-rs/app-server-protocol/src/protocol/v2/mcp.rs b/codex-rs/app-server-protocol/src/protocol/v2/mcp.rs index f89876e485..7ab179cfd2 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/mcp.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/mcp.rs @@ -755,6 +755,7 @@ pub enum McpServerElicitationRequest { message: String, requested_schema: McpElicitationSchema, }, + // TODO(victor): Deprecate once migrated to `openai/elicitation/create`. #[serde(rename = "openai/form", rename_all = "camelCase")] #[ts(rename = "openai/form", rename_all = "camelCase")] OpenAiForm { @@ -764,6 +765,15 @@ pub enum McpServerElicitationRequest { message: String, requested_schema: JsonValue, }, + #[serde(rename = "openaiForm", rename_all = "camelCase")] + #[ts(rename = "openaiForm", rename_all = "camelCase")] + OpenAiElicitationForm { + #[serde(rename = "_meta")] + #[ts(rename = "_meta")] + meta: Option, + message: String, + requested_schema: JsonValue, + }, #[serde(rename_all = "camelCase")] #[ts(rename_all = "camelCase")] Url { @@ -799,6 +809,15 @@ impl TryFrom for McpServerElicitationRequest { message, requested_schema, }), + CoreElicitationRequest::OpenAiElicitationForm { + meta, + message, + requested_schema, + } => Ok(Self::OpenAiElicitationForm { + meta, + message, + requested_schema, + }), CoreElicitationRequest::Url { meta, message, diff --git a/codex-rs/app-server/README.md b/codex-rs/app-server/README.md index 2a0013ba3a..0313f42607 100644 --- a/codex-rs/app-server/README.md +++ b/codex-rs/app-server/README.md @@ -98,6 +98,7 @@ legacy alias for declaring the `openai/form` extension. "capabilities": { "extensions": { "openai/form": {}, + "openai/elicitation": { "form": {} }, "io.modelcontextprotocol/ui": { "mimeTypes": ["text/html;profile=mcp-app"] } @@ -106,6 +107,15 @@ legacy alias for declaring the `openai/form` extension. } ``` +`openai/elicitation.form: {}` declares support for forms received as +`mode: "openaiForm"`. It does not follow from the legacy capability. +App-server retains only the `form` key under `openai/elicitation`, preserving +its value when present. Form requests require an object-valued `form` +declaration. A bare namespace does not imply form support. User verification +requests are not implemented. +Clients must only advertise features supported by both the client and the +connected app-server. + App-server keeps the complete value under `io.modelcontextprotocol/ui`, rather than deriving a WebView boolean, so clients can advertise additional supported MIME types and future extension settings. The MCP extension profile is fixed @@ -1931,16 +1941,25 @@ Order of messages: 1. `mcpServer/elicitation/request` (request) — includes `threadId`, nullable `turnId`, `serverName`, and either: - a form request: `{ "mode": "form", "message": "...", "requestedSchema": { ... } }` - - an OpenAI extended form request: `{ "mode": "openai/form", "message": "...", "requestedSchema": { ... } }` + - an OpenAI form request: `{ "mode": "openaiForm", "message": "...", "requestedSchema": { ... } }` + - a legacy OpenAI extended form request: `{ "mode": "openai/form", "message": "...", "requestedSchema": { ... } }` - a URL request: `{ "mode": "url", "message": "...", "url": "...", "elicitationId": "..." }` 2. Client response — `{ "action": "accept", "content": ... }`, `{ "action": "decline", "content": null }`, or `{ "action": "cancel", "content": null }`. 3. `serverRequest/resolved` — `{ threadId, requestId }` confirms the pending request has been resolved or cleared, including lifecycle cleanup on turn start/complete/interrupt. `turnId` is best-effort. When the elicitation is correlated with an active turn, the request includes that turn id; otherwise it is `null`. -For `openai/form`, app-server forwards `requestedSchema` as opaque JSON. The -client owns validation and rendering of supported field types and must return a -valid `decline` or `cancel` response when it cannot render a form. +MCP `openai/elicitation/create` requests must explicitly specify `mode: "form"`. +App-server forwards them as `mode: "openaiForm"`, preserving +`requestedSchema` as opaque JSON, including `x-openai-*` annotations. The legacy +`openai/form` route remains independent and also preserves its schema. + +The client owns validation and rendering. Graphical clients show an unsupported +state for unknown semantic inputs, never a partial form or generic approval, +and wait for the user to decline or cancel instead of returning a JSON-RPC +error. The TUI automatically declines OpenAI forms, including requests replayed +from another client. Capability advertisement describes the session's initial +client, not all clients that may later attach. For MCP tool approval elicitations, form request `meta` includes `codex_approval_kind: "mcp_tool_call"` and may include `persist: "session"`, diff --git a/codex-rs/app-server/tests/suite/v2/mcp_server_elicitation.rs b/codex-rs/app-server/tests/suite/v2/mcp_server_elicitation.rs index 42db81ff43..ddab6a8ecd 100644 --- a/codex-rs/app-server/tests/suite/v2/mcp_server_elicitation.rs +++ b/codex-rs/app-server/tests/suite/v2/mcp_server_elicitation.rs @@ -112,6 +112,7 @@ enum ElicitationScenario { StandardForm, LegacySep1034Defaults, OpenAiForm, + OpenAiElicitationForm, Strict(StrictReviewScenario), } @@ -272,6 +273,39 @@ async fn mcp_server_openai_form_elicitation_round_trip() -> Result<()> { fixture.finish(request_id, "accepted monthly-review").await } +#[tokio::test(flavor = "multi_thread", worker_threads = 4)] +async fn mcp_server_openai_elicitation_form_round_trip() -> Result<()> { + let mut fixture = + ElicitationRoundTripFixture::start(ElicitationScenario::OpenAiElicitationForm).await?; + let (request_id, params) = fixture.read_elicitation().await?; + assert_eq!( + params, + McpServerElicitationRequestParams { + thread_id: fixture.thread_id.clone(), + turn_id: Some(fixture.turn_id.clone()), + server_name: "codex_apps".to_string(), + request: McpServerElicitationRequest::OpenAiElicitationForm { + meta: Some(json!({ "example/request": "template-picker" })), + message: OPENAI_FORM_MESSAGE.to_string(), + requested_schema: openai_elicitation_form_schema(), + }, + } + ); + + fixture + .mcp + .send_response( + request_id.clone(), + serde_json::to_value(McpServerElicitationRequestResponse { + action: McpServerElicitationAction::Accept, + content: Some(json!({ "template": "monthly-review" })), + meta: Some(json!({ "example/response": "selected" })), + })?, + ) + .await?; + fixture.finish(request_id, "accepted monthly-review").await +} + #[test_case(Review::Approved; "approved")] #[test_case(Review::ApproveForMe; "approve_for_me")] #[test_case(Review::Never; "never")] @@ -666,6 +700,13 @@ impl ElicitationRoundTripFixture { .with_codex_home(codex_home.path()) .build() .await?; + let mut extensions = HashMap::from([( + OPENAI_STANDARD_FORM_INPUT_EXTENSION_ID.to_string(), + json!({}), + )]); + if matches!(scenario, ElicitationScenario::OpenAiElicitationForm) { + extensions.insert("openai/elicitation".to_string(), json!({ "form": {} })); + } timeout( DEFAULT_READ_TIMEOUT, mcp.initialize_with_capabilities( @@ -676,11 +717,11 @@ impl ElicitationRoundTripFixture { }, Some(InitializeCapabilities { experimental_api: true, - mcp_server_openai_form_elicitation: true, - extensions: Some(HashMap::from([( - OPENAI_STANDARD_FORM_INPUT_EXTENSION_ID.to_string(), - json!({}), - )])), + mcp_server_openai_form_elicitation: !matches!( + scenario, + ElicitationScenario::OpenAiElicitationForm + ), + extensions: Some(extensions), ..Default::default() }), ), @@ -979,6 +1020,18 @@ impl ServerHandler for ElicitationAppsMcpServer { Some(json!({})) ); } + let extensions = request.capabilities.extensions.as_ref(); + assert_eq!( + extensions + .and_then(|extensions| extensions.get("openai/elicitation")) + .cloned() + .map(Value::Object), + matches!(self.scenario, ElicitationScenario::OpenAiElicitationForm) + .then(|| json!({ "form": {} })), + ); + if matches!(self.scenario, ElicitationScenario::OpenAiElicitationForm) { + assert!(extensions.is_some_and(|extensions| !extensions.contains_key("openai/form"))); + } context.peer.set_peer_info(request); Ok(self.get_info()) } @@ -1203,6 +1256,34 @@ impl ServerHandler for ElicitationAppsMcpServer { .into(), ) } + ElicitationScenario::OpenAiElicitationForm => { + let result = context + .peer + .send_request(McpServerRequest::CustomRequest(CustomRequest::new( + "openai/elicitation/create", + Some(json!({ + "mode": "form", + "_meta": { "example/request": "template-picker" }, + "message": OPENAI_FORM_MESSAGE, + "requestedSchema": openai_elicitation_form_schema(), + })), + ))) + .await + .map_err(|err| rmcp::ErrorData::internal_error(err.to_string(), None))?; + assert_eq!( + serde_json::to_value(result) + .map_err(|err| rmcp::ErrorData::internal_error(err.to_string(), None))?, + json!({ + "action": "accept", + "content": { "template": "monthly-review" }, + "_meta": { "example/response": "selected" }, + }), + ); + Ok( + CallToolResult::success(vec![ContentBlock::text("accepted monthly-review")]) + .into(), + ) + } ElicitationScenario::OpenAiForm => { let result = context .peer @@ -1258,6 +1339,24 @@ impl ServerHandler for ElicitationAppsMcpServer { } } +fn openai_elicitation_form_schema() -> Value { + json!({ + "type": "object", + "properties": { + "template": { + "type": "string", + "title": "Template", + "oneOf": [{ + "const": "monthly-review", + "title": "Monthly review", + "x-openai-preview": { "src": IMAGE_DATA_URL }, + }], + }, + }, + "required": ["template"], + }) +} + fn sep1034_schema() -> Value { json!({ "type": "object", diff --git a/codex-rs/codex-mcp/src/client_capabilities.rs b/codex-rs/codex-mcp/src/client_capabilities.rs index c4b36abf52..0efc8f8185 100644 --- a/codex-rs/codex-mcp/src/client_capabilities.rs +++ b/codex-rs/codex-mcp/src/client_capabilities.rs @@ -2,6 +2,7 @@ use std::collections::HashMap; use codex_protocol::mcp::ClientMcpExtensions; use codex_protocol::mcp::MCP_APP_UI_EXTENSION_ID; +use codex_protocol::mcp::OPENAI_ELICITATION_EXTENSION_ID; use codex_protocol::mcp::OPENAI_FORM_EXTENSION_ID; use codex_protocol::mcp::OPENAI_STANDARD_FORM_INPUT_EXTENSION_ID; use serde_json::Map; @@ -23,12 +24,19 @@ pub fn client_mcp_extensions( matches!( id.as_str(), OPENAI_FORM_EXTENSION_ID + | OPENAI_ELICITATION_EXTENSION_ID | OPENAI_STANDARD_FORM_INPUT_EXTENSION_ID | MCP_APP_UI_EXTENSION_ID ) }) .map(|(id, value)| (id.clone(), value.clone())) .collect::>(); + if let Some(settings) = selected + .get_mut(OPENAI_ELICITATION_EXTENSION_ID) + .and_then(Value::as_object_mut) + { + settings.retain(|key, _| key == "form"); + } if legacy_openai_form_elicitation { selected .entry(OPENAI_FORM_EXTENSION_ID.to_string()) diff --git a/codex-rs/codex-mcp/src/client_capabilities_tests.rs b/codex-rs/codex-mcp/src/client_capabilities_tests.rs index 35d2ec3b46..726f76636a 100644 --- a/codex-rs/codex-mcp/src/client_capabilities_tests.rs +++ b/codex-rs/codex-mcp/src/client_capabilities_tests.rs @@ -14,7 +14,12 @@ fn selects_only_supported_mcp_extensions() { ], "futureField": {"preserved": true}, }); + let form = json!({"futureField": {"preserved": true}}); let extensions = HashMap::from([ + ( + OPENAI_ELICITATION_EXTENSION_ID.to_string(), + json!({"form": form, "userVerification": {}, "unsupported": {}}), + ), (MCP_APP_UI_EXTENSION_ID.to_string(), app_ui.clone()), (OPENAI_FORM_EXTENSION_ID.to_string(), json!({})), ( @@ -30,6 +35,10 @@ fn selects_only_supported_mcp_extensions() { /*legacy_openai_form_elicitation*/ false, ), ClientMcpExtensions::new(HashMap::from([ + ( + OPENAI_ELICITATION_EXTENSION_ID.to_string(), + json!({"form": form}), + ), (MCP_APP_UI_EXTENSION_ID.to_string(), app_ui), (OPENAI_FORM_EXTENSION_ID.to_string(), json!({})), ( diff --git a/codex-rs/codex-mcp/src/elicitation.rs b/codex-rs/codex-mcp/src/elicitation.rs index 7b934e5d63..b575eb01a5 100644 --- a/codex-rs/codex-mcp/src/elicitation.rs +++ b/codex-rs/codex-mcp/src/elicitation.rs @@ -320,19 +320,24 @@ impl ElicitationRequestManager { let should_surface_form_in_full_access = router.full_access_form_input_enabled() && permission_prompt_is_auto_approved - && matches!( - &elicitation, + && !elicitation + .meta() + .is_some_and(|meta| meta.contains_key(APPROVAL_KIND_KEY)) + && match &elicitation { Elicitation::Mcp( rmcp::model::ElicitRequestParams::FormElicitationParams { - meta, requested_schema, .. - } - ) if !requested_schema.properties.is_empty() - && !meta - .as_ref() - .is_some_and(|meta| meta.contains_key(APPROVAL_KIND_KEY)) - ); + }, + ) => !requested_schema.properties.is_empty(), + Elicitation::OpenAiElicitationForm { + requested_schema, .. + } => requested_schema + .get("properties") + .and_then(Value::as_object) + .is_some_and(|properties| !properties.is_empty()), + Elicitation::Mcp(_) | Elicitation::OpenAiForm { .. } => false, + }; if !should_surface_form_in_full_access { if elicitation_is_rejected_by_policy(approval_policy) { @@ -412,6 +417,15 @@ impl ElicitationRequestManager { message, requested_schema, }, + Elicitation::OpenAiElicitationForm { + meta, + message, + requested_schema, + } => ElicitationRequest::OpenAiElicitationForm { + meta, + message, + requested_schema, + }, }; let (tx, rx) = oneshot::channel(); let _active_elicitation = lifecycle.as_ref().map(ElicitationLifecycle::start); @@ -475,7 +489,9 @@ fn can_auto_accept_elicitation(elicitation: &Elicitation) -> bool { // Auto-accept confirm/approval elicitations without schema requirements. requested_schema.properties.is_empty() } - Elicitation::Mcp(_) | Elicitation::OpenAiForm { .. } => false, + Elicitation::Mcp(_) + | Elicitation::OpenAiForm { .. } + | Elicitation::OpenAiElicitationForm { .. } => false, } } diff --git a/codex-rs/core/src/session/mcp.rs b/codex-rs/core/src/session/mcp.rs index c0a1d0d64a..761183e8a0 100644 --- a/codex-rs/core/src/session/mcp.rs +++ b/codex-rs/core/src/session/mcp.rs @@ -942,7 +942,9 @@ fn guardian_elicitation_review_request( "guardian MCP elicitation review does not support this elicitation mode", ); } - Elicitation::OpenAiForm { .. } => return GuardianElicitationReview::NotRequested, + Elicitation::OpenAiForm { .. } | Elicitation::OpenAiElicitationForm { .. } => { + return GuardianElicitationReview::NotRequested; + } }; let Some(meta) = meta.as_ref().map(|meta| &meta.0.0) else { diff --git a/codex-rs/protocol/src/approvals.rs b/codex-rs/protocol/src/approvals.rs index e4dedf5347..c051430ec4 100644 --- a/codex-rs/protocol/src/approvals.rs +++ b/codex-rs/protocol/src/approvals.rs @@ -389,6 +389,15 @@ pub enum ElicitationRequest { message: String, requested_schema: JsonValue, }, + #[serde(rename = "openaiForm")] + #[ts(rename = "openaiForm")] + OpenAiElicitationForm { + #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")] + #[ts(optional, rename = "_meta")] + meta: Option, + message: String, + requested_schema: JsonValue, + }, Url { #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")] #[ts(optional, rename = "_meta")] diff --git a/codex-rs/protocol/src/mcp.rs b/codex-rs/protocol/src/mcp.rs index 4f2e3e21ac..317174a568 100644 --- a/codex-rs/protocol/src/mcp.rs +++ b/codex-rs/protocol/src/mcp.rs @@ -23,7 +23,9 @@ pub enum McpServerConnectionStatus { Disabled, } -/// Extension ID for OpenAI form elicitation. +/// Extension ID for OpenAI elicitation modes. +pub const OPENAI_ELICITATION_EXTENSION_ID: &str = "openai/elicitation"; +/// Extension ID for legacy OpenAI form elicitation. pub const OPENAI_FORM_EXTENSION_ID: &str = "openai/form"; /// Extension ID for standard MCP form elicitations that require user-entered input. pub const OPENAI_STANDARD_FORM_INPUT_EXTENSION_ID: &str = "openai/standard-form-input"; diff --git a/codex-rs/rmcp-client/src/elicitation_client_service.rs b/codex-rs/rmcp-client/src/elicitation_client_service.rs index 84cef31d04..12cad74bd3 100644 --- a/codex-rs/rmcp-client/src/elicitation_client_service.rs +++ b/codex-rs/rmcp-client/src/elicitation_client_service.rs @@ -1,5 +1,7 @@ use std::sync::Arc; +use codex_protocol::mcp::OPENAI_ELICITATION_EXTENSION_ID; + use rmcp::RoleClient; use rmcp::model::ClientInfo; use rmcp::model::ClientResult; @@ -30,6 +32,14 @@ use crate::rmcp_client::SendElicitation; const MCP_PROGRESS_TOKEN_META_KEY: &str = "progressToken"; const MCP_ELICITATION_CREATE_METHOD: &str = "elicitation/create"; const OPENAI_FORM_METHOD: &str = "openai/form"; +const OPENAI_ELICITATION_METHOD: &str = "openai/elicitation/create"; + +#[derive(Deserialize)] +#[serde(tag = "mode")] +enum OpenAiElicitationRequestParams { + #[serde(rename = "form")] + Form(OpenAiFormRequestParams), +} #[derive(Deserialize)] #[serde(rename_all = "camelCase")] @@ -44,6 +54,7 @@ struct OpenAiFormRequestParams { pub(crate) struct ElicitationClientService { handler: LoggingClientHandler, supports_openai_form: bool, + supports_openai_elicitation_form: bool, send_elicitation: Arc, pause_state: ElicitationPauseState, } @@ -59,6 +70,13 @@ impl ElicitationClientService { .extensions .as_ref() .is_some_and(|extensions| extensions.contains_key(OPENAI_FORM_METHOD)); + let supports_openai_elicitation_form = client_info + .capabilities + .extensions + .as_ref() + .and_then(|extensions| extensions.get(OPENAI_ELICITATION_EXTENSION_ID)) + .and_then(|settings| settings.get("form")) + .is_some_and(Value::is_object); let send_elicitation = Arc::new(send_elicitation); Self { handler: LoggingClientHandler::new( @@ -66,6 +84,7 @@ impl ElicitationClientService { clone_send_elicitation(Arc::clone(&send_elicitation)), ), supports_openai_form, + supports_openai_elicitation_form, send_elicitation, pause_state, } @@ -144,6 +163,33 @@ impl Service for ElicitationClientService { response, )?)) } + ServerRequest::CustomRequest(request) + if request.method == OPENAI_ELICITATION_METHOD + && self.supports_openai_elicitation_form => + { + let params = request + .params_as::() + .map_err(|err| { + rmcp::ErrorData::invalid_params(err.to_string(), /*data*/ None) + })? + .ok_or_else(|| { + rmcp::ErrorData::invalid_params("missing params", /*data*/ None) + })?; + let OpenAiElicitationRequestParams::Form(params) = params; + let response = self + .create_elicitation( + Elicitation::OpenAiElicitationForm { + meta: params.meta, + message: params.message, + requested_schema: params.requested_schema, + }, + context, + ) + .await?; + Ok(ClientResult::CustomResult(elicitation_response_result( + response, + )?)) + } request => { >::handle_request( &self.handler, @@ -209,7 +255,7 @@ fn restore_context_meta( .meta_mut() .get_or_insert_with(RequestMetaObject::new) .extend(context_meta), - Elicitation::OpenAiForm { meta, .. } => { + Elicitation::OpenAiForm { meta, .. } | Elicitation::OpenAiElicitationForm { meta, .. } => { let meta = meta .get_or_insert_with(|| Value::Object(Map::new())) .as_object_mut(); diff --git a/codex-rs/rmcp-client/src/rmcp_client.rs b/codex-rs/rmcp-client/src/rmcp_client.rs index 0dd10638df..b549a0621d 100644 --- a/codex-rs/rmcp-client/src/rmcp_client.rs +++ b/codex-rs/rmcp-client/src/rmcp_client.rs @@ -302,13 +302,20 @@ pub enum Elicitation { message: String, requested_schema: serde_json::Value, }, + OpenAiElicitationForm { + meta: Option, + message: String, + requested_schema: serde_json::Value, + }, } impl Elicitation { pub fn meta(&self) -> Option<&serde_json::Map> { match self { Self::Mcp(request) => request.meta().map(|meta| &meta.0.0), - Self::OpenAiForm { meta, .. } => meta.as_ref().and_then(serde_json::Value::as_object), + Self::OpenAiForm { meta, .. } | Self::OpenAiElicitationForm { meta, .. } => { + meta.as_ref().and_then(serde_json::Value::as_object) + } } } } diff --git a/codex-rs/tui/src/app/thread_routing.rs b/codex-rs/tui/src/app/thread_routing.rs index febc60b970..4124c62914 100644 --- a/codex-rs/tui/src/app/thread_routing.rs +++ b/codex-rs/tui/src/app/thread_routing.rs @@ -301,6 +301,7 @@ impl App { codex_app_server_protocol::McpServerElicitationRequest::OpenAiForm { .. } + | codex_app_server_protocol::McpServerElicitationRequest::OpenAiElicitationForm { .. } | codex_app_server_protocol::McpServerElicitationRequest::Url { .. } => { self.app_event_tx.resolve_elicitation( thread_id, @@ -1673,7 +1674,10 @@ impl App { match ¶ms.request { codex_app_server_protocol::McpServerElicitationRequest::Form { .. } => true, - codex_app_server_protocol::McpServerElicitationRequest::OpenAiForm { .. } => false, + codex_app_server_protocol::McpServerElicitationRequest::OpenAiForm { .. } + | codex_app_server_protocol::McpServerElicitationRequest::OpenAiElicitationForm { + .. + } => false, request @ codex_app_server_protocol::McpServerElicitationRequest::Url { .. } => { let thread_id = ThreadId::from_string(¶ms.thread_id) .unwrap_or_else(|_| self.chat_widget.thread_id().unwrap_or_default()); diff --git a/codex-rs/tui/src/chatwidget/tool_requests.rs b/codex-rs/tui/src/chatwidget/tool_requests.rs index cf01c394ff..4beb0660f3 100644 --- a/codex-rs/tui/src/chatwidget/tool_requests.rs +++ b/codex-rs/tui/src/chatwidget/tool_requests.rs @@ -377,6 +377,7 @@ impl ChatWidget { .push_approval_request(request, &self.config.features); } McpServerElicitationRequest::OpenAiForm { .. } + | McpServerElicitationRequest::OpenAiElicitationForm { .. } | McpServerElicitationRequest::Url { .. } => { self.app_event_tx.resolve_elicitation( thread_id,