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:
2026-04-07 16:29:38 -03:00
parent 13f7392555
commit d8796531ee
17 changed files with 675 additions and 77 deletions

108
README.md
View File

@@ -1,21 +1,18 @@
# MCP-Browser v3.0.0 # MCP-Browser v3.1.1
MCP server para automação de navegador de alta qualidade usando [Playwright](https://playwright.dev/) com **42 tools** para automação avançada. MCP server para automação de navegador de alta qualidade usando [Playwright](https://playwright.dev/) com **60+ tools** para automação avançada.
![Node](https://img.shields.io/badge/Node.js-18+-339933?logo=node.js&logoColor=white) ![Node](https://img.shields.io/badge/Node.js-18+-339933?logo=node.js&logoColor=white)
![MCP](https://img.shields.io/badge/MCP-stdio-blue) ![MCP](https://img.shields.io/badge/MCP-stdio-blue)
![Platform](https://img.shields.io/badge/Windows%20|%20Linux%20|%20macOS-lightgrey) ![Platform](https://img.shields.io/badge/Windows%20|%20Linux%20|%20macOS-lightgrey)
![Tools](https://img.shields.io/badge/42%20tools-green) ![Tools](https://img.shields.io/badge/60%2B%20tools-green)
## Novidades v3.0 ## Novidades v3.1.1
- **Correções de bugs** - hover, storage, evaluateJS com argumentos
- **Melhor responsividade** - Visual overlay adaptável para mobile/tablet
- **Stealth Mode** - Anti-detecção avançada - **Stealth Mode** - Anti-detecção avançada
- **25 novas tools** - Muito mais poder - **60+ ferramentas** - Cobertura completa de automação
- **Multi-Tab real** - Gerenciamento completo
- **PDF generation** - Geração de PDFs
- **Network Monitoring** - Logs de rede
- **Device Emulation** - iPhone, Pixel, etc
- **Performance Metrics** - Métricas detalhadas
## Instalação ## Instalação
@@ -30,7 +27,7 @@ npx playwright install chromium
node server.js node server.js
``` ```
## Tools Disponíveis (42) ## Tools Disponíveis (60+)
### Navegação ### Navegação
| Tool | Descrição | | Tool | Descrição |
@@ -55,7 +52,8 @@ node server.js
| `hover` | Faz hover | | `hover` | Faz hover |
| `select` | Seleciona opção | | `select` | Seleciona opção |
| `press_key` | Pressiona tecla | | `press_key` | Pressiona tecla |
| `scroll_to` | Rola página | | `scroll_to` | Rola página (top/bottom/selector/px) |
| `drag_and_drop` | Arrasta e solta elemento |
| `upload_file` | Upload de arquivo | | `upload_file` | Upload de arquivo |
### Extração ### Extração
@@ -67,6 +65,7 @@ node server.js
| `get_buttons` | Lista botões | | `get_buttons` | Lista botões |
| `get_forms` | Lista formulários | | `get_forms` | Lista formulários |
| `extract_elements` | Extrai elementos | | `extract_elements` | Extrai elementos |
| `get_input_values` | Obtém valores de inputs |
| `get_url` | URL atual | | `get_url` | URL atual |
| `get_title` | Título da página | | `get_title` | Título da página |
| `evaluate_js` | Executa JavaScript | | `evaluate_js` | Executa JavaScript |
@@ -75,8 +74,16 @@ node server.js
| Tool | Descrição | | Tool | Descrição |
|------|-----------| |------|-----------|
| `screenshot` | Screenshot (full/elemento) | | `screenshot` | Screenshot (full/elemento) |
| `annotated_screenshot` | Screenshot com elementos highlights |
| `pdf` | Gera PDF | | `pdf` | Gera PDF |
### Visuais & Overlay
| Tool | Descrição |
|------|-----------|
| `enable_visual_overlay` | Ativa cursor customizado + indicadores |
| `highlight_element` | Destaca elemento com animação |
| `show_toast` | Notificação toast na página |
### Viewport & Device ### Viewport & Device
| Tool | Descrição | | Tool | Descrição |
|------|-----------| |------|-----------|
@@ -109,6 +116,9 @@ node server.js
|------|-----------| |------|-----------|
| `wait` | Aguarda ms | | `wait` | Aguarda ms |
| `wait_for_selector` | Espera elemento | | `wait_for_selector` | Espera elemento |
| `wait_for_element_visible` | Espera elemento visível |
| `wait_for_clickable` | Espera elemento clicável |
| `wait_for_text` | Espera texto aparecer |
### Automação ### Automação
| Tool | Descrição | | Tool | Descrição |
@@ -133,13 +143,16 @@ fill_form_auto({"email": "user@test.com", "password": "123"})
smart_click("Entrar") smart_click("Entrar")
wait_for_url("**dashboard**") wait_for_url("**dashboard**")
// Destacar elemento
highlight_element("button.submit")
// Screenshot com anotações
annotated_screenshot({highlight: [".product-card", ".buy-button"]})
// Extrair dados // Extrair dados
get_links() get_links()
get_text(".product-title") get_text(".product-title")
// Screenshot
screenshot({fullPage: true})
// Emular mobile // Emular mobile
emulate_device("iphone-14") emulate_device("iphone-14")
``` ```
@@ -158,9 +171,24 @@ MCP-Browser/
├── server.js # Servidor MCP ├── server.js # Servidor MCP
├── browser.js # Engine Playwright ├── browser.js # Engine Playwright
├── package.json ├── package.json
└── tools/ # 42 tools └── tools/ # 60+ tools
├── openUrl.js ├── openUrl.js
├── click.js ├── click.js
├── type.js
├── screenshot.js
├── getText.js
├── getLinks.js
├── waitForSelector.js
├── extractElements.js
├── smartClick.js
├── smartType.js
├── getButtons.js
├── smartWaitNavigation.js
├── getForms.js
├── fillFormAuto.js
├── agentFlow.js
├── agentFlowV2.js
├── closeBrowser.js
├── getURL.js ├── getURL.js
├── getTitle.js ├── getTitle.js
├── getHTML.js ├── getHTML.js
@@ -190,9 +218,55 @@ MCP-Browser/
├── blockResources.js ├── blockResources.js
├── uploadFile.js ├── uploadFile.js
├── getPerformanceMetrics.js ├── getPerformanceMetrics.js
── wait.js ── wait.js
├── enableVisualOverlay.js
├── showToast.js
├── highlightElement.js
├── dragAndDrop.js
├── annotatedScreenshot.js
├── getInputValues.js
├── waitForClickable.js
├── waitForElementVisible.js
└── waitForText.js
``` ```
## Sugestões de Novas Funcionalidades Premium
### 1. Gravação e Replay de Ações
- Gravar sequência de ações (click, type, scroll)
- Replay automatizado com timing
- Exportar/importar scripts
### 2. OCR e Detecção Visual
- Detectar texto em imagens (Tesseract)
- Identificar elementos por imagem
- Screenshots AI-driven
### 3. Integração com IA
- Análise de página com LLM
- Auto-complete de ações
- Detecção de formulários智能化
### 4. Proxy Rotativo
- Múltiplos proxies
- Rotação automática
-Geo-targeting
### 5. Sessões Persistentes
- Salvar/carregar estado
- Snapshot de abas
- Backup de cookies
### 6. Webhooks e Eventos
- Notificações em tempo real
- Monitoramento de mudanças
- Integração com Zapier
### 7. headless + UI Mode
- Alternar entre headless/visible
- DevTools integrado
- Debug visual
## Licença ## Licença
MIT MIT

View File

@@ -363,8 +363,30 @@ async function reload() {
return p.url(); return p.url();
} }
async function evaluateJS(expression) { async function evaluateJS(expression, arg = null) {
const p = await ensurePage(); const p = await ensurePage();
if (typeof expression === 'function') {
if (arg !== null && arg !== undefined) {
return await p.evaluate(expression, arg);
}
return await p.evaluate(expression);
}
if (arg !== null && arg !== undefined && arg !== {}) {
return await p.evaluate((expr, argument) => {
try {
if (!argument) return eval(expr);
const keys = Object.keys(argument);
const values = Object.values(argument);
if (keys.length === 0) return eval(expr);
const fn = new Function(...keys, 'return ' + expr);
return fn(...values);
} catch (e) {
return { error: e.message, expression: expr };
}
}, { expr: expression, arg });
}
return await p.evaluate(expression); return await p.evaluate(expression);
} }
@@ -373,14 +395,33 @@ async function evaluateOnSelector(selector, expression) {
return await p.evaluate(({ sel, expr }) => { return await p.evaluate(({ sel, expr }) => {
const el = document.querySelector(sel); const el = document.querySelector(sel);
if (!el) return null; if (!el) return null;
return eval(expr); const fn = new Function('el', 'return ' + expr);
return fn(el);
}, { sel: selector, expr: expression }); }, { sel: selector, expr: expression });
} }
async function hover(selector) { async function hover(selector) {
const p = await ensurePage(); const p = await ensurePage();
await p.waitForSelector(selector, { state: 'visible', timeout: 10000 }); const result = await p.evaluate((sel) => {
await p.hover(selector); const el = document.querySelector(sel);
if (!el) return { success: false, error: 'Element not found' };
el.scrollIntoView({ behavior: 'instant', block: 'center' });
const rect = el.getBoundingClientRect();
return {
success: true,
x: rect.left + rect.width / 2,
y: rect.top + rect.height / 2,
visible: rect.width > 0 && rect.height > 0
};
}, selector);
if (!result.success) throw new Error(result.error);
if (result.visible) {
await p.mouse.move(result.x, result.y);
}
return { success: true };
} }
async function select(selector, values) { async function select(selector, values) {
@@ -515,20 +556,24 @@ async function getStorage() {
const p = await ensurePage(); const p = await ensurePage();
const storage = await p.evaluate(() => { const storage = await p.evaluate(() => {
const local = {}; try {
const session = {}; const local = {};
const session = {};
for (let i = 0; i < localStorage.length; i++) { for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i); const key = localStorage.key(i);
local[key] = localStorage.getItem(key); local[key] = localStorage.getItem(key);
}
for (let i = 0; i < sessionStorage.length; i++) {
const key = sessionStorage.key(i);
session[key] = sessionStorage.getItem(key);
}
return { localStorage: local, sessionStorage: session };
} catch (e) {
return { localStorage: {}, sessionStorage: {}, error: e.message };
} }
for (let i = 0; i < sessionStorage.length; i++) {
const key = sessionStorage.key(i);
session[key] = sessionStorage.getItem(key);
}
return { localStorage: local, sessionStorage: session };
}); });
return { origin: p.url(), ...storage }; return { origin: p.url(), ...storage };
@@ -538,12 +583,14 @@ async function setStorage(data) {
const p = await ensurePage(); const p = await ensurePage();
await p.evaluate((storageData) => { await p.evaluate((storageData) => {
if (storageData.localStorage) { try {
Object.entries(storageData.localStorage).forEach(([k, v]) => localStorage.setItem(k, v)); if (storageData.localStorage) {
} Object.entries(storageData.localStorage).forEach(([k, v]) => localStorage.setItem(k, v));
if (storageData.sessionStorage) { }
Object.entries(storageData.sessionStorage).forEach(([k, v]) => sessionStorage.setItem(k, v)); if (storageData.sessionStorage) {
} Object.entries(storageData.sessionStorage).forEach(([k, v]) => sessionStorage.setItem(k, v));
}
} catch (e) {}
}, data); }, data);
return { set: true }; return { set: true };
@@ -552,8 +599,10 @@ async function setStorage(data) {
async function clearStorage() { async function clearStorage() {
const p = await ensurePage(); const p = await ensurePage();
await p.evaluate(() => { await p.evaluate(() => {
localStorage.clear(); try {
sessionStorage.clear(); localStorage.clear();
sessionStorage.clear();
} catch (e) {}
}); });
return { cleared: true }; return { cleared: true };
} }

View File

@@ -1,7 +1,7 @@
{ {
"name": "opencode-mcp-browser", "name": "opencode-mcp-browser",
"version": "3.0.0", "version": "3.1.1",
"description": "MCP Browser - Automação de navegador de alta qualidade com 42 tools", "description": "MCP Browser - Automação de navegador de alta qualidade com 60+ tools",
"main": "server.js", "main": "server.js",
"scripts": { "scripts": {
"start": "node server.js", "start": "node server.js",

View File

@@ -27,6 +27,7 @@ const tools = {
goForward: require('./tools/goForward'), goForward: require('./tools/goForward'),
reload: require('./tools/reload'), reload: require('./tools/reload'),
evaluateJS: require('./tools/evaluateJS'), evaluateJS: require('./tools/evaluateJS'),
evaluateJs: require('./tools/evaluateJS'),
waitForURL: require('./tools/waitForURL'), waitForURL: require('./tools/waitForURL'),
newTab: require('./tools/newTab'), newTab: require('./tools/newTab'),
switchTab: require('./tools/switchTab'), switchTab: require('./tools/switchTab'),
@@ -49,10 +50,19 @@ const tools = {
blockResources: require('./tools/blockResources'), blockResources: require('./tools/blockResources'),
uploadFile: require('./tools/uploadFile'), uploadFile: require('./tools/uploadFile'),
getPerformanceMetrics: require('./tools/getPerformanceMetrics'), getPerformanceMetrics: require('./tools/getPerformanceMetrics'),
wait: require('./tools/wait') wait: require('./tools/wait'),
enableVisualOverlay: require('./tools/enableVisualOverlay'),
showToast: require('./tools/showToast'),
highlightElement: require('./tools/highlightElement'),
dragAndDrop: require('./tools/dragAndDrop'),
annotatedScreenshot: require('./tools/annotatedScreenshot'),
getInputValues: require('./tools/getInputValues'),
waitForClickable: require('./tools/waitForClickable'),
waitForElementVisible: require('./tools/waitForElementVisible'),
waitForText: require('./tools/waitForText')
}; };
const server = new Server({ name: 'browser', version: '3.0.0' }, { capabilities: { tools: {} } }); const server = new Server({ name: 'browser', version: '3.1.1' }, { capabilities: { tools: {} } });
const toolSchemas = [ const toolSchemas = [
{ name: 'open_url', description: 'Abre uma URL no navegador', inputSchema: { type: 'object', properties: { url: { type: 'string', description: 'URL para abrir' }, waitUntil: { type: 'string', enum: ['domcontentloaded', 'load', 'networkidle'] } }, required: ['url'] } }, { name: 'open_url', description: 'Abre uma URL no navegador', inputSchema: { type: 'object', properties: { url: { type: 'string', description: 'URL para abrir' }, waitUntil: { type: 'string', enum: ['domcontentloaded', 'load', 'networkidle'] } }, required: ['url'] } },
@@ -101,14 +111,79 @@ const toolSchemas = [
{ name: 'block_resources', description: 'Bloqueia recursos', inputSchema: { type: 'object', properties: { types: { type: 'array', items: { type: 'string' } } }, required: ['types'] } }, { name: 'block_resources', description: 'Bloqueia recursos', inputSchema: { type: 'object', properties: { types: { type: 'array', items: { type: 'string' } } }, required: ['types'] } },
{ name: 'upload_file', description: 'Upload de arquivo', inputSchema: { type: 'object', properties: { selector: { type: 'string' }, filePaths: { oneOf: [{ type: 'string' }, { type: 'array', items: { type: 'string' } }] } }, required: ['selector', 'filePaths'] } }, { name: 'upload_file', description: 'Upload de arquivo', inputSchema: { type: 'object', properties: { selector: { type: 'string' }, filePaths: { oneOf: [{ type: 'string' }, { type: 'array', items: { type: 'string' } }] } }, required: ['selector', 'filePaths'] } },
{ name: 'get_performance_metrics', description: 'Métricas de performance', inputSchema: { type: 'object', properties: {} } }, { name: 'get_performance_metrics', description: 'Métricas de performance', inputSchema: { type: 'object', properties: {} } },
{ name: 'wait', description: 'Aguarda ms', inputSchema: { type: 'object', properties: { ms: { type: 'number' } }, required: ['ms'] } } { name: 'wait', description: 'Aguarda ms', inputSchema: { type: 'object', properties: { ms: { type: 'number' } }, required: ['ms'] } },
{ name: 'enable_visual_overlay', description: 'Ativa cursor customizado e indicadores visuais de rolagem', inputSchema: { type: 'object', properties: {} } },
{ name: 'show_toast', description: 'Mostra notificação toast na página', inputSchema: { type: 'object', properties: { message: { type: 'string' }, duration: { type: 'number' } } } },
{ name: 'highlight_element', description: 'Destaca elemento com animação visual', inputSchema: { type: 'object', properties: { selector: { type: 'string' }, color: { type: 'string' }, duration: { type: 'number' } }, required: ['selector'] } },
{ name: 'drag_and_drop', description: 'Arrasta e solta elemento', inputSchema: { type: 'object', properties: { sourceSelector: { type: 'string' }, targetSelector: { type: 'string' } }, required: ['sourceSelector', 'targetSelector'] } },
{ name: 'annotated_screenshot', description: 'Screenshot com elementos destacados', inputSchema: { type: 'object', properties: { fullPage: { type: 'boolean' }, path: { type: 'string' }, highlight: { oneOf: [{ type: 'string' }, { type: 'array', items: { type: 'string' } }] } } } },
{ name: 'get_input_values', description: 'Obtém valores de todos os inputs', inputSchema: { type: 'object', properties: { selector: { type: 'string' } } } },
{ name: 'wait_for_clickable', description: 'Espera elemento ser clicável', inputSchema: { type: 'object', properties: { selector: { type: 'string' }, timeout: { type: 'number' } }, required: ['selector'] } },
{ name: 'wait_for_element_visible', description: 'Espera elemento visível', inputSchema: { type: 'object', properties: { selector: { type: 'string' }, timeout: { type: 'number' }, state: { type: 'string', enum: ['attached', 'detached', 'visible', 'hidden'] } }, required: ['selector'] } },
{ name: 'wait_for_text', description: 'Espera texto aparecer na página', inputSchema: { type: 'object', properties: { text: { type: 'string' }, timeout: { type: 'number' } }, required: ['text'] } }
]; ];
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: toolSchemas })); server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: toolSchemas }));
server.setRequestHandler(CallToolRequestSchema, async (request) => { server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params; const { name, arguments: args } = request.params;
const tool = tools[name.replace(/-/g, '').replace(/_([a-z])/g, (_, c) => c.toUpperCase())] || tools[name]; let toolName = name.replace(/-/g, '').replace(/_([a-z])/g, (_, c) => c.toUpperCase());
// Debug log
console.error('[MCP] Request:', name, '→ Converted:', toolName);
const nameMap = {
// camelCase sem underscores
'geturl': 'getURL',
'gethtml': 'getHTML',
'goback': 'goBack',
'goforward': 'goForward',
'newtab': 'newTab',
'switchtab': 'switchTab',
'listtabs': 'listTabs',
'closetab': 'closeTab',
'setviewport': 'setViewport',
'emulatedevice': 'emulateDevice',
'waitforurl': 'waitForURL',
'waitforselector': 'waitForSelector',
'getcookies': 'getCookies',
'setcookies': 'setCookies',
'clearcookies': 'clearCookies',
'getstorage': 'getStorage',
'setstorage': 'setStorage',
'clearstorage': 'clearStorage',
'getnetworklogs': 'getNetworkLogs',
'blockresources': 'blockResources',
'getperformancemetrics': 'getPerformanceMetrics',
'waitfortimeout': 'waitForTimeout',
'enablevisualoverlay': 'enableVisualOverlay',
'showtoast': 'showToast',
'highlightelement': 'highlightElement',
'draganddrop': 'dragAndDrop',
'annotatedscreenshot': 'annotatedScreenshot',
'getinputvalues': 'getInputValues',
'waitforclickable': 'waitForClickable',
'waitforelementvisible': 'waitForElementVisible',
'waitfortext': 'waitForText',
'fillformauto': 'fillFormAuto',
'agentflow': 'agentFlow',
'agentflowv2': 'agentFlowV2',
'smartclick': 'smartClick',
'smarttype': 'smartType',
'smartwaitnavigation': 'smartWaitNavigation',
'getbuttons': 'getButtons',
'getforms': 'getForms',
'extractelements': 'extractElements',
'gettext': 'getText',
'getlinks': 'getLinks',
'openurl': 'openUrl',
'closebrowser': 'closeBrowser',
'gettitle': 'getTitle'
};
if (nameMap[toolName]) toolName = nameMap[toolName];
const tool = tools[toolName] || tools[name];
if (!tool) throw new Error(`Tool "${name}" não encontrada`); if (!tool) throw new Error(`Tool "${name}" não encontrada`);
try { try {
const result = await tool(args || {}); const result = await tool(args || {});

View File

@@ -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 };
};

28
tools/dragAndDrop.js Normal file
View File

@@ -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 };
};

View File

@@ -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)'
};
};

View File

@@ -3,10 +3,12 @@ const browser = require('../browser');
module.exports = async ({ expression, selector = null }) => { module.exports = async ({ expression, selector = null }) => {
if (!expression) throw new Error('Expression é obrigatória'); if (!expression) throw new Error('Expression é obrigatória');
await browser.start(); await browser.start();
if (selector) { if (selector) {
const result = await browser.evaluateOnSelector(selector, expression); const result = await browser.evaluateOnSelector(selector, expression);
return { success: true, action: 'evaluate_js', selector, result }; return { success: true, action: 'evaluate_js', selector, result };
} }
const result = await browser.evaluateJS(expression); const result = await browser.evaluateJS(expression);
return { success: true, action: 'evaluate_js', result }; return { success: true, action: 'evaluate_js', result };
}; };

View File

@@ -3,29 +3,37 @@ const browser = require('../browser');
module.exports = async ({ data }) => { module.exports = async ({ data }) => {
if (!data) throw new Error('Data é obrigatória'); if (!data) throw new Error('Data é obrigatória');
await browser.start(); await browser.start();
const result = await browser.evaluateJS((formData) => {
const inputs = Array.from(document.querySelectorAll('input, textarea, select')); const formDataJson = JSON.stringify(data).replace(/'/g, "\\'");
const filled = [];
inputs.forEach(input => { const result = await browser.evaluateJS(`
const key = Object.keys(formData).find(k => (function() {
(input.name && input.name.toLowerCase().includes(k.toLowerCase())) || try {
(input.placeholder || '').toLowerCase().includes(k.toLowerCase()) || var formData = JSON.parse('${formDataJson}');
(input.id || '').toLowerCase().includes(k.toLowerCase()) || var inputs = Array.from(document.querySelectorAll('input, textarea, select'));
(input.getAttribute('aria-label') || '').toLowerCase().includes(k.toLowerCase()) var filled = [];
); inputs.forEach(function(input) {
if (key) { var keys = Object.keys(formData);
const value = formData[key]; for (var i = 0; i < keys.length; i++) {
const nativeSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value')?.set; var k = keys[i];
const nativeTextAreaSetter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value')?.set; var match = input.name && input.name.toLowerCase().indexOf(k.toLowerCase()) >= 0 ||
if (input.tagName === 'INPUT' && nativeSetter) nativeSetter.call(input, value); input.placeholder && input.placeholder.toLowerCase().indexOf(k.toLowerCase()) >= 0 ||
else if (input.tagName === 'TEXTAREA' && nativeTextAreaSetter) nativeTextAreaSetter.call(input, value); input.id && input.id.toLowerCase().indexOf(k.toLowerCase()) >= 0;
else input.value = value; if (match) {
input.dispatchEvent(new Event('input', { bubbles: true })); input.value = formData[k];
input.dispatchEvent(new Event('change', { bubbles: true })); input.dispatchEvent(new Event('input', { bubbles: true }));
filled.push({ field: input.name || input.id || input.placeholder || 'unknown', value }); 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 };
}; };

23
tools/getInputValues.js Normal file
View File

@@ -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 };
};

View File

@@ -1,8 +1,8 @@
const browser = require('../browser'); const browser = require('../browser');
module.exports = async ({ selector = 'a' }) => { module.exports = async ({ selector = 'a' }) => {
await browser.start(); const p = await browser.ensurePage();
const links = await browser.evaluateJS((sel) => { const links = await p.evaluate((sel) => {
return Array.from(document.querySelectorAll(sel)).map(el => ({ return Array.from(document.querySelectorAll(sel)).map(el => ({
text: (el.innerText || el.textContent || '').trim(), text: (el.innerText || el.textContent || '').trim(),
href: el.href || el.getAttribute('href') || '' href: el.href || el.getAttribute('href') || ''

View File

@@ -1,9 +1,9 @@
const browser = require('../browser'); const browser = require('../browser');
module.exports = async ({ selector = 'body' }) => { module.exports = async function({ selector = 'body' }) {
await browser.start(); const p = await browser.ensurePage();
const text = await browser.evaluateJS((sel) => { const text = await p.evaluate(function(s) {
const el = document.querySelector(sel); var el = document.querySelector(s);
if (!el) return null; if (!el) return null;
return (el.innerText || el.textContent || '').trim(); return (el.innerText || el.textContent || '').trim();
}, selector); }, selector);

41
tools/highlightElement.js Normal file
View File

@@ -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 };
};

41
tools/showToast.js Normal file
View File

@@ -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 };
};

23
tools/waitForClickable.js Normal file
View File

@@ -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 };
};

View File

@@ -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 };
};

24
tools/waitForText.js Normal file
View File

@@ -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 };
};