mcp_server.rs

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