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::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}