mcp_server.rs

  1use std::path::PathBuf;
  2
  3use crate::claude::tools::{ClaudeTool, EditToolParams, ReadToolParams};
  4use acp_thread::AcpThread;
  5use agent_client_protocol as acp;
  6use anyhow::{Context, Result};
  7use collections::HashMap;
  8use context_server::listener::{McpServerTool, ToolResponse};
  9use context_server::types::{
 10    Implementation, InitializeParams, InitializeResponse, ProtocolVersion, ServerCapabilities,
 11    ToolAnnotations, ToolResponseContent, ToolsCapabilities, requests,
 12};
 13use gpui::{App, AsyncApp, Task, WeakEntity};
 14use schemars::JsonSchema;
 15use serde::{Deserialize, Serialize};
 16
 17pub struct ClaudeZedMcpServer {
 18    server: context_server::listener::McpServer,
 19}
 20
 21pub const SERVER_NAME: &str = "zed";
 22
 23impl ClaudeZedMcpServer {
 24    pub async fn new(
 25        thread_rx: watch::Receiver<WeakEntity<AcpThread>>,
 26        cx: &AsyncApp,
 27    ) -> Result<Self> {
 28        let mut mcp_server = context_server::listener::McpServer::new(cx).await?;
 29        mcp_server.handle_request::<requests::Initialize>(Self::handle_initialize);
 30
 31        mcp_server.add_tool(PermissionTool {
 32            thread_rx: thread_rx.clone(),
 33        });
 34        mcp_server.add_tool(ReadTool {
 35            thread_rx: thread_rx.clone(),
 36        });
 37        mcp_server.add_tool(EditTool {
 38            thread_rx: thread_rx.clone(),
 39        });
 40
 41        Ok(Self { server: mcp_server })
 42    }
 43
 44    pub fn server_config(&self) -> Result<McpServerConfig> {
 45        let zed_path = std::env::current_exe()
 46            .context("finding current executable path for use in mcp_server")?;
 47
 48        Ok(McpServerConfig {
 49            command: zed_path,
 50            args: vec![
 51                "--nc".into(),
 52                self.server.socket_path().display().to_string(),
 53            ],
 54            env: None,
 55        })
 56    }
 57
 58    fn handle_initialize(_: InitializeParams, cx: &App) -> Task<Result<InitializeResponse>> {
 59        cx.foreground_executor().spawn(async move {
 60            Ok(InitializeResponse {
 61                protocol_version: ProtocolVersion("2025-06-18".into()),
 62                capabilities: ServerCapabilities {
 63                    experimental: None,
 64                    logging: None,
 65                    completions: None,
 66                    prompts: None,
 67                    resources: None,
 68                    tools: Some(ToolsCapabilities {
 69                        list_changed: Some(false),
 70                    }),
 71                },
 72                server_info: Implementation {
 73                    name: SERVER_NAME.into(),
 74                    version: "0.1.0".into(),
 75                },
 76                meta: None,
 77            })
 78        })
 79    }
 80}
 81
 82#[derive(Serialize)]
 83#[serde(rename_all = "camelCase")]
 84pub struct McpConfig {
 85    pub mcp_servers: HashMap<String, McpServerConfig>,
 86}
 87
 88#[derive(Serialize, Clone)]
 89#[serde(rename_all = "camelCase")]
 90pub struct McpServerConfig {
 91    pub command: PathBuf,
 92    pub args: Vec<String>,
 93    #[serde(skip_serializing_if = "Option::is_none")]
 94    pub env: Option<HashMap<String, String>>,
 95}
 96
 97// Tools
 98
 99#[derive(Clone)]
