#
tokens: 3571/50000 10/10 files
lines: on (toggle) GitHub
raw markdown copy reset
# 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 | ![keep-mcp](https://github.com/user-attachments/assets/f50c4ae6-4d35-4bb6-a494-51c67385f1b6)
 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 |     
```