@agent360/browser-mcp 1.25.0 → 1.29.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +95 -21
- package/bin/cli.js +102 -44
- package/extension/background.js +1033 -111
- 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 +830 -60
- 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,118 @@ 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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
41
|
+
|
|
42
|
+
// Spaendet kan flyttes med env — ellers ville en test af port-udsultning skulle
|
|
43
|
+
// beslaglaegge de RIGTIGE porte og dermed sulte brugerens oevrige chats imens.
|
|
44
|
+
// Uden env er vaerdierne uaendrede. (Samme moenster som BROWSER_MCP_EXTENSION_ID.)
|
|
45
|
+
const BASE_PORT = Number(process.env.BROWSER_MCP_BASE_PORT) || 9876;
|
|
46
|
+
const MAX_PORT = Number(process.env.BROWSER_MCP_MAX_PORT) || 9895; // 20 ports instead of 10 — zombies die within 5s via parent check
|
|
47
|
+
|
|
48
|
+
// ── Extension connections ───────────────────────────────────────────────────
|
|
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 — scanner BEGGE de samme porte
|
|
52
|
+
// og forbinder til hver eneste server. Maalt med lsof: 2 ESTABLISHED forbindelser
|
|
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 — og et
|
|
55
|
+
// `terminate` fra den forkerte kopi lukkede serveren ned under den rigtige.
|
|
56
|
+
//
|
|
57
|
+
// Nu holdes alle forbindelser med deres identitet, kommandoer sendes kun til den
|
|
58
|
+
// nyeste udgave, og konflikten kan ses (browser_provide_feedback) i stedet for at
|
|
59
|
+
// vise sig som faner der "forsvinder".
|
|
60
|
+
const connections = new Set(); // { ws, seq, extensionId, version, name, since }
|
|
61
|
+
let connSeq = 0;
|
|
62
|
+
let activePort = null;
|
|
63
|
+
let alleePorteOptaget = false; // hele spaendet i brug — se createWSS
|
|
64
|
+
let bindFejl = null; // bind fejlede af en ANDEN grund end optaget port
|
|
65
|
+
|
|
66
|
+
function cmpVersion(a, b) {
|
|
67
|
+
const pa = String(a || '0.0.0').split('.').map(n => parseInt(n, 10) || 0);
|
|
68
|
+
const pb = String(b || '0.0.0').split('.').map(n => parseInt(n, 10) || 0);
|
|
69
|
+
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
|
|
70
|
+
const d = (pa[i] || 0) - (pb[i] || 0);
|
|
71
|
+
if (d) return d > 0 ? 1 : -1;
|
|
50
72
|
}
|
|
51
|
-
|
|
52
|
-
process.stderr.write(`[MCP] Auto-update skipped: ${e.message?.split('\n')[0]}\n`);
|
|
73
|
+
return 0;
|
|
53
74
|
}
|
|
54
75
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
76
|
+
let sidsteKonfliktNoegle = '';
|
|
77
|
+
function advarOmKonflikt(conn) {
|
|
78
|
+
const alle = distinctExtensions();
|
|
79
|
+
if (alle.length < 2) return;
|
|
80
|
+
// Samme konflikt maa ikke skrige ved hver eneste hello — kun naar billedet aendrer sig.
|
|
81
|
+
const noegle = alle.map(c => `${c.extensionId || 'ukendt'}@${c.version || '?'}`).sort().join('|');
|
|
82
|
+
if (noegle === sidsteKonfliktNoegle) return;
|
|
83
|
+
sidsteKonfliktNoegle = noegle;
|
|
84
|
+
const aktiv = activeConnection();
|
|
85
|
+
// 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 — ellers laeser man
|
|
87
|
+
// "kommandoer sendes kun til X" som om X var det rigtige valg.
|
|
88
|
+
const kanVaelge = alle.some(c => c.version);
|
|
89
|
+
process.stderr.write(
|
|
90
|
+
`[MCP] ADVARSEL: ${alle.length} Browser MCP-udvidelser er forbundet til denne server samtidig ` +
|
|
91
|
+
`(${alle.map(c => `${c.extensionId || 'ukendt id'}${c.version ? ' v' + c.version : ''}`).join(', ')}). ` +
|
|
92
|
+
'De deler faner og sessions-tilstand, saa faner kan se ud til at forsvinde. ' +
|
|
93
|
+
(kanVaelge
|
|
94
|
+
? `Kommandoer sendes kun til den nyeste (${aktiv?.extensionId}). `
|
|
95
|
+
: `Ingen af dem oplyser sin version, saa valget (${aktiv?.extensionId}) er vilkaarligt og kan skifte. `) +
|
|
96
|
+
'Ret det ved at slaa alle paa naer én fra paa chrome://extensions.\n',
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function liveConnections() {
|
|
101
|
+
return [...connections].filter(c => c.ws.readyState === 1);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Valget LAASES for serverens levetid.
|
|
105
|
+
//
|
|
106
|
+
// MAALT 21/8 i flow-harnessen: uden laasen skiftede den aktive udvidelse MIDT i en
|
|
107
|
+
// 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
|
+
// men B kendte ikke A's fane og lavede en frisk about:blank. Alt derefter fejlede med
|
|
110
|
+
// "Cannot access contents of url about:blank". 21 af 43 vaerktoejer faldt paa det, og
|
|
111
|
+
// symptomet lignede praecis "faner forsvinder" og "kun én session virker".
|
|
112
|
+
//
|
|
113
|
+
// Faner hoerer til den udvidelse der aabnede dem. Skifter man udvidelse, strander de.
|
|
114
|
+
// Derfor: vaelg én gang, og bliv ved den saa laenge dens forbindelse lever. Doer den,
|
|
115
|
+
// vaelges der forfra — det er en aegte genopretning, ikke et vilkaarligt skift.
|
|
116
|
+
let laastForbindelse = null;
|
|
117
|
+
let harSendtKommando = false;
|
|
118
|
+
const PINNET_UDVIDELSE = (process.env.BROWSER_MCP_EXTENSION_ID || '').trim() || null;
|
|
119
|
+
|
|
120
|
+
function activeConnection() {
|
|
121
|
+
if (laastForbindelse && laastForbindelse.ws.readyState === 1) return laastForbindelse;
|
|
122
|
+
|
|
123
|
+
// 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 — haandtrykket kom
|
|
125
|
+
// med den nyere udgave.
|
|
126
|
+
let best = null;
|
|
127
|
+
for (const c of liveConnections()) {
|
|
128
|
+
if (!best) { best = c; continue; }
|
|
129
|
+
const d = cmpVersion(c.version, best.version);
|
|
130
|
+
if (d > 0 || (d === 0 && c.since > best.since)) best = c;
|
|
131
|
+
}
|
|
132
|
+
laastForbindelse = best;
|
|
133
|
+
return best;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Én post pr. udvidelse. En udvidelse uden identitet taelles for sig selv (dens
|
|
137
|
+
// socket er noeglen), saa to gamle kopier stadig ses som to.
|
|
138
|
+
function distinctExtensions() {
|
|
139
|
+
const byKey = new Map();
|
|
140
|
+
for (const c of liveConnections()) {
|
|
141
|
+
const key = c.extensionId || `legacy:${c.seq}`;
|
|
142
|
+
if (!byKey.has(key)) byKey.set(key, c);
|
|
143
|
+
}
|
|
144
|
+
return [...byKey.values()];
|
|
145
|
+
}
|
|
59
146
|
let wss = null; // Track WSS for graceful shutdown
|
|
60
147
|
let cmdId = 0;
|
|
61
148
|
let lastActivity = Date.now();
|
|
@@ -64,11 +151,46 @@ const pending = new Map();
|
|
|
64
151
|
// Timers hoisted to module scope so gracefulShutdown can clear them deterministically.
|
|
65
152
|
let heartbeat = null;
|
|
66
153
|
let parentCheck = null;
|
|
154
|
+
let tomgangsvagt = null;
|
|
155
|
+
|
|
156
|
+
// ── 4-timers-tomgangen bor paa modul-niveau, ikke i hjerteslaget ────────────
|
|
157
|
+
//
|
|
158
|
+
// FUNDET AF REVIEW 7/9. Tjekket laa INDE i `heartbeat`, som kun findes mens serveren
|
|
159
|
+
// har en port — og som `frigivPort` rydder. Da porten blev doven, betoed det at en
|
|
160
|
+
// proces der ALDRIG binder (en chat der ikke roerer browseren) heller aldrig faar
|
|
161
|
+
// sin tomgang tjekket. Det var den eneste vej hvor en browser-inaktiv server gav sine
|
|
162
|
+
// ~35 MB tilbage; med mange samtidige chats er det maalbart. Nu koerer vagten altid.
|
|
163
|
+
// Graensen staar som literal, ikke bag en konstant: `vagter.test.mjs` laeser tallet
|
|
164
|
+
// direkte ud af kilden for at haandhaeve at den aldrig bliver kort. En kort graense
|
|
165
|
+
// ville lade en aaben chat miste browseren permanent — vaerre end en port der staar
|
|
166
|
+
// optaget lidt for laenge.
|
|
167
|
+
tomgangsvagt = setInterval(() => {
|
|
168
|
+
if (Date.now() - lastActivity > 4 * 60 * 60 * 1000) gracefulShutdown('Idle timeout (4h)');
|
|
169
|
+
}, 60000);
|
|
67
170
|
|
|
68
171
|
// ── WebSocket Server ───────────────────────────────────────────────────────
|
|
69
172
|
|
|
70
173
|
function createWSS(port = BASE_PORT) {
|
|
71
|
-
const server = new WebSocketServer({
|
|
174
|
+
const server = new WebSocketServer({
|
|
175
|
+
host: '127.0.0.1',
|
|
176
|
+
port,
|
|
177
|
+
// Afvis allerede i HAANDTRYKKET, ikke efter. MAALT 23/8: lukkede vi foerst
|
|
178
|
+
// forbindelsen inde i 'connection', naaede den fremmede at faa en aaben socket
|
|
179
|
+
// (og et 101-svar) foer den blev smidt ud. Med verifyClient faar den 401 og
|
|
180
|
+
// ingen socket overhovedet.
|
|
181
|
+
//
|
|
182
|
+
// Chrome saetter ALTID Origin: chrome-extension://<32 tegn> paa en WebSocket fra
|
|
183
|
+
// en udvidelse — verificeret mod den koerende. Alt andet er per definition ikke
|
|
184
|
+
// en udvidelse, saa gaten koster aegte brugere ingenting.
|
|
185
|
+
verifyClient: ({ origin }, godkend) => {
|
|
186
|
+
if (/^chrome-extension:\/\/[a-p]{32}$/.test(origin || '')) return godkend(true);
|
|
187
|
+
process.stderr.write(
|
|
188
|
+
`[MCP] Afviser opkobling uden gyldig chrome-extension-Origin` +
|
|
189
|
+
`${origin ? ` (fik "${String(origin).slice(0, 60)}")` : ' (ingen Origin-header)'}\n`,
|
|
190
|
+
);
|
|
191
|
+
godkend(false, 401, 'only chrome extensions may connect');
|
|
192
|
+
},
|
|
193
|
+
});
|
|
72
194
|
wss = server;
|
|
73
195
|
|
|
74
196
|
server.on('error', (err) => {
|
|
@@ -77,16 +199,81 @@ function createWSS(port = BASE_PORT) {
|
|
|
77
199
|
process.stderr.write(`[MCP] Port ${port} in use, trying ${port + 1}...\n`);
|
|
78
200
|
createWSS(port + 1);
|
|
79
201
|
} else {
|
|
202
|
+
// MAALT 22/8: her stod KUN denne stderr-linje. Ingen laeser stderr fra en
|
|
203
|
+
// MCP-server, saa udtoemte porte var en helt tavs fejl. Hvert vaerktoejskald
|
|
204
|
+
// fejlede bagefter med "extension not connected" — en tekst der oven i koebet
|
|
205
|
+
// siger at serveren koerer og sender brugeren til Chrome Web Store. Begge dele
|
|
206
|
+
// er forkerte naar sandheden er at vi aldrig fik en port. Flaget laeses i
|
|
207
|
+
// sendToExtension, saa agenten kan give brugeren den rigtige forklaring.
|
|
208
|
+
alleePorteOptaget = true;
|
|
80
209
|
process.stderr.write(`[MCP] All ports ${BASE_PORT}-${MAX_PORT} in use. Cannot start.\n`);
|
|
210
|
+
loesPortLoefte(false);
|
|
81
211
|
}
|
|
82
212
|
} else {
|
|
213
|
+
// FUNDET AF REVIEW 7/9. Her stod KUN stderr-linjen. Foer porten blev doven, var
|
|
214
|
+
// det harmloest: en opstartsfejl man kunne se i loggen. Nu venter `sikrePort()`
|
|
215
|
+
// paa et loefte der aldrig blev indfriet — og `sendToExtension` goer
|
|
216
|
+
// `await sikrePort()` UDEN timeout. Enhver anden bind-fejl end EADDRINUSE
|
|
217
|
+
// (EACCES paa en privilegeret port, EADDRNOTAVAIL, en restriktiv firewall)
|
|
218
|
+
// ville derfor faa hvert eneste browser-kald til at haenge tavst for evigt.
|
|
219
|
+
// At haenge uden besked er vaerre end den fejl vi rettede.
|
|
220
|
+
bindFejl = err.message;
|
|
83
221
|
process.stderr.write(`[MCP] WebSocket error: ${err.message}\n`);
|
|
222
|
+
loesPortLoefte(false);
|
|
84
223
|
}
|
|
85
224
|
});
|
|
86
225
|
|
|
87
|
-
server.on('connection', (ws) => {
|
|
88
|
-
|
|
89
|
-
|
|
226
|
+
server.on('connection', (ws, req) => {
|
|
227
|
+
// WebSocket-handshaket fra en udvidelse baerer Origin: chrome-extension://<id>.
|
|
228
|
+
// Den identificerer afsenderen UDEN at udvidelsen behoever at kende haandtrykket,
|
|
229
|
+
// saa en konflikt mellem to indlaeste udvidelser kan opdages ogsaa naar begge er
|
|
230
|
+
// gamle udgaver — hvilket er praecis den situation konflikten opstaar i.
|
|
231
|
+
// Maalt 21/8: to distinkte origins ringede op til hver eneste server.
|
|
232
|
+
const origin = req?.headers?.origin || '';
|
|
233
|
+
const fraOrigin = /^chrome-extension:\/\/([a-p]{32})$/.exec(origin)?.[1] || null;
|
|
234
|
+
|
|
235
|
+
// ── Kun Chrome-udvidelser lukkes ind (MAALT 23/8) ─────────────────────────
|
|
236
|
+
//
|
|
237
|
+
// Reproduceret med en raa WebSocket-klient mod en aegte server: en klient der
|
|
238
|
+
// simpelthen UDELOD Origin-headeren blev accepteret, vandt rollen som aktiv
|
|
239
|
+
// udvidelse med `hello version 99.0.0`, fik `browser_get_cookies` leveret, og
|
|
240
|
+
// kunne lukke serveren med `terminate`. Alle tre trin lykkedes.
|
|
241
|
+
//
|
|
242
|
+
// Serveren lytter kun paa 127.0.0.1, saa angriberen skal koere lokalt — men det
|
|
243
|
+
// goer enhver anden app og ethvert npm-postinstall-script. Og hvad den kan er
|
|
244
|
+
// ikke smaating: laese alt agenten sender til browseren (kodeord fra ask_user,
|
|
245
|
+
// cookies, sidetekst), fodre agenten med opdigtet sideindhold, og slukke
|
|
246
|
+
// browser-adgangen i alle aabne chats.
|
|
247
|
+
//
|
|
248
|
+
// Chrome saetter ALTID `Origin: chrome-extension://<id>` paa en WebSocket fra en
|
|
249
|
+
// udvidelse — verificeret mod den koerende udvidelse. En manglende header er
|
|
250
|
+
// derfor ikke en aeldre udgave; det er noget andet end en udvidelse.
|
|
251
|
+
if (!fraOrigin) {
|
|
252
|
+
process.stderr.write(
|
|
253
|
+
"[MCP] Afviser forbindelse uden gyldig chrome-extension-Origin" +
|
|
254
|
+
(origin ? ` (fik "${origin.slice(0, 60)}")` : " (ingen Origin-header)") + "\n",
|
|
255
|
+
);
|
|
256
|
+
try { ws.close(1008, "only chrome extensions may connect"); } catch {}
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Noedudgang naar flere udvidelser er indlaest og brugeren ikke kan eller vil
|
|
261
|
+
// slaa dem fra: BROWSER_MCP_EXTENSION_ID=<id> binder serveren til én bestemt.
|
|
262
|
+
// Uden den er valget vilkaarligt naar ingen af dem oplyser en version.
|
|
263
|
+
if (PINNET_UDVIDELSE && fraOrigin && fraOrigin !== PINNET_UDVIDELSE) {
|
|
264
|
+
process.stderr.write(`[MCP] Afviser udvidelse ${fraOrigin} — bundet til ${PINNET_UDVIDELSE}\n`);
|
|
265
|
+
try { ws.close(1008, 'not the pinned extension'); } catch {}
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const conn = { ws, seq: ++connSeq, extensionId: fraOrigin, version: null, name: null,
|
|
270
|
+
harHilst: false, helloId: null, since: Date.now() };
|
|
271
|
+
connections.add(conn);
|
|
272
|
+
// Har vi endnu ikke sendt en eneste kommando, er ingen faner i spil, og en
|
|
273
|
+
// nytilkommen udvidelse maa gerne komme i betragtning igen.
|
|
274
|
+
if (!harSendtKommando) laastForbindelse = null;
|
|
275
|
+
process.stderr.write(`[MCP] Chrome extension connected on port ${port}${fraOrigin ? ` (${fraOrigin})` : ''}\n`);
|
|
276
|
+
advarOmKonflikt(conn);
|
|
90
277
|
|
|
91
278
|
// If extension was auto-updated, trigger reload
|
|
92
279
|
if (process.env.BROWSER_MCP_EXTENSION_UPDATED === '1') {
|
|
@@ -101,8 +288,58 @@ function createWSS(port = BASE_PORT) {
|
|
|
101
288
|
let msg;
|
|
102
289
|
try { msg = JSON.parse(data.toString()); } catch { return; }
|
|
103
290
|
|
|
291
|
+
// Identitets-haandtryk fra offscreen-dokumentet (v1.28+).
|
|
292
|
+
if (msg.type === 'hello') {
|
|
293
|
+
// MAALT 23/8: her stod `conn.extensionId = msg.extensionId` — altsaa lod
|
|
294
|
+
// haandtrykket afsenderen OVERSKRIVE sin egen identitet med hvad som helst.
|
|
295
|
+
// Origin-headeren er den eneste kilde Chrome selv saetter og som afsenderen
|
|
296
|
+
// ikke kan forfalske, saa den vinder. Beskedens id gemmes separat: stemmer de
|
|
297
|
+
// ikke overens, er noget galt, og saa maa forbindelsen ikke lukke serveren ned.
|
|
298
|
+
conn.helloId = typeof msg.extensionId === 'string' ? msg.extensionId : null;
|
|
299
|
+
conn.harHilst = true;
|
|
300
|
+
if (conn.helloId && conn.helloId !== conn.extensionId) {
|
|
301
|
+
process.stderr.write(
|
|
302
|
+
`[MCP] Haandtryk oplyser ${conn.helloId} men Origin siger ${conn.extensionId} — ` +
|
|
303
|
+
'bruger Origin\n',
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
conn.version = typeof msg.version === 'string' ? msg.version : null;
|
|
307
|
+
conn.name = typeof msg.name === 'string' ? msg.name : null;
|
|
308
|
+
advarOmKonflikt(conn);
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
|
|
104
312
|
if (msg.type === 'terminate') {
|
|
105
|
-
|
|
313
|
+
// terminate lukker serveren for ALLE chats paa porten, saa den har to gates.
|
|
314
|
+
//
|
|
315
|
+
// Om id-sammenligningen: ORIGIN er autoriteten — Chrome saetter den, og
|
|
316
|
+
// afsenderen kan ikke forfalske den. Haandtrykkets id er kun et ekstra
|
|
317
|
+
// signal. MAALT 23/8 mod den AEGTE udvidelse: den sender hello med id null,
|
|
318
|
+
// fordi chrome.runtime.getManifest() kan fejle i offscreen-dokumentet.
|
|
319
|
+
// Kraevede vi lighed ubetinget, kunne en HELT legitim udvidelse aldrig lukke
|
|
320
|
+
// sin session ned — en fejl jeg selv indfoerte samme aften. Derfor: et id der
|
|
321
|
+
// MANGLER er fint (Origin har allerede bevist hvem det er), mens et id der er
|
|
322
|
+
// TIL STEDE og peger et ANDET sted afvises.
|
|
323
|
+
//
|
|
324
|
+
// 1) Afsenderen skal have sendt et hello der stemmer med sin egen Origin.
|
|
325
|
+
// MAALT 23/8: uden den kunne en forbindelse der lige havde vundet rollen
|
|
326
|
+
// som aktiv slukke browser-adgangen med én besked.
|
|
327
|
+
// 2) Afsenderen skal VAERE den aktive. Uden den kunne en gammel sidelaebende
|
|
328
|
+
// kopi, der lukkede sin sidste fane, rive serveren vaek under den
|
|
329
|
+
// udvidelse der reelt loeste opgaven.
|
|
330
|
+
if (!conn.harHilst || (conn.helloId && conn.helloId !== conn.extensionId)) {
|
|
331
|
+
process.stderr.write('[MCP] terminate ignoreret — intet gyldigt haandtryk\n');
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
if (activeConnection() !== conn) {
|
|
335
|
+
process.stderr.write('[MCP] terminate ignoreret — kom fra en inaktiv udvidelses-forbindelse\n');
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
// AENDRET 7/9: her stod `gracefulShutdown`. En chat der var faerdig med browseren
|
|
339
|
+
// mistede altsaa browseren HELT — og porten blev alligevel hverken frigivet hurtigt
|
|
340
|
+
// nok til andre (processen doede foerst efter oprydning) eller genvundet af chatten
|
|
341
|
+
// selv. Nu slippes kun porten; naeste browser-kald tager en ny (se sikrePort).
|
|
342
|
+
frigivPort('udvidelsen meldte: sidste fane lukket');
|
|
106
343
|
return;
|
|
107
344
|
}
|
|
108
345
|
|
|
@@ -115,42 +352,173 @@ function createWSS(port = BASE_PORT) {
|
|
|
115
352
|
else p.resolve(result);
|
|
116
353
|
});
|
|
117
354
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
355
|
+
// ── En doed socket skal AFVISE de kald der var undervejs (MAALT 23/8) ──────
|
|
356
|
+
// 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 — og med den FORKERTE
|
|
358
|
+
// forklaring, "kommandoen tog for lang tid". For extract_list er timeouten 180
|
|
359
|
+
// sekunder, altsaa tre minutters tavshed hvor sandheden var kendt med det samme.
|
|
360
|
+
const afvisVentende = (grund) => {
|
|
361
|
+
// Kun naar ingen anden levende forbindelse kan svare — ellers ville et helt
|
|
362
|
+
// normalt skift mellem to udvidelser afbryde kald der er fuldt i orden.
|
|
363
|
+
if (!pending.size || liveConnections().length) return;
|
|
364
|
+
const antal = pending.size;
|
|
365
|
+
for (const [id, p] of pending) {
|
|
366
|
+
clearTimeout(p.timer);
|
|
367
|
+
pending.delete(id);
|
|
368
|
+
p.reject(new Error(
|
|
369
|
+
`Forbindelsen til Chrome-udvidelsen forsvandt mens kommandoen koerte (${grund}). ` +
|
|
370
|
+
'Kommandoen naaede maaske at blive udfoert i browseren — tjek tilstanden foer du ' +
|
|
371
|
+
'proever igen. Er udvidelsen slaaet fra eller Chrome lukket, saa start den og proev forfra.',
|
|
372
|
+
));
|
|
122
373
|
}
|
|
374
|
+
process.stderr.write(`[MCP] ${antal} ventende kald afvist — ${grund}\n`);
|
|
375
|
+
};
|
|
376
|
+
|
|
377
|
+
// MAALT 23/8: der fandtes INGEN error-handler. Et ugyldigt WebSocket-frame (fx
|
|
378
|
+
// RSV1 sat) faar 'ws' til at emitte 'error' paa socketen, og en uhaandteret
|
|
379
|
+
// 'error' paa en EventEmitter kaster og draeber hele processen — altsaa alle
|
|
380
|
+
// chats paa den port. Tre linjer lukker det.
|
|
381
|
+
ws.on('error', (e) => {
|
|
382
|
+
process.stderr.write(`[MCP] WebSocket-fejl paa forbindelsen: ${e?.message || e}\n`);
|
|
383
|
+
try { ws.close(); } catch {}
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
ws.on('close', () => {
|
|
387
|
+
connections.delete(conn);
|
|
388
|
+
afvisVentende('udvidelsen koblede fra');
|
|
389
|
+
process.stderr.write(`[MCP] Chrome extension disconnected (${liveConnections().length} tilbage)\n`);
|
|
123
390
|
});
|
|
124
391
|
});
|
|
125
392
|
|
|
126
393
|
server.on('listening', () => {
|
|
127
394
|
activePort = port;
|
|
128
395
|
process.stderr.write(`[MCP] WebSocket server listening on ws://127.0.0.1:${port}\n`);
|
|
396
|
+
loesPortLoefte(true);
|
|
129
397
|
});
|
|
130
398
|
|
|
131
399
|
// Heartbeat + idle timeout (4 hours) — hoisted to module scope so gracefulShutdown can clear it
|
|
400
|
+
// Ryddes foerst: porten kan bindes flere gange i samme proces (se sikrePort), og uden
|
|
401
|
+
// det her ville hver ny binding efterlade en ekstra timer der aldrig blev stoppet.
|
|
402
|
+
if (heartbeat) clearInterval(heartbeat);
|
|
132
403
|
heartbeat = setInterval(() => {
|
|
133
|
-
|
|
134
|
-
extensionSocket.ping();
|
|
135
|
-
}
|
|
136
|
-
if (Date.now() - lastActivity > 4 * 60 * 60 * 1000) {
|
|
137
|
-
gracefulShutdown('Idle timeout (4h)');
|
|
138
|
-
}
|
|
404
|
+
for (const c of liveConnections()) c.ws.ping();
|
|
139
405
|
}, 20000);
|
|
140
406
|
}
|
|
141
407
|
|
|
142
|
-
|
|
408
|
+
// ── Porten tages ved BRUG, ikke ved opstart ─────────────────────────────────
|
|
409
|
+
//
|
|
410
|
+
// MAALT 7/9-2026 paa Gustavs maskine: 37 koerende servere, 20 porte i spaendet, 17 chats
|
|
411
|
+
// helt uden browser. Hver Claude Code-chat starter en server ved opstart — ogsaa de mange
|
|
412
|
+
// chats der aldrig roerer browseren — og her stod `createWSS()` paa modul-niveau. Porten
|
|
413
|
+
// blev altsaa reserveret af en chat der maaske aldrig fik brug for den, og holdt indtil
|
|
414
|
+
// chatten doede eller 4-timers-tomgangen udloeb.
|
|
415
|
+
//
|
|
416
|
+
// Konsekvensen var ensidigt slem: chat nr. 21 fik `alleePorteOptaget = true` ÉN gang og
|
|
417
|
+
// proevede aldrig igen — dens browser var doed hele chattens levetid, selv naar en port
|
|
418
|
+
// blev fri et minut senere.
|
|
419
|
+
//
|
|
420
|
+
// Nu bindes porten foerste gang et vaerktoej faktisk skal bruge udvidelsen, og HVERT kald
|
|
421
|
+
// proever igen hvis det forrige ikke fik en. Udvidelsen genscanner hele spaendet hvert
|
|
422
|
+
// 2. sekund (offscreen.js), saa en port der aabnes sent bliver fundet af sig selv.
|
|
423
|
+
//
|
|
424
|
+
// Bemaerk hvorfor det ikke er en 5-minutters timer der draeber processen: en chat der
|
|
425
|
+
// foerst skal bruge browseren efter en halv time ville saa staa uden. Processen lever
|
|
426
|
+
// videre — det er kun PORTEN der ikke holdes reserveret til noget der ikke sker.
|
|
427
|
+
let portResolver = null;
|
|
428
|
+
let bindLoefte = null;
|
|
429
|
+
|
|
430
|
+
function loesPortLoefte(fik) {
|
|
431
|
+
if (!portResolver) return;
|
|
432
|
+
const r = portResolver;
|
|
433
|
+
portResolver = null;
|
|
434
|
+
bindLoefte = null;
|
|
435
|
+
r(fik);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// Slip porten, men BLIV I LIVE. Forskellen er hele pointen: lukkede vi processen ned,
|
|
439
|
+
// ville en chat der er faerdig med browseren kl. 10 og skal bruge den igen kl. 10:40 staa
|
|
440
|
+
// uden — og Claude Code genstarter ikke en MCP-server midt i en samtale. Processen koster
|
|
441
|
+
// ~35 MB og ingen port; det er porten der er den knappe ressource.
|
|
442
|
+
function frigivPort(grund) {
|
|
443
|
+
if (activePort === null) return;
|
|
444
|
+
process.stderr.write(`[MCP] frigiver port ${activePort} — ${grund}\n`);
|
|
445
|
+
if (heartbeat) { clearInterval(heartbeat); heartbeat = null; }
|
|
446
|
+
const gammel = wss;
|
|
447
|
+
wss = null;
|
|
448
|
+
activePort = null;
|
|
449
|
+
laastForbindelse = null;
|
|
450
|
+
harSendtKommando = false; // naeste binding vaelger udvidelse forfra
|
|
451
|
+
for (const c of connections) { try { c.ws.close(); } catch {} }
|
|
452
|
+
connections.clear();
|
|
453
|
+
try { gammel?.close(); } catch {}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function sikrePort() {
|
|
457
|
+
if (activePort !== null) return Promise.resolve(true);
|
|
458
|
+
if (bindLoefte) return bindLoefte; // en binding er allerede i gang
|
|
459
|
+
alleePorteOptaget = false; // hvert forsoeg starter paa en frisk
|
|
460
|
+
bindFejl = null;
|
|
461
|
+
bindLoefte = new Promise((res) => { portResolver = res; });
|
|
462
|
+
// Vagthund. F8 var ét konkret hul hvor loeftet aldrig blev indfriet; det her lukker
|
|
463
|
+
// KLASSEN. Emitter en fremtidig fejlsti hverken 'listening' eller 'error', svarer
|
|
464
|
+
// kaldet nu med en fejl i stedet for at haenge tavst for evigt. 10 s er rigeligt:
|
|
465
|
+
// en loopback-binding tager millisekunder, og hele spaendet naas paa under ét.
|
|
466
|
+
const vagthund = setTimeout(() => {
|
|
467
|
+
if (!portResolver) return;
|
|
468
|
+
bindFejl = bindFejl || 'bindingen svarede ikke inden for 10 sekunder';
|
|
469
|
+
process.stderr.write('[MCP] port-bindingen svarede aldrig — opgiver dette forsoeg\n');
|
|
470
|
+
loesPortLoefte(false);
|
|
471
|
+
}, 10000);
|
|
472
|
+
bindLoefte.finally(() => clearTimeout(vagthund));
|
|
473
|
+
createWSS();
|
|
474
|
+
return bindLoefte;
|
|
475
|
+
}
|
|
143
476
|
|
|
144
477
|
// ── Send command to extension ───────────────────────────────────────────────
|
|
145
478
|
|
|
146
479
|
async function sendToExtension(method, params = {}, timeoutMs = 30000, _retries = 5) {
|
|
480
|
+
// Skaf en port hvis vi ikke har en. Foerste kald binder; senere kald er en no-op.
|
|
481
|
+
// Fik vi ingen (hele spaendet optaget), proever naeste kald igen — derfor ingen kast her.
|
|
482
|
+
await sikrePort();
|
|
147
483
|
// Retry if extension is temporarily disconnected (reconnects every 2s)
|
|
148
|
-
|
|
484
|
+
const conn = activeConnection();
|
|
485
|
+
if (!conn) {
|
|
149
486
|
if (_retries > 0) {
|
|
150
487
|
await new Promise(r => setTimeout(r, 1500));
|
|
151
488
|
return sendToExtension(method, params, timeoutMs, _retries - 1);
|
|
152
489
|
}
|
|
153
|
-
|
|
490
|
+
// This is the other half of the two-part setup: the server is clearly running (it is
|
|
491
|
+
// throwing this), so what is missing is the extension, Chrome itself, or the connection
|
|
492
|
+
// between them. Say which, and where to get it — the agent relays this text to the user.
|
|
493
|
+
if (bindFejl) {
|
|
494
|
+
throw new Error(
|
|
495
|
+
`Serveren kunne ikke aabne en port paa 127.0.0.1 (${BASE_PORT}-${MAX_PORT}): ${bindFejl}\n` +
|
|
496
|
+
'Det er IKKE Chrome eller udvidelsen — det er operativsystemet eller en firewall der ' +
|
|
497
|
+
'afviser bindingen. Tjek om noget blokerer loopback-porte.\n' +
|
|
498
|
+
'Sig praecis dét til brugeren. Sig IKKE at udvidelsen mangler.',
|
|
499
|
+
);
|
|
500
|
+
}
|
|
501
|
+
if (alleePorteOptaget) {
|
|
502
|
+
throw new Error(
|
|
503
|
+
`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 — de virker fint.\n' +
|
|
505
|
+
`${MAX_PORT - BASE_PORT + 1} andre chats bruger browseren i oejeblikket. ` +
|
|
506
|
+
'Luk en af dem, eller vent til en bliver faerdig — og proev saa kommandoen igen. ' +
|
|
507
|
+
'Denne chat skal IKKE genstartes: hvert kald proever selv at faa en port.\n' +
|
|
508
|
+
'Sig praecis dét til brugeren. Sig IKKE at udvidelsen mangler.',
|
|
509
|
+
);
|
|
510
|
+
}
|
|
511
|
+
throw new Error(
|
|
512
|
+
'Chrome extension not connected after 5 retries.\n' +
|
|
513
|
+
'Browser MCP needs BOTH halves: this MCP server (running) and the Agent360 Browser MCP ' +
|
|
514
|
+
'Chrome extension (apparently not reachable).\n' +
|
|
515
|
+
'Check, in order:\n' +
|
|
516
|
+
' 1. Chrome is actually open and running.\n' +
|
|
517
|
+
' 2. The extension is installed and enabled at chrome://extensions — install it from\n' +
|
|
518
|
+
' https://chromewebstore.google.com/detail/agent360-browser-mcp/jdehgalffmffhfhmmhaokfbfnafnmgcl\n' +
|
|
519
|
+
' 3. Click the extension icon -> Reconnect, and wait 2-3 seconds.\n' +
|
|
520
|
+
'Still stuck: https://browsermcp.dev/docs/troubleshooting/'
|
|
521
|
+
);
|
|
154
522
|
}
|
|
155
523
|
return new Promise((resolve, reject) => {
|
|
156
524
|
const id = ++cmdId;
|
|
@@ -159,7 +527,10 @@ async function sendToExtension(method, params = {}, timeoutMs = 30000, _retries
|
|
|
159
527
|
reject(new Error(`Command timed out after ${timeoutMs}ms: ${method}`));
|
|
160
528
|
}, timeoutMs);
|
|
161
529
|
pending.set(id, { resolve, reject, timer });
|
|
162
|
-
|
|
530
|
+
// pid = Claude Code-processen der ejer denne server. Udvidelsen bruger den til at
|
|
531
|
+
// skelne 'samme chat, ny forbindelse' fra 'en anden chat' naar den adopterer sessioner.
|
|
532
|
+
harSendtKommando = true;
|
|
533
|
+
conn.ws.send(JSON.stringify({ id, method, params, pid: process.ppid }));
|
|
163
534
|
});
|
|
164
535
|
}
|
|
165
536
|
|
|
@@ -244,13 +615,27 @@ If the extension files were updated, ask the user to reload it:
|
|
|
244
615
|
"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
616
|
You cannot navigate to chrome:// pages — the user must do this manually.
|
|
246
617
|
|
|
618
|
+
## When Browser MCP itself is the obstacle — call browser_provide_feedback
|
|
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 — 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:
|
|
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 — 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
|
+
- **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
|
+
- **current** → the install is fine, so this is a genuine gap. Offer the returned submit_url as a clickable link.
|
|
623
|
+
- **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
|
+
Call it once per distinct obstacle, not once per retry.
|
|
625
|
+
|
|
247
626
|
## Sharing wishes / use-cases / bugs
|
|
248
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 — 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
628
|
|
|
629
|
+
// 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 —
|
|
631
|
+
// og hele blokken ovenfor naaede aldrig frem til nogen klient. Verificeret ved at
|
|
632
|
+
// laese initialize-svaret: det havde kun protocolVersion, capabilities og serverInfo.
|
|
633
|
+
// Det betyder at "luk altid faner naar du er faerdig", CAPTCHA-fremgangsmaaden,
|
|
634
|
+
// tekst-selektor-vejledningen og resten aldrig har styret nogen agent. Nu ligger
|
|
635
|
+
// instructions i SAMME options-objekt som capabilities, hvor SDK'en laeser den.
|
|
250
636
|
const mcpServer = new Server(
|
|
251
637
|
{ name: 'agent360-browser', version: PKG_VERSION },
|
|
252
|
-
{ capabilities: { tools: {} } },
|
|
253
|
-
{ instructions: INSTRUCTIONS },
|
|
638
|
+
{ capabilities: { tools: {} }, instructions: INSTRUCTIONS },
|
|
254
639
|
);
|
|
255
640
|
|
|
256
641
|
mcpServer.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
@@ -295,12 +680,21 @@ mcpServer.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
295
680
|
browser_dismiss_overlays: 'dismiss_overlays',
|
|
296
681
|
browser_set_combobox: 'set_combobox',
|
|
297
682
|
browser_drop_file: 'drop_file',
|
|
683
|
+
browser_double_click: 'double_click',
|
|
684
|
+
browser_right_click: 'right_click',
|
|
685
|
+
browser_click_xy: 'click_xy',
|
|
686
|
+
browser_reattach_debugger: 'reattach_debugger',
|
|
687
|
+
browser_extract_list: 'extract_list',
|
|
298
688
|
};
|
|
299
689
|
|
|
300
690
|
if (name === 'browser_about') {
|
|
301
691
|
return handleAbout(args);
|
|
302
692
|
}
|
|
303
693
|
|
|
694
|
+
if (name === 'browser_provide_feedback') {
|
|
695
|
+
return await handleProvideFeedback(args);
|
|
696
|
+
}
|
|
697
|
+
|
|
304
698
|
if (name === 'browser_extract_token') {
|
|
305
699
|
return await handleExtractToken(args);
|
|
306
700
|
}
|
|
@@ -310,8 +704,11 @@ mcpServer.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
310
704
|
return { content: [{ type: 'text', text: `Unknown tool: ${name}` }], isError: true };
|
|
311
705
|
}
|
|
312
706
|
|
|
707
|
+
// extract_list scrolls a container in a loop (up to 300 rounds × wait_ms), so the 30 s
|
|
708
|
+
// default would kill a long mail list mid-walk and report a partial set as complete.
|
|
313
709
|
const timeout = method === 'ask_user' ? (args?.timeout || 120000) + 5000 :
|
|
314
|
-
method === 'solve_captcha' ? 60000 :
|
|
710
|
+
method === 'solve_captcha' ? 60000 :
|
|
711
|
+
method === 'extract_list' ? 180000 : 30000;
|
|
315
712
|
const result = await sendToExtension(method, args || {}, timeout);
|
|
316
713
|
|
|
317
714
|
if (name === 'browser_screenshot' && result?.image) {
|
|
@@ -321,7 +718,18 @@ mcpServer.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
321
718
|
const base64 = result.image.replace(prefix, '');
|
|
322
719
|
|
|
323
720
|
if (args && args.path) {
|
|
324
|
-
|
|
721
|
+
// MAALT 23/8: ingen indeslutning. En sti med ../../.. skrev til
|
|
722
|
+
// /private/tmp/a360-udenfor/x.png. Argumenterne kommer fra en model der laeser
|
|
723
|
+
// FREMMEDE websider, saa en prompt-injektion paa en vilkaarlig side kunne
|
|
724
|
+
// overskrive en fil i brugerens hjemmemappe med PNG-bytes.
|
|
725
|
+
const rod = resolve(process.cwd());
|
|
726
|
+
const targetPath = resolve(rod, args.path);
|
|
727
|
+
if (targetPath !== rod && !targetPath.startsWith(rod + sep)) {
|
|
728
|
+
throw new Error(
|
|
729
|
+
`path skal ligge inden for arbejdsmappen (${rod}). ` +
|
|
730
|
+
`"${args.path}" peger udenfor. Brug en relativ sti uden ../.`,
|
|
731
|
+
);
|
|
732
|
+
}
|
|
325
733
|
mkdirSync(dirname(targetPath), { recursive: true });
|
|
326
734
|
writeFileSync(targetPath, Buffer.from(base64, 'base64'));
|
|
327
735
|
return {
|
|
@@ -351,12 +759,45 @@ mcpServer.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
351
759
|
return response;
|
|
352
760
|
} catch (err) {
|
|
353
761
|
return {
|
|
354
|
-
content: [{ type: 'text', text:
|
|
762
|
+
content: [{ type: 'text', text: forklarSkaevhed(err.message) }],
|
|
355
763
|
isError: true,
|
|
356
764
|
};
|
|
357
765
|
}
|
|
358
766
|
});
|
|
359
767
|
|
|
768
|
+
// Naar udvidelsen er aeldre end serveren, svarer den `Unknown method: X` — og det er
|
|
769
|
+
// alt brugeren ser. Det sker GARANTERET: serveren kommer fra npm og opdateres straks,
|
|
770
|
+
// mens udvidelsen skal gennem Chrome Web Stores review paa 1-3 dage. I det vindue
|
|
771
|
+
// findes otte vaerktoejer i serveren som en 1.25.0-udvidelse ikke kender
|
|
772
|
+
// (click_xy, double_click, right_click, extract_list, reattach_debugger og de tre
|
|
773
|
+
// udklipsholder-vaerktoejer, alle fra v1.26.0).
|
|
774
|
+
//
|
|
775
|
+
// Serveren VED at udvidelsen er gammel: den sendte intet haandtryk. Saa i stedet for
|
|
776
|
+
// en gaadefuld fejl faar brugeren at vide hvorfor — og hvad de kan goere imens.
|
|
777
|
+
const ERSTATNINGER = {
|
|
778
|
+
double_click: 'kald `browser_click` to gange',
|
|
779
|
+
right_click: 'brug `browser_execute_script` med et contextmenu-event',
|
|
780
|
+
click_xy: 'brug `browser_click` med en selector',
|
|
781
|
+
extract_list: 'brug `browser_get_page_content` og scroll med `browser_scroll`',
|
|
782
|
+
reattach_debugger: 'genindlaes udvidelsen paa chrome://extensions',
|
|
783
|
+
};
|
|
784
|
+
|
|
785
|
+
function forklarSkaevhed(besked) {
|
|
786
|
+
const m = /Unknown method: ([a-z_]+)/.exec(besked || '');
|
|
787
|
+
if (!m) return `Error: ${besked}`;
|
|
788
|
+
const aktiv = activeConnection();
|
|
789
|
+
// Kun hvis udvidelsen faktisk er for gammel. Er den ny og metoden alligevel ukendt,
|
|
790
|
+
// er det en aegte fejl og skal ikke bortforklares.
|
|
791
|
+
if (aktiv && aktiv.version) return `Error: ${besked}`;
|
|
792
|
+
const alt = ERSTATNINGER[m[1]];
|
|
793
|
+
return `Error: browser_${m[1]} findes i denne server, men ikke i din Chrome-udvidelse.\n\n` +
|
|
794
|
+
'Udvidelsen opdateres via Chrome Web Store og kan vaere 1-3 dage bagud efter en ' +
|
|
795
|
+
'udgivelse — serveren opdateres med det samme via npm. Alt andet virker imens.\n' +
|
|
796
|
+
(alt ? `\nIndtil da: ${alt}.\n` : '') +
|
|
797
|
+
'\nTjek om en opdatering venter: chrome://extensions → Agent360 Browser MCP. ' +
|
|
798
|
+
'Er den indlaest som "unpacked", saa koer `npx @agent360/browser-mcp install`.';
|
|
799
|
+
}
|
|
800
|
+
|
|
360
801
|
const REPO_URL = 'https://github.com/Agent360dk/browser-mcp';
|
|
361
802
|
const ISSUE_TEMPLATES = { wish: 'wish.yml', use_case: 'use-case.yml', bug: 'bug.yml' };
|
|
362
803
|
|
|
@@ -396,6 +837,253 @@ function handleAbout(args) {
|
|
|
396
837
|
};
|
|
397
838
|
}
|
|
398
839
|
|
|
840
|
+
// ── Selv-diagnose: er installationen overhovedet frisk? ─────────────────────
|
|
841
|
+
// Baggrund (21/8): den udgave der koerte lokalt var npm 1.25.0, mens rettelserne
|
|
842
|
+
// laa uudgivet i repoet. Fejlen viste sig som "sessioner opfoerer sig underligt",
|
|
843
|
+
// ikke som "du koerer en gammel version" — og der fandtes ingen maade at spoerge
|
|
844
|
+
// paa. Derfor spoerger vaerktoejet selv, foer det konkluderer noget som helst.
|
|
845
|
+
|
|
846
|
+
// Friskheds-tjek mod npm. SLUKKET SOM STANDARD siden 22/8.
|
|
847
|
+
//
|
|
848
|
+
// Produktet lover paa forsiden "nothing leaves your machine ... no telemetry", og et
|
|
849
|
+
// opslag i npm-registret ER et kald ud af maskinen — ogsaa selv om det kun sender et
|
|
850
|
+
// pakkenavn og ingen brugerdata. Loeftet vejer tungere end bekvemmeligheden, saa
|
|
851
|
+
// tjekket er nu opt-in: saet BROWSER_MCP_CHECK_NPM=1.
|
|
852
|
+
//
|
|
853
|
+
// Alt det der betyder mest er ren LOKAL maaling og koerer altid: er udvidelsen aeldre
|
|
854
|
+
// end serveren, og er der flere udvidelser forbundet paa én gang.
|
|
855
|
+
const TJEK_NPM = process.env.BROWSER_MCP_CHECK_NPM === '1';
|
|
856
|
+
let npmLatestCache = null; // { version, at }
|
|
857
|
+
const NPM_LATEST_TTL_MS = 10 * 60 * 1000;
|
|
858
|
+
|
|
859
|
+
function npmLatestVersion() {
|
|
860
|
+
if (!TJEK_NPM) return Promise.resolve(null);
|
|
861
|
+
if (npmLatestCache && Date.now() - npmLatestCache.at < NPM_LATEST_TTL_MS) {
|
|
862
|
+
return Promise.resolve(npmLatestCache.version);
|
|
863
|
+
}
|
|
864
|
+
return new Promise((resolve) => {
|
|
865
|
+
// Offline, bag proxy, eller npm mangler paa PATH: svar null i stedet for at fejle.
|
|
866
|
+
// En friskheds-kontrol maa aldrig vaere det der braekker vaerktoejet.
|
|
867
|
+
execFile('npm', ['view', '@agent360/browser-mcp', 'version'], { timeout: 6000 }, (err, stdout) => {
|
|
868
|
+
if (err) return resolve(null);
|
|
869
|
+
const v = String(stdout).trim();
|
|
870
|
+
const ok = /^\d+\.\d+\.\d+/.test(v) ? v : null;
|
|
871
|
+
if (ok) npmLatestCache = { version: ok, at: Date.now() };
|
|
872
|
+
resolve(ok);
|
|
873
|
+
});
|
|
874
|
+
});
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
// ── Lokal logbog over hver graense agenten render ind i ─────────────────────
|
|
878
|
+
//
|
|
879
|
+
// Formaalet er loekken: hver gang et vaerktoej spaerrer vejen, skal det kunne taelles og
|
|
880
|
+
// rettes. Logbogen ligger LOKALT og forlader ikke maskinen.
|
|
881
|
+
//
|
|
882
|
+
// Bevidst ikke auto-indsendelse til et offentligt GitHub-issue: rapporten baerer URL og
|
|
883
|
+
// fejltekst fra den side agenten stod paa — og det er ofte en annoncekonto, en indbakke
|
|
884
|
+
// eller et kundesystem. Et offentligt issue kan ikke tages tilbage.
|
|
885
|
+
const FEEDBACK_LOG = join(homedir(), '.browser-mcp', 'feedback.jsonl');
|
|
886
|
+
const setteFingeraftryk = new Set(); // samme graense logges én gang pr. serverliv
|
|
887
|
+
|
|
888
|
+
function fingeraftryk(kind, tool, what) {
|
|
889
|
+
// Tal, id'er og lange hex-strenge varierer fra gang til gang og maa ikke goere to ens
|
|
890
|
+
// haendelser forskellige.
|
|
891
|
+
const kerne = String(what).toLowerCase()
|
|
892
|
+
.replace(/\b[0-9a-f]{8,}\b/g, '#')
|
|
893
|
+
.replace(/\d+/g, '#')
|
|
894
|
+
.slice(0, 160);
|
|
895
|
+
return `${kind}|${tool || '-'}|${kerne}`;
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
// URL'en reduceres til oprindelse + sti. Query og fragment baerer tokens, sessions-id'er
|
|
899
|
+
// og soegetermer — de har intet at goere i en logbog nogen senere kopierer ind i et issue.
|
|
900
|
+
function afkortUrl(u) {
|
|
901
|
+
if (!u) return null;
|
|
902
|
+
try { const x = new URL(u); return x.origin + x.pathname; } catch { return '(ulaeselig url)'; }
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
function skrivTilLogbog(post) {
|
|
906
|
+
const fp = fingeraftryk(post.kind, post.tool, post.what_happened);
|
|
907
|
+
const foerste = !setteFingeraftryk.has(fp);
|
|
908
|
+
setteFingeraftryk.add(fp);
|
|
909
|
+
if (!foerste) return { logged: false, reason: 'allerede logget i denne session', fingerprint: fp };
|
|
910
|
+
try {
|
|
911
|
+
mkdirSync(dirname(FEEDBACK_LOG), { recursive: true });
|
|
912
|
+
appendFileSync(FEEDBACK_LOG, JSON.stringify({ ...post, fingerprint: fp }) + '\n');
|
|
913
|
+
return { logged: true, path: FEEDBACK_LOG, fingerprint: fp };
|
|
914
|
+
} catch (e) {
|
|
915
|
+
// En logbog der ikke kan skrives maa aldrig vaere det der braekker vaerktoejet.
|
|
916
|
+
return { logged: false, reason: e.message, fingerprint: fp };
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
async function handleProvideFeedback(args) {
|
|
921
|
+
const what = String(args?.what_happened || '').trim();
|
|
922
|
+
const kind = args?.kind || 'blocked';
|
|
923
|
+
const tool = args?.tool || null;
|
|
924
|
+
const url = args?.url || null;
|
|
925
|
+
const attempted = args?.attempted || null;
|
|
926
|
+
|
|
927
|
+
const npmLatest = await npmLatestVersion();
|
|
928
|
+
const exts = distinctExtensions();
|
|
929
|
+
const active = activeConnection();
|
|
930
|
+
|
|
931
|
+
const serverOutdated = npmLatest ? cmpVersion(npmLatest, PKG_VERSION) > 0 : null;
|
|
932
|
+
// Udvidelse og server udgives sammen under samme versionsnummer, saa en
|
|
933
|
+
// udvidelse der er AELDRE end serveren mangler per definition rettelser.
|
|
934
|
+
const extVersion = active ? active.version : null;
|
|
935
|
+
const extOutdated = active
|
|
936
|
+
? (extVersion === null ? true : cmpVersion(PKG_VERSION, extVersion) > 0)
|
|
937
|
+
: null;
|
|
938
|
+
|
|
939
|
+
const findings = [];
|
|
940
|
+
const fix_steps = [];
|
|
941
|
+
|
|
942
|
+
if (exts.length > 1) {
|
|
943
|
+
const kanVaelge = exts.some(c => c.version);
|
|
944
|
+
findings.push(
|
|
945
|
+
`${exts.length} Browser MCP-udvidelser er indlaest i Chrome og forbundet til denne server samtidig ` +
|
|
946
|
+
`(${exts.map(c => `${c.extensionId || 'ukendt id'}${c.version ? ' v' + c.version : ' (oplyser ikke version)'}`).join(' + ')}). ` +
|
|
947
|
+
'Hver af dem foerer sit eget sessions-kort og sine egne fane-grupper i den samme browser, ' +
|
|
948
|
+
'saa faner kan se ud til at forsvinde og sessioner til at smelte sammen. ' +
|
|
949
|
+
(kanVaelge
|
|
950
|
+
? `Denne server sender kun til den nyeste (${active?.extensionId}).`
|
|
951
|
+
: `Ingen af dem oplyser sin version, saa hvilken der styres (${active?.extensionId}) er vilkaarligt og kan skifte mellem sessioner.`),
|
|
952
|
+
);
|
|
953
|
+
fix_steps.push(
|
|
954
|
+
'Aabn chrome://extensions og slaa alle Browser MCP-udvidelser fra paa naer én. ' +
|
|
955
|
+
'Det skal brugeren selv goere — chrome:// kan ikke styres herfra. Behold den nyeste.',
|
|
956
|
+
);
|
|
957
|
+
}
|
|
958
|
+
if (!active && activePort === null) {
|
|
959
|
+
// Ingen port taget endnu = browseren er ikke brugt i denne chat. Der er intet
|
|
960
|
+
// i stykker, og et fix-skridt her ville vaere en falsk alarm — se verdict 'idle'.
|
|
961
|
+
findings.push('Browseren er ikke taget i brug i denne chat endnu, saa der er ingen forbindelse at maale paa. Det er ikke en fejl.');
|
|
962
|
+
} else if (!active) {
|
|
963
|
+
findings.push('Ingen Chrome-udvidelse er forbundet til denne MCP-server lige nu.');
|
|
964
|
+
fix_steps.push('Tjek at Chrome koerer og at udvidelsen er slaaet til paa chrome://extensions, klik derefter paa ikonet → Reconnect.');
|
|
965
|
+
}
|
|
966
|
+
if (serverOutdated) {
|
|
967
|
+
findings.push(`MCP-serveren koerer v${PKG_VERSION}, men npm har v${npmLatest}. Fejlen kan allerede vaere rettet.`);
|
|
968
|
+
fix_steps.push(`Genstart klienten — den henter selv @agent360/browser-mcp@latest (v${npmLatest}).`);
|
|
969
|
+
}
|
|
970
|
+
if (extOutdated) {
|
|
971
|
+
findings.push(
|
|
972
|
+
extVersion === null
|
|
973
|
+
? `Den forbundne udvidelse er saa gammel at den ikke oplyser sin version (foer v${PKG_VERSION}). Den mangler alt hvad der er rettet siden.`
|
|
974
|
+
: `Udvidelsen er v${extVersion}, serveren er v${PKG_VERSION}. Udvidelsen mangler rettelser fra de mellemliggende udgaver.`,
|
|
975
|
+
);
|
|
976
|
+
// 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 — der er ingen nyere
|
|
978
|
+
// version at hente endnu, saa raadet foerer i ring. Serveren kan ikke se hvilken
|
|
979
|
+
// slags installation det er (den gamle udvidelse oplyser intet), saa begge tilfaelde
|
|
980
|
+
// skal staa der — og det skal siges at ventetiden er forventet, ikke en fejl.
|
|
981
|
+
fix_steps.push(
|
|
982
|
+
extVersion === null
|
|
983
|
+
? '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 — ' +
|
|
985
|
+
'det er forventet og gaar over af sig selv. Alt andet virker imens. ' +
|
|
986
|
+
'Er den indlaest som "unpacked": koer `npx @agent360/browser-mcp install` og derefter ' +
|
|
987
|
+
'chrome://extensions → Agent360 Browser MCP → ↻ reload.'
|
|
988
|
+
: 'Opdatér udvidelsen: chrome://extensions → Agent360 Browser MCP → ↻ reload. ' +
|
|
989
|
+
'Er den indlaest som "unpacked", saa koer `npx @agent360/browser-mcp install` foerst.',
|
|
990
|
+
);
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
// ── "ingen forbindelse" og "ingen port endnu" er IKKE det samme ────────────
|
|
994
|
+
//
|
|
995
|
+
// FUNDET AF REVIEW 7/9. Foer porten blev doven, bandt hver server ved opstart, saa
|
|
996
|
+
// udvidelsen var altid forbundet naar dette vaerktoej blev kaldt — og `!active`
|
|
997
|
+
// betoed derfor paalideligt "udvidelsen kan ikke naas". Nu binder en chat foerst en
|
|
998
|
+
// port naar den bruger browseren, saa en HELT SUND chat der ikke har roert den kan
|
|
999
|
+
// staa uden forbindelse. Uden det her skel fik den `disconnected` + fix_steps der
|
|
1000
|
+
// bad brugeren geninstallere — og INSTRUCTIONS beder agenten viderebringe dem.
|
|
1001
|
+
// Vi ville altsaa fortaelle folk at deres installation var i stykker, fordi vi selv
|
|
1002
|
+
// endnu ikke havde aabnet doeren.
|
|
1003
|
+
//
|
|
1004
|
+
// Loesningen er ikke at binde en port her (et diagnose-vaerktoej skal ikke aendre
|
|
1005
|
+
// tilstand for at kunne maale den) — det er at sige praecis hvad der er tilfaeldet.
|
|
1006
|
+
const ingenPortEndnu = activePort === null;
|
|
1007
|
+
const verdict =
|
|
1008
|
+
exts.length > 1 ? 'conflict'
|
|
1009
|
+
: (serverOutdated || extOutdated) ? 'outdated'
|
|
1010
|
+
: (!active && ingenPortEndnu) ? 'idle'
|
|
1011
|
+
: !active ? 'disconnected'
|
|
1012
|
+
: (npmLatest === null ? 'unknown' : 'current');
|
|
1013
|
+
|
|
1014
|
+
const environment = {
|
|
1015
|
+
mcp_server_version: PKG_VERSION,
|
|
1016
|
+
npm_latest_version: npmLatest,
|
|
1017
|
+
server_up_to_date: serverOutdated === null ? null : !serverOutdated,
|
|
1018
|
+
extensions_connected: exts.map(c => ({
|
|
1019
|
+
name: c.name,
|
|
1020
|
+
version: c.version,
|
|
1021
|
+
extension_id: c.extensionId,
|
|
1022
|
+
active: c === active,
|
|
1023
|
+
})),
|
|
1024
|
+
extension_up_to_date: extOutdated === null ? null : !extOutdated,
|
|
1025
|
+
ws_port: activePort,
|
|
1026
|
+
node: process.version,
|
|
1027
|
+
platform: `${process.platform} ${process.arch}`,
|
|
1028
|
+
};
|
|
1029
|
+
|
|
1030
|
+
const issueBody = [
|
|
1031
|
+
what && `**What happened**\n${what}`,
|
|
1032
|
+
tool && `\n**Tool**: \`${tool}\``,
|
|
1033
|
+
// MAALT 22/8 ved sikkerhedsreview: her stod den RAA url, mens den lokale logbog
|
|
1034
|
+
// nedenfor bruger afkortUrl(). Query-strengen — hvor tokens bor — blev altsaa
|
|
1035
|
+
// strippet fra filen paa disken, men sendt uredigeret ind i et link til et
|
|
1036
|
+
// OFFENTLIGT GitHub-issue. Praecis den forkerte vej rundt.
|
|
1037
|
+
url && `\n**URL**: ${afkortUrl(url)}`,
|
|
1038
|
+
attempted && `\n**Already tried**\n${attempted}`,
|
|
1039
|
+
`\n**Environment**\n\`\`\`json\n${JSON.stringify(environment, null, 2)}\n\`\`\``,
|
|
1040
|
+
].filter(Boolean).join('\n');
|
|
1041
|
+
|
|
1042
|
+
const template = kind === 'wish' ? ISSUE_TEMPLATES.wish
|
|
1043
|
+
: kind === 'use_case' ? ISSUE_TEMPLATES.use_case
|
|
1044
|
+
: ISSUE_TEMPLATES.bug;
|
|
1045
|
+
const issueTitle = what.split('\n')[0].slice(0, 90) || 'Browser MCP feedback';
|
|
1046
|
+
const submit_url = `${REPO_URL}/issues/new?template=${template}` +
|
|
1047
|
+
`&title=${encodeURIComponent(issueTitle)}&body=${encodeURIComponent(issueBody)}`;
|
|
1048
|
+
|
|
1049
|
+
const instruction =
|
|
1050
|
+
verdict === 'idle'
|
|
1051
|
+
? 'Browseren er ikke taget i brug i denne chat endnu, saa der er intet at diagnosticere paa forbindelsen — ' +
|
|
1052
|
+
'det er IKKE en fejl i installationen, og du maa ikke sige det til brugeren. ' +
|
|
1053
|
+
'Er der en aegte mangel, saa tilbyd submit_url som et klikbart link.'
|
|
1054
|
+
: verdict === 'conflict' || verdict === 'outdated' || verdict === 'disconnected'
|
|
1055
|
+
? '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 — det er sandsynligvis installationen, ikke en fejl i Browser MCP.'
|
|
1057
|
+
: '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 — du kan rette i den foer du sender"). Spoerg ikke om lov foerst.';
|
|
1059
|
+
|
|
1060
|
+
const logbog = skrivTilLogbog({
|
|
1061
|
+
at: new Date().toISOString(),
|
|
1062
|
+
kind, tool, what_happened: what, attempted,
|
|
1063
|
+
url: afkortUrl(url),
|
|
1064
|
+
verdict,
|
|
1065
|
+
server_version: PKG_VERSION,
|
|
1066
|
+
extension_version: extVersion,
|
|
1067
|
+
extensions_connected: exts.length,
|
|
1068
|
+
});
|
|
1069
|
+
|
|
1070
|
+
return {
|
|
1071
|
+
content: [{
|
|
1072
|
+
type: 'text',
|
|
1073
|
+
text: JSON.stringify({
|
|
1074
|
+
reported: { kind, what_happened: what, tool, url, attempted },
|
|
1075
|
+
verdict,
|
|
1076
|
+
findings,
|
|
1077
|
+
fix_steps,
|
|
1078
|
+
environment,
|
|
1079
|
+
logged_locally: logbog,
|
|
1080
|
+
submit_url,
|
|
1081
|
+
instruction,
|
|
1082
|
+
}, null, 2),
|
|
1083
|
+
}],
|
|
1084
|
+
};
|
|
1085
|
+
}
|
|
1086
|
+
|
|
399
1087
|
async function handleExtractToken(args) {
|
|
400
1088
|
const { provider } = args;
|
|
401
1089
|
const info = PROVIDER_PAGES[provider];
|
|
@@ -431,10 +1119,11 @@ function gracefulShutdown(reason, code = 0) {
|
|
|
431
1119
|
// Stop timers so they can't re-enter gracefulShutdown
|
|
432
1120
|
if (parentCheck) clearInterval(parentCheck);
|
|
433
1121
|
if (heartbeat) clearInterval(heartbeat);
|
|
1122
|
+
if (tomgangsvagt) clearInterval(tomgangsvagt);
|
|
434
1123
|
|
|
435
1124
|
// Close WS with explicit close-frame so extension's onclose handler fires
|
|
436
|
-
|
|
437
|
-
try {
|
|
1125
|
+
for (const c of liveConnections()) {
|
|
1126
|
+
try { c.ws.close(1000, 'mcp-shutdown'); } catch {}
|
|
438
1127
|
}
|
|
439
1128
|
if (wss) try { wss.close(); } catch {}
|
|
440
1129
|
|
|
@@ -447,19 +1136,100 @@ process.on('SIGINT', () => gracefulShutdown('SIGINT'));
|
|
|
447
1136
|
process.on('exit', () => {
|
|
448
1137
|
// Safety net for direct process.exit calls that bypass gracefulShutdown
|
|
449
1138
|
if (wss) try { wss.close(); } catch {}
|
|
450
|
-
|
|
1139
|
+
for (const c of connections) try { c.ws.close(); } catch {}
|
|
451
1140
|
});
|
|
452
1141
|
|
|
453
|
-
//
|
|
454
|
-
//
|
|
1142
|
+
// ── Naar lukker serveren sin port? (MAALT 22/8) ──────────────────────────────
|
|
1143
|
+
//
|
|
1144
|
+
// Symptomet: tyve porte optaget, og ingen chats i live bag dem. Aarsagen var at
|
|
1145
|
+
// tjekket kiggede paa den FORKERTE proces. Kaeden er nemlig ikke to led, men fire:
|
|
1146
|
+
//
|
|
1147
|
+
// Claude Code → wrapper → npm exec → denne server
|
|
1148
|
+
//
|
|
1149
|
+
// `process.ppid` er `npm exec`, ikke Claude Code. Doer chatten, kan `npm exec`
|
|
1150
|
+
// blive haengende som foraeldreloes — og saa ser tjekket en levende foraelder for
|
|
1151
|
+
// evigt. Porten blev holdt i op til fire timer (idle-graensen), og med flere
|
|
1152
|
+
// forladte chats loeb spaendet fuldt.
|
|
1153
|
+
//
|
|
1154
|
+
// Rettelsen er at tjekke HELE kaeden op til roden, ikke kun naermeste led. Doer et
|
|
1155
|
+
// vilkaarligt led, er forbindelsen til den chat der ejer os brudt, og saa er vi
|
|
1156
|
+
// foraeldreloese uanset om vores naermeste foraelder stadig aander.
|
|
1157
|
+
//
|
|
1158
|
+
// Kaeden hentes én gang ved opstart (én ps-kommando), og derefter koster tjekket
|
|
1159
|
+
// 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 — daarligere, men aldrig
|
|
1161
|
+
// vaerre end foer.
|
|
1162
|
+
|
|
1163
|
+
// ps-opslaget bor her (det er en sideeffekt); selve kaede-logikken og doeds-dommen
|
|
1164
|
+
// ligger i vagt.js, saa de kan koeres i en test uden at starte en server.
|
|
1165
|
+
function laesPpid(pid) {
|
|
1166
|
+
try {
|
|
1167
|
+
const ud = execSync(`ps -o ppid= -p ${pid}`, {
|
|
1168
|
+
encoding: 'utf8',
|
|
1169
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
1170
|
+
timeout: 2000, // et haengende ps ville ellers blokere hele opstarten synkront
|
|
1171
|
+
});
|
|
1172
|
+
return Number(String(ud).trim());
|
|
1173
|
+
} catch {
|
|
1174
|
+
return null;
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
|
|
455
1178
|
const parentPid = process.ppid;
|
|
1179
|
+
|
|
1180
|
+
// MAALT 22/8: doer den naermeste foraelder FOER serveren er bootet (~2 sek node+SDK),
|
|
1181
|
+
// laeser process.ppid vaerdien 1 — altsaa launchd. Kaeden blev saa [1], og pid 1 er
|
|
1182
|
+
// baade udoedelig og ejet af root, saa vagten var enten inert eller draebte os selv
|
|
1183
|
+
// paa EPERM. En kaede der kun bestaar af pid 1 vogter ingenting og skal ikke bruges.
|
|
1184
|
+
let vagtKaede = parentPid > 1 ? [parentPid] : [];
|
|
1185
|
+
try {
|
|
1186
|
+
const k = forfaedreKaede(parentPid, laesPpid).filter((x) => x > 1);
|
|
1187
|
+
if (k.length) vagtKaede = k;
|
|
1188
|
+
} catch {}
|
|
1189
|
+
if (!vagtKaede.length) {
|
|
1190
|
+
process.stderr.write('[MCP] ingen brugbar foraelder-kaede (ppid=' + parentPid +
|
|
1191
|
+
') — falder tilbage paa idle-graensen alene\n');
|
|
1192
|
+
}
|
|
1193
|
+
process.stderr.write(`[MCP] vagt-kaede: ${vagtKaede.join(' → ')}\n`);
|
|
1194
|
+
|
|
456
1195
|
parentCheck = setInterval(() => {
|
|
1196
|
+
const doede = vagtKaede.filter((pid) => ledErDoedt(pid));
|
|
1197
|
+
if (!doede.length) return;
|
|
1198
|
+
|
|
1199
|
+
// ── Et doedt led betyder ikke automatisk at chatten er vaek (MAALT 23/8) ──────
|
|
1200
|
+
//
|
|
1201
|
+
// Kaeden blev frosset ved opstart. Men et MELLEMLED kan afslutte helt normalt
|
|
1202
|
+
// mens ejeren koerer videre — maalt to gange paa denne maskine, hvor kaeden gaar
|
|
1203
|
+
// npm exec → wrapper → claude → Code Helper (Plugin) → Code. Et forbigaaende led
|
|
1204
|
+
// der lukkede pænt udloeste "chatten bag denne server er vaek", mens chatten var
|
|
1205
|
+
// uroert. Og risikoen er ensrettet vaerre end 1.25.0, som vogtede ét pid: nu er
|
|
1206
|
+
// hvert af 5-6 led en ny doedsaarsag.
|
|
1207
|
+
//
|
|
1208
|
+
// 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 — vi er bare blevet
|
|
1210
|
+
// reparented, og den nye kaede overtager. Kun naar den vej ogsaa er vaek, lukker vi.
|
|
1211
|
+
//
|
|
1212
|
+
// Prisen er nul i normal drift: genlaesningen koerer kun i det tik hvor et led er
|
|
1213
|
+
// forsvundet, ikke hvert 5. sekund.
|
|
1214
|
+
let frisk = [];
|
|
457
1215
|
try {
|
|
458
|
-
process.
|
|
459
|
-
} catch {
|
|
460
|
-
|
|
1216
|
+
frisk = forfaedreKaede(process.ppid, laesPpid).filter((x) => x > 1 && !ledErDoedt(x));
|
|
1217
|
+
} catch {}
|
|
1218
|
+
|
|
1219
|
+
if (frisk.length) {
|
|
1220
|
+
process.stderr.write(
|
|
1221
|
+
`[MCP] led ${doede.join(', ')} er vaek, men kaeden gaar stadig op: ` +
|
|
1222
|
+
`${frisk.join(' → ')} — fortsaetter\n`,
|
|
1223
|
+
);
|
|
1224
|
+
vagtKaede = frisk;
|
|
1225
|
+
return;
|
|
461
1226
|
}
|
|
462
|
-
|
|
1227
|
+
|
|
1228
|
+
gracefulShutdown(
|
|
1229
|
+
`Proces ${doede[0]} i kaeden doede, og der er ingen levende vej op — ` +
|
|
1230
|
+
'chatten bag denne server er vaek',
|
|
1231
|
+
);
|
|
1232
|
+
}, 5000); // hvert 5. sekund
|
|
463
1233
|
|
|
464
1234
|
// Also listen for stdin close as backup
|
|
465
1235
|
process.stdin.on('end', () => gracefulShutdown('stdin closed'));
|