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,260 @@
1
+ """Browser lifecycle management tool implementations."""
2
+
3
+ import json
4
+ import psutil
5
+ from pathlib import Path
6
+ from ..context import get_context, reset_context
7
+ from ..config import get_env_config, profile_key
8
+ from ..constants import ACTION_LOCK_TTL_SECS
9
+ from ..utils.diagnostics import collect_diagnostics
10
+
11
+ # Import specific functions we need
12
+ from ..browser.driver import (
13
+ _ensure_driver_and_window,
14
+ close_singleton_window,
15
+ _close_extra_blank_windows_safe,
16
+ ensure_process_tag,
17
+ )
18
+ from ..actions.navigation import _wait_document_ready
19
+ from ..actions.screenshots import _make_page_snapshot
20
+ from ..locking.action_lock import _release_action_lock
21
+
22
+
23
+ async def start_browser():
24
+ """
25
+ Start browser session or open new window in existing session.
26
+
27
+ Returns:
28
+ JSON string with session info and snapshot
29
+ """
30
+ ctx = get_context()
31
+
32
+ # Ensure process tag
33
+ if ctx.process_tag is None:
34
+ ctx.process_tag = ensure_process_tag()
35
+
36
+ owner = ctx.process_tag
37
+
38
+ try:
39
+ # Initialize driver and window
40
+ _ensure_driver_and_window()
41
+
42
+ # Check if initialization succeeded
43
+ if not ctx.is_driver_initialized():
44
+ diag = collect_diagnostics(None, None, ctx.config)
45
+ if isinstance(diag, str):
46
+ diag = {"summary": diag}
47
+
48
+ return json.dumps({
49
+ "ok": False,
50
+ "error": "driver_not_initialized",
51
+ "driver_initialized": False,
52
+ "debugger": ctx.get_debugger_address(),
53
+ "diagnostics": diag,
54
+ "message": "Failed to attach/launch a debuggable Chrome session."
55
+ })
56
+
57
+ # Clean up extra blank windows
58
+ handle = getattr(ctx.driver, "current_window_handle", None)
59
+ try:
60
+ _close_extra_blank_windows_safe(
61
+ ctx.driver,
62
+ exclude_handles={handle} if handle else None
63
+ )
64
+ except Exception:
65
+ pass
66
+
67
+ # Wait for page ready and get snapshot
68
+ _wait_document_ready(timeout=5.0)
69
+ try:
70
+ snapshot = _make_page_snapshot()
71
+ except Exception:
72
+ snapshot = None
73
+
74
+ snapshot = snapshot or {
75
+ "url": "about:blank",
76
+ "title": "",
77
+ "html": "",
78
+ "truncated": False,
79
+ }
80
+
81
+ msg = (
82
+ f"Browser session created successfully. "
83
+ f"Session ID: {owner}. "
84
+ f"Current URL: {snapshot.get('url') or 'about:blank'}"
85
+ )
86
+
87
+ payload = {
88
+ "ok": True,
89
+ "session_id": owner,
90
+ "debugger": ctx.get_debugger_address(),
91
+ "lock_ttl_seconds": ACTION_LOCK_TTL_SECS,
92
+ "snapshot": snapshot,
93
+ "message": msg,
94
+ }
95
+
96
+ return json.dumps(payload)
97
+
98
+ except Exception as e:
99
+ diag = collect_diagnostics(ctx.driver, e, ctx.config)
100
+ snapshot = _make_page_snapshot() or {
101
+ "url": "about:blank",
102
+ "title": "",
103
+ "html": "",
104
+ "truncated": False,
105
+ }
106
+ return json.dumps({
107
+ "ok": False,
108
+ "error": str(e),
109
+ "diagnostics": diag,
110
+ "snapshot": snapshot
111
+ })
112
+
113
+
114
+ async def unlock_browser():
115
+ """Release the action lock for this process."""
116
+ ctx = get_context()
117
+
118
+ if ctx.process_tag is None:
119
+ ctx.process_tag = ensure_process_tag()
120
+
121
+ owner = ctx.process_tag
122
+ released = _release_action_lock(owner)
123
+
124
+ return json.dumps({
125
+ "ok": True,
126
+ "released": bool(released)
127
+ })
128
+
129
+ async def close_browser() -> str:
130
+ """Close the browser window for this session."""
131
+ ctx = get_context()
132
+
133
+ try:
134
+ closed = close_singleton_window()
135
+ msg = "Browser window closed successfully" if closed else "No window to close"
136
+
137
+ return json.dumps({
138
+ "ok": True,
139
+ "closed": bool(closed),
140
+ "message": msg
141
+ })
142
+
143
+ except Exception as e:
144
+ diag = collect_diagnostics(ctx.driver, e, ctx.config)
145
+ return json.dumps({
146
+ "ok": False,
147
+ "error": str(e),
148
+ "diagnostics": diag
149
+ })
150
+
151
+ async def force_close_all_chrome() -> str:
152
+ """
153
+ Force close all Chrome processes, quit driver, and clean up all state.
154
+ Use this to recover from stuck Chrome instances.
155
+ """
156
+ ctx = get_context()
157
+ killed_processes = []
158
+ errors = []
159
+
160
+ try:
161
+ # 1. Try to quit the Selenium driver gracefully
162
+ if ctx.driver is not None:
163
+ try:
164
+ ctx.driver.quit()
165
+ except Exception as e:
166
+ errors.append(f"Driver quit failed: {e}")
167
+
168
+ ctx.driver = None
169
+
170
+ # 2. Get config to find which Chrome processes to kill
171
+ user_data_dir = ctx.config.get("user_data_dir", "")
172
+ if not user_data_dir:
173
+ try:
174
+ cfg = get_env_config()
175
+ user_data_dir = cfg.get("user_data_dir", "")
176
+ except Exception as e:
177
+ errors.append(f"Could not get config: {e}")
178
+
179
+ # 3. Kill all Chrome processes using the MCP profile
180
+ chrome_processes_found = []
181
+ for p in psutil.process_iter(["name", "cmdline", "pid"]):
182
+ try:
183
+ if not p.info.get("name"):
184
+ continue
185
+ if "chrome" not in p.info["name"].lower():
186
+ continue
187
+
188
+ chrome_processes_found.append(p)
189
+
190
+ # If we have a user_data_dir, check if this process matches
191
+ if user_data_dir:
192
+ cmd = p.info.get("cmdline")
193
+ if cmd:
194
+ user_data_normalized = user_data_dir.replace("\\", "/").lower()
195
+ for arg in cmd:
196
+ if arg and "--user-data-dir" in arg:
197
+ arg_normalized = arg.replace("\\", "/").lower()
198
+ if user_data_normalized in arg_normalized:
199
+ p.kill()
200
+ killed_processes.append(p.info["pid"])
201
+ break
202
+ except (psutil.NoSuchProcess, psutil.AccessDenied) as e:
203
+ errors.append(f"Could not access process: {e}")
204
+
205
+ # 4. Fallback: If no processes killed but some found, kill them all
206
+ if not killed_processes and chrome_processes_found:
207
+ for p in chrome_processes_found:
208
+ try:
209
+ p.kill()
210
+ killed_processes.append(p.info["pid"])
211
+ except (psutil.NoSuchProcess, psutil.AccessDenied) as e:
212
+ errors.append(f"Could not kill process in fallback: {e}")
213
+
214
+ # 5. Clean up context state
215
+ ctx.debugger_host = None
216
+ ctx.debugger_port = None
217
+ ctx.reset_window_state()
218
+
219
+ # 6. Release locks
220
+ try:
221
+ if ctx.process_tag:
222
+ _release_action_lock(ctx.process_tag)
223
+ except Exception as e:
224
+ errors.append(f"Lock release failed: {e}")
225
+
226
+ # 7. Clean up lock files
227
+ try:
228
+ if user_data_dir:
229
+ lock_dir = Path(ctx.lock_dir)
230
+ if lock_dir.exists():
231
+ profile_key_val = profile_key(ctx.config) if ctx.config else ""
232
+ for lock_file in lock_dir.glob(f"*{profile_key_val}*"):
233
+ try:
234
+ lock_file.unlink()
235
+ except Exception:
236
+ pass
237
+ except Exception as e:
238
+ errors.append(f"Lock file cleanup failed: {e}")
239
+
240
+ msg = f"Force closed Chrome. Killed {len(killed_processes)} processes."
241
+ if errors:
242
+ msg += f" Errors: {'; '.join(errors)}"
243
+
244
+ return json.dumps({
245
+ "ok": True,
246
+ "killed_processes": killed_processes,
247
+ "errors": errors,
248
+ "message": msg
249
+ })
250
+
251
+ except Exception as e:
252
+ return json.dumps({
253
+ "ok": False,
254
+ "error": str(e),
255
+ "killed_processes": killed_processes,
256
+ "errors": errors
257
+ })
258
+
259
+
260
+ __all__ = ['start_browser', 'unlock_browser', 'close_browser', 'force_close_all_chrome']
@@ -0,0 +1,195 @@
1
+ """Debugging and diagnostic tool implementations."""
2
+
3
+ import json
4
+ from pathlib import Path
5
+ from typing import Dict, Any
6
+ from selenium.common.exceptions import TimeoutException
7
+ from ..context import get_context
8
+ from ..utils.diagnostics import collect_diagnostics
9
+ from ..actions.elements import find_element, _wait_clickable_element
10
+ from ..actions.screenshots import _make_page_snapshot
11
+ from ..utils.retry import retry_op
12
+
13
+
14
+ async def get_debug_diagnostics_info() -> str:
15
+ """Get debug diagnostics using context."""
16
+ ctx = get_context()
17
+
18
+ try:
19
+ cfg = ctx.config
20
+ udir = cfg.get("user_data_dir")
21
+ port_file = str(Path(udir) / "DevToolsActivePort") if udir else None
22
+
23
+ # Read DevToolsActivePort
24
+ port_val = None
25
+ if udir:
26
+ p = Path(udir) / "DevToolsActivePort"
27
+ if p.exists():
28
+ try:
29
+ port_val = int(p.read_text().splitlines()[0].strip())
30
+ except Exception:
31
+ port_val = None
32
+
33
+ devtools_http = None
34
+ if port_val:
35
+ import urllib.request, json as _json
36
+ try:
37
+ with urllib.request.urlopen(f"http://127.0.0.1:{port_val}/json/version", timeout=1.0) as r:
38
+ devtools_http = _json.loads(r.read().decode("utf-8"))
39
+ except Exception:
40
+ devtools_http = {"ok": False}
41
+
42
+ diag_summary = collect_diagnostics(driver=ctx.driver, exc=None, config=cfg)
43
+ diagnostics = {
44
+ "summary": diag_summary,
45
+ "driver_initialized": ctx.is_driver_initialized(),
46
+ "debugger": ctx.get_debugger_address(),
47
+ "devtools_active_port_file": {"path": port_file, "port": port_val, "exists": port_val is not None},
48
+ "devtools_http_version": devtools_http,
49
+ "context_state": {
50
+ "driver_initialized": ctx.is_driver_initialized(),
51
+ "window_ready": ctx.is_window_ready(),
52
+ "debugger_address": ctx.get_debugger_address(),
53
+ "process_tag": ctx.process_tag,
54
+ }
55
+ }
56
+
57
+ snapshot = (_make_page_snapshot()
58
+ if ctx.is_driver_initialized()
59
+ else {"url": None, "title": None, "html": "", "truncated": False})
60
+ return json.dumps({"ok": True, "diagnostics": diagnostics, "snapshot": snapshot})
61
+
62
+ except Exception as e:
63
+ diag = collect_diagnostics(driver=ctx.driver, exc=e, config=ctx.config)
64
+ return json.dumps({"ok": False, "error": str(e), "diagnostics": {"summary": diag}})
65
+
66
+ async def debug_element(
67
+ selector,
68
+ selector_type,
69
+ timeout,
70
+ iframe_selector,
71
+ iframe_selector_type,
72
+ shadow_root_selector,
73
+ shadow_root_selector_type,
74
+ max_html_length=5000,
75
+ include_html=True,
76
+ ):
77
+ """
78
+ Debug an element on the page.
79
+
80
+ Args:
81
+ selector: CSS selector, XPath, or ID of the element
82
+ selector_type: Type of selector (css, xpath, id)
83
+ timeout: Maximum time to wait for element
84
+ iframe_selector: Optional iframe selector
85
+ iframe_selector_type: Iframe selector type
86
+ shadow_root_selector: Optional shadow root selector
87
+ shadow_root_selector_type: Shadow root selector type
88
+ max_html_length: Maximum length of outerHTML to return (default: 5000 chars)
89
+ include_html: Whether to include HTML in response (default: True)
90
+
91
+ Returns:
92
+ JSON string with debug information
93
+ """
94
+ ctx = get_context()
95
+
96
+ try:
97
+ info: Dict[str, Any] = {
98
+ "selector": selector,
99
+ "selector_type": selector_type,
100
+ "exists": False,
101
+ "displayed": None,
102
+ "enabled": None,
103
+ "clickable": None,
104
+ "rect": None,
105
+ "outerHTML": None,
106
+ "truncated": False,
107
+ "notes": [],
108
+ }
109
+
110
+ try:
111
+ el = retry_op(fn=lambda: find_element(
112
+ driver=ctx.driver,
113
+ selector=selector,
114
+ selector_type=selector_type,
115
+ timeout=int(timeout),
116
+ visible_only=False,
117
+ iframe_selector=iframe_selector,
118
+ iframe_selector_type=iframe_selector_type,
119
+ shadow_root_selector=shadow_root_selector,
120
+ shadow_root_selector_type=shadow_root_selector_type,
121
+ stay_in_context=True,
122
+ ))
123
+ info["exists"] = True
124
+
125
+ try:
126
+ info["displayed"] = bool(el.is_displayed())
127
+ except Exception:
128
+ info["displayed"] = None
129
+ try:
130
+ info["enabled"] = bool(el.is_enabled())
131
+ except Exception:
132
+ info["enabled"] = None
133
+
134
+ try:
135
+ _wait_clickable_element(el=el, driver=ctx.driver, timeout=timeout)
136
+ info["clickable"] = True
137
+ except Exception:
138
+ info["clickable"] = False
139
+
140
+ try:
141
+ r = el.rect
142
+ info["rect"] = {
143
+ "x": r.get("x"),
144
+ "y": r.get("y"),
145
+ "width": r.get("width"),
146
+ "height": r.get("height"),
147
+ }
148
+ except Exception:
149
+ info["rect"] = None
150
+
151
+ # Get HTML if requested
152
+ if include_html:
153
+ try:
154
+ html = ctx.driver.execute_script("return arguments[0].outerHTML;", el)
155
+ # Clean invalid characters
156
+ html = html.replace('\x00', '').encode('utf-8', errors='ignore').decode('utf-8')
157
+
158
+ # Truncate if too large
159
+ full_length = len(html)
160
+ if max_html_length and len(html) > max_html_length:
161
+ info["outerHTML"] = html[:max_html_length]
162
+ info["truncated"] = True
163
+ info["full_html_length"] = full_length
164
+ info["notes"].append(f"HTML truncated from {full_length} to {max_html_length} chars")
165
+ else:
166
+ info["outerHTML"] = html
167
+ info["truncated"] = False
168
+ except Exception as e:
169
+ info["outerHTML"] = None
170
+ info["notes"].append(f"Could not get HTML: {str(e)}")
171
+ else:
172
+ info["notes"].append("HTML omitted (include_html=False)")
173
+
174
+ except TimeoutException:
175
+ info["notes"].append("Element not found within timeout")
176
+ except Exception as e:
177
+ info["notes"].append(f"Error while probing element: {repr(e)}")
178
+
179
+ snapshot = _make_page_snapshot()
180
+ return json.dumps({"ok": True, "debug": info, "snapshot": snapshot})
181
+
182
+ except Exception as e:
183
+ diag = collect_diagnostics(driver=ctx.driver, exc=e, config=ctx.config)
184
+ snapshot = _make_page_snapshot()
185
+ return json.dumps({"ok": False, "error": str(e), "diagnostics": diag, "snapshot": snapshot})
186
+
187
+ finally:
188
+ try:
189
+ if ctx.is_driver_initialized():
190
+ ctx.driver.switch_to.default_content()
191
+ except Exception:
192
+ pass
193
+
194
+
195
+ __all__ = ['get_debug_diagnostics_info', 'debug_element']
@@ -0,0 +1,58 @@
1
+ """Element extraction tool implementations."""
2
+
3
+ from typing import Optional, List, Dict
4
+ from ..actions.extraction import extract_elements as _extract_elements_action
5
+
6
+
7
+ async def extract_elements(
8
+ selectors: Optional[List[Dict[str, str]]] = None,
9
+ container_selector: Optional[str] = None,
10
+ fields: Optional[List[Dict[str, str]]] = None,
11
+ selector_type: Optional[str] = None,
12
+ wait_for_visible: bool = False,
13
+ timeout: int = 10,
14
+ max_items: Optional[int] = None,
15
+ offset: Optional[int] = None,
16
+ discover_containers: bool = False,
17
+ wait_for_content_loaded: Optional[Dict[str, any]] = None,
18
+ ) -> str:
19
+ """
20
+ Extract content from specific elements on the current page.
21
+
22
+ This is a wrapper around the extraction action that provides the tool interface.
23
+
24
+ Supports two modes:
25
+ - Simple extraction: Use 'selectors' parameter
26
+ - Structured extraction: Use 'container_selector' + 'fields' parameters
27
+ - Discovery mode: Use 'container_selector' + 'discover_containers=True'
28
+
29
+ Args:
30
+ selectors: [MODE 1] Optional list of selector specifications
31
+ container_selector: [MODE 2] CSS or XPath selector for containers
32
+ fields: [MODE 2] List of field extractors with field_name, selector, etc.
33
+ selector_type: [MODE 2] Type of container_selector (auto-detects if None)
34
+ wait_for_visible: [MODE 2] Wait for containers to be visible
35
+ timeout: [MODE 2] Timeout in seconds
36
+ max_items: [MODE 2] Limit number of containers to extract
37
+ offset: [MODE 2] Skip first N containers before extracting (useful for pagination)
38
+ discover_containers: [MODE 2] Return container analysis instead of extraction
39
+ wait_for_content_loaded: [MODE 2] Smart wait config for lazy-loaded content
40
+
41
+ Returns:
42
+ JSON string with extraction results and page snapshot.
43
+ """
44
+ return await _extract_elements_action(
45
+ selectors=selectors,
46
+ container_selector=container_selector,
47
+ fields=fields,
48
+ selector_type=selector_type,
49
+ wait_for_visible=wait_for_visible,
50
+ timeout=timeout,
51
+ max_items=max_items,
52
+ offset=offset,
53
+ discover_containers=discover_containers,
54
+ wait_for_content_loaded=wait_for_content_loaded
55
+ )
56
+
57
+
58
+ __all__ = ['extract_elements']