@diegosouzacdv/jev-browser-mcp 0.1.2 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -0
- package/config/ui-testing.json +40 -8
- package/docs/jev-browser-mcp.md +138 -39
- package/mcp_servers/jev-browser-npm/bin/jev-browser-mcp.cjs +4 -3
- package/mcp_servers/jev-browser-npm/src/config.mjs +141 -38
- package/mcp_servers/jev-browser-npm/src/flow.mjs +768 -226
- package/mcp_servers/jev-browser-npm/src/server.mjs +12 -10
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1712,6 +1712,16 @@ Valores digitados ficam no Playwright e são removidos do conteúdo enviado ao
|
|
|
1712
1712
|
Jev. Não coloque senhas, tokens ou outros segredos na descrição do fluxo ou
|
|
1713
1713
|
nos critérios.
|
|
1714
1714
|
|
|
1715
|
+
O contrato também oferece `select_option`, `hover`, teclas `Enter`/`Escape`/`Tab`,
|
|
1716
|
+
asserções por elemento (`assert_text`, `assert_value`, `assert_visible`,
|
|
1717
|
+
`assert_hidden`) e alvos nomeados em iframes. `upload_file` aceita inputs,
|
|
1718
|
+
botões que abrem o seletor nativo e dropzones; configure `JEV_BROWSER_UPLOAD_ROOT`
|
|
1719
|
+
para limitar os arquivos locais que o MCP pode ler. Um único plano usa o
|
|
1720
|
+
fast-path por padrão e não chama a API Decisions. `options` pode limitar o
|
|
1721
|
+
snapshot a `main` ou `dialog`, bloquear rastreadores, capturar erros de console
|
|
1722
|
+
e rede e salvar screenshot/trace local em falhas. Os caminhos e limites estão
|
|
1723
|
+
descritos no [guia completo](docs/jev-browser-mcp.md).
|
|
1724
|
+
|
|
1715
1725
|
A resposta informa o status, o plano escolhido, as ações executadas, a captura
|
|
1716
1726
|
final e se o resultado esperado foi confirmado. Consulte o [guia completo do
|
|
1717
1727
|
MCP Jev Browser](docs/jev-browser-mcp.md) para schemas, limites, tempos e
|
package/config/ui-testing.json
CHANGED
|
@@ -5,9 +5,9 @@
|
|
|
5
5
|
"harness_browser": "chrome",
|
|
6
6
|
"playwright_mcp_package": "@playwright/mcp@0.0.79",
|
|
7
7
|
"computer_browser": "chrome",
|
|
8
|
-
"computer_user_data_dir": "~/.cache/orquestrador/jev-browser-{browser}",
|
|
9
|
-
"max_flow_steps": 24,
|
|
10
|
-
"max_text_entry_chars": 2000
|
|
8
|
+
"computer_user_data_dir": "~/.cache/orquestrador/jev-browser-{browser}",
|
|
9
|
+
"max_flow_steps": 24,
|
|
10
|
+
"max_text_entry_chars": 2000
|
|
11
11
|
},
|
|
12
12
|
"jev": {
|
|
13
13
|
"provider_url": "https://openrouter.ai/api/alpha/decisions",
|
|
@@ -16,8 +16,40 @@
|
|
|
16
16
|
"request_timeout_seconds": 30,
|
|
17
17
|
"max_flow_chars": 12000,
|
|
18
18
|
"max_snapshot_chars": 12000,
|
|
19
|
-
"max_action_count": 32,
|
|
20
|
-
"max_action_description_chars": 300,
|
|
21
|
-
"max_response_bytes": 65536
|
|
22
|
-
}
|
|
23
|
-
|
|
19
|
+
"max_action_count": 32,
|
|
20
|
+
"max_action_description_chars": 300,
|
|
21
|
+
"max_response_bytes": 65536
|
|
22
|
+
},
|
|
23
|
+
"jev_browser_mcp": {
|
|
24
|
+
"browser": {
|
|
25
|
+
"max_action_timeout_seconds": 8,
|
|
26
|
+
"max_upload_files": 5,
|
|
27
|
+
"max_upload_path_chars": 4096,
|
|
28
|
+
"max_upload_file_bytes": 10485760,
|
|
29
|
+
"max_upload_total_bytes": 26214400,
|
|
30
|
+
"upload_root_env": "JEV_BROWSER_UPLOAD_ROOT",
|
|
31
|
+
"artifact_directory": "~/.cache/orquestrador/jev-browser-artifacts",
|
|
32
|
+
"artifact_directory_env": "JEV_BROWSER_ARTIFACT_DIR",
|
|
33
|
+
"max_frame_snapshots": 5,
|
|
34
|
+
"fast_path_single_plan_default": true,
|
|
35
|
+
"screenshot_on_failure_default": true,
|
|
36
|
+
"trace_on_failure_default": false,
|
|
37
|
+
"capture_console_errors_default": true,
|
|
38
|
+
"capture_network_errors_default": false,
|
|
39
|
+
"block_trackers_default": false,
|
|
40
|
+
"snapshot_scope_default": "body",
|
|
41
|
+
"tracker_host_suffixes": [
|
|
42
|
+
"google-analytics.com",
|
|
43
|
+
"googletagmanager.com",
|
|
44
|
+
"hotjar.com",
|
|
45
|
+
"fonts.googleapis.com",
|
|
46
|
+
"fonts.gstatic.com"
|
|
47
|
+
],
|
|
48
|
+
"blocked_resource_types": ["font", "media"]
|
|
49
|
+
},
|
|
50
|
+
"jev": {
|
|
51
|
+
"max_diagnostic_items": 50,
|
|
52
|
+
"max_diagnostic_chars": 500
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
package/docs/jev-browser-mcp.md
CHANGED
|
@@ -6,7 +6,8 @@ O repositório também contém um pacote Node independente do harness. Ele fala
|
|
|
6
6
|
MCP por `stdio`, executa o Playwright no mesmo processo e pode ser iniciado por
|
|
7
7
|
qualquer harness que aceite `command` e `args` para um servidor MCP. O pacote
|
|
8
8
|
usa o mesmo `config/ui-testing.json` deste repositório; não precisa instalar o
|
|
9
|
-
Python do orquestrador.
|
|
9
|
+
Python do orquestrador. As opções específicas do pacote ficam isoladas no
|
|
10
|
+
bloco `jev_browser_mcp` para preservar o contrato do servidor Python.
|
|
10
11
|
|
|
11
12
|
Instale a versão publicada do npm diretamente no harness. Para fixar uma
|
|
12
13
|
versão em produção, use o número explícito no argumento do pacote:
|
|
@@ -16,7 +17,7 @@ versão em produção, use o número explícito no argumento do pacote:
|
|
|
16
17
|
"mcpServers": {
|
|
17
18
|
"jev-browser": {
|
|
18
19
|
"command": "npx",
|
|
19
|
-
"args": ["--yes", "@diegosouzacdv/jev-browser-mcp@0.
|
|
20
|
+
"args": ["--yes", "@diegosouzacdv/jev-browser-mcp@0.2.0"],
|
|
20
21
|
"env": {
|
|
21
22
|
"OPENROUTER_API_KEY": "${OPENROUTER_API_KEY}",
|
|
22
23
|
"JEV_BROWSER_MODE": "harness"
|
|
@@ -58,12 +59,14 @@ somente quando precisar experimentar uma revisão ainda não publicada.
|
|
|
58
59
|
|
|
59
60
|
O executável oferece `choose_next_action` e `run_browser_flow`, mantém uma
|
|
60
61
|
sessão do browser por processo e reutiliza essa sessão entre chamadas. O plano
|
|
61
|
-
passado ao Jev continua declarativo
|
|
62
|
-
|
|
63
|
-
reações idempotentes a um comentário único.
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
62
|
+
passado ao Jev continua declarativo: clique, preenchimento, seleção nativa,
|
|
63
|
+
hover, espera, teclas aprovadas, asserções por elemento, upload restrito a uma
|
|
64
|
+
raiz local configurada e reações idempotentes a um comentário único. Também
|
|
65
|
+
aceita um nome de iframe para as ações que ocorrem dentro dele. Não aceita
|
|
66
|
+
JavaScript enviado pelo harness, seletores livres nem coordenadas. Ele usa a
|
|
67
|
+
biblioteca Playwright diretamente, sem iniciar um segundo servidor MCP do
|
|
68
|
+
Playwright. O transporte MCP usa `stdio`; toda saída de diagnóstico vai para
|
|
69
|
+
`stderr` para não misturar com JSON-RPC.
|
|
67
70
|
|
|
68
71
|
O modo `computer` grava cookies no perfil exclusivo configurado, e conteúdo da
|
|
69
72
|
página pode conter instruções maliciosas. O Jev recebe a captura acessível com
|
|
@@ -77,16 +80,59 @@ a página inicial, captura o snapshot acessível e pede ao Jev que escolha um
|
|
|
77
80
|
plano. Em seguida, executa o plano inteiro na mesma sessão e confere o
|
|
78
81
|
resultado esperado na tela.
|
|
79
82
|
|
|
80
|
-
Cada plano pode usar
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
83
|
+
Cada plano pode usar:
|
|
84
|
+
|
|
85
|
+
- `click`, `type`, `hover` e `select_option` com papel/nome acessível exatos;
|
|
86
|
+
- `wait_for_text` e `wait_for_condition` (`network_idle`, limitado pelo timeout
|
|
87
|
+
de ação configurado);
|
|
88
|
+
- `press_key` com `PageDown`, `PageUp`, `Home`, `End`, `ArrowDown`, `ArrowUp`,
|
|
89
|
+
`Enter`, `Escape` ou `Tab`;
|
|
90
|
+
- `assert_text`, `assert_value`, `assert_visible` e `assert_hidden`;
|
|
91
|
+
- `upload_file` para input rotulado, botão que abre o seletor de arquivo ou
|
|
92
|
+
dropzone;
|
|
93
|
+
- `like_comment` e `unlike_comment`, que localizam uma linha pelo autor e texto,
|
|
94
|
+
não repetem uma reação já no estado pedido e distinguem `Curtir` de
|
|
95
|
+
`Descurtir`.
|
|
96
|
+
|
|
97
|
+
Para ações em iframe, acrescente `"frame": "payment-iframe"`; o valor precisa
|
|
98
|
+
corresponder ao atributo `name` ou `title` do iframe. O snapshot inclui o
|
|
99
|
+
conteúdo acessível dos iframes nomeados dentro do escopo, limitado pela
|
|
100
|
+
configuração. Se um alvo estiver ausente, o erro inclui até três nomes acessíveis
|
|
101
|
+
próximos quando o snapshot os encontrar. O plano não aceita JavaScript enviado
|
|
102
|
+
pelo harness, coordenadas ou seletores livres.
|
|
103
|
+
|
|
104
|
+
Os aliases `value` → `text` em `type`/`wait_for_text` e `value` → `expected` em
|
|
105
|
+
asserções são aceitos. `comment` também é aceito em planos e passos, mas é
|
|
106
|
+
ignorado e não é enviado ao Jev. Campos fora do contrato continuam sendo
|
|
107
|
+
recusados.
|
|
108
|
+
|
|
109
|
+
### Upload de arquivos
|
|
110
|
+
|
|
111
|
+
Defina `JEV_BROWSER_UPLOAD_ROOT` como uma pasta absoluta que contenha os arquivos
|
|
112
|
+
de fixture. Cada caminho passado ao fluxo também precisa ser absoluto; o MCP
|
|
113
|
+
resolve links simbólicos e recusa qualquer arquivo fora dessa raiz. Só aceita
|
|
114
|
+
arquivos regulares, até 5 arquivos por passo, 10 MiB por arquivo e 25 MiB no
|
|
115
|
+
total do passo, conforme `jev_browser_mcp.browser` em `config/ui-testing.json`.
|
|
116
|
+
Os bytes são lidos e validados no processo local antes de serem entregues ao
|
|
117
|
+
Playwright. O MCP não envia esses bytes ao Jev nem os inclui diretamente na
|
|
118
|
+
resposta; texto que o próprio site exibir na interface ainda pode aparecer no
|
|
119
|
+
snapshot devolvido ao harness.
|
|
120
|
+
|
|
121
|
+
```json
|
|
122
|
+
{"action":"upload_file","target":"input","label":"Nota fiscal","file_path":"/fixtures/nota.pdf"}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Para uma área de arrastar, use o papel e o nome acessível da dropzone. Para um
|
|
126
|
+
botão que abre a janela nativa, use `target: "button"`; sem `target`, a presença
|
|
127
|
+
de `label` seleciona o input e `role`/`name` seleciona esse botão.
|
|
128
|
+
|
|
129
|
+
```json
|
|
130
|
+
{"action":"upload_file","target":"dropzone","role":"region","name":"Anexos","file_paths":["/fixtures/a.pdf","/fixtures/b.png"]}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Se `JEV_BROWSER_UPLOAD_ROOT` não estiver definido, a ação recusa a execução. O
|
|
134
|
+
limite evita que um plano transforme o MCP em leitor arbitrário de arquivos do
|
|
135
|
+
computador.
|
|
90
136
|
|
|
91
137
|
Exemplo de chamada:
|
|
92
138
|
|
|
@@ -95,13 +141,20 @@ Exemplo de chamada:
|
|
|
95
141
|
"flow": "Adicionar o produto ao carrinho e confirmar o resumo",
|
|
96
142
|
"initial_url": "http://127.0.0.1:4173/products/coffee",
|
|
97
143
|
"expected_outcome": "Coffee added to cart",
|
|
144
|
+
"options": {
|
|
145
|
+
"fast_path": true,
|
|
146
|
+
"snapshot_scope": "main",
|
|
147
|
+
"capture_network_errors": true,
|
|
148
|
+
"screenshot_on_failure": true
|
|
149
|
+
},
|
|
98
150
|
"candidate_plans": {
|
|
99
151
|
"add_and_confirm": {
|
|
100
152
|
"description": "Adicionar o produto visível ao carrinho e abrir o resumo",
|
|
101
153
|
"steps": [
|
|
102
154
|
{"action": "click", "role": "button", "name": "Add to cart"},
|
|
103
155
|
{"action": "wait_for_text", "text": "Coffee added to cart"},
|
|
104
|
-
{"action": "click", "role": "link", "name": "View cart"}
|
|
156
|
+
{"action": "click", "role": "link", "name": "View cart"},
|
|
157
|
+
{"action": "assert_text", "role": "heading", "name": "Order summary", "expected": "Coffee"}
|
|
105
158
|
]
|
|
106
159
|
}
|
|
107
160
|
}
|
|
@@ -123,9 +176,11 @@ descritos antes da execução.
|
|
|
123
176
|
|
|
124
177
|
## Configuração
|
|
125
178
|
|
|
126
|
-
Edite `config/ui-testing.json`.
|
|
127
|
-
|
|
128
|
-
|
|
179
|
+
Edite `config/ui-testing.json`. URL, modelo do provedor, variável de credencial
|
|
180
|
+
e limites compartilhados ficam nos blocos `browser` e `jev`. As opções próprias
|
|
181
|
+
do pacote Node ficam no bloco opcional `jev_browser_mcp`, que não altera o
|
|
182
|
+
contrato lido pelo servidor Python. Preserve a estrutura completa exigida pelos
|
|
183
|
+
validadores.
|
|
129
184
|
|
|
130
185
|
`browser.mode` aceita `harness` ou `computer`:
|
|
131
186
|
|
|
@@ -139,8 +194,42 @@ preserve a estrutura completa exigida pelo validador.
|
|
|
139
194
|
`browser.max_flow_steps` limita a soma de passos declarados entre os planos e
|
|
140
195
|
`browser.max_text_entry_chars` limita cada valor digitado. O resultado contém
|
|
141
196
|
`status`, o plano escolhido, as ações executadas, a última captura acessível e
|
|
142
|
-
se o critério esperado apareceu.
|
|
143
|
-
|
|
197
|
+
se o critério esperado apareceu. Quando existem asserções explícitas, `status`
|
|
198
|
+
também pode ser `passed` com todas elas satisfeitas; `assertions_passed` registra
|
|
199
|
+
esse resultado e `expected_outcome_visible` continua descrevendo somente o
|
|
200
|
+
texto global. `incomplete` significa que nenhum critério foi comprovado;
|
|
201
|
+
confiança do Jev não substitui essa verificação.
|
|
202
|
+
|
|
203
|
+
`jev_browser_mcp.browser.max_action_timeout_seconds` limita esperas por ações,
|
|
204
|
+
seletores e condições. `JEV_BROWSER_UPLOAD_ROOT` libera uploads somente dentro
|
|
205
|
+
de uma pasta absoluta escolhida pelo operador.
|
|
206
|
+
`jev_browser_mcp.browser.max_upload_files`, `max_upload_path_chars`,
|
|
207
|
+
`max_upload_file_bytes` e `max_upload_total_bytes` limitam quantidade e tamanho.
|
|
208
|
+
Não configure a raiz como o disco inteiro ou a pasta home: use uma pasta de
|
|
209
|
+
fixtures dedicada.
|
|
210
|
+
|
|
211
|
+
O `run_browser_flow` aceita `options` com `fast_path`, `snapshot_scope`,
|
|
212
|
+
`block_trackers`, `capture_console_errors`, `capture_network_errors`,
|
|
213
|
+
`screenshot_on_failure` e `trace_on_failure`. Sem override, os padrões são lidos
|
|
214
|
+
de `jev_browser_mcp` em `config/ui-testing.json`: um plano candidato pula a chamada Decisions;
|
|
215
|
+
captura de console e screenshot de falha ficam ligadas; trace, rede e bloqueio
|
|
216
|
+
de recursos ficam desligados. `snapshot_scope` aceita `body`, `main` ou `dialog`.
|
|
217
|
+
|
|
218
|
+
`block_trackers: true` bloqueia os domínios e tipos de recurso listados na
|
|
219
|
+
configuração (analytics, Hotjar, fontes externas e mídia). Isso pode alterar o
|
|
220
|
+
layout ou o comportamento do site, então a opção é desligada por padrão.
|
|
221
|
+
|
|
222
|
+
Com `capture_console_errors` e `capture_network_errors`, o retorno traz
|
|
223
|
+
`console_errors` e `network_failures`, limitados em quantidade e tamanho.
|
|
224
|
+
Respostas HTTP 4xx/5xx e requisições que falham são listadas; query strings,
|
|
225
|
+
fragmentos, valores de formulário e nomes de arquivo são removidos ou
|
|
226
|
+
sanitizados. Em falhas, `screenshot_on_failure` salva screenshot local e retorna
|
|
227
|
+
`screenshot_path`. `trace_on_failure: true` também grava um `.zip` compatível
|
|
228
|
+
com o Trace Viewer do Playwright e retorna `trace_path`. O diretório padrão é
|
|
229
|
+
`~/.cache/orquestrador/jev-browser-artifacts`; `JEV_BROWSER_ARTIFACT_DIR` pode
|
|
230
|
+
substituí-lo por uma pasta absoluta fora do pacote. Screenshots e traces podem
|
|
231
|
+
conter dados visíveis da aplicação: mantenha o diretório local protegido e
|
|
232
|
+
compartilhe os arquivos somente se o teste permitir.
|
|
144
233
|
|
|
145
234
|
`harness_browser` e `computer_browser` aceitam `chrome` ou `msedge`.
|
|
146
235
|
`computer_user_data_dir` deve ficar fora do repositório e conter `{browser}`;
|
|
@@ -162,16 +251,20 @@ Quando a versão configurada mudar, aqueça o novo pacote uma vez.
|
|
|
162
251
|
|
|
163
252
|
## Desempenho e evidência
|
|
164
253
|
|
|
165
|
-
O
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
`
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
254
|
+
O pacote Node usa a decisão remota do Jev para escolher entre múltiplos planos.
|
|
255
|
+
Com exatamente um plano e `fast_path` ligado (padrão), executa esse plano sem
|
|
256
|
+
chamar a API Decisions; `jev_decisions` fica em zero. Inclua o fluxo completo em
|
|
257
|
+
um plano candidato para evitar chamadas separadas ao harness; seleção,
|
|
258
|
+
navegação, ações e asserções ficam em uma chamada MCP. O servidor Python
|
|
259
|
+
`mcp_servers/jev_browser_server.py` mantém seu fluxo próprio e usa uma decisão
|
|
260
|
+
remota do Jev por chamada. A sessão Playwright fecha antes de o MCP Python
|
|
261
|
+
retornar. O perfil do modo `computer` preserva o login para chamadas seguintes;
|
|
262
|
+
nesse servidor o processo e a janela não são reutilizados. O pacote Node mantém
|
|
263
|
+
o browser aquecido até o harness encerrar o processo. O snapshot enviado ao Jev
|
|
264
|
+
e devolvido ao harness remove o rodapé e, se ainda exceder o limite, mantém o
|
|
265
|
+
início e o fim da captura com um marcador de truncamento. `snapshot_scope` pode
|
|
266
|
+
limitar a captura a `main` ou `dialog`; iframes nomeados contidos nesse escopo
|
|
267
|
+
também podem ser incluídos.
|
|
175
268
|
|
|
176
269
|
A chamada composta aquecida deve ficar dentro da meta de 20 segundos em páginas
|
|
177
270
|
que respondem normalmente; a inicialização fria, páginas lentas, MFA e conteúdo
|
|
@@ -181,11 +274,17 @@ para localizar o custo. O teto observado em um fluxo sintético local anterior
|
|
|
181
274
|
foi 6,411 s; isso não mede o Instagram nem garante o mesmo tempo em outros sites.
|
|
182
275
|
|
|
183
276
|
O snapshot inicial, o fluxo, o resultado esperado e as descrições dos planos
|
|
184
|
-
são enviados ao endpoint Decisions
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
277
|
+
são enviados ao endpoint Decisions quando há mais de uma opção. Com fast-path,
|
|
278
|
+
um plano não gera chamada remota. Os passos, valores digitados, valores
|
|
279
|
+
esperados pelas asserções, caminhos e conteúdo dos arquivos não são enviados.
|
|
280
|
+
Não inclua segredos em `flow`, `expected_outcome` ou nas descrições. A resposta
|
|
281
|
+
retorna o plano escolhido quando houver decisão, custo/confiança do provedor
|
|
282
|
+
quando disponíveis e o snapshot final sanitizado. O fluxo passa quando o texto
|
|
283
|
+
esperado aparece no snapshot ou quando todas as asserções declaradas passam.
|
|
284
|
+
|
|
285
|
+
`network_idle` é uma espera limitada e opcional; páginas com polling ou conexões
|
|
286
|
+
contínuas podem atingir o timeout. Prefira `wait_for_text` e asserções de
|
|
287
|
+
elemento quando houver um sinal de interface específico.
|
|
189
288
|
|
|
190
289
|
Consulte a [introdução do Jev](https://docs.typesafe.ai/introduction) e o
|
|
191
290
|
[tutorial da API Decisions no OpenRouter](https://openrouter.ai/docs/guides/community/jev-tutorial)
|
|
@@ -9,9 +9,10 @@ Usage:
|
|
|
9
9
|
Install the configured browser for harness mode
|
|
10
10
|
jev-browser-mcp --help Show this help
|
|
11
11
|
|
|
12
|
-
Configuration is read from config/ui-testing.json. Set OPENROUTER_API_KEY in
|
|
13
|
-
the environment and use JEV_BROWSER_MODE=harness or computer to choose a browser.
|
|
14
|
-
|
|
12
|
+
Configuration is read from config/ui-testing.json. Set OPENROUTER_API_KEY in
|
|
13
|
+
the environment and use JEV_BROWSER_MODE=harness or computer to choose a browser.
|
|
14
|
+
For upload_file, set JEV_BROWSER_UPLOAD_ROOT to a dedicated fixture directory.
|
|
15
|
+
`;
|
|
15
16
|
|
|
16
17
|
async function main() {
|
|
17
18
|
const args = process.argv.slice(2);
|
|
@@ -4,19 +4,39 @@ import path from "node:path";
|
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
|
|
6
6
|
const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../..");
|
|
7
|
-
const CONFIG_PATH = path.join(PACKAGE_ROOT, "config", "ui-testing.json");
|
|
8
|
-
const CONFIG_KEYS = {
|
|
9
|
-
root: ["version", "browser", "jev"],
|
|
10
|
-
browser: [
|
|
7
|
+
const CONFIG_PATH = path.join(PACKAGE_ROOT, "config", "ui-testing.json");
|
|
8
|
+
const CONFIG_KEYS = {
|
|
9
|
+
root: ["version", "browser", "jev", "jev_browser_mcp"],
|
|
10
|
+
browser: [
|
|
11
11
|
"mode",
|
|
12
12
|
"harness_browser",
|
|
13
13
|
"computer_browser",
|
|
14
14
|
"playwright_mcp_package",
|
|
15
|
-
"computer_user_data_dir",
|
|
16
|
-
"max_flow_steps",
|
|
17
|
-
"max_text_entry_chars",
|
|
15
|
+
"computer_user_data_dir",
|
|
16
|
+
"max_flow_steps",
|
|
17
|
+
"max_text_entry_chars",
|
|
18
|
+
],
|
|
19
|
+
nodeBrowser: [
|
|
20
|
+
"max_action_timeout_seconds",
|
|
21
|
+
"max_upload_files",
|
|
22
|
+
"max_upload_path_chars",
|
|
23
|
+
"max_upload_file_bytes",
|
|
24
|
+
"max_upload_total_bytes",
|
|
25
|
+
"upload_root_env",
|
|
26
|
+
"artifact_directory",
|
|
27
|
+
"artifact_directory_env",
|
|
28
|
+
"max_frame_snapshots",
|
|
29
|
+
"fast_path_single_plan_default",
|
|
30
|
+
"screenshot_on_failure_default",
|
|
31
|
+
"trace_on_failure_default",
|
|
32
|
+
"capture_console_errors_default",
|
|
33
|
+
"capture_network_errors_default",
|
|
34
|
+
"block_trackers_default",
|
|
35
|
+
"snapshot_scope_default",
|
|
36
|
+
"tracker_host_suffixes",
|
|
37
|
+
"blocked_resource_types",
|
|
18
38
|
],
|
|
19
|
-
jev: [
|
|
39
|
+
jev: [
|
|
20
40
|
"provider_url",
|
|
21
41
|
"credential_env",
|
|
22
42
|
"model",
|
|
@@ -24,8 +44,12 @@ const CONFIG_KEYS = {
|
|
|
24
44
|
"max_flow_chars",
|
|
25
45
|
"max_snapshot_chars",
|
|
26
46
|
"max_action_count",
|
|
27
|
-
"max_action_description_chars",
|
|
28
|
-
"max_response_bytes",
|
|
47
|
+
"max_action_description_chars",
|
|
48
|
+
"max_response_bytes",
|
|
49
|
+
],
|
|
50
|
+
nodeJev: [
|
|
51
|
+
"max_diagnostic_items",
|
|
52
|
+
"max_diagnostic_chars",
|
|
29
53
|
],
|
|
30
54
|
};
|
|
31
55
|
|
|
@@ -49,13 +73,52 @@ function requireKeys(value, keys, scope) {
|
|
|
49
73
|
}
|
|
50
74
|
}
|
|
51
75
|
|
|
52
|
-
function positiveNumber(value, name, integer = false) {
|
|
76
|
+
function positiveNumber(value, name, integer = false) {
|
|
53
77
|
const parsed = Number(value);
|
|
54
78
|
if (!Number.isFinite(parsed) || parsed <= 0 || (integer && !Number.isInteger(parsed))) {
|
|
55
79
|
throw new JevBrowserError(`config/ui-testing.json ${name} must be a positive ${integer ? "integer" : "number"}`);
|
|
56
80
|
}
|
|
57
81
|
return parsed;
|
|
58
|
-
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function expandHomeDirectory(value) {
|
|
85
|
+
if (value.startsWith("~/") || value.startsWith("~\\")) return path.join(homedir(), value.slice(2));
|
|
86
|
+
return value;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function requireOutsidePackage(resolved, name) {
|
|
90
|
+
const insidePackage = path.relative(PACKAGE_ROOT, resolved);
|
|
91
|
+
if (insidePackage === "" || (!insidePackage.startsWith(`..${path.sep}`) && insidePackage !== ".." && !path.isAbsolute(insidePackage))) {
|
|
92
|
+
throw new JevBrowserError(`${name} must be outside the installed package directory`);
|
|
93
|
+
}
|
|
94
|
+
return resolved;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function configuredDirectory(value, name) {
|
|
98
|
+
if (typeof value !== "string" || !value.trim()) throw new JevBrowserError(`config/ui-testing.json ${name} must be a directory path`);
|
|
99
|
+
const expanded = expandHomeDirectory(value.trim());
|
|
100
|
+
if (!path.isAbsolute(expanded)) throw new JevBrowserError(`config/ui-testing.json ${name} must be absolute or start with ~`);
|
|
101
|
+
return path.resolve(expanded);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function environmentName(value, name) {
|
|
105
|
+
if (typeof value !== "string" || !/^[A-Z][A-Z0-9_]*$/.test(value)) {
|
|
106
|
+
throw new JevBrowserError(`config/ui-testing.json ${name} must be an environment variable name`);
|
|
107
|
+
}
|
|
108
|
+
return value;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function booleanValue(value, name) {
|
|
112
|
+
if (typeof value !== "boolean") throw new JevBrowserError(`config/ui-testing.json ${name} must be a boolean`);
|
|
113
|
+
return value;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function stringList(value, name, validator = () => true) {
|
|
117
|
+
if (!Array.isArray(value) || value.some((item) => typeof item !== "string" || !item.trim() || !validator(item))) {
|
|
118
|
+
throw new JevBrowserError(`config/ui-testing.json ${name} must be an array of valid strings`);
|
|
119
|
+
}
|
|
120
|
+
return value.map((item) => item.trim());
|
|
121
|
+
}
|
|
59
122
|
|
|
60
123
|
function validProviderUrl(value) {
|
|
61
124
|
let parsed;
|
|
@@ -70,18 +133,10 @@ function validProviderUrl(value) {
|
|
|
70
133
|
return parsed.toString();
|
|
71
134
|
}
|
|
72
135
|
|
|
73
|
-
function profileDirectory(template, channel, override) {
|
|
74
|
-
|
|
75
|
-
if (
|
|
76
|
-
|
|
77
|
-
}
|
|
78
|
-
if (!path.isAbsolute(value)) throw new JevBrowserError("JEV_BROWSER_PROFILE must be an absolute path");
|
|
79
|
-
const resolved = path.resolve(value);
|
|
80
|
-
const insidePackage = path.relative(PACKAGE_ROOT, resolved);
|
|
81
|
-
if (insidePackage === "" || (!insidePackage.startsWith(`..${path.sep}`) && insidePackage !== ".." && !path.isAbsolute(insidePackage))) {
|
|
82
|
-
throw new JevBrowserError("browser profile must be outside the installed package directory");
|
|
83
|
-
}
|
|
84
|
-
return resolved;
|
|
136
|
+
function profileDirectory(template, channel, override) {
|
|
137
|
+
const value = expandHomeDirectory(override?.trim() || template.replace("{browser}", channel));
|
|
138
|
+
if (!path.isAbsolute(value)) throw new JevBrowserError("JEV_BROWSER_PROFILE must be an absolute path");
|
|
139
|
+
return requireOutsidePackage(path.resolve(value), "browser profile");
|
|
85
140
|
}
|
|
86
141
|
|
|
87
142
|
export function loadSettings({ env = process.env, configPath = CONFIG_PATH } = {}) {
|
|
@@ -91,9 +146,14 @@ export function loadSettings({ env = process.env, configPath = CONFIG_PATH } = {
|
|
|
91
146
|
} catch {
|
|
92
147
|
throw new JevBrowserError("config/ui-testing.json is unreadable or invalid JSON");
|
|
93
148
|
}
|
|
94
|
-
requireKeys(document, CONFIG_KEYS.root, "root");
|
|
95
|
-
requireKeys(document.browser, CONFIG_KEYS.browser, "browser");
|
|
96
|
-
requireKeys(document.jev, CONFIG_KEYS.jev, "jev");
|
|
149
|
+
requireKeys(document, CONFIG_KEYS.root, "root");
|
|
150
|
+
requireKeys(document.browser, CONFIG_KEYS.browser, "browser");
|
|
151
|
+
requireKeys(document.jev, CONFIG_KEYS.jev, "jev");
|
|
152
|
+
requireKeys(document.jev_browser_mcp, ["browser", "jev"], "jev_browser_mcp");
|
|
153
|
+
requireKeys(document.jev_browser_mcp.browser, CONFIG_KEYS.nodeBrowser, "jev_browser_mcp.browser");
|
|
154
|
+
requireKeys(document.jev_browser_mcp.jev, CONFIG_KEYS.nodeJev, "jev_browser_mcp.jev");
|
|
155
|
+
const browserOptions = document.jev_browser_mcp.browser;
|
|
156
|
+
const jevOptions = document.jev_browser_mcp.jev;
|
|
97
157
|
if (document.version !== 1) throw new JevBrowserError("config/ui-testing.json has an unsupported version");
|
|
98
158
|
|
|
99
159
|
if (!new Set(["harness", "computer"]).has(document.browser.mode)) {
|
|
@@ -116,23 +176,45 @@ export function loadSettings({ env = process.env, configPath = CONFIG_PATH } = {
|
|
|
116
176
|
if (!new Set(["chrome", "msedge"]).has(channel)) {
|
|
117
177
|
throw new JevBrowserError("JEV_BROWSER_CHANNEL must be chrome or msedge");
|
|
118
178
|
}
|
|
119
|
-
const profileTemplate = document.browser.computer_user_data_dir;
|
|
179
|
+
const profileTemplate = document.browser.computer_user_data_dir;
|
|
120
180
|
const profileRemainder = typeof profileTemplate === "string" ? profileTemplate.replace("{browser}", "") : "";
|
|
121
181
|
if (typeof profileTemplate !== "string" || profileTemplate.split("{browser}").length !== 2
|
|
122
182
|
|| profileRemainder.includes("{") || profileRemainder.includes("}")) {
|
|
123
183
|
throw new JevBrowserError("config/ui-testing.json browser.computer_user_data_dir must contain {browser}");
|
|
124
184
|
}
|
|
125
185
|
|
|
126
|
-
const configuredProviderUrl = validProviderUrl(document.jev.provider_url);
|
|
186
|
+
const configuredProviderUrl = validProviderUrl(document.jev.provider_url);
|
|
127
187
|
const providerUrl = validProviderUrl(env.JEV_PROVIDER_URL?.trim() || configuredProviderUrl);
|
|
128
|
-
const credentialEnv =
|
|
129
|
-
if (!/^[A-Z][A-Z0-9_]*$/.test(credentialEnv)) {
|
|
130
|
-
throw new JevBrowserError("config/ui-testing.json jev.credential_env must be an environment variable name");
|
|
131
|
-
}
|
|
188
|
+
const credentialEnv = environmentName(document.jev.credential_env, "jev.credential_env");
|
|
132
189
|
const configuredModel = String(document.jev.model || "").trim();
|
|
133
190
|
if (!configuredModel) throw new JevBrowserError("config/ui-testing.json jev.model is required");
|
|
134
|
-
const model = (env.JEV_MODEL?.trim() || configuredModel).trim();
|
|
135
|
-
if (!model) throw new JevBrowserError("Jev model must not be empty");
|
|
191
|
+
const model = (env.JEV_MODEL?.trim() || configuredModel).trim();
|
|
192
|
+
if (!model) throw new JevBrowserError("Jev model must not be empty");
|
|
193
|
+
|
|
194
|
+
const artifactEnv = environmentName(browserOptions.artifact_directory_env, "jev_browser_mcp.browser.artifact_directory_env");
|
|
195
|
+
const artifactDirectory = requireOutsidePackage(configuredDirectory(
|
|
196
|
+
env[artifactEnv]?.trim() || browserOptions.artifact_directory,
|
|
197
|
+
"jev_browser_mcp.browser.artifact_directory",
|
|
198
|
+
), "browser artifact directory");
|
|
199
|
+
const uploadRootEnv = environmentName(browserOptions.upload_root_env, "jev_browser_mcp.browser.upload_root_env");
|
|
200
|
+
const configuredUploadRoot = env[uploadRootEnv]?.trim();
|
|
201
|
+
const uploadRoot = configuredUploadRoot
|
|
202
|
+
? configuredDirectory(configuredUploadRoot, `environment ${uploadRootEnv}`)
|
|
203
|
+
: null;
|
|
204
|
+
const snapshotScope = browserOptions.snapshot_scope_default;
|
|
205
|
+
if (!new Set(["body", "main", "dialog"]).has(snapshotScope)) {
|
|
206
|
+
throw new JevBrowserError("config/ui-testing.json jev_browser_mcp.browser.snapshot_scope_default must be body, main, or dialog");
|
|
207
|
+
}
|
|
208
|
+
const trackerHostSuffixes = stringList(
|
|
209
|
+
browserOptions.tracker_host_suffixes,
|
|
210
|
+
"jev_browser_mcp.browser.tracker_host_suffixes",
|
|
211
|
+
(host) => /^[a-z0-9.-]+$/i.test(host) && !host.startsWith(".") && !host.endsWith("."),
|
|
212
|
+
);
|
|
213
|
+
const blockedResourceTypes = stringList(
|
|
214
|
+
browserOptions.blocked_resource_types,
|
|
215
|
+
"jev_browser_mcp.browser.blocked_resource_types",
|
|
216
|
+
(type) => new Set(["font", "media", "image"]).has(type),
|
|
217
|
+
);
|
|
136
218
|
|
|
137
219
|
return Object.freeze({
|
|
138
220
|
env,
|
|
@@ -140,8 +222,27 @@ export function loadSettings({ env = process.env, configPath = CONFIG_PATH } = {
|
|
|
140
222
|
mode,
|
|
141
223
|
channel,
|
|
142
224
|
profileDir: profileDirectory(profileTemplate, channel, env.JEV_BROWSER_PROFILE),
|
|
143
|
-
maxFlowSteps: positiveNumber(document.browser.max_flow_steps, "browser.max_flow_steps", true),
|
|
144
|
-
maxTextEntryChars: positiveNumber(document.browser.max_text_entry_chars, "browser.max_text_entry_chars", true),
|
|
225
|
+
maxFlowSteps: positiveNumber(document.browser.max_flow_steps, "browser.max_flow_steps", true),
|
|
226
|
+
maxTextEntryChars: positiveNumber(document.browser.max_text_entry_chars, "browser.max_text_entry_chars", true),
|
|
227
|
+
actionTimeoutMs: positiveNumber(browserOptions.max_action_timeout_seconds, "jev_browser_mcp.browser.max_action_timeout_seconds") * 1000,
|
|
228
|
+
maxUploadFiles: positiveNumber(browserOptions.max_upload_files, "jev_browser_mcp.browser.max_upload_files", true),
|
|
229
|
+
maxUploadPathChars: positiveNumber(browserOptions.max_upload_path_chars, "jev_browser_mcp.browser.max_upload_path_chars", true),
|
|
230
|
+
maxUploadFileBytes: positiveNumber(browserOptions.max_upload_file_bytes, "jev_browser_mcp.browser.max_upload_file_bytes", true),
|
|
231
|
+
maxUploadTotalBytes: positiveNumber(browserOptions.max_upload_total_bytes, "jev_browser_mcp.browser.max_upload_total_bytes", true),
|
|
232
|
+
uploadRoot,
|
|
233
|
+
artifactDirectory,
|
|
234
|
+
maxFrameSnapshots: positiveNumber(browserOptions.max_frame_snapshots, "jev_browser_mcp.browser.max_frame_snapshots", true),
|
|
235
|
+
defaults: Object.freeze({
|
|
236
|
+
fastPath: booleanValue(browserOptions.fast_path_single_plan_default, "jev_browser_mcp.browser.fast_path_single_plan_default"),
|
|
237
|
+
screenshotOnFailure: booleanValue(browserOptions.screenshot_on_failure_default, "jev_browser_mcp.browser.screenshot_on_failure_default"),
|
|
238
|
+
traceOnFailure: booleanValue(browserOptions.trace_on_failure_default, "jev_browser_mcp.browser.trace_on_failure_default"),
|
|
239
|
+
captureConsoleErrors: booleanValue(browserOptions.capture_console_errors_default, "jev_browser_mcp.browser.capture_console_errors_default"),
|
|
240
|
+
captureNetworkErrors: booleanValue(browserOptions.capture_network_errors_default, "jev_browser_mcp.browser.capture_network_errors_default"),
|
|
241
|
+
blockTrackers: booleanValue(browserOptions.block_trackers_default, "jev_browser_mcp.browser.block_trackers_default"),
|
|
242
|
+
snapshotScope,
|
|
243
|
+
}),
|
|
244
|
+
trackerHostSuffixes: Object.freeze(trackerHostSuffixes),
|
|
245
|
+
blockedResourceTypes: Object.freeze(blockedResourceTypes),
|
|
145
246
|
}),
|
|
146
247
|
jev: Object.freeze({
|
|
147
248
|
providerUrl,
|
|
@@ -152,7 +253,9 @@ export function loadSettings({ env = process.env, configPath = CONFIG_PATH } = {
|
|
|
152
253
|
maxSnapshotChars: positiveNumber(document.jev.max_snapshot_chars, "jev.max_snapshot_chars", true),
|
|
153
254
|
maxActionCount: positiveNumber(document.jev.max_action_count, "jev.max_action_count", true),
|
|
154
255
|
maxActionDescriptionChars: positiveNumber(document.jev.max_action_description_chars, "jev.max_action_description_chars", true),
|
|
155
|
-
maxResponseBytes: positiveNumber(document.jev.max_response_bytes, "jev.max_response_bytes", true),
|
|
256
|
+
maxResponseBytes: positiveNumber(document.jev.max_response_bytes, "jev.max_response_bytes", true),
|
|
257
|
+
maxDiagnosticItems: positiveNumber(jevOptions.max_diagnostic_items, "jev_browser_mcp.jev.max_diagnostic_items", true),
|
|
258
|
+
maxDiagnosticChars: positiveNumber(jevOptions.max_diagnostic_chars, "jev_browser_mcp.jev.max_diagnostic_chars", true),
|
|
156
259
|
}),
|
|
157
260
|
});
|
|
158
261
|
}
|