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