brave-real-browser-mcp-server 2.23.0 → 2.23.2
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.
|
@@ -24,6 +24,85 @@ export class SelfHealingLocators {
|
|
|
24
24
|
'class', 'aria-role', 'title', 'placeholder', 'alt',
|
|
25
25
|
'href', 'src', 'type', 'value', 'data-*'
|
|
26
26
|
];
|
|
27
|
+
/**
|
|
28
|
+
* Parse Playwright-style :has-text() selector
|
|
29
|
+
* Example: "button:has-text('HubCloud')" -> { tagName: 'button', text: 'HubCloud' }
|
|
30
|
+
* Also supports: "button:contains('text')" and ":text('value')"
|
|
31
|
+
*/
|
|
32
|
+
parseTextSelector(selector) {
|
|
33
|
+
// Match :has-text("text") or :has-text('text')
|
|
34
|
+
const hasTextMatch = selector.match(/^([a-zA-Z]*)(?:\.[^\s:]+)*:has-text\(["']([^"']+)["']\)/i);
|
|
35
|
+
if (hasTextMatch) {
|
|
36
|
+
return {
|
|
37
|
+
tagName: hasTextMatch[1] || undefined,
|
|
38
|
+
text: hasTextMatch[2]
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
// Match :contains("text") jQuery-style
|
|
42
|
+
const containsMatch = selector.match(/^([a-zA-Z]*)(?:\.[^\s:]+)*:contains\(["']([^"']+)["']\)/i);
|
|
43
|
+
if (containsMatch) {
|
|
44
|
+
return {
|
|
45
|
+
tagName: containsMatch[1] || undefined,
|
|
46
|
+
text: containsMatch[2]
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
// Match :text("value") Playwright-style
|
|
50
|
+
const textMatch = selector.match(/:text\(["']([^"']+)["']\)/i);
|
|
51
|
+
if (textMatch) {
|
|
52
|
+
return {
|
|
53
|
+
tagName: undefined,
|
|
54
|
+
text: textMatch[1]
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Find element by text content (for :has-text() support)
|
|
61
|
+
*/
|
|
62
|
+
async findByText(pageInstance, text, tagName) {
|
|
63
|
+
try {
|
|
64
|
+
const element = await pageInstance.evaluateHandle((searchText, tag) => {
|
|
65
|
+
const selector = tag || '*';
|
|
66
|
+
const elements = Array.from(document.querySelectorAll(selector));
|
|
67
|
+
// Find element that contains or exactly matches the text
|
|
68
|
+
const found = elements.find(el => {
|
|
69
|
+
// Skip script and style elements
|
|
70
|
+
if (el.tagName === 'SCRIPT' || el.tagName === 'STYLE')
|
|
71
|
+
return false;
|
|
72
|
+
const textContent = el.textContent?.trim() || '';
|
|
73
|
+
const directText = Array.from(el.childNodes)
|
|
74
|
+
.filter(n => n.nodeType === Node.TEXT_NODE)
|
|
75
|
+
.map(n => n.textContent?.trim())
|
|
76
|
+
.join('');
|
|
77
|
+
// Check direct text first (more accurate)
|
|
78
|
+
if (directText.includes(searchText) || directText === searchText)
|
|
79
|
+
return true;
|
|
80
|
+
// Check if element's own text contains the search text
|
|
81
|
+
// but avoid matching parent elements that contain the text in children
|
|
82
|
+
if (el.children.length === 0 && textContent.includes(searchText))
|
|
83
|
+
return true;
|
|
84
|
+
// For buttons/links with nested elements (icons, spans), check full text
|
|
85
|
+
if (['BUTTON', 'A', 'DIV'].includes(el.tagName) && textContent.includes(searchText)) {
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
return false;
|
|
89
|
+
});
|
|
90
|
+
return found || null;
|
|
91
|
+
}, text, tagName);
|
|
92
|
+
const asElement = element.asElement();
|
|
93
|
+
if (asElement) {
|
|
94
|
+
// Verify element is visible
|
|
95
|
+
const box = await asElement.boundingBox();
|
|
96
|
+
if (box && box.width > 0 && box.height > 0) {
|
|
97
|
+
return asElement;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
catch (e) {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
27
106
|
/**
|
|
28
107
|
* Generate fallback selectors for a failed primary selector
|
|
29
108
|
*/
|
|
@@ -59,6 +138,18 @@ export class SelfHealingLocators {
|
|
|
59
138
|
* Try to find an element using fallback selectors
|
|
60
139
|
*/
|
|
61
140
|
async findElementWithFallbacks(pageInstance, primarySelector, expectedText) {
|
|
141
|
+
// Check if selector uses :has-text(), :contains(), or :text() syntax
|
|
142
|
+
const textSelector = this.parseTextSelector(primarySelector);
|
|
143
|
+
if (textSelector) {
|
|
144
|
+
const element = await this.findByText(pageInstance, textSelector.text, textSelector.tagName);
|
|
145
|
+
if (element) {
|
|
146
|
+
return {
|
|
147
|
+
element,
|
|
148
|
+
usedSelector: `${textSelector.tagName || '*'}[text*="${textSelector.text}"]`,
|
|
149
|
+
strategy: 'text-content'
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
}
|
|
62
153
|
// First try the primary selector
|
|
63
154
|
try {
|
|
64
155
|
const primaryElement = await pageInstance.$(primarySelector);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "brave-real-browser-mcp-server",
|
|
3
|
-
"version": "2.23.
|
|
3
|
+
"version": "2.23.2",
|
|
4
4
|
"description": "🦁 MCP server for Brave Real Browser - NPM Workspaces Monorepo with anti-detection features, SSE streaming, and LSP compatibility",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
"dependencies": {
|
|
50
50
|
"@modelcontextprotocol/sdk": "latest",
|
|
51
51
|
"@types/turndown": "latest",
|
|
52
|
-
"brave-real-browser": "^2.4.
|
|
52
|
+
"brave-real-browser": "^2.4.2",
|
|
53
53
|
"turndown": "latest",
|
|
54
54
|
"vscode-languageserver": "^9.0.1",
|
|
55
55
|
"vscode-languageserver-textdocument": "^1.0.12"
|