@mozilla/firefox-devtools-mcp 0.9.5 → 0.9.7

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mozilla/firefox-devtools-mcp",
3
- "version": "0.9.5",
3
+ "version": "0.9.7",
4
4
  "description": "Model Context Protocol (MCP) server for Firefox DevTools automation",
5
5
  "author": "Mozilla",
6
6
  "license": "MIT OR Apache-2.0",
@@ -8,7 +8,7 @@
8
8
  "main": "dist/index.js",
9
9
  "types": "dist/index.d.ts",
10
10
  "bin": {
11
- "firefox-devtools-mcp": "./dist/index.js"
11
+ "firefox-devtools-mcp": "dist/index.js"
12
12
  },
13
13
  "scripts": {
14
14
  "dev": "tsx watch src/index.moz.ts",
@@ -71,7 +71,7 @@
71
71
  "@modelcontextprotocol/sdk": "1.29.0",
72
72
  "geckodriver": "6.0.2",
73
73
  "selenium-webdriver": "4.36.0",
74
- "ws": "8.18.3",
74
+ "ws": "8.21.0",
75
75
  "yargs": "17.7.2"
76
76
  },
77
77
  "devDependencies": {
@@ -82,8 +82,8 @@
82
82
  "@types/yargs": "17.0.32",
83
83
  "@typescript-eslint/eslint-plugin": "8.58.0",
84
84
  "@typescript-eslint/parser": "8.58.0",
85
- "@vitest/coverage-v8": "3.1.4",
86
- "@vitest/ui": "3.1.4",
85
+ "@vitest/coverage-v8": "4.1.8",
86
+ "@vitest/ui": "4.1.8",
87
87
  "dotenv": "17.2.1",
88
88
  "eslint": "8.57.1",
89
89
  "eslint-config-prettier": "10.1.5",
@@ -93,7 +93,7 @@
93
93
  "tsup": "8.5.0",
94
94
  "tsx": "4.21.0",
95
95
  "typescript": "5.3.3",
96
- "vitest": "3.1.4"
96
+ "vitest": "4.1.8"
97
97
  },
