evernote-mcp-server is a Python MCP server that exposes Evernote notebook and note operations over MCP stdio.
It is primarily documented for people who want to use it from Gemini CLI with the published Docker image. Contributor and maintainer workflows appear later in this README so the first-run path stays simple.
This server exposes Evernote operations as MCP tools. The current tool surface includes:
- Notebook tools:
list_notebooks - Read tools:
search_notes,get_note,get_note_metadata - Write tools:
append_to_note_plaintext,insert_into_note_plaintext,set_note_title,add_tags_by_name,move_note,create_note,delete_note
insert_into_note_plaintext supports non-destructive middle insertion for rich notes by fetching the current ENML, inserting an escaped plaintext block before or after a visible text anchor, and saving the full ENML document back to Evernote with optimistic concurrency. It does not accept raw ENML or HTML.
READ_ONLY=trueby default, so write tools are blocked unless you explicitly opt in.- One-time OAuth bootstrap is required before normal runtime startup can talk to Evernote.
- OAuth access tokens are not stored in
.env; they are saved in the Evernote MCP config directory. - Only
stdiotransport is implemented in v0.1.--transport sseis intentionally not available yet.
Most people should use the released Docker path. It is the shortest route to a working Gemini MCP server and does not require cloning this repository.
- Use Quick Start: Released Docker + Gemini if you want the default end-user setup.
- Use Contributor Setup if you are changing code, running tests, or debugging locally.
- Use Native Windows Local Python Setup if you want Gemini CLI to launch a cloned repo through
.venv\Scripts\python.exewithout WSL, Bash, or Docker. - Use Troubleshooting if setup fails after you follow the matching path below.
These settings apply across all runtime paths. Keep this section as the canonical reference for configuration behavior.
Every runtime path needs the same Evernote-side prerequisites:
- Evernote consumer credentials:
EVERNOTE_CONSUMER_KEYandEVERNOTE_CONSUMER_SECRET - A browser-capable environment for one-time OAuth authorization on localhost
| Variable | Required | Default | Purpose |
|---|---|---|---|
EVERNOTE_CONSUMER_KEY |
Required for auth |
None | Evernote OAuth consumer key |
EVERNOTE_CONSUMER_SECRET |
Required for auth |
None | Evernote OAuth consumer secret |
EVERNOTE_SANDBOX |
No | false |
Use Evernote sandbox OAuth and API endpoints |
READ_ONLY |
No | true |
Block write tools unless set to false |
LOG_LEVEL |
No | INFO |
Process log verbosity |
Accepted boolean values for EVERNOTE_SANDBOX and READ_ONLY:
true/false1/0yes/noon/off
- Released Docker path uses a host env file at
~/.config/evernote-mcp-server/evernote-mcp.env - Contributor local paths use a repository-local
.env, usually created from.env.example
OAuth bootstrap saves the Evernote access token in the Evernote MCP config directory, not in .env.
- On a normal local machine, the saved token lives at
$XDG_CONFIG_HOME/evernote-mcp-server/token.jsonwhenXDG_CONFIG_HOMEis set - Otherwise it lives at
~/.config/evernote-mcp-server/token.json - In the released Docker path, the recommended persistence model is a Docker named volume mounted at
/home/appuser/.config/evernote-mcp-server
This separation matters because startup reads the saved token from disk. If the token is missing, normal server startup fails until you run OAuth bootstrap again.
This is the recommended path for most users. It configures Gemini CLI to run the published GHCR image, creates a persistent env file if needed, and creates a Docker volume for saved OAuth state.
Before you run the installer, make sure these local tools are available:
bashcurldockerpython3- Gemini CLI
Run the release installer to configure Gemini with the latest published v* image tag.
curl -fsSL https://raw.githubusercontent.com/CopyPasteFail/evernote-mcp-server/main/scripts/install_gemini_mcp_release.sh | bashThe installer does three important things for this path:
- configures Gemini MCP settings to use the published GHCR image
- creates
~/.config/evernote-mcp-server/evernote-mcp.envif it does not exist - creates Docker volume
evernote-mcp-authif it does not exist
Edit the generated env file before you try OAuth bootstrap. At minimum, fill in the Evernote consumer credentials. Leave READ_ONLY=true unless you intentionally want write tools enabled.
- Env file:
~/.config/evernote-mcp-server/evernote-mcp.env
After the env file is populated, initialize Docker volume ownership once, then run the one-time OAuth bootstrap command. The installer prints both commands with the configured image tag and volume.
Docker named volumes may initially be owned by root, while this image runs as non-root appuser. Initializing the volume ownership once avoids token save failures:
docker run --rm --user root \
-v evernote-mcp-auth:/home/appuser/.config/evernote-mcp-server \
ghcr.io/CopyPasteFail/evernote-mcp-server:latest \
python -c "import os; p='/home/appuser/.config/evernote-mcp-server'; os.makedirs(p, exist_ok=True); os.chown(p, 1000, 1000); os.chmod(p, 0o700)"Then run container auth:
docker run --rm -it \
--env-file ~/.config/evernote-mcp-server/evernote-mcp.env \
-v evernote-mcp-auth:/home/appuser/.config/evernote-mcp-server \
-p 8765:8765 \
ghcr.io/CopyPasteFail/evernote-mcp-server:latest \
python -m evernote_mcp auth \
--listen-host 0.0.0.0 \
--listen-port 8765 \
--callback-url http://127.0.0.1:8765/callbackOAuth bootstrap opens a browser for Evernote authorization and then saves the token into the mounted config volume so Gemini can reuse it on later runs.
Why this works: the listener binds inside the container (0.0.0.0:8765) while Evernote redirects to a host-visible callback URL (127.0.0.1:8765) through the published Docker port.
WSL Note: Browser auto-open may still fail in some environments; the CLI prints the authorization URL so you can open it manually.
Use this path if you are iterating on code, running tests, or debugging the server locally.
Pick the local runtime you want later, but the repository bootstrap is shared:
- Git
- Python 3.13
- Gemini CLI if you want Gemini to launch your local runtime
- Docker only if you choose the local Docker runtime
Clone the repository and create the local env file before choosing a runtime path.
git clone https://github.com/CopyPasteFail/evernote-mcp-server.git
cd evernote-mcp-server
cp .env.example .envAfter that shared setup, choose one local runtime.
Use this path when you want the fastest edit-run-test loop and direct access to the Python process.
On native Windows, follow the dedicated Native Windows Local Python Setup instead of the POSIX shell commands below.
python3.13 -m venv .venv
source .venv/bin/activate
pip install -r requirements-dev.txt
PYTHONPATH=src python -m evernote_mcp auth
python3 scripts/install_gemini_mcp.py --mode pythonThis path installs development dependencies, runs OAuth bootstrap against your local environment, and optionally updates Gemini settings so Gemini can launch the local Python runtime. Do not use a manually launched --transport stdio process as an end-to-end test; stdio MCP expects JSON-RPC from a client.
The local installer writes Gemini CLI settings. Before running it, confirm Gemini CLI is installed with gemini --version or Get-Command gemini, then run gemini once and sign in with the Google account that has your Gemini subscription. Use --print-config when you only want JSON to inspect or copy into another MCP client manually.
Use this path when you want local code changes packaged in a container while keeping the same general runtime model as Docker-based usage.
docker build -t evernote-mcp-server:local .
docker volume create evernote-mcp-authInitialize volume ownership once:
docker run --rm --user root \
-v evernote-mcp-auth:/home/appuser/.config/evernote-mcp-server \
evernote-mcp-server:local \
python -c "import os; p='/home/appuser/.config/evernote-mcp-server'; os.makedirs(p, exist_ok=True); os.chown(p, 1000, 1000); os.chmod(p, 0o700)"Then run container auth:
docker run --rm -it \
--env-file .env \
-v evernote-mcp-auth:/home/appuser/.config/evernote-mcp-server \
-p 8765:8765 \
evernote-mcp-server:local \
python -m evernote_mcp auth \
--listen-host 0.0.0.0 \
--listen-port 8765 \
--callback-url http://127.0.0.1:8765/callbackThen configure Gemini for local Docker mode:
python3 scripts/install_gemini_mcp.py --mode dockerAfter OAuth bootstrap and Gemini setup, runtime uses the same Docker volume
You can sanity-check that the volume now has the token by running:
docker run --rm -it \
-v evernote-mcp-auth:/home/appuser/.config/evernote-mcp-server \
evernote-mcp-server:local \
python -c "from pathlib import Path; p=Path('/home/appuser/.config/evernote-mcp-server/token.json'); print(p.exists(), p)"It should print True and the path.
After you finish either setup path, verify the server with a simple read-only call before you try anything more complex.
Use a Gemini prompt like this:
Use MCP server "evernote-mcp-server" and call list_notebooks. Return the names of 2 notebooks.
If that works, the MCP wiring, Evernote auth, and saved token state are all in the expected place.
A second sanity check is to keep READ_ONLY=true at first. That confirms write tools stay blocked until you intentionally enable them.
If OAuth bootstrap fails because credentials are missing, verify that both of these are set in the env file for your chosen path:
EVERNOTE_CONSUMER_KEYEVERNOTE_CONSUMER_SECRET
If startup fails because the Evernote token is missing, run OAuth bootstrap again using the same env file and the same persistence location for your chosen path.
If the saved token was created with a different sandbox setting than the current EVERNOTE_SANDBOX value, re-run OAuth bootstrap with the sandbox mode you actually want to use.
If write tools fail, check READ_ONLY. The secure default is true, so writes remain blocked until you set READ_ONLY=false and restart the runtime that reads that env file.
sse transport is intentionally not implemented in v0.1. Use --transport stdio.
These steps are for ongoing development after your local setup already works.
Run the full local check suite before pushing changes.
make lint
make security
make test
make checkRun the container vulnerability scan separately when you need to validate the Docker image locally without making it part of the default contributor loop.
make container-securityThis target builds evernote-mcp-server:local and runs a Trivy image scan that
fails on HIGH or CRITICAL findings.
This optional check requires local Docker and trivy to be installed.
Local Trivy runs are intentionally stricter than GitHub Actions and may fail on
unfixed base-image vulnerabilities that CI ignores.
If you want Git to run make check automatically before pushes to main, point Git at the repository hook path.
git config core.hooksPath .githooks
chmod +x .githooks/pre-pushThis section is intentionally separate from first-time setup and contributor onboarding.
Use the release script to create and push the next semantic version tag from main.
./scripts/release.sh patchSupported bump types are patch, minor, and major.
If tag push is rejected with GH013 and creations being restricted, your repository tag ruleset is blocking tag creation for your actor. Add a bypass actor (maintainer user/team/app) to the v* tag ruleset, then run the release command again.
The release process is guarded:
- releases are created only from tags matching
v* - the script requires a clean working tree on
main - the script fetches
origin/mainand refuses to continue if localHEADdoes not match it make checkmust pass before the tag is created- the GitHub release workflow verifies the tagged commit is reachable from
origin/main, rerunsmake check, then publishes the GHCR image and GitHub release
These scripts are maintainer-only repository hardening utilities and are typically only need to be run once per repository:
Suggested one-time setup for Dependabot defaults and vulnerability alerts:
./scripts/setup-dependabot.shSuggested one-time setup for release tag protection rules:
./scripts/setup-tag-protection.shImportant: tag protection rules that restrict creation must include an explicit bypass actor for the maintainer identity that runs ./scripts/release.sh, otherwise all tag pushes are blocked.
Both scripts use an authenticated GitHub CLI session to apply repository settings.
This README is intentionally focused on onboarding and common workflows. For deeper design and implementation details, read docs/ARCHITECTURE.md.
That document covers:
- transport abstraction and the current
stdio/ futuressesplit - how MCP tools are registered and exposed to clients
- the security model, including token persistence and write-policy enforcement
- the Evernote Thrift integration rationale
- release-model details and future improvement areas