@agent360/browser-mcp 1.28.1 → 1.29.1
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 +161 -46
- package/bin/cli.js +99 -21
- package/extension/background.js +1309 -194
- package/extension/manifest.json +1 -1
- package/extension/offscreen.js +62 -5
- package/extension/popup.html +9 -4
- package/extension/popup.js +5 -5
- package/index.js +380 -109
- package/package.json +5 -5
- package/tools.js +16 -16
- package/vagt.js +3 -3
package/index.js
CHANGED
|
@@ -15,13 +15,13 @@ import { ledErDoedt, forfaedreKaede } from './vagt.js';
|
|
|
15
15
|
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
16
16
|
import { WebSocketServer } from 'ws';
|
|
17
17
|
import { execSync, execFile } from 'child_process';
|
|
18
|
-
import { dirname, join, resolve, sep } from 'path';
|
|
18
|
+
import { dirname, join, resolve, sep, isAbsolute } from 'path';
|
|
19
19
|
import { fileURLToPath } from 'url';
|
|
20
20
|
import { homedir } from 'os';
|
|
21
|
-
import { readFileSync, writeFileSync, mkdirSync, appendFileSync } from 'fs';
|
|
21
|
+
import { readFileSync, writeFileSync, mkdirSync, appendFileSync, realpathSync, lstatSync, statSync } from 'fs';
|
|
22
22
|
import { TOOLS, PROVIDER_PAGES } from './tools.js';
|
|
23
23
|
|
|
24
|
-
// Read version from package.json
|
|
24
|
+
// Read version from package.json - single source of truth, never drifts
|
|
25
25
|
const PKG_VERSION = JSON.parse(
|
|
26
26
|
readFileSync(join(dirname(fileURLToPath(import.meta.url)), 'package.json'), 'utf8')
|
|
27
27
|
).version;
|
|
@@ -32,23 +32,26 @@ const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
|
32
32
|
const repoDir = dirname(__dirname); // parent of mcp-server/
|
|
33
33
|
|
|
34
34
|
// FJERNET 22/8: her koerte `git pull --ff-only` + `npm install` ved hver serveropstart,
|
|
35
|
-
// med cwd = pakkens foraeldremappe. I en npm-installation er det node_modules/@agent360/
|
|
35
|
+
// med cwd = pakkens foraeldremappe. I en npm-installation er det node_modules/@agent360/ -
|
|
36
36
|
// og git soeger OPAD, saa kaldet landede i BRUGERENS EGET repo. Maalt: fra
|
|
37
37
|
// node_modules/@agent360 opløser git toplevel til det omkringliggende projekt.
|
|
38
38
|
// En agent-session maatte altsaa ikke mutere brugerens git-trae uden samtykke.
|
|
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
|
|
47
|
-
// overskrev den. Er der to udgaver af udvidelsen indlaest i den samme Chrome
|
|
48
|
-
// fx en "load unpacked"-kopi ved siden af en anden
|
|
50
|
+
// overskrev den. Er der to udgaver af udvidelsen indlaest i den samme Chrome -
|
|
51
|
+
// fx en "load unpacked"-kopi ved siden af en anden - scanner BEGGE de samme porte
|
|
49
52
|
// og forbinder til hver eneste server. Maalt med lsof: 2 ESTABLISHED forbindelser
|
|
50
53
|
// paa hver af de fire aktive porte. Kommandoerne gik til den der forbandt sidst,
|
|
51
|
-
// mens den anden holdt sit eget sessions-kort og sine egne fane-grupper
|
|
54
|
+
// mens den anden holdt sit eget sessions-kort og sine egne fane-grupper - og et
|
|
52
55
|
// `terminate` fra den forkerte kopi lukkede serveren ned under den rigtige.
|
|
53
56
|
//
|
|
54
57
|
// Nu holdes alle forbindelser med deres identitet, kommandoer sendes kun til den
|
|
@@ -57,7 +60,9 @@ const MAX_PORT = 9895; // 20 ports instead of 10 — zombies die within 5s via p
|
|
|
57
60
|
const connections = new Set(); // { ws, seq, extensionId, version, name, since }
|
|
58
61
|
let connSeq = 0;
|
|
59
62
|
let activePort = null;
|
|
60
|
-
let alleePorteOptaget = false; // hele spaendet i brug
|
|
63
|
+
let alleePorteOptaget = false; // hele spaendet i brug - se createWSS
|
|
64
|
+
let bindFejl = null; // bind fejlede af en ANDEN grund end optaget port
|
|
65
|
+
let portBundetTid = 0; // hvornaar porten sidst blev aaben - se sendToExtension
|
|
61
66
|
|
|
62
67
|
function cmpVersion(a, b) {
|
|
63
68
|
const pa = String(a || '0.0.0').split('.').map(n => parseInt(n, 10) || 0);
|
|
@@ -73,13 +78,13 @@ let sidsteKonfliktNoegle = '';
|
|
|
73
78
|
function advarOmKonflikt(conn) {
|
|
74
79
|
const alle = distinctExtensions();
|
|
75
80
|
if (alle.length < 2) return;
|
|
76
|
-
// Samme konflikt maa ikke skrige ved hver eneste hello
|
|
81
|
+
// Samme konflikt maa ikke skrige ved hver eneste hello - kun naar billedet aendrer sig.
|
|
77
82
|
const noegle = alle.map(c => `${c.extensionId || 'ukendt'}@${c.version || '?'}`).sort().join('|');
|
|
78
83
|
if (noegle === sidsteKonfliktNoegle) return;
|
|
79
84
|
sidsteKonfliktNoegle = noegle;
|
|
80
85
|
const aktiv = activeConnection();
|
|
81
86
|
// Oplyser INGEN af dem en version (alle udgivne udgaver er fra foer haandtrykket),
|
|
82
|
-
// er der intet grundlag for at vaelge. Det skal staa der
|
|
87
|
+
// er der intet grundlag for at vaelge. Det skal staa der - ellers laeser man
|
|
83
88
|
// "kommandoer sendes kun til X" som om X var det rigtige valg.
|
|
84
89
|
const kanVaelge = alle.some(c => c.version);
|
|
85
90
|
process.stderr.write(
|
|
@@ -101,14 +106,14 @@ function liveConnections() {
|
|
|
101
106
|
//
|
|
102
107
|
// MAALT 21/8 i flow-harnessen: uden laasen skiftede den aktive udvidelse MIDT i en
|
|
103
108
|
// koersel. Foerste kommando (navigate) gik til udvidelse A, som aabnede fanen. Et
|
|
104
|
-
// oejeblik senere forbandt udvidelse B og overtog, fordi den var nyere i raekken
|
|
109
|
+
// oejeblik senere forbandt udvidelse B og overtog, fordi den var nyere i raekken -
|
|
105
110
|
// men B kendte ikke A's fane og lavede en frisk about:blank. Alt derefter fejlede med
|
|
106
111
|
// "Cannot access contents of url about:blank". 21 af 43 vaerktoejer faldt paa det, og
|
|
107
112
|
// symptomet lignede praecis "faner forsvinder" og "kun én session virker".
|
|
108
113
|
//
|
|
109
114
|
// Faner hoerer til den udvidelse der aabnede dem. Skifter man udvidelse, strander de.
|
|
110
115
|
// Derfor: vaelg én gang, og bliv ved den saa laenge dens forbindelse lever. Doer den,
|
|
111
|
-
// vaelges der forfra
|
|
116
|
+
// vaelges der forfra - det er en aegte genopretning, ikke et vilkaarligt skift.
|
|
112
117
|
let laastForbindelse = null;
|
|
113
118
|
let harSendtKommando = false;
|
|
114
119
|
const PINNET_UDVIDELSE = (process.env.BROWSER_MCP_EXTENSION_ID || '').trim() || null;
|
|
@@ -117,7 +122,7 @@ function activeConnection() {
|
|
|
117
122
|
if (laastForbindelse && laastForbindelse.ws.readyState === 1) return laastForbindelse;
|
|
118
123
|
|
|
119
124
|
// Nyeste udvidelse vinder. Ved uafgjort: den der forbandt sidst. En udvidelse fra
|
|
120
|
-
// foer haandtrykket har ingen version og taber til en der har én
|
|
125
|
+
// foer haandtrykket har ingen version og taber til en der har én - haandtrykket kom
|
|
121
126
|
// med den nyere udgave.
|
|
122
127
|
let best = null;
|
|
123
128
|
for (const c of liveConnections()) {
|
|
@@ -147,6 +152,22 @@ const pending = new Map();
|
|
|
147
152
|
// Timers hoisted to module scope so gracefulShutdown can clear them deterministically.
|
|
148
153
|
let heartbeat = null;
|
|
149
154
|
let parentCheck = null;
|
|
155
|
+
let tomgangsvagt = null;
|
|
156
|
+
|
|
157
|
+
// ── 4-timers-tomgangen bor paa modul-niveau, ikke i hjerteslaget ────────────
|
|
158
|
+
//
|
|
159
|
+
// FUNDET AF REVIEW 7/9. Tjekket laa INDE i `heartbeat`, som kun findes mens serveren
|
|
160
|
+
// har en port - og som `frigivPort` rydder. Da porten blev doven, betoed det at en
|
|
161
|
+
// proces der ALDRIG binder (en chat der ikke roerer browseren) heller aldrig faar
|
|
162
|
+
// sin tomgang tjekket. Det var den eneste vej hvor en browser-inaktiv server gav sine
|
|
163
|
+
// ~35 MB tilbage; med mange samtidige chats er det maalbart. Nu koerer vagten altid.
|
|
164
|
+
// Graensen staar som literal, ikke bag en konstant: `vagter.test.mjs` laeser tallet
|
|
165
|
+
// direkte ud af kilden for at haandhaeve at den aldrig bliver kort. En kort graense
|
|
166
|
+
// ville lade en aaben chat miste browseren permanent - vaerre end en port der staar
|
|
167
|
+
// optaget lidt for laenge.
|
|
168
|
+
tomgangsvagt = setInterval(() => {
|
|
169
|
+
if (Date.now() - lastActivity > 4 * 60 * 60 * 1000) gracefulShutdown('Idle timeout (4h)');
|
|
170
|
+
}, 60000);
|
|
150
171
|
|
|
151
172
|
// ── WebSocket Server ───────────────────────────────────────────────────────
|
|
152
173
|
|
|
@@ -160,7 +181,7 @@ function createWSS(port = BASE_PORT) {
|
|
|
160
181
|
// ingen socket overhovedet.
|
|
161
182
|
//
|
|
162
183
|
// Chrome saetter ALTID Origin: chrome-extension://<32 tegn> paa en WebSocket fra
|
|
163
|
-
// en udvidelse
|
|
184
|
+
// en udvidelse - verificeret mod den koerende. Alt andet er per definition ikke
|
|
164
185
|
// en udvidelse, saa gaten koster aegte brugere ingenting.
|
|
165
186
|
verifyClient: ({ origin }, godkend) => {
|
|
166
187
|
if (/^chrome-extension:\/\/[a-p]{32}$/.test(origin || '')) return godkend(true);
|
|
@@ -181,15 +202,25 @@ function createWSS(port = BASE_PORT) {
|
|
|
181
202
|
} else {
|
|
182
203
|
// MAALT 22/8: her stod KUN denne stderr-linje. Ingen laeser stderr fra en
|
|
183
204
|
// MCP-server, saa udtoemte porte var en helt tavs fejl. Hvert vaerktoejskald
|
|
184
|
-
// fejlede bagefter med "extension not connected"
|
|
205
|
+
// fejlede bagefter med "extension not connected" - en tekst der oven i koebet
|
|
185
206
|
// siger at serveren koerer og sender brugeren til Chrome Web Store. Begge dele
|
|
186
207
|
// er forkerte naar sandheden er at vi aldrig fik en port. Flaget laeses i
|
|
187
208
|
// sendToExtension, saa agenten kan give brugeren den rigtige forklaring.
|
|
188
209
|
alleePorteOptaget = true;
|
|
189
210
|
process.stderr.write(`[MCP] All ports ${BASE_PORT}-${MAX_PORT} in use. Cannot start.\n`);
|
|
211
|
+
loesPortLoefte(false);
|
|
190
212
|
}
|
|
191
213
|
} else {
|
|
214
|
+
// FUNDET AF REVIEW 7/9. Her stod KUN stderr-linjen. Foer porten blev doven, var
|
|
215
|
+
// det harmloest: en opstartsfejl man kunne se i loggen. Nu venter `sikrePort()`
|
|
216
|
+
// paa et loefte der aldrig blev indfriet - og `sendToExtension` goer
|
|
217
|
+
// `await sikrePort()` UDEN timeout. Enhver anden bind-fejl end EADDRINUSE
|
|
218
|
+
// (EACCES paa en privilegeret port, EADDRNOTAVAIL, en restriktiv firewall)
|
|
219
|
+
// ville derfor faa hvert eneste browser-kald til at haenge tavst for evigt.
|
|
220
|
+
// At haenge uden besked er vaerre end den fejl vi rettede.
|
|
221
|
+
bindFejl = err.message;
|
|
192
222
|
process.stderr.write(`[MCP] WebSocket error: ${err.message}\n`);
|
|
223
|
+
loesPortLoefte(false);
|
|
193
224
|
}
|
|
194
225
|
});
|
|
195
226
|
|
|
@@ -197,7 +228,7 @@ function createWSS(port = BASE_PORT) {
|
|
|
197
228
|
// WebSocket-handshaket fra en udvidelse baerer Origin: chrome-extension://<id>.
|
|
198
229
|
// Den identificerer afsenderen UDEN at udvidelsen behoever at kende haandtrykket,
|
|
199
230
|
// saa en konflikt mellem to indlaeste udvidelser kan opdages ogsaa naar begge er
|
|
200
|
-
// gamle udgaver
|
|
231
|
+
// gamle udgaver - hvilket er praecis den situation konflikten opstaar i.
|
|
201
232
|
// Maalt 21/8: to distinkte origins ringede op til hver eneste server.
|
|
202
233
|
const origin = req?.headers?.origin || '';
|
|
203
234
|
const fraOrigin = /^chrome-extension:\/\/([a-p]{32})$/.exec(origin)?.[1] || null;
|
|
@@ -209,14 +240,14 @@ function createWSS(port = BASE_PORT) {
|
|
|
209
240
|
// udvidelse med `hello version 99.0.0`, fik `browser_get_cookies` leveret, og
|
|
210
241
|
// kunne lukke serveren med `terminate`. Alle tre trin lykkedes.
|
|
211
242
|
//
|
|
212
|
-
// Serveren lytter kun paa 127.0.0.1, saa angriberen skal koere lokalt
|
|
243
|
+
// Serveren lytter kun paa 127.0.0.1, saa angriberen skal koere lokalt - men det
|
|
213
244
|
// goer enhver anden app og ethvert npm-postinstall-script. Og hvad den kan er
|
|
214
245
|
// ikke smaating: laese alt agenten sender til browseren (kodeord fra ask_user,
|
|
215
246
|
// cookies, sidetekst), fodre agenten med opdigtet sideindhold, og slukke
|
|
216
247
|
// browser-adgangen i alle aabne chats.
|
|
217
248
|
//
|
|
218
249
|
// Chrome saetter ALTID `Origin: chrome-extension://<id>` paa en WebSocket fra en
|
|
219
|
-
// udvidelse
|
|
250
|
+
// udvidelse - verificeret mod den koerende udvidelse. En manglende header er
|
|
220
251
|
// derfor ikke en aeldre udgave; det er noget andet end en udvidelse.
|
|
221
252
|
if (!fraOrigin) {
|
|
222
253
|
process.stderr.write(
|
|
@@ -231,7 +262,7 @@ function createWSS(port = BASE_PORT) {
|
|
|
231
262
|
// slaa dem fra: BROWSER_MCP_EXTENSION_ID=<id> binder serveren til én bestemt.
|
|
232
263
|
// Uden den er valget vilkaarligt naar ingen af dem oplyser en version.
|
|
233
264
|
if (PINNET_UDVIDELSE && fraOrigin && fraOrigin !== PINNET_UDVIDELSE) {
|
|
234
|
-
process.stderr.write(`[MCP] Afviser udvidelse ${fraOrigin}
|
|
265
|
+
process.stderr.write(`[MCP] Afviser udvidelse ${fraOrigin} - bundet til ${PINNET_UDVIDELSE}\n`);
|
|
235
266
|
try { ws.close(1008, 'not the pinned extension'); } catch {}
|
|
236
267
|
return;
|
|
237
268
|
}
|
|
@@ -248,7 +279,7 @@ function createWSS(port = BASE_PORT) {
|
|
|
248
279
|
// If extension was auto-updated, trigger reload
|
|
249
280
|
if (process.env.BROWSER_MCP_EXTENSION_UPDATED === '1') {
|
|
250
281
|
process.env.BROWSER_MCP_EXTENSION_UPDATED = '';
|
|
251
|
-
process.stderr.write('[MCP] Extension files updated
|
|
282
|
+
process.stderr.write('[MCP] Extension files updated - triggering auto-reload\n');
|
|
252
283
|
setTimeout(() => {
|
|
253
284
|
sendToExtension('reload_extension', {}, 5000).catch(() => {});
|
|
254
285
|
}, 1000);
|
|
@@ -260,7 +291,7 @@ function createWSS(port = BASE_PORT) {
|
|
|
260
291
|
|
|
261
292
|
// Identitets-haandtryk fra offscreen-dokumentet (v1.28+).
|
|
262
293
|
if (msg.type === 'hello') {
|
|
263
|
-
// MAALT 23/8: her stod `conn.extensionId = msg.extensionId`
|
|
294
|
+
// MAALT 23/8: her stod `conn.extensionId = msg.extensionId` - altsaa lod
|
|
264
295
|
// haandtrykket afsenderen OVERSKRIVE sin egen identitet med hvad som helst.
|
|
265
296
|
// Origin-headeren er den eneste kilde Chrome selv saetter og som afsenderen
|
|
266
297
|
// ikke kan forfalske, saa den vinder. Beskedens id gemmes separat: stemmer de
|
|
@@ -269,25 +300,37 @@ function createWSS(port = BASE_PORT) {
|
|
|
269
300
|
conn.harHilst = true;
|
|
270
301
|
if (conn.helloId && conn.helloId !== conn.extensionId) {
|
|
271
302
|
process.stderr.write(
|
|
272
|
-
`[MCP] Haandtryk oplyser ${conn.helloId} men Origin siger ${conn.extensionId}
|
|
303
|
+
`[MCP] Haandtryk oplyser ${conn.helloId} men Origin siger ${conn.extensionId} - ` +
|
|
273
304
|
'bruger Origin\n',
|
|
274
305
|
);
|
|
275
306
|
}
|
|
276
307
|
conn.version = typeof msg.version === 'string' ? msg.version : null;
|
|
277
308
|
conn.name = typeof msg.name === 'string' ? msg.name : null;
|
|
309
|
+
// Fingeraftryk af udvidelsens egen background.js (plan 1.10 / R2): versionsnummeret siger ikke hvilken KODE der
|
|
310
|
+
// koerer. Udgivelsens flowtest sammenligner det med repoets fil, saa en gammel kopi med samme nummer ikke kan
|
|
311
|
+
// passere som kandidaten.
|
|
312
|
+
conn.kode = typeof msg.kode === 'string' ? msg.kode : null;
|
|
278
313
|
advarOmKonflikt(conn);
|
|
279
314
|
return;
|
|
280
315
|
}
|
|
281
316
|
|
|
317
|
+
// Aftrykket eftersendes, hvis udvidelsen ikke havde beregnet det da den hilste. MAALT 12/9 af Astra: en frist paa
|
|
318
|
+
// 1 s i udvidelsen gjorde et langsomt aftryk til `null`, og gaten afviste sin egen kandidat. Nu venter hilsenen
|
|
319
|
+
// ikke, og aftrykket kommer naar det er klart.
|
|
320
|
+
if (msg.type === 'kode') {
|
|
321
|
+
if (typeof msg.kode === 'string') conn.kode = msg.kode;
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
|
|
282
325
|
if (msg.type === 'terminate') {
|
|
283
326
|
// terminate lukker serveren for ALLE chats paa porten, saa den har to gates.
|
|
284
327
|
//
|
|
285
|
-
// Om id-sammenligningen: ORIGIN er autoriteten
|
|
328
|
+
// Om id-sammenligningen: ORIGIN er autoriteten - Chrome saetter den, og
|
|
286
329
|
// afsenderen kan ikke forfalske den. Haandtrykkets id er kun et ekstra
|
|
287
330
|
// signal. MAALT 23/8 mod den AEGTE udvidelse: den sender hello med id null,
|
|
288
331
|
// fordi chrome.runtime.getManifest() kan fejle i offscreen-dokumentet.
|
|
289
332
|
// Kraevede vi lighed ubetinget, kunne en HELT legitim udvidelse aldrig lukke
|
|
290
|
-
// sin session ned
|
|
333
|
+
// sin session ned - en fejl jeg selv indfoerte samme aften. Derfor: et id der
|
|
291
334
|
// MANGLER er fint (Origin har allerede bevist hvem det er), mens et id der er
|
|
292
335
|
// TIL STEDE og peger et ANDET sted afvises.
|
|
293
336
|
//
|
|
@@ -298,14 +341,18 @@ function createWSS(port = BASE_PORT) {
|
|
|
298
341
|
// kopi, der lukkede sin sidste fane, rive serveren vaek under den
|
|
299
342
|
// udvidelse der reelt loeste opgaven.
|
|
300
343
|
if (!conn.harHilst || (conn.helloId && conn.helloId !== conn.extensionId)) {
|
|
301
|
-
process.stderr.write('[MCP] terminate ignoreret
|
|
344
|
+
process.stderr.write('[MCP] terminate ignoreret - intet gyldigt haandtryk\n');
|
|
302
345
|
return;
|
|
303
346
|
}
|
|
304
347
|
if (activeConnection() !== conn) {
|
|
305
|
-
process.stderr.write('[MCP] terminate ignoreret
|
|
348
|
+
process.stderr.write('[MCP] terminate ignoreret - kom fra en inaktiv udvidelses-forbindelse\n');
|
|
306
349
|
return;
|
|
307
350
|
}
|
|
308
|
-
gracefulShutdown
|
|
351
|
+
// AENDRET 7/9: her stod `gracefulShutdown`. En chat der var faerdig med browseren
|
|
352
|
+
// mistede altsaa browseren HELT - og porten blev alligevel hverken frigivet hurtigt
|
|
353
|
+
// nok til andre (processen doede foerst efter oprydning) eller genvundet af chatten
|
|
354
|
+
// selv. Nu slippes kun porten; naeste browser-kald tager en ny (se sikrePort).
|
|
355
|
+
frigivPort('udvidelsen meldte: sidste fane lukket');
|
|
309
356
|
return;
|
|
310
357
|
}
|
|
311
358
|
|
|
@@ -320,11 +367,11 @@ function createWSS(port = BASE_PORT) {
|
|
|
320
367
|
|
|
321
368
|
// ── En doed socket skal AFVISE de kald der var undervejs (MAALT 23/8) ──────
|
|
322
369
|
// Foer roerte close-handleren ikke `pending`. Maalt: en socket der doede 300 ms
|
|
323
|
-
// inde i et kald gav foerst svar efter 30.011 ms
|
|
370
|
+
// inde i et kald gav foerst svar efter 30.011 ms - og med den FORKERTE
|
|
324
371
|
// forklaring, "kommandoen tog for lang tid". For extract_list er timeouten 180
|
|
325
372
|
// sekunder, altsaa tre minutters tavshed hvor sandheden var kendt med det samme.
|
|
326
373
|
const afvisVentende = (grund) => {
|
|
327
|
-
// Kun naar ingen anden levende forbindelse kan svare
|
|
374
|
+
// Kun naar ingen anden levende forbindelse kan svare - ellers ville et helt
|
|
328
375
|
// normalt skift mellem to udvidelser afbryde kald der er fuldt i orden.
|
|
329
376
|
if (!pending.size || liveConnections().length) return;
|
|
330
377
|
const antal = pending.size;
|
|
@@ -333,16 +380,16 @@ function createWSS(port = BASE_PORT) {
|
|
|
333
380
|
pending.delete(id);
|
|
334
381
|
p.reject(new Error(
|
|
335
382
|
`Forbindelsen til Chrome-udvidelsen forsvandt mens kommandoen koerte (${grund}). ` +
|
|
336
|
-
'Kommandoen naaede maaske at blive udfoert i browseren
|
|
383
|
+
'Kommandoen naaede maaske at blive udfoert i browseren - tjek tilstanden foer du ' +
|
|
337
384
|
'proever igen. Er udvidelsen slaaet fra eller Chrome lukket, saa start den og proev forfra.',
|
|
338
385
|
));
|
|
339
386
|
}
|
|
340
|
-
process.stderr.write(`[MCP] ${antal} ventende kald afvist
|
|
387
|
+
process.stderr.write(`[MCP] ${antal} ventende kald afvist - ${grund}\n`);
|
|
341
388
|
};
|
|
342
389
|
|
|
343
390
|
// MAALT 23/8: der fandtes INGEN error-handler. Et ugyldigt WebSocket-frame (fx
|
|
344
391
|
// RSV1 sat) faar 'ws' til at emitte 'error' paa socketen, og en uhaandteret
|
|
345
|
-
// 'error' paa en EventEmitter kaster og draeber hele processen
|
|
392
|
+
// 'error' paa en EventEmitter kaster og draeber hele processen - altsaa alle
|
|
346
393
|
// chats paa den port. Tre linjer lukker det.
|
|
347
394
|
ws.on('error', (e) => {
|
|
348
395
|
process.stderr.write(`[MCP] WebSocket-fejl paa forbindelsen: ${e?.message || e}\n`);
|
|
@@ -358,39 +405,151 @@ function createWSS(port = BASE_PORT) {
|
|
|
358
405
|
|
|
359
406
|
server.on('listening', () => {
|
|
360
407
|
activePort = port;
|
|
408
|
+
portBundetTid = Date.now();
|
|
361
409
|
process.stderr.write(`[MCP] WebSocket server listening on ws://127.0.0.1:${port}\n`);
|
|
410
|
+
loesPortLoefte(true);
|
|
362
411
|
});
|
|
363
412
|
|
|
364
|
-
// Heartbeat + idle timeout (4 hours)
|
|
413
|
+
// Heartbeat + idle timeout (4 hours) - hoisted to module scope so gracefulShutdown can clear it
|
|
414
|
+
// Ryddes foerst: porten kan bindes flere gange i samme proces (se sikrePort), og uden
|
|
415
|
+
// det her ville hver ny binding efterlade en ekstra timer der aldrig blev stoppet.
|
|
416
|
+
if (heartbeat) clearInterval(heartbeat);
|
|
365
417
|
heartbeat = setInterval(() => {
|
|
366
418
|
for (const c of liveConnections()) c.ws.ping();
|
|
367
|
-
if (Date.now() - lastActivity > 4 * 60 * 60 * 1000) {
|
|
368
|
-
gracefulShutdown('Idle timeout (4h)');
|
|
369
|
-
}
|
|
370
419
|
}, 20000);
|
|
371
420
|
}
|
|
372
421
|
|
|
373
|
-
|
|
422
|
+
// ── Porten tages ved BRUG, ikke ved opstart ─────────────────────────────────
|
|
423
|
+
//
|
|
424
|
+
// MAALT 7/9-2026 paa Gustavs maskine: 37 koerende servere, 20 porte i spaendet, 17 chats
|
|
425
|
+
// helt uden browser. Hver Claude Code-chat starter en server ved opstart - ogsaa de mange
|
|
426
|
+
// chats der aldrig roerer browseren - og her stod `createWSS()` paa modul-niveau. Porten
|
|
427
|
+
// blev altsaa reserveret af en chat der maaske aldrig fik brug for den, og holdt indtil
|
|
428
|
+
// chatten doede eller 4-timers-tomgangen udloeb.
|
|
429
|
+
//
|
|
430
|
+
// Konsekvensen var ensidigt slem: chat nr. 21 fik `alleePorteOptaget = true` ÉN gang og
|
|
431
|
+
// proevede aldrig igen - dens browser var doed hele chattens levetid, selv naar en port
|
|
432
|
+
// blev fri et minut senere.
|
|
433
|
+
//
|
|
434
|
+
// Nu bindes porten foerste gang et vaerktoej faktisk skal bruge udvidelsen, og HVERT kald
|
|
435
|
+
// proever igen hvis det forrige ikke fik en. Udvidelsen genscanner hele spaendet hvert
|
|
436
|
+
// 2. sekund (offscreen.js), saa en port der aabnes sent bliver fundet af sig selv.
|
|
437
|
+
//
|
|
438
|
+
// Bemaerk hvorfor det ikke er en 5-minutters timer der draeber processen: en chat der
|
|
439
|
+
// foerst skal bruge browseren efter en halv time ville saa staa uden. Processen lever
|
|
440
|
+
// videre - det er kun PORTEN der ikke holdes reserveret til noget der ikke sker.
|
|
441
|
+
let portResolver = null;
|
|
442
|
+
let bindLoefte = null;
|
|
443
|
+
|
|
444
|
+
function loesPortLoefte(fik) {
|
|
445
|
+
if (!portResolver) return;
|
|
446
|
+
const r = portResolver;
|
|
447
|
+
portResolver = null;
|
|
448
|
+
bindLoefte = null;
|
|
449
|
+
r(fik);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// Slip porten, men BLIV I LIVE. Forskellen er hele pointen: lukkede vi processen ned,
|
|
453
|
+
// ville en chat der er faerdig med browseren kl. 10 og skal bruge den igen kl. 10:40 staa
|
|
454
|
+
// uden - og Claude Code genstarter ikke en MCP-server midt i en samtale. Processen koster
|
|
455
|
+
// ~35 MB og ingen port; det er porten der er den knappe ressource.
|
|
456
|
+
function frigivPort(grund) {
|
|
457
|
+
if (activePort === null) return;
|
|
458
|
+
process.stderr.write(`[MCP] frigiver port ${activePort} - ${grund}\n`);
|
|
459
|
+
if (heartbeat) { clearInterval(heartbeat); heartbeat = null; }
|
|
460
|
+
const gammel = wss;
|
|
461
|
+
wss = null;
|
|
462
|
+
activePort = null;
|
|
463
|
+
laastForbindelse = null;
|
|
464
|
+
harSendtKommando = false; // naeste binding vaelger udvidelse forfra
|
|
465
|
+
for (const c of connections) { try { c.ws.close(); } catch {} }
|
|
466
|
+
connections.clear();
|
|
467
|
+
try { gammel?.close(); } catch {}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
function sikrePort() {
|
|
471
|
+
if (activePort !== null) return Promise.resolve(true);
|
|
472
|
+
if (bindLoefte) return bindLoefte; // en binding er allerede i gang
|
|
473
|
+
alleePorteOptaget = false; // hvert forsoeg starter paa en frisk
|
|
474
|
+
bindFejl = null;
|
|
475
|
+
portBundetTid = 0; // nulstilles her; saettes naar 'listening' faktisk kommer
|
|
476
|
+
bindLoefte = new Promise((res) => { portResolver = res; });
|
|
477
|
+
// Vagthund. F8 var ét konkret hul hvor loeftet aldrig blev indfriet; det her lukker
|
|
478
|
+
// KLASSEN. Emitter en fremtidig fejlsti hverken 'listening' eller 'error', svarer
|
|
479
|
+
// kaldet nu med en fejl i stedet for at haenge tavst for evigt. 10 s er rigeligt:
|
|
480
|
+
// en loopback-binding tager millisekunder, og hele spaendet naas paa under ét.
|
|
481
|
+
const vagthund = setTimeout(() => {
|
|
482
|
+
if (!portResolver) return;
|
|
483
|
+
bindFejl = bindFejl || 'bindingen svarede ikke inden for 10 sekunder';
|
|
484
|
+
process.stderr.write('[MCP] port-bindingen svarede aldrig - opgiver dette forsoeg\n');
|
|
485
|
+
loesPortLoefte(false);
|
|
486
|
+
}, 10000);
|
|
487
|
+
bindLoefte.finally(() => clearTimeout(vagthund));
|
|
488
|
+
createWSS();
|
|
489
|
+
return bindLoefte;
|
|
490
|
+
}
|
|
374
491
|
|
|
375
492
|
// ── Send command to extension ───────────────────────────────────────────────
|
|
376
493
|
|
|
377
|
-
async function sendToExtension(method, params = {}, timeoutMs = 30000, _retries = 5) {
|
|
494
|
+
async function sendToExtension(method, params = {}, timeoutMs = 30000, _retries = 5, _ekstraRunde = false, _doerAabnetNu = null) {
|
|
495
|
+
// Skaf en port hvis vi ikke har en. Foerste kald binder; senere kald er en no-op.
|
|
496
|
+
// Fik vi ingen (hele spaendet optaget), proever naeste kald igen - derfor ingen kast her.
|
|
497
|
+
await sikrePort();
|
|
498
|
+
// Spoergsmaalet er om doeren var NYAABNET da kaldet begyndte - ikke om den stadig er
|
|
499
|
+
// "ny" efter at vi selv har brugt femten sekunder paa at proeve igen. Derfor maales det
|
|
500
|
+
// ved indgangen og baeres med gennem gentagelserne.
|
|
501
|
+
const doerAabnetNu = _doerAabnetNu !== null
|
|
502
|
+
? _doerAabnetNu
|
|
503
|
+
: Boolean(portBundetTid && (Date.now() - portBundetTid) < 10000);
|
|
378
504
|
// Retry if extension is temporarily disconnected (reconnects every 2s)
|
|
379
505
|
const conn = activeConnection();
|
|
380
506
|
if (!conn) {
|
|
381
507
|
if (_retries > 0) {
|
|
382
508
|
await new Promise(r => setTimeout(r, 1500));
|
|
383
|
-
return sendToExtension(method, params, timeoutMs, _retries - 1);
|
|
509
|
+
return sendToExtension(method, params, timeoutMs, _retries - 1, _ekstraRunde, doerAabnetNu);
|
|
510
|
+
}
|
|
511
|
+
// ── Har vi lige aabnet doeren selv? (MAALT 8/9, #16) ─────────────────────
|
|
512
|
+
//
|
|
513
|
+
// Foer porten blev doven, var udvidelsen for laengst forbundet naar foerste
|
|
514
|
+
// vaerktoejskald kom. Nu starter uret VED kaldet: binding -> op til 2 s til
|
|
515
|
+
// udvidelsens naeste port-scanning -> probe -> WS-haandtryk, som udvidelsens egen
|
|
516
|
+
// bremse lovligt kan holde i flere sekunder. Budgettet er 5 x 1500 ms.
|
|
517
|
+
//
|
|
518
|
+
// Paa en maskine med mange samtidige chats er marginen tynd - og beskeden nedenfor
|
|
519
|
+
// er den vaerst mulige: den sender en bruger hen for at reparere en installation der
|
|
520
|
+
// virker. Derfor: er porten aabnet inden for de sidste 15 sekunder, giver vi den ét
|
|
521
|
+
// ekstra budget, og siger sandheden hvis den stadig er tom.
|
|
522
|
+
if (doerAabnetNu && !_ekstraRunde) {
|
|
523
|
+
return sendToExtension(method, params, timeoutMs, 5, true, doerAabnetNu);
|
|
524
|
+
}
|
|
525
|
+
if (doerAabnetNu) {
|
|
526
|
+
throw new Error(
|
|
527
|
+
`Porten ${activePort} blev aabnet for ${Math.round((Date.now() - portBundetTid) / 1000)} ` +
|
|
528
|
+
'sekunder siden, og udvidelsen har ikke naaet at forbinde endnu. Den scanner hvert ' +
|
|
529
|
+
'2. sekund, saa det tager normalt under fem.\n' +
|
|
530
|
+
'Det er sandsynligvis IKKE en manglende installation - proev kommandoen igen om et ' +
|
|
531
|
+
'oejeblik. Bliver den ved, saa tjek at Chrome koerer og at udvidelsen er slaaet til.\n' +
|
|
532
|
+
'Sig det til brugeren i den raekkefoelge. Bed IKKE om en geninstallation foerst.',
|
|
533
|
+
);
|
|
384
534
|
}
|
|
385
535
|
// This is the other half of the two-part setup: the server is clearly running (it is
|
|
386
536
|
// throwing this), so what is missing is the extension, Chrome itself, or the connection
|
|
387
|
-
// between them. Say which, and where to get it
|
|
537
|
+
// between them. Say which, and where to get it - the agent relays this text to the user.
|
|
538
|
+
if (bindFejl) {
|
|
539
|
+
throw new Error(
|
|
540
|
+
`Serveren kunne ikke aabne en port paa 127.0.0.1 (${BASE_PORT}-${MAX_PORT}): ${bindFejl}\n` +
|
|
541
|
+
'Det er IKKE Chrome eller udvidelsen - det er operativsystemet eller en firewall der ' +
|
|
542
|
+
'afviser bindingen. Tjek om noget blokerer loopback-porte.\n' +
|
|
543
|
+
'Sig praecis dét til brugeren. Sig IKKE at udvidelsen mangler.',
|
|
544
|
+
);
|
|
545
|
+
}
|
|
388
546
|
if (alleePorteOptaget) {
|
|
389
547
|
throw new Error(
|
|
390
|
-
`Alle porte ${BASE_PORT}-${MAX_PORT} er optaget, saa
|
|
391
|
-
'Det er IKKE et problem med Chrome eller udvidelsen
|
|
392
|
-
|
|
393
|
-
'Luk en af dem
|
|
548
|
+
`Alle porte ${BASE_PORT}-${MAX_PORT} er optaget lige nu, saa dette kald fik ingen port. ` +
|
|
549
|
+
'Det er IKKE et problem med Chrome eller udvidelsen - de virker fint.\n' +
|
|
550
|
+
`${MAX_PORT - BASE_PORT + 1} andre chats bruger browseren i oejeblikket. ` +
|
|
551
|
+
'Luk en af dem, eller vent til en bliver faerdig - og proev saa kommandoen igen. ' +
|
|
552
|
+
'Denne chat skal IKKE genstartes: hvert kald proever selv at faa en port.\n' +
|
|
394
553
|
'Sig praecis dét til brugeren. Sig IKKE at udvidelsen mangler.',
|
|
395
554
|
);
|
|
396
555
|
}
|
|
@@ -400,7 +559,7 @@ async function sendToExtension(method, params = {}, timeoutMs = 30000, _retries
|
|
|
400
559
|
'Chrome extension (apparently not reachable).\n' +
|
|
401
560
|
'Check, in order:\n' +
|
|
402
561
|
' 1. Chrome is actually open and running.\n' +
|
|
403
|
-
' 2. The extension is installed and enabled at chrome://extensions
|
|
562
|
+
' 2. The extension is installed and enabled at chrome://extensions - install it from\n' +
|
|
404
563
|
' https://chromewebstore.google.com/detail/agent360-browser-mcp/jdehgalffmffhfhmmhaokfbfnafnmgcl\n' +
|
|
405
564
|
' 3. Click the extension icon -> Reconnect, and wait 2-3 seconds.\n' +
|
|
406
565
|
'Still stuck: https://browsermcp.dev/docs/troubleshooting/'
|
|
@@ -426,14 +585,14 @@ const INSTRUCTIONS = `You control the user's real Chrome browser via this MCP se
|
|
|
426
585
|
|
|
427
586
|
## Key behaviors
|
|
428
587
|
- **Always use browser_ask_user** when you need credentials, 2FA codes, CAPTCHA help, or any user input. Never guess passwords or tokens.
|
|
429
|
-
- **ALWAYS close tabs when done** with browser_close_tab after completing each task. Don't leave tabs open
|
|
430
|
-
- **Check existing tabs first** with browser_list_tabs before navigating
|
|
431
|
-
- **One task per tab**
|
|
588
|
+
- **ALWAYS close tabs when done** with browser_close_tab after completing each task. Don't leave tabs open - close them immediately after extracting the data you need. Use browser_list_tabs to find and close all session tabs when a task is complete.
|
|
589
|
+
- **Check existing tabs first** with browser_list_tabs before navigating - reuse tabs instead of opening duplicates.
|
|
590
|
+
- **One task per tab** - navigate to a URL, do your work, then close or move on.
|
|
432
591
|
- **Tell the user what you're doing** in the browser. "I'm navigating to Stripe to find the API key" not just silently calling tools.
|
|
433
592
|
|
|
434
593
|
## Tab management
|
|
435
594
|
- navigate creates tabs in your session's tab group (visible in Chrome as colored groups)
|
|
436
|
-
- list_tabs only shows YOUR session's tabs
|
|
595
|
+
- list_tabs only shows YOUR session's tabs - other Claude sessions have their own
|
|
437
596
|
- switch_tab lets you jump between your tabs
|
|
438
597
|
- close_tab cleans up when you're done
|
|
439
598
|
|
|
@@ -446,27 +605,31 @@ const INSTRUCTIONS = `You control the user's real Chrome browser via this MCP se
|
|
|
446
605
|
6. After success, extract what you need with browser_get_page_content
|
|
447
606
|
|
|
448
607
|
## Screenshots
|
|
449
|
-
- browser_screenshot captures
|
|
450
|
-
-
|
|
608
|
+
- browser_screenshot captures YOUR session's tab, without pulling it in front of the user
|
|
609
|
+
- It does NOT activate the tab first. That was removed deliberately: the user sits in the
|
|
610
|
+
same window, and yanking their tab away on every screenshot is worse than the alternative
|
|
611
|
+
- The picture always comes from the debugger for YOUR tab. There is no fallback that photographs
|
|
612
|
+
whichever tab happens to be visible, so if the debugger cannot produce a frame the call fails.
|
|
613
|
+
If the whole window is covered, it may be raised briefly as a last resort
|
|
451
614
|
|
|
452
615
|
## Text-based selectors (preferred for dynamic sites)
|
|
453
|
-
- browser_click("text=Get started")
|
|
454
|
-
- browser_click("button:text(Submit)")
|
|
455
|
-
- browser_fill("text=Email", "user@example.com")
|
|
456
|
-
- browser_wait("text=Success")
|
|
616
|
+
- browser_click("text=Get started") - clicks any element containing "Get started"
|
|
617
|
+
- browser_click("button:text(Submit)") - clicks a button containing "Submit"
|
|
618
|
+
- browser_fill("text=Email", "user@example.com") - fills input near "Email" label
|
|
619
|
+
- browser_wait("text=Success") - waits for text to appear
|
|
457
620
|
- These work on ALL sites including Google Cloud, Stripe, Slack (CSP-strict)
|
|
458
621
|
|
|
459
622
|
## Keyboard
|
|
460
|
-
- browser_press_key("Enter")
|
|
461
|
-
- browser_press_key("Tab")
|
|
462
|
-
- browser_press_key("Escape")
|
|
463
|
-
- browser_press_key("ArrowDown")
|
|
464
|
-
- browser_press_key("a", ctrl=true)
|
|
623
|
+
- browser_press_key("Enter") - submit forms
|
|
624
|
+
- browser_press_key("Tab") - navigate between fields
|
|
625
|
+
- browser_press_key("Escape") - close dialogs
|
|
626
|
+
- browser_press_key("ArrowDown") - navigate dropdowns
|
|
627
|
+
- browser_press_key("a", ctrl=true) - select all
|
|
465
628
|
|
|
466
629
|
## CAPTCHA handling
|
|
467
630
|
Use browser_solve_captcha to detect and solve CAPTCHAs automatically:
|
|
468
|
-
1. Call browser_solve_captcha()
|
|
469
|
-
2. If reCAPTCHA v2 checkbox found → call browser_solve_captcha(action="click_checkbox")
|
|
631
|
+
1. Call browser_solve_captcha() - detects CAPTCHA type on page
|
|
632
|
+
2. If reCAPTCHA v2 checkbox found → call browser_solve_captcha(action="click_checkbox") - auto-clicks; often passes when signed into Google
|
|
470
633
|
3. If image challenge appears → call browser_screenshot, analyze the grid visually, then call browser_solve_captcha(action="click_grid", cells=[2,5,7]) with the correct cell indices
|
|
471
634
|
4. If all else fails → call browser_solve_captcha(action="ask_human") to show overlay to user
|
|
472
635
|
5. After solving, retry the action that was blocked
|
|
@@ -482,7 +645,7 @@ For image grid challenges: cells are 0-indexed, left-to-right, top-to-bottom. A
|
|
|
482
645
|
- If a standard selector fails, the extension recursively searches shadow roots
|
|
483
646
|
- Text-based selectors ("text=Submit") also traverse shadow DOM
|
|
484
647
|
|
|
485
|
-
## Hard inputs
|
|
648
|
+
## Hard inputs - use the specialised tools first
|
|
486
649
|
- **Date inputs** → use browser_set_date (NOT browser_fill). Handles native date inputs, masked text inputs (MM/DD/YYYY etc.), AND calendar pickers (MUI, react-datepicker, AntD, Lexical/Meta). 3-path fallback with read-back verification.
|
|
487
650
|
- **Autocomplete / combobox** (Languages on Meta Ads, country selects, async dropdowns) → use browser_set_combobox (NOT browser_select_option). Types partial query, waits for filtered listbox, clicks option. Supports multi-value chips.
|
|
488
651
|
- **Drag-drop file zones without visible file input** → use browser_drop_file (NOT browser_upload_file). Finds hidden input in subtree/parent.
|
|
@@ -492,28 +655,31 @@ For image grid challenges: cells are 0-indexed, left-to-right, top-to-bottom. A
|
|
|
492
655
|
- Element not found → try text-based selector instead of CSS
|
|
493
656
|
- Screenshot fails → debugger fallback is automatic
|
|
494
657
|
- Click doesn't work on SPA → debugger mouse events are used automatically
|
|
658
|
+
- A click, hover or key press answers "CDP svarede ikke inden … ms" → the tab is in the background, and Chrome does not deliver mouse or key input to a tab that is not active. Call browser_switch_tab to that tab, then try again (browser_click on a CSS selector already falls back to a script click)
|
|
659
|
+
- An answer with maaske_landet: true means the action was sent but its effect could not be confirmed. Check the page first (browser_get_page_content or browser_screenshot) and do not repeat it blindly: a second click can submit twice. landed: false means the page showed no visible reaction to the click. landed: null with uvist: true means something on the page changed when the mouse went down, but not from the click itself - it may be a ripple effect, and it may be a menu that opens on mousedown. Read the page before clicking again: a second click closes a menu that is already open. landed: null with uverificeret: true means the mouse button was sent but the page could not be read afterwards - same rule: read the page before repeating. browser_scroll uses uvist with its own meaning - there it means the scroll was sent but the movement could not be seen, and the answer carries ok:true, a note and the measured position
|
|
660
|
+
- browser_fill with afviger: true means the field shows something other than what you typed; read faktisk. uaendret: true means the field showed the same before and after, either because the value was already there in the page's own format or because the page refused it. Check faktisk before moving on
|
|
495
661
|
- CAPTCHA blocks page → use browser_ask_user, let human solve it
|
|
496
662
|
- browser_fill seemingly succeeds but value reverts → switch to browser_set_date or browser_set_combobox (most reverts are React-controlled validators)
|
|
497
663
|
|
|
498
664
|
## Extension updates
|
|
499
|
-
The
|
|
500
|
-
If the extension
|
|
501
|
-
"
|
|
502
|
-
You cannot navigate to chrome:// pages
|
|
503
|
-
|
|
504
|
-
## When Browser MCP itself is the obstacle
|
|
505
|
-
Before you tell the user "browser-mcp can't do X", or give up on a tool that errored, timed out, or did something other than what it promised: call **browser_provide_feedback** with what_happened. Do it yourself, without asking. It checks the install first
|
|
506
|
-
- **outdated** or **disconnected** → relay fix_steps to the user, then retry the action. An outdated install explains most failures, so try the fix first
|
|
665
|
+
The server updates through npm: clients configured with @agent360/browser-mcp@latest get the newest version when a session starts. When that version carries newer extension files, the server copies them into ~/.browser-mcp/extension and tells the connected extension to reload itself. Chrome Web Store installs update through the store.
|
|
666
|
+
If browser_provide_feedback reports that the extension is older than this server, or that more than one Browser MCP extension is connected, ask the user:
|
|
667
|
+
"Please go to chrome://extensions, find 'Agent360 Browser MCP', and click the reload icon (🔄). If two Browser MCP extensions are listed, turn one of them off."
|
|
668
|
+
You cannot navigate to chrome:// pages - the user must do this manually.
|
|
669
|
+
|
|
670
|
+
## When Browser MCP itself is the obstacle - call browser_provide_feedback
|
|
671
|
+
Before you tell the user "browser-mcp can't do X", or give up on a tool that errored, timed out, or did something other than what it promised: call **browser_provide_feedback** with what_happened. Do it yourself, without asking. It checks the install first - the extension against this server, whether more than one Browser MCP extension is connected at once, and (only when BROWSER_MCP_CHECK_NPM=1) this server against the latest on npm - because those explain most "it's broken" moments, and none of them are visible to you otherwise. Read the verdict:
|
|
672
|
+
- **outdated** or **disconnected** → relay fix_steps to the user, then retry the action. An outdated install explains most failures, so try the fix first - but if the behaviour still looks wrong after that, report it anyway. A release window is not a reason to stay silent about a real bug.
|
|
507
673
|
- **conflict** → more than one extension is loaded; tabs and sessions will keep behaving randomly until the user disables the extras at chrome://extensions. Say so plainly.
|
|
508
674
|
- **current** → the install is fine, so this is a genuine gap. Offer the returned submit_url as a clickable link.
|
|
509
675
|
- **unknown** → nothing local looks wrong, but the npm comparison was skipped (it is opt-in via BROWSER_MCP_CHECK_NPM=1). Treat it like **current**.
|
|
510
676
|
Call it once per distinct obstacle, not once per retry.
|
|
511
677
|
|
|
512
678
|
## Sharing wishes / use-cases / bugs
|
|
513
|
-
Browser MCP is community-shaped. Whenever the user (a) says "I wish browser-mcp could…" or expresses a missing feature, (b) hits something broken or unexpected that looks like a bug in browser-mcp itself, or (c) describes something cool they built with it
|
|
679
|
+
Browser MCP is community-shaped. Whenever the user (a) says "I wish browser-mcp could…" or expresses a missing feature, (b) hits something broken or unexpected that looks like a bug in browser-mcp itself, or (c) describes something cool they built with it - call **browser_about** with the matching intent ("wish" / "use_case" / "bug") and a short title + body, then offer the returned submit_url to the user as a clickable link. Don't ask permission, just draft + offer the link. This is the user's path to contribute back, and they usually don't know it exists.`;
|
|
514
680
|
|
|
515
681
|
// FEJL MAALT 21/8: `instructions` laa i et TREDJE argument. Server-konstruktoeren
|
|
516
|
-
// tager kun to (serverInfo, options), saa JavaScript smed objektet vaek i tavshed
|
|
682
|
+
// tager kun to (serverInfo, options), saa JavaScript smed objektet vaek i tavshed -
|
|
517
683
|
// og hele blokken ovenfor naaede aldrig frem til nogen klient. Verificeret ved at
|
|
518
684
|
// laese initialize-svaret: det havde kun protocolVersion, capabilities og serverInfo.
|
|
519
685
|
// Det betyder at "luk altid faner naar du er faerdig", CAPTCHA-fremgangsmaaden,
|
|
@@ -590,6 +756,62 @@ mcpServer.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
590
756
|
return { content: [{ type: 'text', text: `Unknown tool: ${name}` }], isError: true };
|
|
591
757
|
}
|
|
592
758
|
|
|
759
|
+
// MAALT 10/9: skaermbilledets `path` fik en indeslutning 23/8, med begrundelsen
|
|
760
|
+
// "argumenterne kommer fra en model der laeser FREMMEDE websider". Praecis samme
|
|
761
|
+
// argument gaelder upload - og DER forlader filen faktisk maskinen. Stien gik raat
|
|
762
|
+
// videre til DOM.setFileInputFiles, saa upload_file({files:["~/.ssh/id_rsa"]}) paa en
|
|
763
|
+
// vilkaarlig side med et filfelt lagde noeglen i en upload. Vagten manglede netop hvor
|
|
764
|
+
// konsekvensen var stoerst.
|
|
765
|
+
if (method === 'upload_file' || method === 'drop_file') {
|
|
766
|
+
const raa = Array.isArray(args?.files) ? args.files
|
|
767
|
+
// Astra, tredje runde: aliaserne blev lagt SAMMEN, saa file + file_path sendte to filer.
|
|
768
|
+
// Udvidelsen valgte altid den foerste der fandtes - samme prioritet her.
|
|
769
|
+
: [args?.files || args?.file || args?.file_path].filter(Boolean);
|
|
770
|
+
const rod = resolve(process.cwd());
|
|
771
|
+
// MAALT 10/9 af Astra (anden runde), reproduceret paa denne maskine: resolve() fjerner
|
|
772
|
+
// "link/.." som TEKST foer symlinket er fulgt. Med link -> /ude/dir blev "link/../secret"
|
|
773
|
+
// tjekket som <cwd>/secret, mens den UAENDREDE sti blev sendt videre, og operativsystemet
|
|
774
|
+
// laeste /ude/secret. Node's egen realpathSync normaliserer ogsaa foerst - den gav ENOENT,
|
|
775
|
+
// og saa blev den leksikalske sti godkendt. realpathSync.native spoerger operativsystemet,
|
|
776
|
+
// der foelger links foer "..". Det svar tjekkes, og det er DET der sendes videre: ellers kan
|
|
777
|
+
// det der godkendes og det der aabnes vaere to forskellige filer. (Den giver ogsaa den rigtige
|
|
778
|
+
// bogstavstoerrelse, saa /users/... ikke afvises paa macOS.)
|
|
779
|
+
// Tilbage staar et smalt vindue: en proces med skriveadgang til arbejdsmappen kan bytte en
|
|
780
|
+
// mappe ud med et symlink mellem tjekket og det oejeblik Chrome aabner filen.
|
|
781
|
+
let rodReel = rod; try { rodReel = realpathSync.native(rod); } catch {}
|
|
782
|
+
// MAALT 11/9 af Astra (R5 F10): med arbejdsmappen "/" blev praefikset "//", og enhver almindelig fil
|
|
783
|
+
// blev afvist som "udenfor". En rod der allerede ender paa skilletegnet faar ikke et til.
|
|
784
|
+
const medSkille = (r) => (r.endsWith(sep) ? r : r + sep);
|
|
785
|
+
const inde = (p, r) => p === r || p.startsWith(medSkille(r));
|
|
786
|
+
const kanoniske = [];
|
|
787
|
+
for (const f of raa) {
|
|
788
|
+
const udfoldet = String(f).replace(/^~(?=\/|$)/, homedir());
|
|
789
|
+
const raaSti = isAbsolute(udfoldet) ? udfoldet : medSkille(rod) + udfoldet;
|
|
790
|
+
let reel = null; try { reel = realpathSync.native(raaSti); } catch {}
|
|
791
|
+
const afvis = (hvorfor) => ({
|
|
792
|
+
content: [{ type: 'text', text:
|
|
793
|
+
`Filen skal ligge inden for arbejdsmappen (${rod}). "${f}" ${hvorfor}.\n` +
|
|
794
|
+
`Uploads sender filen til en fremmed side, og stien kommer fra en model der ` +
|
|
795
|
+
`laeser de sider. Kopiér filen ind i arbejdsmappen foerst, hvis den skal med.` }],
|
|
796
|
+
isError: true,
|
|
797
|
+
});
|
|
798
|
+
if (!reel) return afvis(inde(resolve(raaSti), rod) ? 'findes ikke (eller kan ikke laeses)' : 'peger udenfor');
|
|
799
|
+
if (!inde(reel, rodReel)) return afvis('peger udenfor');
|
|
800
|
+
// Astra, tredje runde: en MAPPE blev godkendt ud fra mappens egen sti - men Chrome gennemloeber
|
|
801
|
+
// mappen og foelger links i den, saa bundle/key -> ~/.ssh/id_rsa kom med. Og en HARDLINK inde i
|
|
802
|
+
// mappen er samme fil som en fil udenfor; realpath kan ikke se det.
|
|
803
|
+
let st = null; try { st = statSync(reel); } catch {}
|
|
804
|
+
if (!st || !st.isFile()) return afvis('er ikke en almindelig fil (mapper uploades ikke - de kan indeholde links ud af arbejdsmappen)');
|
|
805
|
+
if (st.nlink > 1) return afvis('har flere navne paa disken (hardlink) og kan vaere en fil uden for arbejdsmappen');
|
|
806
|
+
kanoniske.push(reel);
|
|
807
|
+
}
|
|
808
|
+
if (args && kanoniske.length) {
|
|
809
|
+
args.files = kanoniske;
|
|
810
|
+
delete args.file;
|
|
811
|
+
delete args.file_path;
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
|
|
593
815
|
// extract_list scrolls a container in a loop (up to 300 rounds × wait_ms), so the 30 s
|
|
594
816
|
// default would kill a long mail list mid-walk and report a partial set as complete.
|
|
595
817
|
const timeout = method === 'ask_user' ? (args?.timeout || 120000) + 5000 :
|
|
@@ -610,12 +832,31 @@ mcpServer.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
610
832
|
// overskrive en fil i brugerens hjemmemappe med PNG-bytes.
|
|
611
833
|
const rod = resolve(process.cwd());
|
|
612
834
|
const targetPath = resolve(rod, args.path);
|
|
613
|
-
if (targetPath !== rod && !targetPath.startsWith(rod + sep)) {
|
|
835
|
+
if (targetPath !== rod && !targetPath.startsWith(rod.endsWith(sep) ? rod : rod + sep)) { // "/" giver ikke "//" (R5 F10)
|
|
614
836
|
throw new Error(
|
|
615
837
|
`path skal ligge inden for arbejdsmappen (${rod}). ` +
|
|
616
838
|
`"${args.path}" peger udenfor. Brug en relativ sti uden ../.`,
|
|
617
839
|
);
|
|
618
840
|
}
|
|
841
|
+
// MAALT 10/9 af Astra (anden runde): tjekket ovenfor er kun tekst. "ud/x.png" med ud -> en mappe
|
|
842
|
+
// udenfor passerede, og PNG-bytes blev skrevet udenfor. Nu tjekkes det operativsystemet faktisk
|
|
843
|
+
// ville skrive til: stien maa ikke selv vaere et symlink (heller ikke et dinglende, som ville
|
|
844
|
+
// skabe filen i den anden ende), og den dybeste del af stien der findes skal ligge inde.
|
|
845
|
+
// lstat - ikke exists - saa et dinglende link taeller som "findes" og derfor bliver undersoegt.
|
|
846
|
+
const findes = (x) => { try { lstatSync(x); return true; } catch { return false; } };
|
|
847
|
+
let rodReel = rod; try { rodReel = realpathSync.native(rod); } catch {}
|
|
848
|
+
let forfader = dirname(targetPath);
|
|
849
|
+
while (!findes(forfader) && dirname(forfader) !== forfader) forfader = dirname(forfader);
|
|
850
|
+
let forfaderReel = null; try { forfaderReel = realpathSync.native(forfader); } catch {}
|
|
851
|
+
// Astra, tredje runde: en eksisterende HARDLINK er ikke et symlink, men skrivningen trunkerer den faelles fil.
|
|
852
|
+
let erLink = false, flereNavne = false;
|
|
853
|
+
try { const st = lstatSync(targetPath); erLink = st.isSymbolicLink(); flereNavne = st.nlink > 1; } catch {}
|
|
854
|
+
if (erLink || flereNavne || !forfaderReel || (forfaderReel !== rodReel && !forfaderReel.startsWith(rodReel.endsWith(sep) ? rodReel : rodReel + sep))) {
|
|
855
|
+
throw new Error(
|
|
856
|
+
`path skal ligge inden for arbejdsmappen (${rod}). ` +
|
|
857
|
+
`"${args.path}" peger udenfor (via et link). Brug en almindelig mappe i arbejdsmappen.`,
|
|
858
|
+
);
|
|
859
|
+
}
|
|
619
860
|
mkdirSync(dirname(targetPath), { recursive: true });
|
|
620
861
|
writeFileSync(targetPath, Buffer.from(base64, 'base64'));
|
|
621
862
|
return {
|
|
@@ -651,7 +892,7 @@ mcpServer.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
651
892
|
}
|
|
652
893
|
});
|
|
653
894
|
|
|
654
|
-
// Naar udvidelsen er aeldre end serveren, svarer den `Unknown method: X`
|
|
895
|
+
// Naar udvidelsen er aeldre end serveren, svarer den `Unknown method: X` - og det er
|
|
655
896
|
// alt brugeren ser. Det sker GARANTERET: serveren kommer fra npm og opdateres straks,
|
|
656
897
|
// mens udvidelsen skal gennem Chrome Web Stores review paa 1-3 dage. I det vindue
|
|
657
898
|
// findes otte vaerktoejer i serveren som en 1.25.0-udvidelse ikke kender
|
|
@@ -659,7 +900,7 @@ mcpServer.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
659
900
|
// udklipsholder-vaerktoejer, alle fra v1.26.0).
|
|
660
901
|
//
|
|
661
902
|
// Serveren VED at udvidelsen er gammel: den sendte intet haandtryk. Saa i stedet for
|
|
662
|
-
// en gaadefuld fejl faar brugeren at vide hvorfor
|
|
903
|
+
// en gaadefuld fejl faar brugeren at vide hvorfor - og hvad de kan goere imens.
|
|
663
904
|
const ERSTATNINGER = {
|
|
664
905
|
double_click: 'kald `browser_click` to gange',
|
|
665
906
|
right_click: 'brug `browser_execute_script` med et contextmenu-event',
|
|
@@ -678,7 +919,7 @@ function forklarSkaevhed(besked) {
|
|
|
678
919
|
const alt = ERSTATNINGER[m[1]];
|
|
679
920
|
return `Error: browser_${m[1]} findes i denne server, men ikke i din Chrome-udvidelse.\n\n` +
|
|
680
921
|
'Udvidelsen opdateres via Chrome Web Store og kan vaere 1-3 dage bagud efter en ' +
|
|
681
|
-
'udgivelse
|
|
922
|
+
'udgivelse - serveren opdateres med det samme via npm. Alt andet virker imens.\n' +
|
|
682
923
|
(alt ? `\nIndtil da: ${alt}.\n` : '') +
|
|
683
924
|
'\nTjek om en opdatering venter: chrome://extensions → Agent360 Browser MCP. ' +
|
|
684
925
|
'Er den indlaest som "unpacked", saa koer `npx @agent360/browser-mcp install`.';
|
|
@@ -700,11 +941,11 @@ function handleAbout(args) {
|
|
|
700
941
|
|
|
701
942
|
const instruction =
|
|
702
943
|
intent === 'wish'
|
|
703
|
-
? `Share this exact submission link with the user as a clickable link, with a short note like "Click to submit your wish
|
|
944
|
+
? `Share this exact submission link with the user as a clickable link, with a short note like "Click to submit your wish - it'll open a pre-filled GitHub issue you can review before submitting": ${submit_url}`
|
|
704
945
|
: intent === 'use_case'
|
|
705
|
-
? `Share this exact submission link with the user as a clickable link, with a short note like "Click to share your use-case
|
|
946
|
+
? `Share this exact submission link with the user as a clickable link, with a short note like "Click to share your use-case - pre-filled, you can edit before submitting": ${submit_url}`
|
|
706
947
|
: intent === 'bug'
|
|
707
|
-
? `Share this exact bug-report link with the user as a clickable link, with a short note like "Click to report
|
|
948
|
+
? `Share this exact bug-report link with the user as a clickable link, with a short note like "Click to report - pre-filled, please add reproduction steps before submitting": ${submit_url}`
|
|
708
949
|
: `Browser MCP is community-shaped. Open wishlist: ${REPO_URL}/blob/main/WISHLIST.md · Use-cases: ${REPO_URL}/blob/main/USE_CASES.md · Submit anything: ${REPO_URL}/issues/new/choose`;
|
|
709
950
|
|
|
710
951
|
return {
|
|
@@ -726,13 +967,13 @@ function handleAbout(args) {
|
|
|
726
967
|
// ── Selv-diagnose: er installationen overhovedet frisk? ─────────────────────
|
|
727
968
|
// Baggrund (21/8): den udgave der koerte lokalt var npm 1.25.0, mens rettelserne
|
|
728
969
|
// laa uudgivet i repoet. Fejlen viste sig som "sessioner opfoerer sig underligt",
|
|
729
|
-
// ikke som "du koerer en gammel version"
|
|
970
|
+
// ikke som "du koerer en gammel version" - og der fandtes ingen maade at spoerge
|
|
730
971
|
// paa. Derfor spoerger vaerktoejet selv, foer det konkluderer noget som helst.
|
|
731
972
|
|
|
732
973
|
// Friskheds-tjek mod npm. SLUKKET SOM STANDARD siden 22/8.
|
|
733
974
|
//
|
|
734
|
-
// Produktet lover paa forsiden
|
|
735
|
-
// opslag i npm-registret ER et kald ud af maskinen
|
|
975
|
+
// Produktet lover paa forsiden at intet sendes til Agent360 og at der ingen telemetri er, og et
|
|
976
|
+
// opslag i npm-registret ER et kald ud af maskinen - ogsaa selv om det kun sender et
|
|
736
977
|
// pakkenavn og ingen brugerdata. Loeftet vejer tungere end bekvemmeligheden, saa
|
|
737
978
|
// tjekket er nu opt-in: saet BROWSER_MCP_CHECK_NPM=1.
|
|
738
979
|
//
|
|
@@ -766,7 +1007,7 @@ function npmLatestVersion() {
|
|
|
766
1007
|
// rettes. Logbogen ligger LOKALT og forlader ikke maskinen.
|
|
767
1008
|
//
|
|
768
1009
|
// Bevidst ikke auto-indsendelse til et offentligt GitHub-issue: rapporten baerer URL og
|
|
769
|
-
// fejltekst fra den side agenten stod paa
|
|
1010
|
+
// fejltekst fra den side agenten stod paa - og det er ofte en annoncekonto, en indbakke
|
|
770
1011
|
// eller et kundesystem. Et offentligt issue kan ikke tages tilbage.
|
|
771
1012
|
const FEEDBACK_LOG = join(homedir(), '.browser-mcp', 'feedback.jsonl');
|
|
772
1013
|
const setteFingeraftryk = new Set(); // samme graense logges én gang pr. serverliv
|
|
@@ -782,7 +1023,7 @@ function fingeraftryk(kind, tool, what) {
|
|
|
782
1023
|
}
|
|
783
1024
|
|
|
784
1025
|
// URL'en reduceres til oprindelse + sti. Query og fragment baerer tokens, sessions-id'er
|
|
785
|
-
// og soegetermer
|
|
1026
|
+
// og soegetermer - de har intet at goere i en logbog nogen senere kopierer ind i et issue.
|
|
786
1027
|
function afkortUrl(u) {
|
|
787
1028
|
if (!u) return null;
|
|
788
1029
|
try { const x = new URL(u); return x.origin + x.pathname; } catch { return '(ulaeselig url)'; }
|
|
@@ -838,16 +1079,20 @@ async function handleProvideFeedback(args) {
|
|
|
838
1079
|
);
|
|
839
1080
|
fix_steps.push(
|
|
840
1081
|
'Aabn chrome://extensions og slaa alle Browser MCP-udvidelser fra paa naer én. ' +
|
|
841
|
-
'Det skal brugeren selv goere
|
|
1082
|
+
'Det skal brugeren selv goere - chrome:// kan ikke styres herfra. Behold den nyeste.',
|
|
842
1083
|
);
|
|
843
1084
|
}
|
|
844
|
-
if (!active) {
|
|
1085
|
+
if (!active && activePort === null) {
|
|
1086
|
+
// Ingen port taget endnu = browseren er ikke brugt i denne chat. Der er intet
|
|
1087
|
+
// i stykker, og et fix-skridt her ville vaere en falsk alarm - se verdict 'idle'.
|
|
1088
|
+
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.');
|
|
1089
|
+
} else if (!active) {
|
|
845
1090
|
findings.push('Ingen Chrome-udvidelse er forbundet til denne MCP-server lige nu.');
|
|
846
1091
|
fix_steps.push('Tjek at Chrome koerer og at udvidelsen er slaaet til paa chrome://extensions, klik derefter paa ikonet → Reconnect.');
|
|
847
1092
|
}
|
|
848
1093
|
if (serverOutdated) {
|
|
849
1094
|
findings.push(`MCP-serveren koerer v${PKG_VERSION}, men npm har v${npmLatest}. Fejlen kan allerede vaere rettet.`);
|
|
850
|
-
fix_steps.push(`Genstart klienten
|
|
1095
|
+
fix_steps.push(`Genstart klienten - den henter selv @agent360/browser-mcp@latest (v${npmLatest}).`);
|
|
851
1096
|
}
|
|
852
1097
|
if (extOutdated) {
|
|
853
1098
|
findings.push(
|
|
@@ -856,25 +1101,44 @@ async function handleProvideFeedback(args) {
|
|
|
856
1101
|
: `Udvidelsen er v${extVersion}, serveren er v${PKG_VERSION}. Udvidelsen mangler rettelser fra de mellemliggende udgaver.`,
|
|
857
1102
|
);
|
|
858
1103
|
// MAALT 22/8: "↻ reload" er ubrugeligt for en Chrome Web Store-bruger. Butikken
|
|
859
|
-
// skubber paa Googles tidsplan efter et review paa 1-3 dage
|
|
1104
|
+
// skubber paa Googles tidsplan efter et review paa 1-3 dage - der er ingen nyere
|
|
860
1105
|
// version at hente endnu, saa raadet foerer i ring. Serveren kan ikke se hvilken
|
|
861
1106
|
// slags installation det er (den gamle udvidelse oplyser intet), saa begge tilfaelde
|
|
862
|
-
// skal staa der
|
|
1107
|
+
// skal staa der - og det skal siges at ventetiden er forventet, ikke en fejl.
|
|
863
1108
|
fix_steps.push(
|
|
864
1109
|
extVersion === null
|
|
865
1110
|
? 'Kommer udvidelsen fra Chrome Web Store: der er sandsynligvis en nyere version i review ' +
|
|
866
|
-
'(1-3 dage efter en udgivelse). ↻ reload henter den IKKE foer Google har godkendt
|
|
1111
|
+
'(1-3 dage efter en udgivelse). ↻ reload henter den IKKE foer Google har godkendt - ' +
|
|
867
1112
|
'det er forventet og gaar over af sig selv. Alt andet virker imens. ' +
|
|
868
1113
|
'Er den indlaest som "unpacked": koer `npx @agent360/browser-mcp install` og derefter ' +
|
|
869
1114
|
'chrome://extensions → Agent360 Browser MCP → ↻ reload.'
|
|
870
|
-
|
|
871
|
-
|
|
1115
|
+
// MAALT 11/9 af Fable (e2e-review): ogsaa med kendt version kan det vaere en Chrome Web Store-installation, og saa
|
|
1116
|
+
// henter reload ingenting foer Google har godkendt. Begge tilfaelde skal staa der, ellers foerer raadet i ring.
|
|
1117
|
+
: 'Er udvidelsen indlaest som "unpacked": koer `npx @agent360/browser-mcp install` og derefter ' +
|
|
1118
|
+
'chrome://extensions → Agent360 Browser MCP → ↻ reload. Kommer den fra Chrome Web Store: den nye version ' +
|
|
1119
|
+
'ligger sandsynligvis i review (1-3 dage efter en udgivelse), og ↻ reload henter den IKKE foer Google har ' +
|
|
1120
|
+
'godkendt - det er forventet og gaar over af sig selv. Alt andet virker imens.',
|
|
872
1121
|
);
|
|
873
1122
|
}
|
|
874
1123
|
|
|
1124
|
+
// ── "ingen forbindelse" og "ingen port endnu" er IKKE det samme ────────────
|
|
1125
|
+
//
|
|
1126
|
+
// FUNDET AF REVIEW 7/9. Foer porten blev doven, bandt hver server ved opstart, saa
|
|
1127
|
+
// udvidelsen var altid forbundet naar dette vaerktoej blev kaldt - og `!active`
|
|
1128
|
+
// betoed derfor paalideligt "udvidelsen kan ikke naas". Nu binder en chat foerst en
|
|
1129
|
+
// port naar den bruger browseren, saa en HELT SUND chat der ikke har roert den kan
|
|
1130
|
+
// staa uden forbindelse. Uden det her skel fik den `disconnected` + fix_steps der
|
|
1131
|
+
// bad brugeren geninstallere - og INSTRUCTIONS beder agenten viderebringe dem.
|
|
1132
|
+
// Vi ville altsaa fortaelle folk at deres installation var i stykker, fordi vi selv
|
|
1133
|
+
// endnu ikke havde aabnet doeren.
|
|
1134
|
+
//
|
|
1135
|
+
// Loesningen er ikke at binde en port her (et diagnose-vaerktoej skal ikke aendre
|
|
1136
|
+
// tilstand for at kunne maale den) - det er at sige praecis hvad der er tilfaeldet.
|
|
1137
|
+
const ingenPortEndnu = activePort === null;
|
|
875
1138
|
const verdict =
|
|
876
1139
|
exts.length > 1 ? 'conflict'
|
|
877
1140
|
: (serverOutdated || extOutdated) ? 'outdated'
|
|
1141
|
+
: (!active && ingenPortEndnu) ? 'idle'
|
|
878
1142
|
: !active ? 'disconnected'
|
|
879
1143
|
: (npmLatest === null ? 'unknown' : 'current');
|
|
880
1144
|
|
|
@@ -886,6 +1150,8 @@ async function handleProvideFeedback(args) {
|
|
|
886
1150
|
name: c.name,
|
|
887
1151
|
version: c.version,
|
|
888
1152
|
extension_id: c.extensionId,
|
|
1153
|
+
// Fingeraftryk af udvidelsens background.js - det er KODEN, ikke versionsnummeret, udgivelsens gate skal se.
|
|
1154
|
+
code: c.kode ?? null,
|
|
889
1155
|
active: c === active,
|
|
890
1156
|
})),
|
|
891
1157
|
extension_up_to_date: extOutdated === null ? null : !extOutdated,
|
|
@@ -898,7 +1164,7 @@ async function handleProvideFeedback(args) {
|
|
|
898
1164
|
what && `**What happened**\n${what}`,
|
|
899
1165
|
tool && `\n**Tool**: \`${tool}\``,
|
|
900
1166
|
// MAALT 22/8 ved sikkerhedsreview: her stod den RAA url, mens den lokale logbog
|
|
901
|
-
// nedenfor bruger afkortUrl(). Query-strengen
|
|
1167
|
+
// nedenfor bruger afkortUrl(). Query-strengen - hvor tokens bor - blev altsaa
|
|
902
1168
|
// strippet fra filen paa disken, men sendt uredigeret ind i et link til et
|
|
903
1169
|
// OFFENTLIGT GitHub-issue. Praecis den forkerte vej rundt.
|
|
904
1170
|
url && `\n**URL**: ${afkortUrl(url)}`,
|
|
@@ -914,11 +1180,15 @@ async function handleProvideFeedback(args) {
|
|
|
914
1180
|
`&title=${encodeURIComponent(issueTitle)}&body=${encodeURIComponent(issueBody)}`;
|
|
915
1181
|
|
|
916
1182
|
const instruction =
|
|
917
|
-
verdict === '
|
|
1183
|
+
verdict === 'idle'
|
|
1184
|
+
? 'Browseren er ikke taget i brug i denne chat endnu, saa der er intet at diagnosticere paa forbindelsen - ' +
|
|
1185
|
+
'det er IKKE en fejl i installationen, og du maa ikke sige det til brugeren. ' +
|
|
1186
|
+
'Er der en aegte mangel, saa tilbyd submit_url som et klikbart link.'
|
|
1187
|
+
: verdict === 'conflict' || verdict === 'outdated' || verdict === 'disconnected'
|
|
918
1188
|
? 'Fortael brugeren hvad der blev fundet, og giv fix_steps som konkrete skridt. Proev derefter handlingen igen. ' +
|
|
919
|
-
'Del KUN submit_url hvis problemet stadig staar efter at fix_steps er fulgt
|
|
1189
|
+
'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
1190
|
: 'Installationen er frisk, saa det her er sandsynligvis en aegte mangel eller fejl. Fortael brugeren kort hvad der ikke kunne lade sig goere, ' +
|
|
921
|
-
'og tilbyd submit_url som et klikbart link ("forudfyldt
|
|
1191
|
+
'og tilbyd submit_url som et klikbart link ("forudfyldt - du kan rette i den foer du sender"). Spoerg ikke om lov foerst.';
|
|
922
1192
|
|
|
923
1193
|
const logbog = skrivTilLogbog({
|
|
924
1194
|
at: new Date().toISOString(),
|
|
@@ -970,18 +1240,19 @@ async function handleExtractToken(args) {
|
|
|
970
1240
|
|
|
971
1241
|
// ── Graceful shutdown ──────────────────────────────────────────────────────
|
|
972
1242
|
// All shutdown paths funnel through gracefulShutdown so the cleanup chain runs
|
|
973
|
-
// deterministically
|
|
1243
|
+
// deterministically - even on abrupt parent-exit. Without this, process.exit(0)
|
|
974
1244
|
// was racing against WS close-handshake, leaving zombie tabs in Chrome.
|
|
975
1245
|
|
|
976
1246
|
let shuttingDown = false;
|
|
977
1247
|
function gracefulShutdown(reason, code = 0) {
|
|
978
1248
|
if (shuttingDown) return;
|
|
979
1249
|
shuttingDown = true;
|
|
980
|
-
process.stderr.write(`[MCP] ${reason}
|
|
1250
|
+
process.stderr.write(`[MCP] ${reason} - shutting down\n`);
|
|
981
1251
|
|
|
982
1252
|
// Stop timers so they can't re-enter gracefulShutdown
|
|
983
1253
|
if (parentCheck) clearInterval(parentCheck);
|
|
984
1254
|
if (heartbeat) clearInterval(heartbeat);
|
|
1255
|
+
if (tomgangsvagt) clearInterval(tomgangsvagt);
|
|
985
1256
|
|
|
986
1257
|
// Close WS with explicit close-frame so extension's onclose handler fires
|
|
987
1258
|
for (const c of liveConnections()) {
|
|
@@ -1009,7 +1280,7 @@ process.on('exit', () => {
|
|
|
1009
1280
|
// Claude Code → wrapper → npm exec → denne server
|
|
1010
1281
|
//
|
|
1011
1282
|
// `process.ppid` er `npm exec`, ikke Claude Code. Doer chatten, kan `npm exec`
|
|
1012
|
-
// blive haengende som foraeldreloes
|
|
1283
|
+
// blive haengende som foraeldreloes - og saa ser tjekket en levende foraelder for
|
|
1013
1284
|
// evigt. Porten blev holdt i op til fire timer (idle-graensen), og med flere
|
|
1014
1285
|
// forladte chats loeb spaendet fuldt.
|
|
1015
1286
|
//
|
|
@@ -1019,7 +1290,7 @@ process.on('exit', () => {
|
|
|
1019
1290
|
//
|
|
1020
1291
|
// Kaeden hentes én gang ved opstart (én ps-kommando), og derefter koster tjekket
|
|
1021
1292
|
// kun et signal 0 pr. led hvert 5. sekund. Kan kaeden ikke laeses (Windows, eller
|
|
1022
|
-
// ps mangler), falder vi tilbage til det gamle enkelt-tjek
|
|
1293
|
+
// ps mangler), falder vi tilbage til det gamle enkelt-tjek - daarligere, men aldrig
|
|
1023
1294
|
// vaerre end foer.
|
|
1024
1295
|
|
|
1025
1296
|
// ps-opslaget bor her (det er en sideeffekt); selve kaede-logikken og doeds-dommen
|
|
@@ -1040,7 +1311,7 @@ function laesPpid(pid) {
|
|
|
1040
1311
|
const parentPid = process.ppid;
|
|
1041
1312
|
|
|
1042
1313
|
// MAALT 22/8: doer den naermeste foraelder FOER serveren er bootet (~2 sek node+SDK),
|
|
1043
|
-
// laeser process.ppid vaerdien 1
|
|
1314
|
+
// laeser process.ppid vaerdien 1 - altsaa launchd. Kaeden blev saa [1], og pid 1 er
|
|
1044
1315
|
// baade udoedelig og ejet af root, saa vagten var enten inert eller draebte os selv
|
|
1045
1316
|
// paa EPERM. En kaede der kun bestaar af pid 1 vogter ingenting og skal ikke bruges.
|
|
1046
1317
|
let vagtKaede = parentPid > 1 ? [parentPid] : [];
|
|
@@ -1050,7 +1321,7 @@ try {
|
|
|
1050
1321
|
} catch {}
|
|
1051
1322
|
if (!vagtKaede.length) {
|
|
1052
1323
|
process.stderr.write('[MCP] ingen brugbar foraelder-kaede (ppid=' + parentPid +
|
|
1053
|
-
')
|
|
1324
|
+
') - falder tilbage paa idle-graensen alene\n');
|
|
1054
1325
|
}
|
|
1055
1326
|
process.stderr.write(`[MCP] vagt-kaede: ${vagtKaede.join(' → ')}\n`);
|
|
1056
1327
|
|
|
@@ -1061,14 +1332,14 @@ parentCheck = setInterval(() => {
|
|
|
1061
1332
|
// ── Et doedt led betyder ikke automatisk at chatten er vaek (MAALT 23/8) ──────
|
|
1062
1333
|
//
|
|
1063
1334
|
// Kaeden blev frosset ved opstart. Men et MELLEMLED kan afslutte helt normalt
|
|
1064
|
-
// mens ejeren koerer videre
|
|
1335
|
+
// mens ejeren koerer videre - maalt to gange paa denne maskine, hvor kaeden gaar
|
|
1065
1336
|
// npm exec → wrapper → claude → Code Helper (Plugin) → Code. Et forbigaaende led
|
|
1066
1337
|
// der lukkede pænt udloeste "chatten bag denne server er vaek", mens chatten var
|
|
1067
1338
|
// uroert. Og risikoen er ensrettet vaerre end 1.25.0, som vogtede ét pid: nu er
|
|
1068
1339
|
// hvert af 5-6 led en ny doedsaarsag.
|
|
1069
1340
|
//
|
|
1070
1341
|
// Derfor genlaeses kaeden foerst naar noget SER doedt ud. Kan vi stadig gaa fra
|
|
1071
|
-
// vores egen foraelder op til en rod, er vi ikke foraeldreloese
|
|
1342
|
+
// vores egen foraelder op til en rod, er vi ikke foraeldreloese - vi er bare blevet
|
|
1072
1343
|
// reparented, og den nye kaede overtager. Kun naar den vej ogsaa er vaek, lukker vi.
|
|
1073
1344
|
//
|
|
1074
1345
|
// Prisen er nul i normal drift: genlaesningen koerer kun i det tik hvor et led er
|
|
@@ -1081,14 +1352,14 @@ parentCheck = setInterval(() => {
|
|
|
1081
1352
|
if (frisk.length) {
|
|
1082
1353
|
process.stderr.write(
|
|
1083
1354
|
`[MCP] led ${doede.join(', ')} er vaek, men kaeden gaar stadig op: ` +
|
|
1084
|
-
`${frisk.join(' → ')}
|
|
1355
|
+
`${frisk.join(' → ')} - fortsaetter\n`,
|
|
1085
1356
|
);
|
|
1086
1357
|
vagtKaede = frisk;
|
|
1087
1358
|
return;
|
|
1088
1359
|
}
|
|
1089
1360
|
|
|
1090
1361
|
gracefulShutdown(
|
|
1091
|
-
`Proces ${doede[0]} i kaeden doede, og der er ingen levende vej op
|
|
1362
|
+
`Proces ${doede[0]} i kaeden doede, og der er ingen levende vej op - ` +
|
|
1092
1363
|
'chatten bag denne server er vaek',
|
|
1093
1364
|
);
|
|
1094
1365
|
}, 5000); // hvert 5. sekund
|