safari-mcp 2.7.0 → 2.7.3

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
@@ -1,10 +1,6 @@
1
1
  <div align="center">
2
2
 
3
- <picture>
4
- <source media="(prefers-color-scheme: dark)" srcset="assets/banner.svg">
5
- <source media="(prefers-color-scheme: light)" srcset="assets/banner.svg">
6
- <img src="assets/banner.svg" alt="Safari MCP — Native browser automation for AI agents" width="100%">
7
- </picture>
3
+ <img src="social-preview.png" alt="Safari MCP Server — 80 native browser automation tools for AI agents on macOS" width="100%">
8
4
 
9
5
  <br/>
10
6
 
@@ -25,6 +21,7 @@
25
21
 
26
22
  <a href="vscode:mcp/install?%7B%22safari-mcp%22%3A%7B%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22safari-mcp%22%5D%7D%7D"><img src="https://img.shields.io/badge/VS_Code-Install_MCP_Server-0078d4?logo=visual-studio-code&logoColor=white" alt="Install in VS Code"></a>
27
23
  <a href="https://insiders.vscode.dev/redirect?url=vscode-insiders:mcp/install?%7B%22safari-mcp%22%3A%7B%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22safari-mcp%22%5D%7D%7D"><img src="https://img.shields.io/badge/VS_Code_Insiders-Install_MCP_Server-24bfa5?logo=visual-studio-code&logoColor=white" alt="Install in VS Code Insiders"></a>
24
+ <a href="https://paypal.me/achiyaC123"><img src="https://img.shields.io/badge/Sponsor-PayPal-blue?logo=paypal" alt="Sponsor"></a>
28
25
 
29
26
  **80 tools** · **Zero dependencies** · **~5ms per command** · **60% less CPU than Chrome**
30
27
 
@@ -533,6 +530,21 @@ The codebase is two files:
533
530
 
534
531
  ---
535
532
 
533
+ ## Sponsors
534
+
535
+ Safari MCP is free and open source. If it saves you time or CPU cycles, consider supporting its development:
536
+
537
+ <a href="https://paypal.me/achiyaC123"><img src="https://img.shields.io/badge/Sponsor-PayPal-blue?logo=paypal" alt="Sponsor via PayPal"></a>
538
+
539
+ Your support funds:
540
+ - 🧪 Testing across macOS versions and Safari releases
541
+ - 🛠️ New tools and features
542
+ - 📖 Documentation and examples
543
+
544
+ [Become the first sponsor!](https://paypal.me/achiyaC123)
545
+
546
+ ---
547
+
536
548
  ## Star History
537
549
 
538
550
  If Safari MCP saved you from Chrome overhead, consider giving it a ⭐
@@ -5,7 +5,7 @@
5
5
  const HTTP_URL = "http://127.0.0.1:9224";
6
6
  let isConnected = false;
7
7
  let pollAbort = null;
8
- let _targetProfile = null; // Profile name from server (e.g. "אוטומציות")
8
+ let _targetProfile = null; // Profile name from server (e.g. "Automations")
9
9
  let _profileWindowId = null; // Discovered windowId for the profile
10
10
  let _enabled = true; // Toggle from popup — when false, stops polling and rejects commands
11
11
  let _reconnectTimer = null; // Single reconnect timer — prevents exponential growth
@@ -185,7 +185,9 @@ async function executeAndReply(msg) {
185
185
  body: JSON.stringify(response),
186
186
  signal: AbortSignal.timeout(5000),
187
187
  });
188
- } catch {}
188
+ } catch (err) {
189
+ console.warn("Safari MCP Bridge: failed to send result to server:", err.message);
190
+ }
189
191
  }
190
192
 
191
193
  // ========== COMMAND HANDLERS ==========
@@ -1725,11 +1727,15 @@ browser.tabs.onRemoved.addListener((tabId) => {
1725
1727
  cache.tabUrl = null;
1726
1728
  }
1727
1729
  }
1730
+ // Also remove from owned tabs — prevents stale ownership on externally closed tabs
1731
+ for (const [sid, ownedSet] of _sessionOwnedTabs) {
1732
+ ownedSet.delete(tabId);
1733
+ }
1728
1734
  });
1729
1735
 
1730
1736
  // Verify this extension instance is running in the expected profile.
1731
1737
  // Safari extensions see only their own profile's windows/tabs.
