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, ReadToolParams},
 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        let zed_path = std::env::current_exe()
 73            .context("finding current executable path for use in mcp_server")?
 74            .to_string_lossy()
 75            .to_string();
 76
 77        Ok(McpServerConfig {
 78            command: zed_path,
 79            args: vec![
 80                "--nc".into(),
 81                self.server.socket_path().display().to_string(),
 82            ],
 83            env: None,
 84        })
 85    }
 86
 87    fn handle_initialize(_: InitializeParams, cx: &App) -> Task<Result<InitializeResponse>> {
 88        cx.foreground_executor().spawn(async move {
 89            Ok(InitializeResponse {
 90                protocol_version: ProtocolVersion("2025-06-18".into()),
 91                capabilities: ServerCapabilities {
 92                    experimental: None,
 93                    logging: None,
 94                    completions: None,
 95                    prompts: None,
 96                    resources: None,
 97                    tools: Some(ToolsCapabilities {
 98                        list_changed: Some(false),
 99                    }),
100                },
101                server_info: Implementation {
102                    name: SERVER_NAME.into(),
103                    version: "0.1.0".into(),
104                },
105                meta: None,
106            })
107        })
108    }
109
110    fn handle_list_tools(_: (), cx: &App) -> Task<Result<ListToolsResponse>> {
111        cx.foreground_executor().spawn(async move {
112            Ok(ListToolsResponse {
113                tools: vec![
114                    Tool {
115                        name: PERMISSION_TOOL.into(),
116                        input_schema: schemars::schema_for!(PermissionToolParams).into(),
117                        description: None,
118                        annotations: None,
119                    },
120                    Tool {
121                        name: READ_TOOL.into(),
122                        input_schema: schemars::schema_for!(ReadToolParams).into(),
123                        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()),
124                        annotations: Some(ToolAnnotations {
125                            title: Some("Read file".to_string()),
126                            read_only_hint: Some(true),
127                            destructive_hint: Some(false),
128                            open_world_hint: Some(false),
129                            // if time passes the contents might change, but it's not going to do anything different
130                            // true or false seem too strong, let's try a none.
131                            idempotent_hint: None,
132                        }),
133                    },
134                    Tool {
135                        name: EDIT_TOOL.into(),
136                        input_schema: schemars::schema_for!(EditToolParams).into(),
137                        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()),
138                        annotations: Some(ToolAnnotations {
139                            title: Some("Edit file".to_string()),
140                            read_only_hint: Some(false),
141                            destructive_hint: Some(false),
142                            open_world_hint: Some(false),
143                            idempotent_hint: Some(false),
144                        }),
145                    },
146                ],
147                next_cursor: None,
148                meta: None,
149            })
150        })
151    }
152
153    fn handle_call_tool(
154        request: CallToolParams,
155        mut delegate_watch: watch::Receiver<Option<AcpClientDelegate>>,
156        tool_id_map: Rc<RefCell<HashMap<String, acp::ToolCallId>>>,
157        cx: &App,
158    ) -> Task<Result<CallToolResponse>> {
159        cx.spawn(async move |cx| {
160            let Some(delegate) = delegate_watch.recv().await? else {
161                debug_panic!("Sent None delegate");
162                anyhow::bail!("Server not available");
163            };
164
165            if request.name.as_str() == PERMISSION_TOOL {
166                let input =
167                    serde_json::from_value(request.arguments.context("Arguments required")?)?;
168
169                let result =
170                    Self::handle_permissions_tool_call(input, delegate, tool_id_map, cx).await?;
171                Ok(CallToolResponse {
172                    content: vec![ToolResponseContent::Text {
173                        text: serde_json::to_string(&result)?,
174                    }],
175                    is_error: None,
176                    meta: None,
177                })
178            } else if request.name.as_str() == READ_TOOL {
179                let input =
180                    serde_json::from_value(request.arguments.context("Arguments required")?)?;
181
182                let content = Self::handle_read_tool_call(input, delegate, cx).await?;
183                Ok(CallToolResponse {
184                    content,
185                    is_error: None,
186                    meta: None,
187                })
188            } else if request.name.as_str() == EDIT_TOOL {
189                let input =
190                    serde_json::from_value(request.arguments.context("Arguments required")?)?;
191
192                Self::handle_edit_tool_call(input, delegate, cx).await?;
193                Ok(CallToolResponse {
194                    content: vec![],
195                    is_error: None,
196                    meta: None,
197                })
198            } else {
199                anyhow::bail!("Unsupported tool");
200            }
201        })
202    }
203
204    fn handle_read_tool_call(
205        params: ReadToolParams,
206        delegate: AcpClientDelegate,
207        cx: &AsyncApp,
208    ) -> Task<Result<Vec<ToolResponseContent>>> {
209        cx.foreground_executor().spawn(async move {
210            let response = delegate
211                .read_text_file(ReadTextFileParams {
212                    path: params.abs_path,
213                    line: params.offset,
214                    limit: params.limit,
215                })
216                .await?;
217
218            Ok(vec![ToolResponseContent::Text {
219                text: response.content,
220            }])
221        })
222    }
223
224    fn handle_edit_tool_call(
225        params: EditToolParams,
226        delegate: AcpClientDelegate,
227        cx: &AsyncApp,
228    ) -> Task<Result<()>> {
229        cx.foreground_executor().spawn(async move {
230            let response = delegate
231                .read_text_file_reusing_snapshot(ReadTextFileParams {
232                    path: params.abs_path.clone(),
233                    line: None,
234                    limit: None,
235                })
236                .await?;
237
238            let new_content = response.content.replace(&params.old_text, &params.new_text);
239            if new_content == response.content {
240                return Err(anyhow::anyhow!("The old_text was not found in the content"));
241            }
242
243            delegate
244                .write_text_file(WriteTextFileParams {
245                    path: params.abs_path,
246                    content: new_content,
247                })
248                .await?;
249
250            Ok(())
251        })
252    }
253
254    fn handle_permissions_tool_call(
255        params: PermissionToolParams,
256        delegate: AcpClientDelegate,
257        tool_id_map: Rc<RefCell<HashMap<String, acp::ToolCallId>>>,
258        cx: &AsyncApp,
259    ) -> Task<Result<PermissionToolResponse>> {
260        cx.foreground_executor().spawn(async move {
261            let claude_tool = ClaudeTool::infer(&params.tool_name, params.input.clone());
262
263            let tool_call_id = match params.tool_use_id {
264                Some(tool_use_id) => tool_id_map
265                    .borrow()
266                    .get(&tool_use_id)
267                    .cloned()
268                    .context("Tool call ID not found")?,
269
270                None => delegate.push_tool_call(claude_tool.as_acp()).await?.id,
271            };
272
273            let outcome = delegate
274                .request_existing_tool_call_confirmation(
275                    tool_call_id,
276                    claude_tool.confirmation(None),
277                )
278                .await?;
279
280            match outcome {
281                acp::ToolCallConfirmationOutcome::Allow
282                | acp::ToolCallConfirmationOutcome::AlwaysAllow
283                | acp::ToolCallConfirmationOutcome::AlwaysAllowMcpServer
284                | acp::ToolCallConfirmationOutcome::AlwaysAllowTool => Ok(PermissionToolResponse {
285                    behavior: PermissionToolBehavior::Allow,
286                    updated_input: params.input,
287                }),
288                acp::ToolCallConfirmationOutcome::Reject
289                | acp::ToolCallConfirmationOutcome::Cancel => Ok(PermissionToolResponse {
290                    behavior: PermissionToolBehavior::Deny,
291                    updated_input: params.input,
292                }),
293            }
294        })
295    }
296}