@agent360/browser-mcp 1.28.1 → 1.29.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 +1 -1
- package/extension/background.js +99 -2
- package/extension/manifest.json +1 -1
- package/index.js +150 -12
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -127,7 +127,7 @@ The pattern: **anything you would do yourself in a browser, on a site you are al
|
|
|
127
127
|
| | Browser MCP | Playwright MCP | BrowserMCP.io |
|
|
128
128
|
|---|---|---|---|
|
|
129
129
|
| **Browser** | Your real Chrome | Headless (new session) | Your real Chrome |
|
|
130
|
-
| **Maintained** | Actively — latest release v1.
|
|
130
|
+
| **Maintained** | Actively — latest release v1.29.0 (2026-09-07) | Actively (Microsoft) | Last commit Apr 2025 |
|
|
131
131
|
| **Logins/cookies** | Already authenticated | Must log in every time | Already authenticated |
|
|
132
132
|
| **Multi-session** | 20 concurrent sessions with color-coded tab groups | Single session | Single session |
|
|
133
133
|
| **Human-in-the-loop** | `browser_ask_user` — 2FA, CAPTCHA, credential input | None | None |
|
package/extension/background.js
CHANGED
|
@@ -211,6 +211,8 @@ async function evictOldestTabs(session, justAddedTabId) {
|
|
|
211
211
|
async function addTabToSession(port, tabId) {
|
|
212
212
|
const session = getSession(port);
|
|
213
213
|
session.tabIds.add(tabId);
|
|
214
|
+
// Sessionen lever igen — aflys en eventuel port-frigivelse (se tabs.onRemoved).
|
|
215
|
+
chrome.alarms.clear(`frigiv-${port}`).catch(() => {});
|
|
214
216
|
|
|
215
217
|
// LRU eviction: når sessionen overstiger cap, luk de ældste tabs.
|
|
216
218
|
if (session.tabIds.size > MAX_TABS_PER_SESSION) {
|
|
@@ -247,6 +249,29 @@ async function releaseSession(port) {
|
|
|
247
249
|
const session = sessions.get(port);
|
|
248
250
|
if (!session) return;
|
|
249
251
|
|
|
252
|
+
// ── Bad vi selv om frigivelsen? (FUNDET AF REVIEW 7/9) ────────────────────
|
|
253
|
+
//
|
|
254
|
+
// Vejen fra "sessionen er tom" til denne funktion gaar over mindst tre hop:
|
|
255
|
+
// sendMessage -> offscreen sender terminate og lukker WS -> ws.onclose ->
|
|
256
|
+
// session_disconnect. Aabner agenten en fane i de ~50 ms undervejs, lukkede
|
|
257
|
+
// oprydningen herunder DEN fane — paa grundlag af en beslutning der blev truffet
|
|
258
|
+
// foer fanen fandtes. Det er praecis "agenten holder pause og genoptager"-
|
|
259
|
+
// scenariet frigivelsen er bygget til at understoette.
|
|
260
|
+
//
|
|
261
|
+
// En UVENTET afbrydelse (chatten er vaek) skal stadig lukke fanerne — derfor
|
|
262
|
+
// skelnes der, i stedet for bare at tjekke om sessionen er tom.
|
|
263
|
+
if (frivilligtFrigivet.delete(port)) {
|
|
264
|
+
if (session.tabIds.size) {
|
|
265
|
+
// Sessionen arbejder igen. Behold den, saa naeste binding kan adoptere den
|
|
266
|
+
// (adoptOrphanedSession finder donorer med samme pid OG faner).
|
|
267
|
+
persistSessions();
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
sessions.delete(port);
|
|
271
|
+
persistSessions();
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
|
|
250
275
|
// Detach debugger + close all session tabs
|
|
251
276
|
const tabIds = [...session.tabIds];
|
|
252
277
|
for (const tabId of tabIds) {
|
|
@@ -401,7 +426,13 @@ async function debuggerAttach(tabId) {
|
|
|
401
426
|
lastMsg = 'attach resolved but Chrome shows tab not attached (ghost — page likely mid-reload)';
|
|
402
427
|
try { await chrome.debugger.detach({ tabId }); } catch {}
|
|
403
428
|
} catch (e) {
|
|
404
|
-
|
|
429
|
+
// MAALT 31/8 af den nye udvidelses-test: her stod `includes('Already attached')`
|
|
430
|
+
// med stort A. Chromes faktiske besked er "Another debugger is already attached
|
|
431
|
+
// to the tab with id: N" — med lille. Tjekket ramte ALDRIG. Resultat: naar en
|
|
432
|
+
// anden debugger havde fanen (DevTools aabent, en anden udvidelse), blev det
|
|
433
|
+
// behandlet som en fejl, proevet tre gange, og kastet — i stedet for bare at
|
|
434
|
+
// bruge den session der allerede fandtes.
|
|
435
|
+
if (/already attached/i.test(e.message || '')) {
|
|
405
436
|
// Chrome side has session — sync local cache
|
|
406
437
|
debuggerAttached.add(tabId);
|
|
407
438
|
return;
|
|
@@ -504,6 +535,10 @@ async function cdpSend(tabId, method, params = {}) {
|
|
|
504
535
|
// Faner agenten selv lukkede via close_tab. En tom session betyder kun "arbejdet er slut"
|
|
505
536
|
// hvis det var BRUGEREN der lukkede den sidste fane.
|
|
506
537
|
const agentLukkedeFaner = new Set();
|
|
538
|
+
// Porte hvor VI selv har bedt serveren slippe porten, fordi sessionen var tom.
|
|
539
|
+
// Skelnen betyder alt i releaseSession: en frivillig frigivelse maa aldrig lukke
|
|
540
|
+
// faner, mens en uventet afbrydelse (chatten er vaek) netop skal rydde op.
|
|
541
|
+
const frivilligtFrigivet = new Set();
|
|
507
542
|
|
|
508
543
|
// ── Armerede dialog-haandterere, pr. fane ──────────────────────────────────────
|
|
509
544
|
// MAALT 22/8 af flowtesten: handle_dialog var ubrugelig som den var skrevet. Den
|
|
@@ -546,7 +581,25 @@ chrome.tabs.onRemoved.addListener((tabId) => {
|
|
|
546
581
|
if (session.tabIds.size === 0 && !lukketAfAgenten) {
|
|
547
582
|
// Last tab closed — tell offscreen to terminate the MCP server.
|
|
548
583
|
// Resulting WS-close triggers the existing session_disconnect → releaseSession path.
|
|
584
|
+
frivilligtFrigivet.add(port);
|
|
549
585
|
chrome.runtime.sendMessage({ type: 'terminate_mcp_session', port }).catch(() => {});
|
|
586
|
+
} else if (session.tabIds.size === 0) {
|
|
587
|
+
// ── Agenten lukkede selv sin sidste fane (MAALT 7/9-2026) ────────────────
|
|
588
|
+
// Her stod der intet, og det var med vilje: en agent der lukker en fane midt i
|
|
589
|
+
// et forloeb skal ikke miste browseren. Men serverens EGEN instruks siger til
|
|
590
|
+
// hver eneste agent: "ALWAYS close tabs when done". Hver velopdragen chat endte
|
|
591
|
+
// altsaa med at holde sin port til 4-timers-tomgangen udloeb — den dokumenterede
|
|
592
|
+
// god-praksis slog oprydningen ihjel, og 20 porte kunne staa optaget af chats
|
|
593
|
+
// der for laengst var faerdige.
|
|
594
|
+
//
|
|
595
|
+
// Nu: fem minutters henstand. Kommer der en ny fane inden da, aflyses den
|
|
596
|
+
// (se getSessionTab). Sker der intet, bedes serveren slippe porten — den DOER
|
|
597
|
+
// ikke, saa chatten kan hente browseren tilbage naar som helst.
|
|
598
|
+
//
|
|
599
|
+
// chrome.alarms og ikke setTimeout: en MV3-service-worker suspenderes, og en
|
|
600
|
+
// timer ville forsvinde med den.
|
|
601
|
+
chrome.alarms.create(`frigiv-${port}`, { delayInMinutes: 5 });
|
|
602
|
+
persistSessions();
|
|
550
603
|
} else {
|
|
551
604
|
persistSessions();
|
|
552
605
|
}
|
|
@@ -3524,9 +3577,24 @@ async function dispatch(port, method, params) {
|
|
|
3524
3577
|
throw new Error(`Tab ${params.tab_id} does not belong to this session (${session.label})`);
|
|
3525
3578
|
}
|
|
3526
3579
|
const tab = await chrome.tabs.update(params.tab_id, { active: true });
|
|
3580
|
+
// Gør ogsaa VINDUET forrest. Uden det bliver fanen aktiv inde i sit vindue —
|
|
3581
|
+
// document.hasFocus() bliver sand — men document.visibilityState forbliver
|
|
3582
|
+
// 'hidden' fordi vinduet ligger bagved. Chrome struber timere i skjulte
|
|
3583
|
+
// faner, saa Angular-apps (Google Ads, GA4, Search Console) renderer aldrig
|
|
3584
|
+
// faerdigt: man laeser en halvt bygget side og drager forkerte konklusioner.
|
|
3585
|
+
// Kostede to opgaver og en forkert konklusion 31/8-2026.
|
|
3586
|
+
let vinduesFokus = null;
|
|
3587
|
+
try {
|
|
3588
|
+
await chrome.windows.update(tab.windowId, { focused: true });
|
|
3589
|
+
vinduesFokus = true;
|
|
3590
|
+
} catch (e) {
|
|
3591
|
+
// Vinduet kan vaere lukket eller paa et andet Space. Fanen er stadig
|
|
3592
|
+
// aktiv; vi siger bare aerligt at synligheden ikke kunne sikres.
|
|
3593
|
+
vinduesFokus = false;
|
|
3594
|
+
}
|
|
3527
3595
|
session.activeTabId = tab.id;
|
|
3528
3596
|
persistSessions();
|
|
3529
|
-
return { id: tab.id, url: tab.url, title: tab.title };
|
|
3597
|
+
return { id: tab.id, url: tab.url, title: tab.title, windowFocused: vinduesFokus };
|
|
3530
3598
|
}
|
|
3531
3599
|
|
|
3532
3600
|
case 'close_tab': {
|
|
@@ -3898,4 +3966,33 @@ chrome.alarms.onAlarm.addListener((alarm) => {
|
|
|
3898
3966
|
if (alarm.name === 'ensure-offscreen') {
|
|
3899
3967
|
ensureOffscreen().catch(console.error);
|
|
3900
3968
|
}
|
|
3969
|
+
if (alarm.name.startsWith('frigiv-')) {
|
|
3970
|
+
const port = Number(alarm.name.slice('frigiv-'.length));
|
|
3971
|
+
// ── Hvorfor lageret laeses DIREKTE og ikke via restoreSessions() ─────────
|
|
3972
|
+
//
|
|
3973
|
+
// FUNDET AF REVIEW 7/9. Her stod `restoreSessions().then(...)`, og kommentaren
|
|
3974
|
+
// sagde at det var vaernet mod en genstartet service-worker. Den gjorde det
|
|
3975
|
+
// modsatte: `restoreSessions()` gendanner kun sessioner der har GYLDIGE FANER
|
|
3976
|
+
// (`if (validTabIds.size > 0)`) — og en tom session er praecis den her alarm
|
|
3977
|
+
// handler om. En MV3-worker suspenderes efter ~30 sekunder; fristen er paa fem
|
|
3978
|
+
// minutter, saa workeren er naesten altid frisk naar alarmen fyrer. Sessionen
|
|
3979
|
+
// blev derfor aldrig fundet, `if (!s) return` ramte, og porten blev holdt til
|
|
3980
|
+
// 4-timers-tomgangen — altsaa praecis den fejl frigivelsen skulle fjerne.
|
|
3981
|
+
//
|
|
3982
|
+
// restoreSessions() har god grund til ikke at genoplive doede sessioner (andre
|
|
3983
|
+
// kaldere vil ikke arve faner der ikke findes). Derfor rettes det HER.
|
|
3984
|
+
(async () => {
|
|
3985
|
+
const iHukommelsen = sessions.get(port);
|
|
3986
|
+
if (iHukommelsen) {
|
|
3987
|
+
if (iHukommelsen.tabIds.size > 0) return; // den arbejder igen — lad den vaere
|
|
3988
|
+
} else {
|
|
3989
|
+
const { sessions: gemte } = await chrome.storage.local.get({ sessions: {} });
|
|
3990
|
+
const gemt = gemte[String(port)];
|
|
3991
|
+
if (!gemt) return; // sessionen er reelt vaek
|
|
3992
|
+
if ((gemt.tabIds || []).length > 0) return; // den arbejder igen
|
|
3993
|
+
}
|
|
3994
|
+
frivilligtFrigivet.add(port);
|
|
3995
|
+
chrome.runtime.sendMessage({ type: 'terminate_mcp_session', port }).catch(() => {});
|
|
3996
|
+
})().catch(() => {});
|
|
3997
|
+
}
|
|
3901
3998
|
});
|
package/extension/manifest.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"manifest_version": 3,
|
|
3
3
|
"name": "Agent360 Browser MCP",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.29.0",
|
|
5
5
|
"description": "Give Claude Code your real logged-in Chrome. Two parts: this extension + one npx command. Multi-session, local, MIT.",
|
|
6
6
|
"permissions": [
|
|
7
7
|
"tabs",
|
package/index.js
CHANGED
|
@@ -39,8 +39,11 @@ const repoDir = dirname(__dirname); // parent of mcp-server/
|
|
|
39
39
|
// `npx @agent360/browser-mcp@latest` opdaterer allerede serveren; blokken var overfloedig.
|
|
40
40
|
let extensionUpdated = false;
|
|
41
41
|
|
|
42
|
-
|
|
43
|
-
|
|
42
|
+
// Spaendet kan flyttes med env — ellers ville en test af port-udsultning skulle
|
|
43
|
+
// beslaglaegge de RIGTIGE porte og dermed sulte brugerens oevrige chats imens.
|
|
44
|
+
// Uden env er vaerdierne uaendrede. (Samme moenster som BROWSER_MCP_EXTENSION_ID.)
|
|
45
|
+
const BASE_PORT = Number(process.env.BROWSER_MCP_BASE_PORT) || 9876;
|
|
46
|
+
const MAX_PORT = Number(process.env.BROWSER_MCP_MAX_PORT) || 9895; // 20 ports instead of 10 — zombies die within 5s via parent check
|
|
44
47
|
|
|
45
48
|
// ── Extension connections ───────────────────────────────────────────────────
|
|
46
49
|
// FEJL MAALT 21/8: her stod `let extensionSocket = null`, og hver ny forbindelse
|
|
@@ -58,6 +61,7 @@ const connections = new Set(); // { ws, seq, extensionId, version, name, since }
|
|
|
58
61
|
let connSeq = 0;
|
|
59
62
|
let activePort = null;
|
|
60
63
|
let alleePorteOptaget = false; // hele spaendet i brug — se createWSS
|
|
64
|
+
let bindFejl = null; // bind fejlede af en ANDEN grund end optaget port
|
|
61
65
|
|
|
62
66
|
function cmpVersion(a, b) {
|
|
63
67
|
const pa = String(a || '0.0.0').split('.').map(n => parseInt(n, 10) || 0);
|
|
@@ -147,6 +151,22 @@ const pending = new Map();
|
|
|
147
151
|
// Timers hoisted to module scope so gracefulShutdown can clear them deterministically.
|
|
148
152
|
let heartbeat = null;
|
|
149
153
|
let parentCheck = null;
|
|
154
|
+
let tomgangsvagt = null;
|
|
155
|
+
|
|
156
|
+
// ── 4-timers-tomgangen bor paa modul-niveau, ikke i hjerteslaget ────────────
|
|
157
|
+
//
|
|
158
|
+
// FUNDET AF REVIEW 7/9. Tjekket laa INDE i `heartbeat`, som kun findes mens serveren
|
|
159
|
+
// har en port — og som `frigivPort` rydder. Da porten blev doven, betoed det at en
|
|
160
|
+
// proces der ALDRIG binder (en chat der ikke roerer browseren) heller aldrig faar
|
|
161
|
+
// sin tomgang tjekket. Det var den eneste vej hvor en browser-inaktiv server gav sine
|
|
162
|
+
// ~35 MB tilbage; med mange samtidige chats er det maalbart. Nu koerer vagten altid.
|
|
163
|
+
// Graensen staar som literal, ikke bag en konstant: `vagter.test.mjs` laeser tallet
|
|
164
|
+
// direkte ud af kilden for at haandhaeve at den aldrig bliver kort. En kort graense
|
|
165
|
+
// ville lade en aaben chat miste browseren permanent — vaerre end en port der staar
|
|
166
|
+
// optaget lidt for laenge.
|
|
167
|
+
tomgangsvagt = setInterval(() => {
|
|
168
|
+
if (Date.now() - lastActivity > 4 * 60 * 60 * 1000) gracefulShutdown('Idle timeout (4h)');
|
|
169
|
+
}, 60000);
|
|
150
170
|
|
|
151
171
|
// ── WebSocket Server ───────────────────────────────────────────────────────
|
|
152
172
|
|
|
@@ -187,9 +207,19 @@ function createWSS(port = BASE_PORT) {
|
|
|
187
207
|
// sendToExtension, saa agenten kan give brugeren den rigtige forklaring.
|
|
188
208
|
alleePorteOptaget = true;
|
|
189
209
|
process.stderr.write(`[MCP] All ports ${BASE_PORT}-${MAX_PORT} in use. Cannot start.\n`);
|
|
210
|
+
loesPortLoefte(false);
|
|
190
211
|
}
|
|
191
212
|
} else {
|
|
213
|
+
// FUNDET AF REVIEW 7/9. Her stod KUN stderr-linjen. Foer porten blev doven, var
|
|
214
|
+
// det harmloest: en opstartsfejl man kunne se i loggen. Nu venter `sikrePort()`
|
|
215
|
+
// paa et loefte der aldrig blev indfriet — og `sendToExtension` goer
|
|
216
|
+
// `await sikrePort()` UDEN timeout. Enhver anden bind-fejl end EADDRINUSE
|
|
217
|
+
// (EACCES paa en privilegeret port, EADDRNOTAVAIL, en restriktiv firewall)
|
|
218
|
+
// ville derfor faa hvert eneste browser-kald til at haenge tavst for evigt.
|
|
219
|
+
// At haenge uden besked er vaerre end den fejl vi rettede.
|
|
220
|
+
bindFejl = err.message;
|
|
192
221
|
process.stderr.write(`[MCP] WebSocket error: ${err.message}\n`);
|
|
222
|
+
loesPortLoefte(false);
|
|
193
223
|
}
|
|
194
224
|
});
|
|
195
225
|
|
|
@@ -305,7 +335,11 @@ function createWSS(port = BASE_PORT) {
|
|
|
305
335
|
process.stderr.write('[MCP] terminate ignoreret — kom fra en inaktiv udvidelses-forbindelse\n');
|
|
306
336
|
return;
|
|
307
337
|
}
|
|
308
|
-
gracefulShutdown
|
|
338
|
+
// AENDRET 7/9: her stod `gracefulShutdown`. En chat der var faerdig med browseren
|
|
339
|
+
// mistede altsaa browseren HELT — og porten blev alligevel hverken frigivet hurtigt
|
|
340
|
+
// nok til andre (processen doede foerst efter oprydning) eller genvundet af chatten
|
|
341
|
+
// selv. Nu slippes kun porten; naeste browser-kald tager en ny (se sikrePort).
|
|
342
|
+
frigivPort('udvidelsen meldte: sidste fane lukket');
|
|
309
343
|
return;
|
|
310
344
|
}
|
|
311
345
|
|
|
@@ -359,22 +393,93 @@ function createWSS(port = BASE_PORT) {
|
|
|
359
393
|
server.on('listening', () => {
|
|
360
394
|
activePort = port;
|
|
361
395
|
process.stderr.write(`[MCP] WebSocket server listening on ws://127.0.0.1:${port}\n`);
|
|
396
|
+
loesPortLoefte(true);
|
|
362
397
|
});
|
|
363
398
|
|
|
364
399
|
// Heartbeat + idle timeout (4 hours) — hoisted to module scope so gracefulShutdown can clear it
|
|
400
|
+
// Ryddes foerst: porten kan bindes flere gange i samme proces (se sikrePort), og uden
|
|
401
|
+
// det her ville hver ny binding efterlade en ekstra timer der aldrig blev stoppet.
|
|
402
|
+
if (heartbeat) clearInterval(heartbeat);
|
|
365
403
|
heartbeat = setInterval(() => {
|
|
366
404
|
for (const c of liveConnections()) c.ws.ping();
|
|
367
|
-
if (Date.now() - lastActivity > 4 * 60 * 60 * 1000) {
|
|
368
|
-
gracefulShutdown('Idle timeout (4h)');
|
|
369
|
-
}
|
|
370
405
|
}, 20000);
|
|
371
406
|
}
|
|
372
407
|
|
|
373
|
-
|
|
408
|
+
// ── Porten tages ved BRUG, ikke ved opstart ─────────────────────────────────
|
|
409
|
+
//
|
|
410
|
+
// MAALT 7/9-2026 paa Gustavs maskine: 37 koerende servere, 20 porte i spaendet, 17 chats
|
|
411
|
+
// helt uden browser. Hver Claude Code-chat starter en server ved opstart — ogsaa de mange
|
|
412
|
+
// chats der aldrig roerer browseren — og her stod `createWSS()` paa modul-niveau. Porten
|
|
413
|
+
// blev altsaa reserveret af en chat der maaske aldrig fik brug for den, og holdt indtil
|
|
414
|
+
// chatten doede eller 4-timers-tomgangen udloeb.
|
|
415
|
+
//
|
|
416
|
+
// Konsekvensen var ensidigt slem: chat nr. 21 fik `alleePorteOptaget = true` ÉN gang og
|
|
417
|
+
// proevede aldrig igen — dens browser var doed hele chattens levetid, selv naar en port
|
|
418
|
+
// blev fri et minut senere.
|
|
419
|
+
//
|
|
420
|
+
// Nu bindes porten foerste gang et vaerktoej faktisk skal bruge udvidelsen, og HVERT kald
|
|
421
|
+
// proever igen hvis det forrige ikke fik en. Udvidelsen genscanner hele spaendet hvert
|
|
422
|
+
// 2. sekund (offscreen.js), saa en port der aabnes sent bliver fundet af sig selv.
|
|
423
|
+
//
|
|
424
|
+
// Bemaerk hvorfor det ikke er en 5-minutters timer der draeber processen: en chat der
|
|
425
|
+
// foerst skal bruge browseren efter en halv time ville saa staa uden. Processen lever
|
|
426
|
+
// videre — det er kun PORTEN der ikke holdes reserveret til noget der ikke sker.
|
|
427
|
+
let portResolver = null;
|
|
428
|
+
let bindLoefte = null;
|
|
429
|
+
|
|
430
|
+
function loesPortLoefte(fik) {
|
|
431
|
+
if (!portResolver) return;
|
|
432
|
+
const r = portResolver;
|
|
433
|
+
portResolver = null;
|
|
434
|
+
bindLoefte = null;
|
|
435
|
+
r(fik);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// Slip porten, men BLIV I LIVE. Forskellen er hele pointen: lukkede vi processen ned,
|
|
439
|
+
// ville en chat der er faerdig med browseren kl. 10 og skal bruge den igen kl. 10:40 staa
|
|
440
|
+
// uden — og Claude Code genstarter ikke en MCP-server midt i en samtale. Processen koster
|
|
441
|
+
// ~35 MB og ingen port; det er porten der er den knappe ressource.
|
|
442
|
+
function frigivPort(grund) {
|
|
443
|
+
if (activePort === null) return;
|
|
444
|
+
process.stderr.write(`[MCP] frigiver port ${activePort} — ${grund}\n`);
|
|
445
|
+
if (heartbeat) { clearInterval(heartbeat); heartbeat = null; }
|
|
446
|
+
const gammel = wss;
|
|
447
|
+
wss = null;
|
|
448
|
+
activePort = null;
|
|
449
|
+
laastForbindelse = null;
|
|
450
|
+
harSendtKommando = false; // naeste binding vaelger udvidelse forfra
|
|
451
|
+
for (const c of connections) { try { c.ws.close(); } catch {} }
|
|
452
|
+
connections.clear();
|
|
453
|
+
try { gammel?.close(); } catch {}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function sikrePort() {
|
|
457
|
+
if (activePort !== null) return Promise.resolve(true);
|
|
458
|
+
if (bindLoefte) return bindLoefte; // en binding er allerede i gang
|
|
459
|
+
alleePorteOptaget = false; // hvert forsoeg starter paa en frisk
|
|
460
|
+
bindFejl = null;
|
|
461
|
+
bindLoefte = new Promise((res) => { portResolver = res; });
|
|
462
|
+
// Vagthund. F8 var ét konkret hul hvor loeftet aldrig blev indfriet; det her lukker
|
|
463
|
+
// KLASSEN. Emitter en fremtidig fejlsti hverken 'listening' eller 'error', svarer
|
|
464
|
+
// kaldet nu med en fejl i stedet for at haenge tavst for evigt. 10 s er rigeligt:
|
|
465
|
+
// en loopback-binding tager millisekunder, og hele spaendet naas paa under ét.
|
|
466
|
+
const vagthund = setTimeout(() => {
|
|
467
|
+
if (!portResolver) return;
|
|
468
|
+
bindFejl = bindFejl || 'bindingen svarede ikke inden for 10 sekunder';
|
|
469
|
+
process.stderr.write('[MCP] port-bindingen svarede aldrig — opgiver dette forsoeg\n');
|
|
470
|
+
loesPortLoefte(false);
|
|
471
|
+
}, 10000);
|
|
472
|
+
bindLoefte.finally(() => clearTimeout(vagthund));
|
|
473
|
+
createWSS();
|
|
474
|
+
return bindLoefte;
|
|
475
|
+
}
|
|
374
476
|
|
|
375
477
|
// ── Send command to extension ───────────────────────────────────────────────
|
|
376
478
|
|
|
377
479
|
async function sendToExtension(method, params = {}, timeoutMs = 30000, _retries = 5) {
|
|
480
|
+
// Skaf en port hvis vi ikke har en. Foerste kald binder; senere kald er en no-op.
|
|
481
|
+
// Fik vi ingen (hele spaendet optaget), proever naeste kald igen — derfor ingen kast her.
|
|
482
|
+
await sikrePort();
|
|
378
483
|
// Retry if extension is temporarily disconnected (reconnects every 2s)
|
|
379
484
|
const conn = activeConnection();
|
|
380
485
|
if (!conn) {
|
|
@@ -385,12 +490,21 @@ async function sendToExtension(method, params = {}, timeoutMs = 30000, _retries
|
|
|
385
490
|
// This is the other half of the two-part setup: the server is clearly running (it is
|
|
386
491
|
// throwing this), so what is missing is the extension, Chrome itself, or the connection
|
|
387
492
|
// between them. Say which, and where to get it — the agent relays this text to the user.
|
|
493
|
+
if (bindFejl) {
|
|
494
|
+
throw new Error(
|
|
495
|
+
`Serveren kunne ikke aabne en port paa 127.0.0.1 (${BASE_PORT}-${MAX_PORT}): ${bindFejl}\n` +
|
|
496
|
+
'Det er IKKE Chrome eller udvidelsen — det er operativsystemet eller en firewall der ' +
|
|
497
|
+
'afviser bindingen. Tjek om noget blokerer loopback-porte.\n' +
|
|
498
|
+
'Sig praecis dét til brugeren. Sig IKKE at udvidelsen mangler.',
|
|
499
|
+
);
|
|
500
|
+
}
|
|
388
501
|
if (alleePorteOptaget) {
|
|
389
502
|
throw new Error(
|
|
390
|
-
`Alle porte ${BASE_PORT}-${MAX_PORT} er optaget, saa
|
|
503
|
+
`Alle porte ${BASE_PORT}-${MAX_PORT} er optaget lige nu, saa dette kald fik ingen port. ` +
|
|
391
504
|
'Det er IKKE et problem med Chrome eller udvidelsen — de virker fint.\n' +
|
|
392
|
-
|
|
393
|
-
'Luk en af dem
|
|
505
|
+
`${MAX_PORT - BASE_PORT + 1} andre chats bruger browseren i oejeblikket. ` +
|
|
506
|
+
'Luk en af dem, eller vent til en bliver faerdig — og proev saa kommandoen igen. ' +
|
|
507
|
+
'Denne chat skal IKKE genstartes: hvert kald proever selv at faa en port.\n' +
|
|
394
508
|
'Sig praecis dét til brugeren. Sig IKKE at udvidelsen mangler.',
|
|
395
509
|
);
|
|
396
510
|
}
|
|
@@ -841,7 +955,11 @@ async function handleProvideFeedback(args) {
|
|
|
841
955
|
'Det skal brugeren selv goere — chrome:// kan ikke styres herfra. Behold den nyeste.',
|
|
842
956
|
);
|
|
843
957
|
}
|
|
844
|
-
if (!active) {
|
|
958
|
+
if (!active && activePort === null) {
|
|
959
|
+
// Ingen port taget endnu = browseren er ikke brugt i denne chat. Der er intet
|
|
960
|
+
// i stykker, og et fix-skridt her ville vaere en falsk alarm — se verdict 'idle'.
|
|
961
|
+
findings.push('Browseren er ikke taget i brug i denne chat endnu, saa der er ingen forbindelse at maale paa. Det er ikke en fejl.');
|
|
962
|
+
} else if (!active) {
|
|
845
963
|
findings.push('Ingen Chrome-udvidelse er forbundet til denne MCP-server lige nu.');
|
|
846
964
|
fix_steps.push('Tjek at Chrome koerer og at udvidelsen er slaaet til paa chrome://extensions, klik derefter paa ikonet → Reconnect.');
|
|
847
965
|
}
|
|
@@ -872,9 +990,24 @@ async function handleProvideFeedback(args) {
|
|
|
872
990
|
);
|
|
873
991
|
}
|
|
874
992
|
|
|
993
|
+
// ── "ingen forbindelse" og "ingen port endnu" er IKKE det samme ────────────
|
|
994
|
+
//
|
|
995
|
+
// FUNDET AF REVIEW 7/9. Foer porten blev doven, bandt hver server ved opstart, saa
|
|
996
|
+
// udvidelsen var altid forbundet naar dette vaerktoej blev kaldt — og `!active`
|
|
997
|
+
// betoed derfor paalideligt "udvidelsen kan ikke naas". Nu binder en chat foerst en
|
|
998
|
+
// port naar den bruger browseren, saa en HELT SUND chat der ikke har roert den kan
|
|
999
|
+
// staa uden forbindelse. Uden det her skel fik den `disconnected` + fix_steps der
|
|
1000
|
+
// bad brugeren geninstallere — og INSTRUCTIONS beder agenten viderebringe dem.
|
|
1001
|
+
// Vi ville altsaa fortaelle folk at deres installation var i stykker, fordi vi selv
|
|
1002
|
+
// endnu ikke havde aabnet doeren.
|
|
1003
|
+
//
|
|
1004
|
+
// Loesningen er ikke at binde en port her (et diagnose-vaerktoej skal ikke aendre
|
|
1005
|
+
// tilstand for at kunne maale den) — det er at sige praecis hvad der er tilfaeldet.
|
|
1006
|
+
const ingenPortEndnu = activePort === null;
|
|
875
1007
|
const verdict =
|
|
876
1008
|
exts.length > 1 ? 'conflict'
|
|
877
1009
|
: (serverOutdated || extOutdated) ? 'outdated'
|
|
1010
|
+
: (!active && ingenPortEndnu) ? 'idle'
|
|
878
1011
|
: !active ? 'disconnected'
|
|
879
1012
|
: (npmLatest === null ? 'unknown' : 'current');
|
|
880
1013
|
|
|
@@ -914,7 +1047,11 @@ async function handleProvideFeedback(args) {
|
|
|
914
1047
|
`&title=${encodeURIComponent(issueTitle)}&body=${encodeURIComponent(issueBody)}`;
|
|
915
1048
|
|
|
916
1049
|
const instruction =
|
|
917
|
-
verdict === '
|
|
1050
|
+
verdict === 'idle'
|
|
1051
|
+
? 'Browseren er ikke taget i brug i denne chat endnu, saa der er intet at diagnosticere paa forbindelsen — ' +
|
|
1052
|
+
'det er IKKE en fejl i installationen, og du maa ikke sige det til brugeren. ' +
|
|
1053
|
+
'Er der en aegte mangel, saa tilbyd submit_url som et klikbart link.'
|
|
1054
|
+
: verdict === 'conflict' || verdict === 'outdated' || verdict === 'disconnected'
|
|
918
1055
|
? 'Fortael brugeren hvad der blev fundet, og giv fix_steps som konkrete skridt. Proev derefter handlingen igen. ' +
|
|
919
1056
|
'Del KUN submit_url hvis problemet stadig staar efter at fix_steps er fulgt — det er sandsynligvis installationen, ikke en fejl i Browser MCP.'
|
|
920
1057
|
: 'Installationen er frisk, saa det her er sandsynligvis en aegte mangel eller fejl. Fortael brugeren kort hvad der ikke kunne lade sig goere, ' +
|
|
@@ -982,6 +1119,7 @@ function gracefulShutdown(reason, code = 0) {
|
|
|
982
1119
|
// Stop timers so they can't re-enter gracefulShutdown
|
|
983
1120
|
if (parentCheck) clearInterval(parentCheck);
|
|
984
1121
|
if (heartbeat) clearInterval(heartbeat);
|
|
1122
|
+
if (tomgangsvagt) clearInterval(tomgangsvagt);
|
|
985
1123
|
|
|
986
1124
|
// Close WS with explicit close-frame so extension's onclose handler fires
|
|
987
1125
|
for (const c of liveConnections()) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent360/browser-mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.29.0",
|
|
4
4
|
"description": "Browser MCP — give your AI agent a real, logged-in Chrome: test authenticated apps, read 2FA codes from Gmail, scrape behind logins, solve CAPTCHAs. 40 tools, multi-session, human-in-the-loop.",
|
|
5
5
|
"mcpName": "io.github.Agent360dk/browser-mcp",
|
|
6
6
|
"type": "module",
|
|
@@ -64,11 +64,11 @@
|
|
|
64
64
|
"ws": "^8.18.0"
|
|
65
65
|
},
|
|
66
66
|
"overrides": {
|
|
67
|
-
"fast-uri": "^3.1.
|
|
67
|
+
"fast-uri": "^3.1.7",
|
|
68
68
|
"hono": "^4.12.34",
|
|
69
69
|
"ip-address": "^10.3.1",
|
|
70
70
|
"@hono/node-server": "^1.19.15",
|
|
71
71
|
"body-parser": "^2.2.3",
|
|
72
|
-
"qs": "^6.
|
|
72
|
+
"qs": "^6.16.0"
|
|
73
73
|
}
|
|
74
74
|
}
|