mseep-mcp-my-apple-remembers 0.1.1__py3-none-any.whl

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,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)
@@ -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,7 @@
1
+ mcp_my_apple_remembers/__init__.py,sha256=-Tqnho72a8xQvbWE-P9E7Ldb8sjsNSppbVRBYQPBRA8,616
2
+ mcp_my_apple_remembers/server.py,sha256=1c9Bp1eH5cRkVKmFYYPQFdDvJxVMsIJ7uGY75RBjles,15400
3
+ mseep_mcp_my_apple_remembers-0.1.1.dist-info/METADATA,sha256=BiT07P7lVyLa6N6TK1g932fq9A55NVIK6Qlg9Fb2hnY,362
4
+ mseep_mcp_my_apple_remembers-0.1.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
5
+ mseep_mcp_my_apple_remembers-0.1.1.dist-info/entry_points.txt,sha256=qMb5JYMYHiqiYpWjMU8pu7laRvkERr5VCp02dO-3MA8,71
6
+ mseep_mcp_my_apple_remembers-0.1.1.dist-info/licenses/LICENSE,sha256=A-R50GO0syDr2aKWXxQTKHXxSwGU-VdeIqwEQH9-DcQ,1065
7
+ mseep_mcp_my_apple_remembers-0.1.1.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.27.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ mcp_my_apple_remembers = mcp_my_apple_remembers:main
@@ -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.