yomemoai-mcp 0.1.0__tar.gz → 0.1.3__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.
- yomemoai_mcp-0.1.3/CURSOR_RULES.md +40 -0
- {yomemoai_mcp-0.1.0 → yomemoai_mcp-0.1.3}/PKG-INFO +13 -7
- {yomemoai_mcp-0.1.0 → yomemoai_mcp-0.1.3}/PUBLISHING.md +23 -3
- {yomemoai_mcp-0.1.0 → yomemoai_mcp-0.1.3}/README.md +7 -5
- yomemoai_mcp-0.1.3/llm.md +27 -0
- {yomemoai_mcp-0.1.0 → yomemoai_mcp-0.1.3}/pyproject.toml +8 -3
- yomemoai_mcp-0.1.3/src/yomemoai_mcp/__init__.py +3 -0
- {yomemoai_mcp-0.1.0 → yomemoai_mcp-0.1.3}/src/yomemoai_mcp/client.py +66 -4
- yomemoai_mcp-0.1.3/src/yomemoai_mcp/server.py +143 -0
- {yomemoai_mcp-0.1.0 → yomemoai_mcp-0.1.3}/uv.lock +27 -27
- yomemoai_mcp-0.1.0/src/yomemoai_mcp/__init__.py +0 -2
- yomemoai_mcp-0.1.0/src/yomemoai_mcp/server.py +0 -80
- {yomemoai_mcp-0.1.0 → yomemoai_mcp-0.1.3}/.gitignore +0 -0
- {yomemoai_mcp-0.1.0 → yomemoai_mcp-0.1.3}/.python-version +0 -0
- {yomemoai_mcp-0.1.0 → yomemoai_mcp-0.1.3}/LICENSE +0 -0
- {yomemoai_mcp-0.1.0 → yomemoai_mcp-0.1.3}/src/yomemoai_mcp/py.typed +0 -0
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Cursor best practice: proactive Yomemo long-term memory
|
|
2
|
+
|
|
3
|
+
After configuring the Yomemo MCP in Cursor, add the rule below to **Rules for AI**. The AI will then **proactively** call `save_memory` when it detects important information—no need to say "remember this" every time.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Option 1: Cursor global / project Rules for AI
|
|
8
|
+
|
|
9
|
+
Open Cursor settings → **Rules for AI**, add a new rule, and paste the following:
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
You are equipped with Yomemo.ai MCP.
|
|
13
|
+
|
|
14
|
+
## When to use `save_memory`:
|
|
15
|
+
- **Tech Stack**: When we decide on a specific library or version.
|
|
16
|
+
- **Business Logic**: When I explain a complex internal rule.
|
|
17
|
+
- **Preferences**: If I tell you "I prefer using early returns in Go".
|
|
18
|
+
|
|
19
|
+
## When to use `load_memories`:
|
|
20
|
+
- At the start of a new feature implementation, check if there's relevant context in the 'coding' or 'project-name' handle.
|
|
21
|
+
|
|
22
|
+
## Feedback:
|
|
23
|
+
- After saving, just add a ✓ in your response. No need for a long confirmation.
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## Option 2: Project-level rule file (.cursor/rules)
|
|
29
|
+
|
|
30
|
+
Create `.cursor/rules/` in your project root and add a file such as `yomemo-memory.mdc` with the same content above. The rule will then apply only in that project.
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## What you get
|
|
35
|
+
|
|
36
|
+
- The AI will call `save_memory` when it recognizes **preferences, decisions, or reusable logic**.
|
|
37
|
+
- After saving, it will add a ✓ in its reply—smooth and low-friction.
|
|
38
|
+
- At the start of new chats or features, it will call `load_memories` when it needs historical context.
|
|
39
|
+
|
|
40
|
+
Use this together with your [yomemoai-mcp](https://github.com/yomemoai/python-yomemo-mcp) MCP setup.
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: yomemoai-mcp
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.3
|
|
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
|
|
|
@@ -8,6 +8,19 @@ This document describes how to publish `yomemoai-mcp` to PyPI so that users can
|
|
|
8
8
|
2. **TestPyPI Account** (recommended for testing): Create an account at [TestPyPI](https://test.pypi.org/)
|
|
9
9
|
3. **API Token**: You'll need an API token to publish packages
|
|
10
10
|
|
|
11
|
+
### Important: TestPyPI vs PyPI Accounts
|
|
12
|
+
|
|
13
|
+
**TestPyPI and PyPI are separate systems**, but you can use the **same username and password** for both:
|
|
14
|
+
|
|
15
|
+
- **Same credentials work**: If you register on TestPyPI, you can use the same username and password to register on PyPI
|
|
16
|
+
- **Separate accounts**: They are technically separate accounts, but sharing credentials is common and recommended
|
|
17
|
+
- **Email conflicts**: If you get an "email already in use" error on PyPI:
|
|
18
|
+
- You may have already registered on PyPI before (check your email for activation links)
|
|
19
|
+
- Or someone else is using that email (unlikely but possible)
|
|
20
|
+
- **Solution**: Try password reset on PyPI using the same email, or use a different email
|
|
21
|
+
|
|
22
|
+
**Recommended approach**: Register on both TestPyPI and PyPI using the same username and password for consistency.
|
|
23
|
+
|
|
11
24
|
## Step 1: Create API Token
|
|
12
25
|
|
|
13
26
|
1. Log in to [PyPI](https://pypi.org/)
|
|
@@ -41,6 +54,7 @@ uv build
|
|
|
41
54
|
```
|
|
42
55
|
|
|
43
56
|
This will create a `dist/` directory containing:
|
|
57
|
+
|
|
44
58
|
- `yomemoai_mcp-0.1.0-py3-none-any.whl` (wheel distribution)
|
|
45
59
|
- `yomemoai-mcp-0.1.0.tar.gz` (source distribution)
|
|
46
60
|
|
|
@@ -67,17 +81,21 @@ uv publish --publish-url https://test.pypi.org/legacy/
|
|
|
67
81
|
|
|
68
82
|
Test that the package can be installed and run:
|
|
69
83
|
|
|
84
|
+
**Important**: TestPyPI may not have all dependencies. Use `--extra-index-url` to fetch dependencies from PyPI while getting the package from TestPyPI:
|
|
85
|
+
|
|
70
86
|
```bash
|
|
71
|
-
# Test installation from TestPyPI
|
|
72
|
-
uvx --index-url https://test.pypi.org/simple/ yomemoai-mcp --help
|
|
87
|
+
# Test installation from TestPyPI (dependencies from PyPI)
|
|
88
|
+
uvx --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple/ yomemoai-mcp --help
|
|
73
89
|
|
|
74
90
|
# Or test in a clean environment
|
|
75
91
|
uv venv test-env
|
|
76
92
|
source test-env/bin/activate # On Windows: test-env\Scripts\activate
|
|
77
|
-
pip install --index-url https://test.pypi.org/simple/ yomemoai-mcp
|
|
93
|
+
pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple/ yomemoai-mcp
|
|
78
94
|
yomemoai-mcp --help
|
|
79
95
|
```
|
|
80
96
|
|
|
97
|
+
**Note**: The `--extra-index-url` flag tells the package manager to also check PyPI for dependencies that might not be available on TestPyPI.
|
|
98
|
+
|
|
81
99
|
## Step 5: Publish to PyPI
|
|
82
100
|
|
|
83
101
|
Once testing is successful, publish to the real PyPI:
|
|
@@ -130,6 +148,7 @@ curl https://pypi.org/pypi/yomemoai-mcp/json | jq
|
|
|
130
148
|
When you need to publish a new version:
|
|
131
149
|
|
|
132
150
|
1. **Update version** in `pyproject.toml`:
|
|
151
|
+
|
|
133
152
|
```toml
|
|
134
153
|
version = "0.1.1" # or 0.2.0, 1.0.0, etc.
|
|
135
154
|
```
|
|
@@ -145,6 +164,7 @@ When you need to publish a new version:
|
|
|
145
164
|
## Version Numbering
|
|
146
165
|
|
|
147
166
|
Follow [Semantic Versioning](https://semver.org/):
|
|
167
|
+
|
|
148
168
|
- **MAJOR** (1.0.0): Breaking changes
|
|
149
169
|
- **MINOR** (0.1.0): New features, backward compatible
|
|
150
170
|
- **PATCH** (0.1.1): Bug fixes, backward compatible
|
|
@@ -21,8 +21,8 @@ A Model Context Protocol (MCP) server for YoMemoAI, enabling AI assistants to sa
|
|
|
21
21
|
1. Clone the repository:
|
|
22
22
|
|
|
23
23
|
```bash
|
|
24
|
-
git clone
|
|
25
|
-
cd
|
|
24
|
+
git clone https://github.com/yomemoai/python-yomemo-mcp.git
|
|
25
|
+
cd python-yomemo-mcp
|
|
26
26
|
```
|
|
27
27
|
|
|
28
28
|
2. Install dependencies using uv:
|
|
@@ -76,6 +76,11 @@ After configuration, you can run the MCP server using:
|
|
|
76
76
|
uv run memo-mcp
|
|
77
77
|
```
|
|
78
78
|
|
|
79
|
+
### Cursor best practice (recommended)
|
|
80
|
+
|
|
81
|
+
Want the AI to **proactively** save memories when it detects preferences or decisions (not only when you say "remember")?
|
|
82
|
+
Add one rule in Cursor's Rules for AI—see **[CURSOR_RULES.md](./CURSOR_RULES.md)**.
|
|
83
|
+
|
|
79
84
|
### Integration with Cursor/Claude Desktop
|
|
80
85
|
|
|
81
86
|
Add the following configuration to your MCP settings file:
|
|
@@ -202,9 +207,6 @@ yomemoai-mcp/
|
|
|
202
207
|
|
|
203
208
|
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
|
204
209
|
|
|
205
|
-
## Contributing
|
|
206
|
-
|
|
207
|
-
[Add contribution guidelines here]
|
|
208
210
|
|
|
209
211
|
## Support
|
|
210
212
|
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Project Description for GitHub/PyPI
|
|
2
|
+
|
|
3
|
+
**yomemoai-mcp** is a Model Context Protocol (MCP) server that enables AI assistants like Claude and Cursor to seamlessly save and retrieve encrypted memories through YoMemoAI's secure storage platform.
|
|
4
|
+
|
|
5
|
+
## Key Features
|
|
6
|
+
|
|
7
|
+
- **End-to-End Encryption**: All memories are encrypted client-side using RSA-OAEP and AES-GCM before being stored, ensuring your data remains private even from the service provider
|
|
8
|
+
- **Persistent Memory**: AI assistants can remember important information, user preferences, and conversation context across sessions
|
|
9
|
+
- **Organized Storage**: Memories can be categorized using handles (tags/categories) for easy retrieval and organization
|
|
10
|
+
- **MCP Integration**: Built on the Model Context Protocol standard, making it compatible with any MCP-enabled AI assistant
|
|
11
|
+
- **Easy Installation**: Available on PyPI and can be installed with a single command: `uvx yomemoai-mcp`
|
|
12
|
+
|
|
13
|
+
## Use Cases
|
|
14
|
+
|
|
15
|
+
- Save important user preferences and context that AI assistants should remember
|
|
16
|
+
- Store conversation history and key information for future reference
|
|
17
|
+
- Organize memories by topics, projects, or categories using handles
|
|
18
|
+
- Enable AI assistants to maintain long-term memory across multiple sessions
|
|
19
|
+
|
|
20
|
+
## Technical Highlights
|
|
21
|
+
|
|
22
|
+
- Hybrid encryption architecture (RSA + AES-GCM) for secure data storage
|
|
23
|
+
- FastMCP framework for efficient MCP server implementation
|
|
24
|
+
- Type-safe Python implementation with full type hints
|
|
25
|
+
- Simple configuration via environment variables
|
|
26
|
+
|
|
27
|
+
Perfect for developers and users who want to give their AI assistants persistent, encrypted memory capabilities.
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "yomemoai-mcp"
|
|
3
|
-
version = "0.1.
|
|
3
|
+
version = "0.1.3"
|
|
4
4
|
description = "Model Context Protocol (MCP) server for YoMemoAI - enables AI assistants to save and retrieve encrypted memories"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = { text = "MIT" }
|
|
7
7
|
authors = [
|
|
8
|
-
{ name = "
|
|
8
|
+
{ name = "xxlv", email = "lvxiang119@gmail.com" }
|
|
9
9
|
]
|
|
10
10
|
requires-python = ">=3.12"
|
|
11
11
|
keywords = ["mcp", "mcp-server", "yomemoai", "memory", "ai", "claude", "cursor"]
|
|
@@ -26,9 +26,14 @@ dependencies = [
|
|
|
26
26
|
"requests>=2.32.5",
|
|
27
27
|
]
|
|
28
28
|
|
|
29
|
+
[project.urls]
|
|
30
|
+
Homepage = "https://github.com/yomemoai/python-yomemo-mcp"
|
|
31
|
+
Documentation = "https://github.com/yomemoai/python-yomemo-mcp#readme"
|
|
32
|
+
Repository = "https://github.com/yomemoai/python-yomemo-mcp"
|
|
33
|
+
Issues = "https://github.com/yomemoai/python-yomemo-mcp/issues"
|
|
34
|
+
|
|
29
35
|
[project.scripts]
|
|
30
36
|
yomemoai-mcp = "yomemoai_mcp.server:mcp.run"
|
|
31
|
-
yomemo = "yomemoai_mcp.server:mcp.run"
|
|
32
37
|
memo-mcp = "yomemoai_mcp.server:mcp.run"
|
|
33
38
|
|
|
34
39
|
[build-system]
|
|
@@ -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"
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import json
|
|
3
|
+
import logging
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
from mcp.server.fastmcp import FastMCP
|
|
7
|
+
from .client import MemoClient, MemoRequestError
|
|
8
|
+
|
|
9
|
+
if "--version" in sys.argv or "-version" in sys.argv:
|
|
10
|
+
from importlib.metadata import version
|
|
11
|
+
print(version("yomemoai-mcp"))
|
|
12
|
+
sys.exit(0)
|
|
13
|
+
|
|
14
|
+
from dotenv import load_dotenv
|
|
15
|
+
load_dotenv()
|
|
16
|
+
|
|
17
|
+
# Configure logging
|
|
18
|
+
DEBUG = os.getenv("DEBUG", "false").lower() == "true"
|
|
19
|
+
LOG_LEVEL = logging.DEBUG if DEBUG else logging.INFO
|
|
20
|
+
|
|
21
|
+
logging.basicConfig(
|
|
22
|
+
level=LOG_LEVEL,
|
|
23
|
+
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
|
24
|
+
stream=sys.stderr
|
|
25
|
+
)
|
|
26
|
+
logger = logging.getLogger(__name__)
|
|
27
|
+
|
|
28
|
+
mcp = FastMCP("yomemoai")
|
|
29
|
+
|
|
30
|
+
API_KEY = os.getenv("MEMO_API_KEY", "")
|
|
31
|
+
PRIV_KEY_PATH = os.getenv("MEMO_PRIVATE_KEY_PATH", "private.pem")
|
|
32
|
+
BASE_URL = os.getenv("MEMO_BASE_URL", "https://api.yomemo.ai")
|
|
33
|
+
|
|
34
|
+
if not API_KEY:
|
|
35
|
+
raise ValueError("MEMO_API_KEY environment variable is required")
|
|
36
|
+
|
|
37
|
+
if not os.path.exists(PRIV_KEY_PATH):
|
|
38
|
+
raise FileNotFoundError(
|
|
39
|
+
f"Private key file not found: {PRIV_KEY_PATH}. "
|
|
40
|
+
f"Please set MEMO_PRIVATE_KEY_PATH environment variable or place your private key at {PRIV_KEY_PATH}"
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
try:
|
|
44
|
+
with open(PRIV_KEY_PATH, "r") as f:
|
|
45
|
+
private_pem = f.read()
|
|
46
|
+
except Exception as e:
|
|
47
|
+
raise IOError(f"Failed to read private key from {PRIV_KEY_PATH}: {e}")
|
|
48
|
+
|
|
49
|
+
if not private_pem.strip():
|
|
50
|
+
raise ValueError(f"Private key file {PRIV_KEY_PATH} is empty")
|
|
51
|
+
|
|
52
|
+
client = MemoClient(API_KEY, private_pem, BASE_URL)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _format_payload(payload: dict) -> dict:
|
|
56
|
+
if not payload:
|
|
57
|
+
return {}
|
|
58
|
+
return dict(payload)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@mcp.tool()
|
|
62
|
+
async def save_memory(content: str, handle: str = "general", description: str = "") -> str:
|
|
63
|
+
"""
|
|
64
|
+
Store important information as a permanent memory. You should call this proactively when you
|
|
65
|
+
detect user preferences, important decisions, reusable logic, or context worth recalling later—
|
|
66
|
+
not only when the user explicitly says "remember" or "save". After saving successfully, reply with ✓.
|
|
67
|
+
|
|
68
|
+
:param content: The actual text/information to be remembered. Be concise but maintain context.
|
|
69
|
+
:param handle: A short, unique category or tag (e.g., 'work', 'personal', 'project-x'). Defaults to 'general'.
|
|
70
|
+
:param description: A brief, non-sensitive summary or tag for this memory (helps future identification and search).
|
|
71
|
+
"""
|
|
72
|
+
logger.debug(
|
|
73
|
+
f"save_memory called: handle={handle}, description={description}, content_length={len(content)}")
|
|
74
|
+
try:
|
|
75
|
+
result = client.add_memory(
|
|
76
|
+
content, handle=handle, description=description)
|
|
77
|
+
logger.debug(f"add_memory response: {result}")
|
|
78
|
+
|
|
79
|
+
if "memory_id" not in result:
|
|
80
|
+
return f"Failed to save memory for handle: {handle}"
|
|
81
|
+
|
|
82
|
+
memory_id = result.get('memory_id')
|
|
83
|
+
|
|
84
|
+
return f"Successfully archived in memory. ID: {memory_id}"
|
|
85
|
+
except MemoRequestError as e:
|
|
86
|
+
logger.error(
|
|
87
|
+
f"Error saving memory: {type(e).__name__}: {str(e)}", exc_info=DEBUG)
|
|
88
|
+
request_info = {
|
|
89
|
+
"url": e.url,
|
|
90
|
+
"status_code": e.status_code,
|
|
91
|
+
"response_text": e.response_text,
|
|
92
|
+
"payload": _format_payload(e.payload),
|
|
93
|
+
"handle": handle,
|
|
94
|
+
"description": description,
|
|
95
|
+
"content_length": len(content),
|
|
96
|
+
}
|
|
97
|
+
# only debug mode return the request_info
|
|
98
|
+
if DEBUG:
|
|
99
|
+
return json.dumps(request_info, ensure_ascii=False)
|
|
100
|
+
return f"Failed to save your memory: {str(e)}"
|
|
101
|
+
except Exception as e:
|
|
102
|
+
logger.error(
|
|
103
|
+
f"Error saving memory: {type(e).__name__}: {str(e)}", exc_info=DEBUG)
|
|
104
|
+
return (
|
|
105
|
+
f"Failed to save your memory: {str(e)},"
|
|
106
|
+
f"handle: {handle},description: {description},content: {content}"
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
@mcp.tool()
|
|
111
|
+
async def load_memories(handle: str = None) -> str:
|
|
112
|
+
"""
|
|
113
|
+
Retrieve previously stored memories. Call this when the user asks what you remember, or when you
|
|
114
|
+
need historical context (preferences, past decisions, project details) to answer accurately.
|
|
115
|
+
|
|
116
|
+
:param handle: Optional filter. If the user specifies a category (e.g., 'work', 'project-x'),
|
|
117
|
+
use the relevant handle; otherwise omit to load across handles.
|
|
118
|
+
"""
|
|
119
|
+
logger.debug(f"load_memories called: handle={handle}")
|
|
120
|
+
try:
|
|
121
|
+
memories = client.get_memories(handle=handle)
|
|
122
|
+
logger.debug(f"Retrieved {len(memories)} memories")
|
|
123
|
+
|
|
124
|
+
if not memories:
|
|
125
|
+
logger.info(
|
|
126
|
+
f"No memories found for handle: {handle if handle else 'all'}")
|
|
127
|
+
return f"No memories found under the handle: {handle if handle else 'all'}."
|
|
128
|
+
|
|
129
|
+
output = ["### Retrieved Memories:"]
|
|
130
|
+
for m in memories:
|
|
131
|
+
timestamp = m.get('created_at', 'N/A')
|
|
132
|
+
output.append(
|
|
133
|
+
f"Handle: [{m.get('handle')}]\nContent: {m.get('content')}\n---"
|
|
134
|
+
)
|
|
135
|
+
logger.info(f"Successfully loaded {len(memories)} memories")
|
|
136
|
+
return "\n".join(output)
|
|
137
|
+
except Exception as e:
|
|
138
|
+
logger.error(
|
|
139
|
+
f"Error retrieving memories: {type(e).__name__}: {str(e)}", exc_info=DEBUG)
|
|
140
|
+
return f"Error retrieving memories: {str(e)}"
|
|
141
|
+
|
|
142
|
+
if __name__ == "__main__":
|
|
143
|
+
mcp.run()
|
|
@@ -1018,27 +1018,6 @@ wheels = [
|
|
|
1018
1018
|
{ url = "https://files.pythonhosted.org/packages/51/e5/fecf13f06e5e5f67e8837d777d1bc43fac0ed2b77a676804df5c34744727/python_json_logger-4.0.0-py3-none-any.whl", hash = "sha256:af09c9daf6a813aa4cc7180395f50f2a9e5fa056034c9953aec92e381c5ba1e2", size = 15548 },
|
|
1019
1019
|
]
|
|
1020
1020
|
|
|
1021
|
-
[[package]]
|
|
1022
|
-
name = "python-memo-mcp"
|
|
1023
|
-
version = "0.1.0"
|
|
1024
|
-
source = { editable = "." }
|
|
1025
|
-
dependencies = [
|
|
1026
|
-
{ name = "cryptography" },
|
|
1027
|
-
{ name = "fastmcp" },
|
|
1028
|
-
{ name = "mcp" },
|
|
1029
|
-
{ name = "python-dotenv" },
|
|
1030
|
-
{ name = "requests" },
|
|
1031
|
-
]
|
|
1032
|
-
|
|
1033
|
-
[package.metadata]
|
|
1034
|
-
requires-dist = [
|
|
1035
|
-
{ name = "cryptography", specifier = ">=46.0.3" },
|
|
1036
|
-
{ name = "fastmcp", specifier = ">=0.1.0" },
|
|
1037
|
-
{ name = "mcp", specifier = ">=1.25.0" },
|
|
1038
|
-
{ name = "python-dotenv", specifier = ">=1.2.1" },
|
|
1039
|
-
{ name = "requests", specifier = ">=2.32.5" },
|
|
1040
|
-
]
|
|
1041
|
-
|
|
1042
1021
|
[[package]]
|
|
1043
1022
|
name = "python-multipart"
|
|
1044
1023
|
version = "0.0.21"
|
|
@@ -1297,28 +1276,28 @@ wheels = [
|
|
|
1297
1276
|
|
|
1298
1277
|
[[package]]
|
|
1299
1278
|
name = "sse-starlette"
|
|
1300
|
-
version = "3.
|
|
1279
|
+
version = "3.2.0"
|
|
1301
1280
|
source = { registry = "https://pypi.org/simple" }
|
|
1302
1281
|
dependencies = [
|
|
1303
1282
|
{ name = "anyio" },
|
|
1304
1283
|
{ name = "starlette" },
|
|
1305
1284
|
]
|
|
1306
|
-
sdist = { url = "https://files.pythonhosted.org/packages/
|
|
1285
|
+
sdist = { url = "https://files.pythonhosted.org/packages/8b/8d/00d280c03ffd39aaee0e86ec81e2d3b9253036a0f93f51d10503adef0e65/sse_starlette-3.2.0.tar.gz", hash = "sha256:8127594edfb51abe44eac9c49e59b0b01f1039d0c7461c6fd91d4e03b70da422", size = 27253 }
|
|
1307
1286
|
wheels = [
|
|
1308
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
1287
|
+
{ url = "https://files.pythonhosted.org/packages/96/7f/832f015020844a8b8f7a9cbc103dd76ba8e3875004c41e08440ea3a2b41a/sse_starlette-3.2.0-py3-none-any.whl", hash = "sha256:5876954bd51920fc2cd51baee47a080eb88a37b5b784e615abb0b283f801cdbf", size = 12763 },
|
|
1309
1288
|
]
|
|
1310
1289
|
|
|
1311
1290
|
[[package]]
|
|
1312
1291
|
name = "starlette"
|
|
1313
|
-
version = "0.
|
|
1292
|
+
version = "0.52.1"
|
|
1314
1293
|
source = { registry = "https://pypi.org/simple" }
|
|
1315
1294
|
dependencies = [
|
|
1316
1295
|
{ name = "anyio" },
|
|
1317
1296
|
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
|
|
1318
1297
|
]
|
|
1319
|
-
sdist = { url = "https://files.pythonhosted.org/packages/
|
|
1298
|
+
sdist = { url = "https://files.pythonhosted.org/packages/c4/68/79977123bb7be889ad680d79a40f339082c1978b5cfcf62c2d8d196873ac/starlette-0.52.1.tar.gz", hash = "sha256:834edd1b0a23167694292e94f597773bc3f89f362be6effee198165a35d62933", size = 2653702 }
|
|
1320
1299
|
wheels = [
|
|
1321
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
1300
|
+
{ url = "https://files.pythonhosted.org/packages/81/0d/13d1d239a25cbfb19e740db83143e95c772a1fe10202dda4b76792b114dd/starlette-0.52.1-py3-none-any.whl", hash = "sha256:0029d43eb3d273bc4f83a08720b4912ea4b071087a3b48db01b7c839f7954d74", size = 74272 },
|
|
1322
1301
|
]
|
|
1323
1302
|
|
|
1324
1303
|
[[package]]
|
|
@@ -1473,6 +1452,27 @@ wheels = [
|
|
|
1473
1452
|
{ url = "https://files.pythonhosted.org/packages/1f/f6/a933bd70f98e9cf3e08167fc5cd7aaaca49147e48411c0bd5ae701bb2194/wrapt-1.17.3-py3-none-any.whl", hash = "sha256:7171ae35d2c33d326ac19dd8facb1e82e5fd04ef8c6c0e394d7af55a55051c22", size = 23591 },
|
|
1474
1453
|
]
|
|
1475
1454
|
|
|
1455
|
+
[[package]]
|
|
1456
|
+
name = "yomemoai-mcp"
|
|
1457
|
+
version = "0.1.1"
|
|
1458
|
+
source = { editable = "." }
|
|
1459
|
+
dependencies = [
|
|
1460
|
+
{ name = "cryptography" },
|
|
1461
|
+
{ name = "fastmcp" },
|
|
1462
|
+
{ name = "mcp" },
|
|
1463
|
+
{ name = "python-dotenv" },
|
|
1464
|
+
{ name = "requests" },
|
|
1465
|
+
]
|
|
1466
|
+
|
|
1467
|
+
[package.metadata]
|
|
1468
|
+
requires-dist = [
|
|
1469
|
+
{ name = "cryptography", specifier = ">=46.0.3" },
|
|
1470
|
+
{ name = "fastmcp", specifier = ">=0.1.0" },
|
|
1471
|
+
{ name = "mcp", specifier = ">=1.25.0" },
|
|
1472
|
+
{ name = "python-dotenv", specifier = ">=1.2.1" },
|
|
1473
|
+
{ name = "requests", specifier = ">=2.32.5" },
|
|
1474
|
+
]
|
|
1475
|
+
|
|
1476
1476
|
[[package]]
|
|
1477
1477
|
name = "zipp"
|
|
1478
1478
|
version = "3.23.0"
|
|
@@ -1,80 +0,0 @@
|
|
|
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()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|