iflow-mcp_janspoerer-mcp_browser_use 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.
Files changed (50) hide show
  1. iflow_mcp_janspoerer_mcp_browser_use-0.1.0.dist-info/METADATA +26 -0
  2. iflow_mcp_janspoerer_mcp_browser_use-0.1.0.dist-info/RECORD +50 -0
  3. iflow_mcp_janspoerer_mcp_browser_use-0.1.0.dist-info/WHEEL +5 -0
  4. iflow_mcp_janspoerer_mcp_browser_use-0.1.0.dist-info/entry_points.txt +2 -0
  5. iflow_mcp_janspoerer_mcp_browser_use-0.1.0.dist-info/licenses/LICENSE +201 -0
  6. iflow_mcp_janspoerer_mcp_browser_use-0.1.0.dist-info/top_level.txt +1 -0
  7. mcp_browser_use/__init__.py +2 -0
  8. mcp_browser_use/__main__.py +1347 -0
  9. mcp_browser_use/actions/__init__.py +1 -0
  10. mcp_browser_use/actions/elements.py +173 -0
  11. mcp_browser_use/actions/extraction.py +864 -0
  12. mcp_browser_use/actions/keyboard.py +43 -0
  13. mcp_browser_use/actions/navigation.py +73 -0
  14. mcp_browser_use/actions/screenshots.py +85 -0
  15. mcp_browser_use/browser/__init__.py +1 -0
  16. mcp_browser_use/browser/chrome.py +150 -0
  17. mcp_browser_use/browser/chrome_executable.py +204 -0
  18. mcp_browser_use/browser/chrome_launcher.py +330 -0
  19. mcp_browser_use/browser/chrome_process.py +104 -0
  20. mcp_browser_use/browser/devtools.py +230 -0
  21. mcp_browser_use/browser/driver.py +322 -0
  22. mcp_browser_use/browser/process.py +133 -0
  23. mcp_browser_use/cleaners.py +530 -0
  24. mcp_browser_use/config/__init__.py +30 -0
  25. mcp_browser_use/config/environment.py +155 -0
  26. mcp_browser_use/config/paths.py +97 -0
  27. mcp_browser_use/constants.py +68 -0
  28. mcp_browser_use/context.py +150 -0
  29. mcp_browser_use/context_pack.py +85 -0
  30. mcp_browser_use/decorators/__init__.py +13 -0
  31. mcp_browser_use/decorators/ensure.py +84 -0
  32. mcp_browser_use/decorators/envelope.py +83 -0
  33. mcp_browser_use/decorators/locking.py +172 -0
  34. mcp_browser_use/helpers.py +173 -0
  35. mcp_browser_use/helpers_context.py +261 -0
  36. mcp_browser_use/locking/__init__.py +1 -0
  37. mcp_browser_use/locking/action_lock.py +190 -0
  38. mcp_browser_use/locking/file_mutex.py +139 -0
  39. mcp_browser_use/locking/window_registry.py +178 -0
  40. mcp_browser_use/tools/__init__.py +59 -0
  41. mcp_browser_use/tools/browser_management.py +260 -0
  42. mcp_browser_use/tools/debugging.py +195 -0
  43. mcp_browser_use/tools/extraction.py +58 -0
  44. mcp_browser_use/tools/interaction.py +323 -0
  45. mcp_browser_use/tools/navigation.py +84 -0
  46. mcp_browser_use/tools/screenshots.py +116 -0
  47. mcp_browser_use/utils/__init__.py +1 -0
  48. mcp_browser_use/utils/diagnostics.py +85 -0
  49. mcp_browser_use/utils/html_utils.py +118 -0
  50. mcp_browser_use/utils/retry.py +57 -0
