1use std::{
2 collections::{BTreeMap, HashMap},
3 fmt,
4 path::PathBuf,
5 sync::Arc,
6 time::Duration,
7};
8
9use serde_json::Value;
10use thiserror::Error;
11use tokio::sync::Mutex;
12
13use super::{
14 runtime::merge_stdio_env, AppRuntimeDefinition, AppRuntimeEntry, ClientInfo, CodexAppServer,
15 McpConfigError, McpConfigManager, McpError, StdioServerConfig,
16};
17
18#[derive(Clone, Debug, PartialEq)]
20pub struct AppRuntime {
21 pub name: String,
22 pub description: Option<String>,
23 pub tags: Vec<String>,
24 pub metadata: Value,
25 pub env: BTreeMap<String, String>,
26 pub code_home: Option<PathBuf>,
27 pub current_dir: Option<PathBuf>,
28 pub mirror_stdio: Option<bool>,
29 pub startup_timeout_ms: Option<u64>,
30 pub binary: Option<PathBuf>,
31}
32
33impl From<AppRuntimeEntry> for AppRuntime {
34 fn from(entry: AppRuntimeEntry) -> Self {
35 let AppRuntimeEntry { name, definition } = entry;
36 let AppRuntimeDefinition {
37 description,
38 tags,
39 env,
40 code_home,
41 current_dir,
42 mirror_stdio,
43 startup_timeout_ms,
44 binary,
45 metadata,
46 } = definition;
47
48 Self {
49 name,
50 description,
51 tags,
52 metadata,
53 env,
54 code_home,
55 current_dir,
56 mirror_stdio,
57 startup_timeout_ms,
58 binary,
59 }
60 }
61}
62
63impl AppRuntime {
64 pub fn into_launcher(self, defaults: &StdioServerConfig) -> AppRuntimeLauncher {
66 let code_home = self
67 .code_home
68 .clone()
69 .or_else(|| defaults.code_home.clone());
70 let env = merge_stdio_env(code_home.as_deref(), &defaults.env, &self.env);
71
72 let config = StdioServerConfig {
73 binary: self
74 .binary
75 .clone()
76 .unwrap_or_else(|| defaults.binary.clone()),
77 code_home,
78 current_dir: self
79 .current_dir
80 .clone()
81 .or_else(|| defaults.current_dir.clone()),
82 env,
83 app_server_analytics_default_enabled: defaults.app_server_analytics_default_enabled,
84 mirror_stdio: self.mirror_stdio.unwrap_or(defaults.mirror_stdio),
85 startup_timeout: self
86 .startup_timeout_ms
87 .map(Duration::from_millis)
88 .unwrap_or(defaults.startup_timeout),
89 };
90
91 AppRuntimeLauncher {
92 name: self.name,
93 description: self.description,
94 tags: self.tags,
95 metadata: self.metadata,
96 config,
97 }
98 }
99
100 pub fn to_launcher(&self, defaults: &StdioServerConfig) -> AppRuntimeLauncher {
102 self.clone().into_launcher(defaults)
103 }
104}
105
106#[derive(Clone, Debug)]
108pub struct AppRuntimeLauncher {
109 pub name: String,
110 pub description: Option<String>,
111 pub tags: Vec<String>,
112 pub metadata: Value,
113 pub config: StdioServerConfig,
114}
115
116#[derive(Clone, Debug, PartialEq)]
118pub struct AppRuntimeSummary {
119 pub name: String,
120 pub description: Option<String>,
121 pub tags: Vec<String>,
122 pub metadata: Value,
123}
124
125impl From<&AppRuntimeLauncher> for AppRuntimeSummary {
126 fn from(launcher: &AppRuntimeLauncher) -> Self {
127 Self {
128 name: launcher.name.clone(),
129 description: launcher.description.clone(),
130 tags: launcher.tags.clone(),
131 metadata: launcher.metadata.clone(),
132 }
133 }
134}
135
136#[derive(Debug, Error)]
138pub enum AppRuntimeError {
139 #[error("runtime `{0}` not found")]
140 NotFound(String),
141 #[error("failed to start runtime `{name}`: {source}")]
142 Start {
143 name: String,
144 #[source]
145 source: McpError,
146 },
147 #[error("failed to stop runtime `{name}`: {source}")]
148 Stop {
149 name: String,
150 #[source]
151 source: McpError,
152 },
153}
154
155#[derive(Clone, Debug)]
157pub struct AppRuntimeHandle {
158 pub name: String,
159 pub metadata: Value,
160 pub config: StdioServerConfig,
161}
162
163pub struct ManagedAppRuntime {
165 pub name: String,
166 pub metadata: Value,
167 pub config: StdioServerConfig,
168 pub server: CodexAppServer,
169}
170
171impl fmt::Debug for ManagedAppRuntime {
172 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
173 f.debug_struct("ManagedAppRuntime")
174 .field("name", &self.name)
175 .field("metadata", &self.metadata)
176 .field("config", &self.config)
177 .finish()
178 }
179}
180
181impl ManagedAppRuntime {
182 pub async fn stop(&self) -> Result<(), McpError> {
184 self.server.shutdown().await
185 }
186}
187
188impl AppRuntimeHandle {
189 pub async fn start(self, client: ClientInfo) -> Result<ManagedAppRuntime, McpError> {
191 let AppRuntimeHandle {
192 name,
193 metadata,
194 config,
195 } = self;
196
197 let server = CodexAppServer::start(config.clone(), client).await?;
198
199 Ok(ManagedAppRuntime {
200 name,
201 metadata,
202 config,
203 server,
204 })
205 }
206}
207
208#[derive(Clone, Debug)]
210pub struct AppRuntimeManager {
211 launchers: BTreeMap<String, AppRuntimeLauncher>,
212}
213
214impl AppRuntimeManager {
215 pub fn new(launchers: Vec<AppRuntimeLauncher>) -> Self {
217 let mut map = BTreeMap::new();
218 for launcher in launchers {
219 map.insert(launcher.name.clone(), launcher);
220 }
221 Self { launchers: map }
222 }
223
224 pub fn available(&self) -> Vec<AppRuntimeSummary> {
226 self.launchers
227 .values()
228 .map(AppRuntimeSummary::from)
229 .collect()
230 }
231
232 pub fn launcher(&self, name: &str) -> Option<AppRuntimeLauncher> {
234 self.launchers.get(name).cloned()
235 }
236
237 pub fn prepare(&self, name: &str) -> Result<AppRuntimeHandle, AppRuntimeError> {
239 let Some(launcher) = self.launcher(name) else {
240 return Err(AppRuntimeError::NotFound(name.to_string()));
241 };
242
243 Ok(AppRuntimeHandle {
244 name: launcher.name,
245 metadata: launcher.metadata,
246 config: launcher.config,
247 })
248 }
249
250 pub async fn start(
252 &self,
253 name: &str,
254 client: ClientInfo,
255 ) -> Result<ManagedAppRuntime, AppRuntimeError> {
256 let handle = self.prepare(name)?;
257 handle
258 .start(client)
259 .await
260 .map_err(|source| AppRuntimeError::Start {
261 name: name.to_string(),
262 source,
263 })
264 }
265}
266
267#[derive(Clone, Debug)]
269pub struct AppRuntimeApi {
270 manager: AppRuntimeManager,
271}
272
273impl AppRuntimeApi {
274 pub fn new(launchers: Vec<AppRuntimeLauncher>) -> Self {
276 Self {
277 manager: AppRuntimeManager::new(launchers),
278 }
279 }
280
281 pub fn from_config(
283 config: &McpConfigManager,
284 defaults: &StdioServerConfig,
285 ) -> Result<Self, McpConfigError> {
286 let launchers = config.app_runtime_launchers(defaults)?;
287 Ok(Self::new(launchers))
288 }
289
290 pub fn available(&self) -> Vec<AppRuntimeSummary> {
292 self.manager.available()
293 }
294
295 pub fn launcher(&self, name: &str) -> Result<AppRuntimeLauncher, AppRuntimeError> {
297 self.manager
298 .launcher(name)
299 .ok_or_else(|| AppRuntimeError::NotFound(name.to_string()))
300 }
301
302 pub fn prepare(&self, name: &str) -> Result<AppRuntimeHandle, AppRuntimeError> {
304 self.manager.prepare(name)
305 }
306
307 pub async fn start(
309 &self,
310 name: &str,
311 client: ClientInfo,
312 ) -> Result<ManagedAppRuntime, AppRuntimeError> {
313 self.manager.start(name, client).await
314 }
315
316 pub fn stdio_config(&self, name: &str) -> Result<StdioServerConfig, AppRuntimeError> {
318 self.prepare(name).map(|handle| handle.config)
319 }
320
321 pub fn pool(&self) -> AppRuntimePool {
323 AppRuntimePool::new(self.manager.clone())
324 }
325
326 pub fn into_pool(self) -> AppRuntimePool {
328 AppRuntimePool::new(self.manager)
329 }
330
331 pub fn pool_api(&self) -> AppRuntimePoolApi {
333 AppRuntimePoolApi::from_manager(self.manager.clone())
334 }
335
336 pub fn into_pool_api(self) -> AppRuntimePoolApi {
338 AppRuntimePoolApi::from_manager(self.manager)
339 }
340}
341
342#[derive(Clone, Debug)]
346pub struct AppRuntimePool {
347 manager: AppRuntimeManager,
348 running: Arc<Mutex<HashMap<String, Arc<ManagedAppRuntime>>>>,
349}
350
351impl AppRuntimePool {
352 pub fn new(manager: AppRuntimeManager) -> Self {
354 Self {
355 manager,
356 running: Arc::new(Mutex::new(HashMap::new())),
357 }
358 }
359
360 pub fn available(&self) -> Vec<AppRuntimeSummary> {
362 self.manager.available()
363 }
364
365 pub async fn running(&self) -> Vec<AppRuntimeSummary> {
367 let mut names: Vec<String> = {
368 let guard = self.running.lock().await;
369 guard.keys().cloned().collect()
370 };
371
372 names.sort();
373
374 names
375 .into_iter()
376 .filter_map(|name| self.manager.launcher(&name))
377 .map(|launcher| AppRuntimeSummary::from(&launcher))
378 .collect()
379 }
380
381 pub fn launcher(&self, name: &str) -> Option<AppRuntimeLauncher> {
383 self.manager.launcher(name)
384 }
385
386 pub fn prepare(&self, name: &str) -> Result<AppRuntimeHandle, AppRuntimeError> {
388 self.manager.prepare(name)
389 }
390
391 pub async fn start(
393 &self,
394 name: &str,
395 client: ClientInfo,
396 ) -> Result<Arc<ManagedAppRuntime>, AppRuntimeError> {
397 {
398 let guard = self.running.lock().await;
399 if let Some(existing) = guard.get(name) {
400 return Ok(existing.clone());
401 }
402 }
403
404 let runtime = Arc::new(self.manager.start(name, client).await?);
405
406 let mut guard = self.running.lock().await;
407 if let Some(existing) = guard.get(name) {
408 runtime
409 .stop()
410 .await
411 .map_err(|source| AppRuntimeError::Stop {
412 name: name.to_string(),
413 source,
414 })?;
415 return Ok(existing.clone());
416 }
417
418 guard.insert(name.to_string(), runtime.clone());
419 Ok(runtime)
420 }
421
422 pub async fn stop(&self, name: &str) -> Result<(), AppRuntimeError> {
424 let runtime = {
425 let mut guard = self.running.lock().await;
426 guard.remove(name)
427 };
428
429 match runtime {
430 Some(runtime) => runtime
431 .stop()
432 .await
433 .map_err(|source| AppRuntimeError::Stop {
434 name: name.to_string(),
435 source,
436 }),
437 None => Err(AppRuntimeError::NotFound(name.to_string())),
438 }
439 }
440
441 pub async fn stop_all(&self) -> Result<(), AppRuntimeError> {
443 let runtimes: Vec<(String, Arc<ManagedAppRuntime>)> = {
444 let mut guard = self.running.lock().await;
445 guard.drain().collect()
446 };
447
448 let mut first_error: Option<AppRuntimeError> = None;
449
450 for (name, runtime) in runtimes {
451 if let Err(source) = runtime.stop().await {
452 if first_error.is_none() {
453 first_error = Some(AppRuntimeError::Stop { name, source });
454 }
455 }
456 }
457
458 if let Some(err) = first_error {
459 return Err(err);
460 }
461
462 Ok(())
463 }
464}
465
466#[derive(Clone, Debug)]
471pub struct AppRuntimePoolApi {
472 pool: AppRuntimePool,
473}
474
475impl AppRuntimePoolApi {
476 pub fn new(launchers: Vec<AppRuntimeLauncher>) -> Self {
478 Self::from_manager(AppRuntimeManager::new(launchers))
479 }
480
481 pub fn from_config(
483 config: &McpConfigManager,
484 defaults: &StdioServerConfig,
485 ) -> Result<Self, McpConfigError> {
486 let launchers = config.app_runtime_launchers(defaults)?;
487 Ok(Self::new(launchers))
488 }
489
490 pub fn from_manager(manager: AppRuntimeManager) -> Self {
492 Self::from_pool(AppRuntimePool::new(manager))
493 }
494
495 pub fn from_pool(pool: AppRuntimePool) -> Self {
497 Self { pool }
498 }
499
500 pub fn available(&self) -> Vec<AppRuntimeSummary> {
502 self.pool.available()
503 }
504
505 pub async fn running(&self) -> Vec<AppRuntimeSummary> {
507 self.pool.running().await
508 }
509
510 pub fn launcher(&self, name: &str) -> Result<AppRuntimeLauncher, AppRuntimeError> {
512 self.pool
513 .launcher(name)
514 .ok_or_else(|| AppRuntimeError::NotFound(name.to_string()))
515 }
516
517 pub fn prepare(&self, name: &str) -> Result<AppRuntimeHandle, AppRuntimeError> {
519 self.pool.prepare(name)
520 }
521
522 pub async fn start(
524 &self,
525 name: &str,
526 client: ClientInfo,
527 ) -> Result<Arc<ManagedAppRuntime>, AppRuntimeError> {
528 self.pool.start(name, client).await
529 }
530
531 pub async fn stop(&self, name: &str) -> Result<(), AppRuntimeError> {
533 self.pool.stop(name).await
534 }
535
536 pub async fn stop_all(&self) -> Result<(), AppRuntimeError> {
538 self.pool.stop_all().await
539 }
540
541 pub fn stdio_config(&self, name: &str) -> Result<StdioServerConfig, AppRuntimeError> {
543 self.prepare(name).map(|handle| handle.config)
544 }
545}