mirror of
https://github.com/zhom/donutbrowser.git
synced 2026-04-27 06:16:25 +02:00
refactor: match API spec in MCP
This commit is contained in:
@@ -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
@@ -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": [{
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -384,7 +384,8 @@
|
||||
"token": "MCPトークン",
|
||||
"config": "MCP設定",
|
||||
"copyConfig": "設定をコピー"
|
||||
}
|
||||
},
|
||||
"mcpCopyHint": "MCPクライアントの設定にこれを追加して接続してください。"
|
||||
},
|
||||
"import": {
|
||||
"title": "プロファイルをインポート",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -384,7 +384,8 @@
|
||||
"token": "MCP токен",
|
||||
"config": "Конфигурация MCP",
|
||||
"copyConfig": "Копировать конфигурацию"
|
||||
}
|
||||
},
|
||||
"mcpCopyHint": "Добавьте это в конфигурацию вашего MCP-клиента для подключения."
|
||||
},
|
||||
"import": {
|
||||
"title": "Импорт профиля",
|
||||
|
||||
@@ -384,7 +384,8 @@
|
||||
"token": "MCP 令牌",
|
||||
"config": "MCP 配置",
|
||||
"copyConfig": "复制配置"
|
||||
}
|
||||
},
|
||||
"mcpCopyHint": "将此添加到您的MCP客户端配置中以进行连接。"
|
||||
},
|
||||
"import": {
|
||||
"title": "导入配置文件",
|
||||
|
||||
Reference in New Issue
Block a user