1732
- // If the server expects "אוטומציות" but this worker's windows don't match, we're in the wrong profile.
1738
+ // If the server expects a specific profile but this worker's windows don't match, we're in the wrong profile.
1733
1739
  async function _verifyProfileMatch(expectedProfile) {
1734
1740
  try {
1735
1741
  const allWindows = await browser.windows.getAll({ populate: true });
@@ -1774,7 +1780,13 @@ async function _verifyProfileMatch(expectedProfile) {
1774
1780
  }
1775
1781
 
1776
1782
  if (verifyRes && verifyRes.ok) {
1777
- const result = await verifyRes.json();
1783
+ let result;
1784
+ try {
1785
+ result = await verifyRes.json();
1786
+ } catch {
1787
+ console.warn("Safari MCP: profile verification response invalid JSON — rejecting");
1788
+ return false;
1789
+ }
1778
1790
  if (result.match) {
1779
1791
  await browser.storage.local.set({ mcpVerifiedProfile: expectedProfile });
1780
1792
  return true;
@@ -1,6 +1,6 @@
1
1
  // Content script — runs at document_start in MAIN world (before page scripts)
2
2
  // Purpose: Monkey-patch attachShadow to capture CLOSED shadow roots.
3
- // Without this, Reddit 2026 and other sites using closed shadow DOM
3
+ // Without this, Reddit and other sites using closed shadow DOM
4
4
  // are invisible to snapshot/click/fill.
5
5
  // Runs in MAIN world via manifest "world": "MAIN" — no script injection needed,
6
6
  // so CSP cannot block it.
@@ -2,7 +2,7 @@
2
2
  "manifest_version": 3,
3
3
  "name": "Safari MCP Bridge",
4
4
  "description": "Connects AI agents to Safari — fast JS execution, screenshots, clicks with your real cookies/logins",
5
- "version": "2.4",
5
+ "version": "2.7.2",
6
6
  "icons": {
7
7
  "48": "images/icon-48.png",
8
8
  "96": "images/icon-96.png",
@@ -1,5 +1,5 @@
1
1
  <!DOCTYPE html>
2
- <html dir="ltr">
2
+ <html lang="en" dir="ltr">
3
3
  <head>
4
4
  <meta charset="utf-8">
5
5
  <style>
package/index.js CHANGED
@@ -3,7 +3,7 @@
3
3
  // 1. Safari Web Extension (fast, ~5-20ms, keeps logins) — when extension is connected
4
4
  // 2. AppleScript + Swift daemon (~5ms, keeps logins) — always available
5
5
  //
6
- // Extension transport: WebSocket (Chrome) OR HTTP polling (Safari — WebSocket blocked by Apple)
6
+ // Extension transport: HTTP polling (Safari — WebSocket blocked by Apple)
7
7
 
8
8
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
9
9
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
@@ -84,6 +84,13 @@ function _isURLOwned(url) {
84
84
  if (new URL(owned).origin === urlOrigin && urlBase.startsWith(normalize(owned))) return true;
85
85
  } catch {}
86
86
  }
87
+ // Broader same-origin check: if we own ANY URL on this origin, treat all same-origin URLs as owned.
88
+ // This handles redirects like /article/new → /article/edit/ID where path prefix doesn't match.
89
+ for (const owned of _ownedTabURLs) {
90
+ try {
91
+ if (new URL(owned).origin === urlOrigin) return true;
92
+ } catch {}
93
+ }
87
94
  } catch {}
88
95
  return false;
89
96
  }
@@ -386,6 +393,14 @@ try {
386
393
  const { execFile: execFileCb } = await import("node:child_process");
387
394
  const { promisify: pfy } = await import("node:util");
388
395
  const execFileAsync = pfy(execFileCb);
396
+ // Don't launch Safari if it's not running
397
+ try {
398
+ await execFileAsync("pgrep", ["-x", "Safari"], { timeout: 2000 });
399
+ } catch {
400
+ res.writeHead(200, { "Content-Type": "application/json" });
401
+ res.end(JSON.stringify({ match: false, error: "Safari is not running" }));
402
+ return;
403
+ }
389
404
  const script = `tell application "Safari"
390
405
  repeat with w in every window
391
406
  repeat with t in every tab of w
@@ -635,15 +650,19 @@ async function extensionOrFallback(extensionType, extensionPayload, fallbackFn)
635
650
  // must target an owned tab. This prevents navigating/clicking in user's tabs.
636
651
  if (!_noOwnershipCheck.has(extensionType)) {
637
652
  const currentUrl = safari.getActiveTabURL();
638
- if (_ownedTabURLs.size === 0) {
653
+ if (_ownedTabURLs.size === 0 && _openedTabs.size === 0) {
639
654
  // No tabs opened yet — block everything except read-only ops
640
655
  const msg = `⚠️ Tab safety: no tabs opened yet. Call safari_new_tab first before "${extensionType}".`;
641
656
  console.error(`[Safari MCP] ${msg}`);
642
657
  throw new Error(msg);
643
658
  } else if (currentUrl && !_isURLOwned(currentUrl)) {
644
- const msg = `⚠️ Tab safety: refusing "${extensionType}" — current tab (${currentUrl}) was not opened by this MCP session. Use safari_new_tab or safari_switch_tab to target your own tab.`;
645
- console.error(`[Safari MCP] ${msg}`);
646
- throw new Error(msg);
659
+ // about:blank tabs are owned if we have any tracked tabs (new_tab creates them at about:blank)
660
+ const isBlankOwned = (currentUrl === 'about:blank' || currentUrl === 'missing value') && _openedTabs.size > 0;
661
+ if (!isBlankOwned) {
662
+ const msg = `⚠️ Tab safety: refusing "${extensionType}" — current tab (${currentUrl}) was not opened by this MCP session. Use safari_new_tab or safari_switch_tab to target your own tab.`;
663
+ console.error(`[Safari MCP] ${msg}`);
664
+ throw new Error(msg);
665
+ }
647
666
  }
648
667
  }
649
668
  if (_extensionConnected && !_preferAppleScript) {
@@ -1203,9 +1222,13 @@ server.tool(
1203
1222
  const parsed = typeof tabs === 'string' ? JSON.parse(tabs) : tabs;
1204
1223
  const target = parsed.find(t => t.index === index);
1205
1224
  if (target && target.url && !_isURLOwned(target.url)) {
1206
- const msg = `⚠️ Tab safety: refusing switch_tab to index ${index} (${target.url}) — not opened by this MCP session. Use safari_new_tab to open your own tab.`;
1207
- console.error(`[Safari MCP] ${msg}`);
1208
- return { content: [{ type: "text", text: msg }], isError: true };
1225
+ // about:blank / missing value tabs are owned if tracked in _openedTabs
1226
+ const isBlankOwned = (target.url === 'about:blank' || target.url === 'missing value') && _openedTabs.has(index);
1227
+ if (!isBlankOwned) {
1228
+ const msg = `⚠️ Tab safety: refusing switch_tab to index ${index} (${target.url}) — not opened by this MCP session. Use safari_new_tab to open your own tab.`;
1229
+ console.error(`[Safari MCP] ${msg}`);
1230
+ return { content: [{ type: "text", text: msg }], isError: true };
1231
+ }
1209
1232
  }
1210
1233
  } catch {}
1211
1234
  }
package/mcp-helpers.js CHANGED
@@ -1,7 +1,7 @@
1
1
  // MCP Click/Interaction Helpers — injected into every page
2
2
  // This file is loaded by safari.js at startup, escaped, and injected via AppleScript `do JavaScript`.
3
3
  // It is also used by the extension's execInTab for the AppleScript fallback path.
4
- // IMPORTANT: Keep this file compatible with all browsers — no ES6+ modules, use var where possible.
4
+ // IMPORTANT: Keep compatible with all browsers — no ES6+ modules, use var where possible.
5
5
 
6
6
  if (window.__mcpVersion !== 5) {
7
7
  window.__mcpVersion = 5;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "safari-mcp",
3
- "version": "2.7.0",
3
+ "version": "2.7.3",
4
4
  "mcpName": "io.github.achiya-automation/safari-mcp",
5
5
  "description": "Safari browser automation for AI agents — native macOS, zero Chrome overhead. 80 tools via AppleScript + JavaScript.",
6
6
  "type": "module",
@@ -39,8 +39,8 @@
39
39
  "license": "MIT",
40
40
  "homepage": "https://achiya-automation.com",
41
41
  "funding": {
42
- "type": "github",
43
- "url": "https://github.com/sponsors/achiya-automation"
42
+ "type": "individual",
43
+ "url": "https://paypal.me/achiyaC123"
44
44
  },
45
45
  "repository": {
46
46
  "type": "git",
package/safari.js CHANGED
@@ -89,6 +89,21 @@ function cleanupHelper() {
89
89
  process.on("exit", cleanupHelper);
90
90
  process.on("uncaughtException", (err) => { console.error("Uncaught:", err); cleanupHelper(); process.exit(1); });
91
91
 
92
+ // ========== SAFARI RUNNING CHECK ==========
93
+ // Prevent AppleScript from auto-launching Safari when it's closed
94
+ async function isSafariRunning() {
95
+ try {
96
+ const { stdout } = await execFileAsync("pgrep", ["-x", "Safari"], { timeout: 2000 });
97
+ return stdout.trim().length > 0;
98
+ } catch {
99
+ return false; // pgrep exits 1 when no match
100
+ }
101
+ }
102
+
103
+ function safariNotRunningError() {
104
+ return new Error("Safari is not running. Open Safari manually before using Safari MCP tools.");
105
+ }
106
+
92
107
  // ========== CLIPBOARD LOCK ==========
93
108
  // Prevents concurrent clipboard operations from clobbering the user's clipboard.
94
109
  // While locked, any new clipboard operation waits until the current one completes.
@@ -326,6 +341,7 @@ async function resolveActiveTab() {
326
341
 
327
342
  // Run AppleScript — uses execFile (safe, isolated, for complex scripts)
328
343
  async function osascript(script, { timeout = 10000 } = {}) {
344
+ if (!(await isSafariRunning())) throw safariNotRunningError();
329
345
  try {
330
346
  const { stdout } = await execFileAsync("osascript", ["-e", script], {
331
347
  timeout,
@@ -349,6 +365,7 @@ async function osascript(script, { timeout = 10000 } = {}) {
349
365
 
350
366
  // osascriptFast: uses persistent Swift daemon (~5ms) — 18x faster than subprocess (~90ms)
351
367
  async function osascriptFast(script, { timeout = 10000 } = {}) {
368
+ if (!(await isSafariRunning())) throw safariNotRunningError();
352
369
  if (!_helperProc) startHelper();
353
370
  if (_helperProc) {
354
371
  try {
@@ -378,10 +395,11 @@ function _osascriptFastHelper(script, timeout) {
378
395
  const timer = setTimeout(() => {
379
396
  if (resolved) return;
380
397
  resolved = true;
398
+ // DON'T remove from queue — replace with no-op consumer to maintain FIFO order.
399
+ // Without this, the late response would be consumed by the NEXT callback in the queue.
381
400
  const idx = _helperQueue.indexOf(cb);
382
- if (idx >= 0) _helperQueue.splice(idx, 1);
401
+ if (idx >= 0) _helperQueue[idx] = () => {}; // Consume and discard late response
383
402
  _helperConsecutiveTimeouts++;
384
- // Only kill the daemon after 3 consecutive timeouts (single slow script shouldn't kill concurrent ops)
385
403
  if (_helperConsecutiveTimeouts >= 3) {
386
404
  console.error(`[Safari MCP] safari-helper: ${_helperConsecutiveTimeouts} consecutive timeouts — killing daemon`);
387
405
  _helperProc?.kill();
@@ -396,7 +414,7 @@ function _osascriptFastHelper(script, timeout) {
396
414
  if (resolved) return;
397
415
  resolved = true;
398
416
  clearTimeout(timer);
399
- _helperConsecutiveTimeouts = 0; // Reset on success
417
+ _helperConsecutiveTimeouts = 0;
400
418
  try {
401
419
  const parsed = JSON.parse(line);
402
420
  if (parsed.error) reject(new Error(parsed.error));
@@ -406,7 +424,6 @@ function _osascriptFastHelper(script, timeout) {
406
424
  }
407
425
  }
408
426
 
409
- // Check process availability BEFORE pushing to queue (prevents dangling callbacks)
410
427
  if (!_helperProc || !_helperProc.stdin || !_helperProc.stdin.writable) {
411
428
  clearTimeout(timer);
412
429
  reject(new Error("safari-helper not available"));
@@ -440,7 +457,7 @@ function _helperNativeClick(x, y, doubleClick = false, windowId = 0, timeout = 5
440
457
  if (resolved) return;
441
458
  resolved = true;
442
459
  const idx = _helperQueue.indexOf(cb);
443
- if (idx >= 0) _helperQueue.splice(idx, 1);
460
+ if (idx >= 0) _helperQueue[idx] = () => {}; // No-op consumer for late response
444
461
  reject(new Error("native click timeout"));
445
462
  }, timeout);
446
463
 
@@ -479,7 +496,7 @@ function _helperNativeKeyboard(keyCode, flags = [], windowId = 0, timeout = 5000
479
496
  if (resolved) return;
480
497
  resolved = true;
481
498
  const idx = _helperQueue.indexOf(cb);
482
- if (idx >= 0) _helperQueue.splice(idx, 1);
499
+ if (idx >= 0) _helperQueue[idx] = () => {}; // No-op consumer for late response
483
500
  reject(new Error("native keyboard timeout"));
484
501
  }, timeout);
485
502
 
@@ -659,7 +676,7 @@ export async function navigate(url) {
659
676
  const state = await runJS('document.readyState', { timeout: 5000 });
660
677
  if (state === 'complete' || state === 'interactive') {
661
678
  result = await runJS(
662
- `JSON.stringify({title:document.title,url:location.href,blocked:document.title.includes('cannot open')||document.title.includes('אין אפשרות')})`,
679
+ `JSON.stringify({title:document.title,url:location.href,blocked:document.title.includes('cannot open')||document.title.includes('\u05D0\u05D9\u05DF \u05D0\u05E4\u05E9\u05E8\u05D5\u05EA')})`,
663
680
  { timeout: 5000 }
664
681
  );
665
682
  if (state === 'complete') break;
@@ -1164,7 +1181,7 @@ export async function pressKey({ key, modifiers = [] }) {
1164
1181
  const geo = await _getSafariWindowGeometry();
1165
1182
  if (!geo.windowId) throw new Error("Cannot paste into iframe without Safari window ID — would steal focus");
1166
1183
  await _helperNativeKeyboard(9, ["cmd"], geo.windowId);
1167
- await new Promise(r => setTimeout(r, 300));
1184
+ await new Promise(r => setTimeout(r, 100)); // 100ms is enough for Cmd+V to process
1168
1185
  return `Pressed: ${modifiers.join("+")}+v (CGEvent Cmd+V into iframe, no focus steal)`;
1169
1186
  }
1170
1187
 
@@ -1277,22 +1294,22 @@ async function _nativeTypeViaClipboard(text) {
1277
1294
  // MUST have windowId — global CGEvent (windowId=0) would steal focus and move mouse
1278
1295
  const geo = await _getSafariWindowGeometry();
1279
1296
  if (!geo.windowId) {
1280
- // Can't get window ID — restore clipboard and throw
1297
+ // Can't get window ID — restore clipboard and let catch block release lock
1281
1298
  await _restoreClipboard(savedClipboard);
1282
- _releaseClipboardLock();
1283
1299
  throw new Error("Cannot native-paste without Safari window ID — would steal focus");
1284
1300
  }
1285
1301
  // keyCode 9 = V key, flags: ["cmd"]
1286
1302
  await _helperNativeKeyboard(9, ["cmd"], geo.windowId);
1287
1303
 
1288
1304
  // Wait for paste to settle, then restore clipboard immediately
1289
- await new Promise(r => setTimeout(r, 300));
1305
+ await new Promise(r => setTimeout(r, 100)); // 100ms is enough for Cmd+V to process
1290
1306
  await _restoreClipboard(savedClipboard);
1291
1307
  _releaseClipboardLock();
1292
1308
 
1293
1309
  return `Typed ${text.length} chars (native paste into iframe)`;
1294
1310
  } catch (err) {
1295
- _releaseClipboardLock();
1311
+ // Ensure lock is released on ANY error (including the windowId check above)
1312
+ if (_clipboardLocked) _releaseClipboardLock();
1296
1313
  throw err;
1297
1314
  }
1298
1315
  }
@@ -1486,11 +1503,12 @@ export async function screenshot({ fullPage = false } = {}) {
1486
1503
  `tell application "Safari" to return id of ${getTargetWindowRef()}`
1487
1504
  ).catch(() => null) : null;
1488
1505
 
1489
- // Save frontmost app BEFORE screencapture (it may steal focus on macOS Tahoe)
1490
- // Restore it immediately after capture to minimize disruption
1506
+ // On macOS Tahoe, screencapture -l may briefly steal focus.
1507
+ // Save frontmost app so we can detect if focus was stolen and restore it.
1491
1508
  let previousApp = null;
1492
1509
  if (windowId) {
1493
1510
  try {
1511
+ // Use NSWorkspace via AppleScript (lighter than System Events — no AX needed)
1494
1512
  previousApp = (await osascriptFast(
1495
1513
  `tell application "System Events" to return name of first application process whose frontmost is true`
1496
1514
  )).trim();
@@ -1680,6 +1698,8 @@ export async function screenshotElement({ selector }) {
1680
1698
  if (data.length > 100) return data.toString("base64");
1681
1699
  } catch (e) {
1682
1700
  await unlink(tmpFile).catch(() => {});
1701
+ // Also clean cropFile if it was created before the error
1702
+ await unlink(tmpFile.replace('safari-el-', 'safari-el-crop-')).catch(() => {});
1683
1703
  throw new Error(`Element screenshot failed: ${e.message}`);
1684
1704
  }
1685
1705
  }
@@ -2066,7 +2086,7 @@ export async function uploadFile({ selector, filePath }) {
2066
2086
  click button "Cancel" of sheet 1 of w
2067
2087
  on error
2068
2088
  try
2069
- click button "ביטול" of sheet 1 of w
2089
+ click button "\u05D1\u05D9\u05D8\u05D5\u05DC" of sheet 1 of w -- "Cancel" in Hebrew locale
2070
2090
  on error
2071
2091
  key code 53
2072
2092
  end try
@@ -2776,11 +2796,15 @@ export async function importStorageState({ state }) {
2776
2796
  // ========== CLIPBOARD ==========
2777
2797
 
2778
2798
  export async function clipboardRead() {
2799
+ // Acquire lock to avoid reading during a write/restore cycle
2800
+ await _acquireClipboardLock(3000); // Short timeout — reads are fast
2779
2801
  try {
2780
2802
  const text = await execFileAsync("pbpaste", []);
2781
2803
  return text.stdout;
2782
2804
  } catch {
2783
2805
  return "(clipboard empty or contains non-text data)";
2806
+ } finally {
2807
+ _releaseClipboardLock();
2784
2808
  }
2785
2809
  }
2786
2810
 
@@ -1,57 +0,0 @@
1
- // v6 — asks background script for real-time status, falls back to storage
2
- async function checkStatus() {
3
- const enabled = await getVal("mcpEnabled", true);
4
- document.getElementById("enableToggle").checked = enabled;
5
-
6
- if (!enabled) {
7
- setStatus("paused", "Paused", "Toggle to resume");
8
- return;
9
- }
10
-
11
- // Ask background script for real-time status
12
- try {
13
- const resp = await browser.runtime.sendMessage({ action: "getStatus" });
14
- if (resp) {
15
- if (resp.connected) { setStatus("connected", "Connected", "Port 9224"); return; }
16
- if (!resp.enabled) { setStatus("paused", "Paused", "Toggle to resume"); return; }
17
- }
18
- } catch {}
19
-
20
- // Fallback: read from storage
21
- const status = await getVal("mcpStatus", "checking");
22
- switch (status) {
23
- case "connected":
24
- setStatus("connected", "Connected", "Port 9224");
25
- break;
26
- case "paused":
27
- setStatus("paused", "Paused", "Toggle to resume");
28
- break;
29
- case "checking":
30
- setStatus("checking", "Connecting...", "Trying port 9224...");
31
- break;
32
- default:
33
- setStatus("disconnected", "Not connected", "Start the MCP server to connect");
34
- }
35
- }
36
-
37
- function setStatus(dotClass, text, info) {
38
- document.getElementById("statusDot").className = "dot " + dotClass;
39
- document.getElementById("statusText").textContent = text;
40
- document.getElementById("info").textContent = info;
41
- }
42
-
43
- document.getElementById("enableToggle").addEventListener("change", async (e) => {
44
- const enabled = e.target.checked;
45
- await browser.storage.local.set({ mcpEnabled: enabled });
46
- try { browser.runtime.sendMessage({ action: "setEnabled", enabled }); } catch {}
47
- setTimeout(checkStatus, 500);
48
- });
49
-
50
- async function getVal(key, defaultVal) {
51
- try {
52
- const data = await browser.storage.local.get(key);
53
- return data[key] !== undefined ? data[key] : defaultVal;
54
- } catch { return defaultVal; }
55
- }
56
-
57
- checkStatus();