real-browser-mcp-server 1.1.7 → 1.1.8
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/lib/cjs/index.js +384 -0
- package/{lib → dist/lib}/cjs/module/pageController.js +27 -29
- package/{lib → dist/lib}/cjs/module/turnstile.js +23 -12
- package/dist/src/ai/action-parser.js +229 -0
- package/dist/src/ai/core.js +367 -0
- package/dist/src/ai/element-finder.js +409 -0
- package/{src → dist/src}/ai/index.js +35 -50
- package/dist/src/ai/page-analyzer.js +264 -0
- package/dist/src/ai/selector-healer.js +215 -0
- package/dist/src/index.js +116 -0
- package/dist/src/mcp/handlers/browser.js +230 -0
- package/dist/src/mcp/handlers/dom.js +550 -0
- package/dist/src/mcp/handlers/extract.js +451 -0
- package/dist/src/mcp/handlers/helpers.js +514 -0
- package/dist/src/mcp/handlers/index.js +63 -0
- package/dist/src/mcp/handlers/misc.js +1224 -0
- package/dist/src/mcp/handlers/network.js +1134 -0
- package/dist/src/mcp/handlers/state.js +215 -0
- package/dist/src/mcp/handlers/vision.js +475 -0
- package/dist/src/mcp/index.js +166 -0
- package/dist/src/mcp/server.js +117 -0
- package/{src → dist/src}/mcp/tools.js +12 -11
- package/dist/src/shared/tools.js +598 -0
- package/{test → dist/test}/cjs/test.js +119 -169
- package/dist/test/mcp/smoke-test.js +131 -0
- package/lib/esm/module/pageController.mjs +21 -18
- package/lib/esm/module/turnstile.mjs +7 -0
- package/package.json +22 -11
- package/.github/ISSUE_TEMPLATE/general_issue.yaml +0 -58
- package/.github/SETUP.md +0 -111
- package/.github/workflows/publish.yml +0 -162
- package/Dockerfile +0 -78
- package/lib/cjs/adblocker.bin +0 -0
- package/lib/cjs/index.js +0 -396
- package/src/ai/action-parser.js +0 -269
- package/src/ai/core.js +0 -379
- package/src/ai/element-finder.js +0 -466
- package/src/ai/page-analyzer.js +0 -295
- package/src/ai/selector-healer.js +0 -236
- package/src/index.js +0 -128
- package/src/mcp/handlers.js +0 -5306
- package/src/mcp/index.js +0 -190
- package/src/mcp/server.js +0 -141
- package/src/shared/tools.js +0 -625
- package/test/esm/test.mjs +0 -299
- package/test/mcp/smoke-test.js +0 -141
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// @ts-nocheck
|
|
3
|
+
class PageAnalyzer {
|
|
4
|
+
constructor() {
|
|
5
|
+
this.analysisTypes = ['full', 'interactive', 'forms', 'navigation', 'content', 'media'];
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Analyze page structure
|
|
9
|
+
*/
|
|
10
|
+
async analyze(page, options = {}) {
|
|
11
|
+
const { analysisType = 'full', includeSelectors = true, includeScreenshot = false, maxDepth = 10 } = options;
|
|
12
|
+
const analysis = await page.evaluate(({ analysisType, includeSelectors, maxDepth }) => {
|
|
13
|
+
const result = {
|
|
14
|
+
url: window.location.href,
|
|
15
|
+
title: document.title,
|
|
16
|
+
timestamp: new Date().toISOString(),
|
|
17
|
+
viewport: {
|
|
18
|
+
width: window.innerWidth,
|
|
19
|
+
height: window.innerHeight
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
// Helper to generate selector
|
|
23
|
+
const getSelector = (el) => {
|
|
24
|
+
if (!includeSelectors)
|
|
25
|
+
return null;
|
|
26
|
+
if (el.id)
|
|
27
|
+
return `#${el.id}`;
|
|
28
|
+
if (el.name)
|
|
29
|
+
return `[name="${el.name}"]`;
|
|
30
|
+
if (el.className && typeof el.className === 'string') {
|
|
31
|
+
const cls = el.className.split(' ').filter(c => c)[0];
|
|
32
|
+
if (cls)
|
|
33
|
+
return `${el.tagName.toLowerCase()}.${cls}`;
|
|
34
|
+
}
|
|
35
|
+
return el.tagName.toLowerCase();
|
|
36
|
+
};
|
|
37
|
+
// Analyze interactive elements
|
|
38
|
+
if (analysisType === 'full' || analysisType === 'interactive') {
|
|
39
|
+
result.interactive = {
|
|
40
|
+
buttons: [],
|
|
41
|
+
links: [],
|
|
42
|
+
inputs: []
|
|
43
|
+
};
|
|
44
|
+
// Buttons
|
|
45
|
+
document.querySelectorAll('button, [role="button"], input[type="button"], input[type="submit"]').forEach(el => {
|
|
46
|
+
const rect = el.getBoundingClientRect();
|
|
47
|
+
if (rect.width > 0 && rect.height > 0) {
|
|
48
|
+
result.interactive.buttons.push({
|
|
49
|
+
selector: getSelector(el),
|
|
50
|
+
text: el.textContent?.trim().substring(0, 50),
|
|
51
|
+
type: el.type || 'button',
|
|
52
|
+
disabled: el.disabled,
|
|
53
|
+
visible: true
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
// Links
|
|
58
|
+
document.querySelectorAll('a[href]').forEach(el => {
|
|
59
|
+
const rect = el.getBoundingClientRect();
|
|
60
|
+
if (rect.width > 0 && rect.height > 0) {
|
|
61
|
+
result.interactive.links.push({
|
|
62
|
+
selector: getSelector(el),
|
|
63
|
+
text: el.textContent?.trim().substring(0, 50),
|
|
64
|
+
href: el.href,
|
|
65
|
+
target: el.target || '_self',
|
|
66
|
+
visible: true
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
// Inputs
|
|
71
|
+
document.querySelectorAll('input, textarea, select').forEach(el => {
|
|
72
|
+
const rect = el.getBoundingClientRect();
|
|
73
|
+
if (rect.width > 0 && rect.height > 0) {
|
|
74
|
+
result.interactive.inputs.push({
|
|
75
|
+
selector: getSelector(el),
|
|
76
|
+
type: el.type || 'text',
|
|
77
|
+
name: el.name,
|
|
78
|
+
placeholder: el.placeholder,
|
|
79
|
+
required: el.required,
|
|
80
|
+
disabled: el.disabled,
|
|
81
|
+
value: el.type === 'password' ? '***' : el.value?.substring(0, 20),
|
|
82
|
+
visible: true
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
// Analyze forms
|
|
88
|
+
if (analysisType === 'full' || analysisType === 'forms') {
|
|
89
|
+
result.forms = [];
|
|
90
|
+
document.querySelectorAll('form').forEach(form => {
|
|
91
|
+
const formData = {
|
|
92
|
+
selector: getSelector(form),
|
|
93
|
+
action: form.action,
|
|
94
|
+
method: form.method,
|
|
95
|
+
fields: []
|
|
96
|
+
};
|
|
97
|
+
form.querySelectorAll('input, textarea, select').forEach(field => {
|
|
98
|
+
formData.fields.push({
|
|
99
|
+
selector: getSelector(field),
|
|
100
|
+
type: field.type || field.tagName.toLowerCase(),
|
|
101
|
+
name: field.name,
|
|
102
|
+
label: document.querySelector(`label[for="${field.id}"]`)?.textContent?.trim(),
|
|
103
|
+
required: field.required,
|
|
104
|
+
placeholder: field.placeholder
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
// Find submit button
|
|
108
|
+
const submitBtn = form.querySelector('button[type="submit"], input[type="submit"], button:not([type])');
|
|
109
|
+
if (submitBtn) {
|
|
110
|
+
formData.submitButton = {
|
|
111
|
+
selector: getSelector(submitBtn),
|
|
112
|
+
text: submitBtn.textContent?.trim() || submitBtn.value
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
result.forms.push(formData);
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
// Analyze navigation
|
|
119
|
+
if (analysisType === 'full' || analysisType === 'navigation') {
|
|
120
|
+
result.navigation = {
|
|
121
|
+
menus: [],
|
|
122
|
+
breadcrumbs: null,
|
|
123
|
+
pagination: null
|
|
124
|
+
};
|
|
125
|
+
// Find menus
|
|
126
|
+
document.querySelectorAll('nav, [role="navigation"], .nav, .menu, header ul').forEach(nav => {
|
|
127
|
+
const links = [];
|
|
128
|
+
nav.querySelectorAll('a').forEach(a => {
|
|
129
|
+
links.push({
|
|
130
|
+
text: a.textContent?.trim().substring(0, 30),
|
|
131
|
+
href: a.href,
|
|
132
|
+
selector: getSelector(a)
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
if (links.length > 0) {
|
|
136
|
+
result.navigation.menus.push({
|
|
137
|
+
selector: getSelector(nav),
|
|
138
|
+
type: nav.tagName.toLowerCase(),
|
|
139
|
+
links: links.slice(0, 20)
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
// Find breadcrumbs
|
|
144
|
+
const breadcrumb = document.querySelector('[aria-label*="breadcrumb"], .breadcrumb, .breadcrumbs');
|
|
145
|
+
if (breadcrumb) {
|
|
146
|
+
result.navigation.breadcrumbs = {
|
|
147
|
+
selector: getSelector(breadcrumb),
|
|
148
|
+
items: Array.from(breadcrumb.querySelectorAll('a, span')).map(el => el.textContent?.trim()).filter(Boolean)
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
// Find pagination
|
|
152
|
+
const pagination = document.querySelector('.pagination, [aria-label*="pagination"], nav[role="navigation"] a[href*="page"]');
|
|
153
|
+
if (pagination) {
|
|
154
|
+
result.navigation.pagination = {
|
|
155
|
+
selector: getSelector(pagination.closest('nav, .pagination, div')),
|
|
156
|
+
found: true
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
// Analyze main content
|
|
161
|
+
if (analysisType === 'full' || analysisType === 'content') {
|
|
162
|
+
result.content = {
|
|
163
|
+
headings: [],
|
|
164
|
+
paragraphs: 0,
|
|
165
|
+
mainContent: null
|
|
166
|
+
};
|
|
167
|
+
// Headings
|
|
168
|
+
document.querySelectorAll('h1, h2, h3').forEach(h => {
|
|
169
|
+
result.content.headings.push({
|
|
170
|
+
level: parseInt(h.tagName[1]),
|
|
171
|
+
text: h.textContent?.trim().substring(0, 100),
|
|
172
|
+
selector: getSelector(h)
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
// Count paragraphs
|
|
176
|
+
result.content.paragraphs = document.querySelectorAll('p').length;
|
|
177
|
+
// Find main content area
|
|
178
|
+
const main = document.querySelector('main, [role="main"], article, .content, #content');
|
|
179
|
+
if (main) {
|
|
180
|
+
result.content.mainContent = {
|
|
181
|
+
selector: getSelector(main),
|
|
182
|
+
textLength: main.textContent?.length || 0
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
// Analyze media
|
|
187
|
+
if (analysisType === 'full' || analysisType === 'media') {
|
|
188
|
+
result.media = {
|
|
189
|
+
images: [],
|
|
190
|
+
videos: [],
|
|
191
|
+
iframes: []
|
|
192
|
+
};
|
|
193
|
+
// Images
|
|
194
|
+
document.querySelectorAll('img').forEach(img => {
|
|
195
|
+
const rect = img.getBoundingClientRect();
|
|
196
|
+
if (rect.width > 50 && rect.height > 50) {
|
|
197
|
+
result.media.images.push({
|
|
198
|
+
selector: getSelector(img),
|
|
199
|
+
src: img.src?.substring(0, 100),
|
|
200
|
+
alt: img.alt,
|
|
201
|
+
width: Math.round(rect.width),
|
|
202
|
+
height: Math.round(rect.height)
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
// Videos
|
|
207
|
+
document.querySelectorAll('video').forEach(video => {
|
|
208
|
+
result.media.videos.push({
|
|
209
|
+
selector: getSelector(video),
|
|
210
|
+
src: video.src || video.querySelector('source')?.src,
|
|
211
|
+
duration: video.duration,
|
|
212
|
+
controls: video.controls
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
// Iframes (potential embedded content)
|
|
216
|
+
document.querySelectorAll('iframe').forEach(iframe => {
|
|
217
|
+
result.media.iframes.push({
|
|
218
|
+
selector: getSelector(iframe),
|
|
219
|
+
src: iframe.src?.substring(0, 100),
|
|
220
|
+
title: iframe.title
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
// Summary stats
|
|
225
|
+
result.summary = {
|
|
226
|
+
totalButtons: result.interactive?.buttons?.length || 0,
|
|
227
|
+
totalLinks: result.interactive?.links?.length || 0,
|
|
228
|
+
totalInputs: result.interactive?.inputs?.length || 0,
|
|
229
|
+
totalForms: result.forms?.length || 0,
|
|
230
|
+
totalImages: result.media?.images?.length || 0,
|
|
231
|
+
hasNavigation: (result.navigation?.menus?.length || 0) > 0,
|
|
232
|
+
isLoginPage: !!(result.interactive?.inputs?.find(i => i.type === 'password')),
|
|
233
|
+
isSearchPage: !!(result.interactive?.inputs?.find(i => i.type === 'search' || i.name?.includes('search') || i.placeholder?.toLowerCase().includes('search')))
|
|
234
|
+
};
|
|
235
|
+
return result;
|
|
236
|
+
}, { analysisType, includeSelectors, maxDepth });
|
|
237
|
+
// Add screenshot if requested
|
|
238
|
+
if (includeScreenshot) {
|
|
239
|
+
// Playwright/Patchright returns a Buffer; convert to base64 ourselves
|
|
240
|
+
const buf = await page.screenshot({ type: 'jpeg', quality: 50 });
|
|
241
|
+
analysis.screenshot = Buffer.from(buf).toString('base64');
|
|
242
|
+
}
|
|
243
|
+
return analysis;
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Get quick summary of page
|
|
247
|
+
*/
|
|
248
|
+
async quickSummary(page) {
|
|
249
|
+
return await page.evaluate(() => {
|
|
250
|
+
return {
|
|
251
|
+
url: window.location.href,
|
|
252
|
+
title: document.title,
|
|
253
|
+
buttons: document.querySelectorAll('button, [role="button"]').length,
|
|
254
|
+
links: document.querySelectorAll('a[href]').length,
|
|
255
|
+
inputs: document.querySelectorAll('input, textarea').length,
|
|
256
|
+
forms: document.querySelectorAll('form').length,
|
|
257
|
+
images: document.querySelectorAll('img').length,
|
|
258
|
+
hasLogin: !!document.querySelector('input[type="password"]'),
|
|
259
|
+
hasSearch: !!document.querySelector('input[type="search"], [name*="search"], [placeholder*="earch"]')
|
|
260
|
+
};
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
module.exports = PageAnalyzer;
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// @ts-nocheck
|
|
3
|
+
/**
|
|
4
|
+
* AI Selector Healer - Auto-fix broken CSS selectors
|
|
5
|
+
*
|
|
6
|
+
* When a selector stops working (page updated, dynamic content, etc.),
|
|
7
|
+
* this module tries to find alternative selectors that match the same element.
|
|
8
|
+
*/
|
|
9
|
+
class SelectorHealer {
|
|
10
|
+
constructor() {
|
|
11
|
+
this.healingStrategies = [
|
|
12
|
+
'byId',
|
|
13
|
+
'byName',
|
|
14
|
+
'byAriaLabel',
|
|
15
|
+
'byText',
|
|
16
|
+
'byClasses',
|
|
17
|
+
'byAttributes',
|
|
18
|
+
'byStructure',
|
|
19
|
+
'byPosition'
|
|
20
|
+
];
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Heal a broken selector by finding alternatives
|
|
24
|
+
*/
|
|
25
|
+
async heal(page, brokenSelector, options = {}) {
|
|
26
|
+
const { lastKnownText = null, lastKnownAttributes = {}, elementType = null, maxAlternatives = 5 } = options;
|
|
27
|
+
const context = {
|
|
28
|
+
brokenSelector,
|
|
29
|
+
lastKnownText,
|
|
30
|
+
lastKnownAttributes,
|
|
31
|
+
elementType
|
|
32
|
+
};
|
|
33
|
+
// Try to extract info from broken selector
|
|
34
|
+
const selectorInfo = this.parseSelector(brokenSelector);
|
|
35
|
+
// Find alternatives using multiple strategies
|
|
36
|
+
const alternatives = await page.evaluate(({ context, selectorInfo }) => {
|
|
37
|
+
const results = [];
|
|
38
|
+
// Strategy 1: Find by similar ID
|
|
39
|
+
if (selectorInfo.id) {
|
|
40
|
+
const elements = document.querySelectorAll(`[id*="${selectorInfo.id}"]`);
|
|
41
|
+
for (const el of elements) {
|
|
42
|
+
results.push({
|
|
43
|
+
selector: `#${el.id}`,
|
|
44
|
+
confidence: 0.9,
|
|
45
|
+
strategy: 'byId',
|
|
46
|
+
reason: 'Similar ID found'
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
// Strategy 2: Find by name attribute
|
|
51
|
+
if (selectorInfo.name) {
|
|
52
|
+
const elements = document.querySelectorAll(`[name*="${selectorInfo.name}"]`);
|
|
53
|
+
for (const el of elements) {
|
|
54
|
+
results.push({
|
|
55
|
+
selector: `[name="${el.name}"]`,
|
|
56
|
+
confidence: 0.85,
|
|
57
|
+
strategy: 'byName',
|
|
58
|
+
reason: 'Similar name attribute found'
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
// Strategy 3: Find by last known text
|
|
63
|
+
if (context.lastKnownText) {
|
|
64
|
+
const allElements = document.querySelectorAll('button, a, input, label, span, div, [role]');
|
|
65
|
+
for (const el of allElements) {
|
|
66
|
+
const text = el.textContent?.trim();
|
|
67
|
+
if (text && text.toLowerCase().includes(context.lastKnownText.toLowerCase())) {
|
|
68
|
+
let selector = '';
|
|
69
|
+
if (el.id) {
|
|
70
|
+
selector = `#${el.id}`;
|
|
71
|
+
}
|
|
72
|
+
else if (el.className) {
|
|
73
|
+
selector = `${el.tagName.toLowerCase()}.${el.className.split(' ')[0]}`;
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
selector = `${el.tagName.toLowerCase()}`;
|
|
77
|
+
}
|
|
78
|
+
results.push({
|
|
79
|
+
selector,
|
|
80
|
+
confidence: 0.8,
|
|
81
|
+
strategy: 'byText',
|
|
82
|
+
reason: `Contains text: "${text.substring(0, 30)}"`
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
// Strategy 4: Find by similar classes
|
|
88
|
+
if (selectorInfo.classes.length > 0) {
|
|
89
|
+
for (const cls of selectorInfo.classes) {
|
|
90
|
+
const elements = document.querySelectorAll(`[class*="${cls}"]`);
|
|
91
|
+
for (const el of elements) {
|
|
92
|
+
const rect = el.getBoundingClientRect();
|
|
93
|
+
if (rect.width > 0 && rect.height > 0) {
|
|
94
|
+
results.push({
|
|
95
|
+
selector: `.${cls}`,
|
|
96
|
+
confidence: 0.7,
|
|
97
|
+
strategy: 'byClasses',
|
|
98
|
+
reason: `Similar class: ${cls}`
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
// Strategy 5: Find by tag and type
|
|
105
|
+
if (selectorInfo.tag) {
|
|
106
|
+
let selector = selectorInfo.tag;
|
|
107
|
+
if (selectorInfo.type) {
|
|
108
|
+
selector += `[type="${selectorInfo.type}"]`;
|
|
109
|
+
}
|
|
110
|
+
const elements = document.querySelectorAll(selector);
|
|
111
|
+
for (const el of elements) {
|
|
112
|
+
const rect = el.getBoundingClientRect();
|
|
113
|
+
if (rect.width > 0 && rect.height > 0) {
|
|
114
|
+
let uniqueSelector = selector;
|
|
115
|
+
if (el.placeholder) {
|
|
116
|
+
uniqueSelector += `[placeholder="${el.placeholder}"]`;
|
|
117
|
+
}
|
|
118
|
+
else if (el.value) {
|
|
119
|
+
uniqueSelector += `[value="${el.value}"]`;
|
|
120
|
+
}
|
|
121
|
+
results.push({
|
|
122
|
+
selector: uniqueSelector,
|
|
123
|
+
confidence: 0.6,
|
|
124
|
+
strategy: 'byAttributes',
|
|
125
|
+
reason: `Similar element structure`
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
// Strategy 6: Find by aria-label
|
|
131
|
+
if (context.lastKnownAttributes?.['aria-label']) {
|
|
132
|
+
const label = context.lastKnownAttributes['aria-label'];
|
|
133
|
+
const elements = document.querySelectorAll(`[aria-label*="${label}"]`);
|
|
134
|
+
for (const el of elements) {
|
|
135
|
+
results.push({
|
|
136
|
+
selector: `[aria-label="${el.getAttribute('aria-label')}"]`,
|
|
137
|
+
confidence: 0.85,
|
|
138
|
+
strategy: 'byAriaLabel',
|
|
139
|
+
reason: 'Similar aria-label found'
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
// Deduplicate
|
|
144
|
+
const seen = new Set();
|
|
145
|
+
return results.filter(r => {
|
|
146
|
+
if (seen.has(r.selector))
|
|
147
|
+
return false;
|
|
148
|
+
seen.add(r.selector);
|
|
149
|
+
return true;
|
|
150
|
+
});
|
|
151
|
+
}, { context, selectorInfo });
|
|
152
|
+
// Sort by confidence and return top alternatives
|
|
153
|
+
return alternatives
|
|
154
|
+
.sort((a, b) => b.confidence - a.confidence)
|
|
155
|
+
.slice(0, maxAlternatives);
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Parse a CSS selector to extract components
|
|
159
|
+
*/
|
|
160
|
+
parseSelector(selector) {
|
|
161
|
+
const info = {
|
|
162
|
+
tag: null,
|
|
163
|
+
id: null,
|
|
164
|
+
classes: [],
|
|
165
|
+
name: null,
|
|
166
|
+
type: null,
|
|
167
|
+
attributes: {}
|
|
168
|
+
};
|
|
169
|
+
// Extract ID
|
|
170
|
+
const idMatch = selector.match(/#([a-zA-Z0-9_-]+)/);
|
|
171
|
+
if (idMatch)
|
|
172
|
+
info.id = idMatch[1];
|
|
173
|
+
// Extract classes
|
|
174
|
+
const classMatches = selector.matchAll(/\.([a-zA-Z0-9_-]+)/g);
|
|
175
|
+
for (const match of classMatches) {
|
|
176
|
+
info.classes.push(match[1]);
|
|
177
|
+
}
|
|
178
|
+
// Extract tag
|
|
179
|
+
const tagMatch = selector.match(/^([a-zA-Z]+)/);
|
|
180
|
+
if (tagMatch)
|
|
181
|
+
info.tag = tagMatch[1];
|
|
182
|
+
// Extract attributes
|
|
183
|
+
const attrMatches = selector.matchAll(/\[([a-zA-Z-]+)(?:=["']?([^"'\]]+)["']?)?\]/g);
|
|
184
|
+
for (const match of attrMatches) {
|
|
185
|
+
const key = match[1];
|
|
186
|
+
const value = match[2] || true;
|
|
187
|
+
info.attributes[key] = value;
|
|
188
|
+
if (key === 'name')
|
|
189
|
+
info.name = value;
|
|
190
|
+
if (key === 'type')
|
|
191
|
+
info.type = value;
|
|
192
|
+
}
|
|
193
|
+
return info;
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Test if a selector works
|
|
197
|
+
*/
|
|
198
|
+
async testSelector(page, selector) {
|
|
199
|
+
try {
|
|
200
|
+
const element = await page.$(selector);
|
|
201
|
+
if (!element)
|
|
202
|
+
return { valid: false };
|
|
203
|
+
const info = await element.evaluate(el => ({
|
|
204
|
+
visible: el.offsetWidth > 0 && el.offsetHeight > 0,
|
|
205
|
+
tag: el.tagName.toLowerCase(),
|
|
206
|
+
text: el.textContent?.substring(0, 50)
|
|
207
|
+
}));
|
|
208
|
+
return { valid: true, ...info };
|
|
209
|
+
}
|
|
210
|
+
catch {
|
|
211
|
+
return { valid: false };
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
module.exports = SelectorHealer;
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
// @ts-nocheck
|
|
4
|
+
/**
|
|
5
|
+
* Real Browser MCP Server
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* node src/index.js - Start MCP Server (default)
|
|
9
|
+
* node src/index.js --help - Show help
|
|
10
|
+
* node src/index.js --list - List all available tools
|
|
11
|
+
*/
|
|
12
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
|
+
const { TOOLS, TOOL_DISPLAY, CATEGORIES } = require('./shared/tools');
|
|
14
|
+
// ANSI colors for terminal
|
|
15
|
+
const colors = {
|
|
16
|
+
reset: '\x1b[0m',
|
|
17
|
+
bright: '\x1b[1m',
|
|
18
|
+
dim: '\x1b[2m',
|
|
19
|
+
green: '\x1b[32m',
|
|
20
|
+
yellow: '\x1b[33m',
|
|
21
|
+
blue: '\x1b[34m',
|
|
22
|
+
magenta: '\x1b[35m',
|
|
23
|
+
cyan: '\x1b[36m',
|
|
24
|
+
red: '\x1b[31m',
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Display help message
|
|
28
|
+
*/
|
|
29
|
+
function showHelp() {
|
|
30
|
+
console.log(`
|
|
31
|
+
${colors.bright}${colors.cyan}🦁 Real Browser MCP Server${colors.reset}
|
|
32
|
+
|
|
33
|
+
${colors.bright}USAGE:${colors.reset}
|
|
34
|
+
node src/index.js [options]
|
|
35
|
+
|
|
36
|
+
${colors.bright}OPTIONS:${colors.reset}
|
|
37
|
+
${colors.yellow}--help, -h${colors.reset} Show this help message
|
|
38
|
+
${colors.yellow}--verbose, -v${colors.reset} Show detailed tool information
|
|
39
|
+
${colors.yellow}--list${colors.reset} List all available tools
|
|
40
|
+
|
|
41
|
+
${colors.bright}EXAMPLES:${colors.reset}
|
|
42
|
+
node src/index.js # Start MCP server
|
|
43
|
+
node src/index.js --list # List all tools
|
|
44
|
+
|
|
45
|
+
${colors.bright}NPM SCRIPTS:${colors.reset}
|
|
46
|
+
npm run dev # Start MCP server
|
|
47
|
+
npm run mcp # Start MCP server only
|
|
48
|
+
|
|
49
|
+
${colors.bright}ARCHITECTURE:${colors.reset}
|
|
50
|
+
${colors.cyan}MCP Server${colors.reset} → STDIO transport → AI Agents (Claude, Cursor, Copilot)
|
|
51
|
+
|
|
52
|
+
${colors.bright}TOOL CATEGORIES (${TOOLS.length} tools):${colors.reset}
|
|
53
|
+
${Object.entries(CATEGORIES).map(([key, cat]) => {
|
|
54
|
+
const count = TOOLS.filter(t => t.category === key).length;
|
|
55
|
+
return ` ${cat.emoji} ${colors.yellow}${cat.name.padEnd(15)}${colors.reset} ${colors.dim}(${count} tools)${colors.reset}`;
|
|
56
|
+
}).join('\n')}
|
|
57
|
+
`);
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* List all available tools
|
|
61
|
+
*/
|
|
62
|
+
function listTools() {
|
|
63
|
+
console.log(`\n${colors.bright}${colors.cyan}🦁 Available Tools (${TOOLS.length}):${colors.reset}\n`);
|
|
64
|
+
for (const [key, cat] of Object.entries(CATEGORIES)) {
|
|
65
|
+
const tools = TOOLS.filter(t => t.category === key);
|
|
66
|
+
if (tools.length === 0)
|
|
67
|
+
continue;
|
|
68
|
+
console.log(`${colors.bright}${cat.emoji} ${cat.name}${colors.reset} ${colors.dim}(${tools.length})${colors.reset}`);
|
|
69
|
+
console.log(`${colors.dim}${'─'.repeat(50)}${colors.reset}`);
|
|
70
|
+
for (const tool of tools) {
|
|
71
|
+
console.log(` ${tool.emoji} ${colors.yellow}${tool.name.padEnd(25)}${colors.reset} ${colors.dim}${tool.description.substring(0, 40)}${colors.reset}`);
|
|
72
|
+
}
|
|
73
|
+
console.log('');
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Main entry point
|
|
78
|
+
*/
|
|
79
|
+
async function main() {
|
|
80
|
+
const args = process.argv.slice(2);
|
|
81
|
+
// Parse arguments
|
|
82
|
+
const hasHelp = args.includes('--help') || args.includes('-h');
|
|
83
|
+
const hasList = args.includes('--list');
|
|
84
|
+
if (hasHelp) {
|
|
85
|
+
showHelp();
|
|
86
|
+
process.exit(0);
|
|
87
|
+
}
|
|
88
|
+
if (hasList) {
|
|
89
|
+
listTools();
|
|
90
|
+
process.exit(0);
|
|
91
|
+
}
|
|
92
|
+
console.error('');
|
|
93
|
+
console.error(`${colors.bright}${colors.cyan}╔════════════════════════════════════════════════════════════╗${colors.reset}`);
|
|
94
|
+
console.error(`${colors.bright}${colors.cyan}║${colors.reset} ${colors.bright}${colors.magenta}🦁 Real Browser MCP Server${colors.reset} ${colors.cyan}║${colors.reset}`);
|
|
95
|
+
console.error(`${colors.bright}${colors.cyan}║${colors.reset} ${colors.dim}MCP (AI Agents) Server running on STDIO${colors.reset} ${colors.cyan}║${colors.reset}`);
|
|
96
|
+
console.error(`${colors.bright}${colors.cyan}╚════════════════════════════════════════════════════════════╝${colors.reset}`);
|
|
97
|
+
console.error('');
|
|
98
|
+
console.error(`${colors.bright}${colors.blue}🚀 Starting MCP Server...${colors.reset}`);
|
|
99
|
+
// Import and run MCP server
|
|
100
|
+
require('./mcp/index');
|
|
101
|
+
}
|
|
102
|
+
// Export for programmatic use
|
|
103
|
+
module.exports = {
|
|
104
|
+
TOOLS,
|
|
105
|
+
TOOL_DISPLAY,
|
|
106
|
+
CATEGORIES,
|
|
107
|
+
startMCP: () => require('./mcp/index'),
|
|
108
|
+
startBoth: () => require('./mcp/index'),
|
|
109
|
+
};
|
|
110
|
+
// Run if called directly
|
|
111
|
+
if (require.main === module) {
|
|
112
|
+
main().catch(error => {
|
|
113
|
+
console.error(`${colors.red}❌ Fatal error:${colors.reset}`, error.message);
|
|
114
|
+
process.exit(1);
|
|
115
|
+
});
|
|
116
|
+
}
|