1use std::sync::Arc;
12use std::time::Duration;
13
14use envconfig::Envconfig;
15use objectiveai_sdk::mcp::Client;
16
17use crate::session_manager::SessionManager;
18use crate::{AppState, mcp};
19
20const BACKOFF_INITIAL_INTERVAL_MS: u64 = 100;
25const BACKOFF_RANDOMIZATION_FACTOR: f64 = 0.5;
26const BACKOFF_MULTIPLIER: f64 = 1.5;
27const BACKOFF_MAX_INTERVAL_MS: u64 = 1000;
28const BACKOFF_MAX_ELAPSED_TIME_DEFAULT_MS: u64 = 60000;
29
30#[derive(Envconfig)]
31struct EnvConfigBuilder {
32 #[envconfig(from = "ADDRESS")]
33 address: Option<String>,
34 #[envconfig(from = "PORT")]
35 port: Option<u16>,
36 #[envconfig(from = "USER_AGENT")]
37 user_agent: Option<String>,
38 #[envconfig(from = "HTTP_REFERER")]
39 http_referer: Option<String>,
40 #[envconfig(from = "X_TITLE")]
41 x_title: Option<String>,
42 #[envconfig(from = "MCP_CONNECT_TIMEOUT")]
43 mcp_connect_timeout: Option<u64>,
44 #[envconfig(from = "MCP_CALL_TIMEOUT")]
45 mcp_call_timeout: Option<u64>,
46 #[envconfig(from = "MCP_BACKOFF_MAX_ELAPSED_TIME")]
47 mcp_backoff_max_elapsed_time: Option<u64>,
48 #[envconfig(from = "MCP_ENCRYPTION_KEY")]
61 mcp_encryption_key: Option<String>,
62 #[envconfig(from = "SUPPRESS_OUTPUT")]
63 suppress_output: Option<String>,
64 #[envconfig(from = "OBJECTIVEAI_LOGS_DIR")]
68 logs_dir: Option<String>,
69}
70
71impl EnvConfigBuilder {
72 fn build(self) -> ConfigBuilder {
73 ConfigBuilder {
74 address: self.address,
75 port: self.port,
76 user_agent: self.user_agent,
77 http_referer: self.http_referer,
78 x_title: self.x_title,
79 mcp_connect_timeout: self.mcp_connect_timeout,
80 mcp_call_timeout: self.mcp_call_timeout,
81 mcp_backoff_max_elapsed_time: self.mcp_backoff_max_elapsed_time,
82 mcp_encryption_key: match self.mcp_encryption_key.as_deref() {
83 Some(s) => match crate::session_manager::parse_key_env(s) {
84 Ok(opt) => opt,
85 Err(e) => {
86 tracing::error!(error = %e, "MCP_ENCRYPTION_KEY parse failed; falling back to ephemeral key");
87 None
88 }
89 },
90 None => None,
91 },
92 suppress_output: self.suppress_output.map(|v| {
93 matches!(v.to_ascii_lowercase().as_str(), "1" | "true" | "yes" | "on")
94 }),
95 logs_dir: self.logs_dir,
96 }
97 }
98}
99
100#[derive(Default)]
101pub struct ConfigBuilder {
102 pub address: Option<String>,
103 pub port: Option<u16>,
104 pub user_agent: Option<String>,
105 pub http_referer: Option<String>,
106 pub x_title: Option<String>,
107 pub mcp_connect_timeout: Option<u64>,
108 pub mcp_call_timeout: Option<u64>,
109 pub mcp_backoff_max_elapsed_time: Option<u64>,
110 pub mcp_encryption_key: Option<[u8; 32]>,
114 pub suppress_output: Option<bool>,
115 pub logs_dir: Option<String>,
118}
119
120impl Envconfig for ConfigBuilder {
121 #[allow(deprecated)]
122 fn init() -> Result<Self, envconfig::Error> {
123 EnvConfigBuilder::init().map(|e| e.build())
124 }
125
126 fn init_from_env() -> Result<Self, envconfig::Error> {
127 EnvConfigBuilder::init_from_env().map(|e| e.build())
128 }
129
130 fn init_from_hashmap(
131 hashmap: &std::collections::HashMap<String, String>,
132 ) -> Result<Self, envconfig::Error> {
133 EnvConfigBuilder::init_from_hashmap(hashmap).map(|e| e.build())
134 }
135}
136
137impl ConfigBuilder {
138 pub fn build(self) -> Config {
139 Config {
140 address: self.address.unwrap_or_else(|| "0.0.0.0".to_string()),
141 port: self.port.unwrap_or(3000),
142 user_agent: self
143 .user_agent
144 .unwrap_or_else(|| format!("objectiveai-mcp-proxy/{}", env!("CARGO_PKG_VERSION"))),
145 http_referer: self
146 .http_referer
147 .unwrap_or_else(|| "https://objectiveai.dev".to_string()),
148 x_title: self
149 .x_title
150 .unwrap_or_else(|| "ObjectiveAI MCP Proxy".to_string()),
151 mcp_connect_timeout: self.mcp_connect_timeout.unwrap_or(1_800_000),
159 mcp_call_timeout: Some(self.mcp_call_timeout.unwrap_or(1_800_000)),
160 mcp_backoff_max_elapsed_time: self.mcp_backoff_max_elapsed_time.unwrap_or(BACKOFF_MAX_ELAPSED_TIME_DEFAULT_MS),
161 mcp_encryption_key: self.mcp_encryption_key,
162 suppress_output: self.suppress_output.unwrap_or(false),
163 logs_dir: self.logs_dir.map(std::path::PathBuf::from),
164 }
165 }
166}
167
168pub struct Config {
169 pub address: String,
170 pub port: u16,
171 pub user_agent: String,
172 pub http_referer: String,
173 pub x_title: String,
174 pub mcp_connect_timeout: u64,
175 pub mcp_call_timeout: Option<u64>,
178 pub mcp_backoff_max_elapsed_time: u64,
179 pub mcp_encryption_key: Option<[u8; 32]>,
181 pub suppress_output: bool,
182 pub logs_dir: Option<std::path::PathBuf>,
185}
186
187pub async fn setup(
188 config: Config,
189 queue_delegate: Option<std::sync::Arc<dyn crate::QueueDelegate>>,
190 reverse_channel: Option<crate::ReverseChannel>,
191 dropper: Option<crate::Dropper>,
192) -> std::io::Result<(tokio::net::TcpListener, axum::Router)> {
193 let Config {
194 address,
195 port,
196 user_agent,
197 http_referer,
198 x_title,
199 mcp_connect_timeout,
200 mcp_call_timeout,
201 mcp_backoff_max_elapsed_time,
202 mcp_encryption_key: _,
208 suppress_output: _,
209 logs_dir,
210 } = config;
211
212 let client = Client::new(
213 reqwest::Client::new(),
214 user_agent,
215 x_title,
216 http_referer,
217 Some(Duration::from_millis(mcp_connect_timeout)),
218 Duration::from_millis(BACKOFF_INITIAL_INTERVAL_MS),
219 Duration::from_millis(BACKOFF_INITIAL_INTERVAL_MS),
220 BACKOFF_RANDOMIZATION_FACTOR,
221 BACKOFF_MULTIPLIER,
222 Duration::from_millis(BACKOFF_MAX_INTERVAL_MS),
223 Duration::from_millis(mcp_backoff_max_elapsed_time),
224 mcp_call_timeout.map(Duration::from_millis),
225 );
226
227 let sessions = Arc::new(SessionManager::new());
228 if let Some(dropper) = &dropper {
231 dropper.wire(sessions.clone(), reverse_channel.clone());
232 }
233 if let Some(rc) = &reverse_channel {
236 rc.wire_sessions(sessions.clone());
237 }
238 let state = AppState {
239 sessions,
240 client: Arc::new(client),
241 queue_delegate,
242 reverse_channel,
243 dropper,
244 };
245
246 let router = axum::Router::new()
247 .route(
248 "/",
249 axum::routing::post(mcp::handle_post)
250 .get(mcp::handle_get)
251 .delete(mcp::handle_delete),
252 )
253 .with_state(state);
254
255 let router = match logs_dir {
259 Some(dir) => router.layer(axum::middleware::from_fn_with_state(
260 std::sync::Arc::new(crate::logging::ProxyLogger::new(dir)),
261 crate::logging::log_layer,
262 )),
263 None => router,
264 };
265
266 let listener = tokio::net::TcpListener::bind(format!("{address}:{port}")).await?;
267
268 Ok((listener, router))
269}
270
271pub async fn serve(listener: tokio::net::TcpListener, app: axum::Router) -> std::io::Result<()> {
272 axum::serve(listener, app).await
273}
274
275pub async fn run(config: Config) -> std::io::Result<()> {
276 let suppress_output = config.suppress_output;
277 let (listener, app) = setup(config, None, None, None).await?;
279 if !suppress_output {
280 let addr = listener.local_addr()?;
281 eprintln!("listening on {addr}");
282 }
283 serve(listener, app).await
284}