use kodegen_mcp_schema::browser::{
BrowserNavigateArgs, BrowserNavigateOutput, BROWSER_NAVIGATE,
NavigatePrompts,
};
use kodegen_mcp_schema::{Tool, ToolExecutionContext, ToolResponse, McpError};
use std::sync::Arc;
use crate::manager::BrowserManager;
use crate::utils::validate_navigation_timeout;
#[derive(Debug, Clone)]
pub(crate) struct NavigationResult {
pub success: bool,
pub url: String,
pub requested_url: String,
pub redirected: bool,
pub message: String,
}
#[derive(Clone)]
pub struct BrowserNavigateTool {
manager: Arc<BrowserManager>,
}
impl BrowserNavigateTool {
pub fn new(manager: Arc<BrowserManager>) -> Self {
Self { manager }
}
pub(crate) async fn navigate_and_capture_page(
&self,
args: BrowserNavigateArgs,
) -> Result<(chromiumoxide::Page, NavigationResult), McpError> {
if !args.url.starts_with("http://") && !args.url.starts_with("https://") {
return Err(McpError::invalid_arguments(
"URL must start with http:// or https://",
));
}
let browser_arc = self
.manager
.get_or_launch()
.await
.map_err(|e| McpError::Other(anyhow::anyhow!("Browser error: {}", e)))?;
let browser_guard = browser_arc.lock().await;
let wrapper = browser_guard.as_ref().ok_or_else(|| {
McpError::Other(anyhow::anyhow!(
"Browser not available. This is an internal error - please report it."
))
})?;
let page = match crate::browser::get_current_page(wrapper).await {
Ok(existing_page) => existing_page,
Err(_) => {
crate::browser::create_blank_page(wrapper)
.await
.map_err(McpError::Other)?
}
};
let timeout = validate_navigation_timeout(args.timeout_ms, 30000)?;
tokio::time::timeout(timeout, page.goto(&args.url))
.await
.map_err(|_| {
McpError::Other(anyhow::anyhow!(
"Navigation timeout after {}ms for URL: {}. \
Try: (1) Increase timeout_ms parameter (default: 30000), \
(2) Verify URL is accessible in a browser, \
(3) Check if site blocks headless browsers.",
timeout.as_millis(),
args.url
))
})?
.map_err(|e| {
McpError::Other(anyhow::anyhow!(
"Navigation failed for URL: {}. \
Check: (1) URL is correctly formatted, \
(2) Network connectivity, \
(3) URL returns a valid HTTP response. \
Error: {}",
args.url,
e
))
})?;
page.wait_for_navigation()
.await
.map_err(|e| {
McpError::Other(anyhow::anyhow!(
"Failed to wait for page load completion: {}",
e
))
})?;
if let Some(selector) = &args.wait_for_selector {
crate::utils::wait_for_element(&page, selector, timeout).await?;
}
let final_url = page
.url()
.await
.map_err(|e| McpError::Other(anyhow::anyhow!("Failed to get URL: {}", e)))?
.unwrap_or_else(|| args.url.clone());
let result = NavigationResult {
success: true,
url: final_url.clone(),
requested_url: args.url.clone(),
redirected: final_url != args.url,
message: format!("Navigated to {}", final_url),
};
Ok((page, result))
}
}
impl Tool for BrowserNavigateTool {
type Args = BrowserNavigateArgs;
type Prompts = NavigatePrompts;
fn name() -> &'static str {
BROWSER_NAVIGATE
}
fn description() -> &'static str {
"Navigate to a URL in the browser. Opens the page and waits for load completion.\\n\\n\
Returns current URL after navigation (may differ from requested URL due to redirects).\\n\\n\
Example: browser_navigate({\\\"url\\\": \\\"https://www.rust-lang.org\\\"})\\n\
With selector wait: browser_navigate({\\\"url\\\": \\\"https://httpbin.org/html\\\", \\\"wait_for_selector\\\": \\\"body\\\"})"
}
fn read_only() -> bool {
false }
fn open_world() -> bool {
true }
async fn execute(&self, args: Self::Args, _ctx: ToolExecutionContext) -> Result<ToolResponse<BrowserNavigateOutput>, McpError> {
let timeout_ms = args.timeout_ms.unwrap_or(30000);
let (page, result) = self.navigate_and_capture_page(args).await?;
let final_url = result.url;
let redirected = result.redirected;
let requested_url = result.requested_url;
tracing::debug!("{}", result.message);
let summary = if redirected {
format!(
"\x1b[36mNavigate: {}\x1b[0m\n\
Redirected: {} → {} · Timeout: {}ms",
final_url,
requested_url,
final_url,
timeout_ms
)
} else {
format!(
"\x1b[36mNavigate: {}\x1b[0m\n\
Timeout: {}ms",
final_url,
timeout_ms
)
};
let output = BrowserNavigateOutput {
success: result.success,
url: final_url,
title: None,
status_code: None,
};
self.manager.set_current_page(page).await;
Ok(ToolResponse::new(summary, output))
}
}