feat: add mcp

This commit is contained in:
zhom
2026-01-11 03:59:00 +04:00
parent 2725cf9316
commit 149ae81246
4 changed files with 641 additions and 5 deletions
+19 -4
View File
@@ -322,10 +322,12 @@ impl ApiServer {
let api = ApiDoc::openapi();
let v1_routes = v1_routes.layer(middleware::from_fn_with_state(
state.clone(),
auth_middleware,
));
let v1_routes = v1_routes
.layer(middleware::from_fn_with_state(
state.clone(),
auth_middleware,
))
.layer(middleware::from_fn(terms_check_middleware));
let app = Router::new()
.nest("/v1", v1_routes)
@@ -363,6 +365,19 @@ impl ApiServer {
}
}
// Terms and Conditions check middleware
async fn terms_check_middleware(
request: axum::extract::Request,
next: Next,
) -> Result<Response, StatusCode> {
// Check if Wayfern terms have been accepted
if !crate::wayfern_terms::WayfernTermsManager::instance().is_terms_accepted() {
return Err(StatusCode::FORBIDDEN);
}
Ok(next.run(request).await)
}
// Authentication middleware
async fn auth_middleware(
State(state): State<ApiServerState>,
+5
View File
@@ -635,6 +635,11 @@ impl Downloader {
browser_str: String,
version: String,
) -> Result<String, Box<dyn std::error::Error + Send + Sync>> {
// Check if Wayfern terms have been accepted before allowing any browser downloads
if !crate::wayfern_terms::WayfernTermsManager::instance().is_terms_accepted() {
return Err("Please accept Wayfern Terms and Conditions before downloading browsers".into());
}
// Check if this browser-version pair is already being downloaded
let download_key = format!("{browser_str}-{version}");
{
+60 -1
View File
@@ -34,8 +34,11 @@ mod settings_manager;
pub mod sync;
pub mod traffic_stats;
mod wayfern_manager;
mod wayfern_terms;
// mod theme_detector; // removed: theme detection handled in webview via CSS prefers-color-scheme
mod commercial_license;
mod cookie_manager;
mod mcp_server;
mod tag_manager;
mod version_updater;
@@ -235,6 +238,54 @@ async fn copy_profile_cookies(
cookie_manager::CookieManager::copy_cookies(&app_handle, request).await
}
#[tauri::command]
fn check_wayfern_terms_accepted() -> bool {
wayfern_terms::WayfernTermsManager::instance().is_terms_accepted()
}
#[tauri::command]
async fn accept_wayfern_terms() -> Result<(), String> {
wayfern_terms::WayfernTermsManager::instance()
.accept_terms()
.await
}
#[tauri::command]
async fn get_commercial_trial_status(
app_handle: tauri::AppHandle,
) -> Result<commercial_license::TrialStatus, String> {
commercial_license::CommercialLicenseManager::instance()
.get_trial_status(&app_handle)
.await
}
#[tauri::command]
async fn acknowledge_trial_expiration(app_handle: tauri::AppHandle) -> Result<(), String> {
commercial_license::CommercialLicenseManager::instance()
.acknowledge_expiration(&app_handle)
.await
}
#[tauri::command]
fn has_acknowledged_trial_expiration(app_handle: tauri::AppHandle) -> Result<bool, String> {
commercial_license::CommercialLicenseManager::instance().has_acknowledged(&app_handle)
}
#[tauri::command]
async fn start_mcp_server(app_handle: tauri::AppHandle) -> Result<(), String> {
mcp_server::McpServer::instance().start(app_handle).await
}
#[tauri::command]
async fn stop_mcp_server() -> Result<(), String> {
mcp_server::McpServer::instance().stop().await
}
#[tauri::command]
fn get_mcp_server_status() -> bool {
mcp_server::McpServer::instance().is_running()
}
#[tauri::command]
async fn is_geoip_database_available() -> Result<bool, String> {
Ok(GeoIPDownloader::is_geoip_database_available())
@@ -841,7 +892,15 @@ pub fn run() {
is_proxy_in_use_by_synced_profile,
is_group_in_use_by_synced_profile,
read_profile_cookies,
copy_profile_cookies
copy_profile_cookies,
check_wayfern_terms_accepted,
accept_wayfern_terms,
get_commercial_trial_status,
acknowledge_trial_expiration,
has_acknowledged_trial_expiration,
start_mcp_server,
stop_mcp_server,
get_mcp_server_status
])
.run(tauri::generate_context!())
.expect("error while running tauri application");
+557
View File
@@ -0,0 +1,557 @@
#![allow(dead_code)]
use serde::{Deserialize, Serialize};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use tauri::AppHandle;
use tokio::sync::Mutex as AsyncMutex;
use crate::profile::{BrowserProfile, ProfileManager};
use crate::proxy_manager::PROXY_MANAGER;
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,
}
struct McpServerInner {
app_handle: Option<AppHandle>,
}
pub struct McpServer {
inner: Arc<AsyncMutex<McpServerInner>>,
is_running: AtomicBool,
}
impl McpServer {
fn new() -> Self {
Self {
inner: Arc::new(AsyncMutex::new(McpServerInner { app_handle: None })),
is_running: AtomicBool::new(false),
}
}
pub fn instance() -> &'static McpServer {
&MCP_SERVER
}
pub fn is_running(&self) -> bool {
self.is_running.load(Ordering::SeqCst)
}
pub async fn start(&self, app_handle: AppHandle) -> Result<(), String> {
// Check terms acceptance first
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 mut inner = self.inner.lock().await;
inner.app_handle = Some(app_handle);
self.is_running.store(true, Ordering::SeqCst);
log::info!("MCP server started");
Ok(())
}
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;
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"]
}),
},
]
}
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,
_ => 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()
}]
}))
}
}
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 these tools
assert!(tools.len() >= 5);
// Check tool names
let tool_names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
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(&"list_proxies"));
}
#[test]
fn test_mcp_server_initial_state() {
let server = McpServer::new();
assert!(!server.is_running());
}
}