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