mcp_server.rs

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