use base64::Engine as _;
use base64::engine::general_purpose::STANDARD as BASE64;
use rmcp::ErrorData;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use crate::errors::{McpServerError, map_error};
use crate::snapshot::html_trim;
use crate::state::SessionState;
const MAX_INLINE_BYTES: usize = 5 * 1024 * 1024;
#[derive(Debug, Default, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct EmptyInput {}
pub async fn current_tab(s: &SessionState) -> Result<zendriver::Tab, ErrorData> {
let b = s
.browser
.as_ref()
.ok_or_else(|| map_error(McpServerError::BrowserNotOpen))?;
let id = s
.current_tab_id
.as_deref()
.ok_or_else(|| map_error(McpServerError::NoCurrentTab))?;
let tabs = b.tabs().await;
tabs.into_iter()
.find(|t| t.target_id() == id)
.ok_or_else(|| map_error(McpServerError::NoCurrentTab))
}
pub async fn lookup_frame(
tab: &zendriver::Tab,
frame_id: &str,
) -> Result<zendriver::Frame, ErrorData> {
let frames = tab
.frames()
.await
.map_err(|e| map_error(McpServerError::from(e)))?;
frames
.into_iter()
.find(|f| f.id() == frame_id)
.ok_or_else(|| {
map_error(McpServerError::from(
zendriver::ZendriverError::FrameNotFound(frame_id.to_string()),
))
})
}
pub async fn page_snapshot(tab: &zendriver::Tab) -> Result<String, ErrorData> {
let html: String = tab
.evaluate_main("document.documentElement.outerHTML")
.await
.map_err(|e| map_error(McpServerError::from(e)))?;
Ok(html_trim::trim(&html))
}
#[derive(Debug, Clone, Copy, Deserialize, Serialize, JsonSchema, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum ModifierArg {
Alt,
Ctrl,
Meta,
Shift,
}
#[must_use]
pub fn modifiers_to_bits(mods: &[ModifierArg]) -> zendriver::KeyModifiers {
let mut bits = zendriver::KeyModifiers::empty();
for m in mods {
bits |= match m {
ModifierArg::Alt => zendriver::KeyModifiers::ALT,
ModifierArg::Ctrl => zendriver::KeyModifiers::CTRL,
ModifierArg::Meta => zendriver::KeyModifiers::META,
ModifierArg::Shift => zendriver::KeyModifiers::SHIFT,
};
}
bits
}
#[derive(Debug, Serialize, JsonSchema)]
pub struct BlobOutput {
#[serde(skip_serializing_if = "Option::is_none")]
pub saved_path: Option<String>,
pub byte_len: usize,
#[serde(skip_serializing_if = "Option::is_none")]
pub base64: Option<String>,
}
pub fn blob_output(bytes: &[u8], save_path: Option<String>) -> Result<BlobOutput, ErrorData> {
let byte_len = bytes.len();
if let Some(path) = save_path {
std::fs::write(&path, bytes)
.map_err(|e| map_error(McpServerError::from(zendriver::ZendriverError::from(e))))?;
return Ok(BlobOutput {
saved_path: Some(path),
byte_len,
base64: None,
});
}
if byte_len > MAX_INLINE_BYTES {
return Err(ErrorData::invalid_params(
format!(
"Blob is {byte_len} bytes, over the {MAX_INLINE_BYTES}-byte inline limit. Pass `save_path` to write it to disk on the MCP server host instead of inlining it."
),
None,
));
}
Ok(BlobOutput {
saved_path: None,
byte_len,
base64: Some(BASE64.encode(bytes)),
})
}