yomemoai-mcp 0.1.0__py3-none-any.whl → 0.1.2__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.
- yomemoai_mcp/__init__.py +3 -2
- yomemoai_mcp/client.py +66 -4
- yomemoai_mcp/server.py +69 -11
- {yomemoai_mcp-0.1.0.dist-info → yomemoai_mcp-0.1.2.dist-info}/METADATA +13 -7
- yomemoai_mcp-0.1.2.dist-info/RECORD +9 -0
- {yomemoai_mcp-0.1.0.dist-info → yomemoai_mcp-0.1.2.dist-info}/entry_points.txt +0 -1
- yomemoai_mcp-0.1.0.dist-info/RECORD +0 -9
- {yomemoai_mcp-0.1.0.dist-info → yomemoai_mcp-0.1.2.dist-info}/WHEEL +0 -0
- {yomemoai_mcp-0.1.0.dist-info → yomemoai_mcp-0.1.2.dist-info}/licenses/LICENSE +0 -0
yomemoai_mcp/__init__.py
CHANGED
|
@@ -1,2 +1,3 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
"""YoMemoAI MCP Server - Model Context Protocol server for YoMemoAI."""
|
|
2
|
+
|
|
3
|
+
__version__ = "0.1.0"
|
yomemoai_mcp/client.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import base64
|
|
2
2
|
import json
|
|
3
|
+
import logging
|
|
3
4
|
import time
|
|
4
5
|
import requests
|
|
5
6
|
from typing import Dict, List, Optional, Tuple
|
|
@@ -9,6 +10,24 @@ from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
|
|
|
9
10
|
from cryptography.hazmat.backends import default_backend
|
|
10
11
|
import os
|
|
11
12
|
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class MemoRequestError(Exception):
|
|
17
|
+
def __init__(
|
|
18
|
+
self,
|
|
19
|
+
message: str,
|
|
20
|
+
url: str,
|
|
21
|
+
payload: Dict,
|
|
22
|
+
status_code: Optional[int] = None,
|
|
23
|
+
response_text: Optional[str] = None,
|
|
24
|
+
):
|
|
25
|
+
super().__init__(message)
|
|
26
|
+
self.url = url
|
|
27
|
+
self.payload = payload
|
|
28
|
+
self.status_code = status_code
|
|
29
|
+
self.response_text = response_text
|
|
30
|
+
|
|
12
31
|
|
|
13
32
|
class MemoClient:
|
|
14
33
|
"""
|
|
@@ -98,9 +117,20 @@ class MemoClient:
|
|
|
98
117
|
)
|
|
99
118
|
|
|
100
119
|
def add_memory(self, content: str, handle: str = "", description: str = "", metadata: Dict = None):
|
|
120
|
+
# Ensure handle is not empty (API requires it)
|
|
121
|
+
if not handle or not handle.strip():
|
|
122
|
+
handle = "general"
|
|
123
|
+
|
|
124
|
+
# Remove spaces from handle (API doesn't allow spaces)
|
|
125
|
+
handle = handle.replace(" ", "-").lower()
|
|
126
|
+
|
|
101
127
|
token_size = len(content)
|
|
128
|
+
logger.debug(
|
|
129
|
+
f"Adding memory: handle={handle}, content_length={token_size}, description={description}")
|
|
102
130
|
|
|
103
131
|
packed = self.pack_data(content.encode('utf-8'))
|
|
132
|
+
logger.debug(f"Packed data length: {len(packed)}")
|
|
133
|
+
|
|
104
134
|
payload = {
|
|
105
135
|
"ciphertext": packed,
|
|
106
136
|
"description": description,
|
|
@@ -110,10 +140,42 @@ class MemoClient:
|
|
|
110
140
|
"from": "yomemoai-mcp",
|
|
111
141
|
}
|
|
112
142
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
143
|
+
|
|
144
|
+
url = f"{self.base_url}/api/v1/memory"
|
|
145
|
+
logger.debug(f"POST {url}")
|
|
146
|
+
|
|
147
|
+
try:
|
|
148
|
+
resp = self.session.post(url, json=payload)
|
|
149
|
+
logger.debug(f"Response status: {resp.status_code}")
|
|
150
|
+
|
|
151
|
+
# Provide better error messages
|
|
152
|
+
if not resp.ok:
|
|
153
|
+
try:
|
|
154
|
+
error_detail = resp.json().get("error", resp.text)
|
|
155
|
+
except Exception:
|
|
156
|
+
error_detail = resp.text
|
|
157
|
+
logger.error(f"API error {resp.status_code}: {error_detail}")
|
|
158
|
+
raise MemoRequestError(
|
|
159
|
+
f"API error {resp.status_code}: {error_detail}",
|
|
160
|
+
url=url,
|
|
161
|
+
payload=payload,
|
|
162
|
+
status_code=resp.status_code,
|
|
163
|
+
response_text=resp.text,
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
result = resp.json()
|
|
167
|
+
logger.debug(f"Success response: {result}")
|
|
168
|
+
return result
|
|
169
|
+
except requests.exceptions.RequestException as e:
|
|
170
|
+
logger.error(f"Request failed: {type(e).__name__}: {str(e)}")
|
|
171
|
+
raise MemoRequestError(
|
|
172
|
+
f"Request failed: {type(e).__name__}: {str(e)}",
|
|
173
|
+
url=url,
|
|
174
|
+
payload=payload,
|
|
175
|
+
) from e
|
|
176
|
+
except Exception as e:
|
|
177
|
+
logger.error(f"Unexpected error: {type(e).__name__}: {str(e)}")
|
|
178
|
+
raise
|
|
117
179
|
|
|
118
180
|
def get_memories(self, handle: Optional[str] = None) -> List[Dict]:
|
|
119
181
|
url = f"{self.base_url}/api/v1/memory"
|
yomemoai_mcp/server.py
CHANGED
|
@@ -1,11 +1,25 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
+
import json
|
|
3
|
+
import logging
|
|
4
|
+
import sys
|
|
2
5
|
from mcp.server.fastmcp import FastMCP
|
|
3
|
-
from .client import MemoClient
|
|
6
|
+
from .client import MemoClient, MemoRequestError
|
|
4
7
|
import os
|
|
5
8
|
|
|
6
9
|
from dotenv import load_dotenv
|
|
7
10
|
load_dotenv()
|
|
8
11
|
|
|
12
|
+
# Configure logging
|
|
13
|
+
DEBUG = os.getenv("DEBUG", "false").lower() == "true"
|
|
14
|
+
LOG_LEVEL = logging.DEBUG if DEBUG else logging.INFO
|
|
15
|
+
|
|
16
|
+
logging.basicConfig(
|
|
17
|
+
level=LOG_LEVEL,
|
|
18
|
+
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
|
19
|
+
stream=sys.stderr
|
|
20
|
+
)
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
9
23
|
mcp = FastMCP("yomemoai")
|
|
10
24
|
|
|
11
25
|
API_KEY = os.getenv("MEMO_API_KEY", "")
|
|
@@ -33,37 +47,78 @@ if not private_pem.strip():
|
|
|
33
47
|
client = MemoClient(API_KEY, private_pem, BASE_URL)
|
|
34
48
|
|
|
35
49
|
|
|
50
|
+
def _format_payload(payload: dict) -> dict:
|
|
51
|
+
if not payload:
|
|
52
|
+
return {}
|
|
53
|
+
return dict(payload)
|
|
54
|
+
|
|
55
|
+
|
|
36
56
|
@mcp.tool()
|
|
37
57
|
async def save_memory(content: str, handle: str = "general", description: str = "") -> str:
|
|
38
58
|
"""
|
|
39
|
-
Store important information
|
|
40
|
-
|
|
59
|
+
Store important information as a permanent memory. You should call this proactively when you
|
|
60
|
+
detect user preferences, important decisions, reusable logic, or context worth recalling later—
|
|
61
|
+
not only when the user explicitly says "remember" or "save". After saving successfully, reply with ✓.
|
|
41
62
|
|
|
42
63
|
:param content: The actual text/information to be remembered. Be concise but maintain context.
|
|
43
64
|
:param handle: A short, unique category or tag (e.g., 'work', 'personal', 'project-x'). Defaults to 'general'.
|
|
44
|
-
:param description: A brief summary
|
|
65
|
+
:param description: A brief, non-sensitive summary or tag for this memory (helps future identification and search).
|
|
45
66
|
"""
|
|
67
|
+
logger.debug(
|
|
68
|
+
f"save_memory called: handle={handle}, description={description}, content_length={len(content)}")
|
|
46
69
|
try:
|
|
47
70
|
result = client.add_memory(
|
|
48
71
|
content, handle=handle, description=description)
|
|
49
|
-
|
|
72
|
+
logger.debug(f"add_memory response: {result}")
|
|
73
|
+
|
|
74
|
+
if "memory_id" not in result:
|
|
75
|
+
return f"Failed to save memory for handle: {handle}"
|
|
76
|
+
|
|
77
|
+
memory_id = result.get('memory_id')
|
|
78
|
+
|
|
79
|
+
return f"Successfully archived in memory. ID: {memory_id}"
|
|
80
|
+
except MemoRequestError as e:
|
|
81
|
+
logger.error(
|
|
82
|
+
f"Error saving memory: {type(e).__name__}: {str(e)}", exc_info=DEBUG)
|
|
83
|
+
request_info = {
|
|
84
|
+
"url": e.url,
|
|
85
|
+
"status_code": e.status_code,
|
|
86
|
+
"response_text": e.response_text,
|
|
87
|
+
"payload": _format_payload(e.payload),
|
|
88
|
+
"handle": handle,
|
|
89
|
+
"description": description,
|
|
90
|
+
"content_length": len(content),
|
|
91
|
+
}
|
|
92
|
+
# only debug mode return the request_info
|
|
93
|
+
if DEBUG:
|
|
94
|
+
return json.dumps(request_info, ensure_ascii=False)
|
|
95
|
+
return f"Failed to save your memory: {str(e)}"
|
|
50
96
|
except Exception as e:
|
|
51
|
-
|
|
97
|
+
logger.error(
|
|
98
|
+
f"Error saving memory: {type(e).__name__}: {str(e)}", exc_info=DEBUG)
|
|
99
|
+
return (
|
|
100
|
+
f"Failed to save your memory: {str(e)},"
|
|
101
|
+
f"handle: {handle},description: {description},content: {content}"
|
|
102
|
+
)
|
|
52
103
|
|
|
53
104
|
|
|
54
105
|
@mcp.tool()
|
|
55
106
|
async def load_memories(handle: str = None) -> str:
|
|
56
107
|
"""
|
|
57
|
-
Retrieve previously stored memories or
|
|
58
|
-
|
|
59
|
-
you need historical context to answer a question accurately.
|
|
108
|
+
Retrieve previously stored memories. Call this when the user asks what you remember, or when you
|
|
109
|
+
need historical context (preferences, past decisions, project details) to answer accurately.
|
|
60
110
|
|
|
61
|
-
:param handle: Optional filter. If the user specifies a category (e.g., '
|
|
62
|
-
|
|
111
|
+
:param handle: Optional filter. If the user specifies a category (e.g., 'work', 'project-x'),
|
|
112
|
+
use the relevant handle; otherwise omit to load across handles.
|
|
63
113
|
"""
|
|
114
|
+
logger.debug(f"load_memories called: handle={handle}")
|
|
64
115
|
try:
|
|
65
116
|
memories = client.get_memories(handle=handle)
|
|
117
|
+
logger.debug(f"Retrieved {len(memories)} memories")
|
|
118
|
+
|
|
66
119
|
if not memories:
|
|
120
|
+
logger.info(
|
|
121
|
+
f"No memories found for handle: {handle if handle else 'all'}")
|
|
67
122
|
return f"No memories found under the handle: {handle if handle else 'all'}."
|
|
68
123
|
|
|
69
124
|
output = ["### Retrieved Memories:"]
|
|
@@ -72,8 +127,11 @@ async def load_memories(handle: str = None) -> str:
|
|
|
72
127
|
output.append(
|
|
73
128
|
f"Handle: [{m.get('handle')}]\nContent: {m.get('content')}\n---"
|
|
74
129
|
)
|
|
130
|
+
logger.info(f"Successfully loaded {len(memories)} memories")
|
|
75
131
|
return "\n".join(output)
|
|
76
132
|
except Exception as e:
|
|
133
|
+
logger.error(
|
|
134
|
+
f"Error retrieving memories: {type(e).__name__}: {str(e)}", exc_info=DEBUG)
|
|
77
135
|
return f"Error retrieving memories: {str(e)}"
|
|
78
136
|
|
|
79
137
|
if __name__ == "__main__":
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: yomemoai-mcp
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.2
|
|
4
4
|
Summary: Model Context Protocol (MCP) server for YoMemoAI - enables AI assistants to save and retrieve encrypted memories
|
|
5
|
-
|
|
5
|
+
Project-URL: Homepage, https://github.com/yomemoai/python-yomemo-mcp
|
|
6
|
+
Project-URL: Documentation, https://github.com/yomemoai/python-yomemo-mcp#readme
|
|
7
|
+
Project-URL: Repository, https://github.com/yomemoai/python-yomemo-mcp
|
|
8
|
+
Project-URL: Issues, https://github.com/yomemoai/python-yomemo-mcp/issues
|
|
9
|
+
Author-email: xxlv <lvxiang119@gmail.com>
|
|
6
10
|
License: MIT
|
|
7
11
|
License-File: LICENSE
|
|
8
12
|
Keywords: ai,claude,cursor,mcp,mcp-server,memory,yomemoai
|
|
@@ -44,8 +48,8 @@ A Model Context Protocol (MCP) server for YoMemoAI, enabling AI assistants to sa
|
|
|
44
48
|
1. Clone the repository:
|
|
45
49
|
|
|
46
50
|
```bash
|
|
47
|
-
git clone
|
|
48
|
-
cd
|
|
51
|
+
git clone https://github.com/yomemoai/python-yomemo-mcp.git
|
|
52
|
+
cd python-yomemo-mcp
|
|
49
53
|
```
|
|
50
54
|
|
|
51
55
|
2. Install dependencies using uv:
|
|
@@ -99,6 +103,11 @@ After configuration, you can run the MCP server using:
|
|
|
99
103
|
uv run memo-mcp
|
|
100
104
|
```
|
|
101
105
|
|
|
106
|
+
### Cursor best practice (recommended)
|
|
107
|
+
|
|
108
|
+
Want the AI to **proactively** save memories when it detects preferences or decisions (not only when you say "remember")?
|
|
109
|
+
Add one rule in Cursor's Rules for AI—see **[CURSOR_RULES.md](./CURSOR_RULES.md)**.
|
|
110
|
+
|
|
102
111
|
### Integration with Cursor/Claude Desktop
|
|
103
112
|
|
|
104
113
|
Add the following configuration to your MCP settings file:
|
|
@@ -225,9 +234,6 @@ yomemoai-mcp/
|
|
|
225
234
|
|
|
226
235
|
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
|
227
236
|
|
|
228
|
-
## Contributing
|
|
229
|
-
|
|
230
|
-
[Add contribution guidelines here]
|
|
231
237
|
|
|
232
238
|
## Support
|
|
233
239
|
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
yomemoai_mcp/__init__.py,sha256=gDi0t9iKR423OZlwrrtwDaED3ck06XMe_y8mcxisBl0,95
|
|
2
|
+
yomemoai_mcp/client.py,sha256=oZNfRW3ppfSRkWvG3ej-w56lUyfKsc6jOjvV_IY4JxM,6960
|
|
3
|
+
yomemoai_mcp/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
+
yomemoai_mcp/server.py,sha256=K4-yj2KoBrcKFc9-C8v3NcG87sBpUDde1giHJamwho8,5093
|
|
5
|
+
yomemoai_mcp-0.1.2.dist-info/METADATA,sha256=clSl9U0TcQlC602gJlxFZFfWz8epgWXxsHsKZV_2glg,7551
|
|
6
|
+
yomemoai_mcp-0.1.2.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
7
|
+
yomemoai_mcp-0.1.2.dist-info/entry_points.txt,sha256=uV0479NK4ppOUV1T9VSwFTLi87F4deaRjbL8s43R8uI,100
|
|
8
|
+
yomemoai_mcp-0.1.2.dist-info/licenses/LICENSE,sha256=UVt2r_C3iy7rNKJAj_EcLcmcZdXONxJ01oJNsIEqTfw,1066
|
|
9
|
+
yomemoai_mcp-0.1.2.dist-info/RECORD,,
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
yomemoai_mcp/__init__.py,sha256=I2FoPD8dKHgF95vpefoGxZkQ9gi9yeYkEM2ZObyUQ48,58
|
|
2
|
-
yomemoai_mcp/client.py,sha256=vNPnYJAYpdZlnYXBKyyx618-sAtNSzZNTRXENw4G8uo,4850
|
|
3
|
-
yomemoai_mcp/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
-
yomemoai_mcp/server.py,sha256=3J-RLInsVz2ALTQV1YyAQti9mG7Y5YPfYXXO_sYprzg,2973
|
|
5
|
-
yomemoai_mcp-0.1.0.dist-info/METADATA,sha256=04U52HSfVF9ODHRLU04tpkjp3m4BkllM_82EoZlvmbs,7032
|
|
6
|
-
yomemoai_mcp-0.1.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
7
|
-
yomemoai_mcp-0.1.0.dist-info/entry_points.txt,sha256=gWU6hmHA4oQMCYGqffv-f2YamGj-ERq8np2uuCSOhuI,137
|
|
8
|
-
yomemoai_mcp-0.1.0.dist-info/licenses/LICENSE,sha256=UVt2r_C3iy7rNKJAj_EcLcmcZdXONxJ01oJNsIEqTfw,1066
|
|
9
|
-
yomemoai_mcp-0.1.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|