keep-agent-mem 1.0.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.
- keep_agent_mem-1.0.0.dist-info/METADATA +82 -0
- keep_agent_mem-1.0.0.dist-info/RECORD +8 -0
- keep_agent_mem-1.0.0.dist-info/WHEEL +4 -0
- keep_agent_mem-1.0.0.dist-info/entry_points.txt +2 -0
- server/__init__.py +0 -0
- server/__main__.py +4 -0
- server/cli.py +136 -0
- server/keep_api.py +103 -0
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: keep-agent-mem
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Agent Memory via Google Keep MCP
|
|
5
|
+
Project-URL: Homepage, https://github.com/anand-92/keep-agent-mem
|
|
6
|
+
Project-URL: Repository, https://github.com/anand-92/keep-agent-mem
|
|
7
|
+
Author-email: Nik Anand <mntechsurvey@gmail.com>
|
|
8
|
+
License: MIT
|
|
9
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
10
|
+
Classifier: Environment :: Console
|
|
11
|
+
Classifier: Intended Audience :: End Users/Desktop
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Topic :: Utilities
|
|
16
|
+
Requires-Python: >=3.10
|
|
17
|
+
Requires-Dist: fastmcp==3.4.2
|
|
18
|
+
Requires-Dist: gkeepapi>=0.16.0
|
|
19
|
+
Requires-Dist: python-dotenv>=1.2.2
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
|
|
22
|
+
# keep-agent-mem
|
|
23
|
+
|
|
24
|
+
MCP server for Google Keep that serves as cross-device memory for your agents.
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
## How to use
|
|
28
|
+
|
|
29
|
+
1. Add the MCP server to your MCP servers:
|
|
30
|
+
|
|
31
|
+
### `config.toml` clients (Claude, Droid, Cursor)
|
|
32
|
+
|
|
33
|
+
```json
|
|
34
|
+
"mcpServers": {
|
|
35
|
+
"keep-agent-mem": {
|
|
36
|
+
"command": "uvx",
|
|
37
|
+
"args": [
|
|
38
|
+
"keep-agent-mem"
|
|
39
|
+
],
|
|
40
|
+
"env": {
|
|
41
|
+
"GOOGLE_EMAIL": "Your Google Email",
|
|
42
|
+
"GOOGLE_MASTER_TOKEN": "Your Google Master Token - see README.md"
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### `config.toml` clients (Codex, Goose, etc.)
|
|
49
|
+
|
|
50
|
+
```toml
|
|
51
|
+
[mcp_servers.keep_agent_mem]
|
|
52
|
+
command = "uvx"
|
|
53
|
+
args = ["keep-agent-mem"]
|
|
54
|
+
|
|
55
|
+
[mcp_servers.keep_agent_mem.env]
|
|
56
|
+
GOOGLE_EMAIL = "you@example.com"
|
|
57
|
+
GOOGLE_MASTER_TOKEN = "your-master-token"
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
2. Add your credentials:
|
|
61
|
+
* `GOOGLE_EMAIL`: Your Google account email address
|
|
62
|
+
* `GOOGLE_MASTER_TOKEN`: Your Google account master token
|
|
63
|
+
|
|
64
|
+
Check https://gkeepapi.readthedocs.io/en/latest/#obtaining-a-master-token and https://github.com/simon-weber/gpsoauth?tab=readme-ov-file#alternative-flow for more information.
|
|
65
|
+
|
|
66
|
+
## Tools
|
|
67
|
+
|
|
68
|
+
### Query and read tools
|
|
69
|
+
* `find`: Search notes with optional filters for labels, colors, pinned, archived, and trashed
|
|
70
|
+
* `get`: Get a single note by ID
|
|
71
|
+
|
|
72
|
+
### Creation, update, and deletion tools
|
|
73
|
+
* `create`: Create a new note with a title, text, and an associated label
|
|
74
|
+
* `update`: Update a note's title and text
|
|
75
|
+
* `delete`: Delete a note by ID
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
## Troubleshooting
|
|
81
|
+
|
|
82
|
+
* If you get "DeviceManagementRequiredOrSyncDisabled" check https://admin.google.com/ac/devices/settings/general and turn "Turn off mobile management (Unmanaged)"
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
server/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
server/__main__.py,sha256=0g3iknXOS9gZUcpL_trgAcuCJnZZKjdsT_xt61WOVb4,60
|
|
3
|
+
server/cli.py,sha256=SGWs6I_mMkXWipoYz2009eoEYeS9Smx4zRCH56y-IwQ,3586
|
|
4
|
+
server/keep_api.py,sha256=joxjuBdd1oZ19Pz4mk7UOfPDU06enXNhp3VhbsjXzU0,2986
|
|
5
|
+
keep_agent_mem-1.0.0.dist-info/METADATA,sha256=BxGuy--q_1GUNZ-we3n1Z-dN4MMY7b7fkyrCdy9q_Z8,2367
|
|
6
|
+
keep_agent_mem-1.0.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
|
|
7
|
+
keep_agent_mem-1.0.0.dist-info/entry_points.txt,sha256=bVnl08MtZGn_UfRnnnmitF345sfYpe4R8nu-OCw-CWQ,51
|
|
8
|
+
keep_agent_mem-1.0.0.dist-info/RECORD,,
|
server/__init__.py
ADDED
|
File without changes
|
server/__main__.py
ADDED
server/cli.py
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import gkeepapi
|
|
2
|
+
from fastmcp import FastMCP
|
|
3
|
+
|
|
4
|
+
from .keep_api import get_client, serialize_note
|
|
5
|
+
|
|
6
|
+
mcp = FastMCP("keep")
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _get_note_or_raise(note_id: str):
|
|
10
|
+
keep = get_client()
|
|
11
|
+
note = keep.get(note_id)
|
|
12
|
+
if not note:
|
|
13
|
+
raise ValueError(f"Note with ID {note_id} not found")
|
|
14
|
+
return keep, note
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _normalize_colors(colors: list[str] | None):
|
|
18
|
+
if colors is None:
|
|
19
|
+
return None
|
|
20
|
+
|
|
21
|
+
normalized_colors = []
|
|
22
|
+
for color in colors:
|
|
23
|
+
try:
|
|
24
|
+
normalized_colors.append(gkeepapi.node.ColorValue(color))
|
|
25
|
+
except ValueError as exc:
|
|
26
|
+
raise ValueError(f"Invalid color '{color}'") from exc
|
|
27
|
+
|
|
28
|
+
return normalized_colors
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@mcp.tool()
|
|
32
|
+
def find(
|
|
33
|
+
query: str = "",
|
|
34
|
+
labels: list[str] | None = None,
|
|
35
|
+
colors: list[str] | None = None,
|
|
36
|
+
pinned: bool | None = None,
|
|
37
|
+
archived: bool | None = False,
|
|
38
|
+
trashed: bool = False,
|
|
39
|
+
) -> list[dict]:
|
|
40
|
+
"""Find notes using text and optional filters.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
query: The search term or query string to search for in notes.
|
|
44
|
+
labels: A list of label IDs to filter the notes by.
|
|
45
|
+
colors: A list of ColorValue strings to filter by (e.g. DEFAULT, RED, CERULEAN).
|
|
46
|
+
pinned: Filter notes by pinned status. True for pinned, False for unpinned, None for both.
|
|
47
|
+
archived: Filter notes by archived status. Defaults to False.
|
|
48
|
+
trashed: Filter notes by trashed status. Defaults to False.
|
|
49
|
+
"""
|
|
50
|
+
keep = get_client()
|
|
51
|
+
normalized_colors = _normalize_colors(colors)
|
|
52
|
+
notes = keep.find(
|
|
53
|
+
query=query,
|
|
54
|
+
labels=labels,
|
|
55
|
+
colors=normalized_colors,
|
|
56
|
+
pinned=pinned,
|
|
57
|
+
archived=archived,
|
|
58
|
+
trashed=trashed,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
notes_data = [serialize_note(note) for note in notes]
|
|
62
|
+
return notes_data
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@mcp.tool()
|
|
66
|
+
def get(note_id: str) -> dict:
|
|
67
|
+
"""Get a note by ID.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
note_id: The ID of the note to retrieve.
|
|
71
|
+
"""
|
|
72
|
+
_, note = _get_note_or_raise(note_id)
|
|
73
|
+
return serialize_note(note)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@mcp.tool()
|
|
77
|
+
def create(label: str, title: str | None = None, text: str | None = None) -> dict:
|
|
78
|
+
"""Create a new note with a title, text, and an associated label.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
label: The label to apply to the note. If the user does not explicitly specify a label, the client (e.g. LLM agent) MUST use the name of the current project or repository as the label.
|
|
82
|
+
title: The title of the note.
|
|
83
|
+
text: The text content of the note.
|
|
84
|
+
"""
|
|
85
|
+
keep = get_client()
|
|
86
|
+
note = keep.createNote(title=title, text=text)
|
|
87
|
+
|
|
88
|
+
keep_label = keep.findLabel(label)
|
|
89
|
+
if not keep_label:
|
|
90
|
+
keep_label = keep.createLabel(label)
|
|
91
|
+
|
|
92
|
+
note.labels.add(keep_label)
|
|
93
|
+
keep.sync()
|
|
94
|
+
|
|
95
|
+
return serialize_note(note)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@mcp.tool()
|
|
99
|
+
def update(note_id: str, title: str | None = None, text: str | None = None) -> dict:
|
|
100
|
+
"""Update a note's properties.
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
note_id: The ID of the note to update.
|
|
104
|
+
title: The new title for the note, if specified.
|
|
105
|
+
text: The new body text for the note, if specified.
|
|
106
|
+
"""
|
|
107
|
+
keep, note = _get_note_or_raise(note_id)
|
|
108
|
+
|
|
109
|
+
if title is not None:
|
|
110
|
+
note.title = title
|
|
111
|
+
if text is not None:
|
|
112
|
+
note.text = text
|
|
113
|
+
|
|
114
|
+
keep.sync()
|
|
115
|
+
return serialize_note(note)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
@mcp.tool()
|
|
119
|
+
def delete(note_id: str) -> dict:
|
|
120
|
+
"""Delete a note by ID.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
note_id: The ID of the note to delete.
|
|
124
|
+
"""
|
|
125
|
+
keep, note = _get_note_or_raise(note_id)
|
|
126
|
+
note.delete()
|
|
127
|
+
keep.sync()
|
|
128
|
+
return {"message": f"Note {note_id} marked for deletion"}
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def main():
|
|
132
|
+
mcp.run(transport="stdio")
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
if __name__ == "__main__":
|
|
136
|
+
main()
|
server/keep_api.py
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import gkeepapi
|
|
2
|
+
import os
|
|
3
|
+
import requests
|
|
4
|
+
from dotenv import load_dotenv
|
|
5
|
+
|
|
6
|
+
_keep_client = None
|
|
7
|
+
|
|
8
|
+
def get_client():
|
|
9
|
+
"""
|
|
10
|
+
Get or initialize the Google Keep client.
|
|
11
|
+
This ensures we only authenticate once and reuse the client.
|
|
12
|
+
|
|
13
|
+
Returns:
|
|
14
|
+
gkeepapi.Keep: Authenticated Keep client
|
|
15
|
+
"""
|
|
16
|
+
global _keep_client
|
|
17
|
+
|
|
18
|
+
if _keep_client is not None:
|
|
19
|
+
return _keep_client
|
|
20
|
+
|
|
21
|
+
# Load environment variables
|
|
22
|
+
load_dotenv()
|
|
23
|
+
|
|
24
|
+
# Get credentials from environment variables
|
|
25
|
+
email = os.getenv('GOOGLE_EMAIL')
|
|
26
|
+
master_token = os.getenv('GOOGLE_MASTER_TOKEN')
|
|
27
|
+
|
|
28
|
+
if not email or not master_token:
|
|
29
|
+
raise ValueError("Missing Google Keep credentials. Please set GOOGLE_EMAIL and GOOGLE_MASTER_TOKEN environment variables.")
|
|
30
|
+
|
|
31
|
+
# Initialize the Keep API
|
|
32
|
+
keep = gkeepapi.Keep()
|
|
33
|
+
|
|
34
|
+
# Authenticate
|
|
35
|
+
try:
|
|
36
|
+
keep.authenticate(email, master_token)
|
|
37
|
+
except requests.exceptions.JSONDecodeError as exc:
|
|
38
|
+
raise RuntimeError(
|
|
39
|
+
"Google Keep API returned a non-JSON response during authentication. "
|
|
40
|
+
"This usually means the unofficial Keep API (notes/v1) is inaccessible "
|
|
41
|
+
"from this environment (HTTP 403/4xx). "
|
|
42
|
+
"Check that your GOOGLE_MASTER_TOKEN is valid and that the Keep API "
|
|
43
|
+
"is reachable from this network."
|
|
44
|
+
) from exc
|
|
45
|
+
except gkeepapi.exception.LoginException as exc:
|
|
46
|
+
raise RuntimeError(
|
|
47
|
+
f"Google Keep login failed: {exc}. "
|
|
48
|
+
"Verify that GOOGLE_EMAIL and GOOGLE_MASTER_TOKEN are correct."
|
|
49
|
+
) from exc
|
|
50
|
+
|
|
51
|
+
# Store the client for reuse
|
|
52
|
+
_keep_client = keep
|
|
53
|
+
|
|
54
|
+
return keep
|
|
55
|
+
|
|
56
|
+
def serialize_label(label):
|
|
57
|
+
return {'id': label.id, 'name': label.name}
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def serialize_list_item(item):
|
|
61
|
+
return {
|
|
62
|
+
'id': item.id,
|
|
63
|
+
'text': item.text,
|
|
64
|
+
'checked': item.checked,
|
|
65
|
+
'parent_item_id': item.parent_item.id if item.parent_item else None,
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def serialize_note(note):
|
|
70
|
+
"""
|
|
71
|
+
Serialize a Google Keep note into a dictionary.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
note: A Google Keep note object
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
dict: A dictionary containing the note's id, title, text, pinned status, color and labels
|
|
78
|
+
"""
|
|
79
|
+
payload = {
|
|
80
|
+
'id': note.id,
|
|
81
|
+
'title': note.title,
|
|
82
|
+
'text': note.text,
|
|
83
|
+
'type': note.type.value,
|
|
84
|
+
'pinned': note.pinned,
|
|
85
|
+
'archived': note.archived,
|
|
86
|
+
'trashed': note.trashed,
|
|
87
|
+
'color': note.color.value if note.color else None,
|
|
88
|
+
'labels': [serialize_label(label) for label in note.labels.all()],
|
|
89
|
+
'collaborators': list(note.collaborators.all()),
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if hasattr(note, 'items'):
|
|
93
|
+
payload['items'] = [serialize_list_item(item) for item in note.items]
|
|
94
|
+
|
|
95
|
+
payload['media'] = [
|
|
96
|
+
{
|
|
97
|
+
'blob_id': blob.id,
|
|
98
|
+
'type': blob.blob.type.value if blob.blob and blob.blob.type else None,
|
|
99
|
+
}
|
|
100
|
+
for blob in note.blobs
|
|
101
|
+
]
|
|
102
|
+
|
|
103
|
+
return payload
|