yomemoai-mcp 0.1.0__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,2 @@
1
+ def hello() -> str:
2
+ return "Hello from yomemoai-mcp!"
yomemoai_mcp/client.py ADDED
@@ -0,0 +1,131 @@
1
+ import base64
2
+ import json
3
+ import time
4
+ import requests
5
+ from typing import Dict, List, Optional, Tuple
6
+ from cryptography.hazmat.primitives import hashes, serialization
7
+ from cryptography.hazmat.primitives.asymmetric import padding
8
+ from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
9
+ from cryptography.hazmat.backends import default_backend
10
+ import os
11
+
12
+
13
+ class MemoClient:
14
+ """
15
+ Client for yomemoai API
16
+ It's a wrapper for the yomemoai API.
17
+ """
18
+
19
+ def __init__(self, api_key: str, private_key_pem: str, base_url: str):
20
+ self.api_key = api_key
21
+ self.base_url = base_url.rstrip('/')
22
+ self.private_key = serialization.load_pem_private_key(
23
+ self._normalize_pem(private_key_pem),
24
+ password=None,
25
+ backend=default_backend()
26
+ )
27
+ self.public_key = self.private_key.public_key()
28
+ self.session = requests.Session()
29
+ self.session.headers.update(
30
+ {"X-Memo-API-Key": self.api_key, "Content-Type": "application/json"})
31
+
32
+ def _normalize_pem(self, pem_str: str) -> bytes:
33
+ pem_str = pem_str.strip()
34
+ if "-----BEGIN" not in pem_str:
35
+ return f"-----BEGIN PRIVATE KEY-----\n{pem_str}\n-----END PRIVATE KEY-----".encode()
36
+ return pem_str.encode()
37
+
38
+ def pack_data(self, raw_data: bytes) -> str:
39
+ aes_key = os.urandom(32)
40
+ nonce = os.urandom(12)
41
+
42
+ cipher = Cipher(algorithms.AES(aes_key), modes.GCM(
43
+ nonce), backend=default_backend())
44
+ encryptor = cipher.encryptor()
45
+ ciphertext = encryptor.update(raw_data) + encryptor.finalize()
46
+
47
+ combined_data = base64.b64encode(
48
+ nonce + ciphertext + encryptor.tag).decode('utf-8')
49
+
50
+ encrypted_key = self.public_key.encrypt(
51
+ aes_key,
52
+ padding.OAEP(mgf=padding.MGF1(algorithm=hashes.SHA256()),
53
+ algorithm=hashes.SHA256(), label=None)
54
+ )
55
+ key_base64 = base64.b64encode(encrypted_key).decode('utf-8')
56
+
57
+ signature = self.private_key.sign(
58
+ combined_data.encode('utf-8'),
59
+ padding.PKCS1v15(),
60
+ hashes.SHA256()
61
+ )
62
+ sig_base64 = base64.b64encode(signature).decode('utf-8')
63
+
64
+ pkg = {
65
+ "data": combined_data,
66
+ "key": key_base64,
67
+ "signature": sig_base64
68
+ }
69
+ return base64.b64encode(json.dumps(pkg).encode()).decode()
70
+
71
+ def unpack_and_decrypt(self, encrypted_pkg_base64: str) -> bytes:
72
+ pkg_json = base64.b64decode(encrypted_pkg_base64)
73
+ pkg = json.loads(pkg_json)
74
+
75
+ if "key" in pkg and pkg["key"]:
76
+ encrypted_key = base64.b64decode(pkg["key"])
77
+ aes_key = self.private_key.decrypt(
78
+ encrypted_key,
79
+ padding.OAEP(mgf=padding.MGF1(algorithm=hashes.SHA256()),
80
+ algorithm=hashes.SHA256(), label=None)
81
+ )
82
+
83
+ combined_data = base64.b64decode(pkg["data"])
84
+ nonce = combined_data[:12]
85
+ tag = combined_data[-16:]
86
+ ciphertext = combined_data[12:-16]
87
+
88
+ cipher = Cipher(algorithms.AES(aes_key), modes.GCM(
89
+ nonce, tag), backend=default_backend())
90
+ decryptor = cipher.decryptor()
91
+ return decryptor.update(ciphertext) + decryptor.finalize()
92
+ else:
93
+ data = base64.b64decode(pkg["data"])
94
+ return self.private_key.decrypt(
95
+ data,
96
+ padding.OAEP(mgf=padding.MGF1(algorithm=hashes.SHA256()),
97
+ algorithm=hashes.SHA256(), label=None)
98
+ )
99
+
100
+ def add_memory(self, content: str, handle: str = "", description: str = "", metadata: Dict = None):
101
+ token_size = len(content)
102
+
103
+ packed = self.pack_data(content.encode('utf-8'))
104
+ payload = {
105
+ "ciphertext": packed,
106
+ "description": description,
107
+ "handle": handle,
108
+ "metadata": metadata or {
109
+ "token_size": token_size,
110
+ "from": "yomemoai-mcp",
111
+ }
112
+ }
113
+ resp = self.session.post(
114
+ f"{self.base_url}/api/v1/memory", json=payload)
115
+ resp.raise_for_status()
116
+ return resp.json()
117
+
118
+ def get_memories(self, handle: Optional[str] = None) -> List[Dict]:
119
+ url = f"{self.base_url}/api/v1/memory"
120
+ params = {"handle": handle} if handle else {}
121
+ resp = self.session.get(url, params=params)
122
+ resp.raise_for_status()
123
+
124
+ memories = resp.json().get("data", [])
125
+ for m in memories:
126
+ try:
127
+ decrypted = self.unpack_and_decrypt(m["content"])
128
+ m["content"] = decrypted.decode('utf-8')
129
+ except Exception as e:
130
+ print(f"Decryption failed for {m.get('id')}: {e}")
131
+ return memories
yomemoai_mcp/py.typed ADDED
File without changes
yomemoai_mcp/server.py ADDED
@@ -0,0 +1,80 @@
1
+ import asyncio
2
+ from mcp.server.fastmcp import FastMCP
3
+ from .client import MemoClient
4
+ import os
5
+
6
+ from dotenv import load_dotenv
7
+ load_dotenv()
8
+
9
+ mcp = FastMCP("yomemoai")
10
+
11
+ API_KEY = os.getenv("MEMO_API_KEY", "")
12
+ PRIV_KEY_PATH = os.getenv("MEMO_PRIVATE_KEY_PATH", "private.pem")
13
+ BASE_URL = os.getenv("MEMO_BASE_URL", "https://api.yomemo.ai")
14
+
15
+ if not API_KEY:
16
+ raise ValueError("MEMO_API_KEY environment variable is required")
17
+
18
+ if not os.path.exists(PRIV_KEY_PATH):
19
+ raise FileNotFoundError(
20
+ f"Private key file not found: {PRIV_KEY_PATH}. "
21
+ f"Please set MEMO_PRIVATE_KEY_PATH environment variable or place your private key at {PRIV_KEY_PATH}"
22
+ )
23
+
24
+ try:
25
+ with open(PRIV_KEY_PATH, "r") as f:
26
+ private_pem = f.read()
27
+ except Exception as e:
28
+ raise IOError(f"Failed to read private key from {PRIV_KEY_PATH}: {e}")
29
+
30
+ if not private_pem.strip():
31
+ raise ValueError(f"Private key file {PRIV_KEY_PATH} is empty")
32
+
33
+ client = MemoClient(API_KEY, private_pem, BASE_URL)
34
+
35
+
36
+ @mcp.tool()
37
+ async def save_memory(content: str, handle: str = "general", description: str = "") -> str:
38
+ """
39
+ Store important information, user preferences, or conversation context as a permanent memory.
40
+ Use this tool when the user explicitly asks to 'remember', 'save', or 'keep track of' something.
41
+
42
+ :param content: The actual text/information to be remembered. Be concise but maintain context.
43
+ :param handle: A short, unique category or tag (e.g., 'work', 'personal', 'project-x'). Defaults to 'general'.
44
+ :param description: A brief summary of what this memory is about to help with future identification. don't include any sensitive information.
45
+ """
46
+ try:
47
+ result = client.add_memory(
48
+ content, handle=handle, description=description)
49
+ return f"Successfully archived in memory. ID: {result.get('memory_id')}"
50
+ except Exception as e:
51
+ return f"Failed to save memory: {str(e)}"
52
+
53
+
54
+ @mcp.tool()
55
+ async def load_memories(handle: str = None) -> str:
56
+ """
57
+ Retrieve previously stored memories or context.
58
+ Use this tool when the user asks 'what do you remember about...', 'check my notes on...', or when
59
+ you need historical context to answer a question accurately.
60
+
61
+ :param handle: Optional filter. If the user specifies a category (e.g., 'about my job'),
62
+ extract and provide the relevant handle.
63
+ """
64
+ try:
65
+ memories = client.get_memories(handle=handle)
66
+ if not memories:
67
+ return f"No memories found under the handle: {handle if handle else 'all'}."
68
+
69
+ output = ["### Retrieved Memories:"]
70
+ for m in memories:
71
+ timestamp = m.get('created_at', 'N/A')
72
+ output.append(
73
+ f"Handle: [{m.get('handle')}]\nContent: {m.get('content')}\n---"
74
+ )
75
+ return "\n".join(output)
76
+ except Exception as e:
77
+ return f"Error retrieving memories: {str(e)}"
78
+
79
+ if __name__ == "__main__":
80
+ mcp.run()
@@ -0,0 +1,234 @@
1
+ Metadata-Version: 2.4
2
+ Name: yomemoai-mcp
3
+ Version: 0.1.0
4
+ Summary: Model Context Protocol (MCP) server for YoMemoAI - enables AI assistants to save and retrieve encrypted memories
5
+ Author-email: lvxiangxiang <lvxiangxiang@shopline.com>
6
+ License: MIT
7
+ License-File: LICENSE
8
+ Keywords: ai,claude,cursor,mcp,mcp-server,memory,yomemoai
9
+ Classifier: Development Status :: 4 - Beta
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
15
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
16
+ Requires-Python: >=3.12
17
+ Requires-Dist: cryptography>=46.0.3
18
+ Requires-Dist: fastmcp>=0.1.0
19
+ Requires-Dist: mcp>=1.25.0
20
+ Requires-Dist: python-dotenv>=1.2.1
21
+ Requires-Dist: requests>=2.32.5
22
+ Description-Content-Type: text/markdown
23
+
24
+ # yomemoai-mcp
25
+
26
+ A Model Context Protocol (MCP) server for YoMemoAI, enabling AI assistants to save and retrieve encrypted memories.
27
+
28
+ ## Features
29
+
30
+ - 🔐 **Secure Storage**: Encrypted memory storage using RSA-OAEP and AES-GCM
31
+ - 🏷️ **Categorization**: Organize memories with handles (tags/categories)
32
+ - 🔍 **Retrieval**: Query memories by handle or retrieve all memories
33
+ - 🚀 **MCP Integration**: Seamlessly integrates with MCP-compatible AI assistants
34
+
35
+ ## Prerequisites
36
+
37
+ - Python 3.12 or higher
38
+ - [uv](https://github.com/astral-sh/uv) package manager
39
+ - YoMemoAI API key
40
+ - RSA private key (PEM format)
41
+
42
+ ## Installation
43
+
44
+ 1. Clone the repository:
45
+
46
+ ```bash
47
+ git clone <repository-url>
48
+ cd yomemoai-mcp
49
+ ```
50
+
51
+ 2. Install dependencies using uv:
52
+
53
+ ```bash
54
+ uv sync
55
+ ```
56
+
57
+ ## Configuration
58
+
59
+ Create a `.env` file in the project root with the following configuration:
60
+
61
+ ```bash
62
+ # YoMemoAI MCP Server Configuration
63
+
64
+ # Your API key from YoMemoAI (required)
65
+ MEMO_API_KEY=your_api_key_here
66
+
67
+ # Path to your private key file (RSA private key in PEM format)
68
+ # Default: private.pem
69
+ MEMO_PRIVATE_KEY_PATH=private.pem
70
+
71
+ # Base URL of the YoMemoAI API (optional)
72
+ # Default: https://api.yomemo.ai
73
+ MEMO_BASE_URL=https://api.yomemo.ai
74
+ ```
75
+
76
+ **Important Configuration Notes:**
77
+
78
+ 1. **MEMO_API_KEY** (required): Your YoMemoAI API key. You must obtain this from your YoMemoAI account.
79
+
80
+ 2. **MEMO_PRIVATE_KEY_PATH**: Path to your RSA private key file. The private key should be in PEM format:
81
+
82
+ ```
83
+ -----BEGIN PRIVATE KEY-----
84
+ ...
85
+ -----END PRIVATE KEY-----
86
+ ```
87
+
88
+ Make sure this file is secure and never commit it to version control.
89
+
90
+ 3. **MEMO_BASE_URL** (optional): The API base URL. Defaults to `https://api.yomemo.ai` if not specified.
91
+
92
+ ## Usage
93
+
94
+ ### Running the MCP Server
95
+
96
+ After configuration, you can run the MCP server using:
97
+
98
+ ```bash
99
+ uv run memo-mcp
100
+ ```
101
+
102
+ ### Integration with Cursor/Claude Desktop
103
+
104
+ Add the following configuration to your MCP settings file:
105
+
106
+ **For Cursor**: `~/.cursor/mcp.json` (or `%APPDATA%\Cursor\User\mcp.json` on Windows)
107
+ **For Claude Desktop**: `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) or `%APPDATA%\Claude\claude_desktop_config.json` (Windows)
108
+
109
+ #### Option 1: Using `uvx` (Recommended - after publishing to PyPI)
110
+
111
+ If the package is published to PyPI, you can use `uvx` to run it directly. The package provides multiple entry points:
112
+
113
+ **Using `uvx yomemoai-mcp`** (recommended):
114
+
115
+ ```json
116
+ {
117
+ "mcpServers": {
118
+ "yomemoai": {
119
+ "command": "uvx",
120
+ "args": ["yomemoai-mcp"],
121
+ "env": {
122
+ "MEMO_API_KEY": "your_api_key_here",
123
+ "MEMO_PRIVATE_KEY_PATH": "/absolute/path/to/private.pem",
124
+ "MEMO_BASE_URL": "https://api.yomemo.ai"
125
+ }
126
+ }
127
+ }
128
+ }
129
+ ```
130
+
131
+ **How `uvx` works:**
132
+
133
+ - `uvx` automatically downloads the package from PyPI (if not already installed)
134
+ - It runs the package's entry point script defined in `project.scripts`
135
+ - The package name is `yomemoai-mcp`, so `uvx yomemoai-mcp` will automatically use the `yomemoai-mcp` entry point
136
+ - The package also provides alternative entry points: `yomemo` and `memo-mcp` (all point to the same server)
137
+
138
+ **Note**: The package must be published to PyPI before using `uvx`. If the package is not yet published, use Option 2 for local development.
139
+
140
+ #### Option 2: Using local development setup
141
+
142
+ For local development or if the package is not yet published, use the `uv --directory` approach:
143
+
144
+ ```json
145
+ {
146
+ "mcpServers": {
147
+ "yomemoai": {
148
+ "command": "uv",
149
+ "args": [
150
+ "--directory",
151
+ "/absolute/path/to/yomemoai-mcp",
152
+ "run",
153
+ "yomemoai-mcp"
154
+ ],
155
+ "env": {
156
+ "MEMO_API_KEY": "your_api_key_here",
157
+ "MEMO_PRIVATE_KEY_PATH": "/absolute/path/to/private.pem",
158
+ "MEMO_BASE_URL": "https://api.yomemo.ai"
159
+ }
160
+ }
161
+ }
162
+ }
163
+ ```
164
+
165
+ **Important Notes:**
166
+
167
+ - **For Option 1 (`uvx`)**: The package must be published to PyPI. `uvx` will automatically handle installation and execution.
168
+ - **For Option 2 (local)**: Replace `/absolute/path/to/yomemoai-mcp` with the **absolute path** to this repository on your system
169
+ - Replace `/absolute/path/to/private.pem` with the **absolute path** to your private key file
170
+ - The `env` section in the MCP config will override any `.env` file in the project directory
171
+ - After updating the MCP config, restart Cursor/Claude Desktop for changes to take effect
172
+
173
+ ### Available Tools
174
+
175
+ #### `save_memory`
176
+
177
+ Store important information, user preferences, or conversation context as a permanent memory.
178
+
179
+ **Parameters:**
180
+
181
+ - `content` (required): The actual text/information to be remembered
182
+ - `handle` (optional): A short, unique category or tag (e.g., 'work', 'personal', 'project-x'). Defaults to 'general'
183
+ - `description` (optional): A brief summary of what this memory is about
184
+
185
+ #### `load_memories`
186
+
187
+ Retrieve previously stored memories or context.
188
+
189
+ **Parameters:**
190
+
191
+ - `handle` (optional): Filter memories by category. If not specified, returns all memories
192
+
193
+ ## Development
194
+
195
+ ### Project Structure
196
+
197
+ ```
198
+ yomemoai-mcp/
199
+ ├── src/
200
+ │ └── yomemoai_mcp/
201
+ │ ├── __init__.py
202
+ │ ├── server.py # MCP server implementation
203
+ │ ├── client.py # YoMemoAI API client
204
+ │ └── py.typed # Type hints marker
205
+ ├── pyproject.toml # Project configuration
206
+ ├── uv.lock # Dependency lock file
207
+ └── README.md
208
+ ```
209
+
210
+ ### Dependencies
211
+
212
+ - `cryptography`: For encryption/decryption operations
213
+ - `fastmcp`: FastMCP framework for MCP servers
214
+ - `mcp`: Model Context Protocol SDK
215
+ - `python-dotenv`: Environment variable management
216
+ - `requests`: HTTP client for API calls
217
+
218
+ ## Security Notes
219
+
220
+ - **Never commit** your `.env` file or private key files to version control
221
+ - Keep your private key secure and never share it
222
+ - The `.env` file is already included in `.gitignore`
223
+
224
+ ## License
225
+
226
+ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
227
+
228
+ ## Contributing
229
+
230
+ [Add contribution guidelines here]
231
+
232
+ ## Support
233
+
234
+ For issues and questions, please open an issue on the repository.
@@ -0,0 +1,9 @@
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,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.28.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,4 @@
1
+ [console_scripts]
2
+ memo-mcp = yomemoai_mcp.server:mcp.run
3
+ yomemo = yomemoai_mcp.server:mcp.run
4
+ yomemoai-mcp = yomemoai_mcp.server:mcp.run
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 yomemo.ai
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.