safari-mcp 2.21.0 → 2.21.2

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.
Files changed (3) hide show
  1. package/index.js +10 -3
  2. package/package.json +1 -1
  3. package/response.js +21 -0
package/index.js CHANGED
@@ -11,7 +11,7 @@ import { startTransport } from "./transport.js";
11
11
  import { currentSessionId } from "./session-context.js";
12
12
  import { z } from "zod";
13
13
  import * as safari from "./safari.js";
14
- import { textResult, jsonResult, imageResult, errorResult } from "./response.js";
14
+ import { textResult, jsonResult, imageResult, errorResult, evalResult } from "./response.js";
15
15
  import {
16
16
  OWNERSHIP_DIR, BLANK_TAB_SENTINEL,
17
17
  _openedTabs, _ownedTabURLs,
@@ -2850,6 +2850,13 @@ server.tool(
2850
2850
  if (resolvedIndex) safari.setActiveTabIndex(resolvedIndex);
2851
2851
  if (safeResult?.safeUrl) safari.setActiveTabURL(safeResult.safeUrl);
2852
2852
  if (safeResult?.receipt || token) _setActiveReceipt(safeResult?.receipt || token);
2853
+ // A switch by INDEX gets no receipt back, and the session was still holding the one it
2854
+ // minted for the PREVIOUS tab. Every later command auto-attaches that receipt, so the
2855
+ // switch reported `owned: true` and then the next evaluate/click died with "receipt is
2856
+ // forged, stale, ambiguous, or not valid for this origin" — a dead end with no way out
2857
+ // but opening another tab. Drop the stale capability instead: without one the command
2858
+ // targets the tab this switch just selected, which is exactly what was asked for.
2859
+ else _setActiveReceipt("");
2853
2860
  // Say so in the result, not only in the log: an agent that adopted a user's tab should
2854
2861
  // be able to see that from the answer it got (#92, condition 3).
2855
2862
  const reported = adopted ? { ...safeResult, note: "(user tab, opted-in)" } : safeResult;
@@ -2975,7 +2982,7 @@ server.tool(
2975
2982
  return safari.evaluate(args);
2976
2983
  }
2977
2984
  );
2978
- return { content: [{ type: "text", text: (typeof result === 'string' ? result : JSON.stringify(result)) || "(no return value)" }] };
2985
+ return evalResult(result);
2979
2986
  }
2980
2987
  );
2981
2988
 
@@ -2986,7 +2993,7 @@ server.tool(
2986
2993
  async (args) => {
2987
2994
  const script = readFileSync(args.path, "utf8");
2988
2995
  const result = await extensionOrFallback("evaluate", { script }, () => safari.evaluate({ script }));
2989
- return { content: [{ type: "text", text: (typeof result === "string" ? result : JSON.stringify(result)) || "(no return value)" }] };
2996
+ return evalResult(result);
2990
2997
  }
2991
2998
  );
2992
2999
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "safari-mcp",
3
- "version": "2.21.0",
3
+ "version": "2.21.2",
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",
package/response.js CHANGED
@@ -28,3 +28,24 @@ export const errorResult = (msg) => ({
28
28
  content: [{ type: "text", text: msg }],
29
29
  isError: true,
30
30
  });
31
+
32
+ /**
33
+ * `safari_evaluate` / `safari_eval_file` result text.
34
+ *
35
+ * Distinct from `textResult`: only these two tools surface a *script's* return value,
36
+ * so they need to say "the script returned nothing" — and the old form said it with a
37
+ * falsy check, `(… || "(no return value)")`. That collapsed three different outcomes
38
+ * into one string: a script returning `undefined` (nothing), a script returning `""`
39
+ * (a real value), and an injection that never ran at all (a failure). An empty return
40
+ * is a legitimate result and must stay distinguishable from no return.
41
+ */
42
+ export const evalResult = (r) => {
43
+ let text;
44
+ if (typeof r === "string") {
45
+ text = r === "" ? '""' : r;
46
+ } else {
47
+ const json = JSON.stringify(r); // undefined for undefined/function/symbol
48
+ text = json === undefined ? "(no return value)" : json;
49
+ }
50
+ return { content: [{ type: "text", text }] };
51
+ };