safari-mcp 2.7.10 → 2.7.12

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/README.md CHANGED
@@ -13,6 +13,7 @@
13
13
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
14
14
  [![macOS](https://img.shields.io/badge/macOS-only-blue)](https://www.apple.com/macos/)
15
15
  [![GitHub stars](https://img.shields.io/github/stars/achiya-automation/safari-mcp?style=social)](https://github.com/achiya-automation/safari-mcp/stargazers)
16
+ [![Featured in CLI-Anything](https://img.shields.io/badge/Featured%20in-CLI--Anything-blueviolet)](https://github.com/HKUDS/CLI-Anything/pull/212)
16
17
 
17
18
  <a href="vscode:mcp/install?%7B%22safari-mcp%22%3A%7B%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22safari-mcp%22%5D%7D%7D"><img src="https://img.shields.io/badge/VS_Code-Install_Server-0078d4?logo=visual-studio-code&logoColor=white" alt="Install in VS Code"></a>
18
19
  <a href="https://insiders.vscode.dev/redirect?url=vscode-insiders:mcp/install?%7B%22safari-mcp%22%3A%7B%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22safari-mcp%22%5D%7D%7D"><img src="https://img.shields.io/badge/VS_Code_Insiders-Install_Server-24bfa5?logo=visual-studio-code&logoColor=white" alt="Install in VS Code Insiders"></a>
package/index.js CHANGED
@@ -1009,6 +1009,33 @@ server.tool(
1009
1009
  }
1010
1010
  );
1011
1011
 
1012
+ server.tool(
1013
+ "safari_native_keyboard",
1014
+ "OS-level keyboard event via macOS CGEvent — sends a real keypress (with optional modifiers) to the Safari window WITHOUT activating Safari or stealing focus. Use when safari_press_key's JS path doesn't reach React trust-gated handlers (Discord ProseMirror Enter, Slack send, virtualized editors). Keys: enter, return, tab, escape, space, delete, backspace, up/down/left/right, home, end, pageup, pagedown, f1-f6, a-z, 0-9 and common punctuation. Modifiers: cmd, shift, alt, ctrl. Produces isTrusted:true events. Never activates Safari — runs entirely in the background.",
1015
+ {
1016
+ key: z.string().describe("Key name: enter, escape, tab, space, arrow keys, letters, digits, etc."),
1017
+ modifiers: z.array(z.string()).optional().default([]).describe("Modifier keys: cmd, shift, alt, ctrl"),
1018
+ },
1019
+ async (args) => {
1020
+ const result = await safari.nativeKeyboard(args);
1021
+ return { content: [{ type: "text", text: typeof result === 'string' ? result : JSON.stringify(result) }] };
1022
+ }
1023
+ );
1024
+
1025
+ server.tool(
1026
+ "safari_native_type",
1027
+ "Insert text into ANY editor via OS-level clipboard paste (CGEvent Cmd+V targeted to Safari window). Unlike safari_fill which manipulates DOM directly (breaking React/ProseMirror state), this goes through the real paste pipeline — ProseMirror/Slate/Draft.js process the paste event natively and update their internal model. After native_type, pressing Enter (via safari_native_keyboard) will actually submit the form because the framework state matches the DOM. Saves and restores the user's clipboard. No focus stealing. Use for Discord, Slack, and any editor where safari_fill works visually but the content isn't 'really there' when you try to submit.",
1028
+ {
1029
+ value: z.string().describe("Text to insert via clipboard paste"),
1030
+ selector: z.string().optional().describe("CSS selector of the editor element to focus first"),
1031
+ ref: z.string().optional().describe("Ref ID from safari_snapshot to focus first"),
1032
+ },
1033
+ async (args) => {
1034
+ const result = await safari.nativeType(args);
1035
+ return { content: [{ type: "text", text: typeof result === 'string' ? result : JSON.stringify(result) }] };
1036
+ }
1037
+ );
1038
+
1012
1039
  // ========== FORM INPUT ==========
1013
1040
 
1014
1041
  server.tool(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "safari-mcp",
3
- "version": "2.7.10",
3
+ "version": "2.7.12",
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
@@ -1397,6 +1397,62 @@ const jsKeyMap = {
1397
1397
  f1: "F1", f2: "F2", f3: "F3", f4: "F4", f5: "F5", f6: "F6",
1398
1398
  };
1399
1399
 
1400
+ // macOS virtual key codes for CGEvent keyboard (used by _helperNativeKeyboard).
1401
+ // These are the HID-level codes that postToPid uses, NOT JS keyCode values.
1402
+ const macKeyCodeMap = {
1403
+ enter: 36, return: 36, "numpad-enter": 76,
1404
+ tab: 48, space: 49, delete: 51, backspace: 51, escape: 53,
1405
+ up: 126, "arrowup": 126, down: 125, "arrowdown": 125,
1406
+ left: 123, "arrowleft": 123, right: 124, "arrowright": 124,
1407
+ home: 115, end: 119, pageup: 116, pagedown: 121,
1408
+ f1: 122, f2: 120, f3: 99, f4: 118, f5: 96, f6: 97,
1409
+ a: 0, s: 1, d: 2, f: 3, h: 4, g: 5, z: 6, x: 7, c: 8, v: 9,
1410
+ b: 11, q: 12, w: 13, e: 14, r: 15, y: 16, t: 17,
1411
+ "1": 18, "2": 19, "3": 20, "4": 21, "6": 22, "5": 23,
1412
+ "=": 24, "9": 25, "7": 26, "-": 27, "8": 28, "0": 29,
1413
+ "]": 30, o: 31, u: 32, "[": 33, i: 34, p: 35,
1414
+ l: 37, j: 38, "'": 39, k: 40, ";": 41, "\\": 42,
1415
+ ",": 43, "/": 44, n: 45, m: 46, ".": 47, "`": 50,
1416
+ };
1417
+
1418
+ // Native keyboard via CGEvent — sends a single key (with optional modifiers)
1419
+ // to the Safari window WITHOUT activating Safari or moving the mouse.
1420
+ // This produces isTrusted:true events that bypass React trust checks (Discord ProseMirror,
1421
+ // Slack virtualized editors, etc.) without any focus stealing. Requires Safari window ID.
1422
+ // Native type via clipboard paste (CGEvent Cmd+V targeted to Safari window).
1423
+ // Inserts text into ANY editor (ProseMirror, Slate, Draft.js, regular inputs,
1424
+ // contenteditable, cross-origin iframes) by going through the real paste pipeline.
1425
+ // Unlike safari_fill's synthetic paste, this updates the framework's internal
1426
+ // model state, so subsequent operations (like pressing Enter to submit in Discord)
1427
+ // see the text as "really there".
1428
+ export async function nativeType({ value, selector, ref }) {
1429
+ await ensureHelpers();
1430
+ if (!value) throw new Error("nativeType requires 'value'");
1431
+ // Focus the target element if selector/ref provided
1432
+ if (ref || selector) {
1433
+ const sel = ref ? refSelector(ref) : selector.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
1434
+ await runJS(`(function(){ var el=document.querySelector('${sel}'); if(el){el.focus();el.click();} return el?'focused':'not found'; })()`);
1435
+ await new Promise(r => setTimeout(r, 50));
1436
+ }
1437
+ return await _nativeTypeViaClipboard(value);
1438
+ }
1439
+
1440
+ export async function nativeKeyboard({ key, modifiers = [] }) {
1441
+ await ensureHelpers();
1442
+ if (!key) throw new Error("nativeKeyboard requires 'key'");
1443
+ const k = String(key).toLowerCase();
1444
+ const keyCode = macKeyCodeMap[k];
1445
+ if (keyCode === undefined) {
1446
+ throw new Error(`nativeKeyboard: unsupported key "${key}". Supported: ${Object.keys(macKeyCodeMap).join(", ")}`);
1447
+ }
1448
+ const geo = await _getSafariWindowGeometry();
1449
+ if (!geo.windowId) throw new Error("Cannot native-key without Safari window ID — would steal focus");
1450
+ const normalized = (modifiers || []).map(m => String(m).toLowerCase());
1451
+ await _helperNativeKeyboard(keyCode, normalized, geo.windowId);
1452
+ const modsLabel = normalized.length ? normalized.join("+") + "+" : "";
1453
+ return `Native key: ${modsLabel}${k} (CGEvent to window ${geo.windowId}, no focus steal)`;
1454
+ }
1455
+
1400
1456
  // System Events key codes — only used for paste_image, upload_file, save_pdf
1401
1457
  // (functions that truly require OS-level UI interaction)
1402
1458
 
@@ -1523,13 +1579,16 @@ export async function pressKey({ key, modifiers = [] }) {
1523
1579
  return `Pressed: ${modifiers.length ? modifiers.join("+") + "+" : ""}${key}`;
1524
1580
  }
1525
1581
 
1526
- // Native typing via clipboard paste — for cross-origin iframes (Intercom, Zendesk, etc.)
1527
- // JS can't access content inside cross-origin iframes, so we use OS-level CGEvent:
1528
- // 1. Save current clipboard
1529
- // 2. Set clipboard to our text
1530
- // 3. Cmd+V via CGEvent keyboard (targeted to window — NO focus steal, NO activate)
1531
- // 4. Restore clipboard
1532
- // Requires nativeClick to have focused the input inside the iframe first.
1582
+ // ========== NATIVE TYPE VIA CLIPBOARD PASTE ==========
1583
+ // Uses OS-level clipboard + CGEvent Cmd+V to insert text. Produces a REAL paste event
1584
+ // that ProseMirror/Slate/Draft.js process through their native paste handlers, updating
1585
+ // internal model state (not just the DOM). Also works for cross-origin iframes.
1586
+ // NO focus steal — sends CGEvent Cmd+V targeted to Safari window ID.
1587
+ //
1588
+ // Why this matters: synthetic DOM manipulation (safari_fill's "synthetic paste") writes
1589
+ // to the DOM but doesn't update React/ProseMirror state. Discord's onSubmit reads from
1590
+ // state, not DOM, so Enter submits empty. This function fixes that by going through the
1591
+ // real paste pipeline.
1533
1592
  async function _nativeTypeViaClipboard(text) {
1534
1593
  await _acquireClipboardLock();
1535
1594
  try {