mcp_server.rs

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