Skip to main content

mcp_utils/
server.rs

1use futures::future::BoxFuture;
2use rmcp::service::{DynService, ServerInitializeError, serve_server_with_ct};
3use rmcp::transport::{IntoTransport, stdio};
4use rmcp::{RoleServer, ServerHandler, Service};
5use std::env::{temp_dir, var_os};
6use std::fs::{Permissions, create_dir_all, remove_dir, remove_file, set_permissions};
7use std::os::unix::fs::PermissionsExt;
8use std::path::{Path, PathBuf};
9use std::sync::Arc;
10use std::{fmt, io};
11use thiserror::Error;
12use tokio::io::{AsyncRead, AsyncWrite};
13use tokio::net::UnixListener;
14use tokio::task::JoinError;
15use tokio_util::sync::{CancellationToken, DropGuard};
16use uuid::Uuid;
17
18#[derive(Clone)]
19pub struct McpServer {
20    service: Arc<dyn Fn() -> Box<dyn DynService<RoleServer>> + Send + Sync>,
21}
22
23#[derive(Debug)]
24pub struct ServerHandle {
25    connections: CancellationToken,
26    _stop_on_drop: DropGuard,
27    socket: SocketPath,
28}
29
30#[derive(Debug)]
31pub struct UnixSocket {
32    listener: UnixListener,
33    socket: SocketPath,
34}
35
36#[derive(Debug, Error)]
37pub enum ServeError {
38    #[error("Failed to create MCP socket directory {}: {source}", path.display())]
39    SocketDirectory {
40        path: PathBuf,
41        #[source]
42        source: io::Error,
43    },
44    #[error("Failed to bind MCP socket {}: {source}", path.display())]
45    Bind {
46        path: PathBuf,
47        #[source]
48        source: io::Error,
49    },
50    #[error("MCP server initialization failed: {0}")]
51    Init(#[source] Box<ServerInitializeError>),
52    #[error("MCP server task failed: {0}")]
53    Task(#[source] JoinError),
54}
55
56impl McpServer {
57    pub fn new(handler: impl ServerHandler) -> Self {
58        let handler = Arc::new(handler);
59        Self { service: Arc::new(move || Box::new(Arc::clone(&handler)) as Box<dyn DynService<RoleServer>>) }
60    }
61
62    pub async fn serve_stdio(self) -> Result<(), ServeError> {
63        host(self.service(), stdio(), CancellationToken::new()).await
64    }
65
66    pub fn serve_unix(self, socket: UnixSocket) -> ServerHandle {
67        let UnixSocket { listener, socket } = socket;
68        let connections = CancellationToken::new();
69        tokio::spawn(accept_connections(listener, self, connections.clone()));
70        ServerHandle { _stop_on_drop: connections.clone().drop_guard(), connections, socket }
71    }
72
73    #[cfg(feature = "client")]
74    pub(crate) fn serve_in_memory(&self) -> tokio::io::DuplexStream {
75        let (client, server) = tokio::io::duplex(64 * 1024);
76        tokio::spawn(serve_connection(self.service(), server, CancellationToken::new()));
77        client
78    }
79
80    fn service(&self) -> Box<dyn DynService<RoleServer>> {
81        (self.service)()
82    }
83}
84
85impl<H: ServerHandler> From<H> for McpServer {
86    fn from(handler: H) -> Self {
87        Self::new(handler)
88    }
89}
90
91impl fmt::Debug for McpServer {
92    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
93        formatter.debug_struct("McpServer").finish_non_exhaustive()
94    }
95}
96
97impl ServerHandle {
98    pub fn path(&self) -> &Path {
99        self.socket.path()
100    }
101
102    /// Stops accepting connections, ends the open ones, and removes the socket, as dropping the handle does.
103    pub fn stop(&self) {
104        self.connections.cancel();
105        self.socket.remove();
106    }
107}
108
109impl UnixSocket {
110    pub fn bind() -> Result<Self, ServeError> {
111        let socket = SocketPath::allocate()?;
112        let listener = UnixListener::bind(socket.path())
113            .map_err(|source| ServeError::Bind { path: socket.path().to_path_buf(), source })?;
114        Ok(Self { listener, socket })
115    }
116
117    pub fn path(&self) -> &Path {
118        self.socket.path()
119    }
120}
121
122#[derive(Debug)]
123struct SocketPath {
124    directory: PathBuf,
125    socket: PathBuf,
126}
127
128impl SocketPath {
129    fn allocate() -> Result<Self, ServeError> {
130        let short_id = &Uuid::new_v4().simple().to_string()[..8];
131        let runtime_dir =
132            var_os("XDG_RUNTIME_DIR").filter(|path| Path::new(path).is_absolute()).map_or_else(temp_dir, PathBuf::from);
133        let directory = runtime_dir.join("aether").join(format!("aether-{short_id}"));
134        create_dir_all(&directory).map_err(|source| ServeError::SocketDirectory { path: directory.clone(), source })?;
135        let path = Self { socket: directory.join("ipc.sock"), directory };
136        set_permissions(&path.directory, Permissions::from_mode(0o700))
137            .map_err(|source| ServeError::SocketDirectory { path: path.directory.clone(), source })?;
138        Ok(path)
139    }
140
141    fn path(&self) -> &Path {
142        &self.socket
143    }
144
145    fn remove(&self) {
146        let _ = remove_file(&self.socket);
147        let _ = remove_dir(&self.directory);
148    }
149}
150
151impl Drop for SocketPath {
152    fn drop(&mut self) {
153        self.remove();
154    }
155}
156
157async fn accept_connections(listener: UnixListener, server: McpServer, cancellation: CancellationToken) {
158    loop {
159        let accepted = tokio::select! {
160            () = cancellation.cancelled() => break,
161            accepted = listener.accept() => accepted,
162        };
163        let Ok((stream, _)) = accepted else { break };
164        tokio::spawn(serve_connection(server.service(), stream, cancellation.child_token()));
165    }
166}
167
168async fn serve_connection<S>(service: Box<dyn DynService<RoleServer>>, stream: S, cancellation: CancellationToken)
169where
170    S: AsyncRead + AsyncWrite + Send + 'static,
171{
172    if let Err(error) = host(service, stream, cancellation).await {
173        tracing::debug!(%error, "MCP server connection ended with an error");
174    }
175}
176
177fn host<T, U, V, X>(
178    service: T,
179    transport: U,
180    cancellation: CancellationToken,
181) -> BoxFuture<'static, Result<(), ServeError>>
182where
183    T: Service<RoleServer>,
184    U: IntoTransport<RoleServer, V, X>,
185    V: std::error::Error + Send + Sync + 'static,
186{
187    Box::pin(async move {
188        let running = serve_server_with_ct(service, transport, cancellation)
189            .await
190            .map_err(|error| ServeError::Init(Box::new(error)))?;
191        running.waiting().await.map_err(ServeError::Task)?;
192        Ok(())
193    })
194}