brave-real-browser-mcp-server 2.21.2 ā 2.21.4
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.
- package/dist/handlers/content-handlers.js +269 -89
- package/package.json +2 -2
|
@@ -57,106 +57,286 @@ export async function handleFindSelector(args) {
|
|
|
57
57
|
if (!pageInstance) {
|
|
58
58
|
throw new Error('Browser not initialized. Call browser_init first.');
|
|
59
59
|
}
|
|
60
|
-
const { text,
|
|
61
|
-
//
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
60
|
+
const { text, selector, xpath, attributes, description, exact = false, context } = args || {};
|
|
61
|
+
// Ensure elementType has a fallback value
|
|
62
|
+
const elementType = args?.elementType || '*';
|
|
63
|
+
// Priority 1: Direct CSS selector search
|
|
64
|
+
if (selector) {
|
|
65
|
+
const elements = await pageInstance.$$(selector);
|
|
66
|
+
if (elements.length > 0) {
|
|
67
|
+
const elementInfo = await pageInstance.evaluate((sel) => {
|
|
68
|
+
const el = document.querySelector(sel);
|
|
69
|
+
return el ? {
|
|
70
|
+
selector: sel,
|
|
71
|
+
text: el.textContent?.trim().substring(0, 100) || '',
|
|
72
|
+
tagName: el.tagName.toLowerCase()
|
|
73
|
+
} : null;
|
|
74
|
+
}, selector);
|
|
75
|
+
if (elementInfo) {
|
|
76
|
+
return {
|
|
77
|
+
content: [{
|
|
78
|
+
type: 'text',
|
|
79
|
+
text: `Found element: ${elementInfo.selector}\nText: "${elementInfo.text}"\nConfidence: 100\n\n` +
|
|
80
|
+
'š Workflow Status: Element located\n' +
|
|
81
|
+
' ⢠Next step: Use interaction tools (click, type) with this selector\n' +
|
|
82
|
+
' ⢠Selector is validated and ready for automation\n\n' +
|
|
83
|
+
'ā
Element discovery complete - ready for interactions'
|
|
84
|
+
}]
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
throw new Error(`Element not found with selector: ${selector}`);
|
|
81
89
|
}
|
|
82
|
-
|
|
83
|
-
|
|
90
|
+
// Priority 2: XPath expression search
|
|
91
|
+
if (xpath) {
|
|
92
|
+
const elements = await pageInstance.$x(xpath);
|
|
93
|
+
if (elements.length > 0) {
|
|
94
|
+
const elementInfo = await pageInstance.evaluate((xp) => {
|
|
95
|
+
const result = document.evaluate(xp, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null);
|
|
96
|
+
const el = result.singleNodeValue;
|
|
97
|
+
return el ? {
|
|
98
|
+
selector: xp,
|
|
99
|
+
text: el.textContent?.trim().substring(0, 100) || '',
|
|
100
|
+
tagName: el.tagName?.toLowerCase() || 'unknown'
|
|
101
|
+
} : null;
|
|
102
|
+
}, xpath);
|
|
103
|
+
if (elementInfo) {
|
|
104
|
+
return {
|
|
105
|
+
content: [{
|
|
106
|
+
type: 'text',
|
|
107
|
+
text: `Found element via XPath: ${xpath}\nText: "${elementInfo.text}"\nTag: ${elementInfo.tagName}\nConfidence: 95\n\n` +
|
|
108
|
+
'š Workflow Status: Element located\n' +
|
|
109
|
+
' ⢠Next step: Use interaction tools with XPath or convert to CSS selector\n\n' +
|
|
110
|
+
'ā
XPath element discovery complete'
|
|
111
|
+
}]
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
throw new Error(`Element not found with XPath: ${xpath}`);
|
|
84
116
|
}
|
|
85
|
-
//
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
117
|
+
// Priority 3: Attributes JSON matching
|
|
118
|
+
if (attributes) {
|
|
119
|
+
let attrObj;
|
|
120
|
+
try {
|
|
121
|
+
attrObj = JSON.parse(attributes);
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
throw new Error(`Invalid attributes JSON: ${attributes}. Please provide valid JSON.`);
|
|
125
|
+
}
|
|
126
|
+
const results = await pageInstance.evaluate((attrs, elemType) => {
|
|
127
|
+
const elements = document.querySelectorAll(elemType);
|
|
128
|
+
const matches = [];
|
|
129
|
+
elements.forEach(el => {
|
|
130
|
+
let allMatch = true;
|
|
131
|
+
for (const [key, value] of Object.entries(attrs)) {
|
|
132
|
+
const attrValue = el.getAttribute(key);
|
|
133
|
+
if (attrValue !== value && !attrValue?.includes(value)) {
|
|
134
|
+
allMatch = false;
|
|
135
|
+
break;
|
|
136
|
+
}
|
|
101
137
|
}
|
|
102
|
-
if (
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
138
|
+
if (allMatch) {
|
|
139
|
+
let sel = el.tagName.toLowerCase();
|
|
140
|
+
if (el.id)
|
|
141
|
+
sel += '#' + el.id;
|
|
142
|
+
else if (el.className && typeof el.className === 'string') {
|
|
143
|
+
const cls = el.className.trim().split(/\s+/)[0];
|
|
144
|
+
if (cls)
|
|
145
|
+
sel += '.' + cls;
|
|
107
146
|
}
|
|
108
|
-
|
|
109
|
-
|
|
147
|
+
matches.push({
|
|
148
|
+
selector: sel,
|
|
149
|
+
text: el.textContent?.trim().substring(0, 100) || '',
|
|
150
|
+
tagName: el.tagName.toLowerCase()
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
return matches.slice(0, 5);
|
|
155
|
+
}, attrObj, elementType);
|
|
156
|
+
if (results.length > 0) {
|
|
157
|
+
const best = results[0];
|
|
158
|
+
return {
|
|
159
|
+
content: [{
|
|
160
|
+
type: 'text',
|
|
161
|
+
text: `Found element by attributes: ${best.selector}\nText: "${best.text}"\nConfidence: 90\n${results.length > 1 ? `\nAdditional matches: ${results.length - 1}` : ''}\n\n` +
|
|
162
|
+
'š Workflow Status: Element located\n' +
|
|
163
|
+
' ⢠Next step: Use interaction tools (click, type) with this selector\n\n' +
|
|
164
|
+
'ā
Attribute-based element discovery complete'
|
|
165
|
+
}]
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
throw new Error(`No elements found matching attributes: ${attributes}`);
|
|
169
|
+
}
|
|
170
|
+
// Priority 4: Natural language description search (AI-powered fuzzy matching)
|
|
171
|
+
if (description) {
|
|
172
|
+
// Extract keywords from description for fuzzy matching
|
|
173
|
+
const keywords = description.toLowerCase()
|
|
174
|
+
.replace(/[^a-z0-9\s]/g, ' ')
|
|
175
|
+
.split(/\s+/)
|
|
176
|
+
.filter(w => w.length > 2 && !['the', 'and', 'for', 'with', 'that', 'this', 'from', 'button', 'link', 'element'].includes(w));
|
|
177
|
+
if (keywords.length === 0) {
|
|
178
|
+
throw new Error(`Could not extract meaningful keywords from description: "${description}". Please provide more specific terms.`);
|
|
179
|
+
}
|
|
180
|
+
const results = await pageInstance.evaluate((kws, elemType, ctx) => {
|
|
181
|
+
const searchArea = ctx ? document.querySelector(ctx) || document.body : document.body;
|
|
182
|
+
const elements = searchArea.querySelectorAll(elemType);
|
|
183
|
+
const matches = [];
|
|
184
|
+
elements.forEach(el => {
|
|
185
|
+
const elText = (el.textContent || '').toLowerCase();
|
|
186
|
+
const elAttrs = Array.from(el.attributes).map(a => a.value.toLowerCase()).join(' ');
|
|
187
|
+
const combined = elText + ' ' + elAttrs;
|
|
188
|
+
let score = 0;
|
|
189
|
+
for (const kw of kws) {
|
|
190
|
+
if (combined.includes(kw))
|
|
191
|
+
score++;
|
|
192
|
+
}
|
|
193
|
+
if (score > 0) {
|
|
194
|
+
let sel = el.tagName.toLowerCase();
|
|
195
|
+
if (el.id)
|
|
196
|
+
sel += '#' + el.id;
|
|
197
|
+
else if (el.className && typeof el.className === 'string') {
|
|
198
|
+
const cls = el.className.trim().split(/\s+/)[0];
|
|
110
199
|
if (cls)
|
|
111
|
-
|
|
200
|
+
sel += '.' + cls;
|
|
112
201
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
confidence = 80;
|
|
119
|
-
elements.push({
|
|
120
|
-
selector,
|
|
121
|
-
text: elementText.substring(0, 100),
|
|
122
|
-
tagName: element.tagName.toLowerCase(),
|
|
123
|
-
confidence
|
|
202
|
+
matches.push({
|
|
203
|
+
selector: sel,
|
|
204
|
+
text: el.textContent?.trim().substring(0, 100) || '',
|
|
205
|
+
tagName: el.tagName.toLowerCase(),
|
|
206
|
+
score: (score / kws.length) * 100
|
|
124
207
|
});
|
|
125
208
|
}
|
|
126
209
|
});
|
|
210
|
+
// Sort by score descending
|
|
211
|
+
matches.sort((a, b) => b.score - a.score);
|
|
212
|
+
return matches.slice(0, 5);
|
|
213
|
+
}, keywords, elementType, context);
|
|
214
|
+
if (results.length > 0) {
|
|
215
|
+
const best = results[0];
|
|
216
|
+
const additionalMatches = results.slice(1, 3).map((r) => ` ⢠${r.selector} (confidence: ${Math.round(r.score)})`).join('\n');
|
|
217
|
+
return {
|
|
218
|
+
content: [{
|
|
219
|
+
type: 'text',
|
|
220
|
+
text: `Found element: ${best.selector}\nText: "${best.text}"\nConfidence: ${Math.round(best.score)}\n` +
|
|
221
|
+
(additionalMatches ? `\nAlternative matches:\n${additionalMatches}` : '') +
|
|
222
|
+
'\n\nš Workflow Status: Element located\n' +
|
|
223
|
+
' ⢠Next step: Use interaction tools (click, type) with this selector\n' +
|
|
224
|
+
' ⢠Selector is validated and ready for automation\n\n' +
|
|
225
|
+
'ā
Element discovery complete - ready for interactions'
|
|
226
|
+
}]
|
|
227
|
+
};
|
|
127
228
|
}
|
|
128
|
-
|
|
129
|
-
return elements.slice(0, 10);
|
|
130
|
-
}, text, // Use the text argument from args
|
|
131
|
-
searchSelectors, exact);
|
|
132
|
-
if (results.length === 0) {
|
|
133
|
-
throw new Error(`No elements found containing text: "${text}"\n\n` +
|
|
134
|
-
'š” Troubleshooting suggestions:\n' +
|
|
135
|
-
' ⢠Check if the text appears exactly as shown on the page\n' +
|
|
136
|
-
' ⢠Try partial text search with exact=false\n' +
|
|
137
|
-
' ⢠Use get_content to see all available text first\n' +
|
|
138
|
-
' ⢠Verify the page has fully loaded\n' +
|
|
139
|
-
' ⢠Check if the element is hidden or in a different frame');
|
|
229
|
+
throw new Error(`No elements found matching description: "${description}". Keywords searched: ${keywords.join(', ')}`);
|
|
140
230
|
}
|
|
141
|
-
//
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
231
|
+
// Priority 5: Text content search (original functionality)
|
|
232
|
+
if (text) {
|
|
233
|
+
// Enhanced semantic element type mappings
|
|
234
|
+
const semanticMappings = {
|
|
235
|
+
'button': ['button', '[role="button"]', 'input[type="button"]', 'input[type="submit"]'],
|
|
236
|
+
'link': ['a', '[role="link"]'],
|
|
237
|
+
'input': ['input', 'textarea', '[role="textbox"]', '[contenteditable="true"]'],
|
|
238
|
+
'navigation': ['nav', '[role="navigation"]', '.nav', '.navbar', '.menu'],
|
|
239
|
+
'heading': ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', '[role="heading"]'],
|
|
240
|
+
'list': ['ul', 'ol', '[role="list"]', '.list'],
|
|
241
|
+
'article': ['article', '[role="article"]', '.article', '.post'],
|
|
242
|
+
'form': ['form', '[role="form"]'],
|
|
243
|
+
'dialog': ['dialog', '[role="dialog"]', '.modal', '.popup'],
|
|
244
|
+
'tab': ['[role="tab"]', '.tab'],
|
|
245
|
+
'menu': ['[role="menu"]', '.menu', '.dropdown'],
|
|
246
|
+
'checkbox': ['input[type="checkbox"]', '[role="checkbox"]'],
|
|
247
|
+
'radio': ['input[type="radio"]', '[role="radio"]']
|
|
248
|
+
};
|
|
249
|
+
// Convert semantic element type to actual selectors
|
|
250
|
+
let searchSelectors;
|
|
251
|
+
if (semanticMappings[elementType.toLowerCase()]) {
|
|
252
|
+
searchSelectors = semanticMappings[elementType.toLowerCase()];
|
|
253
|
+
}
|
|
254
|
+
else {
|
|
255
|
+
searchSelectors = [elementType];
|
|
256
|
+
}
|
|
257
|
+
// Enhanced selector finding with context support
|
|
258
|
+
const results = await pageInstance.evaluate((searchText, selectors, isExact, ctx) => {
|
|
259
|
+
const searchArea = ctx ? document.querySelector(ctx) || document.body : document.body;
|
|
260
|
+
const elements = [];
|
|
261
|
+
const lowerSearchText = searchText.toLowerCase();
|
|
262
|
+
// Search through specified selectors
|
|
263
|
+
for (const baseSelector of selectors) {
|
|
264
|
+
const candidates = searchArea.querySelectorAll(baseSelector);
|
|
265
|
+
candidates.forEach(element => {
|
|
266
|
+
const elementText = element.textContent?.trim() || '';
|
|
267
|
+
const lowerElementText = elementText.toLowerCase();
|
|
268
|
+
let matches = false;
|
|
269
|
+
if (isExact) {
|
|
270
|
+
matches = lowerElementText === lowerSearchText;
|
|
271
|
+
}
|
|
272
|
+
else {
|
|
273
|
+
matches = lowerElementText.includes(lowerSearchText);
|
|
274
|
+
}
|
|
275
|
+
if (matches) {
|
|
276
|
+
// Generate simple selector
|
|
277
|
+
let selector = element.tagName.toLowerCase();
|
|
278
|
+
if (element.id) {
|
|
279
|
+
selector += '#' + element.id;
|
|
280
|
+
}
|
|
281
|
+
else if (element.className && typeof element.className === 'string') {
|
|
282
|
+
const cls = element.className.trim().split(/\s+/)[0];
|
|
283
|
+
if (cls)
|
|
284
|
+
selector += '.' + cls;
|
|
285
|
+
}
|
|
286
|
+
// Calculate confidence based on match quality
|
|
287
|
+
let confidence = 100;
|
|
288
|
+
if (lowerElementText === lowerSearchText)
|
|
289
|
+
confidence = 100;
|
|
290
|
+
else
|
|
291
|
+
confidence = 80;
|
|
292
|
+
elements.push({
|
|
293
|
+
selector,
|
|
294
|
+
text: elementText.substring(0, 100),
|
|
295
|
+
tagName: element.tagName.toLowerCase(),
|
|
296
|
+
confidence
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
// Return top 10 unique
|
|
302
|
+
return elements.slice(0, 10);
|
|
303
|
+
}, text, searchSelectors, exact, context);
|
|
304
|
+
if (results.length === 0) {
|
|
305
|
+
throw new Error(`No elements found containing text: "${text}"\n\n` +
|
|
306
|
+
'š” Troubleshooting suggestions:\n' +
|
|
307
|
+
' ⢠Check if the text appears exactly as shown on the page\n' +
|
|
308
|
+
' ⢠Try partial text search with exact=false\n' +
|
|
309
|
+
' ⢠Use get_content to see all available text first\n' +
|
|
310
|
+
' ⢠Verify the page has fully loaded\n' +
|
|
311
|
+
' ⢠Check if the element is hidden or in a different frame');
|
|
312
|
+
}
|
|
313
|
+
// Return the best match with additional options
|
|
314
|
+
const bestMatch = results[0];
|
|
315
|
+
const additionalMatches = results.slice(1, 3).map((r) => ` ⢠${r.selector} (confidence: ${r.confidence})`).join('\n');
|
|
316
|
+
const workflowMessage = '\n\nš Workflow Status: Element located\n' +
|
|
317
|
+
' ⢠Next step: Use interaction tools (click, type) with this selector\n' +
|
|
318
|
+
' ⢠Selector is validated and ready for automation\n\n' +
|
|
319
|
+
'ā
Element discovery complete - ready for interactions';
|
|
320
|
+
return {
|
|
321
|
+
content: [
|
|
322
|
+
{
|
|
323
|
+
type: 'text',
|
|
324
|
+
text: `Found element: ${bestMatch.selector}\n` +
|
|
325
|
+
`Text: "${bestMatch.text}"\n` +
|
|
326
|
+
`Confidence: ${bestMatch.confidence}\n` +
|
|
327
|
+
(additionalMatches ? `\nAlternative matches:\n${additionalMatches}` : '') +
|
|
328
|
+
workflowMessage,
|
|
329
|
+
},
|
|
330
|
+
],
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
// No search criteria provided
|
|
334
|
+
throw new Error('No search criteria provided. Please specify at least one of:\n' +
|
|
335
|
+
' ⢠text - Text content to search for\n' +
|
|
336
|
+
' ⢠selector - CSS selector\n' +
|
|
337
|
+
' ⢠xpath - XPath expression\n' +
|
|
338
|
+
' ⢠attributes - JSON string of attributes\n' +
|
|
339
|
+
' ⢠description - Natural language description');
|
|
160
340
|
}, 'Failed to find selector');
|
|
161
341
|
});
|
|
162
342
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "brave-real-browser-mcp-server",
|
|
3
|
-
"version": "2.21.
|
|
3
|
+
"version": "2.21.4",
|
|
4
4
|
"description": "š¦ MCP server for Brave Real Browser - NPM Workspaces Monorepo with anti-detection features",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
"dependencies": {
|
|
42
42
|
"@modelcontextprotocol/sdk": "latest",
|
|
43
43
|
"@types/turndown": "latest",
|
|
44
|
-
"brave-real-browser": "^2.3.
|
|
44
|
+
"brave-real-browser": "^2.3.4",
|
|
45
45
|
"turndown": "latest"
|
|
46
46
|
},
|
|
47
47
|
"peerDependencies": {
|