mcp_server.rs

  1use std::path::PathBuf;
  2
  3use crate::claude::tools::{ClaudeTool, EditToolParams, ReadToolParams};
  4use acp_thread::AcpThread;
  5use agent_client_protocol as acp;
  6use anyhow::{Context, Result};
  7use collections::HashMap;
  8use context_server::listener::{McpServerTool, ToolResponse};
  9use context_server::types::{
 10    Implementation, InitializeParams, InitializeResponse, ProtocolVersion, ServerCapabilities,
 11    ToolAnnotations, ToolResponseContent, ToolsCapabilities, requests,
 12};
 13use gpui::{App, AsyncApp, Task, WeakEntity};
 14use schemars::JsonSchema;
 15use serde::{Deserialize, Serialize};
 16
 17pub struct ClaudeZedMcpServer {
 18    server: context_server::listener::McpServer,
 19}
 20
 21pub const SERVER_NAME: &str = "zed";
 22
 23impl ClaudeZedMcpServer {
 24    pub async fn new(
 25        thread_rx: watch::Receiver<WeakEntity<AcpThread>>,
 26        cx: &AsyncApp,
 27    ) -> Result<Self> {
 28        let mut mcp_server = context_server::listener::McpServer::new(cx).await?;
 29        mcp_server.handle_request::<requests::Initialize>(Self::handle_initialize);
 30
 31        mcp_server.add_tool(PermissionTool {
 32            thread_rx: thread_rx.clone(),
 33        });
 34        mcp_server.add_tool(ReadTool {
 35            thread_rx: thread_rx.clone(),
 36        });
 37        mcp_server.add_tool(EditTool {
 38            thread_rx: thread_rx.clone(),
 39        });
 40
 41        Ok(Self { server: mcp_server })
 42    }
 43
 44    pub fn server_config(&self) -> Result<McpServerConfig> {
 45        let zed_path = std::env::current_exe()
 46            .context("finding current executable path for use in mcp_server")?;
 47
 48        Ok(McpServerConfig {
 49            command: zed_path,
 50            args: vec![
 51                "--nc".into(),
 52                self.server.socket_path().display().to_string(),
 53            ],
 54            env: None,
 55        })
 56    }
 57
 58    fn handle_initialize(_: InitializeParams, cx: &App) -> Task<Result<InitializeResponse>> {
 59        cx.foreground_executor().spawn(async move {
 60            Ok(InitializeResponse {
 61                protocol_version: ProtocolVersion("2025-06-18".into()),
 62                capabilities: ServerCapabilities {
 63                    experimental: None,
 64                    logging: None,
 65                    completions: None,
 66                    prompts: None,
 67                    resources: None,
 68                    tools: Some(ToolsCapabilities {
 69                        list_changed: Some(false),
 70                    }),
 71                },
 72                server_info: Implementation {
 73                    name: SERVER_NAME.into(),
 74                    version: "0.1.0".into(),
 75                },
 76                meta: None,
 77            })
 78        })
 79    }
 80}
 81
 82#[derive(Serialize)]
 83#[serde(rename_all = "camelCase")]
 84pub struct McpConfig {
 85    pub mcp_servers: HashMap<String, McpServerConfig>,
 86}
 87
 88#[derive(Serialize, Clone)]
 89#[serde(rename_all = "camelCase")]
 90pub struct McpServerConfig {
 91    pub command: PathBuf,
 92    pub args: Vec<String>,
 93    #[serde(skip_serializing_if = "Option::is_none")]
 94    pub env: Option<HashMap<String, String>>,
 95}
 96
 97// Tools
 98
 99#[derive(Clone)]
