From f56910556f1821b5799a9083d0b13bb8313e487d Mon Sep 17 00:00:00 2001 From: Agus Zubiaga Date: Mon, 21 Jul 2025 15:19:50 -0300 Subject: [PATCH] Connect to Codex MCP server --- crates/agent_servers/src/agent_servers.rs | 3 + crates/agent_servers/src/claude.rs | 30 +-- crates/agent_servers/src/codex.rs | 191 ++++++++++++++++++ .../src/{claude => }/mcp_server.rs | 40 ++-- crates/agent_servers/src/settings.rs | 11 +- crates/agent_ui/src/agent_panel.rs | 7 + crates/agent_ui/src/agent_ui.rs | 2 + crates/context_server/src/client.rs | 1 + 8 files changed, 247 insertions(+), 38 deletions(-) create mode 100644 crates/agent_servers/src/codex.rs rename crates/agent_servers/src/{claude => }/mcp_server.rs (91%) diff --git a/crates/agent_servers/src/agent_servers.rs b/crates/agent_servers/src/agent_servers.rs index 6d9c77f2968d7b39302391829d2d14b6d4493a91..48536e13ac28d0c16e59d7eb021d5ab39a850cf4 100644 --- a/crates/agent_servers/src/agent_servers.rs +++ b/crates/agent_servers/src/agent_servers.rs @@ -1,5 +1,7 @@ mod claude; +mod codex; mod gemini; +mod mcp_server; mod settings; mod stdio_agent_server; @@ -7,6 +9,7 @@ mod stdio_agent_server; mod e2e_tests; pub use claude::*; +pub use codex::*; pub use gemini::*; pub use settings::*; pub use stdio_agent_server::*; diff --git a/crates/agent_servers/src/claude.rs b/crates/agent_servers/src/claude.rs index 8b3d93a122d07448ddbfb9daf2dfc9226fb11545..6891b1b3c18ec50e9ce95a34818fcfea0eb6c451 100644 --- a/crates/agent_servers/src/claude.rs +++ b/crates/agent_servers/src/claude.rs @@ -1,5 +1,4 @@ -mod mcp_server; -mod tools; +pub mod tools; use collections::HashMap; use project::Project; @@ -27,8 +26,8 @@ use gpui::{App, AppContext, Entity, Task}; use serde::{Deserialize, Serialize}; use util::ResultExt; -use crate::claude::mcp_server::ClaudeMcpServer; use crate::claude::tools::ClaudeTool; +use crate::mcp_server::{McpConfig, ZedMcpServer}; use crate::{AgentServer, AgentServerCommand, AllAgentServersSettings}; use acp_thread::{AcpClientDelegate, AcpThread, AgentConnection}; @@ -70,11 +69,11 @@ impl AgentServer for ClaudeCode { let tool_id_map = Rc::new(RefCell::new(HashMap::default())); let permission_mcp_server = - ClaudeMcpServer::new(delegate_rx, tool_id_map.clone(), cx).await?; + ZedMcpServer::new(delegate_rx, tool_id_map.clone(), cx).await?; let mut mcp_servers = HashMap::default(); mcp_servers.insert( - mcp_server::SERVER_NAME.to_string(), + crate::mcp_server::SERVER_NAME.to_string(), permission_mcp_server.server_config()?, ); let mcp_config = McpConfig { mcp_servers }; @@ -112,8 +111,8 @@ impl AgentServer for ClaudeCode { "--permission-prompt-tool", &format!( "mcp__{}__{}", - mcp_server::SERVER_NAME, - mcp_server::PERMISSION_TOOL + crate::mcp_server::SERVER_NAME, + crate::mcp_server::PERMISSION_TOOL ), "--allowedTools", "mcp__zed__Read,mcp__zed__Edit", @@ -249,7 +248,7 @@ struct ClaudeAgentConnection { delegate: AcpClientDelegate, outgoing_tx: UnboundedSender, end_turn_tx: Rc>>>>, - _mcp_server: Option, + _mcp_server: Option, _handler_task: Task<()>, } @@ -576,21 +575,6 @@ enum PermissionMode { Plan, } -#[derive(Serialize)] -#[serde(rename_all = "camelCase")] -struct McpConfig { - mcp_servers: HashMap, -} - -#[derive(Serialize)] -#[serde(rename_all = "camelCase")] -struct McpServerConfig { - command: String, - args: Vec, - #[serde(skip_serializing_if = "Option::is_none")] - env: Option>, -} - #[cfg(test)] pub(crate) mod tests { use super::*; diff --git a/crates/agent_servers/src/codex.rs b/crates/agent_servers/src/codex.rs new file mode 100644 index 0000000000000000000000000000000000000000..c6f146a46cffcd0b79b28088c122af042d763832 --- /dev/null +++ b/crates/agent_servers/src/codex.rs @@ -0,0 +1,191 @@ +use collections::HashMap; +use context_server::types::CallToolParams; +use context_server::types::requests::CallTool; +use context_server::{ContextServer, ContextServerCommand, ContextServerId}; +use project::Project; +use settings::SettingsStore; +use std::cell::RefCell; +use std::path::{Path, PathBuf}; +use std::rc::Rc; +use std::sync::Arc; + +use agentic_coding_protocol::{self as acp, AnyAgentRequest, AnyAgentResult, ProtocolVersion}; +use anyhow::{Context, Result, anyhow}; +use futures::future::LocalBoxFuture; +use futures::{AsyncWriteExt, FutureExt}; +use gpui::{App, AppContext, Entity, Task}; +use serde::{Deserialize, Serialize}; +use util::ResultExt; + +use crate::mcp_server::{McpConfig, ZedMcpServer}; +use crate::{AgentServer, AgentServerCommand, AllAgentServersSettings}; +use acp_thread::{AcpClientDelegate, AcpThread, AgentConnection}; + +#[derive(Clone)] +pub struct Codex; + +impl AgentServer for Codex { + fn name(&self) -> &'static str { + "Codex" + } + + fn empty_state_headline(&self) -> &'static str { + self.name() + } + + fn empty_state_message(&self) -> &'static str { + "" + } + + fn logo(&self) -> ui::IconName { + ui::IconName::AiOpenAi + } + + fn supports_always_allow(&self) -> bool { + false + } + + fn new_thread( + &self, + root_dir: &Path, + project: &Entity, + cx: &mut App, + ) -> Task>> { + let project = project.clone(); + let root_dir = root_dir.to_path_buf(); + let title = self.name().into(); + cx.spawn(async move |cx| { + let (mut delegate_tx, delegate_rx) = watch::channel(None); + let tool_id_map = Rc::new(RefCell::new(HashMap::default())); + + let zed_mcp_server = ZedMcpServer::new(delegate_rx, tool_id_map.clone(), cx).await?; + + let mut mcp_servers = HashMap::default(); + mcp_servers.insert( + crate::mcp_server::SERVER_NAME.to_string(), + zed_mcp_server.server_config()?, + ); + let mcp_config = McpConfig { mcp_servers }; + + // todo! pass zed mcp server to codex tool + let mcp_config_file = tempfile::NamedTempFile::new()?; + let (mcp_config_file, _mcp_config_path) = mcp_config_file.into_parts(); + + let mut mcp_config_file = smol::fs::File::from(mcp_config_file); + mcp_config_file + .write_all(serde_json::to_string(&mcp_config)?.as_bytes()) + .await?; + mcp_config_file.flush().await?; + + let settings = cx.read_global(|settings: &SettingsStore, _| { + settings.get::(None).codex.clone() + })?; + + let Some(command) = + AgentServerCommand::resolve("codex", &["mcp"], settings, &project, cx).await + else { + anyhow::bail!("Failed to find codex binary"); + }; + + let codex_mcp_client: Arc = ContextServer::stdio( + ContextServerId("codex-mcp-server".into()), + ContextServerCommand { + // todo! should we change ContextServerCommand to take a PathBuf? + path: command.path.to_string_lossy().to_string(), + args: command.args, + env: command.env, + }, + ) + .into(); + + ContextServer::start(codex_mcp_client.clone(), cx).await?; + // todo! stop + + cx.new(|cx| { + // todo! handle notifications + let delegate = AcpClientDelegate::new(cx.entity().downgrade(), cx.to_async()); + delegate_tx.send(Some(delegate.clone())).log_err(); + + let connection = CodexAgentConnection { + root_dir, + codex_mcp_client, + _zed_mcp_server: zed_mcp_server, + }; + + acp_thread::AcpThread::new(connection, title, None, project.clone(), cx) + }) + }) + } +} + +impl AgentConnection for CodexAgentConnection { + /// Send a request to the agent and wait for a response. + fn request_any( + &self, + params: AnyAgentRequest, + ) -> LocalBoxFuture<'static, Result> { + let client = self.codex_mcp_client.client(); + let root_dir = self.root_dir.clone(); + async move { + let client = client.context("Codex MCP server is not initialized")?; + + match params { + // todo: consider sending an empty request so we get the init response? + AnyAgentRequest::InitializeParams(_) => Ok(AnyAgentResult::InitializeResponse( + acp::InitializeResponse { + is_authenticated: true, + protocol_version: ProtocolVersion::latest(), + }, + )), + AnyAgentRequest::AuthenticateParams(_) => { + Err(anyhow!("Authentication not supported")) + } + AnyAgentRequest::SendUserMessageParams(message) => { + client + .request::(CallToolParams { + name: "codex".into(), + arguments: Some(serde_json::to_value(CodexToolCallParam { + prompt: message + .chunks + .into_iter() + .filter_map(|chunk| match chunk { + acp::UserMessageChunk::Text { text } => Some(text), + acp::UserMessageChunk::Path { .. } => { + // todo! + None + } + }) + .collect(), + cwd: root_dir, + })?), + meta: None, + }) + .await?; + + Ok(AnyAgentResult::SendUserMessageResponse( + acp::SendUserMessageResponse, + )) + } + AnyAgentRequest::CancelSendMessageParams(_) => Ok( + AnyAgentResult::CancelSendMessageResponse(acp::CancelSendMessageResponse), + ), + } + } + .boxed_local() + } +} + +struct CodexAgentConnection { + codex_mcp_client: Arc, + root_dir: PathBuf, + _zed_mcp_server: ZedMcpServer, +} + +/// todo! use types from h2a crate when we have one + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub(crate) struct CodexToolCallParam { + pub prompt: String, + pub cwd: PathBuf, +} diff --git a/crates/agent_servers/src/claude/mcp_server.rs b/crates/agent_servers/src/mcp_server.rs similarity index 91% rename from crates/agent_servers/src/claude/mcp_server.rs rename to crates/agent_servers/src/mcp_server.rs index 468027c4c3dd4c3391dc8196e54a840ce01a965b..e9295686c08cab5d006ddc423b9076b754534200 100644 --- a/crates/agent_servers/src/claude/mcp_server.rs +++ b/crates/agent_servers/src/mcp_server.rs @@ -4,26 +4,23 @@ use acp_thread::AcpClientDelegate; use agentic_coding_protocol::{self as acp, Client, ReadTextFileParams, WriteTextFileParams}; use anyhow::{Context, Result}; use collections::HashMap; -use context_server::{ - listener::McpServer, - types::{ - CallToolParams, CallToolResponse, Implementation, InitializeParams, InitializeResponse, - ListToolsResponse, ProtocolVersion, ServerCapabilities, Tool, ToolAnnotations, - ToolResponseContent, ToolsCapabilities, requests, - }, +use context_server::types::{ + CallToolParams, CallToolResponse, Implementation, InitializeParams, InitializeResponse, + ListToolsResponse, ProtocolVersion, ServerCapabilities, Tool, ToolAnnotations, + ToolResponseContent, ToolsCapabilities, requests, }; use gpui::{App, AsyncApp, Task}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use util::debug_panic; -use crate::claude::{ - McpServerConfig, - tools::{ClaudeTool, EditToolParams, EditToolResponse, ReadToolParams, ReadToolResponse}, +// todo! use shared tool inference? +use crate::claude::tools::{ + ClaudeTool, EditToolParams, EditToolResponse, ReadToolParams, ReadToolResponse, }; -pub struct ClaudeMcpServer { - server: McpServer, +pub struct ZedMcpServer { + server: context_server::listener::McpServer, } pub const SERVER_NAME: &str = "zed"; @@ -52,13 +49,13 @@ enum PermissionToolBehavior { Deny, } -impl ClaudeMcpServer { +impl ZedMcpServer { pub async fn new( delegate: watch::Receiver>, tool_id_map: Rc>>, cx: &AsyncApp, ) -> Result { - let mut mcp_server = McpServer::new(cx).await?; + let mut mcp_server = context_server::listener::McpServer::new(cx).await?; mcp_server.handle_request::(Self::handle_initialize); mcp_server.handle_request::(Self::handle_list_tools); mcp_server.handle_request::(move |request, cx| { @@ -298,3 +295,18 @@ impl ClaudeMcpServer { }) } } + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +pub struct McpConfig { + pub mcp_servers: HashMap, +} + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +pub struct McpServerConfig { + command: String, + args: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + env: Option>, +} diff --git a/crates/agent_servers/src/settings.rs b/crates/agent_servers/src/settings.rs index 645674b5f15087250c2364fb9a8a846e163ad54c..aeb34a5e61df382e99e8cb5f8b613993d6bd82b0 100644 --- a/crates/agent_servers/src/settings.rs +++ b/crates/agent_servers/src/settings.rs @@ -13,6 +13,7 @@ pub fn init(cx: &mut App) { pub struct AllAgentServersSettings { pub gemini: Option, pub claude: Option, + pub codex: Option, } #[derive(Deserialize, Serialize, Clone, JsonSchema, Debug)] @@ -29,13 +30,21 @@ impl settings::Settings for AllAgentServersSettings { fn load(sources: SettingsSources, _: &mut App) -> Result { let mut settings = AllAgentServersSettings::default(); - for AllAgentServersSettings { gemini, claude } in sources.defaults_and_customizations() { + for AllAgentServersSettings { + gemini, + claude, + codex, + } in sources.defaults_and_customizations() + { if gemini.is_some() { settings.gemini = gemini.clone(); } if claude.is_some() { settings.claude = claude.clone(); } + if codex.is_some() { + settings.codex = codex.clone(); + } } Ok(settings) diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index 36851e44bac6e2455c19a403e417f9722db6b7c6..a731a7c1bea6f446abc2be3ddf4f124fd34fee95 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -1938,6 +1938,13 @@ impl AgentPanel { } .boxed_clone(), ) + .action( + "New Codex Thread", + NewExternalAgentThread { + agent: Some(crate::ExternalAgent::Codex), + } + .boxed_clone(), + ) }); menu })) diff --git a/crates/agent_ui/src/agent_ui.rs b/crates/agent_ui/src/agent_ui.rs index 7f69e8f66e3bcf37fb56c0384c0b8bf17a37d0f4..17fa4d1ea8e22695b580639de42463e7fe732ebb 100644 --- a/crates/agent_ui/src/agent_ui.rs +++ b/crates/agent_ui/src/agent_ui.rs @@ -147,6 +147,7 @@ enum ExternalAgent { #[default] Gemini, ClaudeCode, + Codex, } impl ExternalAgent { @@ -154,6 +155,7 @@ impl ExternalAgent { match self { ExternalAgent::Gemini => Rc::new(agent_servers::Gemini), ExternalAgent::ClaudeCode => Rc::new(agent_servers::ClaudeCode), + ExternalAgent::Codex => Rc::new(agent_servers::Codex), } } } diff --git a/crates/context_server/src/client.rs b/crates/context_server/src/client.rs index 6b24d9b136efc2d9cc99843e54027058e1602861..befe961841ff82f8cb4b457b7e0ac75d4fb16628 100644 --- a/crates/context_server/src/client.rs +++ b/crates/context_server/src/client.rs @@ -243,6 +243,7 @@ impl Client { } } } else if let Ok(notification) = serde_json::from_str::(&message) { + dbg!(¬ification); let mut notification_handlers = notification_handlers.lock(); if let Some(handler) = notification_handlers.get_mut(notification.method.as_str()) { handler(notification.params.unwrap_or(Value::Null), cx.clone());