Skip to main content
Glama

applescript-mcp

framework.ts11.3 kB
import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { ListToolsRequestSchema, CallToolRequestSchema, ErrorCode, McpError, } from "@modelcontextprotocol/sdk/types.js"; import { exec } from "child_process"; import { promisify } from "util"; import os from "os"; import { ScriptCategory, ScriptDefinition, FrameworkOptions, LogLevel, } from "./types/index.js"; const execAsync = promisify(exec); // Get system information for logging const systemInfo = { platform: os.platform(), release: os.release(), hostname: os.hostname(), arch: os.arch(), nodeVersion: process.version }; export class AppleScriptFramework { private server: Server; private categories: ScriptCategory[] = []; private _initInfo: Record<string, any> = {}; private _isConnected: boolean = false; private _pendingCategories: Array<Record<string, any>> = []; /** * Constructs an instance of AppleScriptFramework. * @param options - Configuration options for the framework. */ constructor(options: FrameworkOptions = {}) { const serverName = options.name || "applescript-server"; const serverVersion = options.version || "1.0.0"; this.server = new Server( { name: serverName, version: serverVersion, }, { capabilities: { tools: {}, logging: {}, // Enable logging capability }, }, ); if (options.debug) { this.enableDebugLogging(); } // Log server initialization with stderr (server isn't connected yet) console.error(`[INFO] AppleScript MCP server initialized - ${serverName} v${serverVersion}`); // Store initialization info for later logging after connection this._initInfo = { name: serverName, version: serverVersion, debug: !!options.debug, system: systemInfo }; } /** * Enables debug logging for the server. * Sets up error handlers and configures detailed logging. */ private enableDebugLogging(): void { console.error("[INFO] Debug logging enabled"); this.server.onerror = (error) => { const errorMessage = error instanceof Error ? error.message : String(error); console.error("[MCP Error]", error); // Only use MCP logging if connected if (this._isConnected) { this.log("error", "MCP server error", { error: errorMessage }); } }; // Set up additional debug event handlers if needed this.server.oninitialized = () => { this._isConnected = true; console.error("[DEBUG] Connection initialized"); // We'll log initialization info in the run method after connection is fully established console.error("[DEBUG] Connection initialized"); }; this.server.onclose = () => { this._isConnected = false; console.error("[DEBUG] Connection closed"); // No MCP logging here since we're disconnected }; } /** * Adds a new script category to the framework. * @param category - The script category to add. */ addCategory(category: ScriptCategory): void { this.categories.push(category); // Use console logging since this is called before server connection console.error(`[DEBUG] Added category: ${category.name} (${category.scripts.length} scripts)`); // Store category info to log via MCP after connection if (!this._pendingCategories) { this._pendingCategories = []; } this._pendingCategories.push({ categoryName: category.name, scriptCount: category.scripts.length, description: category.description }); } /** * Logs a message with the specified severity level. * Uses the MCP server's logging system to record events if available. * Always logs to console for visibility. * * @param level - The severity level of the log message following RFC 5424 syslog levels * @param message - The message to log * @param data - Optional additional data to include with the log message * * @example * // Log a debug message * framework.log("debug", "Processing request", { requestId: "123" }); * * @example * // Log an error * framework.log("error", "Failed to execute script", { scriptName: "calendar_add" }); */ log(level: LogLevel, message: string, data?: Record<string, any>): void { // Format for console output const timestamp = new Date().toISOString(); const dataStr = data ? ` ${JSON.stringify(data)}` : ''; // Always log to stderr for visibility console.error(`[${timestamp}] [${level.toUpperCase()}] ${message}${dataStr}`); // Only try to use MCP logging if we're connected if (this._isConnected) { try { this.server.sendLoggingMessage({ level: level, message: message, data: data || {}, }); } catch (error) { // Silently ignore logging errors - we've already logged to console } } } /** * Executes an AppleScript and returns the result. * @param script - The AppleScript to execute. * @returns The result of the script execution. * @throws Will throw an error if the script execution fails. */ private async executeScript(script: string): Promise<string> { // Log script execution (truncate long scripts for readability) const scriptPreview = script.length > 100 ? script.substring(0, 100) + "..." : script; this.log("debug", "Executing AppleScript", { scriptPreview }); try { const startTime = Date.now(); const { stdout } = await execAsync( `osascript -e '${script.replace(/'/g, "'\"'\"'")}'`, ); const executionTime = Date.now() - startTime; this.log("debug", "AppleScript executed successfully", { executionTimeMs: executionTime, outputLength: stdout.length }); return stdout.trim(); } catch (error) { // Properly type check the error object let errorMessage = "Unknown error occurred"; if (error && typeof error === "object") { if ("message" in error && typeof error.message === "string") { errorMessage = error.message; } else if (error instanceof Error) { errorMessage = error.message; } } else if (typeof error === "string") { errorMessage = error; } this.log("error", "AppleScript execution failed", { error: errorMessage, scriptPreview }); throw new Error(`AppleScript execution failed: ${errorMessage}`); } } /** * Sets up request handlers for the server. */ private setupHandlers(): void { // List available tools this.server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: this.categories.flatMap((category) => category.scripts.map((script) => ({ name: `${category.name}_${script.name}`, // Changed from dot to underscore description: `[${category.description}] ${script.description}`, inputSchema: script.schema || { type: "object", properties: {}, }, })), ), })); // Handle tool execution this.server.setRequestHandler(CallToolRequestSchema, async (request) => { const toolName = request.params.name; this.log("info", "Tool execution requested", { tool: toolName, hasArguments: !!request.params.arguments }); try { // Split on underscore instead of dot const [categoryName, ...scriptNameParts] = toolName.split("_"); const scriptName = scriptNameParts.join("_"); // Rejoin in case script name has underscores const category = this.categories.find((c) => c.name === categoryName); if (!category) { this.log("warning", "Category not found", { categoryName }); throw new McpError( ErrorCode.MethodNotFound, `Category not found: ${categoryName}`, ); } const script = category.scripts.find((s) => s.name === scriptName); if (!script) { this.log("warning", "Script not found", { categoryName, scriptName }); throw new McpError( ErrorCode.MethodNotFound, `Script not found: ${scriptName}`, ); } this.log("debug", "Generating script content", { categoryName, scriptName, isFunction: typeof script.script === "function" }); const scriptContent = typeof script.script === "function" ? script.script(request.params.arguments) : script.script; const result = await this.executeScript(scriptContent); this.log("info", "Tool execution completed successfully", { tool: toolName, resultLength: result.length }); return { content: [ { type: "text", text: result, }, ], }; } catch (error) { if (error instanceof McpError) { this.log("error", "MCP error during tool execution", { tool: toolName, errorCode: error.code, errorMessage: error.message }); throw error; } let errorMessage = "Unknown error occurred"; if (error && typeof error === "object") { if ("message" in error && typeof error.message === "string") { errorMessage = error.message; } else if (error instanceof Error) { errorMessage = error.message; } } else if (typeof error === "string") { errorMessage = error; } this.log("error", "Error during tool execution", { tool: toolName, errorMessage }); return { content: [ { type: "text", text: `Error: ${errorMessage}`, }, ], isError: true, }; } }); } /** * Runs the AppleScript framework server. */ async run(): Promise<void> { console.error("[INFO] Setting up request handlers"); this.setupHandlers(); console.error("[INFO] Initializing StdioServerTransport"); const transport = new StdioServerTransport(); try { console.error("[INFO] Connecting server to transport"); await this.server.connect(transport); this._isConnected = true; // Log server running status using console only const totalScripts = this.categories.reduce((count, category) => count + category.scripts.length, 0); console.error(`[NOTICE] AppleScript MCP server running with ${this.categories.length} categories and ${totalScripts} scripts`); console.error("AppleScript MCP server running"); } catch (error) { let errorMessage = "Unknown error occurred"; if (error && typeof error === "object" && error instanceof Error) { errorMessage = error.message; } console.error("Failed to start AppleScript MCP server:", errorMessage); throw error; } } }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/joshrutkowski/applescript-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server