feat: MCP Browser - servidor MCP para automação de navegador com Playwright
This commit is contained in:
+24
@@ -0,0 +1,24 @@
|
||||
# Dependências
|
||||
node_modules/
|
||||
|
||||
# Screenshots gerados
|
||||
*.png
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
|
||||
# Variáveis de ambiente
|
||||
.env
|
||||
.env.local
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
desktop.ini
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
@@ -0,0 +1,254 @@
|
||||
# MCP-Browser
|
||||
|
||||
MCP server que permite a qualquer agente de IA controlar um navegador real usando [Playwright](https://playwright.dev/).
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
## Funcionalidades
|
||||
|
||||
- Controle total de navegador via IA
|
||||
- 17 tools prontas para uso
|
||||
- Reuso automático de abas (mesmo domínio)
|
||||
- Preenchimento automático de formulários
|
||||
- Fluxos agentísticos com retry
|
||||
- Compatível com qualquer plataforma MCP
|
||||
|
||||
## Pré-requisitos
|
||||
|
||||
| Dependência | Versão mínima |
|
||||
|-------------|---------------|
|
||||
| Node.js | 18+ |
|
||||
| npm | 9+ |
|
||||
|
||||
## Instalação
|
||||
|
||||
```bash
|
||||
# 1. Clonar o repositório
|
||||
git clone https://git.jf.eng.br/jfeng/MCP-Browser.git
|
||||
cd MCP-Browser
|
||||
|
||||
# 2. Instalar dependências
|
||||
npm install
|
||||
|
||||
# 3. Instalar Chromium do Playwright
|
||||
npx playwright install chromium
|
||||
|
||||
# Linux: instalar dependências do sistema
|
||||
sudo npx playwright install-deps chromium
|
||||
```
|
||||
|
||||
## Configuração por plataforma
|
||||
|
||||
Todas as plataformas usam o mesmo padrão:
|
||||
|
||||
```
|
||||
Comando: node
|
||||
Argumento: /caminho/absoluto/MCP-Browser/server.js
|
||||
Transport: stdio
|
||||
```
|
||||
|
||||
### OpenCode
|
||||
|
||||
```bash
|
||||
opencode mcp add
|
||||
```
|
||||
|
||||
- **Nome:** `meu-navegador`
|
||||
- **Tipo:** `Local`
|
||||
- **Comando:** `node /caminho/MCP-Browser/server.js`
|
||||
|
||||
### Cursor
|
||||
|
||||
`~/.cursor/mcp.json` (global) ou `.cursor/mcp.json` (projeto):
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"browser": {
|
||||
"command": "node",
|
||||
"args": ["/caminho/MCP-Browser/server.js"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Claude Desktop
|
||||
|
||||
`%APPDATA%\Claude\claude_desktop_config.json` (Windows)
|
||||
`~/Library/Application Support/Claude/claude_desktop_config.json` (macOS)
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"browser": {
|
||||
"command": "node",
|
||||
"args": ["/caminho/MCP-Browser/server.js"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Windsurf
|
||||
|
||||
`~/.windsurf/mcp.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"browser": {
|
||||
"command": "node",
|
||||
"args": ["/caminho/MCP-Browser/server.js"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Trae
|
||||
|
||||
Settings → Extensions → MCP → Adicionar server:
|
||||
|
||||
- **Name:** `browser`
|
||||
- **Command:** `node`
|
||||
- **Args:** `["/caminho/MCP-Browser/server.js"]`
|
||||
|
||||
### Antigravity
|
||||
|
||||
Configuração de MCP → Novo server:
|
||||
|
||||
- **Name:** `browser`
|
||||
- **Type:** `stdio`
|
||||
- **Command:** `node /caminho/MCP-Browser/server.js`
|
||||
|
||||
### VS Code
|
||||
|
||||
`.vscode/mcp.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"browser": {
|
||||
"command": "node",
|
||||
"args": ["/caminho/MCP-Browser/server.js"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Qualquer plataforma MCP
|
||||
|
||||
Se suporta **MCP stdio transport**, use:
|
||||
|
||||
```
|
||||
Comando: node
|
||||
Argumento: /caminho/absoluto/MCP-Browser/server.js
|
||||
```
|
||||
|
||||
## Tools disponíveis (17)
|
||||
|
||||
| # | Tool | Descrição | Parâmetros |
|
||||
|---|------|-----------|------------|
|
||||
| 1 | `open_url` | Abre uma URL | `url` |
|
||||
| 2 | `click` | Clica por seletor CSS | `selector` |
|
||||
| 3 | `type` | Digita texto em campo | `selector`, `text` |
|
||||
| 4 | `screenshot` | Captura screenshot | `fullPage` (opcional) |
|
||||
| 5 | `get_text` | Extrai texto da página | `selector` (opcional) |
|
||||
| 6 | `get_links` | Lista links | `selector` (opcional) |
|
||||
| 7 | `wait_for_selector` | Espera elemento aparecer | `selector`, `timeout` |
|
||||
| 8 | `extract_elements` | Extrai elementos com atributos | `selector`, `attributes` |
|
||||
| 9 | `smart_click` | Clica por texto visível | `text` |
|
||||
| 10 | `smart_type` | Digita por label/placeholder | `label`, `text` |
|
||||
| 11 | `get_buttons` | Lista botões disponíveis | — |
|
||||
| 12 | `smart_wait_navigation` | Espera mudança de URL | `timeout` (opcional) |
|
||||
| 13 | `get_forms` | Lista formulários e campos | — |
|
||||
| 14 | `fill_form_auto` | Preenche formulário auto | `data` |
|
||||
| 15 | `agent_flow` | Fluxo automático simples | `goal`, `data` |
|
||||
| 16 | `agent_flow_v2` | Fluxo com retry/fallback | `goal`, `data`, `retries` |
|
||||
| 17 | `close_browser` | Fecha o navegador | — |
|
||||
|
||||
## Exemplos de uso
|
||||
|
||||
### Abrir site e extrair conteúdo
|
||||
|
||||
```
|
||||
open_url("https://example.com")
|
||||
get_text()
|
||||
get_links()
|
||||
screenshot()
|
||||
```
|
||||
|
||||
### Login automático
|
||||
|
||||
```
|
||||
open_url("https://site.com/login")
|
||||
fill_form_auto({"email": "user@test.com", "password": "1234"})
|
||||
smart_click("Entrar")
|
||||
smart_wait_navigation()
|
||||
```
|
||||
|
||||
### Scraping com atributos
|
||||
|
||||
```
|
||||
open_url("https://site.com/produtos")
|
||||
extract_elements(".produto", ["href", "data-id"])
|
||||
```
|
||||
|
||||
## Estrutura do projeto
|
||||
|
||||
```
|
||||
MCP-Browser/
|
||||
├── server.js # Servidor MCP (entry point)
|
||||
├── browser.js # Engine Playwright (singleton)
|
||||
├── package.json
|
||||
├── docs.html # Documentação detalhada
|
||||
└── tools/
|
||||
├── openUrl.js
|
||||
├── click.js
|
||||
├── type.js
|
||||
├── screenshot.js
|
||||
├── getText.js
|
||||
├── getLinks.js
|
||||
├── getButtons.js
|
||||
├── getForms.js
|
||||
├── extractElements.js
|
||||
├── waitForSelector.js
|
||||
├── smartClick.js
|
||||
├── smartType.js
|
||||
├── fillFormAuto.js
|
||||
├── smartWaitNavigation.js
|
||||
├── agentFlow.js
|
||||
├── agentFlowV2.js
|
||||
└── closeBrowser.js
|
||||
```
|
||||
|
||||
## Comportamento
|
||||
|
||||
- **Reuso de abas:** mesmo domínio reutiliza aba, domínios diferentes abrem nova aba
|
||||
- **Headed mode:** navegador abre visível. Para headless, edite `browser.js` e mude `headless: true`
|
||||
- **Singleton:** uma única instância do navegador compartilhada entre todas as tools
|
||||
|
||||
## Solução de problemas
|
||||
|
||||
| Problema | Solução |
|
||||
|----------|---------|
|
||||
| Tools não aparecem | Reiniciar a IDE após configurar MCP |
|
||||
| `node: not found` | Instalar Node.js 18+ e adicionar ao PATH |
|
||||
| Chromium não encontrado | `npx playwright install chromium` |
|
||||
| Erro de permissão (Linux) | `sudo npx playwright install-deps chromium` |
|
||||
| Caminho com espaços | Usar aspas no caminho |
|
||||
|
||||
## Caminhos por SO
|
||||
|
||||
| Sistema | Exemplo |
|
||||
|---------|---------|
|
||||
| Windows | `C:/Users/seuuser/mcp-browser/server.js` |
|
||||
| macOS | `/Users/seuuser/mcp-browser/server.js` |
|
||||
| Linux | `/home/seuuser/mcp-browser/server.js` |
|
||||
|
||||
> **Importante:** Nunca usar `\` simples. Sempre `/` ou `\\`.
|
||||
|
||||
## Licença
|
||||
|
||||
MIT
|
||||
+121
@@ -0,0 +1,121 @@
|
||||
const { chromium } = require('playwright');
|
||||
|
||||
let browser = null;
|
||||
let context = null;
|
||||
let page = null;
|
||||
|
||||
async function start() {
|
||||
if (browser) return;
|
||||
if (page) return;
|
||||
|
||||
browser = await chromium.launch({
|
||||
headless: false,
|
||||
args: ['--disable-blink-features=AutomationControlled']
|
||||
});
|
||||
|
||||
context = await browser.newContext({
|
||||
viewport: { width: 1280, height: 720 },
|
||||
userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
|
||||
});
|
||||
|
||||
page = await context.newPage();
|
||||
|
||||
browser.on('disconnected', () => {
|
||||
browser = null;
|
||||
context = null;
|
||||
page = null;
|
||||
});
|
||||
}
|
||||
|
||||
async function ensurePage() {
|
||||
if (!browser) {
|
||||
await start();
|
||||
return page;
|
||||
}
|
||||
|
||||
if (!page || page.isClosed()) {
|
||||
page = await context.newPage();
|
||||
}
|
||||
|
||||
return page;
|
||||
}
|
||||
|
||||
async function goto(url) {
|
||||
const p = await ensurePage();
|
||||
await p.goto(url, {
|
||||
waitUntil: 'domcontentloaded',
|
||||
timeout: 30000
|
||||
});
|
||||
|
||||
await p.waitForTimeout(1500);
|
||||
}
|
||||
|
||||
async function click(selector) {
|
||||
const p = await ensurePage();
|
||||
await p.waitForSelector(selector, { state: 'visible', timeout: 10000 });
|
||||
await p.click(selector);
|
||||
}
|
||||
|
||||
async function type(selector, text) {
|
||||
const p = await ensurePage();
|
||||
await p.waitForSelector(selector, { state: 'visible', timeout: 10000 });
|
||||
await p.fill(selector, text);
|
||||
}
|
||||
|
||||
async function content() {
|
||||
const p = await ensurePage();
|
||||
return await p.content();
|
||||
}
|
||||
|
||||
async function title() {
|
||||
const p = await ensurePage();
|
||||
return await p.title();
|
||||
}
|
||||
|
||||
async function screenshot(path, fullPage = true) {
|
||||
const p = await ensurePage();
|
||||
await p.screenshot({ path, fullPage });
|
||||
}
|
||||
|
||||
async function newTab() {
|
||||
const p = await context.newPage();
|
||||
page = p;
|
||||
return p;
|
||||
}
|
||||
|
||||
async function close() {
|
||||
if (browser) {
|
||||
try {
|
||||
await browser.close();
|
||||
} catch (e) {
|
||||
}
|
||||
browser = null;
|
||||
context = null;
|
||||
page = null;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
start,
|
||||
goto,
|
||||
click,
|
||||
type,
|
||||
content,
|
||||
title,
|
||||
screenshot,
|
||||
close,
|
||||
newTab,
|
||||
ensurePage,
|
||||
|
||||
get page() {
|
||||
return page;
|
||||
},
|
||||
|
||||
get browser() {
|
||||
return browser;
|
||||
},
|
||||
|
||||
get context() {
|
||||
return context;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,444 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="pt-BR">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>MCP-Browser — Documentação</title>
|
||||
<style>
|
||||
:root {
|
||||
--bg: #0f172a;
|
||||
--bg2: #1e293b;
|
||||
--bg3: #334155;
|
||||
--accent: #38bdf8;
|
||||
--green: #22c55e;
|
||||
--yellow: #eab308;
|
||||
--red: #ef4444;
|
||||
--text: #e2e8f0;
|
||||
--text2: #94a3b8;
|
||||
}
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body {
|
||||
font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
|
||||
max-width: 960px;
|
||||
margin: 0 auto;
|
||||
padding: 40px 20px;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
line-height: 1.7;
|
||||
}
|
||||
h1 { color: var(--accent); font-size: 2em; margin-bottom: 8px; }
|
||||
h2 { color: var(--accent); font-size: 1.4em; margin-bottom: 12px; border-bottom: 1px solid var(--bg3); padding-bottom: 8px; }
|
||||
h3 { color: var(--green); font-size: 1.1em; margin: 16px 0 8px; }
|
||||
a { color: var(--accent); text-decoration: none; }
|
||||
a:hover { text-decoration: underline; }
|
||||
pre {
|
||||
background: var(--bg2);
|
||||
padding: 16px;
|
||||
border-radius: 8px;
|
||||
overflow-x: auto;
|
||||
margin: 10px 0 16px;
|
||||
font-size: 0.9em;
|
||||
border-left: 3px solid var(--accent);
|
||||
}
|
||||
code { color: var(--green); font-family: 'Cascadia Code', 'Fira Code', monospace; }
|
||||
.box {
|
||||
background: var(--bg2);
|
||||
padding: 24px;
|
||||
border-radius: 10px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.warn {
|
||||
background: #422006;
|
||||
border-left: 3px solid var(--yellow);
|
||||
padding: 14px 18px;
|
||||
border-radius: 6px;
|
||||
margin: 12px 0;
|
||||
}
|
||||
.ok {
|
||||
background: #052e16;
|
||||
border-left: 3px solid var(--green);
|
||||
padding: 14px 18px;
|
||||
border-radius: 6px;
|
||||
margin: 12px 0;
|
||||
}
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin: 12px 0;
|
||||
}
|
||||
th, td {
|
||||
text-align: left;
|
||||
padding: 10px 14px;
|
||||
border-bottom: 1px solid var(--bg3);
|
||||
}
|
||||
th { color: var(--accent); font-weight: 600; }
|
||||
.badge {
|
||||
display: inline-block;
|
||||
padding: 2px 10px;
|
||||
border-radius: 12px;
|
||||
font-size: 0.8em;
|
||||
font-weight: 600;
|
||||
}
|
||||
.badge-green { background: #166534; color: #86efac; }
|
||||
.badge-blue { background: #1e3a5f; color: #7dd3fc; }
|
||||
ol li, ul li { margin-bottom: 6px; }
|
||||
.platform { margin-bottom: 24px; }
|
||||
.tab-icon { margin-right: 6px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<h1>MCP-Browser</h1>
|
||||
<p style="color: var(--text2); margin-bottom: 30px;">
|
||||
MCP server que permite a qualquer agente de IA controlar um navegador real via Playwright.
|
||||
<span class="badge badge-green">17 tools</span>
|
||||
<span class="badge badge-blue">Windows / Linux / macOS</span>
|
||||
</p>
|
||||
|
||||
<!-- ============================================ -->
|
||||
<div class="box">
|
||||
<h2>Pré-requisitos</h2>
|
||||
|
||||
<table>
|
||||
<tr><th>Dependência</th><th>Versão mínima</th><th>Verificar</th></tr>
|
||||
<tr><td>Node.js</td><td>18+</td><td><code>node -v</code></td></tr>
|
||||
<tr><td>npm</td><td>9+</td><td><code>npm -v</code></td></tr>
|
||||
<tr><td>Playwright</td><td>1.50+</td><td><code>npx playwright --version</code></td></tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- ============================================ -->
|
||||
<div class="box">
|
||||
<h2>Instalação</h2>
|
||||
|
||||
<h3>1. Clonar ou copiar o projeto</h3>
|
||||
<pre><code># Opção A — git clone
|
||||
git clone https://git.jf.eng.br/jfeng/MCP-Browser.git
|
||||
cd MCP-Browser
|
||||
|
||||
# Opção B — copiar a pasta manualmente para qualquer diretório</code></pre>
|
||||
|
||||
<h3>2. Instalar dependências</h3>
|
||||
<pre><code>npm install</code></pre>
|
||||
|
||||
<h3>3. Instalar navegadores do Playwright</h3>
|
||||
<pre><code>npx playwright install chromium</code></pre>
|
||||
|
||||
<div class="warn">
|
||||
<strong>Linux:</strong> Pode precisar instalar dependências do sistema:<br>
|
||||
<code>sudo npx playwright install-deps chromium</code>
|
||||
</div>
|
||||
|
||||
<h3>4. Testar se funciona</h3>
|
||||
<pre><code>node server.js</code></pre>
|
||||
<p>O servidor inicia e aguarda conexão via stdin/stdout (MCP stdio transport). Não aparece nada no terminal — isso é normal.</p>
|
||||
</div>
|
||||
|
||||
<!-- ============================================ -->
|
||||
<div class="box">
|
||||
<h2>Estrutura do projeto</h2>
|
||||
<pre><code>MCP-Browser/
|
||||
├── server.js ← servidor MCP (entry point)
|
||||
├── browser.js ← engine Playwright (singleton)
|
||||
├── package.json
|
||||
├── package-lock.json
|
||||
├── docs.html ← esta documentação
|
||||
├── node_modules/
|
||||
└── tools/
|
||||
├── openUrl.js ← abre URL (reusa aba se mesmo domínio)
|
||||
├── click.js ← clica por seletor CSS
|
||||
├── type.js ← digita texto em campo
|
||||
├── screenshot.js ← captura screenshot
|
||||
├── getText.js ← extrai texto da página
|
||||
├── getLinks.js ← lista todos os links
|
||||
├── getButtons.js ← lista botões disponíveis
|
||||
├── getForms.js ← lista formulários e campos
|
||||
├── extractElements.js ← extrai elementos com atributos
|
||||
├── waitForSelector.js ← espera elemento aparecer
|
||||
├── smartClick.js ← clica por texto visível
|
||||
├── smartType.js ← digita por label/placeholder
|
||||
├── fillFormAuto.js ← preenche formulário automaticamente
|
||||
├── smartWaitNavigation.js ← espera navegação
|
||||
├── agentFlow.js ← fluxo automático simples
|
||||
├── agentFlowV2.js ← fluxo com retry e fallback
|
||||
└── closeBrowser.js ← fecha o navegador</code></pre>
|
||||
</div>
|
||||
|
||||
<!-- ============================================ -->
|
||||
<div class="box">
|
||||
<h2>Configuração por plataforma</h2>
|
||||
|
||||
<!-- OPENCODE -->
|
||||
<div class="platform">
|
||||
<h3>OpenCode</h3>
|
||||
<ol>
|
||||
<li>Abra o terminal e execute:<br>
|
||||
<code>opencode mcp add</code>
|
||||
</li>
|
||||
<li>Preencha os campos:
|
||||
<ul>
|
||||
<li><strong>Nome:</strong> <code>meu-navegador</code></li>
|
||||
<li><strong>Tipo:</strong> <code>Local</code></li>
|
||||
<li><strong>Comando:</strong> <code>node /caminho/absoluto/MCP-Browser/server.js</code></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>Reinicie o OpenCode.</li>
|
||||
<li>Verifique: <code>opencode mcp list</code></li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<!-- CURSOR -->
|
||||
<div class="platform">
|
||||
<h3>Cursor</h3>
|
||||
<ol>
|
||||
<li>Abra <strong>Settings → MCP</strong> (ou <code>.cursor/mcp.json</code> no projeto).</li>
|
||||
<li>Adicione ao arquivo <code>~/.cursor/mcp.json</code> (global) ou <code>.cursor/mcp.json</code> (projeto):</li>
|
||||
</ol>
|
||||
<pre><code>{
|
||||
"mcpServers": {
|
||||
"browser": {
|
||||
"command": "node",
|
||||
"args": ["/caminho/absoluto/MCP-Browser/server.js"]
|
||||
}
|
||||
}
|
||||
}</code></pre>
|
||||
<ol start="3">
|
||||
<li>Reinicie o Cursor. As tools aparecerão no chat do Composer.</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<!-- CLAUDE DESKTOP -->
|
||||
<div class="platform">
|
||||
<h3>Claude Desktop</h3>
|
||||
<ol>
|
||||
<li>Abra o arquivo de configuração:
|
||||
<ul>
|
||||
<li><strong>Windows:</strong> <code>%APPDATA%\Claude\claude_desktop_config.json</code></li>
|
||||
<li><strong>macOS:</strong> <code>~/Library/Application Support/Claude/claude_desktop_config.json</code></li>
|
||||
<li><strong>Linux:</strong> <code>~/.config/Claude/claude_desktop_config.json</code></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>Adicione:</li>
|
||||
</ol>
|
||||
<pre><code>{
|
||||
"mcpServers": {
|
||||
"browser": {
|
||||
"command": "node",
|
||||
"args": ["/caminho/absoluto/MCP-Browser/server.js"]
|
||||
}
|
||||
}
|
||||
}</code></pre>
|
||||
<ol start="3">
|
||||
<li>Reinicie o Claude Desktop.</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<!-- WINDSURF -->
|
||||
<div class="platform">
|
||||
<h3>Windsurf (Codeium)</h3>
|
||||
<ol>
|
||||
<li>Abra <strong>Settings → Cascade → MCP Servers</strong>.</li>
|
||||
<li>Ou edite <code>~/.windsurf/mcp.json</code>:</li>
|
||||
</ol>
|
||||
<pre><code>{
|
||||
"mcpServers": {
|
||||
"browser": {
|
||||
"command": "node",
|
||||
"args": ["/caminho/absoluto/MCP-Browser/server.js"]
|
||||
}
|
||||
}
|
||||
}</code></pre>
|
||||
<ol start="3">
|
||||
<li>Reinicie o Windsurf.</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<!-- TRAE -->
|
||||
<div class="platform">
|
||||
<h3>Trae</h3>
|
||||
<ol>
|
||||
<li>Abra <strong>Settings → Extensions → MCP</strong>.</li>
|
||||
<li>Adicione server manualmente:
|
||||
<ul>
|
||||
<li><strong>Name:</strong> <code>browser</code></li>
|
||||
<li><strong>Command:</strong> <code>node</code></li>
|
||||
<li><strong>Args:</strong> <code>["/caminho/absoluto/MCP-Browser/server.js"]</code></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>Ou edite o arquivo de configuração do Trae equivalente ao <code>mcp.json</code>.</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<!-- ANTIGRAVITY -->
|
||||
<div class="platform">
|
||||
<h3>Antigravity</h3>
|
||||
<ol>
|
||||
<li>Abra a configuração de MCP do Antigravity.</li>
|
||||
<li>Adicione um novo server com:
|
||||
<ul>
|
||||
<li><strong>Name:</strong> <code>browser</code></li>
|
||||
<li><strong>Type:</strong> <code>stdio</code></li>
|
||||
<li><strong>Command:</strong> <code>node /caminho/absoluto/MCP-Browser/server.js</code></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>Salve e reinicie.</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<!-- VS CODE -->
|
||||
<div class="platform">
|
||||
<h3>VS Code (com extensão MCP)</h3>
|
||||
<ol>
|
||||
<li>Instale a extensão <strong>MCP</strong> no VS Code.</li>
|
||||
<li>Abra <code>.vscode/mcp.json</code> no workspace ou <code>settings.json</code> global.</li>
|
||||
<li>Adicione:</li>
|
||||
</ol>
|
||||
<pre><code>{
|
||||
"mcpServers": {
|
||||
"browser": {
|
||||
"command": "node",
|
||||
"args": ["/caminho/absoluto/MCP-Browser/server.js"]
|
||||
}
|
||||
}
|
||||
}</code></pre>
|
||||
</div>
|
||||
|
||||
<!-- GENERIC -->
|
||||
<div class="platform">
|
||||
<h3>Qualquer outra plataforma MCP</h3>
|
||||
<p>Se a plataforma suporta <strong>MCP stdio transport</strong>, use:</p>
|
||||
<pre><code>Comando: node
|
||||
Argumento: /caminho/absoluto/MCP-Browser/server.js
|
||||
Transport: stdio</code></pre>
|
||||
<p>O servidor comunica via <strong>stdin/stdout</strong> — não precisa porta, não precisa daemon.</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- ============================================ -->
|
||||
<div class="box">
|
||||
<h2>Caminhos por sistema operacional</h2>
|
||||
|
||||
<table>
|
||||
<tr><th>Sistema</th><th>Formato do caminho</th><th>Exemplo</th></tr>
|
||||
<tr><td>Windows</td><td>Use <code>/</code> ou <code>\\</code></td><td><code>C:/Users/seuuser/mcp-browser/server.js</code></td></tr>
|
||||
<tr><td>macOS</td><td>Unix path</td><td><code>/Users/seuuser/mcp-browser/server.js</code></td></tr>
|
||||
<tr><td>Linux</td><td>Unix path</td><td><code>/home/seuuser/mcp-browser/server.js</code></td></tr>
|
||||
</table>
|
||||
|
||||
<div class="warn">
|
||||
<strong>Importante:</strong> Nunca use <code>\</code> simples nos caminhos. Sempre <code>/</code> ou <code>\\</code>.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ============================================ -->
|
||||
<div class="box">
|
||||
<h2>Lista de tools (17)</h2>
|
||||
|
||||
<table>
|
||||
<tr><th>#</th><th>Tool</th><th>Descrição</th><th>Parâmetros</th></tr>
|
||||
<tr><td>1</td><td><code>open_url</code></td><td>Abre uma URL no navegador</td><td><code>url</code> (string, obrigatório)</td></tr>
|
||||
<tr><td>2</td><td><code>click</code></td><td>Clica em elemento por seletor CSS</td><td><code>selector</code> (string, obrigatório)</td></tr>
|
||||
<tr><td>3</td><td><code>type</code></td><td>Digita texto em um campo</td><td><code>selector</code>, <code>text</code></td></tr>
|
||||
<tr><td>4</td><td><code>screenshot</code></td><td>Captura screenshot da página</td><td><code>fullPage</code> (bool, opcional)</td></tr>
|
||||
<tr><td>5</td><td><code>get_text</code></td><td>Extrai texto da página</td><td><code>selector</code> (opcional, padrão: body)</td></tr>
|
||||
<tr><td>6</td><td><code>get_links</code></td><td>Lista todos os links</td><td><code>selector</code> (opcional, padrão: a)</td></tr>
|
||||
<tr><td>7</td><td><code>wait_for_selector</code></td><td>Espera elemento aparecer no DOM</td><td><code>selector</code>, <code>timeout</code></td></tr>
|
||||
<tr><td>8</td><td><code>extract_elements</code></td><td>Extrai elementos com atributos</td><td><code>selector</code>, <code>attributes</code> (array)</td></tr>
|
||||
<tr><td>9</td><td><code>smart_click</code></td><td>Clica pelo texto visível</td><td><code>text</code> (string, obrigatório)</td></tr>
|
||||
<tr><td>10</td><td><code>smart_type</code></td><td>Digita buscando por label/placeholder</td><td><code>label</code>, <code>text</code></td></tr>
|
||||
<tr><td>11</td><td><code>get_buttons</code></td><td>Lista botões disponíveis</td><td>—</td></tr>
|
||||
<tr><td>12</td><td><code>smart_wait_navigation</code></td><td>Espera mudança de URL</td><td><code>timeout</code> (opcional)</td></tr>
|
||||
<tr><td>13</td><td><code>get_forms</code></td><td>Lista formulários e seus campos</td><td>—</td></tr>
|
||||
<tr><td>14</td><td><code>fill_form_auto</code></td><td>Preenche formulário automaticamente</td><td><code>data</code> (objeto, obrigatório)</td></tr>
|
||||
<tr><td>15</td><td><code>agent_flow</code></td><td>Fluxo automático simples</td><td><code>goal</code>, <code>data</code> (opcional)</td></tr>
|
||||
<tr><td>16</td><td><code>agent_flow_v2</code></td><td>Fluxo com retry e fallback</td><td><code>goal</code>, <code>data</code>, <code>retries</code></td></tr>
|
||||
<tr><td>17</td><td><code>close_browser</code></td><td>Fecha o navegador</td><td>—</td></tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- ============================================ -->
|
||||
<div class="box">
|
||||
<h2>Exemplos de uso</h2>
|
||||
|
||||
<h3>Abrir site e extrair conteúdo</h3>
|
||||
<pre><code>open_url("https://example.com")
|
||||
get_text()
|
||||
get_links()
|
||||
screenshot()</code></pre>
|
||||
|
||||
<h3>Login automático</h3>
|
||||
<pre><code>open_url("https://site.com/login")
|
||||
fill_form_auto({"email": "user@test.com", "password": "1234"})
|
||||
smart_click("Entrar")
|
||||
smart_wait_navigation()
|
||||
get_text()</code></pre>
|
||||
|
||||
<h3>Scraping com atributos</h3>
|
||||
<pre><code>open_url("https://site.com/produtos")
|
||||
extract_elements(".produto", ["href", "data-id", "class"])</code></pre>
|
||||
|
||||
<h3>Preencher formulário complexo</h3>
|
||||
<pre><code>open_url("https://site.com/contato")
|
||||
smart_type("nome", "João Silva")
|
||||
type("#email", "joao@email.com")
|
||||
smart_type("mensagem", "Olá, quero mais informações")
|
||||
smart_click("Enviar")</code></pre>
|
||||
</div>
|
||||
|
||||
<!-- ============================================ -->
|
||||
<div class="box">
|
||||
<h2>Comportamento do navegador</h2>
|
||||
<ul>
|
||||
<li><strong>Reuso de abas:</strong> Se o navegador já estiver aberto e você abrir uma URL do <strong>mesmo domínio</strong>, a aba atual é reutilizada. Domínios diferentes abrem em <strong>nova aba</strong>.</li>
|
||||
<li><strong>Singleton:</strong> Apenas uma instância do navegador é mantida. Todas as tools compartilham a mesma página ativa.</li>
|
||||
<li><strong>Headed mode:</strong> O navegador abre visível (não headless). Para headless, edite <code>browser.js</code> e mude <code>headless: true</code>.</li>
|
||||
<li><strong>Auto-close:</strong> Use <code>close_browser</code> quando terminar. O navegador não fecha sozinho.</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- ============================================ -->
|
||||
<div class="box">
|
||||
<h2>Problemas comuns</h2>
|
||||
|
||||
<table>
|
||||
<tr><th>Problema</th><th>Solução</th></tr>
|
||||
<tr>
|
||||
<td>Tools não aparecem na plataforma</td>
|
||||
<td>Reiniciar a IDE/plataforma após configurar o MCP.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>node: command not found</code></td>
|
||||
<td>Instalar Node.js 18+ e garantir que está no PATH.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Playwright não encontra Chromium</td>
|
||||
<td><code>npx playwright install chromium</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Erro de permissão no Linux</td>
|
||||
<td><code>sudo npx playwright install-deps chromium</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Página não carrega (conteúdo vazio)</td>
|
||||
<td>O servidor aguarda <code>domcontentloaded</code> + 1.5s. Se a página usa JS pesado, aumente o timeout em <code>browser.js</code>.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Erro <code>ECONNREFUSED</code></td>
|
||||
<td>O servidor MCP usa stdio (stdin/stdout), não HTTP. Verifique se o comando está correto.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Caminho com espaços no Windows</td>
|
||||
<td>Use aspas: <code>"C:/Users/Meu Usuario/mcp/server.js"</code></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- ============================================ -->
|
||||
<div class="box">
|
||||
<h2>Licença</h2>
|
||||
<p>MIT — uso livre.</p>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
Generated
+1184
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "opencode-mcp-browser",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"type": "commonjs",
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.27.1",
|
||||
"playwright": "^1.58.2"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
const { Server } = require('@modelcontextprotocol/sdk/server/index.js');
|
||||
const { StdioServerTransport } = require('@modelcontextprotocol/sdk/server/stdio.js');
|
||||
|
||||
const {
|
||||
ListToolsRequestSchema,
|
||||
CallToolRequestSchema
|
||||
} = require('@modelcontextprotocol/sdk/types.js');
|
||||
|
||||
// TOOLS
|
||||
const openUrl = require('./tools/openUrl');
|
||||
const click = require('./tools/click');
|
||||
const type = require('./tools/type');
|
||||
const screenshot = require('./tools/screenshot');
|
||||
const getText = require('./tools/getText');
|
||||
const getLinks = require('./tools/getLinks');
|
||||
const waitForSelector = require('./tools/waitForSelector');
|
||||
const extractElements = require('./tools/extractElements');
|
||||
const smartClick = require('./tools/smartClick');
|
||||
const smartType = require('./tools/smartType');
|
||||
const getButtons = require('./tools/getButtons');
|
||||
const smartWaitNavigation = require('./tools/smartWaitNavigation');
|
||||
const getForms = require('./tools/getForms');
|
||||
const fillFormAuto = require('./tools/fillFormAuto');
|
||||
const agentFlow = require('./tools/agentFlow');
|
||||
const agentFlowV2 = require('./tools/agentFlowV2');
|
||||
const closeBrowser = require('./tools/closeBrowser');
|
||||
|
||||
// SERVER MCP
|
||||
const server = new Server(
|
||||
{
|
||||
name: "browser",
|
||||
version: "2.0.0"
|
||||
},
|
||||
{
|
||||
capabilities: {
|
||||
tools: {}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// LISTAR TOOLS
|
||||
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
||||
return {
|
||||
tools: [
|
||||
{ name: "open_url", description: "Abre uma URL", inputSchema: { type: "object", properties: { url: { type: "string" } }, required: ["url"] } },
|
||||
{ name: "click", description: "Clica por seletor", inputSchema: { type: "object", properties: { selector: { type: "string" } }, required: ["selector"] } },
|
||||
{ name: "type", description: "Digita texto", inputSchema: { type: "object", properties: { selector: { type: "string" }, text: { type: "string" } }, required: ["selector", "text"] } },
|
||||
{ name: "screenshot", description: "Screenshot", inputSchema: { type: "object", properties: { fullPage: { type: "boolean" } } } },
|
||||
{ name: "get_text", description: "Extrai texto", inputSchema: { type: "object", properties: { selector: { type: "string" } } } },
|
||||
{ name: "get_links", description: "Lista links", inputSchema: { type: "object", properties: { selector: { type: "string" } } } },
|
||||
{ name: "wait_for_selector", description: "Espera elemento", inputSchema: { type: "object", properties: { selector: { type: "string" }, timeout: { type: "number" } }, required: ["selector"] } },
|
||||
{ name: "extract_elements", description: "Extrai elementos", inputSchema: { type: "object", properties: { selector: { type: "string" }, attributes: { type: "array", items: { type: "string" } } }, required: ["selector"] } },
|
||||
{ name: "smart_click", description: "Clica por texto visível", inputSchema: { type: "object", properties: { text: { type: "string" } }, required: ["text"] } },
|
||||
{ name: "smart_type", description: "Digita por label/placeholder", inputSchema: { type: "object", properties: { label: { type: "string" }, text: { type: "string" } }, required: ["label", "text"] } },
|
||||
{ name: "get_buttons", description: "Lista botões disponíveis", inputSchema: { type: "object", properties: {} } },
|
||||
{ name: "smart_wait_navigation", description: "Espera mudança de página", inputSchema: { type: "object", properties: { timeout: { type: "number" } } } },
|
||||
{ name: "get_forms", description: "Lista formulários da página", inputSchema: { type: "object", properties: {} } },
|
||||
{ name: "fill_form_auto", description: "Preenche formulário automaticamente", inputSchema: { type: "object", properties: { data: { type: "object" } }, required: ["data"] } },
|
||||
{ name: "agent_flow", description: "Fluxo automático simples", inputSchema: { type: "object", properties: { goal: { type: "string" }, data: { type: "object" } }, required: ["goal"] } },
|
||||
{ name: "agent_flow_v2", description: "Fluxo automático avançado com retry e fallback", inputSchema: { type: "object", properties: { goal: { type: "string" }, data: { type: "object" }, retries: { type: "number" } }, required: ["goal"] } },
|
||||
{ name: "close_browser", description: "Fecha o navegador", inputSchema: { type: "object", properties: {} } }
|
||||
]
|
||||
};
|
||||
});
|
||||
|
||||
// EXECUTAR TOOL
|
||||
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||
const { name, arguments: args } = request.params;
|
||||
|
||||
const tools = {
|
||||
open_url: openUrl,
|
||||
click,
|
||||
type,
|
||||
screenshot,
|
||||
get_text: getText,
|
||||
get_links: getLinks,
|
||||
wait_for_selector: waitForSelector,
|
||||
extract_elements: extractElements,
|
||||
smart_click: smartClick,
|
||||
smart_type: smartType,
|
||||
get_buttons: getButtons,
|
||||
smart_wait_navigation: smartWaitNavigation,
|
||||
get_forms: getForms,
|
||||
fill_form_auto: fillFormAuto,
|
||||
agent_flow: agentFlow,
|
||||
agent_flow_v2: agentFlowV2,
|
||||
close_browser: closeBrowser
|
||||
};
|
||||
|
||||
if (!tools[name]) {
|
||||
throw new Error("Tool não encontrada");
|
||||
}
|
||||
|
||||
const result = await tools[name](args || {});
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: JSON.stringify(result)
|
||||
}
|
||||
]
|
||||
};
|
||||
});
|
||||
|
||||
// START SERVER
|
||||
const transport = new StdioServerTransport();
|
||||
server.connect(transport);
|
||||
@@ -0,0 +1,62 @@
|
||||
const openUrl = require('./openUrl');
|
||||
const smartClick = require('./smartClick');
|
||||
const smartType = require('./smartType');
|
||||
const fillFormAuto = require('./fillFormAuto');
|
||||
const smartWaitNavigation = require('./smartWaitNavigation');
|
||||
const getButtons = require('./getButtons');
|
||||
const getForms = require('./getForms');
|
||||
|
||||
module.exports = async ({ goal, data = {} }) => {
|
||||
if (!goal) {
|
||||
throw new Error('Goal é obrigatório');
|
||||
}
|
||||
|
||||
let log = [];
|
||||
|
||||
try {
|
||||
if (goal.includes('http')) {
|
||||
await openUrl({ url: goal });
|
||||
log.push("Abriu URL");
|
||||
}
|
||||
|
||||
if (Object.keys(data).length > 0) {
|
||||
const forms = await getForms();
|
||||
|
||||
if (forms.count > 0) {
|
||||
await fillFormAuto({ data });
|
||||
log.push("Formulário preenchido automaticamente");
|
||||
}
|
||||
}
|
||||
|
||||
const buttons = await getButtons();
|
||||
|
||||
const keywords = ['login', 'entrar', 'continuar', 'enviar', 'buscar', 'submit', 'ok', 'confirmar'];
|
||||
|
||||
for (const btn of buttons.buttons) {
|
||||
const match = keywords.find(k =>
|
||||
btn.text.toLowerCase().includes(k)
|
||||
);
|
||||
|
||||
if (match) {
|
||||
await smartClick({ text: btn.text });
|
||||
await smartWaitNavigation({});
|
||||
log.push(`Clicou em: ${btn.text}`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
action: "agent_flow",
|
||||
goal,
|
||||
steps: log
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
steps: log
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,85 @@
|
||||
const openUrl = require('./openUrl');
|
||||
const smartClick = require('./smartClick');
|
||||
const smartType = require('./smartType');
|
||||
const fillFormAuto = require('./fillFormAuto');
|
||||
const smartWaitNavigation = require('./smartWaitNavigation');
|
||||
const getButtons = require('./getButtons');
|
||||
const getForms = require('./getForms');
|
||||
|
||||
const delay = (ms) => new Promise(res => setTimeout(res, ms));
|
||||
|
||||
module.exports = async ({ goal, data = {}, retries = 3 }) => {
|
||||
if (!goal) {
|
||||
throw new Error('Goal é obrigatório');
|
||||
}
|
||||
|
||||
let log = [];
|
||||
let attempt = 0;
|
||||
|
||||
const keywords = ['login', 'entrar', 'continuar', 'enviar', 'buscar', 'submit', 'ok', 'confirmar'];
|
||||
|
||||
while (attempt < retries) {
|
||||
try {
|
||||
log.push(`Tentativa ${attempt + 1}`);
|
||||
|
||||
if (goal.includes('http')) {
|
||||
await openUrl({ url: goal });
|
||||
log.push("Abriu URL");
|
||||
await delay(1500);
|
||||
}
|
||||
|
||||
if (Object.keys(data).length > 0) {
|
||||
const forms = await getForms();
|
||||
|
||||
if (forms.count > 0) {
|
||||
await fillFormAuto({ data });
|
||||
log.push("Formulário preenchido");
|
||||
}
|
||||
}
|
||||
|
||||
const buttons = await getButtons();
|
||||
|
||||
let clicked = false;
|
||||
|
||||
for (const btn of buttons.buttons) {
|
||||
const match = keywords.find(k =>
|
||||
btn.text.toLowerCase().includes(k)
|
||||
);
|
||||
|
||||
if (match) {
|
||||
await smartClick({ text: btn.text });
|
||||
await smartWaitNavigation({});
|
||||
log.push(`Clicou em: ${btn.text}`);
|
||||
clicked = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!clicked && buttons.buttons.length > 0) {
|
||||
await smartClick({ text: buttons.buttons[0].text });
|
||||
await smartWaitNavigation({});
|
||||
log.push(`Fallback click: ${buttons.buttons[0].text}`);
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
action: "agent_flow_v2",
|
||||
attempts: attempt + 1,
|
||||
steps: log
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
log.push(`Erro: ${error.message}`);
|
||||
attempt++;
|
||||
|
||||
await delay(1000);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
action: "agent_flow_v2",
|
||||
attempts: attempt,
|
||||
steps: log
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,19 @@
|
||||
const browser = require('../browser');
|
||||
|
||||
module.exports = async ({ selector }) => {
|
||||
if (!selector) {
|
||||
throw new Error('Selector é obrigatório');
|
||||
}
|
||||
|
||||
await browser.start();
|
||||
const p = await browser.ensurePage();
|
||||
|
||||
await p.waitForSelector(selector, { state: 'visible', timeout: 10000 });
|
||||
await p.click(selector);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
action: "click",
|
||||
selector
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,11 @@
|
||||
const browser = require('../browser');
|
||||
|
||||
module.exports = async () => {
|
||||
await browser.close();
|
||||
|
||||
return {
|
||||
success: true,
|
||||
action: "close_browser",
|
||||
message: "Navegador fechado com sucesso"
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,36 @@
|
||||
const browser = require('../browser');
|
||||
|
||||
module.exports = async ({ selector, attributes = [] }) => {
|
||||
if (!selector) {
|
||||
throw new Error('Selector é obrigatório');
|
||||
}
|
||||
|
||||
await browser.start();
|
||||
const p = await browser.ensurePage();
|
||||
|
||||
const attrsStr = JSON.stringify(Array.isArray(attributes) ? attributes : []);
|
||||
|
||||
const elements = await p.evaluate((sel) => {
|
||||
const attrs = JSON.parse(sel.__attrs);
|
||||
const els = Array.from(document.querySelectorAll(sel.__sel));
|
||||
|
||||
return els.map(el => {
|
||||
const data = {
|
||||
text: (el.innerText || el.textContent || '').trim()
|
||||
};
|
||||
|
||||
for (let i = 0; i < attrs.length; i++) {
|
||||
data[attrs[i]] = el.getAttribute(attrs[i]);
|
||||
}
|
||||
|
||||
return data;
|
||||
});
|
||||
}, { __sel: selector, __attrs: attrsStr });
|
||||
|
||||
return {
|
||||
success: true,
|
||||
action: "extract_elements",
|
||||
count: elements.length,
|
||||
elements
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,62 @@
|
||||
const browser = require('../browser');
|
||||
|
||||
module.exports = async ({ data }) => {
|
||||
if (!data) {
|
||||
throw new Error('Data é obrigatória');
|
||||
}
|
||||
|
||||
await browser.start();
|
||||
const p = await browser.ensurePage();
|
||||
|
||||
const result = await p.evaluate((formData) => {
|
||||
const inputs = Array.from(document.querySelectorAll('input, textarea, select'));
|
||||
|
||||
let filled = [];
|
||||
|
||||
inputs.forEach(input => {
|
||||
const key = Object.keys(formData).find(k =>
|
||||
(input.name && input.name.toLowerCase().includes(k.toLowerCase())) ||
|
||||
(input.placeholder && input.placeholder.toLowerCase().includes(k.toLowerCase())) ||
|
||||
(input.id && input.id.toLowerCase().includes(k.toLowerCase())) ||
|
||||
(input.getAttribute('aria-label') || '').toLowerCase().includes(k.toLowerCase())
|
||||
);
|
||||
|
||||
if (key) {
|
||||
const value = formData[key];
|
||||
|
||||
const nativeInputValueSetter = Object.getOwnPropertyDescriptor(
|
||||
window.HTMLInputElement.prototype, 'value'
|
||||
)?.set;
|
||||
|
||||
const nativeTextAreaValueSetter = Object.getOwnPropertyDescriptor(
|
||||
window.HTMLTextAreaElement.prototype, 'value'
|
||||
)?.set;
|
||||
|
||||
if (input.tagName === 'INPUT' && nativeInputValueSetter) {
|
||||
nativeInputValueSetter.call(input, value);
|
||||
} else if (input.tagName === 'TEXTAREA' && nativeTextAreaValueSetter) {
|
||||
nativeTextAreaValueSetter.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: value
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return filled;
|
||||
}, data);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
action: "fill_form_auto",
|
||||
filled: result,
|
||||
count: result.length
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,23 @@
|
||||
const browser = require('../browser');
|
||||
|
||||
module.exports = async () => {
|
||||
await browser.start();
|
||||
const p = await browser.ensurePage();
|
||||
|
||||
const buttons = await p.evaluate(() => {
|
||||
const elements = Array.from(document.querySelectorAll('button, input[type="submit"], input[type="button"], [role="button"], a.btn, a.button'));
|
||||
|
||||
return elements.map(el => ({
|
||||
text: (el.innerText || el.value || el.getAttribute('aria-label') || '').trim(),
|
||||
type: el.tagName.toLowerCase(),
|
||||
id: el.id || null
|
||||
})).filter(b => b.text !== '');
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
action: "get_buttons",
|
||||
count: buttons.length,
|
||||
buttons
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,32 @@
|
||||
const browser = require('../browser');
|
||||
|
||||
module.exports = async () => {
|
||||
await browser.start();
|
||||
const p = await browser.ensurePage();
|
||||
|
||||
const forms = await p.evaluate(() => {
|
||||
const formElements = Array.from(document.querySelectorAll('form'));
|
||||
|
||||
return formElements.map(form => {
|
||||
const inputs = Array.from(form.querySelectorAll('input, textarea, select'));
|
||||
|
||||
return {
|
||||
action: form.action || null,
|
||||
method: form.method || 'GET',
|
||||
fields: inputs.map(input => ({
|
||||
name: input.name || null,
|
||||
type: input.type || input.tagName.toLowerCase(),
|
||||
placeholder: input.placeholder || '',
|
||||
id: input.id || null
|
||||
}))
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
action: "get_forms",
|
||||
count: forms.length,
|
||||
forms
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
const browser = require('../browser');
|
||||
|
||||
module.exports = async ({ selector = 'a' }) => {
|
||||
await browser.start();
|
||||
const p = await browser.ensurePage();
|
||||
|
||||
const links = await p.evaluate((sel) => {
|
||||
const elements = Array.from(document.querySelectorAll(sel));
|
||||
|
||||
return elements.map(el => ({
|
||||
text: (el.innerText || el.textContent || '').trim(),
|
||||
href: el.href || el.getAttribute('href') || ''
|
||||
})).filter(link => link.href && link.href !== '');
|
||||
}, selector);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
action: "get_links",
|
||||
count: links.length,
|
||||
links
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,21 @@
|
||||
const browser = require('../browser');
|
||||
|
||||
module.exports = async ({ selector = 'body' }) => {
|
||||
await browser.start();
|
||||
const p = await browser.ensurePage();
|
||||
|
||||
const text = await p.evaluate((sel) => {
|
||||
const el = document.querySelector(sel);
|
||||
if (!el) return null;
|
||||
|
||||
const t = el.innerText || el.textContent || '';
|
||||
return t.trim() || null;
|
||||
}, selector);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
action: "get_text",
|
||||
selector,
|
||||
text
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,56 @@
|
||||
const browser = require('../browser');
|
||||
|
||||
module.exports = async ({ url }) => {
|
||||
if (!url) {
|
||||
throw new Error('URL é obrigatória');
|
||||
}
|
||||
|
||||
await browser.start();
|
||||
const p = await browser.ensurePage();
|
||||
|
||||
const currentUrl = p.url();
|
||||
|
||||
if (currentUrl && currentUrl !== 'about:blank') {
|
||||
if (currentUrl.includes(new URL(url).hostname)) {
|
||||
await p.goto(url, {
|
||||
waitUntil: 'domcontentloaded',
|
||||
timeout: 30000
|
||||
});
|
||||
await p.waitForTimeout(1500);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
url,
|
||||
title: await p.title(),
|
||||
tab: 'reused'
|
||||
};
|
||||
}
|
||||
|
||||
const newPage = await browser.newTab();
|
||||
await newPage.goto(url, {
|
||||
waitUntil: 'domcontentloaded',
|
||||
timeout: 30000
|
||||
});
|
||||
await newPage.waitForTimeout(1500);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
url,
|
||||
title: await newPage.title(),
|
||||
tab: 'new'
|
||||
};
|
||||
}
|
||||
|
||||
await p.goto(url, {
|
||||
waitUntil: 'domcontentloaded',
|
||||
timeout: 30000
|
||||
});
|
||||
await p.waitForTimeout(1500);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
url,
|
||||
title: await p.title(),
|
||||
tab: 'existing'
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,16 @@
|
||||
const browser = require('../browser');
|
||||
|
||||
module.exports = async ({ fullPage = true }) => {
|
||||
await browser.start();
|
||||
const p = await browser.ensurePage();
|
||||
|
||||
const path = `screenshot-${Date.now()}.png`;
|
||||
|
||||
await p.screenshot({ path, fullPage });
|
||||
|
||||
return {
|
||||
success: true,
|
||||
action: "screenshot",
|
||||
file: path
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,54 @@
|
||||
const browser = require('../browser');
|
||||
|
||||
module.exports = async ({ text }) => {
|
||||
if (!text) {
|
||||
throw new Error('Texto é obrigatório');
|
||||
}
|
||||
|
||||
await browser.start();
|
||||
const p = await browser.ensurePage();
|
||||
|
||||
const clicked = await p.evaluate((targetText) => {
|
||||
const elements = Array.from(document.querySelectorAll(
|
||||
'a, button, input[type="submit"], input[type="button"], [role="button"], [onclick]'
|
||||
));
|
||||
|
||||
const el = elements.find(e => {
|
||||
const txt = (e.innerText || e.textContent || e.value || e.getAttribute('aria-label') || '').trim().toLowerCase();
|
||||
return txt.includes(targetText.toLowerCase());
|
||||
});
|
||||
|
||||
if (el) {
|
||||
el.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
el.click();
|
||||
|
||||
if (el.tagName === 'A' && el.href) {
|
||||
return { clicked: true, href: el.href };
|
||||
}
|
||||
|
||||
return { clicked: true, href: null };
|
||||
}
|
||||
|
||||
return { clicked: false, href: null };
|
||||
}, text);
|
||||
|
||||
if (!clicked.clicked) {
|
||||
throw new Error(`Elemento não encontrado pelo texto: "${text}"`);
|
||||
}
|
||||
|
||||
if (clicked.href) {
|
||||
try {
|
||||
await p.waitForNavigation({ timeout: 5000, waitUntil: 'domcontentloaded' });
|
||||
} catch (e) {
|
||||
}
|
||||
} else {
|
||||
await p.waitForTimeout(1000);
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
action: "smart_click",
|
||||
text,
|
||||
navigatedTo: clicked.href || null
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,84 @@
|
||||
const browser = require('../browser');
|
||||
|
||||
module.exports = async ({ label, text }) => {
|
||||
if (!label || !text) {
|
||||
throw new Error('Label e text são obrigatórios');
|
||||
}
|
||||
|
||||
await browser.start();
|
||||
const p = await browser.ensurePage();
|
||||
|
||||
const success = await p.evaluate(({ labelText, inputText }) => {
|
||||
function setInputValue(input, value) {
|
||||
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 }));
|
||||
}
|
||||
|
||||
const labels = Array.from(document.querySelectorAll('label'));
|
||||
|
||||
const foundLabel = labels.find(l =>
|
||||
(l.innerText || l.textContent || '').toLowerCase().includes(labelText.toLowerCase())
|
||||
);
|
||||
|
||||
if (foundLabel) {
|
||||
const forAttr = foundLabel.getAttribute('for');
|
||||
|
||||
if (forAttr) {
|
||||
const input = document.getElementById(forAttr);
|
||||
if (input) {
|
||||
setInputValue(input, inputText);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
const input = foundLabel.querySelector('input, textarea');
|
||||
if (input) {
|
||||
setInputValue(input, inputText);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
const inputs = Array.from(document.querySelectorAll('input, textarea'));
|
||||
|
||||
const foundInput = inputs.find(i =>
|
||||
(i.placeholder || '').toLowerCase().includes(labelText.toLowerCase()) ||
|
||||
(i.name || '').toLowerCase().includes(labelText.toLowerCase()) ||
|
||||
(i.id || '').toLowerCase().includes(labelText.toLowerCase()) ||
|
||||
(i.getAttribute('aria-label') || '').toLowerCase().includes(labelText.toLowerCase())
|
||||
);
|
||||
|
||||
if (foundInput) {
|
||||
setInputValue(foundInput, inputText);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}, { labelText: label, inputText: text });
|
||||
|
||||
if (!success) {
|
||||
throw new Error('Campo não encontrado');
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
action: "smart_type",
|
||||
label,
|
||||
text
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
const browser = require('../browser');
|
||||
|
||||
module.exports = async ({ timeout = 15000 }) => {
|
||||
await browser.start();
|
||||
const p = await browser.ensurePage();
|
||||
|
||||
const oldUrl = p.url();
|
||||
|
||||
try {
|
||||
await Promise.race([
|
||||
p.waitForNavigation({ timeout, waitUntil: 'domcontentloaded' }),
|
||||
p.waitForLoadState('domcontentloaded', { timeout })
|
||||
]);
|
||||
} catch (e) {
|
||||
}
|
||||
|
||||
const newUrl = p.url();
|
||||
|
||||
return {
|
||||
success: true,
|
||||
action: "smart_wait_navigation",
|
||||
oldUrl,
|
||||
newUrl,
|
||||
changed: oldUrl !== newUrl
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,20 @@
|
||||
const browser = require('../browser');
|
||||
|
||||
module.exports = async ({ selector, text }) => {
|
||||
if (!selector || !text) {
|
||||
throw new Error('Selector e text são obrigatórios');
|
||||
}
|
||||
|
||||
await browser.start();
|
||||
const p = await browser.ensurePage();
|
||||
|
||||
await p.waitForSelector(selector, { state: 'visible', timeout: 10000 });
|
||||
await p.fill(selector, text);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
action: "type",
|
||||
selector,
|
||||
text
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
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();
|
||||
|
||||
const el = await p.waitForSelector(selector, {
|
||||
timeout,
|
||||
state: 'attached'
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
action: "wait_for_selector",
|
||||
selector,
|
||||
found: !!el
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user