# Directory Structure
```
├── .cursor
│ └── mcp.json
├── .example.env
├── .gitignore
├── .python-version
├── LICENSE
├── pyproject.toml
├── README.md
└── src
└── server
├── __init__.py
├── __main__.py
├── cli.py
└── keep_api.py
```
# Files
--------------------------------------------------------------------------------
/.python-version:
--------------------------------------------------------------------------------
```
1 | 3.10
2 |
```
--------------------------------------------------------------------------------
/.example.env:
--------------------------------------------------------------------------------
```
1 | # Google Keep API Credentials
2 | [email protected]
3 | GOOGLE_MASTER_TOKEN=your-master-token-see-the-readme-on-how-to-get-it
```
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
```
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 | uv.lock
48 | API_DOCS.md
49 | progress
50 |
```
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
```markdown
1 | # keep-mcp
2 |
3 | MCP server for Google Keep
4 |
5 | 
6 |
7 | ## How to use
8 |
9 | 1. Add the MCP server to your MCP servers:
10 |
11 | ```json
12 | "mcpServers": {
13 | "keep-mcp-pipx": {
14 | "command": "pipx",
15 | "args": [
16 | "run",
17 | "keep-mcp"
18 | ],
19 | "env": {
20 | "GOOGLE_EMAIL": "Your Google Email",
21 | "GOOGLE_MASTER_TOKEN": "Your Google Master Token - see README.md"
22 | }
23 | }
24 | }
25 | ```
26 |
27 | 2. Add your credentials:
28 | * `GOOGLE_EMAIL`: Your Google account email address
29 | * `GOOGLE_MASTER_TOKEN`: Your Google account master token
30 |
31 | 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.
32 |
33 | ## Features
34 |
35 | * `find`: Search for notes based on a query string
36 | * `create_note`: Create a new note with title and text (automatically adds keep-mcp label)
37 | * `update_note`: Update a note's title and text
38 | * `delete_note`: Mark a note for deletion
39 |
40 | By default, all destructive and modification operations are restricted to notes that have were created by the MCP server (i.e. have the keep-mcp label). Set `UNSAFE_MODE` to `true` to bypass this restriction.
41 |
42 | ```
43 | "env": {
44 | ...
45 | "UNSAFE_MODE": "true"
46 | }
47 | ```
48 |
49 | ## Publishing
50 |
51 | To publish a new version to PyPI:
52 |
53 | 1. Update the version in `pyproject.toml`
54 | 2. Build the package:
55 | ```bash
56 | pipx run build
57 | ```
58 | 3. Upload to PyPI:
59 | ```bash
60 | pipx run twine upload --repository pypi dist/*
61 | ```
62 |
63 | ## Troubleshooting
64 |
65 | * If you get "DeviceManagementRequiredOrSyncDisabled" check https://admin.google.com/ac/devices/settings/general and turn "Turn off mobile management (Unmanaged)"
66 |
```
--------------------------------------------------------------------------------
/src/server/__init__.py:
--------------------------------------------------------------------------------
```python
1 |
```
--------------------------------------------------------------------------------
/src/server/__main__.py:
--------------------------------------------------------------------------------
```python
1 | from .cli import main
2 |
3 | if __name__ == "__main__":
4 | main()
```
--------------------------------------------------------------------------------
/.cursor/mcp.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "mcpServers": {
3 | "keep-mcp-pipx": {
4 | "command": "pipx",
5 | "args": [
6 | "run",
7 | "--no-cache",
8 | "--spec",
9 | ".",
10 | "mcp"
11 | ],
12 | "env": {
13 | "GOOGLE_EMAIL": "Your Google Email",
14 | "GOOGLE_MASTER_TOKEN": "Your Google Master Token - see README.md",
15 | "UNSAFE_MODE": "false"
16 | }
17 | }
18 | }
19 | }
```
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
```toml
1 | [project]
2 | name = "keep-mcp"
3 | version = "0.2.0"
4 | description = "MCP Server for Google Keep"
5 | readme = "README.md"
6 | requires-python = ">=3.10"
7 | dependencies = [
8 | "gkeepapi>=0.16.0",
9 | "mcp[cli]",
10 | ]
11 | authors = [
12 | { name = "Jannik Feuerhahn", email = "[email protected]" }
13 | ]
14 | license = { file = "LICENSE" }
15 | classifiers = [
16 | "Development Status :: 4 - Beta",
17 | "Environment :: Console",
18 | "Intended Audience :: End Users/Desktop",
19 | "License :: OSI Approved :: MIT License",
20 | "Programming Language :: Python :: 3",
21 | "Programming Language :: Python :: 3.10",
22 | "Topic :: Utilities",
23 | ]
24 |
25 | [project.urls]
26 | Homepage = "https://github.com/feuerdev/keep-mcp"
27 | Repository = "https://github.com/feuerdev/keep-mcp"
28 |
29 | [project.scripts]
30 | mcp = "server.cli:main"
31 |
32 | [build-system]
33 | requires = ["hatchling >= 1.26"]
34 | build-backend = "hatchling.build"
35 |
36 | [tool.hatch.build.targets.wheel]
37 | packages = ["src/server"]
```
--------------------------------------------------------------------------------
/src/server/keep_api.py:
--------------------------------------------------------------------------------
```python
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 | def can_modify_note(note):
61 | """
62 | Check if a note can be modified based on label and environment settings.
63 |
64 | Args:
65 | note: A Google Keep note object
66 |
67 | Returns:
68 | bool: True if the note can be modified, False otherwise
69 | """
70 | unsafe_mode = os.getenv('UNSAFE_MODE', '').lower() == 'true'
71 | return unsafe_mode or has_keep_mcp_label(note)
72 |
73 | def has_keep_mcp_label(note):
74 | """
75 | Check if a note has the keep-mcp label.
76 |
77 | Args:
78 | note: A Google Keep note object
79 |
80 | Returns:
81 | bool: True if the note has the keep-mcp label, False otherwise
82 | """
83 | return any(label.name == 'keep-mcp' for label in note.labels.all())
```
--------------------------------------------------------------------------------
/src/server/cli.py:
--------------------------------------------------------------------------------
```python
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, can_modify_note
9 |
10 | mcp = FastMCP("keep")
11 |
12 | @mcp.tool()
13 | def find(query="") -> str:
14 | """
15 | Find notes based on a search query.
16 |
17 | Args:
18 | query (str, optional): A string to match against the title and text
19 |
20 | Returns:
21 | str: JSON string containing the matching notes with their id, title, text, pinned status, color and labels
22 | """
23 | keep = get_client()
24 | notes = keep.find(query=query, archived=False, trashed=False)
25 |
26 | notes_data = [serialize_note(note) for note in notes]
27 | return json.dumps(notes_data)
28 |
29 | @mcp.tool()
30 | def create_note(title: str = None, text: str = None) -> str:
31 | """
32 | Create a new note with title and text.
33 |
34 | Args:
35 | title (str, optional): The title of the note
36 | text (str, optional): The content of the note
37 |
38 | Returns:
39 | str: JSON string containing the created note's data
40 | """
41 | keep = get_client()
42 | note = keep.createNote(title=title, text=text)
43 |
44 | # Get or create the keep-mcp label
45 | label = keep.findLabel('keep-mcp')
46 | if not label:
47 | label = keep.createLabel('keep-mcp')
48 |
49 | # Add the label to the note
50 | note.labels.add(label)
51 | keep.sync() # Ensure the note is created and labeled on the server
52 |
53 | return json.dumps(serialize_note(note))
54 |
55 | @mcp.tool()
56 | def update_note(note_id: str, title: str = None, text: str = None) -> str:
57 | """
58 | Update a note's properties.
59 |
60 | Args:
61 | note_id (str): The ID of the note to update
62 | title (str, optional): New title for the note
63 | text (str, optional): New text content for the note
64 |
65 | Returns:
66 | str: JSON string containing the updated note's data
67 |
68 | Raises:
69 | ValueError: If the note doesn't exist or cannot be modified
70 | """
71 | keep = get_client()
72 | note = keep.get(note_id)
73 |
74 | if not note:
75 | raise ValueError(f"Note with ID {note_id} not found")
76 |
77 | if not can_modify_note(note):
78 | raise ValueError(f"Note with ID {note_id} cannot be modified (missing keep-mcp label and UNSAFE_MODE is not enabled)")
79 |
80 | if title is not None:
81 | note.title = title
82 | if text is not None:
83 | note.text = text
84 |
85 | keep.sync() # Ensure changes are saved to the server
86 | return json.dumps(serialize_note(note))
87 |
88 | @mcp.tool()
89 | def delete_note(note_id: str) -> str:
90 | """
91 | Delete a note (mark for deletion).
92 |
93 | Args:
94 | note_id (str): The ID of the note to delete
95 |
96 | Returns:
97 | str: Success message
98 |
99 | Raises:
100 | ValueError: If the note doesn't exist or cannot be modified
101 | """
102 | keep = get_client()
103 | note = keep.get(note_id)
104 |
105 | if not note:
106 | raise ValueError(f"Note with ID {note_id} not found")
107 |
108 | if not can_modify_note(note):
109 | raise ValueError(f"Note with ID {note_id} cannot be modified (missing keep-mcp label and UNSAFE_MODE is not enabled)")
110 |
111 | note.delete()
112 | keep.sync() # Ensure deletion is saved to the server
113 | return json.dumps({"message": f"Note {note_id} marked for deletion"})
114 |
115 | def main():
116 | mcp.run(transport='stdio')
117 |
118 |
119 | if __name__ == "__main__":
120 | main()
121 |
```