use serde::Deserialize;
use tauri::plugin::{Builder as PluginBuilder, TauriPlugin};
use tauri::{AppHandle, Listener, Manager, Wry};
mod bridge;
mod handlers;
mod mcp;
mod mcp_tools;
mod protocol;
mod server;
mod state;
use bridge::Bridge;
use server::Server;
use state::{DomEntry, LogEntry, PluginState};
const DEFAULT_BIND_ADDRESS: &str = "0.0.0.0";
const DEFAULT_PORT_RANGE: (u16, u16) = (9555, 9655);
const DEFAULT_MCP_PORT_RANGE: (u16, u16) = (9556, 9656);
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct PushDomPayload {
#[serde(default = "default_main")]
window_id: String,
html: String,
#[serde(default)]
text_content: String,
#[serde(default)]
accessibility_tree: String,
#[serde(default)]
structure_tree: String,
}
fn default_main() -> String {
"main".to_string()
}
#[tauri::command]
async fn push_dom(
app: AppHandle,
payload: PushDomPayload,
) -> Result<(), String> {
let state = app.state::<PluginState>();
let timestamp = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);
state
.push_dom(DomEntry {
window_id: payload.window_id,
html: payload.html,
text_content: payload.text_content,
accessibility_tree: payload.accessibility_tree,
structure_tree: payload.structure_tree,
timestamp,
})
.await;
Ok(())
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct PushLogsPayload {
entries: Vec<LogEntryPayload>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct LogEntryPayload {
level: String,
message: String,
timestamp: u64,
#[serde(default = "default_main")]
window_id: String,
}
#[tauri::command]
async fn push_logs(
app: AppHandle,
payload: PushLogsPayload,
) -> Result<(), String> {
let state = app.state::<PluginState>();
let entries: Vec<LogEntry> = payload
.entries
.into_iter()
.map(|e| LogEntry {
level: e.level,
message: e.message,
timestamp: e.timestamp,
window_id: e.window_id,
})
.collect();
state.push_logs(entries).await;
Ok(())
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct PointedElementPayload {
element: serde_json::Value,
}
#[tauri::command]
async fn set_pointed_element(
app: AppHandle,
payload: PointedElementPayload,
) -> Result<(), String> {
let state = app.state::<PluginState>();
state.set_pointed_element(payload.element).await;
Ok(())
}
pub struct ConnectorBuilder {
bind_address: String,
port_range: (u16, u16),
mcp_port_range: (u16, u16),
mcp_enabled: bool,
}
impl Default for ConnectorBuilder {
fn default() -> Self {
Self::new()
}
}
impl ConnectorBuilder {
pub fn new() -> Self {
Self {
bind_address: DEFAULT_BIND_ADDRESS.to_string(),
port_range: DEFAULT_PORT_RANGE,
mcp_port_range: DEFAULT_MCP_PORT_RANGE,
mcp_enabled: true,
}
}
pub fn bind_address(self, addr: &str) -> Self {
Self {
bind_address: addr.to_string(),
..self
}
}
pub fn port_range(self, start: u16, end: u16) -> Self {
Self {
port_range: (start, end),
..self
}
}
pub fn mcp_port_range(self, start: u16, end: u16) -> Self {
Self {
mcp_port_range: (start, end),
..self
}
}
pub fn disable_mcp(self) -> Self {
Self {
mcp_enabled: false,
..self
}
}
pub fn build(self) -> TauriPlugin<Wry> {
let bind_address = self.bind_address;
let port_range = self.port_range;
let mcp_port_range = self.mcp_port_range;
let mcp_enabled = self.mcp_enabled;
PluginBuilder::<Wry>::new("connector")
.invoke_handler(tauri::generate_handler![
push_dom,
push_logs,
set_pointed_element,
])
.setup(move |app, _api| {
let plugin_state = PluginState::default();
app.manage(plugin_state.clone());
let handle = app.clone();
tauri::async_runtime::spawn(async move {
let bridge = match Bridge::start() {
Ok(b) => b,
Err(e) => {
eprintln!("[connector] Failed to start bridge: {e}");
return;
}
};
bridge.set_app_handle(handle.clone()).await;
let init_script = bridge::bridge_init_script(bridge.port());
for (_label, window) in handle.webview_windows() {
if let Err(e) = window.eval(&init_script) {
eprintln!("[connector] Failed to inject bridge script: {e}");
}
}
let bridge_port = bridge.port();
let handle_for_event = handle.clone();
handle.listen("tauri://webview-created", move |_event| {
let script = bridge::bridge_init_script(bridge_port);
for (_label, window) in handle_for_event.webview_windows() {
let _ = window.eval(&script);
}
});
let app_handle = std::sync::Arc::new(tokio::sync::Mutex::new(
Some(handle.clone()),
));
if mcp_enabled {
match mcp::start(
&bind_address,
mcp_port_range,
bridge.clone(),
plugin_state.clone(),
app_handle.clone(),
)
.await
{
Ok(mcp_port) => {
let config = handle.config();
println!(
"[connector][mcp] MCP ready for '{}' — url: http://{}:{}/sse",
config.product_name.clone().unwrap_or_default(),
bind_address,
mcp_port,
);
}
Err(e) => {
eprintln!("[connector][mcp] Failed to start MCP server: {e}");
}
}
}
let server =
match Server::new(&bind_address, port_range, bridge, plugin_state) {
Ok(s) => s,
Err(e) => {
eprintln!("[connector] Failed to create WS server: {e}");
return;
}
};
let config = handle.config();
println!(
"[connector] Plugin ready for '{}' ({}) — WS on {}:{}",
config.product_name.clone().unwrap_or_default(),
config.identifier,
bind_address,
server.port()
);
server.set_app_handle(handle);
if let Err(e) = server.run(bind_address).await {
eprintln!("[connector] Server error: {e}");
}
});
Ok(())
})
.build()
}
}
pub fn init() -> TauriPlugin<Wry> {
ConnectorBuilder::new().build()
}