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::McpServer> {
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::McpServer {
51 name: SERVER_NAME.into(),
52 command: zed_path,
53 args: vec![
54 "--nc".into(),
55 self.server.socket_path().display().to_string(),
56 ],
57 env: vec![],
58 })
59 }
60
61 fn handle_initialize(_: InitializeParams, cx: &App) -> Task<Result<InitializeResponse>> {
62 cx.foreground_executor().spawn(async move {
63 Ok(InitializeResponse {
64 protocol_version: ProtocolVersion("2025-06-18".into()),
65 capabilities: ServerCapabilities {
66 experimental: None,
67 logging: None,
68 completions: None,
69 prompts: None,
70 resources: None,
71 tools: Some(ToolsCapabilities {
72 list_changed: Some(false),
73 }),
74 },
75 server_info: Implementation {
76 name: SERVER_NAME.into(),
77 version: "0.1.0".into(),
78 },
79 meta: None,
80 })
81 })
82 }
83}
84
85// Tools
86
87#[derive(Clone)]
88pub struct RequestPermissionTool {
89 thread_rx: watch::Receiver<WeakEntity<AcpThread>>,
90}
91
92impl McpServerTool for RequestPermissionTool {
93 type Input = acp::RequestPermissionArguments;
94 type Output = acp::RequestPermissionOutput;
95
96 const NAME: &'static str = "Confirmation";
97
98 fn description(&self) -> &'static str {
99 indoc! {"
100 Request permission for tool calls.
101
102 This tool is meant to be called programmatically by the agent loop, not the LLM.
103 "}
104 }
105
106 async fn run(
107 &self,
108 input: Self::Input,
109 cx: &mut AsyncApp,
110 ) -> Result<ToolResponse<Self::Output>> {
111 let mut thread_rx = self.thread_rx.clone();
112 let Some(thread) = thread_rx.recv().await?.upgrade() else {
113 anyhow::bail!("Thread closed");
114 };
115
116 let result = thread
117 .update(cx, |thread, cx| {
118 thread.request_tool_call_permission(input.tool_call, input.options, cx)
119 })?
120 .await;
121
122 let outcome = match result {
123 Ok(option_id) => acp::RequestPermissionOutcome::Selected { option_id },
124 Err(oneshot::Canceled) => acp::RequestPermissionOutcome::Canceled,
125 };
126
127 Ok(ToolResponse {
128 content: vec![],
129 structured_content: acp::RequestPermissionOutput { outcome },
130 })
131 }
132}
133
134#[derive(Clone)]
135pub struct ReadTextFileTool {
136 thread_rx: watch::Receiver<WeakEntity<AcpThread>>,
137}
138
139impl McpServerTool for ReadTextFileTool {
140 type Input = acp::ReadTextFileArguments;
141 type Output = acp::ReadTextFileOutput;
142
143 const NAME: &'static str = "Read";
144
145 fn description(&self) -> &'static str {
146 "Reads the content of the given file in the project including unsaved changes."
147 }
148
149 async fn run(
150 &self,
151 input: Self::Input,
152 cx: &mut AsyncApp,
153 ) -> Result<ToolResponse<Self::Output>> {
154 let mut thread_rx = self.thread_rx.clone();
155 let Some(thread) = thread_rx.recv().await?.upgrade() else {
156 anyhow::bail!("Thread closed");
157 };
158
159 let content = thread
160 .update(cx, |thread, cx| {
161 thread.read_text_file(input.path, input.line, input.limit, false, cx)
162 })?
163 .await?;
164
165 Ok(ToolResponse {
166 content: vec![],
167 structured_content: acp::ReadTextFileOutput { content },
168 })
169 }
170}
171
172#[derive(Clone)]
173pub struct WriteTextFileTool {
174 thread_rx: watch::Receiver<WeakEntity<AcpThread>>,
175}
176
177impl McpServerTool for WriteTextFileTool {
178 type Input = acp::WriteTextFileArguments;
179 type Output = ();
180
181 const NAME: &'static str = "Write";
182
183 fn description(&self) -> &'static str {
184 "Write to a file replacing its contents"
185 }
186
187 async fn run(
188 &self,
189 input: Self::Input,
190 cx: &mut AsyncApp,
191 ) -> Result<ToolResponse<Self::Output>> {
192 let mut thread_rx = self.thread_rx.clone();
193 let Some(thread) = thread_rx.recv().await?.upgrade() else {
194 anyhow::bail!("Thread closed");
195 };
196
197 thread
198 .update(cx, |thread, cx| {
199 thread.write_text_file(input.path, input.content, cx)
200 })?
201 .await?;
202
203 Ok(ToolResponse {
204 content: vec![],
205 structured_content: (),
206 })
207 }
208}