//! Debug server for IPC communication with MCP server
use std::path::Path;
use std::sync::Arc;
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
use tokio::sync::Mutex;
use tracing::{debug, error, info, warn};
use crate::protocol::{JsonRpcRequest, JsonRpcResponse, METHOD_NOT_FOUND};
use crate::CommandHandler;
use interprocess::local_socket::tokio::{prelude::*, Stream};
use interprocess::local_socket::ListenerOptions;
#[cfg(unix)]
use interprocess::local_socket::GenericFilePath;
#[cfg(windows)]
use interprocess::local_socket::GenericNamespaced;
/// Socket file name in project root (Unix only)
pub const SOCKET_FILE_NAME: &str = ".tauri-mcp.sock";
/// Debug server that listens for commands from MCP server
pub struct DebugServer {
socket_path: String,
handler: Arc<Mutex<Option<Arc<dyn CommandHandler>>>>,
}
impl DebugServer {
pub fn new(project_root: &Path) -> Self {
let socket_path = Self::get_socket_path(project_root);
Self {
socket_path,
handler: Arc::new(Mutex::new(None)),
}
}
/// Maximum Unix socket path length (macOS: 104, Linux: 108)
#[cfg(unix)]
const MAX_SOCKET_PATH_LEN: usize = 104;
/// Get platform-specific socket path
/// Falls back to /tmp/ with a hash if the project path is too long for Unix sockets
#[cfg(unix)]
fn get_socket_path(project_root: &Path) -> String {
let direct_path = project_root
.join(SOCKET_FILE_NAME)
.to_string_lossy()
.to_string();
if direct_path.len() <= Self::MAX_SOCKET_PATH_LEN {
return direct_path;
}
// Path too long for Unix socket - use /tmp/ with a hash for uniqueness
// Uses simple FNV-1a hash (same algorithm used in Node.js side for consistency)
let path_bytes = project_root.to_string_lossy();
let hash = Self::fnv1a_hash(path_bytes.as_bytes());
let tmp_path = format!("/tmp/tauri-mcp-{:x}.sock", hash);
eprintln!(
"[tauri-plugin-mcp] Socket path too long ({} > {}), using: {}",
direct_path.len(),
Self::MAX_SOCKET_PATH_LEN,
tmp_path
);
tmp_path
}
/// FNV-1a hash - simple, deterministic, and easy to implement in both Rust and JS
fn fnv1a_hash(data: &[u8]) -> u64 {
let mut hash: u64 = 0xcbf29ce484222325;
for &byte in data {
hash ^= byte as u64;
hash = hash.wrapping_mul(0x100000001b3);
}
hash
}
#[cfg(windows)]
fn get_socket_path(project_root: &Path) -> String {
// Windows Named Pipe: use hash of project path for uniqueness
// interprocess GenericNamespaced uses @name format, which maps to \\.\pipe\name
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
let path_str = project_root.to_string_lossy();
let path_bytes = path_str.as_bytes();
let mut hasher = DefaultHasher::new();
path_bytes.hash(&mut hasher);
let hash = hasher.finish();
// Use @name format for interprocess GenericNamespaced
// This will be converted to \\.\pipe\tauri-mcp-{hash} internally
let pipe_name = format!("tauri-mcp-{:x}", hash);
eprintln!("[tauri-plugin-mcp] Windows pipe path calculation:");
eprintln!("[tauri-plugin-mcp] project_root: {:?}", project_root);
eprintln!("[tauri-plugin-mcp] path_str: {}", path_str);
eprintln!("[tauri-plugin-mcp] hash: {:x}", hash);
eprintln!("[tauri-plugin-mcp] pipe_name: {}", pipe_name);
eprintln!("[tauri-plugin-mcp] full_path: \\\\.\\pipe\\{}", pipe_name);
pipe_name
}
/// Set the command handler
pub async fn set_handler(&self, handler: Arc<dyn CommandHandler>) {
let mut guard = self.handler.lock().await;
*guard = Some(handler);
}
/// Start the debug server (Unix implementation)
#[cfg(unix)]
pub async fn start(&self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
eprintln!(
"[tauri-plugin-mcp] Starting debug server at: {}",
self.socket_path
);
info!("Starting debug server at: {}", self.socket_path);
// Clean up existing socket
let _ = std::fs::remove_file(&self.socket_path);
let listener = ListenerOptions::new()
.name(self.socket_path.as_str().to_fs_name::<GenericFilePath>()?)
.create_tokio()?;
let handler = Arc::clone(&self.handler);
tokio::spawn(async move {
loop {
match listener.accept().await {
Ok(stream) => {
let handler = Arc::clone(&handler);
tokio::spawn(async move {
if let Err(e) = Self::handle_connection(stream, handler).await {
error!("Connection error: {}", e);
}
});
}
Err(e) => {
error!("Accept error: {}", e);
}
}
}
});
Ok(())
}
/// Start the debug server (Windows implementation using interprocess)
#[cfg(windows)]
pub async fn start(&self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let full_pipe_path = format!(r"\\.\pipe\{}", self.socket_path);
eprintln!(
"[tauri-plugin-mcp] Starting debug server at: {}",
full_pipe_path
);
info!("Starting debug server at: {}", full_pipe_path);
let listener = ListenerOptions::new()
.name(
self.socket_path
.as_str()
.to_ns_name::<GenericNamespaced>()?,
)
.create_tokio()?;
let handler = Arc::clone(&self.handler);
tokio::spawn(async move {
loop {
match listener.accept().await {
Ok(stream) => {
eprintln!("[tauri-plugin-mcp] Client connected!");
let handler = Arc::clone(&handler);
tokio::spawn(async move {
if let Err(e) = Self::handle_connection(stream, handler).await {
eprintln!("[tauri-plugin-mcp] Connection error: {}", e);
error!("Connection error: {}", e);
}
});
}
Err(e) => {
eprintln!("[tauri-plugin-mcp] Accept error: {}", e);
error!("Accept error: {}", e);
}
}
}
});
Ok(())
}
/// Handle a connection (unified for all platforms)
async fn handle_connection(
stream: Stream,
handler: Arc<Mutex<Option<Arc<dyn CommandHandler>>>>,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let (reader, mut writer) = stream.split();
let mut reader = BufReader::new(reader);
let mut line = String::new();
loop {
line.clear();
let bytes_read = reader.read_line(&mut line).await?;
if bytes_read == 0 {
debug!("Client disconnected");
break;
}
let line = line.trim();
if line.is_empty() {
continue;
}
debug!("Received: {}", line);
let response = match serde_json::from_str::<JsonRpcRequest>(line) {
Ok(request) => {
let guard = handler.lock().await;
if let Some(ref h) = *guard {
h.handle_request(request).await
} else {
JsonRpcResponse::error(None, METHOD_NOT_FOUND, "Handler not initialized")
}
}
Err(e) => {
warn!("Failed to parse request: {}", e);
JsonRpcResponse::error(
None,
crate::protocol::PARSE_ERROR,
format!("Parse error: {}", e),
)
}
};
let response_str = serde_json::to_string(&response)?;
debug!("Sending: {}", response_str);
writer.write_all(response_str.as_bytes()).await?;
writer.write_all(b"\n").await?;
writer.flush().await?;
}
Ok(())
}
/// Get the socket path for external use
/// On Unix: returns the file path (e.g., /path/to/.tauri-mcp.sock)
/// On Windows: returns the pipe name without prefix (e.g., tauri-mcp-abc123)
/// Full path is \\.\pipe\{socket_path}
pub fn socket_path(&self) -> &str {
&self.socket_path
}
/// Get the full connection path for clients
/// On Unix: same as socket_path
/// On Windows: returns \\.\pipe\{name}
#[cfg(unix)]
pub fn connection_path(&self) -> String {
self.socket_path.clone()
}
#[cfg(windows)]
pub fn connection_path(&self) -> String {
format!(r"\\.\pipe\{}", self.socket_path)
}
}