voidcrawl_mcp/tools/
session.rs1use std::{env, sync::Arc, time::Duration};
7
8use rmcp::ErrorData;
9use schemars::JsonSchema;
10use serde::{Deserialize, Serialize};
11use tokio::sync::Mutex;
12use uuid::Uuid;
13use void_crawl_core::{BrowserSession, VoidCrawlError};
14
15use crate::{errors::map_err, server::VoidCrawlServer, sessions::DedicatedSession, tools::wait};
16
17pub const DEFAULT_TIMEOUT_SECS: u64 = 30;
18
19#[derive(Debug, Deserialize, JsonSchema, Default)]
20pub struct SessionOpenArgs {
21 #[serde(default)]
25 pub headful: bool,
26 #[serde(default)]
28 pub proxy: Option<String>,
29 #[serde(default)]
37 pub user_data_dir: Option<String>,
38}
39
40#[derive(Debug, Serialize, JsonSchema)]
41pub struct SessionOpenResult {
42 pub session_id: String,
43}
44
45#[derive(Debug, Deserialize, JsonSchema, Default)]
46pub struct SessionNavigateArgs {
47 pub session_id: String,
48 pub url: String,
49 #[serde(default)]
51 pub wait_for: Option<String>,
52 #[serde(default)]
53 pub timeout_secs: Option<u64>,
54}
55
56#[derive(Debug, Serialize, JsonSchema)]
57pub struct SessionNavigateResult {
58 pub url: String,
59 pub status_code: Option<u16>,
60 pub redirected: bool,
61}
62
63#[derive(Debug, Deserialize, JsonSchema, Default)]
64pub struct SessionIdArgs {
65 pub session_id: String,
66}
67
68#[derive(Debug, Serialize, JsonSchema)]
69pub struct SessionContentResult {
70 pub url: Option<String>,
71 pub title: Option<String>,
72 pub html: String,
73}
74
75#[derive(Debug, Serialize, JsonSchema)]
76pub struct SessionCloseResult {
77 pub closed: bool,
78}
79
80pub async fn open(
81 server: &VoidCrawlServer,
82 args: SessionOpenArgs,
83) -> Result<SessionOpenResult, ErrorData> {
84 let mut builder = BrowserSession::builder();
85 builder = if args.headful { builder.headful() } else { builder.headless() };
86 if let Some(proxy) = args.proxy {
87 builder = builder.proxy(proxy);
88 }
89 if let Some(path) = args.user_data_dir {
90 builder = builder.user_data_dir(expand_tilde(&path));
91 }
92 let session = builder.launch().await.map_err(map_err)?;
93 let page = session.new_blank_page().await.map_err(map_err)?;
94 let id = Uuid::new_v4().to_string();
95 let handle =
96 Arc::new(DedicatedSession { session: Arc::new(session), page: Mutex::new(page) });
97 server.state().sessions.insert(id.clone(), handle).await;
98 Ok(SessionOpenResult { session_id: id })
99}
100
101pub async fn navigate(
102 server: &VoidCrawlServer,
103 args: SessionNavigateArgs,
104) -> Result<SessionNavigateResult, ErrorData> {
105 let handle = lookup(server, &args.session_id).await?;
106 let page = handle.page.lock().await;
107 let timeout = Duration::from_secs(args.timeout_secs.unwrap_or(DEFAULT_TIMEOUT_SECS));
108 let resp = page.goto_and_wait_for_idle(&args.url, timeout).await.map_err(map_err)?;
109 wait::apply_post_navigate(&page, args.wait_for.as_deref(), timeout).await.map_err(map_err)?;
110 Ok(SessionNavigateResult {
111 url: resp.url,
112 status_code: resp.status_code,
113 redirected: resp.redirected,
114 })
115}
116
117pub async fn content(
118 server: &VoidCrawlServer,
119 args: SessionIdArgs,
120) -> Result<SessionContentResult, ErrorData> {
121 let handle = lookup(server, &args.session_id).await?;
122 let page = handle.page.lock().await;
123 let html = page.content().await.map_err(map_err)?;
124 let title = page.title().await.ok().flatten();
125 let url = page.url().await.ok().flatten();
126 Ok(SessionContentResult { url, title, html })
127}
128
129pub async fn close(
130 server: &VoidCrawlServer,
131 args: SessionIdArgs,
132) -> Result<SessionCloseResult, ErrorData> {
133 let Some(handle) = server.state().sessions.remove(&args.session_id).await else {
134 return Ok(SessionCloseResult { closed: false });
135 };
136 close_handle(handle).await.map_err(map_err)?;
137 Ok(SessionCloseResult { closed: true })
138}
139
140async fn lookup(server: &VoidCrawlServer, id: &str) -> Result<Arc<DedicatedSession>, ErrorData> {
141 server
142 .state()
143 .sessions
144 .get(id)
145 .await
146 .ok_or_else(|| ErrorData::invalid_params(format!("unknown session_id: {id}"), None))
147}
148
149pub async fn close_handle(handle: Arc<DedicatedSession>) -> Result<(), VoidCrawlError> {
151 handle.session.close().await
152}
153
154fn expand_tilde(path: &str) -> String {
159 let Some(rest) = path.strip_prefix('~') else { return path.to_owned() };
160 let Ok(home) = env::var("HOME") else { return path.to_owned() };
161 if rest.is_empty() {
162 home
163 } else if let Some(tail) = rest.strip_prefix('/') {
164 format!("{home}/{tail}")
165 } else {
166 path.to_owned()
167 }
168}