#![cfg(feature = "fetcher")]
use std::path::PathBuf;
use std::sync::Arc;
use rmcp::ErrorData;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use tokio::sync::Mutex;
use zendriver::{Fetcher, FetcherChannel as Channel, VersionSpec, ZendriverError};
use crate::errors::{McpServerError, map_error};
use crate::state::SessionState;
#[derive(Debug, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct InstallInput {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub version: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub channel: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cache_dir: Option<String>,
}
#[derive(Debug, Serialize, JsonSchema)]
pub struct InstallOutput {
pub path: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub version_requested: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub channel_requested: Option<String>,
}
pub async fn install_chrome(
_state: Arc<Mutex<SessionState>>,
input: InstallInput,
) -> Result<InstallOutput, ErrorData> {
let version_requested = input.version.clone();
let channel_requested = input.channel.clone();
let mut f = Fetcher::new();
if let Some(c) = &input.cache_dir {
f = f.cache_dir(PathBuf::from(c));
}
let spec = match (input.version, input.channel) {
(Some(v), _) => Some(VersionSpec::Explicit(v)),
(None, Some(c)) => Some(VersionSpec::Channel(parse_channel(&c)?)),
(None, None) => None,
};
if let Some(spec) = spec {
f = f.version(spec);
}
let path = f.ensure_chrome().await.map_err(|e| {
map_error(McpServerError::from(ZendriverError::from(e)))
})?;
Ok(InstallOutput {
path: path.display().to_string(),
version_requested,
channel_requested,
})
}
fn parse_channel(s: &str) -> Result<Channel, ErrorData> {
match s.to_ascii_lowercase().as_str() {
"stable" => Ok(Channel::Stable),
"beta" => Ok(Channel::Beta),
"dev" => Ok(Channel::Dev),
"canary" => Ok(Channel::Canary),
other => Err(ErrorData::invalid_request(
format!(
"Unknown channel `{other}`. Expected one of: stable, beta, dev, canary (case-insensitive)."
),
None,
)),
}
}
#[cfg(test)]
#[allow(clippy::panic, clippy::unwrap_used)]
mod tests {
use super::*;
#[tokio::test]
async fn install_with_unknown_channel_errors() {
let state = Arc::new(Mutex::new(SessionState::new()));
let err = install_chrome(
state,
InstallInput {
version: None,
channel: Some("frob".into()),
cache_dir: None,
},
)
.await
.expect_err("expected channel rejection");
assert!(err.message.contains("Unknown channel `frob`"));
}
#[test]
fn parse_channel_is_case_insensitive() {
assert_eq!(parse_channel("Stable").unwrap(), Channel::Stable);
assert_eq!(parse_channel("BETA").unwrap(), Channel::Beta);
assert_eq!(parse_channel("dev").unwrap(), Channel::Dev);
assert_eq!(parse_channel("Canary").unwrap(), Channel::Canary);
}
#[test]
fn parse_channel_rejects_unknown() {
let err = parse_channel("nightly").unwrap_err();
assert!(err.message.contains("Unknown channel"));
assert!(err.message.contains("nightly"));
}
}