mcp_server.rs

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