mseep-mcp-my-apple-remembers 0.1.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,39 @@
1
+ # Environment variables
2
+ .env
3
+
4
+ # Python
5
+ __pycache__/
6
+ *.py[cod]
7
+ *$py.class
8
+ *.so
9
+ .Python
10
+ build/
11
+ develop-eggs/
12
+ dist/
13
+ downloads/
14
+ eggs/
15
+ .eggs/
16
+ lib/
17
+ lib64/
18
+ parts/
19
+ sdist/
20
+ var/
21
+ wheels/
22
+ *.egg-info/
23
+ .installed.cfg
24
+ *.egg
25
+
26
+ # Virtual Environment
27
+ venv/
28
+ env/
29
+ ENV/
30
+
31
+ # IDE
32
+ .idea/
33
+ .vscode/
34
+ *.swp
35
+ *.swo
36
+
37
+ # OS
38
+ .DS_Store
39
+ Thumbs.db
@@ -0,0 +1,14 @@
1
+ # Use Python base image
2
+ FROM python:3.10-slim
3
+
4
+ # Install the project into `/app`
5
+ WORKDIR /app
6
+
7
+ # Copy the entire project
8
+ COPY . .
9
+
10
+ # Install the package
11
+ RUN pip install -e .
12
+
13
+ # Run the server
14
+ CMD ["python", "-m", "mcp_my_apple_remembers.server"]
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 peakmojo
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,13 @@
1
+ Metadata-Version: 2.4
2
+ Name: mseep-mcp_my_apple_remembers
3
+ Version: 0.1.1
4
+ Summary: A simple Apple Remembers MCP server
5
+ Author-email: mseep <support@skydeck.ai>
6
+ License-File: LICENSE
7
+ Requires-Python: >=3.10
8
+ Requires-Dist: mcp>=1.4.1
9
+ Requires-Dist: paramiko>=3.5.1
10
+ Requires-Dist: python-dotenv>=1.0.1
11
+ Description-Content-Type: text/plain
12
+
13
+ Package managed by MseeP.ai
@@ -0,0 +1,92 @@
1
+ # MCP Server - My Apple Remembers
2
+ **A simple MCP server that recalls and saves memories from and to Apple Notes.**
3
+
4
+ [![Docker Pulls](https://img.shields.io/docker/pulls/buryhuang/mcp-my-apple-remembers)](https://hub.docker.com/r/buryhuang/mcp-my-apple-remembers)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
6
+
7
+ <img width="600" alt="image" src="https://github.com/user-attachments/assets/9bd5bc1c-02fe-4e71-88c4-46b3e9438ac0" />
8
+
9
+
10
+ ## Features
11
+
12
+ * **Memory Recall**: Access notes, calendar events, messages, files and other information from your Mac
13
+ * **Memory Persistence**: Save important information to Apple Notes for future reference
14
+ * **Minimal Setup**: Just enable Remote Login on the target Mac
15
+ * **Universal Compatibility**: Works with all macOS versions
16
+
17
+ ## Control in your hand
18
+ You can use prompt to instruct how you want your memory to be save. For example:
19
+ ```
20
+ You should always use Folder "baryhuang" on recall and save memory.
21
+ ```
22
+
23
+ ## Installation
24
+ - [Enable SSH on macOS](https://support.apple.com/guide/mac-help/allow-a-remote-computer-to-access-your-mac-mchlp1066/mac)
25
+ - [Install Docker Desktop for local Mac](https://docs.docker.com/desktop/setup/install/mac-install/)
26
+ - [Add this MCP server to Claude Desktop](https://modelcontextprotocol.io/quickstart/user)
27
+
28
+ You can configure Claude Desktop to use the Docker image by adding the following to your Claude configuration:
29
+ ```json
30
+ {
31
+ "mcpServers": {
32
+ "my-apple-remembers": {
33
+ "command": "docker",
34
+ "args": [
35
+ "run",
36
+ "-i",
37
+ "-e",
38
+ "MACOS_USERNAME=your_macos_username",
39
+ "-e",
40
+ "MACOS_PASSWORD=your_macos_password",
41
+ "-e",
42
+ "MACOS_HOST=localhost",
43
+ "--rm",
44
+ "buryhuang/mcp-my-apple-remembers:latest"
45
+ ]
46
+ }
47
+ }
48
+ }
49
+ ```
50
+
51
+ ## Developer Instructions
52
+ ### Clone the repo
53
+ ```bash
54
+ # Clone the repository
55
+ git clone https://github.com/baryhuang/mcp-my-apple-remembers.git
56
+ cd mcp-my-apple-remembers
57
+ ```
58
+
59
+ ### Building the Docker Image
60
+
61
+ ```bash
62
+ # Build the Docker image
63
+ docker build -t mcp-my-apple-remembers .
64
+ ```
65
+
66
+ ### Publishing Multi-Platform Docker Images
67
+
68
+ ```bash
69
+ # Set up Docker buildx for multi-platform builds
70
+ docker buildx create --use
71
+
72
+ # Build and push the multi-platform image
73
+ docker buildx build --platform linux/amd64,linux/arm64 -t buryhuang/mcp-my-apple-remembers:latest --push .
74
+ ```
75
+
76
+ ### Tools Specifications
77
+
78
+ #### my_apple_recall_memory
79
+ Run AppleScript commands on a remote macOS system to recall memories. This tool helps access Apple Notes, Calendar events, iMessages, chat history, files, and other information on your Mac.
80
+
81
+ #### my_apple_save_memory
82
+ Run AppleScript commands on a remote macOS system to save important information. This tool allows AI to persist relevant information to Apple Notes for future reference.
83
+
84
+ All tools require macOS SSH access, with host and password.
85
+
86
+ ## Security Note
87
+
88
+ Always use secure, authenticated connections when accessing remote macOS machines. This tool should only be used with servers you trust and have permission to access.
89
+
90
+ ## License
91
+
92
+ See the LICENSE file for details.
@@ -0,0 +1,36 @@
1
+ [project]
2
+ name = "mseep-mcp_my_apple_remembers"
3
+ version = "0.1.1"
4
+ description = "A simple Apple Remembers MCP server"
5
+ requires-python = ">=3.10"
6
+ dependencies = [
7
+ "mcp>=1.4.1",
8
+ "python-dotenv>=1.0.1",
9
+ "paramiko>=3.5.1",
10
+ ]
11
+ authors = [
12
+ { name = "mseep", email = "support@skydeck.ai" },
13
+ ]
14
+
15
+ [project.readme]
16
+ content-type = "text/plain"
17
+ text = "Package managed by MseeP.ai"
18
+
19
+ [project.scripts]
20
+ mcp_my_apple_remembers = "mcp_my_apple_remembers:main"
21
+
22
+ [build-system]
23
+ requires = [
24
+ "hatchling",
25
+ ]
26
+ build-backend = "hatchling.build"
27
+
28
+ [tool.uv]
29
+ dev-dependencies = [
30
+ "pyright>=1.1.389",
31
+ ]
32
+
33
+ [tool.hatch.build.targets.wheel]
34
+ packages = [
35
+ "src/mcp_my_apple_remembers",
36
+ ]
@@ -0,0 +1,23 @@
1
+ import argparse
2
+ import asyncio
3
+ import logging
4
+ from . import server
5
+
6
+ logging.basicConfig(level=logging.DEBUG)
7
+ logger = logging.getLogger('mcp_my_apple_remembers')
8
+
9
+ def main():
10
+ logger.debug("Starting mcp_my_apple_remembers main()")
11
+ parser = argparse.ArgumentParser(description='Apple Remembers MCP Server')
12
+ args = parser.parse_args()
13
+
14
+ # Run the async main function
15
+ logger.debug("About to run server.main()")
16
+ asyncio.run(server.main())
17
+ logger.debug("Server main() completed")
18
+
19
+ if __name__ == "__main__":
20
+ main()
21
+
22
+ # Expose important items at package level
23
+ __all__ = ["main", "server"]
@@ -0,0 +1,330 @@
1
+ import logging
2
+ from typing import Any, Dict, List, Optional, Tuple
3
+ from dotenv import load_dotenv
4
+ import json
5
+ import os
6
+ import asyncio
7
+ from datetime import datetime
8
+ import sys
9
+
10
+ # Import MCP server libraries
11
+ from mcp.server.models import InitializationOptions
12
+ import mcp.types as types
13
+ from mcp.server import NotificationOptions, Server
14
+ import mcp.server.stdio
15
+
16
+ # Configure logging
17
+ logging.basicConfig(
18
+ level=logging.INFO,
19
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
20
+ )
21
+ logger = logging.getLogger('mcp_my_apple_remembers')
22
+ logger.setLevel(logging.INFO)
23
+
24
+ # Load environment variables for SSH connection
25
+ MACOS_HOST = os.environ.get('MACOS_HOST', '')
26
+ MACOS_PORT = int(os.environ.get('MACOS_PORT', '22')) # Use SSH default port
27
+ MACOS_USERNAME = os.environ.get('MACOS_USERNAME', '')
28
+ MACOS_PASSWORD = os.environ.get('MACOS_PASSWORD', '')
29
+
30
+ # Log environment variable status (without exposing actual values)
31
+ logger.info(f"MACOS_HOST from environment: {'Set' if MACOS_HOST else 'Not set'}")
32
+ logger.info(f"MACOS_PORT from environment: {MACOS_PORT}")
33
+ logger.info(f"MACOS_USERNAME from environment: {'Set' if MACOS_USERNAME else 'Not set'}")
34
+ logger.info(f"MACOS_PASSWORD from environment: {'Set' if MACOS_PASSWORD else 'Not set (Required)'}")
35
+
36
+ # Validate required environment variables
37
+ if not MACOS_HOST:
38
+ logger.error("MACOS_HOST environment variable is required but not set")
39
+ raise ValueError("MACOS_HOST environment variable is required but not set")
40
+
41
+ if not MACOS_PASSWORD:
42
+ logger.error("MACOS_PASSWORD environment variable is required but not set")
43
+ raise ValueError("MACOS_PASSWORD environment variable is required but not set")
44
+
45
+ async def main():
46
+ """Run the My Apple Remembers MCP server."""
47
+ logger.info("My Apple Remembers server starting")
48
+ server = Server("my-apple-remembers")
49
+
50
+ @server.list_resources()
51
+ async def handle_list_resources() -> list[types.Resource]:
52
+ return []
53
+
54
+ @server.read_resource()
55
+ async def handle_read_resource(uri: types.AnyUrl) -> str:
56
+ return ""
57
+
58
+ @server.list_tools()
59
+ async def handle_list_tools() -> list[types.Tool]:
60
+ """List available tools"""
61
+ return [
62
+ types.Tool(
63
+ name="my_apple_recall_memory",
64
+ description="Run Apple Script on a remote MacOs machine. This call should be used to recall the apple notes, apple calendar, imessages, chat messages, files, context or any other information of a MacOs machine can have access to.",
65
+ inputSchema={
66
+ "type": "object",
67
+ "properties": {
68
+ "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."},
69
+ "timeout": {"type": "integer", "description": "Command execution timeout in seconds (default: 60)"}
70
+ },
71
+ "required": ["code_snippet"]
72
+ },
73
+ ),
74
+ types.Tool(
75
+ name="my_apple_save_memory",
76
+ 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.",
77
+ inputSchema={
78
+ "type": "object",
79
+ "properties": {
80
+ "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."},
81
+ "timeout": {"type": "integer", "description": "Command execution timeout in seconds (default: 60)"}
82
+ },
83
+ "required": ["code_snippet"]
84
+ },
85
+ )
86
+ ]
87
+
88
+ @server.call_tool()
89
+ async def handle_call_tool(
90
+ name: str, arguments: dict[str, Any] | None
91
+ ) -> list[types.TextContent | types.ImageContent | types.EmbeddedResource]:
92
+ """Handle tool execution requests"""
93
+ try:
94
+ logger.info(f"Tool execution requested: {name}")
95
+
96
+ if not arguments:
97
+ arguments = {}
98
+
99
+ if name == "my_apple_recall_memory" or name == "my_apple_save_memory":
100
+ # Use environment variables
101
+ ## 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
102
+ host = MACOS_HOST
103
+ if host == "localhost" or host == "127.0.0.1" or host == "0.0.0.0":
104
+ host = "host.docker.internal"
105
+ port = MACOS_PORT
106
+ username = MACOS_USERNAME
107
+ password = MACOS_PASSWORD
108
+
109
+ logger.info(f"Connecting to {host}:{port} as {username}")
110
+
111
+ # Get parameters from arguments
112
+ code_snippet = arguments.get("code_snippet")
113
+ timeout = int(arguments.get("timeout", 60))
114
+
115
+ # Check if we have required parameters
116
+ if not code_snippet:
117
+ logger.error("Missing required parameter: code_snippet")
118
+ raise ValueError("code_snippet is required to execute on the remote machine")
119
+
120
+ try:
121
+ # Import required libraries
122
+ import paramiko
123
+ import io
124
+ import base64
125
+ import time
126
+ import uuid
127
+ from socket import timeout as socket_timeout
128
+ except ImportError as e:
129
+ logger.error(f"Missing required libraries: {str(e)}")
130
+ return [types.TextContent(
131
+ type="text",
132
+ text=f"Error: Missing required libraries. Please install paramiko: {str(e)}"
133
+ )]
134
+
135
+ # Initialize SSH client
136
+ ssh = paramiko.SSHClient()
137
+ ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
138
+
139
+ try:
140
+ # Connect with password
141
+ ssh.connect(
142
+ hostname=host,
143
+ port=port,
144
+ username=username,
145
+ password=password,
146
+ timeout=10
147
+ )
148
+
149
+ logger.info(f"Successfully connected to {host}")
150
+
151
+ # Determine execution method based on script complexity
152
+ is_multiline = '\n' in code_snippet
153
+
154
+ if is_multiline:
155
+ # File-based execution for multi-line scripts
156
+
157
+ # Generate a unique temporary filename
158
+ temp_script_filename = f"/tmp/applescript_{uuid.uuid4().hex}.scpt"
159
+
160
+ # Open SFTP session and upload the script
161
+ sftp = ssh.open_sftp()
162
+
163
+ try:
164
+ # Create the script file on the remote system
165
+ with sftp.open(temp_script_filename, 'w') as remote_file:
166
+ remote_file.write(code_snippet)
167
+
168
+ logger.info(f"Script file uploaded to {temp_script_filename}")
169
+
170
+ # Set execute permissions
171
+ sftp.chmod(temp_script_filename, 0o755)
172
+
173
+ # Execute the script file
174
+ command = f'osascript {temp_script_filename}'
175
+ logger.info(f"Executing AppleScript file: {command}")
176
+
177
+ finally:
178
+ # Always close SFTP after file operations
179
+ sftp.close()
180
+
181
+ else:
182
+ # Direct command execution for single-line scripts
183
+ escaped_script = code_snippet.replace('"', '\\"')
184
+ command = f'osascript -e "{escaped_script}"'
185
+ logger.info(f"Executing AppleScript command")
186
+
187
+ # Execute command with PTY (pseudo-terminal) for interactive commands
188
+ channel = ssh.get_transport().open_session()
189
+ channel.get_pty()
190
+ channel.settimeout(timeout)
191
+ channel.exec_command(command)
192
+
193
+ # Initialize byte buffers instead of strings
194
+ stdout_buffer = b""
195
+ stderr_buffer = b""
196
+
197
+ # Read from stdout and stderr until closed or timeout
198
+ start_time = time.time()
199
+
200
+ while not channel.exit_status_ready():
201
+ if channel.recv_ready():
202
+ chunk = channel.recv(1024)
203
+ stdout_buffer += chunk
204
+ if channel.recv_stderr_ready():
205
+ chunk = channel.recv_stderr(1024)
206
+ stderr_buffer += chunk
207
+
208
+ # Check timeout
209
+ elapsed = time.time() - start_time
210
+ if elapsed > timeout:
211
+ logger.warning(f"Command execution timed out after {elapsed:.2f} seconds")
212
+ raise TimeoutError(f"Command execution timed out after {timeout} seconds")
213
+
214
+ # Small sleep to prevent CPU spinning
215
+ time.sleep(0.1)
216
+
217
+ # Get any remaining output
218
+ while channel.recv_ready():
219
+ chunk = channel.recv(1024)
220
+ stdout_buffer += chunk
221
+ while channel.recv_stderr_ready():
222
+ chunk = channel.recv_stderr(1024)
223
+ stderr_buffer += chunk
224
+
225
+ # Get exit status
226
+ exit_status = channel.recv_exit_status()
227
+ logger.info(f"Command completed with exit status: {exit_status}")
228
+
229
+ # Cleanup temp file if created
230
+ if is_multiline:
231
+ try:
232
+ # Open a new SFTP session for cleanup
233
+ sftp = ssh.open_sftp()
234
+ sftp.remove(temp_script_filename)
235
+ sftp.close()
236
+ logger.info(f"Script file {temp_script_filename} removed")
237
+ except Exception as e:
238
+ logger.warning(f"Failed to remove script file: {str(e)}")
239
+
240
+ # Decode complete buffers once all data is received
241
+ try:
242
+ output = stdout_buffer.decode('utf-8')
243
+ except UnicodeDecodeError:
244
+ # Fallback to error-tolerant decoding if strict decoding fails
245
+ logger.warning("UTF-8 decoding failed for stdout, using replacement character for errors")
246
+ output = stdout_buffer.decode('utf-8', errors='replace')
247
+
248
+ try:
249
+ stderr_output = stderr_buffer.decode('utf-8')
250
+ except UnicodeDecodeError:
251
+ logger.warning("UTF-8 decoding failed for stderr, using replacement character for errors")
252
+ stderr_output = stderr_buffer.decode('utf-8', errors='replace')
253
+
254
+ # Format response
255
+ response = f"Command executed with exit status: {exit_status}\n\n"
256
+
257
+ if output:
258
+ response += f"STDOUT:\n{output}\n\n"
259
+
260
+ if stderr_output:
261
+ response += f"STDERR:\n{stderr_output}\n"
262
+
263
+ return [types.TextContent(type="text", text=response)]
264
+
265
+ except paramiko.AuthenticationException:
266
+ logger.error(f"Authentication failed for {username}@{host}:{port}")
267
+ return [types.TextContent(
268
+ type="text",
269
+ text=f"Authentication failed for {username}@{host}:{port}. Check credentials."
270
+ )]
271
+ except socket_timeout:
272
+ logger.error(f"Connection timeout while connecting to {host}:{port}")
273
+ return [types.TextContent(
274
+ type="text",
275
+ text=f"Connection timeout while connecting to {host}:{port}."
276
+ )]
277
+ except TimeoutError as e:
278
+ logger.error(f"Command execution timed out: {str(e)}")
279
+ return [types.TextContent(
280
+ type="text",
281
+ text=str(e)
282
+ )]
283
+ except Exception as e:
284
+ logger.error(f"Error executing SSH command: {str(e)}")
285
+ return [types.TextContent(
286
+ type="text",
287
+ text=f"Error executing SSH command: {str(e)}"
288
+ )]
289
+ finally:
290
+ # Close SSH connection
291
+ ssh.close()
292
+ logger.info(f"SSH connection to {host} closed")
293
+ else:
294
+ logger.error(f"Unknown tool requested: {name}")
295
+ raise ValueError(f"Unknown tool: {name}")
296
+
297
+ except Exception as e:
298
+ logger.error(f"Error in handle_call_tool: {str(e)}")
299
+ return [types.TextContent(type="text", text=f"Error: {str(e)}")]
300
+
301
+ async with mcp.server.stdio.stdio_server() as (read_stream, write_stream):
302
+ logger.info("Server running with stdio transport")
303
+ await server.run(
304
+ read_stream,
305
+ write_stream,
306
+ InitializationOptions(
307
+ server_name="my-apple-remembers",
308
+ server_version="0.1.0",
309
+ capabilities=server.get_capabilities(
310
+ notification_options=NotificationOptions(),
311
+ experimental_capabilities={},
312
+ ),
313
+ ),
314
+ )
315
+
316
+ if __name__ == "__main__":
317
+ # Load environment variables from .env file if it exists
318
+ load_dotenv()
319
+
320
+ try:
321
+ # Run the server
322
+ asyncio.run(main())
323
+ except ValueError as e:
324
+ logger.error(f"Initialization failed: {str(e)}")
325
+ print(f"ERROR: {str(e)}")
326
+ sys.exit(1)
327
+ except Exception as e:
328
+ logger.error(f"Unexpected error: {str(e)}")
329
+ print(f"ERROR: Unexpected error occurred: {str(e)}")
330
+ sys.exit(1)