google-keep-notes-mcp 0.1.2__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,141 @@
1
+ Metadata-Version: 2.4
2
+ Name: google-keep-notes-mcp
3
+ Version: 0.1.2
4
+ Summary: MCP server for Google Keep
5
+ Requires-Python: >=3.10
6
+ Description-Content-Type: text/markdown
7
+ License-File: LICENSE
8
+ Requires-Dist: gkeepapi>=0.17.1
9
+ Requires-Dist: google-api-python-client>=2.198.0
10
+ Requires-Dist: google-auth-oauthlib>=1.4.0
11
+ Requires-Dist: markdown>=3.10.2
12
+ Requires-Dist: mcp>=1.28.1
13
+ Requires-Dist: python-docx>=1.2.0
14
+ Requires-Dist: python-dotenv>=1.2.2
15
+ Requires-Dist: reportlab>=5.0.0
16
+ Dynamic: license-file
17
+
18
+ # Google Keep MCP
19
+
20
+ Google Keep MCP is a Model Context Protocol server that lets MCP-capable clients create, read, update, search, archive, and export Google Keep notes.
21
+
22
+ ## What this project is
23
+
24
+ - Installable as a normal Python package
25
+ - Usable from Claude Desktop, Cursor, and any other MCP client
26
+ - Designed so each user logs in with their own Google Keep account
27
+ - Stores login state locally on each user machine
28
+
29
+ ## Install
30
+
31
+ ### With uv
32
+
33
+ ```powershell
34
+ uv add google-keep-mcp
35
+ ```
36
+
37
+ ### With pip
38
+
39
+ ```powershell
40
+ pip install google-keep-mcp
41
+ ```
42
+
43
+ ### From this repo for local testing
44
+
45
+ ```powershell
46
+ cd D:\MyWorld\google-Keep-MCP
47
+ uv pip install -e .
48
+ ```
49
+
50
+ ## Run
51
+
52
+ After install, run the server command:
53
+
54
+ ```powershell
55
+ google-keep-mcp
56
+ ```
57
+
58
+ For local repo testing you can also run it through uv:
59
+
60
+ ```powershell
61
+ uv run --directory D:\MyWorld\google-Keep-MCP google-keep-mcp
62
+ ```
63
+
64
+ ## First-time login
65
+
66
+ Each user must log in once on their own machine.
67
+
68
+ Run:
69
+
70
+ ```powershell
71
+ google-keep-mcp login
72
+ ```
73
+
74
+ Then enter:
75
+
76
+ - your Google email
77
+ - your Google Keep master token
78
+
79
+ The login is saved locally in:
80
+
81
+ - Windows: `C:\Users\<you>\.google_keep_mcp\config.json`
82
+ - macOS/Linux: `~/.google_keep_mcp/config.json`
83
+
84
+ If the token is revoked or removed, the user must log in again.
85
+
86
+ ## Claude Desktop config
87
+
88
+ For local testing, point Claude Desktop to the installed command:
89
+
90
+ ```json
91
+ {
92
+ "mcpServers": {
93
+ "google-keep": {
94
+ "command": "D:\\MyWorld\\google-Keep-MCP\\.venv-test\\Scripts\\google-keep-mcp.exe"
95
+ }
96
+ }
97
+ }
98
+ ```
99
+
100
+ If the package is installed globally or into the active environment, this also works:
101
+
102
+ ```json
103
+ {
104
+ "mcpServers": {
105
+ "google-keep": {
106
+ "command": "google-keep-mcp"
107
+ }
108
+ }
109
+ }
110
+ ```
111
+
112
+ ## Google Tasks reminders
113
+
114
+ Reminder tools use Google Tasks OAuth credentials.
115
+
116
+ Place the credentials file here:
117
+
118
+ ```text
119
+ C:\Users\<you>\.google_keep_mcp\credentials.json
120
+ ```
121
+
122
+ The reminder token is cached here:
123
+
124
+ ```text
125
+ C:\Users\<you>\.google_keep_mcp\token.json
126
+ ```
127
+
128
+ ## What new users need
129
+
130
+ - Install the package
131
+ - Run `google-keep-mcp login`
132
+ - Paste their own Google Keep master token
133
+ - Then use the MCP server from Claude or any other MCP client
134
+
135
+ ## Public release note
136
+
137
+ This package is ready to publish to PyPI, but it is not public until you upload it with your PyPI account.
138
+
139
+ ## License
140
+
141
+ MIT
@@ -0,0 +1,13 @@
1
+ google_keep_notes_mcp-0.1.2.dist-info/licenses/LICENSE,sha256=QrmQfISYGX9OtpEhqdkMmlEOWeg8UZrjvYLt0FmMUb0,1093
2
+ keep_mcp_server/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
+ keep_mcp_server/auth.py,sha256=JqB0zRQZc9iSKJbZjogpAtEF-CQs1BAgpDqNpDrbdyM,1038
4
+ keep_mcp_server/config.py,sha256=4dPN9_1HBd0REtX14UF1VUCOZcTTr3Epbzpvhb56jek,705
5
+ keep_mcp_server/export_tools.py,sha256=Wk2cQYKcIE2Xw4cz5quUJP3JN5sfW8fsyDYSH488pfQ,3474
6
+ keep_mcp_server/keep_tools.py,sha256=v61tmp0-yjKawuATxtGQgOSpin662KafOCsPGaroGJg,13001
7
+ keep_mcp_server/server.py,sha256=Ze_-tAOZ2kcOEQ0RsApXllVvIrn4rkbN4EV_mHSCuIY,3809
8
+ keep_mcp_server/tasks_tools.py,sha256=jciMpdip5tRwV0V7afTCnE_hLqb4rofkfKFHM6_O6l0,2490
9
+ google_keep_notes_mcp-0.1.2.dist-info/METADATA,sha256=_ckgNcO5BpD_NYIUd-uVftaycIVdB72Ciod5SBzJIRQ,2929
10
+ google_keep_notes_mcp-0.1.2.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
11
+ google_keep_notes_mcp-0.1.2.dist-info/entry_points.txt,sha256=50Fai_GxVEwOZNEE3WA3lfPZ21XN3Ko8m9Vx2DRraJs,64
12
+ google_keep_notes_mcp-0.1.2.dist-info/top_level.txt,sha256=nYo2wQPpQQ11hkVtMVlCoJfgsdd5IT0MDS1AQfIzsXY,16
13
+ google_keep_notes_mcp-0.1.2.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ google-keep-mcp = keep_mcp_server.server:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Muhammad Abubakar
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
13
+ all 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
21
+ THE SOFTWARE.
@@ -0,0 +1 @@
1
+ keep_mcp_server
File without changes
@@ -0,0 +1,38 @@
1
+ import os
2
+ import gkeepapi
3
+ from dotenv import load_dotenv
4
+ from .config import load_config, save_config
5
+
6
+ load_dotenv()
7
+
8
+
9
+ def get_keep_client() -> gkeepapi.Keep:
10
+ config = load_config()
11
+
12
+ email = os.getenv("GOOGLE_EMAIL") or config.get("google_email")
13
+ master_token = os.getenv("GOOGLE_MASTER_TOKEN") or config.get("google_master_token")
14
+
15
+ if not email or not master_token:
16
+ raise ValueError(
17
+ "Google Keep auth not configured. Run `google-keep-mcp login` first "
18
+ "or set GOOGLE_EMAIL and GOOGLE_MASTER_TOKEN."
19
+ )
20
+
21
+ keep = gkeepapi.Keep()
22
+
23
+ try:
24
+ keep.authenticate(email, master_token)
25
+ print(f"[auth] Login successful: {email}")
26
+ except Exception as error:
27
+ raise RuntimeError(f"[auth] Authentication failed: {error}")
28
+
29
+ return keep
30
+
31
+
32
+ def save_keep_login(email: str, master_token: str) -> None:
33
+ save_config(
34
+ {
35
+ "google_email": email,
36
+ "google_master_token": master_token,
37
+ }
38
+ )
@@ -0,0 +1,28 @@
1
+ import json
2
+ from pathlib import Path
3
+
4
+
5
+ APP_DIR = Path.home() / ".google_keep_mcp"
6
+ CONFIG_FILE = APP_DIR / "config.json"
7
+ TASKS_TOKEN_FILE = APP_DIR / "token.json"
8
+ TASKS_CREDENTIALS_FILE = APP_DIR / "credentials.json"
9
+
10
+
11
+ def ensure_app_dir() -> None:
12
+ APP_DIR.mkdir(parents=True, exist_ok=True)
13
+
14
+
15
+ def load_config() -> dict:
16
+ if not CONFIG_FILE.exists():
17
+ return {}
18
+ try:
19
+ with CONFIG_FILE.open("r", encoding="utf-8") as file:
20
+ return json.load(file)
21
+ except Exception:
22
+ return {}
23
+
24
+
25
+ def save_config(data: dict) -> None:
26
+ ensure_app_dir()
27
+ with CONFIG_FILE.open("w", encoding="utf-8") as file:
28
+ json.dump(data, file, indent=2)
@@ -0,0 +1,108 @@
1
+ import os
2
+ from pathlib import Path
3
+ from datetime import datetime
4
+
5
+
6
+ def get_export_path(filename: str) -> str:
7
+ """Export folder — Desktop pe save hoga"""
8
+ desktop = Path.home() / "Desktop" / "KeepExports"
9
+ desktop.mkdir(parents=True, exist_ok=True)
10
+ return str(desktop / filename)
11
+
12
+
13
+ def export_as_pdf(title: str, content: str) -> str:
14
+ from reportlab.lib.pagesizes import A4
15
+ from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
16
+ from reportlab.lib.units import inch
17
+ from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer
18
+ from reportlab.lib.enums import TA_LEFT
19
+
20
+ safe_title = "".join(c for c in title if c.isalnum() or c in " _-").strip()
21
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
22
+ filename = f"{safe_title}_{timestamp}.pdf"
23
+ filepath = get_export_path(filename)
24
+
25
+ doc = SimpleDocTemplate(filepath, pagesize=A4,
26
+ rightMargin=inch, leftMargin=inch,
27
+ topMargin=inch, bottomMargin=inch)
28
+
29
+ styles = getSampleStyleSheet()
30
+ title_style = ParagraphStyle('Title', parent=styles['Title'], fontSize=18, spaceAfter=20)
31
+ body_style = ParagraphStyle('Body', parent=styles['Normal'], fontSize=11,
32
+ leading=16, spaceAfter=8)
33
+
34
+ story = []
35
+ story.append(Paragraph(title, title_style))
36
+ story.append(Spacer(1, 0.2 * inch))
37
+
38
+ for line in content.split('\n'):
39
+ line = line.strip()
40
+ if line:
41
+ story.append(Paragraph(line, body_style))
42
+ else:
43
+ story.append(Spacer(1, 0.1 * inch))
44
+
45
+ doc.build(story)
46
+ return filepath
47
+
48
+
49
+ def export_as_docx(title: str, content: str) -> str:
50
+ from docx import Document
51
+ from docx.shared import Pt, Inches
52
+ from docx.enum.text import WD_ALIGN_PARAGRAPH
53
+
54
+ safe_title = "".join(c for c in title if c.isalnum() or c in " _-").strip()
55
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
56
+ filename = f"{safe_title}_{timestamp}.docx"
57
+ filepath = get_export_path(filename)
58
+
59
+ doc = Document()
60
+
61
+ # Title
62
+ title_para = doc.add_heading(title, level=1)
63
+ title_para.alignment = WD_ALIGN_PARAGRAPH.LEFT
64
+
65
+ doc.add_paragraph()
66
+
67
+ # Content
68
+ for line in content.split('\n'):
69
+ doc.add_paragraph(line)
70
+
71
+ doc.save(filepath)
72
+ return filepath
73
+
74
+
75
+ def export_as_markdown(title: str, content: str) -> str:
76
+ safe_title = "".join(c for c in title if c.isalnum() or c in " _-").strip()
77
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
78
+ filename = f"{safe_title}_{timestamp}.md"
79
+ filepath = get_export_path(filename)
80
+
81
+ with open(filepath, 'w', encoding='utf-8') as f:
82
+ f.write(f"# {title}\n\n")
83
+ f.write(content)
84
+
85
+ return filepath
86
+
87
+
88
+ def export_note(title: str, content: str, format: str = "pdf") -> dict:
89
+ """
90
+ format: 'pdf', 'docx', 'md'
91
+ """
92
+ format = format.lower().strip()
93
+
94
+ if format == "pdf":
95
+ filepath = export_as_pdf(title, content)
96
+ elif format in ("docx", "word"):
97
+ filepath = export_as_docx(title, content)
98
+ elif format in ("md", "markdown"):
99
+ filepath = export_as_markdown(title, content)
100
+ else:
101
+ raise ValueError(f"Unsupported format: {format}. Use: pdf, docx, md")
102
+
103
+ return {
104
+ "exported": True,
105
+ "format": format,
106
+ "title": title,
107
+ "filepath": filepath
108
+ }
@@ -0,0 +1,461 @@
1
+ import gkeepapi
2
+ from keep_mcp_server.auth import get_keep_client
3
+
4
+ keep = None
5
+
6
+ def get_keep():
7
+ global keep
8
+ if keep is None:
9
+ keep = get_keep_client()
10
+ return keep
11
+
12
+
13
+ def create_note(title: str, content: str = "") -> dict:
14
+ k = get_keep()
15
+ note = k.createNote(title, content)
16
+ k.sync()
17
+ return {"id": note.id, "title": note.title, "content": note.text}
18
+
19
+
20
+ def list_notes(query: str = None) -> list:
21
+ k = get_keep()
22
+ k.sync()
23
+ if query:
24
+ notes = k.find(query=query)
25
+ else:
26
+ notes = k.all()
27
+ result = []
28
+ for note in notes:
29
+ if not note.trashed and not note.archived:
30
+ result.append({
31
+ "id": note.id,
32
+ "title": note.title,
33
+ "content": note.text,
34
+ "pinned": note.pinned,
35
+ "color": str(note.color),
36
+ })
37
+ return result
38
+
39
+
40
+ def get_note(note_id: str) -> dict:
41
+ k = get_keep()
42
+ note = k.get(note_id)
43
+ if not note:
44
+ raise ValueError(f"Note not found: {note_id}")
45
+ return {"id": note.id, "title": note.title, "content": note.text}
46
+
47
+
48
+ def update_note(note_id: str, title: str = None, content: str = None) -> dict:
49
+ k = get_keep()
50
+ note = k.get(note_id)
51
+ if not note:
52
+ raise ValueError(f"Note not found: {note_id}")
53
+ if title is not None:
54
+ note.title = title
55
+ if content is not None:
56
+ note.text = content
57
+ k.sync()
58
+ return {"id": note.id, "title": note.title, "content": note.text}
59
+
60
+
61
+ def delete_note(note_id: str) -> dict:
62
+ k = get_keep()
63
+ note = k.get(note_id)
64
+ if not note:
65
+ raise ValueError(f"Note not found: {note_id}")
66
+ note.trash()
67
+ k.sync()
68
+ return {"deleted": True, "id": note_id}
69
+
70
+
71
+ def archive_note(note_id: str) -> dict:
72
+ k = get_keep()
73
+ note = k.get(note_id)
74
+ if not note:
75
+ raise ValueError(f"Note not found: {note_id}")
76
+ note.archived = True
77
+ k.sync()
78
+ return {"archived": True, "id": note_id}
79
+
80
+
81
+ def search_notes(keyword: str) -> list:
82
+ k = get_keep()
83
+ k.sync()
84
+ notes = k.find(query=keyword)
85
+ result = []
86
+ for note in notes:
87
+ if not note.trashed:
88
+ result.append({
89
+ "id": note.id,
90
+ "title": note.title,
91
+ "content": note.text,
92
+ })
93
+ return result
94
+
95
+ def set_reminder(note_id: str, reminder_time: str) -> dict:
96
+ """reminder_time format: 'YYYY-MM-DD HH:MM'"""
97
+ from keep_mcp_server.tasks_tools import create_reminder_task
98
+ k = get_keep()
99
+ note = k.get(note_id)
100
+ if not note:
101
+ raise ValueError(f"Note not found: {note_id}")
102
+
103
+ note_url = note.url or f"https://keep.google.com/u/0/#NOTE/{note.id}"
104
+ result = create_reminder_task(note.title, note_url, reminder_time)
105
+
106
+ return {
107
+ "note_id": note_id,
108
+ "reminder_set": reminder_time,
109
+ "task_created": result
110
+ }
111
+
112
+ def pin_note(note_id: str, pinned: bool = True) -> dict:
113
+ k = get_keep()
114
+ note = k.get(note_id)
115
+ if not note:
116
+ raise ValueError(f"Note not found: {note_id}")
117
+ note.pinned = pinned
118
+ k.sync()
119
+ return {"id": note_id, "pinned": pinned}
120
+
121
+
122
+ def change_color(note_id: str, color: str) -> dict:
123
+ """colors: red, blue, green, yellow, white, gray, teal, pink, purple, brown"""
124
+ k = get_keep()
125
+ note = k.get(note_id)
126
+ if not note:
127
+ raise ValueError(f"Note not found: {note_id}")
128
+ color_map = {
129
+ "red": gkeepapi.node.ColorValue.Red,
130
+ "blue": gkeepapi.node.ColorValue.Blue,
131
+ "green": gkeepapi.node.ColorValue.Green,
132
+ "yellow": gkeepapi.node.ColorValue.Yellow,
133
+ "white": gkeepapi.node.ColorValue.White,
134
+ "gray": gkeepapi.node.ColorValue.Gray,
135
+ "teal": gkeepapi.node.ColorValue.Teal,
136
+ "pink": gkeepapi.node.ColorValue.Pink,
137
+ "purple": gkeepapi.node.ColorValue.Purple,
138
+ "brown": gkeepapi.node.ColorValue.Brown,
139
+ }
140
+ if color.lower() not in color_map:
141
+ raise ValueError(f"Invalid color. Choose from: {list(color_map.keys())}")
142
+ note.color = color_map[color.lower()]
143
+ k.sync()
144
+ return {"id": note_id, "color": color}
145
+
146
+ def add_collaborator(note_id: str, email: str) -> dict:
147
+ k = get_keep()
148
+ note = k.get(note_id)
149
+ if not note:
150
+ raise ValueError(f"Note not found: {note_id}")
151
+ note.collaborators.add(email)
152
+ k.sync()
153
+ return {"note_id": note_id, "collaborator_added": email}
154
+
155
+
156
+ def remove_collaborator(note_id: str, email: str) -> dict:
157
+ k = get_keep()
158
+ note = k.get(note_id)
159
+ if not note:
160
+ raise ValueError(f"Note not found: {note_id}")
161
+ note.collaborators.remove(email)
162
+ k.sync()
163
+ return {"note_id": note_id, "collaborator_removed": email}
164
+
165
+ def create_label(name: str) -> dict:
166
+ k = get_keep()
167
+ label = k.findLabel(name)
168
+ if label:
169
+ return {"id": label.id, "name": label.name, "already_existed": True}
170
+ label = k.createLabel(name)
171
+ k.sync()
172
+ return {"id": label.id, "name": label.name, "already_existed": False}
173
+
174
+
175
+ def add_label_to_note(note_id: str, label_name: str) -> dict:
176
+ k = get_keep()
177
+ note = k.get(note_id)
178
+ if not note:
179
+ raise ValueError(f"Note not found: {note_id}")
180
+ label = k.findLabel(label_name)
181
+ if not label:
182
+ label = k.createLabel(label_name)
183
+ note.labels.add(label)
184
+ k.sync()
185
+ return {"note_id": note_id, "label_added": label_name}
186
+
187
+
188
+ def remove_label_from_note(note_id: str, label_name: str) -> dict:
189
+ k = get_keep()
190
+ note = k.get(note_id)
191
+ if not note:
192
+ raise ValueError(f"Note not found: {note_id}")
193
+ label = k.findLabel(label_name)
194
+ if not label:
195
+ raise ValueError(f"Label not found: {label_name}")
196
+ note.labels.remove(label)
197
+ k.sync()
198
+ return {"note_id": note_id, "label_removed": label_name}
199
+
200
+
201
+ def list_labels() -> list:
202
+ k = get_keep()
203
+ return [{"id": l.id, "name": l.name} for l in k.labels()]
204
+
205
+ def get_notes_analytics() -> dict:
206
+ """Detailed analytics of all Google Keep notes"""
207
+ from datetime import datetime, timezone
208
+ from collections import Counter
209
+
210
+ k = get_keep()
211
+ k.sync()
212
+ all_notes = [n for n in k.all() if not n.trashed]
213
+ active = [n for n in all_notes if not n.archived]
214
+ archived = [n for n in all_notes if n.archived]
215
+
216
+ # Label stats
217
+ label_counter = Counter()
218
+ for note in active:
219
+ for label in note.labels.all():
220
+ label_counter[label.name] += 1
221
+
222
+ # Color stats
223
+ color_counter = Counter()
224
+ for note in active:
225
+ color_counter[str(note.color).replace("ColorValue.", "")] += 1
226
+
227
+ # Oldest & newest note
228
+ dated_notes = [n for n in active if n.timestamps.created]
229
+ oldest = min(dated_notes, key=lambda n: n.timestamps.created) if dated_notes else None
230
+ newest = max(dated_notes, key=lambda n: n.timestamps.created) if dated_notes else None
231
+
232
+ # Pinned count
233
+ pinned = [n for n in active if n.pinned]
234
+
235
+ # Notes with content vs empty
236
+ with_content = [n for n in active if n.text and n.text.strip()]
237
+
238
+ return {
239
+ "total_notes": len(active),
240
+ "archived_notes": len(archived),
241
+ "pinned_notes": len(pinned),
242
+ "notes_with_content": len(with_content),
243
+ "empty_notes": len(active) - len(with_content),
244
+ "most_used_labels": [
245
+ {"label": k, "count": v}
246
+ for k, v in label_counter.most_common(5)
247
+ ],
248
+ "color_distribution": dict(color_counter),
249
+ "oldest_note": {
250
+ "title": oldest.title or "Untitled",
251
+ "created": oldest.timestamps.created.strftime("%B %Y")
252
+ } if oldest else None,
253
+ "newest_note": {
254
+ "title": newest.title or "Untitled",
255
+ "created": newest.timestamps.created.strftime("%B %Y")
256
+ } if newest else None,
257
+ }
258
+
259
+ TEMPLATES = {
260
+ "meeting": {
261
+ "title": "Meeting Notes — {date}",
262
+ "content": """👥 Attendees:
263
+ -
264
+
265
+ 🎯 Agenda:
266
+ -
267
+
268
+ 📝 Discussion Points:
269
+ -
270
+
271
+ ✅ Action Items:
272
+ - [ ]
273
+
274
+ ⏰ Next Meeting:
275
+
276
+ 📌 Notes:
277
+ """
278
+ },
279
+ "journal": {
280
+ "title": "Daily Journal — {date}",
281
+ "content": """🌅 Today I am grateful for:
282
+ -
283
+
284
+ 📋 Today's Goals:
285
+ - [ ]
286
+ - [ ]
287
+ - [ ]
288
+
289
+ 💭 How I'm feeling:
290
+
291
+ 📝 Today's highlights:
292
+
293
+ 🌙 End of day reflection:
294
+ """
295
+ },
296
+ "project": {
297
+ "title": "Project Plan — {name}",
298
+ "content": """🎯 Project Goal:
299
+
300
+ 📅 Deadline:
301
+
302
+ ✅ Tasks:
303
+ - [ ]
304
+ - [ ]
305
+ - [ ]
306
+
307
+ 👥 Team Members:
308
+
309
+ ⚠️ Risks:
310
+
311
+ 📊 Progress:
312
+ [ ] Planning
313
+ [ ] In Progress
314
+ [ ] Review
315
+ [ ] Done
316
+
317
+ 📝 Notes:
318
+ """
319
+ },
320
+ "shopping": {
321
+ "title": "Shopping List — {date}",
322
+ "content": """🛒 Items to Buy:
323
+ - [ ]
324
+ - [ ]
325
+ - [ ]
326
+
327
+ 💰 Budget:
328
+
329
+ 🏪 Store:
330
+
331
+ 📝 Notes:
332
+ """
333
+ },
334
+ "ideas": {
335
+ "title": "Ideas — {topic}",
336
+ "content": """💡 Main Idea:
337
+
338
+ 🔍 Details:
339
+
340
+ ✅ Pros:
341
+ -
342
+
343
+ ❌ Cons:
344
+ -
345
+
346
+ 🚀 Next Steps:
347
+ -
348
+
349
+ 📝 Notes:
350
+ """
351
+ }
352
+ }
353
+
354
+
355
+ def create_from_template(template_name: str, extra: str = "") -> dict:
356
+ """
357
+ Template se note banao.
358
+ template_name: meeting, journal, project, shopping, ideas
359
+ extra: project name ya topic (optional)
360
+ """
361
+ from datetime import datetime
362
+
363
+ template_name = template_name.lower().strip()
364
+
365
+ if template_name not in TEMPLATES:
366
+ available = list(TEMPLATES.keys())
367
+ raise ValueError(f"Template '{template_name}' nahi mila. Available: {available}")
368
+
369
+ template = TEMPLATES[template_name]
370
+ date_str = datetime.now().strftime("%d %B %Y")
371
+
372
+ title = template["title"].format(date=date_str, name=extra or "New Project", topic=extra or "New Ideas")
373
+ content = template["content"]
374
+
375
+ k = get_keep()
376
+ note = k.createNote(title, content)
377
+ note.pinned = True
378
+ k.sync()
379
+
380
+ return {
381
+ "id": note.id,
382
+ "title": note.title,
383
+ "template_used": template_name,
384
+ "content_preview": content[:100] + "..."
385
+ }
386
+
387
+
388
+ def list_templates() -> list:
389
+ """Available templates ki list"""
390
+ return [
391
+ {"name": "meeting", "description": "Meeting notes with agenda, attendees, action items"},
392
+ {"name": "journal", "description": "Daily journal with goals and reflection"},
393
+ {"name": "project", "description": "Project plan with tasks, team, deadlines"},
394
+ {"name": "shopping", "description": "Shopping list with budget and store"},
395
+ {"name": "ideas", "description": "Ideas capture with pros, cons, next steps"},
396
+ ]
397
+
398
+
399
+ def get_weekly_digest() -> dict:
400
+ """Smart daily/weekly digest of Google Keep notes"""
401
+ from datetime import datetime, timezone, timedelta
402
+
403
+ k = get_keep()
404
+ k.sync()
405
+ all_notes = [n for n in k.all() if not n.trashed and not n.archived]
406
+
407
+ now = datetime.now(timezone.utc)
408
+ week_ago = now - timedelta(days=7)
409
+ today_start = now.replace(hour=0, minute=0, second=0, microsecond=0)
410
+
411
+ pinned = []
412
+ recent_today = []
413
+ recent_week = []
414
+ by_label = {}
415
+
416
+ for note in all_notes:
417
+ note_data = {
418
+ "id": note.id,
419
+ "title": note.title or "Untitled",
420
+ "preview": note.text[:80] + "..." if len(note.text) > 80 else note.text,
421
+ "pinned": note.pinned,
422
+ "color": str(note.color).replace("ColorValue.", ""),
423
+ "labels": [l.name for l in note.labels.all()]
424
+ }
425
+
426
+ if note.pinned:
427
+ pinned.append(note_data)
428
+
429
+ if note.timestamps.updated:
430
+ if note.timestamps.updated >= today_start:
431
+ recent_today.append(note_data)
432
+ elif note.timestamps.updated >= week_ago:
433
+ recent_week.append(note_data)
434
+
435
+ for label in note.labels.all():
436
+ if label.name not in by_label:
437
+ by_label[label.name] = []
438
+ by_label[label.name].append(note_data)
439
+
440
+ # Most active label
441
+ most_active_label = max(by_label, key=lambda l: len(by_label[l])) if by_label else None
442
+
443
+ return {
444
+ "digest_date": now.strftime("%A, %d %B %Y"),
445
+ "summary": {
446
+ "total_active_notes": len(all_notes),
447
+ "pinned_count": len(pinned),
448
+ "updated_today": len(recent_today),
449
+ "updated_this_week": len(recent_week),
450
+ "total_labels": len(by_label),
451
+ "most_active_label": most_active_label,
452
+ },
453
+ "pinned_notes": pinned,
454
+ "updated_today": recent_today,
455
+ "updated_this_week": recent_week,
456
+ "notes_by_label": {
457
+ label: notes for label, notes in sorted(
458
+ by_label.items(), key=lambda x: len(x[1]), reverse=True
459
+ )
460
+ }
461
+ }
@@ -0,0 +1,145 @@
1
+ from mcp.server.fastmcp import FastMCP
2
+ from keep_mcp_server.keep_tools import (
3
+ create_note, list_notes, get_note, update_note,
4
+ delete_note, archive_note, search_notes,
5
+ set_reminder, pin_note, change_color
6
+ )
7
+ from keep_mcp_server.export_tools import export_note
8
+ from keep_mcp_server.auth import save_keep_login
9
+ from keep_mcp_server.config import load_config
10
+
11
+ mcp = FastMCP("Google Keep MCP")
12
+
13
+
14
+ @mcp.tool()
15
+ def keep_create_note(title: str, content: str = "") -> dict:
16
+ return create_note(title, content)
17
+
18
+
19
+ @mcp.tool()
20
+ def keep_list_notes() -> list:
21
+ return list_notes()
22
+
23
+
24
+ @mcp.tool()
25
+ def keep_get_note(note_id: str) -> dict:
26
+ return get_note(note_id)
27
+
28
+
29
+ @mcp.tool()
30
+ def keep_update_note(note_id: str, title: str = None, content: str = None) -> dict:
31
+ return update_note(note_id, title, content)
32
+
33
+
34
+ @mcp.tool()
35
+ def keep_delete_note(note_id: str) -> dict:
36
+ return delete_note(note_id)
37
+
38
+
39
+ @mcp.tool()
40
+ def keep_archive_note(note_id: str) -> dict:
41
+ return archive_note(note_id)
42
+
43
+
44
+ @mcp.tool()
45
+ def keep_search_notes(keyword: str) -> list:
46
+ return search_notes(keyword)
47
+
48
+
49
+ @mcp.tool()
50
+ def keep_set_reminder(note_id: str, reminder_time: str) -> dict:
51
+ return set_reminder(note_id, reminder_time)
52
+
53
+
54
+ @mcp.tool()
55
+ def keep_pin_note(note_id: str, pinned: bool = True) -> dict:
56
+ return pin_note(note_id, pinned)
57
+
58
+
59
+ @mcp.tool()
60
+ def keep_change_color(note_id: str, color: str) -> dict:
61
+ return change_color(note_id, color)
62
+
63
+
64
+ @mcp.tool()
65
+ def keep_export_note(note_id: str, format: str = "pdf") -> dict:
66
+ from keep_mcp_server.keep_tools import get_note
67
+ note = get_note(note_id)
68
+ return export_note(note["title"], note["content"], format)
69
+
70
+
71
+ @mcp.tool()
72
+ def keep_add_collaborator(note_id: str, email: str) -> dict:
73
+ from keep_mcp_server.keep_tools import add_collaborator
74
+ return add_collaborator(note_id, email)
75
+
76
+
77
+ @mcp.tool()
78
+ def keep_remove_collaborator(note_id: str, email: str) -> dict:
79
+ from keep_mcp_server.keep_tools import remove_collaborator
80
+ return remove_collaborator(note_id, email)
81
+
82
+
83
+ @mcp.tool()
84
+ def keep_create_label(name: str) -> dict:
85
+ from keep_mcp_server.keep_tools import create_label
86
+ return create_label(name)
87
+
88
+
89
+ @mcp.tool()
90
+ def keep_add_label(note_id: str, label_name: str) -> dict:
91
+ from keep_mcp_server.keep_tools import add_label_to_note
92
+ return add_label_to_note(note_id, label_name)
93
+
94
+
95
+ @mcp.tool()
96
+ def keep_remove_label(note_id: str, label_name: str) -> dict:
97
+ from keep_mcp_server.keep_tools import remove_label_from_note
98
+ return remove_label_from_note(note_id, label_name)
99
+
100
+
101
+ @mcp.tool()
102
+ def keep_list_labels() -> list:
103
+ from keep_mcp_server.keep_tools import list_labels
104
+ return list_labels()
105
+
106
+
107
+ @mcp.tool()
108
+ def keep_get_analytics() -> dict:
109
+ from keep_mcp_server.keep_tools import get_notes_analytics
110
+ return get_notes_analytics()
111
+
112
+
113
+ @mcp.tool()
114
+ def keep_list_templates() -> list:
115
+ from keep_mcp_server.keep_tools import list_templates
116
+ return list_templates()
117
+
118
+
119
+ @mcp.tool()
120
+ def keep_create_from_template(template_name: str, extra: str = "") -> dict:
121
+ from keep_mcp_server.keep_tools import create_from_template
122
+ return create_from_template(template_name, extra)
123
+
124
+
125
+ @mcp.tool()
126
+ def keep_get_weekly_digest() -> dict:
127
+ from keep_mcp_server.keep_tools import get_weekly_digest
128
+ return get_weekly_digest()
129
+
130
+
131
+ def main():
132
+ import sys
133
+
134
+ if len(sys.argv) >= 2 and sys.argv[1] == "login":
135
+ email = input("Google email: ").strip()
136
+ master_token = input("Google master token: ").strip()
137
+ save_keep_login(email, master_token)
138
+ print("Saved login to local config.")
139
+ return
140
+
141
+ mcp.run()
142
+
143
+
144
+ if __name__ == "__main__":
145
+ main()
@@ -0,0 +1,68 @@
1
+ import os
2
+ from datetime import datetime, timezone
3
+ from dotenv import load_dotenv
4
+ from google.oauth2.credentials import Credentials
5
+ from google_auth_oauthlib.flow import InstalledAppFlow
6
+ from google.auth.transport.requests import Request
7
+ from googleapiclient.discovery import build
8
+ from .config import TASKS_CREDENTIALS_FILE, TASKS_TOKEN_FILE, ensure_app_dir
9
+
10
+ load_dotenv()
11
+
12
+ SCOPES = ["https://www.googleapis.com/auth/tasks"]
13
+
14
+
15
+ def get_tasks_service():
16
+ print("[tasks] OAuth check kar raha hai...")
17
+ creds = None
18
+
19
+ ensure_app_dir()
20
+
21
+ if TASKS_TOKEN_FILE.exists():
22
+ creds = Credentials.from_authorized_user_file(str(TASKS_TOKEN_FILE), SCOPES)
23
+ print("[tasks] Existing token mila")
24
+
25
+ if not creds or not creds.valid:
26
+ if creds and creds.expired and creds.refresh_token:
27
+ print("[tasks] Token refresh ho raha hai...")
28
+ creds.refresh(Request())
29
+ else:
30
+ print("[tasks] Browser khul raha hai login ke liye...")
31
+ if not TASKS_CREDENTIALS_FILE.exists():
32
+ raise FileNotFoundError(
33
+ f"Google Tasks credentials not found at {TASKS_CREDENTIALS_FILE}. "
34
+ "Put your OAuth client credentials there."
35
+ )
36
+ flow = InstalledAppFlow.from_client_secrets_file(str(TASKS_CREDENTIALS_FILE), SCOPES)
37
+ creds = flow.run_local_server(port=0)
38
+
39
+ with TASKS_TOKEN_FILE.open("w", encoding="utf-8") as file:
40
+ file.write(creds.to_json())
41
+ print("[tasks] Token save ho gaya")
42
+
43
+ return build("tasks", "v1", credentials=creds)
44
+
45
+
46
+ def create_reminder_task(note_title: str, note_url: str, reminder_time: str) -> dict:
47
+ service = get_tasks_service()
48
+
49
+ dt = datetime.strptime(reminder_time, "%Y-%m-%d %H:%M")
50
+ dt = dt.replace(tzinfo=timezone.utc)
51
+ due_rfc3339 = dt.strftime("%Y-%m-%dT%H:%M:%SZ")
52
+
53
+ safe_title = note_title.encode("ascii", "ignore").decode("ascii")
54
+ task_body = {
55
+ "title": f"Reminder: {safe_title} [Keep Note]",
56
+ "notes": str(note_url),
57
+ "due": due_rfc3339,
58
+ }
59
+
60
+ print(f"[tasks] Task bana raha hai: {task_body['title']}")
61
+ result = service.tasks().insert(tasklist="@default", body=task_body).execute()
62
+
63
+ return {
64
+ "task_id": str(result.get("id", "")),
65
+ "task_title": str(result.get("title", "")),
66
+ "due": str(result.get("due", "")),
67
+ "note_url": str(note_url),
68
+ }