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.
- package/extension/background.js +13 -9
- package/index.js +21 -6
- package/package.json +1 -1
- package/safari.js +7 -9
package/extension/background.js
CHANGED
|
@@ -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
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
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", ["-
|
|
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
|
-
|
|
115
|
-
|
|
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) =>
|
|
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) =>
|
|
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) =>
|
|
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.
|
|
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
|
|
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 "
|
|
2198
|
+
click button "Cancel" of sheet 1 of w
|
|
2202
2199
|
on error
|
|
2203
2200
|
try
|
|
2204
|
-
click button "
|
|
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);
|