1use std::sync::Arc;
10
11use envconfig::Envconfig;
12use futures::StreamExt;
13use objectiveai_sdk::cli::command::CommandExecutor;
14use objectiveai_sdk::cli::command::plugins;
15use objectiveai_sdk::cli::command::tools;
16use rmcp::transport::streamable_http_server::{StreamableHttpServerConfig, StreamableHttpService};
17use tokio_util::sync::CancellationToken;
18
19use crate::agent_args_registry::AgentArgumentsRegistry;
20use crate::header_session_manager::HeaderSessionManager;
21use crate::objectiveai::ObjectiveAiMcpCli;
22
23#[derive(Envconfig)]
24struct EnvConfigBuilder {
25 #[envconfig(from = "ADDRESS")]
26 address: Option<String>,
27 #[envconfig(from = "PORT")]
28 port: Option<u16>,
29 #[envconfig(from = "SUPPRESS_OUTPUT")]
30 suppress_output: Option<String>,
31 #[envconfig(from = "OBJECTIVEAI_DIR")]
32 objectiveai_dir: Option<String>,
33 #[envconfig(from = "OBJECTIVEAI_STATE")]
34 objectiveai_state: Option<String>,
35}
36
37impl EnvConfigBuilder {
38 fn build(self) -> ConfigBuilder {
39 ConfigBuilder {
40 address: self.address,
41 port: self.port,
42 suppress_output: self
43 .suppress_output
44 .map(|v| matches!(v.to_ascii_lowercase().as_str(), "1" | "true" | "yes" | "on")),
45 objectiveai_dir: self.objectiveai_dir,
46 objectiveai_state: self.objectiveai_state,
47 }
48 }
49}
50
51#[derive(Default)]
52pub struct ConfigBuilder {
53 pub address: Option<String>,
54 pub port: Option<u16>,
55 pub suppress_output: Option<bool>,
56 pub objectiveai_dir: Option<String>,
57 pub objectiveai_state: Option<String>,
58}
59
60impl Envconfig for ConfigBuilder {
61 #[allow(deprecated)]
62 fn init() -> Result<Self, envconfig::Error> {
63 EnvConfigBuilder::init().map(|e| e.build())
64 }
65
66 fn init_from_env() -> Result<Self, envconfig::Error> {
67 EnvConfigBuilder::init_from_env().map(|e| e.build())
68 }
69
70 fn init_from_hashmap(
71 hashmap: &std::collections::HashMap<String, String>,
72 ) -> Result<Self, envconfig::Error> {
73 EnvConfigBuilder::init_from_hashmap(hashmap).map(|e| e.build())
74 }
75}
76
77impl ConfigBuilder {
78 pub fn build(self) -> Config {
79 Config {
80 address: self.address.unwrap_or_else(|| "127.0.0.1".to_string()),
81 port: self.port.unwrap_or(0),
82 suppress_output: self.suppress_output.unwrap_or(false),
83 objectiveai_dir: match self.objectiveai_dir {
86 Some(dir) => std::path::PathBuf::from(dir),
87 None => dirs::home_dir()
88 .unwrap_or_else(|| std::path::PathBuf::from("."))
89 .join(".objectiveai"),
90 },
91 objectiveai_state: self
92 .objectiveai_state
93 .unwrap_or_else(|| "default".to_string()),
94 }
95 }
96}
97
98pub struct Config {
99 pub address: String,
100 pub port: u16,
101 pub suppress_output: bool,
102 pub objectiveai_dir: std::path::PathBuf,
104 pub objectiveai_state: String,
106}
107
108pub async fn setup<E>(
113 config: Config,
114 executor: E,
115) -> std::io::Result<(tokio::net::TcpListener, axum::Router)>
116where
117 E: CommandExecutor + Send + Sync + 'static,
118 E::Error: std::fmt::Display + Send + 'static,
119{
120 let Config {
121 address,
122 port,
123 suppress_output: _,
124 objectiveai_dir: _,
125 objectiveai_state: _,
126 } = config;
127
128 let executor = Arc::new(executor);
129
130 let (plugins_list, tools_list) =
138 tokio::join!(list_plugins(&*executor), list_tools(&*executor));
139 let plugins_list = Arc::new(plugins_list);
140 let tools_list = Arc::new(tools_list);
141
142 let registry = Arc::new(AgentArgumentsRegistry::new());
149
150 let server = ObjectiveAiMcpCli::with_plugins_and_tools(
151 executor.clone(),
152 (*plugins_list).clone(),
153 (*tools_list).clone(),
154 registry.clone(),
155 );
156 let session_manager = Arc::new(HeaderSessionManager::new(
157 registry.clone(),
158 server.clone(),
159 tools_list.clone(),
160 plugins_list.clone(),
161 ));
162 let ct = CancellationToken::new();
163
164 let service: StreamableHttpService<ObjectiveAiMcpCli<E>, HeaderSessionManager<E>> =
165 StreamableHttpService::new(
166 move || Ok(server.clone()),
167 session_manager,
168 StreamableHttpServerConfig {
169 stateful_mode: true,
170 sse_keep_alive: None,
171 cancellation_token: ct.child_token(),
172 ..Default::default()
173 },
174 );
175
176 let router = axum::Router::new().fallback_service(service);
177 let listener = tokio::net::TcpListener::bind(format!("{address}:{port}")).await?;
178 Ok((listener, router))
179}
180
181pub async fn serve(
182 listener: tokio::net::TcpListener,
183 app: axum::Router,
184) -> std::io::Result<()> {
185 axum::serve(listener, app).await
186}
187
188pub async fn run<E>(config: Config, executor: E) -> std::io::Result<()>
189where
190 E: CommandExecutor + Send + Sync + 'static,
191 E::Error: std::fmt::Display + Send + 'static,
192{
193 let suppress_output = config.suppress_output;
194 let lock_dir = config
195 .objectiveai_dir
196 .join("state")
197 .join(&config.objectiveai_state)
198 .join("locks");
199 let (listener, app) = setup(config, executor).await?;
200
201 let addr = listener.local_addr()?;
210 let connect_ip = match addr.ip() {
211 std::net::IpAddr::V4(v4) if v4.is_unspecified() => {
212 std::net::IpAddr::V4(std::net::Ipv4Addr::LOCALHOST)
213 }
214 std::net::IpAddr::V6(v6) if v6.is_unspecified() => {
215 std::net::IpAddr::V6(std::net::Ipv6Addr::LOCALHOST)
216 }
217 ip => ip,
218 };
219 let connect_url =
220 format!("http://{}", std::net::SocketAddr::new(connect_ip, addr.port()));
221 if objectiveai_sdk::lockfile::try_acquire(&lock_dir, "mcp", &connect_url)
222 .await
223 .is_none()
224 {
225 return Err(std::io::Error::other(
226 "another objectiveai-mcp instance already holds the mcp lock for this state",
227 ));
228 }
229
230 if !suppress_output {
231 eprintln!("listening on {addr}");
232 }
233 serve(listener, app).await
234}
235
236async fn list_plugins<E>(executor: &E) -> Vec<plugins::list::ResponseItem>
240where
241 E: CommandExecutor + Send + Sync + 'static,
242 E::Error: std::fmt::Display + Send + 'static,
243{
244 let request = plugins::list::Request {
245 path_type: plugins::list::Path::PluginsList,
246 offset: None,
247 limit: None,
248 base: Default::default(),
249 };
250 match plugins::list::execute(executor, request, None).await {
251 Ok(stream) => stream.filter_map(|r| async move { r.ok() }).collect().await,
252 Err(_) => Vec::new(),
253 }
254}
255
256async fn list_tools<E>(executor: &E) -> Vec<tools::list::ResponseItem>
258where
259 E: CommandExecutor + Send + Sync + 'static,
260 E::Error: std::fmt::Display + Send + 'static,
261{
262 let request = tools::list::Request {
263 path_type: tools::list::Path::ToolsList,
264 offset: None,
265 limit: None,
266 base: Default::default(),
267 };
268 match tools::list::execute(executor, request, None).await {
269 Ok(stream) => stream.filter_map(|r| async move { r.ok() }).collect().await,
270 Err(_) => Vec::new(),
271 }
272}