safari-mcp 2.3.0 → 2.3.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.
@@ -1647,16 +1647,20 @@ async function _verifyProfileMatch(expectedProfile) {
1647
1647
  await new Promise(r => setTimeout(r, 500));
1648
1648
 
1649
1649
  // Ask the server to verify which profile has this nonce
1650
- const verifyRes = await fetch(`${HTTP_URL}/verify-profile`, {
1651
- method: "POST",
1652
- headers: { "Content-Type": "application/json" },
1653
- body: JSON.stringify({ nonce, expectedProfile }),
1654
- signal: AbortSignal.timeout(5000),
1655
- });
1656
- // Clean up test tab
1657
- await browser.tabs.remove(checkTab.id).catch(() => {});
1650
+ let verifyRes;
1651
+ try {
1652
+ verifyRes = await fetch(`${HTTP_URL}/verify-profile`, {
1653
+ method: "POST",
1654
+ headers: { "Content-Type": "application/json" },
1655
+ body: JSON.stringify({ nonce, expectedProfile }),
1656
+ signal: AbortSignal.timeout(5000),
1657
+ });
1658
+ } finally {
1659
+ // Always clean up test tab — even if fetch fails
1660
+ await browser.tabs.remove(checkTab.id).catch(() => {});
1661
+ }
1658
1662
 
1659
- if (verifyRes.ok) {
1663
+ if (verifyRes && verifyRes.ok) {
1660
1664
  const result = await verifyRes.json();
1661
1665
  if (result.match) {
1662
1666
  await browser.storage.local.set({ mcpVerifiedProfile: expectedProfile });
package/index.js CHANGED
@@ -94,7 +94,7 @@ function _startMemoryMonitor() {
94
94
  // Use pgrep + ps directly — avoids spawning bash, grep, awk pipeline
95
95
  let psOut = "0";
96
96
  try {
97
- const pids = execFileSync("pgrep", ["-f", "WebKit|WebContent"], { encoding: "utf8" }).trim();
97
+ const pids = execFileSync("pgrep", ["-Ef", "WebKit|WebContent"], { encoding: "utf8" }).trim();
98
98
  if (pids) {
99
99
  const pidList = pids.split("\n").join(",");
100
100
  psOut = execFileSync("ps", ["-p", pidList, "-o", "rss="], { encoding: "utf8" }).trim();
@@ -111,8 +111,17 @@ function _startMemoryMonitor() {
111
111
  }
112
112
  if (oldestIdx !== null) {
113
113
  try {
114
- safari.setActiveTabIndex(oldestIdx);
115
- await safari.closeTab();
114
+ // Re-resolve by URL before closing (indices shift as tabs change)
115
+ const info = _openedTabs.get(oldestIdx);
116
+ if (info?.url) {
117
+ const tabs = await safari.listTabs();
118
+ const parsed = typeof tabs === 'string' ? JSON.parse(tabs) : tabs;
119
+ const match = parsed.find(t => t.url === info.url);
120
+ if (match) {
121
+ safari.setActiveTabIndex(match.index);
122
+ await safari.closeTab();
123
+ }
124
+ }
116
125
  } catch {}
117
126
  _untrackTab(oldestIdx);
118
127
  }
@@ -248,9 +257,10 @@ try {
248
257
  }
249
258
 
250
259
  // POST /result — extension sends command result
260
+ const MAX_BODY_SIZE = 10 * 1024 * 1024; // 10 MB cap — prevents DoS from large payloads
251
261
  if (req.method === "POST" && req.url === "/result") {
252
262
  let body = "";
253
- req.on("data", (chunk) => (body += chunk));
263
+ req.on("data", (chunk) => { body += chunk; if (body.length > MAX_BODY_SIZE) { res.writeHead(413); res.end("Payload too large"); req.destroy(); } });
254
264
  req.on("end", () => {
255
265
  try {
256
266
  const msg = JSON.parse(body);
@@ -291,7 +301,7 @@ try {
291
301
  // POST /verify-profile — extension asks server to check which profile has a nonce tab
292
302
  if (req.method === "POST" && req.url === "/verify-profile") {
293
303
  let body = "";
294
- req.on("data", (chunk) => (body += chunk));
304
+ req.on("data", (chunk) => { body += chunk; if (body.length > MAX_BODY_SIZE) { res.writeHead(413); res.end("Payload too large"); req.destroy(); } });
295
305
  req.on("end", async () => {
296
306
  try {
297
307
  const { nonce, expectedProfile } = JSON.parse(body);
@@ -343,7 +353,7 @@ try {
343
353
  // POST /proxy-command — secondary instances send commands through primary
344
354
  if (req.method === "POST" && req.url === "/proxy-command") {
345
355
  let body = "";
346
- req.on("data", (chunk) => (body += chunk));
356
+ req.on("data", (chunk) => { body += chunk; if (body.length > MAX_BODY_SIZE) { res.writeHead(413); res.end("Payload too large"); req.destroy(); } });
347
357
  req.on("end", async () => {
348
358
  try {
349
359
  const { type, payload } = JSON.parse(body);
@@ -391,6 +401,7 @@ setTimeout(() => {
391
401
  }, 2000);
392
402
 
393
403
  async function _checkPrimaryExtension() {
404
+ if (_isExtensionHost) return; // Already hosting — stop polling
394
405
  try {
395
406
  const res = await fetch(`http://127.0.0.1:${HTTP_PORT}/proxy-check`, {
396
407
  method: "GET",
@@ -1126,12 +1137,16 @@ server.tool(
1126
1137
  if (resolved && resolved.url !== 'about:blank') {
1127
1138
  if (urlContains && !resolved.url.includes(urlContains)) continue;
1128
1139
  await extensionOrFallback("switch_tab", { index: resolved.index }, () => safari.switchTab(resolved.index));
1140
+ safari.setActiveTabIndex(resolved.index);
1141
+ safari.setActiveTabURL(resolved.url);
1129
1142
  return { content: [{ type: "text", text: `Found new tab: ${resolved.title} (${resolved.url})` }] };
1130
1143
  }
1131
1144
  continue;
1132
1145
  }
1133
1146
  if (urlContains && !tab.url.includes(urlContains)) continue;
1134
1147
  await extensionOrFallback("switch_tab", { index: tab.index }, () => safari.switchTab(tab.index));
1148
+ safari.setActiveTabIndex(tab.index);
1149
+ safari.setActiveTabURL(tab.url);
1135
1150
  return { content: [{ type: "text", text: `Found new tab: ${tab.title} (${tab.url})` }] };
1136
1151
  }
1137
1152
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "safari-mcp",
3
- "version": "2.3.0",
3
+ "version": "2.3.1",
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",
package/safari.js CHANGED
@@ -6,7 +6,7 @@
6
6
  import { execFile, spawn } from "node:child_process";
7
7
  import { promisify } from "node:util";
8
8
  import { tmpdir } from "node:os";
9
- import { join, dirname } from "node:path";
9
+ import { join, dirname, resolve as resolvePath } from "node:path";
10
10
  import { readFile, writeFile, unlink, appendFile } from "node:fs/promises";
11
11
  import { readFileSync } from "node:fs";
12
12
  import { fileURLToPath } from "node:url";
@@ -84,11 +84,9 @@ function cleanupHelper() {
84
84
  _helperProc = null;
85
85
  }
86
86
  }
87
+ // Signal handlers (SIGINT/SIGTERM/SIGHUP) are registered in index.js only.
88
+ // cleanupHelper runs via process.on("exit"), which fires when index.js calls process.exit().
87
89
  process.on("exit", cleanupHelper);
88
- process.on("SIGINT", () => { cleanupHelper(); process.exit(0); });
89
- process.on("SIGTERM", () => { cleanupHelper(); process.exit(0); });
90
- // SIGHUP = terminal closed, uncaught = crash — still clean up
91
- process.on("SIGHUP", () => { cleanupHelper(); process.exit(0); });
92
90
  process.on("uncaughtException", (err) => { console.error("Uncaught:", err); cleanupHelper(); process.exit(1); });
93
91
 
94
92
  // ========== ACTIVE TAB TRACKING ==========
@@ -2170,8 +2168,7 @@ export async function drag({ sourceSelector, targetSelector, sourceX, sourceY, t
2170
2168
  // ========== FILE PATH SAFETY ==========
2171
2169
  // Prevent reading sensitive system files via upload/paste tools
2172
2170
  function _validateFilePath(filePath) {
2173
- const { resolve } = require("node:path");
2174
- const resolved = resolve(filePath);
2171
+ const resolved = resolvePath(filePath);
2175
2172
  if (resolved.includes('..')) throw new Error("Path traversal not allowed: " + filePath);
2176
2173
  const blocked = ['.ssh', '.gnupg', '.aws', '.config/gcloud', 'credentials', '.env', '.npmrc', '.netrc', 'id_rsa', 'id_ed25519', '.keychain'];
2177
2174
  const lower = resolved.toLowerCase();
@@ -2198,10 +2195,10 @@ export async function uploadFile({ selector, filePath }) {
2198
2195
  repeat with w in every window
2199
2196
  if exists sheet 1 of w then
2200
2197
  try
2201
- click button "ביטול" of sheet 1 of w
2198
+ click button "Cancel" of sheet 1 of w
2202
2199
  on error
2203
2200
  try
2204
- click button "Cancel" of sheet 1 of w
2201
+ click button "ביטול" of sheet 1 of w
2205
2202
  on error
2206
2203
  key code 53
2207
2204
  end try
@@ -2659,6 +2656,7 @@ export async function takeSnapshot({ selector, _gen } = {}) {
2659
2656
  }
2660
2657
 
2661
2658
  window.__mcpRefs = {};
2659
+ window.__mcpRefsTime = Date.now();
2662
2660
  var root = ${root};
2663
2661
  if (!root) return 'Element not found';
2664
2662
  walk(root, 0);