@diegosouzacdv/jev-browser-mcp 0.1.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.
@@ -0,0 +1,206 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { McpServer } from "@modelcontextprotocol/server";
5
+ import { serveStdio } from "@modelcontextprotocol/server/stdio";
6
+ import { chromium } from "playwright";
7
+ import * as z from "zod/v4";
8
+ import packageManifest from "../../../package.json" with { type: "json" };
9
+ import { loadSettings, JevBrowserError } from "./config.mjs";
10
+ import { chooseNextAction, executeBrowserFlow } from "./flow.mjs";
11
+
12
+ const browserPools = new Set();
13
+
14
+ class BrowserPool {
15
+ #settings;
16
+ #contextPromise;
17
+ #context;
18
+ #browser;
19
+ #page;
20
+ #startedAt;
21
+ #flowQueue = Promise.resolve();
22
+
23
+ constructor(settings) {
24
+ this.#settings = settings;
25
+ }
26
+
27
+ async runFlow(work) {
28
+ const previous = this.#flowQueue;
29
+ let release;
30
+ this.#flowQueue = new Promise((resolve) => { release = resolve; });
31
+ await previous;
32
+ try {
33
+ return await work();
34
+ } finally {
35
+ release();
36
+ }
37
+ }
38
+
39
+ async getPage() {
40
+ if (this.#page?.isClosed() && this.#context) {
41
+ this.#page = this.#context.pages()[0] || await this.#context.newPage();
42
+ }
43
+ const isOpening = !this.#contextPromise;
44
+ if (isOpening) {
45
+ this.#startedAt = performance.now();
46
+ this.#contextPromise = this.#open();
47
+ }
48
+ await this.#contextPromise;
49
+ return { page: this.#page, sessionMs: isOpening ? performance.now() - this.#startedAt : 0 };
50
+ }
51
+
52
+ async #open() {
53
+ const { mode, channel, profileDir } = this.#settings.browser;
54
+ try {
55
+ if (mode === "computer") {
56
+ this.#context = await chromium.launchPersistentContext(profileDir, {
57
+ channel,
58
+ headless: false,
59
+ });
60
+ this.#page = this.#context.pages()[0] || await this.#context.newPage();
61
+ } else {
62
+ this.#browser = await chromium.launch({ channel, headless: true });
63
+ this.#page = await this.#browser.newPage();
64
+ this.#context = this.#page.context();
65
+ }
66
+ } catch (error) {
67
+ this.#contextPromise = undefined;
68
+ throw new JevBrowserError(
69
+ `Could not start the configured ${channel} browser (${error?.name || "BrowserError"}); install it with jev-browser-mcp --install-browser`,
70
+ );
71
+ }
72
+ }
73
+
74
+ async close() {
75
+ try {
76
+ if (this.#context) await this.#context.close();
77
+ else if (this.#browser) await this.#browser.close();
78
+ } catch {
79
+ // The host may already have closed the process during shutdown.
80
+ }
81
+ this.#context = undefined;
82
+ this.#browser = undefined;
83
+ this.#page = undefined;
84
+ this.#contextPromise = undefined;
85
+ }
86
+ }
87
+
88
+ function toolError(error) {
89
+ const message = error instanceof JevBrowserError ? error.message : `Jev browser flow failed (${error?.name || "Error"})`;
90
+ return { isError: true, content: [{ type: "text", text: message }] };
91
+ }
92
+
93
+ function toolText(value) {
94
+ return { content: [{ type: "text", text: JSON.stringify(value) }] };
95
+ }
96
+
97
+ export function createJevBrowserServer({ env = process.env, configPath, fetchImpl = fetch } = {}) {
98
+ const settings = loadSettings({ env, ...(configPath ? { configPath } : {}) });
99
+ const browserPool = new BrowserPool(settings);
100
+ browserPools.add(browserPool);
101
+ const server = new McpServer({
102
+ name: "jev-browser",
103
+ version: packageManifest.version,
104
+ description: "Bounded Playwright screen flows selected by Jev through the OpenRouter Decisions API.",
105
+ });
106
+ const closeServer = server.close.bind(server);
107
+ server.close = async () => {
108
+ try {
109
+ await closeServer();
110
+ } finally {
111
+ await browserPool.close();
112
+ browserPools.delete(browserPool);
113
+ }
114
+ };
115
+
116
+ server.registerTool(
117
+ "choose_next_action",
118
+ {
119
+ description: "Ask Jev to select one candidate browser action from the current UI snapshot and full flow. Returns the selected ID; the harness executes it through separately authorized calls.",
120
+ inputSchema: {
121
+ flow: z.string(),
122
+ page_snapshot: z.string(),
123
+ actions: z.record(z.string(), z.string()),
124
+ completed_steps: z.array(z.string()).optional(),
125
+ },
126
+ },
127
+ async ({ flow, page_snapshot: pageSnapshot, actions, completed_steps: completedSteps }) => {
128
+ try {
129
+ return toolText(await chooseNextAction({ flow, pageSnapshot, actions, completedSteps, settings, fetchImpl }));
130
+ } catch (error) {
131
+ return toolError(error);
132
+ }
133
+ },
134
+ );
135
+
136
+ server.registerTool(
137
+ "run_browser_flow",
138
+ {
139
+ description: "Run a bounded screen flow in one warm Playwright browser session. Jev chooses one supplied plan once, the MCP executes accessible-name actions and verifies the requested outcome.",
140
+ inputSchema: {
141
+ flow: z.string(),
142
+ initial_url: z.string(),
143
+ expected_outcome: z.string(),
144
+ candidate_plans: z.record(z.string(), z.unknown()),
145
+ },
146
+ },
147
+ async ({ flow, initial_url: initialUrl, expected_outcome: expectedOutcome, candidate_plans: candidatePlans }) => {
148
+ try {
149
+ return toolText(await browserPool.runFlow(() => executeBrowserFlow({
150
+ flow,
151
+ initialUrl,
152
+ expectedOutcome,
153
+ candidatePlans,
154
+ settings,
155
+ browserPool,
156
+ fetchImpl,
157
+ })));
158
+ } catch (error) {
159
+ return toolError(error);
160
+ }
161
+ },
162
+ );
163
+
164
+ return server;
165
+ }
166
+
167
+ export async function closeBrowserSessions() {
168
+ await Promise.all([...browserPools].map((pool) => pool.close()));
169
+ browserPools.clear();
170
+ }
171
+
172
+ export function installConfiguredBrowser({ env = process.env } = {}) {
173
+ const settings = loadSettings({ env });
174
+ if (settings.browser.mode === "computer") {
175
+ process.stderr.write("Computer mode uses the installed Chrome or Edge; no browser download is needed.\n");
176
+ return 0;
177
+ }
178
+ const entryPath = fileURLToPath(import.meta.resolve("playwright"));
179
+ const cliPath = path.join(path.dirname(entryPath), "cli.js");
180
+ const install = spawnSync(process.execPath, [cliPath, "install", settings.browser.channel], { stdio: "inherit" });
181
+ return install.status ?? 1;
182
+ }
183
+
184
+ export function runStdioServer() {
185
+ const handle = serveStdio(() => createJevBrowserServer());
186
+ const removeSignalHandlers = () => {
187
+ process.removeListener("SIGINT", onInterrupt);
188
+ process.removeListener("SIGTERM", onTerminate);
189
+ };
190
+ const shutdown = async () => {
191
+ removeSignalHandlers();
192
+ await handle.close();
193
+ await closeBrowserSessions();
194
+ };
195
+ const onInterrupt = () => { void shutdown(); };
196
+ const onTerminate = () => { void shutdown(); };
197
+ const onInputClosed = () => {
198
+ removeSignalHandlers();
199
+ void closeBrowserSessions();
200
+ };
201
+ process.once("SIGINT", onInterrupt);
202
+ process.once("SIGTERM", onTerminate);
203
+ process.stdin.once("end", onInputClosed);
204
+ process.stdin.once("close", onInputClosed);
205
+ return handle;
206
+ }
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "@diegosouzacdv/jev-browser-mcp",
3
+ "version": "0.1.0",
4
+ "description": "Portable MCP server for bounded Playwright screen flows selected by Jev",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/diegosouzacdv/orquestrador.git"
9
+ },
10
+ "homepage": "https://github.com/diegosouzacdv/orquestrador/tree/main/docs/jev-browser-mcp.md",
11
+ "engines": {
12
+ "node": ">=22"
13
+ },
14
+ "bin": {
15
+ "jev-browser-mcp": "mcp_servers/jev-browser-npm/bin/jev-browser-mcp.cjs"
16
+ },
17
+ "exports": {
18
+ "./server": "./mcp_servers/jev-browser-npm/src/server.mjs"
19
+ },
20
+ "files": [
21
+ "config/ui-testing.json",
22
+ "docs/jev-browser-mcp.md",
23
+ "mcp_servers/jev-browser-npm/bin/**",
24
+ "mcp_servers/jev-browser-npm/src/**"
25
+ ],
26
+ "scripts": {
27
+ "test:jev-browser-mcp": "node --test mcp_servers/jev-browser-npm/tests/*.test.mjs"
28
+ },
29
+ "dependencies": {
30
+ "@modelcontextprotocol/server": "2.1.0",
31
+ "playwright": "1.63.0",
32
+ "zod": "4.6.5"
33
+ }
34
+ }