use anyhow::{Context, Result};
use serde_json::json;
use tracing::info;
use kodegen_config::{BROWSER_AGENT, BROWSER_CLICK, BROWSER_EXTRACT_TEXT, BROWSER_NAVIGATE, BROWSER_SCREENSHOT, BROWSER_SCROLL, BROWSER_TYPE_TEXT};
mod common;
#[tokio::main]
async fn main() -> Result<()> {
tracing_subscriber::fmt().with_env_filter("info").init();
info!("🌐 Browser Tools Comprehensive Demo\n");
info!("Demonstrating all 9 public browser tools\n");
let (conn, mut server) = common::connect_to_local_http_server().await?;
let workspace_root = common::find_workspace_root()
.context("Failed to find workspace root")?;
let log_path = workspace_root.join("tmp/mcp-client/browser.log");
let client = common::LoggingClient::new(conn.client(), log_path)
.await
.context("Failed to create logging client")?;
let result = run_all_workflows(&client).await;
conn.close().await?;
server.shutdown().await?;
result
}
async fn run_all_workflows(client: &common::LoggingClient) -> Result<()> {
info!("\n╔══════════════════════════════════════════════════════════╗");
info!("║ Workflow 1: docs.rs Search ║");
info!("║ Tools: navigate, click, type_text, extract_text, ║");
info!("║ scroll, screenshot ║");
info!("╚══════════════════════════════════════════════════════════╝\n");
info!("1️⃣ browser_navigate → docs.rs");
client
.call_tool(
BROWSER_NAVIGATE,
json!({
"url": "https://docs.rs",
"wait_for_selector": "input[name=\"query\"]"
}),
)
.await?;
info!(" ✓ Navigated to docs.rs\n");
info!("2️⃣ browser_type_text → \"async\"");
client
.call_tool(
BROWSER_TYPE_TEXT,
json!({
"selector": "input[name=\"query\"]",
"text": "async"
}),
)
.await?;
info!(" ✓ Typed search query\n");
info!("3️⃣ browser_click → Submit button");
client
.call_tool(
BROWSER_CLICK,
json!({
"selector": "button[type=\"submit\"]",
"wait_for_navigation": true
}),
)
.await?;
info!(" ✓ Submitted search\n");
info!("4️⃣ browser_extract_text → Search results");
let result = client.call_tool(BROWSER_EXTRACT_TEXT, json!({})).await?;
let response: Option<serde_json::Value> = result.content.iter().rev().find_map(|c| {
c.as_text()
.and_then(|t| serde_json::from_str(&t.text).ok())
});
if let Some(response) = response {
let extracted = response.get("text").and_then(|v| v.as_str()).unwrap_or("");
let preview = if extracted.len() > 200 {
format!("{}...", &extracted[..200])
} else {
extracted.to_string()
};
info!(" ✓ Extracted {} chars", extracted.len());
info!(" Preview: {}\n", preview);
}
info!("5️⃣ browser_scroll → Scroll down 500px");
client
.call_tool(
BROWSER_SCROLL,
json!({
"y": 500
}),
)
.await?;
info!(" ✓ Scrolled down\n");
info!("6️⃣ browser_screenshot → Capture results");
let result = client
.call_tool(
BROWSER_SCREENSHOT,
json!({
"format": "png"
}),
)
.await?;
let response: Option<serde_json::Value> = result.content.iter().rev().find_map(|c| {
c.as_text()
.and_then(|t| serde_json::from_str(&t.text).ok())
});
if let Some(response) = response {
let size = response
.get("size_bytes")
.and_then(|v| v.as_u64())
.unwrap_or(0);
info!(" ✓ Screenshot: {} bytes\n", size);
}
info!("\n╔══════════════════════════════════════════════════════════╗");
info!("║ Workflow 2: AI-Powered Deep Research ║");
info!("║ Tool: browser_research (action: RESEARCH/READ/LIST/KILL)║");
info!("╚══════════════════════════════════════════════════════════╝\n");
info!("8️⃣ browser_research RESEARCH → \"Rust async programming best practices\"");
info!(" (Starts background research session)\n");
let start_result = client
.call_tool(
"browser_research",
json!({
"action": "RESEARCH",
"query": "Rust async programming best practices",
"max_pages": 3,
"session": 0,
"await_completion_ms": 0 }),
)
.await?;
if let Some(content) = start_result.content.first()
&& let Some(text) = content.as_text()
{
let response: serde_json::Value = serde_json::from_str(&text.text)?;
let session = response.get("session").and_then(|v| v.as_u64()).unwrap_or(0);
let status = response.get("status").and_then(|v| v.as_str()).unwrap_or("unknown");
info!(" ✓ Research session {} started (status: {})", session, status);
}
info!(" ⏳ Polling for completion...\n");
let mut poll_count = 0;
loop {
tokio::time::sleep(tokio::time::Duration::from_secs(5)).await;
poll_count += 1;
let status_result = client
.call_tool(
"browser_research",
json!({
"action": "READ",
"session": 0
}),
)
.await?;
if let Some(content) = status_result.content.first()
&& let Some(text) = content.as_text()
{
let response: serde_json::Value = serde_json::from_str(&text.text)?;
let status = response.get("status").and_then(|v| v.as_str()).unwrap_or("unknown");
let pages = response.get("pages_analyzed").and_then(|v| v.as_u64()).unwrap_or(0);
let completed = response.get("completed").and_then(|v| v.as_bool()).unwrap_or(false);
info!(" [{:02}] Status: {} | Pages: {} ({}s elapsed)",
poll_count, status, pages, poll_count * 5);
if completed {
info!(" ✓ Research complete!\n");
if let Some(summary) = response.get("summary").and_then(|v| v.as_str()) {
info!(" ✓ AI Research Summary:");
let preview = if summary.len() > 500 {
format!("{}...", &summary[..500])
} else {
summary.to_string()
};
info!("\n{}\n", preview);
}
if let Some(sources) = response.get("sources").and_then(|v| v.as_array()) {
info!(" 📚 Sources ({} pages):", sources.len());
for (i, source) in sources.iter().enumerate().take(5) {
let url = source.get("url").and_then(|v| v.as_str()).unwrap_or("Unknown");
let title = source.get("title").and_then(|v| v.as_str()).unwrap_or("Untitled");
info!(" {}. {} - {}", i + 1, title, url);
}
}
break;
}
if let Some(error) = response.get("error").and_then(|v| v.as_str()) {
return Err(anyhow::anyhow!("Research failed: {}", error));
}
}
if poll_count >= 60 {
return Err(anyhow::anyhow!("Research session timed out after 5 minutes"));
}
}
info!("");
info!("\n╔══════════════════════════════════════════════════════════╗");
info!("║ Workflow 3: Autonomous AI Agent ║");
info!("║ Tool: browser_agent ║");
info!("╚══════════════════════════════════════════════════════════╝\n");
info!("9️⃣ browser_agent → Compare axum vs actix-web");
info!(" (AI autonomously navigates and extracts data)\n");
let result = client
.call_tool(
BROWSER_AGENT,
json!({
"action": "PROMPT",
"task": "Compare axum vs actix-web crates on crates.io - find downloads, latest version, and key features for each",
"start_url": "https://crates.io",
"max_steps": 10,
"temperature": 0.3
}),
)
.await?;
info!(" DEBUG: Response has {} content items", result.content.len());
for (i, content) in result.content.iter().enumerate() {
if let Some(text) = content.as_text() {
let preview = if text.text.len() > 200 {
format!("{}...", &text.text[..200])
} else {
text.text.clone()
};
info!(" DEBUG: content[{}] = {}", i, preview);
} else {
info!(" DEBUG: content[{}] = <non-text>", i);
}
}
let response: Option<serde_json::Value> = result.content.iter().rev().find_map(|c| {
c.as_text()
.and_then(|t| serde_json::from_str(&t.text).ok())
});
if response.is_none() {
return Err(anyhow::anyhow!("No JSON metadata found in response"));
}
let response = response.unwrap();
let completed = response.get("completed").and_then(|v| v.as_bool()).unwrap_or(false);
let steps_taken = response.get("steps_taken").and_then(|v| v.as_u64()).unwrap_or(0);
info!(" {} Agent completed in {} steps",
if completed { "✓" } else { "⚠" },
steps_taken
);
if let Some(summary) = response.get("summary").and_then(|v| v.as_str()) {
info!("\n Result:\n{}\n", summary);
}
if let Some(history) = response.get("history").and_then(|v| v.as_array()) {
info!(" History:");
for entry in history {
if let Some(step) = entry.get("step").and_then(|v| v.as_u64())
&& let Some(step_summary) = entry.get("summary").and_then(|v| v.as_str())
{
info!(" Step {}: {}", step, step_summary);
}
}
}
info!("");
info!("\n╔══════════════════════════════════════════════════════════╗");
info!("║ ✅ All 8 Browser Tools Demonstrated ║");
info!("╠══════════════════════════════════════════════════════════╣");
info!("║ Core Automation (6 tools): ║");
info!("║ ✓ browser_navigate ✓ browser_click ║");
info!("║ ✓ browser_type_text ✓ browser_extract_text ║");
info!("║ ✓ browser_scroll ✓ browser_screenshot ║");
info!("║ ║");
info!("║ Advanced Tools (2 tools): ║");
info!("║ ✓ browser_research ✓ browser_agent ║");
info!("╚══════════════════════════════════════════════════════════╝\n");
Ok(())
}