safari-mcp 2.16.0 → 2.16.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/package.json +1 -1
- package/safari.js +58 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "safari-mcp",
|
|
3
|
-
"version": "2.16.
|
|
3
|
+
"version": "2.16.1",
|
|
4
4
|
"mcpName": "io.github.achiya-automation/safari-mcp",
|
|
5
5
|
"description": "Safari browser automation for AI agents — native macOS, zero Chrome overhead. 96 tools via AppleScript + JavaScript.",
|
|
6
6
|
"type": "module",
|
package/safari.js
CHANGED
|
@@ -3262,6 +3262,64 @@ async function _screenshotFronted({ fullPage }) {
|
|
|
3262
3262
|
}
|
|
3263
3263
|
}
|
|
3264
3264
|
|
|
3265
|
+
// Fallback: full-screen capture + crop to the window rect.
|
|
3266
|
+
//
|
|
3267
|
+
// macOS 26 (Tahoe) broke `screencapture -l<windowId>` for Safari: it exits 1
|
|
3268
|
+
// with "could not create image from window" for EVERY window, while a plain
|
|
3269
|
+
// full-screen `screencapture -x` from the same process succeeds. So this is
|
|
3270
|
+
// not a TCC problem — safari_doctor reports Screen Recording as granted and
|
|
3271
|
+
// is telling the truth. Diagnosed 17.08.2026, when the by-id path started
|
|
3272
|
+
// failing mid-session and the misleading "permission may have been lost"
|
|
3273
|
+
// error below sent the investigation after a permission that was never gone.
|
|
3274
|
+
//
|
|
3275
|
+
// Crop math: AppleScript bounds are in points, the capture is in pixels.
|
|
3276
|
+
// devicePixelRatio from the page gives the backing-scale factor.
|
|
3277
|
+
if (windowId) {
|
|
3278
|
+
try {
|
|
3279
|
+
// A locked screen captures as solid black. Say so instead of returning a
|
|
3280
|
+
// black rectangle the caller has to guess at.
|
|
3281
|
+
const locked = await execFileAsync("/bin/sh", ["-c",
|
|
3282
|
+
"ioreg -n Root -d1 -r -a 2>/dev/null | grep -c CGSSessionScreenIsLocked || true"])
|
|
3283
|
+
.then((r) => String(r.stdout).trim() !== "0").catch(() => false);
|
|
3284
|
+
if (locked) {
|
|
3285
|
+
throw new Error("SCREEN_LOCKED");
|
|
3286
|
+
}
|
|
3287
|
+
const boundsRaw = await osascript(
|
|
3288
|
+
`tell application "Safari" to return bounds of ${getTargetWindowRef()}`
|
|
3289
|
+
);
|
|
3290
|
+
const [x1, y1, x2, y2] = String(boundsRaw).split(",").map((v) => parseInt(v.trim(), 10));
|
|
3291
|
+
const dpr = Math.max(1, Math.round(Number(await runJS("window.devicePixelRatio")) || 1));
|
|
3292
|
+
const w = (x2 - x1) * dpr, h = (y2 - y1) * dpr;
|
|
3293
|
+
if ([x1, y1, x2, y2].every(Number.isFinite) && w > 0 && h > 0) {
|
|
3294
|
+
const fullFile = tmpFile.replace(/\.png$/, "-full.png");
|
|
3295
|
+
const cropFile = tmpFile.replace(/\.png$/, "-crop.jpg");
|
|
3296
|
+
try {
|
|
3297
|
+
// Direct first (VS Code / terminal grant), helper second (launchd daemon has no grant of its own)
|
|
3298
|
+
await execFileAsync("/usr/sbin/screencapture", ["-x", fullFile], { timeout: 15000 })
|
|
3299
|
+
.catch(() => osascriptFast(`do shell script "/usr/sbin/screencapture -x '${fullFile}'"`, { timeout: 15000 }));
|
|
3300
|
+
await execFileAsync("sips", [
|
|
3301
|
+
"-c", String(h), String(w), "--cropOffset", String(y1 * dpr), String(x1 * dpr),
|
|
3302
|
+
fullFile, "--out", cropFile,
|
|
3303
|
+
], { timeout: 10000 });
|
|
3304
|
+
await execFileAsync("sips", [
|
|
3305
|
+
"-s", "format", "jpeg", "-s", "formatOptions", "50", "--resampleWidth", "1200",
|
|
3306
|
+
cropFile, "--out", cropFile,
|
|
3307
|
+
], { timeout: 10000 });
|
|
3308
|
+
const cropped = await readFile(cropFile);
|
|
3309
|
+
if (cropped.length > 100) return cropped.toString("base64");
|
|
3310
|
+
} finally {
|
|
3311
|
+
await unlink(fullFile).catch(() => {});
|
|
3312
|
+
await unlink(cropFile).catch(() => {});
|
|
3313
|
+
}
|
|
3314
|
+
}
|
|
3315
|
+
} catch (e) {
|
|
3316
|
+
if (e && e.message === "SCREEN_LOCKED") {
|
|
3317
|
+
throw new Error("screenshot unavailable — the screen is locked, so the capture would be solid black. Unlock the Mac and retry. (Text-based tools — safari_snapshot / safari_read_page / safari_evaluate — work regardless.)");
|
|
3318
|
+
}
|
|
3319
|
+
// crop path failed too — fall through to the JS canvas method
|
|
3320
|
+
}
|
|
3321
|
+
}
|
|
3322
|
+
|
|
3265
3323
|
// Fallback: JS-based screenshot via canvas (no permissions needed)
|
|
3266
3324
|
const dataUrl = await runJS(
|
|
3267
3325
|
`(async function(){` +
|