Skip to main content
Glama
honeybluesky

mcp-my-apple-remembers

by honeybluesky

my_apple_save_memory

Execute AppleScript on a remote MacOS device to save information directly to Apple Notes. Automatically creates timestamped notes for organized storage and retrieval.

Instructions

Run Apple Script on a remote MacOs machine. This call should be used to save relevant information to the apple notes. You decide what information to save. You should always add a new notes with a timestamp as the title.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
code_snippetYesAppleScript code to execute on the remote machine. Can be a single line or multi-line script. You should prefer multi-line scripts for complex operations.
timeoutNoCommand execution timeout in seconds (default: 60)

Implementation Reference

  • The @server.call_tool() handler that implements the execution logic for 'my_apple_save_memory' (shared with 'my_apple_recall_memory'). It establishes an SSH connection to a remote MacOS machine, executes the provided AppleScript using osascript, handles multi-line scripts by uploading temp files, captures output with timeout, and returns the result.
    @server.call_tool()
    async def handle_call_tool(
        name: str, arguments: dict[str, Any] | None
    ) -> list[types.TextContent | types.ImageContent | types.EmbeddedResource]:
        """Handle tool execution requests"""
        try:
            logger.info(f"Tool execution requested: {name}")
            
            if not arguments:
                arguments = {}
            
            if name == "my_apple_recall_memory" or name == "my_apple_save_memory":
                # Use environment variables
                ## Since this is running inside the docker, if user want to connect to the local macos machine, we need to use the host.docker.internal
                host = MACOS_HOST
                if host == "localhost" or host == "127.0.0.1" or host == "0.0.0.0":
                    host = "host.docker.internal"
                port = MACOS_PORT
                username = MACOS_USERNAME
                password = MACOS_PASSWORD
    
                logger.info(f"Connecting to {host}:{port} as {username}")
                
                # Get parameters from arguments
                code_snippet = arguments.get("code_snippet")
                timeout = int(arguments.get("timeout", 60))
                
                # Check if we have required parameters
                if not code_snippet:
                    logger.error("Missing required parameter: code_snippet")
                    raise ValueError("code_snippet is required to execute on the remote machine")
                
                try:
                    # Import required libraries
                    import paramiko
                    import io
                    import base64
                    import time
                    import uuid
                    from socket import timeout as socket_timeout
                except ImportError as e:
                    logger.error(f"Missing required libraries: {str(e)}")
                    return [types.TextContent(
                        type="text",
                        text=f"Error: Missing required libraries. Please install paramiko: {str(e)}"
                    )]
                
                # Initialize SSH client
                ssh = paramiko.SSHClient()
                ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
                
                try:
                    # Connect with password
                    ssh.connect(
                        hostname=host,
                        port=port,
                        username=username,
                        password=password,
                        timeout=10
                    )
                    
                    logger.info(f"Successfully connected to {host}")
                    
                    # Determine execution method based on script complexity
                    is_multiline = '\n' in code_snippet
                    
                    if is_multiline:
                        # File-based execution for multi-line scripts
                        
                        # Generate a unique temporary filename
                        temp_script_filename = f"/tmp/applescript_{uuid.uuid4().hex}.scpt"
                        
                        # Open SFTP session and upload the script
                        sftp = ssh.open_sftp()
                        
                        try:
                            # Create the script file on the remote system
                            with sftp.open(temp_script_filename, 'w') as remote_file:
                                remote_file.write(code_snippet)
                            
                            logger.info(f"Script file uploaded to {temp_script_filename}")
                            
                            # Set execute permissions
                            sftp.chmod(temp_script_filename, 0o755)
                            
                            # Execute the script file
                            command = f'osascript {temp_script_filename}'
                            logger.info(f"Executing AppleScript file: {command}")
                            
                        finally:
                            # Always close SFTP after file operations
                            sftp.close()
                            
                    else:
                        # Direct command execution for single-line scripts
                        escaped_script = code_snippet.replace('"', '\\"')
                        command = f'osascript -e "{escaped_script}"'
                        logger.info(f"Executing AppleScript command")
                    
                    # Execute command with PTY (pseudo-terminal) for interactive commands
                    channel = ssh.get_transport().open_session()
                    channel.get_pty()
                    channel.settimeout(timeout)
                    channel.exec_command(command)
                    
                    # Initialize byte buffers instead of strings
                    stdout_buffer = b""
                    stderr_buffer = b""
                    
                    # Read from stdout and stderr until closed or timeout
                    start_time = time.time()
                    
                    while not channel.exit_status_ready():
                        if channel.recv_ready():
                            chunk = channel.recv(1024)
                            stdout_buffer += chunk
                        if channel.recv_stderr_ready():
                            chunk = channel.recv_stderr(1024)
                            stderr_buffer += chunk
                        
                        # Check timeout
                        elapsed = time.time() - start_time
                        if elapsed > timeout:
                            logger.warning(f"Command execution timed out after {elapsed:.2f} seconds")
                            raise TimeoutError(f"Command execution timed out after {timeout} seconds")
                        
                        # Small sleep to prevent CPU spinning
                        time.sleep(0.1)
                    
                    # Get any remaining output
                    while channel.recv_ready():
                        chunk = channel.recv(1024)
                        stdout_buffer += chunk
                    while channel.recv_stderr_ready():
                        chunk = channel.recv_stderr(1024)
                        stderr_buffer += chunk
                    
                    # Get exit status
                    exit_status = channel.recv_exit_status()
                    logger.info(f"Command completed with exit status: {exit_status}")
                    
                    # Cleanup temp file if created
                    if is_multiline:
                        try:
                            # Open a new SFTP session for cleanup
                            sftp = ssh.open_sftp()
                            sftp.remove(temp_script_filename)
                            sftp.close()
                            logger.info(f"Script file {temp_script_filename} removed")
                        except Exception as e:
                            logger.warning(f"Failed to remove script file: {str(e)}")
                    
                    # Decode complete buffers once all data is received
                    try:
                        output = stdout_buffer.decode('utf-8')
                    except UnicodeDecodeError:
                        # Fallback to error-tolerant decoding if strict decoding fails
                        logger.warning("UTF-8 decoding failed for stdout, using replacement character for errors")
                        output = stdout_buffer.decode('utf-8', errors='replace')
                    
                    try:
                        stderr_output = stderr_buffer.decode('utf-8')
                    except UnicodeDecodeError:
                        logger.warning("UTF-8 decoding failed for stderr, using replacement character for errors")
                        stderr_output = stderr_buffer.decode('utf-8', errors='replace')
                    
                    # Format response
                    response = f"Command executed with exit status: {exit_status}\n\n"
                    
                    if output:
                        response += f"STDOUT:\n{output}\n\n"
                    
                    if stderr_output:
                        response += f"STDERR:\n{stderr_output}\n"
                    
                    return [types.TextContent(type="text", text=response)]
                    
                except paramiko.AuthenticationException:
                    logger.error(f"Authentication failed for {username}@{host}:{port}")
                    return [types.TextContent(
                        type="text", 
                        text=f"Authentication failed for {username}@{host}:{port}. Check credentials."
                    )]
                except socket_timeout:
                    logger.error(f"Connection timeout while connecting to {host}:{port}")
                    return [types.TextContent(
                        type="text",
                        text=f"Connection timeout while connecting to {host}:{port}."
                    )]
                except TimeoutError as e:
                    logger.error(f"Command execution timed out: {str(e)}")
                    return [types.TextContent(
                        type="text",
                        text=str(e)
                    )]
                except Exception as e:
                    logger.error(f"Error executing SSH command: {str(e)}")
                    return [types.TextContent(
                        type="text",
                        text=f"Error executing SSH command: {str(e)}"
                    )]
                finally:
                    # Close SSH connection
                    ssh.close()
                    logger.info(f"SSH connection to {host} closed")
            else:
                logger.error(f"Unknown tool requested: {name}")
                raise ValueError(f"Unknown tool: {name}")
    
        except Exception as e:
            logger.error(f"Error in handle_call_tool: {str(e)}")
            return [types.TextContent(type="text", text=f"Error: {str(e)}")]
  • Registration of the 'my_apple_save_memory' tool in the @server.list_tools() handler.
    types.Tool(
        name="my_apple_save_memory",
        description="Run Apple Script on a remote MacOs machine. This call should be used to save relevant information to the apple notes. You decide what information to save. You should always add a new notes with a timestamp as the title.",
        inputSchema={
            "type": "object",
            "properties": {
                "code_snippet": {"type": "string", "description": "AppleScript code to execute on the remote machine. Can be a single line or multi-line script. You should prefer multi-line scripts for complex operations."},
                "timeout": {"type": "integer", "description": "Command execution timeout in seconds (default: 60)"}
            },
            "required": ["code_snippet"]
        },
    )
  • Input schema definition for the 'my_apple_save_memory' tool, specifying parameters for AppleScript code and optional timeout.
    inputSchema={
        "type": "object",
        "properties": {
            "code_snippet": {"type": "string", "description": "AppleScript code to execute on the remote machine. Can be a single line or multi-line script. You should prefer multi-line scripts for complex operations."},
            "timeout": {"type": "integer", "description": "Command execution timeout in seconds (default: 60)"}
        },
        "required": ["code_snippet"]
    },
