Featured

Deploy OpenClaw in 60 seconds — 20% off logoDeploy OpenClaw in 60 seconds — 20% off

Launch OpenClaw on Hostinger in about 60 seconds and keep your agent live 24/7. Our referral link gives you 20% off, no coupon code needed.

Launch on Hostinger
Run your Hermes agent on Hostinger, fully managed logoRun your Hermes agent on Hostinger, fully managed

Launch Hermes on Hostinger in one click, fully managed, no VPS knowledge needed. Use code ZACAARON10 for 10% off.

Launch on Hostinger
Crawl and scrape any site into clean data, 10% off logoCrawl and scrape any site into clean data, 10% off

Firecrawl crawls and scrapes any site into clean markdown for your agent. Get 1,000 free credits, and new users get 10% off their first purchase.

Try Firecrawl free
6,000+ web scrapers for your AI agent, start free logo6,000+ web scrapers for your AI agent, start free

Apify gives your agent live web data: 6,000+ prebuilt scrapers and actors, MCP-ready. Sign up free with $5 in usage credits.

Try Apify free
One API to scrape, enrich, and extract the internet. logoOne API to scrape, enrich, and extract the internet.

Context.dev gives your agents a single API to scrape, enrich, and extract live web data — no proxies, no parsers, no maintenance.

Start building free
SetupClaw: done-for-you OpenClaw for founders & exec teams logoSetupClaw: done-for-you OpenClaw for founders & exec teams

White-glove OpenClaw for founders and exec teams (4–50+ employees): we install, harden, integrate your tools, and maintain it — secured from day one.

Get it set up for you
SEO data APIs for your agent, $1 free credit logoSEO data APIs for your agent, $1 free credit

DataForSEO gives your agent live access to SERP results, keyword data, backlinks, and on-page SEO data through one API. New accounts get a $1 credit, good for up to 20,000 keyword or backlink lookups.

Try DataForSEO free
Reach 48,000+ AI builders

A flat monthly placement in front of developers actively installing AI tools. No lock-in, cancel anytime.

Advertise here

Works with

Claude CodeClaude DesktopCursorVS CodeClineCodex CLIOpenClaw+ any MCP client

Install to Claude Code

This server doesn't publish a one-line install command. Follow the setup in the source repository.

Summary

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.md

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:

  1. Open your default browser to Evernote's authorization page
  2. Start a callback server on localhost:8080
  3. After you authorize, Evernote redirects back to the callback
  4. The token is saved to .evernote-token.json in 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 calls
  • created — Unix milliseconds when the token was obtained
  • expires — 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.pyEvernoteAPI 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 !filename. 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 updatesupdate_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.

See related servers & alternatives →

Related MCP servers

Browse all →

Related guides

Hand-picked reading to help you choose and use Search servers.