diff --git a/codex-rs/mcp-server/src/mcp_protocol.rs b/codex-rs/mcp-server/src/mcp_protocol.rs index 933c145b..e507376c 100644 --- a/codex-rs/mcp-server/src/mcp_protocol.rs +++ b/codex-rs/mcp-server/src/mcp_protocol.rs @@ -1,6 +1,7 @@ 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; @@ -21,7 +22,7 @@ pub struct MessageId(pub Uuid); pub struct ToolCallRequest { #[serde(rename = "jsonrpc")] pub jsonrpc: &'static str, - pub id: u64, + pub id: RequestId, pub method: &'static str, pub params: ToolCallRequestParams, } @@ -38,7 +39,7 @@ pub enum ToolCallRequestParams { impl ToolCallRequestParams { /// Wrap this request in a JSON-RPC request. #[allow(dead_code)] - pub fn into_request(self, id: u64) -> ToolCallRequest { + pub fn into_request(self, id: RequestId) -> ToolCallRequest { ToolCallRequest { jsonrpc: "2.0", id, @@ -95,70 +96,13 @@ pub struct ConversationStreamArgs { #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct ConversationSendMessageArgs { pub conversation_id: ConversationId, - pub content: Vec, + 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, } - -/// Input items for a message. -/// Following OpenAI's Responses API: https://platform.openai.com/docs/api-reference/responses -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -#[serde(tag = "type", rename_all = "camelCase")] -pub enum MessageInputItem { - Text { - text: String, - }, - Image { - #[serde(flatten)] - source: ImageSource, - #[serde(default, skip_serializing_if = "Option::is_none")] - detail: Option, - }, - File { - #[serde(flatten)] - source: FileSource, - }, -} - -/// Source of an image. -/// Following OpenAI's API: https://platform.openai.com/docs/guides/images-vision#giving-a-model-images-as-input -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -#[serde(untagged)] -pub enum ImageSource { - ImageUrl { image_url: String }, - FileId { file_id: String }, -} - -/// Source of a file. -/// Following OpenAI's Responses API: https://platform.openai.com/docs/guides/pdf-files?api-mode=responses#uploading-files -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -#[serde(untagged)] -pub enum FileSource { - Url { - file_url: String, - }, - Id { - file_id: String, - }, - Base64 { - #[serde(default, skip_serializing_if = "Option::is_none")] - filename: Option, - // Base64-encoded file contents. - file_data: String, - }, -} - -#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub enum ImageDetail { - Low, - High, - Auto, -} - #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct ConversationsListArgs { #[serde(default, skip_serializing_if = "Option::is_none")] @@ -303,6 +247,8 @@ pub enum ClientNotification { #[allow(clippy::expect_used)] #[allow(clippy::unwrap_used)] mod tests { + use std::path::PathBuf; + use super::*; use codex_core::protocol::McpInvocation; use codex_core::protocol::McpToolCallBeginEvent; @@ -331,7 +277,7 @@ mod tests { base_instructions: None, }); - let observed = to_val(&req.into_request(2)); + let observed = to_val(&req.into_request(mcp_types::RequestId::Integer(2))); let expected = json!({ "jsonrpc": "2.0", "id": 2, @@ -354,18 +300,12 @@ mod tests { let req = ToolCallRequestParams::ConversationSendMessage(ConversationSendMessageArgs { conversation_id: ConversationId(uuid!("d0f6ecbe-84a2-41c1-b23d-b20473b25eab")), content: vec![ - MessageInputItem::Text { text: "Hi".into() }, - MessageInputItem::Image { - source: ImageSource::ImageUrl { - image_url: "https://example.com/cat.jpg".into(), - }, - detail: Some(ImageDetail::High), + InputItem::Text { text: "Hi".into() }, + InputItem::Image { + image_url: "https://example.com/cat.jpg".into(), }, - MessageInputItem::File { - source: FileSource::Base64 { - filename: Some("notes.txt".into()), - file_data: "Zm9vYmFy".into(), - }, + InputItem::LocalImage { + path: "notes.txt".into(), }, ], parent_message_id: Some(MessageId(uuid!("67e55044-10b1-426f-9247-bb680e5fe0c8"))), @@ -380,7 +320,7 @@ mod tests { }), }); - let observed = to_val(&req.into_request(2)); + let observed = to_val(&req.into_request(mcp_types::RequestId::Integer(2))); let expected = json!({ "jsonrpc": "2.0", "id": 2, @@ -391,8 +331,8 @@ mod tests { "conversation_id": "d0f6ecbe-84a2-41c1-b23d-b20473b25eab", "content": [ { "type": "text", "text": "Hi" }, - { "type": "image", "image_url": "https://example.com/cat.jpg", "detail": "high" }, - { "type": "file", "filename": "notes.txt", "file_data": "Zm9vYmFy" } + { "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", @@ -414,7 +354,7 @@ mod tests { cursor: Some("abc".into()), }); - let observed = to_val(&req.into_request(2)); + let observed = to_val(&req.into_request(RequestId::Integer(2))); let expected = json!({ "jsonrpc": "2.0", "id": 2, @@ -436,7 +376,7 @@ mod tests { conversation_id: ConversationId(uuid!("67e55044-10b1-426f-9247-bb680e5fe0c8")), }); - let observed = to_val(&req.into_request(2)); + let observed = to_val(&req.into_request(mcp_types::RequestId::Integer(2))); let expected = json!({ "jsonrpc": "2.0", "id": 2, @@ -454,48 +394,44 @@ mod tests { // ----- Message inputs / sources ----- #[test] - fn serialize_message_input_image_file_id_auto_detail() { - let item = MessageInputItem::Image { - source: ImageSource::FileId { - file_id: "file_123".into(), - }, - detail: Some(ImageDetail::Auto), + 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", - "file_id": "file_123", - "detail": "auto" + "image_url": "https://example.com/x.png" }); assert_eq!(observed, expected); } #[test] - fn serialize_message_input_file_url_and_id_variants() { - let url = MessageInputItem::File { - source: FileSource::Url { - file_url: "https://example.com/a.pdf".into(), - }, + fn serialize_message_input_local_image_path() { + let url = InputItem::LocalImage { + path: PathBuf::from("https://example.com/a.pdf"), }; - let id = MessageInputItem::File { - source: FileSource::Id { - file_id: "file_456".into(), - }, + 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!( - to_val(&url), - json!({"type":"file","file_url":"https://example.com/a.pdf"}) + 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" ); - assert_eq!(to_val(&id), json!({"type":"file","file_id":"file_456"})); } #[test] fn serialize_message_input_image_url_without_detail() { - let item = MessageInputItem::Image { - source: ImageSource::ImageUrl { - image_url: "https://example.com/x.png".into(), - }, - detail: None, + let item = InputItem::Image { + image_url: "https://example.com/x.png".into(), }; let observed = to_val(&item); let expected = json!({