google-keep-mcp 1.0.0__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.
- google_keep_mcp-1.0.0/.example.env +3 -0
- google_keep_mcp-1.0.0/.gitignore +54 -0
- google_keep_mcp-1.0.0/.python-version +1 -0
- google_keep_mcp-1.0.0/LICENSE +21 -0
- google_keep_mcp-1.0.0/PKG-INFO +124 -0
- google_keep_mcp-1.0.0/README.md +82 -0
- google_keep_mcp-1.0.0/pyproject.toml +38 -0
- google_keep_mcp-1.0.0/src/server/__init__.py +0 -0
- google_keep_mcp-1.0.0/src/server/__main__.py +4 -0
- google_keep_mcp-1.0.0/src/server/cli.py +485 -0
- google_keep_mcp-1.0.0/src/server/keep_api.py +60 -0
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
*.so
|
|
6
|
+
.Python
|
|
7
|
+
build/
|
|
8
|
+
develop-eggs/
|
|
9
|
+
dist/
|
|
10
|
+
downloads/
|
|
11
|
+
eggs/
|
|
12
|
+
.eggs/
|
|
13
|
+
lib/
|
|
14
|
+
lib64/
|
|
15
|
+
parts/
|
|
16
|
+
sdist/
|
|
17
|
+
var/
|
|
18
|
+
wheels/
|
|
19
|
+
*.egg-info/
|
|
20
|
+
.installed.cfg
|
|
21
|
+
*.egg
|
|
22
|
+
|
|
23
|
+
# Virtual Environment
|
|
24
|
+
.venv/
|
|
25
|
+
venv/
|
|
26
|
+
ENV/
|
|
27
|
+
|
|
28
|
+
# Environment variables
|
|
29
|
+
.env
|
|
30
|
+
.env.local
|
|
31
|
+
.env.development.local
|
|
32
|
+
.env.test.local
|
|
33
|
+
.env.production.local
|
|
34
|
+
|
|
35
|
+
# IDE specific files
|
|
36
|
+
.idea/
|
|
37
|
+
.vscode/
|
|
38
|
+
*.swp
|
|
39
|
+
*.swo
|
|
40
|
+
|
|
41
|
+
# OS specific files
|
|
42
|
+
.DS_Store
|
|
43
|
+
Thumbs.db
|
|
44
|
+
|
|
45
|
+
# Project specific
|
|
46
|
+
.cursor/
|
|
47
|
+
.claude/
|
|
48
|
+
uv.lock
|
|
49
|
+
API_DOCS.md
|
|
50
|
+
progress
|
|
51
|
+
|
|
52
|
+
# OAuth credentials
|
|
53
|
+
client_credentials.json
|
|
54
|
+
token.json
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.12.11
|
|
@@ -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.
|
|
@@ -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,82 @@
|
|
|
1
|
+
# google-keep-mcp
|
|
2
|
+
|
|
3
|
+
An MCP server for Google Keep. Apply the power of AI to organize, update, or just export your Keep notes.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
### Search/List Operations
|
|
8
|
+
|
|
9
|
+
- `find` - Search for notes based on a query string
|
|
10
|
+
- `get_pinned_notes` - Get pinned notes (with optional search)
|
|
11
|
+
- `get_archived_notes` - Get archived notes (with optional search)
|
|
12
|
+
- `get_trashed_notes` - Get trashed notes (with optional search)
|
|
13
|
+
|
|
14
|
+
### Add Operations
|
|
15
|
+
|
|
16
|
+
- `create_note` - Create a new note with title and text
|
|
17
|
+
|
|
18
|
+
### Update Operations
|
|
19
|
+
|
|
20
|
+
- `update_note` - Update a note's title and text
|
|
21
|
+
- `set_note_color` - Set the color of a note (12 colors available)
|
|
22
|
+
- `archive_note` - Archive a note
|
|
23
|
+
- `unarchive_note` - Unarchive a note
|
|
24
|
+
- `restore_note` - Restore a trashed note back to an active note
|
|
25
|
+
|
|
26
|
+
### Delete Operations
|
|
27
|
+
|
|
28
|
+
- `delete_note` - Mark a note for deletion
|
|
29
|
+
- `delete_archived_note` - Permanently delete an archived note
|
|
30
|
+
|
|
31
|
+
### Utility Operations
|
|
32
|
+
|
|
33
|
+
- `get_labels` - Get all labels (with their usage count) for notes
|
|
34
|
+
- `get_note_colors` - Get available note colors and usage statistics
|
|
35
|
+
|
|
36
|
+
## How to use
|
|
37
|
+
|
|
38
|
+
1. Add the MCP server to your MCP servers:
|
|
39
|
+
|
|
40
|
+
```json
|
|
41
|
+
"mcpServers": {
|
|
42
|
+
"google-keep-mcp": {
|
|
43
|
+
"command": "pipx",
|
|
44
|
+
"args": [
|
|
45
|
+
"run",
|
|
46
|
+
"google-keep-mcp"
|
|
47
|
+
],
|
|
48
|
+
"env": {
|
|
49
|
+
"GOOGLE_EMAIL": "Your Google Email",
|
|
50
|
+
"GOOGLE_MASTER_TOKEN": "Your Google Master Token"
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
2. Add your credentials:
|
|
57
|
+
|
|
58
|
+
- `GOOGLE_EMAIL`: Your Google account email address
|
|
59
|
+
- `GOOGLE_MASTER_TOKEN`: Your Google account master token
|
|
60
|
+
|
|
61
|
+
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:
|
|
62
|
+
|
|
63
|
+
- Goto https://myaccount.google.com/apppasswords and create an app password
|
|
64
|
+
- 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)
|
|
65
|
+
- Obtain your master token. **Be careful with this token, it has access to everything in your Google account.**
|
|
66
|
+
|
|
67
|
+
> [!TIP]
|
|
68
|
+
> 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.
|
|
69
|
+
|
|
70
|
+
## Publishing
|
|
71
|
+
|
|
72
|
+
To publish a new version to PyPI:
|
|
73
|
+
|
|
74
|
+
1. Update the version in `pyproject.toml`
|
|
75
|
+
2. Build the package:
|
|
76
|
+
```bash
|
|
77
|
+
pipx run build
|
|
78
|
+
```
|
|
79
|
+
3. Upload to PyPI:
|
|
80
|
+
```bash
|
|
81
|
+
pipx run twine upload --repository pypi dist/*
|
|
82
|
+
```
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "google-keep-mcp"
|
|
3
|
+
version = "1.0.0"
|
|
4
|
+
description = "MCP Server for Google Keep"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.12.11"
|
|
7
|
+
dependencies = [
|
|
8
|
+
"gkeepapi>=0.16.0",
|
|
9
|
+
"mcp[cli]",
|
|
10
|
+
"python-dotenv",
|
|
11
|
+
]
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "Dave Nicoll", email = "d.nicoll@gmail.com" }
|
|
14
|
+
]
|
|
15
|
+
license = { file = "LICENSE" }
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 5 - Production/Stable",
|
|
18
|
+
"Environment :: Console",
|
|
19
|
+
"Intended Audience :: End Users/Desktop",
|
|
20
|
+
"License :: OSI Approved :: MIT License",
|
|
21
|
+
"Programming Language :: Python :: 3",
|
|
22
|
+
"Programming Language :: Python :: 3.12",
|
|
23
|
+
"Topic :: Utilities",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
[project.urls]
|
|
27
|
+
Homepage = "https://github.com/davenicoll/google-keep-mcp"
|
|
28
|
+
Repository = "https://github.com/davenicoll/google-keep-mcp"
|
|
29
|
+
|
|
30
|
+
[project.scripts]
|
|
31
|
+
mcp = "src.server.cli:main"
|
|
32
|
+
|
|
33
|
+
[build-system]
|
|
34
|
+
requires = ["hatchling >= 1.26"]
|
|
35
|
+
build-backend = "hatchling.build"
|
|
36
|
+
|
|
37
|
+
[tool.hatch.build.targets.wheel]
|
|
38
|
+
packages = ["src"]
|
|
File without changes
|
|
@@ -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
|
+
|
|
@@ -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
|
+
|