use std::sync::Arc;
use rmcp::ErrorData;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use tokio::sync::Mutex;
use zendriver::ClickOptions;
use crate::errors::{McpServerError, map_error};
use crate::state::SessionState;
use crate::tools::actions::{ActionOutput, MouseButtonArg};
use crate::tools::common::{ModifierArg, current_tab, modifiers_to_bits, page_snapshot};
#[derive(Debug, Clone, Copy, Deserialize, Serialize, JsonSchema, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum MouseAction {
Move,
Click,
Drag,
}
#[derive(Debug, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct MouseInput {
pub action: MouseAction,
pub x: f64,
pub y: f64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub to_x: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub to_y: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub button: Option<MouseButtonArg>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub click_count: Option<u32>,
#[serde(default)]
pub modifiers: Vec<ModifierArg>,
#[serde(default = "default_steps")]
pub steps: usize,
#[serde(default)]
pub return_snapshot: bool,
}
fn default_steps() -> usize {
20
}
pub async fn mouse(
state: Arc<Mutex<SessionState>>,
input: MouseInput,
) -> Result<ActionOutput, ErrorData> {
let s = state.lock().await;
let tab = current_tab(&s).await?;
match input.action {
MouseAction::Move => tab
.mouse_move(input.x, input.y)
.await
.map_err(|e| map_error(McpServerError::from(e)))?,
MouseAction::Click => {
let opts = ClickOptions {
button: input.button.unwrap_or_default().into(),
click_count: input.click_count.unwrap_or(1),
modifiers: modifiers_to_bits(&input.modifiers),
..Default::default()
};
tab.mouse_click_with(input.x, input.y, opts)
.await
.map_err(|e| map_error(McpServerError::from(e)))?;
}
MouseAction::Drag => {
let to_x = input.to_x.ok_or_else(|| {
ErrorData::invalid_params(
"`to_x` and `to_y` are required for action `drag`".to_string(),
None,
)
})?;
let to_y = input.to_y.ok_or_else(|| {
ErrorData::invalid_params(
"`to_x` and `to_y` are required for action `drag`".to_string(),
None,
)
})?;
tab.mouse_drag((input.x, input.y), (to_x, to_y), input.steps)
.await
.map_err(|e| map_error(McpServerError::from(e)))?;
}
}
let snapshot = if input.return_snapshot {
Some(page_snapshot(&tab).await?)
} else {
None
};
Ok(ActionOutput { ok: true, snapshot })
}
#[cfg(test)]
#[allow(clippy::panic, clippy::unwrap_used)]
mod tests {
use super::*;
#[tokio::test]
async fn mouse_with_no_browser_errors() {
let state = Arc::new(Mutex::new(SessionState::new()));
let err = mouse(
state,
MouseInput {
action: MouseAction::Move,
x: 10.0,
y: 10.0,
to_x: None,
to_y: None,
button: None,
click_count: None,
modifiers: Vec::new(),
steps: 20,
return_snapshot: false,
},
)
.await
.expect_err("expected BrowserNotOpen");
assert!(err.message.contains("Browser not open"));
}
}