@agent360/browser-mcp 1.29.0 → 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 +1210 -192
- 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 +251 -118
- package/package.json +3 -3
- 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,26 +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
|
-
// Spaendet kan flyttes med env
|
|
42
|
+
// Spaendet kan flyttes med env - ellers ville en test af port-udsultning skulle
|
|
43
43
|
// beslaglaegge de RIGTIGE porte og dermed sulte brugerens oevrige chats imens.
|
|
44
44
|
// Uden env er vaerdierne uaendrede. (Samme moenster som BROWSER_MCP_EXTENSION_ID.)
|
|
45
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
|
|
46
|
+
const MAX_PORT = Number(process.env.BROWSER_MCP_MAX_PORT) || 9895; // 20 ports instead of 10 - zombies die within 5s via parent check
|
|
47
47
|
|
|
48
48
|
// ── Extension connections ───────────────────────────────────────────────────
|
|
49
49
|
// FEJL MAALT 21/8: her stod `let extensionSocket = null`, og hver ny forbindelse
|
|
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
|
|
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
|
|
52
52
|
// og forbinder til hver eneste server. Maalt med lsof: 2 ESTABLISHED forbindelser
|
|
53
53
|
// paa hver af de fire aktive porte. Kommandoerne gik til den der forbandt sidst,
|
|
54
|
-
// 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
|
|
55
55
|
// `terminate` fra den forkerte kopi lukkede serveren ned under den rigtige.
|
|
56
56
|
//
|
|
57
57
|
// Nu holdes alle forbindelser med deres identitet, kommandoer sendes kun til den
|
|
@@ -60,8 +60,9 @@ const MAX_PORT = Number(process.env.BROWSER_MCP_MAX_PORT) || 9895; // 20 ports i
|
|
|
60
60
|
const connections = new Set(); // { ws, seq, extensionId, version, name, since }
|
|
61
61
|
let connSeq = 0;
|
|
62
62
|
let activePort = null;
|
|
63
|
-
let alleePorteOptaget = false; // hele spaendet i brug
|
|
63
|
+
let alleePorteOptaget = false; // hele spaendet i brug - se createWSS
|
|
64
64
|
let bindFejl = null; // bind fejlede af en ANDEN grund end optaget port
|
|
65
|
+
let portBundetTid = 0; // hvornaar porten sidst blev aaben - se sendToExtension
|
|
65
66
|
|
|
66
67
|
function cmpVersion(a, b) {
|
|
67
68
|
const pa = String(a || '0.0.0').split('.').map(n => parseInt(n, 10) || 0);
|
|
@@ -77,13 +78,13 @@ let sidsteKonfliktNoegle = '';
|
|
|
77
78
|
function advarOmKonflikt(conn) {
|
|
78
79
|
const alle = distinctExtensions();
|
|
79
80
|
if (alle.length < 2) return;
|
|
80
|
-
// Samme konflikt maa ikke skrige ved hver eneste hello
|
|
81
|
+
// Samme konflikt maa ikke skrige ved hver eneste hello - kun naar billedet aendrer sig.
|
|
81
82
|
const noegle = alle.map(c => `${c.extensionId || 'ukendt'}@${c.version || '?'}`).sort().join('|');
|
|
82
83
|
if (noegle === sidsteKonfliktNoegle) return;
|
|
83
84
|
sidsteKonfliktNoegle = noegle;
|
|
84
85
|
const aktiv = activeConnection();
|
|
85
86
|
// Oplyser INGEN af dem en version (alle udgivne udgaver er fra foer haandtrykket),
|
|
86
|
-
// 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
|
|
87
88
|
// "kommandoer sendes kun til X" som om X var det rigtige valg.
|
|
88
89
|
const kanVaelge = alle.some(c => c.version);
|
|
89
90
|
process.stderr.write(
|
|
@@ -105,14 +106,14 @@ function liveConnections() {
|
|
|
105
106
|
//
|
|
106
107
|
// MAALT 21/8 i flow-harnessen: uden laasen skiftede den aktive udvidelse MIDT i en
|
|
107
108
|
// koersel. Foerste kommando (navigate) gik til udvidelse A, som aabnede fanen. Et
|
|
108
|
-
// 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 -
|
|
109
110
|
// men B kendte ikke A's fane og lavede en frisk about:blank. Alt derefter fejlede med
|
|
110
111
|
// "Cannot access contents of url about:blank". 21 af 43 vaerktoejer faldt paa det, og
|
|
111
112
|
// symptomet lignede praecis "faner forsvinder" og "kun én session virker".
|
|
112
113
|
//
|
|
113
114
|
// Faner hoerer til den udvidelse der aabnede dem. Skifter man udvidelse, strander de.
|
|
114
115
|
// Derfor: vaelg én gang, og bliv ved den saa laenge dens forbindelse lever. Doer den,
|
|
115
|
-
// vaelges der forfra
|
|
116
|
+
// vaelges der forfra - det er en aegte genopretning, ikke et vilkaarligt skift.
|
|
116
117
|
let laastForbindelse = null;
|
|
117
118
|
let harSendtKommando = false;
|
|
118
119
|
const PINNET_UDVIDELSE = (process.env.BROWSER_MCP_EXTENSION_ID || '').trim() || null;
|
|
@@ -121,7 +122,7 @@ function activeConnection() {
|
|
|
121
122
|
if (laastForbindelse && laastForbindelse.ws.readyState === 1) return laastForbindelse;
|
|
122
123
|
|
|
123
124
|
// Nyeste udvidelse vinder. Ved uafgjort: den der forbandt sidst. En udvidelse fra
|
|
124
|
-
// 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
|
|
125
126
|
// med den nyere udgave.
|
|
126
127
|
let best = null;
|
|
127
128
|
for (const c of liveConnections()) {
|
|
@@ -156,13 +157,13 @@ let tomgangsvagt = null;
|
|
|
156
157
|
// ── 4-timers-tomgangen bor paa modul-niveau, ikke i hjerteslaget ────────────
|
|
157
158
|
//
|
|
158
159
|
// FUNDET AF REVIEW 7/9. Tjekket laa INDE i `heartbeat`, som kun findes mens serveren
|
|
159
|
-
// har en port
|
|
160
|
+
// har en port - og som `frigivPort` rydder. Da porten blev doven, betoed det at en
|
|
160
161
|
// proces der ALDRIG binder (en chat der ikke roerer browseren) heller aldrig faar
|
|
161
162
|
// sin tomgang tjekket. Det var den eneste vej hvor en browser-inaktiv server gav sine
|
|
162
163
|
// ~35 MB tilbage; med mange samtidige chats er det maalbart. Nu koerer vagten altid.
|
|
163
164
|
// Graensen staar som literal, ikke bag en konstant: `vagter.test.mjs` laeser tallet
|
|
164
165
|
// direkte ud af kilden for at haandhaeve at den aldrig bliver kort. En kort graense
|
|
165
|
-
// ville lade en aaben chat miste browseren permanent
|
|
166
|
+
// ville lade en aaben chat miste browseren permanent - vaerre end en port der staar
|
|
166
167
|
// optaget lidt for laenge.
|
|
167
168
|
tomgangsvagt = setInterval(() => {
|
|
168
169
|
if (Date.now() - lastActivity > 4 * 60 * 60 * 1000) gracefulShutdown('Idle timeout (4h)');
|
|
@@ -180,7 +181,7 @@ function createWSS(port = BASE_PORT) {
|
|
|
180
181
|
// ingen socket overhovedet.
|
|
181
182
|
//
|
|
182
183
|
// Chrome saetter ALTID Origin: chrome-extension://<32 tegn> paa en WebSocket fra
|
|
183
|
-
// en udvidelse
|
|
184
|
+
// en udvidelse - verificeret mod den koerende. Alt andet er per definition ikke
|
|
184
185
|
// en udvidelse, saa gaten koster aegte brugere ingenting.
|
|
185
186
|
verifyClient: ({ origin }, godkend) => {
|
|
186
187
|
if (/^chrome-extension:\/\/[a-p]{32}$/.test(origin || '')) return godkend(true);
|
|
@@ -201,7 +202,7 @@ function createWSS(port = BASE_PORT) {
|
|
|
201
202
|
} else {
|
|
202
203
|
// MAALT 22/8: her stod KUN denne stderr-linje. Ingen laeser stderr fra en
|
|
203
204
|
// MCP-server, saa udtoemte porte var en helt tavs fejl. Hvert vaerktoejskald
|
|
204
|
-
// fejlede bagefter med "extension not connected"
|
|
205
|
+
// fejlede bagefter med "extension not connected" - en tekst der oven i koebet
|
|
205
206
|
// siger at serveren koerer og sender brugeren til Chrome Web Store. Begge dele
|
|
206
207
|
// er forkerte naar sandheden er at vi aldrig fik en port. Flaget laeses i
|
|
207
208
|
// sendToExtension, saa agenten kan give brugeren den rigtige forklaring.
|
|
@@ -212,7 +213,7 @@ function createWSS(port = BASE_PORT) {
|
|
|
212
213
|
} else {
|
|
213
214
|
// FUNDET AF REVIEW 7/9. Her stod KUN stderr-linjen. Foer porten blev doven, var
|
|
214
215
|
// det harmloest: en opstartsfejl man kunne se i loggen. Nu venter `sikrePort()`
|
|
215
|
-
// paa et loefte der aldrig blev indfriet
|
|
216
|
+
// paa et loefte der aldrig blev indfriet - og `sendToExtension` goer
|
|
216
217
|
// `await sikrePort()` UDEN timeout. Enhver anden bind-fejl end EADDRINUSE
|
|
217
218
|
// (EACCES paa en privilegeret port, EADDRNOTAVAIL, en restriktiv firewall)
|
|
218
219
|
// ville derfor faa hvert eneste browser-kald til at haenge tavst for evigt.
|
|
@@ -227,7 +228,7 @@ function createWSS(port = BASE_PORT) {
|
|
|
227
228
|
// WebSocket-handshaket fra en udvidelse baerer Origin: chrome-extension://<id>.
|
|
228
229
|
// Den identificerer afsenderen UDEN at udvidelsen behoever at kende haandtrykket,
|
|
229
230
|
// saa en konflikt mellem to indlaeste udvidelser kan opdages ogsaa naar begge er
|
|
230
|
-
// gamle udgaver
|
|
231
|
+
// gamle udgaver - hvilket er praecis den situation konflikten opstaar i.
|
|
231
232
|
// Maalt 21/8: to distinkte origins ringede op til hver eneste server.
|
|
232
233
|
const origin = req?.headers?.origin || '';
|
|
233
234
|
const fraOrigin = /^chrome-extension:\/\/([a-p]{32})$/.exec(origin)?.[1] || null;
|
|
@@ -239,14 +240,14 @@ function createWSS(port = BASE_PORT) {
|
|
|
239
240
|
// udvidelse med `hello version 99.0.0`, fik `browser_get_cookies` leveret, og
|
|
240
241
|
// kunne lukke serveren med `terminate`. Alle tre trin lykkedes.
|
|
241
242
|
//
|
|
242
|
-
// 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
|
|
243
244
|
// goer enhver anden app og ethvert npm-postinstall-script. Og hvad den kan er
|
|
244
245
|
// ikke smaating: laese alt agenten sender til browseren (kodeord fra ask_user,
|
|
245
246
|
// cookies, sidetekst), fodre agenten med opdigtet sideindhold, og slukke
|
|
246
247
|
// browser-adgangen i alle aabne chats.
|
|
247
248
|
//
|
|
248
249
|
// Chrome saetter ALTID `Origin: chrome-extension://<id>` paa en WebSocket fra en
|
|
249
|
-
// udvidelse
|
|
250
|
+
// udvidelse - verificeret mod den koerende udvidelse. En manglende header er
|
|
250
251
|
// derfor ikke en aeldre udgave; det er noget andet end en udvidelse.
|
|
251
252
|
if (!fraOrigin) {
|
|
252
253
|
process.stderr.write(
|
|
@@ -261,7 +262,7 @@ function createWSS(port = BASE_PORT) {
|
|
|
261
262
|
// slaa dem fra: BROWSER_MCP_EXTENSION_ID=<id> binder serveren til én bestemt.
|
|
262
263
|
// Uden den er valget vilkaarligt naar ingen af dem oplyser en version.
|
|
263
264
|
if (PINNET_UDVIDELSE && fraOrigin && fraOrigin !== PINNET_UDVIDELSE) {
|
|
264
|
-
process.stderr.write(`[MCP] Afviser udvidelse ${fraOrigin}
|
|
265
|
+
process.stderr.write(`[MCP] Afviser udvidelse ${fraOrigin} - bundet til ${PINNET_UDVIDELSE}\n`);
|
|
265
266
|
try { ws.close(1008, 'not the pinned extension'); } catch {}
|
|
266
267
|
return;
|
|
267
268
|
}
|
|
@@ -278,7 +279,7 @@ function createWSS(port = BASE_PORT) {
|
|
|
278
279
|
// If extension was auto-updated, trigger reload
|
|
279
280
|
if (process.env.BROWSER_MCP_EXTENSION_UPDATED === '1') {
|
|
280
281
|
process.env.BROWSER_MCP_EXTENSION_UPDATED = '';
|
|
281
|
-
process.stderr.write('[MCP] Extension files updated
|
|
282
|
+
process.stderr.write('[MCP] Extension files updated - triggering auto-reload\n');
|
|
282
283
|
setTimeout(() => {
|
|
283
284
|
sendToExtension('reload_extension', {}, 5000).catch(() => {});
|
|
284
285
|
}, 1000);
|
|
@@ -290,7 +291,7 @@ function createWSS(port = BASE_PORT) {
|
|
|
290
291
|
|
|
291
292
|
// Identitets-haandtryk fra offscreen-dokumentet (v1.28+).
|
|
292
293
|
if (msg.type === 'hello') {
|
|
293
|
-
// MAALT 23/8: her stod `conn.extensionId = msg.extensionId`
|
|
294
|
+
// MAALT 23/8: her stod `conn.extensionId = msg.extensionId` - altsaa lod
|
|
294
295
|
// haandtrykket afsenderen OVERSKRIVE sin egen identitet med hvad som helst.
|
|
295
296
|
// Origin-headeren er den eneste kilde Chrome selv saetter og som afsenderen
|
|
296
297
|
// ikke kan forfalske, saa den vinder. Beskedens id gemmes separat: stemmer de
|
|
@@ -299,25 +300,37 @@ function createWSS(port = BASE_PORT) {
|
|
|
299
300
|
conn.harHilst = true;
|
|
300
301
|
if (conn.helloId && conn.helloId !== conn.extensionId) {
|
|
301
302
|
process.stderr.write(
|
|
302
|
-
`[MCP] Haandtryk oplyser ${conn.helloId} men Origin siger ${conn.extensionId}
|
|
303
|
+
`[MCP] Haandtryk oplyser ${conn.helloId} men Origin siger ${conn.extensionId} - ` +
|
|
303
304
|
'bruger Origin\n',
|
|
304
305
|
);
|
|
305
306
|
}
|
|
306
307
|
conn.version = typeof msg.version === 'string' ? msg.version : null;
|
|
307
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;
|
|
308
313
|
advarOmKonflikt(conn);
|
|
309
314
|
return;
|
|
310
315
|
}
|
|
311
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
|
+
|
|
312
325
|
if (msg.type === 'terminate') {
|
|
313
326
|
// terminate lukker serveren for ALLE chats paa porten, saa den har to gates.
|
|
314
327
|
//
|
|
315
|
-
// Om id-sammenligningen: ORIGIN er autoriteten
|
|
328
|
+
// Om id-sammenligningen: ORIGIN er autoriteten - Chrome saetter den, og
|
|
316
329
|
// afsenderen kan ikke forfalske den. Haandtrykkets id er kun et ekstra
|
|
317
330
|
// signal. MAALT 23/8 mod den AEGTE udvidelse: den sender hello med id null,
|
|
318
331
|
// fordi chrome.runtime.getManifest() kan fejle i offscreen-dokumentet.
|
|
319
332
|
// Kraevede vi lighed ubetinget, kunne en HELT legitim udvidelse aldrig lukke
|
|
320
|
-
// sin session ned
|
|
333
|
+
// sin session ned - en fejl jeg selv indfoerte samme aften. Derfor: et id der
|
|
321
334
|
// MANGLER er fint (Origin har allerede bevist hvem det er), mens et id der er
|
|
322
335
|
// TIL STEDE og peger et ANDET sted afvises.
|
|
323
336
|
//
|
|
@@ -328,15 +341,15 @@ function createWSS(port = BASE_PORT) {
|
|
|
328
341
|
// kopi, der lukkede sin sidste fane, rive serveren vaek under den
|
|
329
342
|
// udvidelse der reelt loeste opgaven.
|
|
330
343
|
if (!conn.harHilst || (conn.helloId && conn.helloId !== conn.extensionId)) {
|
|
331
|
-
process.stderr.write('[MCP] terminate ignoreret
|
|
344
|
+
process.stderr.write('[MCP] terminate ignoreret - intet gyldigt haandtryk\n');
|
|
332
345
|
return;
|
|
333
346
|
}
|
|
334
347
|
if (activeConnection() !== conn) {
|
|
335
|
-
process.stderr.write('[MCP] terminate ignoreret
|
|
348
|
+
process.stderr.write('[MCP] terminate ignoreret - kom fra en inaktiv udvidelses-forbindelse\n');
|
|
336
349
|
return;
|
|
337
350
|
}
|
|
338
351
|
// AENDRET 7/9: her stod `gracefulShutdown`. En chat der var faerdig med browseren
|
|
339
|
-
// mistede altsaa browseren HELT
|
|
352
|
+
// mistede altsaa browseren HELT - og porten blev alligevel hverken frigivet hurtigt
|
|
340
353
|
// nok til andre (processen doede foerst efter oprydning) eller genvundet af chatten
|
|
341
354
|
// selv. Nu slippes kun porten; naeste browser-kald tager en ny (se sikrePort).
|
|
342
355
|
frigivPort('udvidelsen meldte: sidste fane lukket');
|
|
@@ -354,11 +367,11 @@ function createWSS(port = BASE_PORT) {
|
|
|
354
367
|
|
|
355
368
|
// ── En doed socket skal AFVISE de kald der var undervejs (MAALT 23/8) ──────
|
|
356
369
|
// Foer roerte close-handleren ikke `pending`. Maalt: en socket der doede 300 ms
|
|
357
|
-
// 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
|
|
358
371
|
// forklaring, "kommandoen tog for lang tid". For extract_list er timeouten 180
|
|
359
372
|
// sekunder, altsaa tre minutters tavshed hvor sandheden var kendt med det samme.
|
|
360
373
|
const afvisVentende = (grund) => {
|
|
361
|
-
// Kun naar ingen anden levende forbindelse kan svare
|
|
374
|
+
// Kun naar ingen anden levende forbindelse kan svare - ellers ville et helt
|
|
362
375
|
// normalt skift mellem to udvidelser afbryde kald der er fuldt i orden.
|
|
363
376
|
if (!pending.size || liveConnections().length) return;
|
|
364
377
|
const antal = pending.size;
|
|
@@ -367,16 +380,16 @@ function createWSS(port = BASE_PORT) {
|
|
|
367
380
|
pending.delete(id);
|
|
368
381
|
p.reject(new Error(
|
|
369
382
|
`Forbindelsen til Chrome-udvidelsen forsvandt mens kommandoen koerte (${grund}). ` +
|
|
370
|
-
'Kommandoen naaede maaske at blive udfoert i browseren
|
|
383
|
+
'Kommandoen naaede maaske at blive udfoert i browseren - tjek tilstanden foer du ' +
|
|
371
384
|
'proever igen. Er udvidelsen slaaet fra eller Chrome lukket, saa start den og proev forfra.',
|
|
372
385
|
));
|
|
373
386
|
}
|
|
374
|
-
process.stderr.write(`[MCP] ${antal} ventende kald afvist
|
|
387
|
+
process.stderr.write(`[MCP] ${antal} ventende kald afvist - ${grund}\n`);
|
|
375
388
|
};
|
|
376
389
|
|
|
377
390
|
// MAALT 23/8: der fandtes INGEN error-handler. Et ugyldigt WebSocket-frame (fx
|
|
378
391
|
// RSV1 sat) faar 'ws' til at emitte 'error' paa socketen, og en uhaandteret
|
|
379
|
-
// 'error' paa en EventEmitter kaster og draeber hele processen
|
|
392
|
+
// 'error' paa en EventEmitter kaster og draeber hele processen - altsaa alle
|
|
380
393
|
// chats paa den port. Tre linjer lukker det.
|
|
381
394
|
ws.on('error', (e) => {
|
|
382
395
|
process.stderr.write(`[MCP] WebSocket-fejl paa forbindelsen: ${e?.message || e}\n`);
|
|
@@ -392,11 +405,12 @@ function createWSS(port = BASE_PORT) {
|
|
|
392
405
|
|
|
393
406
|
server.on('listening', () => {
|
|
394
407
|
activePort = port;
|
|
408
|
+
portBundetTid = Date.now();
|
|
395
409
|
process.stderr.write(`[MCP] WebSocket server listening on ws://127.0.0.1:${port}\n`);
|
|
396
410
|
loesPortLoefte(true);
|
|
397
411
|
});
|
|
398
412
|
|
|
399
|
-
// Heartbeat + idle timeout (4 hours)
|
|
413
|
+
// Heartbeat + idle timeout (4 hours) - hoisted to module scope so gracefulShutdown can clear it
|
|
400
414
|
// Ryddes foerst: porten kan bindes flere gange i samme proces (se sikrePort), og uden
|
|
401
415
|
// det her ville hver ny binding efterlade en ekstra timer der aldrig blev stoppet.
|
|
402
416
|
if (heartbeat) clearInterval(heartbeat);
|
|
@@ -408,13 +422,13 @@ function createWSS(port = BASE_PORT) {
|
|
|
408
422
|
// ── Porten tages ved BRUG, ikke ved opstart ─────────────────────────────────
|
|
409
423
|
//
|
|
410
424
|
// 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
|
|
412
|
-
// chats der aldrig roerer browseren
|
|
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
|
|
413
427
|
// blev altsaa reserveret af en chat der maaske aldrig fik brug for den, og holdt indtil
|
|
414
428
|
// chatten doede eller 4-timers-tomgangen udloeb.
|
|
415
429
|
//
|
|
416
430
|
// Konsekvensen var ensidigt slem: chat nr. 21 fik `alleePorteOptaget = true` ÉN gang og
|
|
417
|
-
// proevede aldrig igen
|
|
431
|
+
// proevede aldrig igen - dens browser var doed hele chattens levetid, selv naar en port
|
|
418
432
|
// blev fri et minut senere.
|
|
419
433
|
//
|
|
420
434
|
// Nu bindes porten foerste gang et vaerktoej faktisk skal bruge udvidelsen, og HVERT kald
|
|
@@ -423,7 +437,7 @@ function createWSS(port = BASE_PORT) {
|
|
|
423
437
|
//
|
|
424
438
|
// Bemaerk hvorfor det ikke er en 5-minutters timer der draeber processen: en chat der
|
|
425
439
|
// foerst skal bruge browseren efter en halv time ville saa staa uden. Processen lever
|
|
426
|
-
// videre
|
|
440
|
+
// videre - det er kun PORTEN der ikke holdes reserveret til noget der ikke sker.
|
|
427
441
|
let portResolver = null;
|
|
428
442
|
let bindLoefte = null;
|
|
429
443
|
|
|
@@ -437,11 +451,11 @@ function loesPortLoefte(fik) {
|
|
|
437
451
|
|
|
438
452
|
// Slip porten, men BLIV I LIVE. Forskellen er hele pointen: lukkede vi processen ned,
|
|
439
453
|
// ville en chat der er faerdig med browseren kl. 10 og skal bruge den igen kl. 10:40 staa
|
|
440
|
-
// uden
|
|
454
|
+
// uden - og Claude Code genstarter ikke en MCP-server midt i en samtale. Processen koster
|
|
441
455
|
// ~35 MB og ingen port; det er porten der er den knappe ressource.
|
|
442
456
|
function frigivPort(grund) {
|
|
443
457
|
if (activePort === null) return;
|
|
444
|
-
process.stderr.write(`[MCP] frigiver port ${activePort}
|
|
458
|
+
process.stderr.write(`[MCP] frigiver port ${activePort} - ${grund}\n`);
|
|
445
459
|
if (heartbeat) { clearInterval(heartbeat); heartbeat = null; }
|
|
446
460
|
const gammel = wss;
|
|
447
461
|
wss = null;
|
|
@@ -458,6 +472,7 @@ function sikrePort() {
|
|
|
458
472
|
if (bindLoefte) return bindLoefte; // en binding er allerede i gang
|
|
459
473
|
alleePorteOptaget = false; // hvert forsoeg starter paa en frisk
|
|
460
474
|
bindFejl = null;
|
|
475
|
+
portBundetTid = 0; // nulstilles her; saettes naar 'listening' faktisk kommer
|
|
461
476
|
bindLoefte = new Promise((res) => { portResolver = res; });
|
|
462
477
|
// Vagthund. F8 var ét konkret hul hvor loeftet aldrig blev indfriet; det her lukker
|
|
463
478
|
// KLASSEN. Emitter en fremtidig fejlsti hverken 'listening' eller 'error', svarer
|
|
@@ -466,7 +481,7 @@ function sikrePort() {
|
|
|
466
481
|
const vagthund = setTimeout(() => {
|
|
467
482
|
if (!portResolver) return;
|
|
468
483
|
bindFejl = bindFejl || 'bindingen svarede ikke inden for 10 sekunder';
|
|
469
|
-
process.stderr.write('[MCP] port-bindingen svarede aldrig
|
|
484
|
+
process.stderr.write('[MCP] port-bindingen svarede aldrig - opgiver dette forsoeg\n');
|
|
470
485
|
loesPortLoefte(false);
|
|
471
486
|
}, 10000);
|
|
472
487
|
bindLoefte.finally(() => clearTimeout(vagthund));
|
|
@@ -476,24 +491,54 @@ function sikrePort() {
|
|
|
476
491
|
|
|
477
492
|
// ── Send command to extension ───────────────────────────────────────────────
|
|
478
493
|
|
|
479
|
-
async function sendToExtension(method, params = {}, timeoutMs = 30000, _retries = 5) {
|
|
494
|
+
async function sendToExtension(method, params = {}, timeoutMs = 30000, _retries = 5, _ekstraRunde = false, _doerAabnetNu = null) {
|
|
480
495
|
// 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
|
|
496
|
+
// Fik vi ingen (hele spaendet optaget), proever naeste kald igen - derfor ingen kast her.
|
|
482
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);
|
|
483
504
|
// Retry if extension is temporarily disconnected (reconnects every 2s)
|
|
484
505
|
const conn = activeConnection();
|
|
485
506
|
if (!conn) {
|
|
486
507
|
if (_retries > 0) {
|
|
487
508
|
await new Promise(r => setTimeout(r, 1500));
|
|
488
|
-
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
|
+
);
|
|
489
534
|
}
|
|
490
535
|
// This is the other half of the two-part setup: the server is clearly running (it is
|
|
491
536
|
// throwing this), so what is missing is the extension, Chrome itself, or the connection
|
|
492
|
-
// 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.
|
|
493
538
|
if (bindFejl) {
|
|
494
539
|
throw new Error(
|
|
495
540
|
`Serveren kunne ikke aabne en port paa 127.0.0.1 (${BASE_PORT}-${MAX_PORT}): ${bindFejl}\n` +
|
|
496
|
-
'Det er IKKE Chrome eller udvidelsen
|
|
541
|
+
'Det er IKKE Chrome eller udvidelsen - det er operativsystemet eller en firewall der ' +
|
|
497
542
|
'afviser bindingen. Tjek om noget blokerer loopback-porte.\n' +
|
|
498
543
|
'Sig praecis dét til brugeren. Sig IKKE at udvidelsen mangler.',
|
|
499
544
|
);
|
|
@@ -501,9 +546,9 @@ async function sendToExtension(method, params = {}, timeoutMs = 30000, _retries
|
|
|
501
546
|
if (alleePorteOptaget) {
|
|
502
547
|
throw new Error(
|
|
503
548
|
`Alle porte ${BASE_PORT}-${MAX_PORT} er optaget lige nu, saa dette kald fik ingen port. ` +
|
|
504
|
-
'Det er IKKE et problem med Chrome eller udvidelsen
|
|
549
|
+
'Det er IKKE et problem med Chrome eller udvidelsen - de virker fint.\n' +
|
|
505
550
|
`${MAX_PORT - BASE_PORT + 1} andre chats bruger browseren i oejeblikket. ` +
|
|
506
|
-
'Luk en af dem, eller vent til en bliver faerdig
|
|
551
|
+
'Luk en af dem, eller vent til en bliver faerdig - og proev saa kommandoen igen. ' +
|
|
507
552
|
'Denne chat skal IKKE genstartes: hvert kald proever selv at faa en port.\n' +
|
|
508
553
|
'Sig praecis dét til brugeren. Sig IKKE at udvidelsen mangler.',
|
|
509
554
|
);
|
|
@@ -514,7 +559,7 @@ async function sendToExtension(method, params = {}, timeoutMs = 30000, _retries
|
|
|
514
559
|
'Chrome extension (apparently not reachable).\n' +
|
|
515
560
|
'Check, in order:\n' +
|
|
516
561
|
' 1. Chrome is actually open and running.\n' +
|
|
517
|
-
' 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' +
|
|
518
563
|
' https://chromewebstore.google.com/detail/agent360-browser-mcp/jdehgalffmffhfhmmhaokfbfnafnmgcl\n' +
|
|
519
564
|
' 3. Click the extension icon -> Reconnect, and wait 2-3 seconds.\n' +
|
|
520
565
|
'Still stuck: https://browsermcp.dev/docs/troubleshooting/'
|
|
@@ -540,14 +585,14 @@ const INSTRUCTIONS = `You control the user's real Chrome browser via this MCP se
|
|
|
540
585
|
|
|
541
586
|
## Key behaviors
|
|
542
587
|
- **Always use browser_ask_user** when you need credentials, 2FA codes, CAPTCHA help, or any user input. Never guess passwords or tokens.
|
|
543
|
-
- **ALWAYS close tabs when done** with browser_close_tab after completing each task. Don't leave tabs open
|
|
544
|
-
- **Check existing tabs first** with browser_list_tabs before navigating
|
|
545
|
-
- **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.
|
|
546
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.
|
|
547
592
|
|
|
548
593
|
## Tab management
|
|
549
594
|
- navigate creates tabs in your session's tab group (visible in Chrome as colored groups)
|
|
550
|
-
- list_tabs only shows YOUR session's tabs
|
|
595
|
+
- list_tabs only shows YOUR session's tabs - other Claude sessions have their own
|
|
551
596
|
- switch_tab lets you jump between your tabs
|
|
552
597
|
- close_tab cleans up when you're done
|
|
553
598
|
|
|
@@ -560,27 +605,31 @@ const INSTRUCTIONS = `You control the user's real Chrome browser via this MCP se
|
|
|
560
605
|
6. After success, extract what you need with browser_get_page_content
|
|
561
606
|
|
|
562
607
|
## Screenshots
|
|
563
|
-
- browser_screenshot captures
|
|
564
|
-
-
|
|
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
|
|
565
614
|
|
|
566
615
|
## Text-based selectors (preferred for dynamic sites)
|
|
567
|
-
- browser_click("text=Get started")
|
|
568
|
-
- browser_click("button:text(Submit)")
|
|
569
|
-
- browser_fill("text=Email", "user@example.com")
|
|
570
|
-
- 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
|
|
571
620
|
- These work on ALL sites including Google Cloud, Stripe, Slack (CSP-strict)
|
|
572
621
|
|
|
573
622
|
## Keyboard
|
|
574
|
-
- browser_press_key("Enter")
|
|
575
|
-
- browser_press_key("Tab")
|
|
576
|
-
- browser_press_key("Escape")
|
|
577
|
-
- browser_press_key("ArrowDown")
|
|
578
|
-
- 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
|
|
579
628
|
|
|
580
629
|
## CAPTCHA handling
|
|
581
630
|
Use browser_solve_captcha to detect and solve CAPTCHAs automatically:
|
|
582
|
-
1. Call browser_solve_captcha()
|
|
583
|
-
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
|
|
584
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
|
|
585
634
|
4. If all else fails → call browser_solve_captcha(action="ask_human") to show overlay to user
|
|
586
635
|
5. After solving, retry the action that was blocked
|
|
@@ -596,7 +645,7 @@ For image grid challenges: cells are 0-indexed, left-to-right, top-to-bottom. A
|
|
|
596
645
|
- If a standard selector fails, the extension recursively searches shadow roots
|
|
597
646
|
- Text-based selectors ("text=Submit") also traverse shadow DOM
|
|
598
647
|
|
|
599
|
-
## Hard inputs
|
|
648
|
+
## Hard inputs - use the specialised tools first
|
|
600
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.
|
|
601
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.
|
|
602
651
|
- **Drag-drop file zones without visible file input** → use browser_drop_file (NOT browser_upload_file). Finds hidden input in subtree/parent.
|
|
@@ -606,28 +655,31 @@ For image grid challenges: cells are 0-indexed, left-to-right, top-to-bottom. A
|
|
|
606
655
|
- Element not found → try text-based selector instead of CSS
|
|
607
656
|
- Screenshot fails → debugger fallback is automatic
|
|
608
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
|
|
609
661
|
- CAPTCHA blocks page → use browser_ask_user, let human solve it
|
|
610
662
|
- browser_fill seemingly succeeds but value reverts → switch to browser_set_date or browser_set_combobox (most reverts are React-controlled validators)
|
|
611
663
|
|
|
612
664
|
## Extension updates
|
|
613
|
-
The
|
|
614
|
-
If the extension
|
|
615
|
-
"
|
|
616
|
-
You cannot navigate to chrome:// pages
|
|
617
|
-
|
|
618
|
-
## When Browser MCP itself is the obstacle
|
|
619
|
-
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
|
|
620
|
-
- **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.
|
|
621
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.
|
|
622
674
|
- **current** → the install is fine, so this is a genuine gap. Offer the returned submit_url as a clickable link.
|
|
623
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**.
|
|
624
676
|
Call it once per distinct obstacle, not once per retry.
|
|
625
677
|
|
|
626
678
|
## Sharing wishes / use-cases / bugs
|
|
627
|
-
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.`;
|
|
628
680
|
|
|
629
681
|
// FEJL MAALT 21/8: `instructions` laa i et TREDJE argument. Server-konstruktoeren
|
|
630
|
-
// 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 -
|
|
631
683
|
// og hele blokken ovenfor naaede aldrig frem til nogen klient. Verificeret ved at
|
|
632
684
|
// laese initialize-svaret: det havde kun protocolVersion, capabilities og serverInfo.
|
|
633
685
|
// Det betyder at "luk altid faner naar du er faerdig", CAPTCHA-fremgangsmaaden,
|
|
@@ -704,6 +756,62 @@ mcpServer.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
704
756
|
return { content: [{ type: 'text', text: `Unknown tool: ${name}` }], isError: true };
|
|
705
757
|
}
|
|
706
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
|
+
|
|
707
815
|
// extract_list scrolls a container in a loop (up to 300 rounds × wait_ms), so the 30 s
|
|
708
816
|
// default would kill a long mail list mid-walk and report a partial set as complete.
|
|
709
817
|
const timeout = method === 'ask_user' ? (args?.timeout || 120000) + 5000 :
|
|
@@ -724,12 +832,31 @@ mcpServer.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
724
832
|
// overskrive en fil i brugerens hjemmemappe med PNG-bytes.
|
|
725
833
|
const rod = resolve(process.cwd());
|
|
726
834
|
const targetPath = resolve(rod, args.path);
|
|
727
|
-
if (targetPath !== rod && !targetPath.startsWith(rod + sep)) {
|
|
835
|
+
if (targetPath !== rod && !targetPath.startsWith(rod.endsWith(sep) ? rod : rod + sep)) { // "/" giver ikke "//" (R5 F10)
|
|
728
836
|
throw new Error(
|
|
729
837
|
`path skal ligge inden for arbejdsmappen (${rod}). ` +
|
|
730
838
|
`"${args.path}" peger udenfor. Brug en relativ sti uden ../.`,
|
|
731
839
|
);
|
|
732
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
|
+
}
|
|
733
860
|
mkdirSync(dirname(targetPath), { recursive: true });
|
|
734
861
|
writeFileSync(targetPath, Buffer.from(base64, 'base64'));
|
|
735
862
|
return {
|
|
@@ -765,7 +892,7 @@ mcpServer.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
765
892
|
}
|
|
766
893
|
});
|
|
767
894
|
|
|
768
|
-
// 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
|
|
769
896
|
// alt brugeren ser. Det sker GARANTERET: serveren kommer fra npm og opdateres straks,
|
|
770
897
|
// mens udvidelsen skal gennem Chrome Web Stores review paa 1-3 dage. I det vindue
|
|
771
898
|
// findes otte vaerktoejer i serveren som en 1.25.0-udvidelse ikke kender
|
|
@@ -773,7 +900,7 @@ mcpServer.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
773
900
|
// udklipsholder-vaerktoejer, alle fra v1.26.0).
|
|
774
901
|
//
|
|
775
902
|
// Serveren VED at udvidelsen er gammel: den sendte intet haandtryk. Saa i stedet for
|
|
776
|
-
// en gaadefuld fejl faar brugeren at vide hvorfor
|
|
903
|
+
// en gaadefuld fejl faar brugeren at vide hvorfor - og hvad de kan goere imens.
|
|
777
904
|
const ERSTATNINGER = {
|
|
778
905
|
double_click: 'kald `browser_click` to gange',
|
|
779
906
|
right_click: 'brug `browser_execute_script` med et contextmenu-event',
|
|
@@ -792,7 +919,7 @@ function forklarSkaevhed(besked) {
|
|
|
792
919
|
const alt = ERSTATNINGER[m[1]];
|
|
793
920
|
return `Error: browser_${m[1]} findes i denne server, men ikke i din Chrome-udvidelse.\n\n` +
|
|
794
921
|
'Udvidelsen opdateres via Chrome Web Store og kan vaere 1-3 dage bagud efter en ' +
|
|
795
|
-
'udgivelse
|
|
922
|
+
'udgivelse - serveren opdateres med det samme via npm. Alt andet virker imens.\n' +
|
|
796
923
|
(alt ? `\nIndtil da: ${alt}.\n` : '') +
|
|
797
924
|
'\nTjek om en opdatering venter: chrome://extensions → Agent360 Browser MCP. ' +
|
|
798
925
|
'Er den indlaest som "unpacked", saa koer `npx @agent360/browser-mcp install`.';
|
|
@@ -814,11 +941,11 @@ function handleAbout(args) {
|
|
|
814
941
|
|
|
815
942
|
const instruction =
|
|
816
943
|
intent === 'wish'
|
|
817
|
-
? `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}`
|
|
818
945
|
: intent === 'use_case'
|
|
819
|
-
? `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}`
|
|
820
947
|
: intent === 'bug'
|
|
821
|
-
? `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}`
|
|
822
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`;
|
|
823
950
|
|
|
824
951
|
return {
|
|
@@ -840,13 +967,13 @@ function handleAbout(args) {
|
|
|
840
967
|
// ── Selv-diagnose: er installationen overhovedet frisk? ─────────────────────
|
|
841
968
|
// Baggrund (21/8): den udgave der koerte lokalt var npm 1.25.0, mens rettelserne
|
|
842
969
|
// laa uudgivet i repoet. Fejlen viste sig som "sessioner opfoerer sig underligt",
|
|
843
|
-
// ikke som "du koerer en gammel version"
|
|
970
|
+
// ikke som "du koerer en gammel version" - og der fandtes ingen maade at spoerge
|
|
844
971
|
// paa. Derfor spoerger vaerktoejet selv, foer det konkluderer noget som helst.
|
|
845
972
|
|
|
846
973
|
// Friskheds-tjek mod npm. SLUKKET SOM STANDARD siden 22/8.
|
|
847
974
|
//
|
|
848
|
-
// Produktet lover paa forsiden
|
|
849
|
-
// 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
|
|
850
977
|
// pakkenavn og ingen brugerdata. Loeftet vejer tungere end bekvemmeligheden, saa
|
|
851
978
|
// tjekket er nu opt-in: saet BROWSER_MCP_CHECK_NPM=1.
|
|
852
979
|
//
|
|
@@ -880,7 +1007,7 @@ function npmLatestVersion() {
|
|
|
880
1007
|
// rettes. Logbogen ligger LOKALT og forlader ikke maskinen.
|
|
881
1008
|
//
|
|
882
1009
|
// Bevidst ikke auto-indsendelse til et offentligt GitHub-issue: rapporten baerer URL og
|
|
883
|
-
// fejltekst fra den side agenten stod paa
|
|
1010
|
+
// fejltekst fra den side agenten stod paa - og det er ofte en annoncekonto, en indbakke
|
|
884
1011
|
// eller et kundesystem. Et offentligt issue kan ikke tages tilbage.
|
|
885
1012
|
const FEEDBACK_LOG = join(homedir(), '.browser-mcp', 'feedback.jsonl');
|
|
886
1013
|
const setteFingeraftryk = new Set(); // samme graense logges én gang pr. serverliv
|
|
@@ -896,7 +1023,7 @@ function fingeraftryk(kind, tool, what) {
|
|
|
896
1023
|
}
|
|
897
1024
|
|
|
898
1025
|
// URL'en reduceres til oprindelse + sti. Query og fragment baerer tokens, sessions-id'er
|
|
899
|
-
// og soegetermer
|
|
1026
|
+
// og soegetermer - de har intet at goere i en logbog nogen senere kopierer ind i et issue.
|
|
900
1027
|
function afkortUrl(u) {
|
|
901
1028
|
if (!u) return null;
|
|
902
1029
|
try { const x = new URL(u); return x.origin + x.pathname; } catch { return '(ulaeselig url)'; }
|
|
@@ -952,12 +1079,12 @@ async function handleProvideFeedback(args) {
|
|
|
952
1079
|
);
|
|
953
1080
|
fix_steps.push(
|
|
954
1081
|
'Aabn chrome://extensions og slaa alle Browser MCP-udvidelser fra paa naer én. ' +
|
|
955
|
-
'Det skal brugeren selv goere
|
|
1082
|
+
'Det skal brugeren selv goere - chrome:// kan ikke styres herfra. Behold den nyeste.',
|
|
956
1083
|
);
|
|
957
1084
|
}
|
|
958
1085
|
if (!active && activePort === null) {
|
|
959
1086
|
// 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
|
|
1087
|
+
// i stykker, og et fix-skridt her ville vaere en falsk alarm - se verdict 'idle'.
|
|
961
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.');
|
|
962
1089
|
} else if (!active) {
|
|
963
1090
|
findings.push('Ingen Chrome-udvidelse er forbundet til denne MCP-server lige nu.');
|
|
@@ -965,7 +1092,7 @@ async function handleProvideFeedback(args) {
|
|
|
965
1092
|
}
|
|
966
1093
|
if (serverOutdated) {
|
|
967
1094
|
findings.push(`MCP-serveren koerer v${PKG_VERSION}, men npm har v${npmLatest}. Fejlen kan allerede vaere rettet.`);
|
|
968
|
-
fix_steps.push(`Genstart klienten
|
|
1095
|
+
fix_steps.push(`Genstart klienten - den henter selv @agent360/browser-mcp@latest (v${npmLatest}).`);
|
|
969
1096
|
}
|
|
970
1097
|
if (extOutdated) {
|
|
971
1098
|
findings.push(
|
|
@@ -974,35 +1101,39 @@ async function handleProvideFeedback(args) {
|
|
|
974
1101
|
: `Udvidelsen er v${extVersion}, serveren er v${PKG_VERSION}. Udvidelsen mangler rettelser fra de mellemliggende udgaver.`,
|
|
975
1102
|
);
|
|
976
1103
|
// MAALT 22/8: "↻ reload" er ubrugeligt for en Chrome Web Store-bruger. Butikken
|
|
977
|
-
// 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
|
|
978
1105
|
// version at hente endnu, saa raadet foerer i ring. Serveren kan ikke se hvilken
|
|
979
1106
|
// slags installation det er (den gamle udvidelse oplyser intet), saa begge tilfaelde
|
|
980
|
-
// skal staa der
|
|
1107
|
+
// skal staa der - og det skal siges at ventetiden er forventet, ikke en fejl.
|
|
981
1108
|
fix_steps.push(
|
|
982
1109
|
extVersion === null
|
|
983
1110
|
? 'Kommer udvidelsen fra Chrome Web Store: der er sandsynligvis en nyere version i review ' +
|
|
984
|
-
'(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 - ' +
|
|
985
1112
|
'det er forventet og gaar over af sig selv. Alt andet virker imens. ' +
|
|
986
1113
|
'Er den indlaest som "unpacked": koer `npx @agent360/browser-mcp install` og derefter ' +
|
|
987
1114
|
'chrome://extensions → Agent360 Browser MCP → ↻ reload.'
|
|
988
|
-
|
|
989
|
-
|
|
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.',
|
|
990
1121
|
);
|
|
991
1122
|
}
|
|
992
1123
|
|
|
993
1124
|
// ── "ingen forbindelse" og "ingen port endnu" er IKKE det samme ────────────
|
|
994
1125
|
//
|
|
995
1126
|
// 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
|
|
1127
|
+
// udvidelsen var altid forbundet naar dette vaerktoej blev kaldt - og `!active`
|
|
997
1128
|
// betoed derfor paalideligt "udvidelsen kan ikke naas". Nu binder en chat foerst en
|
|
998
1129
|
// port naar den bruger browseren, saa en HELT SUND chat der ikke har roert den kan
|
|
999
1130
|
// staa uden forbindelse. Uden det her skel fik den `disconnected` + fix_steps der
|
|
1000
|
-
// bad brugeren geninstallere
|
|
1131
|
+
// bad brugeren geninstallere - og INSTRUCTIONS beder agenten viderebringe dem.
|
|
1001
1132
|
// Vi ville altsaa fortaelle folk at deres installation var i stykker, fordi vi selv
|
|
1002
1133
|
// endnu ikke havde aabnet doeren.
|
|
1003
1134
|
//
|
|
1004
1135
|
// Loesningen er ikke at binde en port her (et diagnose-vaerktoej skal ikke aendre
|
|
1005
|
-
// tilstand for at kunne maale den)
|
|
1136
|
+
// tilstand for at kunne maale den) - det er at sige praecis hvad der er tilfaeldet.
|
|
1006
1137
|
const ingenPortEndnu = activePort === null;
|
|
1007
1138
|
const verdict =
|
|
1008
1139
|
exts.length > 1 ? 'conflict'
|
|
@@ -1019,6 +1150,8 @@ async function handleProvideFeedback(args) {
|
|
|
1019
1150
|
name: c.name,
|
|
1020
1151
|
version: c.version,
|
|
1021
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,
|
|
1022
1155
|
active: c === active,
|
|
1023
1156
|
})),
|
|
1024
1157
|
extension_up_to_date: extOutdated === null ? null : !extOutdated,
|
|
@@ -1031,7 +1164,7 @@ async function handleProvideFeedback(args) {
|
|
|
1031
1164
|
what && `**What happened**\n${what}`,
|
|
1032
1165
|
tool && `\n**Tool**: \`${tool}\``,
|
|
1033
1166
|
// MAALT 22/8 ved sikkerhedsreview: her stod den RAA url, mens den lokale logbog
|
|
1034
|
-
// nedenfor bruger afkortUrl(). Query-strengen
|
|
1167
|
+
// nedenfor bruger afkortUrl(). Query-strengen - hvor tokens bor - blev altsaa
|
|
1035
1168
|
// strippet fra filen paa disken, men sendt uredigeret ind i et link til et
|
|
1036
1169
|
// OFFENTLIGT GitHub-issue. Praecis den forkerte vej rundt.
|
|
1037
1170
|
url && `\n**URL**: ${afkortUrl(url)}`,
|
|
@@ -1048,14 +1181,14 @@ async function handleProvideFeedback(args) {
|
|
|
1048
1181
|
|
|
1049
1182
|
const instruction =
|
|
1050
1183
|
verdict === 'idle'
|
|
1051
|
-
? 'Browseren er ikke taget i brug i denne chat endnu, saa der er intet at diagnosticere paa forbindelsen
|
|
1184
|
+
? 'Browseren er ikke taget i brug i denne chat endnu, saa der er intet at diagnosticere paa forbindelsen - ' +
|
|
1052
1185
|
'det er IKKE en fejl i installationen, og du maa ikke sige det til brugeren. ' +
|
|
1053
1186
|
'Er der en aegte mangel, saa tilbyd submit_url som et klikbart link.'
|
|
1054
1187
|
: verdict === 'conflict' || verdict === 'outdated' || verdict === 'disconnected'
|
|
1055
1188
|
? 'Fortael brugeren hvad der blev fundet, og giv fix_steps som konkrete skridt. Proev derefter handlingen igen. ' +
|
|
1056
|
-
'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.'
|
|
1057
1190
|
: 'Installationen er frisk, saa det her er sandsynligvis en aegte mangel eller fejl. Fortael brugeren kort hvad der ikke kunne lade sig goere, ' +
|
|
1058
|
-
'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.';
|
|
1059
1192
|
|
|
1060
1193
|
const logbog = skrivTilLogbog({
|
|
1061
1194
|
at: new Date().toISOString(),
|
|
@@ -1107,14 +1240,14 @@ async function handleExtractToken(args) {
|
|
|
1107
1240
|
|
|
1108
1241
|
// ── Graceful shutdown ──────────────────────────────────────────────────────
|
|
1109
1242
|
// All shutdown paths funnel through gracefulShutdown so the cleanup chain runs
|
|
1110
|
-
// deterministically
|
|
1243
|
+
// deterministically - even on abrupt parent-exit. Without this, process.exit(0)
|
|
1111
1244
|
// was racing against WS close-handshake, leaving zombie tabs in Chrome.
|
|
1112
1245
|
|
|
1113
1246
|
let shuttingDown = false;
|
|
1114
1247
|
function gracefulShutdown(reason, code = 0) {
|
|
1115
1248
|
if (shuttingDown) return;
|
|
1116
1249
|
shuttingDown = true;
|
|
1117
|
-
process.stderr.write(`[MCP] ${reason}
|
|
1250
|
+
process.stderr.write(`[MCP] ${reason} - shutting down\n`);
|
|
1118
1251
|
|
|
1119
1252
|
// Stop timers so they can't re-enter gracefulShutdown
|
|
1120
1253
|
if (parentCheck) clearInterval(parentCheck);
|
|
@@ -1147,7 +1280,7 @@ process.on('exit', () => {
|
|
|
1147
1280
|
// Claude Code → wrapper → npm exec → denne server
|
|
1148
1281
|
//
|
|
1149
1282
|
// `process.ppid` er `npm exec`, ikke Claude Code. Doer chatten, kan `npm exec`
|
|
1150
|
-
// blive haengende som foraeldreloes
|
|
1283
|
+
// blive haengende som foraeldreloes - og saa ser tjekket en levende foraelder for
|
|
1151
1284
|
// evigt. Porten blev holdt i op til fire timer (idle-graensen), og med flere
|
|
1152
1285
|
// forladte chats loeb spaendet fuldt.
|
|
1153
1286
|
//
|
|
@@ -1157,7 +1290,7 @@ process.on('exit', () => {
|
|
|
1157
1290
|
//
|
|
1158
1291
|
// Kaeden hentes én gang ved opstart (én ps-kommando), og derefter koster tjekket
|
|
1159
1292
|
// kun et signal 0 pr. led hvert 5. sekund. Kan kaeden ikke laeses (Windows, eller
|
|
1160
|
-
// ps mangler), falder vi tilbage til det gamle enkelt-tjek
|
|
1293
|
+
// ps mangler), falder vi tilbage til det gamle enkelt-tjek - daarligere, men aldrig
|
|
1161
1294
|
// vaerre end foer.
|
|
1162
1295
|
|
|
1163
1296
|
// ps-opslaget bor her (det er en sideeffekt); selve kaede-logikken og doeds-dommen
|
|
@@ -1178,7 +1311,7 @@ function laesPpid(pid) {
|
|
|
1178
1311
|
const parentPid = process.ppid;
|
|
1179
1312
|
|
|
1180
1313
|
// MAALT 22/8: doer den naermeste foraelder FOER serveren er bootet (~2 sek node+SDK),
|
|
1181
|
-
// laeser process.ppid vaerdien 1
|
|
1314
|
+
// laeser process.ppid vaerdien 1 - altsaa launchd. Kaeden blev saa [1], og pid 1 er
|
|
1182
1315
|
// baade udoedelig og ejet af root, saa vagten var enten inert eller draebte os selv
|
|
1183
1316
|
// paa EPERM. En kaede der kun bestaar af pid 1 vogter ingenting og skal ikke bruges.
|
|
1184
1317
|
let vagtKaede = parentPid > 1 ? [parentPid] : [];
|
|
@@ -1188,7 +1321,7 @@ try {
|
|
|
1188
1321
|
} catch {}
|
|
1189
1322
|
if (!vagtKaede.length) {
|
|
1190
1323
|
process.stderr.write('[MCP] ingen brugbar foraelder-kaede (ppid=' + parentPid +
|
|
1191
|
-
')
|
|
1324
|
+
') - falder tilbage paa idle-graensen alene\n');
|
|
1192
1325
|
}
|
|
1193
1326
|
process.stderr.write(`[MCP] vagt-kaede: ${vagtKaede.join(' → ')}\n`);
|
|
1194
1327
|
|
|
@@ -1199,14 +1332,14 @@ parentCheck = setInterval(() => {
|
|
|
1199
1332
|
// ── Et doedt led betyder ikke automatisk at chatten er vaek (MAALT 23/8) ──────
|
|
1200
1333
|
//
|
|
1201
1334
|
// Kaeden blev frosset ved opstart. Men et MELLEMLED kan afslutte helt normalt
|
|
1202
|
-
// mens ejeren koerer videre
|
|
1335
|
+
// mens ejeren koerer videre - maalt to gange paa denne maskine, hvor kaeden gaar
|
|
1203
1336
|
// npm exec → wrapper → claude → Code Helper (Plugin) → Code. Et forbigaaende led
|
|
1204
1337
|
// der lukkede pænt udloeste "chatten bag denne server er vaek", mens chatten var
|
|
1205
1338
|
// uroert. Og risikoen er ensrettet vaerre end 1.25.0, som vogtede ét pid: nu er
|
|
1206
1339
|
// hvert af 5-6 led en ny doedsaarsag.
|
|
1207
1340
|
//
|
|
1208
1341
|
// Derfor genlaeses kaeden foerst naar noget SER doedt ud. Kan vi stadig gaa fra
|
|
1209
|
-
// 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
|
|
1210
1343
|
// reparented, og den nye kaede overtager. Kun naar den vej ogsaa er vaek, lukker vi.
|
|
1211
1344
|
//
|
|
1212
1345
|
// Prisen er nul i normal drift: genlaesningen koerer kun i det tik hvor et led er
|
|
@@ -1219,14 +1352,14 @@ parentCheck = setInterval(() => {
|
|
|
1219
1352
|
if (frisk.length) {
|
|
1220
1353
|
process.stderr.write(
|
|
1221
1354
|
`[MCP] led ${doede.join(', ')} er vaek, men kaeden gaar stadig op: ` +
|
|
1222
|
-
`${frisk.join(' → ')}
|
|
1355
|
+
`${frisk.join(' → ')} - fortsaetter\n`,
|
|
1223
1356
|
);
|
|
1224
1357
|
vagtKaede = frisk;
|
|
1225
1358
|
return;
|
|
1226
1359
|
}
|
|
1227
1360
|
|
|
1228
1361
|
gracefulShutdown(
|
|
1229
|
-
`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 - ` +
|
|
1230
1363
|
'chatten bag denne server er vaek',
|
|
1231
1364
|
);
|
|
1232
1365
|
}, 5000); // hvert 5. sekund
|