@agent360/browser-mcp 1.25.0 → 1.28.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 +95 -21
- package/bin/cli.js +102 -44
- package/extension/background.js +934 -109
- package/extension/icons/icon-128.png +0 -0
- package/extension/icons/icon-48.png +0 -0
- package/extension/manifest.json +2 -2
- package/extension/offscreen.js +142 -10
- package/extension/popup.html +36 -2
- package/extension/popup.js +37 -21
- package/index.js +686 -54
- package/package.json +17 -3
- package/tools.js +95 -8
- package/vagt.js +57 -0
package/index.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Agent360 Browser MCP Server
|
|
4
4
|
*
|
|
5
5
|
* Bridges Claude Code (stdio MCP) to Chrome Extension (WebSocket).
|
|
6
|
-
* Auto-selects first available port in range 9876-
|
|
6
|
+
* Auto-selects first available port in range 9876-9895 for multi-session support.
|
|
7
7
|
*
|
|
8
8
|
* Architecture:
|
|
9
9
|
* Claude Code ←(stdio)→ this process ←(WS :port)→ Offscreen Doc ←(sendMessage)→ Service Worker → Chrome APIs
|
|
@@ -11,12 +11,14 @@
|
|
|
11
11
|
|
|
12
12
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
13
13
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
14
|
+
import { ledErDoedt, forfaedreKaede } from './vagt.js';
|
|
14
15
|
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
15
16
|
import { WebSocketServer } from 'ws';
|
|
16
|
-
import { execSync } from 'child_process';
|
|
17
|
-
import { dirname, join, resolve } from 'path';
|
|
17
|
+
import { execSync, execFile } from 'child_process';
|
|
18
|
+
import { dirname, join, resolve, sep } from 'path';
|
|
18
19
|
import { fileURLToPath } from 'url';
|
|
19
|
-
import {
|
|
20
|
+
import { homedir } from 'os';
|
|
21
|
+
import { readFileSync, writeFileSync, mkdirSync, appendFileSync } from 'fs';
|
|
20
22
|
import { TOOLS, PROVIDER_PAGES } from './tools.js';
|
|
21
23
|
|
|
22
24
|
// Read version from package.json — single source of truth, never drifts
|
|
@@ -29,33 +31,114 @@ const PKG_VERSION = JSON.parse(
|
|
|
29
31
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
30
32
|
const repoDir = dirname(__dirname); // parent of mcp-server/
|
|
31
33
|
|
|
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/ —
|
|
36
|
+
// og git soeger OPAD, saa kaldet landede i BRUGERENS EGET repo. Maalt: fra
|
|
37
|
+
// node_modules/@agent360 opløser git toplevel til det omkringliggende projekt.
|
|
38
|
+
// En agent-session maatte altsaa ikke mutere brugerens git-trae uden samtykke.
|
|
39
|
+
// `npx @agent360/browser-mcp@latest` opdaterer allerede serveren; blokken var overfloedig.
|
|
32
40
|
let extensionUpdated = false;
|
|
33
|
-
try {
|
|
34
|
-
const before = execSync('git rev-parse HEAD', { cwd: repoDir }).toString().trim();
|
|
35
|
-
execSync('git pull --ff-only 2>/dev/null', { cwd: repoDir, timeout: 10000 });
|
|
36
|
-
const after = execSync('git rev-parse HEAD', { cwd: repoDir }).toString().trim();
|
|
37
|
-
if (before !== after) {
|
|
38
|
-
extensionUpdated = true;
|
|
39
|
-
process.stderr.write(`[MCP] Updated to ${after.slice(0, 8)} — extension reload recommended\n`);
|
|
40
|
-
// Check if npm deps changed
|
|
41
|
-
try {
|
|
42
|
-
const diff = execSync(`git diff ${before} ${after} -- mcp-server/package.json`, { cwd: repoDir }).toString();
|
|
43
|
-
if (diff) {
|
|
44
|
-
execSync('npm install --silent', { cwd: `${repoDir}/mcp-server`, timeout: 30000 });
|
|
45
|
-
process.stderr.write('[MCP] Dependencies updated\n');
|
|
46
|
-
}
|
|
47
|
-
} catch {}
|
|
48
|
-
} else {
|
|
49
|
-
process.stderr.write('[MCP] Already up to date\n');
|
|
50
|
-
}
|
|
51
|
-
} catch (e) {
|
|
52
|
-
process.stderr.write(`[MCP] Auto-update skipped: ${e.message?.split('\n')[0]}\n`);
|
|
53
|
-
}
|
|
54
41
|
|
|
55
42
|
const BASE_PORT = 9876;
|
|
56
43
|
const MAX_PORT = 9895; // 20 ports instead of 10 — zombies die within 5s via parent check
|
|
57
|
-
|
|
44
|
+
|
|
45
|
+
// ── Extension connections ───────────────────────────────────────────────────
|
|
46
|
+
// 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 — scanner BEGGE de samme porte
|
|
49
|
+
// og forbinder til hver eneste server. Maalt med lsof: 2 ESTABLISHED forbindelser
|
|
50
|
+
// 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 — og et
|
|
52
|
+
// `terminate` fra den forkerte kopi lukkede serveren ned under den rigtige.
|
|
53
|
+
//
|
|
54
|
+
// Nu holdes alle forbindelser med deres identitet, kommandoer sendes kun til den
|
|
55
|
+
// nyeste udgave, og konflikten kan ses (browser_provide_feedback) i stedet for at
|
|
56
|
+
// vise sig som faner der "forsvinder".
|
|
57
|
+
const connections = new Set(); // { ws, seq, extensionId, version, name, since }
|
|
58
|
+
let connSeq = 0;
|
|
58
59
|
let activePort = null;
|
|
60
|
+
let alleePorteOptaget = false; // hele spaendet i brug — se createWSS
|
|
61
|
+
|
|
62
|
+
function cmpVersion(a, b) {
|
|
63
|
+
const pa = String(a || '0.0.0').split('.').map(n => parseInt(n, 10) || 0);
|
|
64
|
+
const pb = String(b || '0.0.0').split('.').map(n => parseInt(n, 10) || 0);
|
|
65
|
+
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
|
|
66
|
+
const d = (pa[i] || 0) - (pb[i] || 0);
|
|
67
|
+
if (d) return d > 0 ? 1 : -1;
|
|
68
|
+
}
|
|
69
|
+
return 0;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
let sidsteKonfliktNoegle = '';
|
|
73
|
+
function advarOmKonflikt(conn) {
|
|
74
|
+
const alle = distinctExtensions();
|
|
75
|
+
if (alle.length < 2) return;
|
|
76
|
+
// Samme konflikt maa ikke skrige ved hver eneste hello — kun naar billedet aendrer sig.
|
|
77
|
+
const noegle = alle.map(c => `${c.extensionId || 'ukendt'}@${c.version || '?'}`).sort().join('|');
|
|
78
|
+
if (noegle === sidsteKonfliktNoegle) return;
|
|
79
|
+
sidsteKonfliktNoegle = noegle;
|
|
80
|
+
const aktiv = activeConnection();
|
|
81
|
+
// 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 — ellers laeser man
|
|
83
|
+
// "kommandoer sendes kun til X" som om X var det rigtige valg.
|
|
84
|
+
const kanVaelge = alle.some(c => c.version);
|
|
85
|
+
process.stderr.write(
|
|
86
|
+
`[MCP] ADVARSEL: ${alle.length} Browser MCP-udvidelser er forbundet til denne server samtidig ` +
|
|
87
|
+
`(${alle.map(c => `${c.extensionId || 'ukendt id'}${c.version ? ' v' + c.version : ''}`).join(', ')}). ` +
|
|
88
|
+
'De deler faner og sessions-tilstand, saa faner kan se ud til at forsvinde. ' +
|
|
89
|
+
(kanVaelge
|
|
90
|
+
? `Kommandoer sendes kun til den nyeste (${aktiv?.extensionId}). `
|
|
91
|
+
: `Ingen af dem oplyser sin version, saa valget (${aktiv?.extensionId}) er vilkaarligt og kan skifte. `) +
|
|
92
|
+
'Ret det ved at slaa alle paa naer én fra paa chrome://extensions.\n',
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function liveConnections() {
|
|
97
|
+
return [...connections].filter(c => c.ws.readyState === 1);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Valget LAASES for serverens levetid.
|
|
101
|
+
//
|
|
102
|
+
// MAALT 21/8 i flow-harnessen: uden laasen skiftede den aktive udvidelse MIDT i en
|
|
103
|
+
// 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 —
|
|
105
|
+
// men B kendte ikke A's fane og lavede en frisk about:blank. Alt derefter fejlede med
|
|
106
|
+
// "Cannot access contents of url about:blank". 21 af 43 vaerktoejer faldt paa det, og
|
|
107
|
+
// symptomet lignede praecis "faner forsvinder" og "kun én session virker".
|
|
108
|
+
//
|
|
109
|
+
// Faner hoerer til den udvidelse der aabnede dem. Skifter man udvidelse, strander de.
|
|
110
|
+
// Derfor: vaelg én gang, og bliv ved den saa laenge dens forbindelse lever. Doer den,
|
|
111
|
+
// vaelges der forfra — det er en aegte genopretning, ikke et vilkaarligt skift.
|
|
112
|
+
let laastForbindelse = null;
|
|
113
|
+
let harSendtKommando = false;
|
|
114
|
+
const PINNET_UDVIDELSE = (process.env.BROWSER_MCP_EXTENSION_ID || '').trim() || null;
|
|
115
|
+
|
|
116
|
+
function activeConnection() {
|
|
117
|
+
if (laastForbindelse && laastForbindelse.ws.readyState === 1) return laastForbindelse;
|
|
118
|
+
|
|
119
|
+
// 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 — haandtrykket kom
|
|
121
|
+
// med den nyere udgave.
|
|
122
|
+
let best = null;
|
|
123
|
+
for (const c of liveConnections()) {
|
|
124
|
+
if (!best) { best = c; continue; }
|
|
125
|
+
const d = cmpVersion(c.version, best.version);
|
|
126
|
+
if (d > 0 || (d === 0 && c.since > best.since)) best = c;
|
|
127
|
+
}
|
|
128
|
+
laastForbindelse = best;
|
|
129
|
+
return best;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Én post pr. udvidelse. En udvidelse uden identitet taelles for sig selv (dens
|
|
133
|
+
// socket er noeglen), saa to gamle kopier stadig ses som to.
|
|
134
|
+
function distinctExtensions() {
|
|
135
|
+
const byKey = new Map();
|
|
136
|
+
for (const c of liveConnections()) {
|
|
137
|
+
const key = c.extensionId || `legacy:${c.seq}`;
|
|
138
|
+
if (!byKey.has(key)) byKey.set(key, c);
|
|
139
|
+
}
|
|
140
|
+
return [...byKey.values()];
|
|
141
|
+
}
|
|
59
142
|
let wss = null; // Track WSS for graceful shutdown
|
|
60
143
|
let cmdId = 0;
|
|
61
144
|
let lastActivity = Date.now();
|
|
@@ -68,7 +151,26 @@ let parentCheck = null;
|
|
|
68
151
|
// ── WebSocket Server ───────────────────────────────────────────────────────
|
|
69
152
|
|
|
70
153
|
function createWSS(port = BASE_PORT) {
|
|
71
|
-
const server = new WebSocketServer({
|
|
154
|
+
const server = new WebSocketServer({
|
|
155
|
+
host: '127.0.0.1',
|
|
156
|
+
port,
|
|
157
|
+
// Afvis allerede i HAANDTRYKKET, ikke efter. MAALT 23/8: lukkede vi foerst
|
|
158
|
+
// forbindelsen inde i 'connection', naaede den fremmede at faa en aaben socket
|
|
159
|
+
// (og et 101-svar) foer den blev smidt ud. Med verifyClient faar den 401 og
|
|
160
|
+
// ingen socket overhovedet.
|
|
161
|
+
//
|
|
162
|
+
// Chrome saetter ALTID Origin: chrome-extension://<32 tegn> paa en WebSocket fra
|
|
163
|
+
// en udvidelse — verificeret mod den koerende. Alt andet er per definition ikke
|
|
164
|
+
// en udvidelse, saa gaten koster aegte brugere ingenting.
|
|
165
|
+
verifyClient: ({ origin }, godkend) => {
|
|
166
|
+
if (/^chrome-extension:\/\/[a-p]{32}$/.test(origin || '')) return godkend(true);
|
|
167
|
+
process.stderr.write(
|
|
168
|
+
`[MCP] Afviser opkobling uden gyldig chrome-extension-Origin` +
|
|
169
|
+
`${origin ? ` (fik "${String(origin).slice(0, 60)}")` : ' (ingen Origin-header)'}\n`,
|
|
170
|
+
);
|
|
171
|
+
godkend(false, 401, 'only chrome extensions may connect');
|
|
172
|
+
},
|
|
173
|
+
});
|
|
72
174
|
wss = server;
|
|
73
175
|
|
|
74
176
|
server.on('error', (err) => {
|
|
@@ -77,6 +179,13 @@ function createWSS(port = BASE_PORT) {
|
|
|
77
179
|
process.stderr.write(`[MCP] Port ${port} in use, trying ${port + 1}...\n`);
|
|
78
180
|
createWSS(port + 1);
|
|
79
181
|
} else {
|
|
182
|
+
// MAALT 22/8: her stod KUN denne stderr-linje. Ingen laeser stderr fra en
|
|
183
|
+
// MCP-server, saa udtoemte porte var en helt tavs fejl. Hvert vaerktoejskald
|
|
184
|
+
// fejlede bagefter med "extension not connected" — en tekst der oven i koebet
|
|
185
|
+
// siger at serveren koerer og sender brugeren til Chrome Web Store. Begge dele
|
|
186
|
+
// er forkerte naar sandheden er at vi aldrig fik en port. Flaget laeses i
|
|
187
|
+
// sendToExtension, saa agenten kan give brugeren den rigtige forklaring.
|
|
188
|
+
alleePorteOptaget = true;
|
|
80
189
|
process.stderr.write(`[MCP] All ports ${BASE_PORT}-${MAX_PORT} in use. Cannot start.\n`);
|
|
81
190
|
}
|
|
82
191
|
} else {
|
|
@@ -84,9 +193,57 @@ function createWSS(port = BASE_PORT) {
|
|
|
84
193
|
}
|
|
85
194
|
});
|
|
86
195
|
|
|
87
|
-
server.on('connection', (ws) => {
|
|
88
|
-
|
|
89
|
-
|
|
196
|
+
server.on('connection', (ws, req) => {
|
|
197
|
+
// WebSocket-handshaket fra en udvidelse baerer Origin: chrome-extension://<id>.
|
|
198
|
+
// Den identificerer afsenderen UDEN at udvidelsen behoever at kende haandtrykket,
|
|
199
|
+
// saa en konflikt mellem to indlaeste udvidelser kan opdages ogsaa naar begge er
|
|
200
|
+
// gamle udgaver — hvilket er praecis den situation konflikten opstaar i.
|
|
201
|
+
// Maalt 21/8: to distinkte origins ringede op til hver eneste server.
|
|
202
|
+
const origin = req?.headers?.origin || '';
|
|
203
|
+
const fraOrigin = /^chrome-extension:\/\/([a-p]{32})$/.exec(origin)?.[1] || null;
|
|
204
|
+
|
|
205
|
+
// ── Kun Chrome-udvidelser lukkes ind (MAALT 23/8) ─────────────────────────
|
|
206
|
+
//
|
|
207
|
+
// Reproduceret med en raa WebSocket-klient mod en aegte server: en klient der
|
|
208
|
+
// simpelthen UDELOD Origin-headeren blev accepteret, vandt rollen som aktiv
|
|
209
|
+
// udvidelse med `hello version 99.0.0`, fik `browser_get_cookies` leveret, og
|
|
210
|
+
// kunne lukke serveren med `terminate`. Alle tre trin lykkedes.
|
|
211
|
+
//
|
|
212
|
+
// Serveren lytter kun paa 127.0.0.1, saa angriberen skal koere lokalt — men det
|
|
213
|
+
// goer enhver anden app og ethvert npm-postinstall-script. Og hvad den kan er
|
|
214
|
+
// ikke smaating: laese alt agenten sender til browseren (kodeord fra ask_user,
|
|
215
|
+
// cookies, sidetekst), fodre agenten med opdigtet sideindhold, og slukke
|
|
216
|
+
// browser-adgangen i alle aabne chats.
|
|
217
|
+
//
|
|
218
|
+
// Chrome saetter ALTID `Origin: chrome-extension://<id>` paa en WebSocket fra en
|
|
219
|
+
// udvidelse — verificeret mod den koerende udvidelse. En manglende header er
|
|
220
|
+
// derfor ikke en aeldre udgave; det er noget andet end en udvidelse.
|
|
221
|
+
if (!fraOrigin) {
|
|
222
|
+
process.stderr.write(
|
|
223
|
+
"[MCP] Afviser forbindelse uden gyldig chrome-extension-Origin" +
|
|
224
|
+
(origin ? ` (fik "${origin.slice(0, 60)}")` : " (ingen Origin-header)") + "\n",
|
|
225
|
+
);
|
|
226
|
+
try { ws.close(1008, "only chrome extensions may connect"); } catch {}
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Noedudgang naar flere udvidelser er indlaest og brugeren ikke kan eller vil
|
|
231
|
+
// slaa dem fra: BROWSER_MCP_EXTENSION_ID=<id> binder serveren til én bestemt.
|
|
232
|
+
// Uden den er valget vilkaarligt naar ingen af dem oplyser en version.
|
|
233
|
+
if (PINNET_UDVIDELSE && fraOrigin && fraOrigin !== PINNET_UDVIDELSE) {
|
|
234
|
+
process.stderr.write(`[MCP] Afviser udvidelse ${fraOrigin} — bundet til ${PINNET_UDVIDELSE}\n`);
|
|
235
|
+
try { ws.close(1008, 'not the pinned extension'); } catch {}
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const conn = { ws, seq: ++connSeq, extensionId: fraOrigin, version: null, name: null,
|
|
240
|
+
harHilst: false, helloId: null, since: Date.now() };
|
|
241
|
+
connections.add(conn);
|
|
242
|
+
// Har vi endnu ikke sendt en eneste kommando, er ingen faner i spil, og en
|
|
243
|
+
// nytilkommen udvidelse maa gerne komme i betragtning igen.
|
|
244
|
+
if (!harSendtKommando) laastForbindelse = null;
|
|
245
|
+
process.stderr.write(`[MCP] Chrome extension connected on port ${port}${fraOrigin ? ` (${fraOrigin})` : ''}\n`);
|
|
246
|
+
advarOmKonflikt(conn);
|
|
90
247
|
|
|
91
248
|
// If extension was auto-updated, trigger reload
|
|
92
249
|
if (process.env.BROWSER_MCP_EXTENSION_UPDATED === '1') {
|
|
@@ -101,7 +258,53 @@ function createWSS(port = BASE_PORT) {
|
|
|
101
258
|
let msg;
|
|
102
259
|
try { msg = JSON.parse(data.toString()); } catch { return; }
|
|
103
260
|
|
|
261
|
+
// Identitets-haandtryk fra offscreen-dokumentet (v1.28+).
|
|
262
|
+
if (msg.type === 'hello') {
|
|
263
|
+
// MAALT 23/8: her stod `conn.extensionId = msg.extensionId` — altsaa lod
|
|
264
|
+
// haandtrykket afsenderen OVERSKRIVE sin egen identitet med hvad som helst.
|
|
265
|
+
// Origin-headeren er den eneste kilde Chrome selv saetter og som afsenderen
|
|
266
|
+
// ikke kan forfalske, saa den vinder. Beskedens id gemmes separat: stemmer de
|
|
267
|
+
// ikke overens, er noget galt, og saa maa forbindelsen ikke lukke serveren ned.
|
|
268
|
+
conn.helloId = typeof msg.extensionId === 'string' ? msg.extensionId : null;
|
|
269
|
+
conn.harHilst = true;
|
|
270
|
+
if (conn.helloId && conn.helloId !== conn.extensionId) {
|
|
271
|
+
process.stderr.write(
|
|
272
|
+
`[MCP] Haandtryk oplyser ${conn.helloId} men Origin siger ${conn.extensionId} — ` +
|
|
273
|
+
'bruger Origin\n',
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
conn.version = typeof msg.version === 'string' ? msg.version : null;
|
|
277
|
+
conn.name = typeof msg.name === 'string' ? msg.name : null;
|
|
278
|
+
advarOmKonflikt(conn);
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
|
|
104
282
|
if (msg.type === 'terminate') {
|
|
283
|
+
// terminate lukker serveren for ALLE chats paa porten, saa den har to gates.
|
|
284
|
+
//
|
|
285
|
+
// Om id-sammenligningen: ORIGIN er autoriteten — Chrome saetter den, og
|
|
286
|
+
// afsenderen kan ikke forfalske den. Haandtrykkets id er kun et ekstra
|
|
287
|
+
// signal. MAALT 23/8 mod den AEGTE udvidelse: den sender hello med id null,
|
|
288
|
+
// fordi chrome.runtime.getManifest() kan fejle i offscreen-dokumentet.
|
|
289
|
+
// Kraevede vi lighed ubetinget, kunne en HELT legitim udvidelse aldrig lukke
|
|
290
|
+
// sin session ned — en fejl jeg selv indfoerte samme aften. Derfor: et id der
|
|
291
|
+
// MANGLER er fint (Origin har allerede bevist hvem det er), mens et id der er
|
|
292
|
+
// TIL STEDE og peger et ANDET sted afvises.
|
|
293
|
+
//
|
|
294
|
+
// 1) Afsenderen skal have sendt et hello der stemmer med sin egen Origin.
|
|
295
|
+
// MAALT 23/8: uden den kunne en forbindelse der lige havde vundet rollen
|
|
296
|
+
// som aktiv slukke browser-adgangen med én besked.
|
|
297
|
+
// 2) Afsenderen skal VAERE den aktive. Uden den kunne en gammel sidelaebende
|
|
298
|
+
// kopi, der lukkede sin sidste fane, rive serveren vaek under den
|
|
299
|
+
// udvidelse der reelt loeste opgaven.
|
|
300
|
+
if (!conn.harHilst || (conn.helloId && conn.helloId !== conn.extensionId)) {
|
|
301
|
+
process.stderr.write('[MCP] terminate ignoreret — intet gyldigt haandtryk\n');
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
if (activeConnection() !== conn) {
|
|
305
|
+
process.stderr.write('[MCP] terminate ignoreret — kom fra en inaktiv udvidelses-forbindelse\n');
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
105
308
|
gracefulShutdown('Terminate signal from extension (last tab closed)');
|
|
106
309
|
return;
|
|
107
310
|
}
|
|
@@ -115,11 +318,41 @@ function createWSS(port = BASE_PORT) {
|
|
|
115
318
|
else p.resolve(result);
|
|
116
319
|
});
|
|
117
320
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
321
|
+
// ── En doed socket skal AFVISE de kald der var undervejs (MAALT 23/8) ──────
|
|
322
|
+
// 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 — og med den FORKERTE
|
|
324
|
+
// forklaring, "kommandoen tog for lang tid". For extract_list er timeouten 180
|
|
325
|
+
// sekunder, altsaa tre minutters tavshed hvor sandheden var kendt med det samme.
|
|
326
|
+
const afvisVentende = (grund) => {
|
|
327
|
+
// Kun naar ingen anden levende forbindelse kan svare — ellers ville et helt
|
|
328
|
+
// normalt skift mellem to udvidelser afbryde kald der er fuldt i orden.
|
|
329
|
+
if (!pending.size || liveConnections().length) return;
|
|
330
|
+
const antal = pending.size;
|
|
331
|
+
for (const [id, p] of pending) {
|
|
332
|
+
clearTimeout(p.timer);
|
|
333
|
+
pending.delete(id);
|
|
334
|
+
p.reject(new Error(
|
|
335
|
+
`Forbindelsen til Chrome-udvidelsen forsvandt mens kommandoen koerte (${grund}). ` +
|
|
336
|
+
'Kommandoen naaede maaske at blive udfoert i browseren — tjek tilstanden foer du ' +
|
|
337
|
+
'proever igen. Er udvidelsen slaaet fra eller Chrome lukket, saa start den og proev forfra.',
|
|
338
|
+
));
|
|
122
339
|
}
|
|
340
|
+
process.stderr.write(`[MCP] ${antal} ventende kald afvist — ${grund}\n`);
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
// MAALT 23/8: der fandtes INGEN error-handler. Et ugyldigt WebSocket-frame (fx
|
|
344
|
+
// RSV1 sat) faar 'ws' til at emitte 'error' paa socketen, og en uhaandteret
|
|
345
|
+
// 'error' paa en EventEmitter kaster og draeber hele processen — altsaa alle
|
|
346
|
+
// chats paa den port. Tre linjer lukker det.
|
|
347
|
+
ws.on('error', (e) => {
|
|
348
|
+
process.stderr.write(`[MCP] WebSocket-fejl paa forbindelsen: ${e?.message || e}\n`);
|
|
349
|
+
try { ws.close(); } catch {}
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
ws.on('close', () => {
|
|
353
|
+
connections.delete(conn);
|
|
354
|
+
afvisVentende('udvidelsen koblede fra');
|
|
355
|
+
process.stderr.write(`[MCP] Chrome extension disconnected (${liveConnections().length} tilbage)\n`);
|
|
123
356
|
});
|
|
124
357
|
});
|
|
125
358
|
|
|
@@ -130,9 +363,7 @@ function createWSS(port = BASE_PORT) {
|
|
|
130
363
|
|
|
131
364
|
// Heartbeat + idle timeout (4 hours) — hoisted to module scope so gracefulShutdown can clear it
|
|
132
365
|
heartbeat = setInterval(() => {
|
|
133
|
-
|
|
134
|
-
extensionSocket.ping();
|
|
135
|
-
}
|
|
366
|
+
for (const c of liveConnections()) c.ws.ping();
|
|
136
367
|
if (Date.now() - lastActivity > 4 * 60 * 60 * 1000) {
|
|
137
368
|
gracefulShutdown('Idle timeout (4h)');
|
|
138
369
|
}
|
|
@@ -145,12 +376,35 @@ createWSS();
|
|
|
145
376
|
|
|
146
377
|
async function sendToExtension(method, params = {}, timeoutMs = 30000, _retries = 5) {
|
|
147
378
|
// Retry if extension is temporarily disconnected (reconnects every 2s)
|
|
148
|
-
|
|
379
|
+
const conn = activeConnection();
|
|
380
|
+
if (!conn) {
|
|
149
381
|
if (_retries > 0) {
|
|
150
382
|
await new Promise(r => setTimeout(r, 1500));
|
|
151
383
|
return sendToExtension(method, params, timeoutMs, _retries - 1);
|
|
152
384
|
}
|
|
153
|
-
|
|
385
|
+
// This is the other half of the two-part setup: the server is clearly running (it is
|
|
386
|
+
// throwing this), so what is missing is the extension, Chrome itself, or the connection
|
|
387
|
+
// between them. Say which, and where to get it — the agent relays this text to the user.
|
|
388
|
+
if (alleePorteOptaget) {
|
|
389
|
+
throw new Error(
|
|
390
|
+
`Alle porte ${BASE_PORT}-${MAX_PORT} er optaget, saa denne server fik aldrig en port. ` +
|
|
391
|
+
'Det er IKKE et problem med Chrome eller udvidelsen — de virker fint.\n' +
|
|
392
|
+
`Du har ${MAX_PORT - BASE_PORT + 1} chats i gang der bruger browseren samtidig. ` +
|
|
393
|
+
'Luk en af dem — pladsen frigives inden for faa sekunder — og genstart saa denne chat.\n' +
|
|
394
|
+
'Sig praecis dét til brugeren. Sig IKKE at udvidelsen mangler.',
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
throw new Error(
|
|
398
|
+
'Chrome extension not connected after 5 retries.\n' +
|
|
399
|
+
'Browser MCP needs BOTH halves: this MCP server (running) and the Agent360 Browser MCP ' +
|
|
400
|
+
'Chrome extension (apparently not reachable).\n' +
|
|
401
|
+
'Check, in order:\n' +
|
|
402
|
+
' 1. Chrome is actually open and running.\n' +
|
|
403
|
+
' 2. The extension is installed and enabled at chrome://extensions — install it from\n' +
|
|
404
|
+
' https://chromewebstore.google.com/detail/agent360-browser-mcp/jdehgalffmffhfhmmhaokfbfnafnmgcl\n' +
|
|
405
|
+
' 3. Click the extension icon -> Reconnect, and wait 2-3 seconds.\n' +
|
|
406
|
+
'Still stuck: https://browsermcp.dev/docs/troubleshooting/'
|
|
407
|
+
);
|
|
154
408
|
}
|
|
155
409
|
return new Promise((resolve, reject) => {
|
|
156
410
|
const id = ++cmdId;
|
|
@@ -159,7 +413,10 @@ async function sendToExtension(method, params = {}, timeoutMs = 30000, _retries
|
|
|
159
413
|
reject(new Error(`Command timed out after ${timeoutMs}ms: ${method}`));
|
|
160
414
|
}, timeoutMs);
|
|
161
415
|
pending.set(id, { resolve, reject, timer });
|
|
162
|
-
|
|
416
|
+
// pid = Claude Code-processen der ejer denne server. Udvidelsen bruger den til at
|
|
417
|
+
// skelne 'samme chat, ny forbindelse' fra 'en anden chat' naar den adopterer sessioner.
|
|
418
|
+
harSendtKommando = true;
|
|
419
|
+
conn.ws.send(JSON.stringify({ id, method, params, pid: process.ppid }));
|
|
163
420
|
});
|
|
164
421
|
}
|
|
165
422
|
|
|
@@ -244,13 +501,27 @@ If the extension files were updated, ask the user to reload it:
|
|
|
244
501
|
"The Browser MCP extension was updated. Please go to chrome://extensions, find 'Agent360 Browser MCP', and click the reload icon (🔄) to apply the update."
|
|
245
502
|
You cannot navigate to chrome:// pages — the user must do this manually.
|
|
246
503
|
|
|
504
|
+
## When Browser MCP itself is the obstacle — call browser_provide_feedback
|
|
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 — this server against the latest on npm, the extension against this server, and whether more than one Browser MCP extension is connected at once — because those three explain most "it's broken" moments, and none of them are visible to you otherwise. Read the verdict:
|
|
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 — 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
|
+
- **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
|
+
- **current** → the install is fine, so this is a genuine gap. Offer the returned submit_url as a clickable link.
|
|
509
|
+
- **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
|
+
Call it once per distinct obstacle, not once per retry.
|
|
511
|
+
|
|
247
512
|
## Sharing wishes / use-cases / bugs
|
|
248
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 — 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.`;
|
|
249
514
|
|
|
515
|
+
// 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 —
|
|
517
|
+
// og hele blokken ovenfor naaede aldrig frem til nogen klient. Verificeret ved at
|
|
518
|
+
// laese initialize-svaret: det havde kun protocolVersion, capabilities og serverInfo.
|
|
519
|
+
// Det betyder at "luk altid faner naar du er faerdig", CAPTCHA-fremgangsmaaden,
|
|
520
|
+
// tekst-selektor-vejledningen og resten aldrig har styret nogen agent. Nu ligger
|
|
521
|
+
// instructions i SAMME options-objekt som capabilities, hvor SDK'en laeser den.
|
|
250
522
|
const mcpServer = new Server(
|
|
251
523
|
{ name: 'agent360-browser', version: PKG_VERSION },
|
|
252
|
-
{ capabilities: { tools: {} } },
|
|
253
|
-
{ instructions: INSTRUCTIONS },
|
|
524
|
+
{ capabilities: { tools: {} }, instructions: INSTRUCTIONS },
|
|
254
525
|
);
|
|
255
526
|
|
|
256
527
|
mcpServer.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
@@ -295,12 +566,21 @@ mcpServer.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
295
566
|
browser_dismiss_overlays: 'dismiss_overlays',
|
|
296
567
|
browser_set_combobox: 'set_combobox',
|
|
297
568
|
browser_drop_file: 'drop_file',
|
|
569
|
+
browser_double_click: 'double_click',
|
|
570
|
+
browser_right_click: 'right_click',
|
|
571
|
+
browser_click_xy: 'click_xy',
|
|
572
|
+
browser_reattach_debugger: 'reattach_debugger',
|
|
573
|
+
browser_extract_list: 'extract_list',
|
|
298
574
|
};
|
|
299
575
|
|
|
300
576
|
if (name === 'browser_about') {
|
|
301
577
|
return handleAbout(args);
|
|
302
578
|
}
|
|
303
579
|
|
|
580
|
+
if (name === 'browser_provide_feedback') {
|
|
581
|
+
return await handleProvideFeedback(args);
|
|
582
|
+
}
|
|
583
|
+
|
|
304
584
|
if (name === 'browser_extract_token') {
|
|
305
585
|
return await handleExtractToken(args);
|
|
306
586
|
}
|
|
@@ -310,8 +590,11 @@ mcpServer.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
310
590
|
return { content: [{ type: 'text', text: `Unknown tool: ${name}` }], isError: true };
|
|
311
591
|
}
|
|
312
592
|
|
|
593
|
+
// extract_list scrolls a container in a loop (up to 300 rounds × wait_ms), so the 30 s
|
|
594
|
+
// default would kill a long mail list mid-walk and report a partial set as complete.
|
|
313
595
|
const timeout = method === 'ask_user' ? (args?.timeout || 120000) + 5000 :
|
|
314
|
-
method === 'solve_captcha' ? 60000 :
|
|
596
|
+
method === 'solve_captcha' ? 60000 :
|
|
597
|
+
method === 'extract_list' ? 180000 : 30000;
|
|
315
598
|
const result = await sendToExtension(method, args || {}, timeout);
|
|
316
599
|
|
|
317
600
|
if (name === 'browser_screenshot' && result?.image) {
|
|
@@ -321,7 +604,18 @@ mcpServer.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
321
604
|
const base64 = result.image.replace(prefix, '');
|
|
322
605
|
|
|
323
606
|
if (args && args.path) {
|
|
324
|
-
|
|
607
|
+
// MAALT 23/8: ingen indeslutning. En sti med ../../.. skrev til
|
|
608
|
+
// /private/tmp/a360-udenfor/x.png. Argumenterne kommer fra en model der laeser
|
|
609
|
+
// FREMMEDE websider, saa en prompt-injektion paa en vilkaarlig side kunne
|
|
610
|
+
// overskrive en fil i brugerens hjemmemappe med PNG-bytes.
|
|
611
|
+
const rod = resolve(process.cwd());
|
|
612
|
+
const targetPath = resolve(rod, args.path);
|
|
613
|
+
if (targetPath !== rod && !targetPath.startsWith(rod + sep)) {
|
|
614
|
+
throw new Error(
|
|
615
|
+
`path skal ligge inden for arbejdsmappen (${rod}). ` +
|
|
616
|
+
`"${args.path}" peger udenfor. Brug en relativ sti uden ../.`,
|
|
617
|
+
);
|
|
618
|
+
}
|
|
325
619
|
mkdirSync(dirname(targetPath), { recursive: true });
|
|
326
620
|
writeFileSync(targetPath, Buffer.from(base64, 'base64'));
|
|
327
621
|
return {
|
|
@@ -351,12 +645,45 @@ mcpServer.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
351
645
|
return response;
|
|
352
646
|
} catch (err) {
|
|
353
647
|
return {
|
|
354
|
-
content: [{ type: 'text', text:
|
|
648
|
+
content: [{ type: 'text', text: forklarSkaevhed(err.message) }],
|
|
355
649
|
isError: true,
|
|
356
650
|
};
|
|
357
651
|
}
|
|
358
652
|
});
|
|
359
653
|
|
|
654
|
+
// Naar udvidelsen er aeldre end serveren, svarer den `Unknown method: X` — og det er
|
|
655
|
+
// alt brugeren ser. Det sker GARANTERET: serveren kommer fra npm og opdateres straks,
|
|
656
|
+
// mens udvidelsen skal gennem Chrome Web Stores review paa 1-3 dage. I det vindue
|
|
657
|
+
// findes otte vaerktoejer i serveren som en 1.25.0-udvidelse ikke kender
|
|
658
|
+
// (click_xy, double_click, right_click, extract_list, reattach_debugger og de tre
|
|
659
|
+
// udklipsholder-vaerktoejer, alle fra v1.26.0).
|
|
660
|
+
//
|
|
661
|
+
// Serveren VED at udvidelsen er gammel: den sendte intet haandtryk. Saa i stedet for
|
|
662
|
+
// en gaadefuld fejl faar brugeren at vide hvorfor — og hvad de kan goere imens.
|
|
663
|
+
const ERSTATNINGER = {
|
|
664
|
+
double_click: 'kald `browser_click` to gange',
|
|
665
|
+
right_click: 'brug `browser_execute_script` med et contextmenu-event',
|
|
666
|
+
click_xy: 'brug `browser_click` med en selector',
|
|
667
|
+
extract_list: 'brug `browser_get_page_content` og scroll med `browser_scroll`',
|
|
668
|
+
reattach_debugger: 'genindlaes udvidelsen paa chrome://extensions',
|
|
669
|
+
};
|
|
670
|
+
|
|
671
|
+
function forklarSkaevhed(besked) {
|
|
672
|
+
const m = /Unknown method: ([a-z_]+)/.exec(besked || '');
|
|
673
|
+
if (!m) return `Error: ${besked}`;
|
|
674
|
+
const aktiv = activeConnection();
|
|
675
|
+
// Kun hvis udvidelsen faktisk er for gammel. Er den ny og metoden alligevel ukendt,
|
|
676
|
+
// er det en aegte fejl og skal ikke bortforklares.
|
|
677
|
+
if (aktiv && aktiv.version) return `Error: ${besked}`;
|
|
678
|
+
const alt = ERSTATNINGER[m[1]];
|
|
679
|
+
return `Error: browser_${m[1]} findes i denne server, men ikke i din Chrome-udvidelse.\n\n` +
|
|
680
|
+
'Udvidelsen opdateres via Chrome Web Store og kan vaere 1-3 dage bagud efter en ' +
|
|
681
|
+
'udgivelse — serveren opdateres med det samme via npm. Alt andet virker imens.\n' +
|
|
682
|
+
(alt ? `\nIndtil da: ${alt}.\n` : '') +
|
|
683
|
+
'\nTjek om en opdatering venter: chrome://extensions → Agent360 Browser MCP. ' +
|
|
684
|
+
'Er den indlaest som "unpacked", saa koer `npx @agent360/browser-mcp install`.';
|
|
685
|
+
}
|
|
686
|
+
|
|
360
687
|
const REPO_URL = 'https://github.com/Agent360dk/browser-mcp';
|
|
361
688
|
const ISSUE_TEMPLATES = { wish: 'wish.yml', use_case: 'use-case.yml', bug: 'bug.yml' };
|
|
362
689
|
|
|
@@ -396,6 +723,230 @@ function handleAbout(args) {
|
|
|
396
723
|
};
|
|
397
724
|
}
|
|
398
725
|
|
|
726
|
+
// ── Selv-diagnose: er installationen overhovedet frisk? ─────────────────────
|
|
727
|
+
// Baggrund (21/8): den udgave der koerte lokalt var npm 1.25.0, mens rettelserne
|
|
728
|
+
// laa uudgivet i repoet. Fejlen viste sig som "sessioner opfoerer sig underligt",
|
|
729
|
+
// ikke som "du koerer en gammel version" — og der fandtes ingen maade at spoerge
|
|
730
|
+
// paa. Derfor spoerger vaerktoejet selv, foer det konkluderer noget som helst.
|
|
731
|
+
|
|
732
|
+
// Friskheds-tjek mod npm. SLUKKET SOM STANDARD siden 22/8.
|
|
733
|
+
//
|
|
734
|
+
// Produktet lover paa forsiden "nothing leaves your machine ... no telemetry", og et
|
|
735
|
+
// opslag i npm-registret ER et kald ud af maskinen — ogsaa selv om det kun sender et
|
|
736
|
+
// pakkenavn og ingen brugerdata. Loeftet vejer tungere end bekvemmeligheden, saa
|
|
737
|
+
// tjekket er nu opt-in: saet BROWSER_MCP_CHECK_NPM=1.
|
|
738
|
+
//
|
|
739
|
+
// Alt det der betyder mest er ren LOKAL maaling og koerer altid: er udvidelsen aeldre
|
|
740
|
+
// end serveren, og er der flere udvidelser forbundet paa én gang.
|
|
741
|
+
const TJEK_NPM = process.env.BROWSER_MCP_CHECK_NPM === '1';
|
|
742
|
+
let npmLatestCache = null; // { version, at }
|
|
743
|
+
const NPM_LATEST_TTL_MS = 10 * 60 * 1000;
|
|
744
|
+
|
|
745
|
+
function npmLatestVersion() {
|
|
746
|
+
if (!TJEK_NPM) return Promise.resolve(null);
|
|
747
|
+
if (npmLatestCache && Date.now() - npmLatestCache.at < NPM_LATEST_TTL_MS) {
|
|
748
|
+
return Promise.resolve(npmLatestCache.version);
|
|
749
|
+
}
|
|
750
|
+
return new Promise((resolve) => {
|
|
751
|
+
// Offline, bag proxy, eller npm mangler paa PATH: svar null i stedet for at fejle.
|
|
752
|
+
// En friskheds-kontrol maa aldrig vaere det der braekker vaerktoejet.
|
|
753
|
+
execFile('npm', ['view', '@agent360/browser-mcp', 'version'], { timeout: 6000 }, (err, stdout) => {
|
|
754
|
+
if (err) return resolve(null);
|
|
755
|
+
const v = String(stdout).trim();
|
|
756
|
+
const ok = /^\d+\.\d+\.\d+/.test(v) ? v : null;
|
|
757
|
+
if (ok) npmLatestCache = { version: ok, at: Date.now() };
|
|
758
|
+
resolve(ok);
|
|
759
|
+
});
|
|
760
|
+
});
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
// ── Lokal logbog over hver graense agenten render ind i ─────────────────────
|
|
764
|
+
//
|
|
765
|
+
// Formaalet er loekken: hver gang et vaerktoej spaerrer vejen, skal det kunne taelles og
|
|
766
|
+
// rettes. Logbogen ligger LOKALT og forlader ikke maskinen.
|
|
767
|
+
//
|
|
768
|
+
// Bevidst ikke auto-indsendelse til et offentligt GitHub-issue: rapporten baerer URL og
|
|
769
|
+
// fejltekst fra den side agenten stod paa — og det er ofte en annoncekonto, en indbakke
|
|
770
|
+
// eller et kundesystem. Et offentligt issue kan ikke tages tilbage.
|
|
771
|
+
const FEEDBACK_LOG = join(homedir(), '.browser-mcp', 'feedback.jsonl');
|
|
772
|
+
const setteFingeraftryk = new Set(); // samme graense logges én gang pr. serverliv
|
|
773
|
+
|
|
774
|
+
function fingeraftryk(kind, tool, what) {
|
|
775
|
+
// Tal, id'er og lange hex-strenge varierer fra gang til gang og maa ikke goere to ens
|
|
776
|
+
// haendelser forskellige.
|
|
777
|
+
const kerne = String(what).toLowerCase()
|
|
778
|
+
.replace(/\b[0-9a-f]{8,}\b/g, '#')
|
|
779
|
+
.replace(/\d+/g, '#')
|
|
780
|
+
.slice(0, 160);
|
|
781
|
+
return `${kind}|${tool || '-'}|${kerne}`;
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
// URL'en reduceres til oprindelse + sti. Query og fragment baerer tokens, sessions-id'er
|
|
785
|
+
// og soegetermer — de har intet at goere i en logbog nogen senere kopierer ind i et issue.
|
|
786
|
+
function afkortUrl(u) {
|
|
787
|
+
if (!u) return null;
|
|
788
|
+
try { const x = new URL(u); return x.origin + x.pathname; } catch { return '(ulaeselig url)'; }
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
function skrivTilLogbog(post) {
|
|
792
|
+
const fp = fingeraftryk(post.kind, post.tool, post.what_happened);
|
|
793
|
+
const foerste = !setteFingeraftryk.has(fp);
|
|
794
|
+
setteFingeraftryk.add(fp);
|
|
795
|
+
if (!foerste) return { logged: false, reason: 'allerede logget i denne session', fingerprint: fp };
|
|
796
|
+
try {
|
|
797
|
+
mkdirSync(dirname(FEEDBACK_LOG), { recursive: true });
|
|
798
|
+
appendFileSync(FEEDBACK_LOG, JSON.stringify({ ...post, fingerprint: fp }) + '\n');
|
|
799
|
+
return { logged: true, path: FEEDBACK_LOG, fingerprint: fp };
|
|
800
|
+
} catch (e) {
|
|
801
|
+
// En logbog der ikke kan skrives maa aldrig vaere det der braekker vaerktoejet.
|
|
802
|
+
return { logged: false, reason: e.message, fingerprint: fp };
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
async function handleProvideFeedback(args) {
|
|
807
|
+
const what = String(args?.what_happened || '').trim();
|
|
808
|
+
const kind = args?.kind || 'blocked';
|
|
809
|
+
const tool = args?.tool || null;
|
|
810
|
+
const url = args?.url || null;
|
|
811
|
+
const attempted = args?.attempted || null;
|
|
812
|
+
|
|
813
|
+
const npmLatest = await npmLatestVersion();
|
|
814
|
+
const exts = distinctExtensions();
|
|
815
|
+
const active = activeConnection();
|
|
816
|
+
|
|
817
|
+
const serverOutdated = npmLatest ? cmpVersion(npmLatest, PKG_VERSION) > 0 : null;
|
|
818
|
+
// Udvidelse og server udgives sammen under samme versionsnummer, saa en
|
|
819
|
+
// udvidelse der er AELDRE end serveren mangler per definition rettelser.
|
|
820
|
+
const extVersion = active ? active.version : null;
|
|
821
|
+
const extOutdated = active
|
|
822
|
+
? (extVersion === null ? true : cmpVersion(PKG_VERSION, extVersion) > 0)
|
|
823
|
+
: null;
|
|
824
|
+
|
|
825
|
+
const findings = [];
|
|
826
|
+
const fix_steps = [];
|
|
827
|
+
|
|
828
|
+
if (exts.length > 1) {
|
|
829
|
+
const kanVaelge = exts.some(c => c.version);
|
|
830
|
+
findings.push(
|
|
831
|
+
`${exts.length} Browser MCP-udvidelser er indlaest i Chrome og forbundet til denne server samtidig ` +
|
|
832
|
+
`(${exts.map(c => `${c.extensionId || 'ukendt id'}${c.version ? ' v' + c.version : ' (oplyser ikke version)'}`).join(' + ')}). ` +
|
|
833
|
+
'Hver af dem foerer sit eget sessions-kort og sine egne fane-grupper i den samme browser, ' +
|
|
834
|
+
'saa faner kan se ud til at forsvinde og sessioner til at smelte sammen. ' +
|
|
835
|
+
(kanVaelge
|
|
836
|
+
? `Denne server sender kun til den nyeste (${active?.extensionId}).`
|
|
837
|
+
: `Ingen af dem oplyser sin version, saa hvilken der styres (${active?.extensionId}) er vilkaarligt og kan skifte mellem sessioner.`),
|
|
838
|
+
);
|
|
839
|
+
fix_steps.push(
|
|
840
|
+
'Aabn chrome://extensions og slaa alle Browser MCP-udvidelser fra paa naer én. ' +
|
|
841
|
+
'Det skal brugeren selv goere — chrome:// kan ikke styres herfra. Behold den nyeste.',
|
|
842
|
+
);
|
|
843
|
+
}
|
|
844
|
+
if (!active) {
|
|
845
|
+
findings.push('Ingen Chrome-udvidelse er forbundet til denne MCP-server lige nu.');
|
|
846
|
+
fix_steps.push('Tjek at Chrome koerer og at udvidelsen er slaaet til paa chrome://extensions, klik derefter paa ikonet → Reconnect.');
|
|
847
|
+
}
|
|
848
|
+
if (serverOutdated) {
|
|
849
|
+
findings.push(`MCP-serveren koerer v${PKG_VERSION}, men npm har v${npmLatest}. Fejlen kan allerede vaere rettet.`);
|
|
850
|
+
fix_steps.push(`Genstart klienten — den henter selv @agent360/browser-mcp@latest (v${npmLatest}).`);
|
|
851
|
+
}
|
|
852
|
+
if (extOutdated) {
|
|
853
|
+
findings.push(
|
|
854
|
+
extVersion === null
|
|
855
|
+
? `Den forbundne udvidelse er saa gammel at den ikke oplyser sin version (foer v${PKG_VERSION}). Den mangler alt hvad der er rettet siden.`
|
|
856
|
+
: `Udvidelsen er v${extVersion}, serveren er v${PKG_VERSION}. Udvidelsen mangler rettelser fra de mellemliggende udgaver.`,
|
|
857
|
+
);
|
|
858
|
+
// 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 — der er ingen nyere
|
|
860
|
+
// version at hente endnu, saa raadet foerer i ring. Serveren kan ikke se hvilken
|
|
861
|
+
// slags installation det er (den gamle udvidelse oplyser intet), saa begge tilfaelde
|
|
862
|
+
// skal staa der — og det skal siges at ventetiden er forventet, ikke en fejl.
|
|
863
|
+
fix_steps.push(
|
|
864
|
+
extVersion === null
|
|
865
|
+
? '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 — ' +
|
|
867
|
+
'det er forventet og gaar over af sig selv. Alt andet virker imens. ' +
|
|
868
|
+
'Er den indlaest som "unpacked": koer `npx @agent360/browser-mcp install` og derefter ' +
|
|
869
|
+
'chrome://extensions → Agent360 Browser MCP → ↻ reload.'
|
|
870
|
+
: 'Opdatér udvidelsen: chrome://extensions → Agent360 Browser MCP → ↻ reload. ' +
|
|
871
|
+
'Er den indlaest som "unpacked", saa koer `npx @agent360/browser-mcp install` foerst.',
|
|
872
|
+
);
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
const verdict =
|
|
876
|
+
exts.length > 1 ? 'conflict'
|
|
877
|
+
: (serverOutdated || extOutdated) ? 'outdated'
|
|
878
|
+
: !active ? 'disconnected'
|
|
879
|
+
: (npmLatest === null ? 'unknown' : 'current');
|
|
880
|
+
|
|
881
|
+
const environment = {
|
|
882
|
+
mcp_server_version: PKG_VERSION,
|
|
883
|
+
npm_latest_version: npmLatest,
|
|
884
|
+
server_up_to_date: serverOutdated === null ? null : !serverOutdated,
|
|
885
|
+
extensions_connected: exts.map(c => ({
|
|
886
|
+
name: c.name,
|
|
887
|
+
version: c.version,
|
|
888
|
+
extension_id: c.extensionId,
|
|
889
|
+
active: c === active,
|
|
890
|
+
})),
|
|
891
|
+
extension_up_to_date: extOutdated === null ? null : !extOutdated,
|
|
892
|
+
ws_port: activePort,
|
|
893
|
+
node: process.version,
|
|
894
|
+
platform: `${process.platform} ${process.arch}`,
|
|
895
|
+
};
|
|
896
|
+
|
|
897
|
+
const issueBody = [
|
|
898
|
+
what && `**What happened**\n${what}`,
|
|
899
|
+
tool && `\n**Tool**: \`${tool}\``,
|
|
900
|
+
// MAALT 22/8 ved sikkerhedsreview: her stod den RAA url, mens den lokale logbog
|
|
901
|
+
// nedenfor bruger afkortUrl(). Query-strengen — hvor tokens bor — blev altsaa
|
|
902
|
+
// strippet fra filen paa disken, men sendt uredigeret ind i et link til et
|
|
903
|
+
// OFFENTLIGT GitHub-issue. Praecis den forkerte vej rundt.
|
|
904
|
+
url && `\n**URL**: ${afkortUrl(url)}`,
|
|
905
|
+
attempted && `\n**Already tried**\n${attempted}`,
|
|
906
|
+
`\n**Environment**\n\`\`\`json\n${JSON.stringify(environment, null, 2)}\n\`\`\``,
|
|
907
|
+
].filter(Boolean).join('\n');
|
|
908
|
+
|
|
909
|
+
const template = kind === 'wish' ? ISSUE_TEMPLATES.wish
|
|
910
|
+
: kind === 'use_case' ? ISSUE_TEMPLATES.use_case
|
|
911
|
+
: ISSUE_TEMPLATES.bug;
|
|
912
|
+
const issueTitle = what.split('\n')[0].slice(0, 90) || 'Browser MCP feedback';
|
|
913
|
+
const submit_url = `${REPO_URL}/issues/new?template=${template}` +
|
|
914
|
+
`&title=${encodeURIComponent(issueTitle)}&body=${encodeURIComponent(issueBody)}`;
|
|
915
|
+
|
|
916
|
+
const instruction =
|
|
917
|
+
verdict === 'conflict' || verdict === 'outdated' || verdict === 'disconnected'
|
|
918
|
+
? '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 — det er sandsynligvis installationen, ikke en fejl i Browser MCP.'
|
|
920
|
+
: '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 — du kan rette i den foer du sender"). Spoerg ikke om lov foerst.';
|
|
922
|
+
|
|
923
|
+
const logbog = skrivTilLogbog({
|
|
924
|
+
at: new Date().toISOString(),
|
|
925
|
+
kind, tool, what_happened: what, attempted,
|
|
926
|
+
url: afkortUrl(url),
|
|
927
|
+
verdict,
|
|
928
|
+
server_version: PKG_VERSION,
|
|
929
|
+
extension_version: extVersion,
|
|
930
|
+
extensions_connected: exts.length,
|
|
931
|
+
});
|
|
932
|
+
|
|
933
|
+
return {
|
|
934
|
+
content: [{
|
|
935
|
+
type: 'text',
|
|
936
|
+
text: JSON.stringify({
|
|
937
|
+
reported: { kind, what_happened: what, tool, url, attempted },
|
|
938
|
+
verdict,
|
|
939
|
+
findings,
|
|
940
|
+
fix_steps,
|
|
941
|
+
environment,
|
|
942
|
+
logged_locally: logbog,
|
|
943
|
+
submit_url,
|
|
944
|
+
instruction,
|
|
945
|
+
}, null, 2),
|
|
946
|
+
}],
|
|
947
|
+
};
|
|
948
|
+
}
|
|
949
|
+
|
|
399
950
|
async function handleExtractToken(args) {
|
|
400
951
|
const { provider } = args;
|
|
401
952
|
const info = PROVIDER_PAGES[provider];
|
|
@@ -433,8 +984,8 @@ function gracefulShutdown(reason, code = 0) {
|
|
|
433
984
|
if (heartbeat) clearInterval(heartbeat);
|
|
434
985
|
|
|
435
986
|
// Close WS with explicit close-frame so extension's onclose handler fires
|
|
436
|
-
|
|
437
|
-
try {
|
|
987
|
+
for (const c of liveConnections()) {
|
|
988
|
+
try { c.ws.close(1000, 'mcp-shutdown'); } catch {}
|
|
438
989
|
}
|
|
439
990
|
if (wss) try { wss.close(); } catch {}
|
|
440
991
|
|
|
@@ -447,19 +998,100 @@ process.on('SIGINT', () => gracefulShutdown('SIGINT'));
|
|
|
447
998
|
process.on('exit', () => {
|
|
448
999
|
// Safety net for direct process.exit calls that bypass gracefulShutdown
|
|
449
1000
|
if (wss) try { wss.close(); } catch {}
|
|
450
|
-
|
|
1001
|
+
for (const c of connections) try { c.ws.close(); } catch {}
|
|
451
1002
|
});
|
|
452
1003
|
|
|
453
|
-
//
|
|
454
|
-
//
|
|
1004
|
+
// ── Naar lukker serveren sin port? (MAALT 22/8) ──────────────────────────────
|
|
1005
|
+
//
|
|
1006
|
+
// Symptomet: tyve porte optaget, og ingen chats i live bag dem. Aarsagen var at
|
|
1007
|
+
// tjekket kiggede paa den FORKERTE proces. Kaeden er nemlig ikke to led, men fire:
|
|
1008
|
+
//
|
|
1009
|
+
// Claude Code → wrapper → npm exec → denne server
|
|
1010
|
+
//
|
|
1011
|
+
// `process.ppid` er `npm exec`, ikke Claude Code. Doer chatten, kan `npm exec`
|
|
1012
|
+
// blive haengende som foraeldreloes — og saa ser tjekket en levende foraelder for
|
|
1013
|
+
// evigt. Porten blev holdt i op til fire timer (idle-graensen), og med flere
|
|
1014
|
+
// forladte chats loeb spaendet fuldt.
|
|
1015
|
+
//
|
|
1016
|
+
// Rettelsen er at tjekke HELE kaeden op til roden, ikke kun naermeste led. Doer et
|
|
1017
|
+
// vilkaarligt led, er forbindelsen til den chat der ejer os brudt, og saa er vi
|
|
1018
|
+
// foraeldreloese uanset om vores naermeste foraelder stadig aander.
|
|
1019
|
+
//
|
|
1020
|
+
// Kaeden hentes én gang ved opstart (én ps-kommando), og derefter koster tjekket
|
|
1021
|
+
// 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 — daarligere, men aldrig
|
|
1023
|
+
// vaerre end foer.
|
|
1024
|
+
|
|
1025
|
+
// ps-opslaget bor her (det er en sideeffekt); selve kaede-logikken og doeds-dommen
|
|
1026
|
+
// ligger i vagt.js, saa de kan koeres i en test uden at starte en server.
|
|
1027
|
+
function laesPpid(pid) {
|
|
1028
|
+
try {
|
|
1029
|
+
const ud = execSync(`ps -o ppid= -p ${pid}`, {
|
|
1030
|
+
encoding: 'utf8',
|
|
1031
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
1032
|
+
timeout: 2000, // et haengende ps ville ellers blokere hele opstarten synkront
|
|
1033
|
+
});
|
|
1034
|
+
return Number(String(ud).trim());
|
|
1035
|
+
} catch {
|
|
1036
|
+
return null;
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
|
|
455
1040
|
const parentPid = process.ppid;
|
|
1041
|
+
|
|
1042
|
+
// MAALT 22/8: doer den naermeste foraelder FOER serveren er bootet (~2 sek node+SDK),
|
|
1043
|
+
// laeser process.ppid vaerdien 1 — altsaa launchd. Kaeden blev saa [1], og pid 1 er
|
|
1044
|
+
// baade udoedelig og ejet af root, saa vagten var enten inert eller draebte os selv
|
|
1045
|
+
// paa EPERM. En kaede der kun bestaar af pid 1 vogter ingenting og skal ikke bruges.
|
|
1046
|
+
let vagtKaede = parentPid > 1 ? [parentPid] : [];
|
|
1047
|
+
try {
|
|
1048
|
+
const k = forfaedreKaede(parentPid, laesPpid).filter((x) => x > 1);
|
|
1049
|
+
if (k.length) vagtKaede = k;
|
|
1050
|
+
} catch {}
|
|
1051
|
+
if (!vagtKaede.length) {
|
|
1052
|
+
process.stderr.write('[MCP] ingen brugbar foraelder-kaede (ppid=' + parentPid +
|
|
1053
|
+
') — falder tilbage paa idle-graensen alene\n');
|
|
1054
|
+
}
|
|
1055
|
+
process.stderr.write(`[MCP] vagt-kaede: ${vagtKaede.join(' → ')}\n`);
|
|
1056
|
+
|
|
456
1057
|
parentCheck = setInterval(() => {
|
|
1058
|
+
const doede = vagtKaede.filter((pid) => ledErDoedt(pid));
|
|
1059
|
+
if (!doede.length) return;
|
|
1060
|
+
|
|
1061
|
+
// ── Et doedt led betyder ikke automatisk at chatten er vaek (MAALT 23/8) ──────
|
|
1062
|
+
//
|
|
1063
|
+
// Kaeden blev frosset ved opstart. Men et MELLEMLED kan afslutte helt normalt
|
|
1064
|
+
// mens ejeren koerer videre — maalt to gange paa denne maskine, hvor kaeden gaar
|
|
1065
|
+
// npm exec → wrapper → claude → Code Helper (Plugin) → Code. Et forbigaaende led
|
|
1066
|
+
// der lukkede pænt udloeste "chatten bag denne server er vaek", mens chatten var
|
|
1067
|
+
// uroert. Og risikoen er ensrettet vaerre end 1.25.0, som vogtede ét pid: nu er
|
|
1068
|
+
// hvert af 5-6 led en ny doedsaarsag.
|
|
1069
|
+
//
|
|
1070
|
+
// 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 — vi er bare blevet
|
|
1072
|
+
// reparented, og den nye kaede overtager. Kun naar den vej ogsaa er vaek, lukker vi.
|
|
1073
|
+
//
|
|
1074
|
+
// Prisen er nul i normal drift: genlaesningen koerer kun i det tik hvor et led er
|
|
1075
|
+
// forsvundet, ikke hvert 5. sekund.
|
|
1076
|
+
let frisk = [];
|
|
457
1077
|
try {
|
|
458
|
-
process.
|
|
459
|
-
} catch {
|
|
460
|
-
|
|
1078
|
+
frisk = forfaedreKaede(process.ppid, laesPpid).filter((x) => x > 1 && !ledErDoedt(x));
|
|
1079
|
+
} catch {}
|
|
1080
|
+
|
|
1081
|
+
if (frisk.length) {
|
|
1082
|
+
process.stderr.write(
|
|
1083
|
+
`[MCP] led ${doede.join(', ')} er vaek, men kaeden gaar stadig op: ` +
|
|
1084
|
+
`${frisk.join(' → ')} — fortsaetter\n`,
|
|
1085
|
+
);
|
|
1086
|
+
vagtKaede = frisk;
|
|
1087
|
+
return;
|
|
461
1088
|
}
|
|
462
|
-
|
|
1089
|
+
|
|
1090
|
+
gracefulShutdown(
|
|
1091
|
+
`Proces ${doede[0]} i kaeden doede, og der er ingen levende vej op — ` +
|
|
1092
|
+
'chatten bag denne server er vaek',
|
|
1093
|
+
);
|
|
1094
|
+
}, 5000); // hvert 5. sekund
|
|
463
1095
|
|
|
464
1096
|
// Also listen for stdin close as backup
|
|
465
1097
|
process.stdin.on('end', () => gracefulShutdown('stdin closed'));
|