Skip to main content

rmcp_in_process_transport/
in_process.rs

1use std::{error::Error, fmt, marker::PhantomData};
2
3use futures::{Sink, Stream};
4use tokio::sync::mpsc::{self, Receiver, Sender};
5
6use rmcp::{
7    Service, serve_client, serve_server,
8    service::{
9        ClientInitializeError, RoleClient, RoleServer, RunningService, RxJsonRpcMessage,
10        ServerInitializeError, ServiceRole, TxJsonRpcMessage,
11    },
12    transport::{IntoTransport, sink_stream::SinkStreamTransport},
13};
14
15#[derive(Debug)]
16pub struct InProcessTransportError(String);
17
18impl fmt::Display for InProcessTransportError {
19    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
20        write!(f, "InProcess transport error: {}", self.0)
21    }
22}
23
24impl Error for InProcessTransportError {}
25
26pub struct ClientSendError(mpsc::error::SendError<TxJsonRpcMessage<RoleClient>>);
27pub struct ServerSendError(mpsc::error::SendError<TxJsonRpcMessage<RoleServer>>);
28
29impl From<ClientSendError> for InProcessTransportError {
30    fn from(err: ClientSendError) -> Self {
31        InProcessTransportError(format!("Failed to send client message: {}", err.0))
32    }
33}
34
35impl From<ServerSendError> for InProcessTransportError {
36    fn from(err: ServerSendError) -> Self {
37        InProcessTransportError(format!("Failed to send server message: {}", err.0))
38    }
39}
40
41impl From<InProcessTransportError> for std::io::Error {
42    fn from(value: InProcessTransportError) -> Self {
43        std::io::Error::other(value)
44    }
45}
46
47impl From<std::io::Error> for InProcessTransportError {
48    fn from(err: std::io::Error) -> Self {
49        InProcessTransportError(format!("IO error: {}", err))
50    }
51}
52
53impl From<ClientInitializeError> for InProcessTransportError {
54    fn from(err: ClientInitializeError) -> Self {
55        InProcessTransportError(format!("Client initialization error: {}", err))
56    }
57}
58
59impl From<ServerInitializeError> for InProcessTransportError {
60    fn from(err: ServerInitializeError) -> Self {
61        InProcessTransportError(format!("Server initialization error: {}", err))
62    }
63}
64
65/// Creates a pair of transports that can be used for client and server
66/// running in the same process. This avoids network overhead entirely.
67///
68/// The `buffer_size` parameter controls how many messages can be buffered before
69/// back-pressure is applied.
70pub fn create_in_process_transport_pair(
71    buffer_size: usize,
72) -> (
73    InProcessTransport<RoleClient>,
74    InProcessTransport<RoleServer>,
75) {
76    // Client to Server channel (client sends, server receives)
77    let (client_tx, server_rx) = mpsc::channel::<TxJsonRpcMessage<RoleClient>>(buffer_size);
78    // Server to Client channel (server sends, client receives)
79    let (server_tx, client_rx) = mpsc::channel::<TxJsonRpcMessage<RoleServer>>(buffer_size);
80
81    let client_transport = InProcessTransport::<RoleClient> {
82        tx: client_tx,
83        rx: client_rx,
84        _phantom: PhantomData,
85    };
86
87    let server_transport = InProcessTransport::<RoleServer> {
88        tx: server_tx,
89        rx: server_rx,
90        _phantom: PhantomData,
91    };
92
93    (client_transport, server_transport)
94}
95
96/// Create a new in-process transport pair with matching client and server sides
97pub fn create_transport_pair(
98    buffer_size: usize,
99) -> (
100    InProcessTransport<RoleClient>,
101    InProcessTransport<RoleServer>,
102) {
103    create_in_process_transport_pair(buffer_size)
104}
105
106/// A unified transport that can be used for both client and server sides
107/// of in-process communication
108pub struct InProcessTransport<R: ServiceRole> {
109    tx: Sender<TxJsonRpcMessage<R>>,
110    rx: Receiver<RxJsonRpcMessage<R>>,
111    _phantom: PhantomData<R>,
112}
113
114impl<R: ServiceRole> InProcessTransport<R> {
115    /// Split the transport into its sink and stream components
116    pub fn split(self) -> (InProcessSink<R>, InProcessStream<R>) {
117        let sink = InProcessSink {
118            tx: self.tx,
119            _phantom: PhantomData,
120            pending: None,
121        };
122        let stream = InProcessStream {
123            rx: self.rx,
124            _phantom: PhantomData,
125        };
126        (sink, stream)
127    }
128
129    /// Create a new transport pair with the given buffer size
130    pub fn new(
131        buffer_size: usize,
132    ) -> (
133        InProcessTransport<RoleClient>,
134        InProcessTransport<RoleServer>,
135    ) {
136        create_in_process_transport_pair(buffer_size)
137    }
138}
139
140/// Extension methods for InProcessTransport to simplify common operations
141pub trait InProcessTransportExt<R: ServiceRole> {
142    /// The error type returned by serve operations
143    type Error;
144
145    /// Create a new service using this transport
146    fn serve<S>(
147        self,
148        service: S,
149    ) -> impl std::future::Future<Output = Result<RunningService<R, S>, Self::Error>> + Send
150    where
151        S: Service<R> + Send + 'static;
152}
153
154impl InProcessTransportExt<RoleClient> for InProcessTransport<RoleClient> {
155    type Error = ClientInitializeError;
156
157    async fn serve<S>(
158        self,
159        service: S,
160    ) -> Result<RunningService<RoleClient, S>, ClientInitializeError>
161    where
162        S: Service<RoleClient> + Send + 'static,
163    {
164        serve_client(service, self).await
165    }
166}
167
168impl InProcessTransportExt<RoleServer> for InProcessTransport<RoleServer> {
169    type Error = ServerInitializeError;
170
171    async fn serve<S>(
172        self,
173        service: S,
174    ) -> Result<RunningService<RoleServer, S>, ServerInitializeError>
175    where
176        S: Service<RoleServer> + Send + 'static,
177    {
178        serve_server(service, self).await
179    }
180}
181
182pin_project_lite::pin_project! {
183    /// The sink component of the in-process transport
184    pub struct InProcessSink<R: ServiceRole> {
185        tx: Sender<TxJsonRpcMessage<R>>,
186        _phantom: PhantomData<R>,
187        // Store any pending message that couldn't be sent
188        pending: Option<TxJsonRpcMessage<R>>,
189    }
190}
191
192pin_project_lite::pin_project! {
193    /// The stream component of the in-process transport
194    pub struct InProcessStream<R: ServiceRole> {
195        #[pin]
196        rx: Receiver<RxJsonRpcMessage<R>>,
197        _phantom: PhantomData<R>,
198    }
199}
200
201impl<R: ServiceRole> Sink<TxJsonRpcMessage<R>> for InProcessSink<R> {
202    type Error = InProcessTransportError;
203
204    fn poll_ready(
205        self: std::pin::Pin<&mut Self>,
206        cx: &mut std::task::Context<'_>,
207    ) -> std::task::Poll<Result<(), Self::Error>> {
208        let this = self.project();
209
210        // First, try to flush any pending message
211        if let Some(pending) = this.pending.take() {
212            match this.tx.try_send(pending) {
213                Ok(()) => {
214                    // Successfully sent pending message, now we're ready
215                    std::task::Poll::Ready(Ok(()))
216                }
217                Err(tokio::sync::mpsc::error::TrySendError::Full(msg)) => {
218                    // Channel still full, store the message back and return Pending
219                    *this.pending = Some(msg);
220                    // Register waker for potential future readiness
221                    cx.waker().wake_by_ref();
222                    std::task::Poll::Pending
223                }
224                Err(tokio::sync::mpsc::error::TrySendError::Closed(_)) => std::task::Poll::Ready(
225                    Err(InProcessTransportError("Channel closed".to_string())),
226                ),
227            }
228        } else {
229            // No pending message, check if channel is closed
230            if this.tx.is_closed() {
231                std::task::Poll::Ready(Err(InProcessTransportError("Channel closed".to_string())))
232            } else {
233                // Channel is open and no pending messages, we're ready
234                std::task::Poll::Ready(Ok(()))
235            }
236        }
237    }
238
239    fn start_send(
240        self: std::pin::Pin<&mut Self>,
241        item: TxJsonRpcMessage<R>,
242    ) -> Result<(), Self::Error> {
243        let this = self.project();
244
245        // Try to send immediately
246        match this.tx.try_send(item) {
247            Ok(()) => Ok(()),
248            Err(tokio::sync::mpsc::error::TrySendError::Full(msg)) => {
249                // Channel full, store message as pending for next poll_ready
250                *this.pending = Some(msg);
251                Ok(())
252            }
253            Err(tokio::sync::mpsc::error::TrySendError::Closed(_)) => {
254                Err(InProcessTransportError("Channel closed".to_string()))
255            }
256        }
257    }
258
259    fn poll_flush(
260        self: std::pin::Pin<&mut Self>,
261        _cx: &mut std::task::Context<'_>,
262    ) -> std::task::Poll<Result<(), Self::Error>> {
263        // MPSC channels don't need explicit flushing
264        std::task::Poll::Ready(Ok(()))
265    }
266
267    fn poll_close(
268        self: std::pin::Pin<&mut Self>,
269        _cx: &mut std::task::Context<'_>,
270    ) -> std::task::Poll<Result<(), Self::Error>> {
271        // Let the sender be dropped naturally
272        std::task::Poll::Ready(Ok(()))
273    }
274}
275
276impl<R: ServiceRole> Stream for InProcessStream<R> {
277    type Item = RxJsonRpcMessage<R>;
278
279    fn poll_next(
280        self: std::pin::Pin<&mut Self>,
281        cx: &mut std::task::Context<'_>,
282    ) -> std::task::Poll<Option<Self::Item>> {
283        self.project().rx.poll_recv(cx)
284    }
285}
286
287// Use the TransportAdapterSinkStream to create a proper Transport
288pub enum TransportAdapterInProcess {}
289
290// Implement the IntoTransport trait for the unified InProcessTransport
291impl<R> IntoTransport<R, InProcessTransportError, TransportAdapterInProcess>
292    for InProcessTransport<R>
293where
294    R: ServiceRole + 'static + Unpin,
295    R::Req: Unpin,
296    R::Resp: Unpin,
297    R::Not: Unpin,
298{
299    fn into_transport(
300        self,
301    ) -> impl rmcp::transport::Transport<R, Error = InProcessTransportError> + 'static {
302        let (sink, stream) = self.split();
303        SinkStreamTransport::new(sink, stream)
304    }
305}
306
307/// A convenience struct that manages an in-process service and provides a client transport
308/// to communicate with it, similar to TokioChildProcess but for in-process communication
309pub struct TokioInProcess {
310    client_transport: InProcessTransport<RoleClient>,
311    _server_task: tokio::task::JoinHandle<()>,
312}
313
314impl TokioInProcess {
315    /// Create a new in-process service with the given service implementation
316    pub async fn new<S>(service: S) -> Result<Self, InProcessTransportError>
317    where
318        S: Service<RoleServer> + Send + 'static,
319    {
320        Self::with_buffer_size(service, 32).await
321    }
322
323    /// Create a new in-process service with the given service implementation and buffer size
324    pub async fn with_buffer_size<S>(
325        service: S,
326        buffer_size: usize,
327    ) -> Result<Self, InProcessTransportError>
328    where
329        S: Service<RoleServer> + Send + 'static,
330    {
331        // Create the transport pair
332        let (client_transport, server_transport) = create_in_process_transport_pair(buffer_size);
333
334        // Start the server in a background task
335        let server_handle = tokio::spawn(async move {
336            tracing::debug!("Server task starting");
337            match server_transport.serve(service).await {
338                Ok(running_service) => {
339                    tracing::debug!("Server initialized successfully, keeping it alive");
340                    // Keep the running service alive by waiting for it to finish
341                    if let Err(e) = running_service.waiting().await {
342                        tracing::error!("Server service error: {:?}", e);
343                    } else {
344                        tracing::debug!("Server service completed normally");
345                    }
346                }
347                Err(e) => {
348                    tracing::error!("Server initialization error: {:?}", e);
349                }
350            }
351        });
352
353        Ok(Self {
354            client_transport,
355            _server_task: server_handle,
356        })
357    }
358}
359
360impl IntoTransport<RoleClient, InProcessTransportError, TransportAdapterInProcess>
361    for TokioInProcess
362{
363    fn into_transport(
364        self,
365    ) -> impl rmcp::transport::Transport<RoleClient, Error = InProcessTransportError> + 'static
366    {
367        // Spawn a task to keep the server task alive
368        tracing::debug!("Moving server task to background keeper");
369        tokio::spawn(async move {
370            tracing::debug!("Background keeper task started");
371            if let Err(e) = self._server_task.await {
372                tracing::error!("Server task failed: {:?}", e);
373            } else {
374                tracing::debug!("Background server task completed normally");
375            }
376        });
377
378        self.client_transport.into_transport()
379    }
380}