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::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<AcpClientDelegate>>,
 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            .to_string_lossy()
 72            .to_string();
 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<AcpClientDelegate>>,
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 result = Self::handle_read_tool_call(input, delegate, cx).await?;
180                Ok(CallToolResponse {
181                    content: vec![ToolResponseContent::Text {
182                        text: serde_json::to_string(&result)?,
183                    }],
184                    is_error: None,
185                    meta: None,
186                })
187            } else if request.name.as_str() == EDIT_TOOL {
188                let input =
189                    serde_json::from_value(request.arguments.context("Arguments required")?)?;
190
191                let result = Self::handle_edit_tool_call(input, delegate, cx).await?;
192                Ok(CallToolResponse {
193                    content: vec![ToolResponseContent::Text {
194                        text: serde_json::to_string(&result)?,
195                    }],
196                    is_error: None,
197                    meta: None,
198                })
199            } else {
200                anyhow::bail!("Unsupported tool");
201            }
202        })
203    }
204
205    fn handle_read_tool_call(
206        params: ReadToolParams,
207        delegate: AcpClientDelegate,
208        cx: &AsyncApp,
209    ) -> Task<Result<ReadToolResponse>> {
210        cx.foreground_executor().spawn(async move {
211            let response = delegate
212                .read_text_file(ReadTextFileParams {
213                    path: params.abs_path,
214                    line: params.offset,
215                    limit: params.limit,
216                })
217                .await?;
218
219            Ok(ReadToolResponse {
220                content: response.content,
221            })
222        })
223    }
224
225    fn handle_edit_tool_call(
226        params: EditToolParams,
227        delegate: AcpClientDelegate,
228        cx: &AsyncApp,
229    ) -> Task<Result<EditToolResponse>> {
230        cx.foreground_executor().spawn(async move {
231            let response = delegate
232                .read_text_file_reusing_snapshot(ReadTextFileParams {
233                    path: params.abs_path.clone(),
234                    line: None,
235                    limit: None,
236                })
237                .await?;
238
239            let new_content = response.content.replace(&params.old_text, &params.new_text);
240            if new_content == response.content {
241                return Err(anyhow::anyhow!("The old_text was not found in the content"));
242            }
243
244            delegate
245                .write_text_file(WriteTextFileParams {
246                    path: params.abs_path,
247                    content: new_content,
248                })
249                .await?;
250
251            Ok(EditToolResponse)
252        })
253    }
254
255    fn handle_permissions_tool_call(
256        params: PermissionToolParams,
257        delegate: AcpClientDelegate,
258        tool_id_map: Rc<RefCell<HashMap<String, acp::ToolCallId>>>,
259        cx: &AsyncApp,
260    ) -> Task<Result<PermissionToolResponse>> {
261        cx.foreground_executor().spawn(async move {
262            let claude_tool = ClaudeTool::infer(&params.tool_name, params.input.clone());
263
264            let tool_call_id = match params.tool_use_id {
265                Some(tool_use_id) => tool_id_map
266                    .borrow()
267                    .get(&tool_use_id)
268                    .cloned()
269                    .context("Tool call ID not found")?,
270
271                None => delegate.push_tool_call(claude_tool.as_acp()).await?.id,
272            };
273
274            let outcome = delegate
275                .request_existing_tool_call_confirmation(
276                    tool_call_id,
277                    claude_tool.confirmation(None),
278                )
279                .await?;
280
281            match outcome {
282                acp::ToolCallConfirmationOutcome::Allow
283                | acp::ToolCallConfirmationOutcome::AlwaysAllow
284                | acp::ToolCallConfirmationOutcome::AlwaysAllowMcpServer
285                | acp::ToolCallConfirmationOutcome::AlwaysAllowTool => Ok(PermissionToolResponse {
286                    behavior: PermissionToolBehavior::Allow,
287                    updated_input: params.input,
288                }),
289                acp::ToolCallConfirmationOutcome::Reject
290                | acp::ToolCallConfirmationOutcome::Cancel => Ok(PermissionToolResponse {
291                    behavior: PermissionToolBehavior::Deny,
292                    updated_input: params.input,
293                }),
294            }
295        })
296    }
297}
298
299#[derive(Serialize)]
300#[serde(rename_all = "camelCase")]
301pub struct McpConfig {
302    pub mcp_servers: HashMap<String, McpServerConfig>,
303}
304
305#[derive(Serialize)]
306#[serde(rename_all = "camelCase")]
307pub struct McpServerConfig {
308    command: String,
309    args: Vec<String>,
310    #[serde(skip_serializing_if = "Option::is_none")]
311    env: Option<HashMap<String, String>>,
312}