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