refactor: match API spec in MCP

This commit is contained in:
zhom
2026-03-14 12:31:34 +04:00
parent 942d193206
commit d0ea3f8903
10 changed files with 390 additions and 44 deletions
+3 -6
View File
@@ -260,9 +260,7 @@ impl MarkovTyper {
if first_error_pos < self.current.len() {
let mut should_correct = false;
if self.last_was_backspace {
should_correct = true;
} else if self.mental_cursor_pos >= self.target.len() {
if self.last_was_backspace || self.mental_cursor_pos >= self.target.len() {
should_correct = true;
} else if !self.current.is_empty() {
let last_char = *self.current.last().unwrap();
@@ -340,10 +338,9 @@ impl MarkovTyper {
}
let typed_char = if self.rng.random::<f64>() < current_prob_error {
let wrong = self
self
.keyboard
.get_random_neighbor(char_intended, &mut self.rng);
wrong
.get_random_neighbor(char_intended, &mut self.rng)
} else {
char_intended
};
+367 -3
View File
@@ -337,6 +337,101 @@ impl McpServer {
"required": ["profile_id"]
}),
},
McpTool {
name: "create_profile".to_string(),
description: "Create a new browser profile".to_string(),
input_schema: serde_json::json!({
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "Name for the new profile"
},
"browser": {
"type": "string",
"enum": ["wayfern", "camoufox"],
"description": "Browser engine to use"
},
"proxy_id": {
"type": "string",
"description": "Optional proxy UUID to assign"
},
"group_id": {
"type": "string",
"description": "Optional group UUID to assign"
},
"tags": {
"type": "array",
"items": { "type": "string" },
"description": "Optional tags for the profile"
}
},
"required": ["name", "browser"]
}),
},
McpTool {
name: "update_profile".to_string(),
description: "Update an existing browser profile's settings".to_string(),
input_schema: serde_json::json!({
"type": "object",
"properties": {
"profile_id": {
"type": "string",
"description": "The UUID of the profile to update"
},
"name": {
"type": "string",
"description": "New name for the profile"
},
"proxy_id": {
"type": "string",
"description": "Proxy UUID to assign (empty string to remove)"
},
"group_id": {
"type": "string",
"description": "Group UUID to assign (empty string to remove)"
},
"tags": {
"type": "array",
"items": { "type": "string" },
"description": "Tags for the profile (replaces existing tags)"
},
"extension_group_id": {
"type": "string",
"description": "Extension group UUID to assign (empty string to remove)"
},
"proxy_bypass_rules": {
"type": "array",
"items": { "type": "string" },
"description": "Proxy bypass rules (replaces existing rules)"
}
},
"required": ["profile_id"]
}),
},
McpTool {
name: "delete_profile".to_string(),
description: "Delete a browser profile and all its data".to_string(),
input_schema: serde_json::json!({
"type": "object",
"properties": {
"profile_id": {
"type": "string",
"description": "The UUID of the profile to delete"
}
},
"required": ["profile_id"]
}),
},
McpTool {
name: "list_tags".to_string(),
description: "List all tags used across profiles".to_string(),
input_schema: serde_json::json!({
"type": "object",
"properties": {},
"required": []
}),
},
McpTool {
name: "list_proxies".to_string(),
description: "List all configured proxies".to_string(),
@@ -926,7 +1021,7 @@ impl McpServer {
},
McpTool {
name: "type_text".to_string(),
description: "Focus an element by CSS selector and type text into it with realistic human-like typing variable speed, natural errors, and self-corrections.".to_string(),
description: "Focus an element by CSS selector and type text into it. By default uses realistic human-like typing with variable speed, natural errors, and self-corrections. Only set instant=true when you are certain the target does not have bot detection (e.g. browser address bars, developer tools, internal apps) — using instant on public websites risks the profile being flagged as a bot.".to_string(),
input_schema: serde_json::json!({
"type": "object",
"properties": {
@@ -946,9 +1041,13 @@ impl McpServer {
"type": "boolean",
"description": "Clear the input before typing (default: true)"
},
"instant": {
"type": "boolean",
"description": "Paste all text at once instead of human typing. WARNING: only use on targets without bot detection — using this on public websites risks the profile being flagged."
},
"wpm": {
"type": "number",
"description": "Target words per minute (default: 60)"
"description": "Target words per minute for human typing (default: 80)"
}
},
"required": ["profile_id", "selector", "text"]
@@ -1068,6 +1167,10 @@ impl McpServer {
"get_profile" => self.handle_get_profile(&arguments).await,
"run_profile" => self.handle_run_profile(&arguments).await,
"kill_profile" => self.handle_kill_profile(&arguments).await,
"create_profile" => self.handle_create_profile(&arguments).await,
"update_profile" => self.handle_update_profile(&arguments).await,
"delete_profile" => self.handle_delete_profile(&arguments).await,
"list_tags" => self.handle_list_tags().await,
"list_proxies" => self.handle_list_proxies().await,
"get_profile_status" => self.handle_get_profile_status(&arguments).await,
// Group management
@@ -1335,6 +1438,253 @@ impl McpServer {
}))
}
async fn handle_create_profile(
&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 browser = arguments
.get("browser")
.and_then(|v| v.as_str())
.ok_or_else(|| McpError {
code: -32602,
message: "Missing browser".to_string(),
})?;
if browser != "wayfern" && browser != "camoufox" {
return Err(McpError {
code: -32602,
message: "browser must be 'wayfern' or 'camoufox'".to_string(),
});
}
let proxy_id = arguments
.get("proxy_id")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let group_id = arguments
.get("group_id")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let tags: Option<Vec<String>> = arguments.get("tags").and_then(|v| {
v.as_array().map(|arr| {
arr
.iter()
.filter_map(|item| item.as_str().map(|s| s.to_string()))
.collect()
})
});
// Pick the latest downloaded version for this browser
let registry = crate::downloaded_browsers_registry::DownloadedBrowsersRegistry::instance();
let versions = registry.get_downloaded_versions(browser);
let version = versions.first().ok_or_else(|| McpError {
code: -32000,
message: format!("No downloaded version found for {browser}. Download it first."),
})?;
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 mut profile = ProfileManager::instance()
.create_profile_with_group(
app_handle, name, browser, version, "stable", proxy_id, None, None, None, group_id, false,
)
.await
.map_err(|e| McpError {
code: -32000,
message: format!("Failed to create profile: {e}"),
})?;
if let Some(tags) = tags {
let _ =
ProfileManager::instance().update_profile_tags(app_handle, &profile.name, tags.clone());
profile.tags = tags;
if let Ok(profiles) = ProfileManager::instance().list_profiles() {
let _ = crate::tag_manager::TAG_MANAGER
.lock()
.map(|manager| manager.rebuild_from_profiles(&profiles));
}
}
Ok(serde_json::json!({
"content": [{
"type": "text",
"text": format!("Profile '{}' created (id: {})", profile.name, profile.id)
}]
}))
}
async fn handle_update_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 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 pm = ProfileManager::instance();
if let Some(new_name) = arguments.get("name").and_then(|v| v.as_str()) {
pm.rename_profile(app_handle, profile_id, new_name)
.map_err(|e| McpError {
code: -32000,
message: format!("Failed to rename profile: {e}"),
})?;
}
if let Some(proxy_id) = arguments.get("proxy_id").and_then(|v| v.as_str()) {
let pid = if proxy_id.is_empty() {
None
} else {
Some(proxy_id.to_string())
};
pm.update_profile_proxy(app_handle.clone(), profile_id, pid)
.await
.map_err(|e| McpError {
code: -32000,
message: format!("Failed to update proxy: {e}"),
})?;
}
if let Some(group_id) = arguments.get("group_id").and_then(|v| v.as_str()) {
let gid = if group_id.is_empty() {
None
} else {
Some(group_id.to_string())
};
pm.assign_profiles_to_group(app_handle, vec![profile_id.to_string()], gid)
.map_err(|e| McpError {
code: -32000,
message: format!("Failed to update group: {e}"),
})?;
}
if let Some(tags) = arguments.get("tags").and_then(|v| v.as_array()) {
let tag_list: Vec<String> = tags
.iter()
.filter_map(|item| item.as_str().map(|s| s.to_string()))
.collect();
pm.update_profile_tags(app_handle, profile_id, tag_list)
.map_err(|e| McpError {
code: -32000,
message: format!("Failed to update tags: {e}"),
})?;
if let Ok(profiles) = pm.list_profiles() {
let _ = crate::tag_manager::TAG_MANAGER
.lock()
.map(|manager| manager.rebuild_from_profiles(&profiles));
}
}
if let Some(ext_group_id) = arguments.get("extension_group_id").and_then(|v| v.as_str()) {
let eid = if ext_group_id.is_empty() {
None
} else {
Some(ext_group_id.to_string())
};
pm.update_profile_extension_group(profile_id, eid)
.map_err(|e| McpError {
code: -32000,
message: format!("Failed to update extension group: {e}"),
})?;
}
if let Some(rules) = arguments
.get("proxy_bypass_rules")
.and_then(|v| v.as_array())
{
let rule_list: Vec<String> = rules
.iter()
.filter_map(|item| item.as_str().map(|s| s.to_string()))
.collect();
pm.update_profile_proxy_bypass_rules(app_handle, profile_id, rule_list)
.map_err(|e| McpError {
code: -32000,
message: format!("Failed to update proxy bypass rules: {e}"),
})?;
}
Ok(serde_json::json!({
"content": [{
"type": "text",
"text": format!("Profile '{profile_id}' updated successfully")
}]
}))
}
async fn handle_delete_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 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()
.delete_profile(app_handle, profile_id)
.map_err(|e| McpError {
code: -32000,
message: format!("Failed to delete profile: {e}"),
})?;
Ok(serde_json::json!({
"content": [{
"type": "text",
"text": format!("Profile '{profile_id}' deleted successfully")
}]
}))
}
async fn handle_list_tags(&self) -> Result<serde_json::Value, McpError> {
let tags = crate::tag_manager::TAG_MANAGER
.lock()
.map_err(|e| McpError {
code: -32000,
message: format!("Failed to access tag manager: {e}"),
})?
.get_all_tags()
.map_err(|e| McpError {
code: -32000,
message: format!("Failed to get tags: {e}"),
})?;
Ok(serde_json::json!({
"content": [{
"type": "text",
"text": serde_json::to_string_pretty(&tags).unwrap_or_default()
}]
}))
}
async fn handle_list_proxies(&self) -> Result<serde_json::Value, McpError> {
let proxies = PROXY_MANAGER.get_stored_proxies();
@@ -3356,6 +3706,10 @@ impl McpServer {
.get("clear_first")
.and_then(|v| v.as_bool())
.unwrap_or(true);
let instant = arguments
.get("instant")
.and_then(|v| v.as_bool())
.unwrap_or(false);
let wpm = arguments.get("wpm").and_then(|v| v.as_f64());
let profile = self.get_running_profile(profile_id)?;
@@ -3413,7 +3767,17 @@ impl McpServer {
});
}
self.send_human_keystrokes(&ws_url, text, wpm).await?;
if instant {
self
.send_cdp(
&ws_url,
"Input.insertText",
serde_json::json!({ "text": text }),
)
.await?;
} else {
self.send_human_keystrokes(&ws_url, text, wpm).await?;
}
Ok(serde_json::json!({
"content": [{
+6 -28
View File
@@ -3,6 +3,7 @@
import { invoke } from "@tauri-apps/api/core";
import { Eye, EyeOff } from "lucide-react";
import { useCallback, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import {
@@ -42,6 +43,7 @@ export function IntegrationsDialog({
isOpen,
onClose,
}: IntegrationsDialogProps) {
const { t } = useTranslation();
const [settings, setSettings] = useState<AppSettings>({
api_enabled: false,
api_port: 10108,
@@ -329,20 +331,7 @@ export function IntegrationsDialog({
</div>
{mcpConfig && (
<div className="space-y-4 p-4 rounded-md border bg-muted/40">
<div className="space-y-2">
<Label className="text-sm font-medium">
Claude Desktop Configuration
</Label>
<p className="text-xs text-muted-foreground">
Copy this configuration to your Claude Desktop config file
at{" "}
<code className="bg-muted px-1 rounded">
~/.config/claude/claude_desktop_config.json
</code>
</p>
</div>
<div className="space-y-3 p-4 rounded-md border bg-muted/40">
<div className="relative">
<pre className="p-3 text-xs font-mono rounded-md bg-background border overflow-x-auto whitespace-pre">
{showMcpToken
@@ -369,20 +358,9 @@ export function IntegrationsDialog({
/>
</div>
</div>
<div className="space-y-2">
<Label className="text-sm font-medium">
Available Tools
</Label>
<ul className="list-disc ml-5 space-y-0.5 text-xs text-muted-foreground">
<li>list_profiles - List browser profiles</li>
<li>run_profile - Launch a browser</li>
<li>kill_profile - Stop a running browser</li>
<li>get_profile_status - Check if browser is running</li>
<li>list_groups, create_group, etc. - Manage groups</li>
<li>list_proxies, create_proxy, etc. - Manage proxies</li>
</ul>
</div>
<p className="text-xs text-muted-foreground">
{t("integrations.mcpCopyHint")}
</p>
</div>
)}
</TabsContent>
+2 -1
View File
@@ -384,7 +384,8 @@
"token": "MCP Token",
"config": "MCP Configuration",
"copyConfig": "Copy Configuration"
}
},
"mcpCopyHint": "Add this to your MCP client config to connect."
},
"import": {
"title": "Import Profile",
+2 -1
View File
@@ -384,7 +384,8 @@
"token": "Token MCP",
"config": "Configuración MCP",
"copyConfig": "Copiar Configuración"
}
},
"mcpCopyHint": "Agrega esto a la configuración de tu cliente MCP para conectarte."
},
"import": {
"title": "Importar Perfil",
+2 -1
View File
@@ -384,7 +384,8 @@
"token": "Jeton MCP",
"config": "Configuration MCP",
"copyConfig": "Copier la configuration"
}
},
"mcpCopyHint": "Ajoutez ceci à la configuration de votre client MCP pour vous connecter."
},
"import": {
"title": "Importer un profil",
+2 -1
View File
@@ -384,7 +384,8 @@
"token": "MCPトークン",
"config": "MCP設定",
"copyConfig": "設定をコピー"
}
},
"mcpCopyHint": "MCPクライアントの設定にこれを追加して接続してください。"
},
"import": {
"title": "プロファイルをインポート",
+2 -1
View File
@@ -384,7 +384,8 @@
"token": "Token MCP",
"config": "Configuração MCP",
"copyConfig": "Copiar Configuração"
}
},
"mcpCopyHint": "Adicione isso à configuração do seu cliente MCP para conectar."
},
"import": {
"title": "Importar Perfil",
+2 -1
View File
@@ -384,7 +384,8 @@
"token": "MCP токен",
"config": "Конфигурация MCP",
"copyConfig": "Копировать конфигурацию"
}
},
"mcpCopyHint": "Добавьте это в конфигурацию вашего MCP-клиента для подключения."
},
"import": {
"title": "Импорт профиля",
+2 -1
View File
@@ -384,7 +384,8 @@
"token": "MCP 令牌",
"config": "MCP 配置",
"copyConfig": "复制配置"
}
},
"mcpCopyHint": "将此添加到您的MCP客户端配置中以进行连接。"
},
"import": {
"title": "导入配置文件",