98
98
  "files": [
99
99
  "dist",
@@ -0,0 +1,216 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Test: zombie geckodriver cleanup
4
+ *
5
+ * Scenario B (SIGKILL): Firefox is completely dead (user clicked [X]).
6
+ * Recovery test: can close() clean up and reconnect after Firefox dies?
7
+ *
8
+ * Scenario C (SIGKILL, non-headless): Same as B with a visible browser window.
9
+ */
10
+ import { FirefoxDevTools } from '../dist/index.js';
11
+ import { execFileSync } from 'node:child_process';
12
+ import { readFileSync } from 'node:fs';
13
+
14
+ // ---------------------------------------------------------------------------
15
+ // Helpers
16
+ // ---------------------------------------------------------------------------
17
+
18
+ function parsePids(output) {
19
+ return output
20
+ .trim()
21
+ .split('\n')
22
+ .map(Number)
23
+ .filter((n) => !isNaN(n));
24
+ }
25
+
26
+ function pgrep(pattern) {
27
+ try {
28
+ return parsePids(execFileSync('pgrep', ['-f', pattern], { encoding: 'utf-8' }));
29
+ } catch {
30
+ return [];
31
+ }
32
+ }
33
+
34
+ function getProcState(pid) {
35
+ try {
36
+ const stat = readFileSync(`/proc/${pid}/stat`, 'utf-8');
37
+ return stat[stat.lastIndexOf(')') + 2];
38
+ } catch {
39
+ return null;
40
+ }
41
+ }
42
+
43
+ function isAlive(pid) {
44
+ const state = getProcState(pid);
45
+ return state !== null && state !== 'Z';
46
+ }
47
+
48
+ function killHard(pid) {
49
+ try {
50
+ process.kill(pid, 'SIGKILL');
51
+ } catch {}
52
+ }
53
+
54
+ function getDescendants(parentPid) {
55
+ const result = [];
56
+ const queue = [parentPid];
57
+ while (queue.length > 0) {
58
+ const pid = queue.shift();
59
+ try {
60
+ const children = parsePids(execFileSync('pgrep', ['-P', String(pid)], { encoding: 'utf-8' }));
61
+ result.push(...children);
62
+ queue.push(...children);
63
+ } catch {}
64
+ }
65
+ return result;
66
+ }
67
+
68
+ function waitForDeath(pid, timeoutMs) {
69
+ return new Promise((resolve) => {
70
+ const start = Date.now();
71
+ const check = () => {
72
+ if (!isAlive(pid)) resolve(true);
73
+ else if (Date.now() - start > timeoutMs) resolve(false);
74
+ else setTimeout(check, 100);
75
+ };
76
+ check();
77
+ });
78
+ }
79
+
80
+ function killAll(pids) {
81
+ for (const pid of pids) killHard(pid);
82
+ }
83
+
84
+ async function reconnect(geckosBefore, excludePids) {
85
+ const r = await launchFirefox(geckosBefore, excludePids);
86
+ await r.devTools.navigate('about:blank');
87
+ console.log(' Navigation works');
88
+ await r.devTools.close();
89
+ return r.geckoPid;
90
+ }
91
+
92
+ // Log unhandled rejections instead of swallowing them silently
93
+ process.on('unhandledRejection', (reason) => {
94
+ console.error('[unhandled rejection]', reason);
95
+ });
96
+
97
+ // ---------------------------------------------------------------------------
98
+ // Launch helper
99
+ // ---------------------------------------------------------------------------
100
+
101
+ async function launchFirefox(geckosBefore, excludePids = [], headless = true) {
102
+ const devTools = new FirefoxDevTools({
103
+ headless,
104
+ viewport: { width: 1280, height: 720 },
105
+ });
106
+ await devTools.connect();
107
+
108
+ const geckoPid = pgrep('geckodriver').find(
109
+ (p) => !geckosBefore.has(p) && !excludePids.includes(p)
110
+ );
111
+ if (!geckoPid) throw new Error('No geckodriver PID found after connect');
112
+
113
+ const firefoxPids = getDescendants(geckoPid);
114
+ if (firefoxPids.length === 0) {
115
+ killHard(geckoPid);
116
+ throw new Error('No Firefox PIDs found under geckodriver');
117
+ }
118
+
119
+ return { devTools, geckoPid, firefoxPids };
120
+ }
121
+
122
+ // ---------------------------------------------------------------------------
123
+ // Test
124
+ // ---------------------------------------------------------------------------
125
+
126
+ async function main() {
127
+ console.log('--- Zombie geckodriver fix test ---\n');
128
+ const geckosBefore = new Set(pgrep('geckodriver'));
129
+ const usedPids = [];
130
+
131
+ console.log('Scenario B: Firefox killed (SIGKILL)');
132
+
133
+ console.log(' 1. Launching Firefox...');
134
+ const b = await launchFirefox(geckosBefore, usedPids);
135
+ console.log(` Geckodriver PID: ${b.geckoPid}, Firefox PIDs: ${b.firefoxPids.join(', ')}`);
136
+
137
+ console.log(' 2. Killing Firefox...');
138
+ killAll(b.firefoxPids);
139
+ for (const pid of b.firefoxPids) {
140
+ if (!(await waitForDeath(pid, 5000))) {
141
+ console.error(` [FATAL] Firefox PID ${pid} survived SIGKILL`);
142
+ killHard(b.geckoPid);
143
+ process.exit(1);
144
+ }
145
+ }
146
+ console.log(' Firefox is dead');
147
+
148
+ console.log(
149
+ ` 3. ${isAlive(b.geckoPid) ? 'Zombie geckodriver detected' : 'Geckodriver died with Firefox (no zombie)'}`
150
+ );
151
+
152
+ console.log(' 4. Running cleanup (close() handles timeout + force-kill)...');
153
+ await b.devTools.close();
154
+
155
+ console.log(' 5. Verifying geckodriver is dead...');
156
+ if (!(await waitForDeath(b.geckoPid, 5000))) {
157
+ console.error(' [FAIL] Geckodriver still alive after cleanup');
158
+ killHard(b.geckoPid);
159
+ process.exit(1);
160
+ }
161
+ console.log(' Geckodriver is dead');
162
+
163
+ console.log(' 6. Reconnecting...');
164
+ usedPids.push(b.geckoPid);
165
+ usedPids.push(await reconnect(geckosBefore, usedPids));
166
+ console.log(' Scenario B: PASS\n');
167
+
168
+ console.log('Scenario C: Firefox killed (SIGKILL) — non-headless');
169
+
170
+ console.log(' 1. Launching Firefox (non-headless)...');
171
+ const c = await launchFirefox(geckosBefore, usedPids, false);
172
+ console.log(` Geckodriver PID: ${c.geckoPid}, Firefox PIDs: ${c.firefoxPids.join(', ')}`);
173
+
174
+ console.log(' 2. Killing Firefox...');
175
+ killAll(c.firefoxPids);
176
+ for (const pid of c.firefoxPids) {
177
+ if (!(await waitForDeath(pid, 5000))) {
178
+ console.error(` [FATAL] Firefox PID ${pid} survived SIGKILL`);
179
+ killHard(c.geckoPid);
180
+ process.exit(1);
181
+ }
182
+ }
183
+ console.log(' Firefox is dead');
184
+
185
+ console.log(
186
+ ` 3. ${isAlive(c.geckoPid) ? 'Zombie geckodriver detected' : 'Geckodriver died with Firefox (no zombie)'}`
187
+ );
188
+
189
+ console.log(' 4. Running cleanup (close() handles timeout + force-kill)...');
190
+ await c.devTools.close();
191
+
192
+ console.log(' 5. Verifying geckodriver is dead...');
193
+ if (!(await waitForDeath(c.geckoPid, 5000))) {
194
+ console.error(' [FAIL] Geckodriver still alive after cleanup');
195
+ killHard(c.geckoPid);
196
+ process.exit(1);
197
+ }
198
+ console.log(' Geckodriver is dead');
199
+
200
+ console.log(' 6. Reconnecting...');
201
+ usedPids.push(c.geckoPid);
202
+ usedPids.push(await reconnect(geckosBefore, usedPids));
203
+ console.log(' Scenario C: PASS\n');
204
+
205
+ // Final cleanup
206
+ const leftover = pgrep('geckodriver').filter((p) => !geckosBefore.has(p));
207
+ for (const pid of leftover) {
208
+ killAll(getDescendants(pid));
209
+ killHard(pid);
210
+ }
211
+ }
212
+
213
+ main().catch((err) => {
214
+ console.error('Fatal error:', err);
215
+ process.exit(1);
216
+ });