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.
- mcp_my_apple_remembers/__init__.py +23 -0
- mcp_my_apple_remembers/server.py +330 -0
- mseep_mcp_my_apple_remembers-0.1.1.dist-info/METADATA +13 -0
- mseep_mcp_my_apple_remembers-0.1.1.dist-info/RECORD +7 -0
- mseep_mcp_my_apple_remembers-0.1.1.dist-info/WHEEL +4 -0
- mseep_mcp_my_apple_remembers-0.1.1.dist-info/entry_points.txt +2 -0
- mseep_mcp_my_apple_remembers-0.1.1.dist-info/licenses/LICENSE +21 -0
@@ -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,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.
|