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.
- mseep_mcp_my_apple_remembers-0.1.1/.gitignore +39 -0
- mseep_mcp_my_apple_remembers-0.1.1/Dockerfile +14 -0
- mseep_mcp_my_apple_remembers-0.1.1/LICENSE +21 -0
- mseep_mcp_my_apple_remembers-0.1.1/PKG-INFO +13 -0
- mseep_mcp_my_apple_remembers-0.1.1/README.md +92 -0
- mseep_mcp_my_apple_remembers-0.1.1/pyproject.toml +36 -0
- mseep_mcp_my_apple_remembers-0.1.1/src/mcp_my_apple_remembers/__init__.py +23 -0
- mseep_mcp_my_apple_remembers-0.1.1/src/mcp_my_apple_remembers/server.py +330 -0
- mseep_mcp_my_apple_remembers-0.1.1/uv.lock +725 -0
@@ -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
|
+
[](https://hub.docker.com/r/buryhuang/mcp-my-apple-remembers)
|
5
|
+
[](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)
|