#!/usr/bin/env node
/**
* osascript MCP Server
*
* Executes AppleScript and JavaScript for Automation (JXA) on macOS.
* File deletion commands are blocked for safety.
*/
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 } from "child_process";
import { promisify } from "util";
const execFileAsync = promisify(execFile);
// Configuration from environment variables
const TIMEOUT = parseInt(process.env.OSASCRIPT_TIMEOUT || "30000", 10);
const LOG_SCRIPTS = process.env.OSASCRIPT_LOG_SCRIPTS !== "false";
// Maximum output buffer size (1MB)
const MAX_BUFFER = 1024 * 1024;
/**
* Security patterns that block file deletion operations.
* These patterns are designed to catch common deletion commands
* while allowing other system operations.
*/
const BLOCKED_PATTERNS = [
// Shell command deletions
/do\s+shell\s+script\s+["'][^"']*\b(rm|rmdir|unlink)\s/i,
/do\s+shell\s+script\s+["'][^"']*\brm\s+-[rf]/i,
/do\s+shell\s+script\s+["'][^"']*\brm\b/i,
// Finder deletions
/\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,
// System Events deletions
/System\s+Events["'\s]+to\s+delete/is,
// JXA deletions
/\.remove\s*\(\s*\)/i,
/\.delete\s*\(\s*\)/i,
/NSFileManager.*removeItem/i,
/FileManager.*removeItem/i,
// Additional shell deletions via JXA
/\$\s*\(\s*["']rm\s/i,
/app\.doShellScript\s*\([^)]*\brm\b/i,
];
/**
* Check if a script contains blocked patterns
* @param {string} script - The script to check
* @returns {{ blocked: boolean, reason?: string }} - Whether blocked and why
*/
function checkScriptSafety(script) {
for (const pattern of BLOCKED_PATTERNS) {
if (pattern.test(script)) {
return {
blocked: true,
reason: `Script contains blocked pattern: ${pattern.toString()}. File deletion operations are not allowed for safety.`,
};
}
}
return { blocked: false };
}
/**
* Escape a string for safe inclusion in AppleScript
* @param {string} str - String to escape
* @returns {string} - Escaped string
*/
function escapeForAppleScript(str) {
return str
.replace(/\\/g, "\\\\")
.replace(/"/g, '\\"')
.replace(/\n/g, "\\n")
.replace(/\r/g, "\\r");
}
/**
* Execute an osascript command
* @param {string} script - The script to execute
* @param {"AppleScript" | "JavaScript"} language - Script language
* @returns {Promise<string>} - Script output
*/
async function executeOsascript(script, language = "AppleScript") {
const args = [];
// Add language flag for JavaScript (JXA)
if (language === "JavaScript") {
args.push("-l", "JavaScript");
}
// Add script via -e flag
args.push("-e", script);
if (LOG_SCRIPTS) {
console.error(`[osascript-mcp] Executing ${language} script:`);
console.error(`[osascript-mcp] ${script.substring(0, 200)}${script.length > 200 ? "..." : ""}`);
}
try {
const { stdout, stderr } = await execFileAsync("/usr/bin/osascript", args, {
timeout: TIMEOUT,
maxBuffer: MAX_BUFFER,
encoding: "utf8",
});
if (stderr && LOG_SCRIPTS) {
console.error(`[osascript-mcp] stderr: ${stderr}`);
}
return stdout.trim();
} catch (error) {
if (error.killed) {
throw new Error(`Script execution timed out after ${TIMEOUT}ms`);
}
if (error.stderr) {
throw new Error(`osascript error: ${error.stderr}`);
}
throw error;
}
}
/**
* Main server class
*/
class OsascriptServer {
constructor() {
this.server = new Server(
{
name: "osascript-mcp",
version: "1.0.0",
},
{
capabilities: {
tools: {},
},
}
);
this.setupHandlers();
}
setupHandlers() {
// List available tools
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: "osascript",
description:
"Execute AppleScript or JavaScript for Automation (JXA) on macOS. " +
"Full access to system automation including shell commands, app control, and System Events. " +
"Only file deletion commands are blocked for safety.",
inputSchema: {
type: "object",
properties: {
script: {
type: "string",
description: "The script to execute",
},
language: {
type: "string",
enum: ["AppleScript", "JavaScript"],
default: "AppleScript",
description:
"Script language: AppleScript (default) or JavaScript for Automation (JXA)",
},
},
required: ["script"],
},
},
],
};
});
// Handle tool calls
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
if (request.params.name !== "osascript") {
throw new Error(`Unknown tool: ${request.params.name}`);
}
const { script, language = "AppleScript" } = request.params.arguments;
if (!script || typeof script !== "string") {
return {
content: [
{
type: "text",
text: "Error: Script is required and must be a string",
},
],
isError: true,
};
}
// Security check
const safetyCheck = checkScriptSafety(script);
if (safetyCheck.blocked) {
return {
content: [
{
type: "text",
text: `Security Error: ${safetyCheck.reason}`,
},
],
isError: true,
};
}
try {
const result = await executeOsascript(script, language);
return {
content: [
{
type: "text",
text: result || "(no output)",
},
],
};
} catch (error) {
return {
content: [
{
type: "text",
text: `Execution Error: ${error.message}`,
},
],
isError: true,
};
}
});
}
async run() {
const transport = new StdioServerTransport();
await this.server.connect(transport);
console.error("[osascript-mcp] Server started and connected via stdio");
}
}
// Start the server
const server = new OsascriptServer();
server.run().catch((error) => {
console.error("[osascript-mcp] Fatal error:", error);
process.exit(1);
});