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