#!/usr/bin/env node
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequestSchema,
ListToolsRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
import { execFile, exec } from "child_process";
import { promisify } from "util";
import * as fs from "fs/promises";
import * as path from "path";
const execFileAsync = promisify(execFile);
const execAsync = promisify(exec);
// Configuration
const TIMEOUT = parseInt(process.env.TIMEOUT || "30000", 10);
const LOG_OPS = process.env.LOG_OPERATIONS !== "false";
// ========================================
// Deletion commands only blocked (Full Power Mode)
// ========================================
const BLOCKED_PATTERNS = [
// Shell deletion commands
/\brm\s+-[rf]/i,
/\brm\s+/i,
/\brmdir\b/i,
/\bunlink\b/i,
/>\s*\/dev\/null.*&&.*rm/i,
// AppleScript deletion
/\bdelete\s+(every\s+)?(file|folder|item|document|disk\s+item)/i,
/\bmove\s+.+\s+to\s+(the\s+)?trash/i,
/\bempty\s+(the\s+)?trash/i,
// Finder deletion
/Finder.*delete/is,
/System\s+Events.*delete/is,
];
function checkNotDeletion(text) {
for (const pattern of BLOCKED_PATTERNS) {
if (pattern.test(text)) {
throw new Error(`Blocked: Contains deletion command`);
}
}
return true;
}
function log(msg) {
if (LOG_OPS) console.error(`[macos-control] ${msg}`);
}
// ========================================
// cliclick helper
// ========================================
async function cliclick(...args) {
log(`cliclick ${args.join(" ")}`);
const { stdout } = await execFileAsync("cliclick", args, { timeout: TIMEOUT });
return stdout.trim();
}
// ========================================
// Mouse operations
// ========================================
async function mouseClick(x, y) {
return await cliclick(`c:${x},${y}`);
}
async function mouseDoubleClick(x, y) {
return await cliclick(`dc:${x},${y}`);
}
async function mouseRightClick(x, y) {
return await cliclick(`rc:${x},${y}`);
}
async function mouseMove(x, y) {
return await cliclick(`m:${x},${y}`);
}
async function mouseDrag(fromX, fromY, toX, toY) {
return await cliclick(`dd:${fromX},${fromY}`, `du:${toX},${toY}`);
}
async function mouseScroll(direction, amount = 3) {
const key = direction === "up" ? "arrow-up" :
direction === "down" ? "arrow-down" :
direction === "left" ? "arrow-left" : "arrow-right";
const cmds = [];
for (let i = 0; i < amount; i++) {
cmds.push(`kp:${key}`);
}
return await cliclick(...cmds);
}
async function mouseGetPosition() {
const result = await cliclick("p:.");
// Output: "123,456"
const [x, y] = result.split(",").map(n => parseInt(n.trim()));
return { x, y };
}
// ========================================
// Keyboard operations
// ========================================
async function keyboardType(text) {
checkNotDeletion(text);
const escaped = text.replace(/'/g, "'\\''");
return await cliclick(`t:'${escaped}'`);
}
async function keyboardPress(key, modifiers = []) {
const combo = modifiers.length > 0
? `${modifiers.join("+")}+${key}`
: key;
return await cliclick(`kp:${combo}`);
}
async function keyboardShortcut(shortcut) {
const shortcuts = {
copy: "cmd+c",
paste: "cmd+v",
cut: "cmd+x",
undo: "cmd+z",
redo: "cmd+shift+z",
save: "cmd+s",
select_all: "cmd+a",
find: "cmd+f",
new: "cmd+n",
open: "cmd+o",
close: "cmd+w",
quit: "cmd+q",
tab: "cmd+t",
refresh: "cmd+r",
};
const combo = shortcuts[shortcut];
if (!combo) throw new Error(`Unknown shortcut: ${shortcut}`);
return await cliclick(`kp:${combo}`);
}
// ========================================
// Screen operations
// ========================================
async function screenCapture(region = null) {
const filename = `/tmp/screenshot_${Date.now()}.png`;
const args = ["-x"]; // Silent
if (region) {
args.push("-R", `${region.x},${region.y},${region.width},${region.height}`);
}
args.push(filename);
await execFileAsync("screencapture", args, { timeout: TIMEOUT });
const data = await fs.readFile(filename);
await fs.unlink(filename);
return {
base64: data.toString("base64"),
mimeType: "image/png"
};
}
async function screenGetSize() {
const script = `
use framework "AppKit"
set screenFrame to current application's NSScreen's mainScreen()'s frame()
set w to item 1 of item 2 of screenFrame
set h to item 2 of item 2 of screenFrame
return (w as integer) & "," & (h as integer)
`;
const { stdout } = await execFileAsync("osascript", ["-e", script], { timeout: TIMEOUT });
const [width, height] = stdout.trim().split(",").map(n => parseInt(n));
return { width, height };
}
async function screenGetColor(x, y) {
const result = await cliclick(`cp:${x},${y}`);
// Output: "127 63 0"
const [r, g, b] = result.split(" ").map(n => parseInt(n));
return { r, g, b, hex: `#${r.toString(16).padStart(2,"0")}${g.toString(16).padStart(2,"0")}${b.toString(16).padStart(2,"0")}` };
}
// ========================================
// Window operations
// ========================================
async function windowList() {
const script = `
tell application "System Events"
set output to ""
repeat with proc in (every process whose visible is true)
set procName to name of proc
try
repeat with win in (every window of proc)
set winName to name of win
set output to output & procName & ": " & winName & "\\n"
end repeat
end try
end repeat
return output
end tell
`;
const { stdout } = await execFileAsync("osascript", ["-e", script], { timeout: TIMEOUT });
return stdout.trim().split("\n").filter(l => l.length > 0);
}
async function windowFocus(appName) {
const script = `tell application "${appName}" to activate`;
await execFileAsync("osascript", ["-e", script], { timeout: TIMEOUT });
return `Focused: ${appName}`;
}
async function windowMove(appName, x, y) {
const script = `
tell application "System Events"
tell process "${appName}"
set position of window 1 to {${x}, ${y}}
end tell
end tell
`;
await execFileAsync("osascript", ["-e", script], { timeout: TIMEOUT });
return `Moved ${appName} to ${x},${y}`;
}
async function windowResize(appName, width, height) {
const script = `
tell application "System Events"
tell process "${appName}"
set size of window 1 to {${width}, ${height}}
end tell
end tell
`;
await execFileAsync("osascript", ["-e", script], { timeout: TIMEOUT });
return `Resized ${appName} to ${width}x${height}`;
}
// ========================================
// Generic execution (Full Power)
// ========================================
async function runAppleScript(script) {
checkNotDeletion(script);
log(`AppleScript: ${script.substring(0, 100)}...`);
const { stdout } = await execFileAsync("osascript", ["-e", script], { timeout: TIMEOUT });
return stdout.trim();
}
async function runShell(command) {
checkNotDeletion(command);
log(`Shell: ${command}`);
const { stdout, stderr } = await execAsync(command, { timeout: TIMEOUT });
return { stdout: stdout.trim(), stderr: stderr.trim() };
}
// ========================================
// MCP Server
// ========================================
const tools = [
{
name: "mouse_click",
description: "Left-click at screen coordinates",
inputSchema: {
type: "object",
properties: {
x: { type: "number", description: "X coordinate" },
y: { type: "number", description: "Y coordinate" }
},
required: ["x", "y"]
}
},
{
name: "mouse_double_click",
description: "Double-click at coordinates",
inputSchema: {
type: "object",
properties: {
x: { type: "number" },
y: { type: "number" }
},
required: ["x", "y"]
}
},
{
name: "mouse_right_click",
description: "Right-click at coordinates",
inputSchema: {
type: "object",
properties: {
x: { type: "number" },
y: { type: "number" }
},
required: ["x", "y"]
}
},
{
name: "mouse_move",
description: "Move cursor to position (without clicking)",
inputSchema: {
type: "object",
properties: {
x: { type: "number" },
y: { type: "number" }
},
required: ["x", "y"]
}
},
{
name: "mouse_drag",
description: "Drag from point A to point B",
inputSchema: {
type: "object",
properties: {
from_x: { type: "number" },
from_y: { type: "number" },
to_x: { type: "number" },
to_y: { type: "number" }
},
required: ["from_x", "from_y", "to_x", "to_y"]
}
},
{
name: "mouse_scroll",
description: "Scroll in a direction",
inputSchema: {
type: "object",
properties: {
direction: { type: "string", enum: ["up", "down", "left", "right"] },
amount: { type: "number", default: 3, description: "Number of scroll steps" }
},
required: ["direction"]
}
},
{
name: "mouse_get_position",
description: "Get current cursor position",
inputSchema: { type: "object", properties: {} }
},
{
name: "keyboard_type",
description: "Type text at current cursor position",
inputSchema: {
type: "object",
properties: {
text: { type: "string", description: "Text to type" }
},
required: ["text"]
}
},
{
name: "keyboard_press",
description: "Press a key or key combination (e.g., 'enter', 'cmd+c', 'shift+tab')",
inputSchema: {
type: "object",
properties: {
key: { type: "string", description: "Key name (enter, tab, escape, space, arrow-up, etc.)" },
modifiers: {
type: "array",
items: { type: "string", enum: ["cmd", "shift", "alt", "ctrl", "fn"] },
default: [],
description: "Modifier keys to hold"
}
},
required: ["key"]
}
},
{
name: "keyboard_shortcut",
description: "Execute common keyboard shortcut",
inputSchema: {
type: "object",
properties: {
shortcut: {
type: "string",
enum: ["copy", "paste", "cut", "undo", "redo", "save", "select_all", "find", "new", "open", "close", "quit", "tab", "refresh"],
description: "Shortcut name"
}
},
required: ["shortcut"]
}
},
{
name: "screen_capture",
description: "Take a screenshot (full screen or region)",
inputSchema: {
type: "object",
properties: {
region: {
type: "object",
properties: {
x: { type: "number" },
y: { type: "number" },
width: { type: "number" },
height: { type: "number" }
},
description: "Optional: capture specific region"
}
}
}
},
{
name: "screen_get_size",
description: "Get main screen dimensions",
inputSchema: { type: "object", properties: {} }
},
{
name: "screen_get_color",
description: "Get pixel color at coordinates",
inputSchema: {
type: "object",
properties: {
x: { type: "number" },
y: { type: "number" }
},
required: ["x", "y"]
}
},
{
name: "window_list",
description: "List all visible windows",
inputSchema: { type: "object", properties: {} }
},
{
name: "window_focus",
description: "Focus/activate an application",
inputSchema: {
type: "object",
properties: {
app_name: { type: "string", description: "Application name (e.g., Safari, Finder, Terminal)" }
},
required: ["app_name"]
}
},
{
name: "window_move",
description: "Move application window to position",
inputSchema: {
type: "object",
properties: {
app_name: { type: "string" },
x: { type: "number" },
y: { type: "number" }
},
required: ["app_name", "x", "y"]
}
},
{
name: "window_resize",
description: "Resize application window",
inputSchema: {
type: "object",
properties: {
app_name: { type: "string" },
width: { type: "number" },
height: { type: "number" }
},
required: ["app_name", "width", "height"]
}
},
{
name: "run_applescript",
description: "Run arbitrary AppleScript code (full power, deletion blocked)",
inputSchema: {
type: "object",
properties: {
script: { type: "string", description: "AppleScript code to execute" }
},
required: ["script"]
}
},
{
name: "run_shell",
description: "Run shell command (full power, deletion blocked)",
inputSchema: {
type: "object",
properties: {
command: { type: "string", description: "Shell command to execute" }
},
required: ["command"]
}
}
];
class MacOSControlServer {
constructor() {
this.server = new Server(
{ name: "macos-control-fullpower", version: "1.0.0" },
{ capabilities: { tools: {} } }
);
this.setupHandlers();
}
setupHandlers() {
this.server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools }));
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
log(`Tool: ${name} Args: ${JSON.stringify(args)}`);
try {
let result;
switch (name) {
case "mouse_click":
result = await mouseClick(args.x, args.y);
break;
case "mouse_double_click":
result = await mouseDoubleClick(args.x, args.y);
break;
case "mouse_right_click":
result = await mouseRightClick(args.x, args.y);
break;
case "mouse_move":
result = await mouseMove(args.x, args.y);
break;
case "mouse_drag":
result = await mouseDrag(args.from_x, args.from_y, args.to_x, args.to_y);
break;
case "mouse_scroll":
result = await mouseScroll(args.direction, args.amount || 3);
break;
case "mouse_get_position":
result = await mouseGetPosition();
break;
case "keyboard_type":
result = await keyboardType(args.text);
break;
case "keyboard_press":
result = await keyboardPress(args.key, args.modifiers || []);
break;
case "keyboard_shortcut":
result = await keyboardShortcut(args.shortcut);
break;
case "screen_capture":
result = await screenCapture(args.region);
break;
case "screen_get_size":
result = await screenGetSize();
break;
case "screen_get_color":
result = await screenGetColor(args.x, args.y);
break;
case "window_list":
result = await windowList();
break;
case "window_focus":
result = await windowFocus(args.app_name);
break;
case "window_move":
result = await windowMove(args.app_name, args.x, args.y);
break;
case "window_resize":
result = await windowResize(args.app_name, args.width, args.height);
break;
case "run_applescript":
result = await runAppleScript(args.script);
break;
case "run_shell":
result = await runShell(args.command);
break;
default:
throw new Error(`Unknown tool: ${name}`);
}
// screen_capture returns image data
if (name === "screen_capture" && result.base64) {
return {
content: [
{
type: "image",
data: result.base64,
mimeType: result.mimeType
}
]
};
}
return {
content: [{ type: "text", text: typeof result === "object" ? JSON.stringify(result, null, 2) : String(result || "OK") }]
};
} catch (error) {
return {
content: [{ type: "text", text: `Error: ${error.message}` }],
isError: true
};
}
});
}
async run() {
const transport = new StdioServerTransport();
await this.server.connect(transport);
log("Server running (Full Power Mode - deletion only blocked)");
}
}
const server = new MacOSControlServer();
server.run().catch(e => {
console.error("Fatal:", e);
process.exit(1);
});