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:
108
README.md
108
README.md
@@ -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.
|
||||||
|
|
||||||

|

|
||||||

|

|
||||||

|

|
||||||

|

|
||||||
|
|
||||||
## 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
|
||||||
|
|||||||
97
browser.js
97
browser.js
@@ -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 };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
83
server.js
83
server.js
@@ -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 || {});
|
||||||
|
|||||||
45
tools/annotatedScreenshot.js
Normal file
45
tools/annotatedScreenshot.js
Normal 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
28
tools/dragAndDrop.js
Normal 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 };
|
||||||
|
};
|
||||||
154
tools/enableVisualOverlay.js
Normal file
154
tools/enableVisualOverlay.js
Normal 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)'
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -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 };
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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
23
tools/getInputValues.js
Normal 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 };
|
||||||
|
};
|
||||||
@@ -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') || ''
|
||||||
|
|||||||
@@ -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
41
tools/highlightElement.js
Normal 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
41
tools/showToast.js
Normal 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
23
tools/waitForClickable.js
Normal 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 };
|
||||||
|
};
|
||||||
11
tools/waitForElementVisible.js
Normal file
11
tools/waitForElementVisible.js
Normal 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
24
tools/waitForText.js
Normal 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 };
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user