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 +1 -0
- package/index.js +27 -0
- package/package.json +1 -1
- package/safari.js +66 -7
package/README.md
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
[](https://opensource.org/licenses/MIT)
|
|
14
14
|
[](https://www.apple.com/macos/)
|
|
15
15
|
[](https://github.com/achiya-automation/safari-mcp/stargazers)
|
|
16
|
+
[](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.
|
|
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
|
-
//
|
|
1527
|
-
//
|
|
1528
|
-
//
|
|
1529
|
-
//
|
|
1530
|
-
//
|
|
1531
|
-
//
|
|
1532
|
-
//
|
|
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 {
|