seleniumboot-mcp 0.3.0__tar.gz → 0.3.2__tar.gz
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.
- {seleniumboot_mcp-0.3.0 → seleniumboot_mcp-0.3.2}/PKG-INFO +11 -3
- {seleniumboot_mcp-0.3.0 → seleniumboot_mcp-0.3.2}/README.md +10 -2
- {seleniumboot_mcp-0.3.0 → seleniumboot_mcp-0.3.2}/pyproject.toml +1 -1
- {seleniumboot_mcp-0.3.0 → seleniumboot_mcp-0.3.2}/src/selenium_mcp/tools/browser_tools.py +501 -4
- {seleniumboot_mcp-0.3.0 → seleniumboot_mcp-0.3.2}/src/selenium_mcp/tools/element_tools.py +101 -0
- {seleniumboot_mcp-0.3.0 → seleniumboot_mcp-0.3.2}/src/seleniumboot_mcp.egg-info/PKG-INFO +11 -3
- {seleniumboot_mcp-0.3.0 → seleniumboot_mcp-0.3.2}/setup.cfg +0 -0
- {seleniumboot_mcp-0.3.0 → seleniumboot_mcp-0.3.2}/src/selenium_mcp/__init__.py +0 -0
- {seleniumboot_mcp-0.3.0 → seleniumboot_mcp-0.3.2}/src/selenium_mcp/server.py +0 -0
- {seleniumboot_mcp-0.3.0 → seleniumboot_mcp-0.3.2}/src/selenium_mcp/tools/__init__.py +0 -0
- {seleniumboot_mcp-0.3.0 → seleniumboot_mcp-0.3.2}/src/selenium_mcp/tools/_locators.py +0 -0
- {seleniumboot_mcp-0.3.0 → seleniumboot_mcp-0.3.2}/src/selenium_mcp/tools/assertion_tools.py +0 -0
- {seleniumboot_mcp-0.3.0 → seleniumboot_mcp-0.3.2}/src/selenium_mcp/tools/codegen_tools.py +0 -0
- {seleniumboot_mcp-0.3.0 → seleniumboot_mcp-0.3.2}/src/seleniumboot_mcp.egg-info/SOURCES.txt +0 -0
- {seleniumboot_mcp-0.3.0 → seleniumboot_mcp-0.3.2}/src/seleniumboot_mcp.egg-info/dependency_links.txt +0 -0
- {seleniumboot_mcp-0.3.0 → seleniumboot_mcp-0.3.2}/src/seleniumboot_mcp.egg-info/entry_points.txt +0 -0
- {seleniumboot_mcp-0.3.0 → seleniumboot_mcp-0.3.2}/src/seleniumboot_mcp.egg-info/requires.txt +0 -0
- {seleniumboot_mcp-0.3.0 → seleniumboot_mcp-0.3.2}/src/seleniumboot_mcp.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: seleniumboot-mcp
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.2
|
|
4
4
|
Summary: A Python MCP server for Selenium WebDriver — 74 tools for browser automation, alerts, frames, shadow DOM, cookies, mobile emulation, self-healing locators, and codegen for Java/C#/Python/Playwright
|
|
5
5
|
Author-email: Raza Tech <razatechnologyservices@gmail.com>
|
|
6
6
|
License: MIT
|
|
@@ -25,7 +25,7 @@ Requires-Dist: selenium>=4.6.0
|
|
|
25
25
|
A Python **Model Context Protocol (MCP)** server for Selenium WebDriver automation.
|
|
26
26
|
Let Claude or GitHub Copilot control a real browser — navigate pages, interact with elements,
|
|
27
27
|
run assertions, and generate ready-to-run **Java TestNG / JUnit 5 / Cucumber / pytest** test code from recorded sessions.
|
|
28
|
-
|
|
28
|
+
82 tools. No ChromeDriver setup. Browser auto-starts on first use.
|
|
29
29
|
|
|
30
30
|
[](https://pypi.org/project/seleniumboot-mcp/)
|
|
31
31
|
[](https://pypi.org/project/seleniumboot-mcp/)
|
|
@@ -107,7 +107,7 @@ Claude controls the real browser, records every action, and on request generates
|
|
|
107
107
|
|
|
108
108
|
---
|
|
109
109
|
|
|
110
|
-
## Tools (
|
|
110
|
+
## Tools (82 total)
|
|
111
111
|
|
|
112
112
|
### Browser
|
|
113
113
|
| Tool | Description |
|
|
@@ -135,6 +135,13 @@ Claude controls the real browser, records every action, and on request generates
|
|
|
135
135
|
| `delete_cookie` / `delete_all_cookies` | Remove cookies |
|
|
136
136
|
| `get_local_storage` / `set_local_storage` | Read or write localStorage |
|
|
137
137
|
| `get_session_storage` / `set_session_storage` | Read or write sessionStorage |
|
|
138
|
+
| `wait_for_network_idle` | Wait until XHR/fetch traffic is quiet — essential for SPAs |
|
|
139
|
+
| `inspect_page` | Discover all inputs, buttons, selects, links with best-fit CSS selectors |
|
|
140
|
+
| `get_network_logs` | Captured XHR/fetch requests — method, URL, status, timing |
|
|
141
|
+
| `mock_response` | Stub fetch/XHR by URL pattern with a canned response |
|
|
142
|
+
| `clear_mock_responses` | Remove all active mock rules |
|
|
143
|
+
| `compare_screenshot` | Pixel diff against a saved baseline — visual regression |
|
|
144
|
+
| `check_accessibility` | Built-in WCAG audit — alt text, labels, headings, keyboard access |
|
|
138
145
|
|
|
139
146
|
### Elements
|
|
140
147
|
| Tool | Description |
|
|
@@ -164,6 +171,7 @@ Claude controls the real browser, records every action, and on request generates
|
|
|
164
171
|
| `switch_to_default_content` | Return to the main page from a frame |
|
|
165
172
|
| `find_shadow_element` | Find element inside a shadow DOM |
|
|
166
173
|
| `get_table_data` | Extract an HTML table as a formatted text grid |
|
|
174
|
+
| `fill_form` | Fill multiple fields at once — auto-detects input/select/checkbox/radio |
|
|
167
175
|
| `get_healed_locators` | View all self-healed selector mappings for the session |
|
|
168
176
|
| `clear_healed_locators` | Reset the self-healing cache |
|
|
169
177
|
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
A Python **Model Context Protocol (MCP)** server for Selenium WebDriver automation.
|
|
4
4
|
Let Claude or GitHub Copilot control a real browser — navigate pages, interact with elements,
|
|
5
5
|
run assertions, and generate ready-to-run **Java TestNG / JUnit 5 / Cucumber / pytest** test code from recorded sessions.
|
|
6
|
-
|
|
6
|
+
82 tools. No ChromeDriver setup. Browser auto-starts on first use.
|
|
7
7
|
|
|
8
8
|
[](https://pypi.org/project/seleniumboot-mcp/)
|
|
9
9
|
[](https://pypi.org/project/seleniumboot-mcp/)
|
|
@@ -85,7 +85,7 @@ Claude controls the real browser, records every action, and on request generates
|
|
|
85
85
|
|
|
86
86
|
---
|
|
87
87
|
|
|
88
|
-
## Tools (
|
|
88
|
+
## Tools (82 total)
|
|
89
89
|
|
|
90
90
|
### Browser
|
|
91
91
|
| Tool | Description |
|
|
@@ -113,6 +113,13 @@ Claude controls the real browser, records every action, and on request generates
|
|
|
113
113
|
| `delete_cookie` / `delete_all_cookies` | Remove cookies |
|
|
114
114
|
| `get_local_storage` / `set_local_storage` | Read or write localStorage |
|
|
115
115
|
| `get_session_storage` / `set_session_storage` | Read or write sessionStorage |
|
|
116
|
+
| `wait_for_network_idle` | Wait until XHR/fetch traffic is quiet — essential for SPAs |
|
|
117
|
+
| `inspect_page` | Discover all inputs, buttons, selects, links with best-fit CSS selectors |
|
|
118
|
+
| `get_network_logs` | Captured XHR/fetch requests — method, URL, status, timing |
|
|
119
|
+
| `mock_response` | Stub fetch/XHR by URL pattern with a canned response |
|
|
120
|
+
| `clear_mock_responses` | Remove all active mock rules |
|
|
121
|
+
| `compare_screenshot` | Pixel diff against a saved baseline — visual regression |
|
|
122
|
+
| `check_accessibility` | Built-in WCAG audit — alt text, labels, headings, keyboard access |
|
|
116
123
|
|
|
117
124
|
### Elements
|
|
118
125
|
| Tool | Description |
|
|
@@ -142,6 +149,7 @@ Claude controls the real browser, records every action, and on request generates
|
|
|
142
149
|
| `switch_to_default_content` | Return to the main page from a frame |
|
|
143
150
|
| `find_shadow_element` | Find element inside a shadow DOM |
|
|
144
151
|
| `get_table_data` | Extract an HTML table as a formatted text grid |
|
|
152
|
+
| `fill_form` | Fill multiple fields at once — auto-detects input/select/checkbox/radio |
|
|
145
153
|
| `get_healed_locators` | View all self-healed selector mappings for the session |
|
|
146
154
|
| `clear_healed_locators` | Reset the self-healing cache |
|
|
147
155
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "seleniumboot-mcp"
|
|
7
|
-
version = "0.3.
|
|
7
|
+
version = "0.3.2"
|
|
8
8
|
description = "A Python MCP server for Selenium WebDriver — 74 tools for browser automation, alerts, frames, shadow DOM, cookies, mobile emulation, self-healing locators, and codegen for Java/C#/Python/Playwright"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -3,8 +3,12 @@ Browser Tools — start, stop, navigate, screenshot, window management,
|
|
|
3
3
|
cookies, localStorage/sessionStorage, console logs, mobile emulation, page scroll
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
|
+
import asyncio
|
|
6
7
|
import base64
|
|
8
|
+
import io
|
|
7
9
|
import json
|
|
10
|
+
import os
|
|
11
|
+
import time
|
|
8
12
|
from selenium import webdriver
|
|
9
13
|
from selenium.webdriver.chrome.options import Options as ChromeOptions
|
|
10
14
|
from selenium.webdriver.firefox.options import Options as FirefoxOptions
|
|
@@ -37,7 +41,7 @@ class BrowserTools:
|
|
|
37
41
|
opts = ChromeOptions()
|
|
38
42
|
opts.add_argument("--no-sandbox")
|
|
39
43
|
opts.add_argument("--disable-dev-shm-usage")
|
|
40
|
-
opts.set_capability("goog:loggingPrefs", {"browser": "ALL"})
|
|
44
|
+
opts.set_capability("goog:loggingPrefs", {"browser": "ALL", "performance": "ALL"})
|
|
41
45
|
self.driver = webdriver.Chrome(options=opts)
|
|
42
46
|
self.record("start_browser", browser="chrome", headless=False)
|
|
43
47
|
return self.driver
|
|
@@ -282,6 +286,116 @@ class BrowserTools:
|
|
|
282
286
|
"required": ["key", "value"],
|
|
283
287
|
},
|
|
284
288
|
),
|
|
289
|
+
# ── Page inspection ──────────────────────────────────────── #
|
|
290
|
+
Tool(
|
|
291
|
+
name="inspect_page",
|
|
292
|
+
description=(
|
|
293
|
+
"Discover all interactive elements on the current page — inputs, buttons, "
|
|
294
|
+
"selects, checkboxes, textareas, and links — with their best CSS selectors and labels. "
|
|
295
|
+
"Use this before writing locators so the AI knows exactly what's on the page."
|
|
296
|
+
),
|
|
297
|
+
inputSchema={"type": "object", "properties": {}},
|
|
298
|
+
),
|
|
299
|
+
# ── Network interception ─────────────────────────────────── #
|
|
300
|
+
Tool(
|
|
301
|
+
name="get_network_logs",
|
|
302
|
+
description="Return captured XHR/fetch network requests (method, URL, status, timing). Chrome only.",
|
|
303
|
+
inputSchema={
|
|
304
|
+
"type": "object",
|
|
305
|
+
"properties": {
|
|
306
|
+
"url_filter": {"type": "string", "description": "Only show requests whose URL contains this string"},
|
|
307
|
+
"method": {"type": "string", "description": "Filter by HTTP method e.g. GET, POST"},
|
|
308
|
+
"limit": {"type": "integer", "default": 50, "description": "Max requests to return"},
|
|
309
|
+
},
|
|
310
|
+
},
|
|
311
|
+
),
|
|
312
|
+
Tool(
|
|
313
|
+
name="mock_response",
|
|
314
|
+
description=(
|
|
315
|
+
"Intercept fetch/XHR requests matching a URL pattern and return a canned response. "
|
|
316
|
+
"Useful for testing without a real backend. Injection survives until the page is reloaded."
|
|
317
|
+
),
|
|
318
|
+
inputSchema={
|
|
319
|
+
"type": "object",
|
|
320
|
+
"properties": {
|
|
321
|
+
"url_pattern": {"type": "string", "description": "Substring or regex pattern to match the request URL"},
|
|
322
|
+
"status": {"type": "integer", "default": 200},
|
|
323
|
+
"body": {"type": "string", "default": "{}", "description": "Response body string"},
|
|
324
|
+
"content_type": {"type": "string", "default": "application/json"},
|
|
325
|
+
},
|
|
326
|
+
"required": ["url_pattern"],
|
|
327
|
+
},
|
|
328
|
+
),
|
|
329
|
+
Tool(
|
|
330
|
+
name="clear_mock_responses",
|
|
331
|
+
description="Remove all active mock response rules injected by mock_response.",
|
|
332
|
+
inputSchema={"type": "object", "properties": {}},
|
|
333
|
+
),
|
|
334
|
+
# ── Visual regression ─────────────────────────────────────── #
|
|
335
|
+
Tool(
|
|
336
|
+
name="compare_screenshot",
|
|
337
|
+
description=(
|
|
338
|
+
"Compare the current page screenshot against a saved baseline. "
|
|
339
|
+
"On first run (or with update_baseline=true) saves the baseline. "
|
|
340
|
+
"Returns the pixel diff percentage. Install Pillow for accurate pixel comparison."
|
|
341
|
+
),
|
|
342
|
+
inputSchema={
|
|
343
|
+
"type": "object",
|
|
344
|
+
"properties": {
|
|
345
|
+
"name": {"type": "string", "default": "default", "description": "Baseline name e.g. 'homepage'"},
|
|
346
|
+
"update_baseline": {"type": "boolean", "default": False},
|
|
347
|
+
"threshold": {"type": "number", "default": 0.1, "description": "Max allowed diff % before failure"},
|
|
348
|
+
},
|
|
349
|
+
},
|
|
350
|
+
),
|
|
351
|
+
# ── Accessibility ─────────────────────────────────────────── #
|
|
352
|
+
Tool(
|
|
353
|
+
name="check_accessibility",
|
|
354
|
+
description=(
|
|
355
|
+
"Run a built-in accessibility audit on the current page. "
|
|
356
|
+
"Checks for missing alt text, unlabelled inputs, empty buttons/links, "
|
|
357
|
+
"missing page title, HTML lang, heading structure, and keyboard accessibility."
|
|
358
|
+
),
|
|
359
|
+
inputSchema={
|
|
360
|
+
"type": "object",
|
|
361
|
+
"properties": {
|
|
362
|
+
"level": {
|
|
363
|
+
"type": "string",
|
|
364
|
+
"enum": ["all", "critical", "serious", "moderate", "minor"],
|
|
365
|
+
"default": "all",
|
|
366
|
+
}
|
|
367
|
+
},
|
|
368
|
+
},
|
|
369
|
+
),
|
|
370
|
+
# ── Network idle ─────────────────────────────────────────── #
|
|
371
|
+
Tool(
|
|
372
|
+
name="wait_for_network_idle",
|
|
373
|
+
description=(
|
|
374
|
+
"Wait until there are no active XHR/fetch requests for a quiet period. "
|
|
375
|
+
"Essential for SPAs and pages that load data asynchronously. "
|
|
376
|
+
"Also waits for document.readyState to be 'complete'."
|
|
377
|
+
),
|
|
378
|
+
inputSchema={
|
|
379
|
+
"type": "object",
|
|
380
|
+
"properties": {
|
|
381
|
+
"idle_time_ms": {
|
|
382
|
+
"type": "integer",
|
|
383
|
+
"default": 500,
|
|
384
|
+
"description": "Milliseconds of network silence required",
|
|
385
|
+
},
|
|
386
|
+
"timeout": {
|
|
387
|
+
"type": "integer",
|
|
388
|
+
"default": 15,
|
|
389
|
+
"description": "Max seconds to wait before giving up",
|
|
390
|
+
},
|
|
391
|
+
"max_inflight": {
|
|
392
|
+
"type": "integer",
|
|
393
|
+
"default": 0,
|
|
394
|
+
"description": "Tolerate up to N in-flight requests (0 = fully idle)",
|
|
395
|
+
},
|
|
396
|
+
},
|
|
397
|
+
},
|
|
398
|
+
),
|
|
285
399
|
]
|
|
286
400
|
|
|
287
401
|
# ------------------------------------------------------------------ #
|
|
@@ -315,8 +429,15 @@ class BrowserTools:
|
|
|
315
429
|
"delete_all_cookies": self._delete_all_cookies,
|
|
316
430
|
"get_local_storage": self._get_local_storage,
|
|
317
431
|
"set_local_storage": self._set_local_storage,
|
|
318
|
-
"get_session_storage":
|
|
319
|
-
"set_session_storage":
|
|
432
|
+
"get_session_storage": self._get_session_storage,
|
|
433
|
+
"set_session_storage": self._set_session_storage,
|
|
434
|
+
"wait_for_network_idle": self._wait_for_network_idle,
|
|
435
|
+
"inspect_page": self._inspect_page,
|
|
436
|
+
"get_network_logs": self._get_network_logs,
|
|
437
|
+
"mock_response": self._mock_response,
|
|
438
|
+
"clear_mock_responses": self._clear_mock_responses,
|
|
439
|
+
"compare_screenshot": self._compare_screenshot,
|
|
440
|
+
"check_accessibility": self._check_accessibility,
|
|
320
441
|
}
|
|
321
442
|
|
|
322
443
|
# ── Browser lifecycle ────────────────────────────────────────────── #
|
|
@@ -344,7 +465,7 @@ class BrowserTools:
|
|
|
344
465
|
opts.add_argument(f"--window-size={w},{h}")
|
|
345
466
|
opts.add_argument("--no-sandbox")
|
|
346
467
|
opts.add_argument("--disable-dev-shm-usage")
|
|
347
|
-
opts.set_capability("goog:loggingPrefs", {"browser": "ALL"})
|
|
468
|
+
opts.set_capability("goog:loggingPrefs", {"browser": "ALL", "performance": "ALL"})
|
|
348
469
|
self.driver = webdriver.Chrome(options=opts)
|
|
349
470
|
elif browser == "firefox":
|
|
350
471
|
opts = FirefoxOptions()
|
|
@@ -563,3 +684,379 @@ class BrowserTools:
|
|
|
563
684
|
)
|
|
564
685
|
self.record("set_session_storage", key=args["key"], value=args["value"])
|
|
565
686
|
return f"✅ sessionStorage['{args['key']}'] = '{args['value']}'"
|
|
687
|
+
|
|
688
|
+
# ── Network idle ─────────────────────────────────────────────────── #
|
|
689
|
+
|
|
690
|
+
_INJECT_TRACKER = """
|
|
691
|
+
if (!window.__smcpNet) {
|
|
692
|
+
window.__smcpNet = { active: 0, last: Date.now() };
|
|
693
|
+
const _track = (d) => {
|
|
694
|
+
window.__smcpNet.active = Math.max(0, window.__smcpNet.active + d);
|
|
695
|
+
window.__smcpNet.last = Date.now();
|
|
696
|
+
};
|
|
697
|
+
const _xhrOpen = XMLHttpRequest.prototype.open;
|
|
698
|
+
XMLHttpRequest.prototype.open = function() {
|
|
699
|
+
_track(1);
|
|
700
|
+
this.addEventListener('loadend', () => _track(-1));
|
|
701
|
+
_xhrOpen.apply(this, arguments);
|
|
702
|
+
};
|
|
703
|
+
if (window.fetch) {
|
|
704
|
+
const _fetch = window.fetch;
|
|
705
|
+
window.fetch = function() {
|
|
706
|
+
_track(1);
|
|
707
|
+
return _fetch.apply(this, arguments).finally(() => _track(-1));
|
|
708
|
+
};
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
"""
|
|
712
|
+
|
|
713
|
+
_POLL_TRACKER = """
|
|
714
|
+
const t = window.__smcpNet || { active: 0, last: Date.now() };
|
|
715
|
+
return { active: t.active, quietMs: Date.now() - t.last, readyState: document.readyState };
|
|
716
|
+
"""
|
|
717
|
+
|
|
718
|
+
async def _wait_for_network_idle(self, args: dict) -> str:
|
|
719
|
+
idle_ms = args.get("idle_time_ms", 500)
|
|
720
|
+
timeout = args.get("timeout", 15)
|
|
721
|
+
max_inflight = args.get("max_inflight", 0)
|
|
722
|
+
driver = self.get_driver()
|
|
723
|
+
|
|
724
|
+
driver.execute_script(self._INJECT_TRACKER)
|
|
725
|
+
|
|
726
|
+
deadline = time.monotonic() + timeout
|
|
727
|
+
while time.monotonic() < deadline:
|
|
728
|
+
state = driver.execute_script(self._POLL_TRACKER)
|
|
729
|
+
active = state.get("active", 0)
|
|
730
|
+
quiet_ms = state.get("quietMs", 0)
|
|
731
|
+
ready = state.get("readyState", "")
|
|
732
|
+
|
|
733
|
+
if ready == "complete" and active <= max_inflight and quiet_ms >= idle_ms:
|
|
734
|
+
elapsed = round(time.monotonic() - (deadline - timeout), 2)
|
|
735
|
+
return (
|
|
736
|
+
f"✅ Network idle — active={active}, "
|
|
737
|
+
f"quiet for {quiet_ms}ms, readyState={ready} ({elapsed}s)"
|
|
738
|
+
)
|
|
739
|
+
await asyncio.sleep(0.1)
|
|
740
|
+
|
|
741
|
+
state = driver.execute_script(self._POLL_TRACKER)
|
|
742
|
+
return (
|
|
743
|
+
f"⚠️ Timed out after {timeout}s — "
|
|
744
|
+
f"active={state.get('active', '?')}, readyState={state.get('readyState', '?')}"
|
|
745
|
+
)
|
|
746
|
+
|
|
747
|
+
# ── Page inspection ──────────────────────────────────────────────── #
|
|
748
|
+
|
|
749
|
+
_INSPECT_SCRIPT = """
|
|
750
|
+
function bestSel(el) {
|
|
751
|
+
if (el.id) return '#' + CSS.escape(el.id);
|
|
752
|
+
if (el.name) return '[name="' + el.name + '"]';
|
|
753
|
+
const cls = (el.className || '').trim().split(/\\s+/)[0];
|
|
754
|
+
if (cls) return el.tagName.toLowerCase() + '.' + CSS.escape(cls);
|
|
755
|
+
return el.tagName.toLowerCase();
|
|
756
|
+
}
|
|
757
|
+
function labelFor(el) {
|
|
758
|
+
if (el.id) {
|
|
759
|
+
const l = document.querySelector('label[for="' + el.id + '"]');
|
|
760
|
+
if (l) return l.textContent.trim();
|
|
761
|
+
}
|
|
762
|
+
const p = el.closest('label');
|
|
763
|
+
if (p) return p.textContent.replace(el.value||'','').trim();
|
|
764
|
+
return el.getAttribute('placeholder') || el.getAttribute('aria-label') || el.name || '';
|
|
765
|
+
}
|
|
766
|
+
const r = {url: location.href, inputs:[], checkboxes:[], radios:[], textareas:[], selects:[], buttons:[], links:[]};
|
|
767
|
+
const vis = el => !!(el.offsetWidth || el.offsetHeight || el.getClientRects().length);
|
|
768
|
+
|
|
769
|
+
document.querySelectorAll('input:not([type="hidden"]):not([type="checkbox"]):not([type="radio"]):not([type="submit"]):not([type="button"]):not([type="image"]):not([type="reset"])').forEach(el => {
|
|
770
|
+
if (!vis(el)) return;
|
|
771
|
+
r.inputs.push({selector: bestSel(el), type: el.type||'text', label: labelFor(el)});
|
|
772
|
+
});
|
|
773
|
+
document.querySelectorAll('input[type="checkbox"]').forEach(el => {
|
|
774
|
+
r.checkboxes.push({selector: bestSel(el), label: labelFor(el), checked: el.checked});
|
|
775
|
+
});
|
|
776
|
+
document.querySelectorAll('input[type="radio"]').forEach(el => {
|
|
777
|
+
r.radios.push({selector: bestSel(el), label: labelFor(el), checked: el.checked, value: el.value});
|
|
778
|
+
});
|
|
779
|
+
document.querySelectorAll('textarea').forEach(el => {
|
|
780
|
+
if (!vis(el)) return;
|
|
781
|
+
r.textareas.push({selector: bestSel(el), label: labelFor(el)});
|
|
782
|
+
});
|
|
783
|
+
document.querySelectorAll('select').forEach(el => {
|
|
784
|
+
if (!vis(el)) return;
|
|
785
|
+
const opts = Array.from(el.options).map(o => o.text.trim()).filter(Boolean);
|
|
786
|
+
r.selects.push({selector: bestSel(el), label: labelFor(el), options: opts});
|
|
787
|
+
});
|
|
788
|
+
document.querySelectorAll('button,input[type="submit"],input[type="button"],input[type="reset"]').forEach(el => {
|
|
789
|
+
if (!vis(el)) return;
|
|
790
|
+
r.buttons.push({selector: bestSel(el), text: (el.textContent||el.value||'').trim(), type: el.type||'button'});
|
|
791
|
+
});
|
|
792
|
+
document.querySelectorAll('a[href]').forEach(el => {
|
|
793
|
+
if (!vis(el)) return;
|
|
794
|
+
const text = el.textContent.trim();
|
|
795
|
+
if (!text) return;
|
|
796
|
+
r.links.push({selector: bestSel(el), text: text.substring(0,60), href: el.getAttribute('href')});
|
|
797
|
+
});
|
|
798
|
+
return r;
|
|
799
|
+
"""
|
|
800
|
+
|
|
801
|
+
async def _inspect_page(self, args: dict) -> str:
|
|
802
|
+
result = self.get_driver().execute_script(self._INSPECT_SCRIPT)
|
|
803
|
+
lines = [f"Page: {result['url']}", ""]
|
|
804
|
+
|
|
805
|
+
def section(title, items, fmt):
|
|
806
|
+
if not items:
|
|
807
|
+
return
|
|
808
|
+
lines.append(f"{title} ({len(items)}):")
|
|
809
|
+
for el in items:
|
|
810
|
+
lines.append(" " + fmt(el))
|
|
811
|
+
|
|
812
|
+
section("INPUTS", result["inputs"],
|
|
813
|
+
lambda e: f"[{e['type']}] {e['selector']}" + (f" — {e['label']}" if e["label"] else ""))
|
|
814
|
+
section("CHECKBOXES", result["checkboxes"],
|
|
815
|
+
lambda e: f"{e['selector']} — {e['label'] or 'unlabeled'} (checked={e['checked']})")
|
|
816
|
+
section("RADIOS", result["radios"],
|
|
817
|
+
lambda e: f"{e['selector']} value='{e['value']}'" + (" ✓" if e["checked"] else ""))
|
|
818
|
+
section("TEXTAREAS", result["textareas"],
|
|
819
|
+
lambda e: f"{e['selector']}" + (f" — {e['label']}" if e["label"] else ""))
|
|
820
|
+
section("SELECTS", result["selects"],
|
|
821
|
+
lambda e: f"{e['selector']} — {e['label'] or 'unlabeled'} → [{', '.join(e['options'][:6])}"
|
|
822
|
+
+ (f" +{len(e['options'])-6} more" if len(e['options']) > 6 else "") + "]")
|
|
823
|
+
|
|
824
|
+
if result["buttons"]:
|
|
825
|
+
lines.append(f"\nBUTTONS ({len(result['buttons'])}):")
|
|
826
|
+
for e in result["buttons"]:
|
|
827
|
+
lines.append(f" [{e['type']}] {e['selector']} — '{e['text']}'")
|
|
828
|
+
|
|
829
|
+
if result["links"]:
|
|
830
|
+
shown = result["links"][:15]
|
|
831
|
+
lines.append(f"\nLINKS ({len(result['links'])}):")
|
|
832
|
+
for e in shown:
|
|
833
|
+
lines.append(f" {e['selector']} — '{e['text']}' → {e['href']}")
|
|
834
|
+
if len(result["links"]) > 15:
|
|
835
|
+
lines.append(f" … +{len(result['links'])-15} more")
|
|
836
|
+
|
|
837
|
+
total = sum(len(result[k]) for k in result if k != "url")
|
|
838
|
+
if total == 0:
|
|
839
|
+
return f"No interactive elements found on {result['url']}"
|
|
840
|
+
return "\n".join(lines)
|
|
841
|
+
|
|
842
|
+
# ── Network interception ─────────────────────────────────────────── #
|
|
843
|
+
|
|
844
|
+
async def _get_network_logs(self, args: dict) -> str:
|
|
845
|
+
url_filter = args.get("url_filter", "")
|
|
846
|
+
method_filter = args.get("method", "").upper()
|
|
847
|
+
limit = args.get("limit", 50)
|
|
848
|
+
|
|
849
|
+
try:
|
|
850
|
+
perf_logs = self.get_driver().get_log("performance")
|
|
851
|
+
except Exception as e:
|
|
852
|
+
return f"Could not get performance logs: {e}"
|
|
853
|
+
|
|
854
|
+
requests: dict = {}
|
|
855
|
+
for entry in perf_logs:
|
|
856
|
+
try:
|
|
857
|
+
msg = json.loads(entry["message"])["message"]
|
|
858
|
+
method = msg.get("method", "")
|
|
859
|
+
params = msg.get("params", {})
|
|
860
|
+
rid = params.get("requestId", "")
|
|
861
|
+
|
|
862
|
+
if method == "Network.requestWillBeSent":
|
|
863
|
+
req = params.get("request", {})
|
|
864
|
+
requests[rid] = {
|
|
865
|
+
"url": req.get("url", ""),
|
|
866
|
+
"method": req.get("method", "GET"),
|
|
867
|
+
"status": None,
|
|
868
|
+
"ms": None,
|
|
869
|
+
"ts": params.get("timestamp", 0),
|
|
870
|
+
}
|
|
871
|
+
elif method == "Network.responseReceived" and rid in requests:
|
|
872
|
+
resp = params.get("response", {})
|
|
873
|
+
requests[rid]["status"] = resp.get("status")
|
|
874
|
+
elapsed = params.get("timestamp", requests[rid]["ts"]) - requests[rid]["ts"]
|
|
875
|
+
requests[rid]["ms"] = round(elapsed * 1000)
|
|
876
|
+
except Exception:
|
|
877
|
+
continue
|
|
878
|
+
|
|
879
|
+
entries = list(requests.values())
|
|
880
|
+
if url_filter:
|
|
881
|
+
entries = [e for e in entries if url_filter.lower() in e["url"].lower()]
|
|
882
|
+
if method_filter:
|
|
883
|
+
entries = [e for e in entries if e["method"] == method_filter]
|
|
884
|
+
entries = entries[-limit:]
|
|
885
|
+
|
|
886
|
+
if not entries:
|
|
887
|
+
return "No network requests captured." + ("" if perf_logs else " Performance logging requires Chrome.")
|
|
888
|
+
|
|
889
|
+
lines = [f"Network requests ({len(entries)}):"]
|
|
890
|
+
for e in entries:
|
|
891
|
+
status = str(e["status"]) if e["status"] else "---"
|
|
892
|
+
ms = f"{e['ms']}ms" if e["ms"] is not None else "?"
|
|
893
|
+
url = e["url"][:80] + ("…" if len(e["url"]) > 80 else "")
|
|
894
|
+
lines.append(f" {e['method']:<6} {status} {url} ({ms})")
|
|
895
|
+
return "\n".join(lines)
|
|
896
|
+
|
|
897
|
+
_MOCK_INJECT = """
|
|
898
|
+
window.__smcpMocks = window.__smcpMocks || [];
|
|
899
|
+
window.__smcpMocks.push({pattern: arguments[0], status: arguments[1], body: arguments[2], ct: arguments[3]});
|
|
900
|
+
if (!window.__smcpMockOn) {
|
|
901
|
+
window.__smcpMockOn = true;
|
|
902
|
+
const _f = window.fetch;
|
|
903
|
+
window.fetch = function(url, opts) {
|
|
904
|
+
const u = typeof url === 'string' ? url : (url && url.url) || '';
|
|
905
|
+
for (const m of (window.__smcpMocks || [])) {
|
|
906
|
+
try { if (u.includes(m.pattern) || new RegExp(m.pattern).test(u))
|
|
907
|
+
return Promise.resolve(new Response(m.body, {status: m.status, headers: {'Content-Type': m.ct}}));
|
|
908
|
+
} catch(e) {}
|
|
909
|
+
}
|
|
910
|
+
return _f.apply(this, arguments);
|
|
911
|
+
};
|
|
912
|
+
}
|
|
913
|
+
return window.__smcpMocks.length;
|
|
914
|
+
"""
|
|
915
|
+
|
|
916
|
+
async def _mock_response(self, args: dict) -> str:
|
|
917
|
+
pattern = args["url_pattern"]
|
|
918
|
+
status = args.get("status", 200)
|
|
919
|
+
body = args.get("body", "{}")
|
|
920
|
+
ct = args.get("content_type", "application/json")
|
|
921
|
+
count = self.get_driver().execute_script(self._MOCK_INJECT, pattern, status, body, ct)
|
|
922
|
+
self.record("mock_response", url_pattern=pattern, status=status)
|
|
923
|
+
return f"✅ Mock added ({count} active): '{pattern}' → {status} {ct}"
|
|
924
|
+
|
|
925
|
+
async def _clear_mock_responses(self, args: dict) -> str:
|
|
926
|
+
self.get_driver().execute_script(
|
|
927
|
+
"window.__smcpMocks = []; window.__smcpMockOn = false;"
|
|
928
|
+
)
|
|
929
|
+
return "✅ All mock responses cleared"
|
|
930
|
+
|
|
931
|
+
# ── Visual regression ─────────────────────────────────────────────── #
|
|
932
|
+
|
|
933
|
+
async def _compare_screenshot(self, args: dict) -> str:
|
|
934
|
+
name = args.get("name", "default").replace("/", "_")
|
|
935
|
+
update = args.get("update_baseline", False)
|
|
936
|
+
threshold = args.get("threshold", 0.1)
|
|
937
|
+
|
|
938
|
+
baseline_dir = os.path.join(os.getcwd(), ".smcp_baselines")
|
|
939
|
+
os.makedirs(baseline_dir, exist_ok=True)
|
|
940
|
+
baseline_path = os.path.join(baseline_dir, f"{name}.png")
|
|
941
|
+
|
|
942
|
+
current = self.get_driver().get_screenshot_as_png()
|
|
943
|
+
|
|
944
|
+
if update or not os.path.exists(baseline_path):
|
|
945
|
+
with open(baseline_path, "wb") as f:
|
|
946
|
+
f.write(current)
|
|
947
|
+
return f"✅ Baseline saved: '{name}' ({len(current):,} bytes → {baseline_path})"
|
|
948
|
+
|
|
949
|
+
with open(baseline_path, "rb") as f:
|
|
950
|
+
baseline = f.read()
|
|
951
|
+
|
|
952
|
+
try:
|
|
953
|
+
from PIL import Image, ImageChops
|
|
954
|
+
img1 = Image.open(io.BytesIO(baseline)).convert("RGB")
|
|
955
|
+
img2 = Image.open(io.BytesIO(current)).convert("RGB")
|
|
956
|
+
|
|
957
|
+
if img1.size != img2.size:
|
|
958
|
+
return (f"⚠️ Size mismatch — baseline {img1.size} vs current {img2.size}. "
|
|
959
|
+
f"Use update_baseline=true to reset.")
|
|
960
|
+
|
|
961
|
+
diff = ImageChops.difference(img1, img2)
|
|
962
|
+
pixels = list(diff.getdata())
|
|
963
|
+
changed = sum(1 for p in pixels if any(c > 10 for c in p))
|
|
964
|
+
pct = round(100.0 * changed / len(pixels), 3)
|
|
965
|
+
|
|
966
|
+
if pct <= threshold:
|
|
967
|
+
return f"✅ Match ({pct:.3f}% diff ≤ {threshold}% threshold) — '{name}'"
|
|
968
|
+
return f"❌ Diff detected ({pct:.3f}% > {threshold}% threshold) — '{name}'"
|
|
969
|
+
|
|
970
|
+
except ImportError:
|
|
971
|
+
if current == baseline:
|
|
972
|
+
return f"✅ Identical (byte match) — '{name}' (install Pillow for pixel diff)"
|
|
973
|
+
diff = sum(a != b for a, b in zip(current, baseline))
|
|
974
|
+
pct = round(100.0 * diff / max(len(current), len(baseline)), 2)
|
|
975
|
+
verdict = "✅" if pct <= threshold else "⚠️"
|
|
976
|
+
return (f"{verdict} {pct:.2f}% byte diff — '{name}' "
|
|
977
|
+
f"(install Pillow for accurate pixel comparison: pip install Pillow)")
|
|
978
|
+
|
|
979
|
+
# ── Accessibility audit ───────────────────────────────────────────── #
|
|
980
|
+
|
|
981
|
+
_A11Y_SCRIPT = """
|
|
982
|
+
const issues = [];
|
|
983
|
+
const $ = s => Array.from(document.querySelectorAll(s));
|
|
984
|
+
const el2str = el => el.outerHTML.substring(0, 100).replace(/\\n/g, ' ');
|
|
985
|
+
|
|
986
|
+
if (!document.title || !document.title.trim())
|
|
987
|
+
issues.push({impact:'serious', id:'document-title', desc:'Page must have a title', el:'<title>'});
|
|
988
|
+
|
|
989
|
+
if (!document.documentElement.lang)
|
|
990
|
+
issues.push({impact:'serious', id:'html-has-lang', desc:'<html> must have a lang attribute', el:'<html>'});
|
|
991
|
+
|
|
992
|
+
$('img').forEach(el => {
|
|
993
|
+
if (!el.hasAttribute('alt'))
|
|
994
|
+
issues.push({impact:'critical', id:'image-alt', desc:'Images must have alt text', el:el2str(el)});
|
|
995
|
+
});
|
|
996
|
+
|
|
997
|
+
$('input:not([type=hidden]):not([type=submit]):not([type=button]):not([type=image]):not([type=reset])').forEach(el => {
|
|
998
|
+
const hasLabel = el.id && document.querySelector('label[for="'+el.id+'"]');
|
|
999
|
+
if (!hasLabel && !el.getAttribute('aria-label') && !el.getAttribute('aria-labelledby') && !el.getAttribute('title'))
|
|
1000
|
+
issues.push({impact:'serious', id:'label', desc:'Form inputs must have a label', el:el2str(el)});
|
|
1001
|
+
});
|
|
1002
|
+
|
|
1003
|
+
$('button').forEach(el => {
|
|
1004
|
+
if (!el.textContent.trim() && !el.getAttribute('aria-label') && !el.getAttribute('title'))
|
|
1005
|
+
issues.push({impact:'serious', id:'button-name', desc:'Buttons must have accessible names', el:el2str(el)});
|
|
1006
|
+
});
|
|
1007
|
+
|
|
1008
|
+
$('a').forEach(el => {
|
|
1009
|
+
if (!el.textContent.trim() && !el.getAttribute('aria-label') && !el.querySelector('img[alt]'))
|
|
1010
|
+
issues.push({impact:'serious', id:'link-name', desc:'Links must have accessible names', el:el2str(el)});
|
|
1011
|
+
});
|
|
1012
|
+
|
|
1013
|
+
const h1s = $('h1');
|
|
1014
|
+
if (!h1s.length)
|
|
1015
|
+
issues.push({impact:'moderate', id:'page-has-h1', desc:'Page should have an <h1>', el:'<body>'});
|
|
1016
|
+
else if (h1s.length > 1)
|
|
1017
|
+
issues.push({impact:'moderate', id:'multiple-h1', desc:'Page should not have more than one <h1>', el:'<body>'});
|
|
1018
|
+
|
|
1019
|
+
$('[onclick]:not(a):not(button):not(input):not(select):not(textarea)').forEach(el => {
|
|
1020
|
+
if (!el.getAttribute('tabindex') && !el.getAttribute('role'))
|
|
1021
|
+
issues.push({impact:'serious', id:'keyboard', desc:'onClick elements need tabindex and role for keyboard access', el:el2str(el)});
|
|
1022
|
+
});
|
|
1023
|
+
|
|
1024
|
+
$('input[type=password]').forEach(el => {
|
|
1025
|
+
if (!el.getAttribute('autocomplete'))
|
|
1026
|
+
issues.push({impact:'minor', id:'autocomplete', desc:'Password inputs should have autocomplete attribute', el:el2str(el)});
|
|
1027
|
+
});
|
|
1028
|
+
|
|
1029
|
+
return {url: location.href, issues};
|
|
1030
|
+
"""
|
|
1031
|
+
|
|
1032
|
+
_IMPACT_ORDER = {"critical": 0, "serious": 1, "moderate": 2, "minor": 3}
|
|
1033
|
+
|
|
1034
|
+
async def _check_accessibility(self, args: dict) -> str:
|
|
1035
|
+
level = args.get("level", "all")
|
|
1036
|
+
result = self.get_driver().execute_script(self._A11Y_SCRIPT)
|
|
1037
|
+
issues = result.get("issues", [])
|
|
1038
|
+
|
|
1039
|
+
if level != "all":
|
|
1040
|
+
issues = [i for i in issues if i["impact"] == level]
|
|
1041
|
+
|
|
1042
|
+
issues.sort(key=lambda i: self._IMPACT_ORDER.get(i["impact"], 4))
|
|
1043
|
+
|
|
1044
|
+
if not issues:
|
|
1045
|
+
return f"✅ No accessibility issues found on {result['url']}"
|
|
1046
|
+
|
|
1047
|
+
counts: dict = {}
|
|
1048
|
+
for i in issues:
|
|
1049
|
+
counts[i["impact"]] = counts.get(i["impact"], 0) + 1
|
|
1050
|
+
summary = ", ".join(
|
|
1051
|
+
f"{v} {k}" for k, v in sorted(counts.items(), key=lambda x: self._IMPACT_ORDER.get(x[0], 4))
|
|
1052
|
+
)
|
|
1053
|
+
|
|
1054
|
+
lines = [
|
|
1055
|
+
f"Accessibility audit: {len(issues)} issue(s) — {summary}",
|
|
1056
|
+
f"Page: {result['url']}",
|
|
1057
|
+
"",
|
|
1058
|
+
]
|
|
1059
|
+
for i in issues:
|
|
1060
|
+
lines.append(f"[{i['impact']}] {i['id']}: {i['desc']}")
|
|
1061
|
+
lines.append(f" {i['el']}")
|
|
1062
|
+
return "\n".join(lines)
|
|
@@ -358,6 +358,28 @@ class ElementTools:
|
|
|
358
358
|
"required": ["selector"],
|
|
359
359
|
},
|
|
360
360
|
),
|
|
361
|
+
Tool(
|
|
362
|
+
name="fill_form",
|
|
363
|
+
description=(
|
|
364
|
+
"Fill multiple form fields in a single call. Pass a map of CSS selector → value. "
|
|
365
|
+
"Automatically handles text inputs, textareas, checkboxes (true/false), "
|
|
366
|
+
"radio buttons, and <select> dropdowns. Optionally clicks a submit button at the end."
|
|
367
|
+
),
|
|
368
|
+
inputSchema={
|
|
369
|
+
"type": "object",
|
|
370
|
+
"properties": {
|
|
371
|
+
"fields": {
|
|
372
|
+
"type": "object",
|
|
373
|
+
"description": "Map of selector → value. Checkboxes: 'true'/'false'. Selects: visible text or value.",
|
|
374
|
+
"additionalProperties": {"type": "string"},
|
|
375
|
+
},
|
|
376
|
+
"by": {"type": "string", "default": "css"},
|
|
377
|
+
"timeout": {"type": "integer", "default": 10},
|
|
378
|
+
"submit": {"type": "string", "description": "Selector of submit button to click after filling (optional)"},
|
|
379
|
+
},
|
|
380
|
+
"required": ["fields"],
|
|
381
|
+
},
|
|
382
|
+
),
|
|
361
383
|
Tool(
|
|
362
384
|
name="get_healed_locators",
|
|
363
385
|
description="Return all self-healed locator mappings from this session. Shows which selectors were automatically repaired and what they were replaced with.",
|
|
@@ -501,6 +523,7 @@ class ElementTools:
|
|
|
501
523
|
"wait_for_element": self._wait_for_element,
|
|
502
524
|
"scroll_to_element": self._scroll_to_element,
|
|
503
525
|
"clear_field": self._clear_field,
|
|
526
|
+
"fill_form": self._fill_form,
|
|
504
527
|
"get_healed_locators": self._get_healed_locators,
|
|
505
528
|
"clear_healed_locators": self._clear_healed_locators,
|
|
506
529
|
"accept_alert": self._accept_alert,
|
|
@@ -636,6 +659,84 @@ class ElementTools:
|
|
|
636
659
|
el.clear()
|
|
637
660
|
return f"✅ Cleared '{args['selector']}'{self._heal_note()}"
|
|
638
661
|
|
|
662
|
+
# ------------------------------------------------------------------ #
|
|
663
|
+
# fill_form handler #
|
|
664
|
+
# ------------------------------------------------------------------ #
|
|
665
|
+
|
|
666
|
+
def _fill_one(self, el, selector: str, value: str, by: str) -> str:
|
|
667
|
+
"""Fill a single element; return a short summary line."""
|
|
668
|
+
tag = el.tag_name.lower()
|
|
669
|
+
input_type = (el.get_attribute("type") or "text").lower()
|
|
670
|
+
|
|
671
|
+
if tag == "select":
|
|
672
|
+
sel_obj = Select(el)
|
|
673
|
+
try:
|
|
674
|
+
sel_obj.select_by_visible_text(value)
|
|
675
|
+
self.browser.record("select_option", selector=selector, by=by, by_text=value)
|
|
676
|
+
except Exception:
|
|
677
|
+
sel_obj.select_by_value(value)
|
|
678
|
+
self.browser.record("select_option", selector=selector, by=by, by_value=value)
|
|
679
|
+
return f" select {selector!r} → '{value}'"
|
|
680
|
+
|
|
681
|
+
elif input_type == "checkbox":
|
|
682
|
+
want = value.strip().lower() in ("true", "1", "yes", "on", "checked")
|
|
683
|
+
if el.is_selected() != want:
|
|
684
|
+
el.click()
|
|
685
|
+
self.browser.record("click", selector=selector, by=by)
|
|
686
|
+
state = "checked" if want else "unchecked"
|
|
687
|
+
return f" checkbox {selector!r} → {state}"
|
|
688
|
+
|
|
689
|
+
elif input_type == "radio":
|
|
690
|
+
if not el.is_selected():
|
|
691
|
+
el.click()
|
|
692
|
+
self.browser.record("click", selector=selector, by=by)
|
|
693
|
+
return f" radio {selector!r} → selected"
|
|
694
|
+
|
|
695
|
+
elif input_type == "file":
|
|
696
|
+
el.send_keys(value)
|
|
697
|
+
self.browser.record("upload_file", selector=selector, by=by, file_path=value)
|
|
698
|
+
return f" file {selector!r} → '{value}'"
|
|
699
|
+
|
|
700
|
+
else:
|
|
701
|
+
el.clear()
|
|
702
|
+
el.send_keys(value)
|
|
703
|
+
sel_lower = selector.lower()
|
|
704
|
+
logged = "***" if any(k in sel_lower for k in ("password", "passwd", "pwd")) else value
|
|
705
|
+
self.browser.record("type_text", selector=selector, by=by, text=logged)
|
|
706
|
+
return f" input {selector!r} → '{logged}'"
|
|
707
|
+
|
|
708
|
+
async def _fill_form(self, args: dict) -> str:
|
|
709
|
+
fields: dict = args.get("fields", {})
|
|
710
|
+
by = args.get("by", "css")
|
|
711
|
+
timeout = args.get("timeout", 10)
|
|
712
|
+
submit_sel = args.get("submit", "")
|
|
713
|
+
|
|
714
|
+
if not fields:
|
|
715
|
+
return "❌ No fields provided."
|
|
716
|
+
|
|
717
|
+
results = []
|
|
718
|
+
errors = []
|
|
719
|
+
for selector, value in fields.items():
|
|
720
|
+
try:
|
|
721
|
+
el = self._find(selector, by, timeout)
|
|
722
|
+
results.append(self._fill_one(el, selector, value, by))
|
|
723
|
+
except Exception as e:
|
|
724
|
+
errors.append(f" ❌ {selector!r}: {e}")
|
|
725
|
+
|
|
726
|
+
if submit_sel:
|
|
727
|
+
try:
|
|
728
|
+
self._find_clickable(submit_sel, by, timeout).click()
|
|
729
|
+
self.browser.record("click", selector=submit_sel, by=by)
|
|
730
|
+
results.append(f" submit {submit_sel!r} → clicked")
|
|
731
|
+
except Exception as e:
|
|
732
|
+
errors.append(f" ❌ submit {submit_sel!r}: {e}")
|
|
733
|
+
|
|
734
|
+
summary = f"✅ Filled {len(results)} field(s)"
|
|
735
|
+
if errors:
|
|
736
|
+
summary += f", {len(errors)} error(s)"
|
|
737
|
+
lines = [summary] + results + errors
|
|
738
|
+
return "\n".join(lines)
|
|
739
|
+
|
|
639
740
|
async def _get_healed_locators(self, args: dict) -> str:
|
|
640
741
|
cache = self.browser._healer_cache
|
|
641
742
|
if not cache:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: seleniumboot-mcp
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.2
|
|
4
4
|
Summary: A Python MCP server for Selenium WebDriver — 74 tools for browser automation, alerts, frames, shadow DOM, cookies, mobile emulation, self-healing locators, and codegen for Java/C#/Python/Playwright
|
|
5
5
|
Author-email: Raza Tech <razatechnologyservices@gmail.com>
|
|
6
6
|
License: MIT
|
|
@@ -25,7 +25,7 @@ Requires-Dist: selenium>=4.6.0
|
|
|
25
25
|
A Python **Model Context Protocol (MCP)** server for Selenium WebDriver automation.
|
|
26
26
|
Let Claude or GitHub Copilot control a real browser — navigate pages, interact with elements,
|
|
27
27
|
run assertions, and generate ready-to-run **Java TestNG / JUnit 5 / Cucumber / pytest** test code from recorded sessions.
|
|
28
|
-
|
|
28
|
+
82 tools. No ChromeDriver setup. Browser auto-starts on first use.
|
|
29
29
|
|
|
30
30
|
[](https://pypi.org/project/seleniumboot-mcp/)
|
|
31
31
|
[](https://pypi.org/project/seleniumboot-mcp/)
|
|
@@ -107,7 +107,7 @@ Claude controls the real browser, records every action, and on request generates
|
|
|
107
107
|
|
|
108
108
|
---
|
|
109
109
|
|
|
110
|
-
## Tools (
|
|
110
|
+
## Tools (82 total)
|
|
111
111
|
|
|
112
112
|
### Browser
|
|
113
113
|
| Tool | Description |
|
|
@@ -135,6 +135,13 @@ Claude controls the real browser, records every action, and on request generates
|
|
|
135
135
|
| `delete_cookie` / `delete_all_cookies` | Remove cookies |
|
|
136
136
|
| `get_local_storage` / `set_local_storage` | Read or write localStorage |
|
|
137
137
|
| `get_session_storage` / `set_session_storage` | Read or write sessionStorage |
|
|
138
|
+
| `wait_for_network_idle` | Wait until XHR/fetch traffic is quiet — essential for SPAs |
|
|
139
|
+
| `inspect_page` | Discover all inputs, buttons, selects, links with best-fit CSS selectors |
|
|
140
|
+
| `get_network_logs` | Captured XHR/fetch requests — method, URL, status, timing |
|
|
141
|
+
| `mock_response` | Stub fetch/XHR by URL pattern with a canned response |
|
|
142
|
+
| `clear_mock_responses` | Remove all active mock rules |
|
|
143
|
+
| `compare_screenshot` | Pixel diff against a saved baseline — visual regression |
|
|
144
|
+
| `check_accessibility` | Built-in WCAG audit — alt text, labels, headings, keyboard access |
|
|
138
145
|
|
|
139
146
|
### Elements
|
|
140
147
|
| Tool | Description |
|
|
@@ -164,6 +171,7 @@ Claude controls the real browser, records every action, and on request generates
|
|
|
164
171
|
| `switch_to_default_content` | Return to the main page from a frame |
|
|
165
172
|
| `find_shadow_element` | Find element inside a shadow DOM |
|
|
166
173
|
| `get_table_data` | Extract an HTML table as a formatted text grid |
|
|
174
|
+
| `fill_form` | Fill multiple fields at once — auto-detects input/select/checkbox/radio |
|
|
167
175
|
| `get_healed_locators` | View all self-healed selector mappings for the session |
|
|
168
176
|
| `clear_healed_locators` | Reset the self-healing cache |
|
|
169
177
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{seleniumboot_mcp-0.3.0 → seleniumboot_mcp-0.3.2}/src/seleniumboot_mcp.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{seleniumboot_mcp-0.3.0 → seleniumboot_mcp-0.3.2}/src/seleniumboot_mcp.egg-info/entry_points.txt
RENAMED
|
File without changes
|
{seleniumboot_mcp-0.3.0 → seleniumboot_mcp-0.3.2}/src/seleniumboot_mcp.egg-info/requires.txt
RENAMED
|
File without changes
|
{seleniumboot_mcp-0.3.0 → seleniumboot_mcp-0.3.2}/src/seleniumboot_mcp.egg-info/top_level.txt
RENAMED
|
File without changes
|