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.
@@ -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,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.30.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ keep-agent-mem = server.cli:main
server/__init__.py ADDED
File without changes
server/__main__.py ADDED
@@ -0,0 +1,4 @@
1
+ from .cli import main
2
+
3
+ if __name__ == "__main__":
4
+ main()
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