Evernote MCP Server
A Python MCP server that gives AI assistants full access to your Evernote account. It provides 13 tools for searching, reading, creating, updating, and deleting notes; managing tags and notebooks; handling attachments and encrypted content.
README
Evernote MCP Server
A Python MCP (Model Context Protocol) server that gives AI assistants full access to your Evernote account. 13 tools for searching, reading, creating, updating, and deleting notes; managing tags and notebooks; handling attachments and encrypted content; monitoring write operations.
Requires Python 3.12+.
Quick Start
# Install
pip install -e .
# Authenticate (one-time, opens browser)
python src/evernote_client.py --auth
# Run the server
python src/evernote_mcp.py
Authentication
The server uses OAuth 1.0a to access your Evernote account. You need to authenticate once; the token lasts approximately one year.
Creating the Token
python src/evernote_client.py --auth
This will:
- Open your default browser to Evernote's authorization page
- Start a callback server on
localhost:8080 - After you authorize, Evernote redirects back to the callback
- The token is saved to
.evernote-token.jsonin the project root
If the browser flow fails (e.g., running on a headless server), the script will prompt you to paste the oauth_verifier value manually after a 5-minute timeout.
Token File
The token is stored in .evernote-token.json:
{
"access_token": "S=s46:U=...:E=...:H=...",
"created": 1771265208024,
"expires": 1802801208024
}
access_token— the OAuth token string used for all API callscreated— Unix milliseconds when the token was obtainedexpires— Unix milliseconds when the token expires (~1 year)
When the token expires, re-run python src/evernote_client.py --auth to get a new one.
Verifying the Token
python src/evernote_client.py --test
This tests the connection and prints your username, user ID, and notebook count.
Configuration
Command-Line Arguments
Transport mode (mutually exclusive; stdio is the default):
| Flag | Default | Description |
|---|---|---|
--stdio |
on (when no other transport specified) | stdio transport for Claude Code / MCP client integration |
--http or --sse |
off | HTTP/SSE server |
--https |
off | HTTPS/SSE server (requires --ssl-cert and --ssl-key) |
HTTP options:
| Flag | Default | Description |
|---|---|---|
--http-port |
8082 |
HTTP server port |
--http-host |
127.0.0.1 |
HTTP server bind address |
HTTPS options:
| Flag | Default | Description |
|---|---|---|
--https-port |
8443 |
HTTPS server port |
--https-host |
127.0.0.1 |
HTTPS server bind address |
--ssl-cert |
(required) | Path to SSL certificate file |
--ssl-key |
(required) | Path to SSL private key file |
Token and queue:
| Flag | Default | Description |
|---|---|---|
--token-file |
.evernote-token.json |
Path to the OAuth token file |
--queue-pace |
10 |
Seconds between queued write operations |
Environment Variables
| Variable | Default | Description |
|---|---|---|
EVERNOTE_TOKEN_FILE |
.evernote-token.json |
Path to the token file (overridden by --token-file) |
EVERNOTE_QUEUE_PACE_SECONDS |
10 |
Write queue pacing (overridden by --queue-pace) |
EVERNOTE_CONSUMER_KEY |
mcpserver-6972 |
OAuth consumer key |
EVERNOTE_CONSUMER_SECRET |
(built-in) | OAuth consumer secret |
EVERNOTE_SANDBOX |
false |
Set to true for sandbox (note: Evernote sandbox is decommissioned) |
Claude Code Integration
Add this to your .mcp.json (or Claude Code settings):
{
"mcpServers": {
"evernote": {
"command": "python",
"args": ["/path/to/evernote-mcp/src/evernote_mcp.py"],
"env": {
"EVERNOTE_TOKEN_FILE": "/path/to/evernote-mcp/.evernote-token.json"
}
}
}
}
Usage Examples
# stdio mode (default, for Claude Code)
python src/evernote_mcp.py
# HTTP on a custom port
python src/evernote_mcp.py --http --http-port 9000
# HTTPS with SSL certificates
python src/evernote_mcp.py --https --ssl-cert cert.pem --ssl-key key.pem
# Custom token file and faster queue pacing
python src/evernote_mcp.py --token-file /secure/token.json --queue-pace 5
Tools Reference
Summary
| Tool | Type | Description |
|---|---|---|
list_notebooks |
Read | List all notebooks |
list_tags |
Read | List all tags with hierarchy |
search_notes |
Read | Search notes; optionally get counts per notebook/tag |
get_note |
Read | Read a note as Markdown; version history; related notes |
get_attachment |
Read | Get resource metadata, data, and OCR text |
get_user_info |
Read | Account info, premium status, upload quota |
create_note |
Write | Create a note from Markdown or clone an existing note |
update_note |
Write | Edit content, metadata, tags, attachments |
delete_note |
Write | Move a note to trash (recoverable 30 days) |
manage_tags |
Write | Create, rename, or delete tags |
add_attachment |
Write | Attach a file to an existing note |
manage_notebooks |
Write/Read | Create, update notebooks; get default notebook |
check_queue |
Queue | Monitor and manage queued write operations |
All write tools go through a paced queue to avoid Evernote rate limits. If the queue is empty and the pacing interval has elapsed, writes execute immediately. Otherwise they are queued and processed in the background.
All tools return {"success": true, ...} on success or {"success": false, "error": "...", "error_type": "..."} on failure.
Read Tools
list_notebooks
List all Evernote notebooks.
Parameters: None
Returns: notebooks (list of {guid, name, stack, defaultNotebook, created, updated}), count
list_tags
List all tags with parent hierarchy and full paths.
Parameters: None
Returns: tags (list of {guid, name, parentGuid, parentName, path}), count
The path field shows the full hierarchy, e.g. "Parent/Child/Grandchild".
search_notes
Search notes using Evernote's search grammar. Can also return note counts per notebook and tag instead of note results.
| Parameter | Type | Default | Description |
|---|---|---|---|
query |
str | "" |
Search query (see Search Grammar) |
notebook |
str | "" |
Scope search to this notebook (name or GUID) |
max_results |
int | 25 |
Maximum notes to return |
offset |
int | 0 |
Pagination offset |
counts_only |
bool | False |
Return note counts per notebook/tag instead of note list |
Returns (normal): notes (list of {guid, title, created, updated, notebookGuid, notebookName, tags, ...}), totalNotes, offset, hasMore
Returns (counts_only): notebookCounts (dict of name to count), tagCounts, trashCount
get_note
Read a note's content as Markdown. Also supports version history (premium) and finding related notes.
| Parameter | Type | Default | Description |
|---|---|---|---|
guid |
str | required | Note GUID |
include_content |
bool | True |
Include note body |
passphrase |
str | "" |
Decrypt <en-crypt> blocks inline with this passphrase |
list_versions |
bool | False |
Return version history instead of content (premium) |
version |
int | 0 |
Return a specific version by updateSequenceNum (premium) |
find_related |
bool | False |
Include related notes and tags |
Returns (normal): note with {guid, title, content_markdown, notebookGuid, notebookName, tags, created, updated, attributes, resources}
Returns (list_versions): versions (list of {updateSequenceNum, updated, saved, title})
Returns (version > 0): The note content at that specific version
Returns (find_related): Normal note response with an additional related field containing related notes and tags
get_attachment
Get resource/attachment metadata and optionally its binary data or OCR recognition text.
Identify the resource by EITHER resource_guid OR the combination of note_guid + resource_hash.
| Parameter | Type | Default | Description |
|---|---|---|---|
resource_guid |
str | "" |
Direct resource GUID |
note_guid |
str | "" |
Note GUID (use with resource_hash) |
resource_hash |
str | "" |
MD5 hex hash of the resource |
include_data |
bool | False |
Include base64-encoded file data |
include_recognition |
bool | False |
Include OCR/recognition text |
Returns: resource with {guid, noteGuid, mime, hash, size, filename, width, height} plus data_base64 (if requested) and recognition (if requested)
get_user_info
Get account information, premium status, upload quota, and sync state.
Parameters: None
Returns: username, email, name, userId, privilege, accounting (upload limits), premium (status, expiration), syncState (current time, update count, uploaded bytes)
Write Tools
All write tools are submitted through a paced queue. When executed immediately, they return the operation result directly. When queued, they return:
{
"success": true,
"queued": true,
"job_id": "abc12345",
"message": "Queued. Use check_queue to monitor.",
"queue_position": 1
}
create_note
Create a new note from Markdown content, or clone an existing note.
| Parameter | Type | Default | Description |
|---|---|---|---|
title |
str | required | Note title |
content |
str | "" |
Note body in Markdown |
notebook |
str | "" |
Target notebook (name or GUID; uses default if empty) |
tags |
str | "" |
Comma-separated tag names (auto-created if they don't exist) |
attachment_paths |
str | "" |
Comma-separated local file paths to attach |
copy_from_guid |
str | "" |
Clone this note instead of creating from content |
When copy_from_guid is set, the title, content, and tags parameters are ignored. The notebook parameter can override the destination notebook.
Returns: note with {guid, title, notebookGuid, notebookName, created}
update_note
Update an existing note's content, metadata, or both. Automatically uses a lightweight metadata-only path (1 API call) when only metadata changes, or a full content path (2 API calls) when content or attachments change.
| Parameter | Type | Default | Description |
|---|---|---|---|
guid |
str | required | Note GUID |
title |
str | "" |
New title (empty = keep current) |
content |
str | "" |
New Markdown content (empty = keep current) |
tags |
str | "" |
Comma-separated tag names, replaces all (empty = keep current) |
notebook |
str | "" |
Move to this notebook (empty = keep current) |
created |
str | "" |
Override created date (ISO or YYYYMMDD) |
reminder_time |
str | "" |
Set reminder (ISO or YYYYMMDD) |
author |
str | "" |
Set author attribute |
source_url |
str | "" |
Set source URL attribute |
latitude |
float | None | Set latitude |
longitude |
float | None | Set longitude |
attachment_paths |
str | "" |
Comma-separated local file paths to attach |
Returns: note with updated fields, update_type ("metadata_only" or "full_content")
delete_note
Move a note to the trash. Recoverable for 30 days through the Evernote UI.
| Parameter | Type | Default | Description |
|---|---|---|---|
guid |
str | required | Note GUID |
Returns: guid, deleted: true
manage_tags
Create, rename, or delete tags.
| Parameter | Type | Default | Description |
|---|---|---|---|
action |
str | required | "create", "rename", or "delete" |
name |
str | "" |
Tag name (for create, or lookup for rename/delete) |
guid |
str | "" |
Tag GUID (alternative to name for rename/delete) |
new_name |
str | "" |
New name (for rename) |
parent |
str | "" |
Parent tag name or GUID (for create) |
add_attachment
Attach a file to an existing note. Provide either a local file path OR base64 data with filename and MIME type.
| Parameter | Type | Default | Description |
|---|---|---|---|
note_guid |
str | required | Note GUID to attach to |
file_path |
str | "" |
Path to a local file |
data_base64 |
str | "" |
Base64-encoded file data (alternative to file_path) |
filename |
str | "" |
Filename (required with data_base64) |
mime_type |
str | "" |
MIME type (required with data_base64, e.g. "image/png") |
manage_notebooks
Create or update notebooks, or get the default notebook.
| Parameter | Type | Default | Description |
|---|---|---|---|
action |
str | required | "create", "update", or "get_default" |
name |
str | "" |
Notebook name |
guid |
str | "" |
Notebook GUID (for update) |
new_name |
str | "" |
New name (for update) |
stack |
str | "" |
Stack name (for create/update) |
The get_default action is a read operation and executes immediately (bypasses the write queue).
Queue Tool
check_queue
Monitor and manage queued write operations.
| Parameter | Type | Default | Description |
|---|---|---|---|
job_id |
str | "" |
Check a specific job (empty = show all) |
status |
str | "" |
Filter: "pending", "processing", "completed", "failed" |
clear_completed |
bool | False |
Remove completed/failed jobs from history |
Returns: jobs list, count, summary (counts per status), paused (bool), pause_remaining_seconds, pace_seconds
Architecture
MCP Client (Claude Code, etc.)
|
v stdio / HTTP+SSE / HTTPS+SSE
evernote_mcp.py -- FastMCP server, 13 tool definitions, transport setup
|
+---> write_queue.py -- Paced write queue, background thread, JSON persistence
|
+---> evernote_client.py -- EvernoteAPI class, OAuth, SDK wrapper, all API methods
|
+---> enml_converter.py -- ENML <-> Markdown conversion, en-crypt decryption
Source Files
src/evernote_mcp.py — Server entry point. Defines all 13 @mcp.tool() functions, argparse for multi-transport startup, and write executor functions that the queue calls. Read tools call the API directly; write tools submit to the queue.
src/evernote_client.py — EvernoteAPI class wrapping the Evernote SDK. All methods return plain dicts. Handles OAuth flow, token persistence, rate limit detection, RTE (Real-Time Editing) conflict detection, name/GUID resolution caches, and transient error retry with exponential backoff.
src/enml_converter.py — Bidirectional ENML-to-Markdown converter. Notes are stored as ENML in Evernote but the MCP server accepts and returns Markdown. Resources are referenced as . Also handles AES-128-CBC decryption of <en-crypt> blocks.
src/write_queue.py — Persistent paced queue for all write operations. Default pace: 10 seconds between API calls. If Evernote returns a rate limit, the queue pauses for the server-specified duration. Queue state is persisted to .evernote-queue.json so pending jobs survive server restarts.
Key Patterns
Write queue pacing — Write operations are paced to avoid Evernote's rate limits. The default is 10 seconds between writes (--queue-pace or EVERNOTE_QUEUE_PACE_SECONDS). If the queue is empty and enough time has passed since the last write, operations execute immediately. Otherwise they are queued and processed by a background thread.
Rate limit vs. RTE conflict — Evernote's API returns RATE_LIMIT_REACHED for two different situations. True rate limits use long durations (900-3600 seconds). When a note is open in the Evernote desktop client (Real-Time Editing), the API also returns RATE_LIMIT_REACHED but with short durations (<300 seconds). The server distinguishes these using a 300-second threshold: short durations raise RTEConflictError, long durations raise RateLimitError.
Metadata vs. content updates — update_note automatically selects the most efficient path. If only metadata is changing (title, tags, notebook, dates), it uses a single API call that avoids RTE conflicts. If content or attachments are changing, it uses a two-call path (get + update).
ENML conversion — Notes are stored as ENML (a restricted XML format) in Evernote. The server converts to/from Markdown automatically. Resources (images, files) are referenced using evernote-resource:<md5hash> URLs. The converter handles checkboxes (<en-todo>), encrypted blocks (<en-crypt>), and strips disallowed HTML tags.
Search Grammar
The search_notes tool accepts Evernote's search grammar:
| Operator | Example | Description |
|---|---|---|
notebook: |
notebook:"My Notebook" |
Search within a specific notebook |
tag: |
tag:important |
Notes with this tag |
-tag: |
-tag:archive |
Notes without this tag |
intitle: |
intitle:meeting |
Search in note titles only |
created: |
created:20240101 |
Notes created on or after this date (YYYYMMDD) |
updated: |
updated:20240601 |
Notes updated on or after this date |
resource: |
resource:image/png |
Notes containing this MIME type |
todo: |
todo:true |
Notes with unchecked checkboxes |
* |
meet* |
Wildcard (matches "meeting", "meetings", etc.) |
"..." |
"exact phrase" |
Exact phrase match |
any: |
any: cat dog |
Match any term (default is all) |
- |
-archive |
Exclude term |
Operators can be combined: notebook:"Work" tag:urgent intitle:project created:20240101
Error Handling
All tools return a consistent response format:
{"success": true, "...": "..."}
On error:
{"success": false, "error": "description", "error_type": "ERROR_CODE"}
| Error Type | Cause | Resolution |
|---|---|---|
RATE_LIMITED |
Evernote API quota exhausted | Wait for retry_after_seconds (included in response) |
RTE_CONFLICT |
Note is open in the Evernote desktop client | Close the note in Evernote and retry |
NOT_FOUND |
Notebook or resource not found | Check the name/GUID |
ERROR |
Other errors | Check the error message for details |
Queued writes that fail are marked as "failed" in the queue. Use check_queue to see failed jobs and their error messages.
Development
Setup
pip install -e ".[dev]"
Running Tests
# All tests
python -m pytest tests/ -v
# With coverage report
python -m pytest tests/ --cov=src --cov-report=term-missing
# Integration tests (requires valid token)
EVERNOTE_INTEGRATION=1 python -m pytest tests/test_integration.py -v
Test Structure
| File | Tests | Covers |
|---|---|---|
test_enml_converter.py |
40 | ENML/Markdown conversion, decryption |
test_evernote_client.py |
69 | API wrapper, OAuth, rate limits, all SDK methods |
test_evernote_mcp.py |
92 | MCP tool functions, executors, error handling |
test_write_queue.py |
29 | Queue logic, persistence, pacing, background processing |
test_integration.py |
3 | Real API calls (skipped by default) |
230 unit tests, 95%+ code coverage. Tests mock the Evernote SDK at the note_store/user_store level for client tests, and mock EvernoteAPI methods for MCP tool tests.
Dependencies
Runtime: evernote3, oauth2, mcp, fastmcp, markdownify, markdown-it-py, beautifulsoup4, lxml, cryptography, uvicorn
Development: pytest, pytest-mock, pytest-cov
License
Copyright (c) 2026 Sean Martin. MIT License. See LICENSE for details.
Recommended Servers
playwright-mcp
A Model Context Protocol server that enables LLMs to interact with web pages through structured accessibility snapshots without requiring vision models or screenshots.
Magic Component Platform (MCP)
An AI-powered tool that generates modern UI components from natural language descriptions, integrating with popular IDEs to streamline UI development workflow.
Audiense Insights MCP Server
Enables interaction with Audiense Insights accounts via the Model Context Protocol, facilitating the extraction and analysis of marketing insights and audience data including demographics, behavior, and influencer engagement.
VeyraX MCP
Single MCP tool to connect all your favorite tools: Gmail, Calendar and 40 more.
graphlit-mcp-server
The Model Context Protocol (MCP) Server enables integration between MCP clients and the Graphlit service. Ingest anything from Slack to Gmail to podcast feeds, in addition to web crawling, into a Graphlit project - and then retrieve relevant contents from the MCP client.
Kagi MCP Server
An MCP server that integrates Kagi search capabilities with Claude AI, enabling Claude to perform real-time web searches when answering questions that require up-to-date information.
E2B
Using MCP to run code via e2b.
Neon Database
MCP server for interacting with Neon Management API and databases
Exa Search
A Model Context Protocol (MCP) server lets AI assistants like Claude use the Exa AI Search API for web searches. This setup allows AI models to get real-time web information in a safe and controlled way.
Qdrant Server
This repository is an example of how to create a MCP server for Qdrant, a vector search engine.