mcp_server.rs

  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}