use std::collections::HashSet;
use std::io::{BufRead, Write};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex};
use std::time::Duration;
use anyhow::{Result, bail};
use serde_json::{Value, json};
const MAX_RETRIES: usize = 4;
const RETRY_DELAY_MS: u64 = 400;
const POLL_INTERVAL_MS: u64 = 1500;
const POLL_INTERVAL_UP_MS: u64 = 5000;
const DEFAULT_PROTOCOL_VERSION: &str = "2025-06-18";
static TOOLS_FALLBACK_JSON: &str = include_str!("tools_fallback.json");
const LOCAL_INIT_INSTRUCTIONS: &str = "Victauri MCP bridge. Tools act on a running Tauri app \
(debug build). While no app is running the tool list is a static fallback and tool calls \
report the backend as unreachable; start the app and the bridge connects automatically — \
the tool list refreshes to the live set with no reconnect.";
#[derive(Clone, Debug)]
struct ServerInfo {
port: u16,
token: Option<String>,
identifier: Option<String>,
product_name: Option<String>,
}
impl ServerInfo {
fn label(&self) -> String {
let name = self
.identifier
.as_deref()
.or(self.product_name.as_deref())
.unwrap_or("<unknown app>");
format!("{name} (port {})", self.port)
}
}
pub async fn run(wait: bool, app: Option<String>) -> Result<()> {
let _ = wait;
let app = app.or_else(|| std::env::var("VICTAURI_APP").ok());
let http = build_client()?;
let connection: Arc<Mutex<Option<ServerInfo>>> = Arc::new(Mutex::new(None));
let session_id: Arc<Mutex<Option<String>>> = Arc::new(Mutex::new(None));
let cached_init: Arc<Mutex<Option<Value>>> = Arc::new(Mutex::new(None));
let stateless: Arc<Mutex<bool>> = Arc::new(Mutex::new(false));
let backend_up = Arc::new(AtomicBool::new(false));
let client_ready = Arc::new(AtomicBool::new(false));
let stdout = Arc::new(Mutex::new(std::io::stdout()));
spawn_availability_poller(
app.clone(),
Arc::clone(&connection),
Arc::clone(&session_id),
Arc::clone(&stateless),
Arc::clone(&backend_up),
Arc::clone(&client_ready),
Arc::clone(&stdout),
);
let (line_tx, mut line_rx) = tokio::sync::mpsc::unbounded_channel::<String>();
std::thread::spawn(move || {
let stdin = std::io::stdin();
for line in stdin.lock().lines() {
let Ok(line) = line else { break };
if line_tx.send(line).is_err() {
break; }
}
});
while let Some(line) = line_rx.recv().await {
let trimmed = line.trim();
if trimmed.is_empty() {
continue;
}
let msg: Value = match serde_json::from_str(trimmed) {
Ok(v) => v,
Err(e) => {
eprintln!("victauri-bridge: invalid JSON on stdin: {e}");
continue;
}
};
let method = msg.get("method").and_then(|m| m.as_str()).unwrap_or("");
let id = msg.get("id").cloned().unwrap_or(Value::Null);
let is_notification = msg.get("id").is_none();
match method {
"initialize" => {
*locked(&cached_init) = Some(msg.clone());
write_value(&stdout, &local_initialize_response(&msg));
}
"notifications/initialized" => {
client_ready.store(true, Ordering::Release);
if backend_up.load(Ordering::Acquire) {
write_notification(&stdout, "notifications/tools/list_changed");
write_notification(&stdout, "notifications/resources/list_changed");
}
}
"ping" => {
write_value(&stdout, &json!({"jsonrpc": "2.0", "id": id, "result": {}}));
}
"tools/list" => {
match forward_when_up(
&http,
&connection,
&session_id,
&stateless,
&cached_init,
app.as_deref(),
&msg,
)
.await
{
Some(payloads) => write_payloads(&stdout, &payloads),
None => write_value(&stdout, &fallback_tools_response(&id)),
}
}
"resources/list" | "resources/templates/list" | "prompts/list" => {
match forward_when_up(
&http,
&connection,
&session_id,
&stateless,
&cached_init,
app.as_deref(),
&msg,
)
.await
{
Some(payloads) => write_payloads(&stdout, &payloads),
None => write_value(&stdout, &empty_list_response(method, &id)),
}
}
_ => {
match forward_with_retries(
&http,
&connection,
&session_id,
&stateless,
&cached_init,
app.as_deref(),
&msg,
)
.await
{
ForwardResult::Payloads(payloads) => write_payloads(&stdout, &payloads),
ForwardResult::Accepted => {}
ForwardResult::Unreachable(err_msg) => {
if !is_notification {
write_value(
&stdout,
&json!({
"jsonrpc": "2.0",
"id": id,
"error": { "code": -32000, "message": err_msg }
}),
);
}
}
}
}
}
}
Ok(())
}
fn local_initialize_response(client_msg: &Value) -> Value {
let id = client_msg.get("id").cloned().unwrap_or(Value::Null);
let protocol_version = client_msg
.get("params")
.and_then(|p| p.get("protocolVersion"))
.and_then(|v| v.as_str())
.unwrap_or(DEFAULT_PROTOCOL_VERSION)
.to_string();
json!({
"jsonrpc": "2.0",
"id": id,
"result": {
"protocolVersion": protocol_version,
"capabilities": {
"tools": { "listChanged": true },
"resources": { "listChanged": true }
},
"serverInfo": { "name": "victauri-bridge", "version": env!("CARGO_PKG_VERSION") },
"instructions": LOCAL_INIT_INSTRUCTIONS
}
})
}
fn fallback_tools() -> Vec<Value> {
let parsed: Vec<Value> = serde_json::from_str(TOOLS_FALLBACK_JSON).unwrap_or_default();
parsed
.into_iter()
.filter_map(|t| {
let name = t.get("name")?.as_str()?.to_string();
let description = t
.get("description")
.and_then(|d| d.as_str())
.unwrap_or_default()
.to_string();
Some(json!({
"name": name,
"description": description,
"inputSchema": { "type": "object" }
}))
})
.collect()
}
fn fallback_tools_response(id: &Value) -> Value {
json!({ "jsonrpc": "2.0", "id": id, "result": { "tools": fallback_tools() } })
}
fn empty_list_response(method: &str, id: &Value) -> Value {
let key = match method {
"resources/list" => "resources",
"resources/templates/list" => "resourceTemplates",
"prompts/list" => "prompts",
_ => "items",
};
json!({ "jsonrpc": "2.0", "id": id, "result": { key: [] } })
}
fn locked<T>(m: &Mutex<T>) -> std::sync::MutexGuard<'_, T> {
m.lock().unwrap_or_else(std::sync::PoisonError::into_inner)
}
fn write_value(stdout: &Arc<Mutex<std::io::Stdout>>, v: &Value) {
let mut o = locked(stdout);
let _ = writeln!(o, "{v}");
let _ = o.flush();
}
fn write_payloads(stdout: &Arc<Mutex<std::io::Stdout>>, payloads: &[String]) {
let mut o = locked(stdout);
for payload in payloads {
let _ = writeln!(o, "{payload}");
}
let _ = o.flush();
}
fn write_notification(stdout: &Arc<Mutex<std::io::Stdout>>, method: &str) {
write_value(stdout, &json!({ "jsonrpc": "2.0", "method": method }));
}
fn error_for_request(msg: &Value, code: i64, message: &str) -> String {
json!({
"jsonrpc": "2.0",
"id": msg.get("id"),
"error": { "code": code, "message": message }
})
.to_string()
}
fn unreachable_message() -> String {
"Victauri backend not reachable: no running Tauri app with the Victauri plugin (debug \
build) was found. Start the app (e.g. `npm run tauri dev` / `pnpm tauri dev`); the bridge \
connects automatically when it comes up — no reconnect needed. If several Victauri apps \
run, select one with `--app <bundle-identifier>` or the VICTAURI_APP env var."
.to_string()
}
#[allow(clippy::too_many_arguments)]
fn spawn_availability_poller(
app: Option<String>,
connection: Arc<Mutex<Option<ServerInfo>>>,
session_id: Arc<Mutex<Option<String>>>,
stateless: Arc<Mutex<bool>>,
backend_up: Arc<AtomicBool>,
client_ready: Arc<AtomicBool>,
stdout: Arc<Mutex<std::io::Stdout>>,
) {
tokio::spawn(async move {
loop {
let interval = if backend_up.load(Ordering::Acquire) {
POLL_INTERVAL_UP_MS
} else {
POLL_INTERVAL_MS
};
tokio::time::sleep(Duration::from_millis(interval)).await;
let found = discover_one(app.as_deref()).await;
let up = found.is_some();
if let Some(info) = found {
*locked(&connection) = Some(info);
} else {
*locked(&connection) = None;
*locked(&session_id) = None;
*locked(&stateless) = false;
}
let was = backend_up.swap(up, Ordering::AcqRel);
if up && !was && client_ready.load(Ordering::Acquire) {
write_notification(&stdout, "notifications/tools/list_changed");
write_notification(&stdout, "notifications/resources/list_changed");
}
}
});
}
enum ForwardResult {
Payloads(Vec<String>),
Accepted,
Unreachable(String),
}
#[allow(clippy::too_many_arguments)]
async fn forward_when_up(
http: &reqwest::Client,
connection: &Arc<Mutex<Option<ServerInfo>>>,
session_id: &Arc<Mutex<Option<String>>>,
stateless: &Arc<Mutex<bool>>,
cached_init: &Arc<Mutex<Option<Value>>>,
app: Option<&str>,
msg: &Value,
) -> Option<Vec<String>> {
match forward_with_retries(
http,
connection,
session_id,
stateless,
cached_init,
app,
msg,
)
.await
{
ForwardResult::Payloads(payloads) => Some(payloads),
_ => None,
}
}
async fn forward_with_retries(
http: &reqwest::Client,
connection: &Arc<Mutex<Option<ServerInfo>>>,
session_id: &Arc<Mutex<Option<String>>>,
stateless: &Arc<Mutex<bool>>,
cached_init: &Arc<Mutex<Option<Value>>>,
app: Option<&str>,
msg: &Value,
) -> ForwardResult {
let is_notification = msg.get("id").is_none();
match scan_once(app).await {
Selection::One(info) => {
*locked(connection) = Some(info);
}
Selection::Ambiguous(labels) => {
*locked(connection) = None;
return ForwardResult::Unreachable(format!(
"Multiple Victauri apps are running:\n {}\nSelect one with \
`--app <bundle-identifier>` or the VICTAURI_APP env var.",
labels.join("\n ")
));
}
Selection::None => {
*locked(connection) = None;
return ForwardResult::Unreachable(unreachable_message());
}
}
let mut last_err = None;
for attempt in 0..MAX_RETRIES {
if !*locked(stateless) {
let need_reinit = locked(session_id).is_none();
if need_reinit {
let init = locked(cached_init).clone();
if let Some(init) = init
&& let Some((port, token)) = conn_parts(connection)
{
if let Ok(out) = post_message(http, port, token.as_deref(), None, &init).await {
let backend_sid = out.session_id.clone();
if let Some(sid) = out.session_id {
*locked(session_id) = Some(sid);
} else if !out.stale_session {
*locked(stateless) = true;
}
let note = json!({"jsonrpc": "2.0", "method": "notifications/initialized"});
let _ = post_message(
http,
port,
token.as_deref(),
backend_sid.as_deref(),
¬e,
)
.await;
}
}
}
}
let Some((port, token)) = conn_parts(connection) else {
return ForwardResult::Unreachable(unreachable_message());
};
let sid = locked(session_id).clone();
match post_message(http, port, token.as_deref(), sid.as_deref(), msg).await {
Ok(out) => {
if let Some(new_sid) = out.session_id {
*locked(session_id) = Some(new_sid);
}
if out.stale_session {
eprintln!(
"victauri-bridge: stale session (HTTP {}), re-establishing (attempt {}/{})",
out.status,
attempt + 1,
MAX_RETRIES
);
*locked(session_id) = None;
if attempt + 1 < MAX_RETRIES {
tokio::time::sleep(Duration::from_millis(RETRY_DELAY_MS)).await;
if let Ok(new_conn) = discover_and_select(false, app).await {
*locked(connection) = Some(new_conn);
}
}
last_err = Some(format!("Victauri returned {}", out.status));
continue;
}
if out.accepted {
if is_notification {
return ForwardResult::Accepted;
}
return ForwardResult::Payloads(vec![error_for_request(
msg,
-32603,
"backend accepted the request with no response (HTTP 202)",
)]);
}
if !is_notification && out.payloads.is_empty() {
return ForwardResult::Payloads(vec![error_for_request(
msg,
-32603,
"backend returned an empty or non-JSON response",
)]);
}
return ForwardResult::Payloads(out.payloads);
}
Err(e) => {
eprintln!(
"victauri-bridge: connection failed (attempt {}/{}): {e}",
attempt + 1,
MAX_RETRIES
);
*locked(session_id) = None;
if attempt + 1 < MAX_RETRIES {
tokio::time::sleep(Duration::from_millis(
RETRY_DELAY_MS * (attempt as u64 + 1),
))
.await;
match discover_and_select(false, app).await {
Ok(new_conn) => {
eprintln!("victauri-bridge: reconnected to {}", new_conn.label());
*locked(connection) = Some(new_conn);
}
Err(_) => {
*locked(connection) = None;
}
}
}
last_err = Some(format!("Victauri server unreachable ({e})"));
continue;
}
}
}
let _ = last_err;
ForwardResult::Unreachable(unreachable_message())
}
fn build_client() -> Result<reqwest::Client> {
reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(120))
.connect_timeout(std::time::Duration::from_secs(10))
.build()
.map_err(Into::into)
}
fn conn_parts(connection: &Arc<Mutex<Option<ServerInfo>>>) -> Option<(u16, Option<String>)> {
locked(connection)
.as_ref()
.map(|s| (s.port, s.token.clone()))
}
struct PostOutcome {
status: u16,
session_id: Option<String>,
stale_session: bool,
accepted: bool,
payloads: Vec<String>,
}
async fn post_message(
http: &reqwest::Client,
port: u16,
token: Option<&str>,
session_id: Option<&str>,
msg: &serde_json::Value,
) -> Result<PostOutcome> {
let url = format!("http://127.0.0.1:{port}/mcp");
let mut req = http
.post(&url)
.header("Content-Type", "application/json")
.header("Accept", "application/json, text/event-stream");
if let Some(t) = token {
req = req.header("Authorization", format!("Bearer {t}"));
}
if let Some(sid) = session_id {
req = req.header("Mcp-Session-Id", sid);
}
let resp = req.json(msg).send().await?;
let status = resp.status().as_u16();
let new_sid = resp
.headers()
.get("mcp-session-id")
.and_then(|v| v.to_str().ok())
.map(String::from);
let stale_session = matches!(status, 404 | 409 | 422);
let accepted = status == 202;
let mut payloads = Vec::new();
if !stale_session && status != 202 {
let content_type = resp
.headers()
.get("content-type")
.and_then(|v| v.to_str().ok())
.unwrap_or("")
.to_string();
let body = resp.text().await.unwrap_or_default();
if !(200..300).contains(&status) {
payloads.push(
serde_json::json!({
"jsonrpc": "2.0",
"id": msg.get("id"),
"error": { "code": -32000, "message": format!("Victauri returned {status}: {body}") }
})
.to_string(),
);
} else if content_type.contains("text/event-stream") {
for sse_line in body.lines() {
if let Some(data) = sse_line.strip_prefix("data: ") {
let data = data.trim();
if !data.is_empty() && serde_json::from_str::<serde_json::Value>(data).is_ok() {
payloads.push(data.to_string());
}
}
}
} else {
let body = body.trim();
if let Ok(parsed) = serde_json::from_str::<serde_json::Value>(body) {
payloads.push(parsed.to_string());
}
}
}
Ok(PostOutcome {
status,
session_id: new_sid,
stale_session,
accepted,
payloads,
})
}
async fn scan_once(app: Option<&str>) -> Selection {
if let Ok(p) = std::env::var("VICTAURI_PORT")
&& let Ok(port) = p.parse::<u16>()
&& health_ok(port).await
{
return Selection::One(ServerInfo {
port,
token: normalize_env_token(std::env::var("VICTAURI_AUTH_TOKEN").ok())
.or_else(|| discover_token_for_port(port)),
identifier: None,
product_name: None,
});
}
let alive = alive_pids();
let is_alive = |pid: u32| {
alive
.as_ref()
.map_or_else(|| is_process_alive(pid), |set| set.contains(&pid))
};
let mut live = Vec::new();
for (pid, s) in discover_entries() {
if is_alive(pid) && health_ok(s.port).await {
live.push(s);
}
}
select(&live, app)
}
async fn discover_one(app: Option<&str>) -> Option<ServerInfo> {
match scan_once(app).await {
Selection::One(s) => Some(s),
_ => None,
}
}
async fn discover_and_select(wait: bool, app: Option<&str>) -> Result<ServerInfo> {
let max_attempts = if wait { 30 } else { 3 };
let delay = Duration::from_secs(1);
for attempt in 0..max_attempts {
match scan_once(app).await {
Selection::One(s) => {
eprintln!("victauri-bridge: connected to {}", s.label());
return Ok(s);
}
Selection::None if attempt + 1 < max_attempts => {
if attempt == 0 {
eprintln!("victauri-bridge: waiting for Victauri server...");
}
tokio::time::sleep(delay).await;
}
Selection::None => {
bail!(
"Could not connect to Victauri server.\n\
Is your Tauri app running (debug build)? Start it with: pnpm run tauri dev"
);
}
Selection::Ambiguous(labels) => {
bail!(
"Multiple Victauri apps are running:\n {}\n\
Specify which one with `victauri bridge --app <identifier>` (or set \
VICTAURI_APP). The identifier is your Tauri bundle identifier.",
labels.join("\n ")
);
}
}
}
bail!("Could not connect to a matching Victauri server")
}
enum Selection {
One(ServerInfo),
None,
Ambiguous(Vec<String>),
}
fn select(live: &[ServerInfo], app: Option<&str>) -> Selection {
if live.is_empty() {
return Selection::None;
}
if let Some(app) = app {
let needle = app.to_ascii_lowercase();
let exact = live.iter().find(|s| {
s.identifier
.as_deref()
.map(str::to_ascii_lowercase)
.as_deref()
== Some(&needle)
|| s.product_name
.as_deref()
.map(str::to_ascii_lowercase)
.as_deref()
== Some(&needle)
});
if let Some(s) = exact {
return Selection::One(s.clone());
}
let partial = live.iter().find(|s| {
s.identifier
.as_deref()
.is_some_and(|i| i.to_ascii_lowercase().contains(&needle))
|| s.product_name
.as_deref()
.is_some_and(|p| p.to_ascii_lowercase().contains(&needle))
});
return match partial {
Some(s) => Selection::One(s.clone()),
None => Selection::None,
};
}
if live.len() == 1 {
Selection::One(live[0].clone())
} else {
Selection::Ambiguous(live.iter().map(ServerInfo::label).collect())
}
}
fn discover_entries() -> Vec<(u32, ServerInfo)> {
let root = std::env::temp_dir().join("victauri");
let mut out = Vec::new();
if !dir_is_trusted(&root) {
return out;
}
let Ok(entries) = std::fs::read_dir(&root) else {
return out;
};
for entry in entries.filter_map(Result::ok) {
let pid_str = entry.file_name().to_string_lossy().to_string();
let Ok(pid) = pid_str.parse::<u32>() else {
continue;
};
let dir = entry.path();
if !dir_is_trusted(&dir) {
continue;
}
let Ok(port_s) = std::fs::read_to_string(dir.join("port")) else {
continue;
};
let Ok(port) = port_s.trim().parse::<u16>() else {
continue;
};
let token = std::fs::read_to_string(dir.join("token"))
.ok()
.map(|t| t.trim().to_string())
.filter(|t| !t.is_empty());
let (identifier, product_name) = std::fs::read_to_string(dir.join("metadata.json"))
.ok()
.and_then(|m| serde_json::from_str::<serde_json::Value>(&m).ok())
.map_or((None, None), |m| {
(
m.get("identifier")
.and_then(|v| v.as_str())
.map(String::from),
m.get("product_name")
.and_then(|v| v.as_str())
.map(String::from),
)
});
out.push((
pid,
ServerInfo {
port,
token,
identifier,
product_name,
},
));
}
out
}
fn discover_servers() -> Vec<ServerInfo> {
discover_entries().into_iter().map(|(_, s)| s).collect()
}
fn normalize_env_token(raw: Option<String>) -> Option<String> {
raw.map(|t| t.trim().to_string()).filter(|t| !t.is_empty())
}
fn discover_token_for_port(port: u16) -> Option<String> {
token_for_port(&discover_servers(), port)
}
fn token_for_port(servers: &[ServerInfo], port: u16) -> Option<String> {
servers
.iter()
.find(|server| server.port == port)
.and_then(|server| server.token.clone())
}
fn health_client() -> &'static reqwest::Client {
static CLIENT: std::sync::OnceLock<reqwest::Client> = std::sync::OnceLock::new();
CLIENT.get_or_init(|| {
reqwest::Client::builder()
.connect_timeout(Duration::from_millis(1200))
.timeout(Duration::from_secs(3))
.build()
.unwrap_or_else(|_| reqwest::Client::new())
})
}
async fn health_ok(port: u16) -> bool {
let url = format!("http://127.0.0.1:{port}/health");
health_client()
.get(&url)
.send()
.await
.is_ok_and(|r| r.status().is_success())
}
#[cfg(windows)]
fn system32_exe(name: &str) -> String {
let root = std::env::var("SystemRoot").unwrap_or_else(|_| "C:\\Windows".to_string());
format!("{root}\\System32\\{name}")
}
#[cfg(not(windows))]
fn abs_bin(name: &str) -> String {
for base in ["/bin", "/usr/bin"] {
let p = format!("{base}/{name}");
if std::path::Path::new(&p).exists() {
return p;
}
}
name.to_string()
}
#[cfg(windows)]
fn alive_pids() -> Option<HashSet<u32>> {
let out = std::process::Command::new(system32_exe("tasklist.exe"))
.args(["/FO", "CSV", "/NH"])
.output()
.ok()?;
if !out.status.success() {
return None;
}
let text = String::from_utf8_lossy(&out.stdout);
let set: HashSet<u32> = text
.lines()
.filter_map(|line| {
line.split("\",\"")
.nth(1)
.and_then(|f| f.trim_matches('"').trim().parse::<u32>().ok())
})
.collect();
(!set.is_empty()).then_some(set)
}
#[cfg(not(windows))]
fn alive_pids() -> Option<HashSet<u32>> {
let out = std::process::Command::new(abs_bin("ps"))
.args(["-A", "-o", "pid="])
.output()
.ok()?;
if !out.status.success() {
return None;
}
let text = String::from_utf8_lossy(&out.stdout);
let set: HashSet<u32> = text
.split_whitespace()
.filter_map(|t| t.parse::<u32>().ok())
.collect();
(!set.is_empty()).then_some(set)
}
#[cfg(windows)]
fn is_process_alive(pid: u32) -> bool {
use std::process::Command;
Command::new(system32_exe("tasklist.exe"))
.args(["/FI", &format!("PID eq {pid}"), "/NH"])
.output()
.is_ok_and(|o| {
let out = String::from_utf8_lossy(&o.stdout);
out.contains(&pid.to_string())
})
}
#[cfg(not(windows))]
fn is_process_alive(pid: u32) -> bool {
std::process::Command::new(abs_bin("kill"))
.args(["-0", &pid.to_string()])
.stderr(std::process::Stdio::null())
.status()
.is_ok_and(|s| s.success())
}
#[cfg(unix)]
fn dir_is_trusted(path: &std::path::Path) -> bool {
use std::os::unix::fs::{MetadataExt, PermissionsExt};
let Ok(meta) = std::fs::symlink_metadata(path) else {
return false;
};
if !meta.file_type().is_dir() {
return false; }
let Some(euid) = current_euid() else {
return false; };
meta.uid() == euid && (meta.permissions().mode() & 0o022) == 0
}
#[cfg(unix)]
fn current_euid() -> Option<u32> {
use std::sync::atomic::{AtomicU64, Ordering};
static NEXT_PROBE: AtomicU64 = AtomicU64::new(0);
for _ in 0..16 {
let sequence = NEXT_PROBE.fetch_add(1, Ordering::Relaxed);
let probe = std::env::temp_dir().join(format!(
".victauri_bridge_uidprobe_{}_{}",
std::process::id(),
sequence
));
if let Some(uid) = uid_from_exclusive_probe(&probe) {
return Some(uid);
}
}
None
}
#[cfg(unix)]
fn uid_from_exclusive_probe(probe: &std::path::Path) -> Option<u32> {
use std::os::unix::fs::{MetadataExt, OpenOptionsExt};
let file = std::fs::OpenOptions::new()
.write(true)
.create_new(true)
.mode(0o600)
.open(probe)
.ok()?;
let uid = file.metadata().ok().map(|m| m.uid());
drop(file);
let _ = std::fs::remove_file(probe);
uid
}
#[cfg(not(unix))]
fn dir_is_trusted(_path: &std::path::Path) -> bool {
true
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn local_initialize_echoes_id_and_protocol_and_advertises_list_changed() {
let client = json!({
"jsonrpc": "2.0", "id": 7, "method": "initialize",
"params": { "protocolVersion": "2025-03-26", "capabilities": {} }
});
let resp = local_initialize_response(&client);
assert_eq!(resp["id"], 7, "must echo the client's request id");
assert_eq!(resp["jsonrpc"], "2.0");
let result = &resp["result"];
assert_eq!(result["protocolVersion"], "2025-03-26");
assert_eq!(result["capabilities"]["tools"]["listChanged"], true);
assert_eq!(result["capabilities"]["resources"]["listChanged"], true);
assert!(
result["capabilities"]["resources"]
.get("subscribe")
.is_none(),
"must not advertise a subscribe capability the server cannot honor"
);
assert_eq!(result["serverInfo"]["name"], "victauri-bridge");
assert_eq!(result["serverInfo"]["version"], env!("CARGO_PKG_VERSION"));
}
#[test]
fn local_initialize_falls_back_to_default_protocol_when_absent() {
let client = json!({ "jsonrpc": "2.0", "id": 1, "method": "initialize", "params": {} });
let resp = local_initialize_response(&client);
assert_eq!(resp["result"]["protocolVersion"], DEFAULT_PROTOCOL_VERSION);
}
#[test]
fn fallback_tools_covers_the_full_surface_with_valid_schemas() {
let tools = fallback_tools();
assert_eq!(
tools.len(),
35,
"expected the full 35-tool fallback surface"
);
for t in &tools {
assert!(t["name"].as_str().is_some_and(|n| !n.is_empty()));
assert!(t["description"].as_str().is_some_and(|d| !d.is_empty()));
assert_eq!(t["inputSchema"]["type"], "object");
}
let names: Vec<&str> = tools.iter().filter_map(|t| t["name"].as_str()).collect();
for expected in [
"eval_js",
"invoke_command",
"query_db",
"introspect",
"screenshot",
] {
assert!(
names.contains(&expected),
"fallback must include {expected}"
);
}
}
#[test]
fn fallback_tools_response_is_a_valid_tools_list_result() {
let resp = fallback_tools_response(&json!(42));
assert_eq!(resp["id"], 42);
assert!(
resp["result"]["tools"]
.as_array()
.is_some_and(|a| a.len() == 35)
);
}
#[test]
fn empty_list_responses_use_the_right_result_key() {
let is_empty_arr = |v: &Value| v.as_array().is_some_and(std::vec::Vec::is_empty);
assert!(is_empty_arr(
&empty_list_response("resources/list", &json!(1))["result"]["resources"]
));
assert!(is_empty_arr(
&empty_list_response("resources/templates/list", &json!(1))["result"]["resourceTemplates"]
));
assert!(is_empty_arr(
&empty_list_response("prompts/list", &json!(1))["result"]["prompts"]
));
}
#[test]
fn unreachable_message_is_actionable() {
let m = unreachable_message();
assert!(m.contains("tauri dev"), "must name how to start the app");
assert!(m.to_lowercase().contains("not reachable") || m.contains("no running"));
}
#[test]
fn alive_pids_enumerates_and_includes_self() {
if let Some(set) = alive_pids() {
assert!(
set.contains(&std::process::id()),
"the live-pid snapshot must include our own running process"
);
}
}
#[cfg(unix)]
#[test]
fn uid_probe_refuses_preplanted_symlink_without_clobbering_target() {
let dir = tempfile::tempdir().unwrap();
let target = dir.path().join("target");
let probe = dir.path().join("probe");
std::fs::write(&target, "must-survive").unwrap();
std::os::unix::fs::symlink(&target, &probe).unwrap();
assert_eq!(uid_from_exclusive_probe(&probe), None);
assert_eq!(std::fs::read_to_string(&target).unwrap(), "must-survive");
}
fn srv(id: &str, name: &str, port: u16) -> ServerInfo {
ServerInfo {
port,
token: None,
identifier: Some(id.to_string()),
product_name: Some(name.to_string()),
}
}
#[test]
fn selects_sole_server_without_app() {
let live = vec![srv("com.a.app", "A", 7373)];
assert!(matches!(select(&live, None), Selection::One(s) if s.port == 7373));
}
#[test]
fn ambiguous_when_multiple_and_no_app() {
let live = vec![srv("com.a.app", "A", 7373), srv("com.b.app", "B", 7374)];
assert!(matches!(select(&live, None), Selection::Ambiguous(v) if v.len() == 2));
}
#[test]
fn selects_by_identifier_among_many() {
let live = vec![srv("com.a.app", "A", 7373), srv("com.4da.app", "4DA", 7374)];
match select(&live, Some("com.4da.app")) {
Selection::One(s) => assert_eq!(s.port, 7374),
_ => panic!("should pick 4DA by identifier"),
}
}
#[test]
fn selects_by_product_name_case_insensitive() {
let live = vec![
srv("com.a.app", "Demo", 7373),
srv("com.4da.app", "4DA", 7374),
];
match select(&live, Some("4da")) {
Selection::One(s) => assert_eq!(s.port, 7374),
_ => panic!("should pick by product name"),
}
}
#[test]
fn no_match_returns_none() {
let live = vec![srv("com.a.app", "A", 7373)];
assert!(matches!(
select(&live, Some("com.nope.app")),
Selection::None
));
}
#[test]
fn token_selection_never_crosses_ports() {
let mut first = srv("com.a.app", "A", 7373);
first.token = Some("token-a".to_string());
let mut second = srv("com.b.app", "B", 7374);
second.token = Some("token-b".to_string());
let servers = vec![first, second];
assert_eq!(token_for_port(&servers, 7374).as_deref(), Some("token-b"));
assert_eq!(token_for_port(&servers, 7999), None);
}
#[test]
fn substring_identifier_match() {
let live = vec![srv("com.victauri.demo", "Demo", 7373)];
match select(&live, Some("demo")) {
Selection::One(s) => assert_eq!(s.port, 7373),
_ => panic!("substring of product/identifier should match"),
}
}
#[test]
fn discover_servers_reads_real_metadata_and_selects() {
let pid = std::process::id(); let dir = std::env::temp_dir().join("victauri").join(pid.to_string());
std::fs::create_dir_all(&dir).unwrap();
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(&dir, std::fs::Permissions::from_mode(0o700)).unwrap();
}
std::fs::write(dir.join("port"), "61999").unwrap();
std::fs::write(dir.join("token"), "tok-xyz").unwrap();
std::fs::write(
dir.join("metadata.json"),
r#"{"pid":1,"port":61999,"identifier":"com.test.discover","product_name":"DiscoverTest"}"#,
)
.unwrap();
let servers = discover_servers();
let mine = servers
.iter()
.find(|s| s.identifier.as_deref() == Some("com.test.discover"))
.expect("bridge should discover the entry written for the live current pid");
assert_eq!(mine.port, 61999);
assert_eq!(mine.token.as_deref(), Some("tok-xyz"));
assert_eq!(mine.product_name.as_deref(), Some("DiscoverTest"));
assert!(matches!(
select(std::slice::from_ref(mine), Some("com.test.discover")),
Selection::One(_)
));
let _ = std::fs::remove_dir_all(&dir);
}
#[cfg(unix)]
#[test]
fn dir_is_trusted_rejects_world_writable_and_symlink() {
use std::os::unix::fs::PermissionsExt;
let base = std::env::temp_dir().join(format!("vic_trust_test_{}", std::process::id()));
let _ = std::fs::remove_dir_all(&base);
std::fs::create_dir_all(&base).unwrap();
let good = base.join("good");
std::fs::create_dir_all(&good).unwrap();
std::fs::set_permissions(&good, std::fs::Permissions::from_mode(0o700)).unwrap();
assert!(dir_is_trusted(&good), "0700 owner dir must be trusted");
let bad = base.join("bad");
std::fs::create_dir_all(&bad).unwrap();
std::fs::set_permissions(&bad, std::fs::Permissions::from_mode(0o777)).unwrap();
assert!(!dir_is_trusted(&bad), "world-writable dir must be rejected");
let link = base.join("link");
let _ = std::os::unix::fs::symlink(&good, &link);
assert!(!dir_is_trusted(&link), "symlinked dir must be rejected");
let _ = std::fs::remove_dir_all(&base);
}
#[test]
fn normalize_env_token_treats_blank_as_unset() {
assert_eq!(normalize_env_token(None), None, "unset -> None");
assert_eq!(
normalize_env_token(Some(String::new())),
None,
"empty -> None"
);
assert_eq!(
normalize_env_token(Some(" ".to_string())),
None,
"spaces -> None"
);
assert_eq!(
normalize_env_token(Some("\t\r\n ".to_string())),
None,
"whitespace -> None"
);
assert_eq!(
normalize_env_token(Some("real-token".to_string())).as_deref(),
Some("real-token"),
"real token preserved"
);
assert_eq!(
normalize_env_token(Some(" padded ".to_string())).as_deref(),
Some("padded"),
"surrounding whitespace trimmed"
);
}
}