diff --git a/src/mcp_client.py b/src/mcp_client.py index 85d0e470..c7c70a18 100644 --- a/src/mcp_client.py +++ b/src/mcp_client.py @@ -1034,6 +1034,58 @@ def get_tree_result(path: str, max_depth: int = 2) -> Result[str]: return Result(data="\n".join(tree_lines)) except Exception as e: return Result(data="", errors=[ErrorInfo(kind=ErrorKind.INTERNAL, message=str(e), source="mcp.get_tree_result", original=e)]) + +def web_search_result(query: str) -> Result[str]: + url = "https://html.duckduckgo.com/html/?q=" + urllib.parse.quote(query) + req = urllib.request.Request(url, headers={'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'}) + try: + with urllib.request.urlopen(req, timeout=10) as resp: + html = resp.read().decode('utf-8', errors='ignore') + parser = _DDGParser() + parser.feed(html) + if not parser.results: + return Result(data=f"No results found for '{query}'") + lines = [f"Search Results for '{query}':"] + for i, r in enumerate(parser.results[:5], 1): + lines.append(f"{i}. {r['title']}\nURL: {r['link']}\nSnippet: {r['snippet']}\n") + return Result(data="\n".join(lines)) + except Exception as e: + return Result(data="", errors=[ErrorInfo(kind=ErrorKind.INTERNAL, message=str(e), source="mcp.web_search_result", original=e)]) + +def fetch_url_result(url: str) -> Result[str]: + if url.startswith("//duckduckgo.com/l/?uddg="): + split_uddg = url.split("uddg=") + if len(split_uddg) > 1: + url = urllib.parse.unquote(split_uddg[1].split("&")[0]) + if not url.startswith("http"): + url = "https://" + url + req = urllib.request.Request(url, headers={'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36'}) + try: + with urllib.request.urlopen(req, timeout=10) as resp: + html = resp.read().decode('utf-8', errors='ignore') + parser = _TextExtractor() + parser.feed(html) + full_text = " ".join(parser.text) + full_text = _re.sub(r'\s+', ' ', full_text) + if not full_text.strip(): + return Result(data=f"FETCH OK: No readable text extracted from {url}. The page might be empty, JavaScript-heavy, or blocked.") + if len(full_text) > 40000: + return Result(data=full_text[:40000] + "\n... (content truncated)") + return Result(data=full_text) + except Exception as e: + return Result(data="", errors=[ErrorInfo(kind=ErrorKind.INTERNAL, message=str(e), source="mcp.fetch_url_result", original=e)]) + +def get_ui_performance_result() -> Result[str]: + if perf_monitor_callback is None: + return Result(data="INFO: UI Performance monitor is not available (headless/CLI mode). This tool is only functional when the Manual Slop GUI is running.") + try: + metrics = perf_monitor_callback() + metric_str = str(metrics) + for char in "{}'": + metric_str = metric_str.replace(char, "") + return Result(data=f"UI Performance Snapshot:\n{metric_str}") + except Exception as e: + return Result(data="", errors=[ErrorInfo(kind=ErrorKind.INTERNAL, message=f"Failed to retrieve UI performance: {str(e)}", source="mcp.get_ui_performance_result", original=e)]) #endregion: Result Variants def edit_file(path: str, old_string: str, new_string: str, replace_all: bool = False) -> str: @@ -1536,67 +1588,41 @@ class _TextExtractor(HTMLParser): self.text.append(cleaned) def web_search(query: str) -> str: - """Search the web using DuckDuckGo HTML and return top results.""" - url = "https://html.duckduckgo.com/html/?q=" + urllib.parse.quote(query) - req = urllib.request.Request(url, headers={'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'}) - try: - with urllib.request.urlopen(req, timeout=10) as resp: - html = resp.read().decode('utf-8', errors='ignore') - parser = _DDGParser() - parser.feed(html) - if not parser.results: - return f"No results found for '{query}'" - lines = [f"Search Results for '{query}':"] - for i, r in enumerate(parser.results[:5], 1): - lines.append(f"{i}. {r['title']}\nURL: {r['link']}\nSnippet: {r['snippet']}\n") - return "\n".join(lines) - except Exception as e: - return f"ERROR searching web for '{query}': {e}" + """Search the web using DuckDuckGo HTML and return top results. + + Thin wrapper over web_search_result; the legacy str shape is + preserved for backward compatibility, but the try/except Exception + lives in the Result variant. + """ + resolved = web_search_result(query) + if resolved.ok: + return resolved.data + return "; ".join(e.ui_message() for e in resolved.errors) def fetch_url(url: str) -> str: - """Fetch a URL and return its text content (stripped of HTML tags).""" - # Correct duckduckgo redirect links if passed - if url.startswith("//duckduckgo.com/l/?uddg="): - split_uddg = url.split("uddg=") - if len(split_uddg) > 1: - url = urllib.parse.unquote(split_uddg[1].split("&")[0]) - if not url.startswith("http"): - url = "https://" + url - req = urllib.request.Request(url, headers={'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36'}) - try: - with urllib.request.urlopen(req, timeout=10) as resp: - html = resp.read().decode('utf-8', errors='ignore') - parser = _TextExtractor() - parser.feed(html) - full_text = " ".join(parser.text) - full_text = _re.sub(r'\s+', ' ', full_text) - if not full_text.strip(): - return f"FETCH OK: No readable text extracted from {url}. The page might be empty, JavaScript-heavy, or blocked." - # Limit to 40k chars to prevent context blowup - if len(full_text) > 40000: - return full_text[:40000] + "\n... (content truncated)" - return full_text - except Exception as e: - return f"ERROR fetching URL '{url}': {e}" - -#endregion: Web + """Fetch a URL and return its text content (stripped of HTML tags). + Thin wrapper over fetch_url_result; the legacy str shape is + preserved for backward compatibility, but the try/except Exception + lives in the Result variant. + """ + resolved = fetch_url_result(url) + if resolved.ok: + return resolved.data + return "; ".join(e.ui_message() for e in resolved.errors) def get_ui_performance() -> str: """ Returns current UI performance metrics (FPS, Frame Time, CPU, Input Lag). [C: tests/test_mcp_perf_tool.py:test_mcp_perf_tool_retrieval] + + Thin wrapper over get_ui_performance_result; the legacy str shape is + preserved for backward compatibility, but the try/except Exception + lives in the Result variant. """ - if perf_monitor_callback is None: - return "INFO: UI Performance monitor is not available (headless/CLI mode). This tool is only functional when the Manual Slop GUI is running." - try: - metrics = perf_monitor_callback() - # Clean up the dict string for the AI - metric_str = str(metrics) - for char in "{}'": - metric_str = metric_str.replace(char, "") - return f"UI Performance Snapshot:\n{metric_str}" - except Exception as e: - return f"ERROR: Failed to retrieve UI performance: {str(e)}" + resolved = get_ui_performance_result() + if resolved.ok: + return resolved.data + return "; ".join(e.ui_message() for e in resolved.errors) # ------------------------------------------------------------------ tool dispatch class StdioMCPServer: