Skip to main content

klieo_core/
server_outbound.rs

1//! Server-initiated outbound JSON-RPC primitive — transport-agnostic.
2//!
3//! Implementations live in transport crates (e.g.
4//! `klieo-mcp-server::outbound::OutboundRequests`). Tools that need
5//! to call back out to the peer obtain an `Arc<dyn ServerOutbound>`
6//! via `ToolCtx::server_outbound` and issue typed requests via
7//! extension traits owned by the same transport crate (e.g.
8//! `McpOutboundExt::sample()` in `klieo-mcp-server`).
9
10use async_trait::async_trait;
11use std::time::Duration;
12
13/// Transport-agnostic surface for issuing a JSON-RPC request from the
14/// server back to its peer.
15///
16/// Implementations correlate the outbound `id` with the matching
17/// inbound response, drive the underlying transport write, and yield
18/// the raw `result` payload (or a typed [`ServerOutboundError`]).
19#[async_trait]
20pub trait ServerOutbound: Send + Sync {
21    /// Issue an outbound JSON-RPC request to the peer and await the
22    /// matched response. Returns the raw `result` payload on success.
23    async fn outbound_request(
24        &self,
25        method: &str,
26        params: serde_json::Value,
27        timeout: Duration,
28    ) -> Result<serde_json::Value, ServerOutboundError>;
29}
30
31/// Failure modes for a [`ServerOutbound::outbound_request`] call.
32#[derive(Debug, thiserror::Error)]
33#[non_exhaustive]
34pub enum ServerOutboundError {
35    /// No response arrived before the supplied timeout elapsed.
36    #[error("outbound request timed out")]
37    Timeout,
38    /// The peer answered with a JSON-RPC error envelope.
39    #[error("peer returned error: code={code} message={message}")]
40    PeerError {
41        /// JSON-RPC error code reported by the peer.
42        code: i64,
43        /// JSON-RPC error message reported by the peer.
44        message: String,
45    },
46    /// The underlying transport closed before a response was received.
47    #[error("transport closed")]
48    TransportClosed,
49    /// The active transport does not support server-initiated outbound
50    /// requests (e.g. plain HTTP without a reverse channel).
51    #[error("outbound channel unsupported on this transport")]
52    Unsupported,
53    /// The outbound request frame could not be serialised. Indicates an
54    /// internal invariant violation rather than a transport failure.
55    #[error("outbound frame serialisation failed: {0}")]
56    Serialisation(#[source] serde_json::Error),
57}
58
59#[cfg(test)]
60mod tests {
61    use super::*;
62
63    #[test]
64    fn display_messages_are_stable() {
65        assert_eq!(
66            ServerOutboundError::Timeout.to_string(),
67            "outbound request timed out"
68        );
69        assert_eq!(
70            ServerOutboundError::PeerError {
71                code: -32601,
72                message: "Method not found".to_string(),
73            }
74            .to_string(),
75            "peer returned error: code=-32601 message=Method not found"
76        );
77        assert_eq!(
78            ServerOutboundError::TransportClosed.to_string(),
79            "transport closed"
80        );
81        assert_eq!(
82            ServerOutboundError::Unsupported.to_string(),
83            "outbound channel unsupported on this transport"
84        );
85    }
86
87    #[test]
88    fn serialisation_variant_display_starts_with_prefix() {
89        let serde_err = serde_json::from_str::<serde_json::Value>("{invalid}").unwrap_err();
90        let msg = ServerOutboundError::Serialisation(serde_err).to_string();
91        assert!(
92            msg.starts_with("outbound frame serialisation failed: "),
93            "unexpected display message: {msg}"
94        );
95    }
96
97    #[test]
98    fn trait_object_is_send_and_sync() {
99        fn assert_send_sync<T: Send + Sync + ?Sized>() {}
100        assert_send_sync::<dyn ServerOutbound>();
101    }
102
103    #[tokio::test]
104    async fn outbound_request_stub_dispatches_and_returns_value() {
105        struct StubOutbound;
106        #[async_trait::async_trait]
107        impl ServerOutbound for StubOutbound {
108            async fn outbound_request(
109                &self,
110                _method: &str,
111                _params: serde_json::Value,
112                _timeout: Duration,
113            ) -> Result<serde_json::Value, ServerOutboundError> {
114                Ok(serde_json::json!({"ok": true}))
115            }
116        }
117        let stub = StubOutbound;
118        let result = stub
119            .outbound_request(
120                "test/method",
121                serde_json::Value::Null,
122                Duration::from_secs(5),
123            )
124            .await
125            .unwrap();
126        assert_eq!(result["ok"], true);
127    }
128}