@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.
Binary file
Binary file
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "manifest_version": 3,
3
3
  "name": "Agent360 Browser MCP",
4
- "version": "1.25.0",
5
- "description": "Control your real Chrome from Claude Code navigate, click, fill, screenshot, solve CAPTCHAs. 34 tools, multi-session.",
4
+ "version": "1.28.1",
5
+ "description": "Give Claude Code your real logged-in Chrome. Two parts: this extension + one npx command. Multi-session, local, MIT.",
6
6
  "permissions": [
7
7
  "tabs",
8
8
  "tabGroups",
@@ -1,24 +1,99 @@
1
1
  /**
2
2
  * Offscreen Document — Persistent WebSocket bridge (multi-session)
3
3
  *
4
- * Scans port range 9876-9885 and maintains connections to ALL active
4
+ * Scans port range 9876-9895 and maintains connections to ALL active
5
5
  * MCP servers. Each Claude Code session gets its own port automatically.
6
6
  * Passes port ID with every command so background.js can track tab ownership.
7
7
  *
8
8
  * Flow: MCP Server(s) ←(WS)→ this ←(chrome.runtime.sendMessage)→ Service Worker → Chrome APIs
9
9
  */
10
10
 
11
+ // Et offscreen-dokument har IKKE chrome.runtime.getManifest() — kaldet kaster
12
+ // "chrome.runtime.getManifest is not a function". Versionen kommer derfor med i
13
+ // dokumentets egen URL, sat af background.js da dokumentet blev oprettet. Det er
14
+ // samtidig den rigtige semantik: et dokument oprettet af en aeldre udgave baerer
15
+ // den aeldre version, saa `offscreenSvarer()` kan se forskel paa "svarer" og
16
+ // "er den udgave vi koerer nu".
17
+ function minVersion() {
18
+ try { return new URLSearchParams(location.search).get('v') || null; } catch { return null; }
19
+ }
20
+
11
21
  const BASE_PORT = 9876;
12
22
  const MAX_PORT = 9895;
13
23
  const connections = new Map(); // port → WebSocket
14
24
 
15
- function scanPorts() {
16
- for (let port = BASE_PORT; port <= MAX_PORT; port++) {
17
- const existing = connections.get(port);
18
- if (existing && (existing.readyState === WebSocket.OPEN || existing.readyState === WebSocket.CONNECTING)) {
19
- continue;
25
+ // ── Hvorfor porte foerst PROBES med fetch, og ikke bare aabnes (MAALT 22/8) ──────
26
+ //
27
+ // Chrome bremser nye WebSocket-haandtryk. Formlen staar i Chromiums egen kilde
28
+ // (services/network/websocket_throttler.cc):
29
+ //
30
+ // forsinkelse = rand(1000..5000) ms × 2^min(p + f/(s+1), 16) / 65536
31
+ //
32
+ // p = haandtryk i luften lige nu · f = mislykkede · s = lykkedes. Bremsen er
33
+ // PER RENDERER-PROCES, ikke per adresse — at sprede sig over 20 porte hjaelper
34
+ // altsaa ingenting. Og den her funktion aabnede foer 20 WebSockets i ét smaek
35
+ // hvert 2. sekund, hvoraf de fleste var mod doede porte. Det satte baade p og f
36
+ // i vejret og laaste eksponenten paa loftet, saa HVER forbindelse betalte den
37
+ // maksimale straf paa op til 5 sekunder.
38
+ //
39
+ // Dertil kom vores egen faelde: connectTimeout stod paa 2000 ms — UNDER bremsens
40
+ // maksimum. Vi draebte altsaa systematisk forbindelser der bare stod og ventede,
41
+ // og Chromiums header siger det rent ud: at destruere en PendingConnection uden
42
+ // at haandtrykket er fuldfoert TAELLER SOM EN FEJL. Hvert drab gjorde bremsen
43
+ // haardere, hvilket draebte flere. En spiral vi selv drev.
44
+ //
45
+ // Maalt effekt: den 10. chat var 15,5 sekunder om at komme op, og 5 af 19 kom
46
+ // aldrig inden for 22 sekunder.
47
+ //
48
+ // Rettelsen er to ting:
49
+ // 1. Find levende porte med et almindeligt HTTP-kald. En ws-server svarer
50
+ // "426 Upgrade Required" paa et GET; en doed port afviser. HTTP-kald taeller
51
+ // IKKE med i WebSocket-bremsen — maalt: 100 fejlede HTTP-probes kostede nul,
52
+ // 400 fejlede WS-forsoeg kostede 14 sekunder. Vi aabner nu kun WebSockets
53
+ // mod porte vi VED der sidder en server paa, saa f falder til ~0.
54
+ // 2. connectTimeout haevet over bremsens 5-sekunders loft, saa vi ikke laengere
55
+ // draeber vores egne ventende forbindelser.
56
+ //
57
+ // Sidegevinst: det er sikrere. Foer aabnede udvidelsen blindt en WebSocket mod
58
+ // hvad der nu maatte lytte paa 9876-9895.
59
+ const PROBE_TIMEOUT_MS = 400;
60
+ let skanner = false;
61
+
62
+ async function harServer(port) {
63
+ try {
64
+ const svar = await fetch(`http://127.0.0.1:${port}/`, {
65
+ signal: AbortSignal.timeout(PROBE_TIMEOUT_MS),
66
+ cache: 'no-store',
67
+ });
68
+ return svar.status === 426; // ws-serverens svar paa et almindeligt GET
69
+ } catch {
70
+ return false; // afvist, timeout eller ingen der lytter
71
+ }
72
+ }
73
+
74
+ async function scanPorts() {
75
+ if (skanner) return; // skanningen er nu asynkron; undgaa overlap
76
+ skanner = true;
77
+ try {
78
+ const kandidater = [];
79
+ for (let port = BASE_PORT; port <= MAX_PORT; port++) {
80
+ const existing = connections.get(port);
81
+ if (existing && (existing.readyState === WebSocket.OPEN || existing.readyState === WebSocket.CONNECTING)) {
82
+ continue;
83
+ }
84
+ kandidater.push(port);
20
85
  }
21
- tryConnect(port);
86
+ if (!kandidater.length) return;
87
+
88
+ // Probes koeres parallelt — de er gratis i bremsens regnskab.
89
+ const levende = await Promise.all(
90
+ kandidater.map(async (port) => (await harServer(port)) ? port : null),
91
+ );
92
+ for (const port of levende) {
93
+ if (port !== null) tryConnect(port);
94
+ }
95
+ } finally {
96
+ skanner = false;
22
97
  }
23
98
  }
24
99
 
@@ -30,13 +105,54 @@ function tryConnect(port) {
30
105
  return;
31
106
  }
32
107
 
108
+ // MAALT 21/8: en og samme udvidelse stod med TO aabne forbindelser til den samme
109
+ // server. Aarsagen laa her: forbindelsen blev foerst skrevet i kortet i onopen.
110
+ // scanPorts koerer hvert 2. sekund og springer kun over hvis kortet allerede har
111
+ // en forbindelse der er OPEN eller CONNECTING — men i vinduet mellem `new WebSocket`
112
+ // og onopen stod der intet i kortet. Naeste scan lavede derfor endnu en. Den foerste
113
+ // blev forældreloes: aldrig lukket, aldrig i kortet, men fuldt aaben.
114
+ // Registrering med det samme lukker vinduet.
115
+ connections.set(port, ws);
116
+
117
+ // 8000, ikke 2000: Chromes bremse kan lovligt holde et haandtryk i op til 5000 ms,
118
+ // og at lukke ned foer det taeller som en fejl der goer bremsen haardere. Se blokken
119
+ // over scanPorts. En haengende port koster nu en plads i 8 sekunder — scanPorts
120
+ // springer allerede CONNECTING over, saa det blokerer intet.
33
121
  const connectTimeout = setTimeout(() => {
34
122
  if (ws.readyState !== WebSocket.OPEN) ws.close();
35
- }, 2000);
123
+ }, 8000);
36
124
 
37
125
  ws.onopen = () => {
38
126
  clearTimeout(connectTimeout);
39
- connections.set(port, ws);
127
+ connections.set(port, ws); // stadig vores? scanPorts har ikke lavet en nyere
128
+
129
+ // ── Identitets-haandtryk (MAALT 21/8) ──────────────────────────────────
130
+ // To udgaver af udvidelsen kan vaere indlaest i den samme Chrome samtidig —
131
+ // fx en "load unpacked"-kopi ved siden af en anden. Begge scanner de samme
132
+ // porte, saa BEGGE forbinder til hver eneste MCP-server. Serveren havde kun
133
+ // én socket-variabel, som hver ny forbindelse overskrev, saa kommandoerne
134
+ // landede hos den der forbandt sidst — vilkaarligt hvilken af de to. Den
135
+ // anden fortsatte med sit eget sessions-kort og sine egne fane-grupper.
136
+ // Haandtrykket giver serveren det den mangler for at kunne se at der er to,
137
+ // vaelge den nyeste, og sige det hoejt i stedet for at gaette i tavshed.
138
+ // MAALT 22/8 mod en aegte Chrome: haandtrykket ankom ALDRIG, selv om fetch-probet
139
+ // — som ligger i samme fil og samme dokument — virkede fint. Den tomme `catch {}`
140
+ // slugte aarsagen i tavshed, saa fejlen var usynlig baade for serveren og for os.
141
+ // To rettelser: haandtrykket sendes nu UANSET om manifest-opslaget lykkes (det er
142
+ // selve beskeden serveren har brug for, ikke felterne i den), og en fejl bliver
143
+ // logget i stedet for at forsvinde.
144
+ let hilsen = { type: 'hello', extensionId: null, version: minVersion(), name: null };
145
+ try {
146
+ hilsen = { type: 'hello', extensionId: chrome.runtime.id, version: minVersion(), name: null };
147
+ } catch (e) {
148
+ console.warn('[Offscreen] kunne ikke bygge haandtrykket:', e?.message || e);
149
+ }
150
+ try {
151
+ ws.send(JSON.stringify(hilsen));
152
+ } catch (e) {
153
+ console.warn('[Offscreen] kunne ikke sende haandtrykket:', e?.message || e);
154
+ }
155
+
40
156
  console.log(`[Offscreen] Connected to MCP server on port ${port} (${connections.size} total)`);
41
157
  updateStatus();
42
158
  };
@@ -44,13 +160,14 @@ function tryConnect(port) {
44
160
  ws.onmessage = async (event) => {
45
161
  let cmd;
46
162
  try { cmd = JSON.parse(event.data); } catch { return; }
47
- const { id, method, params } = cmd;
163
+ const { id, method, params, pid } = cmd;
48
164
 
49
165
  try {
50
166
  // Include port so background.js knows which session owns this command
51
167
  const result = await chrome.runtime.sendMessage({
52
168
  type: 'mcp_command',
53
169
  port,
170
+ pid,
54
171
  method,
55
172
  params: params || {},
56
173
  });
@@ -92,6 +209,21 @@ function updateStatus() {
92
209
  }).catch(() => {});
93
210
  }
94
211
 
212
+ // Svar paa hjerteslaget. Uden dette svar kan background.js ikke skelne et levende
213
+ // dokument fra et der findes men aldrig fik sit script indlaest — og saa bliver et
214
+ // doedt dokument aldrig erstattet.
215
+ chrome.runtime.onMessage.addListener((msg, _sender, sendResponse) => {
216
+ if (msg?.type !== 'bmcp_ping') return;
217
+ // MAALT 22/8: svaret var bare { ok: true }. Det fortalte om broen var I LIVE, ikke
218
+ // om den var OPDATERET — og det er to forskellige spoergsmaal. En bro fra en aeldre
219
+ // udgave svarer lige saa villigt paa ping, saa ensureOffscreen regnede den for rask
220
+ // og udskiftede den aldrig. Resultatet: kode-aendringer slog aldrig igennem uden en
221
+ // fuld genstart af Chrome, heller ikke efter "Genindlaes" paa chrome://extensions.
222
+ // Versionen med i svaret goer forskellen synlig.
223
+ sendResponse({ ok: true, version: minVersion() });
224
+ return true;
225
+ });
226
+
95
227
  // Listen for terminate signals from background.js (sent when last tab in a session closes)
96
228
  chrome.runtime.onMessage.addListener((msg) => {
97
229
  if (msg.type !== 'terminate_mcp_session' || typeof msg.port !== 'number') return;
@@ -37,6 +37,24 @@
37
37
  .color-purple { border-color: #a855f7; } .color-purple .session-header { color: #a855f7; }
38
38
  .color-cyan { border-color: #06b6d4; } .color-cyan .session-header { color: #06b6d4; }
39
39
  .color-orange { border-color: #f97316; } .color-orange .session-header { color: #f97316; }
40
+ /* Shown only while the MCP server is missing — the store can't install it for you */
41
+ .setup { display: none; background: #1e293b; border: 1px solid #334155; border-left: 3px solid #f59e0b; border-radius: 8px; padding: 10px; margin-bottom: 8px; }
42
+ .setup.show { display: block; }
43
+ .setup p { font-size: 11px; line-height: 1.5; color: #94a3b8; margin-bottom: 8px; }
44
+ .setup p b { color: #e2e8f0; }
45
+ .setup code { display: block; background: #0f172a; border-radius: 6px; padding: 8px; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 10px; color: #22c55e; word-break: break-all; margin-bottom: 6px; }
46
+ .setup .row { display: flex; gap: 6px; }
47
+ .setup .row button, .setup .row a { flex: 1; margin: 0; padding: 6px; border-radius: 6px; background: #334155; color: #e2e8f0; font-size: 11px; text-align: center; text-decoration: none; border: none; cursor: pointer; }
48
+ .setup .row a:hover, .setup .row button:hover { background: #475569; }
49
+ /* Shown once both halves are up but the agent has not touched a tab yet — the
50
+ moment a new user checks "did it work?" and needs to know what to say next. */
51
+ .try { display: none; background: #1e293b; border: 1px solid #334155; border-left: 3px solid #22c55e; border-radius: 8px; padding: 10px; margin-bottom: 8px; }
52
+ .try.show { display: block; }
53
+ .try p { font-size: 11px; line-height: 1.5; color: #94a3b8; margin-bottom: 8px; }
54
+ .try p b { color: #e2e8f0; }
55
+ .try .prompt { display: block; background: #0f172a; border-radius: 6px; padding: 8px; font-size: 11px; color: #e2e8f0; margin-bottom: 6px; font-style: italic; }
56
+ .try a { display: block; text-align: center; padding: 6px; border-radius: 6px; background: #334155; color: #e2e8f0; font-size: 11px; text-decoration: none; }
57
+ .try a:hover { background: #475569; }
40
58
  </style>
41
59
  </head>
42
60
  <body>
@@ -44,12 +62,28 @@
44
62
 
45
63
  <div class="status">
46
64
  <div class="dot off" id="dot"></div>
47
- <span class="label" id="label">Checking...</span>
65
+ <span class="label" id="label">Checking…</span>
66
+ </div>
67
+
68
+ <div class="setup" id="setup">
69
+ <p><b>This extension is only half of Browser MCP.</b> It needs a local MCP server to talk to — the Chrome Web Store cannot install that part. Register it once, then restart your agent. Claude Code:</p>
70
+ <code id="setupCmd">claude mcp add --scope user browser-mcp -- npx @agent360/browser-mcp@latest</code>
71
+ <div class="row">
72
+ <button id="copyCmd">Copy command</button>
73
+ <a href="https://browsermcp.dev/docs/install-claude-code/" target="_blank" rel="noopener">Setup guide</a>
74
+ </div>
75
+ </div>
76
+
77
+ <div class="try" id="try">
78
+ <p><b>Connected — you're ready.</b> Nothing happens until you ask your agent for something. Try saying:</p>
79
+ <span class="prompt">"Take a screenshot of my current Chrome tab."</span>
80
+ <span class="prompt">"Open my Gmail tab and tell me who sent my last 3 emails."</span>
81
+ <a href="https://browsermcp.dev/#try" target="_blank" rel="noopener">More things to try</a>
48
82
  </div>
49
83
 
50
84
  <div class="section-title">Sessions</div>
51
85
  <div id="sessions" class="sessions">
52
- <div class="empty">No active sessions</div>
86
+ <div class="empty">No tabs claimed yet</div>
53
87
  </div>
54
88
 
55
89
  <div class="section-title">Action Log</div>
@@ -1,13 +1,30 @@
1
1
  // ── Status ──────────────────────────────────────────────────────────────────
2
2
 
3
- chrome.storage.local.get(['mcpConnected', 'mcpCount', 'mcpPorts'], (result) => {
4
- const connected = result.mcpConnected === true;
5
- const count = result.mcpCount || 0;
3
+ // mcpCount = agent clients on the socket. `sessions` = agents that have actually claimed a
4
+ // tab. They differ, and showing "Connected — 1 session" above "No active sessions" read as a
5
+ // contradiction at exactly the moment a new user checks whether the install worked.
6
+ function paintStatus(connected, count, sessionCount) {
6
7
  document.getElementById('dot').className = `dot ${connected ? 'on' : 'off'}`;
7
- document.getElementById('label').textContent = connected
8
- ? `Forbundet til ${count} session${count > 1 ? 's' : ''}`
9
- : 'Ikke forbundet';
10
- });
8
+ document.getElementById('label').textContent = !connected
9
+ ? 'Not connected no MCP server found'
10
+ : sessionCount > 0
11
+ ? `Connected — ${sessionCount} active session${sessionCount === 1 ? '' : 's'}`
12
+ : `Connected — ready (${count} agent${count === 1 ? '' : 's'} listening)`;
13
+ // The Chrome Web Store can only install the extension. If nothing is listening on
14
+ // ports 9876-9895, the user almost certainly never ran the npx install — say so.
15
+ document.getElementById('setup').classList.toggle('show', !connected);
16
+ // Connected but idle: they got it working and now need to know what to say.
17
+ document.getElementById('try').classList.toggle('show', connected && sessionCount === 0);
18
+ }
19
+
20
+ function refreshStatus() {
21
+ chrome.storage.local.get({ mcpConnected: false, mcpCount: 0, sessions: {} }, (result) => {
22
+ paintStatus(result.mcpConnected === true, result.mcpCount || 0,
23
+ Object.keys(result.sessions || {}).length);
24
+ });
25
+ }
26
+
27
+ refreshStatus();
11
28
 
12
29
  // ── Sessions with tabs ─────────────────────────────────────────────────────
13
30
 
@@ -17,7 +34,7 @@ function renderSessions() {
17
34
  const entries = Object.entries(sessions);
18
35
 
19
36
  if (!entries.length) {
20
- container.innerHTML = '<div class="empty">Ingen aktive sessions</div>';
37
+ container.innerHTML = '<div class="empty">No tabs claimed yet</div>';
21
38
  return;
22
39
  }
23
40
 
@@ -40,7 +57,7 @@ function renderSessions() {
40
57
  const color = session.color || 'blue';
41
58
  const tabHtml = tabInfos.length
42
59
  ? tabInfos.map(u => `<div>• ${u}</div>`).join('')
43
- : '<div>Ingen tabs</div>';
60
+ : '<div>No tabs</div>';
44
61
  return `
45
62
  <div class="session-card color-${color}">
46
63
  <div class="session-header">${session.label} <span style="font-weight:normal;font-size:10px;color:#64748b">port ${port}</span></div>
@@ -59,11 +76,11 @@ function renderLog() {
59
76
  chrome.storage.local.get({ actionLog: [] }, ({ actionLog }) => {
60
77
  const container = document.getElementById('log');
61
78
  if (!actionLog.length) {
62
- container.innerHTML = '<div class="empty">Ingen actions endnu</div>';
79
+ container.innerHTML = '<div class="empty">No actions yet</div>';
63
80
  return;
64
81
  }
65
82
  container.innerHTML = actionLog.slice(0, 30).map(entry => {
66
- const time = new Date(entry.time).toLocaleTimeString('da-DK', { hour: '2-digit', minute: '2-digit', second: '2-digit' });
83
+ const time = new Date(entry.time).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' });
67
84
  const cat = entry.category || 'safe';
68
85
  const cls = cat === 'sensitive' ? 'log-sensitive' : 'log-safe';
69
86
  return `<div class="log-entry"><span class="log-time">${time}</span><span class="log-method ${cls}">${entry.method}</span><span class="log-session">${entry.session || ''}</span></div>`;
@@ -77,20 +94,19 @@ renderLog();
77
94
 
78
95
  document.getElementById('reconnect').addEventListener('click', () => {
79
96
  chrome.runtime.sendMessage({ type: 'reconnect' });
80
- document.getElementById('label').textContent = 'Reconnecting...';
97
+ document.getElementById('label').textContent = 'Reconnecting';
81
98
  setTimeout(() => {
82
- chrome.storage.local.get(['mcpConnected', 'mcpCount'], (result) => {
83
- const connected = result.mcpConnected === true;
84
- const count = result.mcpCount || 0;
85
- document.getElementById('dot').className = `dot ${connected ? 'on' : 'off'}`;
86
- document.getElementById('label').textContent = connected
87
- ? `Forbundet til ${count} session${count > 1 ? 's' : ''}`
88
- : 'Ikke forbundet';
89
- renderSessions();
90
- });
99
+ refreshStatus();
100
+ renderSessions();
91
101
  }, 3000);
92
102
  });
93
103
 
104
+ document.getElementById('copyCmd').addEventListener('click', (e) => {
105
+ navigator.clipboard.writeText(document.getElementById('setupCmd').textContent.trim());
106
+ e.target.textContent = 'Copied!';
107
+ setTimeout(() => { e.target.textContent = 'Copy command'; }, 1500);
108
+ });
109
+
94
110
  document.getElementById('clearLog').addEventListener('click', () => {
95
111
  chrome.storage.local.set({ actionLog: [] }, renderLog);
96
112
  });