use std::{
collections::{BTreeMap, HashMap},
fmt,
path::PathBuf,
sync::Arc,
time::Duration,
};
use serde_json::Value;
use thiserror::Error;
use tokio::sync::Mutex;
use super::{
runtime::merge_stdio_env, AppRuntimeDefinition, AppRuntimeEntry, ClientInfo, CodexAppServer,
McpConfigError, McpConfigManager, McpError, StdioServerConfig,
};
#[derive(Clone, Debug, PartialEq)]
pub struct AppRuntime {
pub name: String,
pub description: Option<String>,
pub tags: Vec<String>,
pub metadata: Value,
pub env: BTreeMap<String, String>,
pub code_home: Option<PathBuf>,
pub current_dir: Option<PathBuf>,
pub mirror_stdio: Option<bool>,
pub startup_timeout_ms: Option<u64>,
pub binary: Option<PathBuf>,
}
impl From<AppRuntimeEntry> for AppRuntime {
fn from(entry: AppRuntimeEntry) -> Self {
let AppRuntimeEntry { name, definition } = entry;
let AppRuntimeDefinition {
description,
tags,
env,
code_home,
current_dir,
mirror_stdio,
startup_timeout_ms,
binary,
metadata,
} = definition;
Self {
name,
description,
tags,
metadata,
env,
code_home,
current_dir,
mirror_stdio,
startup_timeout_ms,
binary,
}
}
}
impl AppRuntime {
pub fn into_launcher(self, defaults: &StdioServerConfig) -> AppRuntimeLauncher {
let code_home = self
.code_home
.clone()
.or_else(|| defaults.code_home.clone());
let env = merge_stdio_env(code_home.as_deref(), &defaults.env, &self.env);
let config = StdioServerConfig {
binary: self
.binary
.clone()
.unwrap_or_else(|| defaults.binary.clone()),
code_home,
current_dir: self
.current_dir
.clone()
.or_else(|| defaults.current_dir.clone()),
env,
app_server_analytics_default_enabled: defaults.app_server_analytics_default_enabled,
mirror_stdio: self.mirror_stdio.unwrap_or(defaults.mirror_stdio),
startup_timeout: self
.startup_timeout_ms
.map(Duration::from_millis)
.unwrap_or(defaults.startup_timeout),
};
AppRuntimeLauncher {
name: self.name,
description: self.description,
tags: self.tags,
metadata: self.metadata,
config,
}
}
pub fn to_launcher(&self, defaults: &StdioServerConfig) -> AppRuntimeLauncher {
self.clone().into_launcher(defaults)
}
}
#[derive(Clone, Debug)]
pub struct AppRuntimeLauncher {
pub name: String,
pub description: Option<String>,
pub tags: Vec<String>,
pub metadata: Value,
pub config: StdioServerConfig,
}
#[derive(Clone, Debug, PartialEq)]
pub struct AppRuntimeSummary {
pub name: String,
pub description: Option<String>,
pub tags: Vec<String>,
pub metadata: Value,
}
impl From<&AppRuntimeLauncher> for AppRuntimeSummary {
fn from(launcher: &AppRuntimeLauncher) -> Self {
Self {
name: launcher.name.clone(),
description: launcher.description.clone(),
tags: launcher.tags.clone(),
metadata: launcher.metadata.clone(),
}
}
}
#[derive(Debug, Error)]
pub enum AppRuntimeError {
#[error("runtime `{0}` not found")]
NotFound(String),
#[error("failed to start runtime `{name}`: {source}")]
Start {
name: String,
#[source]
source: McpError,
},
#[error("failed to stop runtime `{name}`: {source}")]
Stop {
name: String,
#[source]
source: McpError,
},
}
#[derive(Clone, Debug)]
pub struct AppRuntimeHandle {
pub name: String,
pub metadata: Value,
pub config: StdioServerConfig,
}
pub struct ManagedAppRuntime {
pub name: String,
pub metadata: Value,
pub config: StdioServerConfig,
pub server: CodexAppServer,
}
impl fmt::Debug for ManagedAppRuntime {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("ManagedAppRuntime")
.field("name", &self.name)
.field("metadata", &self.metadata)
.field("config", &self.config)
.finish()
}
}
impl ManagedAppRuntime {
pub async fn stop(&self) -> Result<(), McpError> {
self.server.shutdown().await
}
}
impl AppRuntimeHandle {
pub async fn start(self, client: ClientInfo) -> Result<ManagedAppRuntime, McpError> {
let AppRuntimeHandle {
name,
metadata,
config,
} = self;
let server = CodexAppServer::start(config.clone(), client).await?;
Ok(ManagedAppRuntime {
name,
metadata,
config,
server,
})
}
}
#[derive(Clone, Debug)]
pub struct AppRuntimeManager {
launchers: BTreeMap<String, AppRuntimeLauncher>,
}
impl AppRuntimeManager {
pub fn new(launchers: Vec<AppRuntimeLauncher>) -> Self {
let mut map = BTreeMap::new();
for launcher in launchers {
map.insert(launcher.name.clone(), launcher);
}
Self { launchers: map }
}
pub fn available(&self) -> Vec<AppRuntimeSummary> {
self.launchers
.values()
.map(AppRuntimeSummary::from)
.collect()
}
pub fn launcher(&self, name: &str) -> Option<AppRuntimeLauncher> {
self.launchers.get(name).cloned()
}
pub fn prepare(&self, name: &str) -> Result<AppRuntimeHandle, AppRuntimeError> {
let Some(launcher) = self.launcher(name) else {
return Err(AppRuntimeError::NotFound(name.to_string()));
};
Ok(AppRuntimeHandle {
name: launcher.name,
metadata: launcher.metadata,
config: launcher.config,
})
}
pub async fn start(
&self,
name: &str,
client: ClientInfo,
) -> Result<ManagedAppRuntime, AppRuntimeError> {
let handle = self.prepare(name)?;
handle
.start(client)
.await
.map_err(|source| AppRuntimeError::Start {
name: name.to_string(),
source,
})
}
}
#[derive(Clone, Debug)]
pub struct AppRuntimeApi {
manager: AppRuntimeManager,
}
impl AppRuntimeApi {
pub fn new(launchers: Vec<AppRuntimeLauncher>) -> Self {
Self {
manager: AppRuntimeManager::new(launchers),
}
}
pub fn from_config(
config: &McpConfigManager,
defaults: &StdioServerConfig,
) -> Result<Self, McpConfigError> {
let launchers = config.app_runtime_launchers(defaults)?;
Ok(Self::new(launchers))
}
pub fn available(&self) -> Vec<AppRuntimeSummary> {
self.manager.available()
}
pub fn launcher(&self, name: &str) -> Result<AppRuntimeLauncher, AppRuntimeError> {
self.manager
.launcher(name)
.ok_or_else(|| AppRuntimeError::NotFound(name.to_string()))
}
pub fn prepare(&self, name: &str) -> Result<AppRuntimeHandle, AppRuntimeError> {
self.manager.prepare(name)
}
pub async fn start(
&self,
name: &str,
client: ClientInfo,
) -> Result<ManagedAppRuntime, AppRuntimeError> {
self.manager.start(name, client).await
}
pub fn stdio_config(&self, name: &str) -> Result<StdioServerConfig, AppRuntimeError> {
self.prepare(name).map(|handle| handle.config)
}
pub fn pool(&self) -> AppRuntimePool {
AppRuntimePool::new(self.manager.clone())
}
pub fn into_pool(self) -> AppRuntimePool {
AppRuntimePool::new(self.manager)
}
pub fn pool_api(&self) -> AppRuntimePoolApi {
AppRuntimePoolApi::from_manager(self.manager.clone())
}
pub fn into_pool_api(self) -> AppRuntimePoolApi {
AppRuntimePoolApi::from_manager(self.manager)
}
}
#[derive(Clone, Debug)]
pub struct AppRuntimePool {
manager: AppRuntimeManager,
running: Arc<Mutex<HashMap<String, Arc<ManagedAppRuntime>>>>,
}
impl AppRuntimePool {
pub fn new(manager: AppRuntimeManager) -> Self {
Self {
manager,
running: Arc::new(Mutex::new(HashMap::new())),
}
}
pub fn available(&self) -> Vec<AppRuntimeSummary> {
self.manager.available()
}
pub async fn running(&self) -> Vec<AppRuntimeSummary> {
let mut names: Vec<String> = {
let guard = self.running.lock().await;
guard.keys().cloned().collect()
};
names.sort();
names
.into_iter()
.filter_map(|name| self.manager.launcher(&name))
.map(|launcher| AppRuntimeSummary::from(&launcher))
.collect()
}
pub fn launcher(&self, name: &str) -> Option<AppRuntimeLauncher> {
self.manager.launcher(name)
}
pub fn prepare(&self, name: &str) -> Result<AppRuntimeHandle, AppRuntimeError> {
self.manager.prepare(name)
}
pub async fn start(
&self,
name: &str,
client: ClientInfo,
) -> Result<Arc<ManagedAppRuntime>, AppRuntimeError> {
{
let guard = self.running.lock().await;
if let Some(existing) = guard.get(name) {
return Ok(existing.clone());
}
}
let runtime = Arc::new(self.manager.start(name, client).await?);
let mut guard = self.running.lock().await;
if let Some(existing) = guard.get(name) {
runtime
.stop()
.await
.map_err(|source| AppRuntimeError::Stop {
name: name.to_string(),
source,
})?;
return Ok(existing.clone());
}
guard.insert(name.to_string(), runtime.clone());
Ok(runtime)
}
pub async fn stop(&self, name: &str) -> Result<(), AppRuntimeError> {
let runtime = {
let mut guard = self.running.lock().await;
guard.remove(name)
};
match runtime {
Some(runtime) => runtime
.stop()
.await
.map_err(|source| AppRuntimeError::Stop {
name: name.to_string(),
source,
}),
None => Err(AppRuntimeError::NotFound(name.to_string())),
}
}
pub async fn stop_all(&self) -> Result<(), AppRuntimeError> {
let runtimes: Vec<(String, Arc<ManagedAppRuntime>)> = {
let mut guard = self.running.lock().await;
guard.drain().collect()
};
let mut first_error: Option<AppRuntimeError> = None;
for (name, runtime) in runtimes {
if let Err(source) = runtime.stop().await {
if first_error.is_none() {
first_error = Some(AppRuntimeError::Stop { name, source });
}
}
}
if let Some(err) = first_error {
return Err(err);
}
Ok(())
}
}
#[derive(Clone, Debug)]
pub struct AppRuntimePoolApi {
pool: AppRuntimePool,
}
impl AppRuntimePoolApi {
pub fn new(launchers: Vec<AppRuntimeLauncher>) -> Self {
Self::from_manager(AppRuntimeManager::new(launchers))
}
pub fn from_config(
config: &McpConfigManager,
defaults: &StdioServerConfig,
) -> Result<Self, McpConfigError> {
let launchers = config.app_runtime_launchers(defaults)?;
Ok(Self::new(launchers))
}
pub fn from_manager(manager: AppRuntimeManager) -> Self {
Self::from_pool(AppRuntimePool::new(manager))
}
pub fn from_pool(pool: AppRuntimePool) -> Self {
Self { pool }
}
pub fn available(&self) -> Vec<AppRuntimeSummary> {
self.pool.available()
}
pub async fn running(&self) -> Vec<AppRuntimeSummary> {
self.pool.running().await
}
pub fn launcher(&self, name: &str) -> Result<AppRuntimeLauncher, AppRuntimeError> {
self.pool
.launcher(name)
.ok_or_else(|| AppRuntimeError::NotFound(name.to_string()))
}
pub fn prepare(&self, name: &str) -> Result<AppRuntimeHandle, AppRuntimeError> {
self.pool.prepare(name)
}
pub async fn start(
&self,
name: &str,
client: ClientInfo,
) -> Result<Arc<ManagedAppRuntime>, AppRuntimeError> {
self.pool.start(name, client).await
}
pub async fn stop(&self, name: &str) -> Result<(), AppRuntimeError> {
self.pool.stop(name).await
}
pub async fn stop_all(&self) -> Result<(), AppRuntimeError> {
self.pool.stop_all().await
}
pub fn stdio_config(&self, name: &str) -> Result<StdioServerConfig, AppRuntimeError> {
self.prepare(name).map(|handle| handle.config)
}
}