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,43 @@
1
+ """Keyboard input and scrolling."""
2
+
3
+ from selenium.webdriver.common.keys import Keys
4
+
5
+ from ..context import get_context
6
+
7
+
8
+ def send_keys(keys_string: str) -> dict:
9
+ """Send keyboard input."""
10
+ ctx = get_context()
11
+ if not ctx.driver:
12
+ return {"ok": False, "error": "No driver available"}
13
+ try:
14
+ from selenium.webdriver.common.action_chains import ActionChains
15
+ ActionChains(ctx.driver).send_keys(keys_string).perform()
16
+ return {"ok": True}
17
+ except Exception as e:
18
+ return {"ok": False, "error": str(e)}
19
+
20
+
21
+ def scroll(direction: str = "down", amount: int = 300) -> dict:
22
+ """Scroll the page."""
23
+ ctx = get_context()
24
+ if not ctx.driver:
25
+ return {"ok": False, "error": "No driver available"}
26
+ try:
27
+ if direction == "down":
28
+ ctx.driver.execute_script(f"window.scrollBy(0, {amount});")
29
+ elif direction == "up":
30
+ ctx.driver.execute_script(f"window.scrollBy(0, -{amount});")
31
+ elif direction == "top":
32
+ ctx.driver.execute_script("window.scrollTo(0, 0);")
33
+ elif direction == "bottom":
34
+ ctx.driver.execute_script("window.scrollTo(0, document.body.scrollHeight);")
35
+ return {"ok": True}
36
+ except Exception as e:
37
+ return {"ok": False, "error": str(e)}
38
+
39
+
40
+ __all__ = [
41
+ 'send_keys',
42
+ 'scroll',
43
+ ]
@@ -0,0 +1,73 @@
1
+ """Navigation and page interaction."""
2
+
3
+ import time
4
+ from selenium.webdriver.support.ui import WebDriverWait
5
+ from selenium.webdriver.support import expected_conditions as EC
6
+
7
+ from ..context import get_context
8
+
9
+
10
+ def _wait_document_ready(timeout: float = 10.0):
11
+ """Wait for document to be ready."""
12
+ ctx = get_context()
13
+ if not ctx.driver:
14
+ return
15
+
16
+ try:
17
+ WebDriverWait(ctx.driver, timeout).until(
18
+ lambda d: d.execute_script("return document.readyState") in ("interactive", "complete")
19
+ )
20
+ except Exception:
21
+ # Not fatal
22
+ pass
23
+
24
+
25
+ def navigate_to_url(url: str) -> dict:
26
+ """Navigate to URL."""
27
+ ctx = get_context()
28
+ if not ctx.driver:
29
+ return {"ok": False, "error": "No driver available"}
30
+ try:
31
+ ctx.driver.get(url)
32
+ _wait_document_ready()
33
+ return {"ok": True}
34
+ except Exception as e:
35
+ return {"ok": False, "error": str(e)}
36
+
37
+
38
+ def wait_for_element(selector: str, timeout: float = 10.0) -> dict:
39
+ """Wait for element to appear."""
40
+ ctx = get_context()
41
+ if not ctx.driver:
42
+ return {"ok": False, "error": "No driver available"}
43
+ try:
44
+ from selenium.webdriver.common.by import By
45
+ WebDriverWait(ctx.driver, timeout).until(
46
+ EC.presence_of_element_located((By.CSS_SELECTOR, selector))
47
+ )
48
+ return {"ok": True}
49
+ except Exception as e:
50
+ return {"ok": False, "error": str(e)}
51
+
52
+
53
+ def get_current_page_meta() -> dict:
54
+ """Get current page metadata."""
55
+ ctx = get_context()
56
+ if not ctx.driver:
57
+ return {"ok": False, "error": "No driver available"}
58
+ try:
59
+ return {
60
+ "ok": True,
61
+ "url": ctx.driver.current_url,
62
+ "title": ctx.driver.title,
63
+ }
64
+ except Exception as e:
65
+ return {"ok": False, "error": str(e)}
66
+
67
+
68
+ __all__ = [
69
+ '_wait_document_ready',
70
+ 'navigate_to_url',
71
+ 'wait_for_element',
72
+ 'get_current_page_meta',
73
+ ]
@@ -0,0 +1,85 @@
1
+ """Screenshot and page snapshot functionality."""
2
+
3
+ import os
4
+ import time
5
+ import io
6
+ import base64
7
+ from typing import Optional
8
+
9
+ from ..context import get_context
10
+
11
+
12
+ def _make_page_snapshot() -> dict:
13
+ """
14
+ Capture the raw page snapshot (no cleaning, no truncation).
15
+ Returns a dict: {"url": str|None, "title": str|None, "html": str}
16
+ """
17
+ from .navigation import _wait_document_ready
18
+
19
+ ctx = get_context()
20
+ url = None
21
+ title = None
22
+ html = ""
23
+ try:
24
+ if ctx.driver is not None:
25
+ try:
26
+ ctx.driver.switch_to.default_content()
27
+ except Exception:
28
+ pass
29
+ try:
30
+ url = ctx.driver.current_url
31
+ except Exception:
32
+ url = None
33
+ try:
34
+ title = ctx.driver.title
35
+ except Exception:
36
+ title = None
37
+
38
+ # Ensure DOM is ready, then apply configurable settle
39
+ try:
40
+ _wait_document_ready(timeout=5.0)
41
+ except Exception:
42
+ pass
43
+ try:
44
+ settle_ms = int(os.getenv("SNAPSHOT_SETTLE_MS", "200") or "0")
45
+ if settle_ms > 0:
46
+ time.sleep(settle_ms / 1000.0)
47
+ except Exception:
48
+ pass
49
+
50
+ # Prefer outerHTML; fall back to page_source
51
+ try:
52
+ html = ctx.driver.execute_script("return document.documentElement.outerHTML") or ""
53
+ if not html:
54
+ html = ctx.driver.page_source or ""
55
+ except Exception:
56
+ try:
57
+ html = ctx.driver.page_source or ""
58
+ except Exception:
59
+ html = ""
60
+ except Exception:
61
+ pass
62
+ return {"url": url, "title": title, "html": html}
63
+
64
+
65
+ def take_screenshot(filename: Optional[str] = None) -> dict:
66
+ """Take a screenshot."""
67
+ ctx = get_context()
68
+ if not ctx.driver:
69
+ return {"ok": False, "error": "No driver available"}
70
+ try:
71
+ if filename:
72
+ ctx.driver.save_screenshot(filename)
73
+ return {"ok": True, "path": filename}
74
+ else:
75
+ png_data = ctx.driver.get_screenshot_as_png()
76
+ b64 = base64.b64encode(png_data).decode('utf-8')
77
+ return {"ok": True, "data": b64}
78
+ except Exception as e:
79
+ return {"ok": False, "error": str(e)}
80
+
81
+
82
+ __all__ = [
83
+ '_make_page_snapshot',
84
+ 'take_screenshot',
85
+ ]
@@ -0,0 +1 @@
1
+ """Browser management module."""
@@ -0,0 +1,150 @@
1
+ """Chrome browser management - Main orchestration."""
2
+
3
+ import os
4
+ import time
5
+ import platform
6
+ from pathlib import Path
7
+ from typing import Tuple, Optional
8
+ import psutil
9
+
10
+ # Import from refactored modules
11
+ from .chrome_executable import validate_user_data_dir, get_chrome_binary_for_platform
12
+ from .chrome_launcher import (
13
+ try_attach_existing_chrome,
14
+ launch_on_fixed_port,
15
+ launch_on_dynamic_port,
16
+ build_chrome_command,
17
+ launch_chrome_process,
18
+ )
19
+ from .chrome_process import find_chrome_by_port
20
+ from .devtools import devtools_active_port_from_file, is_debugger_listening
21
+ from .process import read_rendezvous, write_rendezvous
22
+ from ..locking.file_mutex import acquire_start_lock, release_start_lock
23
+ from ..constants import START_LOCK_WAIT_SEC
24
+
25
+
26
+ import logging
27
+ logger = logging.getLogger(__name__)
28
+
29
+
30
+ def _launch_chrome_with_debug(cfg: dict, port: int) -> None:
31
+ """
32
+ Launch Chrome with remote debugging on a specific port.
33
+
34
+ This is a simple wrapper around the chrome_launcher functions,
35
+ used by devtools.py when it needs to launch Chrome directly.
36
+
37
+ Args:
38
+ cfg: Configuration dict with user_data_dir, profile_name, chrome_path (optional)
39
+ port: Remote debugging port to use
40
+
41
+ Raises:
42
+ RuntimeError: If Chrome fails to launch
43
+ """
44
+ # Get Chrome binary
45
+ chrome_path = cfg.get("chrome_path")
46
+ if not chrome_path:
47
+ chrome_path = get_chrome_binary_for_platform()
48
+
49
+ # Build command
50
+ cmd = build_chrome_command(
51
+ binary=chrome_path,
52
+ port=port,
53
+ user_data_dir=cfg["user_data_dir"],
54
+ profile_name=cfg.get("profile_name", "Default"),
55
+ )
56
+
57
+ # Launch process
58
+ proc = launch_chrome_process(cmd, port)
59
+
60
+ # On Windows, Chrome's launcher process exits immediately after spawning background processes.
61
+ # This is normal behavior. Only check for immediate exit on non-Windows platforms.
62
+ if platform.system() != "Windows":
63
+ time.sleep(0.2) # Brief wait to check if process exits immediately
64
+ if proc.poll() is not None:
65
+ raise RuntimeError(f"Chrome process exited immediately with code {proc.returncode}")
66
+
67
+ logger.info(f"Launched Chrome on port {port}, pid={proc.pid}")
68
+
69
+
70
+ def start_or_attach_chrome_from_env(config: dict) -> Tuple[str, int, Optional[psutil.Process]]:
71
+ """
72
+ Start or attach to Chrome with remote debugging enabled.
73
+
74
+ This is the main orchestration function that coordinates:
75
+ 1. Directory validation
76
+ 2. Attempting to attach to existing Chrome
77
+ 3. Launching on fixed or dynamic port
78
+ 4. Rendezvous file management
79
+
80
+ Args:
81
+ config: Configuration dict with user_data_dir, profile_name, fixed_port (optional)
82
+
83
+ Returns:
84
+ Tuple of (host, port, proc) where proc is None if attached to existing Chrome
85
+
86
+ Raises:
87
+ RuntimeError: If Chrome fails to start or validation fails
88
+ """
89
+ user_data_dir = config["user_data_dir"]
90
+ fixed_port = config.get("fixed_port")
91
+ host = "127.0.0.1"
92
+
93
+ # Ensure directory exists
94
+ Path(user_data_dir).mkdir(parents=True, exist_ok=True)
95
+
96
+ # Validate directory
97
+ validate_user_data_dir(user_data_dir)
98
+
99
+ # Try to attach to existing Chrome first (if no fixed port specified)
100
+ if not fixed_port:
101
+ result = try_attach_existing_chrome(config, host)
102
+ if result:
103
+ return result
104
+
105
+ # Fixed port path
106
+ if fixed_port:
107
+ return launch_on_fixed_port(config, host, fixed_port)
108
+
109
+ # Rendezvous path (multi-process coordination)
110
+ port, pid = read_rendezvous(config)
111
+ if port:
112
+ return host, port, None
113
+
114
+ got_lock = acquire_start_lock(config, timeout_sec=START_LOCK_WAIT_SEC)
115
+ try:
116
+ if not got_lock:
117
+ # Wait for rendezvous by the process that got the lock
118
+ for _ in range(50):
119
+ port, pid = read_rendezvous(config)
120
+ if port:
121
+ return host, port, None
122
+
123
+ # Also try attaching via DevToolsActivePort if it appears
124
+ p2 = devtools_active_port_from_file(user_data_dir)
125
+ if p2 and is_debugger_listening(host, p2):
126
+ chrome_proc = find_chrome_by_port(p2)
127
+ write_rendezvous(config, p2, chrome_proc.pid if chrome_proc else os.getpid())
128
+ return host, p2, None
129
+
130
+ time.sleep(0.1)
131
+
132
+ raise RuntimeError("Timeout acquiring start lock for Chrome rendezvous.")
133
+
134
+ # Inside lock: recheck rendezvous
135
+ port, pid = read_rendezvous(config)
136
+ if port:
137
+ return host, port, None
138
+
139
+ # Launch Chrome on dynamic port
140
+ return launch_on_dynamic_port(config, host)
141
+
142
+ finally:
143
+ if got_lock:
144
+ release_start_lock(config)
145
+
146
+
147
+ __all__ = [
148
+ 'start_or_attach_chrome_from_env',
149
+ '_launch_chrome_with_debug',
150
+ ]
@@ -0,0 +1,204 @@
1
+ """Chrome executable resolution, version detection, and directory validation."""
2
+
3
+ import os
4
+ import shutil
5
+ import platform
6
+ import subprocess
7
+ from pathlib import Path
8
+
9
+ import logging
10
+ logger = logging.getLogger(__name__)
11
+
12
+
13
+ def resolve_chrome_executable(cfg: dict) -> str:
14
+ """
15
+ Resolve Chrome executable path from config or platform defaults.
16
+
17
+ Args:
18
+ cfg: Configuration dict with optional chrome_path, chrome_executable, etc.
19
+
20
+ Returns:
21
+ str: Path to Chrome executable
22
+
23
+ Raises:
24
+ FileNotFoundError: If Chrome executable cannot be found
25
+ """
26
+ if cfg.get("chrome_path"):
27
+ return cfg["chrome_path"]
28
+
29
+ # Try config keys first
30
+ candidates = [
31
+ cfg.get("chrome_executable"),
32
+ cfg.get("chrome_binary"),
33
+ cfg.get("chrome_executable_path"),
34
+ os.getenv("CHROME_EXECUTABLE_PATH"),
35
+ ]
36
+ # Common macOS fallbacks
37
+ defaults = [
38
+ "/Applications/Google Chrome Beta.app/Contents/MacOS/Google Chrome Beta",
39
+ "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
40
+ "/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary",
41
+ ]
42
+ for p in candidates + defaults:
43
+ if p and os.path.exists(p):
44
+ return p
45
+ raise FileNotFoundError(
46
+ "Chrome executable not found. Set CHROME_EXECUTABLE_PATH to the full binary path, "
47
+ "e.g. /Applications/Google Chrome Beta.app/Contents/MacOS/Google Chrome Beta"
48
+ )
49
+
50
+
51
+ def get_chrome_binary_for_platform(config: dict) -> str:
52
+ """
53
+ Get platform-specific Chrome binary path.
54
+
55
+ Tries to find Chrome binary based on the current platform.
56
+ Returns a reasonable default if not found.
57
+
58
+ Args:
59
+ config: Configuration dict with optional chrome_path
60
+
61
+ Returns:
62
+ str: Path to Chrome binary or "chrome" as fallback
63
+ """
64
+ if config.get("chrome_path"):
65
+ return config["chrome_path"]
66
+
67
+ system = platform.system()
68
+ candidates = []
69
+
70
+ if system == "Windows":
71
+ candidates = [
72
+ r"C:\Program Files\Google\Chrome Beta\Application\chrome.exe",
73
+ r"C:\Program Files (x86)\Google\Chrome Beta\Application\chrome.exe",
74
+ "chrome",
75
+ ]
76
+ elif system == "Darwin":
77
+ candidates = ["/Applications/Google Chrome Beta.app/Contents/MacOS/Google Chrome"]
78
+ else:
79
+ candidates = ["google-chrome", "chrome", "chromium", "chromium-browser"]
80
+
81
+ for c in candidates:
82
+ if os.path.isfile(c) or shutil.which(c):
83
+ return c
84
+
85
+ return "chrome"
86
+
87
+
88
+ def get_chrome_version() -> str:
89
+ """
90
+ Get Chrome version string from registry or executable.
91
+
92
+ Returns:
93
+ str: Chrome version string or error message
94
+ """
95
+ system = platform.system()
96
+ try:
97
+ if system == "Windows":
98
+ try:
99
+ import winreg
100
+ with winreg.OpenKey(winreg.HKEY_CURRENT_USER, r"Software\Google\Chrome\BLBeacon") as key:
101
+ version, _ = winreg.QueryValueEx(key, "version")
102
+ return f"Google Chrome {version}"
103
+ except Exception:
104
+ pass
105
+ # Fallbacks
106
+ for candidate in [
107
+ r"C:\Program Files\Google\Chrome\Application\chrome.exe",
108
+ r"C:\Program Files (x86)\Google\Chrome\Application\chrome.exe",
109
+ "chrome",
110
+ ]:
111
+ try:
112
+ path = candidate if os.path.isfile(candidate) else shutil.which(candidate)
113
+ if path:
114
+ out = subprocess.check_output([path, "--version"], stderr=subprocess.STDOUT).decode().strip()
115
+ return out
116
+ except Exception:
117
+ continue
118
+ return "Error fetching Chrome version: chrome binary not found"
119
+ elif system == "Darwin":
120
+ path = "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"
121
+ out = subprocess.check_output([path, "--version"], stderr=subprocess.STDOUT).decode().strip()
122
+ return out
123
+ else:
124
+ for candidate in ["google-chrome", "chrome", "chromium", "chromium-browser"]:
125
+ try:
126
+ path = shutil.which(candidate)
127
+ if path:
128
+ out = subprocess.check_output([path, "--version"], stderr=subprocess.STDOUT).decode().strip()
129
+ return out
130
+ except Exception:
131
+ continue
132
+ return "Error fetching Chrome version: chrome binary not found"
133
+ except Exception as e:
134
+ return f"Error fetching Chrome version: {e}"
135
+
136
+
137
+ def is_default_user_data_dir(user_data_dir: str) -> bool:
138
+ """
139
+ Return True if user_data_dir is one of Chrome's default roots (where DevTools is refused).
140
+
141
+ Args:
142
+ user_data_dir: Path to Chrome user data directory
143
+
144
+ Returns:
145
+ bool: True if this is a default Chrome directory
146
+ """
147
+ p = Path(user_data_dir).expanduser().resolve()
148
+ system = platform.system()
149
+ defaults = []
150
+
151
+ if system == "Darwin":
152
+ defaults = [
153
+ Path.home() / "Library/Application Support/Google/Chrome",
154
+ Path.home() / "Library/Application Support/Google/Chrome Beta",
155
+ Path.home() / "Library/Application Support/Google/Chrome Canary",
156
+ ]
157
+ elif system == "Windows":
158
+ local = os.environ.get("LOCALAPPDATA", "")
159
+ if local:
160
+ base = Path(local) / "Google"
161
+ defaults = [
162
+ base / "Chrome" / "User Data",
163
+ base / "Chrome Beta" / "User Data",
164
+ base / "Chrome SxS" / "User Data", # Canary
165
+ ]
166
+ else: # Linux
167
+ home = Path.home()
168
+ defaults = [
169
+ home / ".config/google-chrome",
170
+ home / ".config/google-chrome-beta",
171
+ home / ".config/google-chrome-unstable", # Canary
172
+ home / ".config/chromium",
173
+ ]
174
+
175
+ return any(p == d for d in defaults)
176
+
177
+
178
+ def validate_user_data_dir(user_data_dir: str) -> None:
179
+ """
180
+ Validate user_data_dir and raise if it's a default directory.
181
+
182
+ Args:
183
+ user_data_dir: Path to Chrome user data directory
184
+
185
+ Raises:
186
+ RuntimeError: If user_data_dir is a default Chrome directory
187
+ """
188
+ if is_default_user_data_dir(user_data_dir):
189
+ if os.getenv("MCP_ALLOW_DEFAULT_USER_DATA_DIR", "0") != "1":
190
+ raise RuntimeError(
191
+ "Remote debugging is disabled on Chrome's default user-data directories.\n"
192
+ f"Set *_PROFILE_USER_DATA_DIR to a separate path (e.g., '{Path(user_data_dir).parent}/Chrome Beta MCP'), "
193
+ "optionally seed it from your existing profile, then retry.\n"
194
+ "To override (not recommended), set MCP_ALLOW_DEFAULT_USER_DATA_DIR=1."
195
+ )
196
+
197
+
198
+ __all__ = [
199
+ 'resolve_chrome_executable',
200
+ 'get_chrome_binary_for_platform',
201
+ 'get_chrome_version',
202
+ 'is_default_user_data_dir',
203
+ 'validate_user_data_dir',
204
+ ]