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