mcp_server.rs

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