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 type Output = ();
128
129 const NAME: &'static str = "Confirmation";
130
131 fn description(&self) -> &'static str {
132 "Request permission for tool calls"
133 }
134
135 async fn run(
136 &self,
137 input: Self::Input,
138 cx: &mut AsyncApp,
139 ) -> Result<ToolResponse<Self::Output>> {
140 let mut thread_rx = self.thread_rx.clone();
141 let Some(thread) = thread_rx.recv().await?.upgrade() else {
142 anyhow::bail!("Thread closed");
143 };
144
145 let claude_tool = ClaudeTool::infer(&input.tool_name, input.input.clone());
146 let tool_call_id = acp::ToolCallId(input.tool_use_id.context("Tool ID required")?.into());
147 let allow_option_id = acp::PermissionOptionId("allow".into());
148 let reject_option_id = acp::PermissionOptionId("reject".into());
149
150 let chosen_option = thread
151 .update(cx, |thread, cx| {
152 thread.request_tool_call_permission(
153 claude_tool.as_acp(tool_call_id),
154 vec![
155 acp::PermissionOption {
156 id: allow_option_id.clone(),
157 label: "Allow".into(),
158 kind: acp::PermissionOptionKind::AllowOnce,
159 },
160 acp::PermissionOption {
161 id: reject_option_id.clone(),
162 label: "Reject".into(),
163 kind: acp::PermissionOptionKind::RejectOnce,
164 },
165 ],
166 cx,
167 )
168 })?
169 .await?;
170
171 let response = if chosen_option == allow_option_id {
172 PermissionToolResponse {
173 behavior: PermissionToolBehavior::Allow,
174 updated_input: input.input,
175 }
176 } else {
177 PermissionToolResponse {
178 behavior: PermissionToolBehavior::Deny,
179 updated_input: input.input,
180 }
181 };
182
183 Ok(ToolResponse {
184 content: vec![ToolResponseContent::Text {
185 text: serde_json::to_string(&response)?,
186 }],
187 structured_content: (),
188 })
189 }
190}
191
192#[derive(Clone)]
193pub struct ReadTool {
194 thread_rx: watch::Receiver<WeakEntity<AcpThread>>,
195}
196
197impl McpServerTool for ReadTool {
198 type Input = ReadToolParams;
199 type Output = ();
200
201 const NAME: &'static str = "Read";
202
203 fn description(&self) -> &'static str {
204 "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."
205 }
206
207 fn annotations(&self) -> ToolAnnotations {
208 ToolAnnotations {
209 title: Some("Read file".to_string()),
210 read_only_hint: Some(true),
211 destructive_hint: Some(false),
212 open_world_hint: Some(false),
213 idempotent_hint: None,
214 }
215 }
216
217 async fn run(
218 &self,
219 input: Self::Input,
220 cx: &mut AsyncApp,
221 ) -> Result<ToolResponse<Self::Output>> {
222 let mut thread_rx = self.thread_rx.clone();
223 let Some(thread) = thread_rx.recv().await?.upgrade() else {
224 anyhow::bail!("Thread closed");
225 };
226
227 let content = thread
228 .update(cx, |thread, cx| {
229 thread.read_text_file(input.abs_path, input.offset, input.limit, false, cx)
230 })?
231 .await?;
232
233 Ok(ToolResponse {
234 content: vec![ToolResponseContent::Text { text: content }],
235 structured_content: (),
236 })
237 }
238}
239
240#[derive(Clone)]
241pub struct EditTool {
242 thread_rx: watch::Receiver<WeakEntity<AcpThread>>,
243}
244
245impl McpServerTool for EditTool {
246 type Input = EditToolParams;
247 type Output = ();
248
249 const NAME: &'static str = "Edit";
250
251 fn description(&self) -> &'static str {
252 "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."
253 }
254
255 fn annotations(&self) -> ToolAnnotations {
256 ToolAnnotations {
257 title: Some("Edit file".to_string()),
258 read_only_hint: Some(false),
259 destructive_hint: Some(false),
260 open_world_hint: Some(false),
261 idempotent_hint: Some(false),
262 }
263 }
264
265 async fn run(
266 &self,
267 input: Self::Input,
268 cx: &mut AsyncApp,
269 ) -> Result<ToolResponse<Self::Output>> {
270 let mut thread_rx = self.thread_rx.clone();
271 let Some(thread) = thread_rx.recv().await?.upgrade() else {
272 anyhow::bail!("Thread closed");
273 };
274
275 let content = thread
276 .update(cx, |thread, cx| {
277 thread.read_text_file(input.abs_path.clone(), None, None, true, cx)
278 })?
279 .await?;
280
281 let new_content = content.replace(&input.old_text, &input.new_text);
282 if new_content == content {
283 return Err(anyhow::anyhow!("The old_text was not found in the content"));
284 }
285
286 thread
287 .update(cx, |thread, cx| {
288 thread.write_text_file(input.abs_path, new_content, cx)
289 })?
290 .await?;
291
292 Ok(ToolResponse {
293 content: vec![],
294 structured_content: (),
295 })
296 }
297}