@diegosouzacdv/jev-browser-mcp 0.1.2 → 0.3.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 +95 -81
- package/config/ui-testing.json +37 -0
- package/docs/jev-browser-mcp.md +350 -192
- package/mcp_servers/jev-browser-npm/bin/jev-browser-mcp.cjs +4 -3
- package/mcp_servers/jev-browser-npm/src/accessibility.mjs +38 -0
- package/mcp_servers/jev-browser-npm/src/config.mjs +128 -15
- package/mcp_servers/jev-browser-npm/src/flow.mjs +888 -125
- package/mcp_servers/jev-browser-npm/src/server.mjs +5 -3
- package/package.json +2 -1
|
@@ -6,7 +6,7 @@ import { fileURLToPath } from "node:url";
|
|
|
6
6
|
const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../..");
|
|
7
7
|
const CONFIG_PATH = path.join(PACKAGE_ROOT, "config", "ui-testing.json");
|
|
8
8
|
const CONFIG_KEYS = {
|
|
9
|
-
root: ["version", "browser", "jev"],
|
|
9
|
+
root: ["version", "browser", "jev", "jev_browser_mcp"],
|
|
10
10
|
browser: [
|
|
11
11
|
"mode",
|
|
12
12
|
"harness_browser",
|
|
@@ -16,6 +16,30 @@ const CONFIG_KEYS = {
|
|
|
16
16
|
"max_flow_steps",
|
|
17
17
|
"max_text_entry_chars",
|
|
18
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
|
+
"max_download_files",
|
|
26
|
+
"max_download_file_bytes",
|
|
27
|
+
"max_download_total_bytes",
|
|
28
|
+
"max_download_timeout_seconds",
|
|
29
|
+
"upload_root_env",
|
|
30
|
+
"artifact_directory",
|
|
31
|
+
"artifact_directory_env",
|
|
32
|
+
"max_frame_snapshots",
|
|
33
|
+
"fast_path_single_plan_default",
|
|
34
|
+
"screenshot_on_failure_default",
|
|
35
|
+
"trace_on_failure_default",
|
|
36
|
+
"capture_console_errors_default",
|
|
37
|
+
"capture_network_errors_default",
|
|
38
|
+
"block_trackers_default",
|
|
39
|
+
"snapshot_scope_default",
|
|
40
|
+
"tracker_host_suffixes",
|
|
41
|
+
"blocked_resource_types",
|
|
42
|
+
],
|
|
19
43
|
jev: [
|
|
20
44
|
"provider_url",
|
|
21
45
|
"credential_env",
|
|
@@ -27,6 +51,11 @@ const CONFIG_KEYS = {
|
|
|
27
51
|
"max_action_description_chars",
|
|
28
52
|
"max_response_bytes",
|
|
29
53
|
],
|
|
54
|
+
nodeJev: [
|
|
55
|
+
"max_diagnostic_items",
|
|
56
|
+
"max_diagnostic_chars",
|
|
57
|
+
"max_accessibility_violations",
|
|
58
|
+
],
|
|
30
59
|
};
|
|
31
60
|
|
|
32
61
|
export class JevBrowserError extends Error {
|
|
@@ -57,6 +86,45 @@ function positiveNumber(value, name, integer = false) {
|
|
|
57
86
|
return parsed;
|
|
58
87
|
}
|
|
59
88
|
|
|
89
|
+
function expandHomeDirectory(value) {
|
|
90
|
+
if (value.startsWith("~/") || value.startsWith("~\\")) return path.join(homedir(), value.slice(2));
|
|
91
|
+
return value;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function requireOutsidePackage(resolved, name) {
|
|
95
|
+
const insidePackage = path.relative(PACKAGE_ROOT, resolved);
|
|
96
|
+
if (insidePackage === "" || (!insidePackage.startsWith(`..${path.sep}`) && insidePackage !== ".." && !path.isAbsolute(insidePackage))) {
|
|
97
|
+
throw new JevBrowserError(`${name} must be outside the installed package directory`);
|
|
98
|
+
}
|
|
99
|
+
return resolved;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function configuredDirectory(value, name) {
|
|
103
|
+
if (typeof value !== "string" || !value.trim()) throw new JevBrowserError(`config/ui-testing.json ${name} must be a directory path`);
|
|
104
|
+
const expanded = expandHomeDirectory(value.trim());
|
|
105
|
+
if (!path.isAbsolute(expanded)) throw new JevBrowserError(`config/ui-testing.json ${name} must be absolute or start with ~`);
|
|
106
|
+
return path.resolve(expanded);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function environmentName(value, name) {
|
|
110
|
+
if (typeof value !== "string" || !/^[A-Z][A-Z0-9_]*$/.test(value)) {
|
|
111
|
+
throw new JevBrowserError(`config/ui-testing.json ${name} must be an environment variable name`);
|
|
112
|
+
}
|
|
113
|
+
return value;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function booleanValue(value, name) {
|
|
117
|
+
if (typeof value !== "boolean") throw new JevBrowserError(`config/ui-testing.json ${name} must be a boolean`);
|
|
118
|
+
return value;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function stringList(value, name, validator = () => true) {
|
|
122
|
+
if (!Array.isArray(value) || value.some((item) => typeof item !== "string" || !item.trim() || !validator(item))) {
|
|
123
|
+
throw new JevBrowserError(`config/ui-testing.json ${name} must be an array of valid strings`);
|
|
124
|
+
}
|
|
125
|
+
return value.map((item) => item.trim());
|
|
126
|
+
}
|
|
127
|
+
|
|
60
128
|
function validProviderUrl(value) {
|
|
61
129
|
let parsed;
|
|
62
130
|
try {
|
|
@@ -71,17 +139,9 @@ function validProviderUrl(value) {
|
|
|
71
139
|
}
|
|
72
140
|
|
|
73
141
|
function profileDirectory(template, channel, override) {
|
|
74
|
-
|
|
75
|
-
if (value.startsWith("~/") || value.startsWith("~\\")) {
|
|
76
|
-
value = path.join(homedir(), value.slice(2));
|
|
77
|
-
}
|
|
142
|
+
const value = expandHomeDirectory(override?.trim() || template.replace("{browser}", channel));
|
|
78
143
|
if (!path.isAbsolute(value)) throw new JevBrowserError("JEV_BROWSER_PROFILE must be an absolute path");
|
|
79
|
-
|
|
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;
|
|
144
|
+
return requireOutsidePackage(path.resolve(value), "browser profile");
|
|
85
145
|
}
|
|
86
146
|
|
|
87
147
|
export function loadSettings({ env = process.env, configPath = CONFIG_PATH } = {}) {
|
|
@@ -94,6 +154,11 @@ export function loadSettings({ env = process.env, configPath = CONFIG_PATH } = {
|
|
|
94
154
|
requireKeys(document, CONFIG_KEYS.root, "root");
|
|
95
155
|
requireKeys(document.browser, CONFIG_KEYS.browser, "browser");
|
|
96
156
|
requireKeys(document.jev, CONFIG_KEYS.jev, "jev");
|
|
157
|
+
requireKeys(document.jev_browser_mcp, ["browser", "jev"], "jev_browser_mcp");
|
|
158
|
+
requireKeys(document.jev_browser_mcp.browser, CONFIG_KEYS.nodeBrowser, "jev_browser_mcp.browser");
|
|
159
|
+
requireKeys(document.jev_browser_mcp.jev, CONFIG_KEYS.nodeJev, "jev_browser_mcp.jev");
|
|
160
|
+
const browserOptions = document.jev_browser_mcp.browser;
|
|
161
|
+
const jevOptions = document.jev_browser_mcp.jev;
|
|
97
162
|
if (document.version !== 1) throw new JevBrowserError("config/ui-testing.json has an unsupported version");
|
|
98
163
|
|
|
99
164
|
if (!new Set(["harness", "computer"]).has(document.browser.mode)) {
|
|
@@ -125,15 +190,37 @@ export function loadSettings({ env = process.env, configPath = CONFIG_PATH } = {
|
|
|
125
190
|
|
|
126
191
|
const configuredProviderUrl = validProviderUrl(document.jev.provider_url);
|
|
127
192
|
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
|
-
}
|
|
193
|
+
const credentialEnv = environmentName(document.jev.credential_env, "jev.credential_env");
|
|
132
194
|
const configuredModel = String(document.jev.model || "").trim();
|
|
133
195
|
if (!configuredModel) throw new JevBrowserError("config/ui-testing.json jev.model is required");
|
|
134
196
|
const model = (env.JEV_MODEL?.trim() || configuredModel).trim();
|
|
135
197
|
if (!model) throw new JevBrowserError("Jev model must not be empty");
|
|
136
198
|
|
|
199
|
+
const artifactEnv = environmentName(browserOptions.artifact_directory_env, "jev_browser_mcp.browser.artifact_directory_env");
|
|
200
|
+
const artifactDirectory = requireOutsidePackage(configuredDirectory(
|
|
201
|
+
env[artifactEnv]?.trim() || browserOptions.artifact_directory,
|
|
202
|
+
"jev_browser_mcp.browser.artifact_directory",
|
|
203
|
+
), "browser artifact directory");
|
|
204
|
+
const uploadRootEnv = environmentName(browserOptions.upload_root_env, "jev_browser_mcp.browser.upload_root_env");
|
|
205
|
+
const configuredUploadRoot = env[uploadRootEnv]?.trim();
|
|
206
|
+
const uploadRoot = configuredUploadRoot
|
|
207
|
+
? configuredDirectory(configuredUploadRoot, `environment ${uploadRootEnv}`)
|
|
208
|
+
: null;
|
|
209
|
+
const snapshotScope = browserOptions.snapshot_scope_default;
|
|
210
|
+
if (!new Set(["body", "main", "dialog"]).has(snapshotScope)) {
|
|
211
|
+
throw new JevBrowserError("config/ui-testing.json jev_browser_mcp.browser.snapshot_scope_default must be body, main, or dialog");
|
|
212
|
+
}
|
|
213
|
+
const trackerHostSuffixes = stringList(
|
|
214
|
+
browserOptions.tracker_host_suffixes,
|
|
215
|
+
"jev_browser_mcp.browser.tracker_host_suffixes",
|
|
216
|
+
(host) => /^[a-z0-9.-]+$/i.test(host) && !host.startsWith(".") && !host.endsWith("."),
|
|
217
|
+
);
|
|
218
|
+
const blockedResourceTypes = stringList(
|
|
219
|
+
browserOptions.blocked_resource_types,
|
|
220
|
+
"jev_browser_mcp.browser.blocked_resource_types",
|
|
221
|
+
(type) => new Set(["font", "media", "image"]).has(type),
|
|
222
|
+
);
|
|
223
|
+
|
|
137
224
|
return Object.freeze({
|
|
138
225
|
env,
|
|
139
226
|
browser: Object.freeze({
|
|
@@ -142,6 +229,29 @@ export function loadSettings({ env = process.env, configPath = CONFIG_PATH } = {
|
|
|
142
229
|
profileDir: profileDirectory(profileTemplate, channel, env.JEV_BROWSER_PROFILE),
|
|
143
230
|
maxFlowSteps: positiveNumber(document.browser.max_flow_steps, "browser.max_flow_steps", true),
|
|
144
231
|
maxTextEntryChars: positiveNumber(document.browser.max_text_entry_chars, "browser.max_text_entry_chars", true),
|
|
232
|
+
actionTimeoutMs: positiveNumber(browserOptions.max_action_timeout_seconds, "jev_browser_mcp.browser.max_action_timeout_seconds") * 1000,
|
|
233
|
+
maxUploadFiles: positiveNumber(browserOptions.max_upload_files, "jev_browser_mcp.browser.max_upload_files", true),
|
|
234
|
+
maxUploadPathChars: positiveNumber(browserOptions.max_upload_path_chars, "jev_browser_mcp.browser.max_upload_path_chars", true),
|
|
235
|
+
maxUploadFileBytes: positiveNumber(browserOptions.max_upload_file_bytes, "jev_browser_mcp.browser.max_upload_file_bytes", true),
|
|
236
|
+
maxUploadTotalBytes: positiveNumber(browserOptions.max_upload_total_bytes, "jev_browser_mcp.browser.max_upload_total_bytes", true),
|
|
237
|
+
maxDownloadFiles: positiveNumber(browserOptions.max_download_files, "jev_browser_mcp.browser.max_download_files", true),
|
|
238
|
+
maxDownloadFileBytes: positiveNumber(browserOptions.max_download_file_bytes, "jev_browser_mcp.browser.max_download_file_bytes", true),
|
|
239
|
+
maxDownloadTotalBytes: positiveNumber(browserOptions.max_download_total_bytes, "jev_browser_mcp.browser.max_download_total_bytes", true),
|
|
240
|
+
downloadTimeoutMs: positiveNumber(browserOptions.max_download_timeout_seconds, "jev_browser_mcp.browser.max_download_timeout_seconds") * 1000,
|
|
241
|
+
uploadRoot,
|
|
242
|
+
artifactDirectory,
|
|
243
|
+
maxFrameSnapshots: positiveNumber(browserOptions.max_frame_snapshots, "jev_browser_mcp.browser.max_frame_snapshots", true),
|
|
244
|
+
defaults: Object.freeze({
|
|
245
|
+
fastPath: booleanValue(browserOptions.fast_path_single_plan_default, "jev_browser_mcp.browser.fast_path_single_plan_default"),
|
|
246
|
+
screenshotOnFailure: booleanValue(browserOptions.screenshot_on_failure_default, "jev_browser_mcp.browser.screenshot_on_failure_default"),
|
|
247
|
+
traceOnFailure: booleanValue(browserOptions.trace_on_failure_default, "jev_browser_mcp.browser.trace_on_failure_default"),
|
|
248
|
+
captureConsoleErrors: booleanValue(browserOptions.capture_console_errors_default, "jev_browser_mcp.browser.capture_console_errors_default"),
|
|
249
|
+
captureNetworkErrors: booleanValue(browserOptions.capture_network_errors_default, "jev_browser_mcp.browser.capture_network_errors_default"),
|
|
250
|
+
blockTrackers: booleanValue(browserOptions.block_trackers_default, "jev_browser_mcp.browser.block_trackers_default"),
|
|
251
|
+
snapshotScope,
|
|
252
|
+
}),
|
|
253
|
+
trackerHostSuffixes: Object.freeze(trackerHostSuffixes),
|
|
254
|
+
blockedResourceTypes: Object.freeze(blockedResourceTypes),
|
|
145
255
|
}),
|
|
146
256
|
jev: Object.freeze({
|
|
147
257
|
providerUrl,
|
|
@@ -153,6 +263,9 @@ export function loadSettings({ env = process.env, configPath = CONFIG_PATH } = {
|
|
|
153
263
|
maxActionCount: positiveNumber(document.jev.max_action_count, "jev.max_action_count", true),
|
|
154
264
|
maxActionDescriptionChars: positiveNumber(document.jev.max_action_description_chars, "jev.max_action_description_chars", true),
|
|
155
265
|
maxResponseBytes: positiveNumber(document.jev.max_response_bytes, "jev.max_response_bytes", true),
|
|
266
|
+
maxDiagnosticItems: positiveNumber(jevOptions.max_diagnostic_items, "jev_browser_mcp.jev.max_diagnostic_items", true),
|
|
267
|
+
maxDiagnosticChars: positiveNumber(jevOptions.max_diagnostic_chars, "jev_browser_mcp.jev.max_diagnostic_chars", true),
|
|
268
|
+
maxAccessibilityViolations: positiveNumber(jevOptions.max_accessibility_violations, "jev_browser_mcp.jev.max_accessibility_violations", true),
|
|
156
269
|
}),
|
|
157
270
|
});
|
|
158
271
|
}
|