seleniumboot-mcp 0.2.2__tar.gz → 0.2.3__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.2.2 → seleniumboot_mcp-0.2.3}/PKG-INFO +22 -4
- {seleniumboot_mcp-0.2.2 → seleniumboot_mcp-0.2.3}/README.md +21 -3
- {seleniumboot_mcp-0.2.2 → seleniumboot_mcp-0.2.3}/pyproject.toml +1 -1
- {seleniumboot_mcp-0.2.2 → seleniumboot_mcp-0.2.3}/src/selenium_mcp/tools/browser_tools.py +1 -0
- {seleniumboot_mcp-0.2.2 → seleniumboot_mcp-0.2.3}/src/selenium_mcp/tools/element_tools.py +511 -356
- {seleniumboot_mcp-0.2.2 → seleniumboot_mcp-0.2.3}/src/seleniumboot_mcp.egg-info/PKG-INFO +22 -4
- {seleniumboot_mcp-0.2.2 → seleniumboot_mcp-0.2.3}/setup.cfg +0 -0
- {seleniumboot_mcp-0.2.2 → seleniumboot_mcp-0.2.3}/src/selenium_mcp/__init__.py +0 -0
- {seleniumboot_mcp-0.2.2 → seleniumboot_mcp-0.2.3}/src/selenium_mcp/server.py +0 -0
- {seleniumboot_mcp-0.2.2 → seleniumboot_mcp-0.2.3}/src/selenium_mcp/tools/__init__.py +0 -0
- {seleniumboot_mcp-0.2.2 → seleniumboot_mcp-0.2.3}/src/selenium_mcp/tools/_locators.py +0 -0
- {seleniumboot_mcp-0.2.2 → seleniumboot_mcp-0.2.3}/src/selenium_mcp/tools/assertion_tools.py +0 -0
- {seleniumboot_mcp-0.2.2 → seleniumboot_mcp-0.2.3}/src/selenium_mcp/tools/codegen_tools.py +0 -0
- {seleniumboot_mcp-0.2.2 → seleniumboot_mcp-0.2.3}/src/seleniumboot_mcp.egg-info/SOURCES.txt +0 -0
- {seleniumboot_mcp-0.2.2 → seleniumboot_mcp-0.2.3}/src/seleniumboot_mcp.egg-info/dependency_links.txt +0 -0
- {seleniumboot_mcp-0.2.2 → seleniumboot_mcp-0.2.3}/src/seleniumboot_mcp.egg-info/entry_points.txt +0 -0
- {seleniumboot_mcp-0.2.2 → seleniumboot_mcp-0.2.3}/src/seleniumboot_mcp.egg-info/requires.txt +0 -0
- {seleniumboot_mcp-0.2.2 → seleniumboot_mcp-0.2.3}/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.2.
|
|
3
|
+
Version: 0.2.3
|
|
4
4
|
Summary: A Python MCP server for Selenium WebDriver — browser automation, Java TestNG/JUnit5/Cucumber codegen, and Page Object Model generation
|
|
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
|
+
45 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 (45 total)
|
|
111
111
|
|
|
112
112
|
### Browser
|
|
113
113
|
| Tool | Description |
|
|
@@ -143,6 +143,8 @@ Claude controls the real browser, records every action, and on request generates
|
|
|
143
143
|
| `wait_for_element` | Wait: visible / clickable / present / invisible |
|
|
144
144
|
| `scroll_to_element` | Scroll element into view |
|
|
145
145
|
| `clear_field` | Clear input field |
|
|
146
|
+
| `get_healed_locators` | View all self-healed selector mappings for the session |
|
|
147
|
+
| `clear_healed_locators` | Reset the self-healing cache |
|
|
146
148
|
|
|
147
149
|
### Assertions
|
|
148
150
|
| Tool | Description |
|
|
@@ -250,6 +252,22 @@ public class LoginSteps {
|
|
|
250
252
|
|
|
251
253
|
---
|
|
252
254
|
|
|
255
|
+
## Self-Healing Locators
|
|
256
|
+
|
|
257
|
+
When a selector fails to find an element, seleniumboot-mcp automatically tries alternative strategies before giving up:
|
|
258
|
+
|
|
259
|
+
| Primary selector | Alternatives tried |
|
|
260
|
+
|---|---|
|
|
261
|
+
| `#my-id` (CSS) | `by=id "my-id"`, `[id='my-id']` |
|
|
262
|
+
| `.my-class` (CSS) | `by=class "my-class"`, `[class*='my-class']` |
|
|
263
|
+
| `input[type='email']` (CSS) | `//input[@type='email']` (XPath) |
|
|
264
|
+
| `//button[@id='ok']` (XPath) | `button[id='ok']` (CSS), `by=id "ok"` |
|
|
265
|
+
| `"A, B"` comma list | tries A first, then B |
|
|
266
|
+
|
|
267
|
+
Successful fallbacks are **cached** so the healed selector is reused automatically. Use `get_healed_locators` to inspect the cache and update your test code, and `clear_healed_locators` to start fresh.
|
|
268
|
+
|
|
269
|
+
---
|
|
270
|
+
|
|
253
271
|
## Links
|
|
254
272
|
|
|
255
273
|
- **GitHub:** [github.com/seleniumboot/selenium-mcp](https://github.com/seleniumboot/selenium-mcp)
|
|
@@ -266,8 +284,8 @@ public class LoginSteps {
|
|
|
266
284
|
- [x] Auto-start browser on first use (no explicit `start_browser` needed)
|
|
267
285
|
- [x] Page Object Model generation (`generate_java_page_object`)
|
|
268
286
|
- [x] Cucumber / Gherkin step generation (`generate_gherkin`)
|
|
287
|
+
- [x] Self-healing locators — automatic fallback when a selector breaks
|
|
269
288
|
- [ ] CI/CD config generator (GitHub Actions, Jenkins)
|
|
270
|
-
- [ ] Self-healing locators
|
|
271
289
|
|
|
272
290
|
---
|
|
273
291
|
|
|
@@ -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
|
+
45 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 (45 total)
|
|
89
89
|
|
|
90
90
|
### Browser
|
|
91
91
|
| Tool | Description |
|
|
@@ -121,6 +121,8 @@ Claude controls the real browser, records every action, and on request generates
|
|
|
121
121
|
| `wait_for_element` | Wait: visible / clickable / present / invisible |
|
|
122
122
|
| `scroll_to_element` | Scroll element into view |
|
|
123
123
|
| `clear_field` | Clear input field |
|
|
124
|
+
| `get_healed_locators` | View all self-healed selector mappings for the session |
|
|
125
|
+
| `clear_healed_locators` | Reset the self-healing cache |
|
|
124
126
|
|
|
125
127
|
### Assertions
|
|
126
128
|
| Tool | Description |
|
|
@@ -228,6 +230,22 @@ public class LoginSteps {
|
|
|
228
230
|
|
|
229
231
|
---
|
|
230
232
|
|
|
233
|
+
## Self-Healing Locators
|
|
234
|
+
|
|
235
|
+
When a selector fails to find an element, seleniumboot-mcp automatically tries alternative strategies before giving up:
|
|
236
|
+
|
|
237
|
+
| Primary selector | Alternatives tried |
|
|
238
|
+
|---|---|
|
|
239
|
+
| `#my-id` (CSS) | `by=id "my-id"`, `[id='my-id']` |
|
|
240
|
+
| `.my-class` (CSS) | `by=class "my-class"`, `[class*='my-class']` |
|
|
241
|
+
| `input[type='email']` (CSS) | `//input[@type='email']` (XPath) |
|
|
242
|
+
| `//button[@id='ok']` (XPath) | `button[id='ok']` (CSS), `by=id "ok"` |
|
|
243
|
+
| `"A, B"` comma list | tries A first, then B |
|
|
244
|
+
|
|
245
|
+
Successful fallbacks are **cached** so the healed selector is reused automatically. Use `get_healed_locators` to inspect the cache and update your test code, and `clear_healed_locators` to start fresh.
|
|
246
|
+
|
|
247
|
+
---
|
|
248
|
+
|
|
231
249
|
## Links
|
|
232
250
|
|
|
233
251
|
- **GitHub:** [github.com/seleniumboot/selenium-mcp](https://github.com/seleniumboot/selenium-mcp)
|
|
@@ -244,8 +262,8 @@ public class LoginSteps {
|
|
|
244
262
|
- [x] Auto-start browser on first use (no explicit `start_browser` needed)
|
|
245
263
|
- [x] Page Object Model generation (`generate_java_page_object`)
|
|
246
264
|
- [x] Cucumber / Gherkin step generation (`generate_gherkin`)
|
|
265
|
+
- [x] Self-healing locators — automatic fallback when a selector breaks
|
|
247
266
|
- [ ] CI/CD config generator (GitHub Actions, Jenkins)
|
|
248
|
-
- [ ] Self-healing locators
|
|
249
267
|
|
|
250
268
|
---
|
|
251
269
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "seleniumboot-mcp"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.3"
|
|
8
8
|
description = "A Python MCP server for Selenium WebDriver — browser automation, Java TestNG/JUnit5/Cucumber codegen, and Page Object Model generation"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -13,6 +13,7 @@ class BrowserTools:
|
|
|
13
13
|
def __init__(self):
|
|
14
14
|
self.driver = None
|
|
15
15
|
self._session_log = [] # records all actions for codegen
|
|
16
|
+
self._healer_cache: dict[str, tuple[str, str]] = {} # (selector,by) → (healed_selector,healed_by)
|
|
16
17
|
|
|
17
18
|
# ------------------------------------------------------------------ #
|
|
18
19
|
# Session helpers #
|
|
@@ -1,356 +1,511 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Element Tools — find, click, type, select, hover, drag-and-drop, waits
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
from selenium.webdriver.
|
|
8
|
-
from selenium.webdriver.
|
|
9
|
-
from
|
|
10
|
-
from
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
"
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
"
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
"
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
def
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
return
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
)
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
)
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
"
|
|
182
|
-
"
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
"
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
"
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
1
|
+
"""
|
|
2
|
+
Element Tools — find, click, type, select, hover, drag-and-drop, waits
|
|
3
|
+
Includes self-healing locators: when a primary selector fails, alternative
|
|
4
|
+
strategies are tried automatically and the successful one is cached.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from selenium.webdriver.common.by import By
|
|
8
|
+
from selenium.webdriver.common.action_chains import ActionChains
|
|
9
|
+
from selenium.webdriver.support.ui import WebDriverWait, Select
|
|
10
|
+
from selenium.webdriver.support import expected_conditions as EC
|
|
11
|
+
from selenium.common.exceptions import TimeoutException, NoSuchElementException
|
|
12
|
+
from mcp.types import Tool
|
|
13
|
+
from selenium_mcp.tools._locators import BY_MAP
|
|
14
|
+
|
|
15
|
+
LOCATOR_SCHEMA = {
|
|
16
|
+
"selector": {
|
|
17
|
+
"type": "string",
|
|
18
|
+
"description": "CSS selector, XPath, or other locator value"
|
|
19
|
+
},
|
|
20
|
+
"by": {
|
|
21
|
+
"type": "string",
|
|
22
|
+
"enum": list(BY_MAP.keys()),
|
|
23
|
+
"default": "css",
|
|
24
|
+
"description": "Locator strategy"
|
|
25
|
+
},
|
|
26
|
+
"timeout": {
|
|
27
|
+
"type": "integer",
|
|
28
|
+
"default": 10,
|
|
29
|
+
"description": "Wait timeout in seconds"
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class ElementTools:
|
|
35
|
+
def __init__(self, browser_tools):
|
|
36
|
+
self.browser = browser_tools
|
|
37
|
+
self._last_heal: tuple[str, str, str, str] | None = None # (orig_sel, orig_by, new_sel, new_by)
|
|
38
|
+
|
|
39
|
+
def _by(self, strategy: str):
|
|
40
|
+
return BY_MAP.get(strategy, By.CSS_SELECTOR)
|
|
41
|
+
|
|
42
|
+
# ------------------------------------------------------------------ #
|
|
43
|
+
# Self-healing core #
|
|
44
|
+
# ------------------------------------------------------------------ #
|
|
45
|
+
|
|
46
|
+
def _alternatives(self, selector: str, by: str) -> list[tuple[str, str]]:
|
|
47
|
+
"""Generate alternative (selector, by) pairs to try when primary fails."""
|
|
48
|
+
alts: list[tuple[str, str]] = []
|
|
49
|
+
|
|
50
|
+
if by == "css":
|
|
51
|
+
# Multi-selector fallback: split on top-level commas
|
|
52
|
+
parts = [p.strip() for p in selector.split(",") if p.strip()]
|
|
53
|
+
if len(parts) > 1:
|
|
54
|
+
alts.extend((p, "css") for p in parts)
|
|
55
|
+
|
|
56
|
+
# #id → by ID
|
|
57
|
+
if selector.startswith("#") and " " not in selector:
|
|
58
|
+
alts.append((selector[1:], "id"))
|
|
59
|
+
|
|
60
|
+
# .class → by class name (simple single class only)
|
|
61
|
+
if selector.startswith(".") and " " not in selector and "." not in selector[1:]:
|
|
62
|
+
cls = selector[1:]
|
|
63
|
+
alts.append((cls, "class"))
|
|
64
|
+
alts.append((f"[class*='{cls}']", "css"))
|
|
65
|
+
|
|
66
|
+
# tag[attr='val'] → XPath
|
|
67
|
+
if "[" in selector and not selector.startswith("["):
|
|
68
|
+
tag = selector.split("[")[0]
|
|
69
|
+
rest = selector[len(tag):]
|
|
70
|
+
xpath = f"//{tag}{rest.replace('[', '[@').replace('=', '=').replace(']', ']')}"
|
|
71
|
+
alts.append((xpath, "xpath"))
|
|
72
|
+
|
|
73
|
+
# Strip CSS pseudo-classes/elements like :not(...), :first-child, ::before
|
|
74
|
+
import re
|
|
75
|
+
stripped = re.sub(r':{1,2}[\w-]+(\([^)]*\))?', '', selector).strip()
|
|
76
|
+
if stripped and stripped != selector:
|
|
77
|
+
alts.append((stripped, "css"))
|
|
78
|
+
|
|
79
|
+
# Try XPath contains on text if it looks like a label
|
|
80
|
+
if not any(c in selector for c in ("#", ".", "[", ":")):
|
|
81
|
+
alts.append((f"//*[contains(text(),'{selector}')]", "xpath"))
|
|
82
|
+
|
|
83
|
+
elif by == "xpath":
|
|
84
|
+
# Try without axis prefix variations
|
|
85
|
+
if selector.startswith("//"):
|
|
86
|
+
alts.append((selector.lstrip("/"), "xpath"))
|
|
87
|
+
# Try CSS conversion for simple tag[@attr] xpaths
|
|
88
|
+
import re
|
|
89
|
+
m = re.match(r'^//(\w+)\[@(\w+)=[\'"]([^\'"]+)[\'"]\]$', selector)
|
|
90
|
+
if m:
|
|
91
|
+
tag, attr, val = m.groups()
|
|
92
|
+
alts.append((f"{tag}[{attr}='{val}']", "css"))
|
|
93
|
+
if attr == "id":
|
|
94
|
+
alts.append((val, "id"))
|
|
95
|
+
elif attr == "name":
|
|
96
|
+
alts.append((val, "name"))
|
|
97
|
+
|
|
98
|
+
elif by == "id":
|
|
99
|
+
alts.append((f"#{selector}", "css"))
|
|
100
|
+
alts.append((f"[id='{selector}']", "css"))
|
|
101
|
+
alts.append((f"//*[@id='{selector}']", "xpath"))
|
|
102
|
+
|
|
103
|
+
elif by == "name":
|
|
104
|
+
alts.append((f"[name='{selector}']", "css"))
|
|
105
|
+
alts.append((f"//*[@name='{selector}']", "xpath"))
|
|
106
|
+
|
|
107
|
+
return alts
|
|
108
|
+
|
|
109
|
+
def _locate(self, selector: str, by: str, timeout: int, condition_fn=None):
|
|
110
|
+
"""
|
|
111
|
+
Find an element using primary selector, falling back to alternatives.
|
|
112
|
+
Caches successful healed selectors via browser._healer_cache.
|
|
113
|
+
Sets self._last_heal when a fallback was used.
|
|
114
|
+
"""
|
|
115
|
+
self._last_heal = None
|
|
116
|
+
driver = self.browser.get_driver()
|
|
117
|
+
cache_key = (selector, by)
|
|
118
|
+
|
|
119
|
+
# Check if we already have a healed locator for this selector
|
|
120
|
+
if cache_key in self.browser._healer_cache:
|
|
121
|
+
healed_sel, healed_by = self.browser._healer_cache[cache_key]
|
|
122
|
+
by_val = self._by(healed_by)
|
|
123
|
+
cond = condition_fn or EC.presence_of_element_located
|
|
124
|
+
try:
|
|
125
|
+
el = WebDriverWait(driver, timeout).until(cond((by_val, healed_sel)))
|
|
126
|
+
self._last_heal = (selector, by, healed_sel, healed_by)
|
|
127
|
+
return el
|
|
128
|
+
except (TimeoutException, NoSuchElementException):
|
|
129
|
+
# Cached heal no longer works — invalidate and retry fresh
|
|
130
|
+
del self.browser._healer_cache[cache_key]
|
|
131
|
+
|
|
132
|
+
# Try primary
|
|
133
|
+
by_val = self._by(by)
|
|
134
|
+
cond = condition_fn or EC.presence_of_element_located
|
|
135
|
+
original_exc = None
|
|
136
|
+
try:
|
|
137
|
+
return WebDriverWait(driver, timeout).until(cond((by_val, selector)))
|
|
138
|
+
except (TimeoutException, NoSuchElementException) as e:
|
|
139
|
+
original_exc = e
|
|
140
|
+
|
|
141
|
+
# Primary failed — try alternatives with a short timeout
|
|
142
|
+
for alt_sel, alt_by in self._alternatives(selector, by):
|
|
143
|
+
try:
|
|
144
|
+
alt_by_val = self._by(alt_by)
|
|
145
|
+
el = WebDriverWait(driver, 3).until(cond((alt_by_val, alt_sel)))
|
|
146
|
+
# Cache the successful alternative
|
|
147
|
+
self.browser._healer_cache[cache_key] = (alt_sel, alt_by)
|
|
148
|
+
self._last_heal = (selector, by, alt_sel, alt_by)
|
|
149
|
+
return el
|
|
150
|
+
except (TimeoutException, NoSuchElementException):
|
|
151
|
+
continue
|
|
152
|
+
|
|
153
|
+
raise original_exc
|
|
154
|
+
|
|
155
|
+
def _heal_note(self) -> str:
|
|
156
|
+
if self._last_heal:
|
|
157
|
+
orig_sel, orig_by, new_sel, new_by = self._last_heal
|
|
158
|
+
return f" [⚕ healed: '{orig_sel}' ({orig_by}) → '{new_sel}' ({new_by})]"
|
|
159
|
+
return ""
|
|
160
|
+
|
|
161
|
+
# ------------------------------------------------------------------ #
|
|
162
|
+
# Legacy helpers kept for drag_and_drop (needs two separate locates) #
|
|
163
|
+
# ------------------------------------------------------------------ #
|
|
164
|
+
|
|
165
|
+
def _find(self, selector, by="css", timeout=10):
|
|
166
|
+
return self._locate(selector, by, timeout, EC.presence_of_element_located)
|
|
167
|
+
|
|
168
|
+
def _find_clickable(self, selector, by="css", timeout=10):
|
|
169
|
+
return self._locate(selector, by, timeout, EC.element_to_be_clickable)
|
|
170
|
+
|
|
171
|
+
# ------------------------------------------------------------------ #
|
|
172
|
+
# MCP tool definitions #
|
|
173
|
+
# ------------------------------------------------------------------ #
|
|
174
|
+
|
|
175
|
+
def get_tools(self) -> list[Tool]:
|
|
176
|
+
return [
|
|
177
|
+
Tool(
|
|
178
|
+
name="find_element",
|
|
179
|
+
description="Find an element and return its text, tag, and attributes.",
|
|
180
|
+
inputSchema={
|
|
181
|
+
"type": "object",
|
|
182
|
+
"properties": LOCATOR_SCHEMA,
|
|
183
|
+
"required": ["selector"],
|
|
184
|
+
},
|
|
185
|
+
),
|
|
186
|
+
Tool(
|
|
187
|
+
name="find_elements",
|
|
188
|
+
description="Find all matching elements and return their texts.",
|
|
189
|
+
inputSchema={
|
|
190
|
+
"type": "object",
|
|
191
|
+
"properties": LOCATOR_SCHEMA,
|
|
192
|
+
"required": ["selector"],
|
|
193
|
+
},
|
|
194
|
+
),
|
|
195
|
+
Tool(
|
|
196
|
+
name="click",
|
|
197
|
+
description="Click on an element.",
|
|
198
|
+
inputSchema={
|
|
199
|
+
"type": "object",
|
|
200
|
+
"properties": LOCATOR_SCHEMA,
|
|
201
|
+
"required": ["selector"],
|
|
202
|
+
},
|
|
203
|
+
),
|
|
204
|
+
Tool(
|
|
205
|
+
name="type_text",
|
|
206
|
+
description="Clear and type text into an input field.",
|
|
207
|
+
inputSchema={
|
|
208
|
+
"type": "object",
|
|
209
|
+
"properties": {
|
|
210
|
+
**LOCATOR_SCHEMA,
|
|
211
|
+
"text": {"type": "string", "description": "Text to type"},
|
|
212
|
+
"clear_first": {"type": "boolean", "default": True}
|
|
213
|
+
},
|
|
214
|
+
"required": ["selector", "text"],
|
|
215
|
+
},
|
|
216
|
+
),
|
|
217
|
+
Tool(
|
|
218
|
+
name="get_text",
|
|
219
|
+
description="Get the visible text content of an element.",
|
|
220
|
+
inputSchema={
|
|
221
|
+
"type": "object",
|
|
222
|
+
"properties": LOCATOR_SCHEMA,
|
|
223
|
+
"required": ["selector"],
|
|
224
|
+
},
|
|
225
|
+
),
|
|
226
|
+
Tool(
|
|
227
|
+
name="get_attribute",
|
|
228
|
+
description="Get an attribute value from an element.",
|
|
229
|
+
inputSchema={
|
|
230
|
+
"type": "object",
|
|
231
|
+
"properties": {
|
|
232
|
+
**LOCATOR_SCHEMA,
|
|
233
|
+
"attribute": {"type": "string", "description": "Attribute name e.g. href, value, class"}
|
|
234
|
+
},
|
|
235
|
+
"required": ["selector", "attribute"],
|
|
236
|
+
},
|
|
237
|
+
),
|
|
238
|
+
Tool(
|
|
239
|
+
name="select_option",
|
|
240
|
+
description="Select an option from a <select> dropdown by visible text, value, or index.",
|
|
241
|
+
inputSchema={
|
|
242
|
+
"type": "object",
|
|
243
|
+
"properties": {
|
|
244
|
+
**LOCATOR_SCHEMA,
|
|
245
|
+
"by_text": {"type": "string"},
|
|
246
|
+
"by_value": {"type": "string"},
|
|
247
|
+
"by_index": {"type": "integer"},
|
|
248
|
+
},
|
|
249
|
+
"required": ["selector"],
|
|
250
|
+
},
|
|
251
|
+
),
|
|
252
|
+
Tool(
|
|
253
|
+
name="hover",
|
|
254
|
+
description="Hover the mouse over an element.",
|
|
255
|
+
inputSchema={
|
|
256
|
+
"type": "object",
|
|
257
|
+
"properties": LOCATOR_SCHEMA,
|
|
258
|
+
"required": ["selector"],
|
|
259
|
+
},
|
|
260
|
+
),
|
|
261
|
+
Tool(
|
|
262
|
+
name="double_click",
|
|
263
|
+
description="Double-click on an element.",
|
|
264
|
+
inputSchema={
|
|
265
|
+
"type": "object",
|
|
266
|
+
"properties": LOCATOR_SCHEMA,
|
|
267
|
+
"required": ["selector"],
|
|
268
|
+
},
|
|
269
|
+
),
|
|
270
|
+
Tool(
|
|
271
|
+
name="right_click",
|
|
272
|
+
description="Right-click (context menu) on an element.",
|
|
273
|
+
inputSchema={
|
|
274
|
+
"type": "object",
|
|
275
|
+
"properties": LOCATOR_SCHEMA,
|
|
276
|
+
"required": ["selector"],
|
|
277
|
+
},
|
|
278
|
+
),
|
|
279
|
+
Tool(
|
|
280
|
+
name="drag_and_drop",
|
|
281
|
+
description="Drag one element and drop it onto another.",
|
|
282
|
+
inputSchema={
|
|
283
|
+
"type": "object",
|
|
284
|
+
"properties": {
|
|
285
|
+
"source_selector": {"type": "string"},
|
|
286
|
+
"target_selector": {"type": "string"},
|
|
287
|
+
"by": {"type": "string", "default": "css"},
|
|
288
|
+
},
|
|
289
|
+
"required": ["source_selector", "target_selector"],
|
|
290
|
+
},
|
|
291
|
+
),
|
|
292
|
+
Tool(
|
|
293
|
+
name="is_displayed",
|
|
294
|
+
description="Check if an element is visible on the page.",
|
|
295
|
+
inputSchema={
|
|
296
|
+
"type": "object",
|
|
297
|
+
"properties": LOCATOR_SCHEMA,
|
|
298
|
+
"required": ["selector"],
|
|
299
|
+
},
|
|
300
|
+
),
|
|
301
|
+
Tool(
|
|
302
|
+
name="is_enabled",
|
|
303
|
+
description="Check if an element is enabled (not disabled).",
|
|
304
|
+
inputSchema={
|
|
305
|
+
"type": "object",
|
|
306
|
+
"properties": LOCATOR_SCHEMA,
|
|
307
|
+
"required": ["selector"],
|
|
308
|
+
},
|
|
309
|
+
),
|
|
310
|
+
Tool(
|
|
311
|
+
name="wait_for_element",
|
|
312
|
+
description="Wait until an element is visible on the page.",
|
|
313
|
+
inputSchema={
|
|
314
|
+
"type": "object",
|
|
315
|
+
"properties": {
|
|
316
|
+
**LOCATOR_SCHEMA,
|
|
317
|
+
"condition": {
|
|
318
|
+
"type": "string",
|
|
319
|
+
"enum": ["visible", "clickable", "present", "invisible"],
|
|
320
|
+
"default": "visible"
|
|
321
|
+
}
|
|
322
|
+
},
|
|
323
|
+
"required": ["selector"],
|
|
324
|
+
},
|
|
325
|
+
),
|
|
326
|
+
Tool(
|
|
327
|
+
name="scroll_to_element",
|
|
328
|
+
description="Scroll the page to bring an element into view.",
|
|
329
|
+
inputSchema={
|
|
330
|
+
"type": "object",
|
|
331
|
+
"properties": LOCATOR_SCHEMA,
|
|
332
|
+
"required": ["selector"],
|
|
333
|
+
},
|
|
334
|
+
),
|
|
335
|
+
Tool(
|
|
336
|
+
name="clear_field",
|
|
337
|
+
description="Clear the text in an input field.",
|
|
338
|
+
inputSchema={
|
|
339
|
+
"type": "object",
|
|
340
|
+
"properties": LOCATOR_SCHEMA,
|
|
341
|
+
"required": ["selector"],
|
|
342
|
+
},
|
|
343
|
+
),
|
|
344
|
+
Tool(
|
|
345
|
+
name="get_healed_locators",
|
|
346
|
+
description="Return all self-healed locator mappings from this session. Shows which selectors were automatically repaired and what they were replaced with.",
|
|
347
|
+
inputSchema={"type": "object", "properties": {}},
|
|
348
|
+
),
|
|
349
|
+
Tool(
|
|
350
|
+
name="clear_healed_locators",
|
|
351
|
+
description="Clear the self-healing locator cache so all selectors are re-evaluated from scratch.",
|
|
352
|
+
inputSchema={"type": "object", "properties": {}},
|
|
353
|
+
),
|
|
354
|
+
]
|
|
355
|
+
|
|
356
|
+
def get_handlers(self) -> dict:
|
|
357
|
+
return {
|
|
358
|
+
"find_element": self._find_element,
|
|
359
|
+
"find_elements": self._find_elements,
|
|
360
|
+
"click": self._click,
|
|
361
|
+
"type_text": self._type_text,
|
|
362
|
+
"get_text": self._get_text,
|
|
363
|
+
"get_attribute": self._get_attribute,
|
|
364
|
+
"select_option": self._select_option,
|
|
365
|
+
"hover": self._hover,
|
|
366
|
+
"double_click": self._double_click,
|
|
367
|
+
"right_click": self._right_click,
|
|
368
|
+
"drag_and_drop": self._drag_and_drop,
|
|
369
|
+
"is_displayed": self._is_displayed,
|
|
370
|
+
"is_enabled": self._is_enabled,
|
|
371
|
+
"wait_for_element": self._wait_for_element,
|
|
372
|
+
"scroll_to_element": self._scroll_to_element,
|
|
373
|
+
"clear_field": self._clear_field,
|
|
374
|
+
"get_healed_locators": self._get_healed_locators,
|
|
375
|
+
"clear_healed_locators":self._clear_healed_locators,
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
# ------------------------------------------------------------------ #
|
|
379
|
+
# Handlers #
|
|
380
|
+
# ------------------------------------------------------------------ #
|
|
381
|
+
|
|
382
|
+
async def _find_element(self, args: dict) -> str:
|
|
383
|
+
el = self._find(args["selector"], args.get("by", "css"), args.get("timeout", 10))
|
|
384
|
+
return f"tag={el.tag_name} | text='{el.text}' | displayed={el.is_displayed()}{self._heal_note()}"
|
|
385
|
+
|
|
386
|
+
async def _find_elements(self, args: dict) -> str:
|
|
387
|
+
driver = self.browser.get_driver()
|
|
388
|
+
els = driver.find_elements(self._by(args.get("by", "css")), args["selector"])
|
|
389
|
+
results = [f"[{i}] tag={e.tag_name} text='{e.text[:60]}'" for i, e in enumerate(els)]
|
|
390
|
+
return f"Found {len(els)} element(s):\n" + "\n".join(results)
|
|
391
|
+
|
|
392
|
+
async def _click(self, args: dict) -> str:
|
|
393
|
+
el = self._find_clickable(args["selector"], args.get("by", "css"), args.get("timeout", 10))
|
|
394
|
+
el.click()
|
|
395
|
+
self.browser.record("click", selector=args["selector"], by=args.get("by", "css"))
|
|
396
|
+
return f"✅ Clicked '{args['selector']}'{self._heal_note()}"
|
|
397
|
+
|
|
398
|
+
async def _type_text(self, args: dict) -> str:
|
|
399
|
+
el = self._find(args["selector"], args.get("by", "css"), args.get("timeout", 10))
|
|
400
|
+
if args.get("clear_first", True):
|
|
401
|
+
el.clear()
|
|
402
|
+
el.send_keys(args["text"])
|
|
403
|
+
sel_lower = args["selector"].lower()
|
|
404
|
+
logged_text = "***" if any(k in sel_lower for k in ("password", "passwd", "pwd")) else args["text"]
|
|
405
|
+
self.browser.record("type_text", selector=args["selector"], by=args.get("by", "css"), text=logged_text)
|
|
406
|
+
return f"✅ Typed into '{args['selector']}'{self._heal_note()}"
|
|
407
|
+
|
|
408
|
+
async def _get_text(self, args: dict) -> str:
|
|
409
|
+
el = self._find(args["selector"], args.get("by", "css"), args.get("timeout", 10))
|
|
410
|
+
note = self._heal_note()
|
|
411
|
+
return el.text + note if note else el.text
|
|
412
|
+
|
|
413
|
+
async def _get_attribute(self, args: dict) -> str:
|
|
414
|
+
el = self._find(args["selector"], args.get("by", "css"), args.get("timeout", 10))
|
|
415
|
+
val = el.get_attribute(args["attribute"])
|
|
416
|
+
note = self._heal_note()
|
|
417
|
+
return str(val) + note if note else str(val)
|
|
418
|
+
|
|
419
|
+
async def _select_option(self, args: dict) -> str:
|
|
420
|
+
el = self._find(args["selector"], args.get("by", "css"), args.get("timeout", 10))
|
|
421
|
+
sel = Select(el)
|
|
422
|
+
note = self._heal_note()
|
|
423
|
+
if "by_text" in args:
|
|
424
|
+
sel.select_by_visible_text(args["by_text"])
|
|
425
|
+
self.browser.record("select_option", selector=args["selector"], by_text=args["by_text"])
|
|
426
|
+
return f"✅ Selected by text: '{args['by_text']}'{note}"
|
|
427
|
+
elif "by_value" in args:
|
|
428
|
+
sel.select_by_value(args["by_value"])
|
|
429
|
+
self.browser.record("select_option", selector=args["selector"], by_value=args["by_value"])
|
|
430
|
+
return f"✅ Selected by value: '{args['by_value']}'{note}"
|
|
431
|
+
elif "by_index" in args:
|
|
432
|
+
sel.select_by_index(args["by_index"])
|
|
433
|
+
self.browser.record("select_option", selector=args["selector"], by_index=args["by_index"])
|
|
434
|
+
return f"✅ Selected by index: {args['by_index']}{note}"
|
|
435
|
+
return "❌ Provide by_text, by_value, or by_index"
|
|
436
|
+
|
|
437
|
+
async def _hover(self, args: dict) -> str:
|
|
438
|
+
el = self._find(args["selector"], args.get("by", "css"), args.get("timeout", 10))
|
|
439
|
+
ActionChains(self.browser.get_driver()).move_to_element(el).perform()
|
|
440
|
+
self.browser.record("hover", selector=args["selector"], by=args.get("by", "css"))
|
|
441
|
+
return f"✅ Hovered over '{args['selector']}'{self._heal_note()}"
|
|
442
|
+
|
|
443
|
+
async def _double_click(self, args: dict) -> str:
|
|
444
|
+
el = self._find_clickable(args["selector"], args.get("by", "css"), args.get("timeout", 10))
|
|
445
|
+
ActionChains(self.browser.get_driver()).double_click(el).perform()
|
|
446
|
+
self.browser.record("double_click", selector=args["selector"], by=args.get("by", "css"))
|
|
447
|
+
return f"✅ Double-clicked '{args['selector']}'{self._heal_note()}"
|
|
448
|
+
|
|
449
|
+
async def _right_click(self, args: dict) -> str:
|
|
450
|
+
el = self._find(args["selector"], args.get("by", "css"), args.get("timeout", 10))
|
|
451
|
+
ActionChains(self.browser.get_driver()).context_click(el).perform()
|
|
452
|
+
self.browser.record("right_click", selector=args["selector"], by=args.get("by", "css"))
|
|
453
|
+
return f"✅ Right-clicked '{args['selector']}'{self._heal_note()}"
|
|
454
|
+
|
|
455
|
+
async def _drag_and_drop(self, args: dict) -> str:
|
|
456
|
+
by = self._by(args.get("by", "css"))
|
|
457
|
+
driver = self.browser.get_driver()
|
|
458
|
+
timeout = args.get("timeout", 10)
|
|
459
|
+
src = WebDriverWait(driver, timeout).until(EC.presence_of_element_located((by, args["source_selector"])))
|
|
460
|
+
tgt = WebDriverWait(driver, timeout).until(EC.presence_of_element_located((by, args["target_selector"])))
|
|
461
|
+
ActionChains(driver).drag_and_drop(src, tgt).perform()
|
|
462
|
+
return f"✅ Dragged '{args['source_selector']}' → '{args['target_selector']}'"
|
|
463
|
+
|
|
464
|
+
async def _is_displayed(self, args: dict) -> str:
|
|
465
|
+
el = self._find(args["selector"], args.get("by", "css"), args.get("timeout", 10))
|
|
466
|
+
return str(el.is_displayed()) + self._heal_note()
|
|
467
|
+
|
|
468
|
+
async def _is_enabled(self, args: dict) -> str:
|
|
469
|
+
el = self._find(args["selector"], args.get("by", "css"), args.get("timeout", 10))
|
|
470
|
+
return str(el.is_enabled()) + self._heal_note()
|
|
471
|
+
|
|
472
|
+
async def _wait_for_element(self, args: dict) -> str:
|
|
473
|
+
driver = self.browser.get_driver()
|
|
474
|
+
by = self._by(args.get("by", "css"))
|
|
475
|
+
sel = args["selector"]
|
|
476
|
+
timeout = args.get("timeout", 10)
|
|
477
|
+
condition = args.get("condition", "visible")
|
|
478
|
+
|
|
479
|
+
cond_map = {
|
|
480
|
+
"visible": EC.visibility_of_element_located,
|
|
481
|
+
"clickable": EC.element_to_be_clickable,
|
|
482
|
+
"present": EC.presence_of_element_located,
|
|
483
|
+
"invisible": EC.invisibility_of_element_located,
|
|
484
|
+
}
|
|
485
|
+
WebDriverWait(driver, timeout).until(cond_map[condition]((by, sel)))
|
|
486
|
+
return f"✅ Element '{sel}' is {condition}"
|
|
487
|
+
|
|
488
|
+
async def _scroll_to_element(self, args: dict) -> str:
|
|
489
|
+
el = self._find(args["selector"], args.get("by", "css"), args.get("timeout", 10))
|
|
490
|
+
self.browser.get_driver().execute_script("arguments[0].scrollIntoView(true);", el)
|
|
491
|
+
self.browser.record("scroll_to_element", selector=args["selector"], by=args.get("by", "css"))
|
|
492
|
+
return f"✅ Scrolled to '{args['selector']}'{self._heal_note()}"
|
|
493
|
+
|
|
494
|
+
async def _clear_field(self, args: dict) -> str:
|
|
495
|
+
el = self._find(args["selector"], args.get("by", "css"), args.get("timeout", 10))
|
|
496
|
+
el.clear()
|
|
497
|
+
return f"✅ Cleared '{args['selector']}'{self._heal_note()}"
|
|
498
|
+
|
|
499
|
+
async def _get_healed_locators(self, args: dict) -> str:
|
|
500
|
+
cache = self.browser._healer_cache
|
|
501
|
+
if not cache:
|
|
502
|
+
return "No healed locators in this session."
|
|
503
|
+
lines = ["Healed locators this session:"]
|
|
504
|
+
for (orig_sel, orig_by), (new_sel, new_by) in cache.items():
|
|
505
|
+
lines.append(f" [{orig_by}] '{orig_sel}' → [{new_by}] '{new_sel}'")
|
|
506
|
+
return "\n".join(lines)
|
|
507
|
+
|
|
508
|
+
async def _clear_healed_locators(self, args: dict) -> str:
|
|
509
|
+
count = len(self.browser._healer_cache)
|
|
510
|
+
self.browser._healer_cache.clear()
|
|
511
|
+
return f"✅ Cleared {count} healed locator(s) from cache."
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: seleniumboot-mcp
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.3
|
|
4
4
|
Summary: A Python MCP server for Selenium WebDriver — browser automation, Java TestNG/JUnit5/Cucumber codegen, and Page Object Model generation
|
|
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
|
+
45 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 (45 total)
|
|
111
111
|
|
|
112
112
|
### Browser
|
|
113
113
|
| Tool | Description |
|
|
@@ -143,6 +143,8 @@ Claude controls the real browser, records every action, and on request generates
|
|
|
143
143
|
| `wait_for_element` | Wait: visible / clickable / present / invisible |
|
|
144
144
|
| `scroll_to_element` | Scroll element into view |
|
|
145
145
|
| `clear_field` | Clear input field |
|
|
146
|
+
| `get_healed_locators` | View all self-healed selector mappings for the session |
|
|
147
|
+
| `clear_healed_locators` | Reset the self-healing cache |
|
|
146
148
|
|
|
147
149
|
### Assertions
|
|
148
150
|
| Tool | Description |
|
|
@@ -250,6 +252,22 @@ public class LoginSteps {
|
|
|
250
252
|
|
|
251
253
|
---
|
|
252
254
|
|
|
255
|
+
## Self-Healing Locators
|
|
256
|
+
|
|
257
|
+
When a selector fails to find an element, seleniumboot-mcp automatically tries alternative strategies before giving up:
|
|
258
|
+
|
|
259
|
+
| Primary selector | Alternatives tried |
|
|
260
|
+
|---|---|
|
|
261
|
+
| `#my-id` (CSS) | `by=id "my-id"`, `[id='my-id']` |
|
|
262
|
+
| `.my-class` (CSS) | `by=class "my-class"`, `[class*='my-class']` |
|
|
263
|
+
| `input[type='email']` (CSS) | `//input[@type='email']` (XPath) |
|
|
264
|
+
| `//button[@id='ok']` (XPath) | `button[id='ok']` (CSS), `by=id "ok"` |
|
|
265
|
+
| `"A, B"` comma list | tries A first, then B |
|
|
266
|
+
|
|
267
|
+
Successful fallbacks are **cached** so the healed selector is reused automatically. Use `get_healed_locators` to inspect the cache and update your test code, and `clear_healed_locators` to start fresh.
|
|
268
|
+
|
|
269
|
+
---
|
|
270
|
+
|
|
253
271
|
## Links
|
|
254
272
|
|
|
255
273
|
- **GitHub:** [github.com/seleniumboot/selenium-mcp](https://github.com/seleniumboot/selenium-mcp)
|
|
@@ -266,8 +284,8 @@ public class LoginSteps {
|
|
|
266
284
|
- [x] Auto-start browser on first use (no explicit `start_browser` needed)
|
|
267
285
|
- [x] Page Object Model generation (`generate_java_page_object`)
|
|
268
286
|
- [x] Cucumber / Gherkin step generation (`generate_gherkin`)
|
|
287
|
+
- [x] Self-healing locators — automatic fallback when a selector breaks
|
|
269
288
|
- [ ] CI/CD config generator (GitHub Actions, Jenkins)
|
|
270
|
-
- [ ] Self-healing locators
|
|
271
289
|
|
|
272
290
|
---
|
|
273
291
|
|
|
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.2.2 → seleniumboot_mcp-0.2.3}/src/seleniumboot_mcp.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{seleniumboot_mcp-0.2.2 → seleniumboot_mcp-0.2.3}/src/seleniumboot_mcp.egg-info/entry_points.txt
RENAMED
|
File without changes
|
{seleniumboot_mcp-0.2.2 → seleniumboot_mcp-0.2.3}/src/seleniumboot_mcp.egg-info/requires.txt
RENAMED
|
File without changes
|
{seleniumboot_mcp-0.2.2 → seleniumboot_mcp-0.2.3}/src/seleniumboot_mcp.egg-info/top_level.txt
RENAMED
|
File without changes
|