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