# 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:
--------------------------------------------------------------------------------
```
3.10
```
--------------------------------------------------------------------------------
/.example.env:
--------------------------------------------------------------------------------
```
# Google Keep API Credentials
[email protected]
GOOGLE_MASTER_TOKEN=your-master-token-see-the-readme-on-how-to-get-it
```
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
```
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
# Virtual Environment
.venv/
venv/
ENV/
# Environment variables
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
# IDE specific files
.idea/
.vscode/
*.swp
*.swo
# OS specific files
.DS_Store
Thumbs.db
# Project specific
.cursor/
uv.lock
API_DOCS.md
progress
```
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
```markdown
# keep-mcp
MCP server for Google Keep

## How to use
1. Add the MCP server to your MCP servers:
```json
"mcpServers": {
"keep-mcp-pipx": {
"command": "pipx",
"args": [
"run",
"keep-mcp"
],
"env": {
"GOOGLE_EMAIL": "Your Google Email",
"GOOGLE_MASTER_TOKEN": "Your Google Master Token - see README.md"
}
}
}
```
2. Add your credentials:
* `GOOGLE_EMAIL`: Your Google account email address
* `GOOGLE_MASTER_TOKEN`: Your Google account master token
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.
## Features
* `find`: Search for notes based on a query string
* `create_note`: Create a new note with title and text (automatically adds keep-mcp label)
* `update_note`: Update a note's title and text
* `delete_note`: Mark a note for deletion
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.
```
"env": {
...
"UNSAFE_MODE": "true"
}
```
## Publishing
To publish a new version to PyPI:
1. Update the version in `pyproject.toml`
2. Build the package:
```bash
pipx run build
```
3. Upload to PyPI:
```bash
pipx run twine upload --repository pypi dist/*
```
## Troubleshooting
* If you get "DeviceManagementRequiredOrSyncDisabled" check https://admin.google.com/ac/devices/settings/general and turn "Turn off mobile management (Unmanaged)"
```
--------------------------------------------------------------------------------
/src/server/__init__.py:
--------------------------------------------------------------------------------
```python
```
--------------------------------------------------------------------------------
/src/server/__main__.py:
--------------------------------------------------------------------------------
```python
from .cli import main
if __name__ == "__main__":
main()
```
--------------------------------------------------------------------------------
/.cursor/mcp.json:
--------------------------------------------------------------------------------
```json
{
"mcpServers": {
"keep-mcp-pipx": {
"command": "pipx",
"args": [
"run",
"--no-cache",
"--spec",
".",
"mcp"
],
"env": {
"GOOGLE_EMAIL": "Your Google Email",
"GOOGLE_MASTER_TOKEN": "Your Google Master Token - see README.md",
"UNSAFE_MODE": "false"
}
}
}
}
```
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
```toml
[project]
name = "keep-mcp"
version = "0.2.0"
description = "MCP Server for Google Keep"
readme = "README.md"
requires-python = ">=3.10"
dependencies = [
"gkeepapi>=0.16.0",
"mcp[cli]",
]
authors = [
{ name = "Jannik Feuerhahn", email = "[email protected]" }
]
license = { file = "LICENSE" }
classifiers = [
"Development Status :: 4 - Beta",
"Environment :: Console",
"Intended Audience :: End Users/Desktop",
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.10",
"Topic :: Utilities",
]
[project.urls]
Homepage = "https://github.com/feuerdev/keep-mcp"
Repository = "https://github.com/feuerdev/keep-mcp"
[project.scripts]
mcp = "server.cli:main"
[build-system]
requires = ["hatchling >= 1.26"]
build-backend = "hatchling.build"
[tool.hatch.build.targets.wheel]
packages = ["src/server"]
```
--------------------------------------------------------------------------------
/src/server/keep_api.py:
--------------------------------------------------------------------------------
```python
import gkeepapi
import os
from dotenv import load_dotenv
_keep_client = None
def get_client():
"""
Get or initialize the Google Keep client.
This ensures we only authenticate once and reuse the client.
Returns:
gkeepapi.Keep: Authenticated Keep client
"""
global _keep_client
if _keep_client is not None:
return _keep_client
# Load environment variables
load_dotenv()
# Get credentials from environment variables
email = os.getenv('GOOGLE_EMAIL')
master_token = os.getenv('GOOGLE_MASTER_TOKEN')
if not email or not master_token:
raise ValueError("Missing Google Keep credentials. Please set GOOGLE_EMAIL and GOOGLE_MASTER_TOKEN environment variables.")
# Initialize the Keep API
keep = gkeepapi.Keep()
# Authenticate
keep.authenticate(email, master_token)
# Store the client for reuse
_keep_client = keep
return keep
def serialize_note(note):
"""
Serialize a Google Keep note into a dictionary.
Args:
note: A Google Keep note object
Returns:
dict: A dictionary containing the note's id, title, text, pinned status, color and labels
"""
return {
'id': note.id,
'title': note.title,
'text': note.text,
'pinned': note.pinned,
'color': note.color.value if note.color else None,
'labels': [{'id': label.id, 'name': label.name} for label in note.labels.all()]
}
def can_modify_note(note):
"""
Check if a note can be modified based on label and environment settings.
Args:
note: A Google Keep note object
Returns:
bool: True if the note can be modified, False otherwise
"""
unsafe_mode = os.getenv('UNSAFE_MODE', '').lower() == 'true'
return unsafe_mode or has_keep_mcp_label(note)
def has_keep_mcp_label(note):
"""
Check if a note has the keep-mcp label.
Args:
note: A Google Keep note object
Returns:
bool: True if the note has the keep-mcp label, False otherwise
"""
return any(label.name == 'keep-mcp' for label in note.labels.all())
```
--------------------------------------------------------------------------------
/src/server/cli.py:
--------------------------------------------------------------------------------
```python
"""
MCP plugin for Google Keep integration.
Provides tools for interacting with Google Keep notes through MCP.
"""
import json
from mcp.server.fastmcp import FastMCP
from .keep_api import get_client, serialize_note, can_modify_note
mcp = FastMCP("keep")
@mcp.tool()
def find(query="") -> str:
"""
Find notes based on a search query.
Args:
query (str, optional): A string to match against the title and text
Returns:
str: JSON string containing the matching notes with their id, title, text, pinned status, color and labels
"""
keep = get_client()
notes = keep.find(query=query, archived=False, trashed=False)
notes_data = [serialize_note(note) for note in notes]
return json.dumps(notes_data)
@mcp.tool()
def create_note(title: str = None, text: str = None) -> str:
"""
Create a new note with title and text.
Args:
title (str, optional): The title of the note
text (str, optional): The content of the note
Returns:
str: JSON string containing the created note's data
"""
keep = get_client()
note = keep.createNote(title=title, text=text)
# Get or create the keep-mcp label
label = keep.findLabel('keep-mcp')
if not label:
label = keep.createLabel('keep-mcp')
# Add the label to the note
note.labels.add(label)
keep.sync() # Ensure the note is created and labeled on the server
return json.dumps(serialize_note(note))
@mcp.tool()
def update_note(note_id: str, title: str = None, text: str = None) -> str:
"""
Update a note's properties.
Args:
note_id (str): The ID of the note to update
title (str, optional): New title for the note
text (str, optional): New text content for the note
Returns:
str: JSON string containing the updated note's data
Raises:
ValueError: If the note doesn't exist or cannot be modified
"""
keep = get_client()
note = keep.get(note_id)
if not note:
raise ValueError(f"Note with ID {note_id} not found")
if not can_modify_note(note):
raise ValueError(f"Note with ID {note_id} cannot be modified (missing keep-mcp label and UNSAFE_MODE is not enabled)")
if title is not None:
note.title = title
if text is not None:
note.text = text
keep.sync() # Ensure changes are saved to the server
return json.dumps(serialize_note(note))
@mcp.tool()
def delete_note(note_id: str) -> str:
"""
Delete a note (mark for deletion).
Args:
note_id (str): The ID of the note to delete
Returns:
str: Success message
Raises:
ValueError: If the note doesn't exist or cannot be modified
"""
keep = get_client()
note = keep.get(note_id)
if not note:
raise ValueError(f"Note with ID {note_id} not found")
if not can_modify_note(note):
raise ValueError(f"Note with ID {note_id} cannot be modified (missing keep-mcp label and UNSAFE_MODE is not enabled)")
note.delete()
keep.sync() # Ensure deletion is saved to the server
return json.dumps({"message": f"Note {note_id} marked for deletion"})
def main():
mcp.run(transport='stdio')
if __name__ == "__main__":
main()
```