mcp_server.rs

  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}