mcp_server.rs

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