Detailed changes
@@ -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::*;
@@ -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<SdkMessage>,
end_turn_tx: Rc<RefCell<Option<oneshot::Sender<Result<()>>>>>,
- _mcp_server: Option<ClaudeMcpServer>,
+ _mcp_server: Option<ZedMcpServer>,
_handler_task: Task<()>,
}
@@ -576,21 +575,6 @@ enum PermissionMode {
Plan,
}
-#[derive(Serialize)]
-#[serde(rename_all = "camelCase")]
-struct McpConfig {
- mcp_servers: HashMap<String, McpServerConfig>,
-}
-
-#[derive(Serialize)]
-#[serde(rename_all = "camelCase")]
-struct McpServerConfig {
- command: String,
- args: Vec<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- env: Option<HashMap<String, String>>,
-}
-
#[cfg(test)]
pub(crate) mod tests {
use super::*;
@@ -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<Project>,
+ cx: &mut App,
+ ) -> Task<Result<Entity<AcpThread>>> {
+ 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::<AllAgentServersSettings>(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> = 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<acp::AnyAgentResult>> {
+ 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::<CallTool>(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<context_server::ContextServer>,
+ 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,
+}
@@ -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<Option<AcpClientDelegate>>,
tool_id_map: Rc<RefCell<HashMap<String, acp::ToolCallId>>>,
cx: &AsyncApp,
) -> Result<Self> {
- let mut mcp_server = McpServer::new(cx).await?;
+ let mut mcp_server = context_server::listener::McpServer::new(cx).await?;
mcp_server.handle_request::<requests::Initialize>(Self::handle_initialize);
mcp_server.handle_request::<requests::ListTools>(Self::handle_list_tools);
mcp_server.handle_request::<requests::CallTool>(move |request, cx| {
@@ -298,3 +295,18 @@ impl ClaudeMcpServer {
})
}
}
+
+#[derive(Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct McpConfig {
+ pub mcp_servers: HashMap<String, McpServerConfig>,
+}
+
+#[derive(Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct McpServerConfig {
+ command: String,
+ args: Vec<String>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ env: Option<HashMap<String, String>>,
+}
@@ -13,6 +13,7 @@ pub fn init(cx: &mut App) {
pub struct AllAgentServersSettings {
pub gemini: Option<AgentServerSettings>,
pub claude: Option<AgentServerSettings>,
+ pub codex: Option<AgentServerSettings>,
}
#[derive(Deserialize, Serialize, Clone, JsonSchema, Debug)]
@@ -29,13 +30,21 @@ impl settings::Settings for AllAgentServersSettings {
fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> Result<Self> {
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)
@@ -1938,6 +1938,13 @@ impl AgentPanel {
}
.boxed_clone(),
)
+ .action(
+ "New Codex Thread",
+ NewExternalAgentThread {
+ agent: Some(crate::ExternalAgent::Codex),
+ }
+ .boxed_clone(),
+ )
});
menu
}))
@@ -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),
}
}
}
@@ -243,6 +243,7 @@ impl Client {
}
}
} else if let Ok(notification) = serde_json::from_str::<AnyNotification>(&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());