google-keep-mcp 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,124 @@
1
+ Metadata-Version: 2.4
2
+ Name: google-keep-mcp
3
+ Version: 1.0.0
4
+ Summary: MCP Server for Google Keep
5
+ Project-URL: Homepage, https://github.com/davenicoll/google-keep-mcp
6
+ Project-URL: Repository, https://github.com/davenicoll/google-keep-mcp
7
+ Author-email: Dave Nicoll <d.nicoll@gmail.com>
8
+ License: MIT License
9
+
10
+ Copyright (c) 2025 Dave Nicoll
11
+
12
+ Permission is hereby granted, free of charge, to any person obtaining a copy
13
+ of this software and associated documentation files (the "Software"), to deal
14
+ in the Software without restriction, including without limitation the rights
15
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
16
+ copies of the Software, and to permit persons to whom the Software is
17
+ furnished to do so, subject to the following conditions:
18
+
19
+ The above copyright notice and this permission notice shall be included in all
20
+ copies or substantial portions of the Software.
21
+
22
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
23
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
24
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
25
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
26
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
27
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
28
+ SOFTWARE.
29
+ License-File: LICENSE
30
+ Classifier: Development Status :: 5 - Production/Stable
31
+ Classifier: Environment :: Console
32
+ Classifier: Intended Audience :: End Users/Desktop
33
+ Classifier: License :: OSI Approved :: MIT License
34
+ Classifier: Programming Language :: Python :: 3
35
+ Classifier: Programming Language :: Python :: 3.12
36
+ Classifier: Topic :: Utilities
37
+ Requires-Python: >=3.12.11
38
+ Requires-Dist: gkeepapi>=0.16.0
39
+ Requires-Dist: mcp[cli]
40
+ Requires-Dist: python-dotenv
41
+ Description-Content-Type: text/markdown
42
+
43
+ # google-keep-mcp
44
+
45
+ An MCP server for Google Keep. Apply the power of AI to organize, update, or just export your Keep notes.
46
+
47
+ ## Features
48
+
49
+ ### Search/List Operations
50
+
51
+ - `find` - Search for notes based on a query string
52
+ - `get_pinned_notes` - Get pinned notes (with optional search)
53
+ - `get_archived_notes` - Get archived notes (with optional search)
54
+ - `get_trashed_notes` - Get trashed notes (with optional search)
55
+
56
+ ### Add Operations
57
+
58
+ - `create_note` - Create a new note with title and text
59
+
60
+ ### Update Operations
61
+
62
+ - `update_note` - Update a note's title and text
63
+ - `set_note_color` - Set the color of a note (12 colors available)
64
+ - `archive_note` - Archive a note
65
+ - `unarchive_note` - Unarchive a note
66
+ - `restore_note` - Restore a trashed note back to an active note
67
+
68
+ ### Delete Operations
69
+
70
+ - `delete_note` - Mark a note for deletion
71
+ - `delete_archived_note` - Permanently delete an archived note
72
+
73
+ ### Utility Operations
74
+
75
+ - `get_labels` - Get all labels (with their usage count) for notes
76
+ - `get_note_colors` - Get available note colors and usage statistics
77
+
78
+ ## How to use
79
+
80
+ 1. Add the MCP server to your MCP servers:
81
+
82
+ ```json
83
+ "mcpServers": {
84
+ "google-keep-mcp": {
85
+ "command": "pipx",
86
+ "args": [
87
+ "run",
88
+ "google-keep-mcp"
89
+ ],
90
+ "env": {
91
+ "GOOGLE_EMAIL": "Your Google Email",
92
+ "GOOGLE_MASTER_TOKEN": "Your Google Master Token"
93
+ }
94
+ }
95
+ }
96
+ ```
97
+
98
+ 2. Add your credentials:
99
+
100
+ - `GOOGLE_EMAIL`: Your Google account email address
101
+ - `GOOGLE_MASTER_TOKEN`: Your Google account master token
102
+
103
+ Will you will need to create a master token (unless you have a Google Enterprise account, and can access the Google Keep API directly). Follow these steps:
104
+
105
+ - Goto https://myaccount.google.com/apppasswords and create an app password
106
+ - Run this docker command `docker run --rm -it breph/ha-google-home_get-token` and enter your gmail address and app password (make sure it doesn't have a space at the end)
107
+ - Obtain your master token. **Be careful with this token, it has access to everything in your Google account.**
108
+
109
+ > [!TIP]
110
+ > Stuck? Check https://github.com/leikoilja/ha-google-home/issues/890#issuecomment-2515002294, 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.
111
+
112
+ ## Publishing
113
+
114
+ To publish a new version to PyPI:
115
+
116
+ 1. Update the version in `pyproject.toml`
117
+ 2. Build the package:
118
+ ```bash
119
+ pipx run build
120
+ ```
121
+ 3. Upload to PyPI:
122
+ ```bash
123
+ pipx run twine upload --repository pypi dist/*
124
+ ```
@@ -0,0 +1,9 @@
1
+ src/server/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ src/server/__main__.py,sha256=0g3iknXOS9gZUcpL_trgAcuCJnZZKjdsT_xt61WOVb4,60
3
+ src/server/cli.py,sha256=S6eyBZbBWFraIGTk_H9kCUcMuQMB8rRBpxns49BP-yo,14207
4
+ src/server/keep_api.py,sha256=MfgZs2kPkQ10y8f9mrGW8a_LvXkthPfQ_ver4nPQWcI,1516
5
+ google_keep_mcp-1.0.0.dist-info/METADATA,sha256=cG6zX8QLe1C8k3PDCt-oBfwUJlNm-Y3LGqq0WMJC2us,4586
6
+ google_keep_mcp-1.0.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
7
+ google_keep_mcp-1.0.0.dist-info/entry_points.txt,sha256=3SW_xRdbwvf3sSmF5eI5QCFO3l1-R_VH-bK2o_2xaU0,44
8
+ google_keep_mcp-1.0.0.dist-info/licenses/LICENSE,sha256=R8uMIg44aHeMZXk_DhENH1WS0dLOzoEe75uHdi80cI0,1068
9
+ google_keep_mcp-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.27.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ mcp = src.server.cli:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Dave Nicoll
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.
src/server/__init__.py ADDED
File without changes
src/server/__main__.py ADDED
@@ -0,0 +1,4 @@
1
+ from .cli import main
2
+
3
+ if __name__ == "__main__":
4
+ main()
src/server/cli.py ADDED
@@ -0,0 +1,485 @@
1
+ """
2
+ MCP plugin for Google Keep integration.
3
+ Provides tools for interacting with Google Keep notes through MCP.
4
+ """
5
+
6
+ import json
7
+ from mcp.server.fastmcp import FastMCP
8
+ from .keep_api import get_client, serialize_note
9
+
10
+ mcp = FastMCP("keep")
11
+
12
+ # Search/List Operations
13
+
14
+ @mcp.tool()
15
+ def find(query="", include_archived: bool = True, include_trashed: bool = False) -> str:
16
+ """
17
+ Find notes based on a search query.
18
+
19
+ Args:
20
+ query (str, optional): A string to match against the title and text
21
+ include_archived (bool, optional): Include archived notes in results (default: True)
22
+ include_trashed (bool, optional): Include trashed notes in results (default: False)
23
+
24
+ Returns:
25
+ str: JSON string containing the matching notes with their id, title, text, pinned status, color and labels
26
+ """
27
+ keep = get_client()
28
+
29
+ if include_archived and include_trashed:
30
+ # Get all notes
31
+ notes = keep.find(query=query)
32
+ elif include_archived:
33
+ # Get active + archived notes (exclude trashed)
34
+ active_notes = list(keep.find(query=query, archived=False, trashed=False))
35
+ archived_notes = list(keep.find(query=query, archived=True, trashed=False))
36
+ notes = active_notes + archived_notes
37
+ elif include_trashed:
38
+ # Get active + trashed notes (exclude archived)
39
+ active_notes = list(keep.find(query=query, archived=False, trashed=False))
40
+ trashed_notes = list(keep.find(query=query, archived=False, trashed=True))
41
+ notes = active_notes + trashed_notes
42
+ else:
43
+ # Get only active notes
44
+ notes = keep.find(query=query, archived=False, trashed=False)
45
+
46
+ notes_data = [serialize_note(note) for note in notes]
47
+ return json.dumps(notes_data)
48
+
49
+ @mcp.tool()
50
+ def get_pinned_notes(query="") -> str:
51
+ """
52
+ Get only pinned notes (active, non-archived, non-trashed).
53
+
54
+ Args:
55
+ query (str, optional): A string to match against the title and text
56
+
57
+ Returns:
58
+ str: JSON string containing pinned notes
59
+ """
60
+ keep = get_client()
61
+ notes = keep.find(query=query, archived=False, trashed=False)
62
+
63
+ # Filter for only pinned notes
64
+ pinned_notes = [note for note in notes if note.pinned]
65
+
66
+ notes_data = [serialize_note(note) for note in pinned_notes]
67
+ return json.dumps(notes_data)
68
+
69
+ @mcp.tool()
70
+ def get_archived_notes(query="") -> str:
71
+ """
72
+ Get only archived notes.
73
+
74
+ Args:
75
+ query (str, optional): A string to match against the title and text
76
+
77
+ Returns:
78
+ str: JSON string containing archived notes
79
+ """
80
+ keep = get_client()
81
+ notes = keep.find(query=query, archived=True, trashed=False)
82
+
83
+ notes_data = [serialize_note(note) for note in notes]
84
+ return json.dumps(notes_data)
85
+
86
+ @mcp.tool()
87
+ def get_trashed_notes(query="") -> str:
88
+ """
89
+ Get only trashed notes.
90
+
91
+ Args:
92
+ query (str, optional): A string to match against the title and text
93
+
94
+ Returns:
95
+ str: JSON string containing trashed notes
96
+ """
97
+ keep = get_client()
98
+ notes = keep.find(query=query, archived=False, trashed=True)
99
+
100
+ notes_data = [serialize_note(note) for note in notes]
101
+ return json.dumps(notes_data)
102
+
103
+ # Add Operations
104
+
105
+ @mcp.tool()
106
+ def create_note(title: str = None, text: str = None) -> str:
107
+ """
108
+ Create a new note with title and text.
109
+
110
+ Args:
111
+ title (str, optional): The title of the note
112
+ text (str, optional): The content of the note
113
+
114
+ Returns:
115
+ str: JSON string containing the created note's data
116
+ """
117
+ keep = get_client()
118
+ note = keep.createNote(title=title, text=text)
119
+
120
+ keep.sync() # Ensure the note is created and labeled on the server
121
+
122
+ return json.dumps(serialize_note(note))
123
+
124
+ # Update Operations
125
+
126
+ @mcp.tool()
127
+ def update_note(note_id: str, title: str = None, text: str = None) -> str:
128
+ """
129
+ Update a note's properties.
130
+
131
+ Args:
132
+ note_id (str): The ID of the note to update
133
+ title (str, optional): New title for the note
134
+ text (str, optional): New text content for the note
135
+
136
+ Returns:
137
+ str: JSON string containing the updated note's data
138
+
139
+ Raises:
140
+ ValueError: If the note doesn't exist or cannot be modified
141
+ """
142
+ keep = get_client()
143
+ note = keep.get(note_id)
144
+
145
+ if not note:
146
+ raise ValueError(f"Note with ID {note_id} not found")
147
+
148
+
149
+ if title is not None:
150
+ note.title = title
151
+ if text is not None:
152
+ note.text = text
153
+
154
+ keep.sync() # Ensure changes are saved to the server
155
+ return json.dumps(serialize_note(note))
156
+
157
+ @mcp.tool()
158
+ def set_note_color(note_id: str, color: str) -> str:
159
+ """
160
+ Set the color of a note.
161
+
162
+ Args:
163
+ note_id (str): The ID of the note to update
164
+ color (str): The color to set. Valid values: WHITE, RED, ORANGE, YELLOW, GREEN, TEAL, BLUE, CERULEAN, PURPLE, PINK, BROWN, GRAY
165
+
166
+ Returns:
167
+ str: Success message
168
+
169
+ Raises:
170
+ ValueError: If the note doesn't exist, color is invalid, or note cannot be modified
171
+ """
172
+ keep = get_client()
173
+ note = keep.get(note_id)
174
+
175
+ if not note:
176
+ raise ValueError(f"Note with ID {note_id} not found")
177
+
178
+
179
+ # Get the ColorValue enum from the note
180
+ ColorValue = type(note.color)
181
+
182
+ # Map string colors to enum values
183
+ color_mapping = {
184
+ 'WHITE': ColorValue.White,
185
+ 'RED': ColorValue.Red,
186
+ 'ORANGE': ColorValue.Orange,
187
+ 'YELLOW': ColorValue.Yellow,
188
+ 'GREEN': ColorValue.Green,
189
+ 'TEAL': ColorValue.Teal,
190
+ 'BLUE': ColorValue.Blue,
191
+ 'CERULEAN': ColorValue.DarkBlue,
192
+ 'PURPLE': ColorValue.Purple,
193
+ 'PINK': ColorValue.Pink,
194
+ 'BROWN': ColorValue.Brown,
195
+ 'GRAY': ColorValue.Gray
196
+ }
197
+
198
+ color_upper = color.upper()
199
+ if color_upper not in color_mapping:
200
+ valid_colors = ', '.join(color_mapping.keys())
201
+ raise ValueError(f"Invalid color '{color}'. Valid colors: {valid_colors}")
202
+
203
+ old_color = note.color.name
204
+ note.color = color_mapping[color_upper]
205
+ keep.sync()
206
+
207
+ return json.dumps({"message": f"Note {note_id} color changed from {old_color} to {note.color.name}"})
208
+
209
+ @mcp.tool()
210
+ def archive_note(note_id: str) -> str:
211
+ """
212
+ Archive an active note.
213
+
214
+ Args:
215
+ note_id (str): The ID of the note to archive
216
+
217
+ Returns:
218
+ str: Success message
219
+
220
+ Raises:
221
+ ValueError: If the note doesn't exist, is already archived, or cannot be modified
222
+ """
223
+ keep = get_client()
224
+ note = keep.get(note_id)
225
+
226
+ if not note:
227
+ raise ValueError(f"Note with ID {note_id} not found")
228
+
229
+ if note.archived:
230
+ raise ValueError(f"Note with ID {note_id} is already archived")
231
+
232
+ if note.trashed:
233
+ raise ValueError(f"Note with ID {note_id} is in trash and cannot be archived")
234
+
235
+ note.archived = True
236
+ keep.sync()
237
+ return json.dumps({"message": f"Note {note_id} archived successfully"})
238
+
239
+ @mcp.tool()
240
+ def unarchive_note(note_id: str) -> str:
241
+ """
242
+ Unarchive a note (move it back to active notes).
243
+
244
+ Args:
245
+ note_id (str): The ID of the archived note to unarchive
246
+
247
+ Returns:
248
+ str: Success message
249
+
250
+ Raises:
251
+ ValueError: If the note doesn't exist, is not archived, or cannot be modified
252
+ """
253
+ keep = get_client()
254
+ note = keep.get(note_id)
255
+
256
+ if not note:
257
+ raise ValueError(f"Note with ID {note_id} not found")
258
+
259
+ if not note.archived:
260
+ raise ValueError(f"Note with ID {note_id} is not archived")
261
+
262
+
263
+ note.archived = False
264
+ keep.sync()
265
+ return json.dumps({"message": f"Note {note_id} unarchived successfully"})
266
+
267
+ @mcp.tool()
268
+ def restore_note(note_id: str) -> str:
269
+ """
270
+ Restore a trashed note back to active notes.
271
+
272
+ Args:
273
+ note_id (str): The ID of the trashed note to restore
274
+
275
+ Returns:
276
+ str: Success message
277
+
278
+ Raises:
279
+ ValueError: If the note doesn't exist, is not trashed, or cannot be modified
280
+ """
281
+ keep = get_client()
282
+ note = keep.get(note_id)
283
+
284
+ if not note:
285
+ raise ValueError(f"Note with ID {note_id} not found")
286
+
287
+ if not note.trashed:
288
+ raise ValueError(f"Note with ID {note_id} is not in trash")
289
+
290
+
291
+ note.untrash()
292
+ keep.sync()
293
+ return json.dumps({"message": f"Note {note_id} restored from trash"})
294
+
295
+ # Delete Operations
296
+
297
+ @mcp.tool()
298
+ def delete_note(note_id: str) -> str:
299
+ """
300
+ Delete a note (mark for deletion).
301
+
302
+ Args:
303
+ note_id (str): The ID of the note to delete
304
+
305
+ Returns:
306
+ str: Success message
307
+
308
+ Raises:
309
+ ValueError: If the note doesn't exist or cannot be modified
310
+ """
311
+ keep = get_client()
312
+ note = keep.get(note_id)
313
+
314
+ if not note:
315
+ raise ValueError(f"Note with ID {note_id} not found")
316
+
317
+
318
+ note.delete()
319
+ keep.sync() # Ensure deletion is saved to the server
320
+ return json.dumps({"message": f"Note {note_id} marked for deletion"})
321
+
322
+ @mcp.tool()
323
+ def delete_archived_note(note_id: str) -> str:
324
+ """
325
+ Permanently delete an archived note (moves to trash).
326
+
327
+ Args:
328
+ note_id (str): The ID of the archived note to delete
329
+
330
+ Returns:
331
+ str: Success message
332
+
333
+ Raises:
334
+ ValueError: If the note doesn't exist, is not archived, or cannot be modified
335
+ """
336
+ keep = get_client()
337
+ note = keep.get(note_id)
338
+
339
+ if not note:
340
+ raise ValueError(f"Note with ID {note_id} not found")
341
+
342
+ if not note.archived:
343
+ raise ValueError(f"Note with ID {note_id} is not archived")
344
+
345
+
346
+ note.delete()
347
+ keep.sync()
348
+ return json.dumps({"message": f"Archived note {note_id} moved to trash"})
349
+
350
+ # Utility Operations
351
+
352
+ @mcp.tool()
353
+ def get_labels(include_archived: bool = True, include_trashed: bool = False) -> str:
354
+ """
355
+ Analyze all labels across notes and return counts for label management.
356
+
357
+ Args:
358
+ include_archived (bool, optional): Include archived notes in analysis (default: True)
359
+ include_trashed (bool, optional): Include trashed notes in analysis (default: False)
360
+
361
+ Returns:
362
+ str: JSON string containing label analysis with counts, sorted by usage
363
+ """
364
+ keep = get_client()
365
+
366
+ # Get all notes based on include parameters
367
+ if include_archived and include_trashed:
368
+ # Get all notes
369
+ notes = keep.find()
370
+ elif include_archived:
371
+ # Get active + archived notes (exclude trashed)
372
+ active_notes = list(keep.find(archived=False, trashed=False))
373
+ archived_notes = list(keep.find(archived=True, trashed=False))
374
+ notes = active_notes + archived_notes
375
+ elif include_trashed:
376
+ # Get active + trashed notes (exclude archived)
377
+ active_notes = list(keep.find(archived=False, trashed=False))
378
+ trashed_notes = list(keep.find(archived=False, trashed=True))
379
+ notes = active_notes + trashed_notes
380
+ else:
381
+ # Get only active notes
382
+ notes = keep.find(archived=False, trashed=False)
383
+
384
+ # Convert to list if it's a generator
385
+ if hasattr(notes, '__iter__') and not isinstance(notes, list):
386
+ notes = list(notes)
387
+
388
+ # Count labels
389
+ label_counts = {}
390
+ total_notes = len(notes)
391
+ notes_with_labels = 0
392
+ notes_without_labels = 0
393
+
394
+ for note in notes:
395
+ note_labels = list(note.labels.all())
396
+
397
+ if note_labels:
398
+ notes_with_labels += 1
399
+ for label in note_labels:
400
+ label_name = label.name
401
+ label_id = label.id
402
+
403
+ if label_name not in label_counts:
404
+ label_counts[label_name] = {
405
+ 'id': label_id,
406
+ 'name': label_name,
407
+ 'count': 0,
408
+ 'percentage': 0.0
409
+ }
410
+
411
+ label_counts[label_name]['count'] += 1
412
+ else:
413
+ notes_without_labels += 1
414
+
415
+ # Calculate percentages and sort by count
416
+ for label_data in label_counts.values():
417
+ label_data['percentage'] = round((label_data['count'] / total_notes) * 100, 2)
418
+
419
+ # Sort labels by count (descending)
420
+ sorted_labels = sorted(label_counts.values(), key=lambda x: x['count'], reverse=True)
421
+
422
+ # Prepare summary
423
+ analysis = {
424
+ 'summary': {
425
+ 'total_notes_analyzed': total_notes,
426
+ 'notes_with_labels': notes_with_labels,
427
+ 'notes_without_labels': notes_without_labels,
428
+ 'total_unique_labels': len(label_counts),
429
+ 'label_coverage_percentage': round((notes_with_labels / total_notes) * 100, 2) if total_notes > 0 else 0
430
+ },
431
+ 'labels': sorted_labels
432
+ }
433
+
434
+ return json.dumps(analysis)
435
+
436
+ @mcp.tool()
437
+ def get_note_colors() -> str:
438
+ """
439
+ Get available note colors and their current usage statistics.
440
+
441
+ Returns:
442
+ str: JSON string containing available colors and usage statistics
443
+ """
444
+ keep = get_client()
445
+
446
+ # Get a sample note to access ColorValue enum
447
+ notes = list(keep.find())
448
+ if not notes:
449
+ return json.dumps({"available_colors": [], "usage": {}})
450
+
451
+ ColorValue = type(notes[0].color)
452
+
453
+ # Get all available colors
454
+ available_colors = []
455
+ for color in ColorValue:
456
+ available_colors.append({
457
+ 'name': color.name,
458
+ 'value': color.value,
459
+ 'api_name': color.name.upper()
460
+ })
461
+
462
+ # Get current color usage from all notes
463
+ result = find()
464
+ all_notes = json.loads(result)
465
+
466
+ color_usage = {}
467
+ for note in all_notes:
468
+ color = note.get('color', 'DEFAULT')
469
+ if color not in color_usage:
470
+ color_usage[color] = 0
471
+ color_usage[color] += 1
472
+
473
+ return json.dumps({
474
+ "available_colors": available_colors,
475
+ "usage_statistics": color_usage,
476
+ "total_notes": len(all_notes)
477
+ })
478
+
479
+ def main():
480
+ mcp.run(transport='stdio')
481
+
482
+
483
+ if __name__ == "__main__":
484
+ main()
485
+
src/server/keep_api.py ADDED
@@ -0,0 +1,60 @@
1
+ import gkeepapi
2
+ import os
3
+ from dotenv import load_dotenv
4
+
5
+ _keep_client = None
6
+
7
+ def get_client():
8
+ """
9
+ Get or initialize the Google Keep client.
10
+ This ensures we only authenticate once and reuse the client.
11
+
12
+ Returns:
13
+ gkeepapi.Keep: Authenticated Keep client
14
+ """
15
+ global _keep_client
16
+
17
+ if _keep_client is not None:
18
+ return _keep_client
19
+
20
+ # Load environment variables
21
+ load_dotenv()
22
+
23
+ # Get credentials from environment variables
24
+ email = os.getenv('GOOGLE_EMAIL')
25
+ master_token = os.getenv('GOOGLE_MASTER_TOKEN')
26
+
27
+ if not email or not master_token:
28
+ raise ValueError("Missing Google Keep credentials. Please set GOOGLE_EMAIL and GOOGLE_MASTER_TOKEN environment variables.")
29
+
30
+ # Initialize the Keep API
31
+ keep = gkeepapi.Keep()
32
+
33
+ # Authenticate
34
+ keep.authenticate(email, master_token)
35
+
36
+ # Store the client for reuse
37
+ _keep_client = keep
38
+
39
+ return keep
40
+
41
+ def serialize_note(note):
42
+ """
43
+ Serialize a Google Keep note into a dictionary.
44
+
45
+ Args:
46
+ note: A Google Keep note object
47
+
48
+ Returns:
49
+ dict: A dictionary containing the note's id, title, text, pinned status, color and labels
50
+ """
51
+ return {
52
+ 'id': note.id,
53
+ 'title': note.title,
54
+ 'text': note.text,
55
+ 'pinned': note.pinned,
56
+ 'color': note.color.value if note.color else None,
57
+ 'labels': [{'id': label.id, 'name': label.name} for label in note.labels.all()]
58
+ }
59
+
60
+