From d1e71cd20235b0040666c3e6a8f5dce2903972c3 Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Mon, 2 Feb 2026 08:41:02 -0800 Subject: [PATCH 01/28] feat: add MCP protocol types and rmcp adapters (#10356) Currently, types from our custom `mcp-types` crate are part of some of our APIs: https://github.com/openai/codex/blob/03fcd12e77fedf4fa327af27e2e476e1ebc5f651/codex-rs/app-server-protocol/src/protocol/v2.rs#L43-L46 To eliminate this crate in #10349 by switching to `rmcp`, we need our own wrappers for the `rmcp` types that we can use in our API, which is what this PR does. Note this PR introduces the new API types, but we do not make use of them until #10349. --- [//]: # (BEGIN SAPLING FOOTER) Stack created with [Sapling](https://sapling-scm.com). Best reviewed with [ReviewStack](https://reviewstack.dev/openai/codex/pull/10356). * #10357 * #10349 * __->__ #10356 --- codex-rs/protocol/src/lib.rs | 1 + codex-rs/protocol/src/mcp.rs | 324 +++++++++++++++++++++++++++++++++++ 2 files changed, 325 insertions(+) create mode 100644 codex-rs/protocol/src/mcp.rs diff --git a/codex-rs/protocol/src/lib.rs b/codex-rs/protocol/src/lib.rs index 60b01bbd73..5841b1187e 100644 --- a/codex-rs/protocol/src/lib.rs +++ b/codex-rs/protocol/src/lib.rs @@ -6,6 +6,7 @@ pub mod config_types; pub mod custom_prompts; pub mod dynamic_tools; pub mod items; +pub mod mcp; pub mod message_history; pub mod models; pub mod num_format; diff --git a/codex-rs/protocol/src/mcp.rs b/codex-rs/protocol/src/mcp.rs new file mode 100644 index 0000000000..88a1f989c1 --- /dev/null +++ b/codex-rs/protocol/src/mcp.rs @@ -0,0 +1,324 @@ +use schemars::JsonSchema; +use serde::Deserialize; +use serde::Serialize; +use ts_rs::TS; + +/// Types used when representing Model Context Protocol (MCP) values inside the +/// Codex protocol. +/// +/// We intentionally keep these types TS/JSON-schema friendly (via `ts-rs` and +/// `schemars`) so they can be embedded in Codex's own protocol structures. + +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema, TS)] +#[serde(untagged)] +pub enum RequestId { + String(String), + #[ts(type = "number")] + Integer(i64), +} + +impl std::fmt::Display for RequestId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + RequestId::String(s) => f.write_str(s), + RequestId::Integer(i) => i.fmt(f), + } + } +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +pub struct Tool { + pub name: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] + pub title: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] + pub description: Option, + pub input_schema: serde_json::Value, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] + pub output_schema: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] + pub annotations: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] + pub icons: Option>, + #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] + pub meta: Option, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +pub struct Resource { + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] + pub annotations: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] + pub description: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] + pub mime_type: Option, + pub name: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] + #[ts(type = "number")] + pub size: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] + pub title: Option, + pub uri: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] + pub icons: Option>, + #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] + pub meta: Option, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +pub struct ResourceTemplate { + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] + pub annotations: Option, + pub uri_template: String, + pub name: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] + pub title: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] + pub description: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] + pub mime_type: Option, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +pub struct CallToolResult { + pub content: Vec, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] + pub structured_content: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] + pub is_error: Option, + #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] + pub meta: Option, +} + +// === Adapter helpers === +// +// These types and conversions intentionally live in `codex-protocol` so other crates can convert +// “wire-shaped” MCP JSON (typically coming from rmcp model structs serialized with serde) into our +// TS/JsonSchema-friendly protocol types without depending on `mcp-types`. + +fn deserialize_lossy_opt_i64<'de, D>(deserializer: D) -> Result, D::Error> +where + D: serde::Deserializer<'de>, +{ + match Option::::deserialize(deserializer)? { + Some(number) => { + if let Some(v) = number.as_i64() { + Ok(Some(v)) + } else if let Some(v) = number.as_u64() { + Ok(i64::try_from(v).ok()) + } else { + Ok(None) + } + } + None => Ok(None), + } +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +struct ToolSerde { + name: String, + #[serde(default)] + title: Option, + #[serde(default)] + description: Option, + #[serde(default, rename = "inputSchema", alias = "input_schema")] + input_schema: serde_json::Value, + #[serde(default, rename = "outputSchema", alias = "output_schema")] + output_schema: Option, + #[serde(default)] + annotations: Option, + #[serde(default)] + icons: Option>, + #[serde(rename = "_meta", default)] + meta: Option, +} + +impl From for Tool { + fn from(value: ToolSerde) -> Self { + let ToolSerde { + name, + title, + description, + input_schema, + output_schema, + annotations, + icons, + meta, + } = value; + Self { + name, + title, + description, + input_schema, + output_schema, + annotations, + icons, + meta, + } + } +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +struct ResourceSerde { + #[serde(default)] + annotations: Option, + #[serde(default)] + description: Option, + #[serde(rename = "mimeType", alias = "mime_type", default)] + mime_type: Option, + name: String, + #[serde(default, deserialize_with = "deserialize_lossy_opt_i64")] + size: Option, + #[serde(default)] + title: Option, + uri: String, + #[serde(default)] + icons: Option>, + #[serde(rename = "_meta", default)] + meta: Option, +} + +impl From for Resource { + fn from(value: ResourceSerde) -> Self { + let ResourceSerde { + annotations, + description, + mime_type, + name, + size, + title, + uri, + icons, + meta, + } = value; + Self { + annotations, + description, + mime_type, + name, + size, + title, + uri, + icons, + meta, + } + } +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +struct ResourceTemplateSerde { + #[serde(default)] + annotations: Option, + #[serde(rename = "uriTemplate", alias = "uri_template")] + uri_template: String, + name: String, + #[serde(default)] + title: Option, + #[serde(default)] + description: Option, + #[serde(rename = "mimeType", alias = "mime_type", default)] + mime_type: Option, +} + +impl From for ResourceTemplate { + fn from(value: ResourceTemplateSerde) -> Self { + let ResourceTemplateSerde { + annotations, + uri_template, + name, + title, + description, + mime_type, + } = value; + Self { + annotations, + uri_template, + name, + title, + description, + mime_type, + } + } +} + +impl Tool { + pub fn from_mcp_value(value: serde_json::Value) -> Result { + Ok(serde_json::from_value::(value)?.into()) + } +} + +impl Resource { + pub fn from_mcp_value(value: serde_json::Value) -> Result { + Ok(serde_json::from_value::(value)?.into()) + } +} + +impl ResourceTemplate { + pub fn from_mcp_value(value: serde_json::Value) -> Result { + Ok(serde_json::from_value::(value)?.into()) + } +} + +#[cfg(test)] +mod tests { + use pretty_assertions::assert_eq; + + use super::*; + + #[test] + fn resource_size_deserializes_without_narrowing() { + let resource = serde_json::json!({ + "name": "big", + "uri": "file:///tmp/big", + "size": 5_000_000_000u64, + }); + + let parsed = Resource::from_mcp_value(resource).expect("should deserialize"); + assert_eq!(parsed.size, Some(5_000_000_000)); + + let resource = serde_json::json!({ + "name": "negative", + "uri": "file:///tmp/negative", + "size": -1, + }); + + let parsed = Resource::from_mcp_value(resource).expect("should deserialize"); + assert_eq!(parsed.size, Some(-1)); + + let resource = serde_json::json!({ + "name": "too_big_for_i64", + "uri": "file:///tmp/too_big_for_i64", + "size": 18446744073709551615u64, + }); + + let parsed = Resource::from_mcp_value(resource).expect("should deserialize"); + assert_eq!(parsed.size, None); + } +} From 3392c5af243821c85c3b816cbb0623a795a91385 Mon Sep 17 00:00:00 2001 From: Charley Cunningham Date: Mon, 2 Feb 2026 09:53:29 -0800 Subject: [PATCH 02/28] Nicer highlighting of slash commands, /plan accepts prompt args and pasted images (#10269) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary - Make typed slash commands become text elements when the user hits space, including paste‑burst spaces. - Enable `/plan` to accept inline args and submit them in plan mode, mirroring `/review` behavior and blocking submission while a task is running. - Preserve text elements/attachments for slash commands that take args. image ## Changes - Add safe helper to insert element ranges in the textarea. - Extend command‑with‑args pipeline to carry text elements and reuse submission prep. - Update `/plan` dispatch to switch to plan mode then submit prompt + elements. - Document new composer behavior and add tests. ## Notes - `/plan` is blocked during active tasks (same as `/review`). - Slash‑command elementization recognizes built‑ins and `/prompts:` custom commands only. ## Codex author `codex fork 019c16d3-4520-7bb0-9b9d-48720d40a8ab` --- codex-rs/tui/src/bottom_pane/chat_composer.rs | 381 ++++++++++++++++-- codex-rs/tui/src/bottom_pane/mod.rs | 14 + codex-rs/tui/src/bottom_pane/textarea.rs | 40 ++ codex-rs/tui/src/chatwidget.rs | 59 ++- codex-rs/tui/src/chatwidget/tests.rs | 44 ++ codex-rs/tui/src/slash_command.rs | 10 +- docs/tui-chat-composer.md | 5 + 7 files changed, 517 insertions(+), 36 deletions(-) diff --git a/codex-rs/tui/src/bottom_pane/chat_composer.rs b/codex-rs/tui/src/bottom_pane/chat_composer.rs index 979213112f..068674b418 100644 --- a/codex-rs/tui/src/bottom_pane/chat_composer.rs +++ b/codex-rs/tui/src/bottom_pane/chat_composer.rs @@ -4,6 +4,7 @@ //! //! - Editing the input buffer (a [`TextArea`]), including placeholder "elements" for attachments. //! - Routing keys to the active popup (slash commands, file search, skill/apps mentions). +//! - Promoting typed slash commands into atomic elements when the command name is completed. //! - Handling submit vs newline on Enter. //! - Turning raw key streams into explicit paste operations on platforms where terminals //! don't provide reliable bracketed paste (notably Windows). @@ -36,6 +37,8 @@ //! //! The numeric auto-submit path used by the slash popup performs the same pending-paste expansion //! and attachment pruning, and clears pending paste state on success. +//! Slash commands with arguments (like `/plan` and `/review`) reuse the same preparation path so +//! pasted content and text elements are preserved when extracting args. //! //! # Non-bracketed Paste Bursts //! @@ -164,6 +167,7 @@ use std::cell::RefCell; use std::collections::HashMap; use std::collections::HashSet; use std::collections::VecDeque; +use std::ops::Range; use std::path::PathBuf; use std::time::Duration; use std::time::Instant; @@ -184,7 +188,7 @@ pub enum InputResult { text_elements: Vec, }, Command(SlashCommand), - CommandWithArgs(SlashCommand, String), + CommandWithArgs(SlashCommand, String, Vec), None, } @@ -747,6 +751,7 @@ impl ChatComposer { /// Move the cursor to the end of the current text buffer. pub(crate) fn move_cursor_to_end(&mut self) { self.textarea.set_cursor(self.textarea.text().len()); + self.sync_popups(); } pub(crate) fn clear_for_ctrl_c(&mut self) -> Option { @@ -1235,6 +1240,7 @@ impl ChatComposer { self.handle_paste(pasted); } self.textarea.input(input); + let text_after = self.textarea.text(); self.pending_pastes .retain(|(placeholder, _)| text_after.contains(placeholder)); @@ -1798,7 +1804,12 @@ impl ChatComposer { /// Prepare text for submission/queuing. Returns None if submission should be suppressed. /// On success, clears pending paste payloads because placeholders have been expanded. - fn prepare_submission_text(&mut self) -> Option<(String, Vec)> { + /// + /// When `record_history` is true, the final submission is stored for ↑/↓ recall. + fn prepare_submission_text( + &mut self, + record_history: bool, + ) -> Option<(String, Vec)> { let mut text = self.textarea.text().to_string(); let original_input = text.clone(); let original_text_elements = self.textarea.text_elements(); @@ -1896,7 +1907,7 @@ impl ChatComposer { if text.is_empty() && self.attached_images.is_empty() { return None; } - if !text.is_empty() || !self.attached_images.is_empty() { + if record_history && (!text.is_empty() || !self.attached_images.is_empty()) { let local_image_paths = self .attached_images .iter() @@ -1978,7 +1989,7 @@ impl ChatComposer { return (result, true); } - if let Some((text, text_elements)) = self.prepare_submission_text() { + if let Some((text, text_elements)) = self.prepare_submission_text(true) { if should_queue { ( InputResult::Queued { @@ -2026,6 +2037,9 @@ impl ChatComposer { self.windows_degraded_sandbox_active, ) { + if self.reject_slash_command_if_unavailable(cmd) { + return Some(InputResult::None); + } self.textarea.set_text_clearing_elements(""); Some(InputResult::Command(cmd)) } else { @@ -2039,28 +2053,104 @@ impl ChatComposer { if !self.slash_commands_enabled() { return None; } - let original_input = self.textarea.text().to_string(); - let input_starts_with_space = original_input.starts_with(' '); - - if !input_starts_with_space { - let text = self.textarea.text().to_string(); - if let Some((name, rest, _rest_offset)) = parse_slash_name(&text) - && !rest.is_empty() - && !name.contains('/') - && let Some(cmd) = slash_commands::find_builtin_command( - name, - self.collaboration_modes_enabled, - self.connectors_enabled, - self.personality_command_enabled, - self.windows_degraded_sandbox_active, - ) - && matches!(cmd, SlashCommand::Review | SlashCommand::Rename) - { - self.textarea.set_text_clearing_elements(""); - return Some(InputResult::CommandWithArgs(cmd, rest.to_string())); - } + let text = self.textarea.text().to_string(); + if text.starts_with(' ') { + return None; } - None + + let (name, rest, rest_offset) = parse_slash_name(&text)?; + if rest.is_empty() || name.contains('/') { + return None; + } + + let cmd = slash_commands::find_builtin_command( + name, + self.collaboration_modes_enabled, + self.connectors_enabled, + self.personality_command_enabled, + self.windows_degraded_sandbox_active, + )?; + + if !cmd.supports_inline_args() { + return None; + } + if self.reject_slash_command_if_unavailable(cmd) { + return Some(InputResult::None); + } + + let mut args_elements = + Self::slash_command_args_elements(rest, rest_offset, &self.textarea.text_elements()); + let trimmed_rest = rest.trim(); + args_elements = Self::trim_text_elements(rest, trimmed_rest, args_elements); + Some(InputResult::CommandWithArgs( + cmd, + trimmed_rest.to_string(), + args_elements, + )) + } + + /// Expand pending placeholders and extract normalized inline-command args. + /// + /// Inline-arg commands are initially dispatched using the raw draft so command rejection does + /// not consume user input. Once a command is accepted, this helper performs the usual + /// submission preparation (paste expansion, element trimming) and rebases element ranges from + /// full-text offsets to command-arg offsets. + pub(crate) fn prepare_inline_args_submission( + &mut self, + record_history: bool, + ) -> Option<(String, Vec)> { + let (prepared_text, prepared_elements) = self.prepare_submission_text(record_history)?; + let (_, prepared_rest, prepared_rest_offset) = parse_slash_name(&prepared_text)?; + let mut args_elements = Self::slash_command_args_elements( + prepared_rest, + prepared_rest_offset, + &prepared_elements, + ); + let trimmed_rest = prepared_rest.trim(); + args_elements = Self::trim_text_elements(prepared_rest, trimmed_rest, args_elements); + Some((trimmed_rest.to_string(), args_elements)) + } + + fn reject_slash_command_if_unavailable(&self, cmd: SlashCommand) -> bool { + if !self.is_task_running || cmd.available_during_task() { + return false; + } + let message = format!( + "'/{}' is disabled while a task is in progress.", + cmd.command() + ); + self.app_event_tx.send(AppEvent::InsertHistoryCell(Box::new( + history_cell::new_error_event(message), + ))); + true + } + + /// Translate full-text element ranges into command-argument ranges. + /// + /// `rest_offset` is the byte offset where `rest` begins in the full text. + fn slash_command_args_elements( + rest: &str, + rest_offset: usize, + text_elements: &[TextElement], + ) -> Vec { + if rest.is_empty() || text_elements.is_empty() { + return Vec::new(); + } + text_elements + .iter() + .filter_map(|elem| { + if elem.byte_range.end <= rest_offset { + return None; + } + let start = elem.byte_range.start.saturating_sub(rest_offset); + let mut end = elem.byte_range.end.saturating_sub(rest_offset); + if start >= rest.len() { + return None; + } + end = end.min(rest.len()); + (start < end).then_some(elem.map_range(|_| ByteRange { start, end })) + }) + .collect() } /// Handle key event when no popup is visible. @@ -2441,6 +2531,7 @@ impl ChatComposer { } fn sync_popups(&mut self) { + self.sync_slash_command_elements(); if !self.popups_enabled() { self.active_popup = ActivePopup::None; return; @@ -2507,6 +2598,88 @@ impl ChatComposer { } } + /// Keep slash command elements aligned with the current first line. + fn sync_slash_command_elements(&mut self) { + if !self.slash_commands_enabled() { + return; + } + let text = self.textarea.text(); + let first_line_end = text.find('\n').unwrap_or(text.len()); + let first_line = &text[..first_line_end]; + let desired_range = self.slash_command_element_range(first_line); + // Slash commands are only valid at byte 0 of the first line. + // Any slash-shaped element not matching the current desired prefix is stale. + let mut has_desired = false; + let mut stale_ranges = Vec::new(); + for elem in self.textarea.text_elements() { + let Some(payload) = elem.placeholder(text) else { + continue; + }; + if payload.strip_prefix('/').is_none() { + continue; + } + let range = elem.byte_range.start..elem.byte_range.end; + if desired_range.as_ref() == Some(&range) { + has_desired = true; + } else { + stale_ranges.push(range); + } + } + + for range in stale_ranges { + self.textarea.remove_element_range(range); + } + + if let Some(range) = desired_range + && !has_desired + { + self.textarea.add_element_range(range); + } + } + + fn slash_command_element_range(&self, first_line: &str) -> Option> { + let (name, _rest, _rest_offset) = parse_slash_name(first_line)?; + if name.contains('/') { + return None; + } + let element_end = 1 + name.len(); + let has_space_after = first_line + .get(element_end..) + .and_then(|tail| tail.chars().next()) + .is_some_and(char::is_whitespace); + if !has_space_after { + return None; + } + if self.is_known_slash_name(name) { + Some(0..element_end) + } else { + None + } + } + + fn is_known_slash_name(&self, name: &str) -> bool { + let is_builtin = slash_commands::find_builtin_command( + name, + self.collaboration_modes_enabled, + self.connectors_enabled, + self.personality_command_enabled, + self.windows_degraded_sandbox_active, + ) + .is_some(); + if is_builtin { + return true; + } + if let Some(rest) = name.strip_prefix(PROMPTS_CMD_PREFIX) + && let Some(prompt_name) = rest.strip_prefix(':') + { + return self + .custom_prompts + .iter() + .any(|prompt| prompt.name == prompt_name); + } + false + } + /// If the cursor is currently within a slash command on the first line, /// extract the command name and the rest of the line after it. /// Returns None if the cursor is outside a slash command. @@ -4582,7 +4755,7 @@ mod tests { InputResult::Command(cmd) => { assert_eq!(cmd.command(), "init"); } - InputResult::CommandWithArgs(_, _) => { + InputResult::CommandWithArgs(_, _, _) => { panic!("expected command dispatch without args for '/init'") } InputResult::Submitted { text, .. } => { @@ -4596,6 +4769,49 @@ mod tests { assert!(composer.textarea.is_empty(), "composer should be cleared"); } + #[test] + fn slash_command_disabled_while_task_running_keeps_text() { + use crossterm::event::KeyCode; + use crossterm::event::KeyEvent; + use crossterm::event::KeyModifiers; + + let (tx, mut rx) = unbounded_channel::(); + let sender = AppEventSender::new(tx); + let mut composer = ChatComposer::new( + true, + sender, + false, + "Ask Codex to do anything".to_string(), + false, + ); + composer.set_task_running(true); + composer + .textarea + .set_text_clearing_elements("/review these changes"); + + let (result, _needs_redraw) = + composer.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE)); + + assert_eq!(InputResult::None, result); + assert_eq!("/review these changes", composer.textarea.text()); + + let mut found_error = false; + while let Ok(event) = rx.try_recv() { + if let AppEvent::InsertHistoryCell(cell) = event { + let message = cell + .display_lines(80) + .into_iter() + .map(|line| line.to_string()) + .collect::>() + .join("\n"); + assert!(message.contains("disabled while a task is in progress")); + found_error = true; + break; + } + } + assert!(found_error, "expected error history cell to be sent"); + } + #[test] fn extract_args_supports_quoted_paths_single_arg() { let args = extract_positional_args_for_prompt_line( @@ -4683,7 +4899,7 @@ mod tests { composer.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE)); match result { InputResult::Command(cmd) => assert_eq!(cmd.command(), "diff"), - InputResult::CommandWithArgs(_, _) => { + InputResult::CommandWithArgs(_, _, _) => { panic!("expected command dispatch without args for '/diff'") } InputResult::Submitted { text, .. } => { @@ -4697,6 +4913,77 @@ mod tests { assert!(composer.textarea.is_empty()); } + #[test] + fn slash_command_elementizes_on_space() { + let (tx, _rx) = unbounded_channel::(); + let sender = AppEventSender::new(tx); + let mut composer = ChatComposer::new( + true, + sender, + false, + "Ask Codex to do anything".to_string(), + false, + ); + composer.set_collaboration_modes_enabled(true); + + type_chars_humanlike(&mut composer, &['/', 'p', 'l', 'a', 'n', ' ']); + + let text = composer.textarea.text().to_string(); + let elements = composer.textarea.text_elements(); + assert_eq!(text, "/plan "); + assert_eq!(elements.len(), 1); + assert_eq!(elements[0].placeholder(&text), Some("/plan")); + } + + #[test] + fn slash_command_elementizes_only_known_commands() { + let (tx, _rx) = unbounded_channel::(); + let sender = AppEventSender::new(tx); + let mut composer = ChatComposer::new( + true, + sender, + false, + "Ask Codex to do anything".to_string(), + false, + ); + composer.set_collaboration_modes_enabled(true); + + type_chars_humanlike(&mut composer, &['/', 'U', 's', 'e', 'r', 's', ' ']); + + let text = composer.textarea.text().to_string(); + let elements = composer.textarea.text_elements(); + assert_eq!(text, "/Users "); + assert!(elements.is_empty()); + } + + #[test] + fn slash_command_element_removed_when_not_at_start() { + let (tx, _rx) = unbounded_channel::(); + let sender = AppEventSender::new(tx); + let mut composer = ChatComposer::new( + true, + sender, + false, + "Ask Codex to do anything".to_string(), + false, + ); + + type_chars_humanlike(&mut composer, &['/', 'r', 'e', 'v', 'i', 'e', 'w', ' ']); + + let text = composer.textarea.text().to_string(); + let elements = composer.textarea.text_elements(); + assert_eq!(text, "/review "); + assert_eq!(elements.len(), 1); + + composer.textarea.set_cursor(0); + type_chars_humanlike(&mut composer, &['x']); + + let text = composer.textarea.text().to_string(); + let elements = composer.textarea.text_elements(); + assert_eq!(text, "x/review "); + assert!(elements.is_empty()); + } + #[test] fn slash_mention_dispatches_command_and_inserts_at() { use crossterm::event::KeyCode; @@ -4722,7 +5009,7 @@ mod tests { InputResult::Command(cmd) => { assert_eq!(cmd.command(), "mention"); } - InputResult::CommandWithArgs(_, _) => { + InputResult::CommandWithArgs(_, _, _) => { panic!("expected command dispatch without args for '/mention'") } InputResult::Submitted { text, .. } => { @@ -4738,6 +5025,44 @@ mod tests { assert_eq!(composer.textarea.text(), "@"); } + #[test] + fn slash_plan_args_preserve_text_elements() { + use crossterm::event::KeyCode; + use crossterm::event::KeyEvent; + use crossterm::event::KeyModifiers; + + let (tx, _rx) = unbounded_channel::(); + let sender = AppEventSender::new(tx); + let mut composer = ChatComposer::new( + true, + sender, + false, + "Ask Codex to do anything".to_string(), + false, + ); + composer.set_collaboration_modes_enabled(true); + + type_chars_humanlike(&mut composer, &['/', 'p', 'l', 'a', 'n', ' ']); + let placeholder = local_image_label_text(1); + composer.attach_image(PathBuf::from("/tmp/plan.png")); + + let (result, _needs_redraw) = + composer.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE)); + + match result { + InputResult::CommandWithArgs(cmd, args, text_elements) => { + assert_eq!(cmd.command(), "plan"); + assert_eq!(args, placeholder); + assert_eq!(text_elements.len(), 1); + assert_eq!( + text_elements[0].placeholder(&args), + Some(placeholder.as_str()) + ); + } + _ => panic!("expected CommandWithArgs for /plan with args"), + } + } + /// Behavior: multiple paste operations can coexist; placeholders should be expanded to their /// original content on submission. #[test] diff --git a/codex-rs/tui/src/bottom_pane/mod.rs b/codex-rs/tui/src/bottom_pane/mod.rs index a632fd4468..6d3a6755a4 100644 --- a/codex-rs/tui/src/bottom_pane/mod.rs +++ b/codex-rs/tui/src/bottom_pane/mod.rs @@ -218,6 +218,12 @@ impl BottomPane { self.composer.take_mention_paths() } + /// Clear pending attachments and mention paths e.g. when a slash command doesn't submit text. + pub(crate) fn drain_pending_submission_state(&mut self) { + let _ = self.take_recent_submission_images_with_placeholders(); + let _ = self.take_mention_paths(); + } + pub fn set_steer_enabled(&mut self, enabled: bool) { self.composer.set_steer_enabled(enabled); } @@ -404,6 +410,7 @@ impl BottomPane { ) { self.composer .set_text_content(text, text_elements, local_image_paths); + self.composer.move_cursor_to_end(); self.request_redraw(); } @@ -787,6 +794,13 @@ impl BottomPane { .take_recent_submission_images_with_placeholders() } + pub(crate) fn prepare_inline_args_submission( + &mut self, + record_history: bool, + ) -> Option<(String, Vec)> { + self.composer.prepare_inline_args_submission(record_history) + } + fn as_renderable(&'_ self) -> RenderableItem<'_> { if let Some(view) = self.active_view() { RenderableItem::Borrowed(view) diff --git a/codex-rs/tui/src/bottom_pane/textarea.rs b/codex-rs/tui/src/bottom_pane/textarea.rs index 42f7b09bef..a90d5b87c6 100644 --- a/codex-rs/tui/src/bottom_pane/textarea.rs +++ b/codex-rs/tui/src/bottom_pane/textarea.rs @@ -844,6 +844,46 @@ impl TextArea { self.set_cursor(end); } + /// Mark an existing text range as an atomic element without changing the text. + /// + /// This is used to convert already-typed tokens (like `/plan`) into elements + /// so they render and edit atomically. Overlapping or duplicate ranges are ignored. + pub fn add_element_range(&mut self, range: Range) { + let start = self.clamp_pos_to_char_boundary(range.start.min(self.text.len())); + let end = self.clamp_pos_to_char_boundary(range.end.min(self.text.len())); + if start >= end { + return; + } + if self + .elements + .iter() + .any(|e| e.range.start == start && e.range.end == end) + { + return; + } + if self + .elements + .iter() + .any(|e| start < e.range.end && end > e.range.start) + { + return; + } + self.elements.push(TextElement { range: start..end }); + self.elements.sort_by_key(|e| e.range.start); + } + + pub fn remove_element_range(&mut self, range: Range) -> bool { + let start = self.clamp_pos_to_char_boundary(range.start.min(self.text.len())); + let end = self.clamp_pos_to_char_boundary(range.end.min(self.text.len())); + if start >= end { + return false; + } + let len_before = self.elements.len(); + self.elements + .retain(|elem| elem.range.start != start || elem.range.end != end); + len_before != self.elements.len() + } + fn add_element(&mut self, range: Range) { let elem = TextElement { range }; self.elements.push(elem); diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index 09a7d0fc65..078fbb9785 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -2732,8 +2732,8 @@ impl ChatWidget { InputResult::Command(cmd) => { self.dispatch_command(cmd); } - InputResult::CommandWithArgs(cmd, args) => { - self.dispatch_command_with_args(cmd, args); + InputResult::CommandWithArgs(cmd, args, text_elements) => { + self.dispatch_command_with_args(cmd, args, text_elements); } InputResult::None => {} }, @@ -2783,6 +2783,7 @@ impl ChatWidget { cmd.command() ); self.add_to_history(history_cell::new_error_event(message)); + self.bottom_pane.drain_pending_submission_state(); self.request_redraw(); return; } @@ -3019,7 +3020,16 @@ impl ChatWidget { } } - fn dispatch_command_with_args(&mut self, cmd: SlashCommand, args: String) { + fn dispatch_command_with_args( + &mut self, + cmd: SlashCommand, + args: String, + _text_elements: Vec, + ) { + if !cmd.supports_inline_args() { + self.dispatch_command(cmd); + return; + } if !cmd.available_during_task() && self.bottom_pane.is_task_running() { let message = format!( "'/{}' is disabled while a task is in progress.", @@ -3033,7 +3043,12 @@ impl ChatWidget { let trimmed = args.trim(); match cmd { SlashCommand::Rename if !trimmed.is_empty() => { - let Some(name) = codex_core::util::normalize_thread_name(trimmed) else { + let Some((prepared_args, _prepared_elements)) = + self.bottom_pane.prepare_inline_args_submission(false) + else { + return; + }; + let Some(name) = codex_core::util::normalize_thread_name(&prepared_args) else { self.add_error_message("Thread name cannot be empty.".to_string()); return; }; @@ -3042,20 +3057,50 @@ impl ChatWidget { self.request_redraw(); self.app_event_tx .send(AppEvent::CodexOp(Op::SetThreadName { name })); + self.bottom_pane.drain_pending_submission_state(); } - SlashCommand::Collab | SlashCommand::Plan => { - let _ = trimmed; + SlashCommand::Plan if !trimmed.is_empty() => { self.dispatch_command(cmd); + if self.active_mode_kind() != ModeKind::Plan { + return; + } + let Some((prepared_args, prepared_elements)) = + self.bottom_pane.prepare_inline_args_submission(true) + else { + return; + }; + let user_message = UserMessage { + text: prepared_args, + local_images: self + .bottom_pane + .take_recent_submission_images_with_placeholders(), + text_elements: prepared_elements, + mention_paths: self.bottom_pane.take_mention_paths(), + }; + if self.is_session_configured() { + self.reasoning_buffer.clear(); + self.full_reasoning_buffer.clear(); + self.set_status_header(String::from("Working")); + self.submit_user_message(user_message); + } else { + self.queue_user_message(user_message); + } } SlashCommand::Review if !trimmed.is_empty() => { + let Some((prepared_args, _prepared_elements)) = + self.bottom_pane.prepare_inline_args_submission(false) + else { + return; + }; self.submit_op(Op::Review { review_request: ReviewRequest { target: ReviewTarget::Custom { - instructions: trimmed.to_string(), + instructions: prepared_args, }, user_facing_hint: None, }, }); + self.bottom_pane.drain_pending_submission_state(); } _ => self.dispatch_command(cmd), } diff --git a/codex-rs/tui/src/chatwidget/tests.rs b/codex-rs/tui/src/chatwidget/tests.rs index d24504578c..980d15c339 100644 --- a/codex-rs/tui/src/chatwidget/tests.rs +++ b/codex-rs/tui/src/chatwidget/tests.rs @@ -2316,6 +2316,50 @@ async fn plan_slash_command_switches_to_plan_mode() { assert_eq!(chat.current_collaboration_mode(), &initial); } +#[tokio::test] +async fn plan_slash_command_with_args_submits_prompt_in_plan_mode() { + let (mut chat, _rx, mut op_rx) = make_chatwidget_manual(None).await; + chat.set_feature_enabled(Feature::CollaborationModes, true); + + let configured = codex_core::protocol::SessionConfiguredEvent { + session_id: ThreadId::new(), + forked_from_id: None, + thread_name: None, + model: "test-model".to_string(), + model_provider_id: "test-provider".to_string(), + approval_policy: AskForApproval::Never, + sandbox_policy: SandboxPolicy::ReadOnly, + cwd: PathBuf::from("/home/user/project"), + reasoning_effort: Some(ReasoningEffortConfig::default()), + history_log_id: 0, + history_entry_count: 0, + initial_messages: None, + rollout_path: None, + }; + chat.handle_codex_event(Event { + id: "configured".into(), + msg: EventMsg::SessionConfigured(configured), + }); + + chat.bottom_pane + .set_composer_text("/plan build the plan".to_string(), Vec::new(), Vec::new()); + chat.handle_key_event(KeyEvent::from(KeyCode::Enter)); + + let items = match next_submit_op(&mut op_rx) { + Op::UserTurn { items, .. } => items, + other => panic!("expected Op::UserTurn, got {other:?}"), + }; + assert_eq!(items.len(), 1); + assert_eq!( + items[0], + UserInput::Text { + text: "build the plan".to_string(), + text_elements: Vec::new(), + } + ); + assert_eq!(chat.active_collaboration_mode_kind(), ModeKind::Plan); +} + #[tokio::test] async fn collaboration_modes_defaults_to_code_on_startup() { let codex_home = tempdir().expect("tempdir"); diff --git a/codex-rs/tui/src/slash_command.rs b/codex-rs/tui/src/slash_command.rs index 4b68645aef..a71b1ddbe5 100644 --- a/codex-rs/tui/src/slash_command.rs +++ b/codex-rs/tui/src/slash_command.rs @@ -87,6 +87,14 @@ impl SlashCommand { self.into() } + /// Whether this command supports inline args (for example `/review ...`). + pub fn supports_inline_args(self) -> bool { + matches!( + self, + SlashCommand::Review | SlashCommand::Rename | SlashCommand::Plan + ) + } + /// Whether this command can be run while a task is in progress. pub fn available_during_task(self) -> bool { match self { @@ -103,6 +111,7 @@ impl SlashCommand { | SlashCommand::ElevateSandbox | SlashCommand::Experimental | SlashCommand::Review + | SlashCommand::Plan | SlashCommand::Logout => false, SlashCommand::Diff | SlashCommand::Rename @@ -117,7 +126,6 @@ impl SlashCommand { | SlashCommand::Exit => true, SlashCommand::Rollout => true, SlashCommand::TestApproval => true, - SlashCommand::Plan => true, SlashCommand::Collab => true, SlashCommand::Agent => true, } diff --git a/docs/tui-chat-composer.md b/docs/tui-chat-composer.md index 01b5334d94..6211cbcfce 100644 --- a/docs/tui-chat-composer.md +++ b/docs/tui-chat-composer.md @@ -48,6 +48,8 @@ The solution is to detect paste-like _bursts_ and buffer them into a single expl history navigation, etc). - After handling the key, `sync_popups()` runs so popup visibility/filters stay consistent with the latest text + cursor. +- When a slash command name is completed and the user types a space, the `/command` token is + promoted into a text element so it renders distinctly and edits atomically. ### History navigation (↑/↓) @@ -105,6 +107,9 @@ There are multiple submission paths, but they share the same core rules: 5. Clears pending pastes on success and suppresses submission if the final text is empty and there are no attachments. +The same preparation path is reused for slash commands with arguments (for example `/plan` and +`/review`) so pasted content and text elements are preserved when extracting args. + ### Numeric auto-submit path When the slash popup is open and the first line matches a numeric-only custom prompt with From 9d976962ec6baa3fc3df7370927053b86591b1f7 Mon Sep 17 00:00:00 2001 From: pakrym-oai Date: Mon, 2 Feb 2026 10:06:43 -0800 Subject: [PATCH 03/28] Add credits tooltip (#10274) --- codex-rs/tui/src/app.rs | 1 + codex-rs/tui/src/chatwidget.rs | 3 +++ codex-rs/tui/src/history_cell.rs | 4 +++- codex-rs/tui/src/tooltips.rs | 32 ++++++++++++++++++++++++++++++-- 4 files changed, 37 insertions(+), 3 deletions(-) diff --git a/codex-rs/tui/src/app.rs b/codex-rs/tui/src/app.rs index 0e155fdc00..8dc2276b75 100644 --- a/codex-rs/tui/src/app.rs +++ b/codex-rs/tui/src/app.rs @@ -2939,6 +2939,7 @@ mod tests { app.chat_widget.current_model(), event, is_first, + None, )) as Arc }; diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index 078fbb9785..eb2d1ad965 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -815,6 +815,9 @@ impl ChatWidget { &model_for_header, event, self.show_welcome_banner, + self.auth_manager + .auth_cached() + .and_then(|auth| auth.account_plan_type()), ); self.apply_session_info_cell(session_info_cell); diff --git a/codex-rs/tui/src/history_cell.rs b/codex-rs/tui/src/history_cell.rs index 934c9506f5..eb1de452a9 100644 --- a/codex-rs/tui/src/history_cell.rs +++ b/codex-rs/tui/src/history_cell.rs @@ -46,6 +46,7 @@ use codex_core::protocol::McpInvocation; use codex_core::protocol::SessionConfiguredEvent; use codex_core::web_search::web_search_detail; use codex_otel::RuntimeMetricsSummary; +use codex_protocol::account::PlanType; use codex_protocol::models::WebSearchAction; use codex_protocol::openai_models::ReasoningEffort as ReasoningEffortConfig; use codex_protocol::plan_tool::PlanItemArg; @@ -943,6 +944,7 @@ pub(crate) fn new_session_info( requested_model: &str, event: SessionConfiguredEvent, is_first_event: bool, + auth_plan: Option, ) -> SessionInfoCell { let SessionConfiguredEvent { model, @@ -995,7 +997,7 @@ pub(crate) fn new_session_info( parts.push(Box::new(PlainHistoryCell { lines: help_lines })); } else { if config.show_tooltips - && let Some(tooltips) = tooltips::random_tooltip().map(TooltipHistoryCell::new) + && let Some(tooltips) = tooltips::get_tooltip(auth_plan).map(TooltipHistoryCell::new) { parts.push(Box::new(tooltips)); } diff --git a/codex-rs/tui/src/tooltips.rs b/codex-rs/tui/src/tooltips.rs index 1b59eb4f80..b59a478a4d 100644 --- a/codex-rs/tui/src/tooltips.rs +++ b/codex-rs/tui/src/tooltips.rs @@ -1,9 +1,18 @@ use codex_core::features::FEATURES; +use codex_protocol::account::PlanType; use lazy_static::lazy_static; use rand::Rng; const ANNOUNCEMENT_TIP_URL: &str = "https://raw.githubusercontent.com/openai/codex/main/announcement_tip.toml"; + +const PAID_TOOLTIP: &str = + "*New* Try the **Codex App** with 2x rate limits until *April 2nd*. https://chatgpt.com/codex"; +const OTHER_TOOLTIP: &str = + "*New* Build faster with the **Codex App**. Try it now. https://chatgpt.com/codex"; +const FREE_GO_TOOLTIP: &str = + "*New* Codex is included in your plan for free through *March 2nd* – let’s build together."; + const RAW_TOOLTIPS: &str = include_str!("../tooltips.txt"); lazy_static! { @@ -28,11 +37,30 @@ fn experimental_tooltips() -> Vec<&'static str> { } /// Pick a random tooltip to show to the user when starting Codex. -pub(crate) fn random_tooltip() -> Option { +pub(crate) fn get_tooltip(plan: Option) -> Option { + let mut rng = rand::rng(); + + // Leave small chance for a random tooltip to be shown. + if rng.random_ratio(8, 10) { + match plan { + Some(PlanType::Plus) + | Some(PlanType::Business) + | Some(PlanType::Team) + | Some(PlanType::Enterprise) + | Some(PlanType::Pro) => { + return Some(PAID_TOOLTIP.to_string()); + } + Some(PlanType::Go) | Some(PlanType::Free) => { + return Some(FREE_GO_TOOLTIP.to_string()); + } + _ => return Some(OTHER_TOOLTIP.to_string()), + } + } + if let Some(announcement) = announcement::fetch_announcement_tip() { return Some(announcement); } - let mut rng = rand::rng(); + pick_tooltip(&mut rng).map(str::to_string) } From 0b460eda32c031430cc882894acb22ca9cb5480a Mon Sep 17 00:00:00 2001 From: jif-oai Date: Mon, 2 Feb 2026 19:13:48 +0100 Subject: [PATCH 04/28] chore: ignore synthetic messages (#10394) This will be fixed once this is settled: https://www.notion.so/openai/Artificial-context-management-2fb8e50b62b080db8b8ed93b3b19d1a2#2fb8e50b62b080d2bffce2dd1e60972b --- codex-rs/core/src/rollout/list.rs | 7 ++++++- codex-rs/core/src/session_prefix.rs | 11 +++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/codex-rs/core/src/rollout/list.rs b/codex-rs/core/src/rollout/list.rs index f42b4795cd..16bd5b16ed 100644 --- a/codex-rs/core/src/rollout/list.rs +++ b/codex-rs/core/src/rollout/list.rs @@ -15,7 +15,9 @@ use uuid::Uuid; use super::ARCHIVED_SESSIONS_SUBDIR; use super::SESSIONS_SUBDIR; +use crate::instructions::UserInstructions; use crate::protocol::EventMsg; +use crate::session_prefix::is_session_prefix_content; use crate::state_db; use codex_file_search as file_search; use codex_protocol::ThreadId; @@ -982,9 +984,12 @@ async fn read_head_summary(path: &Path, head_limit: usize) -> io::Result bool { let lowered = trimmed.to_ascii_lowercase(); lowered.starts_with(ENVIRONMENT_CONTEXT_OPEN_TAG) || lowered.starts_with(TURN_ABORTED_OPEN_TAG) } + +/// Returns true if `text` starts with a session prefix marker (case-insensitive). +pub(crate) fn is_session_prefix_content(content: &[ContentItem]) -> bool { + if let [ContentItem::InputText { text }] = content { + is_session_prefix(text) + } else { + false + } +} From 34c0534f6eb85eb1976862d666cef3af165848d6 Mon Sep 17 00:00:00 2001 From: jif-oai Date: Mon, 2 Feb 2026 19:26:58 +0000 Subject: [PATCH 05/28] feat: drop sqlx logging (#10398) --- codex-rs/Cargo.lock | 1 + codex-rs/state/Cargo.toml | 1 + codex-rs/state/src/runtime.rs | 5 ++++- 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index 4614e5f24f..57186f82f1 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -1901,6 +1901,7 @@ dependencies = [ "codex-otel", "codex-protocol", "dirs", + "log", "owo-colors", "pretty_assertions", "serde", diff --git a/codex-rs/state/Cargo.toml b/codex-rs/state/Cargo.toml index 6019b5888b..837d451387 100644 --- a/codex-rs/state/Cargo.toml +++ b/codex-rs/state/Cargo.toml @@ -11,6 +11,7 @@ clap = { workspace = true, features = ["derive", "env"] } codex-otel = { workspace = true } codex-protocol = { workspace = true } dirs = { workspace = true } +log = { workspace = true } owo-colors = { workspace = true } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } diff --git a/codex-rs/state/src/runtime.rs b/codex-rs/state/src/runtime.rs index 39c79c32b9..6f2e128967 100644 --- a/codex-rs/state/src/runtime.rs +++ b/codex-rs/state/src/runtime.rs @@ -17,6 +17,8 @@ use chrono::Utc; use codex_otel::OtelManager; use codex_protocol::ThreadId; use codex_protocol::protocol::RolloutItem; +use log::LevelFilter; +use sqlx::ConnectOptions; use sqlx::QueryBuilder; use sqlx::Row; use sqlx::Sqlite; @@ -511,7 +513,8 @@ async fn open_sqlite(path: &Path) -> anyhow::Result { .create_if_missing(true) .journal_mode(SqliteJournalMode::Wal) .synchronous(SqliteSynchronous::Normal) - .busy_timeout(Duration::from_secs(5)); + .busy_timeout(Duration::from_secs(5)) + .log_statements(LevelFilter::Off); let pool = SqlitePoolOptions::new() .max_connections(5) .connect_with(options) From 74327fa59ce3b4ac2432e822d43ff43e5332097c Mon Sep 17 00:00:00 2001 From: pakrym-oai Date: Mon, 2 Feb 2026 11:35:11 -0800 Subject: [PATCH 06/28] Select experimental features with space (#10281) --- .../tui/src/bottom_pane/experimental_features_view.rs | 11 ++++++++--- ...hatwidget__tests__experimental_features_popup.snap | 2 +- codex-rs/tui/src/chatwidget/tests.rs | 6 +++--- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/codex-rs/tui/src/bottom_pane/experimental_features_view.rs b/codex-rs/tui/src/bottom_pane/experimental_features_view.rs index 07cfce163f..1fde95b08f 100644 --- a/codex-rs/tui/src/bottom_pane/experimental_features_view.rs +++ b/codex-rs/tui/src/bottom_pane/experimental_features_view.rs @@ -175,11 +175,16 @@ impl BottomPaneView for ExperimentalFeaturesView { .. } => self.move_down(), KeyEvent { - code: KeyCode::Enter, + code: KeyCode::Char(' '), modifiers: KeyModifiers::NONE, .. } => self.toggle_selected(), KeyEvent { + code: KeyCode::Enter, + modifiers: KeyModifiers::NONE, + .. + } + | KeyEvent { code: KeyCode::Esc, .. } => { self.on_ctrl_c(); @@ -287,9 +292,9 @@ impl Renderable for ExperimentalFeaturesView { fn experimental_popup_hint_line() -> Line<'static> { Line::from(vec![ "Press ".into(), + key_hint::plain(KeyCode::Char(' ')).into(), + " to select or ".into(), key_hint::plain(KeyCode::Enter).into(), - " to toggle or ".into(), - key_hint::plain(KeyCode::Esc).into(), " to save for next conversation".into(), ]) } diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__experimental_features_popup.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__experimental_features_popup.snap index 4101717cf9..9cb2d78522 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__experimental_features_popup.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__experimental_features_popup.snap @@ -8,4 +8,4 @@ expression: popup › [ ] Ghost snapshots Capture undo snapshots each turn. [x] Shell tool Allow the model to run shell commands. - Press enter to toggle or esc to save for next conversation + Press space to select or enter to save for next conversation diff --git a/codex-rs/tui/src/chatwidget/tests.rs b/codex-rs/tui/src/chatwidget/tests.rs index 980d15c339..a70d93a2c4 100644 --- a/codex-rs/tui/src/chatwidget/tests.rs +++ b/codex-rs/tui/src/chatwidget/tests.rs @@ -3036,14 +3036,14 @@ async fn experimental_features_toggle_saves_on_exit() { ); chat.bottom_pane.show_view(Box::new(view)); - chat.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE)); + chat.handle_key_event(KeyEvent::new(KeyCode::Char(' '), KeyModifiers::NONE)); assert!( rx.try_recv().is_err(), - "expected no updates until exiting the popup" + "expected no updates until saving the popup" ); - chat.handle_key_event(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE)); + chat.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE)); let mut updates = None; while let Ok(event) = rx.try_recv() { From 059d386f0316fe8f4b73959a8b1c756d17e48bfd Mon Sep 17 00:00:00 2001 From: jif-oai Date: Mon, 2 Feb 2026 20:30:01 +0000 Subject: [PATCH 07/28] feat: add `--experimental` to `generate-ts` (#10402) Adding a `--experimental` flag to the `generate-ts` fct in the app-sever. It can be called through one of those 2 command ``` just write-app-server-schema --experimental codex app-server generate-ts --experimental ``` --- .../src/bin/write_schema_fixtures.rs | 24 ++++++++++++------ codex-rs/app-server-protocol/src/lib.rs | 2 ++ .../src/schema_fixtures.rs | 25 +++++++++++++++++-- codex-rs/app-server/README.md | 2 ++ justfile | 4 +-- 5 files changed, 46 insertions(+), 11 deletions(-) diff --git a/codex-rs/app-server-protocol/src/bin/write_schema_fixtures.rs b/codex-rs/app-server-protocol/src/bin/write_schema_fixtures.rs index a8e36d1ee8..789d30cea2 100644 --- a/codex-rs/app-server-protocol/src/bin/write_schema_fixtures.rs +++ b/codex-rs/app-server-protocol/src/bin/write_schema_fixtures.rs @@ -13,6 +13,10 @@ struct Args { /// Optional path to the Prettier executable to format generated TypeScript files. #[arg(short = 'p', long = "prettier", value_name = "PRETTIER_BIN")] prettier: Option, + + /// Include experimental API methods and fields in generated fixtures. + #[arg(long = "experimental")] + experimental: bool, } fn main() -> Result<()> { @@ -22,11 +26,17 @@ fn main() -> Result<()> { .schema_root .unwrap_or_else(|| PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("schema")); - codex_app_server_protocol::write_schema_fixtures(&schema_root, args.prettier.as_deref()) - .with_context(|| { - format!( - "failed to regenerate schema fixtures under {}", - schema_root.display() - ) - }) + codex_app_server_protocol::write_schema_fixtures_with_options( + &schema_root, + args.prettier.as_deref(), + codex_app_server_protocol::SchemaFixtureOptions { + experimental_api: args.experimental, + }, + ) + .with_context(|| { + format!( + "failed to regenerate schema fixtures under {}", + schema_root.display() + ) + }) } diff --git a/codex-rs/app-server-protocol/src/lib.rs b/codex-rs/app-server-protocol/src/lib.rs index 7b3f17045b..54a933bb74 100644 --- a/codex-rs/app-server-protocol/src/lib.rs +++ b/codex-rs/app-server-protocol/src/lib.rs @@ -16,5 +16,7 @@ pub use protocol::common::*; pub use protocol::thread_history::*; pub use protocol::v1::*; pub use protocol::v2::*; +pub use schema_fixtures::SchemaFixtureOptions; pub use schema_fixtures::read_schema_fixture_tree; pub use schema_fixtures::write_schema_fixtures; +pub use schema_fixtures::write_schema_fixtures_with_options; diff --git a/codex-rs/app-server-protocol/src/schema_fixtures.rs b/codex-rs/app-server-protocol/src/schema_fixtures.rs index efd8bb2cb0..918011e95c 100644 --- a/codex-rs/app-server-protocol/src/schema_fixtures.rs +++ b/codex-rs/app-server-protocol/src/schema_fixtures.rs @@ -6,6 +6,11 @@ use std::collections::BTreeMap; use std::path::Path; use std::path::PathBuf; +#[derive(Clone, Copy, Debug, Default)] +pub struct SchemaFixtureOptions { + pub experimental_api: bool, +} + pub fn read_schema_fixture_tree(schema_root: &Path) -> Result>> { let typescript_root = schema_root.join("typescript"); let json_root = schema_root.join("json"); @@ -26,14 +31,30 @@ pub fn read_schema_fixture_tree(schema_root: &Path) -> Result) -> Result<()> { + write_schema_fixtures_with_options(schema_root, prettier, SchemaFixtureOptions::default()) +} + +/// Regenerates schema fixtures with configurable options. +pub fn write_schema_fixtures_with_options( + schema_root: &Path, + prettier: Option<&Path>, + options: SchemaFixtureOptions, +) -> Result<()> { let typescript_out_dir = schema_root.join("typescript"); let json_out_dir = schema_root.join("json"); ensure_empty_dir(&typescript_out_dir)?; ensure_empty_dir(&json_out_dir)?; - crate::generate_ts(&typescript_out_dir, prettier)?; - crate::generate_json(&json_out_dir)?; + crate::generate_ts_with_options( + &typescript_out_dir, + prettier, + crate::GenerateTsOptions { + experimental_api: options.experimental_api, + ..crate::GenerateTsOptions::default() + }, + )?; + crate::generate_json_with_experimental(&json_out_dir, options.experimental_api)?; Ok(()) } diff --git a/codex-rs/app-server/README.md b/codex-rs/app-server/README.md index 67bb36d149..1d5b6d15fd 100644 --- a/codex-rs/app-server/README.md +++ b/codex-rs/app-server/README.md @@ -788,6 +788,8 @@ At runtime, clients must send `initialize` with `capabilities.experimentalApi = ```bash just write-app-server-schema + # Include experimental API fields/methods in fixtures. + just write-app-server-schema --experimental ``` 5. Verify the protocol crate: diff --git a/justfile b/justfile index 67577c7c7f..024a31ba2a 100644 --- a/justfile +++ b/justfile @@ -69,8 +69,8 @@ write-config-schema: cargo run -p codex-core --bin codex-write-config-schema # Regenerate vendored app-server protocol schema artifacts. -write-app-server-schema: - cargo run -p codex-app-server-protocol --bin write_schema_fixtures +write-app-server-schema *args: + cargo run -p codex-app-server-protocol --bin write_schema_fixtures -- "$@" # Tail logs from the state SQLite database log *args: From f50c8b2f81d69e49d1f6eafaeebf0f6ba2bbba52 Mon Sep 17 00:00:00 2001 From: viyatb-oai Date: Mon, 2 Feb 2026 12:30:17 -0800 Subject: [PATCH 08/28] fix: unsafe auto-approval of git commands (#10258) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fixes https://github.com/openai/codex/issues/10160 and some more. ## Description Hardens Git command safety to prevent approval bypasses for destructive or write-capable invocations (branch delete, risky push forms, output/config-override flags), so these commands no longer auto-run as “safe.” - `git branch -d` variants (especially in worktrees / with global options like -C / -c) - `git show|diff|log --output` ... style file-write flags - risky Git config override flags (-c, --config-env) that can trigger external execution - dangerous push forms that weren’t fully caught (`--force*`, `--delete`, `+refspec`, `:refspec`) - grouped short-flag delete forms (e.g. stacked branch flags containing `d/D`) will fast follow with a common git policy to bring windows to parity. --------- Co-authored-by: Eric Traut --- .../command_safety/is_dangerous_command.rs | 278 +++++++++++++++++- .../src/command_safety/is_safe_command.rs | 179 ++++++++++- codex-rs/core/src/exec_policy.rs | 24 ++ 3 files changed, 468 insertions(+), 13 deletions(-) diff --git a/codex-rs/core/src/command_safety/is_dangerous_command.rs b/codex-rs/core/src/command_safety/is_dangerous_command.rs index 256f36c600..3e2c669c44 100644 --- a/codex-rs/core/src/command_safety/is_dangerous_command.rs +++ b/codex-rs/core/src/command_safety/is_dangerous_command.rs @@ -27,12 +27,100 @@ pub fn command_might_be_dangerous(command: &[String]) -> bool { false } +fn is_git_global_option_with_value(arg: &str) -> bool { + matches!( + arg, + "-C" | "-c" + | "--config-env" + | "--exec-path" + | "--git-dir" + | "--namespace" + | "--super-prefix" + | "--work-tree" + ) +} + +fn is_git_global_option_with_inline_value(arg: &str) -> bool { + matches!( + arg, + s if s.starts_with("--config-env=") + || s.starts_with("--exec-path=") + || s.starts_with("--git-dir=") + || s.starts_with("--namespace=") + || s.starts_with("--super-prefix=") + || s.starts_with("--work-tree=") + ) || ((arg.starts_with("-C") || arg.starts_with("-c")) && arg.len() > 2) +} + +/// Find the first matching git subcommand, skipping known global options that +/// may appear before it (e.g., `-C`, `-c`, `--git-dir`). +/// +/// Shared with `is_safe_command` to avoid git-global-option bypasses. +pub(crate) fn find_git_subcommand<'a>( + command: &'a [String], + subcommands: &[&str], +) -> Option<(usize, &'a str)> { + let cmd0 = command.first().map(String::as_str)?; + if !cmd0.ends_with("git") { + return None; + } + + let mut skip_next = false; + for (idx, arg) in command.iter().enumerate().skip(1) { + if skip_next { + skip_next = false; + continue; + } + + let arg = arg.as_str(); + + if is_git_global_option_with_inline_value(arg) { + continue; + } + + if is_git_global_option_with_value(arg) { + skip_next = true; + continue; + } + + if arg == "--" || arg.starts_with('-') { + continue; + } + + if subcommands.contains(&arg) { + return Some((idx, arg)); + } + + // In git, the first non-option token is the subcommand. If it isn't + // one of the subcommands we're looking for, we must stop scanning to + // avoid misclassifying later positional args (e.g., branch names). + return None; + } + + None +} + fn is_dangerous_to_call_with_exec(command: &[String]) -> bool { let cmd0 = command.first().map(String::as_str); match cmd0 { - Some(cmd) if cmd.ends_with("git") || cmd.ends_with("/git") => { - matches!(command.get(1).map(String::as_str), Some("reset" | "rm")) + Some(cmd) if cmd.ends_with("git") => { + let Some((subcommand_idx, subcommand)) = + find_git_subcommand(command, &["reset", "rm", "branch", "push", "clean"]) + else { + return false; + }; + + match subcommand { + "reset" | "rm" => true, + "branch" => git_branch_is_delete(&command[subcommand_idx + 1..]), + "push" => git_push_is_dangerous(&command[subcommand_idx + 1..]), + "clean" => git_clean_is_force(&command[subcommand_idx + 1..]), + other => { + debug_assert!(false, "unexpected git subcommand from matcher: {other}"); + false + } + } } Some("rm") => matches!(command.get(1).map(String::as_str), Some("-f" | "-rf")), @@ -45,6 +133,48 @@ fn is_dangerous_to_call_with_exec(command: &[String]) -> bool { } } +fn git_branch_is_delete(branch_args: &[String]) -> bool { + // Git allows stacking short flags (for example, `-dv` or `-vd`). Treat any + // short-flag group containing `d`/`D` as a delete flag. + branch_args.iter().map(String::as_str).any(|arg| { + matches!(arg, "-d" | "-D" | "--delete") + || arg.starts_with("--delete=") + || short_flag_group_contains(arg, 'd') + || short_flag_group_contains(arg, 'D') + }) +} + +fn short_flag_group_contains(arg: &str, target: char) -> bool { + arg.starts_with('-') && !arg.starts_with("--") && arg.chars().skip(1).any(|c| c == target) +} + +fn git_push_is_dangerous(push_args: &[String]) -> bool { + push_args.iter().map(String::as_str).any(|arg| { + matches!( + arg, + "--force" | "--force-with-lease" | "--force-if-includes" | "--delete" | "-f" | "-d" + ) || arg.starts_with("--force-with-lease=") + || arg.starts_with("--force-if-includes=") + || arg.starts_with("--delete=") + || short_flag_group_contains(arg, 'f') + || short_flag_group_contains(arg, 'd') + || git_push_refspec_is_dangerous(arg) + }) +} + +fn git_push_refspec_is_dangerous(arg: &str) -> bool { + // `+` forces updates and `:` deletes remote refs. + (arg.starts_with('+') || arg.starts_with(':')) && arg.len() > 1 +} + +fn git_clean_is_force(clean_args: &[String]) -> bool { + clean_args.iter().map(String::as_str).any(|arg| { + matches!(arg, "--force" | "-f") + || arg.starts_with("--force=") + || short_flag_group_contains(arg, 'f') + }) +} + #[cfg(test)] mod tests { use super::*; @@ -63,7 +193,7 @@ mod tests { assert!(command_might_be_dangerous(&vec_str(&[ "bash", "-lc", - "git reset --hard" + "git reset --hard", ]))); } @@ -72,7 +202,7 @@ mod tests { assert!(command_might_be_dangerous(&vec_str(&[ "zsh", "-lc", - "git reset --hard" + "git reset --hard", ]))); } @@ -86,14 +216,14 @@ mod tests { assert!(!command_might_be_dangerous(&vec_str(&[ "bash", "-lc", - "git status" + "git status", ]))); } #[test] fn sudo_git_reset_is_dangerous() { assert!(command_might_be_dangerous(&vec_str(&[ - "sudo", "git", "reset", "--hard" + "sudo", "git", "reset", "--hard", ]))); } @@ -102,7 +232,141 @@ mod tests { assert!(command_might_be_dangerous(&vec_str(&[ "/usr/bin/git", "reset", - "--hard" + "--hard", + ]))); + } + + #[test] + fn git_branch_delete_is_dangerous() { + assert!(command_might_be_dangerous(&vec_str(&[ + "git", "branch", "-d", "feature", + ]))); + assert!(command_might_be_dangerous(&vec_str(&[ + "git", "branch", "-D", "feature", + ]))); + assert!(command_might_be_dangerous(&vec_str(&[ + "bash", + "-lc", + "git branch --delete feature", + ]))); + } + + #[test] + fn git_branch_delete_with_stacked_short_flags_is_dangerous() { + assert!(command_might_be_dangerous(&vec_str(&[ + "git", "branch", "-dv", "feature", + ]))); + assert!(command_might_be_dangerous(&vec_str(&[ + "git", "branch", "-vd", "feature", + ]))); + assert!(command_might_be_dangerous(&vec_str(&[ + "git", "branch", "-vD", "feature", + ]))); + assert!(command_might_be_dangerous(&vec_str(&[ + "git", "branch", "-Dvv", "feature", + ]))); + } + + #[test] + fn git_branch_delete_with_global_options_is_dangerous() { + assert!(command_might_be_dangerous(&vec_str(&[ + "git", "-C", ".", "branch", "-d", "feature", + ]))); + assert!(command_might_be_dangerous(&vec_str(&[ + "git", + "-c", + "color.ui=false", + "branch", + "-D", + "feature", + ]))); + assert!(command_might_be_dangerous(&vec_str(&[ + "bash", + "-lc", + "git -C . branch -d feature", + ]))); + } + + #[test] + fn git_checkout_reset_is_not_dangerous() { + // The first non-option token is "checkout", so later positional args + // like branch names must not be treated as subcommands. + assert!(!command_might_be_dangerous(&vec_str(&[ + "git", "checkout", "reset", + ]))); + } + + #[test] + fn git_push_force_is_dangerous() { + assert!(command_might_be_dangerous(&vec_str(&[ + "git", "push", "--force", "origin", "main", + ]))); + assert!(command_might_be_dangerous(&vec_str(&[ + "git", "push", "-f", "origin", "main", + ]))); + assert!(command_might_be_dangerous(&vec_str(&[ + "git", + "-C", + ".", + "push", + "--force-with-lease", + "origin", + "main", + ]))); + } + + #[test] + fn git_push_plus_refspec_is_dangerous() { + assert!(command_might_be_dangerous(&vec_str(&[ + "git", "push", "origin", "+main", + ]))); + assert!(command_might_be_dangerous(&vec_str(&[ + "git", + "push", + "origin", + "+refs/heads/main:refs/heads/main", + ]))); + } + + #[test] + fn git_push_delete_flag_is_dangerous() { + assert!(command_might_be_dangerous(&vec_str(&[ + "git", "push", "--delete", "origin", "feature", + ]))); + assert!(command_might_be_dangerous(&vec_str(&[ + "git", "push", "-d", "origin", "feature", + ]))); + } + + #[test] + fn git_push_delete_refspec_is_dangerous() { + assert!(command_might_be_dangerous(&vec_str(&[ + "git", "push", "origin", ":feature", + ]))); + assert!(command_might_be_dangerous(&vec_str(&[ + "bash", + "-lc", + "git push origin :feature", + ]))); + } + + #[test] + fn git_push_without_force_is_not_dangerous() { + assert!(!command_might_be_dangerous(&vec_str(&[ + "git", "push", "origin", "main", + ]))); + } + + #[test] + fn git_clean_force_is_dangerous_even_when_f_is_not_first_flag() { + assert!(command_might_be_dangerous(&vec_str(&[ + "git", "clean", "-fdx", + ]))); + assert!(command_might_be_dangerous(&vec_str(&[ + "git", "clean", "-xdf", + ]))); + assert!(command_might_be_dangerous(&vec_str(&[ + "git", "clean", "--force", ]))); } diff --git a/codex-rs/core/src/command_safety/is_safe_command.rs b/codex-rs/core/src/command_safety/is_safe_command.rs index 01a52026e2..e52079c74b 100644 --- a/codex-rs/core/src/command_safety/is_safe_command.rs +++ b/codex-rs/core/src/command_safety/is_safe_command.rs @@ -1,4 +1,8 @@ use crate::bash::parse_shell_lc_plain_commands; +// Find the first matching git subcommand, skipping known global options that +// may appear before it (e.g., `-C`, `-c`, `--git-dir`). +// Implemented in `is_dangerous_command` and shared here. +use crate::command_safety::is_dangerous_command::find_git_subcommand; use crate::command_safety::windows_safe_commands::is_safe_command_windows; pub fn is_known_safe_command(command: &[String]) -> bool { @@ -131,13 +135,36 @@ fn is_safe_to_call_with_exec(command: &[String]) -> bool { } // Git - Some("git") => matches!( - command.get(1).map(String::as_str), - Some("branch" | "status" | "log" | "diff" | "show") - ), + Some("git") => { + // Global config overrides like `-c core.pager=...` can force git + // to execute arbitrary external commands. With no sandboxing, we + // should always prompt in those cases. + if git_has_config_override_global_option(command) { + return false; + } - // Rust - Some("cargo") if command.get(1).map(String::as_str) == Some("check") => true, + let Some((subcommand_idx, subcommand)) = + find_git_subcommand(command, &["status", "log", "diff", "show", "branch"]) + else { + return false; + }; + + let subcommand_args = &command[subcommand_idx + 1..]; + + match subcommand { + "status" | "log" | "diff" | "show" => { + git_subcommand_args_are_read_only(subcommand_args) + } + "branch" => { + git_subcommand_args_are_read_only(subcommand_args) + && git_branch_is_read_only(subcommand_args) + } + other => { + debug_assert!(false, "unexpected git subcommand from matcher: {other}"); + false + } + } + } // Special-case `sed -n {N|M,N}p` Some("sed") @@ -155,6 +182,60 @@ fn is_safe_to_call_with_exec(command: &[String]) -> bool { } } +// Treat `git branch` as safe only when the arguments clearly indicate +// a read-only query, not a branch mutation (create/rename/delete). +fn git_branch_is_read_only(branch_args: &[String]) -> bool { + if branch_args.is_empty() { + // `git branch` with no additional args lists branches. + return true; + } + + let mut saw_read_only_flag = false; + for arg in branch_args.iter().map(String::as_str) { + match arg { + "--list" | "-l" | "--show-current" | "-a" | "--all" | "-r" | "--remotes" | "-v" + | "-vv" | "--verbose" => { + saw_read_only_flag = true; + } + _ if arg.starts_with("--format=") => { + saw_read_only_flag = true; + } + _ => { + // Any other flag or positional argument may create, rename, or delete branches. + return false; + } + } + } + + saw_read_only_flag +} + +fn git_has_config_override_global_option(command: &[String]) -> bool { + command.iter().map(String::as_str).any(|arg| { + matches!(arg, "-c" | "--config-env") + || (arg.starts_with("-c") && arg.len() > 2) + || arg.starts_with("--config-env=") + }) +} + +fn git_subcommand_args_are_read_only(args: &[String]) -> bool { + // Flags that can write to disk or execute external tools should never be + // auto-approved on an unsandboxed machine. + const UNSAFE_GIT_FLAGS: &[&str] = &[ + "--output", + "--ext-diff", + "--textconv", + "--exec", + "--paginate", + ]; + + !args.iter().map(String::as_str).any(|arg| { + UNSAFE_GIT_FLAGS.contains(&arg) + || arg.starts_with("--output=") + || arg.starts_with("--exec=") + }) +} + // (bash parsing helpers implemented in crate::bash) /* ---------------------------------------------------------- @@ -207,6 +288,12 @@ mod tests { fn known_safe_examples() { assert!(is_safe_to_call_with_exec(&vec_str(&["ls"]))); assert!(is_safe_to_call_with_exec(&vec_str(&["git", "status"]))); + assert!(is_safe_to_call_with_exec(&vec_str(&["git", "branch"]))); + assert!(is_safe_to_call_with_exec(&vec_str(&[ + "git", + "branch", + "--show-current" + ]))); assert!(is_safe_to_call_with_exec(&vec_str(&["base64"]))); assert!(is_safe_to_call_with_exec(&vec_str(&[ "sed", "-n", "1,5p", "file.txt" @@ -231,6 +318,86 @@ mod tests { } } + #[test] + fn git_branch_mutating_flags_are_not_safe() { + assert!(!is_known_safe_command(&vec_str(&[ + "git", "branch", "-d", "feature" + ]))); + assert!(!is_known_safe_command(&vec_str(&[ + "git", + "branch", + "new-branch" + ]))); + } + + #[test] + fn git_branch_global_options_respect_safety_rules() { + use pretty_assertions::assert_eq; + + assert_eq!( + is_known_safe_command(&vec_str(&["git", "-C", ".", "branch", "--show-current"])), + true + ); + assert_eq!( + is_known_safe_command(&vec_str(&["git", "-C", ".", "branch", "-d", "feature"])), + false + ); + assert_eq!( + is_known_safe_command(&vec_str(&["bash", "-lc", "git -C . branch -d feature",])), + false + ); + } + + #[test] + fn git_first_positional_is_the_subcommand() { + // In git, the first non-option token is the subcommand. Later positional + // args (like branch names) must not be treated as subcommands. + assert!(!is_known_safe_command(&vec_str(&[ + "git", "checkout", "status", + ]))); + } + + #[test] + fn git_output_and_config_override_flags_are_not_safe() { + assert!(!is_known_safe_command(&vec_str(&[ + "git", + "log", + "--output=/tmp/git-log-out-test", + "-n", + "1", + ]))); + assert!(!is_known_safe_command(&vec_str(&[ + "git", + "diff", + "--output", + "/tmp/git-diff-out-test", + ]))); + assert!(!is_known_safe_command(&vec_str(&[ + "git", + "show", + "--output=/tmp/git-show-out-test", + "HEAD", + ]))); + assert!(!is_known_safe_command(&vec_str(&[ + "git", + "-c", + "core.pager=cat", + "log", + "-n", + "1", + ]))); + assert!(!is_known_safe_command(&vec_str(&[ + "git", + "-ccore.pager=cat", + "status", + ]))); + } + + #[test] + fn cargo_check_is_not_safe() { + assert!(!is_known_safe_command(&vec_str(&["cargo", "check"]))); + } + #[test] fn zsh_lc_safe_command_sequence() { assert!(is_known_safe_command(&vec_str(&["zsh", "-lc", "ls"]))); diff --git a/codex-rs/core/src/exec_policy.rs b/codex-rs/core/src/exec_policy.rs index c0cb7f8fcf..a2e1e8d40d 100644 --- a/codex-rs/core/src/exec_policy.rs +++ b/codex-rs/core/src/exec_policy.rs @@ -1280,6 +1280,30 @@ prefix_rule( ); } + #[tokio::test] + async fn dangerous_git_push_requires_approval_in_danger_full_access() { + let command = vec_str(&["git", "push", "origin", "+main"]); + let manager = ExecPolicyManager::default(); + let requirement = manager + .create_exec_approval_requirement_for_command(ExecApprovalRequest { + features: &Features::with_defaults(), + command: &command, + approval_policy: AskForApproval::OnRequest, + sandbox_policy: &SandboxPolicy::DangerFullAccess, + sandbox_permissions: SandboxPermissions::UseDefault, + prefix_rule: None, + }) + .await; + + assert_eq!( + requirement, + ExecApprovalRequirement::NeedsApproval { + reason: None, + proposed_execpolicy_amendment: Some(ExecPolicyAmendment::new(command)), + } + ); + } + fn vec_str(items: &[&str]) -> Vec { items.iter().map(std::string::ToString::to_string).collect() } From 0f15ed43256e229d40bac944cbdcf0424fa75ae0 Mon Sep 17 00:00:00 2001 From: Eric Traut Date: Mon, 2 Feb 2026 13:13:14 -0800 Subject: [PATCH 09/28] Updated labeler workflow prompt to include "app" label (#10411) Support for desktop app issues --- .github/workflows/issue-labeler.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/issue-labeler.yml b/.github/workflows/issue-labeler.yml index da77812fec..8146b8c5bd 100644 --- a/.github/workflows/issue-labeler.yml +++ b/.github/workflows/issue-labeler.yml @@ -38,9 +38,10 @@ jobs: - If applicable, add one of the following labels to specify which sub-product or product surface the issue relates to. 1. CLI — the Codex command line interface. 2. extension — VS Code (or other IDE) extension-specific issues. - 3. codex-web — Issues targeting the Codex web UI/Cloud experience. - 4. github-action — Issues with the Codex GitHub action. - 5. iOS — Issues with the Codex iOS app. + 3. app - Issues related to the Codex desktop application. + 4. codex-web — Issues targeting the Codex web UI/Cloud experience. + 5. github-action — Issues with the Codex GitHub action. + 6. iOS — Issues with the Codex iOS app. - Additionally add zero or more of the following labels that are relevant to the issue content. Prefer a small set of precise labels over many broad ones. 1. windows-os — Bugs or friction specific to Windows environments (always when PowerShell is mentioned, path handling, copy/paste, OS-specific auth or tooling failures). From a5066bef7899a99279ed9172a8d31692d4146c0c Mon Sep 17 00:00:00 2001 From: iceweasel-oai Date: Mon, 2 Feb 2026 15:31:08 -0800 Subject: [PATCH 10/28] emit a separate metric when the user cancels UAT during elevated setup (#10399) Currently this shows up as elevated setup failure, which isn't quite accurate. --- codex-rs/core/src/windows_sandbox.rs | 19 +++++++++++++++++++ codex-rs/tui/src/app.rs | 4 +++- .../windows-sandbox-rs/src/setup_error.rs | 3 +++ .../src/setup_orchestrator.rs | 8 +++++++- 4 files changed, 32 insertions(+), 2 deletions(-) diff --git a/codex-rs/core/src/windows_sandbox.rs b/codex-rs/core/src/windows_sandbox.rs index 7b9451c246..9a34a9f90f 100644 --- a/codex-rs/core/src/windows_sandbox.rs +++ b/codex-rs/core/src/windows_sandbox.rs @@ -65,6 +65,25 @@ pub fn elevated_setup_failure_details(_err: &anyhow::Error) -> Option<(String, S None } +#[cfg(target_os = "windows")] +pub fn elevated_setup_failure_metric_name(err: &anyhow::Error) -> &'static str { + if codex_windows_sandbox::extract_setup_failure(err).is_some_and(|failure| { + matches!( + failure.code, + codex_windows_sandbox::SetupErrorCode::OrchestratorHelperLaunchCanceled + ) + }) { + "codex.windows_sandbox.elevated_setup_canceled" + } else { + "codex.windows_sandbox.elevated_setup_failure" + } +} + +#[cfg(not(target_os = "windows"))] +pub fn elevated_setup_failure_metric_name(_err: &anyhow::Error) -> &'static str { + panic!("elevated_setup_failure_metric_name is only supported on Windows") +} + #[cfg(target_os = "windows")] pub fn run_elevated_setup( policy: &SandboxPolicy, diff --git a/codex-rs/tui/src/app.rs b/codex-rs/tui/src/app.rs index 8dc2276b75..48ef0a772d 100644 --- a/codex-rs/tui/src/app.rs +++ b/codex-rs/tui/src/app.rs @@ -1700,7 +1700,9 @@ impl App { tags.push(("message", message)); } otel_manager.counter( - "codex.windows_sandbox.elevated_setup_failure", + codex_core::windows_sandbox::elevated_setup_failure_metric_name( + &err, + ), 1, &tags, ); diff --git a/codex-rs/windows-sandbox-rs/src/setup_error.rs b/codex-rs/windows-sandbox-rs/src/setup_error.rs index 21d260b4d4..9ee9ebf7f5 100644 --- a/codex-rs/windows-sandbox-rs/src/setup_error.rs +++ b/codex-rs/windows-sandbox-rs/src/setup_error.rs @@ -22,6 +22,8 @@ pub enum SetupErrorCode { OrchestratorPayloadSerializeFailed, /// Failed to launch the setup helper process (spawn or ShellExecuteExW). OrchestratorHelperLaunchFailed, + /// User canceled the UAC prompt while launching the helper. + OrchestratorHelperLaunchCanceled, /// Helper exited non-zero and no structured report was available. OrchestratorHelperExitNonzero, /// Helper exited non-zero and reading `setup_error.json` failed. @@ -72,6 +74,7 @@ impl SetupErrorCode { Self::OrchestratorElevationCheckFailed => "orchestrator_elevation_check_failed", Self::OrchestratorPayloadSerializeFailed => "orchestrator_payload_serialize_failed", Self::OrchestratorHelperLaunchFailed => "orchestrator_helper_launch_failed", + Self::OrchestratorHelperLaunchCanceled => "orchestrator_helper_launch_canceled", Self::OrchestratorHelperExitNonzero => "orchestrator_helper_exit_nonzero", Self::OrchestratorHelperReportReadFailed => "orchestrator_helper_report_read_failed", Self::HelperRequestArgsFailed => "helper_request_args_failed", diff --git a/codex-rs/windows-sandbox-rs/src/setup_orchestrator.rs b/codex-rs/windows-sandbox-rs/src/setup_orchestrator.rs index 74137379ec..78e6d566fb 100644 --- a/codex-rs/windows-sandbox-rs/src/setup_orchestrator.rs +++ b/codex-rs/windows-sandbox-rs/src/setup_orchestrator.rs @@ -34,6 +34,7 @@ use windows_sys::Win32::Security::SECURITY_NT_AUTHORITY; pub const SETUP_VERSION: u32 = 5; pub const OFFLINE_USERNAME: &str = "CodexSandboxOffline"; pub const ONLINE_USERNAME: &str = "CodexSandboxOnline"; +const ERROR_CANCELLED: u32 = 1223; const SECURITY_BUILTIN_DOMAIN_RID: u32 = 0x0000_0020; const DOMAIN_ALIAS_RID_ADMINS: u32 = 0x0000_0220; @@ -411,8 +412,13 @@ fn run_setup_exe( let ok = unsafe { ShellExecuteExW(&mut sei) }; if ok == 0 || sei.hProcess == 0 { let last_error = unsafe { GetLastError() }; + let code = if last_error == ERROR_CANCELLED { + SetupErrorCode::OrchestratorHelperLaunchCanceled + } else { + SetupErrorCode::OrchestratorHelperLaunchFailed + }; return Err(failure( - SetupErrorCode::OrchestratorHelperLaunchFailed, + code, format!("ShellExecuteExW failed to launch setup helper: {last_error}"), )); } From 98debeda8aa19336a063645b7414498ad56f17ee Mon Sep 17 00:00:00 2001 From: Dylan Hurd Date: Mon, 2 Feb 2026 15:35:37 -0800 Subject: [PATCH 11/28] chore(tui) /personalities tip (#10377) ## Summary We have /personality now. ## Testing - [x] tested locally --- codex-rs/tui/tooltips.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/codex-rs/tui/tooltips.txt b/codex-rs/tui/tooltips.txt index a8521f359f..e1e4d23712 100644 --- a/codex-rs/tui/tooltips.txt +++ b/codex-rs/tui/tooltips.txt @@ -9,6 +9,7 @@ Use /status to see the current model, approvals, and token usage. Use /fork to branch the current chat into a new thread. Use /init to create an AGENTS.md with project-specific guidance. Use /mcp to list configured MCP tools. +Use /personality to customize how Codex communicates. Use /rename to rename your threads for easier thread resuming. Use the OpenAI docs MCP for API questions; enable it with `codex mcp add openaiDeveloperDocs --url https://developers.openai.com/mcp`. Join the OpenAI community Discord: http://discord.gg/openai From fb2df99cf132c0980d1b54e1a8fb64a1f9ed8906 Mon Sep 17 00:00:00 2001 From: Celia Chen Date: Mon, 2 Feb 2026 16:06:44 -0800 Subject: [PATCH 12/28] [feat] persist thread_dynamic_tools in db (#10252) Persist thread_dynamic_tools in sqlite and read first from it. Fall back to rollout files if it's not found. Persist dynamic tools to both sqlite and rollout files. Saw that new sessions get populated to db correctly & old sessions get backfilled correctly at startup: ``` celia@com-92114 codex-rs % sqlite3 ~/.codex/state.sqlite \ "select thread_id, position,name,description,input_schema from thread_dynamic_tools;" 019c0cad-ec0d-74b2-a787-e8b33a349117|0|geo_lookup|lookup a city|{"properties":{"city":{"type":"string"}},"required":["city"],"type":"object"} .... 019c10ca-aa4b-7620-ae40-c0919fbd7ea7|0|geo_lookup|lookup a city|{"properties":{"city":{"type":"string"}},"required":["city"],"type":"object"} ``` --- codex-rs/app-server-test-client/src/main.rs | 117 ++++++++++++++++-- codex-rs/core/src/codex.rs | 31 ++++- codex-rs/core/src/rollout/metadata.rs | 23 ++++ codex-rs/core/src/state_db.rs | 47 +++++++ codex-rs/core/tests/suite/sqlite_state.rs | 37 +++++- .../migrations/0004_thread_dynamic_tools.sql | 11 ++ codex-rs/state/src/runtime.rs | 109 ++++++++++++++++ 7 files changed, 359 insertions(+), 16 deletions(-) create mode 100644 codex-rs/state/migrations/0004_thread_dynamic_tools.sql diff --git a/codex-rs/app-server-test-client/src/main.rs b/codex-rs/app-server-test-client/src/main.rs index d949c3a734..6d3fc06d81 100644 --- a/codex-rs/app-server-test-client/src/main.rs +++ b/codex-rs/app-server-test-client/src/main.rs @@ -1,7 +1,9 @@ use std::collections::VecDeque; +use std::fs; use std::io::BufRead; use std::io::BufReader; use std::io::Write; +use std::path::Path; use std::process::Child; use std::process::ChildStdin; use std::process::ChildStdout; @@ -24,6 +26,7 @@ use codex_app_server_protocol::ClientRequest; use codex_app_server_protocol::CommandExecutionApprovalDecision; use codex_app_server_protocol::CommandExecutionRequestApprovalParams; use codex_app_server_protocol::CommandExecutionRequestApprovalResponse; +use codex_app_server_protocol::DynamicToolSpec; use codex_app_server_protocol::FileChangeApprovalDecision; use codex_app_server_protocol::FileChangeRequestApprovalParams; use codex_app_server_protocol::FileChangeRequestApprovalResponse; @@ -83,6 +86,15 @@ struct Cli { )] config_overrides: Vec, + /// JSON array of dynamic tool specs or a single tool object. + /// Prefix a filename with '@' to read from a file. + /// + /// Example: + /// --dynamic-tools '[{"name":"demo","description":"Demo","inputSchema":{"type":"object"}}]' + /// --dynamic-tools @/path/to/tools.json + #[arg(long, value_name = "json-or-@file", global = true)] + dynamic_tools: Option, + #[command(subcommand)] command: CliCommand, } @@ -140,23 +152,29 @@ fn main() -> Result<()> { let Cli { codex_bin, config_overrides, + dynamic_tools, command, } = Cli::parse(); + let dynamic_tools = parse_dynamic_tools_arg(&dynamic_tools)?; + match command { CliCommand::SendMessage { user_message } => { + ensure_dynamic_tools_unused(&dynamic_tools, "send-message")?; send_message(&codex_bin, &config_overrides, user_message) } CliCommand::SendMessageV2 { user_message } => { - send_message_v2(&codex_bin, &config_overrides, user_message) + send_message_v2(&codex_bin, &config_overrides, user_message, &dynamic_tools) } CliCommand::TriggerCmdApproval { user_message } => { - trigger_cmd_approval(&codex_bin, &config_overrides, user_message) + trigger_cmd_approval(&codex_bin, &config_overrides, user_message, &dynamic_tools) } CliCommand::TriggerPatchApproval { user_message } => { - trigger_patch_approval(&codex_bin, &config_overrides, user_message) + trigger_patch_approval(&codex_bin, &config_overrides, user_message, &dynamic_tools) + } + CliCommand::NoTriggerCmdApproval => { + no_trigger_cmd_approval(&codex_bin, &config_overrides, &dynamic_tools) } - CliCommand::NoTriggerCmdApproval => no_trigger_cmd_approval(&codex_bin, &config_overrides), CliCommand::SendFollowUpV2 { first_message, follow_up_message, @@ -165,10 +183,20 @@ fn main() -> Result<()> { &config_overrides, first_message, follow_up_message, + &dynamic_tools, ), - CliCommand::TestLogin => test_login(&codex_bin, &config_overrides), - CliCommand::GetAccountRateLimits => get_account_rate_limits(&codex_bin, &config_overrides), - CliCommand::ModelList => model_list(&codex_bin, &config_overrides), + CliCommand::TestLogin => { + ensure_dynamic_tools_unused(&dynamic_tools, "test-login")?; + test_login(&codex_bin, &config_overrides) + } + CliCommand::GetAccountRateLimits => { + ensure_dynamic_tools_unused(&dynamic_tools, "get-account-rate-limits")?; + get_account_rate_limits(&codex_bin, &config_overrides) + } + CliCommand::ModelList => { + ensure_dynamic_tools_unused(&dynamic_tools, "model-list")?; + model_list(&codex_bin, &config_overrides) + } } } @@ -198,14 +226,23 @@ fn send_message_v2( codex_bin: &str, config_overrides: &[String], user_message: String, + dynamic_tools: &Option>, ) -> Result<()> { - send_message_v2_with_policies(codex_bin, config_overrides, user_message, None, None) + send_message_v2_with_policies( + codex_bin, + config_overrides, + user_message, + None, + None, + dynamic_tools, + ) } fn trigger_cmd_approval( codex_bin: &str, config_overrides: &[String], user_message: Option, + dynamic_tools: &Option>, ) -> Result<()> { let default_prompt = "Run `touch /tmp/should-trigger-approval` so I can confirm the file exists."; @@ -216,6 +253,7 @@ fn trigger_cmd_approval( message, Some(AskForApproval::OnRequest), Some(SandboxPolicy::ReadOnly), + dynamic_tools, ) } @@ -223,6 +261,7 @@ fn trigger_patch_approval( codex_bin: &str, config_overrides: &[String], user_message: Option, + dynamic_tools: &Option>, ) -> Result<()> { let default_prompt = "Create a file named APPROVAL_DEMO.txt containing a short hello message using apply_patch."; @@ -233,12 +272,24 @@ fn trigger_patch_approval( message, Some(AskForApproval::OnRequest), Some(SandboxPolicy::ReadOnly), + dynamic_tools, ) } -fn no_trigger_cmd_approval(codex_bin: &str, config_overrides: &[String]) -> Result<()> { +fn no_trigger_cmd_approval( + codex_bin: &str, + config_overrides: &[String], + dynamic_tools: &Option>, +) -> Result<()> { let prompt = "Run `touch should_not_trigger_approval.txt`"; - send_message_v2_with_policies(codex_bin, config_overrides, prompt.to_string(), None, None) + send_message_v2_with_policies( + codex_bin, + config_overrides, + prompt.to_string(), + None, + None, + dynamic_tools, + ) } fn send_message_v2_with_policies( @@ -247,13 +298,17 @@ fn send_message_v2_with_policies( user_message: String, approval_policy: Option, sandbox_policy: Option, + dynamic_tools: &Option>, ) -> Result<()> { let mut client = CodexClient::spawn(codex_bin, config_overrides)?; let initialize = client.initialize()?; println!("< initialize response: {initialize:?}"); - let thread_response = client.thread_start(ThreadStartParams::default())?; + let thread_response = client.thread_start(ThreadStartParams { + dynamic_tools: dynamic_tools.clone(), + ..Default::default() + })?; println!("< thread/start response: {thread_response:?}"); let mut turn_params = TurnStartParams { thread_id: thread_response.thread.id.clone(), @@ -280,13 +335,17 @@ fn send_follow_up_v2( config_overrides: &[String], first_message: String, follow_up_message: String, + dynamic_tools: &Option>, ) -> Result<()> { let mut client = CodexClient::spawn(codex_bin, config_overrides)?; let initialize = client.initialize()?; println!("< initialize response: {initialize:?}"); - let thread_response = client.thread_start(ThreadStartParams::default())?; + let thread_response = client.thread_start(ThreadStartParams { + dynamic_tools: dynamic_tools.clone(), + ..Default::default() + })?; println!("< thread/start response: {thread_response:?}"); let first_turn_params = TurnStartParams { @@ -372,6 +431,40 @@ fn model_list(codex_bin: &str, config_overrides: &[String]) -> Result<()> { Ok(()) } +fn ensure_dynamic_tools_unused( + dynamic_tools: &Option>, + command: &str, +) -> Result<()> { + if dynamic_tools.is_some() { + bail!( + "dynamic tools are only supported for v2 thread/start; remove --dynamic-tools for {command} or use send-message-v2" + ); + } + Ok(()) +} + +fn parse_dynamic_tools_arg(dynamic_tools: &Option) -> Result>> { + let Some(raw_arg) = dynamic_tools.as_deref() else { + return Ok(None); + }; + + let raw_json = if let Some(path) = raw_arg.strip_prefix('@') { + fs::read_to_string(Path::new(path)) + .with_context(|| format!("read dynamic tools file {path}"))? + } else { + raw_arg.to_string() + }; + + let value: Value = serde_json::from_str(&raw_json).context("parse dynamic tools JSON")?; + let tools = match value { + Value::Array(_) => serde_json::from_value(value).context("decode dynamic tools array")?, + Value::Object(_) => vec![serde_json::from_value(value).context("decode dynamic tool")?], + _ => bail!("dynamic tools JSON must be an object or array"), + }; + + Ok(Some(tools)) +} + struct CodexClient { child: Child, stdin: Option, diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index ba5ee02d69..b4e3859500 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -333,9 +333,36 @@ impl Codex { .clone() .or_else(|| conversation_history.get_base_instructions().map(|s| s.text)) .unwrap_or_else(|| model_info.get_model_instructions(config.personality)); - // Respect explicit thread-start tools; fall back to persisted tools when resuming a thread. + + // Respect thread-start tools. When missing (resumed/forked threads), read from the db + // first, then fall back to rollout-file tools. + let persisted_tools = if dynamic_tools.is_empty() + && config.features.enabled(Feature::Sqlite) + { + let thread_id = match &conversation_history { + InitialHistory::Resumed(resumed) => Some(resumed.conversation_id), + InitialHistory::Forked(_) => conversation_history.forked_from_id(), + InitialHistory::New => None, + }; + match thread_id { + Some(thread_id) => { + let state_db_ctx = state_db::open_if_present( + config.codex_home.as_path(), + config.model_provider_id.as_str(), + ) + .await; + state_db::get_dynamic_tools(state_db_ctx.as_deref(), thread_id, "codex_spawn") + .await + } + None => None, + } + } else { + None + }; let dynamic_tools = if dynamic_tools.is_empty() { - conversation_history.get_dynamic_tools().unwrap_or_default() + persisted_tools + .or_else(|| conversation_history.get_dynamic_tools()) + .unwrap_or_default() } else { dynamic_tools }; diff --git a/codex-rs/core/src/rollout/metadata.rs b/codex-rs/core/src/rollout/metadata.rs index 2d59d71af3..42e52f78d6 100644 --- a/codex-rs/core/src/rollout/metadata.rs +++ b/codex-rs/core/src/rollout/metadata.rs @@ -187,6 +187,29 @@ pub(crate) async fn backfill_sessions( warn!("failed to upsert rollout {}: {err}", path.display()); } else { stats.upserted = stats.upserted.saturating_add(1); + if let Ok(meta_line) = rollout::list::read_session_meta_line(&path).await { + if let Err(err) = runtime + .persist_dynamic_tools( + meta_line.meta.id, + meta_line.meta.dynamic_tools.as_deref(), + ) + .await + { + if let Some(otel) = otel { + otel.counter( + DB_ERROR_METRIC, + 1, + &[("stage", "backfill_dynamic_tools")], + ); + } + warn!("failed to backfill dynamic tools {}: {err}", path.display()); + } + } else { + warn!( + "failed to read session meta for dynamic tools {}", + path.display() + ); + } } } Err(err) => { diff --git a/codex-rs/core/src/state_db.rs b/codex-rs/core/src/state_db.rs index a52f46593a..ff95ed946f 100644 --- a/codex-rs/core/src/state_db.rs +++ b/codex-rs/core/src/state_db.rs @@ -9,6 +9,7 @@ use chrono::Timelike; use chrono::Utc; use codex_otel::OtelManager; use codex_protocol::ThreadId; +use codex_protocol::dynamic_tools::DynamicToolSpec; use codex_protocol::protocol::RolloutItem; use codex_protocol::protocol::SessionSource; use codex_state::DB_METRIC_COMPARE_ERROR; @@ -196,6 +197,37 @@ pub async fn find_rollout_path_by_id( }) } +/// Get dynamic tools for a thread id using SQLite. +pub async fn get_dynamic_tools( + context: Option<&codex_state::StateRuntime>, + thread_id: ThreadId, + stage: &str, +) -> Option> { + let ctx = context?; + match ctx.get_dynamic_tools(thread_id).await { + Ok(tools) => tools, + Err(err) => { + warn!("state db get_dynamic_tools failed during {stage}: {err}"); + None + } + } +} + +/// Persist dynamic tools for a thread id using SQLite, if none exist yet. +pub async fn persist_dynamic_tools( + context: Option<&codex_state::StateRuntime>, + thread_id: ThreadId, + tools: Option<&[DynamicToolSpec]>, + stage: &str, +) { + let Some(ctx) = context else { + return; + }; + if let Err(err) = ctx.persist_dynamic_tools(thread_id, tools).await { + warn!("state db persist_dynamic_tools failed during {stage}: {err}"); + } +} + /// Reconcile rollout items into SQLite, falling back to scanning the rollout file. pub async fn reconcile_rollout( context: Option<&codex_state::StateRuntime>, @@ -235,6 +267,21 @@ pub async fn reconcile_rollout( "state db reconcile_rollout upsert failed {}: {err}", rollout_path.display() ); + return; + } + if let Ok(meta_line) = crate::rollout::list::read_session_meta_line(rollout_path).await { + persist_dynamic_tools( + Some(ctx), + meta_line.meta.id, + meta_line.meta.dynamic_tools.as_deref(), + "reconcile_rollout", + ) + .await; + } else { + warn!( + "state db reconcile_rollout missing session meta {}", + rollout_path.display() + ); } } diff --git a/codex-rs/core/tests/suite/sqlite_state.rs b/codex-rs/core/tests/suite/sqlite_state.rs index 2582f90cbe..218da34825 100644 --- a/codex-rs/core/tests/suite/sqlite_state.rs +++ b/codex-rs/core/tests/suite/sqlite_state.rs @@ -1,6 +1,7 @@ use anyhow::Result; use codex_core::features::Feature; use codex_protocol::ThreadId; +use codex_protocol::dynamic_tools::DynamicToolSpec; use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::RolloutItem; use codex_protocol::protocol::RolloutLine; @@ -74,6 +75,28 @@ async fn backfill_scans_existing_rollouts() -> Result<()> { let rollout_rel_path = format!("sessions/2026/01/27/rollout-2026-01-27T12-00-00-{uuid}.jsonl"); let rollout_rel_path_for_hook = rollout_rel_path.clone(); + let dynamic_tools = vec![ + DynamicToolSpec { + name: "geo_lookup".to_string(), + description: "lookup a city".to_string(), + input_schema: json!({ + "type": "object", + "required": ["city"], + "properties": { "city": { "type": "string" } } + }), + }, + DynamicToolSpec { + name: "weather_lookup".to_string(), + description: "lookup weather".to_string(), + input_schema: json!({ + "type": "object", + "required": ["zip"], + "properties": { "zip": { "type": "string" } } + }), + }, + ]; + let dynamic_tools_for_hook = dynamic_tools.clone(); + let mut builder = test_codex() .with_pre_build_hook(move |codex_home| { let rollout_path = codex_home.join(&rollout_rel_path_for_hook); @@ -81,7 +104,6 @@ async fn backfill_scans_existing_rollouts() -> Result<()> { .parent() .expect("rollout path should have parent"); fs::create_dir_all(parent).expect("should create rollout directory"); - let session_meta_line = SessionMetaLine { meta: SessionMeta { id: thread_id, @@ -93,7 +115,7 @@ async fn backfill_scans_existing_rollouts() -> Result<()> { source: SessionSource::default(), model_provider: None, base_instructions: None, - dynamic_tools: None, + dynamic_tools: Some(dynamic_tools_for_hook), }, git: None, }; @@ -155,6 +177,17 @@ async fn backfill_scans_existing_rollouts() -> Result<()> { assert_eq!(metadata.model_provider, default_provider); assert!(metadata.has_user_event); + let mut stored_tools = None; + for _ in 0..40 { + stored_tools = db.get_dynamic_tools(thread_id).await?; + if stored_tools.is_some() { + break; + } + tokio::time::sleep(Duration::from_millis(25)).await; + } + let stored_tools = stored_tools.expect("dynamic tools should be stored"); + assert_eq!(stored_tools, dynamic_tools); + Ok(()) } diff --git a/codex-rs/state/migrations/0004_thread_dynamic_tools.sql b/codex-rs/state/migrations/0004_thread_dynamic_tools.sql new file mode 100644 index 0000000000..0f40b5f800 --- /dev/null +++ b/codex-rs/state/migrations/0004_thread_dynamic_tools.sql @@ -0,0 +1,11 @@ +CREATE TABLE thread_dynamic_tools ( + thread_id TEXT NOT NULL, + position INTEGER NOT NULL, + name TEXT NOT NULL, + description TEXT NOT NULL, + input_schema TEXT NOT NULL, + PRIMARY KEY(thread_id, position), + FOREIGN KEY(thread_id) REFERENCES threads(id) ON DELETE CASCADE +); + +CREATE INDEX idx_thread_dynamic_tools_thread ON thread_dynamic_tools(thread_id); diff --git a/codex-rs/state/src/runtime.rs b/codex-rs/state/src/runtime.rs index 6f2e128967..3b37b6d424 100644 --- a/codex-rs/state/src/runtime.rs +++ b/codex-rs/state/src/runtime.rs @@ -16,8 +16,10 @@ use chrono::DateTime; use chrono::Utc; use codex_otel::OtelManager; use codex_protocol::ThreadId; +use codex_protocol::dynamic_tools::DynamicToolSpec; use codex_protocol::protocol::RolloutItem; use log::LevelFilter; +use serde_json::Value; use sqlx::ConnectOptions; use sqlx::QueryBuilder; use sqlx::Row; @@ -117,6 +119,38 @@ WHERE id = ? .transpose() } + /// Get dynamic tools for a thread, if present. + pub async fn get_dynamic_tools( + &self, + thread_id: ThreadId, + ) -> anyhow::Result>> { + let rows = sqlx::query( + r#" +SELECT name, description, input_schema +FROM thread_dynamic_tools +WHERE thread_id = ? +ORDER BY position ASC + "#, + ) + .bind(thread_id.to_string()) + .fetch_all(self.pool.as_ref()) + .await?; + if rows.is_empty() { + return Ok(None); + } + let mut tools = Vec::with_capacity(rows.len()); + for row in rows { + let input_schema: String = row.try_get("input_schema")?; + let input_schema = serde_json::from_str::(input_schema.as_str())?; + tools.push(DynamicToolSpec { + name: row.try_get("name")?, + description: row.try_get("description")?, + input_schema, + }); + } + Ok(Some(tools)) + } + /// Find a rollout path by thread id using the underlying database. pub async fn find_rollout_path_by_id( &self, @@ -369,6 +403,58 @@ ON CONFLICT(id) DO UPDATE SET Ok(()) } + /// Persist dynamic tools for a thread if none have been stored yet. + /// + /// Dynamic tools are defined at thread start and should not change afterward. + /// This only writes the first time we see tools for a given thread. + pub async fn persist_dynamic_tools( + &self, + thread_id: ThreadId, + tools: Option<&[DynamicToolSpec]>, + ) -> anyhow::Result<()> { + let Some(tools) = tools else { + return Ok(()); + }; + if tools.is_empty() { + return Ok(()); + } + let mut tx = self.pool.begin().await?; + let thread_id = thread_id.to_string(); + let existing: Option = + sqlx::query_scalar("SELECT 1 FROM thread_dynamic_tools WHERE thread_id = ? LIMIT 1") + .bind(thread_id.as_str()) + .fetch_optional(&mut *tx) + .await?; + if existing.is_some() { + tx.commit().await?; + return Ok(()); + } + for (idx, tool) in tools.iter().enumerate() { + let position = i64::try_from(idx).unwrap_or(i64::MAX); + let input_schema = serde_json::to_string(&tool.input_schema)?; + sqlx::query( + r#" +INSERT INTO thread_dynamic_tools ( + thread_id, + position, + name, + description, + input_schema +) VALUES (?, ?, ?, ?, ?) + "#, + ) + .bind(thread_id.as_str()) + .bind(position) + .bind(tool.name.as_str()) + .bind(tool.description.as_str()) + .bind(input_schema) + .execute(&mut *tx) + .await?; + } + tx.commit().await?; + Ok(()) + } + /// Apply rollout items incrementally using the underlying database. pub async fn apply_rollout_items( &self, @@ -390,12 +476,25 @@ ON CONFLICT(id) DO UPDATE SET if let Some(updated_at) = file_modified_time_utc(builder.rollout_path.as_path()).await { metadata.updated_at = updated_at; } + // Keep the thread upsert before dynamic tools to satisfy the foreign key constraint: + // thread_dynamic_tools.thread_id -> threads.id. if let Err(err) = self.upsert_thread(&metadata).await { if let Some(otel) = otel { otel.counter(DB_ERROR_METRIC, 1, &[("stage", "apply_rollout_items")]); } return Err(err); } + let dynamic_tools = extract_dynamic_tools(items); + if let Some(dynamic_tools) = dynamic_tools + && let Err(err) = self + .persist_dynamic_tools(builder.id, dynamic_tools.as_deref()) + .await + { + if let Some(otel) = otel { + otel.counter(DB_ERROR_METRIC, 1, &[("stage", "persist_dynamic_tools")]); + } + return Err(err); + } Ok(()) } @@ -507,6 +606,16 @@ fn push_like_filters<'a>( builder.push(")"); } +fn extract_dynamic_tools(items: &[RolloutItem]) -> Option>> { + items.iter().find_map(|item| match item { + RolloutItem::SessionMeta(meta_line) => Some(meta_line.meta.dynamic_tools.clone()), + RolloutItem::ResponseItem(_) + | RolloutItem::Compacted(_) + | RolloutItem::TurnContext(_) + | RolloutItem::EventMsg(_) => None, + }) +} + async fn open_sqlite(path: &Path) -> anyhow::Result { let options = SqliteConnectOptions::new() .filename(path) From e24058b7a872054e3e76f9301d64eccccb75811d Mon Sep 17 00:00:00 2001 From: Gav Verma Date: Mon, 2 Feb 2026 16:49:23 -0800 Subject: [PATCH 13/28] feat: Read personal skills from .agents/skills (#10437) - Issue: https://github.com/agentskills/agentskills/issues/15 - Follow-up to https://github.com/openai/codex/pull/10317 (for team/repo skills) - This change now also loads personal/user skills from `$HOME/.agents/skills` (or `~/.agents/skills`) in addition to loading from `.agents/skills` inside of git repos. - The location of `.system` skills remains unchanged. - Keeping backwards compatibility with `~/.codex/skills` for now until we fully deprecate. With skills in both personal folders: image We load from both places: image --- codex-rs/Cargo.lock | 1 + codex-rs/core/Cargo.toml | 1 + codex-rs/core/src/skills/loader.rs | 92 +++++++++++++++++++++++++++--- 3 files changed, 86 insertions(+), 8 deletions(-) diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index 57186f82f1..716bc4cd5a 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -1422,6 +1422,7 @@ dependencies = [ "core-foundation 0.9.4", "core_test_support", "ctor 0.6.3", + "dirs", "dunce", "encoding_rs", "env-flags", diff --git a/codex-rs/core/Cargo.toml b/codex-rs/core/Cargo.toml index a857da8b3a..a95981cca2 100644 --- a/codex-rs/core/Cargo.toml +++ b/codex-rs/core/Cargo.toml @@ -45,6 +45,7 @@ codex-utils-pty = { workspace = true } codex-utils-readiness = { workspace = true } codex-utils-string = { workspace = true } codex-windows-sandbox = { package = "codex-windows-sandbox", path = "../windows-sandbox-rs" } +dirs = { workspace = true } dunce = { workspace = true } encoding_rs = { workspace = true } env-flags = { workspace = true } diff --git a/codex-rs/core/src/skills/loader.rs b/codex-rs/core/src/skills/loader.rs index a3c7731a6e..a04f3089b0 100644 --- a/codex-rs/core/src/skills/loader.rs +++ b/codex-rs/core/src/skills/loader.rs @@ -13,6 +13,7 @@ use crate::skills::model::SkillToolDependency; use crate::skills::system::system_cache_root_dir; use codex_app_server_protocol::ConfigLayerSource; use codex_protocol::protocol::SkillScope; +use dirs::home_dir; use dunce::canonicalize as canonicalize_path; use serde::Deserialize; use std::collections::HashSet; @@ -164,7 +165,10 @@ where outcome } -fn skill_roots_from_layer_stack_inner(config_layer_stack: &ConfigLayerStack) -> Vec { +fn skill_roots_from_layer_stack_inner( + config_layer_stack: &ConfigLayerStack, + home_dir: Option<&Path>, +) -> Vec { let mut roots = Vec::new(); for layer in @@ -182,12 +186,21 @@ fn skill_roots_from_layer_stack_inner(config_layer_stack: &ConfigLayerStack) -> }); } ConfigLayerSource::User { .. } => { - // `$CODEX_HOME/skills` (user-installed skills). + // Deprecated user skills location (`$CODEX_HOME/skills`), kept for backward + // compatibility. roots.push(SkillRoot { path: config_folder.as_path().join(SKILLS_DIR_NAME), scope: SkillScope::User, }); + // `$HOME/.agents/skills` (user-installed skills). + if let Some(home_dir) = home_dir { + roots.push(SkillRoot { + path: home_dir.join(AGENTS_DIR_NAME).join(SKILLS_DIR_NAME), + scope: SkillScope::User, + }); + } + // Embedded system skills are cached under `$CODEX_HOME/skills/.system` and are a // special case (not a config layer). roots.push(SkillRoot { @@ -220,15 +233,16 @@ fn skill_roots(config: &Config) -> Vec { #[cfg(test)] pub(crate) fn skill_roots_from_layer_stack( config_layer_stack: &ConfigLayerStack, + home_dir: Option<&Path>, ) -> Vec { - skill_roots_from_layer_stack_inner(config_layer_stack) + skill_roots_from_layer_stack_inner(config_layer_stack, home_dir) } pub(crate) fn skill_roots_from_layer_stack_with_agents( config_layer_stack: &ConfigLayerStack, cwd: &Path, ) -> Vec { - let mut roots = skill_roots_from_layer_stack_inner(config_layer_stack); + let mut roots = skill_roots_from_layer_stack_inner(config_layer_stack, home_dir().as_deref()); roots.extend(repo_agents_skill_roots(config_layer_stack, cwd)); dedupe_skill_roots_by_path(&mut roots); roots @@ -829,7 +843,8 @@ mod tests { let tmp = tempfile::tempdir()?; let system_folder = tmp.path().join("etc/codex"); - let user_folder = tmp.path().join("home/codex"); + let home_folder = tmp.path().join("home"); + let user_folder = home_folder.join("codex"); fs::create_dir_all(&system_folder)?; fs::create_dir_all(&user_folder)?; @@ -853,7 +868,7 @@ mod tests { ConfigRequirementsToml::default(), )?; - let got = skill_roots_from_layer_stack(&stack) + let got = skill_roots_from_layer_stack(&stack, Some(&home_folder)) .into_iter() .map(|root| (root.scope, root.path)) .collect::>(); @@ -862,6 +877,10 @@ mod tests { got, vec![ (SkillScope::User, user_folder.join("skills")), + ( + SkillScope::User, + home_folder.join(AGENTS_DIR_NAME).join(SKILLS_DIR_NAME) + ), ( SkillScope::System, user_folder.join("skills").join(".system") @@ -877,7 +896,8 @@ mod tests { fn skill_roots_from_layer_stack_includes_disabled_project_layers() -> anyhow::Result<()> { let tmp = tempfile::tempdir()?; - let user_folder = tmp.path().join("home/codex"); + let home_folder = tmp.path().join("home"); + let user_folder = home_folder.join("codex"); fs::create_dir_all(&user_folder)?; let project_root = tmp.path().join("repo"); @@ -906,7 +926,7 @@ mod tests { ConfigRequirementsToml::default(), )?; - let got = skill_roots_from_layer_stack(&stack) + let got = skill_roots_from_layer_stack(&stack, Some(&home_folder)) .into_iter() .map(|root| (root.scope, root.path)) .collect::>(); @@ -916,6 +936,10 @@ mod tests { vec![ (SkillScope::Repo, dot_codex.join("skills")), (SkillScope::User, user_folder.join("skills")), + ( + SkillScope::User, + home_folder.join(AGENTS_DIR_NAME).join(SKILLS_DIR_NAME) + ), ( SkillScope::System, user_folder.join("skills").join(".system") @@ -926,6 +950,55 @@ mod tests { Ok(()) } + #[test] + fn loads_skills_from_home_agents_dir_for_user_scope() -> anyhow::Result<()> { + let tmp = tempfile::tempdir()?; + + let home_folder = tmp.path().join("home"); + let user_folder = home_folder.join("codex"); + fs::create_dir_all(&user_folder)?; + + let user_file = AbsolutePathBuf::from_absolute_path(user_folder.join("config.toml"))?; + let layers = vec![ConfigLayerEntry::new( + ConfigLayerSource::User { file: user_file }, + TomlValue::Table(toml::map::Map::new()), + )]; + let stack = ConfigLayerStack::new( + layers, + ConfigRequirements::default(), + ConfigRequirementsToml::default(), + )?; + + let skill_path = write_skill_at( + &home_folder.join(AGENTS_DIR_NAME).join(SKILLS_DIR_NAME), + "agents-home", + "agents-home-skill", + "from home agents", + ); + + let outcome = + load_skills_from_roots(skill_roots_from_layer_stack(&stack, Some(&home_folder))); + assert!( + outcome.errors.is_empty(), + "unexpected errors: {:?}", + outcome.errors + ); + assert_eq!( + outcome.skills, + vec![SkillMetadata { + name: "agents-home-skill".to_string(), + description: "from home agents".to_string(), + short_description: None, + interface: None, + dependencies: None, + path: normalized(&skill_path), + scope: SkillScope::User, + }] + ); + + Ok(()) + } + fn write_skill(codex_home: &TempDir, dir: &str, name: &str, description: &str) -> PathBuf { write_skill_at(&codex_home.path().join("skills"), dir, name, description) } @@ -2136,6 +2209,9 @@ interface: .map(|root| root.scope) .collect(); let mut expected = vec![SkillScope::User, SkillScope::System]; + if home_dir().is_some() { + expected.insert(1, SkillScope::User); + } if cfg!(unix) { expected.push(SkillScope::Admin); } From 019d89ff86587196341edc30c9bb6205f82537ce Mon Sep 17 00:00:00 2001 From: pash-openai Date: Mon, 2 Feb 2026 16:57:29 -0800 Subject: [PATCH 14/28] make codex better at git (#10145) adds basic git context to the session prefix so the model can anchor git actions and be a bit more version-aware. structured it in a multiroot-friendly shape even though we only have one root today --- codex-rs/core/src/client.rs | 66 +++++- codex-rs/core/src/codex.rs | 8 +- codex-rs/core/src/compact.rs | 4 +- codex-rs/core/src/environment_context.rs | 1 + codex-rs/core/src/git_info.rs | 76 ++++++- codex-rs/core/src/lib.rs | 2 + codex-rs/core/src/turn_metadata.rs | 43 ++++ .../core/tests/chat_completions_payload.rs | 2 +- codex-rs/core/tests/chat_completions_sse.rs | 2 +- codex-rs/core/tests/responses_headers.rs | 201 +++++++++++++++++- codex-rs/core/tests/suite/client.rs | 2 +- .../core/tests/suite/client_websockets.rs | 10 +- 12 files changed, 394 insertions(+), 23 deletions(-) create mode 100644 codex-rs/core/src/turn_metadata.rs diff --git a/codex-rs/core/src/client.rs b/codex-rs/core/src/client.rs index 6564e86c49..37fef163af 100644 --- a/codex-rs/core/src/client.rs +++ b/codex-rs/core/src/client.rs @@ -1,10 +1,13 @@ +use std::path::PathBuf; use std::sync::Arc; use std::sync::OnceLock; +use std::sync::RwLock; use crate::api_bridge::CoreAuthProvider; use crate::api_bridge::auth_provider_from_auth; use crate::api_bridge::map_api_error; use crate::auth::UnauthorizedRecovery; +use crate::turn_metadata::build_turn_metadata_header; use codex_api::AggregateStreamExt; use codex_api::ChatClient as ApiChatClient; use codex_api::CompactClient as ApiCompactClient; @@ -72,6 +75,13 @@ use crate::transport_manager::TransportManager; pub const WEB_SEARCH_ELIGIBLE_HEADER: &str = "x-oai-web-search-eligible"; pub const X_CODEX_TURN_STATE_HEADER: &str = "x-codex-turn-state"; +pub const X_CODEX_TURN_METADATA_HEADER: &str = "x-codex-turn-metadata"; + +#[derive(Debug, Default)] +struct TurnMetadataCache { + cwd: Option, + header: Option, +} #[derive(Debug)] struct ModelClientState { @@ -85,6 +95,7 @@ struct ModelClientState { summary: ReasoningSummaryConfig, session_source: SessionSource, transport_manager: TransportManager, + turn_metadata_cache: Arc>, } #[derive(Debug, Clone)] @@ -136,11 +147,13 @@ impl ModelClient { summary, session_source, transport_manager, + turn_metadata_cache: Arc::new(RwLock::new(TurnMetadataCache::default())), }), } } - pub fn new_session(&self) -> ModelClientSession { + pub fn new_session(&self, turn_metadata_cwd: Option) -> ModelClientSession { + self.prewarm_turn_metadata_header(turn_metadata_cwd); ModelClientSession { state: Arc::clone(&self.state), connection: None, @@ -149,6 +162,38 @@ impl ModelClient { turn_state: Arc::new(OnceLock::new()), } } + + /// Refresh turn metadata in the background and update a cached header that request + /// builders can read without blocking. + fn prewarm_turn_metadata_header(&self, turn_metadata_cwd: Option) { + let turn_metadata_cwd = + turn_metadata_cwd.map(|cwd| std::fs::canonicalize(&cwd).unwrap_or(cwd)); + + if let Ok(mut cache) = self.state.turn_metadata_cache.write() + && cache.cwd != turn_metadata_cwd + { + cache.cwd = turn_metadata_cwd.clone(); + cache.header = None; + } + + let Some(cwd) = turn_metadata_cwd else { + return; + }; + let turn_metadata_cache = Arc::clone(&self.state.turn_metadata_cache); + if let Ok(handle) = tokio::runtime::Handle::try_current() { + let _task = handle.spawn(async move { + let header = build_turn_metadata_header(cwd.as_path()) + .await + .and_then(|value| HeaderValue::from_str(value.as_str()).ok()); + + if let Ok(mut cache) = turn_metadata_cache.write() + && cache.cwd.as_ref() == Some(&cwd) + { + cache.header = header; + } + }); + } + } } impl ModelClient { @@ -257,6 +302,14 @@ impl ModelClient { } impl ModelClientSession { + fn turn_metadata_header(&self) -> Option { + self.state + .turn_metadata_cache + .try_read() + .ok() + .and_then(|cache| cache.header.clone()) + } + /// Streams a single model turn using either the Responses or Chat /// Completions wire API, depending on the configured provider. /// @@ -332,6 +385,7 @@ impl ModelClientSession { prompt: &Prompt, compression: Compression, ) -> ApiResponsesOptions { + let turn_metadata_header = self.turn_metadata_header(); let model_info = &self.state.model_info; let default_reasoning_effort = model_info.default_reasoning_level; @@ -380,7 +434,11 @@ impl ModelClientSession { store_override: None, conversation_id: Some(conversation_id), session_source: Some(self.state.session_source.clone()), - extra_headers: build_responses_headers(&self.state.config, Some(&self.turn_state)), + extra_headers: build_responses_headers( + &self.state.config, + Some(&self.turn_state), + turn_metadata_header.as_ref(), + ), compression, turn_state: Some(Arc::clone(&self.turn_state)), } @@ -713,6 +771,7 @@ fn experimental_feature_headers(config: &Config) -> ApiHeaderMap { fn build_responses_headers( config: &Config, turn_state: Option<&Arc>>, + turn_metadata_header: Option<&HeaderValue>, ) -> ApiHeaderMap { let mut headers = experimental_feature_headers(config); headers.insert( @@ -731,6 +790,9 @@ fn build_responses_headers( { headers.insert(X_CODEX_TURN_STATE_HEADER, header_value); } + if let Some(header_value) = turn_metadata_header { + headers.insert(X_CODEX_TURN_METADATA_HEADER, header_value.clone()); + } headers } diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index b4e3859500..31a81846e2 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -119,6 +119,7 @@ use crate::error::Result as CodexResult; use crate::exec::StreamOutput; use crate::exec_policy::ExecPolicyUpdateError; use crate::feedback_tags; +use crate::git_info::get_git_repo_root; use crate::instructions::UserInstructions; use crate::mcp::CODEX_APPS_MCP_SERVER_NAME; use crate::mcp::auth::compute_auth_statuses; @@ -531,7 +532,6 @@ pub(crate) struct TurnContext { pub(crate) truncation_policy: TruncationPolicy, pub(crate) dynamic_tools: Vec, } - impl TurnContext { pub(crate) fn resolve_path(&self, path: Option) -> PathBuf { path.as_ref() @@ -3406,7 +3406,9 @@ pub(crate) async fn run_turn( // many turns, from the perspective of the user, it is a single turn. let turn_diff_tracker = Arc::new(tokio::sync::Mutex::new(TurnDiffTracker::new())); - let mut client_session = turn_context.client.new_session(); + let mut client_session = turn_context + .client + .new_session(Some(turn_context.cwd.clone())); loop { // Note that pending_input would be something like a message the user @@ -4469,8 +4471,6 @@ pub(super) fn get_last_assistant_message_from_turn(responses: &[ResponseItem]) - #[cfg(test)] pub(crate) use tests::make_session_and_context; - -use crate::git_info::get_git_repo_root; #[cfg(test)] pub(crate) use tests::make_session_and_context_with_rx; diff --git a/codex-rs/core/src/compact.rs b/codex-rs/core/src/compact.rs index 7e7445a6f6..b44d43f42e 100644 --- a/codex-rs/core/src/compact.rs +++ b/codex-rs/core/src/compact.rs @@ -335,7 +335,9 @@ async fn drain_to_completed( turn_context: &TurnContext, prompt: &Prompt, ) -> CodexResult<()> { - let mut client_session = turn_context.client.new_session(); + let mut client_session = turn_context + .client + .new_session(Some(turn_context.cwd.clone())); let mut stream = client_session.stream(prompt).await?; loop { let maybe_event = stream.next().await; diff --git a/codex-rs/core/src/environment_context.rs b/codex-rs/core/src/environment_context.rs index f0b0877eba..87692a2f05 100644 --- a/codex-rs/core/src/environment_context.rs +++ b/codex-rs/core/src/environment_context.rs @@ -28,6 +28,7 @@ impl EnvironmentContext { cwd, // should compare all fields except shell shell: _, + .. } = other; self.cwd == *cwd diff --git a/codex-rs/core/src/git_info.rs b/codex-rs/core/src/git_info.rs index 065f1280a4..dcabc3a341 100644 --- a/codex-rs/core/src/git_info.rs +++ b/codex-rs/core/src/git_info.rs @@ -1,3 +1,4 @@ +use std::collections::BTreeMap; use std::collections::HashSet; use std::path::Path; use std::path::PathBuf; @@ -109,6 +110,73 @@ pub async fn collect_git_info(cwd: &Path) -> Option { Some(git_info) } +/// Collect fetch remotes in a multi-root-friendly format: {"origin": "https://..."}. +pub async fn get_git_remote_urls(cwd: &Path) -> Option> { + let is_git_repo = run_git_command_with_timeout(&["rev-parse", "--git-dir"], cwd) + .await? + .status + .success(); + if !is_git_repo { + return None; + } + + get_git_remote_urls_assume_git_repo(cwd).await +} + +/// Collect fetch remotes without checking whether `cwd` is in a git repo. +pub async fn get_git_remote_urls_assume_git_repo(cwd: &Path) -> Option> { + let output = run_git_command_with_timeout(&["remote", "-v"], cwd).await?; + if !output.status.success() { + return None; + } + + let stdout = String::from_utf8(output.stdout).ok()?; + parse_git_remote_urls(stdout.as_str()) +} + +/// Return the current HEAD commit hash without checking whether `cwd` is in a git repo. +pub async fn get_head_commit_hash(cwd: &Path) -> Option { + let output = run_git_command_with_timeout(&["rev-parse", "HEAD"], cwd).await?; + if !output.status.success() { + return None; + } + + let stdout = String::from_utf8(output.stdout).ok()?; + let hash = stdout.trim(); + if hash.is_empty() { + None + } else { + Some(hash.to_string()) + } +} + +fn parse_git_remote_urls(stdout: &str) -> Option> { + let mut remotes = BTreeMap::new(); + for line in stdout.lines() { + let Some(fetch_line) = line.strip_suffix(" (fetch)") else { + continue; + }; + + let Some((name, url_part)) = fetch_line + .split_once('\t') + .or_else(|| fetch_line.split_once(' ')) + else { + continue; + }; + + let url = url_part.trim_start(); + if !url.is_empty() { + remotes.insert(name.to_string(), url.to_string()); + } + } + + if remotes.is_empty() { + None + } else { + Some(remotes) + } +} + /// A minimal commit summary entry used for pickers (subject + timestamp + sha). #[derive(Clone, Debug, Serialize, Deserialize)] pub struct CommitLogEntry { @@ -185,11 +253,9 @@ pub async fn git_diff_to_remote(cwd: &Path) -> Option { /// Run a git command with a timeout to prevent blocking on large repositories async fn run_git_command_with_timeout(args: &[&str], cwd: &Path) -> Option { - let result = timeout( - GIT_COMMAND_TIMEOUT, - Command::new("git").args(args).current_dir(cwd).output(), - ) - .await; + let mut command = Command::new("git"); + command.args(args).current_dir(cwd).kill_on_drop(true); + let result = timeout(GIT_COMMAND_TIMEOUT, command.output()).await; match result { Ok(Ok(output)) => Some(output), diff --git a/codex-rs/core/src/lib.rs b/codex-rs/core/src/lib.rs index e11b81e6fc..1f81f3eab2 100644 --- a/codex-rs/core/src/lib.rs +++ b/codex-rs/core/src/lib.rs @@ -101,6 +101,7 @@ pub mod state_db; pub mod terminal; mod tools; pub mod turn_diff_tracker; +mod turn_metadata; pub use rollout::ARCHIVED_SESSIONS_SUBDIR; pub use rollout::INTERACTIVE_SESSION_SOURCES; pub use rollout::RolloutRecorder; @@ -131,6 +132,7 @@ pub mod util; pub use apply_patch::CODEX_APPLY_PATCH_ARG1; pub use client::WEB_SEARCH_ELIGIBLE_HEADER; +pub use client::X_CODEX_TURN_METADATA_HEADER; pub use command_safety::is_dangerous_command; pub use command_safety::is_safe_command; pub use exec_policy::ExecPolicyError; diff --git a/codex-rs/core/src/turn_metadata.rs b/codex-rs/core/src/turn_metadata.rs new file mode 100644 index 0000000000..58b394a2f2 --- /dev/null +++ b/codex-rs/core/src/turn_metadata.rs @@ -0,0 +1,43 @@ +use std::collections::BTreeMap; +use std::path::Path; + +use serde::Serialize; + +use crate::git_info::get_git_remote_urls_assume_git_repo; +use crate::git_info::get_git_repo_root; +use crate::git_info::get_head_commit_hash; + +#[derive(Serialize)] +struct TurnMetadataWorkspace { + #[serde(skip_serializing_if = "Option::is_none")] + associated_remote_urls: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + latest_git_commit_hash: Option, +} + +#[derive(Serialize)] +struct TurnMetadata { + workspaces: BTreeMap, +} + +pub(crate) async fn build_turn_metadata_header(cwd: &Path) -> Option { + let repo_root = get_git_repo_root(cwd)?; + + let (latest_git_commit_hash, associated_remote_urls) = tokio::join!( + get_head_commit_hash(cwd), + get_git_remote_urls_assume_git_repo(cwd) + ); + if latest_git_commit_hash.is_none() && associated_remote_urls.is_none() { + return None; + } + + let mut workspaces = BTreeMap::new(); + workspaces.insert( + repo_root.to_string_lossy().into_owned(), + TurnMetadataWorkspace { + associated_remote_urls, + latest_git_commit_hash, + }, + ); + serde_json::to_string(&TurnMetadata { workspaces }).ok() +} diff --git a/codex-rs/core/tests/chat_completions_payload.rs b/codex-rs/core/tests/chat_completions_payload.rs index ffa2caa59f..5406b44ad6 100644 --- a/codex-rs/core/tests/chat_completions_payload.rs +++ b/codex-rs/core/tests/chat_completions_payload.rs @@ -102,7 +102,7 @@ async fn run_request(input: Vec) -> Value { SessionSource::Exec, TransportManager::new(), ) - .new_session(); + .new_session(None); let mut prompt = Prompt::default(); prompt.input = input; diff --git a/codex-rs/core/tests/chat_completions_sse.rs b/codex-rs/core/tests/chat_completions_sse.rs index 160699733e..8360237752 100644 --- a/codex-rs/core/tests/chat_completions_sse.rs +++ b/codex-rs/core/tests/chat_completions_sse.rs @@ -103,7 +103,7 @@ async fn run_stream_with_bytes(sse_body: &[u8]) -> Vec { SessionSource::Exec, TransportManager::new(), ) - .new_session(); + .new_session(None); let mut prompt = Prompt::default(); prompt.input = vec![ResponseItem::Message { diff --git a/codex-rs/core/tests/responses_headers.rs b/codex-rs/core/tests/responses_headers.rs index 86d6e8ce61..a40c92d02d 100644 --- a/codex-rs/core/tests/responses_headers.rs +++ b/codex-rs/core/tests/responses_headers.rs @@ -1,3 +1,4 @@ +use std::process::Command; use std::sync::Arc; use codex_app_server_protocol::AuthMode; @@ -23,6 +24,7 @@ use core_test_support::load_default_config_for_test; use core_test_support::responses; use core_test_support::test_codex::test_codex; use futures::StreamExt; +use pretty_assertions::assert_eq; use tempfile::TempDir; use wiremock::matchers::header; @@ -98,7 +100,7 @@ async fn responses_stream_includes_subagent_header_on_review() { session_source, TransportManager::new(), ) - .new_session(); + .new_session(None); let mut prompt = Prompt::default(); prompt.input = vec![ResponseItem::Message { @@ -197,7 +199,7 @@ async fn responses_stream_includes_subagent_header_on_other() { session_source, TransportManager::new(), ) - .new_session(); + .new_session(None); let mut prompt = Prompt::default(); prompt.input = vec![ResponseItem::Message { @@ -354,7 +356,7 @@ async fn responses_respects_model_info_overrides_from_config() { session_source, TransportManager::new(), ) - .new_session(); + .new_session(None); let mut prompt = Prompt::default(); prompt.input = vec![ResponseItem::Message { @@ -393,3 +395,196 @@ async fn responses_respects_model_info_overrides_from_config() { Some("detailed") ); } + +#[tokio::test] +async fn responses_stream_includes_turn_metadata_header_for_git_workspace_e2e() { + core_test_support::skip_if_no_network!(); + + let server = responses::start_mock_server().await; + let response_body = responses::sse(vec![ + responses::ev_response_created("resp-1"), + responses::ev_completed("resp-1"), + ]); + let provider = ModelProviderInfo { + name: "mock".into(), + base_url: Some(format!("{}/v1", server.uri())), + env_key: None, + env_key_instructions: None, + experimental_bearer_token: None, + wire_api: WireApi::Responses, + query_params: None, + http_headers: None, + env_http_headers: None, + request_max_retries: Some(0), + stream_max_retries: Some(0), + stream_idle_timeout_ms: Some(5_000), + requires_openai_auth: false, + supports_websockets: false, + }; + + let codex_home = TempDir::new().expect("failed to create TempDir"); + let mut config = load_default_config_for_test(&codex_home).await; + config.model_provider_id = provider.name.clone(); + config.model_provider = provider.clone(); + let effort = config.model_reasoning_effort; + let summary = config.model_reasoning_summary; + let model = ModelsManager::get_model_offline(config.model.as_deref()); + config.model = Some(model.clone()); + let config = Arc::new(config); + + let conversation_id = ThreadId::new(); + let auth_mode = AuthMode::Chatgpt; + let session_source = + SessionSource::SubAgent(SubAgentSource::Other("turn-metadata-e2e".to_string())); + let model_info = ModelsManager::construct_model_info_offline(model.as_str(), &config); + let otel_manager = OtelManager::new( + conversation_id, + model.as_str(), + model_info.slug.as_str(), + None, + Some("test@test.com".to_string()), + Some(auth_mode), + false, + "test".to_string(), + session_source.clone(), + ); + + let client = ModelClient::new( + Arc::clone(&config), + None, + model_info, + otel_manager, + provider, + effort, + summary, + conversation_id, + session_source, + TransportManager::new(), + ); + + let workspace = TempDir::new().expect("workspace tempdir"); + let cwd = workspace.path(); + + let mut prompt = Prompt::default(); + prompt.input = vec![ResponseItem::Message { + id: None, + role: "user".into(), + content: vec![ContentItem::InputText { + text: "hello".into(), + }], + end_turn: None, + }]; + + let first_request = responses::mount_sse_once(&server, response_body.clone()).await; + let mut first_session = client.new_session(Some(cwd.to_path_buf())); + let mut first_stream = first_session + .stream(&prompt) + .await + .expect("stream first turn"); + while let Some(event) = first_stream.next().await { + if matches!(event, Ok(ResponseEvent::Completed { .. })) { + break; + } + } + assert_eq!( + first_request + .single_request() + .header("x-codex-turn-metadata"), + None + ); + + let git_config_global = cwd.join("empty-git-config"); + std::fs::write(&git_config_global, "").expect("write empty git config"); + let run_git = |args: &[&str]| { + let output = Command::new("git") + .env("GIT_CONFIG_GLOBAL", &git_config_global) + .env("GIT_CONFIG_NOSYSTEM", "1") + .args(args) + .current_dir(cwd) + .output() + .expect("git command should run"); + assert!( + output.status.success(), + "git {:?} failed: stdout={} stderr={}", + args, + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); + output + }; + + run_git(&["init"]); + run_git(&["config", "user.name", "Test User"]); + run_git(&["config", "user.email", "test@example.com"]); + std::fs::write(cwd.join("README.md"), "hello").expect("write README"); + run_git(&["add", "."]); + run_git(&["commit", "-m", "initial commit"]); + run_git(&[ + "remote", + "add", + "origin", + "https://github.com/openai/codex.git", + ]); + + let expected_head = String::from_utf8(run_git(&["rev-parse", "HEAD"]).stdout) + .expect("git rev-parse output should be valid UTF-8") + .trim() + .to_string(); + let expected_origin = String::from_utf8(run_git(&["remote", "get-url", "origin"]).stdout) + .expect("git remote get-url output should be valid UTF-8") + .trim() + .to_string(); + + let repo_root = std::fs::canonicalize(cwd) + .unwrap_or_else(|_| cwd.to_path_buf()) + .to_string_lossy() + .into_owned(); + let deadline = tokio::time::Instant::now() + std::time::Duration::from_secs(5); + loop { + let request_recorder = responses::mount_sse_once(&server, response_body.clone()).await; + let mut session = client.new_session(Some(cwd.to_path_buf())); + tokio::time::sleep(std::time::Duration::from_millis(50)).await; + let mut stream = session.stream(&prompt).await.expect("stream post-git turn"); + while let Some(event) = stream.next().await { + if matches!(event, Ok(ResponseEvent::Completed { .. })) { + break; + } + } + + let maybe_header = request_recorder + .single_request() + .header("x-codex-turn-metadata"); + if let Some(header_value) = maybe_header { + let parsed: serde_json::Value = serde_json::from_str(&header_value) + .expect("x-codex-turn-metadata should be valid JSON"); + let workspace = parsed + .get("workspaces") + .and_then(serde_json::Value::as_object) + .and_then(|workspaces| workspaces.get(&repo_root)) + .expect("metadata should include cwd repo root workspace entry"); + + assert_eq!( + workspace + .get("latest_git_commit_hash") + .and_then(serde_json::Value::as_str), + Some(expected_head.as_str()) + ); + assert_eq!( + workspace + .get("associated_remote_urls") + .and_then(serde_json::Value::as_object) + .and_then(|remotes| remotes.get("origin")) + .and_then(serde_json::Value::as_str), + Some(expected_origin.as_str()) + ); + return; + } + + if tokio::time::Instant::now() >= deadline { + break; + } + tokio::time::sleep(std::time::Duration::from_millis(25)).await; + } + + panic!("x-codex-turn-metadata was never observed within 5s after git setup"); +} diff --git a/codex-rs/core/tests/suite/client.rs b/codex-rs/core/tests/suite/client.rs index 439233d8af..d79ef22b51 100644 --- a/codex-rs/core/tests/suite/client.rs +++ b/codex-rs/core/tests/suite/client.rs @@ -1190,7 +1190,7 @@ async fn azure_responses_request_includes_store_and_reasoning_ids() { SessionSource::Exec, TransportManager::new(), ) - .new_session(); + .new_session(None); let mut prompt = Prompt::default(); prompt.input.push(ResponseItem::Reasoning { diff --git a/codex-rs/core/tests/suite/client_websockets.rs b/codex-rs/core/tests/suite/client_websockets.rs index fc0ff37cf4..1acc3756d0 100644 --- a/codex-rs/core/tests/suite/client_websockets.rs +++ b/codex-rs/core/tests/suite/client_websockets.rs @@ -53,7 +53,7 @@ async fn responses_websocket_streams_request() { .await; let harness = websocket_harness(&server).await; - let mut session = harness.client.new_session(); + let mut session = harness.client.new_session(None); let prompt = prompt_with_input(vec![message_item("hello")]); stream_until_complete(&mut session, &prompt).await; @@ -83,7 +83,7 @@ async fn responses_websocket_emits_websocket_telemetry_events() { let harness = websocket_harness(&server).await; harness.otel_manager.reset_runtime_metrics(); - let mut session = harness.client.new_session(); + let mut session = harness.client.new_session(None); let prompt = prompt_with_input(vec![message_item("hello")]); stream_until_complete(&mut session, &prompt).await; @@ -113,7 +113,7 @@ async fn responses_websocket_emits_reasoning_included_event() { .await; let harness = websocket_harness(&server).await; - let mut session = harness.client.new_session(); + let mut session = harness.client.new_session(None); let prompt = prompt_with_input(vec![message_item("hello")]); let mut stream = session @@ -147,7 +147,7 @@ async fn responses_websocket_appends_on_prefix() { .await; let harness = websocket_harness(&server).await; - let mut session = harness.client.new_session(); + let mut session = harness.client.new_session(None); let prompt_one = prompt_with_input(vec![message_item("hello")]); let prompt_two = prompt_with_input(vec![message_item("hello"), message_item("second")]); @@ -183,7 +183,7 @@ async fn responses_websocket_creates_on_non_prefix() { .await; let harness = websocket_harness(&server).await; - let mut session = harness.client.new_session(); + let mut session = harness.client.new_session(None); let prompt_one = prompt_with_input(vec![message_item("hello")]); let prompt_two = prompt_with_input(vec![message_item("different")]); From d02db8b43d457852a7f2392ff58d556fbb56a9b7 Mon Sep 17 00:00:00 2001 From: Ahmed Ibrahim Date: Mon, 2 Feb 2026 17:37:04 -0800 Subject: [PATCH 15/28] Add `codex app` macOS launcher (#10418) - Add `codex app ` to launch the Codex Desktop app. - On macOS, auto-downloads the DMG if missing; non-macOS prints a link to chatgpt.com/codex. --- codex-rs/cli/Cargo.toml | 2 +- codex-rs/cli/src/app_cmd.rs | 21 +++ codex-rs/cli/src/desktop_app/mac.rs | 281 ++++++++++++++++++++++++++++ codex-rs/cli/src/desktop_app/mod.rs | 11 ++ codex-rs/cli/src/main.rs | 12 ++ codex-rs/tui/tooltips.txt | 1 + 6 files changed, 327 insertions(+), 1 deletion(-) create mode 100644 codex-rs/cli/src/app_cmd.rs create mode 100644 codex-rs/cli/src/desktop_app/mac.rs create mode 100644 codex-rs/cli/src/desktop_app/mod.rs diff --git a/codex-rs/cli/Cargo.toml b/codex-rs/cli/Cargo.toml index 81fcd35a91..cfcfce88f9 100644 --- a/codex-rs/cli/Cargo.toml +++ b/codex-rs/cli/Cargo.toml @@ -40,6 +40,7 @@ owo-colors = { workspace = true } regex-lite = { workspace = true } serde_json = { workspace = true } supports-color = { workspace = true } +tempfile = { workspace = true } tokio = { workspace = true, features = [ "io-std", "macros", @@ -59,4 +60,3 @@ assert_matches = { workspace = true } codex-utils-cargo-bin = { workspace = true } predicates = { workspace = true } pretty_assertions = { workspace = true } -tempfile = { workspace = true } diff --git a/codex-rs/cli/src/app_cmd.rs b/codex-rs/cli/src/app_cmd.rs new file mode 100644 index 0000000000..cb761c131e --- /dev/null +++ b/codex-rs/cli/src/app_cmd.rs @@ -0,0 +1,21 @@ +use clap::Parser; +use std::path::PathBuf; + +const DEFAULT_CODEX_DMG_URL: &str = "https://persistent.oaistatic.com/codex-app-prod/Codex.dmg"; + +#[derive(Debug, Parser)] +pub struct AppCommand { + /// Workspace path to open in Codex Desktop. + #[arg(value_name = "PATH", default_value = ".")] + pub path: PathBuf, + + /// Override the macOS DMG download URL (advanced). + #[arg(long, default_value = DEFAULT_CODEX_DMG_URL)] + pub download_url: String, +} + +#[cfg(target_os = "macos")] +pub async fn run_app(cmd: AppCommand) -> anyhow::Result<()> { + let workspace = std::fs::canonicalize(&cmd.path).unwrap_or(cmd.path); + crate::desktop_app::run_app_open_or_install(workspace, cmd.download_url).await +} diff --git a/codex-rs/cli/src/desktop_app/mac.rs b/codex-rs/cli/src/desktop_app/mac.rs new file mode 100644 index 0000000000..d404f5b7ca --- /dev/null +++ b/codex-rs/cli/src/desktop_app/mac.rs @@ -0,0 +1,281 @@ +use anyhow::Context as _; +use std::path::Path; +use std::path::PathBuf; +use tempfile::Builder; +use tokio::process::Command; + +pub async fn run_mac_app_open_or_install( + workspace: PathBuf, + download_url: String, +) -> anyhow::Result<()> { + if let Some(app_path) = find_existing_codex_app_path() { + eprintln!( + "Opening Codex Desktop at {app_path}...", + app_path = app_path.display() + ); + open_codex_app(&app_path, &workspace).await?; + return Ok(()); + } + eprintln!("Codex Desktop not found; downloading installer..."); + let installed_app = download_and_install_codex_to_user_applications(&download_url) + .await + .context("failed to download/install Codex Desktop")?; + eprintln!( + "Launching Codex Desktop from {installed_app}...", + installed_app = installed_app.display() + ); + open_codex_app(&installed_app, &workspace).await?; + Ok(()) +} + +fn find_existing_codex_app_path() -> Option { + candidate_codex_app_paths() + .into_iter() + .find(|candidate| candidate.is_dir()) +} + +fn candidate_codex_app_paths() -> Vec { + let mut paths = vec![PathBuf::from("/Applications/Codex.app")]; + if let Some(home) = std::env::var_os("HOME") { + paths.push(PathBuf::from(home).join("Applications").join("Codex.app")); + } + paths +} + +async fn open_codex_app(app_path: &Path, workspace: &Path) -> anyhow::Result<()> { + eprintln!( + "Opening workspace {workspace}...", + workspace = workspace.display() + ); + let status = Command::new("open") + .arg("-a") + .arg(app_path) + .arg(workspace) + .status() + .await + .context("failed to invoke `open`")?; + + if status.success() { + return Ok(()); + } + + anyhow::bail!( + "`open -a {app_path} {workspace}` exited with {status}", + app_path = app_path.display(), + workspace = workspace.display() + ); +} + +async fn download_and_install_codex_to_user_applications(dmg_url: &str) -> anyhow::Result { + let temp_dir = Builder::new() + .prefix("codex-app-installer-") + .tempdir() + .context("failed to create temp dir")?; + let tmp_root = temp_dir.path().to_path_buf(); + let _temp_dir = temp_dir; + + let dmg_path = tmp_root.join("Codex.dmg"); + download_dmg(dmg_url, &dmg_path).await?; + + eprintln!("Mounting Codex Desktop installer..."); + let mount_point = mount_dmg(&dmg_path).await?; + eprintln!( + "Installer mounted at {mount_point}.", + mount_point = mount_point.display() + ); + let result = async { + let app_in_volume = find_codex_app_in_mount(&mount_point) + .context("failed to locate Codex.app in mounted dmg")?; + install_codex_app_bundle(&app_in_volume).await + } + .await; + + let detach_result = detach_dmg(&mount_point).await; + if let Err(err) = detach_result { + eprintln!( + "warning: failed to detach dmg at {mount_point}: {err}", + mount_point = mount_point.display() + ); + } + + result +} + +async fn install_codex_app_bundle(app_in_volume: &Path) -> anyhow::Result { + for applications_dir in candidate_applications_dirs()? { + eprintln!( + "Installing Codex Desktop into {applications_dir}...", + applications_dir = applications_dir.display() + ); + std::fs::create_dir_all(&applications_dir).with_context(|| { + format!( + "failed to create applications dir {applications_dir}", + applications_dir = applications_dir.display() + ) + })?; + + let dest_app = applications_dir.join("Codex.app"); + if dest_app.is_dir() { + return Ok(dest_app); + } + + match copy_app_bundle(app_in_volume, &dest_app).await { + Ok(()) => return Ok(dest_app), + Err(err) => { + eprintln!( + "warning: failed to install Codex.app to {applications_dir}: {err}", + applications_dir = applications_dir.display() + ); + } + } + } + + anyhow::bail!("failed to install Codex.app to any applications directory"); +} + +fn candidate_applications_dirs() -> anyhow::Result> { + let mut dirs = vec![PathBuf::from("/Applications")]; + dirs.push(user_applications_dir()?); + Ok(dirs) +} + +async fn download_dmg(url: &str, dest: &Path) -> anyhow::Result<()> { + eprintln!("Downloading installer..."); + let status = Command::new("curl") + .arg("-fL") + .arg("--retry") + .arg("3") + .arg("--retry-delay") + .arg("1") + .arg("-o") + .arg(dest) + .arg(url) + .status() + .await + .context("failed to invoke `curl`")?; + + if status.success() { + return Ok(()); + } + anyhow::bail!("curl download failed with {status}"); +} + +async fn mount_dmg(dmg_path: &Path) -> anyhow::Result { + let output = Command::new("hdiutil") + .arg("attach") + .arg("-nobrowse") + .arg("-readonly") + .arg(dmg_path) + .output() + .await + .context("failed to invoke `hdiutil attach`")?; + + if !output.status.success() { + anyhow::bail!( + "`hdiutil attach` failed with {status}: {stderr}", + status = output.status, + stderr = String::from_utf8_lossy(&output.stderr) + ); + } + + let stdout = String::from_utf8_lossy(&output.stdout); + parse_hdiutil_attach_mount_point(&stdout) + .map(PathBuf::from) + .with_context(|| format!("failed to parse mount point from hdiutil output:\n{stdout}")) +} + +async fn detach_dmg(mount_point: &Path) -> anyhow::Result<()> { + let status = Command::new("hdiutil") + .arg("detach") + .arg(mount_point) + .status() + .await + .context("failed to invoke `hdiutil detach`")?; + + if status.success() { + return Ok(()); + } + anyhow::bail!("hdiutil detach failed with {status}"); +} + +fn find_codex_app_in_mount(mount_point: &Path) -> anyhow::Result { + let direct = mount_point.join("Codex.app"); + if direct.is_dir() { + return Ok(direct); + } + + for entry in std::fs::read_dir(mount_point).with_context(|| { + format!( + "failed to read {mount_point}", + mount_point = mount_point.display() + ) + })? { + let entry = entry.context("failed to read mount directory entry")?; + let path = entry.path(); + if path.extension().is_some_and(|ext| ext == "app") && path.is_dir() { + return Ok(path); + } + } + + anyhow::bail!( + "no .app bundle found at {mount_point}", + mount_point = mount_point.display() + ); +} + +async fn copy_app_bundle(src_app: &Path, dest_app: &Path) -> anyhow::Result<()> { + let status = Command::new("ditto") + .arg(src_app) + .arg(dest_app) + .status() + .await + .context("failed to invoke `ditto`")?; + + if status.success() { + return Ok(()); + } + anyhow::bail!("ditto copy failed with {status}"); +} + +fn user_applications_dir() -> anyhow::Result { + let home = std::env::var_os("HOME").context("HOME is not set")?; + Ok(PathBuf::from(home).join("Applications")) +} + +fn parse_hdiutil_attach_mount_point(output: &str) -> Option { + output.lines().find_map(|line| { + if !line.contains("/Volumes/") { + return None; + } + if let Some((_, mount)) = line.rsplit_once('\t') { + return Some(mount.trim().to_string()); + } + line.split_whitespace() + .find(|field| field.starts_with("/Volumes/")) + .map(str::to_string) + }) +} + +#[cfg(test)] +mod tests { + use super::parse_hdiutil_attach_mount_point; + use pretty_assertions::assert_eq; + + #[test] + fn parses_mount_point_from_tab_separated_hdiutil_output() { + let output = "/dev/disk2s1\tApple_HFS\tCodex\t/Volumes/Codex\n"; + assert_eq!( + parse_hdiutil_attach_mount_point(output).as_deref(), + Some("/Volumes/Codex") + ); + } + + #[test] + fn parses_mount_point_with_spaces() { + let output = "/dev/disk2s1\tApple_HFS\tCodex Installer\t/Volumes/Codex Installer\n"; + assert_eq!( + parse_hdiutil_attach_mount_point(output).as_deref(), + Some("/Volumes/Codex Installer") + ); + } +} diff --git a/codex-rs/cli/src/desktop_app/mod.rs b/codex-rs/cli/src/desktop_app/mod.rs new file mode 100644 index 0000000000..7c42315a87 --- /dev/null +++ b/codex-rs/cli/src/desktop_app/mod.rs @@ -0,0 +1,11 @@ +#[cfg(target_os = "macos")] +mod mac; + +/// Run the app install/open logic for the current OS. +#[cfg(target_os = "macos")] +pub async fn run_app_open_or_install( + workspace: std::path::PathBuf, + download_url: String, +) -> anyhow::Result<()> { + mac::run_mac_app_open_or_install(workspace, download_url).await +} diff --git a/codex-rs/cli/src/main.rs b/codex-rs/cli/src/main.rs index 01aef19176..022d6bbdf5 100644 --- a/codex-rs/cli/src/main.rs +++ b/codex-rs/cli/src/main.rs @@ -31,6 +31,10 @@ use std::io::IsTerminal; use std::path::PathBuf; use supports_color::Stream; +#[cfg(target_os = "macos")] +mod app_cmd; +#[cfg(target_os = "macos")] +mod desktop_app; mod mcp_cmd; #[cfg(not(windows))] mod wsl_paths; @@ -98,6 +102,10 @@ enum Subcommand { /// [experimental] Run the app server or related tooling. AppServer(AppServerCommand), + /// Launch the Codex desktop app (downloads the macOS installer if missing). + #[cfg(target_os = "macos")] + App(app_cmd::AppCommand), + /// Generate shell completion scripts. Completion(CompletionCommand), @@ -564,6 +572,10 @@ async fn cli_main(codex_linux_sandbox_exe: Option) -> anyhow::Result<() )?; } }, + #[cfg(target_os = "macos")] + Some(Subcommand::App(app_cli)) => { + app_cmd::run_app(app_cli).await?; + } Some(Subcommand::Resume(ResumeCommand { session_id, last, diff --git a/codex-rs/tui/tooltips.txt b/codex-rs/tui/tooltips.txt index e1e4d23712..68664a6f4e 100644 --- a/codex-rs/tui/tooltips.txt +++ b/codex-rs/tui/tooltips.txt @@ -9,6 +9,7 @@ Use /status to see the current model, approvals, and token usage. Use /fork to branch the current chat into a new thread. Use /init to create an AGENTS.md with project-specific guidance. Use /mcp to list configured MCP tools. +Run `codex app` to open Codex Desktop (it installs on macOS if needed). Use /personality to customize how Codex communicates. Use /rename to rename your threads for easier thread resuming. Use the OpenAI docs MCP for API questions; enable it with `codex mcp add openaiDeveloperDocs --url https://developers.openai.com/mcp`. From 1096d6453c096a1860ba8e2595729cc69e2e312b Mon Sep 17 00:00:00 2001 From: Charley Cunningham Date: Mon, 2 Feb 2026 17:40:05 -0800 Subject: [PATCH 16/28] Fix plan implementation prompt reappearing after /agent thread switch (#10447) ## Summary This fixes a UX bug (https://github.com/openai/codex/issues/10442) where the **"Implement this plan?"** prompt could reappear after switching agents with `/agent` and then switching back to the original agent during plan execution. ## Root Cause On thread switch, the TUI rebuilds `ChatWidget`, replays buffered thread events, then drains any queued live events. In this flow, a `TurnComplete` can be handled twice for the same logical turn: 1. replayed (`from_replay = true`) 2. then live (`from_replay = false`) `ChatWidget` used `saw_plan_item_this_turn` to decide whether to show the plan implementation prompt, but that flag was only reset on `TurnStarted`. If duplicate completion events occurred, stale `saw_plan_item_this_turn = true` could cause the prompt to re-trigger unexpectedly. ## Fix - Clear `saw_plan_item_this_turn` at the end of `on_task_complete`, after prompt gating runs. - This keeps the flag truly turn-scoped and prevents duplicate `TurnComplete` handling from reopening the prompt. --- codex-rs/tui/src/chatwidget.rs | 5 +++ codex-rs/tui/src/chatwidget/tests.rs | 55 ++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+) diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index eb2d1ad965..cc99aef7cf 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -1070,6 +1070,11 @@ impl ChatWidget { if !from_replay && self.queued_user_messages.is_empty() { self.maybe_prompt_plan_implementation(); } + // Keep this flag for replayed completion events so a subsequent live TurnComplete can + // still show the prompt once after thread switch replay. + if !from_replay { + self.saw_plan_item_this_turn = false; + } // If there is a queued user message, send exactly one now to begin the next turn. self.maybe_send_next_queued_input(); // Emit a notification when the turn completes (suppressed if focused). diff --git a/codex-rs/tui/src/chatwidget/tests.rs b/codex-rs/tui/src/chatwidget/tests.rs index a70d93a2c4..ea0cd43636 100644 --- a/codex-rs/tui/src/chatwidget/tests.rs +++ b/codex-rs/tui/src/chatwidget/tests.rs @@ -1260,6 +1260,61 @@ async fn plan_implementation_popup_skips_replayed_turn_complete() { ); } +#[tokio::test] +async fn plan_implementation_popup_shows_once_when_replay_precedes_live_turn_complete() { + let (mut chat, _rx, _op_rx) = make_chatwidget_manual(Some("gpt-5")).await; + chat.set_feature_enabled(Feature::CollaborationModes, true); + let plan_mask = + collaboration_modes::mask_for_kind(chat.models_manager.as_ref(), ModeKind::Plan) + .expect("expected plan collaboration mask"); + chat.set_collaboration_mask(plan_mask); + + chat.on_task_started(); + chat.on_plan_delta("- Step 1\n- Step 2\n".to_string()); + chat.on_plan_item_completed("- Step 1\n- Step 2\n".to_string()); + + chat.replay_initial_messages(vec![EventMsg::TurnComplete(TurnCompleteEvent { + last_agent_message: Some("Plan details".to_string()), + })]); + let replay_popup = render_bottom_popup(&chat, 80); + assert!( + !replay_popup.contains(PLAN_IMPLEMENTATION_TITLE), + "expected no prompt for replayed turn completion, got {replay_popup:?}" + ); + + chat.handle_codex_event(Event { + id: "live-turn-complete-1".to_string(), + msg: EventMsg::TurnComplete(TurnCompleteEvent { + last_agent_message: Some("Plan details".to_string()), + }), + }); + + let popup = render_bottom_popup(&chat, 80); + assert!( + popup.contains(PLAN_IMPLEMENTATION_TITLE), + "expected prompt for first live turn completion after replay, got {popup:?}" + ); + + chat.handle_key_event(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE)); + let dismissed_popup = render_bottom_popup(&chat, 80); + assert!( + !dismissed_popup.contains(PLAN_IMPLEMENTATION_TITLE), + "expected prompt to dismiss on Esc, got {dismissed_popup:?}" + ); + + chat.handle_codex_event(Event { + id: "live-turn-complete-2".to_string(), + msg: EventMsg::TurnComplete(TurnCompleteEvent { + last_agent_message: Some("Plan details".to_string()), + }), + }); + let duplicate_popup = render_bottom_popup(&chat, 80); + assert!( + !duplicate_popup.contains(PLAN_IMPLEMENTATION_TITLE), + "expected no prompt for duplicate live completion, got {duplicate_popup:?}" + ); +} + #[tokio::test] async fn plan_implementation_popup_skips_when_messages_queued() { let (mut chat, _rx, _op_rx) = make_chatwidget_manual(Some("gpt-5")).await; From 8f5edddf7121fdb676feac511c80da1b6d4dfb16 Mon Sep 17 00:00:00 2001 From: Charley Cunningham Date: Mon, 2 Feb 2026 17:41:30 -0800 Subject: [PATCH 17/28] TUI: Render request_user_input results in history and simplify interrupt handling (#10064) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary This PR improves the TUI experience for `request_user_input` by rendering submitted question/answer sets directly in conversation history with clear, structured formatting. It also intentionally simplifies interrupt behavior for now: on `Esc` / `Ctrl+C`, the questions overlay interrupts the turn without attempting to submit partial answers. Screenshot 2026-02-02 at 4 51 40 PM ## Scope - TUI-only changes. - No core/protocol/app-server behavior changes in this PR. - Resume reconstruction of interrupted question sets is out of scope for this PR. ## What Changed - Added a new history cell: `RequestUserInputResultCell` in `codex-rs/tui/src/history_cell.rs`. - On normal `request_user_input` submission, TUI now inserts that history cell immediately after sending `Op::UserInputAnswer`. - Rendering includes a `Questions` header with `answered/total` count. - Rendering shows each question as a bullet item. - Rendering styles submitted answer lines in cyan. - Rendering styles notes (for option questions) as `note:` lines in cyan. - Rendering styles freeform text (for no-option questions) as `answer:` lines in cyan. - Rendering dims only the `(unanswered)` suffix. - Rendering can include an interrupted suffix and summary text when the cell is marked interrupted. - Rendering redacts secret questions as `••••••` instead of showing raw values. - Added `wrap_with_prefix(...)` in `history_cell.rs` for wrapped prefixed lines. - Added `split_request_user_input_answer(...)` in `history_cell.rs` for decoding `"user_note: ..."` entries. ## Interrupt Behavior (Intentional for this PR) - `Esc` / `Ctrl+C` in the questions overlay now performs `Op::Interrupt` and exits the overlay. - It does **not** submit partial/committed answers on interrupt. - Added TODO comments in `request_user_input` overlay interrupt paths indicating where interrupted partial result emission should be reintroduced once core support is finalized. - Queued `request_user_input` overlays are discarded on interrupt in the current behavior. ## Tests Updated - Updated/added overlay tests in `codex-rs/tui/src/bottom_pane/request_user_input/mod.rs` to reflect interrupt-only behavior. - Added helper assertion for interrupt-only event expectation. - Existing submission-path tests now validate history insertion behavior and expected answer maps. ## Behavior Notes - Completed question flows now produce a readable `Questions` block in transcript history. - Interrupted flows currently do not persist partial answers to model-visible tool output. ## Follow-ups - Reintroduce partial-answer-on-interrupt semantics once core can persist/sequence interrupted `request_user_input` outputs safely. - Optionally add replay/resume rendering for interrupted question sets as a separate PR. ## Codex author `codex fork 019bfb8d-2a65-7313-9be2-ea7100d19a61` --- .../src/bottom_pane/request_user_input/mod.rs | 140 ++++++++++++++--- codex-rs/tui/src/history_cell.rs | 147 ++++++++++++++++++ 2 files changed, 262 insertions(+), 25 deletions(-) diff --git a/codex-rs/tui/src/bottom_pane/request_user_input/mod.rs b/codex-rs/tui/src/bottom_pane/request_user_input/mod.rs index 8fe3401693..59807a8a4c 100644 --- a/codex-rs/tui/src/bottom_pane/request_user_input/mod.rs +++ b/codex-rs/tui/src/bottom_pane/request_user_input/mod.rs @@ -27,6 +27,7 @@ use crate::bottom_pane::bottom_pane_view::BottomPaneView; use crate::bottom_pane::scroll_state::ScrollState; use crate::bottom_pane::selection_popup_common::GenericDisplayRow; use crate::bottom_pane::selection_popup_common::measure_rows_height; +use crate::history_cell; use crate::render::renderable::Renderable; use codex_core::protocol::Op; @@ -722,8 +723,17 @@ impl RequestUserInputOverlay { self.app_event_tx .send(AppEvent::CodexOp(Op::UserInputAnswer { id: self.request.turn_id.clone(), - response: RequestUserInputResponse { answers }, + response: RequestUserInputResponse { + answers: answers.clone(), + }, })); + self.app_event_tx.send(AppEvent::InsertHistoryCell(Box::new( + history_cell::RequestUserInputResultCell { + questions: self.request.questions.clone(), + answers, + interrupted: false, + }, + ))); if let Some(next) = self.queue.pop_front() { self.request = next; self.reset_for_request(); @@ -966,6 +976,8 @@ impl BottomPaneView for RequestUserInputOverlay { } if matches!(key_event.code, KeyCode::Esc) { + // TODO: Emit interrupted request_user_input results (including committed answers) + // once core supports persisting them reliably without follow-up turn issues. self.app_event_tx.send(AppEvent::CodexOp(Op::Interrupt)); self.done = true; return; @@ -1173,6 +1185,8 @@ impl BottomPaneView for RequestUserInputOverlay { fn on_ctrl_c(&mut self) -> CancellationEvent { if self.confirm_unanswered_active() { self.close_unanswered_confirmation(); + // TODO: Emit interrupted request_user_input results (including committed answers) + // once core supports persisting them reliably without follow-up turn issues. self.app_event_tx.send(AppEvent::CodexOp(Op::Interrupt)); self.done = true; return CancellationEvent::Handled; @@ -1182,6 +1196,8 @@ impl BottomPaneView for RequestUserInputOverlay { return CancellationEvent::Handled; } + // TODO: Emit interrupted request_user_input results (including committed answers) + // once core supports persisting them reliably without follow-up turn issues. self.app_event_tx.send(AppEvent::CodexOp(Op::Interrupt)); self.done = true; CancellationEvent::Handled @@ -1234,6 +1250,7 @@ mod tests { use pretty_assertions::assert_eq; use ratatui::buffer::Buffer; use ratatui::layout::Rect; + use std::collections::HashMap; use tokio::sync::mpsc::unbounded_channel; use unicode_width::UnicodeWidthStr; @@ -1245,6 +1262,18 @@ mod tests { (AppEventSender::new(tx_raw), rx) } + fn expect_interrupt_only(rx: &mut tokio::sync::mpsc::UnboundedReceiver) { + let event = rx.try_recv().expect("expected interrupt AppEvent"); + let AppEvent::CodexOp(op) = event else { + panic!("expected CodexOp"); + }; + assert_eq!(op, Op::Interrupt); + assert!( + rx.try_recv().is_err(), + "unexpected AppEvents before interrupt completion" + ); + } + fn question_with_options(id: &str, header: &str) -> RequestUserInputQuestion { RequestUserInputQuestion { id: id.to_string(), @@ -1389,6 +1418,33 @@ mod tests { assert_eq!(overlay.request.turn_id, "turn-3"); } + #[test] + fn interrupt_discards_queued_requests_and_emits_interrupt() { + let (tx, mut rx) = test_sender(); + let mut overlay = RequestUserInputOverlay::new( + request_event("turn-1", vec![question_with_options("q1", "First")]), + tx, + true, + false, + false, + ); + overlay.try_consume_user_input_request(RequestUserInputEvent { + call_id: "call-2".to_string(), + turn_id: "turn-2".to_string(), + questions: vec![question_with_options("q2", "Second")], + }); + overlay.try_consume_user_input_request(RequestUserInputEvent { + call_id: "call-3".to_string(), + turn_id: "turn-3".to_string(), + questions: vec![question_with_options("q3", "Third")], + }); + + overlay.handle_key_event(KeyEvent::from(KeyCode::Esc)); + + assert!(overlay.done, "expected overlay to be done"); + expect_interrupt_only(&mut rx); + } + #[test] fn options_can_submit_empty_when_unanswered() { let (tx, mut rx) = test_sender(); @@ -1403,7 +1459,7 @@ mod tests { overlay.submit_answers(); let event = rx.try_recv().expect("expected AppEvent"); - let AppEvent::CodexOp(Op::UserInputAnswer { id, response }) = event else { + let AppEvent::CodexOp(Op::UserInputAnswer { id, response, .. }) = event else { panic!("expected UserInputAnswer"); }; assert_eq!(id, "turn-1"); @@ -1454,15 +1510,30 @@ mod tests { let first_answer = &overlay.answers[0]; assert!(first_answer.answer_committed); assert_eq!(first_answer.options_state.selected_idx, Some(0)); - assert!(rx.try_recv().is_err()); + assert!( + rx.try_recv().is_err(), + "unexpected AppEvent before full submission" + ); overlay.handle_key_event(KeyEvent::from(KeyCode::Enter)); let event = rx.try_recv().expect("expected AppEvent"); let AppEvent::CodexOp(Op::UserInputAnswer { response, .. }) = event else { panic!("expected UserInputAnswer"); }; - let answer = response.answers.get("q1").expect("answer missing"); - assert_eq!(answer.answers, vec!["Option 1".to_string()]); + let mut expected = HashMap::new(); + expected.insert( + "q1".to_string(), + RequestUserInputAnswer { + answers: vec!["Option 1".to_string()], + }, + ); + expected.insert( + "q2".to_string(), + RequestUserInputAnswer { + answers: vec!["Option 1".to_string()], + }, + ); + assert_eq!(response.answers, expected); } #[test] @@ -1630,6 +1701,10 @@ mod tests { overlay.handle_key_event(KeyEvent::from(KeyCode::Enter)); assert!(overlay.confirm_unanswered_active()); + assert!( + rx.try_recv().is_err(), + "unexpected AppEvent before confirmation submit" + ); overlay.handle_key_event(KeyEvent::from(KeyCode::Char('1'))); overlay.handle_key_event(KeyEvent::from(KeyCode::Enter)); @@ -1657,11 +1732,7 @@ mod tests { overlay.handle_key_event(KeyEvent::from(KeyCode::Esc)); assert_eq!(overlay.done, true); - let event = rx.try_recv().expect("expected AppEvent"); - let AppEvent::CodexOp(op) = event else { - panic!("expected CodexOp"); - }; - assert_eq!(op, Op::Interrupt); + expect_interrupt_only(&mut rx); } #[test] @@ -1678,11 +1749,7 @@ mod tests { overlay.handle_key_event(KeyEvent::from(KeyCode::Esc)); assert_eq!(overlay.done, true); - let event = rx.try_recv().expect("expected AppEvent"); - let AppEvent::CodexOp(op) = event else { - panic!("expected CodexOp"); - }; - assert_eq!(op, Op::Interrupt); + expect_interrupt_only(&mut rx); } #[test] @@ -1697,16 +1764,13 @@ mod tests { ); let answer = overlay.current_answer_mut().expect("answer missing"); answer.options_state.selected_idx = Some(0); + answer.answer_committed = true; overlay.handle_key_event(KeyEvent::from(KeyCode::Tab)); overlay.handle_key_event(KeyEvent::from(KeyCode::Esc)); assert_eq!(overlay.done, true); - let event = rx.try_recv().expect("expected AppEvent"); - let AppEvent::CodexOp(op) = event else { - panic!("expected CodexOp"); - }; - assert_eq!(op, Op::Interrupt); + expect_interrupt_only(&mut rx); } #[test] @@ -1721,17 +1785,42 @@ mod tests { ); let answer = overlay.current_answer_mut().expect("answer missing"); answer.options_state.selected_idx = Some(0); + answer.answer_committed = true; overlay.handle_key_event(KeyEvent::from(KeyCode::Tab)); overlay.handle_key_event(KeyEvent::from(KeyCode::Char('a'))); overlay.handle_key_event(KeyEvent::from(KeyCode::Esc)); assert_eq!(overlay.done, true); - let event = rx.try_recv().expect("expected AppEvent"); - let AppEvent::CodexOp(op) = event else { - panic!("expected CodexOp"); - }; - assert_eq!(op, Op::Interrupt); + expect_interrupt_only(&mut rx); + } + + #[test] + fn esc_drops_committed_answers() { + let (tx, mut rx) = test_sender(); + let mut overlay = RequestUserInputOverlay::new( + request_event( + "turn-1", + vec![ + question_with_options("q1", "First"), + question_without_options("q2", "Second"), + ], + ), + tx, + true, + false, + false, + ); + + overlay.handle_key_event(KeyEvent::from(KeyCode::Enter)); + assert!( + rx.try_recv().is_err(), + "unexpected AppEvent before interruption" + ); + + overlay.handle_key_event(KeyEvent::from(KeyCode::Esc)); + + expect_interrupt_only(&mut rx); } #[test] @@ -1961,6 +2050,7 @@ mod tests { overlay.composer.move_cursor_to_end(); overlay.handle_key_event(KeyEvent::from(KeyCode::Enter)); assert_eq!(overlay.answers[0].answer_committed, true); + let _ = rx.try_recv(); overlay.move_question(false); overlay diff --git a/codex-rs/tui/src/history_cell.rs b/codex-rs/tui/src/history_cell.rs index eb1de452a9..0258e05968 100644 --- a/codex-rs/tui/src/history_cell.rs +++ b/codex-rs/tui/src/history_cell.rs @@ -52,6 +52,8 @@ use codex_protocol::openai_models::ReasoningEffort as ReasoningEffortConfig; use codex_protocol::plan_tool::PlanItemArg; use codex_protocol::plan_tool::StepStatus; use codex_protocol::plan_tool::UpdatePlanArgs; +use codex_protocol::request_user_input::RequestUserInputAnswer; +use codex_protocol::request_user_input::RequestUserInputQuestion; use codex_protocol::user_input::TextElement; use image::DynamicImage; use image::ImageReader; @@ -1762,6 +1764,151 @@ pub(crate) fn new_error_event(message: String) -> PlainHistoryCell { PlainHistoryCell { lines } } +/// Renders a completed (or interrupted) request_user_input exchange in history. +#[derive(Debug)] +pub(crate) struct RequestUserInputResultCell { + pub(crate) questions: Vec, + pub(crate) answers: HashMap, + pub(crate) interrupted: bool, +} + +impl HistoryCell for RequestUserInputResultCell { + fn display_lines(&self, width: u16) -> Vec> { + let width = width.max(1) as usize; + let total = self.questions.len(); + let answered = self + .questions + .iter() + .filter(|question| { + self.answers + .get(&question.id) + .is_some_and(|answer| !answer.answers.is_empty()) + }) + .count(); + let unanswered = total.saturating_sub(answered); + + let mut header = vec!["•".dim(), " ".into(), "Questions".bold()]; + header.push(format!(" {answered}/{total} answered").dim()); + if self.interrupted { + header.push(" (interrupted)".cyan()); + } + + let mut lines: Vec> = vec![header.into()]; + + for question in &self.questions { + let answer = self.answers.get(&question.id); + let answer_missing = match answer { + Some(answer) => answer.answers.is_empty(), + None => true, + }; + let mut question_lines = wrap_with_prefix( + &question.question, + width, + " • ".into(), + " ".into(), + Style::default(), + ); + if answer_missing && let Some(last) = question_lines.last_mut() { + last.spans.push(" (unanswered)".dim()); + } + lines.extend(question_lines); + + let Some(answer) = answer.filter(|answer| !answer.answers.is_empty()) else { + continue; + }; + if question.is_secret { + lines.extend(wrap_with_prefix( + "••••••", + width, + " answer: ".dim(), + " ".dim(), + Style::default().fg(Color::Cyan), + )); + continue; + } + + let (options, note) = split_request_user_input_answer(answer); + + for option in options { + lines.extend(wrap_with_prefix( + &option, + width, + " answer: ".dim(), + " ".dim(), + Style::default().fg(Color::Cyan), + )); + } + if let Some(note) = note { + let (label, continuation, style) = if question.options.is_some() { + ( + " note: ".dim(), + " ".dim(), + Style::default().fg(Color::Cyan), + ) + } else { + ( + " answer: ".dim(), + " ".dim(), + Style::default().fg(Color::Cyan), + ) + }; + lines.extend(wrap_with_prefix(¬e, width, label, continuation, style)); + } + } + + if self.interrupted && unanswered > 0 { + let summary = format!("interrupted with {unanswered} unanswered"); + lines.extend(wrap_with_prefix( + &summary, + width, + " ↳ ".cyan().dim(), + " ".dim(), + Style::default().fg(Color::Cyan).add_modifier(Modifier::DIM), + )); + } + + lines + } +} + +/// Wrap a plain string with textwrap and prefix each line, while applying a style to the content. +fn wrap_with_prefix( + text: &str, + width: usize, + initial_prefix: Span<'static>, + subsequent_prefix: Span<'static>, + style: Style, +) -> Vec> { + let prefix_width = initial_prefix + .content + .width() + .max(subsequent_prefix.content.width()); + let wrap_width = width.saturating_sub(prefix_width).max(1); + let wrapped = textwrap::wrap(text, wrap_width); + let wrapped_lines = wrapped + .into_iter() + .map(|segment| Span::from(segment.to_string()).set_style(style).into()) + .collect::>>(); + prefix_lines(wrapped_lines, initial_prefix, subsequent_prefix) +} + +/// Split a request_user_input answer into option labels and an optional freeform note. +/// Notes are encoded as "user_note: " entries in the answers list. +fn split_request_user_input_answer( + answer: &RequestUserInputAnswer, +) -> (Vec, Option) { + let mut options = Vec::new(); + let mut note = None; + for entry in &answer.answers { + if let Some(note_text) = entry.strip_prefix("user_note: ") { + note = Some(note_text.to_string()); + } else { + options.push(entry.clone()); + } + } + (options, note) +} + /// Render a user‑friendly plan update styled like a checkbox todo list. pub(crate) fn new_plan_update(update: UpdatePlanArgs) -> PlanUpdateCell { let UpdatePlanArgs { explanation, plan } = update; From 66447d5d2cbe390fbe6468f2c4395197fce72935 Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Mon, 2 Feb 2026 17:41:55 -0800 Subject: [PATCH 18/28] feat: replace custom mcp-types crate with equivalents from rmcp (#10349) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We started working with MCP in Codex before https://crates.io/crates/rmcp was mature, so we had our own crate for MCP types that was generated from the MCP schema: https://github.com/openai/codex/blob/8b95d3e082376f4cb23e92641705a22afb28a9da/codex-rs/mcp-types/README.md Now that `rmcp` is more mature, it makes more sense to use their MCP types in Rust, as they handle details (like the `_meta` field) that our custom version ignored. Though one advantage that our custom types had is that our generated types implemented `JsonSchema` and `ts_rs::TS`, whereas the types in `rmcp` do not. As such, part of the work of this PR is leveraging the adapters between `rmcp` types and the serializable types that are API for us (app server and MCP) introduced in #10356. Note this PR results in a number of changes to `codex-rs/app-server-protocol/schema`, which merit special attention during review. We must ensure that these changes are still backwards-compatible, which is possible because we have: ```diff - export type CallToolResult = { content: Array, isError?: boolean, structuredContent?: JsonValue, }; + export type CallToolResult = { content: Array, structuredContent?: JsonValue, isError?: boolean, _meta?: JsonValue, }; ``` so `ContentBlock` has been replaced with the more general `JsonValue`. Note that `ContentBlock` was defined as: ```typescript export type ContentBlock = TextContent | ImageContent | AudioContent | ResourceLink | EmbeddedResource; ``` so the deletion of those individual variants should not be a cause of great concern. Similarly, we have the following change in `codex-rs/app-server-protocol/schema/typescript/Tool.ts`: ``` - export type Tool = { annotations?: ToolAnnotations, description?: string, inputSchema: ToolInputSchema, name: string, outputSchema?: ToolOutputSchema, title?: string, }; + export type Tool = { name: string, title?: string, description?: string, inputSchema: JsonValue, outputSchema?: JsonValue, annotations?: JsonValue, icons?: Array, _meta?: JsonValue, }; ``` so: - `annotations?: ToolAnnotations` ➡️ `JsonValue` - `inputSchema: ToolInputSchema` ➡️ `JsonValue` - `outputSchema?: ToolOutputSchema` ➡️ `JsonValue` and two new fields: `icons?: Array, _meta?: JsonValue` --- [//]: # (BEGIN SAPLING FOOTER) Stack created with [Sapling](https://sapling-scm.com). Best reviewed with [ReviewStack](https://reviewstack.dev/openai/codex/pull/10349). * #10357 * __->__ #10349 * #10356 --- codex-rs/Cargo.lock | 14 +- codex-rs/Cargo.toml | 1 - codex-rs/app-server-protocol/Cargo.toml | 1 - .../schema/json/EventMsg.json | 420 +-------- .../schema/json/ServerNotification.json | 424 +-------- .../codex_app_server_protocol.schemas.json | 836 +----------------- .../json/v1/ForkConversationResponse.json | 420 +-------- .../json/v1/ResumeConversationResponse.json | 420 +-------- .../v1/SessionConfiguredNotification.json | 420 +-------- .../json/v2/ItemCompletedNotification.json | 278 +----- .../json/v2/ItemStartedNotification.json | 278 +----- .../json/v2/ListMcpServerStatusResponse.json | 174 +--- .../schema/json/v2/ReviewStartResponse.json | 278 +----- .../schema/json/v2/ThreadForkResponse.json | 278 +----- .../schema/json/v2/ThreadListResponse.json | 278 +----- .../schema/json/v2/ThreadReadResponse.json | 278 +----- .../schema/json/v2/ThreadResumeResponse.json | 278 +----- .../json/v2/ThreadRollbackResponse.json | 278 +----- .../schema/json/v2/ThreadStartResponse.json | 278 +----- .../json/v2/ThreadStartedNotification.json | 278 +----- .../json/v2/ThreadUnarchiveResponse.json | 278 +----- .../json/v2/TurnCompletedNotification.json | 278 +----- .../schema/json/v2/TurnStartResponse.json | 278 +----- .../json/v2/TurnStartedNotification.json | 278 +----- .../schema/typescript/Annotations.ts | 9 - .../schema/typescript/AudioContent.ts | 9 - .../schema/typescript/BlobResourceContents.ts | 5 - .../schema/typescript/CallToolResult.ts | 3 +- .../schema/typescript/ContentBlock.ts | 10 - .../typescript/ElicitationRequestEvent.ts | 3 +- .../schema/typescript/EmbeddedResource.ts | 13 - .../typescript/EmbeddedResourceResource.ts | 7 - .../schema/typescript/ImageContent.ts | 9 - .../schema/typescript/Resource.ts | 4 +- .../schema/typescript/ResourceLink.ts | 11 - .../schema/typescript/ResourceTemplate.ts | 4 +- .../schema/typescript/Role.ts | 8 - .../schema/typescript/TextContent.ts | 9 - .../schema/typescript/TextResourceContents.ts | 5 - .../schema/typescript/Tool.ts | 6 +- .../schema/typescript/ToolAnnotations.ts | 15 - .../schema/typescript/ToolInputSchema.ts | 9 - .../schema/typescript/ToolOutputSchema.ts | 10 - .../schema/typescript/index.ts | 14 - .../schema/typescript/v2/McpToolCallResult.ts | 3 +- .../app-server-protocol/src/protocol/v2.rs | 14 +- .../src/schema_fixtures.rs | 90 +- codex-rs/app-server-test-client/Cargo.lock | 11 - codex-rs/app-server/Cargo.toml | 2 - .../app-server/src/bespoke_event_handling.rs | 15 +- codex-rs/core/Cargo.toml | 7 +- codex-rs/core/src/codex.rs | 55 +- codex-rs/core/src/mcp/mod.rs | 122 ++- codex-rs/core/src/mcp_connection_manager.rs | 111 ++- codex-rs/core/src/mcp_tool_call.rs | 9 +- codex-rs/core/src/tools/context.rs | 2 +- .../core/src/tools/handlers/mcp_resource.rs | 66 +- codex-rs/core/src/tools/router.rs | 3 +- codex-rs/core/src/tools/spec.rs | 295 +++--- codex-rs/core/tests/common/lib.rs | 2 +- codex-rs/core/tests/suite/rmcp_client.rs | 13 +- codex-rs/exec/Cargo.toml | 3 +- .../src/event_processor_with_human_output.rs | 3 +- codex-rs/exec/src/exec_events.rs | 10 +- .../tests/event_processor_with_json_output.rs | 19 +- codex-rs/mcp-server/Cargo.toml | 2 +- codex-rs/mcp-server/src/codex_tool_config.rs | 81 +- codex-rs/mcp-server/src/codex_tool_runner.rs | 33 +- codex-rs/mcp-server/src/error_code.rs | 2 - codex-rs/mcp-server/src/exec_approval.rs | 35 +- codex-rs/mcp-server/src/lib.rs | 27 +- codex-rs/mcp-server/src/message_processor.rs | 517 +++++------ codex-rs/mcp-server/src/outgoing_message.rs | 125 ++- codex-rs/mcp-server/src/patch_approval.rs | 30 +- codex-rs/mcp-server/tests/common/Cargo.toml | 2 +- codex-rs/mcp-server/tests/common/lib.rs | 6 +- .../mcp-server/tests/common/mcp_process.rs | 168 ++-- codex-rs/mcp-server/tests/suite/codex_tool.rs | 158 ++-- codex-rs/protocol/Cargo.toml | 1 - codex-rs/protocol/src/approvals.rs | 3 +- codex-rs/protocol/src/mcp.rs | 16 +- codex-rs/protocol/src/models.rs | 149 +++- codex-rs/protocol/src/protocol.rs | 10 +- codex-rs/rmcp-client/Cargo.toml | 1 - .../rmcp-client/src/logging_client_handler.rs | 7 +- codex-rs/rmcp-client/src/rmcp_client.rs | 101 +-- codex-rs/rmcp-client/src/utils.rs | 84 -- codex-rs/rmcp-client/tests/resources.rs | 79 +- codex-rs/tui/Cargo.toml | 2 +- .../tui/src/bottom_pane/approval_overlay.rs | 2 +- ...et__tests__binary_size_ideal_response.snap | 2 +- codex-rs/tui/src/history_cell.rs | 219 +++-- 92 files changed, 1629 insertions(+), 8273 deletions(-) delete mode 100644 codex-rs/app-server-protocol/schema/typescript/Annotations.ts delete mode 100644 codex-rs/app-server-protocol/schema/typescript/AudioContent.ts delete mode 100644 codex-rs/app-server-protocol/schema/typescript/BlobResourceContents.ts delete mode 100644 codex-rs/app-server-protocol/schema/typescript/ContentBlock.ts delete mode 100644 codex-rs/app-server-protocol/schema/typescript/EmbeddedResource.ts delete mode 100644 codex-rs/app-server-protocol/schema/typescript/EmbeddedResourceResource.ts delete mode 100644 codex-rs/app-server-protocol/schema/typescript/ImageContent.ts delete mode 100644 codex-rs/app-server-protocol/schema/typescript/ResourceLink.ts delete mode 100644 codex-rs/app-server-protocol/schema/typescript/Role.ts delete mode 100644 codex-rs/app-server-protocol/schema/typescript/TextContent.ts delete mode 100644 codex-rs/app-server-protocol/schema/typescript/TextResourceContents.ts delete mode 100644 codex-rs/app-server-protocol/schema/typescript/ToolAnnotations.ts delete mode 100644 codex-rs/app-server-protocol/schema/typescript/ToolInputSchema.ts delete mode 100644 codex-rs/app-server-protocol/schema/typescript/ToolOutputSchema.ts delete mode 100644 codex-rs/mcp-server/src/error_code.rs diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index 716bc4cd5a..bc8cac244c 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -1108,7 +1108,6 @@ dependencies = [ "codex-utils-absolute-path", "codex-utils-json-to-toml", "core_test_support", - "mcp-types", "os_info", "pretty_assertions", "rmcp", @@ -1137,7 +1136,6 @@ dependencies = [ "codex-utils-absolute-path", "codex-utils-cargo-bin", "inventory", - "mcp-types", "pretty_assertions", "schemars 0.8.22", "serde", @@ -1437,7 +1435,6 @@ dependencies = [ "landlock", "libc", "maplit", - "mcp-types", "multimap", "once_cell", "openssl-sys", @@ -1449,6 +1446,7 @@ dependencies = [ "regex", "regex-lite", "reqwest", + "rmcp", "schemars 0.8.22", "seccompiler", "serde", @@ -1512,10 +1510,10 @@ dependencies = [ "codex-utils-cargo-bin", "core_test_support", "libc", - "mcp-types", "owo-colors", "predicates", "pretty_assertions", + "rmcp", "serde", "serde_json", "shlex", @@ -1717,10 +1715,10 @@ dependencies = [ "codex-protocol", "codex-utils-json-to-toml", "core_test_support", - "mcp-types", "mcp_test_support", "os_info", "pretty_assertions", + "rmcp", "schemars 0.8.22", "serde", "serde_json", @@ -1829,7 +1827,6 @@ dependencies = [ "icu_decimal", "icu_locale_core", "icu_provider", - "mcp-types", "mime_guess", "pretty_assertions", "schemars 0.8.22", @@ -1873,7 +1870,6 @@ dependencies = [ "codex-utils-home-dir", "futures", "keyring", - "mcp-types", "oauth2", "pretty_assertions", "reqwest", @@ -1966,7 +1962,6 @@ dependencies = [ "itertools 0.14.0", "lazy_static", "libc", - "mcp-types", "pathdiff", "pretty_assertions", "pulldown-cmark", @@ -1975,6 +1970,7 @@ dependencies = [ "ratatui-macros", "regex-lite", "reqwest", + "rmcp", "serde", "serde_json", "serial_test", @@ -4672,9 +4668,9 @@ dependencies = [ "codex-mcp-server", "codex-utils-cargo-bin", "core_test_support", - "mcp-types", "os_info", "pretty_assertions", + "rmcp", "serde", "serde_json", "shlex", diff --git a/codex-rs/Cargo.toml b/codex-rs/Cargo.toml index 3171c8a96c..a452e98e7c 100644 --- a/codex-rs/Cargo.toml +++ b/codex-rs/Cargo.toml @@ -112,7 +112,6 @@ codex-utils-string = { path = "utils/string" } codex-windows-sandbox = { path = "windows-sandbox-rs" } core_test_support = { path = "core/tests/common" } exec_server_test_support = { path = "exec-server/tests/common" } -mcp-types = { path = "mcp-types" } mcp_test_support = { path = "mcp-server/tests/common" } # External diff --git a/codex-rs/app-server-protocol/Cargo.toml b/codex-rs/app-server-protocol/Cargo.toml index 2788801051..7b2b8c53d7 100644 --- a/codex-rs/app-server-protocol/Cargo.toml +++ b/codex-rs/app-server-protocol/Cargo.toml @@ -17,7 +17,6 @@ clap = { workspace = true, features = ["derive"] } codex-protocol = { workspace = true } codex-experimental-api-macros = { workspace = true } codex-utils-absolute-path = { workspace = true } -mcp-types = { workspace = true } schemars = { workspace = true } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } diff --git a/codex-rs/app-server-protocol/schema/json/EventMsg.json b/codex-rs/app-server-protocol/schema/json/EventMsg.json index 407626d82d..9e3cbb4156 100644 --- a/codex-rs/app-server-protocol/schema/json/EventMsg.json +++ b/codex-rs/app-server-protocol/schema/json/EventMsg.json @@ -93,34 +93,6 @@ } ] }, - "Annotations": { - "description": "Optional annotations for the client. The client can use annotations to inform how objects are used or displayed", - "properties": { - "audience": { - "items": { - "$ref": "#/definitions/Role" - }, - "type": [ - "array", - "null" - ] - }, - "lastModified": { - "type": [ - "string", - "null" - ] - }, - "priority": { - "format": "double", - "type": [ - "number", - "null" - ] - } - }, - "type": "object" - }, "AskForApproval": { "description": "Determines the conditions under which the user is consulted to approve running the command proposed by Codex.", "oneOf": [ @@ -154,57 +126,6 @@ } ] }, - "AudioContent": { - "description": "Audio provided to or from an LLM.", - "properties": { - "annotations": { - "anyOf": [ - { - "$ref": "#/definitions/Annotations" - }, - { - "type": "null" - } - ] - }, - "data": { - "type": "string" - }, - "mimeType": { - "type": "string" - }, - "type": { - "type": "string" - } - }, - "required": [ - "data", - "mimeType", - "type" - ], - "type": "object" - }, - "BlobResourceContents": { - "properties": { - "blob": { - "type": "string" - }, - "mimeType": { - "type": [ - "string", - "null" - ] - }, - "uri": { - "type": "string" - } - }, - "required": [ - "blob", - "uri" - ], - "type": "object" - }, "ByteRange": { "properties": { "end": { @@ -229,10 +150,9 @@ "CallToolResult": { "description": "The server's response to a tool call.", "properties": { + "_meta": true, "content": { - "items": { - "$ref": "#/definitions/ContentBlock" - }, + "items": true, "type": "array" }, "isError": { @@ -390,25 +310,6 @@ } ] }, - "ContentBlock": { - "anyOf": [ - { - "$ref": "#/definitions/TextContent" - }, - { - "$ref": "#/definitions/ImageContent" - }, - { - "$ref": "#/definitions/AudioContent" - }, - { - "$ref": "#/definitions/ResourceLink" - }, - { - "$ref": "#/definitions/EmbeddedResource" - } - ] - }, "ContentItem": { "oneOf": [ { @@ -544,42 +445,6 @@ ], "type": "object" }, - "EmbeddedResource": { - "description": "The contents of a resource, embedded into a prompt or tool call result.\n\nIt is up to the client how best to render embedded resources for the benefit of the LLM and/or the user.", - "properties": { - "annotations": { - "anyOf": [ - { - "$ref": "#/definitions/Annotations" - }, - { - "type": "null" - } - ] - }, - "resource": { - "$ref": "#/definitions/EmbeddedResourceResource" - }, - "type": { - "type": "string" - } - }, - "required": [ - "resource", - "type" - ], - "type": "object" - }, - "EmbeddedResourceResource": { - "anyOf": [ - { - "$ref": "#/definitions/TextResourceContents" - }, - { - "$ref": "#/definitions/BlobResourceContents" - } - ] - }, "EventMsg": { "description": "Response event from the agent NOTE: Make sure none of these values have optional types, as it will mess up the extension code-gen.", "oneOf": [ @@ -3071,36 +2936,6 @@ ], "type": "object" }, - "ImageContent": { - "description": "An image provided to or from an LLM.", - "properties": { - "annotations": { - "anyOf": [ - { - "$ref": "#/definitions/Annotations" - }, - { - "type": "null" - } - ] - }, - "data": { - "type": "string" - }, - "mimeType": { - "type": "string" - }, - "type": { - "type": "string" - } - }, - "required": [ - "data", - "mimeType", - "type" - ], - "type": "object" - }, "LocalShellAction": { "oneOf": [ { @@ -3599,7 +3434,8 @@ "format": "int64", "type": "integer" } - ] + ], + "description": "ID of a request, which can be either a string or an integer." }, "RequestUserInputQuestion": { "properties": { @@ -3655,22 +3491,21 @@ "Resource": { "description": "A known resource that the server is capable of reading.", "properties": { - "annotations": { - "anyOf": [ - { - "$ref": "#/definitions/Annotations" - }, - { - "type": "null" - } - ] - }, + "_meta": true, + "annotations": true, "description": { "type": [ "string", "null" ] }, + "icons": { + "items": true, + "type": [ + "array", + "null" + ] + }, "mimeType": { "type": [ "string", @@ -3703,74 +3538,10 @@ ], "type": "object" }, - "ResourceLink": { - "description": "A resource that the server is capable of reading, included in a prompt or tool call result.\n\nNote: resource links returned by tools are not guaranteed to appear in the results of `resources/list` requests.", - "properties": { - "annotations": { - "anyOf": [ - { - "$ref": "#/definitions/Annotations" - }, - { - "type": "null" - } - ] - }, - "description": { - "type": [ - "string", - "null" - ] - }, - "mimeType": { - "type": [ - "string", - "null" - ] - }, - "name": { - "type": "string" - }, - "size": { - "format": "int64", - "type": [ - "integer", - "null" - ] - }, - "title": { - "type": [ - "string", - "null" - ] - }, - "type": { - "type": "string" - }, - "uri": { - "type": "string" - } - }, - "required": [ - "name", - "type", - "uri" - ], - "type": "object" - }, "ResourceTemplate": { "description": "A template description for resources available on the server.", "properties": { - "annotations": { - "anyOf": [ - { - "$ref": "#/definitions/Annotations" - }, - { - "type": "null" - } - ] - }, + "annotations": true, "description": { "type": [ "string", @@ -4361,14 +4132,6 @@ } ] }, - "Role": { - "description": "The sender or recipient of messages and data in a conversation.", - "enum": [ - "assistant", - "user" - ], - "type": "string" - }, "SandboxPolicy": { "description": "Determines execution restrictions for model shell commands.", "oneOf": [ @@ -4678,32 +4441,6 @@ ], "type": "string" }, - "TextContent": { - "description": "Text provided to or from an LLM.", - "properties": { - "annotations": { - "anyOf": [ - { - "$ref": "#/definitions/Annotations" - }, - { - "type": "null" - } - ] - }, - "text": { - "type": "string" - }, - "type": { - "type": "string" - } - }, - "required": [ - "text", - "type" - ], - "type": "object" - }, "TextElement": { "properties": { "byte_range": { @@ -4727,27 +4464,6 @@ ], "type": "object" }, - "TextResourceContents": { - "properties": { - "mimeType": { - "type": [ - "string", - "null" - ] - }, - "text": { - "type": "string" - }, - "uri": { - "type": "string" - } - }, - "required": [ - "text", - "uri" - ], - "type": "object" - }, "ThreadId": { "type": "string" }, @@ -4808,38 +4524,26 @@ "Tool": { "description": "Definition for a tool the client can call.", "properties": { - "annotations": { - "anyOf": [ - { - "$ref": "#/definitions/ToolAnnotations" - }, - { - "type": "null" - } - ] - }, + "_meta": true, + "annotations": true, "description": { "type": [ "string", "null" ] }, - "inputSchema": { - "$ref": "#/definitions/ToolInputSchema" + "icons": { + "items": true, + "type": [ + "array", + "null" + ] }, + "inputSchema": true, "name": { "type": "string" }, - "outputSchema": { - "anyOf": [ - { - "$ref": "#/definitions/ToolOutputSchema" - }, - { - "type": "null" - } - ] - }, + "outputSchema": true, "title": { "type": [ "string", @@ -4853,82 +4557,6 @@ ], "type": "object" }, - "ToolAnnotations": { - "description": "Additional properties describing a Tool to clients.\n\nNOTE: all properties in ToolAnnotations are **hints**. They are not guaranteed to provide a faithful description of tool behavior (including descriptive properties like `title`).\n\nClients should never make tool use decisions based on ToolAnnotations received from untrusted servers.", - "properties": { - "destructiveHint": { - "type": [ - "boolean", - "null" - ] - }, - "idempotentHint": { - "type": [ - "boolean", - "null" - ] - }, - "openWorldHint": { - "type": [ - "boolean", - "null" - ] - }, - "readOnlyHint": { - "type": [ - "boolean", - "null" - ] - }, - "title": { - "type": [ - "string", - "null" - ] - } - }, - "type": "object" - }, - "ToolInputSchema": { - "description": "A JSON Schema object defining the expected parameters for the tool.", - "properties": { - "properties": true, - "required": { - "items": { - "type": "string" - }, - "type": [ - "array", - "null" - ] - }, - "type": { - "default": "object", - "type": "string" - } - }, - "type": "object" - }, - "ToolOutputSchema": { - "description": "An optional JSON Schema object defining the structure of the tool's output returned in the structuredContent field of a CallToolResult.", - "properties": { - "properties": true, - "required": { - "items": { - "type": "string" - }, - "type": [ - "array", - "null" - ] - }, - "type": { - "default": "object", - "type": "string" - } - }, - "type": "object" - }, "TurnAbortReason": { "enum": [ "interrupted", diff --git a/codex-rs/app-server-protocol/schema/json/ServerNotification.json b/codex-rs/app-server-protocol/schema/json/ServerNotification.json index b6777d5c8b..6df9a010b1 100644 --- a/codex-rs/app-server-protocol/schema/json/ServerNotification.json +++ b/codex-rs/app-server-protocol/schema/json/ServerNotification.json @@ -165,34 +165,6 @@ } ] }, - "Annotations": { - "description": "Optional annotations for the client. The client can use annotations to inform how objects are used or displayed", - "properties": { - "audience": { - "items": { - "$ref": "#/definitions/Role" - }, - "type": [ - "array", - "null" - ] - }, - "lastModified": { - "type": [ - "string", - "null" - ] - }, - "priority": { - "format": "double", - "type": [ - "number", - "null" - ] - } - }, - "type": "object" - }, "AskForApproval": { "description": "Determines the conditions under which the user is consulted to approve running the command proposed by Codex.", "oneOf": [ @@ -226,36 +198,6 @@ } ] }, - "AudioContent": { - "description": "Audio provided to or from an LLM.", - "properties": { - "annotations": { - "anyOf": [ - { - "$ref": "#/definitions/Annotations" - }, - { - "type": "null" - } - ] - }, - "data": { - "type": "string" - }, - "mimeType": { - "type": "string" - }, - "type": { - "type": "string" - } - }, - "required": [ - "data", - "mimeType", - "type" - ], - "type": "object" - }, "AuthMode": { "description": "Authentication mode for OpenAI-backed providers.", "oneOf": [ @@ -298,27 +240,6 @@ }, "type": "object" }, - "BlobResourceContents": { - "properties": { - "blob": { - "type": "string" - }, - "mimeType": { - "type": [ - "string", - "null" - ] - }, - "uri": { - "type": "string" - } - }, - "required": [ - "blob", - "uri" - ], - "type": "object" - }, "ByteRange": { "properties": { "end": { @@ -362,10 +283,9 @@ "CallToolResult": { "description": "The server's response to a tool call.", "properties": { + "_meta": true, "content": { - "items": { - "$ref": "#/definitions/ContentBlock" - }, + "items": true, "type": "array" }, "isError": { @@ -889,25 +809,6 @@ ], "type": "object" }, - "ContentBlock": { - "anyOf": [ - { - "$ref": "#/definitions/TextContent" - }, - { - "$ref": "#/definitions/ImageContent" - }, - { - "$ref": "#/definitions/AudioContent" - }, - { - "$ref": "#/definitions/ResourceLink" - }, - { - "$ref": "#/definitions/EmbeddedResource" - } - ] - }, "ContentItem": { "oneOf": [ { @@ -1099,42 +1000,6 @@ ], "type": "object" }, - "EmbeddedResource": { - "description": "The contents of a resource, embedded into a prompt or tool call result.\n\nIt is up to the client how best to render embedded resources for the benefit of the LLM and/or the user.", - "properties": { - "annotations": { - "anyOf": [ - { - "$ref": "#/definitions/Annotations" - }, - { - "type": "null" - } - ] - }, - "resource": { - "$ref": "#/definitions/EmbeddedResourceResource" - }, - "type": { - "type": "string" - } - }, - "required": [ - "resource", - "type" - ], - "type": "object" - }, - "EmbeddedResourceResource": { - "anyOf": [ - { - "$ref": "#/definitions/TextResourceContents" - }, - { - "$ref": "#/definitions/BlobResourceContents" - } - ] - }, "ErrorNotification": { "properties": { "error": { @@ -3714,36 +3579,6 @@ ], "type": "object" }, - "ImageContent": { - "description": "An image provided to or from an LLM.", - "properties": { - "annotations": { - "anyOf": [ - { - "$ref": "#/definitions/Annotations" - }, - { - "type": "null" - } - ] - }, - "data": { - "type": "string" - }, - "mimeType": { - "type": "string" - }, - "type": { - "type": "string" - } - }, - "required": [ - "data", - "mimeType", - "type" - ], - "type": "object" - }, "ItemCompletedNotification": { "properties": { "item": { @@ -4037,9 +3872,7 @@ "McpToolCallResult": { "properties": { "content": { - "items": { - "$ref": "#/definitions/ContentBlock" - }, + "items": true, "type": "array" }, "structuredContent": true @@ -4641,7 +4474,8 @@ "format": "int64", "type": "integer" } - ] + ], + "description": "ID of a request, which can be either a string or an integer." }, "RequestUserInputQuestion": { "properties": { @@ -4697,22 +4531,21 @@ "Resource": { "description": "A known resource that the server is capable of reading.", "properties": { - "annotations": { - "anyOf": [ - { - "$ref": "#/definitions/Annotations" - }, - { - "type": "null" - } - ] - }, + "_meta": true, + "annotations": true, "description": { "type": [ "string", "null" ] }, + "icons": { + "items": true, + "type": [ + "array", + "null" + ] + }, "mimeType": { "type": [ "string", @@ -4745,74 +4578,10 @@ ], "type": "object" }, - "ResourceLink": { - "description": "A resource that the server is capable of reading, included in a prompt or tool call result.\n\nNote: resource links returned by tools are not guaranteed to appear in the results of `resources/list` requests.", - "properties": { - "annotations": { - "anyOf": [ - { - "$ref": "#/definitions/Annotations" - }, - { - "type": "null" - } - ] - }, - "description": { - "type": [ - "string", - "null" - ] - }, - "mimeType": { - "type": [ - "string", - "null" - ] - }, - "name": { - "type": "string" - }, - "size": { - "format": "int64", - "type": [ - "integer", - "null" - ] - }, - "title": { - "type": [ - "string", - "null" - ] - }, - "type": { - "type": "string" - }, - "uri": { - "type": "string" - } - }, - "required": [ - "name", - "type", - "uri" - ], - "type": "object" - }, "ResourceTemplate": { "description": "A template description for resources available on the server.", "properties": { - "annotations": { - "anyOf": [ - { - "$ref": "#/definitions/Annotations" - }, - { - "type": "null" - } - ] - }, + "annotations": true, "description": { "type": [ "string", @@ -5403,14 +5172,6 @@ } ] }, - "Role": { - "description": "The sender or recipient of messages and data in a conversation.", - "enum": [ - "assistant", - "user" - ], - "type": "string" - }, "SandboxPolicy": { "description": "Determines execution restrictions for model shell commands.", "oneOf": [ @@ -5874,32 +5635,6 @@ ], "type": "object" }, - "TextContent": { - "description": "Text provided to or from an LLM.", - "properties": { - "annotations": { - "anyOf": [ - { - "$ref": "#/definitions/Annotations" - }, - { - "type": "null" - } - ] - }, - "text": { - "type": "string" - }, - "type": { - "type": "string" - } - }, - "required": [ - "text", - "type" - ], - "type": "object" - }, "TextElement": { "properties": { "byteRange": { @@ -5982,27 +5717,6 @@ ], "type": "object" }, - "TextResourceContents": { - "properties": { - "mimeType": { - "type": [ - "string", - "null" - ] - }, - "text": { - "type": "string" - }, - "uri": { - "type": "string" - } - }, - "required": [ - "text", - "uri" - ], - "type": "object" - }, "Thread": { "properties": { "cliVersion": { @@ -6714,38 +6428,26 @@ "Tool": { "description": "Definition for a tool the client can call.", "properties": { - "annotations": { - "anyOf": [ - { - "$ref": "#/definitions/ToolAnnotations" - }, - { - "type": "null" - } - ] - }, + "_meta": true, + "annotations": true, "description": { "type": [ "string", "null" ] }, - "inputSchema": { - "$ref": "#/definitions/ToolInputSchema" + "icons": { + "items": true, + "type": [ + "array", + "null" + ] }, + "inputSchema": true, "name": { "type": "string" }, - "outputSchema": { - "anyOf": [ - { - "$ref": "#/definitions/ToolOutputSchema" - }, - { - "type": "null" - } - ] - }, + "outputSchema": true, "title": { "type": [ "string", @@ -6759,82 +6461,6 @@ ], "type": "object" }, - "ToolAnnotations": { - "description": "Additional properties describing a Tool to clients.\n\nNOTE: all properties in ToolAnnotations are **hints**. They are not guaranteed to provide a faithful description of tool behavior (including descriptive properties like `title`).\n\nClients should never make tool use decisions based on ToolAnnotations received from untrusted servers.", - "properties": { - "destructiveHint": { - "type": [ - "boolean", - "null" - ] - }, - "idempotentHint": { - "type": [ - "boolean", - "null" - ] - }, - "openWorldHint": { - "type": [ - "boolean", - "null" - ] - }, - "readOnlyHint": { - "type": [ - "boolean", - "null" - ] - }, - "title": { - "type": [ - "string", - "null" - ] - } - }, - "type": "object" - }, - "ToolInputSchema": { - "description": "A JSON Schema object defining the expected parameters for the tool.", - "properties": { - "properties": true, - "required": { - "items": { - "type": "string" - }, - "type": [ - "array", - "null" - ] - }, - "type": { - "default": "object", - "type": "string" - } - }, - "type": "object" - }, - "ToolOutputSchema": { - "description": "An optional JSON Schema object defining the structure of the tool's output returned in the structuredContent field of a CallToolResult.", - "properties": { - "properties": true, - "required": { - "items": { - "type": "string" - }, - "type": [ - "array", - "null" - ] - }, - "type": { - "default": "object", - "type": "string" - } - }, - "type": "object" - }, "Turn": { "properties": { "error": { 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 aca840f6cb..e92d46e7b6 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 @@ -123,34 +123,6 @@ } ] }, - "Annotations": { - "description": "Optional annotations for the client. The client can use annotations to inform how objects are used or displayed", - "properties": { - "audience": { - "items": { - "$ref": "#/definitions/Role" - }, - "type": [ - "array", - "null" - ] - }, - "lastModified": { - "type": [ - "string", - "null" - ] - }, - "priority": { - "format": "double", - "type": [ - "number", - "null" - ] - } - }, - "type": "object" - }, "ApplyPatchApprovalParams": { "$schema": "http://json-schema.org/draft-07/schema#", "properties": { @@ -258,36 +230,6 @@ } ] }, - "AudioContent": { - "description": "Audio provided to or from an LLM.", - "properties": { - "annotations": { - "anyOf": [ - { - "$ref": "#/definitions/Annotations" - }, - { - "type": "null" - } - ] - }, - "data": { - "type": "string" - }, - "mimeType": { - "type": "string" - }, - "type": { - "type": "string" - } - }, - "required": [ - "data", - "mimeType", - "type" - ], - "type": "object" - }, "AuthMode": { "description": "Authentication mode for OpenAI-backed providers.", "oneOf": [ @@ -332,27 +274,6 @@ "title": "AuthStatusChangeNotification", "type": "object" }, - "BlobResourceContents": { - "properties": { - "blob": { - "type": "string" - }, - "mimeType": { - "type": [ - "string", - "null" - ] - }, - "uri": { - "type": "string" - } - }, - "required": [ - "blob", - "uri" - ], - "type": "object" - }, "ByteRange": { "properties": { "end": { @@ -377,10 +298,9 @@ "CallToolResult": { "description": "The server's response to a tool call.", "properties": { + "_meta": true, "content": { - "items": { - "$ref": "#/definitions/ContentBlock" - }, + "items": true, "type": "array" }, "isError": { @@ -2105,25 +2025,6 @@ "title": "CommandExecutionRequestApprovalResponse", "type": "object" }, - "ContentBlock": { - "anyOf": [ - { - "$ref": "#/definitions/TextContent" - }, - { - "$ref": "#/definitions/ImageContent" - }, - { - "$ref": "#/definitions/AudioContent" - }, - { - "$ref": "#/definitions/ResourceLink" - }, - { - "$ref": "#/definitions/EmbeddedResource" - } - ] - }, "ContentItem": { "oneOf": [ { @@ -2383,42 +2284,6 @@ "title": "DynamicToolCallResponse", "type": "object" }, - "EmbeddedResource": { - "description": "The contents of a resource, embedded into a prompt or tool call result.\n\nIt is up to the client how best to render embedded resources for the benefit of the LLM and/or the user.", - "properties": { - "annotations": { - "anyOf": [ - { - "$ref": "#/definitions/Annotations" - }, - { - "type": "null" - } - ] - }, - "resource": { - "$ref": "#/definitions/EmbeddedResourceResource" - }, - "type": { - "type": "string" - } - }, - "required": [ - "resource", - "type" - ], - "type": "object" - }, - "EmbeddedResourceResource": { - "anyOf": [ - { - "$ref": "#/definitions/TextResourceContents" - }, - { - "$ref": "#/definitions/BlobResourceContents" - } - ] - }, "EventMsg": { "$schema": "http://json-schema.org/draft-07/schema#", "description": "Response event from the agent NOTE: Make sure none of these values have optional types, as it will mess up the extension code-gen.", @@ -5408,36 +5273,6 @@ ], "type": "object" }, - "ImageContent": { - "description": "An image provided to or from an LLM.", - "properties": { - "annotations": { - "anyOf": [ - { - "$ref": "#/definitions/Annotations" - }, - { - "type": "null" - } - ] - }, - "data": { - "type": "string" - }, - "mimeType": { - "type": "string" - }, - "type": { - "type": "string" - } - }, - "required": [ - "data", - "mimeType", - "type" - ], - "type": "object" - }, "InitializeCapabilities": { "description": "Client-declared capabilities negotiated during initialize.", "properties": { @@ -6539,7 +6374,8 @@ "format": "int64", "type": "integer" } - ] + ], + "description": "ID of a request, which can be either a string or an integer." }, "RequestUserInputQuestion": { "properties": { @@ -6595,22 +6431,21 @@ "Resource": { "description": "A known resource that the server is capable of reading.", "properties": { - "annotations": { - "anyOf": [ - { - "$ref": "#/definitions/Annotations" - }, - { - "type": "null" - } - ] - }, + "_meta": true, + "annotations": true, "description": { "type": [ "string", "null" ] }, + "icons": { + "items": true, + "type": [ + "array", + "null" + ] + }, "mimeType": { "type": [ "string", @@ -6643,74 +6478,10 @@ ], "type": "object" }, - "ResourceLink": { - "description": "A resource that the server is capable of reading, included in a prompt or tool call result.\n\nNote: resource links returned by tools are not guaranteed to appear in the results of `resources/list` requests.", - "properties": { - "annotations": { - "anyOf": [ - { - "$ref": "#/definitions/Annotations" - }, - { - "type": "null" - } - ] - }, - "description": { - "type": [ - "string", - "null" - ] - }, - "mimeType": { - "type": [ - "string", - "null" - ] - }, - "name": { - "type": "string" - }, - "size": { - "format": "int64", - "type": [ - "integer", - "null" - ] - }, - "title": { - "type": [ - "string", - "null" - ] - }, - "type": { - "type": "string" - }, - "uri": { - "type": "string" - } - }, - "required": [ - "name", - "type", - "uri" - ], - "type": "object" - }, "ResourceTemplate": { "description": "A template description for resources available on the server.", "properties": { - "annotations": { - "anyOf": [ - { - "$ref": "#/definitions/Annotations" - }, - { - "type": "null" - } - ] - }, + "annotations": true, "description": { "type": [ "string", @@ -7431,14 +7202,6 @@ } ] }, - "Role": { - "description": "The sender or recipient of messages and data in a conversation.", - "enum": [ - "assistant", - "user" - ], - "type": "string" - }, "SandboxMode": { "enum": [ "read-only", @@ -8841,32 +8604,6 @@ } ] }, - "TextContent": { - "description": "Text provided to or from an LLM.", - "properties": { - "annotations": { - "anyOf": [ - { - "$ref": "#/definitions/Annotations" - }, - { - "type": "null" - } - ] - }, - "text": { - "type": "string" - }, - "type": { - "type": "string" - } - }, - "required": [ - "text", - "type" - ], - "type": "object" - }, "TextElement": { "properties": { "byte_range": { @@ -8890,27 +8627,6 @@ ], "type": "object" }, - "TextResourceContents": { - "properties": { - "mimeType": { - "type": [ - "string", - "null" - ] - }, - "text": { - "type": "string" - }, - "uri": { - "type": "string" - } - }, - "required": [ - "text", - "uri" - ], - "type": "object" - }, "ThreadId": { "type": "string" }, @@ -8971,38 +8687,26 @@ "Tool": { "description": "Definition for a tool the client can call.", "properties": { - "annotations": { - "anyOf": [ - { - "$ref": "#/definitions/ToolAnnotations" - }, - { - "type": "null" - } - ] - }, + "_meta": true, + "annotations": true, "description": { "type": [ "string", "null" ] }, - "inputSchema": { - "$ref": "#/definitions/ToolInputSchema" + "icons": { + "items": true, + "type": [ + "array", + "null" + ] }, + "inputSchema": true, "name": { "type": "string" }, - "outputSchema": { - "anyOf": [ - { - "$ref": "#/definitions/ToolOutputSchema" - }, - { - "type": "null" - } - ] - }, + "outputSchema": true, "title": { "type": [ "string", @@ -9016,82 +8720,6 @@ ], "type": "object" }, - "ToolAnnotations": { - "description": "Additional properties describing a Tool to clients.\n\nNOTE: all properties in ToolAnnotations are **hints**. They are not guaranteed to provide a faithful description of tool behavior (including descriptive properties like `title`).\n\nClients should never make tool use decisions based on ToolAnnotations received from untrusted servers.", - "properties": { - "destructiveHint": { - "type": [ - "boolean", - "null" - ] - }, - "idempotentHint": { - "type": [ - "boolean", - "null" - ] - }, - "openWorldHint": { - "type": [ - "boolean", - "null" - ] - }, - "readOnlyHint": { - "type": [ - "boolean", - "null" - ] - }, - "title": { - "type": [ - "string", - "null" - ] - } - }, - "type": "object" - }, - "ToolInputSchema": { - "description": "A JSON Schema object defining the expected parameters for the tool.", - "properties": { - "properties": true, - "required": { - "items": { - "type": "string" - }, - "type": [ - "array", - "null" - ] - }, - "type": { - "default": "object", - "type": "string" - } - }, - "type": "object" - }, - "ToolOutputSchema": { - "description": "An optional JSON Schema object defining the structure of the tool's output returned in the structuredContent field of a CallToolResult.", - "properties": { - "properties": true, - "required": { - "items": { - "type": "string" - }, - "type": [ - "array", - "null" - ] - }, - "type": { - "default": "object", - "type": "string" - } - }, - "type": "object" - }, "ToolRequestUserInputAnswer": { "description": "EXPERIMENTAL. Captures a user's answer to a request_user_input question.", "properties": { @@ -9940,34 +9568,6 @@ }, "type": "object" }, - "Annotations": { - "description": "Optional annotations for the client. The client can use annotations to inform how objects are used or displayed", - "properties": { - "audience": { - "items": { - "$ref": "#/definitions/v2/Role" - }, - "type": [ - "array", - "null" - ] - }, - "lastModified": { - "type": [ - "string", - "null" - ] - }, - "priority": { - "format": "double", - "type": [ - "number", - "null" - ] - } - }, - "type": "object" - }, "AppInfo": { "properties": { "description": { @@ -10072,36 +9672,6 @@ ], "type": "string" }, - "AudioContent": { - "description": "Audio provided to or from an LLM.", - "properties": { - "annotations": { - "anyOf": [ - { - "$ref": "#/definitions/v2/Annotations" - }, - { - "type": "null" - } - ] - }, - "data": { - "type": "string" - }, - "mimeType": { - "type": "string" - }, - "type": { - "type": "string" - } - }, - "required": [ - "data", - "mimeType", - "type" - ], - "type": "object" - }, "AuthMode": { "description": "Authentication mode for OpenAI-backed providers.", "oneOf": [ @@ -10128,27 +9698,6 @@ } ] }, - "BlobResourceContents": { - "properties": { - "blob": { - "type": "string" - }, - "mimeType": { - "type": [ - "string", - "null" - ] - }, - "uri": { - "type": "string" - } - }, - "required": [ - "blob", - "uri" - ], - "type": "object" - }, "ByteRange": { "properties": { "end": { @@ -11280,25 +10829,6 @@ "title": "ConfigWriteResponse", "type": "object" }, - "ContentBlock": { - "anyOf": [ - { - "$ref": "#/definitions/v2/TextContent" - }, - { - "$ref": "#/definitions/v2/ImageContent" - }, - { - "$ref": "#/definitions/v2/AudioContent" - }, - { - "$ref": "#/definitions/v2/ResourceLink" - }, - { - "$ref": "#/definitions/v2/EmbeddedResource" - } - ] - }, "ContentItem": { "oneOf": [ { @@ -11440,42 +10970,6 @@ ], "type": "object" }, - "EmbeddedResource": { - "description": "The contents of a resource, embedded into a prompt or tool call result.\n\nIt is up to the client how best to render embedded resources for the benefit of the LLM and/or the user.", - "properties": { - "annotations": { - "anyOf": [ - { - "$ref": "#/definitions/v2/Annotations" - }, - { - "type": "null" - } - ] - }, - "resource": { - "$ref": "#/definitions/v2/EmbeddedResourceResource" - }, - "type": { - "type": "string" - } - }, - "required": [ - "resource", - "type" - ], - "type": "object" - }, - "EmbeddedResourceResource": { - "anyOf": [ - { - "$ref": "#/definitions/v2/TextResourceContents" - }, - { - "$ref": "#/definitions/v2/BlobResourceContents" - } - ] - }, "ErrorNotification": { "$schema": "http://json-schema.org/draft-07/schema#", "properties": { @@ -11769,36 +11263,6 @@ }, "type": "object" }, - "ImageContent": { - "description": "An image provided to or from an LLM.", - "properties": { - "annotations": { - "anyOf": [ - { - "$ref": "#/definitions/v2/Annotations" - }, - { - "type": "null" - } - ] - }, - "data": { - "type": "string" - }, - "mimeType": { - "type": "string" - }, - "type": { - "type": "string" - } - }, - "required": [ - "data", - "mimeType", - "type" - ], - "type": "object" - }, "ItemCompletedNotification": { "$schema": "http://json-schema.org/draft-07/schema#", "properties": { @@ -12242,9 +11706,7 @@ "McpToolCallResult": { "properties": { "content": { - "items": { - "$ref": "#/definitions/v2/ContentBlock" - }, + "items": true, "type": "array" }, "structuredContent": true @@ -12882,22 +12344,21 @@ "Resource": { "description": "A known resource that the server is capable of reading.", "properties": { - "annotations": { - "anyOf": [ - { - "$ref": "#/definitions/v2/Annotations" - }, - { - "type": "null" - } - ] - }, + "_meta": true, + "annotations": true, "description": { "type": [ "string", "null" ] }, + "icons": { + "items": true, + "type": [ + "array", + "null" + ] + }, "mimeType": { "type": [ "string", @@ -12930,74 +12391,10 @@ ], "type": "object" }, - "ResourceLink": { - "description": "A resource that the server is capable of reading, included in a prompt or tool call result.\n\nNote: resource links returned by tools are not guaranteed to appear in the results of `resources/list` requests.", - "properties": { - "annotations": { - "anyOf": [ - { - "$ref": "#/definitions/v2/Annotations" - }, - { - "type": "null" - } - ] - }, - "description": { - "type": [ - "string", - "null" - ] - }, - "mimeType": { - "type": [ - "string", - "null" - ] - }, - "name": { - "type": "string" - }, - "size": { - "format": "int64", - "type": [ - "integer", - "null" - ] - }, - "title": { - "type": [ - "string", - "null" - ] - }, - "type": { - "type": "string" - }, - "uri": { - "type": "string" - } - }, - "required": [ - "name", - "type", - "uri" - ], - "type": "object" - }, "ResourceTemplate": { "description": "A template description for resources available on the server.", "properties": { - "annotations": { - "anyOf": [ - { - "$ref": "#/definitions/v2/Annotations" - }, - { - "type": "null" - } - ] - }, + "annotations": true, "description": { "type": [ "string", @@ -13520,14 +12917,6 @@ } ] }, - "Role": { - "description": "The sender or recipient of messages and data in a conversation.", - "enum": [ - "assistant", - "user" - ], - "type": "string" - }, "SandboxMode": { "enum": [ "read-only", @@ -14050,32 +13439,6 @@ "title": "TerminalInteractionNotification", "type": "object" }, - "TextContent": { - "description": "Text provided to or from an LLM.", - "properties": { - "annotations": { - "anyOf": [ - { - "$ref": "#/definitions/v2/Annotations" - }, - { - "type": "null" - } - ] - }, - "text": { - "type": "string" - }, - "type": { - "type": "string" - } - }, - "required": [ - "text", - "type" - ], - "type": "object" - }, "TextElement": { "properties": { "byteRange": { @@ -14135,27 +13498,6 @@ ], "type": "object" }, - "TextResourceContents": { - "properties": { - "mimeType": { - "type": [ - "string", - "null" - ] - }, - "text": { - "type": "string" - }, - "uri": { - "type": "string" - } - }, - "required": [ - "text", - "uri" - ], - "type": "object" - }, "Thread": { "properties": { "cliVersion": { @@ -15499,38 +14841,26 @@ "Tool": { "description": "Definition for a tool the client can call.", "properties": { - "annotations": { - "anyOf": [ - { - "$ref": "#/definitions/v2/ToolAnnotations" - }, - { - "type": "null" - } - ] - }, + "_meta": true, + "annotations": true, "description": { "type": [ "string", "null" ] }, - "inputSchema": { - "$ref": "#/definitions/v2/ToolInputSchema" + "icons": { + "items": true, + "type": [ + "array", + "null" + ] }, + "inputSchema": true, "name": { "type": "string" }, - "outputSchema": { - "anyOf": [ - { - "$ref": "#/definitions/v2/ToolOutputSchema" - }, - { - "type": "null" - } - ] - }, + "outputSchema": true, "title": { "type": [ "string", @@ -15544,82 +14874,6 @@ ], "type": "object" }, - "ToolAnnotations": { - "description": "Additional properties describing a Tool to clients.\n\nNOTE: all properties in ToolAnnotations are **hints**. They are not guaranteed to provide a faithful description of tool behavior (including descriptive properties like `title`).\n\nClients should never make tool use decisions based on ToolAnnotations received from untrusted servers.", - "properties": { - "destructiveHint": { - "type": [ - "boolean", - "null" - ] - }, - "idempotentHint": { - "type": [ - "boolean", - "null" - ] - }, - "openWorldHint": { - "type": [ - "boolean", - "null" - ] - }, - "readOnlyHint": { - "type": [ - "boolean", - "null" - ] - }, - "title": { - "type": [ - "string", - "null" - ] - } - }, - "type": "object" - }, - "ToolInputSchema": { - "description": "A JSON Schema object defining the expected parameters for the tool.", - "properties": { - "properties": true, - "required": { - "items": { - "type": "string" - }, - "type": [ - "array", - "null" - ] - }, - "type": { - "default": "object", - "type": "string" - } - }, - "type": "object" - }, - "ToolOutputSchema": { - "description": "An optional JSON Schema object defining the structure of the tool's output returned in the structuredContent field of a CallToolResult.", - "properties": { - "properties": true, - "required": { - "items": { - "type": "string" - }, - "type": [ - "array", - "null" - ] - }, - "type": { - "default": "object", - "type": "string" - } - }, - "type": "object" - }, "ToolsV2": { "properties": { "view_image": { diff --git a/codex-rs/app-server-protocol/schema/json/v1/ForkConversationResponse.json b/codex-rs/app-server-protocol/schema/json/v1/ForkConversationResponse.json index f4c68a9f9d..ae6e705221 100644 --- a/codex-rs/app-server-protocol/schema/json/v1/ForkConversationResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v1/ForkConversationResponse.json @@ -93,34 +93,6 @@ } ] }, - "Annotations": { - "description": "Optional annotations for the client. The client can use annotations to inform how objects are used or displayed", - "properties": { - "audience": { - "items": { - "$ref": "#/definitions/Role" - }, - "type": [ - "array", - "null" - ] - }, - "lastModified": { - "type": [ - "string", - "null" - ] - }, - "priority": { - "format": "double", - "type": [ - "number", - "null" - ] - } - }, - "type": "object" - }, "AskForApproval": { "description": "Determines the conditions under which the user is consulted to approve running the command proposed by Codex.", "oneOf": [ @@ -154,57 +126,6 @@ } ] }, - "AudioContent": { - "description": "Audio provided to or from an LLM.", - "properties": { - "annotations": { - "anyOf": [ - { - "$ref": "#/definitions/Annotations" - }, - { - "type": "null" - } - ] - }, - "data": { - "type": "string" - }, - "mimeType": { - "type": "string" - }, - "type": { - "type": "string" - } - }, - "required": [ - "data", - "mimeType", - "type" - ], - "type": "object" - }, - "BlobResourceContents": { - "properties": { - "blob": { - "type": "string" - }, - "mimeType": { - "type": [ - "string", - "null" - ] - }, - "uri": { - "type": "string" - } - }, - "required": [ - "blob", - "uri" - ], - "type": "object" - }, "ByteRange": { "properties": { "end": { @@ -229,10 +150,9 @@ "CallToolResult": { "description": "The server's response to a tool call.", "properties": { + "_meta": true, "content": { - "items": { - "$ref": "#/definitions/ContentBlock" - }, + "items": true, "type": "array" }, "isError": { @@ -390,25 +310,6 @@ } ] }, - "ContentBlock": { - "anyOf": [ - { - "$ref": "#/definitions/TextContent" - }, - { - "$ref": "#/definitions/ImageContent" - }, - { - "$ref": "#/definitions/AudioContent" - }, - { - "$ref": "#/definitions/ResourceLink" - }, - { - "$ref": "#/definitions/EmbeddedResource" - } - ] - }, "ContentItem": { "oneOf": [ { @@ -544,42 +445,6 @@ ], "type": "object" }, - "EmbeddedResource": { - "description": "The contents of a resource, embedded into a prompt or tool call result.\n\nIt is up to the client how best to render embedded resources for the benefit of the LLM and/or the user.", - "properties": { - "annotations": { - "anyOf": [ - { - "$ref": "#/definitions/Annotations" - }, - { - "type": "null" - } - ] - }, - "resource": { - "$ref": "#/definitions/EmbeddedResourceResource" - }, - "type": { - "type": "string" - } - }, - "required": [ - "resource", - "type" - ], - "type": "object" - }, - "EmbeddedResourceResource": { - "anyOf": [ - { - "$ref": "#/definitions/TextResourceContents" - }, - { - "$ref": "#/definitions/BlobResourceContents" - } - ] - }, "EventMsg": { "description": "Response event from the agent NOTE: Make sure none of these values have optional types, as it will mess up the extension code-gen.", "oneOf": [ @@ -3071,36 +2936,6 @@ ], "type": "object" }, - "ImageContent": { - "description": "An image provided to or from an LLM.", - "properties": { - "annotations": { - "anyOf": [ - { - "$ref": "#/definitions/Annotations" - }, - { - "type": "null" - } - ] - }, - "data": { - "type": "string" - }, - "mimeType": { - "type": "string" - }, - "type": { - "type": "string" - } - }, - "required": [ - "data", - "mimeType", - "type" - ], - "type": "object" - }, "LocalShellAction": { "oneOf": [ { @@ -3599,7 +3434,8 @@ "format": "int64", "type": "integer" } - ] + ], + "description": "ID of a request, which can be either a string or an integer." }, "RequestUserInputQuestion": { "properties": { @@ -3655,22 +3491,21 @@ "Resource": { "description": "A known resource that the server is capable of reading.", "properties": { - "annotations": { - "anyOf": [ - { - "$ref": "#/definitions/Annotations" - }, - { - "type": "null" - } - ] - }, + "_meta": true, + "annotations": true, "description": { "type": [ "string", "null" ] }, + "icons": { + "items": true, + "type": [ + "array", + "null" + ] + }, "mimeType": { "type": [ "string", @@ -3703,74 +3538,10 @@ ], "type": "object" }, - "ResourceLink": { - "description": "A resource that the server is capable of reading, included in a prompt or tool call result.\n\nNote: resource links returned by tools are not guaranteed to appear in the results of `resources/list` requests.", - "properties": { - "annotations": { - "anyOf": [ - { - "$ref": "#/definitions/Annotations" - }, - { - "type": "null" - } - ] - }, - "description": { - "type": [ - "string", - "null" - ] - }, - "mimeType": { - "type": [ - "string", - "null" - ] - }, - "name": { - "type": "string" - }, - "size": { - "format": "int64", - "type": [ - "integer", - "null" - ] - }, - "title": { - "type": [ - "string", - "null" - ] - }, - "type": { - "type": "string" - }, - "uri": { - "type": "string" - } - }, - "required": [ - "name", - "type", - "uri" - ], - "type": "object" - }, "ResourceTemplate": { "description": "A template description for resources available on the server.", "properties": { - "annotations": { - "anyOf": [ - { - "$ref": "#/definitions/Annotations" - }, - { - "type": "null" - } - ] - }, + "annotations": true, "description": { "type": [ "string", @@ -4361,14 +4132,6 @@ } ] }, - "Role": { - "description": "The sender or recipient of messages and data in a conversation.", - "enum": [ - "assistant", - "user" - ], - "type": "string" - }, "SandboxPolicy": { "description": "Determines execution restrictions for model shell commands.", "oneOf": [ @@ -4678,32 +4441,6 @@ ], "type": "string" }, - "TextContent": { - "description": "Text provided to or from an LLM.", - "properties": { - "annotations": { - "anyOf": [ - { - "$ref": "#/definitions/Annotations" - }, - { - "type": "null" - } - ] - }, - "text": { - "type": "string" - }, - "type": { - "type": "string" - } - }, - "required": [ - "text", - "type" - ], - "type": "object" - }, "TextElement": { "properties": { "byte_range": { @@ -4727,27 +4464,6 @@ ], "type": "object" }, - "TextResourceContents": { - "properties": { - "mimeType": { - "type": [ - "string", - "null" - ] - }, - "text": { - "type": "string" - }, - "uri": { - "type": "string" - } - }, - "required": [ - "text", - "uri" - ], - "type": "object" - }, "ThreadId": { "type": "string" }, @@ -4808,38 +4524,26 @@ "Tool": { "description": "Definition for a tool the client can call.", "properties": { - "annotations": { - "anyOf": [ - { - "$ref": "#/definitions/ToolAnnotations" - }, - { - "type": "null" - } - ] - }, + "_meta": true, + "annotations": true, "description": { "type": [ "string", "null" ] }, - "inputSchema": { - "$ref": "#/definitions/ToolInputSchema" + "icons": { + "items": true, + "type": [ + "array", + "null" + ] }, + "inputSchema": true, "name": { "type": "string" }, - "outputSchema": { - "anyOf": [ - { - "$ref": "#/definitions/ToolOutputSchema" - }, - { - "type": "null" - } - ] - }, + "outputSchema": true, "title": { "type": [ "string", @@ -4853,82 +4557,6 @@ ], "type": "object" }, - "ToolAnnotations": { - "description": "Additional properties describing a Tool to clients.\n\nNOTE: all properties in ToolAnnotations are **hints**. They are not guaranteed to provide a faithful description of tool behavior (including descriptive properties like `title`).\n\nClients should never make tool use decisions based on ToolAnnotations received from untrusted servers.", - "properties": { - "destructiveHint": { - "type": [ - "boolean", - "null" - ] - }, - "idempotentHint": { - "type": [ - "boolean", - "null" - ] - }, - "openWorldHint": { - "type": [ - "boolean", - "null" - ] - }, - "readOnlyHint": { - "type": [ - "boolean", - "null" - ] - }, - "title": { - "type": [ - "string", - "null" - ] - } - }, - "type": "object" - }, - "ToolInputSchema": { - "description": "A JSON Schema object defining the expected parameters for the tool.", - "properties": { - "properties": true, - "required": { - "items": { - "type": "string" - }, - "type": [ - "array", - "null" - ] - }, - "type": { - "default": "object", - "type": "string" - } - }, - "type": "object" - }, - "ToolOutputSchema": { - "description": "An optional JSON Schema object defining the structure of the tool's output returned in the structuredContent field of a CallToolResult.", - "properties": { - "properties": true, - "required": { - "items": { - "type": "string" - }, - "type": [ - "array", - "null" - ] - }, - "type": { - "default": "object", - "type": "string" - } - }, - "type": "object" - }, "TurnAbortReason": { "enum": [ "interrupted", diff --git a/codex-rs/app-server-protocol/schema/json/v1/ResumeConversationResponse.json b/codex-rs/app-server-protocol/schema/json/v1/ResumeConversationResponse.json index fcb39cd05c..cdad2018c6 100644 --- a/codex-rs/app-server-protocol/schema/json/v1/ResumeConversationResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v1/ResumeConversationResponse.json @@ -93,34 +93,6 @@ } ] }, - "Annotations": { - "description": "Optional annotations for the client. The client can use annotations to inform how objects are used or displayed", - "properties": { - "audience": { - "items": { - "$ref": "#/definitions/Role" - }, - "type": [ - "array", - "null" - ] - }, - "lastModified": { - "type": [ - "string", - "null" - ] - }, - "priority": { - "format": "double", - "type": [ - "number", - "null" - ] - } - }, - "type": "object" - }, "AskForApproval": { "description": "Determines the conditions under which the user is consulted to approve running the command proposed by Codex.", "oneOf": [ @@ -154,57 +126,6 @@ } ] }, - "AudioContent": { - "description": "Audio provided to or from an LLM.", - "properties": { - "annotations": { - "anyOf": [ - { - "$ref": "#/definitions/Annotations" - }, - { - "type": "null" - } - ] - }, - "data": { - "type": "string" - }, - "mimeType": { - "type": "string" - }, - "type": { - "type": "string" - } - }, - "required": [ - "data", - "mimeType", - "type" - ], - "type": "object" - }, - "BlobResourceContents": { - "properties": { - "blob": { - "type": "string" - }, - "mimeType": { - "type": [ - "string", - "null" - ] - }, - "uri": { - "type": "string" - } - }, - "required": [ - "blob", - "uri" - ], - "type": "object" - }, "ByteRange": { "properties": { "end": { @@ -229,10 +150,9 @@ "CallToolResult": { "description": "The server's response to a tool call.", "properties": { + "_meta": true, "content": { - "items": { - "$ref": "#/definitions/ContentBlock" - }, + "items": true, "type": "array" }, "isError": { @@ -390,25 +310,6 @@ } ] }, - "ContentBlock": { - "anyOf": [ - { - "$ref": "#/definitions/TextContent" - }, - { - "$ref": "#/definitions/ImageContent" - }, - { - "$ref": "#/definitions/AudioContent" - }, - { - "$ref": "#/definitions/ResourceLink" - }, - { - "$ref": "#/definitions/EmbeddedResource" - } - ] - }, "ContentItem": { "oneOf": [ { @@ -544,42 +445,6 @@ ], "type": "object" }, - "EmbeddedResource": { - "description": "The contents of a resource, embedded into a prompt or tool call result.\n\nIt is up to the client how best to render embedded resources for the benefit of the LLM and/or the user.", - "properties": { - "annotations": { - "anyOf": [ - { - "$ref": "#/definitions/Annotations" - }, - { - "type": "null" - } - ] - }, - "resource": { - "$ref": "#/definitions/EmbeddedResourceResource" - }, - "type": { - "type": "string" - } - }, - "required": [ - "resource", - "type" - ], - "type": "object" - }, - "EmbeddedResourceResource": { - "anyOf": [ - { - "$ref": "#/definitions/TextResourceContents" - }, - { - "$ref": "#/definitions/BlobResourceContents" - } - ] - }, "EventMsg": { "description": "Response event from the agent NOTE: Make sure none of these values have optional types, as it will mess up the extension code-gen.", "oneOf": [ @@ -3071,36 +2936,6 @@ ], "type": "object" }, - "ImageContent": { - "description": "An image provided to or from an LLM.", - "properties": { - "annotations": { - "anyOf": [ - { - "$ref": "#/definitions/Annotations" - }, - { - "type": "null" - } - ] - }, - "data": { - "type": "string" - }, - "mimeType": { - "type": "string" - }, - "type": { - "type": "string" - } - }, - "required": [ - "data", - "mimeType", - "type" - ], - "type": "object" - }, "LocalShellAction": { "oneOf": [ { @@ -3599,7 +3434,8 @@ "format": "int64", "type": "integer" } - ] + ], + "description": "ID of a request, which can be either a string or an integer." }, "RequestUserInputQuestion": { "properties": { @@ -3655,22 +3491,21 @@ "Resource": { "description": "A known resource that the server is capable of reading.", "properties": { - "annotations": { - "anyOf": [ - { - "$ref": "#/definitions/Annotations" - }, - { - "type": "null" - } - ] - }, + "_meta": true, + "annotations": true, "description": { "type": [ "string", "null" ] }, + "icons": { + "items": true, + "type": [ + "array", + "null" + ] + }, "mimeType": { "type": [ "string", @@ -3703,74 +3538,10 @@ ], "type": "object" }, - "ResourceLink": { - "description": "A resource that the server is capable of reading, included in a prompt or tool call result.\n\nNote: resource links returned by tools are not guaranteed to appear in the results of `resources/list` requests.", - "properties": { - "annotations": { - "anyOf": [ - { - "$ref": "#/definitions/Annotations" - }, - { - "type": "null" - } - ] - }, - "description": { - "type": [ - "string", - "null" - ] - }, - "mimeType": { - "type": [ - "string", - "null" - ] - }, - "name": { - "type": "string" - }, - "size": { - "format": "int64", - "type": [ - "integer", - "null" - ] - }, - "title": { - "type": [ - "string", - "null" - ] - }, - "type": { - "type": "string" - }, - "uri": { - "type": "string" - } - }, - "required": [ - "name", - "type", - "uri" - ], - "type": "object" - }, "ResourceTemplate": { "description": "A template description for resources available on the server.", "properties": { - "annotations": { - "anyOf": [ - { - "$ref": "#/definitions/Annotations" - }, - { - "type": "null" - } - ] - }, + "annotations": true, "description": { "type": [ "string", @@ -4361,14 +4132,6 @@ } ] }, - "Role": { - "description": "The sender or recipient of messages and data in a conversation.", - "enum": [ - "assistant", - "user" - ], - "type": "string" - }, "SandboxPolicy": { "description": "Determines execution restrictions for model shell commands.", "oneOf": [ @@ -4678,32 +4441,6 @@ ], "type": "string" }, - "TextContent": { - "description": "Text provided to or from an LLM.", - "properties": { - "annotations": { - "anyOf": [ - { - "$ref": "#/definitions/Annotations" - }, - { - "type": "null" - } - ] - }, - "text": { - "type": "string" - }, - "type": { - "type": "string" - } - }, - "required": [ - "text", - "type" - ], - "type": "object" - }, "TextElement": { "properties": { "byte_range": { @@ -4727,27 +4464,6 @@ ], "type": "object" }, - "TextResourceContents": { - "properties": { - "mimeType": { - "type": [ - "string", - "null" - ] - }, - "text": { - "type": "string" - }, - "uri": { - "type": "string" - } - }, - "required": [ - "text", - "uri" - ], - "type": "object" - }, "ThreadId": { "type": "string" }, @@ -4808,38 +4524,26 @@ "Tool": { "description": "Definition for a tool the client can call.", "properties": { - "annotations": { - "anyOf": [ - { - "$ref": "#/definitions/ToolAnnotations" - }, - { - "type": "null" - } - ] - }, + "_meta": true, + "annotations": true, "description": { "type": [ "string", "null" ] }, - "inputSchema": { - "$ref": "#/definitions/ToolInputSchema" + "icons": { + "items": true, + "type": [ + "array", + "null" + ] }, + "inputSchema": true, "name": { "type": "string" }, - "outputSchema": { - "anyOf": [ - { - "$ref": "#/definitions/ToolOutputSchema" - }, - { - "type": "null" - } - ] - }, + "outputSchema": true, "title": { "type": [ "string", @@ -4853,82 +4557,6 @@ ], "type": "object" }, - "ToolAnnotations": { - "description": "Additional properties describing a Tool to clients.\n\nNOTE: all properties in ToolAnnotations are **hints**. They are not guaranteed to provide a faithful description of tool behavior (including descriptive properties like `title`).\n\nClients should never make tool use decisions based on ToolAnnotations received from untrusted servers.", - "properties": { - "destructiveHint": { - "type": [ - "boolean", - "null" - ] - }, - "idempotentHint": { - "type": [ - "boolean", - "null" - ] - }, - "openWorldHint": { - "type": [ - "boolean", - "null" - ] - }, - "readOnlyHint": { - "type": [ - "boolean", - "null" - ] - }, - "title": { - "type": [ - "string", - "null" - ] - } - }, - "type": "object" - }, - "ToolInputSchema": { - "description": "A JSON Schema object defining the expected parameters for the tool.", - "properties": { - "properties": true, - "required": { - "items": { - "type": "string" - }, - "type": [ - "array", - "null" - ] - }, - "type": { - "default": "object", - "type": "string" - } - }, - "type": "object" - }, - "ToolOutputSchema": { - "description": "An optional JSON Schema object defining the structure of the tool's output returned in the structuredContent field of a CallToolResult.", - "properties": { - "properties": true, - "required": { - "items": { - "type": "string" - }, - "type": [ - "array", - "null" - ] - }, - "type": { - "default": "object", - "type": "string" - } - }, - "type": "object" - }, "TurnAbortReason": { "enum": [ "interrupted", diff --git a/codex-rs/app-server-protocol/schema/json/v1/SessionConfiguredNotification.json b/codex-rs/app-server-protocol/schema/json/v1/SessionConfiguredNotification.json index 24e71bc27a..e6ed4d0427 100644 --- a/codex-rs/app-server-protocol/schema/json/v1/SessionConfiguredNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v1/SessionConfiguredNotification.json @@ -93,34 +93,6 @@ } ] }, - "Annotations": { - "description": "Optional annotations for the client. The client can use annotations to inform how objects are used or displayed", - "properties": { - "audience": { - "items": { - "$ref": "#/definitions/Role" - }, - "type": [ - "array", - "null" - ] - }, - "lastModified": { - "type": [ - "string", - "null" - ] - }, - "priority": { - "format": "double", - "type": [ - "number", - "null" - ] - } - }, - "type": "object" - }, "AskForApproval": { "description": "Determines the conditions under which the user is consulted to approve running the command proposed by Codex.", "oneOf": [ @@ -154,57 +126,6 @@ } ] }, - "AudioContent": { - "description": "Audio provided to or from an LLM.", - "properties": { - "annotations": { - "anyOf": [ - { - "$ref": "#/definitions/Annotations" - }, - { - "type": "null" - } - ] - }, - "data": { - "type": "string" - }, - "mimeType": { - "type": "string" - }, - "type": { - "type": "string" - } - }, - "required": [ - "data", - "mimeType", - "type" - ], - "type": "object" - }, - "BlobResourceContents": { - "properties": { - "blob": { - "type": "string" - }, - "mimeType": { - "type": [ - "string", - "null" - ] - }, - "uri": { - "type": "string" - } - }, - "required": [ - "blob", - "uri" - ], - "type": "object" - }, "ByteRange": { "properties": { "end": { @@ -229,10 +150,9 @@ "CallToolResult": { "description": "The server's response to a tool call.", "properties": { + "_meta": true, "content": { - "items": { - "$ref": "#/definitions/ContentBlock" - }, + "items": true, "type": "array" }, "isError": { @@ -390,25 +310,6 @@ } ] }, - "ContentBlock": { - "anyOf": [ - { - "$ref": "#/definitions/TextContent" - }, - { - "$ref": "#/definitions/ImageContent" - }, - { - "$ref": "#/definitions/AudioContent" - }, - { - "$ref": "#/definitions/ResourceLink" - }, - { - "$ref": "#/definitions/EmbeddedResource" - } - ] - }, "ContentItem": { "oneOf": [ { @@ -544,42 +445,6 @@ ], "type": "object" }, - "EmbeddedResource": { - "description": "The contents of a resource, embedded into a prompt or tool call result.\n\nIt is up to the client how best to render embedded resources for the benefit of the LLM and/or the user.", - "properties": { - "annotations": { - "anyOf": [ - { - "$ref": "#/definitions/Annotations" - }, - { - "type": "null" - } - ] - }, - "resource": { - "$ref": "#/definitions/EmbeddedResourceResource" - }, - "type": { - "type": "string" - } - }, - "required": [ - "resource", - "type" - ], - "type": "object" - }, - "EmbeddedResourceResource": { - "anyOf": [ - { - "$ref": "#/definitions/TextResourceContents" - }, - { - "$ref": "#/definitions/BlobResourceContents" - } - ] - }, "EventMsg": { "description": "Response event from the agent NOTE: Make sure none of these values have optional types, as it will mess up the extension code-gen.", "oneOf": [ @@ -3071,36 +2936,6 @@ ], "type": "object" }, - "ImageContent": { - "description": "An image provided to or from an LLM.", - "properties": { - "annotations": { - "anyOf": [ - { - "$ref": "#/definitions/Annotations" - }, - { - "type": "null" - } - ] - }, - "data": { - "type": "string" - }, - "mimeType": { - "type": "string" - }, - "type": { - "type": "string" - } - }, - "required": [ - "data", - "mimeType", - "type" - ], - "type": "object" - }, "LocalShellAction": { "oneOf": [ { @@ -3599,7 +3434,8 @@ "format": "int64", "type": "integer" } - ] + ], + "description": "ID of a request, which can be either a string or an integer." }, "RequestUserInputQuestion": { "properties": { @@ -3655,22 +3491,21 @@ "Resource": { "description": "A known resource that the server is capable of reading.", "properties": { - "annotations": { - "anyOf": [ - { - "$ref": "#/definitions/Annotations" - }, - { - "type": "null" - } - ] - }, + "_meta": true, + "annotations": true, "description": { "type": [ "string", "null" ] }, + "icons": { + "items": true, + "type": [ + "array", + "null" + ] + }, "mimeType": { "type": [ "string", @@ -3703,74 +3538,10 @@ ], "type": "object" }, - "ResourceLink": { - "description": "A resource that the server is capable of reading, included in a prompt or tool call result.\n\nNote: resource links returned by tools are not guaranteed to appear in the results of `resources/list` requests.", - "properties": { - "annotations": { - "anyOf": [ - { - "$ref": "#/definitions/Annotations" - }, - { - "type": "null" - } - ] - }, - "description": { - "type": [ - "string", - "null" - ] - }, - "mimeType": { - "type": [ - "string", - "null" - ] - }, - "name": { - "type": "string" - }, - "size": { - "format": "int64", - "type": [ - "integer", - "null" - ] - }, - "title": { - "type": [ - "string", - "null" - ] - }, - "type": { - "type": "string" - }, - "uri": { - "type": "string" - } - }, - "required": [ - "name", - "type", - "uri" - ], - "type": "object" - }, "ResourceTemplate": { "description": "A template description for resources available on the server.", "properties": { - "annotations": { - "anyOf": [ - { - "$ref": "#/definitions/Annotations" - }, - { - "type": "null" - } - ] - }, + "annotations": true, "description": { "type": [ "string", @@ -4361,14 +4132,6 @@ } ] }, - "Role": { - "description": "The sender or recipient of messages and data in a conversation.", - "enum": [ - "assistant", - "user" - ], - "type": "string" - }, "SandboxPolicy": { "description": "Determines execution restrictions for model shell commands.", "oneOf": [ @@ -4678,32 +4441,6 @@ ], "type": "string" }, - "TextContent": { - "description": "Text provided to or from an LLM.", - "properties": { - "annotations": { - "anyOf": [ - { - "$ref": "#/definitions/Annotations" - }, - { - "type": "null" - } - ] - }, - "text": { - "type": "string" - }, - "type": { - "type": "string" - } - }, - "required": [ - "text", - "type" - ], - "type": "object" - }, "TextElement": { "properties": { "byte_range": { @@ -4727,27 +4464,6 @@ ], "type": "object" }, - "TextResourceContents": { - "properties": { - "mimeType": { - "type": [ - "string", - "null" - ] - }, - "text": { - "type": "string" - }, - "uri": { - "type": "string" - } - }, - "required": [ - "text", - "uri" - ], - "type": "object" - }, "ThreadId": { "type": "string" }, @@ -4808,38 +4524,26 @@ "Tool": { "description": "Definition for a tool the client can call.", "properties": { - "annotations": { - "anyOf": [ - { - "$ref": "#/definitions/ToolAnnotations" - }, - { - "type": "null" - } - ] - }, + "_meta": true, + "annotations": true, "description": { "type": [ "string", "null" ] }, - "inputSchema": { - "$ref": "#/definitions/ToolInputSchema" + "icons": { + "items": true, + "type": [ + "array", + "null" + ] }, + "inputSchema": true, "name": { "type": "string" }, - "outputSchema": { - "anyOf": [ - { - "$ref": "#/definitions/ToolOutputSchema" - }, - { - "type": "null" - } - ] - }, + "outputSchema": true, "title": { "type": [ "string", @@ -4853,82 +4557,6 @@ ], "type": "object" }, - "ToolAnnotations": { - "description": "Additional properties describing a Tool to clients.\n\nNOTE: all properties in ToolAnnotations are **hints**. They are not guaranteed to provide a faithful description of tool behavior (including descriptive properties like `title`).\n\nClients should never make tool use decisions based on ToolAnnotations received from untrusted servers.", - "properties": { - "destructiveHint": { - "type": [ - "boolean", - "null" - ] - }, - "idempotentHint": { - "type": [ - "boolean", - "null" - ] - }, - "openWorldHint": { - "type": [ - "boolean", - "null" - ] - }, - "readOnlyHint": { - "type": [ - "boolean", - "null" - ] - }, - "title": { - "type": [ - "string", - "null" - ] - } - }, - "type": "object" - }, - "ToolInputSchema": { - "description": "A JSON Schema object defining the expected parameters for the tool.", - "properties": { - "properties": true, - "required": { - "items": { - "type": "string" - }, - "type": [ - "array", - "null" - ] - }, - "type": { - "default": "object", - "type": "string" - } - }, - "type": "object" - }, - "ToolOutputSchema": { - "description": "An optional JSON Schema object defining the structure of the tool's output returned in the structuredContent field of a CallToolResult.", - "properties": { - "properties": true, - "required": { - "items": { - "type": "string" - }, - "type": [ - "array", - "null" - ] - }, - "type": { - "default": "object", - "type": "string" - } - }, - "type": "object" - }, "TurnAbortReason": { "enum": [ "interrupted", diff --git a/codex-rs/app-server-protocol/schema/json/v2/ItemCompletedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/ItemCompletedNotification.json index 866ce9aa2b..56ef813231 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ItemCompletedNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ItemCompletedNotification.json @@ -1,85 +1,6 @@ { "$schema": "http://json-schema.org/draft-07/schema#", "definitions": { - "Annotations": { - "description": "Optional annotations for the client. The client can use annotations to inform how objects are used or displayed", - "properties": { - "audience": { - "items": { - "$ref": "#/definitions/Role" - }, - "type": [ - "array", - "null" - ] - }, - "lastModified": { - "type": [ - "string", - "null" - ] - }, - "priority": { - "format": "double", - "type": [ - "number", - "null" - ] - } - }, - "type": "object" - }, - "AudioContent": { - "description": "Audio provided to or from an LLM.", - "properties": { - "annotations": { - "anyOf": [ - { - "$ref": "#/definitions/Annotations" - }, - { - "type": "null" - } - ] - }, - "data": { - "type": "string" - }, - "mimeType": { - "type": "string" - }, - "type": { - "type": "string" - } - }, - "required": [ - "data", - "mimeType", - "type" - ], - "type": "object" - }, - "BlobResourceContents": { - "properties": { - "blob": { - "type": "string" - }, - "mimeType": { - "type": [ - "string", - "null" - ] - }, - "uri": { - "type": "string" - } - }, - "required": [ - "blob", - "uri" - ], - "type": "object" - }, "ByteRange": { "properties": { "end": { @@ -263,61 +184,6 @@ ], "type": "string" }, - "ContentBlock": { - "anyOf": [ - { - "$ref": "#/definitions/TextContent" - }, - { - "$ref": "#/definitions/ImageContent" - }, - { - "$ref": "#/definitions/AudioContent" - }, - { - "$ref": "#/definitions/ResourceLink" - }, - { - "$ref": "#/definitions/EmbeddedResource" - } - ] - }, - "EmbeddedResource": { - "description": "The contents of a resource, embedded into a prompt or tool call result.\n\nIt is up to the client how best to render embedded resources for the benefit of the LLM and/or the user.", - "properties": { - "annotations": { - "anyOf": [ - { - "$ref": "#/definitions/Annotations" - }, - { - "type": "null" - } - ] - }, - "resource": { - "$ref": "#/definitions/EmbeddedResourceResource" - }, - "type": { - "type": "string" - } - }, - "required": [ - "resource", - "type" - ], - "type": "object" - }, - "EmbeddedResourceResource": { - "anyOf": [ - { - "$ref": "#/definitions/TextResourceContents" - }, - { - "$ref": "#/definitions/BlobResourceContents" - } - ] - }, "FileUpdateChange": { "properties": { "diff": { @@ -337,36 +203,6 @@ ], "type": "object" }, - "ImageContent": { - "description": "An image provided to or from an LLM.", - "properties": { - "annotations": { - "anyOf": [ - { - "$ref": "#/definitions/Annotations" - }, - { - "type": "null" - } - ] - }, - "data": { - "type": "string" - }, - "mimeType": { - "type": "string" - }, - "type": { - "type": "string" - } - }, - "required": [ - "data", - "mimeType", - "type" - ], - "type": "object" - }, "McpToolCallError": { "properties": { "message": { @@ -381,9 +217,7 @@ "McpToolCallResult": { "properties": { "content": { - "items": { - "$ref": "#/definitions/ContentBlock" - }, + "items": true, "type": "array" }, "structuredContent": true @@ -468,95 +302,6 @@ } ] }, - "ResourceLink": { - "description": "A resource that the server is capable of reading, included in a prompt or tool call result.\n\nNote: resource links returned by tools are not guaranteed to appear in the results of `resources/list` requests.", - "properties": { - "annotations": { - "anyOf": [ - { - "$ref": "#/definitions/Annotations" - }, - { - "type": "null" - } - ] - }, - "description": { - "type": [ - "string", - "null" - ] - }, - "mimeType": { - "type": [ - "string", - "null" - ] - }, - "name": { - "type": "string" - }, - "size": { - "format": "int64", - "type": [ - "integer", - "null" - ] - }, - "title": { - "type": [ - "string", - "null" - ] - }, - "type": { - "type": "string" - }, - "uri": { - "type": "string" - } - }, - "required": [ - "name", - "type", - "uri" - ], - "type": "object" - }, - "Role": { - "description": "The sender or recipient of messages and data in a conversation.", - "enum": [ - "assistant", - "user" - ], - "type": "string" - }, - "TextContent": { - "description": "Text provided to or from an LLM.", - "properties": { - "annotations": { - "anyOf": [ - { - "$ref": "#/definitions/Annotations" - }, - { - "type": "null" - } - ] - }, - "text": { - "type": "string" - }, - "type": { - "type": "string" - } - }, - "required": [ - "text", - "type" - ], - "type": "object" - }, "TextElement": { "properties": { "byteRange": { @@ -580,27 +325,6 @@ ], "type": "object" }, - "TextResourceContents": { - "properties": { - "mimeType": { - "type": [ - "string", - "null" - ] - }, - "text": { - "type": "string" - }, - "uri": { - "type": "string" - } - }, - "required": [ - "text", - "uri" - ], - "type": "object" - }, "ThreadItem": { "oneOf": [ { diff --git a/codex-rs/app-server-protocol/schema/json/v2/ItemStartedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/ItemStartedNotification.json index 2fd71404f6..625c99af93 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ItemStartedNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ItemStartedNotification.json @@ -1,85 +1,6 @@ { "$schema": "http://json-schema.org/draft-07/schema#", "definitions": { - "Annotations": { - "description": "Optional annotations for the client. The client can use annotations to inform how objects are used or displayed", - "properties": { - "audience": { - "items": { - "$ref": "#/definitions/Role" - }, - "type": [ - "array", - "null" - ] - }, - "lastModified": { - "type": [ - "string", - "null" - ] - }, - "priority": { - "format": "double", - "type": [ - "number", - "null" - ] - } - }, - "type": "object" - }, - "AudioContent": { - "description": "Audio provided to or from an LLM.", - "properties": { - "annotations": { - "anyOf": [ - { - "$ref": "#/definitions/Annotations" - }, - { - "type": "null" - } - ] - }, - "data": { - "type": "string" - }, - "mimeType": { - "type": "string" - }, - "type": { - "type": "string" - } - }, - "required": [ - "data", - "mimeType", - "type" - ], - "type": "object" - }, - "BlobResourceContents": { - "properties": { - "blob": { - "type": "string" - }, - "mimeType": { - "type": [ - "string", - "null" - ] - }, - "uri": { - "type": "string" - } - }, - "required": [ - "blob", - "uri" - ], - "type": "object" - }, "ByteRange": { "properties": { "end": { @@ -263,61 +184,6 @@ ], "type": "string" }, - "ContentBlock": { - "anyOf": [ - { - "$ref": "#/definitions/TextContent" - }, - { - "$ref": "#/definitions/ImageContent" - }, - { - "$ref": "#/definitions/AudioContent" - }, - { - "$ref": "#/definitions/ResourceLink" - }, - { - "$ref": "#/definitions/EmbeddedResource" - } - ] - }, - "EmbeddedResource": { - "description": "The contents of a resource, embedded into a prompt or tool call result.\n\nIt is up to the client how best to render embedded resources for the benefit of the LLM and/or the user.", - "properties": { - "annotations": { - "anyOf": [ - { - "$ref": "#/definitions/Annotations" - }, - { - "type": "null" - } - ] - }, - "resource": { - "$ref": "#/definitions/EmbeddedResourceResource" - }, - "type": { - "type": "string" - } - }, - "required": [ - "resource", - "type" - ], - "type": "object" - }, - "EmbeddedResourceResource": { - "anyOf": [ - { - "$ref": "#/definitions/TextResourceContents" - }, - { - "$ref": "#/definitions/BlobResourceContents" - } - ] - }, "FileUpdateChange": { "properties": { "diff": { @@ -337,36 +203,6 @@ ], "type": "object" }, - "ImageContent": { - "description": "An image provided to or from an LLM.", - "properties": { - "annotations": { - "anyOf": [ - { - "$ref": "#/definitions/Annotations" - }, - { - "type": "null" - } - ] - }, - "data": { - "type": "string" - }, - "mimeType": { - "type": "string" - }, - "type": { - "type": "string" - } - }, - "required": [ - "data", - "mimeType", - "type" - ], - "type": "object" - }, "McpToolCallError": { "properties": { "message": { @@ -381,9 +217,7 @@ "McpToolCallResult": { "properties": { "content": { - "items": { - "$ref": "#/definitions/ContentBlock" - }, + "items": true, "type": "array" }, "structuredContent": true @@ -468,95 +302,6 @@ } ] }, - "ResourceLink": { - "description": "A resource that the server is capable of reading, included in a prompt or tool call result.\n\nNote: resource links returned by tools are not guaranteed to appear in the results of `resources/list` requests.", - "properties": { - "annotations": { - "anyOf": [ - { - "$ref": "#/definitions/Annotations" - }, - { - "type": "null" - } - ] - }, - "description": { - "type": [ - "string", - "null" - ] - }, - "mimeType": { - "type": [ - "string", - "null" - ] - }, - "name": { - "type": "string" - }, - "size": { - "format": "int64", - "type": [ - "integer", - "null" - ] - }, - "title": { - "type": [ - "string", - "null" - ] - }, - "type": { - "type": "string" - }, - "uri": { - "type": "string" - } - }, - "required": [ - "name", - "type", - "uri" - ], - "type": "object" - }, - "Role": { - "description": "The sender or recipient of messages and data in a conversation.", - "enum": [ - "assistant", - "user" - ], - "type": "string" - }, - "TextContent": { - "description": "Text provided to or from an LLM.", - "properties": { - "annotations": { - "anyOf": [ - { - "$ref": "#/definitions/Annotations" - }, - { - "type": "null" - } - ] - }, - "text": { - "type": "string" - }, - "type": { - "type": "string" - } - }, - "required": [ - "text", - "type" - ], - "type": "object" - }, "TextElement": { "properties": { "byteRange": { @@ -580,27 +325,6 @@ ], "type": "object" }, - "TextResourceContents": { - "properties": { - "mimeType": { - "type": [ - "string", - "null" - ] - }, - "text": { - "type": "string" - }, - "uri": { - "type": "string" - } - }, - "required": [ - "text", - "uri" - ], - "type": "object" - }, "ThreadItem": { "oneOf": [ { diff --git a/codex-rs/app-server-protocol/schema/json/v2/ListMcpServerStatusResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ListMcpServerStatusResponse.json index 2ee4926965..fc181c2702 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ListMcpServerStatusResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ListMcpServerStatusResponse.json @@ -1,34 +1,6 @@ { "$schema": "http://json-schema.org/draft-07/schema#", "definitions": { - "Annotations": { - "description": "Optional annotations for the client. The client can use annotations to inform how objects are used or displayed", - "properties": { - "audience": { - "items": { - "$ref": "#/definitions/Role" - }, - "type": [ - "array", - "null" - ] - }, - "lastModified": { - "type": [ - "string", - "null" - ] - }, - "priority": { - "format": "double", - "type": [ - "number", - "null" - ] - } - }, - "type": "object" - }, "McpAuthStatus": { "enum": [ "unsupported", @@ -77,22 +49,21 @@ "Resource": { "description": "A known resource that the server is capable of reading.", "properties": { - "annotations": { - "anyOf": [ - { - "$ref": "#/definitions/Annotations" - }, - { - "type": "null" - } - ] - }, + "_meta": true, + "annotations": true, "description": { "type": [ "string", "null" ] }, + "icons": { + "items": true, + "type": [ + "array", + "null" + ] + }, "mimeType": { "type": [ "string", @@ -128,16 +99,7 @@ "ResourceTemplate": { "description": "A template description for resources available on the server.", "properties": { - "annotations": { - "anyOf": [ - { - "$ref": "#/definitions/Annotations" - }, - { - "type": "null" - } - ] - }, + "annotations": true, "description": { "type": [ "string", @@ -169,49 +131,29 @@ ], "type": "object" }, - "Role": { - "description": "The sender or recipient of messages and data in a conversation.", - "enum": [ - "assistant", - "user" - ], - "type": "string" - }, "Tool": { "description": "Definition for a tool the client can call.", "properties": { - "annotations": { - "anyOf": [ - { - "$ref": "#/definitions/ToolAnnotations" - }, - { - "type": "null" - } - ] - }, + "_meta": true, + "annotations": true, "description": { "type": [ "string", "null" ] }, - "inputSchema": { - "$ref": "#/definitions/ToolInputSchema" + "icons": { + "items": true, + "type": [ + "array", + "null" + ] }, + "inputSchema": true, "name": { "type": "string" }, - "outputSchema": { - "anyOf": [ - { - "$ref": "#/definitions/ToolOutputSchema" - }, - { - "type": "null" - } - ] - }, + "outputSchema": true, "title": { "type": [ "string", @@ -224,82 +166,6 @@ "name" ], "type": "object" - }, - "ToolAnnotations": { - "description": "Additional properties describing a Tool to clients.\n\nNOTE: all properties in ToolAnnotations are **hints**. They are not guaranteed to provide a faithful description of tool behavior (including descriptive properties like `title`).\n\nClients should never make tool use decisions based on ToolAnnotations received from untrusted servers.", - "properties": { - "destructiveHint": { - "type": [ - "boolean", - "null" - ] - }, - "idempotentHint": { - "type": [ - "boolean", - "null" - ] - }, - "openWorldHint": { - "type": [ - "boolean", - "null" - ] - }, - "readOnlyHint": { - "type": [ - "boolean", - "null" - ] - }, - "title": { - "type": [ - "string", - "null" - ] - } - }, - "type": "object" - }, - "ToolInputSchema": { - "description": "A JSON Schema object defining the expected parameters for the tool.", - "properties": { - "properties": true, - "required": { - "items": { - "type": "string" - }, - "type": [ - "array", - "null" - ] - }, - "type": { - "default": "object", - "type": "string" - } - }, - "type": "object" - }, - "ToolOutputSchema": { - "description": "An optional JSON Schema object defining the structure of the tool's output returned in the structuredContent field of a CallToolResult.", - "properties": { - "properties": true, - "required": { - "items": { - "type": "string" - }, - "type": [ - "array", - "null" - ] - }, - "type": { - "default": "object", - "type": "string" - } - }, - "type": "object" } }, "properties": { diff --git a/codex-rs/app-server-protocol/schema/json/v2/ReviewStartResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ReviewStartResponse.json index 341ff78c0a..dfbd76a20b 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ReviewStartResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ReviewStartResponse.json @@ -1,85 +1,6 @@ { "$schema": "http://json-schema.org/draft-07/schema#", "definitions": { - "Annotations": { - "description": "Optional annotations for the client. The client can use annotations to inform how objects are used or displayed", - "properties": { - "audience": { - "items": { - "$ref": "#/definitions/Role" - }, - "type": [ - "array", - "null" - ] - }, - "lastModified": { - "type": [ - "string", - "null" - ] - }, - "priority": { - "format": "double", - "type": [ - "number", - "null" - ] - } - }, - "type": "object" - }, - "AudioContent": { - "description": "Audio provided to or from an LLM.", - "properties": { - "annotations": { - "anyOf": [ - { - "$ref": "#/definitions/Annotations" - }, - { - "type": "null" - } - ] - }, - "data": { - "type": "string" - }, - "mimeType": { - "type": "string" - }, - "type": { - "type": "string" - } - }, - "required": [ - "data", - "mimeType", - "type" - ], - "type": "object" - }, - "BlobResourceContents": { - "properties": { - "blob": { - "type": "string" - }, - "mimeType": { - "type": [ - "string", - "null" - ] - }, - "uri": { - "type": "string" - } - }, - "required": [ - "blob", - "uri" - ], - "type": "object" - }, "ByteRange": { "properties": { "end": { @@ -405,61 +326,6 @@ ], "type": "string" }, - "ContentBlock": { - "anyOf": [ - { - "$ref": "#/definitions/TextContent" - }, - { - "$ref": "#/definitions/ImageContent" - }, - { - "$ref": "#/definitions/AudioContent" - }, - { - "$ref": "#/definitions/ResourceLink" - }, - { - "$ref": "#/definitions/EmbeddedResource" - } - ] - }, - "EmbeddedResource": { - "description": "The contents of a resource, embedded into a prompt or tool call result.\n\nIt is up to the client how best to render embedded resources for the benefit of the LLM and/or the user.", - "properties": { - "annotations": { - "anyOf": [ - { - "$ref": "#/definitions/Annotations" - }, - { - "type": "null" - } - ] - }, - "resource": { - "$ref": "#/definitions/EmbeddedResourceResource" - }, - "type": { - "type": "string" - } - }, - "required": [ - "resource", - "type" - ], - "type": "object" - }, - "EmbeddedResourceResource": { - "anyOf": [ - { - "$ref": "#/definitions/TextResourceContents" - }, - { - "$ref": "#/definitions/BlobResourceContents" - } - ] - }, "FileUpdateChange": { "properties": { "diff": { @@ -479,36 +345,6 @@ ], "type": "object" }, - "ImageContent": { - "description": "An image provided to or from an LLM.", - "properties": { - "annotations": { - "anyOf": [ - { - "$ref": "#/definitions/Annotations" - }, - { - "type": "null" - } - ] - }, - "data": { - "type": "string" - }, - "mimeType": { - "type": "string" - }, - "type": { - "type": "string" - } - }, - "required": [ - "data", - "mimeType", - "type" - ], - "type": "object" - }, "McpToolCallError": { "properties": { "message": { @@ -523,9 +359,7 @@ "McpToolCallResult": { "properties": { "content": { - "items": { - "$ref": "#/definitions/ContentBlock" - }, + "items": true, "type": "array" }, "structuredContent": true @@ -610,95 +444,6 @@ } ] }, - "ResourceLink": { - "description": "A resource that the server is capable of reading, included in a prompt or tool call result.\n\nNote: resource links returned by tools are not guaranteed to appear in the results of `resources/list` requests.", - "properties": { - "annotations": { - "anyOf": [ - { - "$ref": "#/definitions/Annotations" - }, - { - "type": "null" - } - ] - }, - "description": { - "type": [ - "string", - "null" - ] - }, - "mimeType": { - "type": [ - "string", - "null" - ] - }, - "name": { - "type": "string" - }, - "size": { - "format": "int64", - "type": [ - "integer", - "null" - ] - }, - "title": { - "type": [ - "string", - "null" - ] - }, - "type": { - "type": "string" - }, - "uri": { - "type": "string" - } - }, - "required": [ - "name", - "type", - "uri" - ], - "type": "object" - }, - "Role": { - "description": "The sender or recipient of messages and data in a conversation.", - "enum": [ - "assistant", - "user" - ], - "type": "string" - }, - "TextContent": { - "description": "Text provided to or from an LLM.", - "properties": { - "annotations": { - "anyOf": [ - { - "$ref": "#/definitions/Annotations" - }, - { - "type": "null" - } - ] - }, - "text": { - "type": "string" - }, - "type": { - "type": "string" - } - }, - "required": [ - "text", - "type" - ], - "type": "object" - }, "TextElement": { "properties": { "byteRange": { @@ -722,27 +467,6 @@ ], "type": "object" }, - "TextResourceContents": { - "properties": { - "mimeType": { - "type": [ - "string", - "null" - ] - }, - "text": { - "type": "string" - }, - "uri": { - "type": "string" - } - }, - "required": [ - "text", - "uri" - ], - "type": "object" - }, "ThreadItem": { "oneOf": [ { diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json index 0a3826d8fd..61b12ceff3 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json @@ -5,34 +5,6 @@ "description": "A path that is guaranteed to be absolute and normalized (though it is not guaranteed to be canonicalized or exist on the filesystem).\n\nIMPORTANT: When deserializing an `AbsolutePathBuf`, a base path must be set using [AbsolutePathBufGuard::new]. If no base path is set, the deserialization will fail unless the path being deserialized is already absolute.", "type": "string" }, - "Annotations": { - "description": "Optional annotations for the client. The client can use annotations to inform how objects are used or displayed", - "properties": { - "audience": { - "items": { - "$ref": "#/definitions/Role" - }, - "type": [ - "array", - "null" - ] - }, - "lastModified": { - "type": [ - "string", - "null" - ] - }, - "priority": { - "format": "double", - "type": [ - "number", - "null" - ] - } - }, - "type": "object" - }, "AskForApproval": { "enum": [ "untrusted", @@ -42,57 +14,6 @@ ], "type": "string" }, - "AudioContent": { - "description": "Audio provided to or from an LLM.", - "properties": { - "annotations": { - "anyOf": [ - { - "$ref": "#/definitions/Annotations" - }, - { - "type": "null" - } - ] - }, - "data": { - "type": "string" - }, - "mimeType": { - "type": "string" - }, - "type": { - "type": "string" - } - }, - "required": [ - "data", - "mimeType", - "type" - ], - "type": "object" - }, - "BlobResourceContents": { - "properties": { - "blob": { - "type": "string" - }, - "mimeType": { - "type": [ - "string", - "null" - ] - }, - "uri": { - "type": "string" - } - }, - "required": [ - "blob", - "uri" - ], - "type": "object" - }, "ByteRange": { "properties": { "end": { @@ -418,61 +339,6 @@ ], "type": "string" }, - "ContentBlock": { - "anyOf": [ - { - "$ref": "#/definitions/TextContent" - }, - { - "$ref": "#/definitions/ImageContent" - }, - { - "$ref": "#/definitions/AudioContent" - }, - { - "$ref": "#/definitions/ResourceLink" - }, - { - "$ref": "#/definitions/EmbeddedResource" - } - ] - }, - "EmbeddedResource": { - "description": "The contents of a resource, embedded into a prompt or tool call result.\n\nIt is up to the client how best to render embedded resources for the benefit of the LLM and/or the user.", - "properties": { - "annotations": { - "anyOf": [ - { - "$ref": "#/definitions/Annotations" - }, - { - "type": "null" - } - ] - }, - "resource": { - "$ref": "#/definitions/EmbeddedResourceResource" - }, - "type": { - "type": "string" - } - }, - "required": [ - "resource", - "type" - ], - "type": "object" - }, - "EmbeddedResourceResource": { - "anyOf": [ - { - "$ref": "#/definitions/TextResourceContents" - }, - { - "$ref": "#/definitions/BlobResourceContents" - } - ] - }, "FileUpdateChange": { "properties": { "diff": { @@ -515,36 +381,6 @@ }, "type": "object" }, - "ImageContent": { - "description": "An image provided to or from an LLM.", - "properties": { - "annotations": { - "anyOf": [ - { - "$ref": "#/definitions/Annotations" - }, - { - "type": "null" - } - ] - }, - "data": { - "type": "string" - }, - "mimeType": { - "type": "string" - }, - "type": { - "type": "string" - } - }, - "required": [ - "data", - "mimeType", - "type" - ], - "type": "object" - }, "McpToolCallError": { "properties": { "message": { @@ -559,9 +395,7 @@ "McpToolCallResult": { "properties": { "content": { - "items": { - "$ref": "#/definitions/ContentBlock" - }, + "items": true, "type": "array" }, "structuredContent": true @@ -665,69 +499,6 @@ ], "type": "string" }, - "ResourceLink": { - "description": "A resource that the server is capable of reading, included in a prompt or tool call result.\n\nNote: resource links returned by tools are not guaranteed to appear in the results of `resources/list` requests.", - "properties": { - "annotations": { - "anyOf": [ - { - "$ref": "#/definitions/Annotations" - }, - { - "type": "null" - } - ] - }, - "description": { - "type": [ - "string", - "null" - ] - }, - "mimeType": { - "type": [ - "string", - "null" - ] - }, - "name": { - "type": "string" - }, - "size": { - "format": "int64", - "type": [ - "integer", - "null" - ] - }, - "title": { - "type": [ - "string", - "null" - ] - }, - "type": { - "type": "string" - }, - "uri": { - "type": "string" - } - }, - "required": [ - "name", - "type", - "uri" - ], - "type": "object" - }, - "Role": { - "description": "The sender or recipient of messages and data in a conversation.", - "enum": [ - "assistant", - "user" - ], - "type": "string" - }, "SandboxPolicy": { "oneOf": [ { @@ -900,32 +671,6 @@ } ] }, - "TextContent": { - "description": "Text provided to or from an LLM.", - "properties": { - "annotations": { - "anyOf": [ - { - "$ref": "#/definitions/Annotations" - }, - { - "type": "null" - } - ] - }, - "text": { - "type": "string" - }, - "type": { - "type": "string" - } - }, - "required": [ - "text", - "type" - ], - "type": "object" - }, "TextElement": { "properties": { "byteRange": { @@ -949,27 +694,6 @@ ], "type": "object" }, - "TextResourceContents": { - "properties": { - "mimeType": { - "type": [ - "string", - "null" - ] - }, - "text": { - "type": "string" - }, - "uri": { - "type": "string" - } - }, - "required": [ - "text", - "uri" - ], - "type": "object" - }, "Thread": { "properties": { "cliVersion": { diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadListResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadListResponse.json index eb4ec81f68..50c3d60e37 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadListResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadListResponse.json @@ -1,85 +1,6 @@ { "$schema": "http://json-schema.org/draft-07/schema#", "definitions": { - "Annotations": { - "description": "Optional annotations for the client. The client can use annotations to inform how objects are used or displayed", - "properties": { - "audience": { - "items": { - "$ref": "#/definitions/Role" - }, - "type": [ - "array", - "null" - ] - }, - "lastModified": { - "type": [ - "string", - "null" - ] - }, - "priority": { - "format": "double", - "type": [ - "number", - "null" - ] - } - }, - "type": "object" - }, - "AudioContent": { - "description": "Audio provided to or from an LLM.", - "properties": { - "annotations": { - "anyOf": [ - { - "$ref": "#/definitions/Annotations" - }, - { - "type": "null" - } - ] - }, - "data": { - "type": "string" - }, - "mimeType": { - "type": "string" - }, - "type": { - "type": "string" - } - }, - "required": [ - "data", - "mimeType", - "type" - ], - "type": "object" - }, - "BlobResourceContents": { - "properties": { - "blob": { - "type": "string" - }, - "mimeType": { - "type": [ - "string", - "null" - ] - }, - "uri": { - "type": "string" - } - }, - "required": [ - "blob", - "uri" - ], - "type": "object" - }, "ByteRange": { "properties": { "end": { @@ -405,61 +326,6 @@ ], "type": "string" }, - "ContentBlock": { - "anyOf": [ - { - "$ref": "#/definitions/TextContent" - }, - { - "$ref": "#/definitions/ImageContent" - }, - { - "$ref": "#/definitions/AudioContent" - }, - { - "$ref": "#/definitions/ResourceLink" - }, - { - "$ref": "#/definitions/EmbeddedResource" - } - ] - }, - "EmbeddedResource": { - "description": "The contents of a resource, embedded into a prompt or tool call result.\n\nIt is up to the client how best to render embedded resources for the benefit of the LLM and/or the user.", - "properties": { - "annotations": { - "anyOf": [ - { - "$ref": "#/definitions/Annotations" - }, - { - "type": "null" - } - ] - }, - "resource": { - "$ref": "#/definitions/EmbeddedResourceResource" - }, - "type": { - "type": "string" - } - }, - "required": [ - "resource", - "type" - ], - "type": "object" - }, - "EmbeddedResourceResource": { - "anyOf": [ - { - "$ref": "#/definitions/TextResourceContents" - }, - { - "$ref": "#/definitions/BlobResourceContents" - } - ] - }, "FileUpdateChange": { "properties": { "diff": { @@ -502,36 +368,6 @@ }, "type": "object" }, - "ImageContent": { - "description": "An image provided to or from an LLM.", - "properties": { - "annotations": { - "anyOf": [ - { - "$ref": "#/definitions/Annotations" - }, - { - "type": "null" - } - ] - }, - "data": { - "type": "string" - }, - "mimeType": { - "type": "string" - }, - "type": { - "type": "string" - } - }, - "required": [ - "data", - "mimeType", - "type" - ], - "type": "object" - }, "McpToolCallError": { "properties": { "message": { @@ -546,9 +382,7 @@ "McpToolCallResult": { "properties": { "content": { - "items": { - "$ref": "#/definitions/ContentBlock" - }, + "items": true, "type": "array" }, "structuredContent": true @@ -633,69 +467,6 @@ } ] }, - "ResourceLink": { - "description": "A resource that the server is capable of reading, included in a prompt or tool call result.\n\nNote: resource links returned by tools are not guaranteed to appear in the results of `resources/list` requests.", - "properties": { - "annotations": { - "anyOf": [ - { - "$ref": "#/definitions/Annotations" - }, - { - "type": "null" - } - ] - }, - "description": { - "type": [ - "string", - "null" - ] - }, - "mimeType": { - "type": [ - "string", - "null" - ] - }, - "name": { - "type": "string" - }, - "size": { - "format": "int64", - "type": [ - "integer", - "null" - ] - }, - "title": { - "type": [ - "string", - "null" - ] - }, - "type": { - "type": "string" - }, - "uri": { - "type": "string" - } - }, - "required": [ - "name", - "type", - "uri" - ], - "type": "object" - }, - "Role": { - "description": "The sender or recipient of messages and data in a conversation.", - "enum": [ - "assistant", - "user" - ], - "type": "string" - }, "SessionSource": { "oneOf": [ { @@ -773,32 +544,6 @@ } ] }, - "TextContent": { - "description": "Text provided to or from an LLM.", - "properties": { - "annotations": { - "anyOf": [ - { - "$ref": "#/definitions/Annotations" - }, - { - "type": "null" - } - ] - }, - "text": { - "type": "string" - }, - "type": { - "type": "string" - } - }, - "required": [ - "text", - "type" - ], - "type": "object" - }, "TextElement": { "properties": { "byteRange": { @@ -822,27 +567,6 @@ ], "type": "object" }, - "TextResourceContents": { - "properties": { - "mimeType": { - "type": [ - "string", - "null" - ] - }, - "text": { - "type": "string" - }, - "uri": { - "type": "string" - } - }, - "required": [ - "text", - "uri" - ], - "type": "object" - }, "Thread": { "properties": { "cliVersion": { diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadReadResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadReadResponse.json index 02dab752e3..c4fa3b2028 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadReadResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadReadResponse.json @@ -1,85 +1,6 @@ { "$schema": "http://json-schema.org/draft-07/schema#", "definitions": { - "Annotations": { - "description": "Optional annotations for the client. The client can use annotations to inform how objects are used or displayed", - "properties": { - "audience": { - "items": { - "$ref": "#/definitions/Role" - }, - "type": [ - "array", - "null" - ] - }, - "lastModified": { - "type": [ - "string", - "null" - ] - }, - "priority": { - "format": "double", - "type": [ - "number", - "null" - ] - } - }, - "type": "object" - }, - "AudioContent": { - "description": "Audio provided to or from an LLM.", - "properties": { - "annotations": { - "anyOf": [ - { - "$ref": "#/definitions/Annotations" - }, - { - "type": "null" - } - ] - }, - "data": { - "type": "string" - }, - "mimeType": { - "type": "string" - }, - "type": { - "type": "string" - } - }, - "required": [ - "data", - "mimeType", - "type" - ], - "type": "object" - }, - "BlobResourceContents": { - "properties": { - "blob": { - "type": "string" - }, - "mimeType": { - "type": [ - "string", - "null" - ] - }, - "uri": { - "type": "string" - } - }, - "required": [ - "blob", - "uri" - ], - "type": "object" - }, "ByteRange": { "properties": { "end": { @@ -405,61 +326,6 @@ ], "type": "string" }, - "ContentBlock": { - "anyOf": [ - { - "$ref": "#/definitions/TextContent" - }, - { - "$ref": "#/definitions/ImageContent" - }, - { - "$ref": "#/definitions/AudioContent" - }, - { - "$ref": "#/definitions/ResourceLink" - }, - { - "$ref": "#/definitions/EmbeddedResource" - } - ] - }, - "EmbeddedResource": { - "description": "The contents of a resource, embedded into a prompt or tool call result.\n\nIt is up to the client how best to render embedded resources for the benefit of the LLM and/or the user.", - "properties": { - "annotations": { - "anyOf": [ - { - "$ref": "#/definitions/Annotations" - }, - { - "type": "null" - } - ] - }, - "resource": { - "$ref": "#/definitions/EmbeddedResourceResource" - }, - "type": { - "type": "string" - } - }, - "required": [ - "resource", - "type" - ], - "type": "object" - }, - "EmbeddedResourceResource": { - "anyOf": [ - { - "$ref": "#/definitions/TextResourceContents" - }, - { - "$ref": "#/definitions/BlobResourceContents" - } - ] - }, "FileUpdateChange": { "properties": { "diff": { @@ -502,36 +368,6 @@ }, "type": "object" }, - "ImageContent": { - "description": "An image provided to or from an LLM.", - "properties": { - "annotations": { - "anyOf": [ - { - "$ref": "#/definitions/Annotations" - }, - { - "type": "null" - } - ] - }, - "data": { - "type": "string" - }, - "mimeType": { - "type": "string" - }, - "type": { - "type": "string" - } - }, - "required": [ - "data", - "mimeType", - "type" - ], - "type": "object" - }, "McpToolCallError": { "properties": { "message": { @@ -546,9 +382,7 @@ "McpToolCallResult": { "properties": { "content": { - "items": { - "$ref": "#/definitions/ContentBlock" - }, + "items": true, "type": "array" }, "structuredContent": true @@ -633,69 +467,6 @@ } ] }, - "ResourceLink": { - "description": "A resource that the server is capable of reading, included in a prompt or tool call result.\n\nNote: resource links returned by tools are not guaranteed to appear in the results of `resources/list` requests.", - "properties": { - "annotations": { - "anyOf": [ - { - "$ref": "#/definitions/Annotations" - }, - { - "type": "null" - } - ] - }, - "description": { - "type": [ - "string", - "null" - ] - }, - "mimeType": { - "type": [ - "string", - "null" - ] - }, - "name": { - "type": "string" - }, - "size": { - "format": "int64", - "type": [ - "integer", - "null" - ] - }, - "title": { - "type": [ - "string", - "null" - ] - }, - "type": { - "type": "string" - }, - "uri": { - "type": "string" - } - }, - "required": [ - "name", - "type", - "uri" - ], - "type": "object" - }, - "Role": { - "description": "The sender or recipient of messages and data in a conversation.", - "enum": [ - "assistant", - "user" - ], - "type": "string" - }, "SessionSource": { "oneOf": [ { @@ -773,32 +544,6 @@ } ] }, - "TextContent": { - "description": "Text provided to or from an LLM.", - "properties": { - "annotations": { - "anyOf": [ - { - "$ref": "#/definitions/Annotations" - }, - { - "type": "null" - } - ] - }, - "text": { - "type": "string" - }, - "type": { - "type": "string" - } - }, - "required": [ - "text", - "type" - ], - "type": "object" - }, "TextElement": { "properties": { "byteRange": { @@ -822,27 +567,6 @@ ], "type": "object" }, - "TextResourceContents": { - "properties": { - "mimeType": { - "type": [ - "string", - "null" - ] - }, - "text": { - "type": "string" - }, - "uri": { - "type": "string" - } - }, - "required": [ - "text", - "uri" - ], - "type": "object" - }, "Thread": { "properties": { "cliVersion": { diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json index dad4ccbfeb..0534f6e16e 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json @@ -5,34 +5,6 @@ "description": "A path that is guaranteed to be absolute and normalized (though it is not guaranteed to be canonicalized or exist on the filesystem).\n\nIMPORTANT: When deserializing an `AbsolutePathBuf`, a base path must be set using [AbsolutePathBufGuard::new]. If no base path is set, the deserialization will fail unless the path being deserialized is already absolute.", "type": "string" }, - "Annotations": { - "description": "Optional annotations for the client. The client can use annotations to inform how objects are used or displayed", - "properties": { - "audience": { - "items": { - "$ref": "#/definitions/Role" - }, - "type": [ - "array", - "null" - ] - }, - "lastModified": { - "type": [ - "string", - "null" - ] - }, - "priority": { - "format": "double", - "type": [ - "number", - "null" - ] - } - }, - "type": "object" - }, "AskForApproval": { "enum": [ "untrusted", @@ -42,57 +14,6 @@ ], "type": "string" }, - "AudioContent": { - "description": "Audio provided to or from an LLM.", - "properties": { - "annotations": { - "anyOf": [ - { - "$ref": "#/definitions/Annotations" - }, - { - "type": "null" - } - ] - }, - "data": { - "type": "string" - }, - "mimeType": { - "type": "string" - }, - "type": { - "type": "string" - } - }, - "required": [ - "data", - "mimeType", - "type" - ], - "type": "object" - }, - "BlobResourceContents": { - "properties": { - "blob": { - "type": "string" - }, - "mimeType": { - "type": [ - "string", - "null" - ] - }, - "uri": { - "type": "string" - } - }, - "required": [ - "blob", - "uri" - ], - "type": "object" - }, "ByteRange": { "properties": { "end": { @@ -418,61 +339,6 @@ ], "type": "string" }, - "ContentBlock": { - "anyOf": [ - { - "$ref": "#/definitions/TextContent" - }, - { - "$ref": "#/definitions/ImageContent" - }, - { - "$ref": "#/definitions/AudioContent" - }, - { - "$ref": "#/definitions/ResourceLink" - }, - { - "$ref": "#/definitions/EmbeddedResource" - } - ] - }, - "EmbeddedResource": { - "description": "The contents of a resource, embedded into a prompt or tool call result.\n\nIt is up to the client how best to render embedded resources for the benefit of the LLM and/or the user.", - "properties": { - "annotations": { - "anyOf": [ - { - "$ref": "#/definitions/Annotations" - }, - { - "type": "null" - } - ] - }, - "resource": { - "$ref": "#/definitions/EmbeddedResourceResource" - }, - "type": { - "type": "string" - } - }, - "required": [ - "resource", - "type" - ], - "type": "object" - }, - "EmbeddedResourceResource": { - "anyOf": [ - { - "$ref": "#/definitions/TextResourceContents" - }, - { - "$ref": "#/definitions/BlobResourceContents" - } - ] - }, "FileUpdateChange": { "properties": { "diff": { @@ -515,36 +381,6 @@ }, "type": "object" }, - "ImageContent": { - "description": "An image provided to or from an LLM.", - "properties": { - "annotations": { - "anyOf": [ - { - "$ref": "#/definitions/Annotations" - }, - { - "type": "null" - } - ] - }, - "data": { - "type": "string" - }, - "mimeType": { - "type": "string" - }, - "type": { - "type": "string" - } - }, - "required": [ - "data", - "mimeType", - "type" - ], - "type": "object" - }, "McpToolCallError": { "properties": { "message": { @@ -559,9 +395,7 @@ "McpToolCallResult": { "properties": { "content": { - "items": { - "$ref": "#/definitions/ContentBlock" - }, + "items": true, "type": "array" }, "structuredContent": true @@ -665,69 +499,6 @@ ], "type": "string" }, - "ResourceLink": { - "description": "A resource that the server is capable of reading, included in a prompt or tool call result.\n\nNote: resource links returned by tools are not guaranteed to appear in the results of `resources/list` requests.", - "properties": { - "annotations": { - "anyOf": [ - { - "$ref": "#/definitions/Annotations" - }, - { - "type": "null" - } - ] - }, - "description": { - "type": [ - "string", - "null" - ] - }, - "mimeType": { - "type": [ - "string", - "null" - ] - }, - "name": { - "type": "string" - }, - "size": { - "format": "int64", - "type": [ - "integer", - "null" - ] - }, - "title": { - "type": [ - "string", - "null" - ] - }, - "type": { - "type": "string" - }, - "uri": { - "type": "string" - } - }, - "required": [ - "name", - "type", - "uri" - ], - "type": "object" - }, - "Role": { - "description": "The sender or recipient of messages and data in a conversation.", - "enum": [ - "assistant", - "user" - ], - "type": "string" - }, "SandboxPolicy": { "oneOf": [ { @@ -900,32 +671,6 @@ } ] }, - "TextContent": { - "description": "Text provided to or from an LLM.", - "properties": { - "annotations": { - "anyOf": [ - { - "$ref": "#/definitions/Annotations" - }, - { - "type": "null" - } - ] - }, - "text": { - "type": "string" - }, - "type": { - "type": "string" - } - }, - "required": [ - "text", - "type" - ], - "type": "object" - }, "TextElement": { "properties": { "byteRange": { @@ -949,27 +694,6 @@ ], "type": "object" }, - "TextResourceContents": { - "properties": { - "mimeType": { - "type": [ - "string", - "null" - ] - }, - "text": { - "type": "string" - }, - "uri": { - "type": "string" - } - }, - "required": [ - "text", - "uri" - ], - "type": "object" - }, "Thread": { "properties": { "cliVersion": { diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadRollbackResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadRollbackResponse.json index 3ebb5052d3..148bbe7d7d 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadRollbackResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadRollbackResponse.json @@ -1,85 +1,6 @@ { "$schema": "http://json-schema.org/draft-07/schema#", "definitions": { - "Annotations": { - "description": "Optional annotations for the client. The client can use annotations to inform how objects are used or displayed", - "properties": { - "audience": { - "items": { - "$ref": "#/definitions/Role" - }, - "type": [ - "array", - "null" - ] - }, - "lastModified": { - "type": [ - "string", - "null" - ] - }, - "priority": { - "format": "double", - "type": [ - "number", - "null" - ] - } - }, - "type": "object" - }, - "AudioContent": { - "description": "Audio provided to or from an LLM.", - "properties": { - "annotations": { - "anyOf": [ - { - "$ref": "#/definitions/Annotations" - }, - { - "type": "null" - } - ] - }, - "data": { - "type": "string" - }, - "mimeType": { - "type": "string" - }, - "type": { - "type": "string" - } - }, - "required": [ - "data", - "mimeType", - "type" - ], - "type": "object" - }, - "BlobResourceContents": { - "properties": { - "blob": { - "type": "string" - }, - "mimeType": { - "type": [ - "string", - "null" - ] - }, - "uri": { - "type": "string" - } - }, - "required": [ - "blob", - "uri" - ], - "type": "object" - }, "ByteRange": { "properties": { "end": { @@ -405,61 +326,6 @@ ], "type": "string" }, - "ContentBlock": { - "anyOf": [ - { - "$ref": "#/definitions/TextContent" - }, - { - "$ref": "#/definitions/ImageContent" - }, - { - "$ref": "#/definitions/AudioContent" - }, - { - "$ref": "#/definitions/ResourceLink" - }, - { - "$ref": "#/definitions/EmbeddedResource" - } - ] - }, - "EmbeddedResource": { - "description": "The contents of a resource, embedded into a prompt or tool call result.\n\nIt is up to the client how best to render embedded resources for the benefit of the LLM and/or the user.", - "properties": { - "annotations": { - "anyOf": [ - { - "$ref": "#/definitions/Annotations" - }, - { - "type": "null" - } - ] - }, - "resource": { - "$ref": "#/definitions/EmbeddedResourceResource" - }, - "type": { - "type": "string" - } - }, - "required": [ - "resource", - "type" - ], - "type": "object" - }, - "EmbeddedResourceResource": { - "anyOf": [ - { - "$ref": "#/definitions/TextResourceContents" - }, - { - "$ref": "#/definitions/BlobResourceContents" - } - ] - }, "FileUpdateChange": { "properties": { "diff": { @@ -502,36 +368,6 @@ }, "type": "object" }, - "ImageContent": { - "description": "An image provided to or from an LLM.", - "properties": { - "annotations": { - "anyOf": [ - { - "$ref": "#/definitions/Annotations" - }, - { - "type": "null" - } - ] - }, - "data": { - "type": "string" - }, - "mimeType": { - "type": "string" - }, - "type": { - "type": "string" - } - }, - "required": [ - "data", - "mimeType", - "type" - ], - "type": "object" - }, "McpToolCallError": { "properties": { "message": { @@ -546,9 +382,7 @@ "McpToolCallResult": { "properties": { "content": { - "items": { - "$ref": "#/definitions/ContentBlock" - }, + "items": true, "type": "array" }, "structuredContent": true @@ -633,69 +467,6 @@ } ] }, - "ResourceLink": { - "description": "A resource that the server is capable of reading, included in a prompt or tool call result.\n\nNote: resource links returned by tools are not guaranteed to appear in the results of `resources/list` requests.", - "properties": { - "annotations": { - "anyOf": [ - { - "$ref": "#/definitions/Annotations" - }, - { - "type": "null" - } - ] - }, - "description": { - "type": [ - "string", - "null" - ] - }, - "mimeType": { - "type": [ - "string", - "null" - ] - }, - "name": { - "type": "string" - }, - "size": { - "format": "int64", - "type": [ - "integer", - "null" - ] - }, - "title": { - "type": [ - "string", - "null" - ] - }, - "type": { - "type": "string" - }, - "uri": { - "type": "string" - } - }, - "required": [ - "name", - "type", - "uri" - ], - "type": "object" - }, - "Role": { - "description": "The sender or recipient of messages and data in a conversation.", - "enum": [ - "assistant", - "user" - ], - "type": "string" - }, "SessionSource": { "oneOf": [ { @@ -773,32 +544,6 @@ } ] }, - "TextContent": { - "description": "Text provided to or from an LLM.", - "properties": { - "annotations": { - "anyOf": [ - { - "$ref": "#/definitions/Annotations" - }, - { - "type": "null" - } - ] - }, - "text": { - "type": "string" - }, - "type": { - "type": "string" - } - }, - "required": [ - "text", - "type" - ], - "type": "object" - }, "TextElement": { "properties": { "byteRange": { @@ -822,27 +567,6 @@ ], "type": "object" }, - "TextResourceContents": { - "properties": { - "mimeType": { - "type": [ - "string", - "null" - ] - }, - "text": { - "type": "string" - }, - "uri": { - "type": "string" - } - }, - "required": [ - "text", - "uri" - ], - "type": "object" - }, "Thread": { "properties": { "cliVersion": { diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json index d88d3694de..c85d7ce97a 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json @@ -5,34 +5,6 @@ "description": "A path that is guaranteed to be absolute and normalized (though it is not guaranteed to be canonicalized or exist on the filesystem).\n\nIMPORTANT: When deserializing an `AbsolutePathBuf`, a base path must be set using [AbsolutePathBufGuard::new]. If no base path is set, the deserialization will fail unless the path being deserialized is already absolute.", "type": "string" }, - "Annotations": { - "description": "Optional annotations for the client. The client can use annotations to inform how objects are used or displayed", - "properties": { - "audience": { - "items": { - "$ref": "#/definitions/Role" - }, - "type": [ - "array", - "null" - ] - }, - "lastModified": { - "type": [ - "string", - "null" - ] - }, - "priority": { - "format": "double", - "type": [ - "number", - "null" - ] - } - }, - "type": "object" - }, "AskForApproval": { "enum": [ "untrusted", @@ -42,57 +14,6 @@ ], "type": "string" }, - "AudioContent": { - "description": "Audio provided to or from an LLM.", - "properties": { - "annotations": { - "anyOf": [ - { - "$ref": "#/definitions/Annotations" - }, - { - "type": "null" - } - ] - }, - "data": { - "type": "string" - }, - "mimeType": { - "type": "string" - }, - "type": { - "type": "string" - } - }, - "required": [ - "data", - "mimeType", - "type" - ], - "type": "object" - }, - "BlobResourceContents": { - "properties": { - "blob": { - "type": "string" - }, - "mimeType": { - "type": [ - "string", - "null" - ] - }, - "uri": { - "type": "string" - } - }, - "required": [ - "blob", - "uri" - ], - "type": "object" - }, "ByteRange": { "properties": { "end": { @@ -418,61 +339,6 @@ ], "type": "string" }, - "ContentBlock": { - "anyOf": [ - { - "$ref": "#/definitions/TextContent" - }, - { - "$ref": "#/definitions/ImageContent" - }, - { - "$ref": "#/definitions/AudioContent" - }, - { - "$ref": "#/definitions/ResourceLink" - }, - { - "$ref": "#/definitions/EmbeddedResource" - } - ] - }, - "EmbeddedResource": { - "description": "The contents of a resource, embedded into a prompt or tool call result.\n\nIt is up to the client how best to render embedded resources for the benefit of the LLM and/or the user.", - "properties": { - "annotations": { - "anyOf": [ - { - "$ref": "#/definitions/Annotations" - }, - { - "type": "null" - } - ] - }, - "resource": { - "$ref": "#/definitions/EmbeddedResourceResource" - }, - "type": { - "type": "string" - } - }, - "required": [ - "resource", - "type" - ], - "type": "object" - }, - "EmbeddedResourceResource": { - "anyOf": [ - { - "$ref": "#/definitions/TextResourceContents" - }, - { - "$ref": "#/definitions/BlobResourceContents" - } - ] - }, "FileUpdateChange": { "properties": { "diff": { @@ -515,36 +381,6 @@ }, "type": "object" }, - "ImageContent": { - "description": "An image provided to or from an LLM.", - "properties": { - "annotations": { - "anyOf": [ - { - "$ref": "#/definitions/Annotations" - }, - { - "type": "null" - } - ] - }, - "data": { - "type": "string" - }, - "mimeType": { - "type": "string" - }, - "type": { - "type": "string" - } - }, - "required": [ - "data", - "mimeType", - "type" - ], - "type": "object" - }, "McpToolCallError": { "properties": { "message": { @@ -559,9 +395,7 @@ "McpToolCallResult": { "properties": { "content": { - "items": { - "$ref": "#/definitions/ContentBlock" - }, + "items": true, "type": "array" }, "structuredContent": true @@ -665,69 +499,6 @@ ], "type": "string" }, - "ResourceLink": { - "description": "A resource that the server is capable of reading, included in a prompt or tool call result.\n\nNote: resource links returned by tools are not guaranteed to appear in the results of `resources/list` requests.", - "properties": { - "annotations": { - "anyOf": [ - { - "$ref": "#/definitions/Annotations" - }, - { - "type": "null" - } - ] - }, - "description": { - "type": [ - "string", - "null" - ] - }, - "mimeType": { - "type": [ - "string", - "null" - ] - }, - "name": { - "type": "string" - }, - "size": { - "format": "int64", - "type": [ - "integer", - "null" - ] - }, - "title": { - "type": [ - "string", - "null" - ] - }, - "type": { - "type": "string" - }, - "uri": { - "type": "string" - } - }, - "required": [ - "name", - "type", - "uri" - ], - "type": "object" - }, - "Role": { - "description": "The sender or recipient of messages and data in a conversation.", - "enum": [ - "assistant", - "user" - ], - "type": "string" - }, "SandboxPolicy": { "oneOf": [ { @@ -900,32 +671,6 @@ } ] }, - "TextContent": { - "description": "Text provided to or from an LLM.", - "properties": { - "annotations": { - "anyOf": [ - { - "$ref": "#/definitions/Annotations" - }, - { - "type": "null" - } - ] - }, - "text": { - "type": "string" - }, - "type": { - "type": "string" - } - }, - "required": [ - "text", - "type" - ], - "type": "object" - }, "TextElement": { "properties": { "byteRange": { @@ -949,27 +694,6 @@ ], "type": "object" }, - "TextResourceContents": { - "properties": { - "mimeType": { - "type": [ - "string", - "null" - ] - }, - "text": { - "type": "string" - }, - "uri": { - "type": "string" - } - }, - "required": [ - "text", - "uri" - ], - "type": "object" - }, "Thread": { "properties": { "cliVersion": { diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartedNotification.json index 0c8d47136e..44aca775c1 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartedNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartedNotification.json @@ -1,85 +1,6 @@ { "$schema": "http://json-schema.org/draft-07/schema#", "definitions": { - "Annotations": { - "description": "Optional annotations for the client. The client can use annotations to inform how objects are used or displayed", - "properties": { - "audience": { - "items": { - "$ref": "#/definitions/Role" - }, - "type": [ - "array", - "null" - ] - }, - "lastModified": { - "type": [ - "string", - "null" - ] - }, - "priority": { - "format": "double", - "type": [ - "number", - "null" - ] - } - }, - "type": "object" - }, - "AudioContent": { - "description": "Audio provided to or from an LLM.", - "properties": { - "annotations": { - "anyOf": [ - { - "$ref": "#/definitions/Annotations" - }, - { - "type": "null" - } - ] - }, - "data": { - "type": "string" - }, - "mimeType": { - "type": "string" - }, - "type": { - "type": "string" - } - }, - "required": [ - "data", - "mimeType", - "type" - ], - "type": "object" - }, - "BlobResourceContents": { - "properties": { - "blob": { - "type": "string" - }, - "mimeType": { - "type": [ - "string", - "null" - ] - }, - "uri": { - "type": "string" - } - }, - "required": [ - "blob", - "uri" - ], - "type": "object" - }, "ByteRange": { "properties": { "end": { @@ -405,61 +326,6 @@ ], "type": "string" }, - "ContentBlock": { - "anyOf": [ - { - "$ref": "#/definitions/TextContent" - }, - { - "$ref": "#/definitions/ImageContent" - }, - { - "$ref": "#/definitions/AudioContent" - }, - { - "$ref": "#/definitions/ResourceLink" - }, - { - "$ref": "#/definitions/EmbeddedResource" - } - ] - }, - "EmbeddedResource": { - "description": "The contents of a resource, embedded into a prompt or tool call result.\n\nIt is up to the client how best to render embedded resources for the benefit of the LLM and/or the user.", - "properties": { - "annotations": { - "anyOf": [ - { - "$ref": "#/definitions/Annotations" - }, - { - "type": "null" - } - ] - }, - "resource": { - "$ref": "#/definitions/EmbeddedResourceResource" - }, - "type": { - "type": "string" - } - }, - "required": [ - "resource", - "type" - ], - "type": "object" - }, - "EmbeddedResourceResource": { - "anyOf": [ - { - "$ref": "#/definitions/TextResourceContents" - }, - { - "$ref": "#/definitions/BlobResourceContents" - } - ] - }, "FileUpdateChange": { "properties": { "diff": { @@ -502,36 +368,6 @@ }, "type": "object" }, - "ImageContent": { - "description": "An image provided to or from an LLM.", - "properties": { - "annotations": { - "anyOf": [ - { - "$ref": "#/definitions/Annotations" - }, - { - "type": "null" - } - ] - }, - "data": { - "type": "string" - }, - "mimeType": { - "type": "string" - }, - "type": { - "type": "string" - } - }, - "required": [ - "data", - "mimeType", - "type" - ], - "type": "object" - }, "McpToolCallError": { "properties": { "message": { @@ -546,9 +382,7 @@ "McpToolCallResult": { "properties": { "content": { - "items": { - "$ref": "#/definitions/ContentBlock" - }, + "items": true, "type": "array" }, "structuredContent": true @@ -633,69 +467,6 @@ } ] }, - "ResourceLink": { - "description": "A resource that the server is capable of reading, included in a prompt or tool call result.\n\nNote: resource links returned by tools are not guaranteed to appear in the results of `resources/list` requests.", - "properties": { - "annotations": { - "anyOf": [ - { - "$ref": "#/definitions/Annotations" - }, - { - "type": "null" - } - ] - }, - "description": { - "type": [ - "string", - "null" - ] - }, - "mimeType": { - "type": [ - "string", - "null" - ] - }, - "name": { - "type": "string" - }, - "size": { - "format": "int64", - "type": [ - "integer", - "null" - ] - }, - "title": { - "type": [ - "string", - "null" - ] - }, - "type": { - "type": "string" - }, - "uri": { - "type": "string" - } - }, - "required": [ - "name", - "type", - "uri" - ], - "type": "object" - }, - "Role": { - "description": "The sender or recipient of messages and data in a conversation.", - "enum": [ - "assistant", - "user" - ], - "type": "string" - }, "SessionSource": { "oneOf": [ { @@ -773,32 +544,6 @@ } ] }, - "TextContent": { - "description": "Text provided to or from an LLM.", - "properties": { - "annotations": { - "anyOf": [ - { - "$ref": "#/definitions/Annotations" - }, - { - "type": "null" - } - ] - }, - "text": { - "type": "string" - }, - "type": { - "type": "string" - } - }, - "required": [ - "text", - "type" - ], - "type": "object" - }, "TextElement": { "properties": { "byteRange": { @@ -822,27 +567,6 @@ ], "type": "object" }, - "TextResourceContents": { - "properties": { - "mimeType": { - "type": [ - "string", - "null" - ] - }, - "text": { - "type": "string" - }, - "uri": { - "type": "string" - } - }, - "required": [ - "text", - "uri" - ], - "type": "object" - }, "Thread": { "properties": { "cliVersion": { diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadUnarchiveResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadUnarchiveResponse.json index 46478554bb..055c3fd4a9 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadUnarchiveResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadUnarchiveResponse.json @@ -1,85 +1,6 @@ { "$schema": "http://json-schema.org/draft-07/schema#", "definitions": { - "Annotations": { - "description": "Optional annotations for the client. The client can use annotations to inform how objects are used or displayed", - "properties": { - "audience": { - "items": { - "$ref": "#/definitions/Role" - }, - "type": [ - "array", - "null" - ] - }, - "lastModified": { - "type": [ - "string", - "null" - ] - }, - "priority": { - "format": "double", - "type": [ - "number", - "null" - ] - } - }, - "type": "object" - }, - "AudioContent": { - "description": "Audio provided to or from an LLM.", - "properties": { - "annotations": { - "anyOf": [ - { - "$ref": "#/definitions/Annotations" - }, - { - "type": "null" - } - ] - }, - "data": { - "type": "string" - }, - "mimeType": { - "type": "string" - }, - "type": { - "type": "string" - } - }, - "required": [ - "data", - "mimeType", - "type" - ], - "type": "object" - }, - "BlobResourceContents": { - "properties": { - "blob": { - "type": "string" - }, - "mimeType": { - "type": [ - "string", - "null" - ] - }, - "uri": { - "type": "string" - } - }, - "required": [ - "blob", - "uri" - ], - "type": "object" - }, "ByteRange": { "properties": { "end": { @@ -405,61 +326,6 @@ ], "type": "string" }, - "ContentBlock": { - "anyOf": [ - { - "$ref": "#/definitions/TextContent" - }, - { - "$ref": "#/definitions/ImageContent" - }, - { - "$ref": "#/definitions/AudioContent" - }, - { - "$ref": "#/definitions/ResourceLink" - }, - { - "$ref": "#/definitions/EmbeddedResource" - } - ] - }, - "EmbeddedResource": { - "description": "The contents of a resource, embedded into a prompt or tool call result.\n\nIt is up to the client how best to render embedded resources for the benefit of the LLM and/or the user.", - "properties": { - "annotations": { - "anyOf": [ - { - "$ref": "#/definitions/Annotations" - }, - { - "type": "null" - } - ] - }, - "resource": { - "$ref": "#/definitions/EmbeddedResourceResource" - }, - "type": { - "type": "string" - } - }, - "required": [ - "resource", - "type" - ], - "type": "object" - }, - "EmbeddedResourceResource": { - "anyOf": [ - { - "$ref": "#/definitions/TextResourceContents" - }, - { - "$ref": "#/definitions/BlobResourceContents" - } - ] - }, "FileUpdateChange": { "properties": { "diff": { @@ -502,36 +368,6 @@ }, "type": "object" }, - "ImageContent": { - "description": "An image provided to or from an LLM.", - "properties": { - "annotations": { - "anyOf": [ - { - "$ref": "#/definitions/Annotations" - }, - { - "type": "null" - } - ] - }, - "data": { - "type": "string" - }, - "mimeType": { - "type": "string" - }, - "type": { - "type": "string" - } - }, - "required": [ - "data", - "mimeType", - "type" - ], - "type": "object" - }, "McpToolCallError": { "properties": { "message": { @@ -546,9 +382,7 @@ "McpToolCallResult": { "properties": { "content": { - "items": { - "$ref": "#/definitions/ContentBlock" - }, + "items": true, "type": "array" }, "structuredContent": true @@ -633,69 +467,6 @@ } ] }, - "ResourceLink": { - "description": "A resource that the server is capable of reading, included in a prompt or tool call result.\n\nNote: resource links returned by tools are not guaranteed to appear in the results of `resources/list` requests.", - "properties": { - "annotations": { - "anyOf": [ - { - "$ref": "#/definitions/Annotations" - }, - { - "type": "null" - } - ] - }, - "description": { - "type": [ - "string", - "null" - ] - }, - "mimeType": { - "type": [ - "string", - "null" - ] - }, - "name": { - "type": "string" - }, - "size": { - "format": "int64", - "type": [ - "integer", - "null" - ] - }, - "title": { - "type": [ - "string", - "null" - ] - }, - "type": { - "type": "string" - }, - "uri": { - "type": "string" - } - }, - "required": [ - "name", - "type", - "uri" - ], - "type": "object" - }, - "Role": { - "description": "The sender or recipient of messages and data in a conversation.", - "enum": [ - "assistant", - "user" - ], - "type": "string" - }, "SessionSource": { "oneOf": [ { @@ -773,32 +544,6 @@ } ] }, - "TextContent": { - "description": "Text provided to or from an LLM.", - "properties": { - "annotations": { - "anyOf": [ - { - "$ref": "#/definitions/Annotations" - }, - { - "type": "null" - } - ] - }, - "text": { - "type": "string" - }, - "type": { - "type": "string" - } - }, - "required": [ - "text", - "type" - ], - "type": "object" - }, "TextElement": { "properties": { "byteRange": { @@ -822,27 +567,6 @@ ], "type": "object" }, - "TextResourceContents": { - "properties": { - "mimeType": { - "type": [ - "string", - "null" - ] - }, - "text": { - "type": "string" - }, - "uri": { - "type": "string" - } - }, - "required": [ - "text", - "uri" - ], - "type": "object" - }, "Thread": { "properties": { "cliVersion": { diff --git a/codex-rs/app-server-protocol/schema/json/v2/TurnCompletedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/TurnCompletedNotification.json index 918f82a456..798caa5995 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/TurnCompletedNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/TurnCompletedNotification.json @@ -1,85 +1,6 @@ { "$schema": "http://json-schema.org/draft-07/schema#", "definitions": { - "Annotations": { - "description": "Optional annotations for the client. The client can use annotations to inform how objects are used or displayed", - "properties": { - "audience": { - "items": { - "$ref": "#/definitions/Role" - }, - "type": [ - "array", - "null" - ] - }, - "lastModified": { - "type": [ - "string", - "null" - ] - }, - "priority": { - "format": "double", - "type": [ - "number", - "null" - ] - } - }, - "type": "object" - }, - "AudioContent": { - "description": "Audio provided to or from an LLM.", - "properties": { - "annotations": { - "anyOf": [ - { - "$ref": "#/definitions/Annotations" - }, - { - "type": "null" - } - ] - }, - "data": { - "type": "string" - }, - "mimeType": { - "type": "string" - }, - "type": { - "type": "string" - } - }, - "required": [ - "data", - "mimeType", - "type" - ], - "type": "object" - }, - "BlobResourceContents": { - "properties": { - "blob": { - "type": "string" - }, - "mimeType": { - "type": [ - "string", - "null" - ] - }, - "uri": { - "type": "string" - } - }, - "required": [ - "blob", - "uri" - ], - "type": "object" - }, "ByteRange": { "properties": { "end": { @@ -405,61 +326,6 @@ ], "type": "string" }, - "ContentBlock": { - "anyOf": [ - { - "$ref": "#/definitions/TextContent" - }, - { - "$ref": "#/definitions/ImageContent" - }, - { - "$ref": "#/definitions/AudioContent" - }, - { - "$ref": "#/definitions/ResourceLink" - }, - { - "$ref": "#/definitions/EmbeddedResource" - } - ] - }, - "EmbeddedResource": { - "description": "The contents of a resource, embedded into a prompt or tool call result.\n\nIt is up to the client how best to render embedded resources for the benefit of the LLM and/or the user.", - "properties": { - "annotations": { - "anyOf": [ - { - "$ref": "#/definitions/Annotations" - }, - { - "type": "null" - } - ] - }, - "resource": { - "$ref": "#/definitions/EmbeddedResourceResource" - }, - "type": { - "type": "string" - } - }, - "required": [ - "resource", - "type" - ], - "type": "object" - }, - "EmbeddedResourceResource": { - "anyOf": [ - { - "$ref": "#/definitions/TextResourceContents" - }, - { - "$ref": "#/definitions/BlobResourceContents" - } - ] - }, "FileUpdateChange": { "properties": { "diff": { @@ -479,36 +345,6 @@ ], "type": "object" }, - "ImageContent": { - "description": "An image provided to or from an LLM.", - "properties": { - "annotations": { - "anyOf": [ - { - "$ref": "#/definitions/Annotations" - }, - { - "type": "null" - } - ] - }, - "data": { - "type": "string" - }, - "mimeType": { - "type": "string" - }, - "type": { - "type": "string" - } - }, - "required": [ - "data", - "mimeType", - "type" - ], - "type": "object" - }, "McpToolCallError": { "properties": { "message": { @@ -523,9 +359,7 @@ "McpToolCallResult": { "properties": { "content": { - "items": { - "$ref": "#/definitions/ContentBlock" - }, + "items": true, "type": "array" }, "structuredContent": true @@ -610,95 +444,6 @@ } ] }, - "ResourceLink": { - "description": "A resource that the server is capable of reading, included in a prompt or tool call result.\n\nNote: resource links returned by tools are not guaranteed to appear in the results of `resources/list` requests.", - "properties": { - "annotations": { - "anyOf": [ - { - "$ref": "#/definitions/Annotations" - }, - { - "type": "null" - } - ] - }, - "description": { - "type": [ - "string", - "null" - ] - }, - "mimeType": { - "type": [ - "string", - "null" - ] - }, - "name": { - "type": "string" - }, - "size": { - "format": "int64", - "type": [ - "integer", - "null" - ] - }, - "title": { - "type": [ - "string", - "null" - ] - }, - "type": { - "type": "string" - }, - "uri": { - "type": "string" - } - }, - "required": [ - "name", - "type", - "uri" - ], - "type": "object" - }, - "Role": { - "description": "The sender or recipient of messages and data in a conversation.", - "enum": [ - "assistant", - "user" - ], - "type": "string" - }, - "TextContent": { - "description": "Text provided to or from an LLM.", - "properties": { - "annotations": { - "anyOf": [ - { - "$ref": "#/definitions/Annotations" - }, - { - "type": "null" - } - ] - }, - "text": { - "type": "string" - }, - "type": { - "type": "string" - } - }, - "required": [ - "text", - "type" - ], - "type": "object" - }, "TextElement": { "properties": { "byteRange": { @@ -722,27 +467,6 @@ ], "type": "object" }, - "TextResourceContents": { - "properties": { - "mimeType": { - "type": [ - "string", - "null" - ] - }, - "text": { - "type": "string" - }, - "uri": { - "type": "string" - } - }, - "required": [ - "text", - "uri" - ], - "type": "object" - }, "ThreadItem": { "oneOf": [ { diff --git a/codex-rs/app-server-protocol/schema/json/v2/TurnStartResponse.json b/codex-rs/app-server-protocol/schema/json/v2/TurnStartResponse.json index d0d09a3f20..65b2a66be0 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/TurnStartResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/TurnStartResponse.json @@ -1,85 +1,6 @@ { "$schema": "http://json-schema.org/draft-07/schema#", "definitions": { - "Annotations": { - "description": "Optional annotations for the client. The client can use annotations to inform how objects are used or displayed", - "properties": { - "audience": { - "items": { - "$ref": "#/definitions/Role" - }, - "type": [ - "array", - "null" - ] - }, - "lastModified": { - "type": [ - "string", - "null" - ] - }, - "priority": { - "format": "double", - "type": [ - "number", - "null" - ] - } - }, - "type": "object" - }, - "AudioContent": { - "description": "Audio provided to or from an LLM.", - "properties": { - "annotations": { - "anyOf": [ - { - "$ref": "#/definitions/Annotations" - }, - { - "type": "null" - } - ] - }, - "data": { - "type": "string" - }, - "mimeType": { - "type": "string" - }, - "type": { - "type": "string" - } - }, - "required": [ - "data", - "mimeType", - "type" - ], - "type": "object" - }, - "BlobResourceContents": { - "properties": { - "blob": { - "type": "string" - }, - "mimeType": { - "type": [ - "string", - "null" - ] - }, - "uri": { - "type": "string" - } - }, - "required": [ - "blob", - "uri" - ], - "type": "object" - }, "ByteRange": { "properties": { "end": { @@ -405,61 +326,6 @@ ], "type": "string" }, - "ContentBlock": { - "anyOf": [ - { - "$ref": "#/definitions/TextContent" - }, - { - "$ref": "#/definitions/ImageContent" - }, - { - "$ref": "#/definitions/AudioContent" - }, - { - "$ref": "#/definitions/ResourceLink" - }, - { - "$ref": "#/definitions/EmbeddedResource" - } - ] - }, - "EmbeddedResource": { - "description": "The contents of a resource, embedded into a prompt or tool call result.\n\nIt is up to the client how best to render embedded resources for the benefit of the LLM and/or the user.", - "properties": { - "annotations": { - "anyOf": [ - { - "$ref": "#/definitions/Annotations" - }, - { - "type": "null" - } - ] - }, - "resource": { - "$ref": "#/definitions/EmbeddedResourceResource" - }, - "type": { - "type": "string" - } - }, - "required": [ - "resource", - "type" - ], - "type": "object" - }, - "EmbeddedResourceResource": { - "anyOf": [ - { - "$ref": "#/definitions/TextResourceContents" - }, - { - "$ref": "#/definitions/BlobResourceContents" - } - ] - }, "FileUpdateChange": { "properties": { "diff": { @@ -479,36 +345,6 @@ ], "type": "object" }, - "ImageContent": { - "description": "An image provided to or from an LLM.", - "properties": { - "annotations": { - "anyOf": [ - { - "$ref": "#/definitions/Annotations" - }, - { - "type": "null" - } - ] - }, - "data": { - "type": "string" - }, - "mimeType": { - "type": "string" - }, - "type": { - "type": "string" - } - }, - "required": [ - "data", - "mimeType", - "type" - ], - "type": "object" - }, "McpToolCallError": { "properties": { "message": { @@ -523,9 +359,7 @@ "McpToolCallResult": { "properties": { "content": { - "items": { - "$ref": "#/definitions/ContentBlock" - }, + "items": true, "type": "array" }, "structuredContent": true @@ -610,95 +444,6 @@ } ] }, - "ResourceLink": { - "description": "A resource that the server is capable of reading, included in a prompt or tool call result.\n\nNote: resource links returned by tools are not guaranteed to appear in the results of `resources/list` requests.", - "properties": { - "annotations": { - "anyOf": [ - { - "$ref": "#/definitions/Annotations" - }, - { - "type": "null" - } - ] - }, - "description": { - "type": [ - "string", - "null" - ] - }, - "mimeType": { - "type": [ - "string", - "null" - ] - }, - "name": { - "type": "string" - }, - "size": { - "format": "int64", - "type": [ - "integer", - "null" - ] - }, - "title": { - "type": [ - "string", - "null" - ] - }, - "type": { - "type": "string" - }, - "uri": { - "type": "string" - } - }, - "required": [ - "name", - "type", - "uri" - ], - "type": "object" - }, - "Role": { - "description": "The sender or recipient of messages and data in a conversation.", - "enum": [ - "assistant", - "user" - ], - "type": "string" - }, - "TextContent": { - "description": "Text provided to or from an LLM.", - "properties": { - "annotations": { - "anyOf": [ - { - "$ref": "#/definitions/Annotations" - }, - { - "type": "null" - } - ] - }, - "text": { - "type": "string" - }, - "type": { - "type": "string" - } - }, - "required": [ - "text", - "type" - ], - "type": "object" - }, "TextElement": { "properties": { "byteRange": { @@ -722,27 +467,6 @@ ], "type": "object" }, - "TextResourceContents": { - "properties": { - "mimeType": { - "type": [ - "string", - "null" - ] - }, - "text": { - "type": "string" - }, - "uri": { - "type": "string" - } - }, - "required": [ - "text", - "uri" - ], - "type": "object" - }, "ThreadItem": { "oneOf": [ { diff --git a/codex-rs/app-server-protocol/schema/json/v2/TurnStartedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/TurnStartedNotification.json index 9dd23a33e6..1a63c0d7d0 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/TurnStartedNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/TurnStartedNotification.json @@ -1,85 +1,6 @@ { "$schema": "http://json-schema.org/draft-07/schema#", "definitions": { - "Annotations": { - "description": "Optional annotations for the client. The client can use annotations to inform how objects are used or displayed", - "properties": { - "audience": { - "items": { - "$ref": "#/definitions/Role" - }, - "type": [ - "array", - "null" - ] - }, - "lastModified": { - "type": [ - "string", - "null" - ] - }, - "priority": { - "format": "double", - "type": [ - "number", - "null" - ] - } - }, - "type": "object" - }, - "AudioContent": { - "description": "Audio provided to or from an LLM.", - "properties": { - "annotations": { - "anyOf": [ - { - "$ref": "#/definitions/Annotations" - }, - { - "type": "null" - } - ] - }, - "data": { - "type": "string" - }, - "mimeType": { - "type": "string" - }, - "type": { - "type": "string" - } - }, - "required": [ - "data", - "mimeType", - "type" - ], - "type": "object" - }, - "BlobResourceContents": { - "properties": { - "blob": { - "type": "string" - }, - "mimeType": { - "type": [ - "string", - "null" - ] - }, - "uri": { - "type": "string" - } - }, - "required": [ - "blob", - "uri" - ], - "type": "object" - }, "ByteRange": { "properties": { "end": { @@ -405,61 +326,6 @@ ], "type": "string" }, - "ContentBlock": { - "anyOf": [ - { - "$ref": "#/definitions/TextContent" - }, - { - "$ref": "#/definitions/ImageContent" - }, - { - "$ref": "#/definitions/AudioContent" - }, - { - "$ref": "#/definitions/ResourceLink" - }, - { - "$ref": "#/definitions/EmbeddedResource" - } - ] - }, - "EmbeddedResource": { - "description": "The contents of a resource, embedded into a prompt or tool call result.\n\nIt is up to the client how best to render embedded resources for the benefit of the LLM and/or the user.", - "properties": { - "annotations": { - "anyOf": [ - { - "$ref": "#/definitions/Annotations" - }, - { - "type": "null" - } - ] - }, - "resource": { - "$ref": "#/definitions/EmbeddedResourceResource" - }, - "type": { - "type": "string" - } - }, - "required": [ - "resource", - "type" - ], - "type": "object" - }, - "EmbeddedResourceResource": { - "anyOf": [ - { - "$ref": "#/definitions/TextResourceContents" - }, - { - "$ref": "#/definitions/BlobResourceContents" - } - ] - }, "FileUpdateChange": { "properties": { "diff": { @@ -479,36 +345,6 @@ ], "type": "object" }, - "ImageContent": { - "description": "An image provided to or from an LLM.", - "properties": { - "annotations": { - "anyOf": [ - { - "$ref": "#/definitions/Annotations" - }, - { - "type": "null" - } - ] - }, - "data": { - "type": "string" - }, - "mimeType": { - "type": "string" - }, - "type": { - "type": "string" - } - }, - "required": [ - "data", - "mimeType", - "type" - ], - "type": "object" - }, "McpToolCallError": { "properties": { "message": { @@ -523,9 +359,7 @@ "McpToolCallResult": { "properties": { "content": { - "items": { - "$ref": "#/definitions/ContentBlock" - }, + "items": true, "type": "array" }, "structuredContent": true @@ -610,95 +444,6 @@ } ] }, - "ResourceLink": { - "description": "A resource that the server is capable of reading, included in a prompt or tool call result.\n\nNote: resource links returned by tools are not guaranteed to appear in the results of `resources/list` requests.", - "properties": { - "annotations": { - "anyOf": [ - { - "$ref": "#/definitions/Annotations" - }, - { - "type": "null" - } - ] - }, - "description": { - "type": [ - "string", - "null" - ] - }, - "mimeType": { - "type": [ - "string", - "null" - ] - }, - "name": { - "type": "string" - }, - "size": { - "format": "int64", - "type": [ - "integer", - "null" - ] - }, - "title": { - "type": [ - "string", - "null" - ] - }, - "type": { - "type": "string" - }, - "uri": { - "type": "string" - } - }, - "required": [ - "name", - "type", - "uri" - ], - "type": "object" - }, - "Role": { - "description": "The sender or recipient of messages and data in a conversation.", - "enum": [ - "assistant", - "user" - ], - "type": "string" - }, - "TextContent": { - "description": "Text provided to or from an LLM.", - "properties": { - "annotations": { - "anyOf": [ - { - "$ref": "#/definitions/Annotations" - }, - { - "type": "null" - } - ] - }, - "text": { - "type": "string" - }, - "type": { - "type": "string" - } - }, - "required": [ - "text", - "type" - ], - "type": "object" - }, "TextElement": { "properties": { "byteRange": { @@ -722,27 +467,6 @@ ], "type": "object" }, - "TextResourceContents": { - "properties": { - "mimeType": { - "type": [ - "string", - "null" - ] - }, - "text": { - "type": "string" - }, - "uri": { - "type": "string" - } - }, - "required": [ - "text", - "uri" - ], - "type": "object" - }, "ThreadItem": { "oneOf": [ { diff --git a/codex-rs/app-server-protocol/schema/typescript/Annotations.ts b/codex-rs/app-server-protocol/schema/typescript/Annotations.ts deleted file mode 100644 index b89cbd725f..0000000000 --- a/codex-rs/app-server-protocol/schema/typescript/Annotations.ts +++ /dev/null @@ -1,9 +0,0 @@ -// 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 { Role } from "./Role"; - -/** - * Optional annotations for the client. The client can use annotations to inform how objects are used or displayed - */ -export type Annotations = { audience?: Array, lastModified?: string, priority?: number, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/AudioContent.ts b/codex-rs/app-server-protocol/schema/typescript/AudioContent.ts deleted file mode 100644 index 90dd930c93..0000000000 --- a/codex-rs/app-server-protocol/schema/typescript/AudioContent.ts +++ /dev/null @@ -1,9 +0,0 @@ -// 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 { Annotations } from "./Annotations"; - -/** - * Audio provided to or from an LLM. - */ -export type AudioContent = { annotations?: Annotations, data: string, mimeType: string, type: string, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/BlobResourceContents.ts b/codex-rs/app-server-protocol/schema/typescript/BlobResourceContents.ts deleted file mode 100644 index 362adcfe14..0000000000 --- a/codex-rs/app-server-protocol/schema/typescript/BlobResourceContents.ts +++ /dev/null @@ -1,5 +0,0 @@ -// 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. - -export type BlobResourceContents = { blob: string, mimeType?: string, uri: string, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/CallToolResult.ts b/codex-rs/app-server-protocol/schema/typescript/CallToolResult.ts index 6c6b590d1a..e7a471d465 100644 --- a/codex-rs/app-server-protocol/schema/typescript/CallToolResult.ts +++ b/codex-rs/app-server-protocol/schema/typescript/CallToolResult.ts @@ -1,10 +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 { ContentBlock } from "./ContentBlock"; import type { JsonValue } from "./serde_json/JsonValue"; /** * The server's response to a tool call. */ -export type CallToolResult = { content: Array, isError?: boolean, structuredContent?: JsonValue, }; +export type CallToolResult = { content: Array, structuredContent?: JsonValue, isError?: boolean, _meta?: JsonValue, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/ContentBlock.ts b/codex-rs/app-server-protocol/schema/typescript/ContentBlock.ts deleted file mode 100644 index 2dd62d87b4..0000000000 --- a/codex-rs/app-server-protocol/schema/typescript/ContentBlock.ts +++ /dev/null @@ -1,10 +0,0 @@ -// 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 { AudioContent } from "./AudioContent"; -import type { EmbeddedResource } from "./EmbeddedResource"; -import type { ImageContent } from "./ImageContent"; -import type { ResourceLink } from "./ResourceLink"; -import type { TextContent } from "./TextContent"; - -export type ContentBlock = TextContent | ImageContent | AudioContent | ResourceLink | EmbeddedResource; diff --git a/codex-rs/app-server-protocol/schema/typescript/ElicitationRequestEvent.ts b/codex-rs/app-server-protocol/schema/typescript/ElicitationRequestEvent.ts index a4d641fd44..045e304bdc 100644 --- a/codex-rs/app-server-protocol/schema/typescript/ElicitationRequestEvent.ts +++ b/codex-rs/app-server-protocol/schema/typescript/ElicitationRequestEvent.ts @@ -1,6 +1,5 @@ // 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 { RequestId } from "./RequestId"; -export type ElicitationRequestEvent = { server_name: string, id: RequestId, message: string, }; +export type ElicitationRequestEvent = { server_name: string, id: string | number, message: string, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/EmbeddedResource.ts b/codex-rs/app-server-protocol/schema/typescript/EmbeddedResource.ts deleted file mode 100644 index 1b81e37ab1..0000000000 --- a/codex-rs/app-server-protocol/schema/typescript/EmbeddedResource.ts +++ /dev/null @@ -1,13 +0,0 @@ -// 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 { Annotations } from "./Annotations"; -import type { EmbeddedResourceResource } from "./EmbeddedResourceResource"; - -/** - * The contents of a resource, embedded into a prompt or tool call result. - * - * It is up to the client how best to render embedded resources for the benefit - * of the LLM and/or the user. - */ -export type EmbeddedResource = { annotations?: Annotations, resource: EmbeddedResourceResource, type: string, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/EmbeddedResourceResource.ts b/codex-rs/app-server-protocol/schema/typescript/EmbeddedResourceResource.ts deleted file mode 100644 index 58f70f6e82..0000000000 --- a/codex-rs/app-server-protocol/schema/typescript/EmbeddedResourceResource.ts +++ /dev/null @@ -1,7 +0,0 @@ -// 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 { BlobResourceContents } from "./BlobResourceContents"; -import type { TextResourceContents } from "./TextResourceContents"; - -export type EmbeddedResourceResource = TextResourceContents | BlobResourceContents; diff --git a/codex-rs/app-server-protocol/schema/typescript/ImageContent.ts b/codex-rs/app-server-protocol/schema/typescript/ImageContent.ts deleted file mode 100644 index 0537639c02..0000000000 --- a/codex-rs/app-server-protocol/schema/typescript/ImageContent.ts +++ /dev/null @@ -1,9 +0,0 @@ -// 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 { Annotations } from "./Annotations"; - -/** - * An image provided to or from an LLM. - */ -export type ImageContent = { annotations?: Annotations, data: string, mimeType: string, type: string, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/Resource.ts b/codex-rs/app-server-protocol/schema/typescript/Resource.ts index 238cd9008c..6eca7941d6 100644 --- a/codex-rs/app-server-protocol/schema/typescript/Resource.ts +++ b/codex-rs/app-server-protocol/schema/typescript/Resource.ts @@ -1,9 +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 { Annotations } from "./Annotations"; +import type { JsonValue } from "./serde_json/JsonValue"; /** * A known resource that the server is capable of reading. */ -export type Resource = { annotations?: Annotations, description?: string, mimeType?: string, name: string, size?: bigint, title?: string, uri: string, }; +export type Resource = { annotations?: JsonValue, description?: string, mimeType?: string, name: string, size?: number, title?: string, uri: string, icons?: Array, _meta?: JsonValue, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/ResourceLink.ts b/codex-rs/app-server-protocol/schema/typescript/ResourceLink.ts deleted file mode 100644 index fe934c3a5d..0000000000 --- a/codex-rs/app-server-protocol/schema/typescript/ResourceLink.ts +++ /dev/null @@ -1,11 +0,0 @@ -// 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 { Annotations } from "./Annotations"; - -/** - * A resource that the server is capable of reading, included in a prompt or tool call result. - * - * Note: resource links returned by tools are not guaranteed to appear in the results of `resources/list` requests. - */ -export type ResourceLink = { annotations?: Annotations, description?: string, mimeType?: string, name: string, size?: bigint, title?: string, type: string, uri: string, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/ResourceTemplate.ts b/codex-rs/app-server-protocol/schema/typescript/ResourceTemplate.ts index 03c9a116c8..6dc395129c 100644 --- a/codex-rs/app-server-protocol/schema/typescript/ResourceTemplate.ts +++ b/codex-rs/app-server-protocol/schema/typescript/ResourceTemplate.ts @@ -1,9 +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 { Annotations } from "./Annotations"; +import type { JsonValue } from "./serde_json/JsonValue"; /** * A template description for resources available on the server. */ -export type ResourceTemplate = { annotations?: Annotations, description?: string, mimeType?: string, name: string, title?: string, uriTemplate: string, }; +export type ResourceTemplate = { annotations?: JsonValue, uriTemplate: string, name: string, title?: string, description?: string, mimeType?: string, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/Role.ts b/codex-rs/app-server-protocol/schema/typescript/Role.ts deleted file mode 100644 index 009307efce..0000000000 --- a/codex-rs/app-server-protocol/schema/typescript/Role.ts +++ /dev/null @@ -1,8 +0,0 @@ -// 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. - -/** - * The sender or recipient of messages and data in a conversation. - */ -export type Role = "assistant" | "user"; diff --git a/codex-rs/app-server-protocol/schema/typescript/TextContent.ts b/codex-rs/app-server-protocol/schema/typescript/TextContent.ts deleted file mode 100644 index 760102f81d..0000000000 --- a/codex-rs/app-server-protocol/schema/typescript/TextContent.ts +++ /dev/null @@ -1,9 +0,0 @@ -// 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 { Annotations } from "./Annotations"; - -/** - * Text provided to or from an LLM. - */ -export type TextContent = { annotations?: Annotations, text: string, type: string, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/TextResourceContents.ts b/codex-rs/app-server-protocol/schema/typescript/TextResourceContents.ts deleted file mode 100644 index 5bef79210c..0000000000 --- a/codex-rs/app-server-protocol/schema/typescript/TextResourceContents.ts +++ /dev/null @@ -1,5 +0,0 @@ -// 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. - -export type TextResourceContents = { mimeType?: string, text: string, uri: string, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/Tool.ts b/codex-rs/app-server-protocol/schema/typescript/Tool.ts index 8255eecf2c..b795916140 100644 --- a/codex-rs/app-server-protocol/schema/typescript/Tool.ts +++ b/codex-rs/app-server-protocol/schema/typescript/Tool.ts @@ -1,11 +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 { ToolAnnotations } from "./ToolAnnotations"; -import type { ToolInputSchema } from "./ToolInputSchema"; -import type { ToolOutputSchema } from "./ToolOutputSchema"; +import type { JsonValue } from "./serde_json/JsonValue"; /** * Definition for a tool the client can call. */ -export type Tool = { annotations?: ToolAnnotations, description?: string, inputSchema: ToolInputSchema, name: string, outputSchema?: ToolOutputSchema, title?: string, }; +export type Tool = { name: string, title?: string, description?: string, inputSchema: JsonValue, outputSchema?: JsonValue, annotations?: JsonValue, icons?: Array, _meta?: JsonValue, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/ToolAnnotations.ts b/codex-rs/app-server-protocol/schema/typescript/ToolAnnotations.ts deleted file mode 100644 index 70348a9964..0000000000 --- a/codex-rs/app-server-protocol/schema/typescript/ToolAnnotations.ts +++ /dev/null @@ -1,15 +0,0 @@ -// 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. - -/** - * Additional properties describing a Tool to clients. - * - * NOTE: all properties in ToolAnnotations are **hints**. - * They are not guaranteed to provide a faithful description of - * tool behavior (including descriptive properties like `title`). - * - * Clients should never make tool use decisions based on ToolAnnotations - * received from untrusted servers. - */ -export type ToolAnnotations = { destructiveHint?: boolean, idempotentHint?: boolean, openWorldHint?: boolean, readOnlyHint?: boolean, title?: string, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/ToolInputSchema.ts b/codex-rs/app-server-protocol/schema/typescript/ToolInputSchema.ts deleted file mode 100644 index 78f40bbe40..0000000000 --- a/codex-rs/app-server-protocol/schema/typescript/ToolInputSchema.ts +++ /dev/null @@ -1,9 +0,0 @@ -// 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 { JsonValue } from "./serde_json/JsonValue"; - -/** - * A JSON Schema object defining the expected parameters for the tool. - */ -export type ToolInputSchema = { properties?: JsonValue, required?: Array, type: string, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/ToolOutputSchema.ts b/codex-rs/app-server-protocol/schema/typescript/ToolOutputSchema.ts deleted file mode 100644 index 9270c8e7b7..0000000000 --- a/codex-rs/app-server-protocol/schema/typescript/ToolOutputSchema.ts +++ /dev/null @@ -1,10 +0,0 @@ -// 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 { JsonValue } from "./serde_json/JsonValue"; - -/** - * An optional JSON Schema object defining the structure of the tool's output returned in - * the structuredContent field of a CallToolResult. - */ -export type ToolOutputSchema = { properties?: JsonValue, required?: Array, type: string, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/index.ts b/codex-rs/app-server-protocol/schema/typescript/index.ts index fafc6aad9d..191f993864 100644 --- a/codex-rs/app-server-protocol/schema/typescript/index.ts +++ b/codex-rs/app-server-protocol/schema/typescript/index.ts @@ -14,18 +14,15 @@ export type { AgentReasoningRawContentDeltaEvent } from "./AgentReasoningRawCont export type { AgentReasoningRawContentEvent } from "./AgentReasoningRawContentEvent"; export type { AgentReasoningSectionBreakEvent } from "./AgentReasoningSectionBreakEvent"; export type { AgentStatus } from "./AgentStatus"; -export type { Annotations } from "./Annotations"; export type { ApplyPatchApprovalParams } from "./ApplyPatchApprovalParams"; export type { ApplyPatchApprovalRequestEvent } from "./ApplyPatchApprovalRequestEvent"; export type { ApplyPatchApprovalResponse } from "./ApplyPatchApprovalResponse"; export type { ArchiveConversationParams } from "./ArchiveConversationParams"; export type { ArchiveConversationResponse } from "./ArchiveConversationResponse"; export type { AskForApproval } from "./AskForApproval"; -export type { AudioContent } from "./AudioContent"; export type { AuthMode } from "./AuthMode"; export type { AuthStatusChangeNotification } from "./AuthStatusChangeNotification"; export type { BackgroundEventEvent } from "./BackgroundEventEvent"; -export type { BlobResourceContents } from "./BlobResourceContents"; export type { ByteRange } from "./ByteRange"; export type { CallToolResult } from "./CallToolResult"; export type { CancelLoginChatGptParams } from "./CancelLoginChatGptParams"; @@ -44,7 +41,6 @@ export type { CollabWaitingBeginEvent } from "./CollabWaitingBeginEvent"; export type { CollabWaitingEndEvent } from "./CollabWaitingEndEvent"; export type { CollaborationMode } from "./CollaborationMode"; export type { CollaborationModeMask } from "./CollaborationModeMask"; -export type { ContentBlock } from "./ContentBlock"; export type { ContentItem } from "./ContentItem"; export type { ContextCompactedEvent } from "./ContextCompactedEvent"; export type { ContextCompactionItem } from "./ContextCompactionItem"; @@ -55,8 +51,6 @@ export type { CustomPrompt } from "./CustomPrompt"; export type { DeprecationNoticeEvent } from "./DeprecationNoticeEvent"; export type { DynamicToolCallRequest } from "./DynamicToolCallRequest"; export type { ElicitationRequestEvent } from "./ElicitationRequestEvent"; -export type { EmbeddedResource } from "./EmbeddedResource"; -export type { EmbeddedResourceResource } from "./EmbeddedResourceResource"; export type { ErrorEvent } from "./ErrorEvent"; export type { EventMsg } from "./EventMsg"; export type { ExecApprovalRequestEvent } from "./ExecApprovalRequestEvent"; @@ -92,7 +86,6 @@ export type { GitDiffToRemoteParams } from "./GitDiffToRemoteParams"; export type { GitDiffToRemoteResponse } from "./GitDiffToRemoteResponse"; export type { GitSha } from "./GitSha"; export type { HistoryEntry } from "./HistoryEntry"; -export type { ImageContent } from "./ImageContent"; export type { InitializeCapabilities } from "./InitializeCapabilities"; export type { InitializeParams } from "./InitializeParams"; export type { InitializeResponse } from "./InitializeResponse"; @@ -152,7 +145,6 @@ export type { RequestUserInputEvent } from "./RequestUserInputEvent"; export type { RequestUserInputQuestion } from "./RequestUserInputQuestion"; export type { RequestUserInputQuestionOption } from "./RequestUserInputQuestionOption"; export type { Resource } from "./Resource"; -export type { ResourceLink } from "./ResourceLink"; export type { ResourceTemplate } from "./ResourceTemplate"; export type { ResponseItem } from "./ResponseItem"; export type { ResumeConversationParams } from "./ResumeConversationParams"; @@ -164,7 +156,6 @@ export type { ReviewLineRange } from "./ReviewLineRange"; export type { ReviewOutputEvent } from "./ReviewOutputEvent"; export type { ReviewRequest } from "./ReviewRequest"; export type { ReviewTarget } from "./ReviewTarget"; -export type { Role } from "./Role"; export type { SandboxMode } from "./SandboxMode"; export type { SandboxPolicy } from "./SandboxPolicy"; export type { SandboxSettings } from "./SandboxSettings"; @@ -191,9 +182,7 @@ export type { StepStatus } from "./StepStatus"; export type { StreamErrorEvent } from "./StreamErrorEvent"; export type { SubAgentSource } from "./SubAgentSource"; export type { TerminalInteractionEvent } from "./TerminalInteractionEvent"; -export type { TextContent } from "./TextContent"; export type { TextElement } from "./TextElement"; -export type { TextResourceContents } from "./TextResourceContents"; export type { ThreadId } from "./ThreadId"; export type { ThreadNameUpdatedEvent } from "./ThreadNameUpdatedEvent"; export type { ThreadRolledBackEvent } from "./ThreadRolledBackEvent"; @@ -201,9 +190,6 @@ export type { TokenCountEvent } from "./TokenCountEvent"; export type { TokenUsage } from "./TokenUsage"; export type { TokenUsageInfo } from "./TokenUsageInfo"; export type { Tool } from "./Tool"; -export type { ToolAnnotations } from "./ToolAnnotations"; -export type { ToolInputSchema } from "./ToolInputSchema"; -export type { ToolOutputSchema } from "./ToolOutputSchema"; export type { Tools } from "./Tools"; export type { TurnAbortReason } from "./TurnAbortReason"; export type { TurnAbortedEvent } from "./TurnAbortedEvent"; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/McpToolCallResult.ts b/codex-rs/app-server-protocol/schema/typescript/v2/McpToolCallResult.ts index 93f942a7f9..f493a86094 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/McpToolCallResult.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/McpToolCallResult.ts @@ -1,7 +1,6 @@ // 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 { ContentBlock } from "../ContentBlock"; import type { JsonValue } from "../serde_json/JsonValue"; -export type McpToolCallResult = { content: Array, structuredContent: JsonValue | null, }; +export type McpToolCallResult = { content: Array, structuredContent: JsonValue | null, }; diff --git a/codex-rs/app-server-protocol/src/protocol/v2.rs b/codex-rs/app-server-protocol/src/protocol/v2.rs index 3ca344ee49..4f37e2889d 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2.rs @@ -15,6 +15,9 @@ use codex_protocol::config_types::Verbosity; use codex_protocol::config_types::WebSearchMode; use codex_protocol::items::AgentMessageContent as CoreAgentMessageContent; use codex_protocol::items::TurnItem as CoreTurnItem; +use codex_protocol::mcp::Resource as McpResource; +use codex_protocol::mcp::ResourceTemplate as McpResourceTemplate; +use codex_protocol::mcp::Tool as McpTool; use codex_protocol::models::ResponseItem; use codex_protocol::openai_models::ReasoningEffort; use codex_protocol::parse_command::ParsedCommand as CoreParsedCommand; @@ -41,10 +44,6 @@ use codex_protocol::user_input::ByteRange as CoreByteRange; use codex_protocol::user_input::TextElement as CoreTextElement; use codex_protocol::user_input::UserInput as CoreUserInput; use codex_utils_absolute_path::AbsolutePathBuf; -use mcp_types::ContentBlock as McpContentBlock; -use mcp_types::Resource as McpResource; -use mcp_types::ResourceTemplate as McpResourceTemplate; -use mcp_types::Tool as McpTool; use schemars::JsonSchema; use serde::Deserialize; use serde::Serialize; @@ -2360,7 +2359,12 @@ impl From for CollabAgentState { #[serde(rename_all = "camelCase")] #[ts(export_to = "v2/")] pub struct McpToolCallResult { - pub content: Vec, + // NOTE: `rmcp::model::Content` (and its `RawContent` variants) would be a more precise Rust + // representation of MCP content blocks. We intentionally use `serde_json::Value` here because + // this crate exports JSON schema + TS types (`schemars`/`ts-rs`), and the rmcp model types + // aren't set up to be schema/TS friendly (and would introduce heavier coupling to rmcp's Rust + // representations). Using `JsonValue` keeps the payload wire-shaped and easy to export. + pub content: Vec, pub structured_content: Option, } diff --git a/codex-rs/app-server-protocol/src/schema_fixtures.rs b/codex-rs/app-server-protocol/src/schema_fixtures.rs index 918011e95c..5412da8632 100644 --- a/codex-rs/app-server-protocol/src/schema_fixtures.rs +++ b/codex-rs/app-server-protocol/src/schema_fixtures.rs @@ -2,6 +2,7 @@ use anyhow::Context; use anyhow::Result; use serde_json::Map; use serde_json::Value; +use std::cmp::Ordering; use std::collections::BTreeMap; use std::path::Path; use std::path::PathBuf; @@ -92,7 +93,49 @@ fn read_file_bytes(path: &Path) -> Result> { fn canonicalize_json(value: &Value) -> Value { match value { - Value::Array(items) => Value::Array(items.iter().map(canonicalize_json).collect()), + Value::Array(items) => { + // NOTE: We sort some JSON arrays to make schema fixture comparisons stable across + // platforms. + // + // In general, JSON array ordering is significant. However, this code path is used + // only by `schema_fixtures_match_generated` to compare our *vendored* JSON schema + // files against freshly generated output. Some parts of schema generation end up + // with non-deterministic ordering across platforms (often due to map iteration order + // upstream), which can cause Windows CI failures even when the generated schema is + // semantically equivalent. + // + // JSON Schema itself also contains a number of array-valued keywords whose ordering + // does not affect validation semantics (e.g. `required`, `type`, `enum`, `anyOf`, + // `oneOf`, `allOf`). That makes it reasonable to treat many schema-emitted arrays as + // order-insensitive for the purpose of fixture diffs. + // + // To avoid accidentally changing the meaning of arrays where order *could* matter + // (e.g. tuple validation / `prefixItems`-style arrays), we only sort arrays when we + // can derive a stable sort key for *every* element. If we cannot, we preserve the + // original ordering. + let items = items.iter().map(canonicalize_json).collect::>(); + let mut sortable = Vec::with_capacity(items.len()); + for item in &items { + let Some(key) = schema_array_item_sort_key(item) else { + return Value::Array(items); + }; + let stable = serde_json::to_string(item).unwrap_or_default(); + sortable.push((key, stable)); + } + + let mut items = items.into_iter().zip(sortable).collect::>(); + + items.sort_by( + |(_, (key_left, stable_left)), (_, (key_right, stable_right))| match key_left + .cmp(key_right) + { + Ordering::Equal => stable_left.cmp(stable_right), + other => other, + }, + ); + + Value::Array(items.into_iter().map(|(item, _)| item).collect()) + } Value::Object(map) => { let mut entries: Vec<_> = map.iter().collect(); entries.sort_by(|(left, _), (right, _)| left.cmp(right)); @@ -106,6 +149,25 @@ fn canonicalize_json(value: &Value) -> Value { } } +fn schema_array_item_sort_key(item: &Value) -> Option { + match item { + Value::Null => Some("null".to_string()), + Value::Bool(b) => Some(format!("b:{b}")), + Value::Number(n) => Some(format!("n:{n}")), + Value::String(s) => Some(format!("s:{s}")), + Value::Object(map) => { + if let Some(Value::String(reference)) = map.get("$ref") { + Some(format!("ref:{reference}")) + } else if let Some(Value::String(title)) = map.get("title") { + Some(format!("title:{title}")) + } else { + None + } + } + Value::Array(_) => None, + } +} + fn collect_files_recursive(root: &Path) -> Result>> { let mut files = BTreeMap::new(); @@ -146,3 +208,29 @@ fn collect_files_recursive(root: &Path) -> Result>> { Ok(files) } + +#[cfg(test)] +mod tests { + use super::*; + use pretty_assertions::assert_eq; + + #[test] + fn canonicalize_json_sorts_string_arrays() { + let value = serde_json::json!(["b", "a"]); + let expected = serde_json::json!(["a", "b"]); + assert_eq!(canonicalize_json(&value), expected); + } + + #[test] + fn canonicalize_json_sorts_schema_ref_arrays() { + let value = serde_json::json!([ + {"$ref": "#/definitions/B"}, + {"$ref": "#/definitions/A"} + ]); + let expected = serde_json::json!([ + {"$ref": "#/definitions/A"}, + {"$ref": "#/definitions/B"} + ]); + assert_eq!(canonicalize_json(&value), expected); + } +} diff --git a/codex-rs/app-server-test-client/Cargo.lock b/codex-rs/app-server-test-client/Cargo.lock index 1720850cd2..c6e4241d2c 100644 --- a/codex-rs/app-server-test-client/Cargo.lock +++ b/codex-rs/app-server-test-client/Cargo.lock @@ -175,7 +175,6 @@ dependencies = [ "base64", "icu_decimal", "icu_locale_core", - "mcp-types", "mime_guess", "serde", "serde_json", @@ -521,16 +520,6 @@ version = "0.4.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" -[[package]] -name = "mcp-types" -version = "0.45.0" -source = "git+https://github.com/openai/codex.git?tag=rust-v0.45.0#a7c7869c23f88f6c468281e6f438ba4a91b81f26" -dependencies = [ - "serde", - "serde_json", - "ts-rs", -] - [[package]] name = "memchr" version = "2.7.6" diff --git a/codex-rs/app-server/Cargo.toml b/codex-rs/app-server/Cargo.toml index 11c57ebbc8..778d57bab0 100644 --- a/codex-rs/app-server/Cargo.toml +++ b/codex-rs/app-server/Cargo.toml @@ -35,7 +35,6 @@ codex-utils-json-to-toml = { workspace = true } chrono = { workspace = true } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } -mcp-types = { workspace = true } tempfile = { workspace = true } time = { workspace = true } toml = { workspace = true } @@ -60,7 +59,6 @@ axum = { workspace = true, default-features = false, features = [ base64 = { workspace = true } codex-execpolicy = { workspace = true } core_test_support = { workspace = true } -mcp-types = { workspace = true } os_info = { workspace = true } pretty_assertions = { workspace = true } rmcp = { workspace = true, default-features = false, features = [ diff --git a/codex-rs/app-server/src/bespoke_event_handling.rs b/codex-rs/app-server/src/bespoke_event_handling.rs index bf8fad3a9c..55b185e5a8 100644 --- a/codex-rs/app-server/src/bespoke_event_handling.rs +++ b/codex-rs/app-server/src/bespoke_event_handling.rs @@ -1841,12 +1841,11 @@ mod tests { use codex_core::protocol::RateLimitWindow; use codex_core::protocol::TokenUsage; use codex_core::protocol::TokenUsageInfo; + use codex_protocol::mcp::CallToolResult; use codex_protocol::plan_tool::PlanItemArg; use codex_protocol::plan_tool::StepStatus; - use mcp_types::CallToolResult; - use mcp_types::ContentBlock; - use mcp_types::TextContent; use pretty_assertions::assert_eq; + use rmcp::model::Content; use serde_json::Value as JsonValue; use std::collections::HashMap; use std::time::Duration; @@ -2374,15 +2373,15 @@ mod tests { #[tokio::test] async fn test_construct_mcp_tool_call_end_notification_success() { - let content = vec![ContentBlock::TextContent(TextContent { - annotations: None, - text: "{\"resources\":[]}".to_string(), - r#type: "text".to_string(), - })]; + let content = vec![ + serde_json::to_value(Content::text("{\"resources\":[]}")) + .expect("content should serialize"), + ]; let result = CallToolResult { content: content.clone(), is_error: Some(false), structured_content: None, + meta: None, }; let end_event = McpToolCallEndEvent { diff --git a/codex-rs/core/Cargo.toml b/codex-rs/core/Cargo.toml index a95981cca2..38da38b929 100644 --- a/codex-rs/core/Cargo.toml +++ b/codex-rs/core/Cargo.toml @@ -57,7 +57,6 @@ indexmap = { workspace = true } indoc = { workspace = true } keyring = { workspace = true, features = ["crypto-rust"] } libc = { workspace = true } -mcp-types = { workspace = true } multimap = { workspace = true } once_cell = { workspace = true } os_info = { workspace = true } @@ -65,6 +64,12 @@ rand = { workspace = true } regex = { workspace = true } regex-lite = { workspace = true } reqwest = { workspace = true, features = ["json", "stream"] } +rmcp = { workspace = true, default-features = false, features = [ + "base64", + "macros", + "schemars", + "server", +] } schemars = { workspace = true } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index 31a81846e2..6cc389b531 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -50,6 +50,7 @@ use codex_protocol::dynamic_tools::DynamicToolSpec; use codex_protocol::items::PlanItem; use codex_protocol::items::TurnItem; use codex_protocol::items::UserMessageItem; +use codex_protocol::mcp::CallToolResult; use codex_protocol::models::BaseInstructions; use codex_protocol::models::format_allow_prefixes; use codex_protocol::openai_models::ModelInfo; @@ -72,14 +73,12 @@ use codex_rmcp_client::OAuthCredentialsStoreMode; use futures::future::BoxFuture; use futures::prelude::*; use futures::stream::FuturesOrdered; -use mcp_types::CallToolResult; -use mcp_types::ListResourceTemplatesRequestParams; -use mcp_types::ListResourceTemplatesResult; -use mcp_types::ListResourcesRequestParams; -use mcp_types::ListResourcesResult; -use mcp_types::ReadResourceRequestParams; -use mcp_types::ReadResourceResult; -use mcp_types::RequestId; +use rmcp::model::ListResourceTemplatesResult; +use rmcp::model::ListResourcesResult; +use rmcp::model::PaginatedRequestParam; +use rmcp::model::ReadResourceRequestParam; +use rmcp::model::ReadResourceResult; +use rmcp::model::RequestId; use serde_json; use serde_json::Value; use tokio::sync::Mutex; @@ -2224,7 +2223,7 @@ impl Session { pub async fn list_resources( &self, server: &str, - params: Option, + params: Option, ) -> anyhow::Result { self.services .mcp_connection_manager @@ -2237,7 +2236,7 @@ impl Session { pub async fn list_resource_templates( &self, server: &str, - params: Option, + params: Option, ) -> anyhow::Result { self.services .mcp_connection_manager @@ -2250,7 +2249,7 @@ impl Session { pub async fn read_resource( &self, server: &str, - params: ReadResourceRequestParams, + params: ReadResourceRequestParam, ) -> anyhow::Result { self.services .mcp_connection_manager @@ -2577,10 +2576,10 @@ mod handlers { use codex_protocol::config_types::ModeKind; use codex_protocol::config_types::Settings; use codex_protocol::dynamic_tools::DynamicToolResponse; + use codex_protocol::mcp::RequestId as ProtocolRequestId; use codex_protocol::user_input::UserInput; use codex_rmcp_client::ElicitationAction; use codex_rmcp_client::ElicitationResponse; - use mcp_types::RequestId; use std::path::PathBuf; use std::sync::Arc; use tracing::info; @@ -2709,7 +2708,7 @@ mod handlers { pub async fn resolve_elicitation( sess: &Arc, server_name: String, - request_id: RequestId, + request_id: ProtocolRequestId, decision: codex_protocol::approvals::ElicitationAction, ) { let action = match decision { @@ -2724,6 +2723,12 @@ mod handlers { ElicitationAction::Decline | ElicitationAction::Cancel => None, }; let response = ElicitationResponse { action, content }; + let request_id = match request_id { + ProtocolRequestId::String(value) => { + rmcp::model::NumberOrString::String(std::sync::Arc::from(value)) + } + ProtocolRequestId::Integer(value) => rmcp::model::NumberOrString::Number(value), + }; if let Err(err) = sess .resolve_elicitation(server_name, request_id, response) .await @@ -4516,8 +4521,7 @@ mod tests { use std::time::Duration; use tokio::time::sleep; - use mcp_types::ContentBlock; - use mcp_types::TextContent; + use codex_protocol::mcp::CallToolResult as McpCallToolResult; use pretty_assertions::assert_eq; use serde::Deserialize; use serde_json::json; @@ -5101,7 +5105,7 @@ mod tests { #[test] fn prefers_structured_content_when_present() { - let ctr = CallToolResult { + let ctr = McpCallToolResult { // Content present but should be ignored because structured_content is set. content: vec![text_block("ignored")], is_error: None, @@ -5109,6 +5113,7 @@ mod tests { "ok": true, "value": 42 })), + meta: None, }; let got = FunctionCallOutputPayload::from(&ctr); @@ -5147,10 +5152,11 @@ mod tests { #[test] fn falls_back_to_content_when_structured_is_null() { - let ctr = CallToolResult { + let ctr = McpCallToolResult { content: vec![text_block("hello"), text_block("world")], is_error: None, structured_content: Some(serde_json::Value::Null), + meta: None, }; let got = FunctionCallOutputPayload::from(&ctr); @@ -5166,10 +5172,11 @@ mod tests { #[test] fn success_flag_reflects_is_error_true() { - let ctr = CallToolResult { + let ctr = McpCallToolResult { content: vec![text_block("unused")], is_error: Some(true), structured_content: Some(json!({ "message": "bad" })), + meta: None, }; let got = FunctionCallOutputPayload::from(&ctr); @@ -5184,10 +5191,11 @@ mod tests { #[test] fn success_flag_true_with_no_error_and_content_used() { - let ctr = CallToolResult { + let ctr = McpCallToolResult { content: vec![text_block("alpha")], is_error: Some(false), structured_content: None, + meta: None, }; let got = FunctionCallOutputPayload::from(&ctr); @@ -5238,11 +5246,10 @@ mod tests { } } - fn text_block(s: &str) -> ContentBlock { - ContentBlock::TextContent(TextContent { - annotations: None, - text: s.to_string(), - r#type: "text".to_string(), + fn text_block(s: &str) -> serde_json::Value { + json!({ + "type": "text", + "text": s, }) } diff --git a/codex-rs/core/src/mcp/mod.rs b/codex-rs/core/src/mcp/mod.rs index 12c61ddaf1..cb0b5e2f26 100644 --- a/codex-rs/core/src/mcp/mod.rs +++ b/codex-rs/core/src/mcp/mod.rs @@ -1,6 +1,5 @@ pub mod auth; mod skill_dependencies; - pub(crate) use skill_dependencies::maybe_prompt_and_install_mcp_dependencies; use std::collections::HashMap; @@ -9,9 +8,12 @@ use std::path::PathBuf; use std::time::Duration; use async_channel::unbounded; +use codex_protocol::mcp::Resource; +use codex_protocol::mcp::ResourceTemplate; +use codex_protocol::mcp::Tool; use codex_protocol::protocol::McpListToolsResponseEvent; use codex_protocol::protocol::SandboxPolicy; -use mcp_types::Tool as McpTool; +use serde_json::Value; use tokio_util::sync::CancellationToken; use crate::AuthManager; @@ -201,8 +203,8 @@ pub fn split_qualified_tool_name(qualified_name: &str) -> Option<(String, String } pub fn group_tools_by_server( - tools: &HashMap, -) -> HashMap> { + tools: &HashMap, +) -> HashMap> { let mut grouped = HashMap::new(); for (qualified_name, tool) in tools { if let Some((server_name, tool_name)) = split_qualified_tool_name(qualified_name) { @@ -230,11 +232,96 @@ pub(crate) async fn collect_mcp_snapshot_from_manager( .map(|(name, entry)| (name.clone(), entry.auth_status)) .collect(); + let tools = tools + .into_iter() + .filter_map(|(name, tool)| match serde_json::to_value(tool.tool) { + Ok(value) => match Tool::from_mcp_value(value) { + Ok(tool) => Some((name, tool)), + Err(err) => { + tracing::warn!("Failed to convert MCP tool '{name}': {err}"); + None + } + }, + Err(err) => { + tracing::warn!("Failed to serialize MCP tool '{name}': {err}"); + None + } + }) + .collect(); + + let resources = resources + .into_iter() + .map(|(name, resources)| { + let resources = resources + .into_iter() + .filter_map(|resource| match serde_json::to_value(resource) { + Ok(value) => match Resource::from_mcp_value(value.clone()) { + Ok(resource) => Some(resource), + Err(err) => { + let (uri, resource_name) = match value { + Value::Object(obj) => ( + obj.get("uri") + .and_then(|v| v.as_str().map(ToString::to_string)), + obj.get("name") + .and_then(|v| v.as_str().map(ToString::to_string)), + ), + _ => (None, None), + }; + + tracing::warn!( + "Failed to convert MCP resource (uri={uri:?}, name={resource_name:?}): {err}" + ); + None + } + }, + Err(err) => { + tracing::warn!("Failed to serialize MCP resource: {err}"); + None + } + }) + .collect::>(); + (name, resources) + }) + .collect(); + + let resource_templates = resource_templates + .into_iter() + .map(|(name, templates)| { + let templates = templates + .into_iter() + .filter_map(|template| match serde_json::to_value(template) { + Ok(value) => match ResourceTemplate::from_mcp_value(value.clone()) { + Ok(template) => Some(template), + Err(err) => { + let (uri_template, template_name) = match value { + Value::Object(obj) => ( + obj.get("uriTemplate") + .or_else(|| obj.get("uri_template")) + .and_then(|v| v.as_str().map(ToString::to_string)), + obj.get("name") + .and_then(|v| v.as_str().map(ToString::to_string)), + ), + _ => (None, None), + }; + + tracing::warn!( + "Failed to convert MCP resource template (uri_template={uri_template:?}, name={template_name:?}): {err}" + ); + None + } + }, + Err(err) => { + tracing::warn!("Failed to serialize MCP resource template: {err}"); + None + } + }) + .collect::>(); + (name, templates) + }) + .collect(); + McpListToolsResponseEvent { - tools: tools - .into_iter() - .map(|(name, tool)| (name, tool.tool)) - .collect(), + tools, resources, resource_templates, auth_statuses, @@ -244,21 +331,18 @@ pub(crate) async fn collect_mcp_snapshot_from_manager( #[cfg(test)] mod tests { use super::*; - use mcp_types::ToolInputSchema; use pretty_assertions::assert_eq; - fn make_tool(name: &str) -> McpTool { - McpTool { - annotations: None, - description: None, - input_schema: ToolInputSchema { - properties: None, - required: None, - r#type: "object".to_string(), - }, + fn make_tool(name: &str) -> Tool { + Tool { name: name.to_string(), - output_schema: None, title: None, + description: None, + input_schema: serde_json::json!({"type": "object", "properties": {}}), + output_schema: None, + annotations: None, + icons: None, + meta: None, } } diff --git a/codex-rs/core/src/mcp_connection_manager.rs b/codex-rs/core/src/mcp_connection_manager.rs index 1b5048ab0c..1a5ec559b8 100644 --- a/codex-rs/core/src/mcp_connection_manager.rs +++ b/codex-rs/core/src/mcp_connection_manager.rs @@ -23,6 +23,8 @@ use async_channel::Sender; use codex_async_utils::CancelErr; use codex_async_utils::OrCancelExt; use codex_protocol::approvals::ElicitationRequestEvent; +use codex_protocol::mcp::CallToolResult; +use codex_protocol::mcp::RequestId as ProtocolRequestId; use codex_protocol::protocol::Event; use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::McpStartupCompleteEvent; @@ -37,22 +39,23 @@ use codex_rmcp_client::SendElicitation; use futures::future::BoxFuture; use futures::future::FutureExt; use futures::future::Shared; -use mcp_types::ClientCapabilities; -use mcp_types::Implementation; -use mcp_types::ListResourceTemplatesRequestParams; -use mcp_types::ListResourceTemplatesResult; -use mcp_types::ListResourcesRequestParams; -use mcp_types::ListResourcesResult; -use mcp_types::ReadResourceRequestParams; -use mcp_types::ReadResourceResult; -use mcp_types::RequestId; -use mcp_types::Resource; -use mcp_types::ResourceTemplate; -use mcp_types::Tool; +use rmcp::model::ClientCapabilities; +use rmcp::model::ElicitationCapability; +use rmcp::model::Implementation; +use rmcp::model::InitializeRequestParam; +use rmcp::model::ListResourceTemplatesResult; +use rmcp::model::ListResourcesResult; +use rmcp::model::PaginatedRequestParam; +use rmcp::model::ProtocolVersion; +use rmcp::model::ReadResourceRequestParam; +use rmcp::model::ReadResourceResult; +use rmcp::model::RequestId; +use rmcp::model::Resource; +use rmcp::model::ResourceTemplate; +use rmcp::model::Tool; use serde::Deserialize; use serde::Serialize; -use serde_json::json; use sha1::Digest; use sha1::Sha1; use tokio::sync::Mutex; @@ -198,7 +201,14 @@ impl ElicitationRequestManager { id: "mcp_elicitation_request".to_string(), msg: EventMsg::ElicitationRequest(ElicitationRequestEvent { server_name, - id, + id: match id.clone() { + rmcp::model::NumberOrString::String(value) => { + ProtocolRequestId::String(value.to_string()) + } + rmcp::model::NumberOrString::Number(value) => { + ProtocolRequestId::Integer(value) + } + }, message: elicitation.message, }), }) @@ -493,7 +503,7 @@ impl McpConnectionManager { let mut cursor: Option = None; loop { - let params = cursor.as_ref().map(|next| ListResourcesRequestParams { + let params = cursor.as_ref().map(|next| PaginatedRequestParam { cursor: Some(next.clone()), }); let response = match client.list_resources(params, timeout).await { @@ -558,11 +568,9 @@ impl McpConnectionManager { let mut cursor: Option = None; loop { - let params = cursor - .as_ref() - .map(|next| ListResourceTemplatesRequestParams { - cursor: Some(next.clone()), - }); + let params = cursor.as_ref().map(|next| PaginatedRequestParam { + cursor: Some(next.clone()), + }); let response = match client.list_resource_templates(params, timeout).await { Ok(result) => result, Err(err) => return (server_name_cloned, Err(err)), @@ -615,7 +623,7 @@ impl McpConnectionManager { server: &str, tool: &str, arguments: Option, - ) -> Result { + ) -> Result { let client = self.client_by_name(server).await?; if !client.tool_filter.allows(tool) { return Err(anyhow!( @@ -623,18 +631,34 @@ impl McpConnectionManager { )); } - client + let result: rmcp::model::CallToolResult = client .client .call_tool(tool.to_string(), arguments, client.tool_timeout) .await - .with_context(|| format!("tool call failed for `{server}/{tool}`")) + .with_context(|| format!("tool call failed for `{server}/{tool}`"))?; + + let content = result + .content + .into_iter() + .map(|content| { + serde_json::to_value(content) + .unwrap_or_else(|_| serde_json::Value::String("".to_string())) + }) + .collect(); + + Ok(CallToolResult { + content, + structured_content: result.structured_content, + is_error: result.is_error, + meta: result.meta.and_then(|meta| serde_json::to_value(meta).ok()), + }) } /// List resources from the specified server. pub async fn list_resources( &self, server: &str, - params: Option, + params: Option, ) -> Result { let managed = self.client_by_name(server).await?; let timeout = managed.tool_timeout; @@ -650,7 +674,7 @@ impl McpConnectionManager { pub async fn list_resource_templates( &self, server: &str, - params: Option, + params: Option, ) -> Result { let managed = self.client_by_name(server).await?; let client = managed.client.clone(); @@ -666,7 +690,7 @@ impl McpConnectionManager { pub async fn read_resource( &self, server: &str, - params: ReadResourceRequestParams, + params: ReadResourceRequestParam, ) -> Result { let managed = self.client_by_name(server).await?; let client = managed.client.clone(); @@ -849,25 +873,25 @@ async fn start_server_task( tx_event: Sender, elicitation_requests: ElicitationRequestManager, ) -> Result { - let params = mcp_types::InitializeRequestParams { + let params = InitializeRequestParam { capabilities: ClientCapabilities { experimental: None, roots: None, sampling: None, // https://modelcontextprotocol.io/specification/2025-06-18/client/elicitation#capabilities // indicates this should be an empty object. - elicitation: Some(json!({})), + elicitation: Some(ElicitationCapability { + schema_validation: None, + }), }, client_info: Implementation { name: "codex-mcp-client".to_owned(), version: env!("CARGO_PKG_VERSION").to_owned(), title: Some("Codex".into()), - // This field is used by Codex when it is an MCP - // server: it should not be used when Codex is - // an MCP client. - user_agent: None, + icons: None, + website_url: None, }, - protocol_version: mcp_types::MCP_SCHEMA_VERSION.to_owned(), + protocol_version: ProtocolVersion::V_2025_06_18, }; let send_elicitation = elicitation_requests.make_sender(server_name.clone(), tx_event); @@ -964,7 +988,7 @@ async fn list_tools_for_client( } ToolInfo { server_name: server_name.to_owned(), - tool_name: tool_def.name.clone(), + tool_name: tool_def.name.to_string(), tool: tool_def, connector_id: tool.connector_id, connector_name, @@ -1047,24 +1071,23 @@ mod mcp_init_error_display_tests {} mod tests { use super::*; use codex_protocol::protocol::McpAuthStatus; - use mcp_types::ToolInputSchema; + use rmcp::model::JsonObject; use std::collections::HashSet; + use std::sync::Arc; fn create_test_tool(server_name: &str, tool_name: &str) -> ToolInfo { ToolInfo { server_name: server_name.to_string(), tool_name: tool_name.to_string(), tool: Tool { - annotations: None, - description: Some(format!("Test tool: {tool_name}")), - input_schema: ToolInputSchema { - properties: None, - required: None, - r#type: "object".to_string(), - }, - name: tool_name.to_string(), - output_schema: None, + name: tool_name.to_string().into(), title: None, + description: Some(format!("Test tool: {tool_name}").into()), + input_schema: Arc::new(JsonObject::default()), + output_schema: None, + annotations: None, + icons: None, + meta: None, }, connector_id: None, connector_name: None, diff --git a/codex-rs/core/src/mcp_tool_call.rs b/codex-rs/core/src/mcp_tool_call.rs index 6425392fb5..75248f34cc 100644 --- a/codex-rs/core/src/mcp_tool_call.rs +++ b/codex-rs/core/src/mcp_tool_call.rs @@ -10,6 +10,7 @@ use crate::protocol::EventMsg; use crate::protocol::McpInvocation; use crate::protocol::McpToolCallBeginEvent; use crate::protocol::McpToolCallEndEvent; +use codex_protocol::mcp::CallToolResult; use codex_protocol::models::FunctionCallOutputPayload; use codex_protocol::models::ResponseInputItem; use codex_protocol::protocol::AskForApproval; @@ -18,7 +19,7 @@ use codex_protocol::request_user_input::RequestUserInputArgs; use codex_protocol::request_user_input::RequestUserInputQuestion; use codex_protocol::request_user_input::RequestUserInputQuestionOption; use codex_protocol::request_user_input::RequestUserInputResponse; -use mcp_types::ToolAnnotations; +use rmcp::model::ToolAnnotations; use std::sync::Arc; /// Handles the specified tool call dispatches the appropriate @@ -72,7 +73,7 @@ pub(crate) async fn handle_mcp_tool_call( .await; let start = Instant::now(); - let result = sess + let result: Result = sess .call_tool(&server, &tool_name, arguments_value.clone()) .await .map_err(|e| format!("tool call error: {e:?}")); @@ -134,7 +135,7 @@ pub(crate) async fn handle_mcp_tool_call( let start = Instant::now(); // Perform the tool call. - let result = sess + let result: Result = sess .call_tool(&server, &tool_name, arguments_value.clone()) .await .map_err(|e| format!("tool call error: {e:?}")); @@ -341,7 +342,7 @@ async fn notify_mcp_tool_call_skip( call_id: &str, invocation: McpInvocation, message: String, -) -> Result { +) -> Result { let tool_call_begin_event = EventMsg::McpToolCallBegin(McpToolCallBeginEvent { call_id: call_id.to_string(), invocation: invocation.clone(), diff --git a/codex-rs/core/src/tools/context.rs b/codex-rs/core/src/tools/context.rs index abe488681e..f0bbb158f5 100644 --- a/codex-rs/core/src/tools/context.rs +++ b/codex-rs/core/src/tools/context.rs @@ -4,12 +4,12 @@ use crate::tools::TELEMETRY_PREVIEW_MAX_BYTES; use crate::tools::TELEMETRY_PREVIEW_MAX_LINES; use crate::tools::TELEMETRY_PREVIEW_TRUNCATION_NOTICE; use crate::turn_diff_tracker::TurnDiffTracker; +use codex_protocol::mcp::CallToolResult; use codex_protocol::models::FunctionCallOutputContentItem; use codex_protocol::models::FunctionCallOutputPayload; use codex_protocol::models::ResponseInputItem; use codex_protocol::models::ShellToolCallParams; use codex_utils_string::take_bytes_at_char_boundary; -use mcp_types::CallToolResult; use std::borrow::Cow; use std::sync::Arc; use tokio::sync::Mutex; diff --git a/codex-rs/core/src/tools/handlers/mcp_resource.rs b/codex-rs/core/src/tools/handlers/mcp_resource.rs index 62f7a83e1a..df421bd6d3 100644 --- a/codex-rs/core/src/tools/handlers/mcp_resource.rs +++ b/codex-rs/core/src/tools/handlers/mcp_resource.rs @@ -4,17 +4,14 @@ use std::time::Duration; use std::time::Instant; use async_trait::async_trait; -use mcp_types::CallToolResult; -use mcp_types::ContentBlock; -use mcp_types::ListResourceTemplatesRequestParams; -use mcp_types::ListResourceTemplatesResult; -use mcp_types::ListResourcesRequestParams; -use mcp_types::ListResourcesResult; -use mcp_types::ReadResourceRequestParams; -use mcp_types::ReadResourceResult; -use mcp_types::Resource; -use mcp_types::ResourceTemplate; -use mcp_types::TextContent; +use codex_protocol::mcp::CallToolResult; +use rmcp::model::ListResourceTemplatesResult; +use rmcp::model::ListResourcesResult; +use rmcp::model::PaginatedRequestParam; +use rmcp::model::ReadResourceRequestParam; +use rmcp::model::ReadResourceResult; +use rmcp::model::Resource; +use rmcp::model::ResourceTemplate; use serde::Deserialize; use serde::Serialize; use serde::de::DeserializeOwned; @@ -264,7 +261,7 @@ async fn handle_list_resources( let payload_result: Result = async { if let Some(server_name) = server.clone() { - let params = cursor.clone().map(|value| ListResourcesRequestParams { + let params = cursor.clone().map(|value| PaginatedRequestParam { cursor: Some(value), }); let result = session @@ -371,11 +368,9 @@ async fn handle_list_resource_templates( let payload_result: Result = async { if let Some(server_name) = server.clone() { - let params = cursor - .clone() - .map(|value| ListResourceTemplatesRequestParams { - cursor: Some(value), - }); + let params = cursor.clone().map(|value| PaginatedRequestParam { + cursor: Some(value), + }); let result = session .list_resource_templates(&server_name, params) .await @@ -482,7 +477,7 @@ async fn handle_read_resource( let payload_result: Result = async { let result = session - .read_resource(&server, ReadResourceRequestParams { uri: uri.clone() }) + .read_resource(&server, ReadResourceRequestParam { uri: uri.clone() }) .await .map_err(|err| { FunctionCallError::RespondToModel(format!("resources/read failed: {err:#}")) @@ -551,13 +546,10 @@ async fn handle_read_resource( fn call_tool_result_from_content(content: &str, success: Option) -> CallToolResult { CallToolResult { - content: vec![ContentBlock::TextContent(TextContent { - annotations: None, - text: content.to_string(), - r#type: "text".to_string(), - })], - is_error: success.map(|value| !value), + content: vec![serde_json::json!({"type": "text", "text": content})], structured_content: None, + is_error: success.map(|value| !value), + meta: None, } } @@ -678,32 +670,33 @@ where #[cfg(test)] mod tests { use super::*; - use mcp_types::ListResourcesResult; - use mcp_types::ResourceTemplate; use pretty_assertions::assert_eq; + use rmcp::model::AnnotateAble; use serde_json::json; fn resource(uri: &str, name: &str) -> Resource { - Resource { - annotations: None, + rmcp::model::RawResource { + uri: uri.to_string(), + name: name.to_string(), + title: None, description: None, mime_type: None, - name: name.to_string(), size: None, - title: None, - uri: uri.to_string(), + icons: None, + meta: None, } + .no_annotation() } fn template(uri_template: &str, name: &str) -> ResourceTemplate { - ResourceTemplate { - annotations: None, - description: None, - mime_type: None, + rmcp::model::RawResourceTemplate { + uri_template: uri_template.to_string(), name: name.to_string(), title: None, - uri_template: uri_template.to_string(), + description: None, + mime_type: None, } + .no_annotation() } #[test] @@ -719,6 +712,7 @@ mod tests { #[test] fn list_resources_payload_from_single_server_copies_next_cursor() { let result = ListResourcesResult { + meta: None, next_cursor: Some("cursor-1".to_string()), resources: vec![resource("memo://id", "memo")], }; diff --git a/codex-rs/core/src/tools/router.rs b/codex-rs/core/src/tools/router.rs index d3ff84a2b0..51328ccc9f 100644 --- a/codex-rs/core/src/tools/router.rs +++ b/codex-rs/core/src/tools/router.rs @@ -15,6 +15,7 @@ use codex_protocol::models::LocalShellAction; use codex_protocol::models::ResponseInputItem; use codex_protocol::models::ResponseItem; use codex_protocol::models::ShellToolCallParams; +use rmcp::model::Tool; use std::collections::HashMap; use std::sync::Arc; use tracing::instrument; @@ -34,7 +35,7 @@ pub struct ToolRouter { impl ToolRouter { pub fn from_config( config: &ToolsConfig, - mcp_tools: Option>, + mcp_tools: Option>, dynamic_tools: &[DynamicToolSpec], ) -> Self { let builder = build_specs(config, mcp_tools, dynamic_tools); diff --git a/codex-rs/core/src/tools/spec.rs b/codex-rs/core/src/tools/spec.rs index a51372b9b3..c08feeef9a 100644 --- a/codex-rs/core/src/tools/spec.rs +++ b/codex-rs/core/src/tools/spec.rs @@ -1087,20 +1087,26 @@ pub(crate) fn create_tools_json_for_chat_completions_api( pub(crate) fn mcp_tool_to_openai_tool( fully_qualified_name: String, - tool: mcp_types::Tool, + tool: rmcp::model::Tool, ) -> Result { - let mcp_types::Tool { + let rmcp::model::Tool { description, - mut input_schema, + input_schema, .. } = tool; - // OpenAI models mandate the "properties" field in the schema. The Agents - // SDK fixed this by inserting an empty object for "properties" if it is not - // already present https://github.com/openai/openai-agents-python/issues/449 - // so here we do the same. - if input_schema.properties.is_none() { - input_schema.properties = Some(serde_json::Value::Object(serde_json::Map::new())); + let mut serialized_input_schema = serde_json::Value::Object(input_schema.as_ref().clone()); + + // OpenAI models mandate the "properties" field in the schema. Some MCP + // servers omit it (or set it to null), so we insert an empty object to + // match the behavior of the Agents SDK. + if let serde_json::Value::Object(obj) = &mut serialized_input_schema + && obj.get("properties").is_none_or(serde_json::Value::is_null) + { + obj.insert( + "properties".to_string(), + serde_json::Value::Object(serde_json::Map::new()), + ); } // Serialize to a raw JSON value so we can sanitize schemas coming from MCP @@ -1108,13 +1114,12 @@ pub(crate) fn mcp_tool_to_openai_tool( // Schemas (e.g. using enum/anyOf), or use unsupported variants like // `integer`. Our internal JsonSchema is a small subset and requires // `type`, so we coerce/sanitize here for compatibility. - let mut serialized_input_schema = serde_json::to_value(input_schema)?; sanitize_json_schema(&mut serialized_input_schema); let input_schema = serde_json::from_value::(serialized_input_schema)?; Ok(ResponsesApiTool { name: fully_qualified_name, - description: description.unwrap_or_default(), + description: description.map(Into::into).unwrap_or_default(), strict: false, parameters: input_schema, }) @@ -1254,7 +1259,7 @@ fn sanitize_json_schema(value: &mut JsonValue) { /// Builds the tool registry builder while collecting tool specs for later serialization. pub(crate) fn build_specs( config: &ToolsConfig, - mcp_tools: Option>, + mcp_tools: Option>, dynamic_tools: &[DynamicToolSpec], ) -> ToolRegistryBuilder { use crate::tools::handlers::ApplyPatchHandler; @@ -1410,7 +1415,7 @@ pub(crate) fn build_specs( } if let Some(mcp_tools) = mcp_tools { - let mut entries: Vec<(String, mcp_types::Tool)> = mcp_tools.into_iter().collect(); + let mut entries: Vec<(String, rmcp::model::Tool)> = mcp_tools.into_iter().collect(); entries.sort_by(|a, b| a.0.cmp(&b.0)); for (name, tool) in entries.into_iter() { @@ -1452,11 +1457,50 @@ mod tests { use crate::config::test_config; use crate::models_manager::manager::ModelsManager; use crate::tools::registry::ConfiguredToolSpec; - use mcp_types::ToolInputSchema; use pretty_assertions::assert_eq; use super::*; + fn mcp_tool( + name: &str, + description: &str, + input_schema: serde_json::Value, + ) -> rmcp::model::Tool { + rmcp::model::Tool { + name: name.to_string().into(), + title: None, + description: Some(description.to_string().into()), + input_schema: std::sync::Arc::new(rmcp::model::object(input_schema)), + output_schema: None, + annotations: None, + icons: None, + meta: None, + } + } + + #[test] + fn mcp_tool_to_openai_tool_inserts_empty_properties() { + let mut schema = rmcp::model::JsonObject::new(); + schema.insert("type".to_string(), serde_json::json!("object")); + + let tool = rmcp::model::Tool { + name: "no_props".to_string().into(), + title: None, + description: Some("No properties".to_string().into()), + input_schema: std::sync::Arc::new(schema), + output_schema: None, + annotations: None, + icons: None, + meta: None, + }; + + let openai_tool = + mcp_tool_to_openai_tool("server/no_props".to_string(), tool).expect("convert tool"); + let parameters = serde_json::to_value(openai_tool.parameters).expect("serialize schema"); + + assert_eq!(parameters.get("properties"), Some(&serde_json::json!({}))); + } + fn tool_name(tool: &ToolSpec) -> &str { match tool { ToolSpec::Function(ResponsesApiTool { name, .. }) => name, @@ -2026,37 +2070,26 @@ mod tests { &tools_config, Some(HashMap::from([( "test_server/do_something_cool".to_string(), - mcp_types::Tool { - name: "do_something_cool".to_string(), - input_schema: ToolInputSchema { - properties: Some(serde_json::json!({ - "string_argument": { - "type": "string", - }, - "number_argument": { - "type": "number", - }, + mcp_tool( + "do_something_cool", + "Do something cool", + serde_json::json!({ + "type": "object", + "properties": { + "string_argument": { "type": "string" }, + "number_argument": { "type": "number" }, "object_argument": { "type": "object", "properties": { "string_property": { "type": "string" }, "number_property": { "type": "number" }, }, - "required": [ - "string_property", - "number_property", - ], - "additionalProperties": Some(false), + "required": ["string_property", "number_property"], + "additionalProperties": false, }, - })), - required: None, - r#type: "object".to_string(), - }, - output_schema: None, - title: None, - annotations: None, - description: Some("Do something cool".to_string()), - }, + }, + }), + ), )])), &[], ) @@ -2120,51 +2153,18 @@ mod tests { }); // Intentionally construct a map with keys that would sort alphabetically. - let tools_map: HashMap = HashMap::from([ + let tools_map: HashMap = HashMap::from([ ( "test_server/do".to_string(), - mcp_types::Tool { - name: "a".to_string(), - input_schema: ToolInputSchema { - properties: Some(serde_json::json!({})), - required: None, - r#type: "object".to_string(), - }, - output_schema: None, - title: None, - annotations: None, - description: Some("a".to_string()), - }, + mcp_tool("a", "a", serde_json::json!({"type": "object"})), ), ( "test_server/something".to_string(), - mcp_types::Tool { - name: "b".to_string(), - input_schema: ToolInputSchema { - properties: Some(serde_json::json!({})), - required: None, - r#type: "object".to_string(), - }, - output_schema: None, - title: None, - annotations: None, - description: Some("b".to_string()), - }, + mcp_tool("b", "b", serde_json::json!({"type": "object"})), ), ( "test_server/cool".to_string(), - mcp_types::Tool { - name: "c".to_string(), - input_schema: ToolInputSchema { - properties: Some(serde_json::json!({})), - required: None, - r#type: "object".to_string(), - }, - output_schema: None, - title: None, - annotations: None, - description: Some("c".to_string()), - }, + mcp_tool("c", "c", serde_json::json!({"type": "object"})), ), ]); @@ -2200,22 +2200,16 @@ mod tests { &tools_config, Some(HashMap::from([( "dash/search".to_string(), - mcp_types::Tool { - name: "search".to_string(), - input_schema: ToolInputSchema { - properties: Some(serde_json::json!({ - "query": { - "description": "search query" - } - })), - required: None, - r#type: "object".to_string(), - }, - output_schema: None, - title: None, - annotations: None, - description: Some("Search docs".to_string()), - }, + mcp_tool( + "search", + "Search docs", + serde_json::json!({ + "type": "object", + "properties": { + "query": {"description": "search query"} + } + }), + ), )])), &[], ) @@ -2258,20 +2252,14 @@ mod tests { &tools_config, Some(HashMap::from([( "dash/paginate".to_string(), - mcp_types::Tool { - name: "paginate".to_string(), - input_schema: ToolInputSchema { - properties: Some(serde_json::json!({ - "page": { "type": "integer" } - })), - required: None, - r#type: "object".to_string(), - }, - output_schema: None, - title: None, - annotations: None, - description: Some("Pagination".to_string()), - }, + mcp_tool( + "paginate", + "Pagination", + serde_json::json!({ + "type": "object", + "properties": {"page": {"type": "integer"}} + }), + ), )])), &[], ) @@ -2313,20 +2301,14 @@ mod tests { &tools_config, Some(HashMap::from([( "dash/tags".to_string(), - mcp_types::Tool { - name: "tags".to_string(), - input_schema: ToolInputSchema { - properties: Some(serde_json::json!({ - "tags": { "type": "array" } - })), - required: None, - r#type: "object".to_string(), - }, - output_schema: None, - title: None, - annotations: None, - description: Some("Tags".to_string()), - }, + mcp_tool( + "tags", + "Tags", + serde_json::json!({ + "type": "object", + "properties": {"tags": {"type": "array"}} + }), + ), )])), &[], ) @@ -2370,20 +2352,16 @@ mod tests { &tools_config, Some(HashMap::from([( "dash/value".to_string(), - mcp_types::Tool { - name: "value".to_string(), - input_schema: ToolInputSchema { - properties: Some(serde_json::json!({ - "value": { "anyOf": [ { "type": "string" }, { "type": "number" } ] } - })), - required: None, - r#type: "object".to_string(), - }, - output_schema: None, - title: None, - annotations: None, - description: Some("AnyOf Value".to_string()), - }, + mcp_tool( + "value", + "AnyOf Value", + serde_json::json!({ + "type": "object", + "properties": { + "value": {"anyOf": [{"type": "string"}, {"type": "number"}]} + } + }), + ), )])), &[], ) @@ -2482,46 +2460,33 @@ Examples of valid command strings: &tools_config, Some(HashMap::from([( "test_server/do_something_cool".to_string(), - mcp_types::Tool { - name: "do_something_cool".to_string(), - input_schema: ToolInputSchema { - properties: Some(serde_json::json!({ - "string_argument": { - "type": "string", - }, - "number_argument": { - "type": "number", - }, + mcp_tool( + "do_something_cool", + "Do something cool", + serde_json::json!({ + "type": "object", + "properties": { + "string_argument": {"type": "string"}, + "number_argument": {"type": "number"}, "object_argument": { "type": "object", "properties": { - "string_property": { "type": "string" }, - "number_property": { "type": "number" }, + "string_property": {"type": "string"}, + "number_property": {"type": "number"} }, - "required": [ - "string_property", - "number_property", - ], + "required": ["string_property", "number_property"], "additionalProperties": { "type": "object", "properties": { - "addtl_prop": { "type": "string" }, + "addtl_prop": {"type": "string"} }, - "required": [ - "addtl_prop", - ], - "additionalProperties": false, - }, - }, - })), - required: None, - r#type: "object".to_string(), - }, - output_schema: None, - title: None, - annotations: None, - description: Some("Do something cool".to_string()), - }, + "required": ["addtl_prop"], + "additionalProperties": false + } + } + } + }), + ), )])), &[], ) diff --git a/codex-rs/core/tests/common/lib.rs b/codex-rs/core/tests/common/lib.rs index 5b80f80ba3..5df2a74edd 100644 --- a/codex-rs/core/tests/common/lib.rs +++ b/codex-rs/core/tests/common/lib.rs @@ -209,7 +209,7 @@ where use tokio::time::timeout; loop { // Allow a bit more time to accommodate async startup work (e.g. config IO, tool discovery) - let ev = timeout(wait_time.max(Duration::from_secs(5)), codex.next_event()) + let ev = timeout(wait_time.max(Duration::from_secs(10)), codex.next_event()) .await .expect("timeout waiting for event") .expect("stream ended unexpectedly"); diff --git a/codex-rs/core/tests/suite/rmcp_client.rs b/codex-rs/core/tests/suite/rmcp_client.rs index 7eaeef6590..cdf6a1adc5 100644 --- a/codex-rs/core/tests/suite/rmcp_client.rs +++ b/codex-rs/core/tests/suite/rmcp_client.rs @@ -26,7 +26,6 @@ use core_test_support::skip_if_no_network; use core_test_support::stdio_server_bin; use core_test_support::test_codex::test_codex; use core_test_support::wait_for_event; -use mcp_types::ContentBlock; use serde_json::Value; use serde_json::json; use serial_test::serial; @@ -307,14 +306,10 @@ async fn stdio_image_responses_round_trip() -> anyhow::Result<()> { let base64_only = OPENAI_PNG .strip_prefix("data:image/png;base64,") .expect("data url prefix"); - match &result.content[0] { - ContentBlock::ImageContent(img) => { - assert_eq!(img.mime_type, "image/png"); - assert_eq!(img.r#type, "image"); - assert_eq!(img.data, base64_only); - } - other => panic!("expected image content, got {other:?}"), - } + let entry = result.content[0].as_object().expect("content object"); + assert_eq!(entry.get("type"), Some(&json!("image"))); + assert_eq!(entry.get("mimeType"), Some(&json!("image/png"))); + assert_eq!(entry.get("data"), Some(&json!(base64_only))); wait_for_event(&fixture.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; diff --git a/codex-rs/exec/Cargo.toml b/codex-rs/exec/Cargo.toml index 862d8d1694..860f4034be 100644 --- a/codex-rs/exec/Cargo.toml +++ b/codex-rs/exec/Cargo.toml @@ -28,7 +28,6 @@ codex-common = { workspace = true, features = [ codex-core = { workspace = true } codex-protocol = { workspace = true } codex-utils-absolute-path = { workspace = true } -mcp-types = { workspace = true } owo-colors = { workspace = true } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } @@ -56,9 +55,9 @@ assert_cmd = { workspace = true } codex-utils-cargo-bin = { workspace = true } core_test_support = { workspace = true } libc = { workspace = true } -mcp-types = { workspace = true } predicates = { workspace = true } pretty_assertions = { workspace = true } +rmcp = { workspace = true } tempfile = { workspace = true } uuid = { workspace = true } walkdir = { workspace = true } diff --git a/codex-rs/exec/src/event_processor_with_human_output.rs b/codex-rs/exec/src/event_processor_with_human_output.rs index 8469dc42ed..3b4a3bf86b 100644 --- a/codex-rs/exec/src/event_processor_with_human_output.rs +++ b/codex-rs/exec/src/event_processor_with_human_output.rs @@ -375,7 +375,8 @@ impl EventProcessor for EventProcessorWithHumanOutput { ts_msg!(self, "{}", title.style(title_style)); if let Ok(res) = result { - let val: serde_json::Value = res.into(); + let val = serde_json::to_value(res) + .unwrap_or_else(|_| serde_json::Value::String("".to_string())); let pretty = serde_json::to_string_pretty(&val).unwrap_or_else(|_| val.to_string()); diff --git a/codex-rs/exec/src/exec_events.rs b/codex-rs/exec/src/exec_events.rs index 47c67a6dca..368098f16b 100644 --- a/codex-rs/exec/src/exec_events.rs +++ b/codex-rs/exec/src/exec_events.rs @@ -1,5 +1,4 @@ use codex_protocol::models::WebSearchAction; -use mcp_types::ContentBlock as McpContentBlock; use serde::Deserialize; use serde::Serialize; use serde_json::Value as JsonValue; @@ -256,7 +255,14 @@ pub struct CollabToolCallItem { /// Result payload produced by an MCP tool invocation. #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, TS)] pub struct McpToolCallItemResult { - pub content: Vec, + // NOTE: `rmcp::model::Content` (and its `RawContent` variants) would be a + // more precise Rust representation of MCP content blocks. We intentionally + // use `serde_json::Value` here because this crate exports JSON schema + TS + // types (`schemars`/`ts-rs`), and the rmcp model types aren't set up to be + // schema/TS friendly (and would introduce heavier coupling to rmcp's Rust + // representations). Using `JsonValue` keeps the payload wire-shaped and + // easy to export. + pub content: Vec, pub structured_content: Option, } diff --git a/codex-rs/exec/tests/event_processor_with_json_output.rs b/codex-rs/exec/tests/event_processor_with_json_output.rs index 47b7a9f331..6a853857da 100644 --- a/codex-rs/exec/tests/event_processor_with_json_output.rs +++ b/codex-rs/exec/tests/event_processor_with_json_output.rs @@ -56,6 +56,7 @@ use codex_exec::exec_events::Usage; use codex_exec::exec_events::WebSearchItem; use codex_protocol::ThreadId; use codex_protocol::config_types::ModeKind; +use codex_protocol::mcp::CallToolResult; use codex_protocol::models::WebSearchAction; use codex_protocol::plan_tool::PlanItemArg; use codex_protocol::plan_tool::StepStatus; @@ -63,10 +64,8 @@ use codex_protocol::plan_tool::UpdatePlanArgs; use codex_protocol::protocol::CodexErrorInfo; use codex_protocol::protocol::ExecCommandOutputDeltaEvent; use codex_protocol::protocol::ExecOutputStream; -use mcp_types::CallToolResult; -use mcp_types::ContentBlock; -use mcp_types::TextContent; use pretty_assertions::assert_eq; +use rmcp::model::Content; use serde_json::json; use std::path::PathBuf; use std::time::Duration; @@ -385,6 +384,7 @@ fn mcp_tool_call_begin_and_end_emit_item_events() { content: Vec::new(), is_error: None, structured_content: None, + meta: None, }), }), ); @@ -499,13 +499,10 @@ fn mcp_tool_call_defaults_arguments_and_preserves_structured_content() { invocation, duration: Duration::from_millis(10), result: Ok(CallToolResult { - content: vec![ContentBlock::TextContent(TextContent { - annotations: None, - text: "done".to_string(), - r#type: "text".to_string(), - })], + content: vec![serde_json::to_value(Content::text("done")).unwrap()], is_error: None, structured_content: Some(json!({ "status": "ok" })), + meta: None, }), }), ); @@ -520,11 +517,7 @@ fn mcp_tool_call_defaults_arguments_and_preserves_structured_content() { tool: "tool_z".to_string(), arguments: serde_json::Value::Null, result: Some(McpToolCallItemResult { - content: vec![ContentBlock::TextContent(TextContent { - annotations: None, - text: "done".to_string(), - r#type: "text".to_string(), - })], + content: vec![serde_json::to_value(Content::text("done")).unwrap()], structured_content: Some(json!({ "status": "ok" })), }), error: None, diff --git a/codex-rs/mcp-server/Cargo.toml b/codex-rs/mcp-server/Cargo.toml index 6236384c95..7a952342de 100644 --- a/codex-rs/mcp-server/Cargo.toml +++ b/codex-rs/mcp-server/Cargo.toml @@ -22,7 +22,7 @@ codex-common = { workspace = true, features = ["cli"] } codex-core = { workspace = true } codex-protocol = { workspace = true } codex-utils-json-to-toml = { workspace = true } -mcp-types = { workspace = true } +rmcp = { workspace = true } schemars = { workspace = true } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } diff --git a/codex-rs/mcp-server/src/codex_tool_config.rs b/codex-rs/mcp-server/src/codex_tool_config.rs index 8131d7da52..94bf4369a9 100644 --- a/codex-rs/mcp-server/src/codex_tool_config.rs +++ b/codex-rs/mcp-server/src/codex_tool_config.rs @@ -6,15 +6,15 @@ use codex_core::protocol::AskForApproval; use codex_protocol::ThreadId; use codex_protocol::config_types::SandboxMode; use codex_utils_json_to_toml::json_to_toml; -use mcp_types::Tool; -use mcp_types::ToolInputSchema; -use mcp_types::ToolOutputSchema; +use rmcp::model::JsonObject; +use rmcp::model::Tool; use schemars::JsonSchema; use schemars::r#gen::SchemaSettings; use serde::Deserialize; use serde::Serialize; use std::collections::HashMap; use std::path::PathBuf; +use std::sync::Arc; /// Client-supplied configuration for a `codex` tool-call. #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)] @@ -115,35 +115,35 @@ pub(crate) fn create_tool_for_codex_tool_call_param() -> Tool { .into_generator() .into_root_schema_for::(); - #[expect(clippy::expect_used)] - let schema_value = - serde_json::to_value(&schema).expect("Codex tool schema should serialise to JSON"); - - let tool_input_schema = - serde_json::from_value::(schema_value).unwrap_or_else(|e| { - panic!("failed to create Tool from schema: {e}"); - }); + let input_schema = create_tool_input_schema(schema, "Codex tool schema should serialize"); Tool { - name: "codex".to_string(), + name: "codex".into(), title: Some("Codex".to_string()), - input_schema: tool_input_schema, + input_schema, output_schema: Some(codex_tool_output_schema()), description: Some( - "Run a Codex session. Accepts configuration parameters matching the Codex Config struct.".to_string(), + "Run a Codex session. Accepts configuration parameters matching the Codex Config struct." + .into(), ), annotations: None, + icons: None, + meta: None, } } -fn codex_tool_output_schema() -> ToolOutputSchema { - ToolOutputSchema { - properties: Some(serde_json::json!({ +fn codex_tool_output_schema() -> Arc { + let schema = serde_json::json!({ + "type": "object", + "properties": { "threadId": { "type": "string" }, "content": { "type": "string" } - })), - required: Some(vec!["threadId".to_string(), "content".to_string()]), - r#type: "object".to_string(), + }, + "required": ["threadId", "content"], + }); + match schema { + serde_json::Value::Object(map) => Arc::new(map), + _ => unreachable!("json literal must be an object"), } } @@ -237,27 +237,46 @@ pub(crate) fn create_tool_for_codex_tool_call_reply_param() -> Tool { .into_generator() .into_root_schema_for::(); - #[expect(clippy::expect_used)] - let schema_value = - serde_json::to_value(&schema).expect("Codex reply tool schema should serialise to JSON"); - - let tool_input_schema = - serde_json::from_value::(schema_value).unwrap_or_else(|e| { - panic!("failed to create Tool from schema: {e}"); - }); + let input_schema = create_tool_input_schema(schema, "Codex reply tool schema should serialize"); Tool { - name: "codex-reply".to_string(), + name: "codex-reply".into(), title: Some("Codex Reply".to_string()), - input_schema: tool_input_schema, + input_schema, output_schema: Some(codex_tool_output_schema()), description: Some( - "Continue a Codex conversation by providing the thread id and prompt.".to_string(), + "Continue a Codex conversation by providing the thread id and prompt.".into(), ), annotations: None, + icons: None, + meta: None, } } +fn create_tool_input_schema( + schema: schemars::schema::RootSchema, + panic_message: &str, +) -> Arc { + #[expect(clippy::expect_used)] + let schema_value = serde_json::to_value(&schema).expect(panic_message); + let mut schema_object = match schema_value { + serde_json::Value::Object(object) => object, + _ => panic!("tool schema should serialize to a JSON object"), + }; + + // Prefer keeping the "core" JSON Schema keys while still preserving `$defs` + // in case any `$ref` leaks into the generated schema (even though we try + // to inline subschemas). + let mut input_schema = JsonObject::new(); + for key in ["properties", "required", "type", "$defs", "definitions"] { + if let Some(value) = schema_object.remove(key) { + input_schema.insert(key.to_string(), value); + } + } + + Arc::new(input_schema) +} + #[cfg(test)] mod tests { use super::*; diff --git a/codex-rs/mcp-server/src/codex_tool_runner.rs b/codex-rs/mcp-server/src/codex_tool_runner.rs index 1dbdbc767b..6273607a22 100644 --- a/codex-rs/mcp-server/src/codex_tool_runner.rs +++ b/codex-rs/mcp-server/src/codex_tool_runner.rs @@ -23,15 +23,12 @@ use codex_core::protocol::Submission; use codex_core::protocol::TurnCompleteEvent; use codex_protocol::ThreadId; use codex_protocol::user_input::UserInput; -use mcp_types::CallToolResult; -use mcp_types::ContentBlock; -use mcp_types::RequestId; -use mcp_types::TextContent; +use rmcp::model::CallToolResult; +use rmcp::model::Content; +use rmcp::model::RequestId; use serde_json::json; use tokio::sync::Mutex; -pub(crate) const INVALID_PARAMS_ERROR_CODE: i64 = -32602; - /// To adhere to MCP `tools/call` response format, include the Codex /// `threadId` in the `structured_content` field of the response. /// Some MCP clients ignore `content` when `structuredContent` is present, so @@ -42,11 +39,7 @@ pub(crate) fn create_call_tool_result_with_thread_id( is_error: Option, ) -> CallToolResult { let content_text = text; - let content = vec![ContentBlock::TextContent(TextContent { - r#type: "text".to_string(), - text: content_text.clone(), - annotations: None, - })]; + let content = vec![Content::text(content_text.clone())]; let structured_content = json!({ "threadId": thread_id, "content": content_text, @@ -55,6 +48,7 @@ pub(crate) fn create_call_tool_result_with_thread_id( content, is_error, structured_content: Some(structured_content), + meta: None, } } @@ -78,13 +72,10 @@ pub async fn run_codex_tool_session( Ok(res) => res, Err(e) => { let result = CallToolResult { - content: vec![ContentBlock::TextContent(TextContent { - r#type: "text".to_string(), - text: format!("Failed to start Codex session: {e}"), - annotations: None, - })], + content: vec![Content::text(format!("Failed to start Codex session: {e}"))], is_error: Some(true), structured_content: None, + meta: None, }; outgoing.send_response(id.clone(), result).await; return; @@ -109,10 +100,7 @@ pub async fn run_codex_tool_session( // Use the original MCP request ID as the `sub_id` for the Codex submission so that // any events emitted for this tool-call can be correlated with the // originating `tools/call` request. - let sub_id = match &id { - RequestId::String(s) => s.clone(), - RequestId::Integer(n) => n.to_string(), - }; + let sub_id = id.to_string(); running_requests_id_to_codex_uuid .lock() .await @@ -207,10 +195,7 @@ async fn run_codex_tool_session_inner( request_id: RequestId, running_requests_id_to_codex_uuid: Arc>>, ) { - let request_id_str = match &request_id { - RequestId::String(s) => s.clone(), - RequestId::Integer(n) => n.to_string(), - }; + let request_id_str = request_id.to_string(); // Stream events until the task needs to pause for user interaction or // completes. diff --git a/codex-rs/mcp-server/src/error_code.rs b/codex-rs/mcp-server/src/error_code.rs deleted file mode 100644 index 1ffd889d40..0000000000 --- a/codex-rs/mcp-server/src/error_code.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub(crate) const INVALID_REQUEST_ERROR_CODE: i64 = -32600; -pub(crate) const INTERNAL_ERROR_CODE: i64 = -32603; diff --git a/codex-rs/mcp-server/src/exec_approval.rs b/codex-rs/mcp-server/src/exec_approval.rs index a98099dcf2..c7914d1e60 100644 --- a/codex-rs/mcp-server/src/exec_approval.rs +++ b/codex-rs/mcp-server/src/exec_approval.rs @@ -6,20 +6,16 @@ use codex_core::protocol::Op; use codex_core::protocol::ReviewDecision; use codex_protocol::ThreadId; use codex_protocol::parse_command::ParsedCommand; -use mcp_types::ElicitRequest; -use mcp_types::ElicitRequestParamsRequestedSchema; -use mcp_types::JSONRPCErrorError; -use mcp_types::ModelContextProtocolRequest; -use mcp_types::RequestId; +use rmcp::model::ErrorData; +use rmcp::model::RequestId; use serde::Deserialize; use serde::Serialize; +use serde_json::Value; use serde_json::json; use tracing::error; -use crate::codex_tool_runner::INVALID_PARAMS_ERROR_CODE; - -/// Conforms to [`mcp_types::ElicitRequestParams`] so that it can be used as the -/// `params` field of an [`ElicitRequest`]. +/// Conforms to the MCP elicitation request params shape, so it can be used as +/// the `params` field of an `elicitation/create` request. #[derive(Debug, Deserialize, Serialize)] pub struct ExecApprovalElicitRequestParams { // These fields are required so that `params` @@ -27,7 +23,7 @@ pub struct ExecApprovalElicitRequestParams { pub message: String, #[serde(rename = "requestedSchema")] - pub requested_schema: ElicitRequestParamsRequestedSchema, + pub requested_schema: Value, // These are additional fields the client can use to // correlate the request with the codex tool call. @@ -73,11 +69,7 @@ pub(crate) async fn handle_exec_approval_request( let params = ExecApprovalElicitRequestParams { message, - requested_schema: ElicitRequestParamsRequestedSchema { - r#type: "object".to_string(), - properties: json!({}), - required: None, - }, + requested_schema: json!({"type":"object","properties":{}}), thread_id, codex_elicitation: "exec-approval".to_string(), codex_mcp_tool_call_id: tool_call_id.clone(), @@ -94,14 +86,7 @@ pub(crate) async fn handle_exec_approval_request( error!("{message}"); outgoing - .send_error( - request_id.clone(), - JSONRPCErrorError { - code: INVALID_PARAMS_ERROR_CODE, - message, - data: None, - }, - ) + .send_error(request_id.clone(), ErrorData::invalid_params(message, None)) .await; return; @@ -109,7 +94,7 @@ pub(crate) async fn handle_exec_approval_request( }; let on_response = outgoing - .send_request(ElicitRequest::METHOD, Some(params_json)) + .send_request("elicitation/create", Some(params_json)) .await; // Listen for the response on a separate task so we don't block the main agent loop. @@ -124,7 +109,7 @@ pub(crate) async fn handle_exec_approval_request( async fn on_exec_approval_response( event_id: String, - receiver: tokio::sync::oneshot::Receiver, + receiver: tokio::sync::oneshot::Receiver, codex: Arc, ) { let response = receiver.await; diff --git a/codex-rs/mcp-server/src/lib.rs b/codex-rs/mcp-server/src/lib.rs index dabd7cca0f..eed176c575 100644 --- a/codex-rs/mcp-server/src/lib.rs +++ b/codex-rs/mcp-server/src/lib.rs @@ -8,7 +8,10 @@ use std::path::PathBuf; use codex_common::CliConfigOverrides; use codex_core::config::Config; -use mcp_types::JSONRPCMessage; +use rmcp::model::ClientNotification; +use rmcp::model::ClientRequest; +use rmcp::model::JsonRpcMessage; +use serde_json::Value; use tokio::io::AsyncBufReadExt; use tokio::io::AsyncWriteExt; use tokio::io::BufReader; @@ -21,13 +24,13 @@ use tracing_subscriber::EnvFilter; mod codex_tool_config; mod codex_tool_runner; -mod error_code; mod exec_approval; pub(crate) mod message_processor; mod outgoing_message; mod patch_approval; use crate::message_processor::MessageProcessor; +use crate::outgoing_message::OutgoingJsonRpcMessage; use crate::outgoing_message::OutgoingMessage; use crate::outgoing_message::OutgoingMessageSender; @@ -43,6 +46,8 @@ pub use crate::patch_approval::PatchApprovalResponse; /// plenty for an interactive CLI. const CHANNEL_CAPACITY: usize = 128; +type IncomingMessage = JsonRpcMessage; + pub async fn run_main( codex_linux_sandbox_exe: Option, cli_config_overrides: CliConfigOverrides, @@ -55,7 +60,7 @@ pub async fn run_main( .init(); // Set up channels. - let (incoming_tx, mut incoming_rx) = mpsc::channel::(CHANNEL_CAPACITY); + let (incoming_tx, mut incoming_rx) = mpsc::channel::(CHANNEL_CAPACITY); let (outgoing_tx, mut outgoing_rx) = mpsc::unbounded_channel::(); // Task: read from stdin, push to `incoming_tx`. @@ -66,14 +71,14 @@ pub async fn run_main( let mut lines = reader.lines(); while let Some(line) = lines.next_line().await.unwrap_or_default() { - match serde_json::from_str::(&line) { + match serde_json::from_str::(&line) { Ok(msg) => { if incoming_tx.send(msg).await.is_err() { // Receiver gone – nothing left to do. break; } } - Err(e) => error!("Failed to deserialize JSONRPCMessage: {e}"), + Err(e) => error!("Failed to deserialize JSON-RPC message: {e}"), } } @@ -106,10 +111,10 @@ pub async fn run_main( async move { while let Some(msg) = incoming_rx.recv().await { match msg { - JSONRPCMessage::Request(r) => processor.process_request(r).await, - JSONRPCMessage::Response(r) => processor.process_response(r).await, - JSONRPCMessage::Notification(n) => processor.process_notification(n).await, - JSONRPCMessage::Error(e) => processor.process_error(e), + JsonRpcMessage::Request(r) => processor.process_request(r).await, + JsonRpcMessage::Response(r) => processor.process_response(r).await, + JsonRpcMessage::Notification(n) => processor.process_notification(n).await, + JsonRpcMessage::Error(e) => processor.process_error(e), } } @@ -121,7 +126,7 @@ pub async fn run_main( let stdout_writer_handle = tokio::spawn(async move { let mut stdout = io::stdout(); while let Some(outgoing_message) = outgoing_rx.recv().await { - let msg: JSONRPCMessage = outgoing_message.into(); + let msg: OutgoingJsonRpcMessage = outgoing_message.into(); match serde_json::to_string(&msg) { Ok(json) => { if let Err(e) = stdout.write_all(json.as_bytes()).await { @@ -133,7 +138,7 @@ pub async fn run_main( break; } } - Err(e) => error!("Failed to serialize JSONRPCMessage: {e}"), + Err(e) => error!("Failed to serialize JSON-RPC message: {e}"), } } diff --git a/codex-rs/mcp-server/src/message_processor.rs b/codex-rs/mcp-server/src/message_processor.rs index 9d947cda3f..1d9ab192aa 100644 --- a/codex-rs/mcp-server/src/message_processor.rs +++ b/codex-rs/mcp-server/src/message_processor.rs @@ -1,41 +1,40 @@ use std::collections::HashMap; use std::path::PathBuf; -use crate::codex_tool_config::CodexToolCallParam; -use crate::codex_tool_config::CodexToolCallReplyParam; -use crate::codex_tool_config::create_tool_for_codex_tool_call_param; -use crate::codex_tool_config::create_tool_for_codex_tool_call_reply_param; -use crate::error_code::INVALID_REQUEST_ERROR_CODE; -use crate::outgoing_message::OutgoingMessageSender; -use codex_protocol::ThreadId; -use codex_protocol::protocol::SessionSource; - use codex_core::AuthManager; use codex_core::ThreadManager; use codex_core::config::Config; use codex_core::default_client::USER_AGENT_SUFFIX; use codex_core::default_client::get_codex_user_agent; use codex_core::protocol::Submission; -use mcp_types::CallToolRequestParams; -use mcp_types::CallToolResult; -use mcp_types::ClientRequest as McpClientRequest; -use mcp_types::ContentBlock; -use mcp_types::JSONRPCError; -use mcp_types::JSONRPCErrorError; -use mcp_types::JSONRPCNotification; -use mcp_types::JSONRPCRequest; -use mcp_types::JSONRPCResponse; -use mcp_types::ListToolsResult; -use mcp_types::ModelContextProtocolRequest; -use mcp_types::RequestId; -use mcp_types::ServerCapabilitiesTools; -use mcp_types::ServerNotification; -use mcp_types::TextContent; +use codex_protocol::ThreadId; +use codex_protocol::protocol::SessionSource; +use rmcp::model::CallToolRequestParam; +use rmcp::model::CallToolResult; +use rmcp::model::ClientNotification; +use rmcp::model::ClientRequest; +use rmcp::model::ErrorCode; +use rmcp::model::ErrorData; +use rmcp::model::Implementation; +use rmcp::model::InitializeResult; +use rmcp::model::JsonRpcError; +use rmcp::model::JsonRpcNotification; +use rmcp::model::JsonRpcRequest; +use rmcp::model::JsonRpcResponse; +use rmcp::model::RequestId; +use rmcp::model::ServerCapabilities; +use rmcp::model::ToolsCapability; use serde_json::json; use std::sync::Arc; use tokio::sync::Mutex; use tokio::task; +use crate::codex_tool_config::CodexToolCallParam; +use crate::codex_tool_config::CodexToolCallReplyParam; +use crate::codex_tool_config::create_tool_for_codex_tool_call_param; +use crate::codex_tool_config::create_tool_for_codex_tool_call_reply_param; +use crate::outgoing_message::OutgoingMessageSender; + pub(crate) struct MessageProcessor { outgoing: Arc, initialized: bool, @@ -72,126 +71,113 @@ impl MessageProcessor { } } - pub(crate) async fn process_request(&mut self, request: JSONRPCRequest) { - // Hold on to the ID so we can respond. + pub(crate) async fn process_request(&mut self, request: JsonRpcRequest) { let request_id = request.id.clone(); + let client_request = request.request; - let client_request = match McpClientRequest::try_from(request) { - Ok(client_request) => client_request, - Err(e) => { - tracing::warn!("Failed to convert request: {e}"); - return; - } - }; - - // Dispatch to a dedicated handler for each request type. match client_request { - McpClientRequest::InitializeRequest(params) => { - self.handle_initialize(request_id, params).await; + ClientRequest::InitializeRequest(params) => { + self.handle_initialize(request_id, params.params).await; } - McpClientRequest::PingRequest(params) => { - self.handle_ping(request_id, params).await; + ClientRequest::PingRequest(_params) => { + self.handle_ping(request_id).await; } - McpClientRequest::ListResourcesRequest(params) => { - self.handle_list_resources(params); + ClientRequest::ListResourcesRequest(params) => { + self.handle_list_resources(params.params); } - McpClientRequest::ListResourceTemplatesRequest(params) => { - self.handle_list_resource_templates(params); + ClientRequest::ListResourceTemplatesRequest(params) => { + self.handle_list_resource_templates(params.params); } - McpClientRequest::ReadResourceRequest(params) => { - self.handle_read_resource(params); + ClientRequest::ReadResourceRequest(params) => { + self.handle_read_resource(params.params); } - McpClientRequest::SubscribeRequest(params) => { - self.handle_subscribe(params); + ClientRequest::SubscribeRequest(params) => { + self.handle_subscribe(params.params); } - McpClientRequest::UnsubscribeRequest(params) => { - self.handle_unsubscribe(params); + ClientRequest::UnsubscribeRequest(params) => { + self.handle_unsubscribe(params.params); } - McpClientRequest::ListPromptsRequest(params) => { - self.handle_list_prompts(params); + ClientRequest::ListPromptsRequest(params) => { + self.handle_list_prompts(params.params); } - McpClientRequest::GetPromptRequest(params) => { - self.handle_get_prompt(params); + ClientRequest::GetPromptRequest(params) => { + self.handle_get_prompt(params.params); } - McpClientRequest::ListToolsRequest(params) => { - self.handle_list_tools(request_id, params).await; + ClientRequest::ListToolsRequest(params) => { + self.handle_list_tools(request_id, params.params).await; } - McpClientRequest::CallToolRequest(params) => { - self.handle_call_tool(request_id, params).await; + ClientRequest::CallToolRequest(params) => { + self.handle_call_tool(request_id, params.params).await; } - McpClientRequest::SetLevelRequest(params) => { - self.handle_set_level(params); + ClientRequest::SetLevelRequest(params) => { + self.handle_set_level(params.params); } - McpClientRequest::CompleteRequest(params) => { - self.handle_complete(params); + ClientRequest::CompleteRequest(params) => { + self.handle_complete(params.params); + } + ClientRequest::CustomRequest(custom) => { + let method = custom.method.clone(); + self.outgoing + .send_error( + request_id, + ErrorData::new( + ErrorCode::METHOD_NOT_FOUND, + format!("method not found: {method}"), + Some(json!({ "method": method })), + ), + ) + .await; } } } - /// Handle a standalone JSON-RPC response originating from the peer. - pub(crate) async fn process_response(&mut self, response: JSONRPCResponse) { + pub(crate) async fn process_response(&mut self, response: JsonRpcResponse) { tracing::info!("<- response: {:?}", response); - let JSONRPCResponse { id, result, .. } = response; + let JsonRpcResponse { id, result, .. } = response; self.outgoing.notify_client_response(id, result).await } - /// Handle a fire-and-forget JSON-RPC notification. - pub(crate) async fn process_notification(&mut self, notification: JSONRPCNotification) { - let server_notification = match ServerNotification::try_from(notification) { - Ok(n) => n, - Err(e) => { - tracing::warn!("Failed to convert notification: {e}"); - return; + pub(crate) async fn process_notification( + &mut self, + notification: JsonRpcNotification, + ) { + match notification.notification { + ClientNotification::CancelledNotification(params) => { + self.handle_cancelled_notification(params.params).await; } - }; - - // Similar to requests, route each notification type to its own stub - // handler so additional logic can be implemented incrementally. - match server_notification { - ServerNotification::CancelledNotification(params) => { - self.handle_cancelled_notification(params).await; + ClientNotification::ProgressNotification(params) => { + self.handle_progress_notification(params.params); } - ServerNotification::ProgressNotification(params) => { - self.handle_progress_notification(params); + ClientNotification::RootsListChangedNotification(_params) => { + self.handle_roots_list_changed(); } - ServerNotification::ResourceListChangedNotification(params) => { - self.handle_resource_list_changed(params); + ClientNotification::InitializedNotification(_) => { + self.handle_initialized_notification(); } - ServerNotification::ResourceUpdatedNotification(params) => { - self.handle_resource_updated(params); - } - ServerNotification::PromptListChangedNotification(params) => { - self.handle_prompt_list_changed(params); - } - ServerNotification::ToolListChangedNotification(params) => { - self.handle_tool_list_changed(params); - } - ServerNotification::LoggingMessageNotification(params) => { - self.handle_logging_message(params); + ClientNotification::CustomNotification(_) => { + tracing::warn!("ignoring custom client notification"); } } } - /// Handle an error object received from the peer. - pub(crate) fn process_error(&mut self, err: JSONRPCError) { + pub(crate) fn process_error(&mut self, err: JsonRpcError) { tracing::error!("<- error: {:?}", err); } async fn handle_initialize( &mut self, id: RequestId, - params: ::Params, + params: rmcp::model::InitializeRequestParam, ) { tracing::info!("initialize -> params: {:?}", params); if self.initialized { - // Already initialised: send JSON-RPC error response. - let error = JSONRPCErrorError { - code: INVALID_REQUEST_ERROR_CODE, - message: "initialize called more than once".to_string(), - data: None, - }; - self.outgoing.send_error(id, error).await; + self.outgoing + .send_error( + id, + ErrorData::invalid_request("initialize called more than once", None), + ) + .await; return; } @@ -203,109 +189,109 @@ impl MessageProcessor { *suffix = Some(user_agent_suffix); } - self.initialized = true; + let server_info = Implementation { + name: "codex-mcp-server".to_string(), + title: Some("Codex".to_string()), + version: env!("CARGO_PKG_VERSION").to_string(), + icons: None, + website_url: None, + }; - // Build a minimal InitializeResult. Fill with placeholders. - let result = mcp_types::InitializeResult { - capabilities: mcp_types::ServerCapabilities { - completions: None, - experimental: None, - logging: None, - prompts: None, - resources: None, - tools: Some(ServerCapabilitiesTools { + // Preserve Codex's existing non-spec `serverInfo.user_agent` field. + let mut server_info_value = match serde_json::to_value(&server_info) { + Ok(value) => value, + Err(err) => { + self.outgoing + .send_error( + id, + ErrorData::internal_error( + format!("failed to serialize server info: {err}"), + None, + ), + ) + .await; + return; + } + }; + if let serde_json::Value::Object(ref mut obj) = server_info_value { + obj.insert("user_agent".to_string(), json!(get_codex_user_agent())); + } + + let mut result_value = match serde_json::to_value(InitializeResult { + capabilities: ServerCapabilities { + tools: Some(ToolsCapability { list_changed: Some(true), }), + ..Default::default() }, instructions: None, protocol_version: params.protocol_version.clone(), - server_info: mcp_types::Implementation { - name: "codex-mcp-server".to_string(), - version: env!("CARGO_PKG_VERSION").to_string(), - title: Some("Codex".to_string()), - user_agent: Some(get_codex_user_agent()), - }, + server_info, + }) { + Ok(value) => value, + Err(err) => { + self.outgoing + .send_error( + id, + ErrorData::internal_error( + format!("failed to serialize initialize response: {err}"), + None, + ), + ) + .await; + return; + } }; - self.send_response::(id, result) - .await; + if let serde_json::Value::Object(ref mut obj) = result_value { + obj.insert("serverInfo".to_string(), server_info_value); + } + + self.initialized = true; + self.outgoing.send_response(id, result_value).await; } - async fn send_response(&self, id: RequestId, result: T::Result) - where - T: ModelContextProtocolRequest, - { - self.outgoing.send_response(id, result).await; + async fn handle_ping(&self, id: RequestId) { + tracing::info!("ping"); + self.outgoing.send_response(id, json!({})).await; } - async fn handle_ping( - &self, - id: RequestId, - params: ::Params, - ) { - tracing::info!("ping -> params: {:?}", params); - let result = json!({}); - self.send_response::(id, result) - .await; - } - - fn handle_list_resources( - &self, - params: ::Params, - ) { + fn handle_list_resources(&self, params: Option) { tracing::info!("resources/list -> params: {:?}", params); } - fn handle_list_resource_templates( - &self, - params: - ::Params, - ) { + fn handle_list_resource_templates(&self, params: Option) { tracing::info!("resources/templates/list -> params: {:?}", params); } - fn handle_read_resource( - &self, - params: ::Params, - ) { + fn handle_read_resource(&self, params: rmcp::model::ReadResourceRequestParam) { tracing::info!("resources/read -> params: {:?}", params); } - fn handle_subscribe( - &self, - params: ::Params, - ) { + fn handle_subscribe(&self, params: rmcp::model::SubscribeRequestParam) { tracing::info!("resources/subscribe -> params: {:?}", params); } - fn handle_unsubscribe( - &self, - params: ::Params, - ) { + fn handle_unsubscribe(&self, params: rmcp::model::UnsubscribeRequestParam) { tracing::info!("resources/unsubscribe -> params: {:?}", params); } - fn handle_list_prompts( - &self, - params: ::Params, - ) { + fn handle_list_prompts(&self, params: Option) { tracing::info!("prompts/list -> params: {:?}", params); } - fn handle_get_prompt( - &self, - params: ::Params, - ) { + fn handle_get_prompt(&self, params: rmcp::model::GetPromptRequestParam) { tracing::info!("prompts/get -> params: {:?}", params); } async fn handle_list_tools( &self, id: RequestId, - params: ::Params, + params: Option, ) { tracing::trace!("tools/list -> {params:?}"); - let result = ListToolsResult { + let result = rmcp::model::ListToolsResult { + meta: None, tools: vec![ create_tool_for_codex_tool_call_param(), create_tool_for_codex_tool_call_reply_param(), @@ -313,19 +299,14 @@ impl MessageProcessor { next_cursor: None, }; - self.send_response::(id, result) - .await; + self.outgoing.send_response(id, result).await; } - async fn handle_call_tool( - &self, - id: RequestId, - params: ::Params, - ) { + async fn handle_call_tool(&self, id: RequestId, params: CallToolRequestParam) { tracing::info!("tools/call -> params: {:?}", params); - let CallToolRequestParams { name, arguments } = params; + let CallToolRequestParam { name, arguments } = params; - match name.as_str() { + match name.as_ref() { "codex" => self.handle_tool_call_codex(id, arguments).await, "codex-reply" => { self.handle_tool_call_codex_session_reply(id, arguments) @@ -333,20 +314,22 @@ impl MessageProcessor { } _ => { let result = CallToolResult { - content: vec![ContentBlock::TextContent(TextContent { - r#type: "text".to_string(), - text: format!("Unknown tool '{name}'"), - annotations: None, - })], - is_error: Some(true), + content: vec![rmcp::model::Content::text(format!("Unknown tool '{name}'"))], structured_content: None, + is_error: Some(true), + meta: None, }; - self.send_response::(id, result) - .await; + self.outgoing.send_response(id, result).await; } } } - async fn handle_tool_call_codex(&self, id: RequestId, arguments: Option) { + + async fn handle_tool_call_codex( + &self, + id: RequestId, + arguments: Option, + ) { + let arguments = arguments.map(serde_json::Value::Object); let (initial_prompt, config): (String, Config) = match arguments { Some(json_val) => match serde_json::from_value::(json_val) { Ok(tool_cfg) => match tool_cfg @@ -356,50 +339,40 @@ impl MessageProcessor { Ok(cfg) => cfg, Err(e) => { let result = CallToolResult { - content: vec![ContentBlock::TextContent(TextContent { - r#type: "text".to_owned(), - text: format!( - "Failed to load Codex configuration from overrides: {e}" - ), - annotations: None, - })], - is_error: Some(true), + content: vec![rmcp::model::Content::text(format!( + "Failed to load Codex configuration from overrides: {e}" + ))], structured_content: None, + is_error: Some(true), + meta: None, }; - self.send_response::(id, result) - .await; + self.outgoing.send_response(id, result).await; return; } }, Err(e) => { let result = CallToolResult { - content: vec![ContentBlock::TextContent(TextContent { - r#type: "text".to_owned(), - text: format!("Failed to parse configuration for Codex tool: {e}"), - annotations: None, - })], - is_error: Some(true), + content: vec![rmcp::model::Content::text(format!( + "Failed to parse configuration for Codex tool: {e}" + ))], structured_content: None, + is_error: Some(true), + meta: None, }; - self.send_response::(id, result) - .await; + self.outgoing.send_response(id, result).await; return; } }, None => { let result = CallToolResult { - content: vec![ContentBlock::TextContent(TextContent { - r#type: "text".to_string(), - text: - "Missing arguments for codex tool-call; the `prompt` field is required." - .to_string(), - annotations: None, - })], - is_error: Some(true), + content: vec![rmcp::model::Content::text( + "Missing arguments for codex tool-call; the `prompt` field is required.", + )], structured_content: None, + is_error: Some(true), + meta: None, }; - self.send_response::(id, result) - .await; + self.outgoing.send_response(id, result).await; return; } }; @@ -428,8 +401,9 @@ impl MessageProcessor { async fn handle_tool_call_codex_session_reply( &self, request_id: RequestId, - arguments: Option, + arguments: Option, ) { + let arguments = arguments.map(serde_json::Value::Object); tracing::info!("tools/call -> params: {:?}", arguments); // parse arguments @@ -439,16 +413,14 @@ impl MessageProcessor { Err(e) => { tracing::error!("Failed to parse Codex tool call reply parameters: {e}"); let result = CallToolResult { - content: vec![ContentBlock::TextContent(TextContent { - r#type: "text".to_owned(), - text: format!("Failed to parse configuration for Codex tool: {e}"), - annotations: None, - })], - is_error: Some(true), + content: vec![rmcp::model::Content::text(format!( + "Failed to parse configuration for Codex tool: {e}" + ))], structured_content: None, + is_error: Some(true), + meta: None, }; - self.send_response::(request_id, result) - .await; + self.outgoing.send_response(request_id, result).await; return; } }, @@ -457,16 +429,14 @@ impl MessageProcessor { "Missing arguments for codex-reply tool-call; the `thread_id` and `prompt` fields are required." ); let result = CallToolResult { - content: vec![ContentBlock::TextContent(TextContent { - r#type: "text".to_owned(), - text: "Missing arguments for codex-reply tool-call; the `thread_id` and `prompt` fields are required.".to_owned(), - annotations: None, - })], - is_error: Some(true), + content: vec![rmcp::model::Content::text( + "Missing arguments for codex-reply tool-call; the `thread_id` and `prompt` fields are required.", + )], structured_content: None, + is_error: Some(true), + meta: None, }; - self.send_response::(request_id, result) - .await; + self.outgoing.send_response(request_id, result).await; return; } }; @@ -476,16 +446,14 @@ impl MessageProcessor { Err(e) => { tracing::error!("Failed to parse thread_id: {e}"); let result = CallToolResult { - content: vec![ContentBlock::TextContent(TextContent { - r#type: "text".to_owned(), - text: format!("Failed to parse thread_id: {e}"), - annotations: None, - })], - is_error: Some(true), + content: vec![rmcp::model::Content::text(format!( + "Failed to parse thread_id: {e}" + ))], structured_content: None, + is_error: Some(true), + meta: None, }; - self.send_response::(request_id, result) - .await; + self.outgoing.send_response(request_id, result).await; return; } }; @@ -528,17 +496,11 @@ impl MessageProcessor { }); } - fn handle_set_level( - &self, - params: ::Params, - ) { + fn handle_set_level(&self, params: rmcp::model::SetLevelRequestParam) { tracing::info!("logging/setLevel -> params: {:?}", params); } - fn handle_complete( - &self, - params: ::Params, - ) { + fn handle_complete(&self, params: rmcp::model::CompleteRequestParam) { tracing::info!("completion/complete -> params: {:?}", params); } @@ -546,16 +508,10 @@ impl MessageProcessor { // Notification handlers // --------------------------------------------------------------------- - async fn handle_cancelled_notification( - &self, - params: ::Params, - ) { + async fn handle_cancelled_notification(&self, params: rmcp::model::CancelledNotificationParam) { let request_id = params.request_id; // Create a stable string form early for logging and submission id. - let request_id_string = match &request_id { - RequestId::String(s) => s.clone(), - RequestId::Integer(i) => i.to_string(), - }; + let request_id_string = request_id.to_string(); // Obtain the thread id while holding the first lock, then release. let thread_id = { @@ -563,7 +519,7 @@ impl MessageProcessor { match map_guard.get(&request_id) { Some(id) => *id, None => { - tracing::warn!("Session not found for request_id: {}", request_id_string); + tracing::warn!("Session not found for request_id: {request_id_string}"); return; } } @@ -580,13 +536,13 @@ impl MessageProcessor { }; // Submit interrupt to Codex. - let err = codex_arc + if let Err(e) = codex_arc .submit_with_id(Submission { id: request_id_string, op: codex_core::protocol::Op::Interrupt, }) - .await; - if let Err(e) = err { + .await + { tracing::error!("Failed to submit interrupt to Codex: {e}"); return; } @@ -597,48 +553,15 @@ impl MessageProcessor { .remove(&request_id); } - fn handle_progress_notification( - &self, - params: ::Params, - ) { + fn handle_progress_notification(&self, params: rmcp::model::ProgressNotificationParam) { tracing::info!("notifications/progress -> params: {:?}", params); } - fn handle_resource_list_changed( - &self, - params: ::Params, - ) { - tracing::info!( - "notifications/resources/list_changed -> params: {:?}", - params - ); + fn handle_roots_list_changed(&self) { + tracing::info!("notifications/roots/list_changed"); } - fn handle_resource_updated( - &self, - params: ::Params, - ) { - tracing::info!("notifications/resources/updated -> params: {:?}", params); - } - - fn handle_prompt_list_changed( - &self, - params: ::Params, - ) { - tracing::info!("notifications/prompts/list_changed -> params: {:?}", params); - } - - fn handle_tool_list_changed( - &self, - params: ::Params, - ) { - tracing::info!("notifications/tools/list_changed -> params: {:?}", params); - } - - fn handle_logging_message( - &self, - params: ::Params, - ) { - tracing::info!("notifications/message -> params: {:?}", params); + fn handle_initialized_notification(&self) { + tracing::info!("notifications/initialized"); } } diff --git a/codex-rs/mcp-server/src/outgoing_message.rs b/codex-rs/mcp-server/src/outgoing_message.rs index 954e50d9de..e512eedbd7 100644 --- a/codex-rs/mcp-server/src/outgoing_message.rs +++ b/codex-rs/mcp-server/src/outgoing_message.rs @@ -4,28 +4,30 @@ use std::sync::atomic::Ordering; use codex_core::protocol::Event; use codex_protocol::ThreadId; -use mcp_types::JSONRPC_VERSION; -use mcp_types::JSONRPCError; -use mcp_types::JSONRPCErrorError; -use mcp_types::JSONRPCMessage; -use mcp_types::JSONRPCNotification; -use mcp_types::JSONRPCRequest; -use mcp_types::JSONRPCResponse; -use mcp_types::RequestId; -use mcp_types::Result; +use rmcp::model::CustomNotification; +use rmcp::model::CustomRequest; +use rmcp::model::ErrorData; +use rmcp::model::JsonRpcError; +use rmcp::model::JsonRpcMessage; +use rmcp::model::JsonRpcNotification; +use rmcp::model::JsonRpcRequest; +use rmcp::model::JsonRpcResponse; +use rmcp::model::JsonRpcVersion2_0; +use rmcp::model::RequestId; use serde::Serialize; +use serde_json::Value; use tokio::sync::Mutex; use tokio::sync::mpsc; use tokio::sync::oneshot; use tracing::warn; -use crate::error_code::INTERNAL_ERROR_CODE; +pub(crate) type OutgoingJsonRpcMessage = JsonRpcMessage; /// Sends messages to the client and manages request callbacks. pub(crate) struct OutgoingMessageSender { next_request_id: AtomicI64, sender: mpsc::UnboundedSender, - request_id_to_callback: Mutex>>, + request_id_to_callback: Mutex>>, } impl OutgoingMessageSender { @@ -41,8 +43,8 @@ impl OutgoingMessageSender { &self, method: &str, params: Option, - ) -> oneshot::Receiver { - let id = RequestId::Integer(self.next_request_id.fetch_add(1, Ordering::Relaxed)); + ) -> oneshot::Receiver { + let id = RequestId::Number(self.next_request_id.fetch_add(1, Ordering::Relaxed)); let outgoing_message_id = id.clone(); let (tx_approve, rx_approve) = oneshot::channel(); { @@ -59,7 +61,7 @@ impl OutgoingMessageSender { rx_approve } - pub(crate) async fn notify_client_response(&self, id: RequestId, result: Result) { + pub(crate) async fn notify_client_response(&self, id: RequestId, result: Value) { let entry = { let mut request_id_to_callback = self.request_id_to_callback.lock().await; request_id_to_callback.remove_entry(&id) @@ -78,23 +80,20 @@ impl OutgoingMessageSender { } pub(crate) async fn send_response(&self, id: RequestId, response: T) { - match serde_json::to_value(response) { - Ok(result) => { - let outgoing_message = OutgoingMessage::Response(OutgoingResponse { id, result }); - let _ = self.sender.send(outgoing_message); - } + let result = match serde_json::to_value(response) { + Ok(result) => result, Err(err) => { self.send_error( id, - JSONRPCErrorError { - code: INTERNAL_ERROR_CODE, - message: format!("failed to serialize response: {err}"), - data: None, - }, + ErrorData::internal_error(format!("failed to serialize response: {err}"), None), ) .await; + return; } - } + }; + + let outgoing_message = OutgoingMessage::Response(OutgoingResponse { id, result }); + let _ = self.sender.send(outgoing_message); } /// This is used with the MCP server, but not the more general JSON-RPC app @@ -130,7 +129,7 @@ impl OutgoingMessageSender { let _ = self.sender.send(outgoing_message); } - pub(crate) async fn send_error(&self, id: RequestId, error: JSONRPCErrorError) { + pub(crate) async fn send_error(&self, id: RequestId, error: ErrorData) { let outgoing_message = OutgoingMessage::Error(OutgoingError { id, error }); let _ = self.sender.send(outgoing_message); } @@ -144,34 +143,32 @@ pub(crate) enum OutgoingMessage { Error(OutgoingError), } -impl From for JSONRPCMessage { +impl From for OutgoingJsonRpcMessage { fn from(val: OutgoingMessage) -> Self { use OutgoingMessage::*; match val { Request(OutgoingRequest { id, method, params }) => { - JSONRPCMessage::Request(JSONRPCRequest { - jsonrpc: JSONRPC_VERSION.into(), + JsonRpcMessage::Request(JsonRpcRequest { + jsonrpc: JsonRpcVersion2_0, id, - method, - params, + request: CustomRequest::new(method, params), }) } Notification(OutgoingNotification { method, params }) => { - JSONRPCMessage::Notification(JSONRPCNotification { - jsonrpc: JSONRPC_VERSION.into(), - method, - params, + JsonRpcMessage::Notification(JsonRpcNotification { + jsonrpc: JsonRpcVersion2_0, + notification: CustomNotification::new(method, params), }) } Response(OutgoingResponse { id, result }) => { - JSONRPCMessage::Response(JSONRPCResponse { - jsonrpc: JSONRPC_VERSION.into(), + JsonRpcMessage::Response(JsonRpcResponse { + jsonrpc: JsonRpcVersion2_0, id, result, }) } - Error(OutgoingError { id, error }) => JSONRPCMessage::Error(JSONRPCError { - jsonrpc: JSONRPC_VERSION.into(), + Error(OutgoingError { id, error }) => JsonRpcMessage::Error(JsonRpcError { + jsonrpc: JsonRpcVersion2_0, id, error, }), @@ -220,12 +217,12 @@ pub(crate) struct OutgoingNotificationMeta { #[derive(Debug, Clone, PartialEq, Serialize)] pub(crate) struct OutgoingResponse { pub id: RequestId, - pub result: Result, + pub result: Value, } #[derive(Debug, Clone, PartialEq, Serialize)] pub(crate) struct OutgoingError { - pub error: JSONRPCErrorError, + pub error: ErrorData, pub id: RequestId, } @@ -246,6 +243,48 @@ mod tests { use super::*; + #[test] + fn outgoing_request_serializes_as_jsonrpc_request() { + let msg: OutgoingJsonRpcMessage = OutgoingMessage::Request(OutgoingRequest { + id: RequestId::Number(1), + method: "elicitation/create".to_string(), + params: Some(json!({ "k": "v" })), + }) + .into(); + + let value = serde_json::to_value(msg).expect("message should serialize"); + let obj = value.as_object().expect("json object"); + + assert_eq!(obj.get("jsonrpc"), Some(&json!("2.0"))); + assert_eq!(obj.get("id"), Some(&json!(1))); + assert_eq!(obj.get("method"), Some(&json!("elicitation/create"))); + assert_eq!(obj.get("params"), Some(&json!({ "k": "v" }))); + assert!( + obj.get("request").is_none(), + "rmcp request must flatten to JSON-RPC method/params" + ); + } + + #[test] + fn outgoing_notification_serializes_as_jsonrpc_notification() { + let msg: OutgoingJsonRpcMessage = OutgoingMessage::Notification(OutgoingNotification { + method: "notifications/initialized".to_string(), + params: None, + }) + .into(); + + let value = serde_json::to_value(msg).expect("message should serialize"); + let obj = value.as_object().expect("json object"); + + assert_eq!(obj.get("jsonrpc"), Some(&json!("2.0"))); + assert_eq!(obj.get("method"), Some(&json!("notifications/initialized"))); + assert_eq!(obj.get("params"), Some(&serde_json::Value::Null)); + assert!( + obj.get("notification").is_none(), + "rmcp notification must flatten to JSON-RPC method/params" + ); + } + #[tokio::test] async fn test_send_event_as_notification() -> Result<()> { let (outgoing_tx, mut outgoing_rx) = mpsc::unbounded_channel::(); @@ -316,7 +355,7 @@ mod tests { msg: EventMsg::SessionConfigured(session_configured_event.clone()), }; let meta = OutgoingNotificationMeta { - request_id: Some(RequestId::String("123".to_string())), + request_id: Some(RequestId::String("123".into())), thread_id: None, }; @@ -381,7 +420,7 @@ mod tests { msg: EventMsg::SessionConfigured(session_configured_event.clone()), }; let meta = OutgoingNotificationMeta { - request_id: Some(RequestId::String("123".to_string())), + request_id: Some(RequestId::String("123".into())), thread_id: Some(thread_id), }; diff --git a/codex-rs/mcp-server/src/patch_approval.rs b/codex-rs/mcp-server/src/patch_approval.rs index 5c3073959d..55938e257b 100644 --- a/codex-rs/mcp-server/src/patch_approval.rs +++ b/codex-rs/mcp-server/src/patch_approval.rs @@ -7,24 +7,21 @@ use codex_core::protocol::FileChange; use codex_core::protocol::Op; use codex_core::protocol::ReviewDecision; use codex_protocol::ThreadId; -use mcp_types::ElicitRequest; -use mcp_types::ElicitRequestParamsRequestedSchema; -use mcp_types::JSONRPCErrorError; -use mcp_types::ModelContextProtocolRequest; -use mcp_types::RequestId; +use rmcp::model::ErrorData; +use rmcp::model::RequestId; use serde::Deserialize; use serde::Serialize; +use serde_json::Value; use serde_json::json; use tracing::error; -use crate::codex_tool_runner::INVALID_PARAMS_ERROR_CODE; use crate::outgoing_message::OutgoingMessageSender; #[derive(Debug, Deserialize, Serialize)] pub struct PatchApprovalElicitRequestParams { pub message: String, #[serde(rename = "requestedSchema")] - pub requested_schema: ElicitRequestParamsRequestedSchema, + pub requested_schema: Value, #[serde(rename = "threadId")] pub thread_id: ThreadId, pub codex_elicitation: String, @@ -64,11 +61,7 @@ pub(crate) async fn handle_patch_approval_request( let params = PatchApprovalElicitRequestParams { message: message_lines.join("\n"), - requested_schema: ElicitRequestParamsRequestedSchema { - r#type: "object".to_string(), - properties: json!({}), - required: None, - }, + requested_schema: json!({"type":"object","properties":{}}), thread_id, codex_elicitation: "patch-approval".to_string(), codex_mcp_tool_call_id: tool_call_id.clone(), @@ -85,14 +78,7 @@ pub(crate) async fn handle_patch_approval_request( error!("{message}"); outgoing - .send_error( - request_id.clone(), - JSONRPCErrorError { - code: INVALID_PARAMS_ERROR_CODE, - message, - data: None, - }, - ) + .send_error(request_id.clone(), ErrorData::invalid_params(message, None)) .await; return; @@ -100,7 +86,7 @@ pub(crate) async fn handle_patch_approval_request( }; let on_response = outgoing - .send_request(ElicitRequest::METHOD, Some(params_json)) + .send_request("elicitation/create", Some(params_json)) .await; // Listen for the response on a separate task so we don't block the main agent loop. @@ -115,7 +101,7 @@ pub(crate) async fn handle_patch_approval_request( pub(crate) async fn on_patch_approval_response( event_id: String, - receiver: tokio::sync::oneshot::Receiver, + receiver: tokio::sync::oneshot::Receiver, codex: Arc, ) { let response = receiver.await; diff --git a/codex-rs/mcp-server/tests/common/Cargo.toml b/codex-rs/mcp-server/tests/common/Cargo.toml index aba984edab..1dec2d09ac 100644 --- a/codex-rs/mcp-server/tests/common/Cargo.toml +++ b/codex-rs/mcp-server/tests/common/Cargo.toml @@ -12,7 +12,7 @@ anyhow = { workspace = true } codex-core = { workspace = true } codex-mcp-server = { workspace = true } codex-utils-cargo-bin = { workspace = true } -mcp-types = { workspace = true } +rmcp = { workspace = true } os_info = { workspace = true } pretty_assertions = { workspace = true } serde = { workspace = true } diff --git a/codex-rs/mcp-server/tests/common/lib.rs b/codex-rs/mcp-server/tests/common/lib.rs index 364c708651..c2c3757efc 100644 --- a/codex-rs/mcp-server/tests/common/lib.rs +++ b/codex-rs/mcp-server/tests/common/lib.rs @@ -6,14 +6,16 @@ pub use core_test_support::format_with_current_shell; pub use core_test_support::format_with_current_shell_display_non_login; pub use core_test_support::format_with_current_shell_non_login; pub use mcp_process::McpProcess; -use mcp_types::JSONRPCResponse; pub use mock_model_server::create_mock_chat_completions_server; pub use responses::create_apply_patch_sse_response; pub use responses::create_final_assistant_message_sse_response; pub use responses::create_shell_command_sse_response; +use rmcp::model::JsonRpcResponse; use serde::de::DeserializeOwned; -pub fn to_response(response: JSONRPCResponse) -> anyhow::Result { +pub fn to_response( + response: JsonRpcResponse, +) -> anyhow::Result { let value = serde_json::to_value(response.result)?; let codex_response = serde_json::from_value(value)?; Ok(codex_response) diff --git a/codex-rs/mcp-server/tests/common/mcp_process.rs b/codex-rs/mcp-server/tests/common/mcp_process.rs index 9a3f076fb1..c019120282 100644 --- a/codex-rs/mcp-server/tests/common/mcp_process.rs +++ b/codex-rs/mcp-server/tests/common/mcp_process.rs @@ -12,19 +12,21 @@ use tokio::process::ChildStdout; use anyhow::Context; use codex_mcp_server::CodexToolCallParam; -use mcp_types::CallToolRequestParams; -use mcp_types::ClientCapabilities; -use mcp_types::Implementation; -use mcp_types::InitializeRequestParams; -use mcp_types::JSONRPC_VERSION; -use mcp_types::JSONRPCMessage; -use mcp_types::JSONRPCNotification; -use mcp_types::JSONRPCRequest; -use mcp_types::JSONRPCResponse; -use mcp_types::ModelContextProtocolNotification; -use mcp_types::ModelContextProtocolRequest; -use mcp_types::RequestId; use pretty_assertions::assert_eq; +use rmcp::model::CallToolRequestParam; +use rmcp::model::ClientCapabilities; +use rmcp::model::CustomNotification; +use rmcp::model::CustomRequest; +use rmcp::model::ElicitationCapability; +use rmcp::model::Implementation; +use rmcp::model::InitializeRequestParam; +use rmcp::model::JsonRpcMessage; +use rmcp::model::JsonRpcNotification; +use rmcp::model::JsonRpcRequest; +use rmcp::model::JsonRpcResponse; +use rmcp::model::JsonRpcVersion2_0; +use rmcp::model::ProtocolVersion; +use rmcp::model::RequestId; use serde_json::json; use tokio::process::Command; @@ -110,9 +112,11 @@ impl McpProcess { pub async fn initialize(&mut self) -> anyhow::Result<()> { let request_id = self.next_request_id.fetch_add(1, Ordering::Relaxed); - let params = InitializeRequestParams { + let params = InitializeRequestParam { capabilities: ClientCapabilities { - elicitation: Some(json!({})), + elicitation: Some(ElicitationCapability { + schema_validation: None, + }), experimental: None, roots: None, sampling: None, @@ -121,17 +125,17 @@ impl McpProcess { name: "elicitation test".into(), title: Some("Elicitation Test".into()), version: "0.0.0".into(), - user_agent: None, + icons: None, + website_url: None, }, - protocol_version: mcp_types::MCP_SCHEMA_VERSION.into(), + protocol_version: ProtocolVersion::V_2025_03_26, }; let params_value = serde_json::to_value(params)?; - self.send_jsonrpc_message(JSONRPCMessage::Request(JSONRPCRequest { - jsonrpc: JSONRPC_VERSION.into(), - id: RequestId::Integer(request_id), - method: mcp_types::InitializeRequest::METHOD.into(), - params: Some(params_value), + self.send_jsonrpc_message(JsonRpcMessage::Request(JsonRpcRequest { + jsonrpc: JsonRpcVersion2_0, + id: RequestId::Number(request_id), + request: CustomRequest::new("initialize", Some(params_value)), })) .await?; @@ -146,33 +150,38 @@ impl McpProcess { os_info.architecture().unwrap_or("unknown"), codex_core::terminal::user_agent() ); + let JsonRpcMessage::Response(JsonRpcResponse { + jsonrpc, + id, + result, + }) = initialized + else { + anyhow::bail!("expected initialize response message, got: {initialized:?}") + }; + assert_eq!(jsonrpc, JsonRpcVersion2_0); + assert_eq!(id, RequestId::Number(request_id)); assert_eq!( - JSONRPCMessage::Response(JSONRPCResponse { - jsonrpc: JSONRPC_VERSION.into(), - id: RequestId::Integer(request_id), - result: json!({ - "capabilities": { - "tools": { - "listChanged": true - }, + result, + json!({ + "capabilities": { + "tools": { + "listChanged": true }, - "serverInfo": { - "name": "codex-mcp-server", - "title": "Codex", - "version": "0.0.0", - "user_agent": user_agent - }, - "protocolVersion": mcp_types::MCP_SCHEMA_VERSION - }) - }), - initialized + }, + "serverInfo": { + "name": "codex-mcp-server", + "title": "Codex", + "version": "0.0.0", + "user_agent": user_agent + }, + "protocolVersion": ProtocolVersion::V_2025_03_26 + }) ); // Send notifications/initialized to ack the response. - self.send_jsonrpc_message(JSONRPCMessage::Notification(JSONRPCNotification { - jsonrpc: JSONRPC_VERSION.into(), - method: mcp_types::InitializedNotification::METHOD.into(), - params: None, + self.send_jsonrpc_message(JsonRpcMessage::Notification(JsonRpcNotification { + jsonrpc: JsonRpcVersion2_0, + notification: CustomNotification::new("notifications/initialized", None), })) .await?; @@ -185,12 +194,15 @@ impl McpProcess { &mut self, params: CodexToolCallParam, ) -> anyhow::Result { - let codex_tool_call_params = CallToolRequestParams { - name: "codex".to_string(), - arguments: Some(serde_json::to_value(params)?), + let codex_tool_call_params = CallToolRequestParam { + name: "codex".into(), + arguments: Some(match serde_json::to_value(params)? { + serde_json::Value::Object(map) => map, + _ => unreachable!("params serialize to object"), + }), }; self.send_request( - mcp_types::CallToolRequest::METHOD, + "tools/call", Some(serde_json::to_value(codex_tool_call_params)?), ) .await @@ -203,11 +215,10 @@ impl McpProcess { ) -> anyhow::Result { let request_id = self.next_request_id.fetch_add(1, Ordering::Relaxed); - let message = JSONRPCMessage::Request(JSONRPCRequest { - jsonrpc: JSONRPC_VERSION.into(), - id: RequestId::Integer(request_id), - method: method.to_string(), - params, + let message = JsonRpcMessage::Request(JsonRpcRequest { + jsonrpc: JsonRpcVersion2_0, + id: RequestId::Number(request_id), + request: CustomRequest::new(method, params), }); self.send_jsonrpc_message(message).await?; Ok(request_id) @@ -218,15 +229,18 @@ impl McpProcess { id: RequestId, result: serde_json::Value, ) -> anyhow::Result<()> { - self.send_jsonrpc_message(JSONRPCMessage::Response(JSONRPCResponse { - jsonrpc: JSONRPC_VERSION.into(), + self.send_jsonrpc_message(JsonRpcMessage::Response(JsonRpcResponse { + jsonrpc: JsonRpcVersion2_0, id, result, })) .await } - async fn send_jsonrpc_message(&mut self, message: JSONRPCMessage) -> anyhow::Result<()> { + async fn send_jsonrpc_message( + &mut self, + message: JsonRpcMessage, + ) -> anyhow::Result<()> { eprintln!("writing message to stdin: {message:?}"); let payload = serde_json::to_string(&message)?; self.stdin.write_all(payload.as_bytes()).await?; @@ -235,31 +249,37 @@ impl McpProcess { Ok(()) } - async fn read_jsonrpc_message(&mut self) -> anyhow::Result { + async fn read_jsonrpc_message( + &mut self, + ) -> anyhow::Result> { let mut line = String::new(); self.stdout.read_line(&mut line).await?; - let message = serde_json::from_str::(&line)?; + let message = serde_json::from_str::< + JsonRpcMessage, + >(&line)?; eprintln!("read message from stdout: {message:?}"); Ok(message) } - pub async fn read_stream_until_request_message(&mut self) -> anyhow::Result { + pub async fn read_stream_until_request_message( + &mut self, + ) -> anyhow::Result> { eprintln!("in read_stream_until_request_message()"); loop { let message = self.read_jsonrpc_message().await?; match message { - JSONRPCMessage::Notification(_) => { + JsonRpcMessage::Notification(_) => { eprintln!("notification: {message:?}"); } - JSONRPCMessage::Request(jsonrpc_request) => { + JsonRpcMessage::Request(jsonrpc_request) => { return Ok(jsonrpc_request); } - JSONRPCMessage::Error(_) => { + JsonRpcMessage::Error(_) => { anyhow::bail!("unexpected JSONRPCMessage::Error: {message:?}"); } - JSONRPCMessage::Response(_) => { + JsonRpcMessage::Response(_) => { anyhow::bail!("unexpected JSONRPCMessage::Response: {message:?}"); } } @@ -269,22 +289,22 @@ impl McpProcess { pub async fn read_stream_until_response_message( &mut self, request_id: RequestId, - ) -> anyhow::Result { + ) -> anyhow::Result> { eprintln!("in read_stream_until_response_message({request_id:?})"); loop { let message = self.read_jsonrpc_message().await?; match message { - JSONRPCMessage::Notification(_) => { + JsonRpcMessage::Notification(_) => { eprintln!("notification: {message:?}"); } - JSONRPCMessage::Request(_) => { + JsonRpcMessage::Request(_) => { anyhow::bail!("unexpected JSONRPCMessage::Request: {message:?}"); } - JSONRPCMessage::Error(_) => { + JsonRpcMessage::Error(_) => { anyhow::bail!("unexpected JSONRPCMessage::Error: {message:?}"); } - JSONRPCMessage::Response(jsonrpc_response) => { + JsonRpcMessage::Response(jsonrpc_response) => { if jsonrpc_response.id == request_id { return Ok(jsonrpc_response); } @@ -297,15 +317,15 @@ impl McpProcess { /// Method "codex/event" with params.msg.type == "task_complete". pub async fn read_stream_until_legacy_task_complete_notification( &mut self, - ) -> anyhow::Result { + ) -> anyhow::Result> { eprintln!("in read_stream_until_legacy_task_complete_notification()"); loop { let message = self.read_jsonrpc_message().await?; match message { - JSONRPCMessage::Notification(notification) => { - let is_match = if notification.method == "codex/event" { - if let Some(params) = ¬ification.params { + JsonRpcMessage::Notification(notification) => { + let is_match = if notification.notification.method == "codex/event" { + if let Some(params) = ¬ification.notification.params { params .get("msg") .and_then(|m| m.get("type")) @@ -324,13 +344,13 @@ impl McpProcess { eprintln!("ignoring notification: {notification:?}"); } } - JSONRPCMessage::Request(_) => { + JsonRpcMessage::Request(_) => { anyhow::bail!("unexpected JSONRPCMessage::Request: {message:?}"); } - JSONRPCMessage::Error(_) => { + JsonRpcMessage::Error(_) => { anyhow::bail!("unexpected JSONRPCMessage::Error: {message:?}"); } - JSONRPCMessage::Response(_) => { + JsonRpcMessage::Response(_) => { anyhow::bail!("unexpected JSONRPCMessage::Response: {message:?}"); } } diff --git a/codex-rs/mcp-server/tests/suite/codex_tool.rs b/codex-rs/mcp-server/tests/suite/codex_tool.rs index 31c451d24f..15836b9057 100644 --- a/codex-rs/mcp-server/tests/suite/codex_tool.rs +++ b/codex-rs/mcp-server/tests/suite/codex_tool.rs @@ -12,14 +12,10 @@ use codex_mcp_server::ExecApprovalElicitRequestParams; use codex_mcp_server::ExecApprovalResponse; use codex_mcp_server::PatchApprovalElicitRequestParams; use codex_mcp_server::PatchApprovalResponse; -use mcp_types::ElicitRequest; -use mcp_types::ElicitRequestParamsRequestedSchema; -use mcp_types::JSONRPC_VERSION; -use mcp_types::JSONRPCRequest; -use mcp_types::JSONRPCResponse; -use mcp_types::ModelContextProtocolRequest; -use mcp_types::RequestId; use pretty_assertions::assert_eq; +use rmcp::model::JsonRpcResponse; +use rmcp::model::JsonRpcVersion2_0; +use rmcp::model::RequestId; use serde_json::json; use tempfile::TempDir; use tokio::time::timeout; @@ -106,22 +102,27 @@ async fn shell_command_approval_triggers_elicitation() -> anyhow::Result<()> { ) .await??; + assert_eq!(elicitation_request.jsonrpc, JsonRpcVersion2_0); + assert_eq!(elicitation_request.request.method, "elicitation/create"); + let elicitation_request_id = elicitation_request.id.clone(); let params = serde_json::from_value::( elicitation_request + .request .params .clone() .ok_or_else(|| anyhow::anyhow!("elicitation_request.params must be set"))?, )?; - let expected_elicitation_request = create_expected_elicitation_request( - elicitation_request_id.clone(), - expected_shell_command, - workdir_for_shell_function_call.path(), - codex_request_id.to_string(), - params.codex_event_id.clone(), - params.thread_id, - )?; - assert_eq!(expected_elicitation_request, elicitation_request); + assert_eq!( + elicitation_request.request.params, + Some(create_expected_elicitation_request_params( + expected_shell_command, + workdir_for_shell_function_call.path(), + codex_request_id.to_string(), + params.codex_event_id.clone(), + params.thread_id, + )?) + ); // Accept the `git init` request by responding to the elicitation. mcp_process @@ -146,13 +147,13 @@ async fn shell_command_approval_triggers_elicitation() -> anyhow::Result<()> { // Verify the original `codex` tool call completes and that the file was created. let codex_response = timeout( DEFAULT_READ_TIMEOUT, - mcp_process.read_stream_until_response_message(RequestId::Integer(codex_request_id)), + mcp_process.read_stream_until_response_message(RequestId::Number(codex_request_id)), ) .await??; assert_eq!( - JSONRPCResponse { - jsonrpc: JSONRPC_VERSION.into(), - id: RequestId::Integer(codex_request_id), + JsonRpcResponse { + jsonrpc: JsonRpcVersion2_0, + id: RequestId::Number(codex_request_id), result: json!({ "content": [ { @@ -174,41 +175,32 @@ async fn shell_command_approval_triggers_elicitation() -> anyhow::Result<()> { Ok(()) } -fn create_expected_elicitation_request( - elicitation_request_id: RequestId, +fn create_expected_elicitation_request_params( command: Vec, workdir: &Path, codex_mcp_tool_call_id: String, codex_event_id: String, thread_id: codex_protocol::ThreadId, -) -> anyhow::Result { +) -> anyhow::Result { let expected_message = format!( "Allow Codex to run `{}` in `{}`?", shlex::try_join(command.iter().map(std::convert::AsRef::as_ref))?, workdir.to_string_lossy() ); let codex_parsed_cmd = parse_command::parse_command(&command); - Ok(JSONRPCRequest { - jsonrpc: JSONRPC_VERSION.into(), - id: elicitation_request_id, - method: ElicitRequest::METHOD.to_string(), - params: Some(serde_json::to_value(&ExecApprovalElicitRequestParams { - message: expected_message, - requested_schema: ElicitRequestParamsRequestedSchema { - r#type: "object".to_string(), - properties: json!({}), - required: None, - }, - thread_id, - codex_elicitation: "exec-approval".to_string(), - codex_mcp_tool_call_id, - codex_event_id, - codex_command: command, - codex_cwd: workdir.to_path_buf(), - codex_call_id: "call1234".to_string(), - codex_parsed_cmd, - })?), - }) + let params_json = serde_json::to_value(ExecApprovalElicitRequestParams { + message: expected_message, + requested_schema: json!({"type":"object","properties":{}}), + thread_id, + codex_elicitation: "exec-approval".to_string(), + codex_mcp_tool_call_id, + codex_event_id, + codex_command: command, + codex_cwd: workdir.to_path_buf(), + codex_call_id: "call1234".to_string(), + codex_parsed_cmd, + })?; + Ok(params_json) } /// Test that patch approval triggers an elicitation request to the MCP and that @@ -267,9 +259,13 @@ async fn patch_approval_triggers_elicitation() -> anyhow::Result<()> { ) .await??; + assert_eq!(elicitation_request.jsonrpc, JsonRpcVersion2_0); + assert_eq!(elicitation_request.request.method, "elicitation/create"); + let elicitation_request_id = elicitation_request.id.clone(); let params = serde_json::from_value::( elicitation_request + .request .params .clone() .ok_or_else(|| anyhow::anyhow!("elicitation_request.params must be set"))?, @@ -284,16 +280,17 @@ async fn patch_approval_triggers_elicitation() -> anyhow::Result<()> { }, ); - let expected_elicitation_request = create_expected_patch_approval_elicitation_request( - elicitation_request_id.clone(), - expected_changes, - None, // No grant_root expected - None, // No reason expected - codex_request_id.to_string(), - params.codex_event_id.clone(), - params.thread_id, - )?; - assert_eq!(expected_elicitation_request, elicitation_request); + assert_eq!( + elicitation_request.request.params, + Some(create_expected_patch_approval_elicitation_request_params( + expected_changes, + None, // No grant_root expected + None, // No reason expected + codex_request_id.to_string(), + params.codex_event_id.clone(), + params.thread_id, + )?) + ); // Accept the patch approval request by responding to the elicitation mcp_process @@ -308,13 +305,13 @@ async fn patch_approval_triggers_elicitation() -> anyhow::Result<()> { // Verify the original `codex` tool call completes let codex_response = timeout( DEFAULT_READ_TIMEOUT, - mcp_process.read_stream_until_response_message(RequestId::Integer(codex_request_id)), + mcp_process.read_stream_until_response_message(RequestId::Number(codex_request_id)), ) .await??; assert_eq!( - JSONRPCResponse { - jsonrpc: JSONRPC_VERSION.into(), - id: RequestId::Integer(codex_request_id), + JsonRpcResponse { + jsonrpc: JsonRpcVersion2_0, + id: RequestId::Number(codex_request_id), result: json!({ "content": [ { @@ -375,11 +372,11 @@ async fn codex_tool_passes_base_instructions() -> anyhow::Result<()> { let codex_response = timeout( DEFAULT_READ_TIMEOUT, - mcp_process.read_stream_until_response_message(RequestId::Integer(codex_request_id)), + mcp_process.read_stream_until_response_message(RequestId::Number(codex_request_id)), ) .await??; - assert_eq!(codex_response.jsonrpc, JSONRPC_VERSION); - assert_eq!(codex_response.id, RequestId::Integer(codex_request_id)); + assert_eq!(codex_response.jsonrpc, JsonRpcVersion2_0); + assert_eq!(codex_response.id, RequestId::Number(codex_request_id)); assert_eq!( codex_response.result, json!({ @@ -430,42 +427,33 @@ async fn codex_tool_passes_base_instructions() -> anyhow::Result<()> { Ok(()) } -fn create_expected_patch_approval_elicitation_request( - elicitation_request_id: RequestId, +fn create_expected_patch_approval_elicitation_request_params( changes: HashMap, grant_root: Option, reason: Option, codex_mcp_tool_call_id: String, codex_event_id: String, thread_id: codex_protocol::ThreadId, -) -> anyhow::Result { +) -> anyhow::Result { let mut message_lines = Vec::new(); if let Some(r) = &reason { message_lines.push(r.clone()); } message_lines.push("Allow Codex to apply proposed code changes?".to_string()); + let params_json = serde_json::to_value(PatchApprovalElicitRequestParams { + message: message_lines.join("\n"), + requested_schema: json!({"type":"object","properties":{}}), + thread_id, + codex_elicitation: "patch-approval".to_string(), + codex_mcp_tool_call_id, + codex_event_id, + codex_reason: reason, + codex_grant_root: grant_root, + codex_changes: changes, + codex_call_id: "call1234".to_string(), + })?; - Ok(JSONRPCRequest { - jsonrpc: JSONRPC_VERSION.into(), - id: elicitation_request_id, - method: ElicitRequest::METHOD.to_string(), - params: Some(serde_json::to_value(&PatchApprovalElicitRequestParams { - message: message_lines.join("\n"), - requested_schema: ElicitRequestParamsRequestedSchema { - r#type: "object".to_string(), - properties: json!({}), - required: None, - }, - thread_id, - codex_elicitation: "patch-approval".to_string(), - codex_mcp_tool_call_id, - codex_event_id, - codex_reason: reason, - codex_grant_root: grant_root, - codex_changes: changes, - codex_call_id: "call1234".to_string(), - })?), - }) + Ok(params_json) } /// This handle is used to ensure that the MockServer and TempDir are not dropped while diff --git a/codex-rs/protocol/Cargo.toml b/codex-rs/protocol/Cargo.toml index c3d9d30915..b58393936b 100644 --- a/codex-rs/protocol/Cargo.toml +++ b/codex-rs/protocol/Cargo.toml @@ -19,7 +19,6 @@ codex-utils-image = { workspace = true } icu_decimal = { workspace = true } icu_locale_core = { workspace = true } icu_provider = { workspace = true, features = ["sync"] } -mcp-types = { workspace = true } mime_guess = { workspace = true } schemars = { workspace = true } serde = { workspace = true, features = ["derive"] } diff --git a/codex-rs/protocol/src/approvals.rs b/codex-rs/protocol/src/approvals.rs index 78050dfa86..635cd5223d 100644 --- a/codex-rs/protocol/src/approvals.rs +++ b/codex-rs/protocol/src/approvals.rs @@ -1,9 +1,9 @@ use std::collections::HashMap; use std::path::PathBuf; +use crate::mcp::RequestId; use crate::parse_command::ParsedCommand; use crate::protocol::FileChange; -use mcp_types::RequestId; use schemars::JsonSchema; use serde::Deserialize; use serde::Serialize; @@ -62,6 +62,7 @@ pub struct ExecApprovalRequestEvent { #[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)] pub struct ElicitationRequestEvent { pub server_name: String, + #[ts(type = "string | number")] pub id: RequestId, pub message: String, // TODO: MCP servers can request we fill out a schema for the elicitation. We don't support diff --git a/codex-rs/protocol/src/mcp.rs b/codex-rs/protocol/src/mcp.rs index 88a1f989c1..d2a8b0ccd8 100644 --- a/codex-rs/protocol/src/mcp.rs +++ b/codex-rs/protocol/src/mcp.rs @@ -1,14 +1,14 @@ +//! Types used when representing Model Context Protocol (MCP) values inside the +//! Codex protocol. +//! +//! We intentionally keep these types TS/JSON-schema friendly (via `ts-rs` and +//! `schemars`) so they can be embedded in Codex's own protocol structures. use schemars::JsonSchema; use serde::Deserialize; use serde::Serialize; use ts_rs::TS; -/// Types used when representing Model Context Protocol (MCP) values inside the -/// Codex protocol. -/// -/// We intentionally keep these types TS/JSON-schema friendly (via `ts-rs` and -/// `schemars`) so they can be embedded in Codex's own protocol structures. - +/// 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)] pub enum RequestId { @@ -26,6 +26,7 @@ impl std::fmt::Display for RequestId { } } +/// Definition for a tool the client can call. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema, TS)] #[serde(rename_all = "camelCase")] pub struct Tool { @@ -51,6 +52,7 @@ pub struct Tool { pub meta: Option, } +/// A known resource that the server is capable of reading. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema, TS)] #[serde(rename_all = "camelCase")] pub struct Resource { @@ -80,6 +82,7 @@ pub struct Resource { pub meta: Option, } +/// A template description for resources available on the server. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema, TS)] #[serde(rename_all = "camelCase")] pub struct ResourceTemplate { @@ -99,6 +102,7 @@ pub struct ResourceTemplate { pub mime_type: Option, } +/// The server's response to a tool call. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema, TS)] #[serde(rename_all = "camelCase")] pub struct CallToolResult { diff --git a/codex-rs/protocol/src/models.rs b/codex-rs/protocol/src/models.rs index 9dffb02565..07c8856fe6 100644 --- a/codex-rs/protocol/src/models.rs +++ b/codex-rs/protocol/src/models.rs @@ -2,8 +2,6 @@ use std::collections::HashMap; use std::path::Path; use codex_utils_image::load_and_resize_to_fit; -use mcp_types::CallToolResult; -use mcp_types::ContentBlock; use serde::Deserialize; use serde::Deserializer; use serde::Serialize; @@ -24,6 +22,8 @@ use codex_git::GhostCommit; use codex_utils_image::error::ImageProcessingError; use schemars::JsonSchema; +use crate::mcp::CallToolResult; + /// Controls whether a command should use the session sandbox or bypass it. #[derive( Debug, Clone, Copy, Default, Eq, Hash, PartialEq, Serialize, Deserialize, JsonSchema, TS, @@ -833,6 +833,7 @@ impl From<&CallToolResult> for FunctionCallOutputPayload { content, structured_content, is_error, + meta: _, } = call_tool_result; let is_success = is_error != &Some(true); @@ -869,7 +870,7 @@ impl From<&CallToolResult> for FunctionCallOutputPayload { } }; - let content_items = convert_content_blocks_to_items(content); + let content_items = convert_mcp_content_to_items(content); FunctionCallOutputPayload { content: serialized_content, @@ -879,32 +880,45 @@ impl From<&CallToolResult> for FunctionCallOutputPayload { } } -fn convert_content_blocks_to_items( - blocks: &[ContentBlock], +fn convert_mcp_content_to_items( + contents: &[serde_json::Value], ) -> Option> { + #[derive(serde::Deserialize)] + #[serde(tag = "type")] + enum McpContent { + #[serde(rename = "text")] + Text { text: String }, + #[serde(rename = "image")] + Image { + data: String, + #[serde(rename = "mimeType", alias = "mime_type")] + mime_type: Option, + }, + #[serde(other)] + Unknown, + } + let mut saw_image = false; - let mut items = Vec::with_capacity(blocks.len()); - tracing::warn!("Blocks: {:?}", blocks); - for block in blocks { - match block { - ContentBlock::TextContent(text) => { - items.push(FunctionCallOutputContentItem::InputText { - text: text.text.clone(), - }); - } - ContentBlock::ImageContent(image) => { + let mut items = Vec::with_capacity(contents.len()); + + for content in contents { + let item = match serde_json::from_value::(content.clone()) { + Ok(McpContent::Text { text }) => FunctionCallOutputContentItem::InputText { text }, + Ok(McpContent::Image { data, mime_type }) => { saw_image = true; - // Just in case the content doesn't include a data URL, add it. - let image_url = if image.data.starts_with("data:") { - image.data.clone() + let image_url = if data.starts_with("data:") { + data } else { - format!("data:{};base64,{}", image.mime_type, image.data) + let mime_type = mime_type.unwrap_or_else(|| "application/octet-stream".into()); + format!("data:{mime_type};base64,{data}") }; - items.push(FunctionCallOutputContentItem::InputImage { image_url }); + FunctionCallOutputContentItem::InputImage { image_url } } - // TODO: render audio, resource, and embedded resource content to the model. - _ => return None, - } + Ok(McpContent::Unknown) | Err(_) => FunctionCallOutputContentItem::InputText { + text: serde_json::to_string(content).unwrap_or_else(|_| "".to_string()), + }, + }; + items.push(item); } if saw_image { Some(items) } else { None } @@ -936,12 +950,54 @@ mod tests { use crate::protocol::AskForApproval; use anyhow::Result; use codex_execpolicy::Policy; - use mcp_types::ImageContent; - use mcp_types::TextContent; use pretty_assertions::assert_eq; use std::path::PathBuf; use tempfile::tempdir; + #[test] + fn convert_mcp_content_to_items_preserves_data_urls() { + let contents = vec![serde_json::json!({ + "type": "image", + "data": "data:image/png;base64,Zm9v", + "mimeType": "image/png", + })]; + + let items = convert_mcp_content_to_items(&contents).expect("expected image items"); + assert_eq!( + items, + vec![FunctionCallOutputContentItem::InputImage { + image_url: "data:image/png;base64,Zm9v".to_string(), + }] + ); + } + + #[test] + fn convert_mcp_content_to_items_builds_data_urls_when_missing_prefix() { + let contents = vec![serde_json::json!({ + "type": "image", + "data": "Zm9v", + "mimeType": "image/png", + })]; + + let items = convert_mcp_content_to_items(&contents).expect("expected image items"); + assert_eq!( + items, + vec![FunctionCallOutputContentItem::InputImage { + image_url: "data:image/png;base64,Zm9v".to_string(), + }] + ); + } + + #[test] + fn convert_mcp_content_to_items_returns_none_without_images() { + let contents = vec![serde_json::json!({ + "type": "text", + "text": "hello", + })]; + + assert_eq!(convert_mcp_content_to_items(&contents), None); + } + #[test] fn converts_sandbox_mode_into_developer_instructions() { let workspace_write: DeveloperInstructions = SandboxMode::WorkspaceWrite.into(); @@ -1124,20 +1180,12 @@ mod tests { fn serializes_image_outputs_as_array() -> Result<()> { let call_tool_result = CallToolResult { content: vec![ - ContentBlock::TextContent(TextContent { - annotations: None, - text: "caption".into(), - r#type: "text".into(), - }), - ContentBlock::ImageContent(ImageContent { - annotations: None, - data: "BASE64".into(), - mime_type: "image/png".into(), - r#type: "image".into(), - }), + serde_json::json!({"type":"text","text":"caption"}), + serde_json::json!({"type":"image","data":"BASE64","mimeType":"image/png"}), ], - is_error: None, structured_content: None, + is_error: Some(false), + meta: None, }; let payload = FunctionCallOutputPayload::from(&call_tool_result); @@ -1169,6 +1217,33 @@ mod tests { Ok(()) } + #[test] + fn preserves_existing_image_data_urls() -> Result<()> { + let call_tool_result = CallToolResult { + content: vec![serde_json::json!({ + "type": "image", + "data": "data:image/png;base64,BASE64", + "mimeType": "image/png" + })], + structured_content: None, + is_error: Some(false), + meta: None, + }; + + let payload = FunctionCallOutputPayload::from(&call_tool_result); + let Some(items) = payload.content_items else { + panic!("expected content items"); + }; + assert_eq!( + items, + vec![FunctionCallOutputContentItem::InputImage { + image_url: "data:image/png;base64,BASE64".into(), + }] + ); + + Ok(()) + } + #[test] fn deserializes_array_payload_into_items() -> Result<()> { let json = r#"[ diff --git a/codex-rs/protocol/src/protocol.rs b/codex-rs/protocol/src/protocol.rs index 6a6939620f..71a7973c6d 100644 --- a/codex-rs/protocol/src/protocol.rs +++ b/codex-rs/protocol/src/protocol.rs @@ -23,6 +23,11 @@ use crate::dynamic_tools::DynamicToolCallRequest; use crate::dynamic_tools::DynamicToolResponse; use crate::dynamic_tools::DynamicToolSpec; use crate::items::TurnItem; +use crate::mcp::CallToolResult; +use crate::mcp::RequestId; +use crate::mcp::Resource as McpResource; +use crate::mcp::ResourceTemplate as McpResourceTemplate; +use crate::mcp::Tool as McpTool; use crate::message_history::HistoryEntry; use crate::models::BaseInstructions; use crate::models::ContentItem; @@ -35,11 +40,6 @@ use crate::plan_tool::UpdatePlanArgs; use crate::request_user_input::RequestUserInputResponse; use crate::user_input::UserInput; use codex_utils_absolute_path::AbsolutePathBuf; -use mcp_types::CallToolResult; -use mcp_types::RequestId; -use mcp_types::Resource as McpResource; -use mcp_types::ResourceTemplate as McpResourceTemplate; -use mcp_types::Tool as McpTool; use schemars::JsonSchema; use serde::Deserialize; use serde::Serialize; diff --git a/codex-rs/rmcp-client/Cargo.toml b/codex-rs/rmcp-client/Cargo.toml index 4b2d6964f4..2b8752fcea 100644 --- a/codex-rs/rmcp-client/Cargo.toml +++ b/codex-rs/rmcp-client/Cargo.toml @@ -18,7 +18,6 @@ codex-protocol = { workspace = true } codex-utils-home-dir = { workspace = true } futures = { workspace = true, default-features = false, features = ["std"] } keyring = { workspace = true, features = ["crypto-rust"] } -mcp-types = { path = "../mcp-types" } oauth2 = "5" reqwest = { version = "0.12", default-features = false, features = [ "json", diff --git a/codex-rs/rmcp-client/src/logging_client_handler.rs b/codex-rs/rmcp-client/src/logging_client_handler.rs index 0d2c3aaa97..8db730df06 100644 --- a/codex-rs/rmcp-client/src/logging_client_handler.rs +++ b/codex-rs/rmcp-client/src/logging_client_handler.rs @@ -9,7 +9,6 @@ use rmcp::model::CreateElicitationResult; use rmcp::model::LoggingLevel; use rmcp::model::LoggingMessageNotificationParam; use rmcp::model::ProgressNotificationParam; -use rmcp::model::RequestId; use rmcp::model::ResourceUpdatedNotificationParam; use rmcp::service::NotificationContext; use rmcp::service::RequestContext; @@ -41,11 +40,7 @@ impl ClientHandler for LoggingClientHandler { request: CreateElicitationRequestParam, context: RequestContext, ) -> Result { - let id = match context.id { - RequestId::String(id) => mcp_types::RequestId::String(id.to_string()), - RequestId::Number(id) => mcp_types::RequestId::Integer(id), - }; - (self.send_elicitation)(id, request) + (self.send_elicitation)(context.id, request) .await .map_err(|err| rmcp::ErrorData::internal_error(err.to_string(), None)) } diff --git a/codex-rs/rmcp-client/src/rmcp_client.rs b/codex-rs/rmcp-client/src/rmcp_client.rs index c1bf6d39d3..12b4b52b91 100644 --- a/codex-rs/rmcp-client/src/rmcp_client.rs +++ b/codex-rs/rmcp-client/src/rmcp_client.rs @@ -10,22 +10,9 @@ use anyhow::Result; use anyhow::anyhow; use futures::FutureExt; use futures::future::BoxFuture; -use mcp_types::CallToolRequestParams; -use mcp_types::CallToolResult; -use mcp_types::InitializeRequestParams; -use mcp_types::InitializeResult; -use mcp_types::ListResourceTemplatesRequestParams; -use mcp_types::ListResourceTemplatesResult; -use mcp_types::ListResourcesRequestParams; -use mcp_types::ListResourcesResult; -use mcp_types::ListToolsRequestParams; -use mcp_types::ListToolsResult; -use mcp_types::ReadResourceRequestParams; -use mcp_types::ReadResourceResult; -use mcp_types::RequestId; -use mcp_types::Tool; use reqwest::header::HeaderMap; use rmcp::model::CallToolRequestParam; +use rmcp::model::CallToolResult; use rmcp::model::ClientNotification; use rmcp::model::ClientRequest; use rmcp::model::CreateElicitationRequestParam; @@ -34,9 +21,16 @@ use rmcp::model::CustomNotification; use rmcp::model::CustomRequest; use rmcp::model::Extensions; use rmcp::model::InitializeRequestParam; +use rmcp::model::InitializeResult; +use rmcp::model::ListResourceTemplatesResult; +use rmcp::model::ListResourcesResult; +use rmcp::model::ListToolsResult; use rmcp::model::PaginatedRequestParam; use rmcp::model::ReadResourceRequestParam; +use rmcp::model::ReadResourceResult; +use rmcp::model::RequestId; use rmcp::model::ServerResult; +use rmcp::model::Tool; use rmcp::service::RoleClient; use rmcp::service::RunningService; use rmcp::service::{self}; @@ -62,9 +56,6 @@ use crate::oauth::StoredOAuthTokens; use crate::program_resolver; use crate::utils::apply_default_headers; use crate::utils::build_default_headers; -use crate::utils::convert_call_tool_result; -use crate::utils::convert_to_mcp; -use crate::utils::convert_to_rmcp; use crate::utils::create_env_for_mcp_server; use crate::utils::run_with_timeout; @@ -229,12 +220,11 @@ impl RmcpClient { /// https://modelcontextprotocol.io/specification/2025-06-18/basic/lifecycle#initialization pub async fn initialize( &self, - params: InitializeRequestParams, + params: InitializeRequestParam, timeout: Option, send_elicitation: SendElicitation, ) -> Result { - let rmcp_params: InitializeRequestParam = convert_to_rmcp(params.clone())?; - let client_handler = LoggingClientHandler::new(rmcp_params, send_elicitation); + let client_handler = LoggingClientHandler::new(params.clone(), send_elicitation); let (transport, oauth_persistor) = { let mut guard = self.state.lock().await; @@ -275,7 +265,7 @@ impl RmcpClient { .peer() .peer_info() .ok_or_else(|| anyhow!("handshake succeeded but server info was missing"))?; - let initialize_result = convert_to_mcp(initialize_result_rmcp)?; + let initialize_result = initialize_result_rmcp.clone(); { let mut guard = self.state.lock().await; @@ -296,28 +286,26 @@ impl RmcpClient { pub async fn list_tools( &self, - params: Option, + params: Option, timeout: Option, ) -> Result { - let result = self.list_tools_with_connector_ids(params, timeout).await?; - Ok(ListToolsResult { - next_cursor: result.next_cursor, - tools: result.tools.into_iter().map(|tool| tool.tool).collect(), - }) + self.refresh_oauth_if_needed().await; + let service = self.service().await?; + let fut = service.list_tools(params); + let result = run_with_timeout(fut, timeout, "tools/list").await?; + self.persist_oauth_tokens().await; + Ok(result) } pub async fn list_tools_with_connector_ids( &self, - params: Option, + params: Option, timeout: Option, ) -> Result { self.refresh_oauth_if_needed().await; let service = self.service().await?; - let rmcp_params = params - .map(convert_to_rmcp::<_, PaginatedRequestParam>) - .transpose()?; - let fut = service.list_tools(rmcp_params); + let fut = service.list_tools(params); let result = run_with_timeout(fut, timeout, "tools/list").await?; let tools = result .tools @@ -327,7 +315,6 @@ impl RmcpClient { let connector_id = Self::meta_string(meta, "connector_id"); let connector_name = Self::meta_string(meta, "connector_name") .or_else(|| Self::meta_string(meta, "connector_display_name")); - let tool = convert_to_mcp(tool)?; Ok(ToolWithConnectorId { tool, connector_id, @@ -352,53 +339,43 @@ impl RmcpClient { pub async fn list_resources( &self, - params: Option, + params: Option, timeout: Option, ) -> Result { self.refresh_oauth_if_needed().await; let service = self.service().await?; - let rmcp_params = params - .map(convert_to_rmcp::<_, PaginatedRequestParam>) - .transpose()?; - let fut = service.list_resources(rmcp_params); + let fut = service.list_resources(params); let result = run_with_timeout(fut, timeout, "resources/list").await?; - let converted = convert_to_mcp(result)?; self.persist_oauth_tokens().await; - Ok(converted) + Ok(result) } pub async fn list_resource_templates( &self, - params: Option, + params: Option, timeout: Option, ) -> Result { self.refresh_oauth_if_needed().await; let service = self.service().await?; - let rmcp_params = params - .map(convert_to_rmcp::<_, PaginatedRequestParam>) - .transpose()?; - let fut = service.list_resource_templates(rmcp_params); + let fut = service.list_resource_templates(params); let result = run_with_timeout(fut, timeout, "resources/templates/list").await?; - let converted = convert_to_mcp(result)?; self.persist_oauth_tokens().await; - Ok(converted) + Ok(result) } pub async fn read_resource( &self, - params: ReadResourceRequestParams, + params: ReadResourceRequestParam, timeout: Option, ) -> Result { self.refresh_oauth_if_needed().await; let service = self.service().await?; - let rmcp_params: ReadResourceRequestParam = convert_to_rmcp(params)?; - let fut = service.read_resource(rmcp_params); + let fut = service.read_resource(params); let result = run_with_timeout(fut, timeout, "resources/read").await?; - let converted = convert_to_mcp(result)?; self.persist_oauth_tokens().await; - Ok(converted) + Ok(result) } pub async fn call_tool( @@ -409,13 +386,23 @@ impl RmcpClient { ) -> Result { self.refresh_oauth_if_needed().await; let service = self.service().await?; - let params = CallToolRequestParams { arguments, name }; - let rmcp_params: CallToolRequestParam = convert_to_rmcp(params)?; + let arguments = match arguments { + Some(Value::Object(map)) => Some(map), + Some(other) => { + return Err(anyhow!( + "MCP tool arguments must be a JSON object, got {other}" + )); + } + None => None, + }; + let rmcp_params = CallToolRequestParam { + name: name.into(), + arguments, + }; let fut = service.call_tool(rmcp_params); - let rmcp_result = run_with_timeout(fut, timeout, "tools/call").await?; - let converted = convert_call_tool_result(rmcp_result)?; + let result = run_with_timeout(fut, timeout, "tools/call").await?; self.persist_oauth_tokens().await; - Ok(converted) + Ok(result) } pub async fn send_custom_notification( diff --git a/codex-rs/rmcp-client/src/utils.rs b/codex-rs/rmcp-client/src/utils.rs index 8deb4d402b..e47c1d14b6 100644 --- a/codex-rs/rmcp-client/src/utils.rs +++ b/codex-rs/rmcp-client/src/utils.rs @@ -5,14 +5,11 @@ use std::time::Duration; use anyhow::Context; use anyhow::Result; use anyhow::anyhow; -use mcp_types::CallToolResult; use reqwest::ClientBuilder; use reqwest::header::HeaderMap; use reqwest::header::HeaderName; use reqwest::header::HeaderValue; -use rmcp::model::CallToolResult as RmcpCallToolResult; use rmcp::service::ServiceError; -use serde_json::Value; use tokio::time; pub(crate) async fn run_with_timeout( @@ -33,45 +30,6 @@ where } } -pub(crate) fn convert_call_tool_result(result: RmcpCallToolResult) -> Result { - let mut value = serde_json::to_value(result)?; - if let Some(obj) = value.as_object_mut() - && (obj.get("content").is_none() - || obj.get("content").is_some_and(serde_json::Value::is_null)) - { - obj.insert("content".to_string(), Value::Array(Vec::new())); - } - serde_json::from_value(value).context("failed to convert call tool result") -} - -/// Convert from mcp-types to Rust SDK types. -/// -/// The Rust SDK types are the same as our mcp-types crate because they are both -/// derived from the same MCP specification. -/// As a result, it should be safe to convert directly from one to the other. -pub(crate) fn convert_to_rmcp(value: T) -> Result -where - T: serde::Serialize, - U: serde::de::DeserializeOwned, -{ - let json = serde_json::to_value(value)?; - serde_json::from_value(json).map_err(|err| anyhow!(err)) -} - -/// Convert from Rust SDK types to mcp-types. -/// -/// The Rust SDK types are the same as our mcp-types crate because they are both -/// derived from the same MCP specification. -/// As a result, it should be safe to convert directly from one to the other. -pub(crate) fn convert_to_mcp(value: T) -> Result -where - T: serde::Serialize, - U: serde::de::DeserializeOwned, -{ - let json = serde_json::to_value(value)?; - serde_json::from_value(json).map_err(|err| anyhow!(err)) -} - pub(crate) fn create_env_for_mcp_server( extra_env: Option>, env_vars: &[String], @@ -203,10 +161,7 @@ pub(crate) const DEFAULT_ENV_VARS: &[&str] = &[ #[cfg(test)] mod tests { use super::*; - use mcp_types::ContentBlock; use pretty_assertions::assert_eq; - use rmcp::model::CallToolResult as RmcpCallToolResult; - use serde_json::json; use serial_test::serial; use std::ffi::OsString; @@ -260,43 +215,4 @@ mod tests { let env = create_env_for_mcp_server(None, &[custom_var.to_string()]); assert_eq!(env.get(custom_var), Some(&value.to_string())); } - - #[test] - fn convert_call_tool_result_defaults_missing_content() -> Result<()> { - let structured_content = json!({ "key": "value" }); - let rmcp_result = RmcpCallToolResult { - content: vec![], - structured_content: Some(structured_content.clone()), - is_error: Some(true), - meta: None, - }; - - let result = convert_call_tool_result(rmcp_result)?; - - assert!(result.content.is_empty()); - assert_eq!(result.structured_content, Some(structured_content)); - assert_eq!(result.is_error, Some(true)); - - Ok(()) - } - - #[test] - fn convert_call_tool_result_preserves_existing_content() -> Result<()> { - let rmcp_result = RmcpCallToolResult::success(vec![rmcp::model::Content::text("hello")]); - - let result = convert_call_tool_result(rmcp_result)?; - - assert_eq!(result.content.len(), 1); - match &result.content[0] { - ContentBlock::TextContent(text_content) => { - assert_eq!(text_content.text, "hello"); - assert_eq!(text_content.r#type, "text"); - } - other => panic!("expected text content got {other:?}"), - } - assert_eq!(result.structured_content, None); - assert_eq!(result.is_error, Some(false)); - - Ok(()) - } } diff --git a/codex-rs/rmcp-client/tests/resources.rs b/codex-rs/rmcp-client/tests/resources.rs index 3d627ebbf4..9bfd77c183 100644 --- a/codex-rs/rmcp-client/tests/resources.rs +++ b/codex-rs/rmcp-client/tests/resources.rs @@ -7,15 +7,15 @@ use codex_rmcp_client::ElicitationResponse; use codex_rmcp_client::RmcpClient; use codex_utils_cargo_bin::CargoBinError; use futures::FutureExt as _; -use mcp_types::ClientCapabilities; -use mcp_types::Implementation; -use mcp_types::InitializeRequestParams; -use mcp_types::ListResourceTemplatesResult; -use mcp_types::ReadResourceRequestParams; -use mcp_types::ReadResourceResultContents; -use mcp_types::Resource; -use mcp_types::ResourceTemplate; -use mcp_types::TextResourceContents; +use rmcp::model::AnnotateAble; +use rmcp::model::ClientCapabilities; +use rmcp::model::ElicitationCapability; +use rmcp::model::Implementation; +use rmcp::model::InitializeRequestParam; +use rmcp::model::ListResourceTemplatesResult; +use rmcp::model::ProtocolVersion; +use rmcp::model::ReadResourceRequestParam; +use rmcp::model::ResourceContents; use serde_json::json; const RESOURCE_URI: &str = "memo://codex/example-note"; @@ -24,21 +24,24 @@ fn stdio_server_bin() -> Result { codex_utils_cargo_bin::cargo_bin("test_stdio_server") } -fn init_params() -> InitializeRequestParams { - InitializeRequestParams { +fn init_params() -> InitializeRequestParam { + InitializeRequestParam { capabilities: ClientCapabilities { experimental: None, roots: None, sampling: None, - elicitation: Some(json!({})), + elicitation: Some(ElicitationCapability { + schema_validation: None, + }), }, client_info: Implementation { name: "codex-test".into(), version: "0.0.0-test".into(), title: Some("Codex rmcp resource test".into()), - user_agent: None, + icons: None, + website_url: None, }, - protocol_version: mcp_types::MCP_SCHEMA_VERSION.to_string(), + protocol_version: ProtocolVersion::V_2025_06_18, } } @@ -79,15 +82,17 @@ async fn rmcp_client_can_list_and_read_resources() -> anyhow::Result<()> { .expect("memo resource present"); assert_eq!( memo, - &Resource { - annotations: None, + &rmcp::model::RawResource { + uri: RESOURCE_URI.to_string(), + name: "example-note".to_string(), + title: Some("Example Note".to_string()), description: Some("A sample MCP resource exposed for integration tests.".to_string()), mime_type: Some("text/plain".to_string()), - name: "example-note".to_string(), size: None, - title: Some("Example Note".to_string()), - uri: RESOURCE_URI.to_string(), + icons: None, + meta: None, } + .no_annotation() ); let templates = client .list_resource_templates(None, Some(Duration::from_secs(5))) @@ -95,39 +100,39 @@ async fn rmcp_client_can_list_and_read_resources() -> anyhow::Result<()> { assert_eq!( templates, ListResourceTemplatesResult { + meta: None, next_cursor: None, - resource_templates: vec![ResourceTemplate { - annotations: None, - description: Some( - "Template for memo://codex/{slug} resources used in tests.".to_string() - ), - mime_type: Some("text/plain".to_string()), - name: "codex-memo".to_string(), - title: Some("Codex Memo".to_string()), - uri_template: "memo://codex/{slug}".to_string(), - }], + resource_templates: vec![ + rmcp::model::RawResourceTemplate { + uri_template: "memo://codex/{slug}".to_string(), + name: "codex-memo".to_string(), + title: Some("Codex Memo".to_string()), + description: Some( + "Template for memo://codex/{slug} resources used in tests.".to_string(), + ), + mime_type: Some("text/plain".to_string()), + } + .no_annotation() + ], } ); let read = client .read_resource( - ReadResourceRequestParams { + ReadResourceRequestParam { uri: RESOURCE_URI.to_string(), }, Some(Duration::from_secs(5)), ) .await?; - let ReadResourceResultContents::TextResourceContents(text) = - read.contents.first().expect("resource contents present") - else { - panic!("expected text resource"); - }; + let text = read.contents.first().expect("resource contents present"); assert_eq!( text, - &TextResourceContents { - text: "This is a sample MCP resource served by the rmcp test server.".to_string(), + &ResourceContents::TextResourceContents { uri: RESOURCE_URI.to_string(), mime_type: Some("text/plain".to_string()), + text: "This is a sample MCP resource served by the rmcp test server.".to_string(), + meta: None, } ); diff --git a/codex-rs/tui/Cargo.toml b/codex-rs/tui/Cargo.toml index d859aa945e..8bdd473220 100644 --- a/codex-rs/tui/Cargo.toml +++ b/codex-rs/tui/Cargo.toml @@ -54,7 +54,6 @@ dunce = { workspace = true } image = { workspace = true, features = ["jpeg", "png"] } itertools = { workspace = true } lazy_static = { workspace = true } -mcp-types = { workspace = true } pathdiff = { workspace = true } pulldown-cmark = { workspace = true } rand = { workspace = true } @@ -67,6 +66,7 @@ ratatui = { workspace = true, features = [ ratatui-macros = { workspace = true } regex-lite = { workspace = true } reqwest = { version = "0.12", features = ["json"] } +rmcp = { workspace = true } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true, features = ["preserve_order"] } shlex = { workspace = true } diff --git a/codex-rs/tui/src/bottom_pane/approval_overlay.rs b/codex-rs/tui/src/bottom_pane/approval_overlay.rs index 0f0445fee8..15f252e86f 100644 --- a/codex-rs/tui/src/bottom_pane/approval_overlay.rs +++ b/codex-rs/tui/src/bottom_pane/approval_overlay.rs @@ -23,11 +23,11 @@ use codex_core::protocol::ExecPolicyAmendment; use codex_core::protocol::FileChange; use codex_core::protocol::Op; use codex_core::protocol::ReviewDecision; +use codex_protocol::mcp::RequestId; use crossterm::event::KeyCode; use crossterm::event::KeyEvent; use crossterm::event::KeyEventKind; use crossterm::event::KeyModifiers; -use mcp_types::RequestId; use ratatui::buffer::Buffer; use ratatui::layout::Rect; use ratatui::style::Stylize; diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__binary_size_ideal_response.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__binary_size_ideal_response.snap index 77738439a1..38fb05e28d 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__binary_size_ideal_response.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__binary_size_ideal_response.snap @@ -27,7 +27,7 @@ expression: "lines[start_idx..].join(\"\\n\")" exec, linux-sandbox, tui, login, ollama, and mcp. • Ran for d in ansi-escape apply-patch arg0 cli common core exec execpolicy - │ file-search linux-sandbox login mcp-client mcp-server mcp-types ollama + │ file-search linux-sandbox login mcp-client mcp-server ollama │ tui; do echo "--- $d/Cargo.toml"; sed -n '1,200p' $d/Cargo.toml; echo; │ … +1 lines └ --- ansi-escape/Cargo.toml diff --git a/codex-rs/tui/src/history_cell.rs b/codex-rs/tui/src/history_cell.rs index 0258e05968..a91b62a754 100644 --- a/codex-rs/tui/src/history_cell.rs +++ b/codex-rs/tui/src/history_cell.rs @@ -47,6 +47,8 @@ use codex_core::protocol::SessionConfiguredEvent; use codex_core::web_search::web_search_detail; use codex_otel::RuntimeMetricsSummary; use codex_protocol::account::PlanType; +use codex_protocol::mcp::Resource; +use codex_protocol::mcp::ResourceTemplate; use codex_protocol::models::WebSearchAction; use codex_protocol::openai_models::ReasoningEffort as ReasoningEffortConfig; use codex_protocol::plan_tool::PlanItemArg; @@ -57,10 +59,6 @@ use codex_protocol::request_user_input::RequestUserInputQuestion; use codex_protocol::user_input::TextElement; use image::DynamicImage; use image::ImageReader; -use mcp_types::EmbeddedResourceResource; -use mcp_types::Resource; -use mcp_types::ResourceLink; -use mcp_types::ResourceTemplate; use ratatui::prelude::*; use ratatui::style::Color; use ratatui::style::Modifier; @@ -1203,7 +1201,7 @@ pub(crate) struct McpToolCallCell { invocation: McpInvocation, start_time: Instant, duration: Option, - result: Option>, + result: Option>, animations_enabled: bool, } @@ -1230,7 +1228,7 @@ impl McpToolCallCell { pub(crate) fn complete( &mut self, duration: Duration, - result: Result, + result: Result, ) -> Option> { let image_cell = try_new_completed_mcp_tool_call_with_image_output(&result) .map(|cell| Box::new(cell) as Box); @@ -1253,23 +1251,32 @@ impl McpToolCallCell { self.result = Some(Err("interrupted".to_string())); } - fn render_content_block(block: &mcp_types::ContentBlock, width: usize) -> String { - match block { - mcp_types::ContentBlock::TextContent(text) => { + fn render_content_block(block: &serde_json::Value, width: usize) -> String { + let content = match serde_json::from_value::(block.clone()) { + Ok(content) => content, + Err(_) => { + return format_and_truncate_tool_result( + &block.to_string(), + TOOL_CALL_MAX_LINES, + width, + ); + } + }; + + match content.raw { + rmcp::model::RawContent::Text(text) => { format_and_truncate_tool_result(&text.text, TOOL_CALL_MAX_LINES, width) } - mcp_types::ContentBlock::ImageContent(_) => "".to_string(), - mcp_types::ContentBlock::AudioContent(_) => "