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.
- iflow_mcp_janspoerer_mcp_browser_use-0.1.0.dist-info/METADATA +26 -0
- iflow_mcp_janspoerer_mcp_browser_use-0.1.0.dist-info/RECORD +50 -0
- iflow_mcp_janspoerer_mcp_browser_use-0.1.0.dist-info/WHEEL +5 -0
- iflow_mcp_janspoerer_mcp_browser_use-0.1.0.dist-info/entry_points.txt +2 -0
- iflow_mcp_janspoerer_mcp_browser_use-0.1.0.dist-info/licenses/LICENSE +201 -0
- iflow_mcp_janspoerer_mcp_browser_use-0.1.0.dist-info/top_level.txt +1 -0
- mcp_browser_use/__init__.py +2 -0
- mcp_browser_use/__main__.py +1347 -0
- mcp_browser_use/actions/__init__.py +1 -0
- mcp_browser_use/actions/elements.py +173 -0
- mcp_browser_use/actions/extraction.py +864 -0
- mcp_browser_use/actions/keyboard.py +43 -0
- mcp_browser_use/actions/navigation.py +73 -0
- mcp_browser_use/actions/screenshots.py +85 -0
- mcp_browser_use/browser/__init__.py +1 -0
- mcp_browser_use/browser/chrome.py +150 -0
- mcp_browser_use/browser/chrome_executable.py +204 -0
- mcp_browser_use/browser/chrome_launcher.py +330 -0
- mcp_browser_use/browser/chrome_process.py +104 -0
- mcp_browser_use/browser/devtools.py +230 -0
- mcp_browser_use/browser/driver.py +322 -0
- mcp_browser_use/browser/process.py +133 -0
- mcp_browser_use/cleaners.py +530 -0
- mcp_browser_use/config/__init__.py +30 -0
- mcp_browser_use/config/environment.py +155 -0
- mcp_browser_use/config/paths.py +97 -0
- mcp_browser_use/constants.py +68 -0
- mcp_browser_use/context.py +150 -0
- mcp_browser_use/context_pack.py +85 -0
- mcp_browser_use/decorators/__init__.py +13 -0
- mcp_browser_use/decorators/ensure.py +84 -0
- mcp_browser_use/decorators/envelope.py +83 -0
- mcp_browser_use/decorators/locking.py +172 -0
- mcp_browser_use/helpers.py +173 -0
- mcp_browser_use/helpers_context.py +261 -0
- mcp_browser_use/locking/__init__.py +1 -0
- mcp_browser_use/locking/action_lock.py +190 -0
- mcp_browser_use/locking/file_mutex.py +139 -0
- mcp_browser_use/locking/window_registry.py +178 -0
- mcp_browser_use/tools/__init__.py +59 -0
- mcp_browser_use/tools/browser_management.py +260 -0
- mcp_browser_use/tools/debugging.py +195 -0
- mcp_browser_use/tools/extraction.py +58 -0
- mcp_browser_use/tools/interaction.py +323 -0
- mcp_browser_use/tools/navigation.py +84 -0
- mcp_browser_use/tools/screenshots.py +116 -0
- mcp_browser_use/utils/__init__.py +1 -0
- mcp_browser_use/utils/diagnostics.py +85 -0
- mcp_browser_use/utils/html_utils.py +118 -0
- 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']
|