@@ -0,0 +1,322 @@
1
+ """WebDriver creation and window management."""
2
+
3
+ import os
4
+ import time
5
+ import shutil
6
+ import subprocess
7
+ from typing import Optional
8
+ from selenium import webdriver
9
+ from selenium.common.exceptions import (
10
+ NoSuchWindowException,
11
+ WebDriverException,
12
+ )
13
+
14
+ import logging
15
+ logger = logging.getLogger(__name__)
16
+
17
+ # Import context for state management
18
+ from ..context import get_context
19
+ from .devtools import _ensure_debugger_ready, _handle_for_target
20
+ from .process import make_process_tag, ensure_process_tag, chromedriver_log_path
21
+ from ..locking.window_registry import (
22
+ cleanup_orphaned_windows,
23
+ _register_window,
24
+ _unregister_window,
25
+ )
26
+
27
+
28
+ def _ensure_driver() -> None:
29
+ """Attach Selenium to the debuggable Chrome instance (headed by default)."""
30
+ ctx = get_context()
31
+
32
+ if ctx.driver is not None:
33
+ return
34
+
35
+ _ensure_debugger_ready(ctx.config)
36
+
37
+ if not (ctx.debugger_host and ctx.debugger_port):
38
+ return
39
+
40
+ ctx.driver = create_webdriver(
41
+ ctx.debugger_host,
42
+ ctx.debugger_port,
43
+ ctx.config
44
+ )
45
+
46
+
47
+ def _validate_window_context(driver: webdriver.Chrome, expected_target_id: str) -> bool:
48
+ """
49
+ Validate that the current window context matches the expected target.
50
+ Returns True if validation passes, False otherwise.
51
+ Handles NoSuchWindowException gracefully.
52
+ """
53
+ if not expected_target_id:
54
+ return False
55
+
56
+ try:
57
+ # Check if current window handle exists and matches expected target
58
+ current_handle = driver.current_window_handle
59
+ if current_handle and current_handle.endswith(expected_target_id):
60
+ return True
61
+
62
+ # Double-check by getting target info via CDP
63
+ try:
64
+ info = driver.execute_cdp_cmd("Target.getTargetInfo", {}) or {}
65
+ current_target = (info.get("targetInfo") or {}).get("targetId") or info.get("targetId")
66
+ return current_target == expected_target_id
67
+ except Exception:
68
+ pass
69
+
70
+ return False
71
+ except Exception:
72
+ # NoSuchWindowException or other window-related exceptions
73
+ return False
74
+
75
+
76
+ def _ensure_singleton_window(driver: webdriver.Chrome):
77
+ """Ensure we have a singleton window for this process."""
78
+ ctx = get_context()
79
+
80
+ # 0) If we already have a target, validate context
81
+ if ctx.target_id:
82
+ if _validate_window_context(driver, ctx.target_id):
83
+ return
84
+
85
+ # Context validation failed - attempt recovery
86
+ h = _handle_for_target(driver, ctx.target_id)
87
+ if h:
88
+ try:
89
+ driver.switch_to.window(h)
90
+ if _validate_window_context(driver, ctx.target_id):
91
+ return
92
+ except Exception:
93
+ pass
94
+
95
+ # Recovery failed - clear target and recreate
96
+ ctx.reset_window_state()
97
+
98
+ # 1) Create new window if we don't have a target
99
+ if not ctx.target_id:
100
+ # Cleanup orphaned windows
101
+ try:
102
+ cleanup_orphaned_windows(driver)
103
+ except Exception as e:
104
+ logger.debug(f"Window cleanup failed (non-critical): {e}")
105
+
106
+ try:
107
+ win = driver.execute_cdp_cmd("Browser.createWindow", {"state": "normal"})
108
+ if not isinstance(win, dict):
109
+ raise RuntimeError(f"Browser.createWindow returned {win!r}")
110
+
111
+ ctx.window_id = win.get("windowId")
112
+ ctx.target_id = win.get("targetId")
113
+
114
+ if not ctx.target_id:
115
+ # Fallback
116
+ t = driver.execute_cdp_cmd("Target.createTarget", {"url": "about:blank", "newWindow": True})
117
+ if not isinstance(t, dict) or "targetId" not in t:
118
+ raise RuntimeError(f"Target.createTarget returned {t!r}")
119
+
120
+ ctx.target_id = t["targetId"]
121
+
122
+ if not ctx.window_id:
123
+ try:
124
+ w = driver.execute_cdp_cmd("Browser.getWindowForTarget", {"targetId": ctx.target_id}) or {}
125
+ ctx.window_id = w.get("windowId")
126
+ except Exception:
127
+ ctx.window_id = None
128
+ except Exception:
129
+ # Last resort
130
+ t = driver.execute_cdp_cmd("Target.createTarget", {"url": "about:blank", "newWindow": True})
131
+ if not isinstance(t, dict) or "targetId" not in t:
132
+ raise RuntimeError(f"Target.createTarget returned {t!r}")
133
+
134
+ ctx.target_id = t["targetId"]
135
+ try:
136
+ w = driver.execute_cdp_cmd("Browser.getWindowForTarget", {"targetId": ctx.target_id}) or {}
137
+ ctx.window_id = w.get("windowId")
138
+ except Exception:
139
+ ctx.window_id = None
140
+
141
+ # 2) Map targetId -> Selenium handle
142
+ h = _handle_for_target(driver, ctx.target_id)
143
+ if not h:
144
+ for _ in range(20):
145
+ time.sleep(0.05)
146
+ h = _handle_for_target(driver, ctx.target_id)
147
+ if h:
148
+ break
149
+
150
+ if h:
151
+ driver.switch_to.window(h)
152
+
153
+ if not _validate_window_context(driver, ctx.target_id):
154
+ raise RuntimeError(f"Failed to establish correct window context for target {ctx.target_id}")
155
+
156
+ # Register window
157
+ try:
158
+ owner = ensure_process_tag()
159
+ _register_window(owner, ctx.target_id, ctx.window_id)
160
+ except Exception as e:
161
+ logger.debug(f"Window registration failed (non-critical): {e}")
162
+ else:
163
+ raise RuntimeError(f"Failed to find window handle for target {ctx.target_id}")
164
+
165
+
166
+ def _ensure_driver_and_window() -> None:
167
+ """Ensure both driver and window are ready."""
168
+ _ensure_driver()
169
+
170
+ ctx = get_context()
171
+ if ctx.driver is None:
172
+ return
173
+
174
+ _ensure_singleton_window(ctx.driver)
175
+
176
+
177
+ def _close_extra_blank_windows_safe(driver, exclude_handles=None) -> int:
178
+ """Close extra blank windows, only within our own OS window."""
179
+ exclude = set(exclude_handles or ())
180
+
181
+ ctx = get_context()
182
+ own_window_id = ctx.window_id
183
+ if own_window_id is None:
184
+ return 0
185
+
186
+ try:
187
+ keep = driver.current_window_handle
188
+ except Exception:
189
+ keep = None
190
+
191
+ closed = 0
192
+ for h in list(getattr(driver, "window_handles", [])):
193
+ if h in exclude or (keep and h == keep):
194
+ continue
195
+ try:
196
+ driver.switch_to.window(h)
197
+ # Map this handle -> targetId -> windowId
198
+ info = driver.execute_cdp_cmd("Target.getTargetInfo", {}) or {}
199
+ tid = (info.get("targetInfo") or {}).get("targetId") or info.get("targetId")
200
+ if not tid:
201
+ continue
202
+ w = driver.execute_cdp_cmd("Browser.getWindowForTarget", {"targetId": tid}) or {}
203
+ if w.get("windowId") != own_window_id:
204
+ # Belongs to another agent's OS window; do not touch
205
+ continue
206
+
207
+ url = (driver.current_url or "").lower()
208
+ title = (driver.title or "").strip()
209
+ if url in ("about:blank", "chrome://newtab/") or (not url and not title):
210
+ driver.close()
211
+ closed += 1
212
+ except Exception:
213
+ continue
214
+
215
+ # Restore our original window if it still exists
216
+ if keep and keep in getattr(driver, "window_handles", []):
217
+ try:
218
+ driver.switch_to.window(keep)
219
+ except Exception:
220
+ pass
221
+ return closed
222
+
223
+
224
+ def close_singleton_window() -> bool:
225
+ """Close the singleton window without quitting Chrome."""
226
+ ctx = get_context()
227
+
228
+ if ctx.driver is None or not ctx.target_id:
229
+ return False
230
+
231
+ closed = False
232
+ try:
233
+ ctx.driver.execute_cdp_cmd("Target.closeTarget", {"targetId": ctx.target_id})
234
+ closed = True
235
+ except Exception:
236
+ # Fallback
237
+ try:
238
+ h = _handle_for_target(ctx.driver, ctx.target_id)
239
+ if h:
240
+ ctx.driver.switch_to.window(h)
241
+ ctx.driver.close()
242
+ closed = True
243
+ except Exception:
244
+ pass
245
+
246
+ # Unregister window
247
+ if closed:
248
+ try:
249
+ owner = ensure_process_tag()
250
+ _unregister_window(owner)
251
+ except Exception as e:
252
+ logger.debug(f"Window unregistration failed (non-critical): {e}")
253
+
254
+ ctx.reset_window_state()
255
+ return closed
256
+
257
+
258
+ def create_webdriver(debugger_host: str, debugger_port: int, config: dict) -> webdriver.Chrome:
259
+ from selenium.webdriver.chrome.options import Options
260
+ from selenium.webdriver.chrome.service import Service as ChromeService
261
+
262
+ options = Options()
263
+ chrome_path = config.get("chrome_path")
264
+ if chrome_path:
265
+ options.binary_location = chrome_path
266
+ options.add_experimental_option("debuggerAddress", f"{debugger_host}:{debugger_port}")
267
+
268
+ # Handle differing Selenium versions that accept log_output vs. log_path
269
+ log_file = chromedriver_log_path(config)
270
+ try:
271
+ service = ChromeService(log_output=log_file) # newer Selenium
272
+ except TypeError:
273
+ service = ChromeService(log_path=log_file) # older Selenium
274
+
275
+ driver = webdriver.Chrome(service=service, options=options)
276
+ return driver
277
+
278
+
279
+ def _cleanup_own_blank_tabs(driver):
280
+ handle = getattr(driver, "current_window_handle", None)
281
+ try:
282
+ _close_extra_blank_windows_safe(
283
+ driver,
284
+ exclude_handles={handle} if handle else None,
285
+ )
286
+ except Exception:
287
+ pass
288
+
289
+
290
+ def get_chromedriver_capability_version(driver: Optional[webdriver.Chrome] = None) -> Optional[str]:
291
+ """
292
+ Best effort Chromedriver version string.
293
+ - If a driver is provided, prefer driver.capabilities['chromedriverVersion'].
294
+ - Else, fall back to `chromedriver --version` if available in PATH.
295
+ """
296
+ try:
297
+ if driver:
298
+ v = driver.capabilities.get("chromedriverVersion")
299
+ if isinstance(v, str) and v:
300
+ # Typically like "114.0.5735.90 (some hash)"
301
+ return v.split(" ")[0]
302
+ path = shutil.which("chromedriver")
303
+ if path:
304
+ out = subprocess.check_output([path, "--version"], stderr=subprocess.STDOUT).decode().strip()
305
+ return out
306
+ except Exception:
307
+ pass
308
+ return None
309
+
310
+
311
+ __all__ = [
312
+ 'create_webdriver',
313
+ '_ensure_driver',
314
+ '_ensure_driver_and_window',
315
+ '_ensure_singleton_window',
316
+ 'close_singleton_window',
317
+ '_cleanup_own_blank_tabs',
318
+ '_close_extra_blank_windows_safe',
319
+ 'get_chromedriver_capability_version',
320
+ '_validate_window_context',
321
+ 'ensure_process_tag',
322
+ ]
@@ -0,0 +1,133 @@
1
+ """Process and port management."""
2
+
3
+ import os
4
+ import json
5
+ import time
6
+ import socket
7
+ import tempfile
8
+ import psutil
9
+ from typing import Optional, Tuple
10
+
11
+ from ..constants import RENDEZVOUS_TTL_SEC
12
+ from ..config.environment import profile_key
13
+
14
+
15
+ def _is_port_open(host: str, port: int, timeout: float = 0.25) -> bool:
16
+ """Check if a port is open."""
17
+ try:
18
+ with socket.create_connection((host, port), timeout=timeout):
19
+ return True
20
+ except Exception:
21
+ return False
22
+
23
+
24
+ def get_free_port() -> int:
25
+ """Get a free port by binding to port 0."""
26
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
27
+ s.bind(("127.0.0.1", 0))
28
+ return s.getsockname()[1]
29
+
30
+
31
+ def make_process_tag() -> str:
32
+ """Create a unique process tag."""
33
+ import uuid
34
+ return f"agent:{uuid.uuid4().hex}"
35
+
36
+
37
+ def ensure_process_tag() -> str:
38
+ """
39
+ Get or create the process tag for this session.
40
+
41
+ Uses the context to store the tag persistently across the session.
42
+
43
+ Returns:
44
+ The process tag string
45
+ """
46
+ from ..context import get_context
47
+
48
+ ctx = get_context()
49
+ if ctx.process_tag is None:
50
+ ctx.process_tag = make_process_tag()
51
+ return ctx.process_tag
52
+
53
+
54
+ def _read_json(path: str) -> Optional[dict]:
55
+ """Read JSON file, return None on error."""
56
+ try:
57
+ with open(path, "r") as f:
58
+ return json.load(f)
59
+ except Exception:
60
+ return None
61
+
62
+
63
+ def rendezvous_path(config: dict) -> str:
64
+ """Get path to rendezvous file for this profile."""
65
+ return os.path.join(tempfile.gettempdir(), f"mcp_chrome_rendezvous_{profile_key(config)}.json")
66
+
67
+
68
+ def chromedriver_log_path(config: dict) -> str:
69
+ """Get path to chromedriver log file for this profile and process."""
70
+ return os.path.join(tempfile.gettempdir(), f"chromedriver_shared_{profile_key(config)}_{os.getpid()}.log")
71
+
72
+
73
+ def read_rendezvous(config: dict) -> Tuple[Optional[int], Optional[int]]:
74
+ """
75
+ Read rendezvous file to find existing Chrome debug port and PID.
76
+
77
+ Returns:
78
+ Tuple of (port, pid) or (None, None) if not found/invalid
79
+ """
80
+ from .devtools import is_debugger_listening
81
+
82
+ path = rendezvous_path(config)
83
+ try:
84
+ if not os.path.exists(path):
85
+ return None, None
86
+ if (time.time() - os.path.getmtime(path)) > RENDEZVOUS_TTL_SEC:
87
+ return None, None
88
+ data = _read_json(path) or {}
89
+ port = int(data.get("port", 0)) or None
90
+ pid = int(data.get("pid", 0)) or None
91
+ if not port or not pid:
92
+ return None, None
93
+ if not psutil.pid_exists(pid):
94
+ return None, None
95
+ if not is_debugger_listening("127.0.0.1", port):
96
+ return None, None
97
+ return port, pid
98
+ except Exception:
99
+ return None, None
100
+
101
+
102
+ def write_rendezvous(config: dict, port: int, pid: int) -> None:
103
+ """Write rendezvous file with Chrome debug port and PID."""
104
+ path = rendezvous_path(config)
105
+ tmp = path + ".tmp"
106
+ data = {"port": port, "pid": pid, "ts": time.time()}
107
+ try:
108
+ with open(tmp, "w") as f:
109
+ json.dump(data, f)
110
+ os.replace(tmp, path)
111
+ except Exception:
112
+ pass
113
+
114
+
115
+ def clear_rendezvous(config: dict) -> None:
116
+ """Remove rendezvous file."""
117
+ try:
118
+ os.remove(rendezvous_path(config))
119
+ except Exception:
120
+ pass
121
+
122
+
123
+ __all__ = [
124
+ '_is_port_open',
125
+ 'get_free_port',
126
+ 'make_process_tag',
127
+ '_read_json',
128
+ 'read_rendezvous',
129
+ 'write_rendezvous',
130
+ 'clear_rendezvous',
131
+ 'rendezvous_path',
132
+ 'chromedriver_log_path',
133
+ ]