seleniumboot-mcp 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.
- selenium_mcp/__init__.py +0 -0
- selenium_mcp/server.py +78 -0
- selenium_mcp/tools/__init__.py +0 -0
- selenium_mcp/tools/assertion_tools.py +245 -0
- selenium_mcp/tools/browser_tools.py +240 -0
- selenium_mcp/tools/codegen_tools.py +391 -0
- selenium_mcp/tools/element_tools.py +364 -0
- seleniumboot_mcp-0.1.0.dist-info/METADATA +229 -0
- seleniumboot_mcp-0.1.0.dist-info/RECORD +16 -0
- seleniumboot_mcp-0.1.0.dist-info/WHEEL +5 -0
- seleniumboot_mcp-0.1.0.dist-info/entry_points.txt +2 -0
- seleniumboot_mcp-0.1.0.dist-info/top_level.txt +2 -0
- tools/assertion_tools.py +245 -0
- tools/browser_tools.py +240 -0
- tools/codegen_tools.py +391 -0
- tools/element_tools.py +364 -0
selenium_mcp/__init__.py
ADDED
|
File without changes
|
selenium_mcp/server.py
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Selenium MCP Server
|
|
3
|
+
A Python-based MCP server for Selenium WebDriver automation.
|
|
4
|
+
Serves both Python and Java test automation users.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import asyncio
|
|
8
|
+
import logging
|
|
9
|
+
import tempfile
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from mcp.server import Server
|
|
12
|
+
from mcp.server.stdio import stdio_server
|
|
13
|
+
from mcp.types import Tool, TextContent, ImageContent
|
|
14
|
+
|
|
15
|
+
from selenium_mcp.tools.browser_tools import BrowserTools
|
|
16
|
+
from selenium_mcp.tools.element_tools import ElementTools
|
|
17
|
+
from selenium_mcp.tools.assertion_tools import AssertionTools
|
|
18
|
+
from selenium_mcp.tools.codegen_tools import CodegenTools
|
|
19
|
+
|
|
20
|
+
_log_path = Path(tempfile.gettempdir()) / "selenium-mcp.log"
|
|
21
|
+
logging.basicConfig(
|
|
22
|
+
filename=str(_log_path),
|
|
23
|
+
level=logging.DEBUG,
|
|
24
|
+
format="%(asctime)s [%(levelname)s] %(message)s"
|
|
25
|
+
)
|
|
26
|
+
log = logging.getLogger(__name__)
|
|
27
|
+
|
|
28
|
+
app = Server("selenium-mcp")
|
|
29
|
+
|
|
30
|
+
browser = BrowserTools()
|
|
31
|
+
element = ElementTools(browser)
|
|
32
|
+
assertion = AssertionTools(browser)
|
|
33
|
+
codegen = CodegenTools(browser)
|
|
34
|
+
|
|
35
|
+
ALL_TOOLS = [
|
|
36
|
+
*browser.get_tools(),
|
|
37
|
+
*element.get_tools(),
|
|
38
|
+
*assertion.get_tools(),
|
|
39
|
+
*codegen.get_tools(),
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
TOOL_HANDLERS = {
|
|
43
|
+
**browser.get_handlers(),
|
|
44
|
+
**element.get_handlers(),
|
|
45
|
+
**assertion.get_handlers(),
|
|
46
|
+
**codegen.get_handlers(),
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@app.list_tools()
|
|
51
|
+
async def list_tools() -> list[Tool]:
|
|
52
|
+
return ALL_TOOLS
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@app.call_tool()
|
|
56
|
+
async def call_tool(name: str, arguments: dict) -> list[TextContent]:
|
|
57
|
+
log.info(f"Tool called: {name} | args: {arguments}")
|
|
58
|
+
handler = TOOL_HANDLERS.get(name)
|
|
59
|
+
if not handler:
|
|
60
|
+
return [TextContent(type="text", text=f"Unknown tool: {name}")]
|
|
61
|
+
try:
|
|
62
|
+
result = await handler(arguments)
|
|
63
|
+
if isinstance(result, str) and result.startswith("screenshot:base64:"):
|
|
64
|
+
b64 = result[len("screenshot:base64:"):]
|
|
65
|
+
return [ImageContent(type="image", data=b64, mimeType="image/png")]
|
|
66
|
+
return [TextContent(type="text", text=result)]
|
|
67
|
+
except Exception as e:
|
|
68
|
+
log.error(f"Tool error [{name}]: {e}")
|
|
69
|
+
return [TextContent(type="text", text=f"Error: {str(e)}")]
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
async def main():
|
|
73
|
+
async with stdio_server() as (r, w):
|
|
74
|
+
await app.run(r, w, app.create_initialization_options())
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
if __name__ == "__main__":
|
|
78
|
+
asyncio.run(main())
|
|
File without changes
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Assertion Tools — validate page state, element state, text, URL, title
|
|
3
|
+
Returns PASS/FAIL with detail — useful for AI-driven test verification.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from selenium.webdriver.common.by import By
|
|
7
|
+
from selenium.webdriver.support.ui import WebDriverWait
|
|
8
|
+
from selenium.webdriver.support import expected_conditions as EC
|
|
9
|
+
from mcp.types import Tool
|
|
10
|
+
|
|
11
|
+
BY_MAP = {
|
|
12
|
+
"css": By.CSS_SELECTOR,
|
|
13
|
+
"xpath": By.XPATH,
|
|
14
|
+
"id": By.ID,
|
|
15
|
+
"name": By.NAME,
|
|
16
|
+
"tag": By.TAG_NAME,
|
|
17
|
+
"class": By.CLASS_NAME,
|
|
18
|
+
"link": By.LINK_TEXT,
|
|
19
|
+
"partial_link": By.PARTIAL_LINK_TEXT,
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class AssertionTools:
|
|
24
|
+
def __init__(self, browser_tools):
|
|
25
|
+
self.browser = browser_tools
|
|
26
|
+
|
|
27
|
+
def _driver(self):
|
|
28
|
+
return self.browser.get_driver()
|
|
29
|
+
|
|
30
|
+
def _by(self, strategy: str):
|
|
31
|
+
return BY_MAP.get(strategy, By.CSS_SELECTOR)
|
|
32
|
+
|
|
33
|
+
def get_tools(self) -> list[Tool]:
|
|
34
|
+
return [
|
|
35
|
+
Tool(
|
|
36
|
+
name="assert_title",
|
|
37
|
+
description="Assert the page title equals or contains expected text.",
|
|
38
|
+
inputSchema={
|
|
39
|
+
"type": "object",
|
|
40
|
+
"properties": {
|
|
41
|
+
"expected": {"type": "string"},
|
|
42
|
+
"exact": {"type": "boolean", "default": False}
|
|
43
|
+
},
|
|
44
|
+
"required": ["expected"],
|
|
45
|
+
},
|
|
46
|
+
),
|
|
47
|
+
Tool(
|
|
48
|
+
name="assert_url",
|
|
49
|
+
description="Assert the current URL equals or contains expected value.",
|
|
50
|
+
inputSchema={
|
|
51
|
+
"type": "object",
|
|
52
|
+
"properties": {
|
|
53
|
+
"expected": {"type": "string"},
|
|
54
|
+
"exact": {"type": "boolean", "default": False}
|
|
55
|
+
},
|
|
56
|
+
"required": ["expected"],
|
|
57
|
+
},
|
|
58
|
+
),
|
|
59
|
+
Tool(
|
|
60
|
+
name="assert_text",
|
|
61
|
+
description="Assert element text equals or contains expected value.",
|
|
62
|
+
inputSchema={
|
|
63
|
+
"type": "object",
|
|
64
|
+
"properties": {
|
|
65
|
+
"selector": {"type": "string"},
|
|
66
|
+
"by": {"type": "string", "default": "css"},
|
|
67
|
+
"expected": {"type": "string"},
|
|
68
|
+
"exact": {"type": "boolean", "default": False},
|
|
69
|
+
"timeout": {"type": "integer", "default": 10}
|
|
70
|
+
},
|
|
71
|
+
"required": ["selector", "expected"],
|
|
72
|
+
},
|
|
73
|
+
),
|
|
74
|
+
Tool(
|
|
75
|
+
name="assert_element_visible",
|
|
76
|
+
description="Assert an element is visible on the page.",
|
|
77
|
+
inputSchema={
|
|
78
|
+
"type": "object",
|
|
79
|
+
"properties": {
|
|
80
|
+
"selector": {"type": "string"},
|
|
81
|
+
"by": {"type": "string", "default": "css"},
|
|
82
|
+
"timeout": {"type": "integer", "default": 10}
|
|
83
|
+
},
|
|
84
|
+
"required": ["selector"],
|
|
85
|
+
},
|
|
86
|
+
),
|
|
87
|
+
Tool(
|
|
88
|
+
name="assert_element_not_visible",
|
|
89
|
+
description="Assert an element is NOT visible (hidden or absent).",
|
|
90
|
+
inputSchema={
|
|
91
|
+
"type": "object",
|
|
92
|
+
"properties": {
|
|
93
|
+
"selector": {"type": "string"},
|
|
94
|
+
"by": {"type": "string", "default": "css"},
|
|
95
|
+
"timeout": {"type": "integer", "default": 3}
|
|
96
|
+
},
|
|
97
|
+
"required": ["selector"],
|
|
98
|
+
},
|
|
99
|
+
),
|
|
100
|
+
Tool(
|
|
101
|
+
name="assert_attribute",
|
|
102
|
+
description="Assert an element attribute has the expected value.",
|
|
103
|
+
inputSchema={
|
|
104
|
+
"type": "object",
|
|
105
|
+
"properties": {
|
|
106
|
+
"selector": {"type": "string"},
|
|
107
|
+
"by": {"type": "string", "default": "css"},
|
|
108
|
+
"attribute": {"type": "string"},
|
|
109
|
+
"expected": {"type": "string"},
|
|
110
|
+
"exact": {"type": "boolean", "default": True},
|
|
111
|
+
"timeout": {"type": "integer", "default": 10}
|
|
112
|
+
},
|
|
113
|
+
"required": ["selector", "attribute", "expected"],
|
|
114
|
+
},
|
|
115
|
+
),
|
|
116
|
+
Tool(
|
|
117
|
+
name="assert_page_contains",
|
|
118
|
+
description="Assert the full page source or visible text contains a string.",
|
|
119
|
+
inputSchema={
|
|
120
|
+
"type": "object",
|
|
121
|
+
"properties": {
|
|
122
|
+
"text": {"type": "string"},
|
|
123
|
+
"source_only": {
|
|
124
|
+
"type": "boolean",
|
|
125
|
+
"default": False,
|
|
126
|
+
"description": "If true, checks page source; otherwise checks body visible text"
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
"required": ["text"],
|
|
130
|
+
},
|
|
131
|
+
),
|
|
132
|
+
Tool(
|
|
133
|
+
name="assert_element_count",
|
|
134
|
+
description="Assert number of elements matching selector equals expected count.",
|
|
135
|
+
inputSchema={
|
|
136
|
+
"type": "object",
|
|
137
|
+
"properties": {
|
|
138
|
+
"selector": {"type": "string"},
|
|
139
|
+
"by": {"type": "string", "default": "css"},
|
|
140
|
+
"expected_count": {"type": "integer"}
|
|
141
|
+
},
|
|
142
|
+
"required": ["selector", "expected_count"],
|
|
143
|
+
},
|
|
144
|
+
),
|
|
145
|
+
]
|
|
146
|
+
|
|
147
|
+
def get_handlers(self) -> dict:
|
|
148
|
+
return {
|
|
149
|
+
"assert_title": self._assert_title,
|
|
150
|
+
"assert_url": self._assert_url,
|
|
151
|
+
"assert_text": self._assert_text,
|
|
152
|
+
"assert_element_visible": self._assert_element_visible,
|
|
153
|
+
"assert_element_not_visible": self._assert_element_not_visible,
|
|
154
|
+
"assert_attribute": self._assert_attribute,
|
|
155
|
+
"assert_page_contains": self._assert_page_contains,
|
|
156
|
+
"assert_element_count": self._assert_element_count,
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
# ------------------------------------------------------------------ #
|
|
160
|
+
# Helpers #
|
|
161
|
+
# ------------------------------------------------------------------ #
|
|
162
|
+
def _pass(self, msg): return f"✅ PASS | {msg}"
|
|
163
|
+
def _fail(self, msg): return f"❌ FAIL | {msg}"
|
|
164
|
+
|
|
165
|
+
# ------------------------------------------------------------------ #
|
|
166
|
+
# Handlers #
|
|
167
|
+
# ------------------------------------------------------------------ #
|
|
168
|
+
async def _assert_title(self, args: dict) -> str:
|
|
169
|
+
actual = self._driver().title
|
|
170
|
+
exp = args["expected"]
|
|
171
|
+
ok = (actual == exp) if args.get("exact", False) else (exp.lower() in actual.lower())
|
|
172
|
+
return self._pass(f"title='{actual}'") if ok else self._fail(f"expected='{exp}' | actual='{actual}'")
|
|
173
|
+
|
|
174
|
+
async def _assert_url(self, args: dict) -> str:
|
|
175
|
+
actual = self._driver().current_url
|
|
176
|
+
exp = args["expected"]
|
|
177
|
+
ok = (actual == exp) if args.get("exact", False) else (exp in actual)
|
|
178
|
+
return self._pass(f"url='{actual}'") if ok else self._fail(f"expected='{exp}' | actual='{actual}'")
|
|
179
|
+
|
|
180
|
+
async def _assert_text(self, args: dict) -> str:
|
|
181
|
+
by = self._by(args.get("by", "css"))
|
|
182
|
+
timeout = args.get("timeout", 10)
|
|
183
|
+
try:
|
|
184
|
+
el = WebDriverWait(self._driver(), timeout).until(
|
|
185
|
+
EC.visibility_of_element_located((by, args["selector"]))
|
|
186
|
+
)
|
|
187
|
+
except Exception:
|
|
188
|
+
return self._fail(f"element '{args['selector']}' not found within {timeout}s")
|
|
189
|
+
actual = el.text
|
|
190
|
+
exp = args["expected"]
|
|
191
|
+
ok = (actual == exp) if args.get("exact", False) else (exp.lower() in actual.lower())
|
|
192
|
+
return self._pass(f"text='{actual}'") if ok else self._fail(f"expected='{exp}' | actual='{actual}'")
|
|
193
|
+
|
|
194
|
+
async def _assert_element_visible(self, args: dict) -> str:
|
|
195
|
+
by = self._by(args.get("by", "css"))
|
|
196
|
+
timeout = args.get("timeout", 10)
|
|
197
|
+
try:
|
|
198
|
+
WebDriverWait(self._driver(), timeout).until(
|
|
199
|
+
EC.visibility_of_element_located((by, args["selector"]))
|
|
200
|
+
)
|
|
201
|
+
return self._pass(f"'{args['selector']}' is visible")
|
|
202
|
+
except Exception:
|
|
203
|
+
return self._fail(f"'{args['selector']}' is NOT visible")
|
|
204
|
+
|
|
205
|
+
async def _assert_element_not_visible(self, args: dict) -> str:
|
|
206
|
+
by = self._by(args.get("by", "css"))
|
|
207
|
+
timeout = args.get("timeout", 3)
|
|
208
|
+
try:
|
|
209
|
+
WebDriverWait(self._driver(), timeout).until(
|
|
210
|
+
EC.invisibility_of_element_located((by, args["selector"]))
|
|
211
|
+
)
|
|
212
|
+
return self._pass(f"'{args['selector']}' is not visible")
|
|
213
|
+
except Exception:
|
|
214
|
+
return self._fail(f"'{args['selector']}' IS visible (expected hidden)")
|
|
215
|
+
|
|
216
|
+
async def _assert_attribute(self, args: dict) -> str:
|
|
217
|
+
by = self._by(args.get("by", "css"))
|
|
218
|
+
timeout = args.get("timeout", 10)
|
|
219
|
+
try:
|
|
220
|
+
el = WebDriverWait(self._driver(), timeout).until(
|
|
221
|
+
EC.presence_of_element_located((by, args["selector"]))
|
|
222
|
+
)
|
|
223
|
+
except Exception:
|
|
224
|
+
return self._fail(f"element '{args['selector']}' not found within {timeout}s")
|
|
225
|
+
actual = el.get_attribute(args["attribute"])
|
|
226
|
+
exp = args["expected"]
|
|
227
|
+
ok = (actual == exp) if args.get("exact", True) else (exp in (actual or ""))
|
|
228
|
+
return self._pass(f"{args['attribute']}='{actual}'") if ok else self._fail(f"expected='{exp}' | actual='{actual}'")
|
|
229
|
+
|
|
230
|
+
async def _assert_page_contains(self, args: dict) -> str:
|
|
231
|
+
text = args["text"]
|
|
232
|
+
if args.get("source_only", False):
|
|
233
|
+
content = self._driver().page_source
|
|
234
|
+
else:
|
|
235
|
+
content = self._driver().find_element(By.TAG_NAME, "body").text
|
|
236
|
+
ok = text.lower() in content.lower()
|
|
237
|
+
return self._pass(f"page contains '{text}'") if ok else self._fail(f"'{text}' not found on page")
|
|
238
|
+
|
|
239
|
+
async def _assert_element_count(self, args: dict) -> str:
|
|
240
|
+
by = self._by(args.get("by", "css"))
|
|
241
|
+
els = self._driver().find_elements(by, args["selector"])
|
|
242
|
+
actual = len(els)
|
|
243
|
+
exp = args["expected_count"]
|
|
244
|
+
ok = actual == exp
|
|
245
|
+
return self._pass(f"count={actual}") if ok else self._fail(f"expected={exp} | actual={actual}")
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Browser Tools — start, stop, navigate, screenshot, window management
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import base64
|
|
6
|
+
from selenium import webdriver
|
|
7
|
+
from selenium.webdriver.chrome.options import Options as ChromeOptions
|
|
8
|
+
from selenium.webdriver.firefox.options import Options as FirefoxOptions
|
|
9
|
+
from mcp.types import Tool
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class BrowserTools:
|
|
13
|
+
def __init__(self):
|
|
14
|
+
self.driver = None
|
|
15
|
+
self._session_log = [] # records all actions for codegen
|
|
16
|
+
|
|
17
|
+
# ------------------------------------------------------------------ #
|
|
18
|
+
# Session helpers #
|
|
19
|
+
# ------------------------------------------------------------------ #
|
|
20
|
+
def record(self, action: str, **kwargs):
|
|
21
|
+
"""Append an action to the session log for code generation."""
|
|
22
|
+
self._session_log.append({"action": action, **kwargs})
|
|
23
|
+
|
|
24
|
+
def get_driver(self):
|
|
25
|
+
if not self.driver:
|
|
26
|
+
raise RuntimeError("No browser session. Call start_browser first.")
|
|
27
|
+
return self.driver
|
|
28
|
+
|
|
29
|
+
# ------------------------------------------------------------------ #
|
|
30
|
+
# MCP tool definitions #
|
|
31
|
+
# ------------------------------------------------------------------ #
|
|
32
|
+
def get_tools(self) -> list[Tool]:
|
|
33
|
+
return [
|
|
34
|
+
Tool(
|
|
35
|
+
name="start_browser",
|
|
36
|
+
description="Start a browser session. Supports chrome and firefox.",
|
|
37
|
+
inputSchema={
|
|
38
|
+
"type": "object",
|
|
39
|
+
"properties": {
|
|
40
|
+
"browser": {
|
|
41
|
+
"type": "string",
|
|
42
|
+
"enum": ["chrome", "firefox"],
|
|
43
|
+
"default": "chrome"
|
|
44
|
+
},
|
|
45
|
+
"headless": {
|
|
46
|
+
"type": "boolean",
|
|
47
|
+
"default": False
|
|
48
|
+
},
|
|
49
|
+
"window_size": {
|
|
50
|
+
"type": "string",
|
|
51
|
+
"description": "e.g. 1920x1080",
|
|
52
|
+
"default": "1920x1080"
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
),
|
|
57
|
+
Tool(
|
|
58
|
+
name="navigate",
|
|
59
|
+
description="Navigate the browser to a URL.",
|
|
60
|
+
inputSchema={
|
|
61
|
+
"type": "object",
|
|
62
|
+
"properties": {
|
|
63
|
+
"url": {"type": "string", "description": "Full URL including https://"}
|
|
64
|
+
},
|
|
65
|
+
"required": ["url"],
|
|
66
|
+
},
|
|
67
|
+
),
|
|
68
|
+
Tool(
|
|
69
|
+
name="take_screenshot",
|
|
70
|
+
description="Take a screenshot and return it as base64.",
|
|
71
|
+
inputSchema={"type": "object", "properties": {}},
|
|
72
|
+
),
|
|
73
|
+
Tool(
|
|
74
|
+
name="get_page_source",
|
|
75
|
+
description="Return the current page HTML source.",
|
|
76
|
+
inputSchema={"type": "object", "properties": {}},
|
|
77
|
+
),
|
|
78
|
+
Tool(
|
|
79
|
+
name="get_page_title",
|
|
80
|
+
description="Return the current page title.",
|
|
81
|
+
inputSchema={"type": "object", "properties": {}},
|
|
82
|
+
),
|
|
83
|
+
Tool(
|
|
84
|
+
name="get_current_url",
|
|
85
|
+
description="Return the current URL.",
|
|
86
|
+
inputSchema={"type": "object", "properties": {}},
|
|
87
|
+
),
|
|
88
|
+
Tool(
|
|
89
|
+
name="close_browser",
|
|
90
|
+
description="Close and quit the browser session.",
|
|
91
|
+
inputSchema={"type": "object", "properties": {}},
|
|
92
|
+
),
|
|
93
|
+
Tool(
|
|
94
|
+
name="switch_to_window",
|
|
95
|
+
description="Switch to a browser window/tab by index.",
|
|
96
|
+
inputSchema={
|
|
97
|
+
"type": "object",
|
|
98
|
+
"properties": {
|
|
99
|
+
"index": {"type": "integer", "default": 0}
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
),
|
|
103
|
+
Tool(
|
|
104
|
+
name="go_back",
|
|
105
|
+
description="Navigate back in browser history.",
|
|
106
|
+
inputSchema={"type": "object", "properties": {}},
|
|
107
|
+
),
|
|
108
|
+
Tool(
|
|
109
|
+
name="go_forward",
|
|
110
|
+
description="Navigate forward in browser history.",
|
|
111
|
+
inputSchema={"type": "object", "properties": {}},
|
|
112
|
+
),
|
|
113
|
+
Tool(
|
|
114
|
+
name="refresh",
|
|
115
|
+
description="Refresh the current page.",
|
|
116
|
+
inputSchema={"type": "object", "properties": {}},
|
|
117
|
+
),
|
|
118
|
+
Tool(
|
|
119
|
+
name="execute_script",
|
|
120
|
+
description="Execute JavaScript in the browser.",
|
|
121
|
+
inputSchema={
|
|
122
|
+
"type": "object",
|
|
123
|
+
"properties": {
|
|
124
|
+
"script": {"type": "string"},
|
|
125
|
+
"args": {"type": "array", "default": []}
|
|
126
|
+
},
|
|
127
|
+
"required": ["script"],
|
|
128
|
+
},
|
|
129
|
+
),
|
|
130
|
+
]
|
|
131
|
+
|
|
132
|
+
# ------------------------------------------------------------------ #
|
|
133
|
+
# Handlers #
|
|
134
|
+
# ------------------------------------------------------------------ #
|
|
135
|
+
def get_handlers(self) -> dict:
|
|
136
|
+
return {
|
|
137
|
+
"start_browser": self._start_browser,
|
|
138
|
+
"navigate": self._navigate,
|
|
139
|
+
"take_screenshot": self._take_screenshot,
|
|
140
|
+
"get_page_source": self._get_page_source,
|
|
141
|
+
"get_page_title": self._get_page_title,
|
|
142
|
+
"get_current_url": self._get_current_url,
|
|
143
|
+
"close_browser": self._close_browser,
|
|
144
|
+
"switch_to_window": self._switch_to_window,
|
|
145
|
+
"go_back": self._go_back,
|
|
146
|
+
"go_forward": self._go_forward,
|
|
147
|
+
"refresh": self._refresh,
|
|
148
|
+
"execute_script": self._execute_script,
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async def _start_browser(self, args: dict) -> str:
|
|
152
|
+
if self.driver:
|
|
153
|
+
try:
|
|
154
|
+
self.driver.quit()
|
|
155
|
+
except Exception:
|
|
156
|
+
pass
|
|
157
|
+
self.driver = None
|
|
158
|
+
|
|
159
|
+
browser = args.get("browser", "chrome")
|
|
160
|
+
headless = args.get("headless", False)
|
|
161
|
+
try:
|
|
162
|
+
w, h = args.get("window_size", "1920x1080").lower().replace(" ", "").split("x")
|
|
163
|
+
int(w), int(h)
|
|
164
|
+
except (ValueError, AttributeError):
|
|
165
|
+
w, h = "1920", "1080"
|
|
166
|
+
|
|
167
|
+
if browser == "chrome":
|
|
168
|
+
opts = ChromeOptions()
|
|
169
|
+
if headless:
|
|
170
|
+
opts.add_argument("--headless=new")
|
|
171
|
+
opts.add_argument(f"--window-size={w},{h}")
|
|
172
|
+
opts.add_argument("--no-sandbox")
|
|
173
|
+
opts.add_argument("--disable-dev-shm-usage")
|
|
174
|
+
self.driver = webdriver.Chrome(options=opts)
|
|
175
|
+
elif browser == "firefox":
|
|
176
|
+
opts = FirefoxOptions()
|
|
177
|
+
if headless:
|
|
178
|
+
opts.add_argument("--headless")
|
|
179
|
+
self.driver = webdriver.Firefox(options=opts)
|
|
180
|
+
self.driver.set_window_size(int(w), int(h))
|
|
181
|
+
else:
|
|
182
|
+
return f"Unsupported browser: {browser}"
|
|
183
|
+
|
|
184
|
+
self._session_log = []
|
|
185
|
+
self.record("start_browser", browser=browser, headless=headless)
|
|
186
|
+
return f"✅ {browser.capitalize()} started ({w}x{h}, headless={headless})"
|
|
187
|
+
|
|
188
|
+
async def _navigate(self, args: dict) -> str:
|
|
189
|
+
url = args["url"]
|
|
190
|
+
self.get_driver().get(url)
|
|
191
|
+
self.record("navigate", url=url)
|
|
192
|
+
return f"✅ Navigated to {url}"
|
|
193
|
+
|
|
194
|
+
async def _take_screenshot(self, args: dict) -> str:
|
|
195
|
+
png = self.get_driver().get_screenshot_as_png()
|
|
196
|
+
b64 = base64.b64encode(png).decode()
|
|
197
|
+
return f"screenshot:base64:{b64}"
|
|
198
|
+
|
|
199
|
+
async def _get_page_source(self, args: dict) -> str:
|
|
200
|
+
return self.get_driver().page_source
|
|
201
|
+
|
|
202
|
+
async def _get_page_title(self, args: dict) -> str:
|
|
203
|
+
return self.get_driver().title
|
|
204
|
+
|
|
205
|
+
async def _get_current_url(self, args: dict) -> str:
|
|
206
|
+
return self.get_driver().current_url
|
|
207
|
+
|
|
208
|
+
async def _close_browser(self, args: dict) -> str:
|
|
209
|
+
if self.driver:
|
|
210
|
+
self.driver.quit()
|
|
211
|
+
self.driver = None
|
|
212
|
+
return "✅ Browser closed"
|
|
213
|
+
|
|
214
|
+
async def _switch_to_window(self, args: dict) -> str:
|
|
215
|
+
idx = args.get("index", 0)
|
|
216
|
+
handles = self.get_driver().window_handles
|
|
217
|
+
if idx >= len(handles):
|
|
218
|
+
return f"No window at index {idx}. Available: {len(handles)}"
|
|
219
|
+
self.get_driver().switch_to.window(handles[idx])
|
|
220
|
+
return f"✅ Switched to window {idx}"
|
|
221
|
+
|
|
222
|
+
async def _go_back(self, args: dict) -> str:
|
|
223
|
+
self.get_driver().back()
|
|
224
|
+
self.record("go_back")
|
|
225
|
+
return "✅ Navigated back"
|
|
226
|
+
|
|
227
|
+
async def _go_forward(self, args: dict) -> str:
|
|
228
|
+
self.get_driver().forward()
|
|
229
|
+
self.record("go_forward")
|
|
230
|
+
return "✅ Navigated forward"
|
|
231
|
+
|
|
232
|
+
async def _refresh(self, args: dict) -> str:
|
|
233
|
+
self.get_driver().refresh()
|
|
234
|
+
self.record("refresh")
|
|
235
|
+
return "✅ Page refreshed"
|
|
236
|
+
|
|
237
|
+
async def _execute_script(self, args: dict) -> str:
|
|
238
|
+
result = self.get_driver().execute_script(args["script"], *args.get("args", []))
|
|
239
|
+
self.record("execute_script", script=args["script"])
|
|
240
|
+
return str(result)
|