pforge_runtime/
transport.rs1use crate::{Error, Result};
6use pforge_config::TransportType;
7#[cfg(feature = "sse")]
23#[allow(deprecated)]
24use pmcp::shared::{OptimizedSseConfig, OptimizedSseTransport};
25use pmcp::shared::{StdioTransport, Transport};
26#[cfg(feature = "websocket")]
27use pmcp::shared::{WebSocketConfig, WebSocketTransport};
28#[cfg(any(feature = "sse", feature = "websocket"))]
29use std::time::Duration;
30
31pub fn create_transport(transport_type: &TransportType) -> Result<Box<dyn Transport>> {
33 match transport_type {
34 TransportType::Stdio => {
35 let transport = StdioTransport::new();
36 Ok(Box::new(transport))
37 }
38 TransportType::Sse => create_sse_transport(),
39 TransportType::WebSocket => create_websocket_transport(),
40 }
41}
42
43#[allow(clippy::unnecessary_wraps)]
46#[cfg(feature = "sse")]
47fn create_sse_transport() -> Result<Box<dyn Transport>> {
48 let config = OptimizedSseConfig {
49 url: "http://localhost:8080/sse".to_string(),
50 connection_timeout: Duration::from_secs(30),
51 keepalive_interval: Duration::from_secs(15),
52 max_reconnects: 5,
53 reconnect_delay: Duration::from_secs(1),
54 buffer_size: 100,
55 flush_interval: Duration::from_millis(100),
56 enable_pooling: true,
57 max_connections: 10,
58 enable_compression: false,
59 };
60 #[allow(deprecated)]
61 let transport = OptimizedSseTransport::new(config);
62 Ok(Box::new(transport))
63}
64
65#[cfg(not(feature = "sse"))]
66fn create_sse_transport() -> Result<Box<dyn Transport>> {
67 Err(Error::feature_disabled("sse", "transport `sse`"))
68}
69
70#[cfg(feature = "websocket")]
71fn create_websocket_transport() -> Result<Box<dyn Transport>> {
72 let url = "ws://localhost:8080/ws"
73 .parse()
74 .map_err(|e| Error::Handler(format!("Invalid WebSocket URL: {}", e)))?;
75
76 let config = WebSocketConfig {
77 url,
78 auto_reconnect: true,
79 reconnect_delay: Duration::from_secs(1),
80 max_reconnect_delay: Duration::from_secs(30),
81 max_reconnect_attempts: Some(5),
82 ping_interval: Some(Duration::from_secs(30)),
83 request_timeout: Duration::from_secs(10),
84 };
85 let transport = WebSocketTransport::new(config);
86 Ok(Box::new(transport))
87}
88
89#[cfg(not(feature = "websocket"))]
90fn create_websocket_transport() -> Result<Box<dyn Transport>> {
91 Err(Error::feature_disabled(
92 "websocket",
93 "transport `websocket`",
94 ))
95}
96
97#[cfg(test)]
98mod tests {
99 use super::*;
100
101 #[test]
102 fn test_create_stdio_transport() {
103 let transport = create_transport(&TransportType::Stdio);
104 assert!(transport.is_ok());
105 let t = transport.unwrap();
106 assert_eq!(t.transport_type(), "stdio");
107 }
108
109 #[cfg(feature = "sse")]
115 #[tokio::test]
116 async fn test_create_sse_transport() {
117 let transport = create_transport(&TransportType::Sse);
118 assert!(transport.is_ok());
119 }
120
121 #[cfg(not(feature = "sse"))]
122 #[test]
123 fn test_sse_without_feature_errors_and_names_the_feature() {
124 let msg = create_transport(&TransportType::Sse)
125 .expect_err("sse must fail when compiled out, not fall back to stdio")
126 .to_string();
127 assert!(
128 msg.contains("sse") && msg.contains("--features"),
129 "the error must tell the operator how to fix it, got: {msg}"
130 );
131 }
132
133 #[cfg(feature = "websocket")]
134 #[test]
135 fn test_create_websocket_transport() {
136 let transport = create_transport(&TransportType::WebSocket);
137 assert!(transport.is_ok());
138 }
139
140 #[cfg(not(feature = "websocket"))]
141 #[test]
142 fn test_websocket_without_feature_errors_and_names_the_feature() {
143 let msg = create_transport(&TransportType::WebSocket)
144 .expect_err("websocket must fail when compiled out, not fall back to stdio")
145 .to_string();
146 assert!(
147 msg.contains("websocket") && msg.contains("--features"),
148 "the error must tell the operator how to fix it, got: {msg}"
149 );
150 }
151
152 #[test]
155 fn test_stdio_works_in_every_feature_configuration() {
156 assert!(create_transport(&TransportType::Stdio).is_ok());
157 }
158}