1use crate::context::Peer;
24use futures::channel::oneshot;
25use mcpkit_core::error::McpError;
26use mcpkit_core::protocol::{Message, Notification, Request, RequestId, Response};
27use std::borrow::Cow;
28use std::collections::HashMap;
29use std::future::Future;
30use std::pin::Pin;
31use std::sync::atomic::{AtomicU64, Ordering};
32use std::sync::{Arc, RwLock};
33use std::time::{Duration, Instant};
34
35pub const RECONNECT_GRACE: Duration = Duration::from_secs(5);
41
42const GRACE_POLL: Duration = Duration::from_millis(100);
44
45#[derive(Debug, Default)]
56pub struct SessionOutbound {
57 next_id: AtomicU64,
58 pending: RwLock<HashMap<RequestId, oneshot::Sender<Response>>>,
59}
60
61impl SessionOutbound {
62 #[must_use]
64 pub fn new() -> Self {
65 Self {
66 next_id: AtomicU64::new(1),
67 pending: RwLock::new(HashMap::new()),
68 }
69 }
70
71 #[must_use]
73 pub fn next_id(&self) -> RequestId {
74 RequestId::Number(self.next_id.fetch_add(1, Ordering::Relaxed))
75 }
76
77 #[must_use]
80 pub fn register(&self, id: RequestId) -> oneshot::Receiver<Response> {
81 let (tx, rx) = oneshot::channel();
82 if let Ok(mut pending) = self.pending.write() {
83 pending.insert(id, tx);
84 }
85 rx
86 }
87
88 pub fn remove(&self, id: &RequestId) {
90 if let Ok(mut pending) = self.pending.write() {
91 pending.remove(id);
92 }
93 }
94
95 pub fn resolve(&self, response: Response) -> bool {
99 let sender = self
100 .pending
101 .write()
102 .ok()
103 .and_then(|mut pending| pending.remove(&response.id));
104 match sender {
105 Some(sender) => {
106 let _ = sender.send(response);
107 true
108 }
109 None => false,
110 }
111 }
112
113 pub fn fail_all(&self) {
117 if let Ok(mut pending) = self.pending.write() {
118 pending.clear();
119 }
120 }
121}
122
123#[derive(Debug)]
132pub struct OutboundOwner(Arc<SessionOutbound>);
133
134impl OutboundOwner {
135 #[must_use]
137 pub fn new() -> Self {
138 Self(Arc::new(SessionOutbound::new()))
139 }
140
141 #[must_use]
143 pub fn outbound(&self) -> &Arc<SessionOutbound> {
144 &self.0
145 }
146}
147
148impl Default for OutboundOwner {
149 fn default() -> Self {
150 Self::new()
151 }
152}
153
154impl Drop for OutboundOwner {
155 fn drop(&mut self) {
156 self.0.fail_all();
157 }
158}
159
160#[derive(Debug, Clone, PartialEq, Eq)]
166pub enum SinkError {
167 NoClientStream,
170 Serialization(String),
172}
173
174impl std::fmt::Display for SinkError {
175 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
176 match self {
177 Self::NoClientStream => {
178 write!(
179 f,
180 "client has no open SSE stream; server-initiated requests require one"
181 )
182 }
183 Self::Serialization(e) => write!(f, "failed to serialize message: {e}"),
184 }
185 }
186}
187
188impl std::error::Error for SinkError {}
189
190pub trait SessionSink: Send + Sync {
198 fn send_notification(
202 &self,
203 message: Message,
204 ) -> Pin<Box<dyn Future<Output = Result<(), SinkError>> + Send + '_>>;
205
206 fn send_request(
211 &self,
212 message: Message,
213 ) -> Pin<Box<dyn Future<Output = Result<(), SinkError>> + Send + '_>>;
214
215 fn has_live_stream(&self) -> bool;
218}
219
220#[cfg(feature = "tokio")]
224pub struct StreamRegistrySink {
225 registry: Arc<crate::streams::StreamRegistry>,
226}
227
228#[cfg(feature = "tokio")]
229impl StreamRegistrySink {
230 #[must_use]
232 pub fn new(registry: Arc<crate::streams::StreamRegistry>) -> Self {
233 Self { registry }
234 }
235}
236
237#[cfg(feature = "tokio")]
238impl SessionSink for StreamRegistrySink {
239 fn send_notification(
240 &self,
241 message: Message,
242 ) -> Pin<Box<dyn Future<Output = Result<(), SinkError>> + Send + '_>> {
243 Box::pin(async move {
244 let json = serde_json::to_string(&message)
245 .map_err(|e| SinkError::Serialization(e.to_string()))?;
246 let _ = self.registry.send("message", json);
249 Ok(())
250 })
251 }
252
253 fn send_request(
254 &self,
255 message: Message,
256 ) -> Pin<Box<dyn Future<Output = Result<(), SinkError>> + Send + '_>> {
257 Box::pin(async move {
258 let json = serde_json::to_string(&message)
259 .map_err(|e| SinkError::Serialization(e.to_string()))?;
260 self.registry
261 .send("message", json)
262 .map(|_| ())
263 .ok_or(SinkError::NoClientStream)
264 })
265 }
266
267 fn has_live_stream(&self) -> bool {
268 self.registry.has_live_stream()
269 }
270}
271
272#[derive(Debug, Clone, Copy)]
284pub struct PeerTimeouts {
285 pub default: Duration,
287 pub elicitation: Duration,
289}
290
291impl Default for PeerTimeouts {
292 fn default() -> Self {
293 Self {
294 default: Duration::from_secs(60),
295 elicitation: Duration::from_secs(300),
296 }
297 }
298}
299
300impl PeerTimeouts {
301 fn resolve(&self, method: &str) -> Duration {
302 if method.starts_with("elicitation/") {
303 self.elicitation
304 } else {
305 self.default
306 }
307 }
308}
309
310pub struct SessionPeer {
312 sink: Arc<dyn SessionSink>,
313 outbound: Arc<SessionOutbound>,
314 timeouts: PeerTimeouts,
315 grace: Duration,
316}
317
318impl SessionPeer {
319 #[must_use]
321 pub fn new(
322 sink: Arc<dyn SessionSink>,
323 outbound: Arc<SessionOutbound>,
324 timeouts: PeerTimeouts,
325 ) -> Self {
326 Self {
327 sink,
328 outbound,
329 timeouts,
330 grace: RECONNECT_GRACE,
331 }
332 }
333
334 #[doc(hidden)]
337 #[must_use]
338 pub fn with_reconnect_grace(mut self, grace: Duration) -> Self {
339 self.grace = grace;
340 self
341 }
342
343 async fn no_stream_for_grace(sink: Arc<dyn SessionSink>, grace: Duration) {
346 let mut none_since: Option<Instant> = None;
347 loop {
348 if sink.has_live_stream() {
349 none_since = None;
350 } else {
351 let since = *none_since.get_or_insert_with(Instant::now);
352 if since.elapsed() >= grace {
353 return;
354 }
355 }
356 mcpkit_transport::runtime::sleep(GRACE_POLL).await;
357 }
358 }
359}
360
361impl std::fmt::Debug for SessionPeer {
362 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
363 f.debug_struct("SessionPeer")
364 .field("timeouts", &self.timeouts)
365 .field("grace", &self.grace)
366 .finish_non_exhaustive()
367 }
368}
369
370impl Peer for SessionPeer {
371 fn notify(
372 &self,
373 notification: Notification,
374 ) -> Pin<Box<dyn Future<Output = Result<(), McpError>> + Send + '_>> {
375 let sink = Arc::clone(&self.sink);
376 Box::pin(async move {
377 sink.send_notification(Message::Notification(notification))
378 .await
379 .map_err(|e| McpError::internal(e.to_string()))
380 })
381 }
382
383 fn request(
384 &self,
385 method: Cow<'static, str>,
386 params: Option<serde_json::Value>,
387 ) -> Pin<Box<dyn Future<Output = Result<Response, McpError>> + Send + '_>> {
388 let sink = Arc::clone(&self.sink);
389 let outbound = Arc::clone(&self.outbound);
390 let timeout = self.timeouts.resolve(&method);
391 let grace = self.grace;
392 Box::pin(async move {
393 use futures::future::{Either, select};
394
395 let started = Instant::now();
396 let id = outbound.next_id();
397 let rx = outbound.register(id.clone());
398 let request = match params {
399 Some(p) => Request::with_params(method, id.clone(), p),
400 None => Request::new(method, id.clone()),
401 };
402 let message = Message::Request(request);
403
404 let send_deadline = grace.min(timeout);
407 loop {
408 match sink.send_request(message.clone()).await {
409 Ok(()) => break,
410 Err(SinkError::NoClientStream) if started.elapsed() < send_deadline => {
411 mcpkit_transport::runtime::sleep(GRACE_POLL).await;
412 }
413 Err(e) => {
414 outbound.remove(&id);
415 return Err(McpError::internal(e.to_string()));
416 }
417 }
418 }
419
420 let remaining = timeout.saturating_sub(started.elapsed());
426 let deadline = mcpkit_transport::runtime::sleep(remaining);
427 let watcher = Self::no_stream_for_grace(Arc::clone(&sink), grace);
428 futures::pin_mut!(deadline);
429 futures::pin_mut!(watcher);
430 let interrupt = select(deadline, watcher);
431 match select(rx, interrupt).await {
432 Either::Left((Ok(response), _)) => Ok(response),
433 Either::Left((Err(_canceled), _)) => {
434 outbound.remove(&id);
435 Err(McpError::internal("session closed before a reply arrived"))
436 }
437 Either::Right((Either::Left(((), _)), _)) => {
438 outbound.remove(&id);
439 Err(McpError::internal(format!(
440 "server-initiated request timed out after {timeout:?}"
441 )))
442 }
443 Either::Right((Either::Right(((), _)), _)) => {
444 outbound.remove(&id);
445 Err(McpError::internal(
446 "client has had no open SSE stream for the reconnect grace; \
447 server-initiated request abandoned",
448 ))
449 }
450 }
451 })
452 }
453}
454
455#[cfg(test)]
456mod tests {
457 use super::*;
458 use std::sync::Mutex;
459 use std::sync::atomic::AtomicBool;
460
461 struct MockSink {
463 live: AtomicBool,
464 sent: Mutex<Vec<Message>>,
465 }
466
467 impl MockSink {
468 fn new(live: bool) -> Arc<Self> {
469 Arc::new(Self {
470 live: AtomicBool::new(live),
471 sent: Mutex::new(Vec::new()),
472 })
473 }
474 }
475
476 impl SessionSink for MockSink {
477 fn send_notification(
478 &self,
479 message: Message,
480 ) -> Pin<Box<dyn Future<Output = Result<(), SinkError>> + Send + '_>> {
481 self.sent.lock().unwrap().push(message);
483 Box::pin(async { Ok(()) })
484 }
485 fn send_request(
486 &self,
487 message: Message,
488 ) -> Pin<Box<dyn Future<Output = Result<(), SinkError>> + Send + '_>> {
489 if self.live.load(Ordering::SeqCst) {
490 self.sent.lock().unwrap().push(message);
491 Box::pin(async { Ok(()) })
492 } else {
493 Box::pin(async { Err(SinkError::NoClientStream) })
494 }
495 }
496 fn has_live_stream(&self) -> bool {
497 self.live.load(Ordering::SeqCst)
498 }
499 }
500
501 fn peer(sink: Arc<MockSink>) -> SessionPeer {
502 SessionPeer::new(
503 sink,
504 Arc::new(SessionOutbound::new()),
505 PeerTimeouts::default(),
506 )
507 }
508
509 fn sent_request_id(sink: &MockSink) -> RequestId {
510 let sent = sink.sent.lock().unwrap();
511 match sent.first().expect("a request was sent") {
512 Message::Request(r) => r.id.clone(),
513 other => panic!("expected request, got {other:?}"),
514 }
515 }
516
517 #[tokio::test]
518 async fn request_correlates_response() {
519 let sink = MockSink::new(true);
520 let outbound = Arc::new(SessionOutbound::new());
521 let p = SessionPeer::new(sink.clone(), Arc::clone(&outbound), PeerTimeouts::default());
522
523 let fut = p.request(Cow::Borrowed("roots/list"), None);
524 futures::pin_mut!(fut);
525 assert!(futures::poll!(fut.as_mut()).is_pending());
527 let id = sent_request_id(&sink);
528
529 assert!(outbound.resolve(Response::success(id, serde_json::json!({"roots": []}))));
531 let response = fut.await.expect("correlated");
532 assert_eq!(response.result.unwrap()["roots"], serde_json::json!([]));
533 }
534
535 #[tokio::test]
536 async fn timeout_cleans_up_pending() {
537 let sink = MockSink::new(true);
538 let outbound = Arc::new(SessionOutbound::new());
539 let p = SessionPeer::new(
540 sink.clone(),
541 Arc::clone(&outbound),
542 PeerTimeouts {
543 default: Duration::from_millis(50),
544 elicitation: Duration::from_millis(50),
545 },
546 );
547
548 let err = p
549 .request(Cow::Borrowed("roots/list"), None)
550 .await
551 .unwrap_err();
552 assert!(err.to_string().contains("timed out"), "{err}");
553 let id = sent_request_id(&sink);
555 assert!(!outbound.resolve(Response::success(id, serde_json::json!({}))));
556 }
557
558 #[tokio::test]
559 async fn owner_drop_fails_pending_waiters() {
560 let sink = MockSink::new(true);
561 let owner = OutboundOwner::new();
562 let p = SessionPeer::new(
563 sink.clone(),
564 Arc::clone(owner.outbound()),
565 PeerTimeouts::default(),
566 );
567
568 let fut = p.request(Cow::Borrowed("roots/list"), None);
569 futures::pin_mut!(fut);
570 assert!(futures::poll!(fut.as_mut()).is_pending());
571
572 drop(owner);
574 let err = fut.await.unwrap_err();
575 assert!(err.to_string().contains("closed"), "{err}");
576 }
577
578 #[tokio::test]
579 async fn notifications_never_fail_without_stream() {
580 let sink = MockSink::new(false);
581 let p = peer(sink.clone());
582 p.notify(Notification::new("notifications/progress"))
583 .await
584 .expect("best-effort notification must not error");
585 assert_eq!(sink.sent.lock().unwrap().len(), 1);
586 }
587
588 #[tokio::test]
589 async fn request_fails_fast_after_grace_without_stream() {
590 let sink = MockSink::new(false);
591 let p = peer(sink.clone()).with_reconnect_grace(Duration::from_millis(50));
592
593 let started = Instant::now();
594 let err = p
595 .request(Cow::Borrowed("roots/list"), None)
596 .await
597 .unwrap_err();
598 assert!(
599 err.to_string().contains("SSE stream"),
600 "expected no-stream error, got: {err}"
601 );
602 assert!(
603 started.elapsed() < Duration::from_secs(5),
604 "must fail at the grace, not the request timeout"
605 );
606 }
607
608 #[tokio::test]
609 async fn request_survives_reconnect_within_grace() {
610 let sink = MockSink::new(false);
611 let outbound = Arc::new(SessionOutbound::new());
612 let p = SessionPeer::new(sink.clone(), Arc::clone(&outbound), PeerTimeouts::default())
613 .with_reconnect_grace(Duration::from_secs(2));
614
615 let sink2 = sink.clone();
616 let reconnect = tokio::spawn(async move {
617 tokio::time::sleep(Duration::from_millis(150)).await;
618 sink2.live.store(true, Ordering::SeqCst);
619 });
620
621 let fut = p.request(Cow::Borrowed("roots/list"), None);
622 futures::pin_mut!(fut);
623 loop {
625 assert!(
626 futures::poll!(fut.as_mut()).is_pending(),
627 "request should still be awaiting its response"
628 );
629 if !sink.sent.lock().unwrap().is_empty() {
630 break;
631 }
632 tokio::time::sleep(Duration::from_millis(20)).await;
633 }
634 let id = sent_request_id(&sink);
635 assert!(outbound.resolve(Response::success(id, serde_json::json!({}))));
636 fut.await.expect("survived the blip");
637 reconnect.await.unwrap();
638 }
639
640 #[tokio::test]
641 async fn midflight_stream_loss_fails_after_grace() {
642 let sink = MockSink::new(true);
643 let p = peer(sink.clone()).with_reconnect_grace(Duration::from_millis(80));
644
645 let sink2 = sink.clone();
646 let killer = tokio::spawn(async move {
647 tokio::time::sleep(Duration::from_millis(50)).await;
648 sink2.live.store(false, Ordering::SeqCst);
649 });
650
651 let started = Instant::now();
652 let err = p
653 .request(Cow::Borrowed("roots/list"), None)
654 .await
655 .unwrap_err();
656 assert!(
657 err.to_string().contains("reconnect grace"),
658 "expected mid-flight grace failure, got: {err}"
659 );
660 assert!(started.elapsed() < Duration::from_secs(5));
661 killer.await.unwrap();
662 }
663
664 #[tokio::test]
665 async fn cross_session_ids_do_not_collide() {
666 let a = Arc::new(SessionOutbound::new());
669 let b = Arc::new(SessionOutbound::new());
670 let id_a = a.next_id();
671 let _rx_a = a.register(id_a.clone());
672 let id_b = b.next_id();
673 assert_eq!(id_a, id_b, "both sessions allocate id 1");
674
675 assert!(!b.resolve(Response::success(id_b, serde_json::json!({})))); assert!(a.resolve(Response::success(id_a, serde_json::json!({}))));
678 }
679
680 #[test]
681 fn elicitation_gets_the_longer_timeout() {
682 let t = PeerTimeouts::default();
683 assert_eq!(t.resolve("elicitation/create"), t.elicitation);
684 assert_eq!(t.resolve("elicitation/createUrl"), t.elicitation);
685 assert_eq!(t.resolve("roots/list"), t.default);
686 assert_eq!(t.resolve("sampling/createMessage"), t.default);
687 }
688}