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