vibesurf 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of vibesurf might be problematic. Click here for more details.

Files changed (70) hide show
  1. vibe_surf/__init__.py +12 -0
  2. vibe_surf/_version.py +34 -0
  3. vibe_surf/agents/__init__.py +0 -0
  4. vibe_surf/agents/browser_use_agent.py +1106 -0
  5. vibe_surf/agents/prompts/__init__.py +1 -0
  6. vibe_surf/agents/prompts/vibe_surf_prompt.py +176 -0
  7. vibe_surf/agents/report_writer_agent.py +360 -0
  8. vibe_surf/agents/vibe_surf_agent.py +1632 -0
  9. vibe_surf/backend/__init__.py +0 -0
  10. vibe_surf/backend/api/__init__.py +3 -0
  11. vibe_surf/backend/api/activity.py +243 -0
  12. vibe_surf/backend/api/config.py +740 -0
  13. vibe_surf/backend/api/files.py +322 -0
  14. vibe_surf/backend/api/models.py +257 -0
  15. vibe_surf/backend/api/task.py +300 -0
  16. vibe_surf/backend/database/__init__.py +13 -0
  17. vibe_surf/backend/database/manager.py +129 -0
  18. vibe_surf/backend/database/models.py +164 -0
  19. vibe_surf/backend/database/queries.py +922 -0
  20. vibe_surf/backend/database/schemas.py +100 -0
  21. vibe_surf/backend/llm_config.py +182 -0
  22. vibe_surf/backend/main.py +137 -0
  23. vibe_surf/backend/migrations/__init__.py +16 -0
  24. vibe_surf/backend/migrations/init_db.py +303 -0
  25. vibe_surf/backend/migrations/seed_data.py +236 -0
  26. vibe_surf/backend/shared_state.py +601 -0
  27. vibe_surf/backend/utils/__init__.py +7 -0
  28. vibe_surf/backend/utils/encryption.py +164 -0
  29. vibe_surf/backend/utils/llm_factory.py +225 -0
  30. vibe_surf/browser/__init__.py +8 -0
  31. vibe_surf/browser/agen_browser_profile.py +130 -0
  32. vibe_surf/browser/agent_browser_session.py +416 -0
  33. vibe_surf/browser/browser_manager.py +296 -0
  34. vibe_surf/browser/utils.py +790 -0
  35. vibe_surf/browser/watchdogs/__init__.py +0 -0
  36. vibe_surf/browser/watchdogs/action_watchdog.py +291 -0
  37. vibe_surf/browser/watchdogs/dom_watchdog.py +954 -0
  38. vibe_surf/chrome_extension/background.js +558 -0
  39. vibe_surf/chrome_extension/config.js +48 -0
  40. vibe_surf/chrome_extension/content.js +284 -0
  41. vibe_surf/chrome_extension/dev-reload.js +47 -0
  42. vibe_surf/chrome_extension/icons/convert-svg.js +33 -0
  43. vibe_surf/chrome_extension/icons/logo-preview.html +187 -0
  44. vibe_surf/chrome_extension/icons/logo.png +0 -0
  45. vibe_surf/chrome_extension/manifest.json +53 -0
  46. vibe_surf/chrome_extension/popup.html +134 -0
  47. vibe_surf/chrome_extension/scripts/api-client.js +473 -0
  48. vibe_surf/chrome_extension/scripts/main.js +491 -0
  49. vibe_surf/chrome_extension/scripts/markdown-it.min.js +3 -0
  50. vibe_surf/chrome_extension/scripts/session-manager.js +599 -0
  51. vibe_surf/chrome_extension/scripts/ui-manager.js +3687 -0
  52. vibe_surf/chrome_extension/sidepanel.html +347 -0
  53. vibe_surf/chrome_extension/styles/animations.css +471 -0
  54. vibe_surf/chrome_extension/styles/components.css +670 -0
  55. vibe_surf/chrome_extension/styles/main.css +2307 -0
  56. vibe_surf/chrome_extension/styles/settings.css +1100 -0
  57. vibe_surf/cli.py +357 -0
  58. vibe_surf/controller/__init__.py +0 -0
  59. vibe_surf/controller/file_system.py +53 -0
  60. vibe_surf/controller/mcp_client.py +68 -0
  61. vibe_surf/controller/vibesurf_controller.py +616 -0
  62. vibe_surf/controller/views.py +37 -0
  63. vibe_surf/llm/__init__.py +21 -0
  64. vibe_surf/llm/openai_compatible.py +237 -0
  65. vibesurf-0.1.0.dist-info/METADATA +97 -0
  66. vibesurf-0.1.0.dist-info/RECORD +70 -0
  67. vibesurf-0.1.0.dist-info/WHEEL +5 -0
  68. vibesurf-0.1.0.dist-info/entry_points.txt +2 -0
  69. vibesurf-0.1.0.dist-info/licenses/LICENSE +201 -0
  70. vibesurf-0.1.0.dist-info/top_level.txt +1 -0
