Files
donutbrowser/src-tauri/src/mcp_server.rs
T

1942 lines
54 KiB
Rust

#![allow(dead_code)]
use axum::{
body::Body,
extract::State,
http::{header, Request, StatusCode},
middleware::{self, Next},
response::{IntoResponse, Response},
routing::post,
Json, Router,
};
use serde::{Deserialize, Serialize};
use std::net::SocketAddr;
use std::sync::atomic::{AtomicBool, AtomicU16, Ordering};
use std::sync::Arc;
use tauri::AppHandle;
use tokio::net::TcpListener;
use tokio::sync::Mutex as AsyncMutex;
use crate::browser::ProxySettings;
use crate::group_manager::GROUP_MANAGER;
use crate::profile::{BrowserProfile, ProfileManager};
use crate::proxy_manager::PROXY_MANAGER;
use crate::settings_manager::SettingsManager;
use crate::wayfern_terms::WayfernTermsManager;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct McpTool {
pub name: String,
pub description: String,
pub input_schema: serde_json::Value,
}
#[derive(Debug, Deserialize)]
#[allow(dead_code)]
pub struct McpRequest {
jsonrpc: String,
id: serde_json::Value,
method: String,
params: Option<serde_json::Value>,
}
#[derive(Debug, Serialize)]
pub struct McpResponse {
jsonrpc: String,
id: serde_json::Value,
#[serde(skip_serializing_if = "Option::is_none")]
result: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
error: Option<McpError>,
}
#[derive(Debug, Serialize)]
pub struct McpError {
code: i32,
message: String,
}
const DEFAULT_MCP_PORT: u16 = 51080;
struct McpServerInner {
app_handle: Option<AppHandle>,
token: Option<String>,
shutdown_tx: Option<tokio::sync::oneshot::Sender<()>>,
}
#[derive(Clone)]
struct McpHttpState {
server: &'static McpServer,
token: String,
}
pub struct McpServer {
inner: Arc<AsyncMutex<McpServerInner>>,
is_running: AtomicBool,
port: AtomicU16,
}
impl McpServer {
fn new() -> Self {
Self {
inner: Arc::new(AsyncMutex::new(McpServerInner {
app_handle: None,
token: None,
shutdown_tx: None,
})),
is_running: AtomicBool::new(false),
port: AtomicU16::new(0),
}
}
pub fn instance() -> &'static McpServer {
&MCP_SERVER
}
pub fn is_running(&self) -> bool {
self.is_running.load(Ordering::SeqCst)
}
pub fn get_port(&self) -> Option<u16> {
let port = self.port.load(Ordering::SeqCst);
if port > 0 {
Some(port)
} else {
None
}
}
pub async fn start(&self, app_handle: AppHandle) -> Result<u16, String> {
if !WayfernTermsManager::instance().is_terms_accepted() {
return Err(
"Wayfern Terms and Conditions must be accepted before starting MCP server".to_string(),
);
}
if self.is_running() {
return Err("MCP server is already running".to_string());
}
let settings_manager = SettingsManager::instance();
let settings = settings_manager
.load_settings()
.map_err(|e| format!("Failed to load settings: {e}"))?;
// Get or generate token
let existing_token = settings_manager
.get_mcp_token(&app_handle)
.await
.ok()
.flatten();
let token = if let Some(t) = existing_token {
t
} else {
settings_manager
.generate_mcp_token(&app_handle)
.await
.map_err(|e| format!("Failed to generate MCP token: {e}"))?
};
// Determine port (use saved port, or try default, or random)
let preferred_port = settings.mcp_port.unwrap_or(DEFAULT_MCP_PORT);
let actual_port = self.bind_to_available_port(preferred_port).await?;
// Save port if it changed
if settings.mcp_port != Some(actual_port) {
let mut new_settings = settings;
new_settings.mcp_port = Some(actual_port);
settings_manager
.save_settings(&new_settings)
.map_err(|e| format!("Failed to save settings: {e}"))?;
}
// Store state
let mut inner = self.inner.lock().await;
inner.app_handle = Some(app_handle);
inner.token = Some(token.clone());
// Create shutdown channel
let (shutdown_tx, shutdown_rx) = tokio::sync::oneshot::channel::<()>();
inner.shutdown_tx = Some(shutdown_tx);
self.port.store(actual_port, Ordering::SeqCst);
self.is_running.store(true, Ordering::SeqCst);
// Start HTTP server in background
let http_state = McpHttpState {
server: McpServer::instance(),
token,
};
tokio::spawn(Self::run_http_server(actual_port, http_state, shutdown_rx));
log::info!("[mcp] Server started on port {}", actual_port);
Ok(actual_port)
}
async fn bind_to_available_port(&self, preferred: u16) -> Result<u16, String> {
let addr = SocketAddr::from(([127, 0, 0, 1], preferred));
if TcpListener::bind(addr).await.is_ok() {
return Ok(preferred);
}
// Try random ports in 51000-51999 range
for _ in 0..10 {
let port = 51000 + (rand::random::<u16>() % 1000);
let addr = SocketAddr::from(([127, 0, 0, 1], port));
if TcpListener::bind(addr).await.is_ok() {
return Ok(port);
}
}
Err("Could not find available port for MCP server".to_string())
}
async fn run_http_server(
port: u16,
state: McpHttpState,
shutdown_rx: tokio::sync::oneshot::Receiver<()>,
) {
let app = Router::new()
.route("/mcp", post(Self::handle_mcp_post))
.layer(middleware::from_fn_with_state(
state.clone(),
Self::auth_middleware,
))
.with_state(state);
let addr = SocketAddr::from(([127, 0, 0, 1], port));
let listener = match TcpListener::bind(addr).await {
Ok(l) => l,
Err(e) => {
log::error!("[mcp] Failed to bind to port {}: {}", port, e);
return;
}
};
log::info!(
"[mcp] HTTP server listening on http://127.0.0.1:{}/mcp",
port
);
let server = axum::serve(listener, app).with_graceful_shutdown(async {
let _ = shutdown_rx.await;
log::info!("[mcp] HTTP server shutting down");
});
if let Err(e) = server.await {
log::error!("[mcp] HTTP server error: {}", e);
}
}
async fn auth_middleware(
State(state): State<McpHttpState>,
req: Request<Body>,
next: Next,
) -> Result<Response, StatusCode> {
let auth_header = req
.headers()
.get(header::AUTHORIZATION)
.and_then(|h| h.to_str().ok());
let token = auth_header.and_then(|h| h.strip_prefix("Bearer "));
if token != Some(&state.token) {
return Err(StatusCode::UNAUTHORIZED);
}
Ok(next.run(req).await)
}
async fn handle_mcp_post(
State(state): State<McpHttpState>,
Json(request): Json<McpRequest>,
) -> impl IntoResponse {
let response = state.server.handle_request(request).await;
Json(response)
}
pub async fn stop(&self) -> Result<(), String> {
if !self.is_running() {
return Err("MCP server is not running".to_string());
}
let mut inner = self.inner.lock().await;
inner.app_handle = None;
inner.token = None;
// Send shutdown signal
if let Some(tx) = inner.shutdown_tx.take() {
let _ = tx.send(());
}
self.port.store(0, Ordering::SeqCst);
self.is_running.store(false, Ordering::SeqCst);
log::info!("[mcp] Server stopped");
Ok(())
}
pub fn get_tools(&self) -> Vec<McpTool> {
vec![
McpTool {
name: "list_profiles".to_string(),
description: "List all Wayfern and Camoufox browser profiles".to_string(),
input_schema: serde_json::json!({
"type": "object",
"properties": {},
"required": []
}),
},
McpTool {
name: "get_profile".to_string(),
description: "Get details of a specific browser profile".to_string(),
input_schema: serde_json::json!({
"type": "object",
"properties": {
"profile_id": {
"type": "string",
"description": "The UUID of the profile to retrieve"
}
},
"required": ["profile_id"]
}),
},
McpTool {
name: "run_profile".to_string(),
description: "Launch a browser profile with an optional URL".to_string(),
input_schema: serde_json::json!({
"type": "object",
"properties": {
"profile_id": {
"type": "string",
"description": "The UUID of the profile to launch"
},
"url": {
"type": "string",
"description": "Optional URL to open in the browser"
},
"headless": {
"type": "boolean",
"description": "Run the browser in headless mode"
}
},
"required": ["profile_id"]
}),
},
McpTool {
name: "kill_profile".to_string(),
description: "Stop a running browser profile".to_string(),
input_schema: serde_json::json!({
"type": "object",
"properties": {
"profile_id": {
"type": "string",
"description": "The UUID of the profile to stop"
}
},
"required": ["profile_id"]
}),
},
McpTool {
name: "list_proxies".to_string(),
description: "List all configured proxies".to_string(),
input_schema: serde_json::json!({
"type": "object",
"properties": {},
"required": []
}),
},
McpTool {
name: "get_profile_status".to_string(),
description: "Check if a browser profile is currently running".to_string(),
input_schema: serde_json::json!({
"type": "object",
"properties": {
"profile_id": {
"type": "string",
"description": "The UUID of the profile to check"
}
},
"required": ["profile_id"]
}),
},
// Group management tools
McpTool {
name: "list_groups".to_string(),
description: "List all profile groups".to_string(),
input_schema: serde_json::json!({
"type": "object",
"properties": {},
"required": []
}),
},
McpTool {
name: "get_group".to_string(),
description: "Get details of a specific group".to_string(),
input_schema: serde_json::json!({
"type": "object",
"properties": {
"group_id": {
"type": "string",
"description": "The UUID of the group to retrieve"
}
},
"required": ["group_id"]
}),
},
McpTool {
name: "create_group".to_string(),
description: "Create a new profile group".to_string(),
input_schema: serde_json::json!({
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "The name for the new group"
}
},
"required": ["name"]
}),
},
McpTool {
name: "update_group".to_string(),
description: "Update an existing group's name".to_string(),
input_schema: serde_json::json!({
"type": "object",
"properties": {
"group_id": {
"type": "string",
"description": "The UUID of the group to update"
},
"name": {
"type": "string",
"description": "The new name for the group"
}
},
"required": ["group_id", "name"]
}),
},
McpTool {
name: "delete_group".to_string(),
description: "Delete a profile group".to_string(),
input_schema: serde_json::json!({
"type": "object",
"properties": {
"group_id": {
"type": "string",
"description": "The UUID of the group to delete"
}
},
"required": ["group_id"]
}),
},
McpTool {
name: "assign_profiles_to_group".to_string(),
description: "Assign one or more profiles to a group".to_string(),
input_schema: serde_json::json!({
"type": "object",
"properties": {
"profile_ids": {
"type": "array",
"items": { "type": "string" },
"description": "Array of profile UUIDs to assign"
},
"group_id": {
"type": "string",
"description": "The UUID of the group to assign to (null to remove from group)"
}
},
"required": ["profile_ids"]
}),
},
// Full proxy management tools
McpTool {
name: "get_proxy".to_string(),
description: "Get details of a specific proxy".to_string(),
input_schema: serde_json::json!({
"type": "object",
"properties": {
"proxy_id": {
"type": "string",
"description": "The UUID of the proxy to retrieve"
}
},
"required": ["proxy_id"]
}),
},
McpTool {
name: "create_proxy".to_string(),
description: "Create a new proxy configuration".to_string(),
input_schema: serde_json::json!({
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "The name for the new proxy"
},
"proxy_type": {
"type": "string",
"enum": ["http", "https", "socks4", "socks5"],
"description": "The type of proxy"
},
"host": {
"type": "string",
"description": "The proxy host address"
},
"port": {
"type": "integer",
"description": "The proxy port number"
},
"username": {
"type": "string",
"description": "Optional username for authentication"
},
"password": {
"type": "string",
"description": "Optional password for authentication"
}
},
"required": ["name", "proxy_type", "host", "port"]
}),
},
McpTool {
name: "update_proxy".to_string(),
description: "Update an existing proxy configuration".to_string(),
input_schema: serde_json::json!({
"type": "object",
"properties": {
"proxy_id": {
"type": "string",
"description": "The UUID of the proxy to update"
},
"name": {
"type": "string",
"description": "New name for the proxy"
},
"proxy_type": {
"type": "string",
"enum": ["http", "https", "socks4", "socks5"],
"description": "The type of proxy"
},
"host": {
"type": "string",
"description": "The proxy host address"
},
"port": {
"type": "integer",
"description": "The proxy port number"
},
"username": {
"type": "string",
"description": "Optional username for authentication"
},
"password": {
"type": "string",
"description": "Optional password for authentication"
}
},
"required": ["proxy_id"]
}),
},
McpTool {
name: "delete_proxy".to_string(),
description: "Delete a proxy configuration".to_string(),
input_schema: serde_json::json!({
"type": "object",
"properties": {
"proxy_id": {
"type": "string",
"description": "The UUID of the proxy to delete"
}
},
"required": ["proxy_id"]
}),
},
McpTool {
name: "export_proxies".to_string(),
description: "Export all proxy configurations".to_string(),
input_schema: serde_json::json!({
"type": "object",
"properties": {
"format": {
"type": "string",
"enum": ["json", "txt"],
"description": "Export format (json for structured data, txt for URL format)"
}
},
"required": ["format"]
}),
},
McpTool {
name: "import_proxies".to_string(),
description: "Import proxy configurations from JSON or TXT content".to_string(),
input_schema: serde_json::json!({
"type": "object",
"properties": {
"content": {
"type": "string",
"description": "The proxy configuration content to import"
},
"format": {
"type": "string",
"enum": ["json", "txt"],
"description": "Import format (json or txt)"
},
"name_prefix": {
"type": "string",
"description": "Optional prefix for imported proxy names (default: 'Imported')"
}
},
"required": ["content", "format"]
}),
},
// VPN management tools
McpTool {
name: "import_vpn".to_string(),
description: "Import a WireGuard (.conf) or OpenVPN (.ovpn) configuration".to_string(),
input_schema: serde_json::json!({
"type": "object",
"properties": {
"content": {
"type": "string",
"description": "Raw VPN config file content"
},
"filename": {
"type": "string",
"description": "Original filename (.conf or .ovpn) for type detection"
},
"name": {
"type": "string",
"description": "Optional display name for the VPN config"
}
},
"required": ["content", "filename"]
}),
},
McpTool {
name: "list_vpn_configs".to_string(),
description: "List all stored VPN configurations".to_string(),
input_schema: serde_json::json!({
"type": "object",
"properties": {},
"required": []
}),
},
McpTool {
name: "delete_vpn".to_string(),
description: "Delete a VPN configuration".to_string(),
input_schema: serde_json::json!({
"type": "object",
"properties": {
"vpn_id": {
"type": "string",
"description": "The UUID of the VPN config to delete"
}
},
"required": ["vpn_id"]
}),
},
McpTool {
name: "connect_vpn".to_string(),
description: "Connect to a VPN configuration".to_string(),
input_schema: serde_json::json!({
"type": "object",
"properties": {
"vpn_id": {
"type": "string",
"description": "The UUID of the VPN config to connect"
}
},
"required": ["vpn_id"]
}),
},
McpTool {
name: "disconnect_vpn".to_string(),
description: "Disconnect from a VPN".to_string(),
input_schema: serde_json::json!({
"type": "object",
"properties": {
"vpn_id": {
"type": "string",
"description": "The UUID of the VPN to disconnect"
}
},
"required": ["vpn_id"]
}),
},
McpTool {
name: "get_vpn_status".to_string(),
description: "Get the connection status of a VPN".to_string(),
input_schema: serde_json::json!({
"type": "object",
"properties": {
"vpn_id": {
"type": "string",
"description": "The UUID of the VPN to check"
}
},
"required": ["vpn_id"]
}),
},
]
}
pub async fn handle_request(&self, request: McpRequest) -> McpResponse {
if !self.is_running() {
return McpResponse {
jsonrpc: "2.0".to_string(),
id: request.id,
result: None,
error: Some(McpError {
code: -32001,
message: "MCP server is not running".to_string(),
}),
};
}
let result = match request.method.as_str() {
"tools/list" => self.handle_tools_list().await,
"tools/call" => self.handle_tool_call(request.params).await,
_ => Err(McpError {
code: -32601,
message: format!("Method not found: {}", request.method),
}),
};
match result {
Ok(value) => McpResponse {
jsonrpc: "2.0".to_string(),
id: request.id,
result: Some(value),
error: None,
},
Err(error) => McpResponse {
jsonrpc: "2.0".to_string(),
id: request.id,
result: None,
error: Some(error),
},
}
}
async fn handle_tools_list(&self) -> Result<serde_json::Value, McpError> {
Ok(serde_json::json!({
"tools": self.get_tools()
}))
}
async fn handle_tool_call(
&self,
params: Option<serde_json::Value>,
) -> Result<serde_json::Value, McpError> {
let params = params.ok_or_else(|| McpError {
code: -32602,
message: "Missing parameters".to_string(),
})?;
let tool_name = params
.get("name")
.and_then(|v| v.as_str())
.ok_or_else(|| McpError {
code: -32602,
message: "Missing tool name".to_string(),
})?;
let arguments = params
.get("arguments")
.cloned()
.unwrap_or(serde_json::json!({}));
match tool_name {
"list_profiles" => self.handle_list_profiles().await,
"get_profile" => self.handle_get_profile(&arguments).await,
"run_profile" => self.handle_run_profile(&arguments).await,
"kill_profile" => self.handle_kill_profile(&arguments).await,
"list_proxies" => self.handle_list_proxies().await,
"get_profile_status" => self.handle_get_profile_status(&arguments).await,
// Group management
"list_groups" => self.handle_list_groups().await,
"get_group" => self.handle_get_group(&arguments).await,
"create_group" => self.handle_create_group(&arguments).await,
"update_group" => self.handle_update_group(&arguments).await,
"delete_group" => self.handle_delete_group(&arguments).await,
"assign_profiles_to_group" => self.handle_assign_profiles_to_group(&arguments).await,
// Full proxy management
"get_proxy" => self.handle_get_proxy(&arguments).await,
"create_proxy" => self.handle_create_proxy(&arguments).await,
"update_proxy" => self.handle_update_proxy(&arguments).await,
"delete_proxy" => self.handle_delete_proxy(&arguments).await,
// Proxy import/export
"export_proxies" => self.handle_export_proxies(&arguments).await,
"import_proxies" => self.handle_import_proxies(&arguments).await,
// VPN management
"import_vpn" => self.handle_import_vpn(&arguments).await,
"list_vpn_configs" => self.handle_list_vpn_configs().await,
"delete_vpn" => self.handle_delete_vpn(&arguments).await,
"connect_vpn" => self.handle_connect_vpn(&arguments).await,
"disconnect_vpn" => self.handle_disconnect_vpn(&arguments).await,
"get_vpn_status" => self.handle_get_vpn_status(&arguments).await,
_ => Err(McpError {
code: -32602,
message: format!("Unknown tool: {tool_name}"),
}),
}
}
async fn handle_list_profiles(&self) -> Result<serde_json::Value, McpError> {
let profiles = ProfileManager::instance()
.list_profiles()
.map_err(|e| McpError {
code: -32000,
message: format!("Failed to list profiles: {e}"),
})?;
// Filter to only Wayfern and Camoufox profiles
let filtered: Vec<&BrowserProfile> = profiles
.iter()
.filter(|p| p.browser == "wayfern" || p.browser == "camoufox")
.collect();
Ok(serde_json::json!({
"content": [{
"type": "text",
"text": serde_json::to_string_pretty(&filtered).unwrap_or_default()
}]
}))
}
async fn handle_get_profile(
&self,
arguments: &serde_json::Value,
) -> Result<serde_json::Value, McpError> {
let profile_id = arguments
.get("profile_id")
.and_then(|v| v.as_str())
.ok_or_else(|| McpError {
code: -32602,
message: "Missing profile_id".to_string(),
})?;
let profiles = ProfileManager::instance()
.list_profiles()
.map_err(|e| McpError {
code: -32000,
message: format!("Failed to list profiles: {e}"),
})?;
let profile = profiles
.iter()
.find(|p| p.id.to_string() == profile_id)
.ok_or_else(|| McpError {
code: -32000,
message: format!("Profile not found: {profile_id}"),
})?;
// Check if it's a Wayfern or Camoufox profile
if profile.browser != "wayfern" && profile.browser != "camoufox" {
return Err(McpError {
code: -32000,
message: "MCP only supports Wayfern and Camoufox profiles".to_string(),
});
}
Ok(serde_json::json!({
"content": [{
"type": "text",
"text": serde_json::to_string_pretty(&profile).unwrap_or_default()
}]
}))
}
async fn handle_run_profile(
&self,
arguments: &serde_json::Value,
) -> Result<serde_json::Value, McpError> {
let profile_id = arguments
.get("profile_id")
.and_then(|v| v.as_str())
.ok_or_else(|| McpError {
code: -32602,
message: "Missing profile_id".to_string(),
})?;
let url = arguments.get("url").and_then(|v| v.as_str());
let _headless = arguments
.get("headless")
.and_then(|v| v.as_bool())
.unwrap_or(false);
// Get the profile
let profiles = ProfileManager::instance()
.list_profiles()
.map_err(|e| McpError {
code: -32000,
message: format!("Failed to list profiles: {e}"),
})?;
let profile = profiles
.iter()
.find(|p| p.id.to_string() == profile_id)
.ok_or_else(|| McpError {
code: -32000,
message: format!("Profile not found: {profile_id}"),
})?;
// Check if it's a Wayfern or Camoufox profile
if profile.browser != "wayfern" && profile.browser != "camoufox" {
return Err(McpError {
code: -32000,
message: "MCP only supports Wayfern and Camoufox profiles".to_string(),
});
}
// Get app handle to launch
let inner = self.inner.lock().await;
let app_handle = inner.app_handle.as_ref().ok_or_else(|| McpError {
code: -32000,
message: "MCP server not properly initialized".to_string(),
})?;
// Launch the browser
crate::browser_runner::BrowserRunner::instance()
.launch_browser(
app_handle.clone(),
profile,
url.map(|s| s.to_string()),
None,
)
.await
.map_err(|e| McpError {
code: -32000,
message: format!("Failed to launch browser: {e}"),
})?;
Ok(serde_json::json!({
"content": [{
"type": "text",
"text": format!("Browser profile '{}' launched successfully", profile.name)
}]
}))
}
async fn handle_kill_profile(
&self,
arguments: &serde_json::Value,
) -> Result<serde_json::Value, McpError> {
let profile_id = arguments
.get("profile_id")
.and_then(|v| v.as_str())
.ok_or_else(|| McpError {
code: -32602,
message: "Missing profile_id".to_string(),
})?;
// Get the profile
let profiles = ProfileManager::instance()
.list_profiles()
.map_err(|e| McpError {
code: -32000,
message: format!("Failed to list profiles: {e}"),
})?;
let profile = profiles
.iter()
.find(|p| p.id.to_string() == profile_id)
.ok_or_else(|| McpError {
code: -32000,
message: format!("Profile not found: {profile_id}"),
})?;
// Check if it's a Wayfern or Camoufox profile
if profile.browser != "wayfern" && profile.browser != "camoufox" {
return Err(McpError {
code: -32000,
message: "MCP only supports Wayfern and Camoufox profiles".to_string(),
});
}
// Get app handle to kill
let inner = self.inner.lock().await;
let app_handle = inner.app_handle.as_ref().ok_or_else(|| McpError {
code: -32000,
message: "MCP server not properly initialized".to_string(),
})?;
// Kill the browser
crate::browser_runner::BrowserRunner::instance()
.kill_browser_process(app_handle.clone(), profile)
.await
.map_err(|e| McpError {
code: -32000,
message: format!("Failed to kill browser: {e}"),
})?;
Ok(serde_json::json!({
"content": [{
"type": "text",
"text": format!("Browser profile '{}' stopped successfully", profile.name)
}]
}))
}
async fn handle_list_proxies(&self) -> Result<serde_json::Value, McpError> {
let proxies = PROXY_MANAGER.get_stored_proxies();
Ok(serde_json::json!({
"content": [{
"type": "text",
"text": serde_json::to_string_pretty(&proxies).unwrap_or_default()
}]
}))
}
async fn handle_get_profile_status(
&self,
arguments: &serde_json::Value,
) -> Result<serde_json::Value, McpError> {
let profile_id = arguments
.get("profile_id")
.and_then(|v| v.as_str())
.ok_or_else(|| McpError {
code: -32602,
message: "Missing profile_id".to_string(),
})?;
// Get the profile
let profiles = ProfileManager::instance()
.list_profiles()
.map_err(|e| McpError {
code: -32000,
message: format!("Failed to list profiles: {e}"),
})?;
let profile = profiles
.iter()
.find(|p| p.id.to_string() == profile_id)
.ok_or_else(|| McpError {
code: -32000,
message: format!("Profile not found: {profile_id}"),
})?;
// Check if it's a Wayfern or Camoufox profile
if profile.browser != "wayfern" && profile.browser != "camoufox" {
return Err(McpError {
code: -32000,
message: "MCP only supports Wayfern and Camoufox profiles".to_string(),
});
}
let is_running = profile.process_id.is_some();
Ok(serde_json::json!({
"content": [{
"type": "text",
"text": serde_json::json!({
"profile_id": profile_id,
"is_running": is_running
}).to_string()
}]
}))
}
// Group management handlers
async fn handle_list_groups(&self) -> Result<serde_json::Value, McpError> {
let groups = GROUP_MANAGER
.lock()
.map_err(|e| McpError {
code: -32000,
message: format!("Failed to lock group manager: {e}"),
})?
.get_all_groups()
.map_err(|e| McpError {
code: -32000,
message: format!("Failed to list groups: {e}"),
})?;
Ok(serde_json::json!({
"content": [{
"type": "text",
"text": serde_json::to_string_pretty(&groups).unwrap_or_default()
}]
}))
}
async fn handle_get_group(
&self,
arguments: &serde_json::Value,
) -> Result<serde_json::Value, McpError> {
let group_id = arguments
.get("group_id")
.and_then(|v| v.as_str())
.ok_or_else(|| McpError {
code: -32602,
message: "Missing group_id".to_string(),
})?;
let groups = GROUP_MANAGER
.lock()
.map_err(|e| McpError {
code: -32000,
message: format!("Failed to lock group manager: {e}"),
})?
.get_all_groups()
.map_err(|e| McpError {
code: -32000,
message: format!("Failed to list groups: {e}"),
})?;
let group = groups
.iter()
.find(|g| g.id == group_id)
.ok_or_else(|| McpError {
code: -32000,
message: format!("Group not found: {group_id}"),
})?;
Ok(serde_json::json!({
"content": [{
"type": "text",
"text": serde_json::to_string_pretty(&group).unwrap_or_default()
}]
}))
}
async fn handle_create_group(
&self,
arguments: &serde_json::Value,
) -> Result<serde_json::Value, McpError> {
let name = arguments
.get("name")
.and_then(|v| v.as_str())
.ok_or_else(|| McpError {
code: -32602,
message: "Missing name".to_string(),
})?;
let inner = self.inner.lock().await;
let app_handle = inner.app_handle.as_ref().ok_or_else(|| McpError {
code: -32000,
message: "MCP server not properly initialized".to_string(),
})?;
let group = GROUP_MANAGER
.lock()
.map_err(|e| McpError {
code: -32000,
message: format!("Failed to lock group manager: {e}"),
})?
.create_group(app_handle, name.to_string())
.map_err(|e| McpError {
code: -32000,
message: format!("Failed to create group: {e}"),
})?;
Ok(serde_json::json!({
"content": [{
"type": "text",
"text": format!("Group '{}' created successfully with ID: {}", group.name, group.id)
}]
}))
}
async fn handle_update_group(
&self,
arguments: &serde_json::Value,
) -> Result<serde_json::Value, McpError> {
let group_id = arguments
.get("group_id")
.and_then(|v| v.as_str())
.ok_or_else(|| McpError {
code: -32602,
message: "Missing group_id".to_string(),
})?;
let name = arguments
.get("name")
.and_then(|v| v.as_str())
.ok_or_else(|| McpError {
code: -32602,
message: "Missing name".to_string(),
})?;
let inner = self.inner.lock().await;
let app_handle = inner.app_handle.as_ref().ok_or_else(|| McpError {
code: -32000,
message: "MCP server not properly initialized".to_string(),
})?;
let group = GROUP_MANAGER
.lock()
.map_err(|e| McpError {
code: -32000,
message: format!("Failed to lock group manager: {e}"),
})?
.update_group(app_handle, group_id.to_string(), name.to_string())
.map_err(|e| McpError {
code: -32000,
message: format!("Failed to update group: {e}"),
})?;
Ok(serde_json::json!({
"content": [{
"type": "text",
"text": format!("Group '{}' updated successfully", group.name)
}]
}))
}
async fn handle_delete_group(
&self,
arguments: &serde_json::Value,
) -> Result<serde_json::Value, McpError> {
let group_id = arguments
.get("group_id")
.and_then(|v| v.as_str())
.ok_or_else(|| McpError {
code: -32602,
message: "Missing group_id".to_string(),
})?;
let inner = self.inner.lock().await;
let app_handle = inner.app_handle.as_ref().ok_or_else(|| McpError {
code: -32000,
message: "MCP server not properly initialized".to_string(),
})?;
GROUP_MANAGER
.lock()
.map_err(|e| McpError {
code: -32000,
message: format!("Failed to lock group manager: {e}"),
})?
.delete_group(app_handle, group_id.to_string())
.map_err(|e| McpError {
code: -32000,
message: format!("Failed to delete group: {e}"),
})?;
Ok(serde_json::json!({
"content": [{
"type": "text",
"text": format!("Group '{}' deleted successfully", group_id)
}]
}))
}
async fn handle_assign_profiles_to_group(
&self,
arguments: &serde_json::Value,
) -> Result<serde_json::Value, McpError> {
let profile_ids: Vec<String> = arguments
.get("profile_ids")
.and_then(|v| v.as_array())
.ok_or_else(|| McpError {
code: -32602,
message: "Missing profile_ids".to_string(),
})?
.iter()
.filter_map(|v| v.as_str().map(|s| s.to_string()))
.collect();
let group_id = arguments
.get("group_id")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let inner = self.inner.lock().await;
let app_handle = inner.app_handle.as_ref().ok_or_else(|| McpError {
code: -32000,
message: "MCP server not properly initialized".to_string(),
})?;
ProfileManager::instance()
.assign_profiles_to_group(app_handle, profile_ids.clone(), group_id.clone())
.map_err(|e| McpError {
code: -32000,
message: format!("Failed to assign profiles to group: {e}"),
})?;
let group_name = group_id.as_deref().unwrap_or("default");
Ok(serde_json::json!({
"content": [{
"type": "text",
"text": format!("{} profile(s) assigned to group '{}'", profile_ids.len(), group_name)
}]
}))
}
// Full proxy management handlers
async fn handle_get_proxy(
&self,
arguments: &serde_json::Value,
) -> Result<serde_json::Value, McpError> {
let proxy_id = arguments
.get("proxy_id")
.and_then(|v| v.as_str())
.ok_or_else(|| McpError {
code: -32602,
message: "Missing proxy_id".to_string(),
})?;
let proxies = PROXY_MANAGER.get_stored_proxies();
let proxy = proxies
.iter()
.find(|p| p.id == proxy_id)
.ok_or_else(|| McpError {
code: -32000,
message: format!("Proxy not found: {proxy_id}"),
})?;
Ok(serde_json::json!({
"content": [{
"type": "text",
"text": serde_json::to_string_pretty(&proxy).unwrap_or_default()
}]
}))
}
async fn handle_create_proxy(
&self,
arguments: &serde_json::Value,
) -> Result<serde_json::Value, McpError> {
let name = arguments
.get("name")
.and_then(|v| v.as_str())
.ok_or_else(|| McpError {
code: -32602,
message: "Missing name".to_string(),
})?;
let proxy_type = arguments
.get("proxy_type")
.and_then(|v| v.as_str())
.ok_or_else(|| McpError {
code: -32602,
message: "Missing proxy_type".to_string(),
})?;
let host = arguments
.get("host")
.and_then(|v| v.as_str())
.ok_or_else(|| McpError {
code: -32602,
message: "Missing host".to_string(),
})?;
let port = arguments
.get("port")
.and_then(|v| v.as_u64())
.ok_or_else(|| McpError {
code: -32602,
message: "Missing port".to_string(),
})? as u16;
let username = arguments
.get("username")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let password = arguments
.get("password")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let proxy_settings = ProxySettings {
proxy_type: proxy_type.to_string(),
host: host.to_string(),
port,
username,
password,
};
let inner = self.inner.lock().await;
let app_handle = inner.app_handle.as_ref().ok_or_else(|| McpError {
code: -32000,
message: "MCP server not properly initialized".to_string(),
})?;
let proxy = PROXY_MANAGER
.create_stored_proxy(app_handle, name.to_string(), proxy_settings)
.map_err(|e| McpError {
code: -32000,
message: format!("Failed to create proxy: {e}"),
})?;
Ok(serde_json::json!({
"content": [{
"type": "text",
"text": format!("Proxy '{}' created successfully with ID: {}", proxy.name, proxy.id)
}]
}))
}
async fn handle_update_proxy(
&self,
arguments: &serde_json::Value,
) -> Result<serde_json::Value, McpError> {
let proxy_id = arguments
.get("proxy_id")
.and_then(|v| v.as_str())
.ok_or_else(|| McpError {
code: -32602,
message: "Missing proxy_id".to_string(),
})?;
let name = arguments
.get("name")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
// Build proxy_settings if any settings fields are provided
let has_settings = arguments.get("proxy_type").is_some()
|| arguments.get("host").is_some()
|| arguments.get("port").is_some();
let proxy_settings = if has_settings {
// Get existing proxy to use as defaults
let proxies = PROXY_MANAGER.get_stored_proxies();
let existing = proxies
.iter()
.find(|p| p.id == proxy_id)
.ok_or_else(|| McpError {
code: -32000,
message: format!("Proxy not found: {proxy_id}"),
})?;
let proxy_type = arguments
.get("proxy_type")
.and_then(|v| v.as_str())
.map(|s| s.to_string())
.unwrap_or_else(|| existing.proxy_settings.proxy_type.clone());
let host = arguments
.get("host")
.and_then(|v| v.as_str())
.map(|s| s.to_string())
.unwrap_or_else(|| existing.proxy_settings.host.clone());
let port = arguments
.get("port")
.and_then(|v| v.as_u64())
.map(|p| p as u16)
.unwrap_or(existing.proxy_settings.port);
let username = arguments
.get("username")
.and_then(|v| v.as_str())
.map(|s| s.to_string())
.or_else(|| existing.proxy_settings.username.clone());
let password = arguments
.get("password")
.and_then(|v| v.as_str())
.map(|s| s.to_string())
.or_else(|| existing.proxy_settings.password.clone());
Some(ProxySettings {
proxy_type,
host,
port,
username,
password,
})
} else {
None
};
let inner = self.inner.lock().await;
let app_handle = inner.app_handle.as_ref().ok_or_else(|| McpError {
code: -32000,
message: "MCP server not properly initialized".to_string(),
})?;
let proxy = PROXY_MANAGER
.update_stored_proxy(app_handle, proxy_id, name, proxy_settings)
.map_err(|e| McpError {
code: -32000,
message: format!("Failed to update proxy: {e}"),
})?;
Ok(serde_json::json!({
"content": [{
"type": "text",
"text": format!("Proxy '{}' updated successfully", proxy.name)
}]
}))
}
async fn handle_delete_proxy(
&self,
arguments: &serde_json::Value,
) -> Result<serde_json::Value, McpError> {
let proxy_id = arguments
.get("proxy_id")
.and_then(|v| v.as_str())
.ok_or_else(|| McpError {
code: -32602,
message: "Missing proxy_id".to_string(),
})?;
let inner = self.inner.lock().await;
let app_handle = inner.app_handle.as_ref().ok_or_else(|| McpError {
code: -32000,
message: "MCP server not properly initialized".to_string(),
})?;
PROXY_MANAGER
.delete_stored_proxy(app_handle, proxy_id)
.map_err(|e| McpError {
code: -32000,
message: format!("Failed to delete proxy: {e}"),
})?;
Ok(serde_json::json!({
"content": [{
"type": "text",
"text": format!("Proxy '{}' deleted successfully", proxy_id)
}]
}))
}
async fn handle_export_proxies(
&self,
arguments: &serde_json::Value,
) -> Result<serde_json::Value, McpError> {
let format = arguments
.get("format")
.and_then(|v| v.as_str())
.ok_or_else(|| McpError {
code: -32602,
message: "Missing format".to_string(),
})?;
let content = match format {
"json" => PROXY_MANAGER.export_proxies_json().map_err(|e| McpError {
code: -32000,
message: format!("Failed to export proxies: {e}"),
})?,
"txt" => PROXY_MANAGER.export_proxies_txt(),
_ => {
return Err(McpError {
code: -32602,
message: format!("Invalid format '{}', must be 'json' or 'txt'", format),
})
}
};
Ok(serde_json::json!({
"content": [{
"type": "text",
"text": content
}]
}))
}
async fn handle_import_proxies(
&self,
arguments: &serde_json::Value,
) -> Result<serde_json::Value, McpError> {
let content = arguments
.get("content")
.and_then(|v| v.as_str())
.ok_or_else(|| McpError {
code: -32602,
message: "Missing content".to_string(),
})?;
let format = arguments
.get("format")
.and_then(|v| v.as_str())
.ok_or_else(|| McpError {
code: -32602,
message: "Missing format".to_string(),
})?;
let name_prefix = arguments
.get("name_prefix")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let inner = self.inner.lock().await;
let app_handle = inner.app_handle.as_ref().ok_or_else(|| McpError {
code: -32000,
message: "MCP server not properly initialized".to_string(),
})?;
let result = match format {
"json" => PROXY_MANAGER
.import_proxies_json(app_handle, content)
.map_err(|e| McpError {
code: -32000,
message: format!("Failed to import proxies: {e}"),
})?,
"txt" => {
use crate::proxy_manager::{ProxyManager, ProxyParseResult};
let parse_results = ProxyManager::parse_txt_proxies(content);
let parsed: Vec<_> = parse_results
.into_iter()
.filter_map(|r| {
if let ProxyParseResult::Parsed(p) = r {
Some(p)
} else {
None
}
})
.collect();
if parsed.is_empty() {
return Err(McpError {
code: -32000,
message: "No valid proxies found in content".to_string(),
});
}
PROXY_MANAGER
.import_proxies_from_parsed(app_handle, parsed, name_prefix)
.map_err(|e| McpError {
code: -32000,
message: format!("Failed to import proxies: {e}"),
})?
}
_ => {
return Err(McpError {
code: -32602,
message: format!("Invalid format '{}', must be 'json' or 'txt'", format),
})
}
};
Ok(serde_json::json!({
"content": [{
"type": "text",
"text": format!(
"Import complete: {} imported, {} skipped, {} errors",
result.imported_count,
result.skipped_count,
result.errors.len()
)
}]
}))
}
// VPN management handlers
async fn handle_import_vpn(
&self,
arguments: &serde_json::Value,
) -> Result<serde_json::Value, McpError> {
let content = arguments
.get("content")
.and_then(|v| v.as_str())
.ok_or_else(|| McpError {
code: -32602,
message: "Missing content".to_string(),
})?;
let filename = arguments
.get("filename")
.and_then(|v| v.as_str())
.ok_or_else(|| McpError {
code: -32602,
message: "Missing filename".to_string(),
})?;
let name = arguments
.get("name")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let storage = crate::vpn::VPN_STORAGE.lock().map_err(|e| McpError {
code: -32000,
message: format!("Failed to lock VPN storage: {e}"),
})?;
let config = storage
.import_config(content, filename, name)
.map_err(|e| McpError {
code: -32000,
message: format!("Failed to import VPN config: {e}"),
})?;
Ok(serde_json::json!({
"content": [{
"type": "text",
"text": format!(
"VPN '{}' ({}) imported successfully with ID: {}",
config.name,
config.vpn_type,
config.id
)
}]
}))
}
async fn handle_list_vpn_configs(&self) -> Result<serde_json::Value, McpError> {
let storage = crate::vpn::VPN_STORAGE.lock().map_err(|e| McpError {
code: -32000,
message: format!("Failed to lock VPN storage: {e}"),
})?;
let configs = storage.list_configs().map_err(|e| McpError {
code: -32000,
message: format!("Failed to list VPN configs: {e}"),
})?;
Ok(serde_json::json!({
"content": [{
"type": "text",
"text": serde_json::to_string_pretty(&configs).unwrap_or_default()
}]
}))
}
async fn handle_delete_vpn(
&self,
arguments: &serde_json::Value,
) -> Result<serde_json::Value, McpError> {
let vpn_id = arguments
.get("vpn_id")
.and_then(|v| v.as_str())
.ok_or_else(|| McpError {
code: -32602,
message: "Missing vpn_id".to_string(),
})?;
// First disconnect if connected
{
let mut manager = crate::vpn::TUNNEL_MANAGER.lock().await;
if manager.is_tunnel_active(vpn_id) {
if let Some(tunnel) = manager.get_tunnel_mut(vpn_id) {
let _ = tunnel.disconnect().await;
}
manager.remove_tunnel(vpn_id);
}
}
let storage = crate::vpn::VPN_STORAGE.lock().map_err(|e| McpError {
code: -32000,
message: format!("Failed to lock VPN storage: {e}"),
})?;
storage.delete_config(vpn_id).map_err(|e| McpError {
code: -32000,
message: format!("Failed to delete VPN config: {e}"),
})?;
Ok(serde_json::json!({
"content": [{
"type": "text",
"text": format!("VPN '{}' deleted successfully", vpn_id)
}]
}))
}
async fn handle_connect_vpn(
&self,
arguments: &serde_json::Value,
) -> Result<serde_json::Value, McpError> {
let vpn_id = arguments
.get("vpn_id")
.and_then(|v| v.as_str())
.ok_or_else(|| McpError {
code: -32602,
message: "Missing vpn_id".to_string(),
})?;
// Load config from storage
let config = {
let storage = crate::vpn::VPN_STORAGE.lock().map_err(|e| McpError {
code: -32000,
message: format!("Failed to lock VPN storage: {e}"),
})?;
storage.load_config(vpn_id).map_err(|e| McpError {
code: -32000,
message: format!("Failed to load VPN config: {e}"),
})?
};
let mut manager = crate::vpn::TUNNEL_MANAGER.lock().await;
// Check if already connected
if manager.is_tunnel_active(vpn_id) {
return Ok(serde_json::json!({
"content": [{
"type": "text",
"text": format!("VPN '{}' is already connected", config.name)
}]
}));
}
let mut tunnel: Box<dyn crate::vpn::VpnTunnel> = match config.vpn_type {
crate::vpn::VpnType::WireGuard => {
let wg_config =
crate::vpn::parse_wireguard_config(&config.config_data).map_err(|e| McpError {
code: -32000,
message: format!("Invalid WireGuard config: {e}"),
})?;
Box::new(crate::vpn::WireGuardTunnel::new(
vpn_id.to_string(),
wg_config,
))
}
crate::vpn::VpnType::OpenVPN => {
let ovpn_config =
crate::vpn::parse_openvpn_config(&config.config_data).map_err(|e| McpError {
code: -32000,
message: format!("Invalid OpenVPN config: {e}"),
})?;
Box::new(crate::vpn::OpenVpnTunnel::new(
vpn_id.to_string(),
ovpn_config,
))
}
};
tunnel.connect().await.map_err(|e| McpError {
code: -32000,
message: format!("Failed to connect VPN: {e}"),
})?;
manager.register_tunnel(vpn_id.to_string(), tunnel);
// Update last_used timestamp
{
let storage = crate::vpn::VPN_STORAGE.lock().map_err(|e| McpError {
code: -32000,
message: format!("Failed to lock VPN storage: {e}"),
})?;
let _ = storage.update_last_used(vpn_id);
}
Ok(serde_json::json!({
"content": [{
"type": "text",
"text": format!("VPN '{}' connected successfully", config.name)
}]
}))
}
async fn handle_disconnect_vpn(
&self,
arguments: &serde_json::Value,
) -> Result<serde_json::Value, McpError> {
let vpn_id = arguments
.get("vpn_id")
.and_then(|v| v.as_str())
.ok_or_else(|| McpError {
code: -32602,
message: "Missing vpn_id".to_string(),
})?;
let mut manager = crate::vpn::TUNNEL_MANAGER.lock().await;
if let Some(tunnel) = manager.get_tunnel_mut(vpn_id) {
tunnel.disconnect().await.map_err(|e| McpError {
code: -32000,
message: format!("Failed to disconnect VPN: {e}"),
})?;
}
manager.remove_tunnel(vpn_id);
Ok(serde_json::json!({
"content": [{
"type": "text",
"text": format!("VPN '{}' disconnected successfully", vpn_id)
}]
}))
}
async fn handle_get_vpn_status(
&self,
arguments: &serde_json::Value,
) -> Result<serde_json::Value, McpError> {
let vpn_id = arguments
.get("vpn_id")
.and_then(|v| v.as_str())
.ok_or_else(|| McpError {
code: -32602,
message: "Missing vpn_id".to_string(),
})?;
let manager = crate::vpn::TUNNEL_MANAGER.lock().await;
let status = if let Some(tunnel) = manager.get_tunnel(vpn_id) {
tunnel.get_status()
} else {
crate::vpn::VpnStatus {
connected: false,
vpn_id: vpn_id.to_string(),
connected_at: None,
bytes_sent: None,
bytes_received: None,
last_handshake: None,
}
};
Ok(serde_json::json!({
"content": [{
"type": "text",
"text": serde_json::to_string_pretty(&status).unwrap_or_default()
}]
}))
}
}
lazy_static::lazy_static! {
static ref MCP_SERVER: McpServer = McpServer::new();
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_mcp_tools_count() {
let server = McpServer::new();
let tools = server.get_tools();
// Should have at least 24 tools (18 + 6 VPN tools)
assert!(tools.len() >= 24);
// Check tool names
let tool_names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
// Profile tools
assert!(tool_names.contains(&"list_profiles"));
assert!(tool_names.contains(&"get_profile"));
assert!(tool_names.contains(&"run_profile"));
assert!(tool_names.contains(&"kill_profile"));
assert!(tool_names.contains(&"get_profile_status"));
// Group tools
assert!(tool_names.contains(&"list_groups"));
assert!(tool_names.contains(&"get_group"));
assert!(tool_names.contains(&"create_group"));
assert!(tool_names.contains(&"update_group"));
assert!(tool_names.contains(&"delete_group"));
assert!(tool_names.contains(&"assign_profiles_to_group"));
// Proxy tools
assert!(tool_names.contains(&"list_proxies"));
assert!(tool_names.contains(&"get_proxy"));
assert!(tool_names.contains(&"create_proxy"));
assert!(tool_names.contains(&"update_proxy"));
assert!(tool_names.contains(&"delete_proxy"));
// Proxy import/export tools
assert!(tool_names.contains(&"export_proxies"));
assert!(tool_names.contains(&"import_proxies"));
// VPN tools
assert!(tool_names.contains(&"import_vpn"));
assert!(tool_names.contains(&"list_vpn_configs"));
assert!(tool_names.contains(&"delete_vpn"));
assert!(tool_names.contains(&"connect_vpn"));
assert!(tool_names.contains(&"disconnect_vpn"));
assert!(tool_names.contains(&"get_vpn_status"));
}
#[test]
fn test_mcp_server_initial_state() {
let server = McpServer::new();
assert!(!server.is_running());
}
}