mcp_server.rs

  1use std::{cell::RefCell, rc::Rc};
  2
  3use acp_thread::AcpClientDelegate;
  4use agentic_coding_protocol::{self as acp, Client, ReadTextFileParams, WriteTextFileParams};
  5use anyhow::{Context, Result};
  6use collections::HashMap;
  7use context_server::{
  8    listener::McpServer,
  9    types::{
 10        CallToolParams, CallToolResponse, Implementation, InitializeParams, InitializeResponse,
 11        ListToolsResponse, ProtocolVersion, ServerCapabilities, Tool, ToolAnnotations,
 12        ToolResponseContent, ToolsCapabilities, requests,
 13    },
 14};
 15use gpui::{App, AsyncApp, Task};
 16use schemars::JsonSchema;
 17use serde::{Deserialize, Serialize};
 18use util::debug_panic;
 19
 20use crate::claude::{
 21    McpServerConfig,
 22    tools::{ClaudeTool, EditToolParams, EditToolResponse, ReadToolParams, ReadToolResponse},
 23};
 24
 25pub struct ClaudeMcpServer {
 26    server: McpServer,
 27}
 28
 29pub const SERVER_NAME: &str = "zed";
 30pub const READ_TOOL: &str = "Read";
 31pub const EDIT_TOOL: &str = "Edit";
 32pub const PERMISSION_TOOL: &str = "Confirmation";
 33
 34#[derive(Deserialize, JsonSchema, Debug)]
 35struct PermissionToolParams {
 36    tool_name: String,
 37    input: serde_json::Value,
 38    tool_use_id: Option<String>,
 39}
 40
 41#[derive(Serialize)]
 42#[serde(rename_all = "camelCase")]
 43struct PermissionToolResponse {
 44    behavior: PermissionToolBehavior,
 45    updated_input: serde_json::Value,
 46}
 47
 48#[derive(Serialize)]
 49#[serde(rename_all = "snake_case")]
 50enum PermissionToolBehavior {
 51    Allow,
 52    Deny,
 53}
 54
 55impl ClaudeMcpServer {
 56    pub async fn new(
 57        delegate: watch::Receiver<Option<AcpClientDelegate>>,
 58        tool_id_map: Rc<RefCell<HashMap<String, acp::ToolCallId>>>,
 59        cx: &AsyncApp,
 60    ) -> Result<Self> {
 61        let mut mcp_server = McpServer::new(cx).await?;
 62        mcp_server.handle_request::<requests::Initialize>(Self::handle_initialize);
 63        mcp_server.handle_request::<requests::ListTools>(Self::handle_list_tools);
 64        mcp_server.handle_request::<requests::CallTool>(move |request, cx| {
 65            Self::handle_call_tool(request, delegate.clone(), tool_id_map.clone(), cx)
 66        });
 67
 68        Ok(Self { server: mcp_server })
 69    }
 70
 71    pub fn server_config(&self) -> Result<McpServerConfig> {
 72        #[cfg(not(target_os = "windows"))]
 73        let zed_path = util::get_shell_safe_zed_path()?;
 74        #[cfg(target_os = "windows")]
 75        let zed_path = std::env::current_exe()
 76            .context("finding current executable path for use in mcp_server")?
 77            .to_string_lossy()
 78            .to_string();
 79
 80        Ok(McpServerConfig {
 81            command: zed_path,
 82            args: vec![
 83                "--nc".into(),
 84                self.server.socket_path().display().to_string(),
 85            ],
 86            env: None,
 87        })
 88    }
 89
 90    fn handle_initialize(_: InitializeParams, cx: &App) -> Task<Result<InitializeResponse>> {
 91        cx.foreground_executor().spawn(async move {
 92            Ok(InitializeResponse {
 93                protocol_version: ProtocolVersion("2025-06-18".into()),
 94                capabilities: ServerCapabilities {
 95                    experimental: None,
 96                    logging: None,
 97                    completions: None,
 98                    prompts: None,
 99                    resources: None,
100                    tools: Some(ToolsCapabilities {
101                        list_changed: Some(false),
102                    }),
103                },
104                server_info: Implementation {
105                    name: SERVER_NAME.into(),
106                    version: "0.1.0".into(),
107                },
108                meta: None,
109            })
110        })
111    }
112
113    fn handle_list_tools(_: (), cx: &App) -> Task<Result<ListToolsResponse>> {
114        cx.foreground_executor().spawn(async move {
115            Ok(ListToolsResponse {
116                tools: vec![
117                    Tool {
118                        name: PERMISSION_TOOL.into(),
119                        input_schema: schemars::schema_for!(PermissionToolParams).into(),
120                        description: None,
121                        annotations: None,
122                    },
123                    Tool {
124                        name: READ_TOOL.into(),
125                        input_schema: schemars::schema_for!(ReadToolParams).into(),
126                        description: Some("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.".to_string()),
127                        annotations: Some(ToolAnnotations {
128                            title: Some("Read file".to_string()),
129                            read_only_hint: Some(true),
130                            destructive_hint: Some(false),
131                            open_world_hint: Some(false),
132                            // if time passes the contents might change, but it's not going to do anything different
133                            // true or false seem too strong, let's try a none.
134                            idempotent_hint: None,
135                        }),
136                    },
137                    Tool {
138                        name: EDIT_TOOL.into(),
139                        input_schema: schemars::schema_for!(EditToolParams).into(),
140                        description: Some("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.".to_string()),
141                        annotations: Some(ToolAnnotations {
142                            title: Some("Edit file".to_string()),
143                            read_only_hint: Some(false),
144                            destructive_hint: Some(false),
145                            open_world_hint: Some(false),
146                            idempotent_hint: Some(false),
147                        }),
148                    },
149                ],
150                next_cursor: None,
151                meta: None,
152            })
153        })
154    }
155
156    fn handle_call_tool(
157        request: CallToolParams,
158        mut delegate_watch: watch::Receiver<Option<AcpClientDelegate>>,
159        tool_id_map: Rc<RefCell<HashMap<String, acp::ToolCallId>>>,
160        cx: &App,
161    ) -> Task<Result<CallToolResponse>> {
162        cx.spawn(async move |cx| {
163            let Some(delegate) = delegate_watch.recv().await? else {
164                debug_panic!("Sent None delegate");
165                anyhow::bail!("Server not available");
166            };
167
168            if request.name.as_str() == PERMISSION_TOOL {
169                let input =
170                    serde_json::from_value(request.arguments.context("Arguments required")?)?;
171
172                let result =
173                    Self::handle_permissions_tool_call(input, delegate, tool_id_map, cx).await?;
174                Ok(CallToolResponse {
175                    content: vec![ToolResponseContent::Text {
176                        text: serde_json::to_string(&result)?,
177                    }],
178                    is_error: None,
179                    meta: None,
180                })
181            } else if request.name.as_str() == READ_TOOL {
182                let input =
183                    serde_json::from_value(request.arguments.context("Arguments required")?)?;
184
185                let result = Self::handle_read_tool_call(input, delegate, cx).await?;
186                Ok(CallToolResponse {
187                    content: vec![ToolResponseContent::Text {
188                        text: serde_json::to_string(&result)?,
189                    }],
190                    is_error: None,
191                    meta: None,
192                })
193            } else if request.name.as_str() == EDIT_TOOL {
194                let input =
195                    serde_json::from_value(request.arguments.context("Arguments required")?)?;
196
197                let result = Self::handle_edit_tool_call(input, delegate, cx).await?;
198                Ok(CallToolResponse {
199                    content: vec![ToolResponseContent::Text {
200                        text: serde_json::to_string(&result)?,
201                    }],
202                    is_error: None,
203                    meta: None,
204                })
205            } else {
206                anyhow::bail!("Unsupported tool");
207            }
208        })
209    }
210
211    fn handle_read_tool_call(
212        params: ReadToolParams,
213        delegate: AcpClientDelegate,
214        cx: &AsyncApp,
215    ) -> Task<Result<ReadToolResponse>> {
216        cx.foreground_executor().spawn(async move {
217            let response = delegate
218                .read_text_file(ReadTextFileParams {
219                    path: params.abs_path,
220                    line: params.offset,
221                    limit: params.limit,
222                })
223                .await?;
224
225            Ok(ReadToolResponse {
226                content: response.content,
227            })
228        })
229    }
230
231    fn handle_edit_tool_call(
232        params: EditToolParams,
233        delegate: AcpClientDelegate,
234        cx: &AsyncApp,
235    ) -> Task<Result<EditToolResponse>> {
236        cx.foreground_executor().spawn(async move {
237            let response = delegate
238                .read_text_file_reusing_snapshot(ReadTextFileParams {
239                    path: params.abs_path.clone(),
240                    line: None,
241                    limit: None,
242                })
243                .await?;
244
245            let new_content = response.content.replace(&params.old_text, &params.new_text);
246            if new_content == response.content {
247                return Err(anyhow::anyhow!("The old_text was not found in the content"));
248            }
249
250            delegate
251                .write_text_file(WriteTextFileParams {
252                    path: params.abs_path,
253                    content: new_content,
254                })
255                .await?;
256
257            Ok(EditToolResponse)
258        })
259    }
260
261    fn handle_permissions_tool_call(
262        params: PermissionToolParams,
263        delegate: AcpClientDelegate,
264        tool_id_map: Rc<RefCell<HashMap<String, acp::ToolCallId>>>,
265        cx: &AsyncApp,
266    ) -> Task<Result<PermissionToolResponse>> {
267        cx.foreground_executor().spawn(async move {
268            let claude_tool = ClaudeTool::infer(&params.tool_name, params.input.clone());
269
270            let tool_call_id = match params.tool_use_id {
271                Some(tool_use_id) => tool_id_map
272                    .borrow()
273                    .get(&tool_use_id)
274                    .cloned()
275                    .context("Tool call ID not found")?,
276
277                None => delegate.push_tool_call(claude_tool.as_acp()).await?.id,
278            };
279
280            let outcome = delegate
281                .request_existing_tool_call_confirmation(
282                    tool_call_id,
283                    claude_tool.confirmation(None),
284                )
285                .await?;
286
287            match outcome {
288                acp::ToolCallConfirmationOutcome::Allow
289                | acp::ToolCallConfirmationOutcome::AlwaysAllow
290                | acp::ToolCallConfirmationOutcome::AlwaysAllowMcpServer
291                | acp::ToolCallConfirmationOutcome::AlwaysAllowTool => Ok(PermissionToolResponse {
292                    behavior: PermissionToolBehavior::Allow,
293                    updated_input: params.input,
294                }),
295                acp::ToolCallConfirmationOutcome::Reject
296                | acp::ToolCallConfirmationOutcome::Cancel => Ok(PermissionToolResponse {
297                    behavior: PermissionToolBehavior::Deny,
298                    updated_input: params.input,
299                }),
300            }
301        })
302    }
303}