safari-mcp 2.7.11 → 2.7.13
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 +14 -0
- package/package.json +1 -1
- package/safari.js +34 -8
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
|
@@ -1022,6 +1022,20 @@ server.tool(
|
|
|
1022
1022
|
}
|
|
1023
1023
|
);
|
|
1024
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
|
+
|
|
1025
1039
|
// ========== FORM INPUT ==========
|
|
1026
1040
|
|
|
1027
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.13",
|
|
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
|
@@ -1419,6 +1419,24 @@ const macKeyCodeMap = {
|
|
|
1419
1419
|
// to the Safari window WITHOUT activating Safari or moving the mouse.
|
|
1420
1420
|
// This produces isTrusted:true events that bypass React trust checks (Discord ProseMirror,
|
|
1421
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
|
+
|
|
1422
1440
|
export async function nativeKeyboard({ key, modifiers = [] }) {
|
|
1423
1441
|
await ensureHelpers();
|
|
1424
1442
|
if (!key) throw new Error("nativeKeyboard requires 'key'");
|
|
@@ -1542,7 +1560,12 @@ export async function pressKey({ key, modifiers = [] }) {
|
|
|
1542
1560
|
if (!prevented) {
|
|
1543
1561
|
if ('${safeKey}' === 'Enter') {
|
|
1544
1562
|
if (el.tagName === 'INPUT') { el.form && el.form.dispatchEvent(new Event('submit',{bubbles:true})); }
|
|
1545
|
-
else { document.execCommand('insertLineBreak'); }
|
|
1563
|
+
else if (el.tagName === 'TEXTAREA') { document.execCommand('insertLineBreak'); }
|
|
1564
|
+
else if (el.isContentEditable && ${shiftKey}) { document.execCommand('insertLineBreak'); }
|
|
1565
|
+
// ContentEditable + Enter (no Shift): do NOT insertLineBreak.
|
|
1566
|
+
// Modern editors (Discord Slate, Slack, Notion, Medium) handle Enter
|
|
1567
|
+
// in their own keydown listener to trigger submit/send/newBlock.
|
|
1568
|
+
// insertLineBreak would double-act: the app submits AND we add a newline.
|
|
1546
1569
|
} else if ('${safeKey}' === 'Tab') {
|
|
1547
1570
|
var focusable = [...document.querySelectorAll('input,textarea,select,button,a,[tabindex]')].filter(function(e){return e.tabIndex>=0;});
|
|
1548
1571
|
var idx = focusable.indexOf(el);
|
|
@@ -1561,13 +1584,16 @@ export async function pressKey({ key, modifiers = [] }) {
|
|
|
1561
1584
|
return `Pressed: ${modifiers.length ? modifiers.join("+") + "+" : ""}${key}`;
|
|
1562
1585
|
}
|
|
1563
1586
|
|
|
1564
|
-
//
|
|
1565
|
-
//
|
|
1566
|
-
//
|
|
1567
|
-
//
|
|
1568
|
-
//
|
|
1569
|
-
//
|
|
1570
|
-
//
|
|
1587
|
+
// ========== NATIVE TYPE VIA CLIPBOARD PASTE ==========
|
|
1588
|
+
// Uses OS-level clipboard + CGEvent Cmd+V to insert text. Produces a REAL paste event
|
|
1589
|
+
// that ProseMirror/Slate/Draft.js process through their native paste handlers, updating
|
|
1590
|
+
// internal model state (not just the DOM). Also works for cross-origin iframes.
|
|
1591
|
+
// NO focus steal — sends CGEvent Cmd+V targeted to Safari window ID.
|
|
1592
|
+
//
|
|
1593
|
+
// Why this matters: synthetic DOM manipulation (safari_fill's "synthetic paste") writes
|
|
1594
|
+
// to the DOM but doesn't update React/ProseMirror state. Discord's onSubmit reads from
|
|
1595
|
+
// state, not DOM, so Enter submits empty. This function fixes that by going through the
|
|
1596
|
+
// real paste pipeline.
|
|
1571
1597
|
async function _nativeTypeViaClipboard(text) {
|
|
1572
1598
|
await _acquireClipboardLock();
|
|
1573
1599
|
try {
|