1use std::path::PathBuf;
2
3use crate::claude::tools::{ClaudeTool, EditToolParams, ReadToolParams};
4use acp_thread::AcpThread;
5use agent_client_protocol as acp;
6use anyhow::{Context, Result};
7use collections::HashMap;
8use context_server::listener::{McpServerTool, ToolResponse};
9use context_server::types::{
10 Implementation, InitializeParams, InitializeResponse, ProtocolVersion, ServerCapabilities,
11 ToolAnnotations, ToolResponseContent, ToolsCapabilities, requests,
12};
13use gpui::{App, AsyncApp, Task, WeakEntity};
14use schemars::JsonSchema;
15use serde::{Deserialize, Serialize};
16
17pub struct ClaudeZedMcpServer {
18 server: context_server::listener::McpServer,
19}
20
21pub const SERVER_NAME: &str = "zed";
22
23impl ClaudeZedMcpServer {
24 pub async fn new(
25 thread_rx: watch::Receiver<WeakEntity<AcpThread>>,
26 cx: &AsyncApp,
27 ) -> Result<Self> {
28 let mut mcp_server = context_server::listener::McpServer::new(cx).await?;
29 mcp_server.handle_request::<requests::Initialize>(Self::handle_initialize);
30
31 mcp_server.add_tool(PermissionTool {
32 thread_rx: thread_rx.clone(),
33 });
34 mcp_server.add_tool(ReadTool {
35 thread_rx: thread_rx.clone(),
36 });
37 mcp_server.add_tool(EditTool {
38 thread_rx: thread_rx.clone(),
39 });
40
41 Ok(Self { server: mcp_server })
42 }
43
44 pub fn server_config(&self) -> Result<McpServerConfig> {
45 let zed_path = std::env::current_exe()
46 .context("finding current executable path for use in mcp_server")?;
47
48 Ok(McpServerConfig {
49 command: zed_path,
50 args: vec![
51 "--nc".into(),
52 self.server.socket_path().display().to_string(),
53 ],
54 env: None,
55 })
56 }
57
58 fn handle_initialize(_: InitializeParams, cx: &App) -> Task<Result<InitializeResponse>> {
59 cx.foreground_executor().spawn(async move {
60 Ok(InitializeResponse {
61 protocol_version: ProtocolVersion("2025-06-18".into()),
62 capabilities: ServerCapabilities {
63 experimental: None,
64 logging: None,
65 completions: None,
66 prompts: None,
67 resources: None,
68 tools: Some(ToolsCapabilities {
69 list_changed: Some(false),
70 }),
71 },
72 server_info: Implementation {
73 name: SERVER_NAME.into(),
74 version: "0.1.0".into(),
75 },
76 meta: None,
77 })
78 })
79 }
80}
81
82#[derive(Serialize)]
83#[serde(rename_all = "camelCase")]
84pub struct McpConfig {
85 pub mcp_servers: HashMap<String, McpServerConfig>,
86}
87
88#[derive(Serialize, Clone)]
89#[serde(rename_all = "camelCase")]
90pub struct McpServerConfig {
91 pub command: PathBuf,
92 pub args: Vec<String>,
93 #[serde(skip_serializing_if = "Option::is_none")]
94 pub env: Option<HashMap<String, String>>,
95}
96
97// Tools
98
99#[derive(Clone)]
100pub struct PermissionTool {
101 thread_rx: watch::Receiver<WeakEntity<AcpThread>>,
102}
103
104#[derive(Deserialize, JsonSchema, Debug)]
105pub struct PermissionToolParams {
106 tool_name: String,
107 input: serde_json::Value,
108 tool_use_id: Option<String>,
109}
110
111#[derive(Serialize)]
112#[serde(rename_all = "camelCase")]
113pub struct PermissionToolResponse {
114 behavior: PermissionToolBehavior,
115 updated_input: serde_json::Value,
116}
117
118#[derive(Serialize)]
119#[serde(rename_all = "snake_case")]
120enum PermissionToolBehavior {
121 Allow,
122 Deny,
123}
124
125impl McpServerTool for PermissionTool {
126 type Input = PermissionToolParams;
127 const NAME: &'static str = "Confirmation";
128
129 fn description(&self) -> &'static str {
130 "Request permission for tool calls"
131 }
132
133 async fn run(&self, input: Self::Input, cx: &mut AsyncApp) -> Result<ToolResponse> {
134 let mut thread_rx = self.thread_rx.clone();
135 let Some(thread) = thread_rx.recv().await?.upgrade() else {
136 anyhow::bail!("Thread closed");
137 };
138
139 let claude_tool = ClaudeTool::infer(&input.tool_name, input.input.clone());
140 let tool_call_id = acp::ToolCallId(input.tool_use_id.context("Tool ID required")?.into());
141 let allow_option_id = acp::PermissionOptionId("allow".into());
142 let reject_option_id = acp::PermissionOptionId("reject".into());
143
144 let chosen_option = thread
145 .update(cx, |thread, cx| {
146 thread.request_tool_call_permission(
147 claude_tool.as_acp(tool_call_id),
148 vec![
149 acp::PermissionOption {
150 id: allow_option_id.clone(),
151 label: "Allow".into(),
152 kind: acp::PermissionOptionKind::AllowOnce,
153 },
154 acp::PermissionOption {
155 id: reject_option_id.clone(),
156 label: "Reject".into(),
157 kind: acp::PermissionOptionKind::RejectOnce,
158 },
159 ],
160 cx,
161 )
162 })?
163 .await?;
164
165 let response = if chosen_option == allow_option_id {
166 PermissionToolResponse {
167 behavior: PermissionToolBehavior::Allow,
168 updated_input: input.input,
169 }
170 } else {
171 PermissionToolResponse {
172 behavior: PermissionToolBehavior::Deny,
173 updated_input: input.input,
174 }
175 };
176
177 Ok(ToolResponse {
178 content: vec![ToolResponseContent::Text {
179 text: serde_json::to_string(&response)?,
180 }],
181 structured_content: None,
182 })
183 }
184}
185
186#[derive(Clone)]
187pub struct ReadTool {
188 thread_rx: watch::Receiver<WeakEntity<AcpThread>>,
189}
190
191impl McpServerTool for ReadTool {
192 type Input = ReadToolParams;
193 const NAME: &'static str = "Read";
194
195 fn description(&self) -> &'static str {
196 "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."
197 }
198
199 fn annotations(&self) -> ToolAnnotations {
200 ToolAnnotations {
201 title: Some("Read file".to_string()),
202 read_only_hint: Some(true),
203 destructive_hint: Some(false),
204 open_world_hint: Some(false),
205 idempotent_hint: None,
206 }
207 }
208
209 async fn run(&self, input: Self::Input, cx: &mut AsyncApp) -> Result<ToolResponse> {
210 let mut thread_rx = self.thread_rx.clone();
211 let Some(thread) = thread_rx.recv().await?.upgrade() else {
212 anyhow::bail!("Thread closed");
213 };
214
215 let content = thread
216 .update(cx, |thread, cx| {
217 thread.read_text_file(input.abs_path, input.offset, input.limit, false, cx)
218 })?
219 .await?;
220
221 Ok(ToolResponse {
222 content: vec![ToolResponseContent::Text { text: content }],
223 structured_content: None,
224 })
225 }
226}
227
228#[derive(Clone)]
229pub struct EditTool {
230 thread_rx: watch::Receiver<WeakEntity<AcpThread>>,
231}
232
233impl McpServerTool for EditTool {
234 type Input = EditToolParams;
235 const NAME: &'static str = "Edit";
236
237 fn description(&self) -> &'static str {
238 "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."
239 }
240
241 fn annotations(&self) -> ToolAnnotations {
242 ToolAnnotations {
243 title: Some("Edit file".to_string()),
244 read_only_hint: Some(false),
245 destructive_hint: Some(false),
246 open_world_hint: Some(false),
247 idempotent_hint: Some(false),
248 }
249 }
250
251 async fn run(&self, input: Self::Input, cx: &mut AsyncApp) -> Result<ToolResponse> {
252 let mut thread_rx = self.thread_rx.clone();
253 let Some(thread) = thread_rx.recv().await?.upgrade() else {
254 anyhow::bail!("Thread closed");
255 };
256
257 let content = thread
258 .update(cx, |thread, cx| {
259 thread.read_text_file(input.abs_path.clone(), None, None, true, cx)
260 })?
261 .await?;
262
263 let new_content = content.replace(&input.old_text, &input.new_text);
264 if new_content == content {
265 return Err(anyhow::anyhow!("The old_text was not found in the content"));
266 }
267
268 thread
269 .update(cx, |thread, cx| {
270 thread.write_text_file(input.abs_path, new_content, cx)
271 })?
272 .await?;
273
274 Ok(ToolResponse {
275 content: vec![],
276 structured_content: None,
277 })
278 }
279}