feat: MCP Browser v3.1.1 - 56+ tools, visual overlays, fixes
- Added 9 new tools: visual overlay, highlight, toast, drag-drop, etc. - Fixed evaluateJS to support function arguments - Fixed hover for non-visible elements - Fixed storage operations with try/catch - Added 10 new wait tools: clickable, element visible, text - Fixed tool name mapping in server.js for MCP protocol - Updated README with 60+ tools documentation - Version bump to 3.1.1
This commit is contained in:
@@ -0,0 +1,45 @@
|
||||
const browser = require('../browser');
|
||||
|
||||
module.exports = async ({ fullPage = true, path: customPath = null, highlight = null }) => {
|
||||
await browser.start();
|
||||
const p = await browser.ensurePage();
|
||||
|
||||
if (highlight) {
|
||||
const selectors = Array.isArray(highlight) ? highlight : [highlight];
|
||||
await p.evaluate((sels) => {
|
||||
sels.forEach((sel, i) => {
|
||||
const el = document.querySelector(sel);
|
||||
if (!el) return;
|
||||
const rect = el.getBoundingClientRect();
|
||||
const marker = document.createElement('div');
|
||||
marker.id = 'mcp-marker-' + i;
|
||||
marker.style.cssText = `
|
||||
position: fixed;
|
||||
left: ${rect.left}px;
|
||||
top: ${rect.top}px;
|
||||
width: ${rect.width}px;
|
||||
height: ${rect.height}px;
|
||||
border: 2px dashed #ff6b6b;
|
||||
background: rgba(255, 107, 107, 0.1);
|
||||
pointer-events: none;
|
||||
z-index: 999998;
|
||||
`;
|
||||
marker.setAttribute('data-selector', sel);
|
||||
document.body.appendChild(marker);
|
||||
});
|
||||
}, selectors);
|
||||
}
|
||||
|
||||
const file = await browser.screenshot({ fullPage, path: customPath });
|
||||
|
||||
if (highlight) {
|
||||
await p.evaluate((sels) => {
|
||||
sels.forEach((sel, i) => {
|
||||
const marker = document.getElementById('mcp-marker-' + i);
|
||||
if (marker) marker.remove();
|
||||
});
|
||||
}, Array.isArray(highlight) ? highlight : [highlight]);
|
||||
}
|
||||
|
||||
return { success: true, action: 'annotated_screenshot', file, highlighted: highlight };
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
const browser = require('../browser');
|
||||
|
||||
module.exports = async ({ sourceSelector, targetSelector }) => {
|
||||
if (!sourceSelector || !targetSelector) {
|
||||
throw new Error('sourceSelector e targetSelector são obrigatórios');
|
||||
}
|
||||
await browser.start();
|
||||
|
||||
const result = await browser.evaluateJS(`
|
||||
(function() {
|
||||
const source = document.querySelector('${sourceSelector}');
|
||||
const target = document.querySelector('${targetSelector}');
|
||||
if (!source || !target) return { success: false, error: 'Element not found' };
|
||||
|
||||
const sourceRect = source.getBoundingClientRect();
|
||||
const targetRect = target.getBoundingClientRect();
|
||||
|
||||
source.dispatchEvent(new DragEvent('dragstart', { bubbles: true }));
|
||||
target.dispatchEvent(new DragEvent('dragover', { bubbles: true }));
|
||||
target.dispatchEvent(new DragEvent('drop', { bubbles: true }));
|
||||
source.dispatchEvent(new DragEvent('dragend', { bubbles: true }));
|
||||
|
||||
return { success: true, sourceRect, targetRect };
|
||||
})()
|
||||
`);
|
||||
|
||||
return { success: true, action: 'drag_and_drop', ...result };
|
||||
};
|
||||
@@ -0,0 +1,154 @@
|
||||
const browser = require('../browser');
|
||||
|
||||
module.exports = async () => {
|
||||
await browser.start();
|
||||
|
||||
const p = await browser.ensurePage();
|
||||
|
||||
await p.addInitScript(`
|
||||
(function() {
|
||||
if (document.getElementById('mcp-cursor-overlay')) return;
|
||||
|
||||
const style = document.createElement('style');
|
||||
style.textContent = \`
|
||||
#mcp-cursor-overlay {
|
||||
all: initial;
|
||||
pointer-events: none;
|
||||
z-index: 999998;
|
||||
}
|
||||
#mcp-custom-cursor {
|
||||
position: fixed;
|
||||
pointer-events: none;
|
||||
z-index: 999999;
|
||||
display: none;
|
||||
filter: drop-shadow(2px 2px 2px rgba(0,0,0,0.3));
|
||||
transition: transform 0.05s ease;
|
||||
}
|
||||
#mcp-scroll-indicator {
|
||||
position: fixed;
|
||||
bottom: 20px;
|
||||
right: 20px;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border-radius: 50%;
|
||||
background: rgba(59, 130, 246, 0.9);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: white;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
#mcp-scroll-progress {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 3px;
|
||||
background: linear-gradient(90deg, #3B82F6, #8B5CF6);
|
||||
z-index: 999999;
|
||||
transition: width 0.1s ease;
|
||||
box-shadow: 0 2px 4px rgba(59, 130, 246, 0.3);
|
||||
}
|
||||
#mcp-action-toast {
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
padding: 12px 20px;
|
||||
background: rgba(16, 185, 129, 0.95);
|
||||
color: white;
|
||||
border-radius: 8px;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
z-index: 999999;
|
||||
transform: translateX(120%);
|
||||
transition: transform 0.3s ease, opacity 0.3s ease;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
|
||||
opacity: 1;
|
||||
}
|
||||
@media (max-width: 768px) {
|
||||
#mcp-scroll-indicator {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
font-size: 10px;
|
||||
bottom: 10px;
|
||||
right: 10px;
|
||||
}
|
||||
#mcp-action-toast {
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
left: 10px;
|
||||
font-size: 13px;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
@media (max-width: 480px) {
|
||||
#mcp-scroll-indicator {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
font-size: 9px;
|
||||
bottom: 8px;
|
||||
right: 8px;
|
||||
}
|
||||
}
|
||||
\`;
|
||||
document.head.appendChild(style);
|
||||
|
||||
const cursor = document.createElement('div');
|
||||
cursor.id = 'mcp-cursor-overlay';
|
||||
cursor.innerHTML = \`
|
||||
<svg id="mcp-custom-cursor" width="32" height="32" viewBox="0 0 32 32">
|
||||
<path d="M4 2L28 18L18 20L14 28L10 26L12 18L4 2Z" fill="#3B82F6" stroke="white" stroke-width="1.5"/>
|
||||
</svg>
|
||||
<div id="mcp-scroll-indicator">0%</div>
|
||||
<div id="mcp-scroll-progress"></div>
|
||||
<div id="mcp-action-toast">Action completed</div>
|
||||
\`;
|
||||
document.body.appendChild(cursor);
|
||||
|
||||
let cursorTimeout;
|
||||
document.addEventListener('mousemove', (e) => {
|
||||
const cursorEl = document.getElementById('mcp-custom-cursor');
|
||||
cursorEl.style.display = 'block';
|
||||
cursorEl.style.left = (e.clientX - 4) + 'px';
|
||||
cursorEl.style.top = (e.clientY - 4) + 'px';
|
||||
|
||||
clearTimeout(cursorTimeout);
|
||||
cursorTimeout = setTimeout(() => {
|
||||
cursorEl.style.display = 'none';
|
||||
}, 2000);
|
||||
});
|
||||
|
||||
let scrollTimeout;
|
||||
document.addEventListener('scroll', () => {
|
||||
const scrollTop = window.scrollY;
|
||||
const docHeight = document.documentElement.scrollHeight - window.innerHeight;
|
||||
const scrollPercent = docHeight > 0 ? Math.round((scrollTop / docHeight) * 100) : 0;
|
||||
|
||||
const indicator = document.getElementById('mcp-scroll-indicator');
|
||||
const progress = document.getElementById('mcp-scroll-progress');
|
||||
|
||||
indicator.textContent = scrollPercent + '%';
|
||||
progress.style.width = scrollPercent + '%';
|
||||
|
||||
if (scrollPercent > 0 && scrollPercent < 100) {
|
||||
indicator.style.display = 'flex';
|
||||
}
|
||||
|
||||
clearTimeout(scrollTimeout);
|
||||
scrollTimeout = setTimeout(() => {
|
||||
indicator.style.opacity = '0.5';
|
||||
}, 2000);
|
||||
});
|
||||
})();
|
||||
`);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
action: 'enable_visual_overlay',
|
||||
message: 'Cursor customizado e indicadores de rolagem ativados (responsivo)'
|
||||
};
|
||||
};
|
||||
@@ -3,10 +3,12 @@ const browser = require('../browser');
|
||||
module.exports = async ({ expression, selector = null }) => {
|
||||
if (!expression) throw new Error('Expression é obrigatória');
|
||||
await browser.start();
|
||||
|
||||
if (selector) {
|
||||
const result = await browser.evaluateOnSelector(selector, expression);
|
||||
return { success: true, action: 'evaluate_js', selector, result };
|
||||
}
|
||||
|
||||
const result = await browser.evaluateJS(expression);
|
||||
return { success: true, action: 'evaluate_js', result };
|
||||
};
|
||||
|
||||
+32
-24
@@ -3,29 +3,37 @@ const browser = require('../browser');
|
||||
module.exports = async ({ data }) => {
|
||||
if (!data) throw new Error('Data é obrigatória');
|
||||
await browser.start();
|
||||
const result = await browser.evaluateJS((formData) => {
|
||||
const inputs = Array.from(document.querySelectorAll('input, textarea, select'));
|
||||
const filled = [];
|
||||
inputs.forEach(input => {
|
||||
const key = Object.keys(formData).find(k =>
|
||||
(input.name && input.name.toLowerCase().includes(k.toLowerCase())) ||
|
||||
(input.placeholder || '').toLowerCase().includes(k.toLowerCase()) ||
|
||||
(input.id || '').toLowerCase().includes(k.toLowerCase()) ||
|
||||
(input.getAttribute('aria-label') || '').toLowerCase().includes(k.toLowerCase())
|
||||
);
|
||||
if (key) {
|
||||
const value = formData[key];
|
||||
const nativeSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value')?.set;
|
||||
const nativeTextAreaSetter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value')?.set;
|
||||
if (input.tagName === 'INPUT' && nativeSetter) nativeSetter.call(input, value);
|
||||
else if (input.tagName === 'TEXTAREA' && nativeTextAreaSetter) nativeTextAreaSetter.call(input, value);
|
||||
else input.value = value;
|
||||
input.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
input.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
filled.push({ field: input.name || input.id || input.placeholder || 'unknown', value });
|
||||
|
||||
const formDataJson = JSON.stringify(data).replace(/'/g, "\\'");
|
||||
|
||||
const result = await browser.evaluateJS(`
|
||||
(function() {
|
||||
try {
|
||||
var formData = JSON.parse('${formDataJson}');
|
||||
var inputs = Array.from(document.querySelectorAll('input, textarea, select'));
|
||||
var filled = [];
|
||||
inputs.forEach(function(input) {
|
||||
var keys = Object.keys(formData);
|
||||
for (var i = 0; i < keys.length; i++) {
|
||||
var k = keys[i];
|
||||
var match = input.name && input.name.toLowerCase().indexOf(k.toLowerCase()) >= 0 ||
|
||||
input.placeholder && input.placeholder.toLowerCase().indexOf(k.toLowerCase()) >= 0 ||
|
||||
input.id && input.id.toLowerCase().indexOf(k.toLowerCase()) >= 0;
|
||||
if (match) {
|
||||
input.value = formData[k];
|
||||
input.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
input.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
filled.push({ field: input.name || input.id || 'unknown', value: formData[k] });
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
return filled;
|
||||
} catch (e) {
|
||||
return { error: e.message };
|
||||
}
|
||||
});
|
||||
return filled;
|
||||
}, data);
|
||||
return { success: true, action: 'fill_form_auto', filled: result, count: result.length };
|
||||
})()
|
||||
`);
|
||||
|
||||
return { success: true, action: 'fill_form_auto', filled: result, count: Array.isArray(result) ? result.length : 0 };
|
||||
};
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
const browser = require('../browser');
|
||||
|
||||
module.exports = async ({ selector = 'input, textarea, select' }) => {
|
||||
await browser.start();
|
||||
const inputs = await browser.evaluateJS(`
|
||||
(function() {
|
||||
return Array.from(document.querySelectorAll('${selector}')).map(function(el) {
|
||||
return {
|
||||
tag: el.tagName.toLowerCase(),
|
||||
id: el.id || null,
|
||||
name: el.name || null,
|
||||
type: el.type || null,
|
||||
placeholder: el.placeholder || null,
|
||||
value: el.value || '',
|
||||
checked: el.checked || null,
|
||||
required: el.required || false,
|
||||
disabled: el.disabled || false
|
||||
};
|
||||
});
|
||||
})()
|
||||
`);
|
||||
return { success: true, action: 'get_input_values', count: inputs.length, inputs };
|
||||
};
|
||||
+2
-2
@@ -1,8 +1,8 @@
|
||||
const browser = require('../browser');
|
||||
|
||||
module.exports = async ({ selector = 'a' }) => {
|
||||
await browser.start();
|
||||
const links = await browser.evaluateJS((sel) => {
|
||||
const p = await browser.ensurePage();
|
||||
const links = await p.evaluate((sel) => {
|
||||
return Array.from(document.querySelectorAll(sel)).map(el => ({
|
||||
text: (el.innerText || el.textContent || '').trim(),
|
||||
href: el.href || el.getAttribute('href') || ''
|
||||
|
||||
+4
-4
@@ -1,9 +1,9 @@
|
||||
const browser = require('../browser');
|
||||
|
||||
module.exports = async ({ selector = 'body' }) => {
|
||||
await browser.start();
|
||||
const text = await browser.evaluateJS((sel) => {
|
||||
const el = document.querySelector(sel);
|
||||
module.exports = async function({ selector = 'body' }) {
|
||||
const p = await browser.ensurePage();
|
||||
const text = await p.evaluate(function(s) {
|
||||
var el = document.querySelector(s);
|
||||
if (!el) return null;
|
||||
return (el.innerText || el.textContent || '').trim();
|
||||
}, selector);
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
const browser = require('../browser');
|
||||
|
||||
module.exports = async ({ selector, color = '#ff6b6b', duration = 500 }) => {
|
||||
if (!selector) throw new Error('Selector é obrigatório');
|
||||
await browser.start();
|
||||
|
||||
const p = await browser.ensurePage();
|
||||
|
||||
const result = await p.evaluate(({ sel, clr, dur }) => {
|
||||
const el = document.querySelector(sel);
|
||||
if (!el) return { success: false, error: 'Element not found' };
|
||||
|
||||
const rect = el.getBoundingClientRect();
|
||||
const highlight = document.createElement('div');
|
||||
highlight.id = 'mcp-highlight-' + Date.now();
|
||||
highlight.style.cssText = `
|
||||
position: fixed;
|
||||
left: ${rect.left}px;
|
||||
top: ${rect.top}px;
|
||||
width: ${rect.width}px;
|
||||
height: ${rect.height}px;
|
||||
border: 3px solid ${clr};
|
||||
border-radius: 4px;
|
||||
pointer-events: none;
|
||||
z-index: 999999;
|
||||
box-shadow: 0 0 10px ${clr};
|
||||
animation: mcp-pulse ${dur}ms ease-out;
|
||||
`;
|
||||
|
||||
const style = document.createElement('style');
|
||||
style.textContent = '@keyframes mcp-pulse { 0% { opacity: 1; transform: scale(1); } 100% { opacity: 0; transform: scale(1.1); } }';
|
||||
document.head.appendChild(style);
|
||||
document.body.appendChild(highlight);
|
||||
|
||||
setTimeout(() => highlight.remove(), dur);
|
||||
return { success: true };
|
||||
}, { sel: selector, clr: color, dur: duration });
|
||||
|
||||
if (!result.success) throw new Error(result.error);
|
||||
return { success: true, action: 'highlight_element', selector, color };
|
||||
};
|
||||
@@ -0,0 +1,41 @@
|
||||
const browser = require('../browser');
|
||||
|
||||
module.exports = async ({ message = 'Action', duration = 2000 }) => {
|
||||
await browser.start();
|
||||
|
||||
const p = await browser.ensurePage();
|
||||
|
||||
await p.evaluate(({ msg, dur }) => {
|
||||
let toast = document.getElementById('mcp-action-toast');
|
||||
if (!toast) {
|
||||
const toastEl = document.createElement('div');
|
||||
toastEl.id = 'mcp-action-toast';
|
||||
toastEl.style.cssText = `
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
padding: 12px 20px;
|
||||
background: rgba(16, 185, 129, 0.95);
|
||||
color: white;
|
||||
border-radius: 8px;
|
||||
font-family: -apple-system, BlinkMacSystemFont, sans-serif;
|
||||
font-size: 14px;
|
||||
z-index: 999999;
|
||||
transform: translateX(120%);
|
||||
transition: transform 0.3s ease;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.2);
|
||||
`;
|
||||
document.body.appendChild(toastEl);
|
||||
toast = toastEl;
|
||||
}
|
||||
|
||||
toast.textContent = msg;
|
||||
toast.style.transform = 'translateX(0)';
|
||||
|
||||
setTimeout(() => {
|
||||
toast.style.transform = 'translateX(120%)';
|
||||
}, dur);
|
||||
}, { msg: message, dur: duration });
|
||||
|
||||
return { success: true, action: 'show_toast', message };
|
||||
};
|
||||
@@ -0,0 +1,23 @@
|
||||
const browser = require('../browser');
|
||||
|
||||
module.exports = async ({ selector, timeout = 15000 }) => {
|
||||
if (!selector) throw new Error('Selector é obrigatório');
|
||||
await browser.start();
|
||||
|
||||
const p = await browser.ensurePage();
|
||||
await p.waitForSelector(selector, { state: 'visible', timeout });
|
||||
|
||||
const result = await p.evaluate((sel) => {
|
||||
const el = document.querySelector(sel);
|
||||
if (!el) return { clickable: false, reason: 'not found' };
|
||||
|
||||
const rect = el.getBoundingClientRect();
|
||||
const isVisible = rect.width > 0 && rect.height > 0;
|
||||
const isEnabled = !el.disabled;
|
||||
const isClickable = isVisible && isEnabled && window.getComputedStyle(el).pointerEvents !== 'none';
|
||||
|
||||
return { clickable: isClickable, visible: isVisible, enabled: isEnabled, rect };
|
||||
}, selector);
|
||||
|
||||
return { success: true, action: 'wait_for_clickable', selector, ...result };
|
||||
};
|
||||
@@ -0,0 +1,11 @@
|
||||
const browser = require('../browser');
|
||||
|
||||
module.exports = async ({ selector, timeout = 15000, state = 'visible' }) => {
|
||||
if (!selector) throw new Error('Selector é obrigatório');
|
||||
await browser.start();
|
||||
|
||||
const p = await browser.ensurePage();
|
||||
await p.waitForSelector(selector, { state, timeout });
|
||||
|
||||
return { success: true, action: 'wait_for_element_visible', selector, state };
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
const browser = require('../browser');
|
||||
|
||||
module.exports = async ({ text, timeout = 15000 }) => {
|
||||
if (!text) throw new Error('Texto é obrigatório');
|
||||
await browser.start();
|
||||
|
||||
const p = await browser.ensurePage();
|
||||
await p.waitForFunction((t) => {
|
||||
return document.body.innerText.toLowerCase().includes(t.toLowerCase());
|
||||
}, text, { timeout });
|
||||
|
||||
const found = await p.evaluate((t) => {
|
||||
const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT);
|
||||
let node;
|
||||
while (node = walker.nextNode()) {
|
||||
if (node.textContent.toLowerCase().includes(t.toLowerCase())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}, text);
|
||||
|
||||
return { success: true, action: 'wait_for_text', text, found };
|
||||
};
|
||||
Reference in New Issue
Block a user