Skip to main content

codex/mcp/
app.rs

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/// Stored app runtime converted into launch-ready config with metadata intact.
19#[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    /// Converts an app runtime into a launch-ready config using provided defaults.
65    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    /// Convenience clone-preserving conversion to a launcher.
101    pub fn to_launcher(&self, defaults: &StdioServerConfig) -> AppRuntimeLauncher {
102        self.clone().into_launcher(defaults)
103    }
104}
105
106/// Launch-ready stdio config bundled with app metadata.
107#[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/// Summarized app runtime metadata for listing.
117#[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/// Errors surfaced while reading app runtimes.
137#[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/// Prepared app runtime with merged stdio config and metadata.
156#[derive(Clone, Debug)]
157pub struct AppRuntimeHandle {
158    pub name: String,
159    pub metadata: Value,
160    pub config: StdioServerConfig,
161}
162
163/// Running app-server instance with metadata preserved.
164pub 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    /// Gracefully shut down the app-server.
183    pub async fn stop(&self) -> Result<(), McpError> {
184        self.server.shutdown().await
185    }
186}
187
188impl AppRuntimeHandle {
189    /// Launch the app-server using the prepared stdio config.
190    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/// Non-destructive manager for app runtimes backed by launch-ready configs.
209#[derive(Clone, Debug)]
210pub struct AppRuntimeManager {
211    launchers: BTreeMap<String, AppRuntimeLauncher>,
212}
213
214impl AppRuntimeManager {
215    /// Construct a runtime manager from prepared launchers.
216    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    /// Returns the available app runtimes with metadata intact.
225    pub fn available(&self) -> Vec<AppRuntimeSummary> {
226        self.launchers
227            .values()
228            .map(AppRuntimeSummary::from)
229            .collect()
230    }
231
232    /// Returns a cloned launcher by name without mutating storage.
233    pub fn launcher(&self, name: &str) -> Option<AppRuntimeLauncher> {
234        self.launchers.get(name).cloned()
235    }
236
237    /// Returns a prepared config + metadata for launching the app server.
238    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    /// Start an app-server runtime using the prepared config and metadata.
251    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/// Read-only helpers around [`AppRuntimeManager`] backed by stored config.
268#[derive(Clone, Debug)]
269pub struct AppRuntimeApi {
270    manager: AppRuntimeManager,
271}
272
273impl AppRuntimeApi {
274    /// Build an API from already prepared launchers.
275    pub fn new(launchers: Vec<AppRuntimeLauncher>) -> Self {
276        Self {
277            manager: AppRuntimeManager::new(launchers),
278        }
279    }
280
281    /// Load app runtimes from disk and merge Workstream A stdio defaults.
282    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    /// List available runtimes and metadata.
291    pub fn available(&self) -> Vec<AppRuntimeSummary> {
292        self.manager.available()
293    }
294
295    /// Returns the launch-ready config bundle for the given runtime.
296    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    /// Prepare a stdio config + metadata for a runtime.
303    pub fn prepare(&self, name: &str) -> Result<AppRuntimeHandle, AppRuntimeError> {
304        self.manager.prepare(name)
305    }
306
307    /// Start an app runtime and return a managed handle.
308    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    /// Convenience accessor for the merged stdio config.
317    pub fn stdio_config(&self, name: &str) -> Result<StdioServerConfig, AppRuntimeError> {
318        self.prepare(name).map(|handle| handle.config)
319    }
320
321    /// Build a pooled lifecycle manager that can reuse running runtimes.
322    pub fn pool(&self) -> AppRuntimePool {
323        AppRuntimePool::new(self.manager.clone())
324    }
325
326    /// Consume the API and return a pooled lifecycle manager.
327    pub fn into_pool(self) -> AppRuntimePool {
328        AppRuntimePool::new(self.manager)
329    }
330
331    /// Build a pooled lifecycle API that can reuse running runtimes.
332    pub fn pool_api(&self) -> AppRuntimePoolApi {
333        AppRuntimePoolApi::from_manager(self.manager.clone())
334    }
335
336    /// Consume the API and return a pooled lifecycle API.
337    pub fn into_pool_api(self) -> AppRuntimePoolApi {
338        AppRuntimePoolApi::from_manager(self.manager)
339    }
340}
341
342/// Async pool that starts, reuses, and stops app runtimes without mutating config.
343///
344/// Runtime metadata and resume hints remain intact when runtimes are reused or restarted.
345#[derive(Clone, Debug)]
346pub struct AppRuntimePool {
347    manager: AppRuntimeManager,
348    running: Arc<Mutex<HashMap<String, Arc<ManagedAppRuntime>>>>,
349}
350
351impl AppRuntimePool {
352    /// Create a new pool backed by launch-ready runtime configs.
353    pub fn new(manager: AppRuntimeManager) -> Self {
354        Self {
355            manager,
356            running: Arc::new(Mutex::new(HashMap::new())),
357        }
358    }
359
360    /// List available runtimes and metadata without touching stored definitions.
361    pub fn available(&self) -> Vec<AppRuntimeSummary> {
362        self.manager.available()
363    }
364
365    /// List currently running runtimes with metadata/resume hints preserved.
366    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    /// Returns a launch-ready config bundle for the given runtime.
382    pub fn launcher(&self, name: &str) -> Option<AppRuntimeLauncher> {
383        self.manager.launcher(name)
384    }
385
386    /// Prepare a stdio config + metadata for a runtime without starting it.
387    pub fn prepare(&self, name: &str) -> Result<AppRuntimeHandle, AppRuntimeError> {
388        self.manager.prepare(name)
389    }
390
391    /// Start (or reuse) an app runtime. Subsequent calls reuse an existing instance.
392    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    /// Stop a running runtime and remove it from the pool.
423    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    /// Stop all running runtimes (best-effort) and clear the pool.
442    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/// Public API around [`AppRuntimePool`] that exposes pooled lifecycle helpers.
467///
468/// Operations reuse running runtimes when available while preserving stored definitions
469/// and app metadata/resume hints.
470#[derive(Clone, Debug)]
471pub struct AppRuntimePoolApi {
472    pool: AppRuntimePool,
473}
474
475impl AppRuntimePoolApi {
476    /// Build a pooled API from prepared launchers.
477    pub fn new(launchers: Vec<AppRuntimeLauncher>) -> Self {
478        Self::from_manager(AppRuntimeManager::new(launchers))
479    }
480
481    /// Load app runtimes from disk and merge Workstream A stdio defaults.
482    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    /// Build a pooled API from a runtime manager.
491    pub fn from_manager(manager: AppRuntimeManager) -> Self {
492        Self::from_pool(AppRuntimePool::new(manager))
493    }
494
495    /// Wrap an existing pool in the API surface.
496    pub fn from_pool(pool: AppRuntimePool) -> Self {
497        Self { pool }
498    }
499
500    /// List available runtimes and metadata.
501    pub fn available(&self) -> Vec<AppRuntimeSummary> {
502        self.pool.available()
503    }
504
505    /// List running runtimes with metadata intact.
506    pub async fn running(&self) -> Vec<AppRuntimeSummary> {
507        self.pool.running().await
508    }
509
510    /// Returns the launch-ready config bundle for the given runtime.
511    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    /// Prepare a stdio config + metadata for a runtime.
518    pub fn prepare(&self, name: &str) -> Result<AppRuntimeHandle, AppRuntimeError> {
519        self.pool.prepare(name)
520    }
521
522    /// Start (or reuse) an app runtime.
523    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    /// Stop a running runtime and remove it from the pool.
532    pub async fn stop(&self, name: &str) -> Result<(), AppRuntimeError> {
533        self.pool.stop(name).await
534    }
535
536    /// Stop all running runtimes (best-effort) and clear the pool.
537    pub async fn stop_all(&self) -> Result<(), AppRuntimeError> {
538        self.pool.stop_all().await
539    }
540
541    /// Convenience accessor for the merged stdio config.
542    pub fn stdio_config(&self, name: &str) -> Result<StdioServerConfig, AppRuntimeError> {
543        self.prepare(name).map(|handle| handle.config)
544    }
545}