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