vibe_surf/cli.py ADDED
@@ -0,0 +1,357 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ VibeSurf CLI
4
+ A command-line interface for VibeSurf browser automation tool.
5
+ """
6
+
7
+ import os
8
+ import sys
9
+ import glob
10
+ import socket
11
+ import platform
12
+ import importlib.util
13
+ from pathlib import Path
14
+ from typing import Optional
15
+
16
+ try:
17
+ from rich.console import Console
18
+ from rich.panel import Panel
19
+ from rich.prompt import Prompt, Confirm
20
+ from rich.text import Text
21
+ from rich import print as rprint
22
+ except ImportError:
23
+ print("Error: rich library is required. Install with: pip install rich")
24
+ sys.exit(1)
25
+
26
+ # Logo components with styling for rich panels
27
+ VIBESURF_LOGO = """
28
+ [white]██╗ ██╗██╗██████╗ ███████╗[/] [darkorange]███████╗██╗ ██╗██████╗ ███████╗[/]
29
+ [white]██║ ██║██║██╔══██╗██╔════╝[/] [darkorange]██╔════╝██║ ██║██╔══██╗██╔════╝[/]
30
+ [white]██║ ██║██║██████╔╝█████╗ [/] [darkorange]███████╗██║ ██║██████╔╝█████╗ [/]
31
+ [white]╚██╗ ██╔╝██║██╔══██╗██╔══╝ [/] [darkorange]╚════██║██║ ██║██╔══██╗██╔══╝ [/]
32
+ [white] ╚████╔╝ ██║██████╔╝███████╗[/] [darkorange]███████║╚██████╔╝██║ ██║██║ [/]
33
+ [white] ╚═══╝ ╚═╝╚═════╝ ╚══════╝[/] [darkorange]╚══════╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ [/]
34
+ """
35
+
36
+ console = Console()
37
+
38
+
39
+ def find_chrome_browser() -> Optional[str]:
40
+ """Find Chrome browser executable."""
41
+ system = platform.system()
42
+ patterns = []
43
+
44
+ # Get playwright browsers path from environment variable if set
45
+ playwright_path = os.environ.get('PLAYWRIGHT_BROWSERS_PATH')
46
+
47
+ if system == 'Darwin': # macOS
48
+ if not playwright_path:
49
+ playwright_path = '~/Library/Caches/ms-playwright'
50
+ patterns = [
51
+ '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
52
+ f'{playwright_path}/chromium-*/chrome-mac/Chromium.app/Contents/MacOS/Chromium',
53
+ '/Applications/Chromium.app/Contents/MacOS/Chromium',
54
+ '/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary',
55
+ ]
56
+ elif system == 'Linux':
57
+ if not playwright_path:
58
+ playwright_path = '~/.cache/ms-playwright'
59
+ patterns = [
60
+ '/usr/bin/google-chrome-stable',
61
+ '/usr/bin/google-chrome',
62
+ '/usr/local/bin/google-chrome',
63
+ f'{playwright_path}/chromium-*/chrome-linux/chrome',
64
+ '/usr/bin/chromium',
65
+ '/usr/bin/chromium-browser',
66
+ '/usr/local/bin/chromium',
67
+ '/snap/bin/chromium',
68
+ '/usr/bin/google-chrome-beta',
69
+ '/usr/bin/google-chrome-dev',
70
+ ]
71
+ elif system == 'Windows':
72
+ if not playwright_path:
73
+ playwright_path = r'%LOCALAPPDATA%\ms-playwright'
74
+ patterns = [
75
+ r'C:\Program Files\Google\Chrome\Application\chrome.exe',
76
+ r'C:\Program Files (x86)\Google\Chrome\Application\chrome.exe',
77
+ r'%LOCALAPPDATA%\Google\Chrome\Application\chrome.exe',
78
+ r'%PROGRAMFILES%\Google\Chrome\Application\chrome.exe',
79
+ r'%PROGRAMFILES(X86)%\Google\Chrome\Application\chrome.exe',
80
+ f'{playwright_path}\\chromium-*\\chrome-win\\chrome.exe',
81
+ r'C:\Program Files\Chromium\Application\chrome.exe',
82
+ r'C:\Program Files (x86)\Chromium\Application\chrome.exe',
83
+ r'%LOCALAPPDATA%\Chromium\Application\chrome.exe',
84
+ ]
85
+
86
+ return _find_browser_from_patterns(patterns)
87
+
88
+
89
+ def find_edge_browser() -> Optional[str]:
90
+ """Find Microsoft Edge browser executable."""
91
+ system = platform.system()
92
+ patterns = []
93
+
94
+ if system == 'Darwin': # macOS
95
+ patterns = [
96
+ '/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge',
97
+ ]
98
+ elif system == 'Linux':
99
+ patterns = [
100
+ '/usr/bin/microsoft-edge-stable',
101
+ '/usr/bin/microsoft-edge',
102
+ '/usr/bin/microsoft-edge-beta',
103
+ '/usr/bin/microsoft-edge-dev',
104
+ ]
105
+ elif system == 'Windows':
106
+ patterns = [
107
+ r'C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe',
108
+ r'C:\Program Files\Microsoft\Edge\Application\msedge.exe',
109
+ r'%LOCALAPPDATA%\Microsoft\Edge\Application\msedge.exe',
110
+ ]
111
+
112
+ return _find_browser_from_patterns(patterns)
113
+
114
+
115
+ def _find_browser_from_patterns(patterns: list[str]) -> Optional[str]:
116
+ """Helper function to find browser from patterns."""
117
+ system = platform.system()
118
+
119
+ for pattern in patterns:
120
+ # Expand user home directory
121
+ expanded_pattern = Path(pattern).expanduser()
122
+
123
+ # Handle Windows environment variables
124
+ if system == 'Windows':
125
+ pattern_str = str(expanded_pattern)
126
+ for env_var in ['%LOCALAPPDATA%', '%PROGRAMFILES%', '%PROGRAMFILES(X86)%']:
127
+ if env_var in pattern_str:
128
+ env_key = env_var.strip('%').replace('(X86)', ' (x86)')
129
+ env_value = os.environ.get(env_key, '')
130
+ if env_value:
131
+ pattern_str = pattern_str.replace(env_var, env_value)
132
+ expanded_pattern = Path(pattern_str)
133
+
134
+ # Convert to string for glob
135
+ pattern_str = str(expanded_pattern)
136
+
137
+ # Check if pattern contains wildcards
138
+ if '*' in pattern_str:
139
+ # Use glob to expand the pattern
140
+ matches = glob.glob(pattern_str)
141
+ if matches:
142
+ # Sort matches and take the last one (alphanumerically highest version)
143
+ matches.sort()
144
+ browser_path = matches[-1]
145
+ if Path(browser_path).exists() and Path(browser_path).is_file():
146
+ return browser_path
147
+ else:
148
+ # Direct path check
149
+ if expanded_pattern.exists() and expanded_pattern.is_file():
150
+ return str(expanded_pattern)
151
+
152
+ return None
153
+
154
+
155
+ def is_port_available(port: int) -> bool:
156
+ """Check if a port is available."""
157
+ try:
158
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
159
+ sock.settimeout(1)
160
+ result = sock.connect_ex(('127.0.0.1', port))
161
+ return result != 0
162
+ except Exception:
163
+ return False
164
+
165
+
166
+ def find_available_port(start_port: int) -> int:
167
+ """Find the next available port starting from start_port."""
168
+ port = start_port
169
+ while port <= 65535:
170
+ if is_port_available(port):
171
+ return port
172
+ port += 1
173
+ raise RuntimeError("No available ports found")
174
+
175
+
176
+ def select_browser() -> Optional[str]:
177
+ """Interactive browser selection."""
178
+ console.print("\n[bold cyan]🌐 Browser Selection[/bold cyan]")
179
+ console.print("VibeSurf supports Chrome and Edge browsers.\n")
180
+
181
+ options = []
182
+ browsers = {}
183
+
184
+ # Check for Chrome
185
+ chrome_path = find_chrome_browser()
186
+ if chrome_path:
187
+ options.append("1")
188
+ browsers["1"] = ("Chrome", chrome_path)
189
+ console.print(f"[green]1.[/green] Chrome ([dim]{chrome_path}[/dim])")
190
+
191
+ # Check for Edge
192
+ edge_path = find_edge_browser()
193
+ if edge_path:
194
+ option_num = "2" if "1" not in options else "1" if not chrome_path else "2"
195
+ options.append(option_num)
196
+ browsers[option_num] = ("Edge", edge_path)
197
+ console.print(f"[green]{option_num}.[/green] Microsoft Edge ([dim]{edge_path}[/dim])")
198
+
199
+ # Custom browser option
200
+ custom_option = str(len(options) + 1)
201
+ options.append(custom_option)
202
+ console.print(f"[yellow]{custom_option}.[/yellow] Custom browser path")
203
+
204
+ # Quit option
205
+ quit_option = str(len(options) + 1)
206
+ options.append(quit_option)
207
+ console.print(f"[red]{quit_option}.[/red] Quit")
208
+
209
+ if not chrome_path and not edge_path:
210
+ console.print("\n[yellow]⚠️ No supported browsers found automatically.[/yellow]")
211
+
212
+ while True:
213
+ choice = Prompt.ask(
214
+ "\n[bold]Select a browser",
215
+ choices=options,
216
+ default="1" if options else None
217
+ )
218
+
219
+ if choice == quit_option:
220
+ console.print("[yellow]👋 Goodbye![/yellow]")
221
+ return None
222
+ elif choice == custom_option:
223
+ while True:
224
+ custom_path = Prompt.ask("[bold]Enter browser executable path")
225
+ if custom_path.strip():
226
+ custom_path = custom_path.strip().strip('"\'')
227
+ if Path(custom_path).exists() and Path(custom_path).is_file():
228
+ console.print(f"[green]✅ Browser found: {custom_path}[/green]")
229
+ return custom_path
230
+ else:
231
+ console.print(f"[red]❌ Browser not found: {custom_path}[/red]")
232
+ if not Confirm.ask("[yellow]Try again?[/yellow]", default=True):
233
+ break
234
+ else:
235
+ console.print("[red]❌ Path cannot be empty[/red]")
236
+ else:
237
+ if choice in browsers:
238
+ browser_name, browser_path = browsers[choice]
239
+ console.print(f"[green]✅ Selected {browser_name}: {browser_path}[/green]")
240
+ return browser_path
241
+
242
+ return None
243
+
244
+
245
+ def configure_port() -> int:
246
+ """Configure backend port."""
247
+ console.print("\n[bold cyan]🔌 Port Configuration[/bold cyan]")
248
+
249
+ # Get port from environment variable
250
+ env_port = os.environ.get('VIBESURF_BACKEND_PORT', '').strip()
251
+ default_port = 9335
252
+
253
+ if env_port:
254
+ try:
255
+ default_port = int(env_port)
256
+ except ValueError:
257
+ console.print(f"[yellow]⚠️ Invalid VIBESURF_BACKEND_PORT: {env_port}. Using default: {default_port}[/yellow]")
258
+
259
+ # Check if default port is available
260
+ if is_port_available(default_port):
261
+ console.print(f"[green]✅ Port {default_port} is available[/green]")
262
+ selected_port = default_port
263
+ else:
264
+ console.print(f"[yellow]⚠️ Port {default_port} is occupied, finding next available port...[/yellow]")
265
+ selected_port = find_available_port(default_port + 1)
266
+ console.print(f"[green]✅ Using port {selected_port}[/green]")
267
+
268
+ # Set environment variable
269
+ os.environ['VIBESURF_BACKEND_PORT'] = str(selected_port)
270
+ return selected_port
271
+
272
+
273
+ def configure_extension_path() -> str:
274
+ """Configure extension path."""
275
+ console.print("\n[bold cyan]🧩 Extension Configuration[/bold cyan]")
276
+
277
+ # Get extension path from environment variable
278
+ env_extension = os.environ.get('VIBESURF_EXTENSION', '').strip()
279
+
280
+ if env_extension and Path(env_extension).exists():
281
+ console.print(f"[green]✅ Using extension from environment: {env_extension}[/green]")
282
+ return env_extension
283
+
284
+ # Default to chrome_extension in parent directory of this file
285
+ default_extension = Path(__file__).parent / "chrome_extension"
286
+
287
+ if default_extension.exists():
288
+ extension_path = str(default_extension.resolve())
289
+ console.print(f"[green]✅ Using default extension: {extension_path}[/green]")
290
+ os.environ['VIBESURF_EXTENSION'] = extension_path
291
+ return extension_path
292
+ else:
293
+ console.print(f"[red]❌ Extension not found at: {default_extension}[/red]")
294
+ console.print("[yellow]⚠️ VibeSurf may not function properly without the extension[/yellow]")
295
+ return str(default_extension)
296
+
297
+
298
+ def start_backend(port: int) -> None:
299
+ """Start the VibeSurf backend."""
300
+ console.print(f"\n[bold cyan]🚀 Starting VibeSurf Backend on port {port}[/bold cyan]")
301
+
302
+ try:
303
+ import uvicorn
304
+
305
+ from vibe_surf.backend.main import app
306
+
307
+ console.print("[green]✅ Backend modules loaded successfully[/green]")
308
+ console.print(f"[cyan]🌍 Access VibeSurf at: http://127.0.0.1:{port}[/cyan]")
309
+ console.print("[yellow]📝 Press Ctrl+C to stop the server[/yellow]\n")
310
+
311
+ # Run the server
312
+ uvicorn.run(app, host="127.0.0.1", port=port, log_level="info")
313
+
314
+ except KeyboardInterrupt:
315
+ console.print("\n[yellow]🛑 Server stopped by user[/yellow]")
316
+ except ImportError as e:
317
+ console.print(f"[red]❌ Failed to import backend modules: {e}[/red]")
318
+ console.print("[yellow]💡 Make sure you're running from the VibeSurf project directory[/yellow]")
319
+ sys.exit(1)
320
+ except Exception as e:
321
+ console.print(f"[red]❌ Failed to start backend: {e}[/red]")
322
+ sys.exit(1)
323
+
324
+
325
+ def main():
326
+ """Main CLI entry point."""
327
+ try:
328
+ # Display logo
329
+ console.print(Panel(VIBESURF_LOGO, title="[bold cyan]VibeSurf CLI[/bold cyan]", border_style="cyan"))
330
+ console.print("[dim]A powerful browser automation tool for vibe surfing 🏄‍♂️[/dim]\n")
331
+
332
+ # Browser selection
333
+ browser_path = select_browser()
334
+ if not browser_path:
335
+ return
336
+
337
+ # Port configuration
338
+ port = configure_port()
339
+
340
+ # Extension configuration
341
+ extension_path = configure_extension_path()
342
+
343
+ # Set browser path in environment
344
+ os.environ['VIBESURF_BROWSER_PATH'] = browser_path
345
+
346
+ # Start backend
347
+ start_backend(port)
348
+
349
+ except KeyboardInterrupt:
350
+ console.print("\n[yellow]👋 Goodbye![/yellow]")
351
+ except Exception as e:
352
+ console.print(f"\n[red]❌ Unexpected error: {e}[/red]")
353
+ sys.exit(1)
354
+
355
+
356
+ if __name__ == "__main__":
357
+ main()
File without changes
@@ -0,0 +1,53 @@
1
+ from browser_use.filesystem.file_system import FileSystem, FileSystemError, INVALID_FILENAME_ERROR_MESSAGE
2
+
3
+
4
+ class CustomFileSystem(FileSystem):
5
+ async def read_file(self, full_filename: str, external_file: bool = False) -> str:
6
+ """Read file content using file-specific read method and return appropriate message to LLM"""
7
+ if external_file:
8
+ try:
9
+ try:
10
+ _, extension = self._parse_filename(full_filename)
11
+ except Exception:
12
+ return f'Error: Invalid filename format {full_filename}. Must be alphanumeric with a supported extension.'
13
+ if extension in ['md', 'txt', 'json', 'csv']:
14
+ import anyio
15
+
16
+ async with await anyio.open_file(full_filename, 'r', encoding="utf-8") as f:
17
+ content = await f.read()
18
+ return f'Read from file {full_filename}.\n<content>\n{content}\n</content>'
19
+ elif extension == 'pdf':
20
+ import pypdf
21
+
22
+ reader = pypdf.PdfReader(full_filename)
23
+ num_pages = len(reader.pages)
24
+ MAX_PDF_PAGES = 10
25
+ extra_pages = num_pages - MAX_PDF_PAGES
26
+ extracted_text = ''
27
+ for page in reader.pages[:MAX_PDF_PAGES]:
28
+ extracted_text += page.extract_text()
29
+ extra_pages_text = f'{extra_pages} more pages...' if extra_pages > 0 else ''
30
+ return f'Read from file {full_filename}.\n<content>\n{extracted_text}\n{extra_pages_text}</content>'
31
+ else:
32
+ return f'Error: Cannot read file {full_filename} as {extension} extension is not supported.'
33
+ except FileNotFoundError:
34
+ return f"Error: File '{full_filename}' not found."
35
+ except PermissionError:
36
+ return f"Error: Permission denied to read file '{full_filename}'."
37
+ except Exception as e:
38
+ return f"Error: Could not read file '{full_filename}'."
39
+
40
+ if not self._is_valid_filename(full_filename):
41
+ return INVALID_FILENAME_ERROR_MESSAGE
42
+
43
+ file_obj = self.get_file(full_filename)
44
+ if not file_obj:
45
+ return f"File '{full_filename}' not found."
46
+
47
+ try:
48
+ content = file_obj.read()
49
+ return f'Read from file {full_filename}.\n<content>\n{content}\n</content>'
50
+ except FileSystemError as e:
51
+ return str(e)
52
+ except Exception:
53
+ return f"Error: Could not read file '{full_filename}'."
@@ -0,0 +1,68 @@
1
+ import asyncio
2
+ import logging
3
+ import time
4
+ from typing import Any
5
+
6
+ from pydantic import BaseModel, ConfigDict, Field, create_model
7
+
8
+ from browser_use.agent.views import ActionResult
9
+ from browser_use.controller.registry.service import Registry
10
+ from browser_use.controller.service import Controller
11
+ from browser_use.telemetry import MCPClientTelemetryEvent, ProductTelemetry
12
+ from browser_use.utils import get_browser_use_version
13
+ from browser_use.mcp.client import MCPClient
14
+ from mcp import ClientSession, StdioServerParameters, types
15
+ from mcp.client.stdio import stdio_client
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+
20
+ class VibeSurfMCPClient(MCPClient):
21
+ async def connect(self, timeout: int = 200) -> None:
22
+ """Connect to the MCP server and discover available tools."""
23
+ if self._connected:
24
+ logger.debug(f'Already connected to {self.server_name}')
25
+ return
26
+
27
+ start_time = time.time()
28
+ error_msg = None
29
+
30
+ try:
31
+ logger.info(f"🔌 Connecting to MCP server '{self.server_name}': {self.command} {' '.join(self.args)}")
32
+
33
+ # Create server parameters
34
+ server_params = StdioServerParameters(command=self.command, args=self.args, env=self.env)
35
+
36
+ # Start stdio client in background task
37
+ self._stdio_task = asyncio.create_task(self._run_stdio_client(server_params))
38
+
39
+ # Wait for connection to be established
40
+ retries = 0
41
+ max_retries = timeout / 0.1 # 10 second timeout (increased for parallel test execution)
42
+ while not self._connected and retries < max_retries:
43
+ await asyncio.sleep(0.1)
44
+ retries += 1
45
+
46
+ if not self._connected:
47
+ error_msg = f"Failed to connect to MCP server '{self.server_name}' after {max_retries * 0.1} seconds"
48
+ raise RuntimeError(error_msg)
49
+
50
+ logger.info(f"📦 Discovered {len(self._tools)} tools from '{self.server_name}': {list(self._tools.keys())}")
51
+
52
+ except Exception as e:
53
+ error_msg = str(e)
54
+ raise
55
+ finally:
56
+ # Capture telemetry for connect action
57
+ duration = time.time() - start_time
58
+ self._telemetry.capture(
59
+ MCPClientTelemetryEvent(
60
+ server_name=self.server_name,
61
+ command=self.command,
62
+ tools_discovered=len(self._tools),
63
+ version=get_browser_use_version(),
64
+ action='connect',
65
+ duration_seconds=duration,
66
+ error_message=error_msg,
67
+ )
68
+ )