1use acp_thread::AcpThread;
2use agent_client_protocol as acp;
3use anyhow::Result;
4use context_server::listener::{McpServerTool, ToolResponse};
5use context_server::types::{
6 Implementation, InitializeParams, InitializeResponse, ProtocolVersion, ServerCapabilities,
7 ToolsCapabilities, requests,
8};
9use futures::channel::oneshot;
10use gpui::{App, AsyncApp, Task, WeakEntity};
11use indoc::indoc;
12
13pub struct ZedMcpServer {
14 server: context_server::listener::McpServer,
15}
16
17pub const SERVER_NAME: &str = "zed";
18
19impl ZedMcpServer {
20 pub async fn new(
21 thread_rx: watch::Receiver<WeakEntity<AcpThread>>,
22 cx: &AsyncApp,
23 ) -> Result<Self> {
24 let mut mcp_server = context_server::listener::McpServer::new(cx).await?;
25 mcp_server.handle_request::<requests::Initialize>(Self::handle_initialize);
26
27 mcp_server.add_tool(RequestPermissionTool {
28 thread_rx: thread_rx.clone(),
29 });
30 mcp_server.add_tool(ReadTextFileTool {
31 thread_rx: thread_rx.clone(),
32 });
33 mcp_server.add_tool(WriteTextFileTool {
34 thread_rx: thread_rx.clone(),
35 });
36
37 Ok(Self { server: mcp_server })
38 }
39
40 pub fn server_config(&self) -> Result<acp::McpServerConfig> {
41 #[cfg(not(test))]
42 let zed_path = anyhow::Context::context(
43 std::env::current_exe(),
44 "finding current executable path for use in mcp_server",
45 )?;
46
47 #[cfg(test)]
48 let zed_path = crate::e2e_tests::get_zed_path();
49
50 Ok(acp::McpServerConfig {
51 command: zed_path,
52 args: vec![
53 "--nc".into(),
54 self.server.socket_path().display().to_string(),
55 ],
56 env: None,
57 })
58 }
59
60 fn handle_initialize(_: InitializeParams, cx: &App) -> Task<Result<InitializeResponse>> {
61 cx.foreground_executor().spawn(async move {
62 Ok(InitializeResponse {
63 protocol_version: ProtocolVersion("2025-06-18".into()),
64 capabilities: ServerCapabilities {
65 experimental: None,
66 logging: None,
67 completions: None,
68 prompts: None,
69 resources: None,
70 tools: Some(ToolsCapabilities {
71 list_changed: Some(false),
72 }),
73 },
74 server_info: Implementation {
75 name: SERVER_NAME.into(),
76 version: "0.1.0".into(),
77 },
78 meta: None,
79 })
80 })
81 }
82}
83
84// Tools
85
86#[derive(Clone)]
87pub struct RequestPermissionTool {
88 thread_rx: watch::Receiver<WeakEntity<AcpThread>>,
89}
90
91impl McpServerTool for RequestPermissionTool {
92 type Input = acp::RequestPermissionArguments;
93 type Output = acp::RequestPermissionOutput;
94
95 const NAME: &'static str = "Confirmation";
96
97 fn description(&self) -> &'static str {
98 indoc! {"
99 Request permission for tool calls.
100
101 This tool is meant to be called programmatically by the agent loop, not the LLM.
102 "}
103 }
104
105 async fn run(
106 &self,
107 input: Self::Input,
108 cx: &mut AsyncApp,
109 ) -> Result<ToolResponse<Self::Output>> {
110 let mut thread_rx = self.thread_rx.clone();
111 let Some(thread) = thread_rx.recv().await?.upgrade() else {
112 anyhow::bail!("Thread closed");
113 };
114
115 let result = thread
116 .update(cx, |thread, cx| {
117 thread.request_tool_call_permission(input.tool_call, input.options, cx)
118 })?
119 .await;
120
121 let outcome = match result {
122 Ok(option_id) => acp::RequestPermissionOutcome::Selected { option_id },
123 Err(oneshot::Canceled) => acp::RequestPermissionOutcome::Canceled,
124 };
125
126 Ok(ToolResponse {
127 content: vec![],
128 structured_content: acp::RequestPermissionOutput { outcome },
129 })
130 }
131}
132
133#[derive(Clone)]
134pub struct ReadTextFileTool {
135 thread_rx: watch::Receiver<WeakEntity<AcpThread>>,
136}
137
138impl McpServerTool for ReadTextFileTool {
139 type Input = acp::ReadTextFileArguments;
140 type Output = acp::ReadTextFileOutput;
141
142 const NAME: &'static str = "Read";
143
144 fn description(&self) -> &'static str {
145 "Reads the content of the given file in the project including unsaved changes."
146 }
147
148 async fn run(
149 &self,
150 input: Self::Input,
151 cx: &mut AsyncApp,
152 ) -> Result<ToolResponse<Self::Output>> {
153 let mut thread_rx = self.thread_rx.clone();
154 let Some(thread) = thread_rx.recv().await?.upgrade() else {
155 anyhow::bail!("Thread closed");
156 };
157
158 let content = thread
159 .update(cx, |thread, cx| {
160 thread.read_text_file(input.path, input.line, input.limit, false, cx)
161 })?
162 .await?;
163
164 Ok(ToolResponse {
165 content: vec![],
166 structured_content: acp::ReadTextFileOutput { content },
167 })
168 }
169}
170
171#[derive(Clone)]
172pub struct WriteTextFileTool {
173 thread_rx: watch::Receiver<WeakEntity<AcpThread>>,
174}
175
176impl McpServerTool for WriteTextFileTool {
177 type Input = acp::WriteTextFileArguments;
178 type Output = ();
179
180 const NAME: &'static str = "Write";
181
182 fn description(&self) -> &'static str {
183 "Write to a file replacing its contents"
184 }
185
186 async fn run(
187 &self,
188 input: Self::Input,
189 cx: &mut AsyncApp,
190 ) -> Result<ToolResponse<Self::Output>> {
191 let mut thread_rx = self.thread_rx.clone();
192 let Some(thread) = thread_rx.recv().await?.upgrade() else {
193 anyhow::bail!("Thread closed");
194 };
195
196 thread
197 .update(cx, |thread, cx| {
198 thread.write_text_file(input.path, input.content, cx)
199 })?
200 .await?;
201
202 Ok(ToolResponse {
203 content: vec![],
204 structured_content: (),
205 })
206 }
207}