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,155 @@
1
+ """Environment configuration and validation."""
2
+
3
+ import os
4
+ import hashlib
5
+ from pathlib import Path
6
+ from typing import Optional
7
+
8
+ import logging
9
+ logger = logging.getLogger(__name__)
10
+
11
+
12
+ def get_env_config() -> dict:
13
+ """
14
+ Read environment variables and validate required ones.
15
+
16
+ Prioritizes Chrome Beta over Chrome Canary over Chrome. This is to free the Chrome instance. Chrome is likely
17
+ used by the user already. It is easier to separate the executables. If a user already has the Chrome executable open,
18
+ the MCP will not work properly as the Chrome DevTool Debug mode will not open when Chrome is already open in normal mode.
19
+ We prioritize Chrome Beta because it is more stable than Canary.
20
+
21
+ Required: Either CHROME_PROFILE_USER_DATA_DIR, BETA_PROFILE_USER_DATA_DIR, or CANARY_PROFILE_USER_DATA_DIR
22
+ Optional: CHROME_PROFILE_NAME (default 'Default')
23
+ CHROME_EXECUTABLE_PATH
24
+ BETA_EXECUTABLE_PATH (overrides CHROME_EXECUTABLE_PATH)
25
+ CANARY_EXECUTABLE_PATH (overrides BETA and CHROME)
26
+ CHROME_REMOTE_DEBUG_PORT
27
+
28
+ If BETA_EXECUTABLE_PATH is set, expects:
29
+ BETA_PROFILE_USER_DATA_DIR
30
+ BETA_PROFILE_NAME
31
+ If CANARY_EXECUTABLE_PATH is set, expects:
32
+ CANARY_PROFILE_USER_DATA_DIR
33
+ CANARY_PROFILE_NAME
34
+ """
35
+ # Base (generic) config
36
+ user_data_dir = (os.getenv("CHROME_PROFILE_USER_DATA_DIR") or "").strip()
37
+ if not user_data_dir and not os.getenv("BETA_PROFILE_USER_DATA_DIR") and not os.getenv("CANARY_PROFILE_USER_DATA_DIR"):
38
+ raise EnvironmentError("CHROME_PROFILE_USER_DATA_DIR is required.")
39
+
40
+ profile_name = (os.getenv("CHROME_PROFILE_NAME") or "Default").strip() or "Default"
41
+ chrome_path = (os.getenv("CHROME_EXECUTABLE_PATH") or "").strip() or None
42
+
43
+ # Prefer Beta > Canary > Generic Chrome
44
+ canary_path = (os.getenv("CANARY_EXECUTABLE_PATH") or "").strip()
45
+ if canary_path:
46
+ chrome_path = canary_path
47
+ user_data_dir = (os.getenv("CANARY_PROFILE_USER_DATA_DIR") or "").strip()
48
+ profile_name = (os.getenv("CANARY_PROFILE_NAME") or "").strip() or "Default"
49
+ if not user_data_dir:
50
+ raise EnvironmentError("CANARY_PROFILE_USER_DATA_DIR is required when CANARY_EXECUTABLE_PATH is set.")
51
+
52
+ beta_path = (os.getenv("BETA_EXECUTABLE_PATH") or "").strip()
53
+ if beta_path:
54
+ chrome_path = beta_path
55
+ user_data_dir = (os.getenv("BETA_PROFILE_USER_DATA_DIR") or "").strip()
56
+ profile_name = (os.getenv("BETA_PROFILE_NAME") or "").strip() or "Default"
57
+ if not user_data_dir:
58
+ raise EnvironmentError("BETA_PROFILE_USER_DATA_DIR is required when BETA_EXECUTABLE_PATH is set.")
59
+
60
+ fixed_port_env = (os.getenv("CHROME_REMOTE_DEBUG_PORT") or "").strip()
61
+ fixed_port = int(fixed_port_env) if fixed_port_env.isdigit() else None
62
+
63
+ if not user_data_dir:
64
+ raise EnvironmentError(
65
+ "No user_data_dir selected. Set CHROME_PROFILE_USER_DATA_DIR, or provide "
66
+ "BETA_EXECUTABLE_PATH + BETA_PROFILE_USER_DATA_DIR (or CANARY_* equivalents)."
67
+ )
68
+
69
+ return {
70
+ "user_data_dir": user_data_dir,
71
+ "profile_name": profile_name,
72
+ "chrome_path": chrome_path,
73
+ "fixed_port": fixed_port,
74
+ }
75
+
76
+
77
+ def profile_key(config: Optional[dict] = None) -> str:
78
+ """
79
+ Stable key used by cross-process locks, based on absolute user_data_dir + profile_name.
80
+ - Hard-fails if CHROME_PROFILE_USER_DATA_DIR is missing/blank.
81
+ - If CHROME_PROFILE_STRICT=1 and the directory doesn't exist, hard-fail.
82
+ Otherwise we allow Chrome to create it and we normalize the path for stability.
83
+ """
84
+ if config is None:
85
+ config = get_env_config()
86
+
87
+ user_data_dir = (config.get("user_data_dir") or "").strip()
88
+ profile_name = (config.get("profile_name") or "Default").strip() or "Default"
89
+
90
+ if not user_data_dir:
91
+ raise EnvironmentError("CHROME_PROFILE_USER_DATA_DIR is required and cannot be empty.")
92
+
93
+ strict = os.getenv("CHROME_PROFILE_STRICT", "0") == "1"
94
+ p = Path(user_data_dir)
95
+ if strict and not p.exists():
96
+ raise FileNotFoundError(f"user_data_dir does not exist: {p}")
97
+
98
+ # Normalize to a stable absolute string
99
+ try:
100
+ user_data_dir = str(p.resolve())
101
+ except Exception:
102
+ user_data_dir = str(p.absolute())
103
+
104
+ raw = f"{user_data_dir}|{profile_name}"
105
+ return hashlib.sha256(raw.encode("utf-8")).hexdigest()
106
+
107
+
108
+ def is_default_user_data_dir(user_data_dir: str) -> bool:
109
+ """
110
+ Check if the given user_data_dir matches the platform's default Chrome profile location.
111
+
112
+ This is used to determine whether we can safely kill all Chrome processes or if we should
113
+ be more conservative (to avoid killing the user's main browser).
114
+
115
+ Returns True if user_data_dir matches a known default Chrome profile path for the current platform.
116
+ """
117
+ import platform
118
+
119
+ system = platform.system()
120
+ user_data_dir_resolved = str(Path(user_data_dir).resolve())
121
+
122
+ # Define default profile locations for each platform
123
+ if system == "Darwin": # macOS
124
+ default_paths = [
125
+ str(Path.home() / "Library" / "Application Support" / "Google" / "Chrome"),
126
+ str(Path.home() / "Library" / "Application Support" / "Google" / "Chrome Beta"),
127
+ str(Path.home() / "Library" / "Application Support" / "Google" / "Chrome Canary"),
128
+ ]
129
+ elif system == "Windows":
130
+ default_paths = [
131
+ str(Path.home() / "AppData" / "Local" / "Google" / "Chrome" / "User Data"),
132
+ str(Path.home() / "AppData" / "Local" / "Google" / "Chrome Beta" / "User Data"),
133
+ str(Path.home() / "AppData" / "Local" / "Google" / "Chrome SxS" / "User Data"), # Canary
134
+ ]
135
+ elif system == "Linux":
136
+ default_paths = [
137
+ str(Path.home() / ".config" / "google-chrome"),
138
+ str(Path.home() / ".config" / "google-chrome-beta"),
139
+ str(Path.home() / ".config" / "google-chrome-unstable"), # Canary equivalent
140
+ ]
141
+ else:
142
+ # Unknown platform - assume it's not default to be safe
143
+ return False
144
+
145
+ # Resolve all default paths and check for matches
146
+ for default_path in default_paths:
147
+ try:
148
+ default_resolved = str(Path(default_path).resolve())
149
+ if user_data_dir_resolved == default_resolved:
150
+ return True
151
+ except Exception:
152
+ # If we can't resolve a path, skip it
153
+ continue
154
+
155
+ return False
@@ -0,0 +1,97 @@
1
+ """Path utilities and management for browser configuration."""
2
+
3
+ import os
4
+ import tempfile
5
+ from pathlib import Path
6
+
7
+ from .environment import profile_key
8
+
9
+
10
+ _DEFAULT_LOCK_DIR = None
11
+
12
+
13
+ def get_lock_dir() -> str:
14
+ """
15
+ Get the lock directory path.
16
+
17
+ Uses MCP_BROWSER_LOCK_DIR env var if set, otherwise uses:
18
+ <repo_root>/tmp/mcp_locks
19
+
20
+ The directory is created if it doesn't exist.
21
+
22
+ Returns:
23
+ Absolute path to lock directory
24
+ """
25
+ global _DEFAULT_LOCK_DIR
26
+
27
+ if _DEFAULT_LOCK_DIR is None:
28
+ # Calculate default: <repo_root>/tmp/mcp_locks
29
+ repo_root = Path(__file__).parent.parent.parent.parent
30
+ _DEFAULT_LOCK_DIR = str(repo_root / "tmp" / "mcp_locks")
31
+
32
+ lock_dir = os.getenv("MCP_BROWSER_LOCK_DIR") or _DEFAULT_LOCK_DIR
33
+
34
+ # Ensure directory exists
35
+ Path(lock_dir).mkdir(parents=True, exist_ok=True)
36
+
37
+ return lock_dir
38
+
39
+
40
+ def rendezvous_path(config: dict) -> str:
41
+ """Get the path to the rendezvous file for inter-process communication."""
42
+ return os.path.join(tempfile.gettempdir(), f"mcp_chrome_rendezvous_{profile_key(config)}.json")
43
+
44
+
45
+ def start_lock_dir(config: dict) -> str:
46
+ """Get the directory path for the startup lock."""
47
+ return os.path.join(tempfile.gettempdir(), f"mcp_chrome_start_lock_{profile_key(config)}")
48
+
49
+
50
+ def chromedriver_log_path(config: dict) -> str:
51
+ """Get the path to the ChromeDriver log file."""
52
+ return os.path.join(tempfile.gettempdir(), f"chromedriver_shared_{profile_key(config)}_{os.getpid()}.log")
53
+
54
+
55
+ def _lock_paths():
56
+ """
57
+ Return paths for the action lock files (softlock JSON, mutex, and startup mutex).
58
+ These paths are based on the profile key and LOCK_DIR environment variable.
59
+ """
60
+ from ..config.environment import get_env_config
61
+
62
+ # Get LOCK_DIR from global state
63
+ _DEFAULT_LOCK_DIR = str(Path(__file__).parent.parent.parent.parent / "tmp" / "mcp_locks")
64
+ LOCK_DIR = os.getenv("MCP_BROWSER_LOCK_DIR") or _DEFAULT_LOCK_DIR
65
+ Path(LOCK_DIR).mkdir(parents=True, exist_ok=True)
66
+
67
+ key = profile_key(get_env_config()) # stable across processes; independent of port
68
+ base = Path(LOCK_DIR)
69
+ base.mkdir(parents=True, exist_ok=True)
70
+ softlock_json = base / f"{key}.softlock.json"
71
+ softlock_mutex = base / f"{key}.softlock.mutex"
72
+ startup_mutex = base / f"{key}.startup.mutex"
73
+ return str(softlock_json), str(softlock_mutex), str(startup_mutex)
74
+
75
+
76
+ def _window_registry_path() -> str:
77
+ """
78
+ Return the path to the window registry file for tracking window ownership.
79
+ """
80
+ from ..config.environment import get_env_config
81
+
82
+ # Get LOCK_DIR from global state
83
+ _DEFAULT_LOCK_DIR = str(Path(__file__).parent.parent.parent.parent / "tmp" / "mcp_locks")
84
+ LOCK_DIR = os.getenv("MCP_BROWSER_LOCK_DIR") or _DEFAULT_LOCK_DIR
85
+
86
+ key = profile_key(get_env_config())
87
+ return os.path.join(LOCK_DIR, f"{key}.window_registry.json")
88
+
89
+
90
+ def _same_dir(a: str, b: str) -> bool:
91
+ """Compare two directory paths for equality, normalizing for platform differences."""
92
+ if not a or not b:
93
+ return False
94
+ try:
95
+ return os.path.normcase(os.path.realpath(a)) == os.path.normcase(os.path.realpath(b))
96
+ except Exception:
97
+ return a == b
@@ -0,0 +1,68 @@
1
+ """
2
+ Global constants and configuration defaults.
3
+ No dependencies - safe to import from anywhere.
4
+
5
+ This module extracts constants from helpers.py to break circular dependencies.
6
+ """
7
+
8
+ import os
9
+
10
+ # ============================================================================
11
+ # Lock Configuration
12
+ # ============================================================================
13
+
14
+ ACTION_LOCK_TTL_SECS = int(os.getenv("MCP_ACTION_LOCK_TTL", "30"))
15
+ """Time-to-live for action locks in seconds."""
16
+
17
+ ACTION_LOCK_WAIT_SECS = int(os.getenv("MCP_ACTION_LOCK_WAIT", "60"))
18
+ """Maximum time to wait for action lock acquisition in seconds."""
19
+
20
+ FILE_MUTEX_STALE_SECS = int(os.getenv("MCP_FILE_MUTEX_STALE_SECS", "60"))
21
+ """Consider file mutex stale after this many seconds."""
22
+
23
+
24
+ # ============================================================================
25
+ # Window Registry Configuration
26
+ # ============================================================================
27
+
28
+ WINDOW_REGISTRY_STALE_THRESHOLD = int(os.getenv("MCP_WINDOW_REGISTRY_STALE_SECS", "300"))
29
+ """Consider window registry entry stale after this many seconds."""
30
+
31
+
32
+ # ============================================================================
33
+ # Rendering Configuration
34
+ # ============================================================================
35
+
36
+ MAX_SNAPSHOT_CHARS = int(os.getenv("MCP_MAX_SNAPSHOT_CHARS", "10000"))
37
+ """Maximum characters in HTML snapshots."""
38
+
39
+
40
+ # ============================================================================
41
+ # Chrome Startup Configuration
42
+ # ============================================================================
43
+
44
+ START_LOCK_WAIT_SEC = 8.0
45
+ """How long to wait to acquire the startup lock."""
46
+
47
+ RENDEZVOUS_TTL_SEC = 24 * 3600
48
+ """How long a rendezvous file is considered valid (24 hours)."""
49
+
50
+
51
+ # ============================================================================
52
+ # Feature Flags
53
+ # ============================================================================
54
+
55
+ ALLOW_ATTACH_ANY = os.getenv("MCP_ATTACH_ANY_PROFILE", "0") == "1"
56
+ """Allow attaching to any Chrome profile, not just the configured one."""
57
+
58
+
59
+ __all__ = [
60
+ "ACTION_LOCK_TTL_SECS",
61
+ "ACTION_LOCK_WAIT_SECS",
62
+ "FILE_MUTEX_STALE_SECS",
63
+ "WINDOW_REGISTRY_STALE_THRESHOLD",
64
+ "MAX_SNAPSHOT_CHARS",
65
+ "START_LOCK_WAIT_SEC",
66
+ "RENDEZVOUS_TTL_SEC",
67
+ "ALLOW_ATTACH_ANY",
68
+ ]
@@ -0,0 +1,150 @@
1
+ """
2
+ Centralized browser state management.
3
+
4
+ This module replaces module-level globals with a context object,
5
+ providing a single source of truth for browser session state.
6
+
7
+ Thread Safety:
8
+ The BrowserContext itself is NOT thread-safe. Access should be
9
+ coordinated using the locking decorators (exclusive_browser_access).
10
+
11
+ Usage:
12
+ from mcp_browser_use.context import get_context
13
+
14
+ ctx = get_context()
15
+ if ctx.driver is None:
16
+ # Initialize driver
17
+ ctx.driver = create_driver()
18
+ """
19
+
20
+ from typing import Optional
21
+ from selenium import webdriver
22
+ from dataclasses import dataclass, field
23
+ import asyncio
24
+
25
+
26
+ @dataclass
27
+ class BrowserContext:
28
+ """
29
+ Encapsulates all browser session state.
30
+
31
+ This replaces the module-level globals:
32
+ DRIVER, DEBUGGER_HOST, DEBUGGER_PORT, TARGET_ID,
33
+ WINDOW_ID, MY_TAG, LOCK_DIR, etc.
34
+
35
+ Attributes:
36
+ driver: Selenium WebDriver instance
37
+ debugger_host: Chrome DevTools debugger hostname
38
+ debugger_port: Chrome DevTools debugger port
39
+ target_id: Chrome DevTools Protocol target ID for this window
40
+ window_id: Chrome window ID (from Browser.getWindowForTarget)
41
+ process_tag: Unique identifier for this process/session
42
+ config: Environment configuration dictionary
43
+ lock_dir: Directory for lock files
44
+ intra_process_lock: Asyncio lock for serializing operations within this process
45
+ """
46
+
47
+ # Driver state
48
+ driver: Optional[webdriver.Chrome] = None
49
+ debugger_host: Optional[str] = None
50
+ debugger_port: Optional[int] = None
51
+
52
+ # Window state
53
+ target_id: Optional[str] = None
54
+ window_id: Optional[int] = None
55
+
56
+ # Process identity
57
+ process_tag: Optional[str] = None
58
+
59
+ # Configuration (should be immutable after initialization)
60
+ config: dict = field(default_factory=dict)
61
+
62
+ # Lock directory
63
+ lock_dir: str = ""
64
+
65
+ # Intra-process lock
66
+ intra_process_lock: Optional[asyncio.Lock] = None
67
+
68
+ def is_driver_initialized(self) -> bool:
69
+ """Check if driver is initialized."""
70
+ return self.driver is not None
71
+
72
+ def is_window_ready(self) -> bool:
73
+ """Check if browser window is ready."""
74
+ return (
75
+ self.driver is not None
76
+ and self.target_id is not None
77
+ )
78
+
79
+ def get_debugger_address(self) -> Optional[str]:
80
+ """Get debugger address as host:port string."""
81
+ if self.debugger_host and self.debugger_port:
82
+ return f"{self.debugger_host}:{self.debugger_port}"
83
+ return None
84
+
85
+ def reset_window_state(self) -> None:
86
+ """Reset window state (useful after window close)."""
87
+ self.target_id = None
88
+ self.window_id = None
89
+
90
+ def get_intra_process_lock(self) -> asyncio.Lock:
91
+ """Get or create the intra-process asyncio lock."""
92
+ if self.intra_process_lock is None:
93
+ self.intra_process_lock = asyncio.Lock()
94
+ return self.intra_process_lock
95
+
96
+
97
+ # ============================================================================
98
+ # Global Context Management
99
+ # ============================================================================
100
+
101
+ _global_context: Optional[BrowserContext] = None
102
+
103
+
104
+ def get_context() -> BrowserContext:
105
+ """
106
+ Get or create the global browser context.
107
+
108
+ This is a singleton pattern - all calls return the same context instance.
109
+ Use reset_context() to clear the singleton (mainly for testing).
110
+
111
+ Returns:
112
+ The global BrowserContext instance
113
+ """
114
+ global _global_context
115
+
116
+ if _global_context is None:
117
+ # Lazy initialization - import here to avoid circular dependencies
118
+ try:
119
+ from .config.environment import get_env_config
120
+ from .config.paths import get_lock_dir
121
+
122
+ _global_context = BrowserContext(
123
+ config=get_env_config(),
124
+ lock_dir=get_lock_dir(),
125
+ )
126
+ except Exception:
127
+ # If config not available yet, create minimal context
128
+ _global_context = BrowserContext()
129
+
130
+ return _global_context
131
+
132
+
133
+ def reset_context() -> None:
134
+ """
135
+ Reset the global context.
136
+
137
+ ⚠️ WARNING: This is primarily for testing. In production code,
138
+ use close_browser() instead of directly resetting context.
139
+
140
+ This will clear all state including driver, window IDs, etc.
141
+ """
142
+ global _global_context
143
+ _global_context = None
144
+
145
+
146
+ __all__ = [
147
+ "BrowserContext",
148
+ "get_context",
149
+ "reset_context",
150
+ ]
@@ -0,0 +1,85 @@
1
+ # mcp_browser_use/context_pack.py
2
+
3
+ from dataclasses import dataclass, field
4
+ from typing import List, Dict, Optional, Any
5
+
6
+
7
+ class ReturnMode:
8
+ HTML = "html"
9
+ TEXT = "text"
10
+ OUTLINE = "outline"
11
+ DOMPATHS = "dompaths"
12
+ MIXED = "mixed"
13
+
14
+ class CleaningLevel:
15
+ RAW_VISIBLE = 0
16
+ LIGHT = 1
17
+ DEFAULT = 2
18
+ AGGRESSIVE = 3
19
+
20
+ @dataclass
21
+ class IframeInfo:
22
+ index: int
23
+ name: Optional[str]
24
+ id: Optional[str]
25
+ src: Optional[str]
26
+ same_origin: Optional[bool]
27
+ css_path: Optional[str]
28
+ visible: Optional[bool]
29
+ summary_title: Optional[str] = None
30
+ subtree_id: Optional[str] = None
31
+
32
+ @dataclass
33
+ class OutlineItem:
34
+ level: int
35
+ text: str
36
+ word_count: int
37
+ css_path: Optional[str]
38
+ subtree_id: Optional[str]
39
+
40
+ @dataclass
41
+ class CatalogInteractive:
42
+ role: Optional[str]
43
+ text_excerpt: str
44
+ css_path: Optional[str]
45
+ xpath: Optional[str]
46
+ nth_path: Optional[str]
47
+ clickable: Optional[bool]
48
+ enabled: Optional[bool]
49
+
50
+ @dataclass
51
+ class ContextPack:
52
+ # meta
53
+ window_tag: Optional[str]
54
+ url: Optional[str]
55
+ title: Optional[str]
56
+ page_fingerprint: Optional[str] = None
57
+ lock_owner: Optional[str] = None
58
+ lock_expires_at: Optional[str] = None
59
+
60
+ # stats
61
+ cleaning_level_applied: int = CleaningLevel.DEFAULT
62
+ approx_tokens: int = 0
63
+ pruned_counts: Dict[str, int] = field(default_factory=dict)
64
+ tokens_budget: Optional[int] = None
65
+ nodes_kept: Optional[int] = None
66
+ nodes_pruned: Optional[int] = None
67
+ hard_capped: bool = False
68
+
69
+ # presence flags
70
+ snapshot_mode: str = ReturnMode.OUTLINE
71
+ outline_present: bool = False
72
+ diff_present: bool = False
73
+ iframe_index_present: bool = False
74
+
75
+ # payloads
76
+ outline: List[OutlineItem] = field(default_factory=list)
77
+ html: Optional[str] = None
78
+ text: Optional[str] = None
79
+ dompaths: Optional[List[Dict[str, Any]]] = None
80
+ mixed: Optional[Dict[str, Any]] = None
81
+ catalogs: Optional[Dict[str, Any]] = None
82
+ forms: Optional[Dict[str, Any]] = None
83
+ iframe_index: Optional[List[IframeInfo]] = None
84
+
85
+ errors: List[Dict[str, Any]] = field(default_factory=list)
@@ -0,0 +1,13 @@
1
+ # mcp_browser_use/decorators/__init__.py
2
+ #
3
+ # Re-exports decorators from their respective modules.
4
+
5
+ from .ensure import ensure_driver_ready
6
+ from .locking import exclusive_browser_access
7
+ from .envelope import tool_envelope
8
+
9
+ __all__ = [
10
+ "ensure_driver_ready",
11
+ "exclusive_browser_access",
12
+ "tool_envelope",
13
+ ]
@@ -0,0 +1,84 @@
1
+ # mcp_browser_use/decorators/ensure.py
2
+ import json
3
+ import inspect
4
+ import functools
5
+
6
+ def ensure_driver_ready(_func=None, *, include_snapshot=False, include_diagnostics=False):
7
+ def decorator(fn):
8
+ if inspect.iscoroutinefunction(fn):
9
+ @functools.wraps(fn)
10
+ async def wrapper(*args, **kwargs):
11
+ import mcp_browser_use.helpers as helpers # module import, not from-import
12
+ from mcp_browser_use.config.environment import get_env_config
13
+ from mcp_browser_use.utils.diagnostics import collect_diagnostics
14
+
15
+ # Check if driver is already initialized, but don't auto-initialize
16
+ if helpers.get_context().driver is None:
17
+ payload = {
18
+ "ok": False,
19
+ "error": "browser_not_started",
20
+ "message": "Browser session not started. Please call 'start_browser' first before using browser actions."
21
+ }
22
+ if include_snapshot:
23
+ payload["snapshot"] = {"url": None, "title": None, "html": "", "truncated": False}
24
+ if include_diagnostics:
25
+ try:
26
+ payload["diagnostics"] = collect_diagnostics(None, None, get_env_config())
27
+ except Exception:
28
+ pass
29
+ return json.dumps(payload)
30
+
31
+ # Ensure we have a valid window for this driver
32
+ try:
33
+ helpers._ensure_singleton_window(helpers.get_context().driver)
34
+ except Exception:
35
+ payload = {
36
+ "ok": False,
37
+ "error": "browser_window_lost",
38
+ "message": "Browser window was lost. Please call 'start_browser' to create a new session."
39
+ }
40
+ if include_snapshot:
41
+ payload["snapshot"] = {"url": None, "title": None, "html": "", "truncated": False}
42
+ return json.dumps(payload)
43
+
44
+ return await fn(*args, **kwargs)
45
+ return wrapper
46
+ else:
47
+ @functools.wraps(fn)
48
+ def wrapper(*args, **kwargs):
49
+ import mcp_browser_use.helpers as helpers # module import, not from-import
50
+ from mcp_browser_use.config.environment import get_env_config
51
+ from mcp_browser_use.utils.diagnostics import collect_diagnostics
52
+
53
+ # Check if driver is already initialized, but don't auto-initialize
54
+ if helpers.get_context().driver is None:
55
+ payload = {
56
+ "ok": False,
57
+ "error": "browser_not_started",
58
+ "message": "Browser session not started. Please call 'start_browser' first before using browser actions."
59
+ }
60
+ if include_snapshot:
61
+ payload["snapshot"] = {"url": None, "title": None, "html": "", "truncated": False}
62
+ if include_diagnostics:
63
+ try:
64
+ payload["diagnostics"] = collect_diagnostics(None, None, get_env_config())
65
+ except Exception:
66
+ pass
67
+ return json.dumps(payload)
68
+
69
+ # Ensure we have a valid window for this driver
70
+ try:
71
+ helpers._ensure_singleton_window(helpers.get_context().driver)
72
+ except Exception:
73
+ payload = {
74
+ "ok": False,
75
+ "error": "browser_window_lost",
76
+ "message": "Browser window was lost. Please call 'start_browser' to create a new session."
77
+ }
78
+ if include_snapshot:
79
+ payload["snapshot"] = {"url": None, "title": None, "html": "", "truncated": False}
80
+ return json.dumps(payload)
81
+
82
+ return fn(*args, **kwargs)
83
+ return wrapper
84
+ return decorator if _func is None else decorator(_func)