diff --git a/.github/actions/codex/bun.lock b/.github/actions/codex/bun.lock index 15b55fafa7..cd2c52143c 100644 --- a/.github/actions/codex/bun.lock +++ b/.github/actions/codex/bun.lock @@ -9,7 +9,7 @@ }, "devDependencies": { "@types/bun": "^1.2.20", - "@types/node": "^24.2.1", + "@types/node": "^24.3.0", "prettier": "^3.6.2", "typescript": "^5.9.2", }, @@ -50,7 +50,7 @@ "@types/bun": ["@types/bun@1.2.20", "", { "dependencies": { "bun-types": "1.2.20" } }, "sha512-dX3RGzQ8+KgmMw7CsW4xT5ITBSCrSbfHc36SNT31EOUg/LA9JWq0VDdEXDRSe1InVWpd2yLUM1FUF/kEOyTzYA=="], - "@types/node": ["@types/node@24.2.1", "", { "dependencies": { "undici-types": "~7.10.0" } }, "sha512-DRh5K+ka5eJic8CjH7td8QpYEV6Zo10gfRkjHCO3weqZHWDtAaSTFtl4+VMqOJ4N5jcuhZ9/l+yy8rVgw7BQeQ=="], + "@types/node": ["@types/node@24.3.0", "", { "dependencies": { "undici-types": "~7.10.0" } }, "sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow=="], "@types/react": ["@types/react@19.1.8", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g=="], @@ -82,6 +82,8 @@ "@octokit/plugin-rest-endpoint-methods/@octokit/types": ["@octokit/types@12.6.0", "", { "dependencies": { "@octokit/openapi-types": "^20.0.0" } }, "sha512-1rhSOfRa6H9w4YwK0yrf5faDaDTb+yLyBUKOCV4xtCDB5VmIPqd/v9yr9o6SAzOAlRxMiRiCic6JVM1/kunVkw=="], + "bun-types/@types/node": ["@types/node@24.2.1", "", { "dependencies": { "undici-types": "~7.10.0" } }, "sha512-DRh5K+ka5eJic8CjH7td8QpYEV6Zo10gfRkjHCO3weqZHWDtAaSTFtl4+VMqOJ4N5jcuhZ9/l+yy8rVgw7BQeQ=="], + "@octokit/plugin-paginate-rest/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@20.0.0", "", {}, "sha512-EtqRBEjp1dL/15V7WiX5LJMIxxkdiGJnabzYx5Apx4FkQIFgAfKumXeYAqqJCj1s+BMX4cPFIFC4OLCR6stlnA=="], "@octokit/plugin-rest-endpoint-methods/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@20.0.0", "", {}, "sha512-EtqRBEjp1dL/15V7WiX5LJMIxxkdiGJnabzYx5Apx4FkQIFgAfKumXeYAqqJCj1s+BMX4cPFIFC4OLCR6stlnA=="], diff --git a/.github/actions/codex/package.json b/.github/actions/codex/package.json index 208d9fef78..8e0e540314 100644 --- a/.github/actions/codex/package.json +++ b/.github/actions/codex/package.json @@ -14,7 +14,7 @@ }, "devDependencies": { "@types/bun": "^1.2.20", - "@types/node": "^24.2.1", + "@types/node": "^24.3.0", "prettier": "^3.6.2", "typescript": "^5.9.2" } diff --git a/AGENTS.md b/AGENTS.md index b2f21c7c6b..f1744a87e5 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -12,6 +12,10 @@ Before finalizing a change to `codex-rs`, run `just fmt` (in `codex-rs` director 1. Run the test for the specific project that was changed. For example, if changes were made in `codex-rs/tui`, run `cargo test -p codex-tui`. 2. Once those pass, if any changes were made in common, core, or protocol, run the complete test suite with `cargo test --all-features`. +## TUI style conventions + +See `codex-rs/tui/styles.md`. + ## TUI code conventions - Use concise styling helpers from ratatui’s Stylize trait. diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index f118cb67a0..639a946752 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -665,6 +665,7 @@ dependencies = [ "codex-exec", "codex-login", "codex-mcp-server", + "codex-protocol", "codex-tui", "serde_json", "tokio", @@ -678,6 +679,7 @@ version = "0.0.0" dependencies = [ "clap", "codex-core", + "codex-protocol", "serde", "toml 0.9.5", ] @@ -752,6 +754,7 @@ dependencies = [ "codex-common", "codex-core", "codex-ollama", + "codex-protocol", "core_test_support", "libc", "owo-colors", @@ -856,6 +859,7 @@ dependencies = [ "codex-arg0", "codex-core", "codex-login", + "codex-protocol", "mcp-types", "mcp_test_support", "pretty_assertions", @@ -896,6 +900,7 @@ name = "codex-protocol" version = "0.0.0" dependencies = [ "mcp-types", + "pretty_assertions", "serde", "serde_bytes", "serde_json", @@ -2771,6 +2776,7 @@ version = "0.0.0" dependencies = [ "serde", "serde_json", + "ts-rs", ] [[package]] @@ -2781,8 +2787,10 @@ dependencies = [ "assert_cmd", "codex-core", "codex-mcp-server", + "codex-protocol", "mcp-types", "pretty_assertions", + "serde", "serde_json", "shlex", "tempfile", @@ -4700,6 +4708,15 @@ dependencies = [ "winapi", ] +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + [[package]] name = "terminal_size" version = "0.4.2" @@ -5199,6 +5216,29 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]] +name = "ts-rs" +version = "11.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ef1b7a6d914a34127ed8e1fa927eb7088903787bcded4fa3eef8f85ee1568be" +dependencies = [ + "serde_json", + "thiserror 2.0.12", + "ts-rs-macros", +] + +[[package]] +name = "ts-rs-macros" +version = "11.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9d4ed7b4c18cc150a6a0a1e9ea1ecfa688791220781af6e119f9599a8502a0a" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.104", + "termcolor", +] + [[package]] name = "tui-input" version = "0.14.0" diff --git a/codex-rs/cli/Cargo.toml b/codex-rs/cli/Cargo.toml index 0f370691cf..0183ae28d8 100644 --- a/codex-rs/cli/Cargo.toml +++ b/codex-rs/cli/Cargo.toml @@ -25,6 +25,7 @@ codex-core = { path = "../core" } codex-exec = { path = "../exec" } codex-login = { path = "../login" } codex-mcp-server = { path = "../mcp-server" } +codex-protocol = { path = "../protocol" } codex-tui = { path = "../tui" } serde_json = "1" tokio = { version = "1", features = [ diff --git a/codex-rs/cli/src/debug_sandbox.rs b/codex-rs/cli/src/debug_sandbox.rs index 30e144ed34..6fe7f003c7 100644 --- a/codex-rs/cli/src/debug_sandbox.rs +++ b/codex-rs/cli/src/debug_sandbox.rs @@ -3,11 +3,11 @@ use std::path::PathBuf; use codex_common::CliConfigOverrides; use codex_core::config::Config; use codex_core::config::ConfigOverrides; -use codex_core::config_types::SandboxMode; use codex_core::exec_env::create_env; use codex_core::landlock::spawn_command_under_linux_sandbox; use codex_core::seatbelt::spawn_command_under_seatbelt; use codex_core::spawn::StdioPolicy; +use codex_protocol::config_types::SandboxMode; use crate::LandlockCommand; use crate::SeatbeltCommand; diff --git a/codex-rs/clippy.toml b/codex-rs/clippy.toml index 9e614bec42..5a6ff7f052 100644 --- a/codex-rs/clippy.toml +++ b/codex-rs/clippy.toml @@ -1,2 +1,9 @@ allow-expect-in-tests = true -allow-unwrap-in-tests = true \ No newline at end of file +allow-unwrap-in-tests = true +disallowed-methods = [ + { path = "ratatui::style::Color::Rgb", reason = "Use ANSI colors, which work better in various terminal themes." }, + { path = "ratatui::style::Color::Indexed", reason = "Use ANSI colors, which work better in various terminal themes." }, + { path = "ratatui::style::Stylize::white", reason = "Avoid hardcoding white; prefer default fg or dim/bold. Exception: Disable this rule if rendering over a hardcoded ANSI background." }, + { path = "ratatui::style::Stylize::black", reason = "Avoid hardcoding black; prefer default fg or dim/bold. Exception: Disable this rule if rendering over a hardcoded ANSI background." }, + { path = "ratatui::style::Stylize::yellow", reason = "Avoid yellow; prefer other colors in `tui/styles.md`." }, +] diff --git a/codex-rs/common/Cargo.toml b/codex-rs/common/Cargo.toml index 1723098b8a..b10600574c 100644 --- a/codex-rs/common/Cargo.toml +++ b/codex-rs/common/Cargo.toml @@ -9,6 +9,7 @@ workspace = true [dependencies] clap = { version = "4", features = ["derive", "wrap_help"], optional = true } codex-core = { path = "../core" } +codex-protocol = { path = "../protocol" } serde = { version = "1", optional = true } toml = { version = "0.9", optional = true } diff --git a/codex-rs/common/src/sandbox_mode_cli_arg.rs b/codex-rs/common/src/sandbox_mode_cli_arg.rs index 588637aebb..fa5662ce66 100644 --- a/codex-rs/common/src/sandbox_mode_cli_arg.rs +++ b/codex-rs/common/src/sandbox_mode_cli_arg.rs @@ -7,7 +7,7 @@ //! `config.toml`. use clap::ValueEnum; -use codex_core::config_types::SandboxMode; +use codex_protocol::config_types::SandboxMode; #[derive(Clone, Copy, Debug, ValueEnum)] #[value(rename_all = "kebab-case")] diff --git a/codex-rs/core/src/config_types.rs b/codex-rs/core/src/config_types.rs index 4bdcdb6022..8ff9720c36 100644 --- a/codex-rs/core/src/config_types.rs +++ b/codex-rs/core/src/config_types.rs @@ -78,21 +78,6 @@ pub enum HistoryPersistence { #[derive(Deserialize, Debug, Clone, PartialEq, Default)] pub struct Tui {} -#[derive(Deserialize, Debug, Clone, Copy, PartialEq, Default, Serialize, Display)] -#[serde(rename_all = "kebab-case")] -#[strum(serialize_all = "kebab-case")] -pub enum SandboxMode { - #[serde(rename = "read-only")] - #[default] - ReadOnly, - - #[serde(rename = "workspace-write")] - WorkspaceWrite, - - #[serde(rename = "danger-full-access")] - DangerFullAccess, -} - #[derive(Deserialize, Debug, Clone, PartialEq, Default)] pub struct SandboxWorkspaceWrite { #[serde(default)] diff --git a/codex-rs/core/src/environment_context.rs b/codex-rs/core/src/environment_context.rs index d31ddbc15e..b37645a32b 100644 --- a/codex-rs/core/src/environment_context.rs +++ b/codex-rs/core/src/environment_context.rs @@ -2,11 +2,11 @@ use serde::Deserialize; use serde::Serialize; use strum_macros::Display as DeriveDisplay; -use crate::config_types::SandboxMode; use crate::models::ContentItem; use crate::models::ResponseItem; use crate::protocol::AskForApproval; use crate::protocol::SandboxPolicy; +use codex_protocol::config_types::SandboxMode; use std::fmt::Display; use std::path::PathBuf; diff --git a/codex-rs/exec/Cargo.toml b/codex-rs/exec/Cargo.toml index 9847788d92..89dc3951e7 100644 --- a/codex-rs/exec/Cargo.toml +++ b/codex-rs/exec/Cargo.toml @@ -26,6 +26,7 @@ codex-common = { path = "../common", features = [ ] } codex-core = { path = "../core" } codex-ollama = { path = "../ollama" } +codex-protocol = { path = "../protocol" } owo-colors = "4.2.0" serde_json = "1" shlex = "1.3.0" @@ -41,8 +42,8 @@ tracing-subscriber = { version = "0.3.19", features = ["env-filter"] } [dev-dependencies] assert_cmd = "2" +core_test_support = { path = "../core/tests/common" } libc = "0.2" predicates = "3" tempfile = "3.13.0" wiremock = "0.6" -core_test_support = { path = "../core/tests/common" } diff --git a/codex-rs/exec/src/lib.rs b/codex-rs/exec/src/lib.rs index e6b4d7fb0c..e18314fbc7 100644 --- a/codex-rs/exec/src/lib.rs +++ b/codex-rs/exec/src/lib.rs @@ -13,7 +13,6 @@ use codex_core::ConversationManager; use codex_core::NewConversation; use codex_core::config::Config; use codex_core::config::ConfigOverrides; -use codex_core::config_types::SandboxMode; use codex_core::protocol::AskForApproval; use codex_core::protocol::Event; use codex_core::protocol::EventMsg; @@ -22,6 +21,7 @@ use codex_core::protocol::Op; use codex_core::protocol::TaskCompleteEvent; use codex_core::util::is_inside_git_repo; use codex_ollama::DEFAULT_OSS_MODEL; +use codex_protocol::config_types::SandboxMode; use event_processor_with_human_output::EventProcessorWithHumanOutput; use event_processor_with_json_output::EventProcessorWithJsonOutput; use tracing::debug; diff --git a/codex-rs/mcp-server/Cargo.toml b/codex-rs/mcp-server/Cargo.toml index 6274ba8e41..3f5c2a0fa2 100644 --- a/codex-rs/mcp-server/Cargo.toml +++ b/codex-rs/mcp-server/Cargo.toml @@ -19,6 +19,7 @@ anyhow = "1" codex-arg0 = { path = "../arg0" } codex-core = { path = "../core" } codex-login = { path = "../login" } +codex-protocol = { path = "../protocol" } mcp-types = { path = "../mcp-types" } schemars = "0.8.22" serde = { version = "1", features = ["derive"] } diff --git a/codex-rs/mcp-server/src/codex_message_processor.rs b/codex-rs/mcp-server/src/codex_message_processor.rs index c90fab868b..d13bdbf346 100644 --- a/codex-rs/mcp-server/src/codex_message_processor.rs +++ b/codex-rs/mcp-server/src/codex_message_processor.rs @@ -25,36 +25,36 @@ use crate::error_code::INVALID_REQUEST_ERROR_CODE; use crate::json_to_toml::json_to_toml; use crate::outgoing_message::OutgoingMessageSender; use crate::outgoing_message::OutgoingNotification; -use crate::wire_format::APPLY_PATCH_APPROVAL_METHOD; -use crate::wire_format::AddConversationListenerParams; -use crate::wire_format::AddConversationSubscriptionResponse; -use crate::wire_format::ApplyPatchApprovalParams; -use crate::wire_format::ApplyPatchApprovalResponse; -use crate::wire_format::ClientRequest; -use crate::wire_format::ConversationId; -use crate::wire_format::EXEC_COMMAND_APPROVAL_METHOD; -use crate::wire_format::ExecCommandApprovalParams; -use crate::wire_format::ExecCommandApprovalResponse; -use crate::wire_format::InputItem as WireInputItem; -use crate::wire_format::InterruptConversationParams; -use crate::wire_format::InterruptConversationResponse; -use crate::wire_format::LOGIN_CHATGPT_COMPLETE_EVENT; -use crate::wire_format::LoginChatGptCompleteNotification; -use crate::wire_format::LoginChatGptResponse; -use crate::wire_format::NewConversationParams; -use crate::wire_format::NewConversationResponse; -use crate::wire_format::RemoveConversationListenerParams; -use crate::wire_format::RemoveConversationSubscriptionResponse; -use crate::wire_format::SendUserMessageParams; -use crate::wire_format::SendUserMessageResponse; -use crate::wire_format::SendUserTurnParams; -use crate::wire_format::SendUserTurnResponse; use codex_core::protocol::InputItem as CoreInputItem; use codex_core::protocol::Op; use codex_login::CLIENT_ID; use codex_login::ServerOptions as LoginServerOptions; use codex_login::ShutdownHandle; use codex_login::run_login_server; +use codex_protocol::mcp_protocol::APPLY_PATCH_APPROVAL_METHOD; +use codex_protocol::mcp_protocol::AddConversationListenerParams; +use codex_protocol::mcp_protocol::AddConversationSubscriptionResponse; +use codex_protocol::mcp_protocol::ApplyPatchApprovalParams; +use codex_protocol::mcp_protocol::ApplyPatchApprovalResponse; +use codex_protocol::mcp_protocol::ClientRequest; +use codex_protocol::mcp_protocol::ConversationId; +use codex_protocol::mcp_protocol::EXEC_COMMAND_APPROVAL_METHOD; +use codex_protocol::mcp_protocol::ExecCommandApprovalParams; +use codex_protocol::mcp_protocol::ExecCommandApprovalResponse; +use codex_protocol::mcp_protocol::InputItem as WireInputItem; +use codex_protocol::mcp_protocol::InterruptConversationParams; +use codex_protocol::mcp_protocol::InterruptConversationResponse; +use codex_protocol::mcp_protocol::LOGIN_CHATGPT_COMPLETE_EVENT; +use codex_protocol::mcp_protocol::LoginChatGptCompleteNotification; +use codex_protocol::mcp_protocol::LoginChatGptResponse; +use codex_protocol::mcp_protocol::NewConversationParams; +use codex_protocol::mcp_protocol::NewConversationResponse; +use codex_protocol::mcp_protocol::RemoveConversationListenerParams; +use codex_protocol::mcp_protocol::RemoveConversationSubscriptionResponse; +use codex_protocol::mcp_protocol::SendUserMessageParams; +use codex_protocol::mcp_protocol::SendUserMessageResponse; +use codex_protocol::mcp_protocol::SendUserTurnParams; +use codex_protocol::mcp_protocol::SendUserTurnResponse; // Duration before a ChatGPT login attempt is abandoned. const LOGIN_CHATGPT_TIMEOUT: Duration = Duration::from_secs(10 * 60); @@ -237,7 +237,7 @@ impl CodexMessageProcessor { self.outgoing .send_response( request_id, - crate::wire_format::CancelLoginChatGptResponse {}, + codex_protocol::mcp_protocol::CancelLoginChatGptResponse {}, ) .await; } else { @@ -595,7 +595,7 @@ fn derive_config_from_params( profile, cwd, approval_policy, - sandbox, + sandbox: sandbox_mode, config: cli_overrides, base_instructions, include_plan_tool, @@ -605,8 +605,8 @@ fn derive_config_from_params( model, config_profile: profile, cwd: cwd.map(PathBuf::from), - approval_policy: approval_policy.map(Into::into), - sandbox_mode: sandbox.map(Into::into), + approval_policy, + sandbox_mode, model_provider: None, codex_linux_sandbox_exe, base_instructions, diff --git a/codex-rs/mcp-server/src/codex_tool_config.rs b/codex-rs/mcp-server/src/codex_tool_config.rs index 906921a030..5993c10faf 100644 --- a/codex-rs/mcp-server/src/codex_tool_config.rs +++ b/codex-rs/mcp-server/src/codex_tool_config.rs @@ -1,7 +1,7 @@ //! Configuration object accepted by the `codex` MCP tool-call. -use codex_core::config_types::SandboxMode; use codex_core::protocol::AskForApproval; +use codex_protocol::config_types::SandboxMode; use mcp_types::Tool; use mcp_types::ToolInputSchema; use schemars::JsonSchema; diff --git a/codex-rs/mcp-server/src/conversation_loop.rs b/codex-rs/mcp-server/src/conversation_loop.rs deleted file mode 100644 index 7a3ae3e76d..0000000000 --- a/codex-rs/mcp-server/src/conversation_loop.rs +++ /dev/null @@ -1,124 +0,0 @@ -use std::sync::Arc; - -use crate::exec_approval::handle_exec_approval_request; -use crate::outgoing_message::OutgoingMessageSender; -use crate::outgoing_message::OutgoingNotificationMeta; -use crate::patch_approval::handle_patch_approval_request; -use codex_core::CodexConversation; -use codex_core::protocol::AgentMessageEvent; -use codex_core::protocol::ApplyPatchApprovalRequestEvent; -use codex_core::protocol::EventMsg; -use codex_core::protocol::ExecApprovalRequestEvent; -use mcp_types::RequestId; -use tracing::error; - -pub async fn run_conversation_loop( - codex: Arc, - outgoing: Arc, - request_id: RequestId, -) { - let request_id_str = match &request_id { - RequestId::String(s) => s.clone(), - RequestId::Integer(n) => n.to_string(), - }; - - // Stream events until the task needs to pause for user interaction or - // completes. - loop { - match codex.next_event().await { - Ok(event) => { - outgoing - .send_event_as_notification( - &event, - Some(OutgoingNotificationMeta::new(Some(request_id.clone()))), - ) - .await; - - match event.msg { - EventMsg::ExecApprovalRequest(ExecApprovalRequestEvent { - command, - cwd, - call_id, - reason: _, - }) => { - handle_exec_approval_request( - command, - cwd, - outgoing.clone(), - codex.clone(), - request_id.clone(), - request_id_str.clone(), - event.id.clone(), - call_id, - ) - .await; - } - EventMsg::Error(_) => { - error!("Codex runtime error"); - } - EventMsg::ApplyPatchApprovalRequest(ApplyPatchApprovalRequestEvent { - call_id, - reason, - grant_root, - changes, - }) => { - handle_patch_approval_request( - call_id, - reason, - grant_root, - changes, - outgoing.clone(), - codex.clone(), - request_id.clone(), - request_id_str.clone(), - event.id.clone(), - ) - .await; - } - EventMsg::TaskComplete(_) => {} - EventMsg::SessionConfigured(_) => { - tracing::error!("unexpected SessionConfigured event"); - } - EventMsg::AgentMessageDelta(_) => { - // TODO: think how we want to support this in the MCP - } - EventMsg::AgentReasoningDelta(_) => { - // TODO: think how we want to support this in the MCP - } - EventMsg::AgentMessage(AgentMessageEvent { .. }) => { - // TODO: think how we want to support this in the MCP - } - EventMsg::AgentReasoningRawContent(_) - | EventMsg::AgentReasoningRawContentDelta(_) - | EventMsg::TaskStarted - | EventMsg::TokenCount(_) - | EventMsg::AgentReasoning(_) - | EventMsg::AgentReasoningSectionBreak(_) - | EventMsg::McpToolCallBegin(_) - | EventMsg::McpToolCallEnd(_) - | EventMsg::ExecCommandBegin(_) - | EventMsg::ExecCommandEnd(_) - | EventMsg::TurnDiff(_) - | EventMsg::BackgroundEvent(_) - | EventMsg::ExecCommandOutputDelta(_) - | EventMsg::PatchApplyBegin(_) - | EventMsg::PatchApplyEnd(_) - | EventMsg::GetHistoryEntryResponse(_) - | EventMsg::PlanUpdate(_) - | EventMsg::TurnAborted(_) - | EventMsg::ShutdownComplete => { - // For now, we do not do anything extra for these - // events. Note that - // send(codex_event_to_notification(&event)) above has - // already dispatched these events as notifications, - // though we may want to do give different treatment to - // individual events in the future. - } - } - } - Err(e) => { - error!("Codex runtime error: {e}"); - } - } - } -} diff --git a/codex-rs/mcp-server/src/lib.rs b/codex-rs/mcp-server/src/lib.rs index b6dcf8246d..e22df1f9ef 100644 --- a/codex-rs/mcp-server/src/lib.rs +++ b/codex-rs/mcp-server/src/lib.rs @@ -18,16 +18,12 @@ use tracing_subscriber::EnvFilter; mod codex_message_processor; mod codex_tool_config; mod codex_tool_runner; -mod conversation_loop; mod error_code; mod exec_approval; mod json_to_toml; -pub mod mcp_protocol; pub(crate) mod message_processor; mod outgoing_message; mod patch_approval; -pub(crate) mod tool_handlers; -pub mod wire_format; use crate::message_processor::MessageProcessor; use crate::outgoing_message::OutgoingMessage; diff --git a/codex-rs/mcp-server/src/mcp_protocol.rs b/codex-rs/mcp-server/src/mcp_protocol.rs deleted file mode 100644 index 26c6655f3b..0000000000 --- a/codex-rs/mcp-server/src/mcp_protocol.rs +++ /dev/null @@ -1,1054 +0,0 @@ -use codex_core::config_types::SandboxMode; -use codex_core::protocol::AskForApproval; -use codex_core::protocol::EventMsg; -use codex_core::protocol::InputItem; -use serde::Deserialize; -use serde::Serialize; -use strum_macros::Display; -use uuid::Uuid; - -use mcp_types::CallToolResult; -use mcp_types::ContentBlock; -use mcp_types::RequestId; -use mcp_types::TextContent; - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -#[serde(transparent)] -pub struct ConversationId(pub Uuid); - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -#[serde(transparent)] -pub struct MessageId(pub Uuid); - -// Requests -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ToolCallRequest { - #[serde(rename = "jsonrpc")] - pub jsonrpc: &'static str, - pub id: RequestId, - pub method: &'static str, - pub params: ToolCallRequestParams, -} - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -#[serde(tag = "name", content = "arguments", rename_all = "camelCase")] -pub enum ToolCallRequestParams { - ConversationCreate(ConversationCreateArgs), - ConversationStream(ConversationStreamArgs), - ConversationSendMessage(ConversationSendMessageArgs), - ConversationsList(ConversationsListArgs), -} - -impl ToolCallRequestParams { - /// Wrap this request in a JSON-RPC request. - #[allow(dead_code)] - pub fn into_request(self, id: RequestId) -> ToolCallRequest { - ToolCallRequest { - jsonrpc: "2.0", - id, - method: "tools/call", - params: self, - } - } -} - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct ConversationCreateArgs { - pub prompt: String, - pub model: String, - pub cwd: String, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub approval_policy: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub sandbox: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub config: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub profile: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub base_instructions: Option, -} - -/// Optional overrides for an existing conversation's execution context when sending a message. -/// Fields left as `None` inherit the current conversation/session settings. -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct ConversationOverrides { - #[serde(default, skip_serializing_if = "Option::is_none")] - pub model: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub cwd: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub approval_policy: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub sandbox: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub config: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub profile: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub base_instructions: Option, -} - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct ConversationStreamArgs { - pub conversation_id: ConversationId, -} - -/// If omitted, the message continues from the latest turn. -/// Set to resume/edit from an earlier parent message in the thread. -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct ConversationSendMessageArgs { - pub conversation_id: ConversationId, - pub content: Vec, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub parent_message_id: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - #[serde(flatten)] - pub conversation_overrides: Option, -} -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct ConversationsListArgs { - #[serde(default, skip_serializing_if = "Option::is_none")] - pub limit: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub cursor: Option, -} - -// Responses -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct ToolCallResponse { - pub request_id: RequestId, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub is_error: Option, - #[serde(default, skip_serializing_if = "Option::is_none", flatten)] - pub result: Option, -} - -impl From for CallToolResult { - fn from(val: ToolCallResponse) -> Self { - let ToolCallResponse { - request_id: _request_id, - is_error, - result, - } = val; - match result { - Some(res) => match serde_json::to_value(&res) { - Ok(v) => CallToolResult { - content: vec![ContentBlock::TextContent(TextContent { - r#type: "text".to_string(), - text: v.to_string(), - annotations: None, - })], - is_error, - structured_content: Some(v), - }, - Err(e) => CallToolResult { - content: vec![ContentBlock::TextContent(TextContent { - r#type: "text".to_string(), - text: format!("Failed to serialize tool result: {e}"), - annotations: None, - })], - is_error: Some(true), - structured_content: None, - }, - }, - None => CallToolResult { - content: vec![], - is_error, - structured_content: None, - }, - } - } -} - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -#[serde(untagged)] -pub enum ToolCallResponseResult { - ConversationCreate(ConversationCreateResult), - ConversationStream(ConversationStreamResult), - ConversationSendMessage(ConversationSendMessageResult), - ConversationsList(ConversationsListResult), -} - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -#[serde(untagged)] -pub enum ConversationCreateResult { - Ok { - conversation_id: ConversationId, - model: String, - }, - Error { - message: String, - }, -} - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct ConversationStreamResult {} - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -// TODO: remove this status because we have is_error field in the response. -#[serde(tag = "status", rename_all = "camelCase")] -pub enum ConversationSendMessageResult { - Ok, - Error { message: String }, -} - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct ConversationsListResult { - pub conversations: Vec, - #[serde(skip_serializing_if = "Option::is_none")] - pub next_cursor: Option, -} - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct ConversationSummary { - pub conversation_id: ConversationId, - pub title: String, -} - -// Notifications -#[derive(Debug, Clone, Deserialize, Display)] -pub enum ServerNotification { - InitialState(InitialStateNotificationParams), - StreamDisconnected(StreamDisconnectedNotificationParams), - CodexEvent(Box), -} - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct NotificationMeta { - #[serde(skip_serializing_if = "Option::is_none")] - pub conversation_id: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub request_id: Option, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct InitialStateNotificationParams { - #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")] - pub meta: Option, - pub initial_state: InitialStatePayload, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct InitialStatePayload { - #[serde(default)] - pub events: Vec, -} - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct StreamDisconnectedNotificationParams { - #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")] - pub meta: Option, - pub reason: String, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct CodexEventNotificationParams { - #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")] - pub meta: Option, - pub msg: EventMsg, -} - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct CancelNotificationParams { - pub request_id: RequestId, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub reason: Option, -} - -impl Serialize for ServerNotification { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - use serde::ser::SerializeMap; - - let mut map = serializer.serialize_map(Some(2))?; - match self { - ServerNotification::CodexEvent(p) => { - map.serialize_entry("method", &format!("notifications/{}", p.msg))?; - map.serialize_entry("params", p)?; - } - ServerNotification::InitialState(p) => { - map.serialize_entry("method", "notifications/initial_state")?; - map.serialize_entry("params", p)?; - } - ServerNotification::StreamDisconnected(p) => { - map.serialize_entry("method", "notifications/stream_disconnected")?; - map.serialize_entry("params", p)?; - } - } - map.end() - } -} - -#[derive(Debug, Clone, Deserialize, Serialize)] -#[serde(tag = "method", content = "params", rename_all = "camelCase")] -pub enum ClientNotification { - #[serde(rename = "notifications/cancelled")] - Cancelled(CancelNotificationParams), -} - -#[cfg(test)] -mod tests { - use std::path::PathBuf; - - use super::*; - use codex_core::protocol::McpInvocation; - use codex_core::protocol::McpToolCallBeginEvent; - use pretty_assertions::assert_eq; - use serde::Serialize; - use serde_json::Value; - use serde_json::json; - use uuid::uuid; - - fn to_val(v: &T) -> Value { - serde_json::to_value(v).expect("serialize to Value") - } - - // ----- Requests ----- - - #[test] - fn serialize_tool_call_request_params_conversation_create_minimal() { - let req = ToolCallRequestParams::ConversationCreate(ConversationCreateArgs { - prompt: "".into(), - model: "o3".into(), - cwd: "/repo".into(), - approval_policy: None, - sandbox: None, - config: None, - profile: None, - base_instructions: None, - }); - - let observed = to_val(&req.into_request(mcp_types::RequestId::Integer(2))); - let expected = json!({ - "jsonrpc": "2.0", - "id": 2, - "method": "tools/call", - "params": { - "name": "conversationCreate", - "arguments": { - "prompt": "", - "model": "o3", - "cwd": "/repo" - } - } - }); - assert_eq!(observed, expected); - } - - #[test] - fn serialize_tool_call_request_params_conversation_send_message_with_overrides_and_parent_message_id() - { - let req = ToolCallRequestParams::ConversationSendMessage(ConversationSendMessageArgs { - conversation_id: ConversationId(uuid!("d0f6ecbe-84a2-41c1-b23d-b20473b25eab")), - content: vec![ - InputItem::Text { text: "Hi".into() }, - InputItem::Image { - image_url: "https://example.com/cat.jpg".into(), - }, - InputItem::LocalImage { - path: "notes.txt".into(), - }, - ], - parent_message_id: Some(MessageId(uuid!("67e55044-10b1-426f-9247-bb680e5fe0c8"))), - conversation_overrides: Some(ConversationOverrides { - model: Some("o4-mini".into()), - cwd: Some("/workdir".into()), - approval_policy: None, - sandbox: Some(SandboxMode::DangerFullAccess), - config: Some(json!({"temp": 0.2})), - profile: Some("eng".into()), - base_instructions: Some("Be terse".into()), - }), - }); - - let observed = to_val(&req.into_request(mcp_types::RequestId::Integer(2))); - let expected = json!({ - "jsonrpc": "2.0", - "id": 2, - "method": "tools/call", - "params": { - "name": "conversationSendMessage", - "arguments": { - "conversation_id": "d0f6ecbe-84a2-41c1-b23d-b20473b25eab", - "content": [ - { "type": "text", "text": "Hi" }, - { "type": "image", "image_url": "https://example.com/cat.jpg" }, - { "type": "local_image", "path": "notes.txt" } - ], - "parent_message_id": "67e55044-10b1-426f-9247-bb680e5fe0c8", - "model": "o4-mini", - "cwd": "/workdir", - "sandbox": "danger-full-access", - "config": { "temp": 0.2 }, - "profile": "eng", - "base_instructions": "Be terse" - } - } - }); - assert_eq!(observed, expected); - } - - #[test] - fn serialize_tool_call_request_params_conversations_list_with_opts() { - let req = ToolCallRequestParams::ConversationsList(ConversationsListArgs { - limit: Some(50), - cursor: Some("abc".into()), - }); - - let observed = to_val(&req.into_request(RequestId::Integer(2))); - let expected = json!({ - "jsonrpc": "2.0", - "id": 2, - "method": "tools/call", - "params": { - "name": "conversationsList", - "arguments": { - "limit": 50, - "cursor": "abc" - } - } - }); - assert_eq!(observed, expected); - } - - #[test] - fn serialize_tool_call_request_params_conversation_stream() { - let req = ToolCallRequestParams::ConversationStream(ConversationStreamArgs { - conversation_id: ConversationId(uuid!("67e55044-10b1-426f-9247-bb680e5fe0c8")), - }); - - let observed = to_val(&req.into_request(mcp_types::RequestId::Integer(2))); - let expected = json!({ - "jsonrpc": "2.0", - "id": 2, - "method": "tools/call", - "params": { - "name": "conversationStream", - "arguments": { - "conversation_id": "67e55044-10b1-426f-9247-bb680e5fe0c8" - } - } - }); - assert_eq!(observed, expected); - } - - // ----- Message inputs / sources ----- - - #[test] - fn serialize_message_input_image_url() { - let item = InputItem::Image { - image_url: "https://example.com/x.png".into(), - }; - let observed = to_val(&item); - let expected = json!({ - "type": "image", - "image_url": "https://example.com/x.png" - }); - assert_eq!(observed, expected); - } - - #[test] - fn serialize_message_input_local_image_path() { - let url = InputItem::LocalImage { - path: PathBuf::from("https://example.com/a.pdf"), - }; - let id = InputItem::LocalImage { - path: PathBuf::from("file_456"), - }; - let observed_url = to_val(&url); - let expected_url = json!({"type":"local_image","path":"https://example.com/a.pdf"}); - assert_eq!( - observed_url, expected_url, - "LocalImage with URL path should serialize as image_url" - ); - let observed_id = to_val(&id); - let expected_id = json!({"type":"local_image","path":"file_456"}); - assert_eq!( - observed_id, expected_id, - "LocalImage with file id should serialize as image_url" - ); - } - - #[test] - fn serialize_message_input_image_url_without_detail() { - let item = InputItem::Image { - image_url: "https://example.com/x.png".into(), - }; - let observed = to_val(&item); - let expected = json!({ - "type": "image", - "image_url": "https://example.com/x.png" - }); - assert_eq!(observed, expected); - } - - // ----- Responses ----- - - #[test] - fn response_success_conversation_create_full_schema() { - let env = ToolCallResponse { - request_id: RequestId::Integer(1), - is_error: None, - result: Some(ToolCallResponseResult::ConversationCreate( - ConversationCreateResult::Ok { - conversation_id: ConversationId(uuid!("d0f6ecbe-84a2-41c1-b23d-b20473b25eab")), - model: "o3".into(), - }, - )), - }; - let req_id = env.request_id.clone(); - let observed = to_val(&CallToolResult::from(env)); - let expected = json!({ - "content": [ - { "type": "text", "text": "{\"conversation_id\":\"d0f6ecbe-84a2-41c1-b23d-b20473b25eab\",\"model\":\"o3\"}" } - ], - "structuredContent": { - "conversation_id": "d0f6ecbe-84a2-41c1-b23d-b20473b25eab", - "model": "o3" - } - }); - assert_eq!( - observed, expected, - "response (ConversationCreate) must match" - ); - assert_eq!(req_id, RequestId::Integer(1)); - } - - #[test] - fn response_error_conversation_create_full_schema() { - let env = ToolCallResponse { - request_id: RequestId::Integer(2), - is_error: Some(true), - result: Some(ToolCallResponseResult::ConversationCreate( - ConversationCreateResult::Error { - message: "Failed to initialize session".into(), - }, - )), - }; - let req_id = env.request_id.clone(); - let observed = to_val(&CallToolResult::from(env)); - let expected = json!({ - "content": [ - { "type": "text", "text": "{\"message\":\"Failed to initialize session\"}" } - ], - "isError": true, - "structuredContent": { - "message": "Failed to initialize session" - } - }); - assert_eq!( - observed, expected, - "error response (ConversationCreate) must match" - ); - assert_eq!(req_id, RequestId::Integer(2)); - } - - #[test] - fn response_success_conversation_stream_empty_result_object() { - let env = ToolCallResponse { - request_id: RequestId::Integer(2), - is_error: None, - result: Some(ToolCallResponseResult::ConversationStream( - ConversationStreamResult {}, - )), - }; - let req_id = env.request_id.clone(); - let observed = to_val(&CallToolResult::from(env)); - let expected = json!({ - "content": [ { "type": "text", "text": "{}" } ], - "structuredContent": {} - }); - assert_eq!( - observed, expected, - "response (ConversationStream) must have empty object result" - ); - assert_eq!(req_id, RequestId::Integer(2)); - } - - #[test] - fn response_success_send_message_accepted_full_schema() { - let env = ToolCallResponse { - request_id: RequestId::Integer(3), - is_error: None, - result: Some(ToolCallResponseResult::ConversationSendMessage( - ConversationSendMessageResult::Ok, - )), - }; - let req_id = env.request_id.clone(); - let observed = to_val(&CallToolResult::from(env)); - let expected = json!({ - "content": [ { "type": "text", "text": "{\"status\":\"ok\"}" } ], - "structuredContent": { "status": "ok" } - }); - assert_eq!( - observed, expected, - "response (ConversationSendMessageAccepted) must match" - ); - assert_eq!(req_id, RequestId::Integer(3)); - } - - #[test] - fn response_success_conversations_list_with_next_cursor_full_schema() { - let env = ToolCallResponse { - request_id: RequestId::Integer(4), - is_error: None, - result: Some(ToolCallResponseResult::ConversationsList( - ConversationsListResult { - conversations: vec![ConversationSummary { - conversation_id: ConversationId(uuid!( - "67e55044-10b1-426f-9247-bb680e5fe0c8" - )), - title: "Refactor config loader".into(), - }], - next_cursor: Some("next123".into()), - }, - )), - }; - let req_id = env.request_id.clone(); - let observed = to_val(&CallToolResult::from(env)); - let expected = json!({ - "content": [ - { "type": "text", "text": "{\"conversations\":[{\"conversation_id\":\"67e55044-10b1-426f-9247-bb680e5fe0c8\",\"title\":\"Refactor config loader\"}],\"next_cursor\":\"next123\"}" } - ], - "structuredContent": { - "conversations": [ - { - "conversation_id": "67e55044-10b1-426f-9247-bb680e5fe0c8", - "title": "Refactor config loader" - } - ], - "next_cursor": "next123" - } - }); - assert_eq!( - observed, expected, - "response (ConversationsList with cursor) must match" - ); - assert_eq!(req_id, RequestId::Integer(4)); - } - - #[test] - fn response_error_only_is_error_and_request_id_string() { - let env = ToolCallResponse { - request_id: RequestId::Integer(4), - is_error: Some(true), - result: None, - }; - let req_id = env.request_id.clone(); - let observed = to_val(&CallToolResult::from(env)); - let expected = json!({ - "content": [], - "isError": true - }); - assert_eq!( - observed, expected, - "error response must omit `result` and include `isError`" - ); - assert_eq!(req_id, RequestId::Integer(4)); - } - - // ----- Notifications ----- - - #[test] - fn serialize_notification_initial_state_minimal() { - let params = InitialStateNotificationParams { - meta: Some(NotificationMeta { - conversation_id: Some(ConversationId(uuid!( - "67e55044-10b1-426f-9247-bb680e5fe0c8" - ))), - request_id: Some(RequestId::Integer(44)), - }), - initial_state: InitialStatePayload { - events: vec![ - CodexEventNotificationParams { - meta: None, - msg: EventMsg::TaskStarted, - }, - CodexEventNotificationParams { - meta: None, - msg: EventMsg::AgentMessageDelta( - codex_core::protocol::AgentMessageDeltaEvent { - delta: "Loading...".into(), - }, - ), - }, - ], - }, - }; - - let observed = to_val(&ServerNotification::InitialState(params.clone())); - let expected = json!({ - "method": "notifications/initial_state", - "params": { - "_meta": { - "conversationId": "67e55044-10b1-426f-9247-bb680e5fe0c8", - "requestId": 44 - }, - "initial_state": { - "events": [ - { "msg": { "type": "task_started" } }, - { "msg": { "type": "agent_message_delta", "delta": "Loading..." } } - ] - } - } - }); - assert_eq!(observed, expected); - } - - #[test] - fn serialize_notification_initial_state_omits_empty_events_full_json() { - let params = InitialStateNotificationParams { - meta: None, - initial_state: InitialStatePayload { events: vec![] }, - }; - - let observed = to_val(&ServerNotification::InitialState(params)); - let expected = json!({ - "method": "notifications/initial_state", - "params": { - "initial_state": { "events": [] } - } - }); - assert_eq!(observed, expected); - } - - #[test] - fn serialize_notification_stream_disconnected() { - let params = StreamDisconnectedNotificationParams { - meta: Some(NotificationMeta { - conversation_id: Some(ConversationId(uuid!( - "67e55044-10b1-426f-9247-bb680e5fe0c8" - ))), - request_id: None, - }), - reason: "New stream() took over".into(), - }; - - let observed = to_val(&ServerNotification::StreamDisconnected(params)); - let expected = json!({ - "method": "notifications/stream_disconnected", - "params": { - "_meta": { "conversationId": "67e55044-10b1-426f-9247-bb680e5fe0c8" }, - "reason": "New stream() took over" - } - }); - assert_eq!(observed, expected); - } - - #[test] - fn serialize_notification_codex_event_uses_eventmsg_type_in_method() { - let params = CodexEventNotificationParams { - meta: Some(NotificationMeta { - conversation_id: Some(ConversationId(uuid!( - "67e55044-10b1-426f-9247-bb680e5fe0c8" - ))), - request_id: Some(RequestId::Integer(44)), - }), - msg: EventMsg::AgentMessage(codex_core::protocol::AgentMessageEvent { - message: "hi".into(), - }), - }; - - let observed = to_val(&ServerNotification::CodexEvent(Box::new(params))); - let expected = json!({ - "method": "notifications/agent_message", - "params": { - "_meta": { - "conversationId": "67e55044-10b1-426f-9247-bb680e5fe0c8", - "requestId": 44 - }, - "msg": { "type": "agent_message", "message": "hi" } - } - }); - assert_eq!(observed, expected); - } - - #[test] - fn serialize_notification_codex_event_task_started_full_json() { - let params = CodexEventNotificationParams { - meta: Some(NotificationMeta { - conversation_id: Some(ConversationId(uuid!( - "67e55044-10b1-426f-9247-bb680e5fe0c8" - ))), - request_id: Some(RequestId::Integer(7)), - }), - msg: EventMsg::TaskStarted, - }; - - let observed = to_val(&ServerNotification::CodexEvent(Box::new(params))); - let expected = json!({ - "method": "notifications/task_started", - "params": { - "_meta": { - "conversationId": "67e55044-10b1-426f-9247-bb680e5fe0c8", - "requestId": 7 - }, - "msg": { "type": "task_started" } - } - }); - assert_eq!(observed, expected); - } - - #[test] - fn serialize_notification_codex_event_agent_message_delta_full_json() { - let params = CodexEventNotificationParams { - meta: None, - msg: EventMsg::AgentMessageDelta(codex_core::protocol::AgentMessageDeltaEvent { - delta: "stream...".into(), - }), - }; - - let observed = to_val(&ServerNotification::CodexEvent(Box::new(params))); - let expected = json!({ - "method": "notifications/agent_message_delta", - "params": { - "msg": { "type": "agent_message_delta", "delta": "stream..." } - } - }); - assert_eq!(observed, expected); - } - - #[test] - fn serialize_notification_codex_event_agent_message_full_json() { - let params = CodexEventNotificationParams { - meta: Some(NotificationMeta { - conversation_id: Some(ConversationId(uuid!( - "67e55044-10b1-426f-9247-bb680e5fe0c8" - ))), - request_id: Some(RequestId::Integer(44)), - }), - msg: EventMsg::AgentMessage(codex_core::protocol::AgentMessageEvent { - message: "hi".into(), - }), - }; - - let observed = to_val(&ServerNotification::CodexEvent(Box::new(params))); - let expected = json!({ - "method": "notifications/agent_message", - "params": { - "_meta": { - "conversationId": "67e55044-10b1-426f-9247-bb680e5fe0c8", - "requestId": 44 - }, - "msg": { "type": "agent_message", "message": "hi" } - } - }); - assert_eq!(observed, expected); - } - - #[test] - fn serialize_notification_codex_event_agent_reasoning_full_json() { - let params = CodexEventNotificationParams { - meta: None, - msg: EventMsg::AgentReasoning(codex_core::protocol::AgentReasoningEvent { - text: "thinking…".into(), - }), - }; - - let observed = to_val(&ServerNotification::CodexEvent(Box::new(params))); - let expected = json!({ - "method": "notifications/agent_reasoning", - "params": { - "msg": { "type": "agent_reasoning", "text": "thinking…" } - } - }); - assert_eq!(observed, expected); - } - - #[test] - fn serialize_notification_codex_event_token_count_full_json() { - let usage = codex_core::protocol::TokenUsage { - input_tokens: 10, - cached_input_tokens: Some(2), - output_tokens: 5, - reasoning_output_tokens: Some(1), - total_tokens: 16, - }; - let params = CodexEventNotificationParams { - meta: None, - msg: EventMsg::TokenCount(usage), - }; - - let observed = to_val(&ServerNotification::CodexEvent(Box::new(params))); - let expected = json!({ - "method": "notifications/token_count", - "params": { - "msg": { - "type": "token_count", - "input_tokens": 10, - "cached_input_tokens": 2, - "output_tokens": 5, - "reasoning_output_tokens": 1, - "total_tokens": 16 - } - } - }); - assert_eq!(observed, expected); - } - - #[test] - fn serialize_notification_codex_event_session_configured_full_json() { - let params = CodexEventNotificationParams { - meta: Some(NotificationMeta { - conversation_id: Some(ConversationId(uuid!( - "67e55044-10b1-426f-9247-bb680e5fe0c8" - ))), - request_id: None, - }), - msg: EventMsg::SessionConfigured(codex_core::protocol::SessionConfiguredEvent { - session_id: uuid!("67e55044-10b1-426f-9247-bb680e5fe0c8"), - model: "codex-mini-latest".into(), - history_log_id: 42, - history_entry_count: 3, - }), - }; - - let observed = to_val(&ServerNotification::CodexEvent(Box::new(params))); - let expected = json!({ - "method": "notifications/session_configured", - "params": { - "_meta": { "conversationId": "67e55044-10b1-426f-9247-bb680e5fe0c8" }, - "msg": { - "type": "session_configured", - "session_id": "67e55044-10b1-426f-9247-bb680e5fe0c8", - "model": "codex-mini-latest", - "history_log_id": 42, - "history_entry_count": 3 - } - } - }); - assert_eq!(observed, expected); - } - - #[test] - fn serialize_notification_codex_event_exec_command_begin_full_json() { - let params = CodexEventNotificationParams { - meta: None, - msg: EventMsg::ExecCommandBegin(codex_core::protocol::ExecCommandBeginEvent { - call_id: "c1".into(), - command: vec!["bash".into(), "-lc".into(), "echo hi".into()], - cwd: std::path::PathBuf::from("/work"), - parsed_cmd: vec![], - }), - }; - - let observed = to_val(&ServerNotification::CodexEvent(Box::new(params))); - let expected = json!({ - "method": "notifications/exec_command_begin", - "params": { - "msg": { - "type": "exec_command_begin", - "call_id": "c1", - "command": ["bash", "-lc", "echo hi"], - "cwd": "/work", - "parsed_cmd": [] - } - } - }); - assert_eq!(observed, expected); - } - - #[test] - fn serialize_notification_codex_event_mcp_tool_call_begin_full_json() { - let params = CodexEventNotificationParams { - meta: None, - msg: EventMsg::McpToolCallBegin(McpToolCallBeginEvent { - call_id: "m1".into(), - invocation: McpInvocation { - server: "calc".into(), - tool: "add".into(), - arguments: Some(json!({"a":1,"b":2})), - }, - }), - }; - - let observed = to_val(&ServerNotification::CodexEvent(Box::new(params))); - let expected = json!({ - "method": "notifications/mcp_tool_call_begin", - "params": { - "msg": { - "type": "mcp_tool_call_begin", - "call_id": "m1", - "invocation": { - "server": "calc", - "tool": "add", - "arguments": { "a": 1, "b": 2 } - } - } - } - }); - assert_eq!(observed, expected); - } - - #[test] - fn serialize_notification_codex_event_patch_apply_end_full_json() { - let params = CodexEventNotificationParams { - meta: None, - msg: EventMsg::PatchApplyEnd(codex_core::protocol::PatchApplyEndEvent { - call_id: "p1".into(), - stdout: "ok".into(), - stderr: "".into(), - success: true, - }), - }; - - let observed = to_val(&ServerNotification::CodexEvent(Box::new(params))); - let expected = json!({ - "method": "notifications/patch_apply_end", - "params": { - "msg": { - "type": "patch_apply_end", - "call_id": "p1", - "stdout": "ok", - "stderr": "", - "success": true - } - } - }); - assert_eq!(observed, expected); - } - - // ----- Cancelled notifications ----- - - #[test] - fn serialize_notification_cancelled_with_reason_full_json() { - let params = CancelNotificationParams { - request_id: RequestId::String("r-123".into()), - reason: Some("user_cancelled".into()), - }; - - let observed = to_val(&ClientNotification::Cancelled(params)); - let expected = json!({ - "method": "notifications/cancelled", - "params": { - "requestId": "r-123", - "reason": "user_cancelled" - } - }); - assert_eq!(observed, expected); - } - - #[test] - fn serialize_notification_cancelled_without_reason_full_json() { - let params = CancelNotificationParams { - request_id: RequestId::Integer(77), - reason: None, - }; - - let observed = to_val(&ClientNotification::Cancelled(params)); - - // Check exact structure: reason must be omitted. - assert_eq!(observed["method"], "notifications/cancelled"); - assert_eq!(observed["params"]["requestId"], 77); - assert!( - observed["params"].get("reason").is_none(), - "reason must be omitted when None" - ); - } -} diff --git a/codex-rs/mcp-server/src/message_processor.rs b/codex-rs/mcp-server/src/message_processor.rs index 763e51bf81..1ddcc6bc28 100644 --- a/codex-rs/mcp-server/src/message_processor.rs +++ b/codex-rs/mcp-server/src/message_processor.rs @@ -1,5 +1,4 @@ use std::collections::HashMap; -use std::collections::HashSet; use std::path::PathBuf; use std::sync::Arc; @@ -9,18 +8,12 @@ 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::mcp_protocol::ToolCallRequestParams; -use crate::mcp_protocol::ToolCallResponse; -use crate::mcp_protocol::ToolCallResponseResult; use crate::outgoing_message::OutgoingMessageSender; -use crate::tool_handlers::create_conversation::handle_create_conversation; -use crate::tool_handlers::send_message::handle_send_message; -use crate::wire_format::ClientRequest; +use codex_protocol::mcp_protocol::ClientRequest; use codex_core::ConversationManager; use codex_core::config::Config as CodexConfig; use codex_core::protocol::Submission; -use mcp_types::CallToolRequest; use mcp_types::CallToolRequestParams; use mcp_types::CallToolResult; use mcp_types::ClientRequest as McpClientRequest; @@ -48,7 +41,6 @@ pub(crate) struct MessageProcessor { codex_linux_sandbox_exe: Option, conversation_manager: Arc, running_requests_id_to_codex_uuid: Arc>>, - running_session_ids: Arc>>, } impl MessageProcessor { @@ -72,22 +64,9 @@ impl MessageProcessor { codex_linux_sandbox_exe, conversation_manager, running_requests_id_to_codex_uuid: Arc::new(Mutex::new(HashMap::new())), - running_session_ids: Arc::new(Mutex::new(HashSet::new())), } } - pub(crate) fn get_conversation_manager(&self) -> &ConversationManager { - &self.conversation_manager - } - - pub(crate) fn outgoing(&self) -> Arc { - self.outgoing.clone() - } - - pub(crate) fn running_session_ids(&self) -> Arc>> { - self.running_session_ids.clone() - } - pub(crate) async fn process_request(&mut self, request: JSONRPCRequest) { if let Ok(request_json) = serde_json::to_value(request.clone()) && let Ok(codex_request) = serde_json::from_value::(request_json) @@ -341,14 +320,6 @@ impl MessageProcessor { params: ::Params, ) { tracing::info!("tools/call -> params: {:?}", params); - // Serialize params into JSON and try to parse as new type - if let Ok(new_params) = - serde_json::to_value(¶ms).and_then(serde_json::from_value::) - { - // New tool call matched → forward - self.handle_new_tool_calls(id, new_params).await; - return; - } let CallToolRequestParams { name, arguments } = params; match name.as_str() { @@ -372,30 +343,6 @@ impl MessageProcessor { } } } - async fn handle_new_tool_calls(&self, request_id: RequestId, params: ToolCallRequestParams) { - match params { - ToolCallRequestParams::ConversationCreate(args) => { - handle_create_conversation(self, request_id, args).await; - } - ToolCallRequestParams::ConversationSendMessage(args) => { - handle_send_message(self, request_id, args).await; - } - _ => { - let result = CallToolResult { - content: vec![ContentBlock::TextContent(TextContent { - r#type: "text".to_string(), - text: "Unknown tool".to_string(), - annotations: None, - })], - is_error: Some(true), - structured_content: None, - }; - self.send_response::(request_id, result) - .await; - } - } - } - async fn handle_tool_call_codex(&self, id: RequestId, arguments: Option) { let (initial_prompt, config): (String, CodexConfig) = match arguments { Some(json_val) => match serde_json::from_value::(json_val) { @@ -692,20 +639,4 @@ impl MessageProcessor { ) { tracing::info!("notifications/message -> params: {:?}", params); } - - pub(crate) async fn send_response_with_optional_error( - &self, - id: RequestId, - message: Option, - error: Option, - ) { - let response = ToolCallResponse { - request_id: id.clone(), - is_error: error, - result: message, - }; - let result: CallToolResult = response.into(); - self.send_response::(id.clone(), result) - .await; - } } diff --git a/codex-rs/mcp-server/src/outgoing_message.rs b/codex-rs/mcp-server/src/outgoing_message.rs index f408ca0a45..c5e51a3494 100644 --- a/codex-rs/mcp-server/src/outgoing_message.rs +++ b/codex-rs/mcp-server/src/outgoing_message.rs @@ -119,9 +119,6 @@ impl OutgoingMessageSender { params: Some(params.clone()), }) .await; - - self.send_event_as_notification_new_schema(event, Some(params.clone())) - .await; } pub(crate) async fn send_notification(&self, notification: OutgoingNotification) { @@ -129,19 +126,6 @@ impl OutgoingMessageSender { let _ = self.sender.send(outgoing_message).await; } - // should be backwards compatible. - // it will replace send_event_as_notification eventually. - async fn send_event_as_notification_new_schema( - &self, - event: &Event, - params: Option, - ) { - let outgoing_message = OutgoingMessage::Notification(OutgoingNotification { - method: event.msg.to_string(), - params, - }); - let _ = self.sender.send(outgoing_message).await; - } pub(crate) async fn send_error(&self, id: RequestId, error: JSONRPCErrorError) { let outgoing_message = OutgoingMessage::Error(OutgoingError { id, error }); let _ = self.sender.send(outgoing_message).await; @@ -281,17 +265,6 @@ mod tests { panic!("Event must serialize"); }; assert_eq!(params, Some(expected_params.clone())); - - let result2 = outgoing_rx.recv().await.unwrap(); - let OutgoingMessage::Notification(OutgoingNotification { - method: method2, - params: params2, - }) = result2 - else { - panic!("expected Notification for second message"); - }; - assert_eq!(method2, event.msg.to_string()); - assert_eq!(params2, Some(expected_params)); } #[tokio::test] @@ -336,16 +309,5 @@ mod tests { } }); assert_eq!(params.unwrap(), expected_params); - - let result2 = outgoing_rx.recv().await.unwrap(); - let OutgoingMessage::Notification(OutgoingNotification { - method: method2, - params: params2, - }) = result2 - else { - panic!("expected Notification for second message"); - }; - assert_eq!(method2, event.msg.to_string()); - assert_eq!(params2.unwrap(), expected_params); } } diff --git a/codex-rs/mcp-server/src/tool_handlers/create_conversation.rs b/codex-rs/mcp-server/src/tool_handlers/create_conversation.rs deleted file mode 100644 index eee2e1d5f4..0000000000 --- a/codex-rs/mcp-server/src/tool_handlers/create_conversation.rs +++ /dev/null @@ -1,127 +0,0 @@ -use std::path::PathBuf; - -use codex_core::NewConversation; -use codex_core::config::Config as CodexConfig; -use codex_core::config::ConfigOverrides; -use mcp_types::RequestId; - -use crate::conversation_loop::run_conversation_loop; -use crate::json_to_toml::json_to_toml; -use crate::mcp_protocol::ConversationCreateArgs; -use crate::mcp_protocol::ConversationCreateResult; -use crate::mcp_protocol::ConversationId; -use crate::mcp_protocol::ToolCallResponseResult; -use crate::message_processor::MessageProcessor; - -pub(crate) async fn handle_create_conversation( - message_processor: &MessageProcessor, - id: RequestId, - args: ConversationCreateArgs, -) { - // Build ConfigOverrides from args - let ConversationCreateArgs { - prompt: _, // not used here; creation only establishes the session - model, - cwd, - approval_policy, - sandbox, - config, - profile, - base_instructions, - } = args; - - // Convert config overrides JSON into CLI-style TOML overrides - let cli_overrides: Vec<(String, toml::Value)> = match config { - Some(v) => match v.as_object() { - Some(map) => map - .into_iter() - .map(|(k, v)| (k.clone(), json_to_toml(v.clone()))) - .collect(), - None => Vec::new(), - }, - None => Vec::new(), - }; - - let overrides = ConfigOverrides { - model: Some(model.clone()), - cwd: Some(PathBuf::from(cwd)), - approval_policy, - sandbox_mode: sandbox, - model_provider: None, - config_profile: profile, - codex_linux_sandbox_exe: None, - base_instructions, - include_plan_tool: None, - include_apply_patch_tool: None, - disable_response_storage: None, - show_raw_agent_reasoning: None, - }; - - let cfg: CodexConfig = match CodexConfig::load_with_cli_overrides(cli_overrides, overrides) { - Ok(cfg) => cfg, - Err(e) => { - message_processor - .send_response_with_optional_error( - id, - Some(ToolCallResponseResult::ConversationCreate( - ConversationCreateResult::Error { - message: format!("Failed to load config: {e}"), - }, - )), - Some(true), - ) - .await; - return; - } - }; - - // Initialize Codex session via server API - let NewConversation { - conversation_id: session_id, - conversation, - session_configured, - } = match message_processor - .get_conversation_manager() - .new_conversation(cfg) - .await - { - Ok(conv) => conv, - Err(e) => { - message_processor - .send_response_with_optional_error( - id, - Some(ToolCallResponseResult::ConversationCreate( - ConversationCreateResult::Error { - message: format!("Failed to initialize session: {e}"), - }, - )), - Some(true), - ) - .await; - return; - } - }; - - let effective_model = session_configured.model.clone(); - - // Run the conversation loop in the background so this request can return immediately. - let outgoing = message_processor.outgoing(); - let spawn_id = id.clone(); - tokio::spawn(async move { - run_conversation_loop(conversation.clone(), outgoing, spawn_id).await; - }); - - // Reply with the new conversation id and effective model - message_processor - .send_response_with_optional_error( - id, - Some(ToolCallResponseResult::ConversationCreate( - ConversationCreateResult::Ok { - conversation_id: ConversationId(session_id), - model: effective_model, - }, - )), - Some(false), - ) - .await; -} diff --git a/codex-rs/mcp-server/src/tool_handlers/send_message.rs b/codex-rs/mcp-server/src/tool_handlers/send_message.rs deleted file mode 100644 index 985854f852..0000000000 --- a/codex-rs/mcp-server/src/tool_handlers/send_message.rs +++ /dev/null @@ -1,114 +0,0 @@ -use codex_core::protocol::Op; -use codex_core::protocol::Submission; -use mcp_types::RequestId; - -use crate::mcp_protocol::ConversationSendMessageArgs; -use crate::mcp_protocol::ConversationSendMessageResult; -use crate::mcp_protocol::ToolCallResponseResult; -use crate::message_processor::MessageProcessor; - -pub(crate) async fn handle_send_message( - message_processor: &MessageProcessor, - id: RequestId, - arguments: ConversationSendMessageArgs, -) { - let ConversationSendMessageArgs { - conversation_id, - content: items, - parent_message_id: _, - conversation_overrides: _, - } = arguments; - - if items.is_empty() { - message_processor - .send_response_with_optional_error( - id, - Some(ToolCallResponseResult::ConversationSendMessage( - ConversationSendMessageResult::Error { - message: "No content items provided".to_string(), - }, - )), - Some(true), - ) - .await; - return; - } - - let session_id = conversation_id.0; - let Ok(codex) = message_processor - .get_conversation_manager() - .get_conversation(session_id) - .await - else { - message_processor - .send_response_with_optional_error( - id, - Some(ToolCallResponseResult::ConversationSendMessage( - ConversationSendMessageResult::Error { - message: "Session does not exist".to_string(), - }, - )), - Some(true), - ) - .await; - return; - }; - - let running = { - let running_sessions = message_processor.running_session_ids(); - let mut running_sessions = running_sessions.lock().await; - !running_sessions.insert(session_id) - }; - - if running { - message_processor - .send_response_with_optional_error( - id, - Some(ToolCallResponseResult::ConversationSendMessage( - ConversationSendMessageResult::Error { - message: "Session is already running".to_string(), - }, - )), - Some(true), - ) - .await; - return; - } - - let request_id_string = match &id { - RequestId::String(s) => s.clone(), - RequestId::Integer(i) => i.to_string(), - }; - - let submit_res = codex - .submit_with_id(Submission { - id: request_id_string, - op: Op::UserInput { items }, - }) - .await; - - if let Err(e) = submit_res { - message_processor - .send_response_with_optional_error( - id, - Some(ToolCallResponseResult::ConversationSendMessage( - ConversationSendMessageResult::Error { - message: format!("Failed to submit user input: {e}"), - }, - )), - Some(true), - ) - .await; - return; - } - - message_processor - .send_response_with_optional_error( - id, - Some(ToolCallResponseResult::ConversationSendMessage( - ConversationSendMessageResult::Ok, - )), - Some(false), - ) - .await; -} diff --git a/codex-rs/mcp-server/tests/codex_message_processor_flow.rs b/codex-rs/mcp-server/tests/codex_message_processor_flow.rs index 5b89f3fe42..092b135291 100644 --- a/codex-rs/mcp-server/tests/codex_message_processor_flow.rs +++ b/codex-rs/mcp-server/tests/codex_message_processor_flow.rs @@ -5,26 +5,26 @@ use codex_core::protocol::SandboxPolicy; use codex_core::protocol_config_types::ReasoningEffort; use codex_core::protocol_config_types::ReasoningSummary; use codex_core::spawn::CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR; -use codex_mcp_server::wire_format::AddConversationListenerParams; -use codex_mcp_server::wire_format::AddConversationSubscriptionResponse; -use codex_mcp_server::wire_format::EXEC_COMMAND_APPROVAL_METHOD; -use codex_mcp_server::wire_format::NewConversationParams; -use codex_mcp_server::wire_format::NewConversationResponse; -use codex_mcp_server::wire_format::RemoveConversationListenerParams; -use codex_mcp_server::wire_format::RemoveConversationSubscriptionResponse; -use codex_mcp_server::wire_format::SendUserMessageParams; -use codex_mcp_server::wire_format::SendUserMessageResponse; -use codex_mcp_server::wire_format::SendUserTurnParams; -use codex_mcp_server::wire_format::SendUserTurnResponse; +use codex_protocol::mcp_protocol::AddConversationListenerParams; +use codex_protocol::mcp_protocol::AddConversationSubscriptionResponse; +use codex_protocol::mcp_protocol::EXEC_COMMAND_APPROVAL_METHOD; +use codex_protocol::mcp_protocol::NewConversationParams; +use codex_protocol::mcp_protocol::NewConversationResponse; +use codex_protocol::mcp_protocol::RemoveConversationListenerParams; +use codex_protocol::mcp_protocol::RemoveConversationSubscriptionResponse; +use codex_protocol::mcp_protocol::SendUserMessageParams; +use codex_protocol::mcp_protocol::SendUserMessageResponse; +use codex_protocol::mcp_protocol::SendUserTurnParams; +use codex_protocol::mcp_protocol::SendUserTurnResponse; use mcp_test_support::McpProcess; use mcp_test_support::create_final_assistant_message_sse_response; use mcp_test_support::create_mock_chat_completions_server; use mcp_test_support::create_shell_sse_response; +use mcp_test_support::to_response; use mcp_types::JSONRPCNotification; use mcp_types::JSONRPCResponse; use mcp_types::RequestId; use pretty_assertions::assert_eq; -use serde::de::DeserializeOwned; use std::env; use tempfile::TempDir; use tokio::time::timeout; @@ -113,7 +113,7 @@ async fn test_codex_jsonrpc_conversation_flow() { let send_user_id = mcp .send_send_user_message_request(SendUserMessageParams { conversation_id, - items: vec![codex_mcp_server::wire_format::InputItem::Text { + items: vec![codex_protocol::mcp_protocol::InputItem::Text { text: "text".to_string(), }], }) @@ -168,12 +168,6 @@ async fn test_codex_jsonrpc_conversation_flow() { to_response(remove_listener_resp).expect("deserialize removeConversationListener response"); } -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) -} - #[tokio::test(flavor = "multi_thread", worker_threads = 4)] async fn test_send_user_turn_changes_approval_policy_behavior() { if env::var(CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR).is_ok() { @@ -269,7 +263,7 @@ async fn test_send_user_turn_changes_approval_policy_behavior() { let send_user_id = mcp .send_send_user_message_request(SendUserMessageParams { conversation_id, - items: vec![codex_mcp_server::wire_format::InputItem::Text { + items: vec![codex_protocol::mcp_protocol::InputItem::Text { text: "run python".to_string(), }], }) @@ -317,7 +311,7 @@ async fn test_send_user_turn_changes_approval_policy_behavior() { let send_turn_id = mcp .send_send_user_turn_request(SendUserTurnParams { conversation_id, - items: vec![codex_mcp_server::wire_format::InputItem::Text { + items: vec![codex_protocol::mcp_protocol::InputItem::Text { text: "run python again".to_string(), }], cwd: working_directory.clone(), diff --git a/codex-rs/mcp-server/tests/common/Cargo.toml b/codex-rs/mcp-server/tests/common/Cargo.toml index 420b97811d..a9b4ddfbdd 100644 --- a/codex-rs/mcp-server/tests/common/Cargo.toml +++ b/codex-rs/mcp-server/tests/common/Cargo.toml @@ -1,7 +1,7 @@ [package] +edition = "2024" name = "mcp_test_support" version = { workspace = true } -edition = "2024" [lib] path = "lib.rs" @@ -9,10 +9,12 @@ path = "lib.rs" [dependencies] anyhow = "1" assert_cmd = "2" -codex-mcp-server = { path = "../.." } codex-core = { path = "../../../core" } +codex-mcp-server = { path = "../.." } +codex-protocol = { path = "../../../protocol" } mcp-types = { path = "../../../mcp-types" } pretty_assertions = "1.4.1" +serde = { version = "1" } serde_json = "1" shlex = "1.3.0" tempfile = "3" @@ -22,5 +24,5 @@ tokio = { version = "1", features = [ "process", "rt-multi-thread", ] } +uuid = { version = "1", features = ["serde", "v4"] } wiremock = "0.6" -uuid = { version = "1", features = ["serde", "v4"] } \ No newline at end of file diff --git a/codex-rs/mcp-server/tests/common/lib.rs b/codex-rs/mcp-server/tests/common/lib.rs index b338e2e8ce..d088b184ea 100644 --- a/codex-rs/mcp-server/tests/common/lib.rs +++ b/codex-rs/mcp-server/tests/common/lib.rs @@ -3,7 +3,15 @@ mod mock_model_server; mod responses; 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_sse_response; +use serde::de::DeserializeOwned; + +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 dc7833441c..6793dcaf08 100644 --- a/codex-rs/mcp-server/tests/common/mcp_process.rs +++ b/codex-rs/mcp-server/tests/common/mcp_process.rs @@ -11,18 +11,13 @@ use tokio::process::ChildStdout; use anyhow::Context; use assert_cmd::prelude::*; -use codex_core::protocol::InputItem; use codex_mcp_server::CodexToolCallParam; -use codex_mcp_server::CodexToolCallReplyParam; -use codex_mcp_server::mcp_protocol::ConversationCreateArgs; -use codex_mcp_server::mcp_protocol::ConversationId; -use codex_mcp_server::mcp_protocol::ConversationSendMessageArgs; -use codex_mcp_server::mcp_protocol::ToolCallRequestParams; -use codex_mcp_server::wire_format::AddConversationListenerParams; -use codex_mcp_server::wire_format::NewConversationParams; -use codex_mcp_server::wire_format::RemoveConversationListenerParams; -use codex_mcp_server::wire_format::SendUserMessageParams; -use codex_mcp_server::wire_format::SendUserTurnParams; +use codex_protocol::mcp_protocol::AddConversationListenerParams; +use codex_protocol::mcp_protocol::InterruptConversationParams; +use codex_protocol::mcp_protocol::NewConversationParams; +use codex_protocol::mcp_protocol::RemoveConversationListenerParams; +use codex_protocol::mcp_protocol::SendUserMessageParams; +use codex_protocol::mcp_protocol::SendUserTurnParams; use mcp_types::CallToolRequestParams; use mcp_types::ClientCapabilities; @@ -40,7 +35,6 @@ use pretty_assertions::assert_eq; use serde_json::json; use std::process::Command as StdCommand; use tokio::process::Command; -use uuid::Uuid; pub struct McpProcess { next_request_id: AtomicI64, @@ -167,83 +161,6 @@ impl McpProcess { .await } - pub async fn send_codex_reply_tool_call( - &mut self, - session_id: &str, - prompt: &str, - ) -> anyhow::Result { - let codex_tool_call_params = CallToolRequestParams { - name: "codex-reply".to_string(), - arguments: Some(serde_json::to_value(CodexToolCallReplyParam { - prompt: prompt.to_string(), - session_id: session_id.to_string(), - })?), - }; - self.send_request( - mcp_types::CallToolRequest::METHOD, - Some(serde_json::to_value(codex_tool_call_params)?), - ) - .await - } - - pub async fn send_user_message_tool_call( - &mut self, - message: &str, - session_id: &str, - ) -> anyhow::Result { - let params = ToolCallRequestParams::ConversationSendMessage(ConversationSendMessageArgs { - conversation_id: ConversationId(Uuid::parse_str(session_id)?), - content: vec![InputItem::Text { - text: message.to_string(), - }], - parent_message_id: None, - conversation_overrides: None, - }); - self.send_request( - mcp_types::CallToolRequest::METHOD, - Some(serde_json::to_value(params)?), - ) - .await - } - - pub async fn send_conversation_create_tool_call( - &mut self, - prompt: &str, - model: &str, - cwd: &str, - ) -> anyhow::Result { - let params = ToolCallRequestParams::ConversationCreate(ConversationCreateArgs { - prompt: prompt.to_string(), - model: model.to_string(), - cwd: cwd.to_string(), - approval_policy: None, - sandbox: None, - config: None, - profile: None, - base_instructions: None, - }); - self.send_request( - mcp_types::CallToolRequest::METHOD, - Some(serde_json::to_value(params)?), - ) - .await - } - - pub async fn send_conversation_create_with_args( - &mut self, - args: ConversationCreateArgs, - ) -> anyhow::Result { - let params = ToolCallRequestParams::ConversationCreate(args); - self.send_request( - mcp_types::CallToolRequest::METHOD, - Some(serde_json::to_value(params)?), - ) - .await - } - - // --------------------------------------------------------------------- - // Codex JSON-RPC (non-tool) helpers - // --------------------------------------------------------------------- /// Send a `newConversation` JSON-RPC request. pub async fn send_new_conversation_request( &mut self, @@ -291,6 +208,15 @@ impl McpProcess { self.send_request("sendUserTurn", params).await } + /// Send a `interruptConversation` JSON-RPC request. + pub async fn send_interrupt_conversation_request( + &mut self, + params: InterruptConversationParams, + ) -> anyhow::Result { + let params = Some(serde_json::to_value(params)?); + self.send_request("interruptConversation", params).await + } + async fn send_request( &mut self, method: &str, @@ -335,6 +261,7 @@ impl McpProcess { let message = serde_json::from_str::(&line)?; Ok(message) } + pub async fn read_stream_until_request_message(&mut self) -> anyhow::Result { loop { let message = self.read_jsonrpc_message().await?; @@ -384,6 +311,33 @@ impl McpProcess { } } + pub async fn read_stream_until_error_message( + &mut self, + request_id: RequestId, + ) -> anyhow::Result { + loop { + let message = self.read_jsonrpc_message().await?; + eprint!("message: {message:?}"); + + match message { + JSONRPCMessage::Notification(_) => { + eprintln!("notification: {message:?}"); + } + JSONRPCMessage::Request(_) => { + anyhow::bail!("unexpected JSONRPCMessage::Request: {message:?}"); + } + JSONRPCMessage::Response(_) => { + // Keep scanning; we're waiting for an error with matching id. + } + JSONRPCMessage::Error(err) => { + if err.id == request_id { + return Ok(err); + } + } + } + } + } + pub async fn read_stream_until_notification_message( &mut self, method: &str, @@ -411,80 +365,6 @@ impl McpProcess { } } - pub async fn read_stream_until_configured_response_message( - &mut self, - ) -> anyhow::Result { - let mut sid_old: Option = None; - let mut sid_new: Option = None; - loop { - let message = self.read_jsonrpc_message().await?; - eprint!("message: {message:?}"); - - match message { - JSONRPCMessage::Notification(notification) => { - if let Some(params) = notification.params { - // Back-compat schema: method == "codex/event" and msg.type == "session_configured" - if notification.method == "codex/event" { - if let Some(msg) = params.get("msg") { - if msg.get("type").and_then(|v| v.as_str()) - == Some("session_configured") - { - if let Some(session_id) = - msg.get("session_id").and_then(|v| v.as_str()) - { - sid_old = Some(session_id.to_string()); - } - } - } - } - // New schema: method is the Display of EventMsg::SessionConfigured => "SessionConfigured" - if notification.method == "session_configured" { - if let Some(msg) = params.get("msg") { - if let Some(session_id) = - msg.get("session_id").and_then(|v| v.as_str()) - { - sid_new = Some(session_id.to_string()); - } - } - } - } - - if sid_old.is_some() && sid_new.is_some() { - // Both seen, they must match - assert_eq!( - sid_old.as_ref().unwrap(), - sid_new.as_ref().unwrap(), - "session_id mismatch between old and new schema" - ); - return Ok(sid_old.unwrap()); - } - } - JSONRPCMessage::Request(_) => { - anyhow::bail!("unexpected JSONRPCMessage::Request: {message:?}"); - } - JSONRPCMessage::Error(_) => { - anyhow::bail!("unexpected JSONRPCMessage::Error: {message:?}"); - } - JSONRPCMessage::Response(_) => { - anyhow::bail!("unexpected JSONRPCMessage::Response: {message:?}"); - } - } - } - } - - pub async fn send_notification( - &mut self, - method: &str, - params: Option, - ) -> anyhow::Result<()> { - self.send_jsonrpc_message(JSONRPCMessage::Notification(JSONRPCNotification { - jsonrpc: JSONRPC_VERSION.into(), - method: method.to_string(), - params, - })) - .await - } - /// Reads notifications until a legacy TaskComplete event is observed: /// Method "codex/event" with params.msg.type == "task_complete". pub async fn read_stream_until_legacy_task_complete_notification( diff --git a/codex-rs/mcp-server/tests/create_conversation.rs b/codex-rs/mcp-server/tests/create_conversation.rs index 2349b0b9e4..81cc1f3ee7 100644 --- a/codex-rs/mcp-server/tests/create_conversation.rs +++ b/codex-rs/mcp-server/tests/create_conversation.rs @@ -1,8 +1,16 @@ use std::path::Path; +use codex_protocol::mcp_protocol::AddConversationListenerParams; +use codex_protocol::mcp_protocol::AddConversationSubscriptionResponse; +use codex_protocol::mcp_protocol::InputItem; +use codex_protocol::mcp_protocol::NewConversationParams; +use codex_protocol::mcp_protocol::NewConversationResponse; +use codex_protocol::mcp_protocol::SendUserMessageParams; +use codex_protocol::mcp_protocol::SendUserMessageResponse; use mcp_test_support::McpProcess; use mcp_test_support::create_final_assistant_message_sse_response; use mcp_test_support::create_mock_chat_completions_server; +use mcp_test_support::to_response; use mcp_types::JSONRPCResponse; use mcp_types::RequestId; use pretty_assertions::assert_eq; @@ -33,43 +41,64 @@ async fn test_conversation_create_and_send_message_ok() { .expect("init timeout") .expect("init failed"); - // Create a conversation via the new tool. - let req_id = mcp - .send_conversation_create_tool_call("", "o3", "/repo") + // Create a conversation via the new JSON-RPC API. + let new_conv_id = mcp + .send_new_conversation_request(NewConversationParams { + model: Some("o3".to_string()), + ..Default::default() + }) .await - .expect("send conversationCreate"); - - let resp: JSONRPCResponse = timeout( + .expect("send newConversation"); + let new_conv_resp: JSONRPCResponse = timeout( DEFAULT_READ_TIMEOUT, - mcp.read_stream_until_response_message(RequestId::Integer(req_id)), + mcp.read_stream_until_response_message(RequestId::Integer(new_conv_id)), ) .await - .expect("create response timeout") - .expect("create response error"); + .expect("newConversation timeout") + .expect("newConversation resp"); + let NewConversationResponse { + conversation_id, + model, + } = to_response::(new_conv_resp) + .expect("deserialize newConversation response"); + assert_eq!(model, "o3"); - // Structured content must include status=ok, a UUID conversation_id and the model we passed. - let sc = &resp.result["structuredContent"]; - let conv_id = sc["conversation_id"].as_str().expect("uuid string"); - assert!(!conv_id.is_empty()); - assert_eq!(sc["model"], json!("o3")); - - // Now send a message to the created conversation and expect an OK result. - let send_id = mcp - .send_user_message_tool_call("Hello", conv_id) + // Add a listener so we receive notifications for this conversation (not strictly required for this test). + let add_listener_id = mcp + .send_add_conversation_listener_request(AddConversationListenerParams { conversation_id }) .await - .expect("send message"); + .expect("send addConversationListener"); + let _sub: AddConversationSubscriptionResponse = + to_response::( + timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_response_message(RequestId::Integer(add_listener_id)), + ) + .await + .expect("addConversationListener timeout") + .expect("addConversationListener resp"), + ) + .expect("deserialize addConversationListener response"); + // Now send a user message via the wire API and expect an OK (empty object) result. + let send_id = mcp + .send_send_user_message_request(SendUserMessageParams { + conversation_id, + items: vec![InputItem::Text { + text: "Hello".to_string(), + }], + }) + .await + .expect("send sendUserMessage"); let send_resp: JSONRPCResponse = timeout( DEFAULT_READ_TIMEOUT, mcp.read_stream_until_response_message(RequestId::Integer(send_id)), ) .await - .expect("send response timeout") - .expect("send response error"); - assert_eq!( - send_resp.result["structuredContent"], - json!({ "status": "ok" }) - ); + .expect("sendUserMessage timeout") + .expect("sendUserMessage resp"); + let _ok: SendUserMessageResponse = to_response::(send_resp) + .expect("deserialize sendUserMessage response"); // avoid race condition by waiting for the mock server to receive the chat.completions request let deadline = std::time::Instant::now() + DEFAULT_READ_TIMEOUT; diff --git a/codex-rs/mcp-server/tests/interrupt.rs b/codex-rs/mcp-server/tests/interrupt.rs index 365972e0ac..113a8dd291 100644 --- a/codex-rs/mcp-server/tests/interrupt.rs +++ b/codex-rs/mcp-server/tests/interrupt.rs @@ -3,15 +3,24 @@ use std::path::Path; +use codex_core::protocol::TurnAbortReason; use codex_core::spawn::CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR; -use codex_mcp_server::CodexToolCallParam; -use serde_json::json; +use codex_protocol::mcp_protocol::AddConversationListenerParams; +use codex_protocol::mcp_protocol::InterruptConversationParams; +use codex_protocol::mcp_protocol::InterruptConversationResponse; +use codex_protocol::mcp_protocol::NewConversationParams; +use codex_protocol::mcp_protocol::NewConversationResponse; +use codex_protocol::mcp_protocol::SendUserMessageParams; +use codex_protocol::mcp_protocol::SendUserMessageResponse; +use mcp_types::JSONRPCResponse; +use mcp_types::RequestId; use tempfile::TempDir; use tokio::time::timeout; use mcp_test_support::McpProcess; use mcp_test_support::create_mock_chat_completions_server; use mcp_test_support::create_shell_sse_response; +use mcp_test_support::to_response; const DEFAULT_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10); @@ -39,93 +48,91 @@ async fn shell_command_interruption() -> anyhow::Result<()> { let shell_command = vec![ "powershell".to_string(), "-Command".to_string(), - "Start-Sleep -Seconds 60".to_string(), + "Start-Sleep -Seconds 10".to_string(), ]; #[cfg(not(target_os = "windows"))] - let shell_command = vec!["sleep".to_string(), "60".to_string()]; - let workdir_for_shell_function_call = TempDir::new()?; + let shell_command = vec!["sleep".to_string(), "10".to_string()]; + + let tmp = TempDir::new()?; + // Temporary Codex home with config pointing at the mock server. + let codex_home = tmp.path().join("codex_home"); + std::fs::create_dir(&codex_home)?; + let working_directory = tmp.path().join("workdir"); + std::fs::create_dir(&working_directory)?; // Create mock server with a single SSE response: the long sleep command - let server = create_mock_chat_completions_server(vec![ - create_shell_sse_response( - shell_command.clone(), - Some(workdir_for_shell_function_call.path()), - Some(60_000), // 60 seconds timeout in ms - "call_sleep", - )?, - create_shell_sse_response( - shell_command.clone(), - Some(workdir_for_shell_function_call.path()), - Some(60_000), // 60 seconds timeout in ms - "call_sleep", - )?, - ]) + let server = create_mock_chat_completions_server(vec![create_shell_sse_response( + shell_command.clone(), + Some(&working_directory), + Some(10_000), // 10 seconds timeout in ms + "call_sleep", + )?]) .await; + create_config_toml(&codex_home, server.uri())?; - // Create Codex configuration - let codex_home = TempDir::new()?; - create_config_toml(codex_home.path(), server.uri())?; - let mut mcp_process = McpProcess::new(codex_home.path()).await?; - timeout(DEFAULT_READ_TIMEOUT, mcp_process.initialize()).await??; + // Start MCP server and initialize. + let mut mcp = McpProcess::new(&codex_home).await?; + timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; - // Send codex tool call that triggers "sleep 60" - let codex_request_id = mcp_process - .send_codex_tool_call(CodexToolCallParam { - cwd: None, - prompt: "First Run: run `sleep 60`".to_string(), - model: None, - profile: None, - approval_policy: None, - sandbox: None, - config: None, - base_instructions: None, - include_plan_tool: None, + // 1) newConversation + let new_conv_id = mcp + .send_new_conversation_request(NewConversationParams { + cwd: Some(working_directory.to_string_lossy().into_owned()), + ..Default::default() }) .await?; + let new_conv_resp: JSONRPCResponse = timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_response_message(RequestId::Integer(new_conv_id)), + ) + .await??; + let new_conv_resp = to_response::(new_conv_resp)?; + let NewConversationResponse { + conversation_id, .. + } = new_conv_resp; - let session_id = mcp_process - .read_stream_until_configured_response_message() + // 2) addConversationListener + let add_listener_id = mcp + .send_add_conversation_listener_request(AddConversationListenerParams { conversation_id }) .await?; + let _add_listener_resp: JSONRPCResponse = timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_response_message(RequestId::Integer(add_listener_id)), + ) + .await??; + + // 3) sendUserMessage (should trigger notifications; we only validate an OK response) + let send_user_id = mcp + .send_send_user_message_request(SendUserMessageParams { + conversation_id, + items: vec![codex_protocol::mcp_protocol::InputItem::Text { + text: "run first sleep command".to_string(), + }], + }) + .await?; + let send_user_resp: JSONRPCResponse = timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_response_message(RequestId::Integer(send_user_id)), + ) + .await??; + let SendUserMessageResponse {} = to_response::(send_user_resp)?; // Give the command a moment to start tokio::time::sleep(std::time::Duration::from_secs(1)).await; - // Send interrupt notification - mcp_process - .send_notification( - "notifications/cancelled", - Some(json!({ "requestId": codex_request_id })), - ) + // 4) send interrupt request + let interrupt_id = mcp + .send_interrupt_conversation_request(InterruptConversationParams { conversation_id }) .await?; - - // Expect Codex to emit a TurnAborted event notification - let _turn_aborted = timeout( + let interrupt_resp: JSONRPCResponse = timeout( DEFAULT_READ_TIMEOUT, - mcp_process.read_stream_until_notification_message("turn_aborted"), + mcp.read_stream_until_response_message(RequestId::Integer(interrupt_id)), ) .await??; + let InterruptConversationResponse { abort_reason } = + to_response::(interrupt_resp)?; + assert_eq!(TurnAbortReason::Interrupted, abort_reason); - let codex_reply_request_id = mcp_process - .send_codex_reply_tool_call(&session_id, "Second Run: run `sleep 60`") - .await?; - - // Give the command a moment to start - tokio::time::sleep(std::time::Duration::from_secs(1)).await; - - // Send interrupt notification - mcp_process - .send_notification( - "notifications/cancelled", - Some(json!({ "requestId": codex_reply_request_id })), - ) - .await?; - - // Expect Codex to emit a TurnAborted event notification - let _turn_aborted = timeout( - DEFAULT_READ_TIMEOUT, - mcp_process.read_stream_until_notification_message("turn_aborted"), - ) - .await??; Ok(()) } diff --git a/codex-rs/mcp-server/tests/send_message.rs b/codex-rs/mcp-server/tests/send_message.rs index bf2966ef05..8f607776fc 100644 --- a/codex-rs/mcp-server/tests/send_message.rs +++ b/codex-rs/mcp-server/tests/send_message.rs @@ -1,16 +1,21 @@ use std::path::Path; -use std::thread::sleep; -use std::time::Duration; -use codex_mcp_server::CodexToolCallParam; +use codex_protocol::mcp_protocol::AddConversationListenerParams; +use codex_protocol::mcp_protocol::AddConversationSubscriptionResponse; +use codex_protocol::mcp_protocol::ConversationId; +use codex_protocol::mcp_protocol::InputItem; +use codex_protocol::mcp_protocol::NewConversationParams; +use codex_protocol::mcp_protocol::NewConversationResponse; +use codex_protocol::mcp_protocol::SendUserMessageParams; +use codex_protocol::mcp_protocol::SendUserMessageResponse; use mcp_test_support::McpProcess; use mcp_test_support::create_final_assistant_message_sse_response; use mcp_test_support::create_mock_chat_completions_server; -use mcp_types::JSONRPC_VERSION; +use mcp_test_support::to_response; +use mcp_types::JSONRPCNotification; use mcp_types::JSONRPCResponse; use mcp_types::RequestId; use pretty_assertions::assert_eq; -use serde_json::json; use tempfile::TempDir; use tokio::time::timeout; @@ -31,76 +36,94 @@ async fn test_send_message_success() { create_config_toml(codex_home.path(), &server.uri()).expect("write config.toml"); // Start MCP server process and initialize. - let mut mcp_process = McpProcess::new(codex_home.path()) + let mut mcp = McpProcess::new(codex_home.path()) .await .expect("spawn mcp process"); - timeout(DEFAULT_READ_TIMEOUT, mcp_process.initialize()) + timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()) .await .expect("init timed out") .expect("init failed"); - // Kick off a Codex session so we have a valid session_id. - let codex_request_id = mcp_process - .send_codex_tool_call(CodexToolCallParam { - prompt: "Start a session".to_string(), - ..Default::default() - }) + // Start a conversation using the new wire API. + let new_conv_id = mcp + .send_new_conversation_request(NewConversationParams::default()) .await - .expect("send codex tool call"); - - // Wait for the session_configured event to get the session_id. - let session_id = mcp_process - .read_stream_until_configured_response_message() - .await - .expect("read session_configured"); - - // The original codex call will finish quickly given our mock; consume its response. - timeout( + .expect("send newConversation"); + let new_conv_resp: JSONRPCResponse = timeout( DEFAULT_READ_TIMEOUT, - mcp_process.read_stream_until_response_message(RequestId::Integer(codex_request_id)), + mcp.read_stream_until_response_message(RequestId::Integer(new_conv_id)), ) .await - .expect("codex response timeout") - .expect("codex response error"); + .expect("newConversation timeout") + .expect("newConversation resp"); + let NewConversationResponse { + conversation_id, .. + } = to_response::<_>(new_conv_resp).expect("deserialize newConversation response"); - // Now exercise the send-user-message tool. - let send_msg_request_id = mcp_process - .send_user_message_tool_call("Hello again", &session_id) + // 2) addConversationListener + let add_listener_id = mcp + .send_add_conversation_listener_request(AddConversationListenerParams { conversation_id }) .await - .expect("send send-message tool call"); + .expect("send addConversationListener"); + let add_listener_resp: JSONRPCResponse = timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_response_message(RequestId::Integer(add_listener_id)), + ) + .await + .expect("addConversationListener timeout") + .expect("addConversationListener resp"); + let AddConversationSubscriptionResponse { subscription_id: _ } = + to_response::<_>(add_listener_resp).expect("deserialize addConversationListener response"); + + // Now exercise sendUserMessage twice. + send_message("Hello", conversation_id, &mut mcp).await; + send_message("Hello again", conversation_id, &mut mcp).await; +} + +#[expect(clippy::expect_used)] +async fn send_message(message: &str, conversation_id: ConversationId, mcp: &mut McpProcess) { + // Now exercise sendUserMessage. + let send_id = mcp + .send_send_user_message_request(SendUserMessageParams { + conversation_id, + items: vec![InputItem::Text { + text: message.to_string(), + }], + }) + .await + .expect("send sendUserMessage"); let response: JSONRPCResponse = timeout( DEFAULT_READ_TIMEOUT, - mcp_process.read_stream_until_response_message(RequestId::Integer(send_msg_request_id)), + mcp.read_stream_until_response_message(RequestId::Integer(send_id)), ) .await - .expect("send-user-message response timeout") - .expect("send-user-message response error"); + .expect("sendUserMessage response timeout") + .expect("sendUserMessage response error"); + let _ok: SendUserMessageResponse = to_response::(response) + .expect("deserialize sendUserMessage response"); + + // Verify the task_finished notification is received. + // Note this also ensures that the final request to the server was made. + let task_finished_notification: JSONRPCNotification = timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_notification_message("codex/event/task_complete"), + ) + .await + .expect("task_finished_notification timeout") + .expect("task_finished_notification resp"); + let serde_json::Value::Object(map) = task_finished_notification + .params + .expect("notification should have params") + else { + panic!("task_finished_notification should have params"); + }; assert_eq!( - JSONRPCResponse { - jsonrpc: JSONRPC_VERSION.into(), - id: RequestId::Integer(send_msg_request_id), - result: json!({ - "content": [ - { - "text": "{\"status\":\"ok\"}", - "type": "text", - } - ], - "isError": false, - "structuredContent": { - "status": "ok" - } - }), - }, - response + map.get("conversationId") + .expect("should have conversationId"), + &serde_json::Value::String(conversation_id.to_string()) ); - // wait for the server to hear the user message - sleep(Duration::from_secs(5)); - - // Ensure the server and tempdir live until end of test - drop(server); } #[tokio::test] @@ -113,24 +136,26 @@ async fn test_send_message_session_not_found() { .expect("timeout") .expect("init"); - let unknown = uuid::Uuid::new_v4().to_string(); + let unknown = ConversationId(uuid::Uuid::new_v4()); let req_id = mcp - .send_user_message_tool_call("ping", &unknown) + .send_send_user_message_request(SendUserMessageParams { + conversation_id: unknown, + items: vec![InputItem::Text { + text: "ping".to_string(), + }], + }) .await - .expect("send tool"); + .expect("send sendUserMessage"); - let resp: JSONRPCResponse = timeout( + // Expect an error response for unknown conversation. + let err = timeout( DEFAULT_READ_TIMEOUT, - mcp.read_stream_until_response_message(RequestId::Integer(req_id)), + mcp.read_stream_until_error_message(RequestId::Integer(req_id)), ) .await .expect("timeout") - .expect("resp"); - - let result = resp.result.clone(); - let content = result["content"][0]["text"].as_str().unwrap_or(""); - assert!(content.contains("Session does not exist")); - assert_eq!(result["isError"], json!(true)); + .expect("error"); + assert_eq!(err.id, RequestId::Integer(req_id)); } // --------------------------------------------------------------------------- diff --git a/codex-rs/mcp-types/Cargo.toml b/codex-rs/mcp-types/Cargo.toml index db849d5f0e..7ebc10af8d 100644 --- a/codex-rs/mcp-types/Cargo.toml +++ b/codex-rs/mcp-types/Cargo.toml @@ -9,3 +9,4 @@ workspace = true [dependencies] serde = { version = "1", features = ["derive"] } serde_json = "1" +ts-rs = { version = "11", features = ["serde-json-impl"] } diff --git a/codex-rs/mcp-types/generate_mcp_types.py b/codex-rs/mcp-types/generate_mcp_types.py index 38f57e9a1b..d71a6e59a2 100755 --- a/codex-rs/mcp-types/generate_mcp_types.py +++ b/codex-rs/mcp-types/generate_mcp_types.py @@ -17,9 +17,9 @@ from typing import Any, Literal SCHEMA_VERSION = "2025-06-18" JSONRPC_VERSION = "2.0" -STANDARD_DERIVE = "#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]\n" +STANDARD_DERIVE = "#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)]\n" STANDARD_HASHABLE_DERIVE = ( - "#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, Hash, Eq)]\n" + "#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, Hash, Eq, TS)]\n" ) # Will be populated with the schema's `definitions` map in `main()` so that @@ -75,6 +75,8 @@ use serde::Serialize; use serde::de::DeserializeOwned; use std::convert::TryFrom; +use ts_rs::TS; + pub const MCP_SCHEMA_VERSION: &str = "{SCHEMA_VERSION}"; pub const JSONRPC_VERSION: &str = "{JSONRPC_VERSION}"; diff --git a/codex-rs/mcp-types/src/lib.rs b/codex-rs/mcp-types/src/lib.rs index cf09d67e35..988bec2ade 100644 --- a/codex-rs/mcp-types/src/lib.rs +++ b/codex-rs/mcp-types/src/lib.rs @@ -10,6 +10,8 @@ use serde::Serialize; use serde::de::DeserializeOwned; use std::convert::TryFrom; +use ts_rs::TS; + pub const MCP_SCHEMA_VERSION: &str = "2025-06-18"; pub const JSONRPC_VERSION: &str = "2.0"; @@ -31,7 +33,7 @@ fn default_jsonrpc() -> String { } /// Optional annotations for the client. The client can use annotations to inform how objects are used or displayed -#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)] pub struct Annotations { #[serde(default, skip_serializing_if = "Option::is_none")] pub audience: Option>, @@ -46,7 +48,7 @@ pub struct Annotations { } /// Audio provided to or from an LLM. -#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)] pub struct AudioContent { #[serde(default, skip_serializing_if = "Option::is_none")] pub annotations: Option, @@ -57,14 +59,14 @@ pub struct AudioContent { } /// Base interface for metadata with name (identifier) and title (display name) properties. -#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)] pub struct BaseMetadata { pub name: String, #[serde(default, skip_serializing_if = "Option::is_none")] pub title: Option, } -#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)] pub struct BlobResourceContents { pub blob: String, #[serde(rename = "mimeType", default, skip_serializing_if = "Option::is_none")] @@ -72,7 +74,7 @@ pub struct BlobResourceContents { pub uri: String, } -#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)] pub struct BooleanSchema { #[serde(default, skip_serializing_if = "Option::is_none")] pub default: Option, @@ -83,7 +85,7 @@ pub struct BooleanSchema { pub r#type: String, // &'static str = "boolean" } -#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)] pub enum CallToolRequest {} impl ModelContextProtocolRequest for CallToolRequest { @@ -92,7 +94,7 @@ impl ModelContextProtocolRequest for CallToolRequest { type Result = CallToolResult; } -#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)] pub struct CallToolRequestParams { #[serde(default, skip_serializing_if = "Option::is_none")] pub arguments: Option, @@ -100,7 +102,7 @@ pub struct CallToolRequestParams { } /// The server's response to a tool call. -#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)] pub struct CallToolResult { pub content: Vec, #[serde(rename = "isError", default, skip_serializing_if = "Option::is_none")] @@ -121,7 +123,7 @@ impl From for serde_json::Value { } } -#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)] pub enum CancelledNotification {} impl ModelContextProtocolNotification for CancelledNotification { @@ -129,7 +131,7 @@ impl ModelContextProtocolNotification for CancelledNotification { type Params = CancelledNotificationParams; } -#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)] pub struct CancelledNotificationParams { #[serde(default, skip_serializing_if = "Option::is_none")] pub reason: Option, @@ -138,7 +140,7 @@ pub struct CancelledNotificationParams { } /// Capabilities a client may support. Known capabilities are defined here, in this schema, but this is not a closed set: any client can define its own, additional capabilities. -#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)] pub struct ClientCapabilities { #[serde(default, skip_serializing_if = "Option::is_none")] pub elicitation: Option, @@ -151,7 +153,7 @@ pub struct ClientCapabilities { } /// Present if the client supports listing roots. -#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)] pub struct ClientCapabilitiesRoots { #[serde( rename = "listChanged", @@ -161,7 +163,7 @@ pub struct ClientCapabilitiesRoots { pub list_changed: Option, } -#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)] #[serde(untagged)] pub enum ClientNotification { CancelledNotification(CancelledNotification), @@ -170,7 +172,7 @@ pub enum ClientNotification { RootsListChangedNotification(RootsListChangedNotification), } -#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)] #[serde(tag = "method", content = "params")] pub enum ClientRequest { #[serde(rename = "initialize")] @@ -203,7 +205,7 @@ pub enum ClientRequest { CompleteRequest(::Params), } -#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)] #[serde(untagged)] pub enum ClientResult { Result(Result), @@ -212,7 +214,7 @@ pub enum ClientResult { ElicitResult(ElicitResult), } -#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)] pub enum CompleteRequest {} impl ModelContextProtocolRequest for CompleteRequest { @@ -221,7 +223,7 @@ impl ModelContextProtocolRequest for CompleteRequest { type Result = CompleteResult; } -#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)] pub struct CompleteRequestParams { pub argument: CompleteRequestParamsArgument, #[serde(default, skip_serializing_if = "Option::is_none")] @@ -230,20 +232,20 @@ pub struct CompleteRequestParams { } /// Additional, optional context for completions -#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)] pub struct CompleteRequestParamsContext { #[serde(default, skip_serializing_if = "Option::is_none")] pub arguments: Option, } /// The argument's information -#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)] pub struct CompleteRequestParamsArgument { pub name: String, pub value: String, } -#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)] #[serde(untagged)] pub enum CompleteRequestParamsRef { PromptReference(PromptReference), @@ -251,12 +253,12 @@ pub enum CompleteRequestParamsRef { } /// The server's response to a completion/complete request -#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)] pub struct CompleteResult { pub completion: CompleteResultCompletion, } -#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)] pub struct CompleteResultCompletion { #[serde(rename = "hasMore", default, skip_serializing_if = "Option::is_none")] pub has_more: Option, @@ -273,7 +275,7 @@ impl From for serde_json::Value { } } -#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)] #[serde(untagged)] pub enum ContentBlock { TextContent(TextContent), @@ -283,7 +285,7 @@ pub enum ContentBlock { EmbeddedResource(EmbeddedResource), } -#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)] pub enum CreateMessageRequest {} impl ModelContextProtocolRequest for CreateMessageRequest { @@ -292,7 +294,7 @@ impl ModelContextProtocolRequest for CreateMessageRequest { type Result = CreateMessageResult; } -#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)] pub struct CreateMessageRequestParams { #[serde( rename = "includeContext", @@ -328,7 +330,7 @@ pub struct CreateMessageRequestParams { } /// The client's response to a sampling/create_message request from the server. The client should inform the user before returning the sampled message, to allow them to inspect the response (human in the loop) and decide whether to allow the server to see it. -#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)] pub struct CreateMessageResult { pub content: CreateMessageResultContent, pub model: String, @@ -341,7 +343,7 @@ pub struct CreateMessageResult { pub stop_reason: Option, } -#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)] #[serde(untagged)] pub enum CreateMessageResultContent { TextContent(TextContent), @@ -357,10 +359,10 @@ impl From for serde_json::Value { } } -#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)] pub struct Cursor(String); -#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)] pub enum ElicitRequest {} impl ModelContextProtocolRequest for ElicitRequest { @@ -369,7 +371,7 @@ impl ModelContextProtocolRequest for ElicitRequest { type Result = ElicitResult; } -#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)] pub struct ElicitRequestParams { pub message: String, #[serde(rename = "requestedSchema")] @@ -378,7 +380,7 @@ pub struct ElicitRequestParams { /// A restricted subset of JSON Schema. /// Only top-level properties are allowed, without nesting. -#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)] pub struct ElicitRequestParamsRequestedSchema { pub properties: serde_json::Value, #[serde(default, skip_serializing_if = "Option::is_none")] @@ -387,7 +389,7 @@ pub struct ElicitRequestParamsRequestedSchema { } /// The client's response to an elicitation request. -#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)] pub struct ElicitResult { pub action: String, #[serde(default, skip_serializing_if = "Option::is_none")] @@ -406,7 +408,7 @@ impl From for serde_json::Value { /// /// It is up to the client how best to render embedded resources for the benefit /// of the LLM and/or the user. -#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)] pub struct EmbeddedResource { #[serde(default, skip_serializing_if = "Option::is_none")] pub annotations: Option, @@ -414,7 +416,7 @@ pub struct EmbeddedResource { pub r#type: String, // &'static str = "resource" } -#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)] #[serde(untagged)] pub enum EmbeddedResourceResource { TextResourceContents(TextResourceContents), @@ -423,7 +425,7 @@ pub enum EmbeddedResourceResource { pub type EmptyResult = Result; -#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)] pub struct EnumSchema { #[serde(default, skip_serializing_if = "Option::is_none")] pub description: Option, @@ -435,7 +437,7 @@ pub struct EnumSchema { pub r#type: String, // &'static str = "string" } -#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)] pub enum GetPromptRequest {} impl ModelContextProtocolRequest for GetPromptRequest { @@ -444,7 +446,7 @@ impl ModelContextProtocolRequest for GetPromptRequest { type Result = GetPromptResult; } -#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)] pub struct GetPromptRequestParams { #[serde(default, skip_serializing_if = "Option::is_none")] pub arguments: Option, @@ -452,7 +454,7 @@ pub struct GetPromptRequestParams { } /// The server's response to a prompts/get request from the client. -#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)] pub struct GetPromptResult { #[serde(default, skip_serializing_if = "Option::is_none")] pub description: Option, @@ -468,7 +470,7 @@ impl From for serde_json::Value { } /// An image provided to or from an LLM. -#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)] pub struct ImageContent { #[serde(default, skip_serializing_if = "Option::is_none")] pub annotations: Option, @@ -479,7 +481,7 @@ pub struct ImageContent { } /// Describes the name and version of an MCP implementation, with an optional title for UI representation. -#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)] pub struct Implementation { pub name: String, #[serde(default, skip_serializing_if = "Option::is_none")] @@ -487,7 +489,7 @@ pub struct Implementation { pub version: String, } -#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)] pub enum InitializeRequest {} impl ModelContextProtocolRequest for InitializeRequest { @@ -496,7 +498,7 @@ impl ModelContextProtocolRequest for InitializeRequest { type Result = InitializeResult; } -#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)] pub struct InitializeRequestParams { pub capabilities: ClientCapabilities, #[serde(rename = "clientInfo")] @@ -506,7 +508,7 @@ pub struct InitializeRequestParams { } /// After receiving an initialize request from the client, the server sends this response. -#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)] pub struct InitializeResult { pub capabilities: ServerCapabilities, #[serde(default, skip_serializing_if = "Option::is_none")] @@ -525,7 +527,7 @@ impl From for serde_json::Value { } } -#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)] pub enum InitializedNotification {} impl ModelContextProtocolNotification for InitializedNotification { @@ -534,7 +536,7 @@ impl ModelContextProtocolNotification for InitializedNotification { } /// A response to a request that indicates an error occurred. -#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)] pub struct JSONRPCError { pub error: JSONRPCErrorError, pub id: RequestId, @@ -542,7 +544,7 @@ pub struct JSONRPCError { pub jsonrpc: String, } -#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)] pub struct JSONRPCErrorError { pub code: i64, #[serde(default, skip_serializing_if = "Option::is_none")] @@ -551,7 +553,7 @@ pub struct JSONRPCErrorError { } /// Refers to any valid JSON-RPC object that can be decoded off the wire, or encoded to be sent. -#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)] #[serde(untagged)] pub enum JSONRPCMessage { Request(JSONRPCRequest), @@ -561,7 +563,7 @@ pub enum JSONRPCMessage { } /// A notification which does not expect a response. -#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)] pub struct JSONRPCNotification { #[serde(rename = "jsonrpc", default = "default_jsonrpc")] pub jsonrpc: String, @@ -571,7 +573,7 @@ pub struct JSONRPCNotification { } /// A request that expects a response. -#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)] pub struct JSONRPCRequest { pub id: RequestId, #[serde(rename = "jsonrpc", default = "default_jsonrpc")] @@ -582,7 +584,7 @@ pub struct JSONRPCRequest { } /// A successful (non-error) response to a request. -#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)] pub struct JSONRPCResponse { pub id: RequestId, #[serde(rename = "jsonrpc", default = "default_jsonrpc")] @@ -590,7 +592,7 @@ pub struct JSONRPCResponse { pub result: Result, } -#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)] pub enum ListPromptsRequest {} impl ModelContextProtocolRequest for ListPromptsRequest { @@ -599,14 +601,14 @@ impl ModelContextProtocolRequest for ListPromptsRequest { type Result = ListPromptsResult; } -#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)] pub struct ListPromptsRequestParams { #[serde(default, skip_serializing_if = "Option::is_none")] pub cursor: Option, } /// The server's response to a prompts/list request from the client. -#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)] pub struct ListPromptsResult { #[serde( rename = "nextCursor", @@ -625,7 +627,7 @@ impl From for serde_json::Value { } } -#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)] pub enum ListResourceTemplatesRequest {} impl ModelContextProtocolRequest for ListResourceTemplatesRequest { @@ -634,14 +636,14 @@ impl ModelContextProtocolRequest for ListResourceTemplatesRequest { type Result = ListResourceTemplatesResult; } -#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)] pub struct ListResourceTemplatesRequestParams { #[serde(default, skip_serializing_if = "Option::is_none")] pub cursor: Option, } /// The server's response to a resources/templates/list request from the client. -#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)] pub struct ListResourceTemplatesResult { #[serde( rename = "nextCursor", @@ -661,7 +663,7 @@ impl From for serde_json::Value { } } -#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)] pub enum ListResourcesRequest {} impl ModelContextProtocolRequest for ListResourcesRequest { @@ -670,14 +672,14 @@ impl ModelContextProtocolRequest for ListResourcesRequest { type Result = ListResourcesResult; } -#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)] pub struct ListResourcesRequestParams { #[serde(default, skip_serializing_if = "Option::is_none")] pub cursor: Option, } /// The server's response to a resources/list request from the client. -#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)] pub struct ListResourcesResult { #[serde( rename = "nextCursor", @@ -696,7 +698,7 @@ impl From for serde_json::Value { } } -#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)] pub enum ListRootsRequest {} impl ModelContextProtocolRequest for ListRootsRequest { @@ -708,7 +710,7 @@ impl ModelContextProtocolRequest for ListRootsRequest { /// The client's response to a roots/list request from the server. /// This result contains an array of Root objects, each representing a root directory /// or file that the server can operate on. -#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)] pub struct ListRootsResult { pub roots: Vec, } @@ -721,7 +723,7 @@ impl From for serde_json::Value { } } -#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)] pub enum ListToolsRequest {} impl ModelContextProtocolRequest for ListToolsRequest { @@ -730,14 +732,14 @@ impl ModelContextProtocolRequest for ListToolsRequest { type Result = ListToolsResult; } -#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)] pub struct ListToolsRequestParams { #[serde(default, skip_serializing_if = "Option::is_none")] pub cursor: Option, } /// The server's response to a tools/list request from the client. -#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)] pub struct ListToolsResult { #[serde( rename = "nextCursor", @@ -760,7 +762,7 @@ impl From for serde_json::Value { /// /// These map to syslog message severities, as specified in RFC-5424: /// https://datatracker.ietf.org/doc/html/rfc5424#section-6.2.1 -#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)] pub enum LoggingLevel { #[serde(rename = "alert")] Alert, @@ -780,7 +782,7 @@ pub enum LoggingLevel { Warning, } -#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)] pub enum LoggingMessageNotification {} impl ModelContextProtocolNotification for LoggingMessageNotification { @@ -788,7 +790,7 @@ impl ModelContextProtocolNotification for LoggingMessageNotification { type Params = LoggingMessageNotificationParams; } -#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)] pub struct LoggingMessageNotificationParams { pub data: serde_json::Value, pub level: LoggingLevel, @@ -800,7 +802,7 @@ pub struct LoggingMessageNotificationParams { /// /// Keys not declared here are currently left unspecified by the spec and are up /// to the client to interpret. -#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)] pub struct ModelHint { #[serde(default, skip_serializing_if = "Option::is_none")] pub name: Option, @@ -817,7 +819,7 @@ pub struct ModelHint { /// These preferences are always advisory. The client MAY ignore them. It is also /// up to the client to decide how to interpret these preferences and how to /// balance them against other considerations. -#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)] pub struct ModelPreferences { #[serde( rename = "costPriority", @@ -841,14 +843,14 @@ pub struct ModelPreferences { pub speed_priority: Option, } -#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)] pub struct Notification { pub method: String, #[serde(default, skip_serializing_if = "Option::is_none")] pub params: Option, } -#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)] pub struct NumberSchema { #[serde(default, skip_serializing_if = "Option::is_none")] pub description: Option, @@ -861,20 +863,20 @@ pub struct NumberSchema { pub r#type: String, } -#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)] pub struct PaginatedRequest { pub method: String, #[serde(default, skip_serializing_if = "Option::is_none")] pub params: Option, } -#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)] pub struct PaginatedRequestParams { #[serde(default, skip_serializing_if = "Option::is_none")] pub cursor: Option, } -#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)] pub struct PaginatedResult { #[serde( rename = "nextCursor", @@ -892,7 +894,7 @@ impl From for serde_json::Value { } } -#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)] pub enum PingRequest {} impl ModelContextProtocolRequest for PingRequest { @@ -903,7 +905,7 @@ impl ModelContextProtocolRequest for PingRequest { /// Restricted schema definitions that only allow primitive types /// without nested objects or arrays. -#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)] #[serde(untagged)] pub enum PrimitiveSchemaDefinition { StringSchema(StringSchema), @@ -912,7 +914,7 @@ pub enum PrimitiveSchemaDefinition { EnumSchema(EnumSchema), } -#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)] pub enum ProgressNotification {} impl ModelContextProtocolNotification for ProgressNotification { @@ -920,7 +922,7 @@ impl ModelContextProtocolNotification for ProgressNotification { type Params = ProgressNotificationParams; } -#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)] pub struct ProgressNotificationParams { #[serde(default, skip_serializing_if = "Option::is_none")] pub message: Option, @@ -931,7 +933,7 @@ pub struct ProgressNotificationParams { pub total: Option, } -#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, Hash, Eq)] +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, Hash, Eq, TS)] #[serde(untagged)] pub enum ProgressToken { String(String), @@ -939,7 +941,7 @@ pub enum ProgressToken { } /// A prompt or prompt template that the server offers. -#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)] pub struct Prompt { #[serde(default, skip_serializing_if = "Option::is_none")] pub arguments: Option>, @@ -951,7 +953,7 @@ pub struct Prompt { } /// Describes an argument that a prompt can accept. -#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)] pub struct PromptArgument { #[serde(default, skip_serializing_if = "Option::is_none")] pub description: Option, @@ -962,7 +964,7 @@ pub struct PromptArgument { pub title: Option, } -#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)] pub enum PromptListChangedNotification {} impl ModelContextProtocolNotification for PromptListChangedNotification { @@ -974,14 +976,14 @@ impl ModelContextProtocolNotification for PromptListChangedNotification { /// /// This is similar to `SamplingMessage`, but also supports the embedding of /// resources from the MCP server. -#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)] pub struct PromptMessage { pub content: ContentBlock, pub role: Role, } /// Identifies a prompt. -#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)] pub struct PromptReference { pub name: String, #[serde(default, skip_serializing_if = "Option::is_none")] @@ -989,7 +991,7 @@ pub struct PromptReference { pub r#type: String, // &'static str = "ref/prompt" } -#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)] pub enum ReadResourceRequest {} impl ModelContextProtocolRequest for ReadResourceRequest { @@ -998,18 +1000,18 @@ impl ModelContextProtocolRequest for ReadResourceRequest { type Result = ReadResourceResult; } -#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)] pub struct ReadResourceRequestParams { pub uri: String, } /// The server's response to a resources/read request from the client. -#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)] pub struct ReadResourceResult { pub contents: Vec, } -#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)] #[serde(untagged)] pub enum ReadResourceResultContents { TextResourceContents(TextResourceContents), @@ -1024,14 +1026,14 @@ impl From for serde_json::Value { } } -#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)] pub struct Request { pub method: String, #[serde(default, skip_serializing_if = "Option::is_none")] pub params: Option, } -#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, Hash, Eq)] +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, Hash, Eq, TS)] #[serde(untagged)] pub enum RequestId { String(String), @@ -1039,7 +1041,7 @@ pub enum RequestId { } /// A known resource that the server is capable of reading. -#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)] pub struct Resource { #[serde(default, skip_serializing_if = "Option::is_none")] pub annotations: Option, @@ -1056,7 +1058,7 @@ pub struct Resource { } /// The contents of a specific resource or sub-resource. -#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)] pub struct ResourceContents { #[serde(rename = "mimeType", default, skip_serializing_if = "Option::is_none")] pub mime_type: Option, @@ -1066,7 +1068,7 @@ pub struct ResourceContents { /// 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. -#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)] pub struct ResourceLink { #[serde(default, skip_serializing_if = "Option::is_none")] pub annotations: Option, @@ -1083,7 +1085,7 @@ pub struct ResourceLink { pub uri: String, } -#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)] pub enum ResourceListChangedNotification {} impl ModelContextProtocolNotification for ResourceListChangedNotification { @@ -1092,7 +1094,7 @@ impl ModelContextProtocolNotification for ResourceListChangedNotification { } /// A template description for resources available on the server. -#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)] pub struct ResourceTemplate { #[serde(default, skip_serializing_if = "Option::is_none")] pub annotations: Option, @@ -1108,13 +1110,13 @@ pub struct ResourceTemplate { } /// A reference to a resource or resource template definition. -#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)] pub struct ResourceTemplateReference { pub r#type: String, // &'static str = "ref/resource" pub uri: String, } -#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)] pub enum ResourceUpdatedNotification {} impl ModelContextProtocolNotification for ResourceUpdatedNotification { @@ -1122,7 +1124,7 @@ impl ModelContextProtocolNotification for ResourceUpdatedNotification { type Params = ResourceUpdatedNotificationParams; } -#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)] pub struct ResourceUpdatedNotificationParams { pub uri: String, } @@ -1130,7 +1132,7 @@ pub struct ResourceUpdatedNotificationParams { pub type Result = serde_json::Value; /// The sender or recipient of messages and data in a conversation. -#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)] pub enum Role { #[serde(rename = "assistant")] Assistant, @@ -1139,14 +1141,14 @@ pub enum Role { } /// Represents a root directory or file that the server can operate on. -#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)] pub struct Root { #[serde(default, skip_serializing_if = "Option::is_none")] pub name: Option, pub uri: String, } -#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)] pub enum RootsListChangedNotification {} impl ModelContextProtocolNotification for RootsListChangedNotification { @@ -1155,13 +1157,13 @@ impl ModelContextProtocolNotification for RootsListChangedNotification { } /// Describes a message issued to or received from an LLM API. -#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)] pub struct SamplingMessage { pub content: SamplingMessageContent, pub role: Role, } -#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)] #[serde(untagged)] pub enum SamplingMessageContent { TextContent(TextContent), @@ -1170,7 +1172,7 @@ pub enum SamplingMessageContent { } /// Capabilities that a server may support. Known capabilities are defined here, in this schema, but this is not a closed set: any server can define its own, additional capabilities. -#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)] pub struct ServerCapabilities { #[serde(default, skip_serializing_if = "Option::is_none")] pub completions: Option, @@ -1187,7 +1189,7 @@ pub struct ServerCapabilities { } /// Present if the server offers any tools to call. -#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)] pub struct ServerCapabilitiesTools { #[serde( rename = "listChanged", @@ -1198,7 +1200,7 @@ pub struct ServerCapabilitiesTools { } /// Present if the server offers any resources to read. -#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)] pub struct ServerCapabilitiesResources { #[serde( rename = "listChanged", @@ -1211,7 +1213,7 @@ pub struct ServerCapabilitiesResources { } /// Present if the server offers any prompt templates. -#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)] pub struct ServerCapabilitiesPrompts { #[serde( rename = "listChanged", @@ -1221,7 +1223,7 @@ pub struct ServerCapabilitiesPrompts { pub list_changed: Option, } -#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)] #[serde(tag = "method", content = "params")] pub enum ServerNotification { #[serde(rename = "notifications/cancelled")] @@ -1250,7 +1252,7 @@ pub enum ServerNotification { ), } -#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)] #[serde(untagged)] pub enum ServerRequest { PingRequest(PingRequest), @@ -1259,7 +1261,7 @@ pub enum ServerRequest { ElicitRequest(ElicitRequest), } -#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)] #[serde(untagged)] #[allow(clippy::large_enum_variant)] pub enum ServerResult { @@ -1275,7 +1277,7 @@ pub enum ServerResult { CompleteResult(CompleteResult), } -#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)] pub enum SetLevelRequest {} impl ModelContextProtocolRequest for SetLevelRequest { @@ -1284,12 +1286,12 @@ impl ModelContextProtocolRequest for SetLevelRequest { type Result = Result; } -#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)] pub struct SetLevelRequestParams { pub level: LoggingLevel, } -#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)] pub struct StringSchema { #[serde(default, skip_serializing_if = "Option::is_none")] pub description: Option, @@ -1304,7 +1306,7 @@ pub struct StringSchema { pub r#type: String, // &'static str = "string" } -#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)] pub enum SubscribeRequest {} impl ModelContextProtocolRequest for SubscribeRequest { @@ -1313,13 +1315,13 @@ impl ModelContextProtocolRequest for SubscribeRequest { type Result = Result; } -#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)] pub struct SubscribeRequestParams { pub uri: String, } /// Text provided to or from an LLM. -#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)] pub struct TextContent { #[serde(default, skip_serializing_if = "Option::is_none")] pub annotations: Option, @@ -1327,7 +1329,7 @@ pub struct TextContent { pub r#type: String, // &'static str = "text" } -#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)] pub struct TextResourceContents { #[serde(rename = "mimeType", default, skip_serializing_if = "Option::is_none")] pub mime_type: Option, @@ -1336,7 +1338,7 @@ pub struct TextResourceContents { } /// Definition for a tool the client can call. -#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)] pub struct Tool { #[serde(default, skip_serializing_if = "Option::is_none")] pub annotations: Option, @@ -1357,7 +1359,7 @@ pub struct Tool { /// An optional JSON Schema object defining the structure of the tool's output returned in /// the structuredContent field of a CallToolResult. -#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)] pub struct ToolOutputSchema { #[serde(default, skip_serializing_if = "Option::is_none")] pub properties: Option, @@ -1367,7 +1369,7 @@ pub struct ToolOutputSchema { } /// A JSON Schema object defining the expected parameters for the tool. -#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)] pub struct ToolInputSchema { #[serde(default, skip_serializing_if = "Option::is_none")] pub properties: Option, @@ -1384,7 +1386,7 @@ pub struct ToolInputSchema { /// /// Clients should never make tool use decisions based on ToolAnnotations /// received from untrusted servers. -#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)] pub struct ToolAnnotations { #[serde( rename = "destructiveHint", @@ -1414,7 +1416,7 @@ pub struct ToolAnnotations { pub title: Option, } -#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)] pub enum ToolListChangedNotification {} impl ModelContextProtocolNotification for ToolListChangedNotification { @@ -1422,7 +1424,7 @@ impl ModelContextProtocolNotification for ToolListChangedNotification { type Params = Option; } -#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)] pub enum UnsubscribeRequest {} impl ModelContextProtocolRequest for UnsubscribeRequest { @@ -1431,7 +1433,7 @@ impl ModelContextProtocolRequest for UnsubscribeRequest { type Result = Result; } -#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)] pub struct UnsubscribeRequestParams { pub uri: String, } diff --git a/codex-rs/protocol/Cargo.toml b/codex-rs/protocol/Cargo.toml index 43c5eac8f1..9525d0439f 100644 --- a/codex-rs/protocol/Cargo.toml +++ b/codex-rs/protocol/Cargo.toml @@ -18,3 +18,6 @@ serde_json = "1" strum = "0.27.2" strum_macros = "0.27.2" uuid = { version = "1", features = ["serde", "v4"] } + +[dev-dependencies] +pretty_assertions = "1.4.1" diff --git a/codex-rs/protocol/src/config_types.rs b/codex-rs/protocol/src/config_types.rs index 7245a3040a..4d72e27af8 100644 --- a/codex-rs/protocol/src/config_types.rs +++ b/codex-rs/protocol/src/config_types.rs @@ -30,3 +30,18 @@ pub enum ReasoningSummary { /// Option to disable reasoning summaries. None, } + +#[derive(Deserialize, Debug, Clone, Copy, PartialEq, Default, Serialize, Display)] +#[serde(rename_all = "kebab-case")] +#[strum(serialize_all = "kebab-case")] +pub enum SandboxMode { + #[serde(rename = "read-only")] + #[default] + ReadOnly, + + #[serde(rename = "workspace-write")] + WorkspaceWrite, + + #[serde(rename = "danger-full-access")] + DangerFullAccess, +} diff --git a/codex-rs/protocol/src/lib.rs b/codex-rs/protocol/src/lib.rs index ec6a4195d9..458ae6590c 100644 --- a/codex-rs/protocol/src/lib.rs +++ b/codex-rs/protocol/src/lib.rs @@ -1,4 +1,5 @@ pub mod config_types; +pub mod mcp_protocol; pub mod message_history; pub mod parse_command; pub mod plan_tool; diff --git a/codex-rs/mcp-server/src/wire_format.rs b/codex-rs/protocol/src/mcp_protocol.rs similarity index 94% rename from codex-rs/mcp-server/src/wire_format.rs rename to codex-rs/protocol/src/mcp_protocol.rs index 786a512ea8..5110f46976 100644 --- a/codex-rs/mcp-server/src/wire_format.rs +++ b/codex-rs/protocol/src/mcp_protocol.rs @@ -2,19 +2,17 @@ use std::collections::HashMap; use std::fmt::Display; use std::path::PathBuf; -use codex_core::protocol::AskForApproval; -use codex_core::protocol::FileChange; -use codex_core::protocol::ReviewDecision; -use codex_core::protocol::SandboxPolicy; -use codex_core::protocol::TurnAbortReason; -use codex_core::protocol_config_types::ReasoningEffort; -use codex_core::protocol_config_types::ReasoningSummary; +use crate::config_types::ReasoningEffort; +use crate::config_types::ReasoningSummary; +use crate::config_types::SandboxMode; +use crate::protocol::AskForApproval; +use crate::protocol::FileChange; +use crate::protocol::ReviewDecision; +use crate::protocol::SandboxPolicy; +use crate::protocol::TurnAbortReason; use mcp_types::RequestId; use serde::Deserialize; use serde::Serialize; - -use crate::codex_tool_config::CodexToolCallApprovalPolicy; -use crate::codex_tool_config::CodexToolCallSandboxMode; use uuid::Uuid; #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] @@ -91,11 +89,11 @@ pub struct NewConversationParams { /// Approval policy for shell commands generated by the model: /// `untrusted`, `on-failure`, `on-request`, `never`. #[serde(skip_serializing_if = "Option::is_none")] - pub approval_policy: Option, + pub approval_policy: Option, /// Sandbox mode: `read-only`, `workspace-write`, or `danger-full-access`. #[serde(skip_serializing_if = "Option::is_none")] - pub sandbox: Option, + pub sandbox: Option, /// Individual config settings that will override what is in /// CODEX_HOME/config.toml. @@ -308,7 +306,7 @@ mod tests { model: Some("gpt-5".to_string()), profile: None, cwd: None, - approval_policy: Some(CodexToolCallApprovalPolicy::OnRequest), + approval_policy: Some(AskForApproval::OnRequest), sandbox: None, config: None, base_instructions: None, diff --git a/codex-rs/protocol/src/protocol.rs b/codex-rs/protocol/src/protocol.rs index 4c9ba6cff6..4b9a290206 100644 --- a/codex-rs/protocol/src/protocol.rs +++ b/codex-rs/protocol/src/protocol.rs @@ -752,7 +752,7 @@ pub struct TurnAbortedEvent { pub reason: TurnAbortReason, } -#[derive(Debug, Clone, Deserialize, Serialize)] +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)] #[serde(rename_all = "snake_case")] pub enum TurnAbortReason { Interrupted, diff --git a/codex-rs/tui/src/bottom_pane/selection_popup_common.rs b/codex-rs/tui/src/bottom_pane/selection_popup_common.rs index 0a0574c600..6098a957da 100644 --- a/codex-rs/tui/src/bottom_pane/selection_popup_common.rs +++ b/codex-rs/tui/src/bottom_pane/selection_popup_common.rs @@ -101,7 +101,7 @@ pub(crate) fn render_rows( if Some(i) == state.selected_idx { cell = cell.style( Style::default() - .fg(Color::Yellow) + .fg(Color::Cyan) .add_modifier(Modifier::BOLD), ); } else if *is_current { diff --git a/codex-rs/tui/src/colors.rs b/codex-rs/tui/src/colors.rs deleted file mode 100644 index 0ba386df37..0000000000 --- a/codex-rs/tui/src/colors.rs +++ /dev/null @@ -1,4 +0,0 @@ -use ratatui::style::Color; - -pub(crate) const LIGHT_BLUE: Color = Color::Rgb(134, 238, 255); -pub(crate) const SUCCESS_GREEN: Color = Color::Rgb(169, 230, 158); diff --git a/codex-rs/tui/src/history_cell.rs b/codex-rs/tui/src/history_cell.rs index faf01c5e1a..555226dddd 100644 --- a/codex-rs/tui/src/history_cell.rs +++ b/codex-rs/tui/src/history_cell.rs @@ -1,4 +1,3 @@ -use crate::colors::LIGHT_BLUE; use crate::diff_render::create_diff_summary; use crate::exec_command::relativize_to_home; use crate::exec_command::strip_bash_lc_and_escape; @@ -252,12 +251,13 @@ fn new_parsed_command( lines.push(Line::from(spans)); } Some(o) if o.exit_code == 0 => { - lines.push(Line::from("✓ Completed".green().bold())); + lines.push(Line::from(vec!["✓".green(), " Completed".into()])); } Some(o) => { - lines.push(Line::from( - format!("✗ Failed (exit {})", o.exit_code).red().bold(), - )); + lines.push(Line::from(vec![ + "✗".red(), + format!(" Failed (exit {})", o.exit_code).into(), + ])); } }; @@ -304,7 +304,7 @@ fn new_parsed_command( let prefix = if j == 0 { first_prefix } else { " " }; lines.push(Line::from(vec![ Span::styled(prefix, Style::default().add_modifier(Modifier::DIM)), - Span::styled(line_text.to_string(), Style::default().fg(LIGHT_BLUE)), + line_text.to_string().dim(), ])); } } @@ -711,7 +711,7 @@ pub(crate) fn new_plan_update(update: UpdatePlanArgs) -> PlainHistoryCell { Span::styled( step, Style::default() - .fg(Color::Blue) + .fg(Color::Cyan) .add_modifier(Modifier::BOLD), ), ), @@ -913,9 +913,9 @@ fn format_mcp_invocation<'a>(invocation: McpInvocation) -> Line<'a> { .unwrap_or_default(); let invocation_spans = vec![ - Span::styled(invocation.server.clone(), Style::default().fg(Color::Blue)), + Span::styled(invocation.server.clone(), Style::default().fg(Color::Cyan)), Span::raw("."), - Span::styled(invocation.tool.clone(), Style::default().fg(Color::Blue)), + Span::styled(invocation.tool.clone(), Style::default().fg(Color::Cyan)), Span::raw("("), Span::styled(args_str, Style::default().add_modifier(Modifier::DIM)), Span::raw(")"), diff --git a/codex-rs/tui/src/lib.rs b/codex-rs/tui/src/lib.rs index 7d605d683c..e43367a013 100644 --- a/codex-rs/tui/src/lib.rs +++ b/codex-rs/tui/src/lib.rs @@ -2,6 +2,7 @@ // The standalone `codex-tui` binary prints a short help message before the // alternate‑screen mode starts; that file opts‑out locally via `allow`. #![deny(clippy::print_stdout, clippy::print_stderr)] +#![deny(clippy::disallowed_methods)] use app::App; use codex_core::BUILT_IN_OSS_MODEL_PROVIDER_ID; use codex_core::config::Config; @@ -9,11 +10,11 @@ use codex_core::config::ConfigOverrides; use codex_core::config::ConfigToml; use codex_core::config::find_codex_home; use codex_core::config::load_config_as_toml_with_cli_overrides; -use codex_core::config_types::SandboxMode; use codex_core::protocol::AskForApproval; use codex_core::protocol::SandboxPolicy; use codex_login::CodexAuth; use codex_ollama::DEFAULT_OSS_MODEL; +use codex_protocol::config_types::SandboxMode; use std::fs::OpenOptions; use std::path::PathBuf; use tracing::error; @@ -28,7 +29,6 @@ mod bottom_pane; mod chatwidget; mod citation_regex; mod cli; -mod colors; mod common; pub mod custom_terminal; mod diff_render; diff --git a/codex-rs/tui/src/onboarding/auth.rs b/codex-rs/tui/src/onboarding/auth.rs index de98678623..8407e84e21 100644 --- a/codex-rs/tui/src/onboarding/auth.rs +++ b/codex-rs/tui/src/onboarding/auth.rs @@ -9,6 +9,7 @@ use ratatui::prelude::Widget; use ratatui::style::Color; use ratatui::style::Modifier; use ratatui::style::Style; +use ratatui::style::Stylize; use ratatui::text::Line; use ratatui::text::Span; use ratatui::widgets::Paragraph; @@ -19,8 +20,6 @@ use codex_login::AuthMode; use crate::app_event::AppEvent; use crate::app_event_sender::AppEventSender; -use crate::colors::LIGHT_BLUE; -use crate::colors::SUCCESS_GREEN; use crate::onboarding::onboarding_screen::KeyboardHandler; use crate::onboarding::onboarding_screen::StepStateProvider; use crate::shimmer::shimmer_spans; @@ -131,11 +130,8 @@ impl AuthModeWidget { let line1 = if is_selected { Line::from(vec![ - Span::styled( - format!("{} {}. ", caret, idx + 1), - Style::default().fg(LIGHT_BLUE).add_modifier(Modifier::DIM), - ), - Span::styled(text.to_owned(), Style::default().fg(LIGHT_BLUE)), + format!("{} {}. ", caret, idx + 1).cyan().dim(), + text.to_string().cyan(), ]) } else { Line::from(format!(" {}. {text}", idx + 1)) @@ -143,7 +139,8 @@ impl AuthModeWidget { let line2 = if is_selected { Line::from(format!(" {description}")) - .style(Style::default().fg(LIGHT_BLUE).add_modifier(Modifier::DIM)) + .fg(Color::Cyan) + .add_modifier(Modifier::DIM) } else { Line::from(format!(" {description}")) .style(Style::default().add_modifier(Modifier::DIM)) @@ -166,6 +163,8 @@ impl AuthModeWidget { )); lines.push(Line::from("")); lines.push( + // AE: Following styles.md, this should probably be Cyan because it's a user input tip. + // But leaving this for a future cleanup. Line::from(" Press Enter to continue") .style(Style::default().add_modifier(Modifier::DIM)), ); @@ -197,12 +196,7 @@ impl AuthModeWidget { lines.push(Line::from(" If the link doesn't open automatically, open the following link to authenticate:")); lines.push(Line::from(vec![ Span::raw(" "), - Span::styled( - state.auth_url.as_str(), - Style::default() - .fg(LIGHT_BLUE) - .add_modifier(Modifier::UNDERLINED), - ), + state.auth_url.as_str().cyan().underlined(), ])); lines.push(Line::from("")); } @@ -218,8 +212,7 @@ impl AuthModeWidget { fn render_chatgpt_success_message(&self, area: Rect, buf: &mut Buffer) { let lines = vec![ - Line::from("✓ Signed in with your ChatGPT account") - .style(Style::default().fg(SUCCESS_GREEN)), + Line::from("✓ Signed in with your ChatGPT account").fg(Color::Green), Line::from(""), Line::from("> Before you start:"), Line::from(""), @@ -233,8 +226,7 @@ impl AuthModeWidget { ]) .style(Style::default().add_modifier(Modifier::DIM)), Line::from(""), - Line::from(" Codex can make mistakes") - .style(Style::default().fg(Color::White)), + Line::from(" Codex can make mistakes"), Line::from(" Review the code it writes and commands it runs") .style(Style::default().add_modifier(Modifier::DIM)), Line::from(""), @@ -248,7 +240,7 @@ impl AuthModeWidget { ]) .style(Style::default().add_modifier(Modifier::DIM)), Line::from(""), - Line::from(" Press Enter to continue").style(Style::default().fg(LIGHT_BLUE)), + Line::from(" Press Enter to continue").fg(Color::Cyan), ]; Paragraph::new(lines) @@ -257,10 +249,7 @@ impl AuthModeWidget { } fn render_chatgpt_success(&self, area: Rect, buf: &mut Buffer) { - let lines = vec![ - Line::from("✓ Signed in with your ChatGPT account") - .style(Style::default().fg(SUCCESS_GREEN)), - ]; + let lines = vec![Line::from("✓ Signed in with your ChatGPT account").fg(Color::Green)]; Paragraph::new(lines) .wrap(Wrap { trim: false }) @@ -268,8 +257,7 @@ impl AuthModeWidget { } fn render_env_var_found(&self, area: Rect, buf: &mut Buffer) { - let lines = - vec![Line::from("✓ Using OPENAI_API_KEY").style(Style::default().fg(SUCCESS_GREEN))]; + let lines = vec![Line::from("✓ Using OPENAI_API_KEY").fg(Color::Green)]; Paragraph::new(lines) .wrap(Wrap { trim: false }) @@ -281,7 +269,7 @@ impl AuthModeWidget { Line::from( " To use Codex with the OpenAI API, set OPENAI_API_KEY in your environment", ) - .style(Style::default().fg(Color::Blue)), + .style(Style::default().fg(Color::Cyan)), Line::from(""), Line::from(" Press Enter to return") .style(Style::default().add_modifier(Modifier::DIM)), diff --git a/codex-rs/tui/src/onboarding/trust_directory.rs b/codex-rs/tui/src/onboarding/trust_directory.rs index 3be9bac1ac..84f7aef8eb 100644 --- a/codex-rs/tui/src/onboarding/trust_directory.rs +++ b/codex-rs/tui/src/onboarding/trust_directory.rs @@ -18,8 +18,6 @@ use ratatui::widgets::Paragraph; use ratatui::widgets::WidgetRef; use ratatui::widgets::Wrap; -use crate::colors::LIGHT_BLUE; - use crate::onboarding::onboarding_screen::KeyboardHandler; use crate::onboarding::onboarding_screen::StepStateProvider; @@ -77,13 +75,7 @@ impl WidgetRef for &TrustDirectoryWidget { |idx: usize, option: TrustDirectorySelection, text: &str| -> Line<'static> { let is_selected = self.highlighted == option; if is_selected { - Line::from(vec![ - Span::styled( - format!("> {}. ", idx + 1), - Style::default().fg(LIGHT_BLUE).add_modifier(Modifier::DIM), - ), - Span::styled(text.to_owned(), Style::default().fg(LIGHT_BLUE)), - ]) + Line::from(format!("> {}. {text}", idx + 1)).cyan() } else { Line::from(format!(" {}. {}", idx + 1, text)) } @@ -117,6 +109,8 @@ impl WidgetRef for &TrustDirectoryWidget { lines.push(Line::from(format!(" {error}")).fg(Color::Red)); lines.push(Line::from("")); } + // AE: Following styles.md, this should probably be Cyan because it's a user input tip. + // But leaving this for a future cleanup. lines.push(Line::from(" Press Enter to continue").add_modifier(Modifier::DIM)); Paragraph::new(lines) diff --git a/codex-rs/tui/src/shimmer.rs b/codex-rs/tui/src/shimmer.rs index 0d0e5d0bd3..2523f6eccf 100644 --- a/codex-rs/tui/src/shimmer.rs +++ b/codex-rs/tui/src/shimmer.rs @@ -46,9 +46,14 @@ pub(crate) fn shimmer_spans(text: &str) -> Vec> { let brightness = 0.4 + 0.6 * t; let level = (brightness * 255.0).clamp(0.0, 255.0) as u8; let style = if has_true_color { - Style::default() - .fg(Color::Rgb(level, level, level)) - .add_modifier(Modifier::BOLD) + // Allow custom RGB colors, as the implementation is thoughtfully + // adjusting the level of the default foreground color. + #[allow(clippy::disallowed_methods)] + { + Style::default() + .fg(Color::Rgb(level, level, level)) + .add_modifier(Modifier::BOLD) + } } else { color_for_level(level) }; diff --git a/codex-rs/tui/src/user_approval_widget.rs b/codex-rs/tui/src/user_approval_widget.rs index 7a4e3d9008..34cac5b4cd 100644 --- a/codex-rs/tui/src/user_approval_widget.rs +++ b/codex-rs/tui/src/user_approval_widget.rs @@ -145,7 +145,7 @@ impl UserApprovalWidget<'_> { } => { let cmd = strip_bash_lc_and_escape(command); let mut contents: Vec = to_command_display( - vec!["? ".fg(Color::Blue), "Codex wants to run ".bold()], + vec!["? ".fg(Color::Cyan), "Codex wants to run ".bold()], cmd, vec![], ); diff --git a/codex-rs/tui/styles.md b/codex-rs/tui/styles.md new file mode 100644 index 0000000000..378970ada7 --- /dev/null +++ b/codex-rs/tui/styles.md @@ -0,0 +1,21 @@ +# Headers, primary, and secondary text + +- **Headers:** Use `bold`. For markdown with various header levels, leave in the `#` signs. +- **Primary text:** Default. +- **Secondary text:** Use `dim`. + +# Foreground colors + +- **Default:** Most of the time, just use the default foreground color. `reset` can help get it back. +- **User input tips, selection, and status indicators:** Use ANSI `cyan`. +- **Success and additions:** Use ANSI `green`. +- **Errors, failures and deletions:** Use ANSI `red`. +- **Codex:** Use ANSI `magenta`. + +# Avoid + +- Avoid custom colors because there's no guarantee that they'll contrast well or look good in various terminal color themes. (`shimmer.rs` is an exception that works well because we take the default colors and just adjust their levels.) +- Avoid ANSI `black` & `white` as foreground colors because the default terminal theme color will do a better job. (Use `reset` if you need to in order to get those.) The exception is if you need contrast rendering over a manually colored background. +- Avoid ANSI `blue` and `yellow` because for now the style guide doesn't use them. Prefer a foreground color mentioned above. + +(There are some rules to try to catch this in `clippy.toml`.)