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": [{