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,323 @@
1
+ """Element interaction tool implementations."""
2
+
3
+ import json
4
+ import time
5
+ from typing import Optional
6
+ from selenium.common.exceptions import (
7
+ TimeoutException,
8
+ StaleElementReferenceException,
9
+ ElementClickInterceptedException,
10
+ )
11
+ from ..context import get_context
12
+ from ..utils.diagnostics import collect_diagnostics
13
+ from ..actions.elements import find_element, _wait_clickable_element
14
+ from ..actions.navigation import _wait_document_ready
15
+ from ..actions.screenshots import _make_page_snapshot
16
+ from ..utils.retry import retry_op
17
+
18
+
19
+ async def fill_text(
20
+ selector,
21
+ text,
22
+ selector_type,
23
+ clear_first,
24
+ timeout,
25
+ iframe_selector,
26
+ iframe_selector_type,
27
+ shadow_root_selector,
28
+ shadow_root_selector_type,
29
+ ):
30
+ """Fill text into an element."""
31
+ ctx = get_context()
32
+
33
+ try:
34
+ el = retry_op(fn=lambda: find_element(
35
+ driver=ctx.driver,
36
+ selector=selector,
37
+ selector_type=selector_type,
38
+ timeout=int(timeout),
39
+ visible_only=True,
40
+ iframe_selector=iframe_selector,
41
+ iframe_selector_type=iframe_selector_type,
42
+ shadow_root_selector=shadow_root_selector,
43
+ shadow_root_selector_type=shadow_root_selector_type,
44
+ stay_in_context=True,
45
+ ))
46
+
47
+ if clear_first:
48
+ try:
49
+ el.clear()
50
+ except Exception:
51
+ pass
52
+ el.send_keys(text)
53
+ _wait_document_ready(timeout=5.0)
54
+
55
+ snapshot = _make_page_snapshot()
56
+ return json.dumps({"ok": True, "action": "fill_text", "selector": selector, "snapshot": snapshot})
57
+
58
+ except Exception as e:
59
+ diag = collect_diagnostics(driver=ctx.driver, exc=e, config=ctx.config)
60
+ snapshot = _make_page_snapshot()
61
+ return json.dumps({"ok": False, "error": str(e), "diagnostics": diag, "snapshot": snapshot})
62
+
63
+ finally:
64
+ try:
65
+ if ctx.is_driver_initialized():
66
+ ctx.driver.switch_to.default_content()
67
+ except Exception:
68
+ pass
69
+
70
+ async def click_element(
71
+ selector,
72
+ selector_type,
73
+ timeout,
74
+ force_js,
75
+ iframe_selector,
76
+ iframe_selector_type,
77
+ shadow_root_selector,
78
+ shadow_root_selector_type,
79
+ ) -> str:
80
+ """Click an element."""
81
+ ctx = get_context()
82
+
83
+ try:
84
+ el = retry_op(fn=lambda: find_element(
85
+ driver=ctx.driver,
86
+ selector=selector,
87
+ selector_type=selector_type,
88
+ timeout=int(timeout),
89
+ visible_only=True,
90
+ iframe_selector=iframe_selector,
91
+ iframe_selector_type=iframe_selector_type,
92
+ shadow_root_selector=shadow_root_selector,
93
+ shadow_root_selector_type=shadow_root_selector_type,
94
+ stay_in_context=True,
95
+ ))
96
+
97
+ _wait_clickable_element(el=el, driver=ctx.driver, timeout=timeout)
98
+
99
+ if force_js:
100
+ ctx.driver.execute_script("arguments[0].click();", el)
101
+ else:
102
+ try:
103
+ el.click()
104
+ except (ElementClickInterceptedException, StaleElementReferenceException):
105
+ el = retry_op(fn=lambda: find_element(
106
+ driver=ctx.driver,
107
+ selector=selector,
108
+ selector_type=selector_type,
109
+ timeout=int(timeout),
110
+ visible_only=True,
111
+ iframe_selector=iframe_selector,
112
+ iframe_selector_type=iframe_selector_type,
113
+ shadow_root_selector=shadow_root_selector,
114
+ shadow_root_selector_type=shadow_root_selector_type,
115
+ stay_in_context=True,
116
+ ))
117
+ ctx.driver.execute_script("arguments[0].click();", el)
118
+
119
+ _wait_document_ready(timeout=10.0)
120
+
121
+ snapshot = _make_page_snapshot()
122
+ return json.dumps({
123
+ "ok": True,
124
+ "action": "click",
125
+ "selector": selector,
126
+ "selector_type": selector_type,
127
+ "snapshot": snapshot,
128
+ })
129
+
130
+ except TimeoutException:
131
+ snapshot = _make_page_snapshot()
132
+ return json.dumps({
133
+ "ok": False,
134
+ "error": "timeout",
135
+ "selector": selector,
136
+ "selector_type": selector_type,
137
+ "snapshot": snapshot,
138
+ })
139
+
140
+ except Exception as e:
141
+ diag = collect_diagnostics(driver=ctx.driver, exc=e, config=ctx.config)
142
+ snapshot = _make_page_snapshot()
143
+ return json.dumps({"ok": False, "error": str(e), "diagnostics": diag, "snapshot": snapshot})
144
+
145
+ finally:
146
+ try:
147
+ if ctx.is_driver_initialized():
148
+ ctx.driver.switch_to.default_content()
149
+ except Exception:
150
+ pass
151
+
152
+
153
+ async def send_keys(
154
+ key: str,
155
+ selector: Optional[str] = None,
156
+ selector_type: str = "css",
157
+ timeout: float = 10.0,
158
+ ) -> str:
159
+ """
160
+ Send keyboard keys to an element or to the active element.
161
+
162
+ Args:
163
+ key: Key to send (ENTER, TAB, ESCAPE, ARROW_DOWN, etc.)
164
+ selector: Optional CSS selector, XPath, or ID of element to send keys to
165
+ selector_type: Type of selector (css, xpath, id)
166
+ timeout: Maximum time to wait for element in seconds
167
+
168
+ Returns:
169
+ JSON string with ok status, action, key sent, and page snapshot
170
+ """
171
+ ctx = get_context()
172
+
173
+ try:
174
+ from selenium.webdriver.common.keys import Keys
175
+
176
+ if not ctx.is_driver_initialized():
177
+ return json.dumps({"ok": False, "error": "driver_not_initialized"})
178
+
179
+ # Map string key names to Selenium Keys
180
+ key_mapping = {
181
+ "ENTER": Keys.ENTER,
182
+ "RETURN": Keys.RETURN,
183
+ "TAB": Keys.TAB,
184
+ "ESCAPE": Keys.ESCAPE,
185
+ "ESC": Keys.ESCAPE,
186
+ "SPACE": Keys.SPACE,
187
+ "BACKSPACE": Keys.BACKSPACE,
188
+ "DELETE": Keys.DELETE,
189
+ "ARROW_UP": Keys.ARROW_UP,
190
+ "ARROW_DOWN": Keys.ARROW_DOWN,
191
+ "ARROW_LEFT": Keys.ARROW_LEFT,
192
+ "ARROW_RIGHT": Keys.ARROW_RIGHT,
193
+ "PAGE_UP": Keys.PAGE_UP,
194
+ "PAGE_DOWN": Keys.PAGE_DOWN,
195
+ "HOME": Keys.HOME,
196
+ "END": Keys.END,
197
+ "F1": Keys.F1,
198
+ "F2": Keys.F2,
199
+ "F3": Keys.F3,
200
+ "F4": Keys.F4,
201
+ "F5": Keys.F5,
202
+ "F6": Keys.F6,
203
+ "F7": Keys.F7,
204
+ "F8": Keys.F8,
205
+ "F9": Keys.F9,
206
+ "F10": Keys.F10,
207
+ "F11": Keys.F11,
208
+ "F12": Keys.F12,
209
+ }
210
+
211
+ selenium_key = key_mapping.get(key.upper(), key)
212
+
213
+ if selector:
214
+ # Send keys to specific element
215
+ el = retry_op(fn=lambda: find_element(
216
+ driver=ctx.driver,
217
+ selector=selector,
218
+ selector_type=selector_type,
219
+ timeout=int(timeout),
220
+ visible_only=True,
221
+ ))
222
+ el.send_keys(selenium_key)
223
+ else:
224
+ # Send keys to active element (usually body or focused element)
225
+ from selenium.webdriver.common.action_chains import ActionChains
226
+ ActionChains(ctx.driver).send_keys(selenium_key).perform()
227
+
228
+ time.sleep(0.2) # Brief pause
229
+ snapshot = _make_page_snapshot()
230
+
231
+ return json.dumps({
232
+ "ok": True,
233
+ "action": "send_keys",
234
+ "key": key,
235
+ "selector": selector,
236
+ "snapshot": snapshot,
237
+ })
238
+
239
+ except Exception as e:
240
+ diag = collect_diagnostics(driver=ctx.driver, exc=e, config=ctx.config)
241
+ snapshot = _make_page_snapshot()
242
+ return json.dumps({"ok": False, "error": str(e), "diagnostics": diag, "snapshot": snapshot})
243
+
244
+ async def wait_for_element(
245
+ selector: str,
246
+ selector_type: str = "css",
247
+ timeout: float = 10.0,
248
+ condition: str = "visible",
249
+ iframe_selector: Optional[str] = None,
250
+ iframe_selector_type: str = "css",
251
+ ) -> str:
252
+ """
253
+ Wait for an element to meet a specific condition.
254
+
255
+ Args:
256
+ selector: CSS selector, XPath, or ID of the element
257
+ selector_type: Type of selector (css, xpath, id)
258
+ timeout: Maximum time to wait in seconds
259
+ condition: Condition to wait for - 'present', 'visible', or 'clickable'
260
+ iframe_selector: Optional selector for iframe containing the element
261
+ iframe_selector_type: Selector type for the iframe
262
+
263
+ Returns:
264
+ JSON string with ok status, element found status, and page snapshot
265
+ """
266
+ ctx = get_context()
267
+
268
+ try:
269
+ if not ctx.is_driver_initialized():
270
+ return json.dumps({"ok": False, "error": "driver_not_initialized"})
271
+
272
+ visible_only = condition in ("visible", "clickable")
273
+
274
+ el = find_element(
275
+ driver=ctx.driver,
276
+ selector=selector,
277
+ selector_type=selector_type,
278
+ timeout=int(timeout),
279
+ visible_only=visible_only,
280
+ iframe_selector=iframe_selector,
281
+ iframe_selector_type=iframe_selector_type,
282
+ )
283
+
284
+ if condition == "clickable":
285
+ _wait_clickable_element(el=el, driver=ctx.driver, timeout=timeout)
286
+
287
+ snapshot = _make_page_snapshot()
288
+ return json.dumps({
289
+ "ok": True,
290
+ "action": "wait_for_element",
291
+ "selector": selector,
292
+ "condition": condition,
293
+ "found": True,
294
+ "snapshot": snapshot,
295
+ "message": f"Element '{selector}' is now {condition}"
296
+ })
297
+
298
+ except TimeoutException:
299
+ snapshot = _make_page_snapshot()
300
+ return json.dumps({
301
+ "ok": False,
302
+ "error": "timeout",
303
+ "selector": selector,
304
+ "condition": condition,
305
+ "found": False,
306
+ "snapshot": snapshot,
307
+ "message": f"Element '{selector}' did not become {condition} within {timeout}s"
308
+ })
309
+
310
+ except Exception as e:
311
+ diag = collect_diagnostics(driver=ctx.driver, exc=e, config=ctx.config)
312
+ snapshot = _make_page_snapshot()
313
+ return json.dumps({"ok": False, "error": str(e), "diagnostics": diag, "snapshot": snapshot})
314
+
315
+ finally:
316
+ try:
317
+ if ctx.is_driver_initialized():
318
+ ctx.driver.switch_to.default_content()
319
+ except Exception:
320
+ pass
321
+
322
+
323
+ __all__ = ['fill_text', 'click_element', 'send_keys', 'wait_for_element']
@@ -0,0 +1,84 @@
1
+ """Navigation and scrolling tool implementations."""
2
+
3
+ import json
4
+ import time
5
+ from selenium.webdriver.support.ui import WebDriverWait
6
+ from ..context import get_context
7
+ from ..utils.diagnostics import collect_diagnostics
8
+ from ..actions.navigation import _wait_document_ready
9
+ from ..actions.screenshots import _make_page_snapshot
10
+
11
+
12
+ async def navigate_to_url(
13
+ url: str,
14
+ wait_for: str = "load", # "load" or "complete"
15
+ timeout_sec: int = 30,
16
+ ) -> str:
17
+ """Navigate to a URL and return JSON with a raw snapshot."""
18
+ ctx = get_context()
19
+
20
+ try:
21
+ if not ctx.is_driver_initialized():
22
+ return json.dumps({"ok": False, "error": "driver_not_initialized"})
23
+
24
+ ctx.driver.get(url)
25
+
26
+ # DOM readiness
27
+ try:
28
+ _wait_document_ready(timeout=min(max(timeout_sec, 0), 60))
29
+ except Exception:
30
+ pass
31
+
32
+ if (wait_for or "load").lower() == "complete":
33
+ try:
34
+ WebDriverWait(ctx.driver, timeout_sec).until(
35
+ lambda d: d.execute_script("return document.readyState") == "complete"
36
+ )
37
+ except Exception:
38
+ pass
39
+
40
+ snapshot = _make_page_snapshot()
41
+ return json.dumps({"ok": True, "action": "navigate", "url": url, "snapshot": snapshot})
42
+
43
+ except Exception as e:
44
+ diag = collect_diagnostics(driver=ctx.driver, exc=e, config=ctx.config)
45
+ snapshot = _make_page_snapshot()
46
+ return json.dumps({"ok": False, "error": str(e), "diagnostics": diag, "snapshot": snapshot})
47
+
48
+
49
+ async def scroll(x: int, y: int) -> str:
50
+ """
51
+ Scroll the page by the specified pixel amounts.
52
+
53
+ Args:
54
+ x: Horizontal scroll amount in pixels (positive = right, negative = left)
55
+ y: Vertical scroll amount in pixels (positive = down, negative = up)
56
+
57
+ Returns:
58
+ JSON string with ok status, action, scroll amounts, and page snapshot
59
+ """
60
+ ctx = get_context()
61
+
62
+ try:
63
+ if not ctx.is_driver_initialized():
64
+ return json.dumps({"ok": False, "error": "driver_not_initialized"})
65
+
66
+ ctx.driver.execute_script(f"window.scrollBy({int(x)}, {int(y)});")
67
+ time.sleep(0.3) # Brief pause to allow scroll to complete
68
+
69
+ snapshot = _make_page_snapshot()
70
+ return json.dumps({
71
+ "ok": True,
72
+ "action": "scroll",
73
+ "x": int(x),
74
+ "y": int(y),
75
+ "snapshot": snapshot,
76
+ })
77
+
78
+ except Exception as e:
79
+ diag = collect_diagnostics(driver=ctx.driver, exc=e, config=ctx.config)
80
+ snapshot = _make_page_snapshot()
81
+ return json.dumps({"ok": False, "error": str(e), "diagnostics": diag, "snapshot": snapshot})
82
+
83
+
84
+ __all__ = ['navigate_to_url', 'scroll']
@@ -0,0 +1,116 @@
1
+ """Screenshot capture tool implementations."""
2
+
3
+ import io
4
+ import json
5
+ import base64
6
+ from typing import Optional
7
+ from ..context import get_context
8
+ from ..utils.diagnostics import collect_diagnostics
9
+ from ..actions.screenshots import _make_page_snapshot
10
+
11
+
12
+ async def take_screenshot(screenshot_path, return_base64, return_snapshot, thumbnail_width=None) -> str:
13
+ """
14
+ Take a screenshot of the current page.
15
+
16
+ Args:
17
+ screenshot_path: Optional path to save the full screenshot
18
+ return_base64: Whether to return base64 encoded image
19
+ return_snapshot: Whether to return page HTML snapshot
20
+ thumbnail_width: Optional width in pixels for thumbnail (requires return_base64=True)
21
+ Default: 200px if return_base64 is True (accounts for MCP overhead)
22
+
23
+ Returns:
24
+ JSON string with ok status, saved path, optional base64 thumbnail, and snapshot
25
+ """
26
+ ctx = get_context()
27
+
28
+ try:
29
+ if not ctx.is_driver_initialized():
30
+ return json.dumps({"ok": False, "error": "driver_not_initialized"})
31
+
32
+ # Get full screenshot
33
+ png_bytes = ctx.driver.get_screenshot_as_png()
34
+
35
+ # Save full screenshot to disk if path provided
36
+ if screenshot_path:
37
+ with open(screenshot_path, "wb") as f:
38
+ f.write(png_bytes)
39
+
40
+ payload = {"ok": True, "saved_to": screenshot_path}
41
+
42
+ # Handle base64 return with thumbnail
43
+ if return_base64:
44
+ # Default thumbnail width to 200px to account for MCP protocol overhead (~3x)
45
+ # 200px thumbnail = ~6K tokens, plus MCP overhead = ~18K total (under 25K limit)
46
+ if thumbnail_width is None:
47
+ thumbnail_width = 200
48
+
49
+ # Validate thumbnail width
50
+ if thumbnail_width < 50:
51
+ return json.dumps({
52
+ "ok": False,
53
+ "error": "thumbnail_width_too_small",
54
+ "message": "thumbnail_width must be at least 50 pixels",
55
+ "min_width": 50,
56
+ })
57
+
58
+ try:
59
+ from PIL import Image
60
+ except ImportError:
61
+ return json.dumps({
62
+ "ok": False,
63
+ "error": "pillow_not_installed",
64
+ "message": "Pillow is required for thumbnails. Install with: pip install Pillow",
65
+ })
66
+
67
+ try:
68
+ # Create thumbnail
69
+ img = Image.open(io.BytesIO(png_bytes))
70
+ original_size = img.size
71
+
72
+ # Calculate thumbnail dimensions maintaining aspect ratio
73
+ aspect_ratio = img.height / img.width
74
+ thumb_height = int(thumbnail_width * aspect_ratio)
75
+
76
+ # Resize to thumbnail
77
+ img.thumbnail((thumbnail_width, thumb_height), Image.Resampling.LANCZOS)
78
+
79
+ # Encode thumbnail to base64
80
+ thumb_buffer = io.BytesIO()
81
+ img.save(thumb_buffer, format='PNG', optimize=True)
82
+ thumb_b64 = base64.b64encode(thumb_buffer.getvalue()).decode('utf-8')
83
+
84
+ payload["base64"] = thumb_b64
85
+ payload["thumbnail_width"] = thumbnail_width
86
+ payload["thumbnail_height"] = img.height
87
+ payload["original_width"] = original_size[0]
88
+ payload["original_height"] = original_size[1]
89
+ payload["message"] = f"Screenshot saved (thumbnail: {thumbnail_width}x{img.height}px, original: {original_size[0]}x{original_size[1]}px)"
90
+
91
+ except Exception as thumb_error:
92
+ # Thumbnail failed but full screenshot was saved
93
+ return json.dumps({
94
+ "ok": True,
95
+ "saved_to": screenshot_path,
96
+ "thumbnail_error": str(thumb_error),
97
+ "message": "Full screenshot saved, but thumbnail generation failed"
98
+ })
99
+
100
+ if return_snapshot:
101
+ payload["snapshot"] = _make_page_snapshot()
102
+ else:
103
+ payload["snapshot"] = "Omitted to save tokens."
104
+
105
+ return json.dumps(payload)
106
+
107
+ except Exception as e:
108
+ diag = collect_diagnostics(ctx.driver, e, ctx.config)
109
+ if return_snapshot:
110
+ snapshot = _make_page_snapshot()
111
+ else:
112
+ snapshot = "Omitted to save tokens."
113
+ return json.dumps({"ok": False, "error": str(e), "diagnostics": diag, "snapshot": snapshot})
114
+
115
+
116
+ __all__ = ['take_screenshot']
@@ -0,0 +1 @@
1
+ """Utility functions and helpers."""
@@ -0,0 +1,85 @@
1
+ """Diagnostics and debugging information utility functions."""
2
+
3
+ import sys
4
+ import platform
5
+ from typing import Optional
6
+ from selenium import webdriver
7
+ import selenium
8
+
9
+ from ..context import get_context
10
+ from ..browser.chrome_executable import get_chrome_binary_for_platform
11
+
12
+
13
+ def collect_diagnostics(
14
+ driver: Optional[webdriver.Chrome] = None,
15
+ exc: Optional[Exception] = None,
16
+ config: Optional[dict] = None
17
+ ) -> str:
18
+ """
19
+ Collect diagnostic information about the browser, driver, and environment.
20
+
21
+ Args:
22
+ driver: Selenium WebDriver instance (if None, will try to get from context)
23
+ exc: Exception that occurred (can be None)
24
+ config: Configuration dictionary (if None, will get from context)
25
+
26
+ Returns:
27
+ str: Formatted diagnostic information
28
+ """
29
+ ctx = get_context()
30
+
31
+ # Use context if parameters not provided
32
+ if driver is None:
33
+ driver = ctx.driver
34
+
35
+ if config is None:
36
+ config = ctx.config
37
+
38
+ # Get Chrome binary path
39
+ chrome_path = config.get('chrome_path')
40
+ if not chrome_path:
41
+ try:
42
+ chrome_path = get_chrome_binary_for_platform()
43
+ except Exception:
44
+ chrome_path = '<unknown>'
45
+
46
+ parts = [
47
+ f"OS : {platform.system()} {platform.release()}",
48
+ f"Python : {sys.version.split()[0]}",
49
+ f"Selenium : {getattr(selenium, '__version__', '?')}",
50
+ f"User-data dir : {config.get('user_data_dir')}",
51
+ f"Profile name : {config.get('profile_name')}",
52
+ f"Chrome binary : {chrome_path}",
53
+ f"Driver initialized: {driver is not None}",
54
+ f"Debugger address : {ctx.get_debugger_address() or '<none>'}",
55
+ f"Window ready : {ctx.is_window_ready()}",
56
+ ]
57
+
58
+ if driver:
59
+ try:
60
+ ver = driver.execute_cdp_cmd("Browser.getVersion", {}) or {}
61
+ parts.append(f"Browser version : {ver.get('product', '<unknown>')}")
62
+ except Exception:
63
+ parts.append("Browser version : <unknown>")
64
+
65
+ cap = getattr(driver, "capabilities", None) or {}
66
+ drv_ver = cap.get("chromedriverVersion") or cap.get("browserVersion") or "<unknown>"
67
+ parts.append(f"Driver version : {drv_ver}")
68
+ opts = cap.get("goog:chromeOptions") or {}
69
+ args = opts.get("args") or []
70
+ # Ensure args is iterable
71
+ if not isinstance(args, (list, tuple)):
72
+ args = []
73
+ parts.append(f"Chrome args : {' '.join(args)}")
74
+
75
+ if exc:
76
+ parts += [
77
+ "---- ERROR ----",
78
+ f"Error type : {type(exc).__name__}",
79
+ f"Error message : {exc}",
80
+ ]
81
+
82
+ return "\n".join(parts)
83
+
84
+
85
+ __all__ = ['collect_diagnostics']