Behavior2/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations provided, the description carries full burden for behavioral disclosure. It mentions executing on a remote machine and saving to Apple Notes, but doesn't cover important behavioral aspects like authentication requirements, error handling, what happens if the remote machine is unavailable, whether this is a synchronous or asynchronous operation, or potential side effects beyond note creation.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is reasonably concise with three sentences that each serve a purpose: stating the core function, specifying the intended use case, and providing implementation guidance. It's front-loaded with the main purpose. The third sentence could be more tightly integrated with the second for better flow.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness2/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a tool that executes remote AppleScript with no annotations and no output schema, the description is incomplete. It doesn't explain what the tool returns, error conditions, authentication requirements, or important behavioral constraints. The guidance about timestamps and note creation is helpful but insufficient for a remote execution tool with potential security and reliability implications.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100%, so the schema already fully documents both parameters. The description doesn't add any parameter-specific information beyond what's in the schema. It mentions AppleScript and saving to notes, which aligns with the code_snippet parameter purpose, but provides no additional semantic context about parameter usage or constraints.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the tool's purpose: 'Run Apple Script on a remote MacOs machine' and specifies it's for saving information to Apple Notes. It distinguishes from the sibling tool 'my_apple_recall_memory' by focusing on saving rather than recalling. However, it doesn't explicitly contrast the two tools in the description text itself.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines3/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description provides some usage context: 'This call should be used to save relevant information to the apple notes' and gives implementation guidance about timestamps. However, it doesn't explicitly state when to use this tool versus alternatives or when not to use it. The sibling tool name suggests a recall function, but no explicit comparison is made.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Install Server

Other Tools

Related Tools

Latest Blog Posts

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/honeybluesky/my-apple-remembers'

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