100pub struct PermissionTool {
101    thread_rx: watch::Receiver<WeakEntity<AcpThread>>,
102}
103
104#[derive(Deserialize, JsonSchema, Debug)]
105pub struct PermissionToolParams {
106    tool_name: String,
107    input: serde_json::Value,
108    tool_use_id: Option<String>,
109}
110
111#[derive(Serialize)]
112#[serde(rename_all = "camelCase")]
113pub struct PermissionToolResponse {
114    behavior: PermissionToolBehavior,
115    updated_input: serde_json::Value,
116}
117
118#[derive(Serialize)]
119#[serde(rename_all = "snake_case")]
120enum PermissionToolBehavior {
121    Allow,
122    Deny,
123}
124
125impl McpServerTool for PermissionTool {
126    type Input = PermissionToolParams;
127    type Output = ();
128
129    const NAME: &'static str = "Confirmation";
130
131    fn description(&self) -> &'static str {
132        "Request permission for tool calls"
133    }
134
135    async fn run(
136        &self,
137        input: Self::Input,
138        cx: &mut AsyncApp,
139    ) -> Result<ToolResponse<Self::Output>> {
140        let mut thread_rx = self.thread_rx.clone();
141        let Some(thread) = thread_rx.recv().await?.upgrade() else {
142            anyhow::bail!("Thread closed");
143        };
144
145        let claude_tool = ClaudeTool::infer(&input.tool_name, input.input.clone());
146        let tool_call_id = acp::ToolCallId(input.tool_use_id.context("Tool ID required")?.into());
147        let allow_option_id = acp::PermissionOptionId("allow".into());
148        let reject_option_id = acp::PermissionOptionId("reject".into());
149
150        let chosen_option = thread
151            .update(cx, |thread, cx| {
152                thread.request_tool_call_permission(
153                    claude_tool.as_acp(tool_call_id),
154                    vec![
155                        acp::PermissionOption {
156                            id: allow_option_id.clone(),
157                            label: "Allow".into(),
158                            kind: acp::PermissionOptionKind::AllowOnce,
159                        },
160                        acp::PermissionOption {
161                            id: reject_option_id.clone(),
162                            label: "Reject".into(),
163                            kind: acp::PermissionOptionKind::RejectOnce,
164                        },
165                    ],
166                    cx,
167                )
168            })?
169            .await?;
170
171        let response = if chosen_option == allow_option_id {
172            PermissionToolResponse {
173                behavior: PermissionToolBehavior::Allow,
174                updated_input: input.input,
175            }
176        } else {
177            PermissionToolResponse {
178                behavior: PermissionToolBehavior::Deny,
179                updated_input: input.input,
180            }
181        };
182
183        Ok(ToolResponse {
184            content: vec![ToolResponseContent::Text {
185                text: serde_json::to_string(&response)?,
186            }],
187            structured_content: (),
188        })
189    }
190}
191
192#[derive(Clone)]
193pub struct ReadTool {
194    thread_rx: watch::Receiver<WeakEntity<AcpThread>>,
195}
196
197impl McpServerTool for ReadTool {
198    type Input = ReadToolParams;
199    type Output = ();
200
201    const NAME: &'static str = "Read";
202
203    fn description(&self) -> &'static str {
204        "Read the contents of a file. In sessions with mcp__zed__Read always use it instead of Read as it contains the most up-to-date contents."
205    }
206
207    fn annotations(&self) -> ToolAnnotations {
208        ToolAnnotations {
209            title: Some("Read file".to_string()),
210            read_only_hint: Some(true),
211            destructive_hint: Some(false),
212            open_world_hint: Some(false),
213            idempotent_hint: None,
214        }
215    }
216
217    async fn run(
218        &self,
219        input: Self::Input,
220        cx: &mut AsyncApp,
221    ) -> Result<ToolResponse<Self::Output>> {
222        let mut thread_rx = self.thread_rx.clone();
223        let Some(thread) = thread_rx.recv().await?.upgrade() else {
224            anyhow::bail!("Thread closed");
225        };
226
227        let content = thread
228            .update(cx, |thread, cx| {
229                thread.read_text_file(input.abs_path, input.offset, input.limit, false, cx)
230            })?
231            .await?;
232
233        Ok(ToolResponse {
234            content: vec![ToolResponseContent::Text { text: content }],
235            structured_content: (),
236        })
237    }
238}
239
240#[derive(Clone)]
241pub struct EditTool {
242    thread_rx: watch::Receiver<WeakEntity<AcpThread>>,
243}
244
245impl McpServerTool for EditTool {
246    type Input = EditToolParams;
247    type Output = ();
248
249    const NAME: &'static str = "Edit";
250
251    fn description(&self) -> &'static str {
252        "Edits a file. In sessions with mcp__zed__Edit always use it instead of Edit as it will show the diff to the user better."
253    }
254
255    fn annotations(&self) -> ToolAnnotations {
256        ToolAnnotations {
257            title: Some("Edit file".to_string()),
258            read_only_hint: Some(false),
259            destructive_hint: Some(false),
260            open_world_hint: Some(false),
261            idempotent_hint: Some(false),
262        }
263    }
264
265    async fn run(
266        &self,
267        input: Self::Input,
268        cx: &mut AsyncApp,
269    ) -> Result<ToolResponse<Self::Output>> {
270        let mut thread_rx = self.thread_rx.clone();
271        let Some(thread) = thread_rx.recv().await?.upgrade() else {
272            anyhow::bail!("Thread closed");
273        };
274
275        let content = thread
276            .update(cx, |thread, cx| {
277                thread.read_text_file(input.abs_path.clone(), None, None, true, cx)
278            })?
279            .await?;
280
281        let new_content = content.replace(&input.old_text, &input.new_text);
282        if new_content == content {
283            return Err(anyhow::anyhow!("The old_text was not found in the content"));
284        }
285
286        thread
287            .update(cx, |thread, cx| {
288                thread.write_text_file(input.abs_path, new_content, cx)
289            })?
290            .await?;
291
292        Ok(ToolResponse {
293            content: vec![],
294            structured_content: (),
295        })
296    }
297}