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