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.
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)