100pub struct PermissionTool {
101    thread_rx: watch::Receiver<WeakEntity<AcpThread>>,
102}
103
104#[derive(Deserialize, JsonSchema, Debug)]
105pub struct PermissionToolParams {
106    tool_name: String,
107    input: serde_json::Value,
108    tool_use_id: Option<String>,
109}
110
111#[derive(Serialize)]
112#[serde(rename_all = "camelCase")]
113pub struct PermissionToolResponse {
114    behavior: PermissionToolBehavior,
115    updated_input: serde_json::Value,
116}
117
118#[derive(Serialize)]
119#[serde(rename_all = "snake_case")]
120enum PermissionToolBehavior {
121    Allow,
122    Deny,
123}
124
125impl McpServerTool for PermissionTool {
126    type Input = PermissionToolParams;
127    const NAME: &'static str = "Confirmation";
128
129    fn description(&self) -> &'static str {
130        "Request permission for tool calls"
131    }
132
133    async fn run(&self, input: Self::Input, cx: &mut AsyncApp) -> Result<ToolResponse> {
134        let mut thread_rx = self.thread_rx.clone();
135        let Some(thread) = thread_rx.recv().await?.upgrade() else {
136            anyhow::bail!("Thread closed");
137        };
138
139        let claude_tool = ClaudeTool::infer(&input.tool_name, input.input.clone());
140        let tool_call_id = acp::ToolCallId(input.tool_use_id.context("Tool ID required")?.into());
141        let allow_option_id = acp::PermissionOptionId("allow".into());
142        let reject_option_id = acp::PermissionOptionId("reject".into());
143
144        let chosen_option = thread
145            .update(cx, |thread, cx| {
146                thread.request_tool_call_permission(
147                    claude_tool.as_acp(tool_call_id),
148                    vec![
149                        acp::PermissionOption {
150                            id: allow_option_id.clone(),
151                            label: "Allow".into(),
152                            kind: acp::PermissionOptionKind::AllowOnce,
153                        },
154                        acp::PermissionOption {
155                            id: reject_option_id.clone(),
156                            label: "Reject".into(),
157                            kind: acp::PermissionOptionKind::RejectOnce,
158                        },
159                    ],
160                    cx,
161                )
162            })?
163            .await?;
164
165        let response = if chosen_option == allow_option_id {
166            PermissionToolResponse {
167                behavior: PermissionToolBehavior::Allow,
168                updated_input: input.input,
169            }
170        } else {
171            PermissionToolResponse {
172                behavior: PermissionToolBehavior::Deny,
173                updated_input: input.input,
174            }
175        };
176
177        Ok(ToolResponse {
178            content: vec![ToolResponseContent::Text {
179                text: serde_json::to_string(&response)?,
180            }],
181            structured_content: None,
182        })
183    }
184}
185
186#[derive(Clone)]
187pub struct ReadTool {
188    thread_rx: watch::Receiver<WeakEntity<AcpThread>>,
189}
190
191impl McpServerTool for ReadTool {
192    type Input = ReadToolParams;
193    const NAME: &'static str = "Read";
194
195    fn description(&self) -> &'static str {
196        "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."
197    }
198
199    fn annotations(&self) -> ToolAnnotations {
200        ToolAnnotations {
201            title: Some("Read file".to_string()),
202            read_only_hint: Some(true),
203            destructive_hint: Some(false),
204            open_world_hint: Some(false),
205            idempotent_hint: None,
206        }
207    }
208
209    async fn run(&self, input: Self::Input, cx: &mut AsyncApp) -> Result<ToolResponse> {
210        let mut thread_rx = self.thread_rx.clone();
211        let Some(thread) = thread_rx.recv().await?.upgrade() else {
212            anyhow::bail!("Thread closed");
213        };
214
215        let content = thread
216            .update(cx, |thread, cx| {
217                thread.read_text_file(input.abs_path, input.offset, input.limit, false, cx)
218            })?
219            .await?;
220
221        Ok(ToolResponse {
222            content: vec![ToolResponseContent::Text { text: content }],
223            structured_content: None,
224        })
225    }
226}
227
228#[derive(Clone)]
229pub struct EditTool {
230    thread_rx: watch::Receiver<WeakEntity<AcpThread>>,
231}
232
233impl McpServerTool for EditTool {
234    type Input = EditToolParams;
235    const NAME: &'static str = "Edit";
236
237    fn description(&self) -> &'static str {
238        "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."
239    }
240
241    fn annotations(&self) -> ToolAnnotations {
242        ToolAnnotations {
243            title: Some("Edit file".to_string()),
244            read_only_hint: Some(false),
245            destructive_hint: Some(false),
246            open_world_hint: Some(false),
247            idempotent_hint: Some(false),
248        }
249    }
250
251    async fn run(&self, input: Self::Input, cx: &mut AsyncApp) -> Result<ToolResponse> {
252        let mut thread_rx = self.thread_rx.clone();
253        let Some(thread) = thread_rx.recv().await?.upgrade() else {
254            anyhow::bail!("Thread closed");
255        };
256
257        let content = thread
258            .update(cx, |thread, cx| {
259                thread.read_text_file(input.abs_path.clone(), None, None, true, cx)
260            })?
261            .await?;
262
263        let new_content = content.replace(&input.old_text, &input.new_text);
264        if new_content == content {
265            return Err(anyhow::anyhow!("The old_text was not found in the content"));
266        }
267
268        thread
269            .update(cx, |thread, cx| {
270                thread.write_text_file(input.abs_path, new_content, cx)
271            })?
272            .await?;
273
274        Ok(ToolResponse {
275            content: vec![],
276            structured_content: None,
277        })
278    }
279}