chrome-ai-bridge 2.4.0 → 2.5.2

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 CHANGED
@@ -3,7 +3,7 @@
3
3
  [![npm](https://img.shields.io/npm/v/chrome-ai-bridge.svg)](https://npmjs.org/package/chrome-ai-bridge)
4
4
  [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](LICENSE)
5
5
 
6
- > **⚠️ Requires Chrome Extension** — This MCP server controls ChatGPT/Gemini tabs via a browser extension.
6
+ > **Requires Chrome Extension** — This CLI tool controls ChatGPT/Gemini tabs via a browser extension.
7
7
 
8
8
  Let your AI assistant (Claude Code, Cursor, etc.) consult ChatGPT and Gemini for second opinions.
9
9
 
@@ -12,20 +12,20 @@ Let your AI assistant (Claude Code, Cursor, etc.) consult ChatGPT and Gemini for
12
12
  ## How it works
13
13
 
14
14
  ```
15
- Your AI Assistant → MCP Server → Chrome Extension → ChatGPT/Gemini tabs
15
+ Your AI Assistant → cab CLI / daemon → Chrome Extension → ChatGPT/Gemini tabs
16
16
  ```
17
17
 
18
- 1. **Chrome Extension** (you install) bridges MCP server and browser
19
- 2. **MCP Server** (npm package) receives requests from your AI assistant
18
+ 1. **Chrome Extension** (you install) bridges the daemon and browser
19
+ 2. **`cab` CLI / daemon** (npm package) receives requests from your AI assistant via REST API
20
20
  3. **Extension** controls ChatGPT/Gemini tabs via CDP (Chrome DevTools Protocol)
21
21
 
22
- **Why an extension?** ChatGPT and Gemini don't have public APIs. The extension automates the web UI while you stay logged in.
22
+ **Why an extension?** ChatGPT and Gemini don't have free public APIs. The extension automates the web UI while you stay logged in.
23
23
 
24
24
  ---
25
25
 
26
26
  ## What is this?
27
27
 
28
- chrome-ai-bridge is a [Model Context Protocol](https://modelcontextprotocol.io/) server that gives AI assistants the ability to:
28
+ chrome-ai-bridge is a CLI tool and daemon that gives AI assistants the ability to:
29
29
 
30
30
  - **Consult other AIs**: Ask ChatGPT and Gemini questions via browser
31
31
  - **Get multiple perspectives**: Query both AIs in parallel for second opinions
@@ -71,7 +71,7 @@ Network extraction is the primary path. DOM extraction remains as an automatic f
71
71
 
72
72
  ## Quick Start
73
73
 
74
- > **⚠️ Both steps are required** — The extension and MCP server work together.
74
+ > **Both steps are required** — The extension and daemon work together.
75
75
 
76
76
  ### Step 1: Install Chrome Extension
77
77
 
@@ -95,30 +95,23 @@ Then load the extension in Chrome:
95
95
 
96
96
  You should see "Chrome AI Bridge" appear in your extensions list.
97
97
 
98
- ### Step 2: Configure your MCP client
98
+ ### Step 2: Install the `cab` CLI
99
99
 
100
- **For Claude Code** (`~/.claude.json`):
101
-
102
- ```json
103
- {
104
- "mcpServers": {
105
- "chrome-ai-bridge": {
106
- "command": "npx",
107
- "args": ["chrome-ai-bridge@latest"]
108
- }
109
- }
110
- }
100
+ ```bash
101
+ npm install -g chrome-ai-bridge
111
102
  ```
112
103
 
113
104
  ### Step 3: Connect the Extension
114
105
 
115
106
  1. Open ChatGPT (https://chatgpt.com) or Gemini (https://gemini.google.com) in Chrome
116
107
  2. Log in to both services
117
- 3. The extension will automatically connect when the MCP server starts
108
+ 3. The extension will automatically connect when the daemon starts
118
109
 
119
110
  ### Step 4: Verify it works
120
111
 
121
- Restart your AI client and try: `"Ask ChatGPT how to implement OAuth in Node.js"`
112
+ ```bash
113
+ cab ask chatgpt "How do I implement OAuth in Node.js?"
114
+ ```
122
115
 
123
116
  ---
124
117
 
@@ -187,7 +180,7 @@ User: "Ask ChatGPT specifically about this"
187
180
 
188
181
  | Variable | Description |
189
182
  |----------|-------------|
190
- | `MCP_DISABLE_WEB_LLM` | Set `true` to disable ChatGPT/Gemini tools |
183
+ | `CAI_DISABLE_WEB_LLM` | Set `true` to disable ChatGPT/Gemini tools |
191
184
 
192
185
  ---
193
186
 
@@ -201,17 +194,10 @@ cd chrome-ai-bridge
201
194
  npm install && npm run build
202
195
  ```
203
196
 
204
- Configure `~/.claude.json` to use local build:
205
-
206
- ```json
207
- {
208
- "mcpServers": {
209
- "chrome-ai-bridge": {
210
- "command": "node",
211
- "args": ["/path/to/chrome-ai-bridge/scripts/cli.mjs"]
212
- }
213
- }
214
- }
197
+ For local development, run the daemon directly:
198
+
199
+ ```bash
200
+ node /path/to/chrome-ai-bridge/scripts/cli.mjs
215
201
  ```
216
202
 
217
203
  ### Commands
@@ -230,7 +216,7 @@ chrome-ai-bridge/
230
216
  ├── src/
231
217
  │ ├── fast-cdp/ # CDP client and AI chat logic
232
218
  │ ├── extension/ # Chrome extension source
233
- │ ├── main.ts # MCP server entry point
219
+ │ ├── main.ts # Daemon entry point
234
220
  │ └── index.ts # Main exports
235
221
  ├── scripts/
236
222
  │ └── cli.mjs # CLI entry point
@@ -261,7 +247,7 @@ npm run cdp:gemini
261
247
  | Guide | Description |
262
248
  |-------|-------------|
263
249
  | [Technical Spec](docs/SPEC.md) | Detailed architecture and implementation |
264
- | [Setup Guide](docs/user/setup.md) | Detailed MCP configuration |
250
+ | [Setup Guide](docs/user/setup.md) | Detailed setup and configuration |
265
251
  | [Troubleshooting](docs/user/troubleshooting.md) | Problem solving |
266
252
  | [CI Policy](docs/ci-policy.md) | Required checks and browser E2E lane policy |
267
253
  | [Technical Spec - Architecture](docs/SPEC.md#1-architecture-overview) | Extension architecture |
@@ -276,10 +262,12 @@ npm run cdp:gemini
276
262
  2. Verify ChatGPT/Gemini tabs are open and logged in
277
263
  3. Check the extension popup for connection status
278
264
 
279
- ### MCP server not responding
265
+ ### Daemon not responding
280
266
 
281
267
  ```bash
282
- npx clear-npx-cache && npx chrome-ai-bridge@latest
268
+ cab status
269
+ # or restart:
270
+ cab daemon restart
283
271
  ```
284
272
 
285
273
  ### ChatGPT/Gemini not responding
@@ -295,9 +283,9 @@ npx clear-npx-cache && npx chrome-ai-bridge@latest
295
283
  ## Architecture (v2.0.0)
296
284
 
297
285
  ```
298
- ┌─────────────────┐ MCP ┌──────────────────┐
299
- │ Claude Code │ ◀──────────────────▶│ MCP Server
300
- │ (MCP Client) │ │ (Node.js) │
286
+ ┌─────────────────┐ REST API ┌──────────────────┐
287
+ │ Claude Code │ ◀──────────────────▶│ Chrome AI Bridge
288
+ │ (cab CLI) │ │ (Node.js) │
301
289
  └─────────────────┘ └────────┬─────────┘
302
290
 
303
291
 
@@ -1,17 +1,17 @@
1
1
  # chrome-ai-bridge Extension
2
2
 
3
- このChrome拡張機能は、chrome-ai-bridge MCPサーバーとChromeブラウザのタブを接続します。
3
+ このChrome拡張機能は、chrome-ai-bridge サーバーとChromeブラウザのタブを接続します。
4
4
 
5
5
  ## アーキテクチャ
6
6
 
7
7
  ```
8
- chrome-ai-bridge MCPサーバー (プロセス1)
8
+ chrome-ai-bridge サーバー (プロセス1)
9
9
  ↓ WebSocket
10
10
  Extension (TabShareExtension)
11
11
  ↓ chrome.debugger API
12
12
  Chrome Tab #101 (ChatGPT)
13
13
 
14
- chrome-ai-bridge MCPサーバー (プロセス2)
14
+ chrome-ai-bridge サーバー (プロセス2)
15
15
  ↓ WebSocket
16
16
  Extension (TabShareExtension)
17
17
  ↓ chrome.debugger API
@@ -66,10 +66,10 @@ npm run build
66
66
  }
67
67
  ```
68
68
 
69
- ### MCPサーバー起動
69
+ ### サーバー起動
70
70
 
71
71
  1. Claude Codeを起動
72
- 2. MCPサーバーが自動的に起動し、WebSocket Relayサーバーが立ち上がります
72
+ 2. サーバーが自動的に起動し、WebSocket Relayサーバーが立ち上がります
73
73
  3. ログに以下のようなメッセージが表示されます:
74
74
 
75
75
  ```
@@ -112,7 +112,7 @@ chrome-extension://[EXTENSION_ID]/ui/connect.html?mcpRelayUrl=ws://127.0.0.1:123
112
112
 
113
113
  ### 接続確認
114
114
 
115
- MCPサーバーのログに以下のメッセージが表示されれば成功:
115
+ サーバーのログに以下のメッセージが表示されれば成功:
116
116
 
117
117
  ```
118
118
  [Extension Bridge] Extension connected to tab 101
@@ -128,8 +128,8 @@ src/extension/
128
128
  ├── ui/
129
129
  │ ├── connect.html # タブ選択UI
130
130
  │ └── connect.js # UIロジック
131
- ├── relay-server.ts # WebSocketサーバー (MCPサーバー側)
132
- └── extension-transport.ts # Puppeteer Transport実装 (MCPサーバー側)
131
+ ├── relay-server.ts # WebSocketサーバー (サーバー側)
132
+ └── extension-transport.ts # Puppeteer Transport実装 (サーバー側)
133
133
  ```
134
134
 
135
135
  ## トラブルシューティング
@@ -144,10 +144,10 @@ src/extension/
144
144
 
145
145
  ### Invalid token
146
146
 
147
- **原因**: MCPサーバーのトークンが一致しない
147
+ **原因**: サーバーのトークンが一致しない
148
148
 
149
149
  **解決策**:
150
- 1. MCPサーバーのログからトークンを確認
150
+ 1. サーバーのログからトークンを確認
151
151
  2. URLパラメータに正しいトークンを含める
152
152
 
153
153
  ### Tab not found
@@ -269,7 +269,7 @@ class TabShareExtension {
269
269
  case 'connectToRelay':
270
270
  this._connectToRelay(
271
271
  sender.tab?.id,
272
- message.mcpRelayUrl,
272
+ message.relayUrl,
273
273
  message.sessionId,
274
274
  ).then(
275
275
  () => sendResponse({success: true}),
@@ -292,7 +292,7 @@ class TabShareExtension {
292
292
  sender.tab?.id,
293
293
  message.tabId || sender.tab?.id,
294
294
  message.windowId || sender.tab?.windowId,
295
- message.mcpRelayUrl,
295
+ message.relayUrl,
296
296
  message.tabUrl,
297
297
  message.newTab,
298
298
  message.sessionId,
@@ -331,8 +331,8 @@ class TabShareExtension {
331
331
  return `selector:${selectorTabId}`;
332
332
  }
333
333
 
334
- async _connectToRelay(selectorTabId, mcpRelayUrl, sessionId) {
335
- if (!mcpRelayUrl) {
334
+ async _connectToRelay(selectorTabId, relayUrl, sessionId) {
335
+ if (!relayUrl) {
336
336
  logError('relay', 'Missing relay URL');
337
337
  throw new Error('Missing relay URL');
338
338
  }
@@ -344,10 +344,10 @@ class TabShareExtension {
344
344
  this._pendingTabSelection.delete(pendingKey);
345
345
  ensureKeepAliveAlarm('replace-stale-pending');
346
346
  }
347
- logInfo('relay', 'Connecting to relay', {mcpRelayUrl, selectorTabId, sessionId, pendingKey});
347
+ logInfo('relay', 'Connecting to relay', {relayUrl, selectorTabId, sessionId, pendingKey});
348
348
 
349
349
  const openSocket = async attempt => {
350
- const socket = new WebSocket(mcpRelayUrl);
350
+ const socket = new WebSocket(relayUrl);
351
351
  await new Promise((resolve, reject) => {
352
352
  let settled = false;
353
353
  const finish = (handler) => {
@@ -393,7 +393,7 @@ class TabShareExtension {
393
393
  let lastError;
394
394
  const maxAttempts = 5;
395
395
  for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
396
- logDebug('relay', `WebSocket attempt ${attempt + 1}/${maxAttempts}`, {mcpRelayUrl});
396
+ logDebug('relay', `WebSocket attempt ${attempt + 1}/${maxAttempts}`, {relayUrl});
397
397
  try {
398
398
  socket = await openSocket(attempt);
399
399
  logInfo('relay', 'WebSocket connected', {attempt: attempt + 1});
@@ -428,7 +428,7 @@ class TabShareExtension {
428
428
  selectorTabId,
429
429
  tabId,
430
430
  windowId,
431
- mcpRelayUrl,
431
+ relayUrl,
432
432
  tabUrl,
433
433
  newTab,
434
434
  sessionId,
@@ -482,14 +482,14 @@ class TabShareExtension {
482
482
 
483
483
  const pending = this._pendingTabSelection.get(pendingKey);
484
484
  if (!pending) {
485
- logDebug('connect', 'No pending connection, creating relay', {selectorTabId, sessionId, mcpRelayUrl});
485
+ logDebug('connect', 'No pending connection, creating relay', {selectorTabId, sessionId, relayUrl});
486
486
  // If no pending connection, create one now.
487
- await this._connectToRelay(selectorTabId, mcpRelayUrl, sessionId);
487
+ await this._connectToRelay(selectorTabId, relayUrl, sessionId);
488
488
  }
489
489
  const newPending = this._pendingTabSelection.get(pendingKey);
490
490
  if (!newPending) {
491
- logError('connect', 'No active MCP relay connection');
492
- throw new Error('No active MCP relay connection');
491
+ logError('connect', 'No active relay connection');
492
+ throw new Error('No active relay connection');
493
493
  }
494
494
 
495
495
  if (
@@ -521,7 +521,7 @@ class TabShareExtension {
521
521
  this._tabSessionOwners.set(tabId, sessionId || `selector:${selectorTabId}`);
522
522
  logInfo('connect', 'Tab connected successfully', {tabId, windowId, sessionId});
523
523
  ensureKeepAliveAlarm('tab-connected');
524
- // バッジのみ設定(フォーカスはMCPサーバー側が必要に応じて制御)
524
+ // バッジのみ設定(フォーカスはサーバー側が必要に応じて制御)
525
525
  await this._setConnectedTab(tabId, true);
526
526
  }
527
527
 
@@ -601,6 +601,9 @@ class TabShareExtension {
601
601
  async _getDebugLogs(filter, limit) {
602
602
  const result = await chrome.storage.local.get('logs');
603
603
  const rawLogs = Array.isArray(result.logs) ? result.logs : [];
604
+ const toIso = value => (typeof value === 'number' && value > 0
605
+ ? new Date(value).toISOString()
606
+ : null);
604
607
  const normalized = rawLogs.map(logEntry => ({
605
608
  ts: logEntry.timestamp || logEntry.ts || new Date().toISOString(),
606
609
  category: logEntry.category || 'unknown',
@@ -628,6 +631,25 @@ class TabShareExtension {
628
631
  activeConnections: Array.from(this._activeConnections.keys()),
629
632
  pendingTabSelection: Array.from(this._pendingTabSelection.keys()),
630
633
  tabSessionOwners: Object.fromEntries(this._tabSessionOwners.entries()),
634
+ discovery: {
635
+ mode: discoveryMode,
636
+ intervalMs: getDiscoveryIntervalMs(),
637
+ isRunning: isDiscoveryRunning,
638
+ hasScheduledTick: discoveryIntervalId !== null,
639
+ emptyDiscoveryStreak,
640
+ lastSuccessfulPort,
641
+ lastTickStartedAt: toIso(lastDiscoveryTickStartedAt),
642
+ lastTickFinishedAt: toIso(lastDiscoveryTickFinishedAt),
643
+ lastTickDurationMs: lastDiscoveryTickDurationMs,
644
+ lastSummary: lastDiscoverySummary,
645
+ lastError: lastDiscoveryError,
646
+ lastRelayProbeAt: toIso(lastRelayProbeAt),
647
+ lastRelayProbePort,
648
+ lastRelayProbeStatus,
649
+ lastRelayProbeError,
650
+ keepAliveActive,
651
+ lastKeepAliveAlarmAt: toIso(lastKeepAliveAlarmAt),
652
+ },
631
653
  },
632
654
  };
633
655
  }
@@ -717,7 +739,7 @@ class TabShareExtension {
717
739
 
718
740
  const tabShareExtension = new TabShareExtension();
719
741
 
720
- const DISCOVERY_ALARM = 'mcp-relay-discovery';
742
+ const DISCOVERY_ALARM = 'cab-relay-discovery';
721
743
  const KEEPALIVE_ALARM = 'keepAlive';
722
744
  const KEEPALIVE_PERIOD_MINUTES = 0.5;
723
745
  const DISCOVERY_PORTS = [38765, 38766, 38767, 38768, 38769, 38770, 38771, 38772, 38773, 38774, 38775];
@@ -745,6 +767,16 @@ let isDiscoveryRunning = false;
745
767
  let discoveryMode = DISCOVERY_MODE.FAST;
746
768
  let emptyDiscoveryStreak = 0;
747
769
  let keepAliveActive = false;
770
+ let lastDiscoveryTickStartedAt = 0;
771
+ let lastDiscoveryTickFinishedAt = 0;
772
+ let lastDiscoveryTickDurationMs = null;
773
+ let lastDiscoverySummary = null;
774
+ let lastDiscoveryError = null;
775
+ let lastRelayProbeAt = 0;
776
+ let lastRelayProbePort = null;
777
+ let lastRelayProbeStatus = null;
778
+ let lastRelayProbeError = null;
779
+ let lastKeepAliveAlarmAt = 0;
748
780
 
749
781
  // リロード時クールダウン: 5秒間は「新しいrelay」検出をスキップ
750
782
  // ただし reloadExtension コマンド経由のリロード時はスキップしない
@@ -855,7 +887,7 @@ function buildConnectUrl(
855
887
  sessionId,
856
888
  allowTabTakeover = false,
857
889
  ) {
858
- const params = new URLSearchParams({mcpRelayUrl: wsUrl});
890
+ const params = new URLSearchParams({relayUrl: wsUrl});
859
891
  if (tabUrl) params.set('tabUrl', tabUrl);
860
892
  if (newTab) params.set('newTab', 'true');
861
893
  if (autoMode) params.set('auto', 'true');
@@ -915,16 +947,34 @@ async function ensureConnectUiTab(
915
947
  async function fetchRelayInfo(port, timeoutMs = 800) {
916
948
  const discoveryUrl = `http://127.0.0.1:${port}/relay-info`;
917
949
  let timer = null;
950
+ lastRelayProbeAt = Date.now();
951
+ lastRelayProbePort = port;
918
952
  try {
919
953
  const controller = new AbortController();
920
954
  timer = setTimeout(() => controller.abort(), timeoutMs);
921
955
  const res = await fetch(discoveryUrl, {signal: controller.signal});
922
- if (!res.ok) return null;
956
+ if (!res.ok) {
957
+ lastRelayProbeStatus = `http-${res.status}`;
958
+ lastRelayProbeError = null;
959
+ return null;
960
+ }
923
961
  const data = await res.json();
924
- if (!data?.wsUrl) return null;
962
+ if (!data?.wsUrl) {
963
+ lastRelayProbeStatus = 'invalid-payload';
964
+ lastRelayProbeError = null;
965
+ return null;
966
+ }
967
+ lastRelayProbeStatus = 'ok';
968
+ lastRelayProbeError = null;
925
969
  lastSuccessfulPort = port;
926
970
  return data;
927
- } catch {
971
+ } catch (error) {
972
+ const message =
973
+ error && typeof error === 'object' && 'message' in error
974
+ ? String(error.message)
975
+ : String(error);
976
+ lastRelayProbeStatus = 'fetch-error';
977
+ lastRelayProbeError = message;
928
978
  return null;
929
979
  } finally {
930
980
  if (timer) {
@@ -958,7 +1008,7 @@ async function autoConnectRelay(best) {
958
1008
  let targetTabId;
959
1009
  try {
960
1010
  // autoConnectRelay経由の場合はフォーカスしない(active: false)
961
- // newTab: relay の要求を尊重する(MCP サーバーが newTab: true を指定した場合は新規タブ作成を許可)
1011
+ // newTab: relay の要求を尊重する(サーバーが newTab: true を指定した場合は新規タブ作成を許可)
962
1012
  targetTabId = await tabShareExtension._resolveTabId(
963
1013
  tabUrl,
964
1014
  preferredTabId,
@@ -1040,7 +1090,7 @@ async function autoOpenConnectUi() {
1040
1090
  failureCount: 0,
1041
1091
  };
1042
1092
 
1043
- // リロード直後はタブを開かない(既存MCPサーバーとの再接続を防ぐ)
1093
+ // リロード直後はタブを開かない(既存サーバーとの再接続を防ぐ)
1044
1094
  // ただし reloadExtension コマンド経由の場合はスキップしない
1045
1095
  const elapsed = Date.now() - extensionStartTime;
1046
1096
  if (elapsed < COOLDOWN_MS && !cooldownDisabled) {
@@ -1081,7 +1131,7 @@ async function autoOpenConnectUi() {
1081
1131
  }
1082
1132
 
1083
1133
  // Deduplicate: when multiple relays serve the same tabUrl, prefer the newest (by startedAt)
1084
- // This prevents stale MCP servers from stealing connections from active ones
1134
+ // This prevents stale servers from stealing connections from active ones
1085
1135
  const bestByTabUrl = new Map();
1086
1136
  for (const relay of newRelays) {
1087
1137
  const tabUrl = relay.data?.tabUrl || '';
@@ -1157,9 +1207,9 @@ async function autoOpenConnectUi() {
1157
1207
  return result;
1158
1208
  }
1159
1209
 
1160
- // Discovery is now passive - only triggered by MCP server requests
1210
+ // Discovery is now passive - only triggered by Chrome AI Bridge requests
1161
1211
  // The extension no longer auto-opens tabs on install/startup
1162
- // MCPサーバーからの明示的な接続要求時のみ動作する
1212
+ // サーバーからの明示的な接続要求時のみ動作する
1163
1213
 
1164
1214
  // Clear any existing discovery alarms from previous sessions
1165
1215
  // This prevents leftover alarms from auto-opening tabs
@@ -1223,21 +1273,38 @@ function scheduleDiscoveryTick(delayMs) {
1223
1273
  }
1224
1274
 
1225
1275
  isDiscoveryRunning = true;
1276
+ lastDiscoveryTickStartedAt = Date.now();
1277
+ lastDiscoveryError = null;
1278
+ lastDiscoverySummary = null;
1226
1279
  try {
1227
1280
  const result = await autoOpenConnectUi();
1281
+ lastDiscoverySummary = {
1282
+ ...result,
1283
+ mode: discoveryMode,
1284
+ intervalMs: getDiscoveryIntervalMs(),
1285
+ };
1228
1286
  updateDiscoveryMode(result);
1229
1287
  } catch (error) {
1288
+ lastDiscoveryError =
1289
+ error && typeof error === 'object' && 'message' in error
1290
+ ? String(error.message)
1291
+ : String(error);
1230
1292
  logWarn('discovery', 'Discovery cycle failed', {
1231
- error: error?.message || String(error),
1293
+ error: lastDiscoveryError,
1232
1294
  });
1233
1295
  emptyDiscoveryStreak = 0;
1234
1296
  setDiscoveryMode(DISCOVERY_MODE.FAST, 'cycle-error');
1235
1297
  } finally {
1298
+ lastDiscoveryTickFinishedAt = Date.now();
1299
+ lastDiscoveryTickDurationMs =
1300
+ lastDiscoveryTickFinishedAt - lastDiscoveryTickStartedAt;
1236
1301
  isDiscoveryRunning = false;
1237
- ensureKeepAliveAlarm('discovery-cycle');
1238
1302
  }
1239
1303
 
1240
1304
  scheduleDiscoveryTick(getDiscoveryIntervalMs());
1305
+ // Must run AFTER scheduleDiscoveryTick so discoveryIntervalId is set,
1306
+ // otherwise shouldKeepAlive() returns false and clears the alarm.
1307
+ ensureKeepAliveAlarm('discovery-cycle');
1241
1308
  }, Math.max(0, delayMs));
1242
1309
  }
1243
1310
 
@@ -1270,6 +1337,7 @@ function kickDiscovery(reason) {
1270
1337
 
1271
1338
  chrome.alarms.onAlarm.addListener((alarm) => {
1272
1339
  if (alarm.name === KEEPALIVE_ALARM) {
1340
+ lastKeepAliveAlarmAt = Date.now();
1273
1341
  const {activeCount, pendingCount} = getConnectionCounts();
1274
1342
  if (activeCount > 0 || pendingCount > 0) {
1275
1343
  logDebug('keepalive', 'Alarm triggered', {activeCount, pendingCount});
@@ -1288,7 +1356,7 @@ chrome.alarms.onAlarm.addListener((alarm) => {
1288
1356
  });
1289
1357
 
1290
1358
  // Note: We no longer register an onAlarm listener for DISCOVERY_ALARM
1291
- // The scheduleDiscovery function is only called on explicit MCP requests
1359
+ // The scheduleDiscovery function is only called on explicit server requests
1292
1360
 
1293
1361
  // Discovery auto-starts on Chrome startup
1294
1362
  // connect.html only opens when user clicks the extension icon
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "manifest_version": 3,
3
3
  "name": "chrome-ai-bridge Extension",
4
- "version": "2.0.28",
4
+ "version": "2.0.30",
5
5
  "key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqDLiSB+b/gbnQ4zWRP65jnd27KmzpjJyR1JAQIjCD/dNORzTgk6+G0TjYEDZLIDceKHzGJudqnwq4q9g3T1eJ0SECZNXnaoE00WkgXfAUSQn6cmmXR3aQGFky/zbCmxnkRa0vYupxszlhw0yrlSZrIJd/weWF75Byh0zJfZ84kqDDhaj7TlB5laHICnoSLmPTif4mQcUW9oOKmAJPriPw4CWATKZsrQ4X46djxefSmfbqYfb9rttAqJVst40gO0Gsl6GOGxMHMds5Cl9GELc0dI3Gpobw07hQldZb8TeyilI/SnOaeS3HPtrp+KyEgRu8SgRdlrvuq6DeEZsP+kK7wIDAQAB",
6
- "description": "Bridge between Chrome tabs and chrome-ai-bridge MCP server",
6
+ "description": "Bridge between Chrome tabs and Chrome AI Bridge",
7
7
  "permissions": ["debugger", "activeTab", "tabs", "storage", "alarms"],
8
8
  "host_permissions": ["<all_urls>"],
9
9
  "background": {
@@ -466,9 +466,23 @@ export class RelayServer extends EventEmitter {
466
466
  this.stopDiscoveryServer();
467
467
 
468
468
  if (this.wss) {
469
+ // Capture ref before nulling so isReady() returns false immediately
470
+ const wss = this.wss;
471
+ this.wss = null;
472
+
469
473
  return new Promise((resolve) => {
470
- this.wss!.close(() => {
471
- this.wss = null;
474
+ const timeout = setTimeout(() => {
475
+ debugLog('[RelayServer] stop() timed out after 5s — force-terminating remaining clients');
476
+ for (const client of wss.clients) {
477
+ try { client.terminate(); } catch { /* ignore */ }
478
+ }
479
+ try { wss.close(); } catch { /* ignore */ }
480
+ debugLog('[RelayServer] Server stopped (forced)');
481
+ resolve();
482
+ }, 5000);
483
+
484
+ wss.close(() => {
485
+ clearTimeout(timeout);
472
486
  debugLog('[RelayServer] Server stopped');
473
487
  resolve();
474
488
  });
@@ -382,7 +382,7 @@
382
382
  </div>
383
383
 
384
384
  <div id="tab-selection" class="hidden">
385
- <div class="section-label">Select page to expose to MCP server:</div>
385
+ <div class="section-label">Select page to expose to Chrome AI Bridge:</div>
386
386
  <div class="tabs-list" id="tabs-list"></div>
387
387
  </div>
388
388