safari-mcp 2.19.0 → 2.19.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 +37 -1
- package/extension/manifest.json +1 -1
- package/index.js +31 -1
- package/package.json +1 -1
package/extension/background.js
CHANGED
|
@@ -732,7 +732,16 @@ async function pollForCommands(pollGeneration) {
|
|
|
732
732
|
_bridgeFetch(`${bridgeUrl}/heartbeat`, { method: "POST" }).catch(() => {});
|
|
733
733
|
}, 5000);
|
|
734
734
|
await previousExecution.catch(() => {});
|
|
735
|
-
|
|
735
|
+
// 7.9.26: a command whose handleCommand never settles (a tab that never answers,
|
|
736
|
+
// a runtime call that hangs) used to park this loop for good: the beat kept the
|
|
737
|
+
// host believing we were alive, /poll went silent for hours, connectToServer
|
|
738
|
+
// waited on the same claim, and only a Safari restart brought the profile back.
|
|
739
|
+
// Bound it: past the host's hard ceiling we answer with an error, release the
|
|
740
|
+
// claim and keep polling; the abandoned promise may settle later into the void.
|
|
741
|
+
await Promise.race([
|
|
742
|
+
executeAndReply(msg, bridgeUrl),
|
|
743
|
+
_wedgedCommandGuard(msg, bridgeUrl),
|
|
744
|
+
]);
|
|
736
745
|
}
|
|
737
746
|
} finally {
|
|
738
747
|
if (beat) clearInterval(beat);
|
|
@@ -764,6 +773,33 @@ async function pollForCommands(pollGeneration) {
|
|
|
764
773
|
}
|
|
765
774
|
}
|
|
766
775
|
|
|
776
|
+
// Ceiling for one command. The host's own hard deadline is max(4×timeout, 180s) and its
|
|
777
|
+
// longest tool timeout is 75s, so nothing legitimate is still awaited after five minutes.
|
|
778
|
+
const _WEDGED_COMMAND_MS = 330000;
|
|
779
|
+
function _wedgedCommandGuard(msg, bridgeUrl) {
|
|
780
|
+
return new Promise((resolve) => {
|
|
781
|
+
setTimeout(async () => {
|
|
782
|
+
console.log("Safari MCP: command wedged, releasing the poll loop", msg && msg.type);
|
|
783
|
+
try {
|
|
784
|
+
await _bridgeFetch(`${bridgeUrl}/result`, {
|
|
785
|
+
method: "POST",
|
|
786
|
+
headers: { "Content-Type": "application/json" },
|
|
787
|
+
body: JSON.stringify({
|
|
788
|
+
type: "response",
|
|
789
|
+
id: msg.id,
|
|
790
|
+
result: null,
|
|
791
|
+
error: `command ${msg.type} did not settle within ${_WEDGED_COMMAND_MS}ms`,
|
|
792
|
+
}),
|
|
793
|
+
signal: AbortSignal.timeout(5000),
|
|
794
|
+
});
|
|
795
|
+
} catch (e) {
|
|
796
|
+
/* the host most likely expired it already */
|
|
797
|
+
}
|
|
798
|
+
resolve();
|
|
799
|
+
}, _WEDGED_COMMAND_MS);
|
|
800
|
+
});
|
|
801
|
+
}
|
|
802
|
+
|
|
767
803
|
// ========== SHARED: Execute command and send response ==========
|
|
768
804
|
|
|
769
805
|
async function executeAndReply(msg, bridgeUrl = HTTP_URL) {
|
package/extension/manifest.json
CHANGED
|
@@ -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.10.
|
|
5
|
+
"version": "2.10.10",
|
|
6
6
|
"icons": {
|
|
7
7
|
"48": "images/icon-48.png",
|
|
8
8
|
"96": "images/icon-96.png",
|
package/index.js
CHANGED
|
@@ -908,15 +908,23 @@ try {
|
|
|
908
908
|
const heartbeatingWorkerId = _requireActiveHttpWorker(req, res);
|
|
909
909
|
if (!heartbeatingWorkerId) return;
|
|
910
910
|
_extensionLastHeartbeat = Date.now();
|
|
911
|
-
_extensionLastPollTime = Date.now();
|
|
912
911
|
// Re-arm in-flight deadlines: the worker told us it is still on the command.
|
|
913
912
|
// hardDeadline inside armTimer keeps this from extending indefinitely.
|
|
913
|
+
let liveInFlight = false;
|
|
914
914
|
for (const pending of _pendingRequests.values()) {
|
|
915
915
|
if (pending.dispatchedWorkerId !== heartbeatingWorkerId) continue;
|
|
916
916
|
if (!pending.armTimer || Date.now() >= pending.hardDeadline) continue;
|
|
917
|
+
liveInFlight = true;
|
|
917
918
|
clearTimeout(pending.timer);
|
|
918
919
|
pending.timer = pending.armTimer();
|
|
919
920
|
}
|
|
921
|
+
// A beat proves liveness only while the worker is on a command we still wait for.
|
|
922
|
+
// 7.9.26: a worker wedged inside one command (its handleCommand never settled) kept
|
|
923
|
+
// beating every 5s for hours after that command's hard deadline had expired; each
|
|
924
|
+
// beat refreshed the stale clock, so "HTTP poll timeout" never fired, no successor
|
|
925
|
+
// could take the lease, and every tool call died with "Extension timeout". Once
|
|
926
|
+
// nothing dispatched to this worker is in flight, its beats no longer count as polls.
|
|
927
|
+
if (liveInFlight) _extensionLastPollTime = Date.now();
|
|
920
928
|
res.writeHead(204);
|
|
921
929
|
res.end();
|
|
922
930
|
return;
|
|
@@ -1219,6 +1227,18 @@ _staleHttpTimer.unref(); // stale-detection must not keep the Node process aliv
|
|
|
1219
1227
|
|
|
1220
1228
|
// ========== SHARED EXTENSION LOGIC ==========
|
|
1221
1229
|
|
|
1230
|
+
// A worker that keeps beating but never finishes a command holds the profile lease
|
|
1231
|
+
// hostage. Releasing it is the same bookkeeping the stale timer does on poll timeout.
|
|
1232
|
+
function _dropWedgedHttpWorker(reason) {
|
|
1233
|
+
if (!_extensionConnected && !_activeHttpWorkerId) return;
|
|
1234
|
+
_extensionConnected = false;
|
|
1235
|
+
_activeHttpWorkerId = "";
|
|
1236
|
+
_connectingHttpWorkers.clear();
|
|
1237
|
+
_reloadHttpWorkerHandoff = null;
|
|
1238
|
+
_drainOnDisconnect(`wedged worker: ${reason}`);
|
|
1239
|
+
console.error(`[Safari MCP] Extension worker dropped as wedged (${reason})`);
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1222
1242
|
// Drain pending requests and command queue on disconnect — allows fast fallback to AppleScript
|
|
1223
1243
|
function _drainOnDisconnect(reason) {
|
|
1224
1244
|
if (process.env.SAFARI_PROFILE) {
|
|
@@ -1301,6 +1321,7 @@ function sendToExtension(type, payload = {}, timeoutMs = 30000) {
|
|
|
1301
1321
|
// hardDeadline is the ceiling, so a worker that beats forever cannot hang us.
|
|
1302
1322
|
const hardDeadline = Date.now() + Math.max(timeoutMs * 4, 180000);
|
|
1303
1323
|
const expire = () => {
|
|
1324
|
+
const pending = _pendingRequests.get(id);
|
|
1304
1325
|
_pendingRequests.delete(id);
|
|
1305
1326
|
if (reloadHandoff) _cancelReloadHttpWorkerHandoff(reloadHandoff);
|
|
1306
1327
|
// Also drop it from the HTTP poll queue — otherwise the extension could poll this command
|
|
@@ -1308,6 +1329,15 @@ function sendToExtension(type, payload = {}, timeoutMs = 30000) {
|
|
|
1308
1329
|
const qi = _commandQueue.findIndex(c => c.id === id);
|
|
1309
1330
|
if (qi >= 0) _commandQueue.splice(qi, 1);
|
|
1310
1331
|
reject(new Error(`Extension timeout after ${timeoutMs}ms`));
|
|
1332
|
+
// The worker took this command and never answered, even past the hard ceiling: it is
|
|
1333
|
+
// wedged (see /heartbeat). Drop its lease now instead of waiting for the stale timer,
|
|
1334
|
+
// so a fresh worker — or Safari re-spawning this one — can connect at once.
|
|
1335
|
+
if (
|
|
1336
|
+
pending && pending.dispatchedWorkerId && pending.dispatchedWorkerId === _activeHttpWorkerId &&
|
|
1337
|
+
Date.now() >= pending.hardDeadline
|
|
1338
|
+
) {
|
|
1339
|
+
_dropWedgedHttpWorker(`${type} exceeded its hard deadline`);
|
|
1340
|
+
}
|
|
1311
1341
|
};
|
|
1312
1342
|
const armTimer = () => setTimeout(expire, Math.min(timeoutMs, Math.max(0, hardDeadline - Date.now())));
|
|
1313
1343
|
_pendingRequests.set(id, {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "safari-mcp",
|
|
3
|
-
"version": "2.19.
|
|
3
|
+
"version": "2.19.1",
|
|
4
4
|
"mcpName": "io.github.achiya-automation/safari-mcp",
|
|
5
5
|
"description": "Safari browser automation for AI agents — native macOS, zero Chrome overhead. 97 tools via AppleScript + JavaScript.",
|
|
6
6
|
"type": "module",
|