execute_script
Automate macOS tasks by executing AppleScript or JavaScript for Automation (JXA) scripts to control applications like Terminal, Safari, or Finder. Supports pre-defined KB scripts, raw code, or external script paths with flexible input options and execution settings.
Instructions
Automate macOS tasks using AppleScript or JXA (JavaScript for Automation) to control applications like Terminal, Chrome, Safari, Finder, etc.
1. Script Source (Choose one):
kb_script_id(string): Preferred. Executes a pre-defined script from the knowledge base by its ID. Useget_scripting_tipsto find IDs and inputs. Supports placeholder substitution viainput_dataorarguments. Ex:kb_script_id: "safari_get_front_tab_url".script_content(string): Executes raw AppleScript/JXA code. Good for simple or dynamic scripts. Ex:script_content: "tell application \"Finder\" to empty trash".script_path(string): Executes a script from an absolute POSIX path on the server. Ex:/Users/user/myscripts/myscript.applescript.
2. Script Inputs (Optional):
input_data(JSON object): Forkb_script_id, provides named inputs (e.g.,--MCP_INPUT:keyName). Values (string, number, boolean, simple array/object) are auto-converted. Ex:input_data: { "folder_name": "New Docs" }.arguments(array of strings): Forscript_path(passes toon run argv/run(argv)). Forkb_script_id, used for positional args (e.g.,--MCP_ARG_1).
3. Execution Options (Optional):
language('applescript' | 'javascript'): Specify forscript_content/script_path(default: 'applescript'). Inferred forkb_script_id.timeout_seconds(integer, optional, default: 60): Sets the maximum time (in seconds) the script is allowed to run. Increase for potentially long-running operations.output_format_mode(enum, optional, default: 'auto'): Controlsosascriptoutput formatting.'auto': Smart default - resolves to'human_readable'for AppleScript and'direct'for JXA.'human_readable': For AppleScript, uses-s hflag.'structured_error': For AppleScript, uses-s sflag (structured errors).'structured_output_and_error': For AppleScript, uses-s ssflag (structured output & errors).'direct': No special output flags (recommended for JXA).
include_executed_script_in_output(boolean, optional, default: false): Iftrue, the final script content (after any placeholder substitutions) or script path that was executed will be included in the response. This is useful for debugging and understanding exactly what was run. Defaults to false.include_substitution_logs(boolean, default: false): Forkb_script_id, includes detailed placeholder substitution logs.report_execution_time(boolean, optional, default: false): Iftrue, an additional message with the formatted script execution time will be included in the response. Defaults to false.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| arguments | No | ||
| include_executed_script_in_output | No | ||
| include_substitution_logs | No | ||
| input_data | No | ||
| kb_script_id | No | ||
| language | No | ||
| output_format_mode | No | ||
| report_execution_time | No | ||
| script_content | No | ||
| script_path | No | ||
| timeout_seconds | No |
Implementation Reference
- src/server.ts:129-340 (handler)The core handler function for the 'execute_script' tool. Parses input schema, handles script sources (kb_script_id, script_content, script_path), performs placeholder substitution for KB scripts, executes the script using ScriptExecutor, processes output including substitution logs if requested, detects errors, and returns structured ExecuteScriptResponse.async (args: unknown) => { const input = ExecuteScriptInputSchema.parse(args); let execution_time_seconds: number | undefined; let scriptContentToExecute: string | undefined = input.script_content; let scriptPathToExecute: string | undefined = input.script_path; let languageToUse: 'applescript' | 'javascript'; let finalArgumentsForScriptFile = input.arguments || []; let substitutionLogs: string[] = []; logger.debug('execute_script called with input:', input); // Construct the main part of the response first const mainOutputContent: { type: 'text'; text: string }[] = []; if (input.kb_script_id) { const kb = await getKnowledgeBase(); const tip = kb.tips.find((t: { id: string }) => t.id === input.kb_script_id); if (!tip) { throw new sdkTypes.McpError(sdkTypes.ErrorCode.InvalidParams, `Knowledge base script with ID '${input.kb_script_id}' not found.`); } if (!tip.script) { throw new sdkTypes.McpError(sdkTypes.ErrorCode.InternalError, `Knowledge base script ID '${input.kb_script_id}' has no script content.`); } languageToUse = tip.language; scriptPathToExecute = undefined; finalArgumentsForScriptFile = []; if (tip.script) { // Check if tip.script exists before substitution const substitutionResult: SubstitutionResult = substitutePlaceholders({ scriptContent: tip.script, // Use tip.script directly inputData: input.input_data, args: input.arguments, // Pass input.arguments which might be undefined includeSubstitutionLogs: input.include_substitution_logs || false, }); scriptContentToExecute = substitutionResult.substitutedScript; substitutionLogs = substitutionResult.logs; } logger.info('Executing Knowledge Base script', { id: tip.id, finalLength: scriptContentToExecute?.length }); } else if (input.script_path || input.script_content) { languageToUse = input.language || 'applescript'; if (input.script_path) { logger.debug('Executing script from path', { scriptPath: input.script_path, language: languageToUse }); } else if (input.script_content) { logger.debug('Executing script from content', { language: languageToUse, initialLength: input.script_content.length }); } } else { throw new sdkTypes.McpError(sdkTypes.ErrorCode.InvalidParams, "No script source provided (content, path, or KB ID)."); } // Log the actual script to be executed (especially useful for KB scripts after substitution) if (scriptContentToExecute) { logger.debug('Final script content to be executed:', { language: languageToUse, script: scriptContentToExecute }); } else if (scriptPathToExecute) { // For scriptPath, we don't log content here, just that it's a path-based execution logger.debug('Executing script via path (content not logged here):', { scriptPath: scriptPathToExecute, language: languageToUse }); } try { const result = await scriptExecutor.execute( { content: scriptContentToExecute, path: scriptPathToExecute }, { language: languageToUse, timeoutMs: (input.timeout_seconds || 60) * 1000, output_format_mode: input.output_format_mode || 'auto', arguments: scriptPathToExecute ? finalArgumentsForScriptFile : [], } ); execution_time_seconds = result.execution_time_seconds; if (result.stderr) { logger.warn('Script execution produced stderr (even on success)', { stderr: result.stderr }); } let isError = false; const errorPattern = /^\s*error[:\s-]/i; if (errorPattern.test(result.stdout)) { isError = true; } if (input.include_substitution_logs && substitutionLogs.length > 0) { const logsHeader = "\n--- Substitution Logs ---\n"; const logsString = substitutionLogs.join('\n'); // Prepend to main result, not just stdout string if other parts exist mainOutputContent.push({ type: 'text', text: `${logsHeader}${logsString}\n\n--- Original STDOUT ---\n${result.stdout}` }); } mainOutputContent.push({ type: 'text', text: result.stdout }); if (input.include_executed_script_in_output) { let scriptIdentifier = "Script source not determined (should not happen)."; if (scriptContentToExecute) { scriptIdentifier = `\n--- Executed Script Content ---\n${scriptContentToExecute}`; } else if (scriptPathToExecute) { scriptIdentifier = `\n--- Executed Script Path ---\n${scriptPathToExecute}`; } mainOutputContent.push({ type: 'text', text: scriptIdentifier }); } // Now, construct the final response with potential first-call info const finalResponseContent: { type: 'text'; text: string }[] = []; finalResponseContent.push(...mainOutputContent); // Add the actual script output if (!IS_E2E_TESTING && !hasEmittedFirstCallInfo) { finalResponseContent.push({ type: 'text', text: serverInfoMessage }); hasEmittedFirstCallInfo = true; } const response: ExecuteScriptResponse = { content: finalResponseContent, isError, }; if (input.report_execution_time) { const ms = result.execution_time_seconds * 1000; let timeMessage = "Script executed in "; if (ms < 1) { // Less than 1 millisecond timeMessage += "<1 millisecond."; } else if (ms < 1000) { // 1ms up to 999ms timeMessage += `${ms.toFixed(0)} milliseconds.`; } else if (ms < 60000) { // 1 second up to 59.999 seconds timeMessage += `${(ms / 1000).toFixed(2)} seconds.`; } else { const totalSeconds = ms / 1000; const minutes = Math.floor(totalSeconds / 60); const remainingSeconds = Math.round(totalSeconds % 60); timeMessage += `${minutes} minute(s) and ${remainingSeconds} seconds.`; } response.content.push({ type: 'text', text: `${timeMessage}` }); } return response; } catch (error: unknown) { const typedError = error as ScriptExecutionError; const execError = error as ScriptExecutionError; execution_time_seconds = execError.execution_time_seconds; let baseErrorMessage = 'Script execution failed. '; if (execError.name === "UnsupportedPlatformError") { throw new sdkTypes.McpError(sdkTypes.ErrorCode.InvalidRequest, execError.message); } if (execError.name === "ScriptFileAccessError") { throw new sdkTypes.McpError(sdkTypes.ErrorCode.InvalidParams, execError.message); } if (execError.isTimeout) { throw new sdkTypes.McpError(sdkTypes.ErrorCode.RequestTimeout, `Script execution timed out after ${input.timeout_seconds || 60} seconds.`); } baseErrorMessage += execError.stderr?.trim() ? `Details: ${execError.stderr.trim()}` : (execError.message || 'No specific error message from script.'); let finalErrorMessage = baseErrorMessage; const permissionErrorPattern = /Not authorized|access for assistive devices is disabled|errAEEventNotPermitted|errAEAccessDenied|-1743|-10004/i; const likelyPermissionError = execError.stderr && permissionErrorPattern.test(execError.stderr); // Sometimes exit code 1 with no stderr can also be a silent permission issue const possibleSilentPermissionError = execError.exitCode === 1 && !execError.stderr?.trim(); if (likelyPermissionError || possibleSilentPermissionError) { finalErrorMessage = `${baseErrorMessage}\n\nPOSSIBLE PERMISSION ISSUE: Ensure the application running this server (e.g., Terminal, Node) has required permissions in 'System Settings > Privacy & Security > Automation' and 'Accessibility'. See README.md. The target application for the script may also need specific permissions.`; } // Append the attempted script to the error message let scriptIdentifierForError = "Script source not determined (should not happen)."; if (scriptContentToExecute) { scriptIdentifierForError = `\n\n--- Script Attempted (Content) ---\n${scriptContentToExecute}`; } else if (scriptPathToExecute) { scriptIdentifierForError = `\n\n--- Script Attempted (Path) ---\n${scriptPathToExecute}`; } finalErrorMessage += scriptIdentifierForError; if (input.include_substitution_logs && substitutionLogs.length > 0) { finalErrorMessage += `\n\n--- Substitution Logs ---\n${substitutionLogs.join('\n')}`; } logger.error('execute_script handler error', { execution_time_seconds }); // Construct a complete error response, with potential first-call info const errorOutputParts: string[] = [finalErrorMessage]; if (!IS_E2E_TESTING && !hasEmittedFirstCallInfo) { errorOutputParts.push(serverInfoMessage); hasEmittedFirstCallInfo = true; } const errorResponse: ExecuteScriptResponse = { content: [{ type: 'text', text: errorOutputParts.join('\n\n') }], isError: true, }; if (input.report_execution_time && typedError.execution_time_seconds !== undefined) { const ms = typedError.execution_time_seconds * 1000; let timeMessage = "Script execution failed after "; if (ms < 1) { // Less than 1 millisecond timeMessage += "<1 millisecond."; } else if (ms < 1000) { // 1ms up to 999ms timeMessage += `${ms.toFixed(0)} milliseconds.`; } else if (ms < 60000) { // 1 second up to 59.999 seconds timeMessage += `${(ms / 1000).toFixed(2)} seconds.`; } else { const totalSeconds = ms / 1000; const minutes = Math.floor(totalSeconds / 60); const remainingSeconds = Math.round(totalSeconds % 60); timeMessage += `${minutes} minute(s) and ${remainingSeconds} seconds.`; } errorResponse.content.push({ type: 'text', text: `\n${timeMessage}` }); } return errorResponse; } }
- src/server.ts:102-341 (registration)Registration of the 'execute_script' tool with MCP server using server.tool(), including name, description, input shape, and handler function.server.tool( 'execute_script', `Automate macOS tasks using AppleScript or JXA (JavaScript for Automation) to control applications like Terminal, Chrome, Safari, Finder, etc. **1. Script Source (Choose one):** * \`kb_script_id\` (string): **Preferred.** Executes a pre-defined script from the knowledge base by its ID. Use \`get_scripting_tips\` to find IDs and inputs. Supports placeholder substitution via \`input_data\` or \`arguments\`. Ex: \`kb_script_id: "safari_get_front_tab_url"\`. * \`script_content\` (string): Executes raw AppleScript/JXA code. Good for simple or dynamic scripts. Ex: \`script_content: "tell application \\"Finder\\" to empty trash"\`. * \`script_path\` (string): Executes a script from an absolute POSIX path on the server. Ex: \`/Users/user/myscripts/myscript.applescript\`. **2. Script Inputs (Optional):** * \`input_data\` (JSON object): For \`kb_script_id\`, provides named inputs (e.g., \`--MCP_INPUT:keyName\`). Values (string, number, boolean, simple array/object) are auto-converted. Ex: \`input_data: { "folder_name": "New Docs" }\`. * \`arguments\` (array of strings): For \`script_path\` (passes to \`on run argv\` / \`run(argv)\`). For \`kb_script_id\`, used for positional args (e.g., \`--MCP_ARG_1\`). **3. Execution Options (Optional):** * \`language\` ('applescript' | 'javascript'): Specify for \`script_content\`/\`script_path\` (default: 'applescript'). Inferred for \`kb_script_id\`. * \`timeout_seconds\` (integer, optional, default: 60): Sets the maximum time (in seconds) the script is allowed to run. Increase for potentially long-running operations. * \`output_format_mode\` (enum, optional, default: 'auto'): Controls \`osascript\` output formatting. * \`'auto'\`: Smart default - resolves to \`'human_readable'\` for AppleScript and \`'direct'\` for JXA. * \`'human_readable'\`: For AppleScript, uses \`-s h\` flag. * \`'structured_error'\`: For AppleScript, uses \`-s s\` flag (structured errors). * \`'structured_output_and_error'\`: For AppleScript, uses \`-s ss\` flag (structured output & errors). * \`'direct'\`: No special output flags (recommended for JXA). * \`include_executed_script_in_output\` (boolean, optional, default: false): If \`true\`, the final script content (after any placeholder substitutions) or script path that was executed will be included in the response. This is useful for debugging and understanding exactly what was run. Defaults to false. * \`include_substitution_logs\` (boolean, default: false): For \`kb_script_id\`, includes detailed placeholder substitution logs. * \`report_execution_time\` (boolean, optional, default: false): If \`true\`, an additional message with the formatted script execution time will be included in the response. Defaults to false. `, ExecuteScriptInputShape, async (args: unknown) => { const input = ExecuteScriptInputSchema.parse(args); let execution_time_seconds: number | undefined; let scriptContentToExecute: string | undefined = input.script_content; let scriptPathToExecute: string | undefined = input.script_path; let languageToUse: 'applescript' | 'javascript'; let finalArgumentsForScriptFile = input.arguments || []; let substitutionLogs: string[] = []; logger.debug('execute_script called with input:', input); // Construct the main part of the response first const mainOutputContent: { type: 'text'; text: string }[] = []; if (input.kb_script_id) { const kb = await getKnowledgeBase(); const tip = kb.tips.find((t: { id: string }) => t.id === input.kb_script_id); if (!tip) { throw new sdkTypes.McpError(sdkTypes.ErrorCode.InvalidParams, `Knowledge base script with ID '${input.kb_script_id}' not found.`); } if (!tip.script) { throw new sdkTypes.McpError(sdkTypes.ErrorCode.InternalError, `Knowledge base script ID '${input.kb_script_id}' has no script content.`); } languageToUse = tip.language; scriptPathToExecute = undefined; finalArgumentsForScriptFile = []; if (tip.script) { // Check if tip.script exists before substitution const substitutionResult: SubstitutionResult = substitutePlaceholders({ scriptContent: tip.script, // Use tip.script directly inputData: input.input_data, args: input.arguments, // Pass input.arguments which might be undefined includeSubstitutionLogs: input.include_substitution_logs || false, }); scriptContentToExecute = substitutionResult.substitutedScript; substitutionLogs = substitutionResult.logs; } logger.info('Executing Knowledge Base script', { id: tip.id, finalLength: scriptContentToExecute?.length }); } else if (input.script_path || input.script_content) { languageToUse = input.language || 'applescript'; if (input.script_path) { logger.debug('Executing script from path', { scriptPath: input.script_path, language: languageToUse }); } else if (input.script_content) { logger.debug('Executing script from content', { language: languageToUse, initialLength: input.script_content.length }); } } else { throw new sdkTypes.McpError(sdkTypes.ErrorCode.InvalidParams, "No script source provided (content, path, or KB ID)."); } // Log the actual script to be executed (especially useful for KB scripts after substitution) if (scriptContentToExecute) { logger.debug('Final script content to be executed:', { language: languageToUse, script: scriptContentToExecute }); } else if (scriptPathToExecute) { // For scriptPath, we don't log content here, just that it's a path-based execution logger.debug('Executing script via path (content not logged here):', { scriptPath: scriptPathToExecute, language: languageToUse }); } try { const result = await scriptExecutor.execute( { content: scriptContentToExecute, path: scriptPathToExecute }, { language: languageToUse, timeoutMs: (input.timeout_seconds || 60) * 1000, output_format_mode: input.output_format_mode || 'auto', arguments: scriptPathToExecute ? finalArgumentsForScriptFile : [], } ); execution_time_seconds = result.execution_time_seconds; if (result.stderr) { logger.warn('Script execution produced stderr (even on success)', { stderr: result.stderr }); } let isError = false; const errorPattern = /^\s*error[:\s-]/i; if (errorPattern.test(result.stdout)) { isError = true; } if (input.include_substitution_logs && substitutionLogs.length > 0) { const logsHeader = "\n--- Substitution Logs ---\n"; const logsString = substitutionLogs.join('\n'); // Prepend to main result, not just stdout string if other parts exist mainOutputContent.push({ type: 'text', text: `${logsHeader}${logsString}\n\n--- Original STDOUT ---\n${result.stdout}` }); } mainOutputContent.push({ type: 'text', text: result.stdout }); if (input.include_executed_script_in_output) { let scriptIdentifier = "Script source not determined (should not happen)."; if (scriptContentToExecute) { scriptIdentifier = `\n--- Executed Script Content ---\n${scriptContentToExecute}`; } else if (scriptPathToExecute) { scriptIdentifier = `\n--- Executed Script Path ---\n${scriptPathToExecute}`; } mainOutputContent.push({ type: 'text', text: scriptIdentifier }); } // Now, construct the final response with potential first-call info const finalResponseContent: { type: 'text'; text: string }[] = []; finalResponseContent.push(...mainOutputContent); // Add the actual script output if (!IS_E2E_TESTING && !hasEmittedFirstCallInfo) { finalResponseContent.push({ type: 'text', text: serverInfoMessage }); hasEmittedFirstCallInfo = true; } const response: ExecuteScriptResponse = { content: finalResponseContent, isError, }; if (input.report_execution_time) { const ms = result.execution_time_seconds * 1000; let timeMessage = "Script executed in "; if (ms < 1) { // Less than 1 millisecond timeMessage += "<1 millisecond."; } else if (ms < 1000) { // 1ms up to 999ms timeMessage += `${ms.toFixed(0)} milliseconds.`; } else if (ms < 60000) { // 1 second up to 59.999 seconds timeMessage += `${(ms / 1000).toFixed(2)} seconds.`; } else { const totalSeconds = ms / 1000; const minutes = Math.floor(totalSeconds / 60); const remainingSeconds = Math.round(totalSeconds % 60); timeMessage += `${minutes} minute(s) and ${remainingSeconds} seconds.`; } response.content.push({ type: 'text', text: `${timeMessage}` }); } return response; } catch (error: unknown) { const typedError = error as ScriptExecutionError; const execError = error as ScriptExecutionError; execution_time_seconds = execError.execution_time_seconds; let baseErrorMessage = 'Script execution failed. '; if (execError.name === "UnsupportedPlatformError") { throw new sdkTypes.McpError(sdkTypes.ErrorCode.InvalidRequest, execError.message); } if (execError.name === "ScriptFileAccessError") { throw new sdkTypes.McpError(sdkTypes.ErrorCode.InvalidParams, execError.message); } if (execError.isTimeout) { throw new sdkTypes.McpError(sdkTypes.ErrorCode.RequestTimeout, `Script execution timed out after ${input.timeout_seconds || 60} seconds.`); } baseErrorMessage += execError.stderr?.trim() ? `Details: ${execError.stderr.trim()}` : (execError.message || 'No specific error message from script.'); let finalErrorMessage = baseErrorMessage; const permissionErrorPattern = /Not authorized|access for assistive devices is disabled|errAEEventNotPermitted|errAEAccessDenied|-1743|-10004/i; const likelyPermissionError = execError.stderr && permissionErrorPattern.test(execError.stderr); // Sometimes exit code 1 with no stderr can also be a silent permission issue const possibleSilentPermissionError = execError.exitCode === 1 && !execError.stderr?.trim(); if (likelyPermissionError || possibleSilentPermissionError) { finalErrorMessage = `${baseErrorMessage}\n\nPOSSIBLE PERMISSION ISSUE: Ensure the application running this server (e.g., Terminal, Node) has required permissions in 'System Settings > Privacy & Security > Automation' and 'Accessibility'. See README.md. The target application for the script may also need specific permissions.`; } // Append the attempted script to the error message let scriptIdentifierForError = "Script source not determined (should not happen)."; if (scriptContentToExecute) { scriptIdentifierForError = `\n\n--- Script Attempted (Content) ---\n${scriptContentToExecute}`; } else if (scriptPathToExecute) { scriptIdentifierForError = `\n\n--- Script Attempted (Path) ---\n${scriptPathToExecute}`; } finalErrorMessage += scriptIdentifierForError; if (input.include_substitution_logs && substitutionLogs.length > 0) { finalErrorMessage += `\n\n--- Substitution Logs ---\n${substitutionLogs.join('\n')}`; } logger.error('execute_script handler error', { execution_time_seconds }); // Construct a complete error response, with potential first-call info const errorOutputParts: string[] = [finalErrorMessage]; if (!IS_E2E_TESTING && !hasEmittedFirstCallInfo) { errorOutputParts.push(serverInfoMessage); hasEmittedFirstCallInfo = true; } const errorResponse: ExecuteScriptResponse = { content: [{ type: 'text', text: errorOutputParts.join('\n\n') }], isError: true, }; if (input.report_execution_time && typedError.execution_time_seconds !== undefined) { const ms = typedError.execution_time_seconds * 1000; let timeMessage = "Script execution failed after "; if (ms < 1) { // Less than 1 millisecond timeMessage += "<1 millisecond."; } else if (ms < 1000) { // 1ms up to 999ms timeMessage += `${ms.toFixed(0)} milliseconds.`; } else if (ms < 60000) { // 1 second up to 59.999 seconds timeMessage += `${(ms / 1000).toFixed(2)} seconds.`; } else { const totalSeconds = ms / 1000; const minutes = Math.floor(totalSeconds / 60); const remainingSeconds = Math.round(totalSeconds % 60); timeMessage += `${minutes} minute(s) and ${remainingSeconds} seconds.`; } errorResponse.content.push({ type: 'text', text: `\n${timeMessage}` }); } return errorResponse; } } );
- src/schemas.ts:12-50 (schema)Zod schema for validating ExecuteScriptInput, ensuring exactly one script source (kb_script_id, script_content, or script_path) is provided, with optional params like language, timeout, etc.export const ExecuteScriptInputSchema = z.object({ kb_script_id: z.string().optional().describe( 'The ID of a knowledge base script to execute. Replaces script_content and script_path if provided.', ), script_content: z.string().optional().describe( 'The content of the script to execute. Required if kb_script_id or script_path is not provided.', ), script_path: z.string().optional().describe( 'The path to the script file to execute. Required if kb_script_id or script_content is not provided.', ), arguments: z.array(z.string()).optional().describe( 'Optional arguments to pass to the script. For AppleScript, these are passed to the main `run` handler. For JXA, these are passed to the `run` function.', ), input_data: z.record(z.unknown()).optional().describe( 'Optional JSON object to provide named inputs for --MCP_INPUT placeholders in knowledge base scripts.', ), language: z.enum(['applescript', 'javascript']).optional().describe( "Specifies the scripting language. Crucial for `script_content` and `script_path` if not 'applescript'. Defaults to 'applescript'. Inferred if using `kb_script_id`.", ), timeout_seconds: z.number().int().optional().default(60).describe( 'The timeout for the script execution in seconds. Defaults to 60.', ), output_format_mode: z.enum(['auto', 'human_readable', 'structured_error', 'structured_output_and_error', 'direct']).optional().default('auto').describe( "Controls osascript output formatting. \n'auto': (Default) Smart selection based on language (AppleScript: human_readable, JXA: direct). \n'human_readable': AppleScript -s h. \n'structured_error': AppleScript -s s. \n'structured_output_and_error': AppleScript -s ss. \n'direct': No -s flags (recommended for JXA)." ), report_execution_time: z.boolean().optional().default(false).describe( 'If true, the tool will return an additional message containing the formatted script execution time. Defaults to false.', ), include_executed_script_in_output: z.boolean().optional().default(false) .describe("If true, the executed script content (after substitutions) or path will be included in the output."), include_substitution_logs: z.boolean().optional().default(false) .describe("If true, detailed logs of placeholder substitutions will be included in the output.") }).refine(data => { const sources = [data.script_content, data.script_path, data.kb_script_id].filter(s => s !== undefined && s !== null && s !== ''); return sources.length === 1; }, { message: "Exactly one of 'script_content', 'script_path', or 'kb_script_id' must be provided and be non-empty.", path: ["script_content", "script_path", "kb_script_id"], });
- src/ScriptExecutor.ts:12-131 (helper)Helper class that performs the actual osascript execution via child_process.execFile, handling AppleScript/JXA languages, output modes, timeouts, and returning structured results or ScriptExecutionError.export class ScriptExecutor { public async execute( scriptSource: { content?: string; path?: string }, options: ScriptExecutionOptions = {} ): Promise<ScriptExecutionResult> { if (os.platform() !== 'darwin') { const platformError = new Error('AppleScript/JXA execution is only supported on macOS.') as ScriptExecutionError; platformError.name = "UnsupportedPlatformError"; throw platformError; } const { language = 'applescript', timeoutMs = 30000, // Default 30 seconds output_format_mode = 'auto', // Default to auto arguments: scriptArgs = [], } = options; const osaArgs: string[] = []; if (language === 'javascript') { osaArgs.push('-l', 'JavaScript'); } // Determine resolved output mode based on 'auto' logic if necessary let resolved_mode = output_format_mode; if (resolved_mode === 'auto') { if (language === 'javascript') { resolved_mode = 'direct'; } else { // AppleScript resolved_mode = 'human_readable'; } } // Add -s flags based on the resolved mode switch (resolved_mode) { case 'human_readable': osaArgs.push('-s', 'h'); break; case 'structured_error': osaArgs.push('-s', 's'); break; case 'structured_output_and_error': osaArgs.push('-s', 's', '-s', 's'); // Equivalent to -ss break; case 'direct': // No -s flags for direct mode break; } let scriptToLog: string; if (scriptSource.content !== undefined) { osaArgs.push('-e', scriptSource.content); scriptToLog = scriptSource.content.length > 200 ? `${scriptSource.content.substring(0, 200)}...` : scriptSource.content; } else if (scriptSource.path) { try { await fs.access(scriptSource.path, fs.constants.R_OK); } catch (accessError) { logger.error('Script file access error', { path: scriptSource.path, error: (accessError as Error).message }); const fileError = new Error(`Script file not found or not readable: ${scriptSource.path}`) as ScriptExecutionError; fileError.name = "ScriptFileAccessError"; throw fileError; } osaArgs.push(scriptSource.path); scriptToLog = `File: ${scriptSource.path}`; } else { // This case should be prevented by Zod validation in server.ts const sourceError = new Error('Either scriptContent or scriptPath must be provided.') as ScriptExecutionError; sourceError.name = "InvalidScriptSourceError"; throw sourceError; } // Add script arguments AFTER script path or -e flags osaArgs.push(...scriptArgs); logger.debug('Executing osascript', { command: 'osascript', args: osaArgs.map(arg => arg.length > 50 ? `${arg.substring(0,50)}...` : arg), scriptToLog }); const scriptStartTime = Date.now(); try { const { stdout, stderr } = await execFileAsync('osascript', osaArgs, { timeout: timeoutMs, windowsHide: true }); const current_execution_time_seconds = parseFloat(((Date.now() - scriptStartTime) / 1000).toFixed(3)); const stdoutString = stdout.toString(); const stderrString = stderr.toString(); if (stderrString?.trim()) { logger.warn('osascript produced stderr output on successful execution', { stderr: stderrString.trim() }); } return { stdout: stdoutString.trim(), stderr: stderrString.trim(), execution_time_seconds: current_execution_time_seconds }; } catch (error: unknown) { const current_execution_time_seconds = parseFloat(((Date.now() - scriptStartTime) / 1000).toFixed(3)); const nodeError = error as ExecFileException; // Error from execFileAsync const executionError: ScriptExecutionError = new Error(nodeError.message) as ScriptExecutionError; executionError.name = nodeError.name; // Preserve original error name if meaningful executionError.stdout = nodeError.stdout?.toString(); executionError.stderr = nodeError.stderr?.toString(); executionError.exitCode = nodeError.code; // string or number executionError.signal = nodeError.signal; executionError.killed = !!nodeError.killed; executionError.isTimeout = !!nodeError.killed; // 'killed' is true if process was terminated by timeout executionError.originalError = nodeError; // Preserve original node error executionError.execution_time_seconds = current_execution_time_seconds; // Set the calculated time logger.error('osascript execution failed', { message: executionError.message, stdout: executionError.stdout?.trim(), stderr: executionError.stderr?.trim(), exitCode: executionError.exitCode, signal: executionError.signal, isTimeout: executionError.isTimeout, scriptToLog, execution_time_seconds: current_execution_time_seconds, }); throw executionError; } }
- src/types.ts:101-108 (schema)TypeScript interfaces defining ExecuteScriptInput parameters and ExecuteScriptResponse output structure used throughout the tool implementation.export interface ExecuteScriptResponse { content: Array<{ type: 'text'; text: string; }>; isError?: boolean; [key: string]: unknown; // Required by MCP SDK for tool responses }