From e24058b7a872054e3e76f9301d64eccccb75811d Mon Sep 17 00:00:00 2001 From: Gav Verma Date: Mon, 2 Feb 2026 16:49:23 -0800 Subject: [PATCH 01/76] 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 02/76] 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 03/76] 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 04/76] 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 05/76] 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 06/76] 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(_) => "