notes-mcp
Click on "Install Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@notes-mcpcreate a note in MCP Inbox about project ideas"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
Notes MCP Server
A comprehensive system for creating and managing notes in Apple Notes on macOS. This project provides these main components:
MCP Server - Direct MCP (Model Context Protocol) server for creating notes via stdio/JSON-RPC
Pull Worker - Background service that processes note creation jobs from a GitHub Gist queue
Tailscale Ingress API - HTTP API endpoint (Tailnet-only) for creating notes without copy/paste
Notes Export - Local tool for exporting Apple Notes metadata/content to SQLite or JSONL
All components share the same security controls: authentication, rate limiting, folder allowlisting, confirmation modes, and audit logging.
Prerequisites
macOS (required for Apple Notes integration)
Python 3.11 or higher
Apple Notes app installed and configured
Automation permissions granted to Terminal/Python for controlling Notes (see Permissions section)
Quick install on macOS
On a Mac you can install the worker, Python env, and Tailscale ingress with one command. Secrets are stored in macOS Keychain (no plain-text files).
One command (installs to ~/notes_mcp):
curl -fsSL https://raw.githubusercontent.com/apj72/notes_mcp/main/scripts/install_macos.sh | zshOr run locally after cloning:
zsh scripts/install_macos.shWhat to expect:
Pre-flight: macOS and Xcode Command Line Tools are checked. If CLI tools are missing, you’ll be prompted to install them; re-run the installer afterward.
Prerequisites: Homebrew is installed if needed, then
git,python@3.11, and (unless skipped) Tailscale.Repo: If you used the curl one-liner, the script clones the repo to
~/notes_mcpand continues from there.Secrets: You’ll be prompted once for:
GitHub PAT (Personal Access Token with
gistscope)Gist ID (your queue Gist)
An MCP token is generated and stored in Keychain; the installer prints it once (store it for your MCP client).
Tailscale: If Tailscale isn’t logged in, the installer will tell you to open the Tailscale app and log in, then re-run with
--skip-brew(or finish Tailscale setup and run again).Services: The worker and ingress launchd agents are installed and started.
Options:
--install-dir <path>— Install to a different directory (default:~/notes_mcp).--force— Overwrite Keychain entries and reinstall.--skip-tailscale— Install the worker only; skip Tailscale and ingress.--skip-brew— Assume Homebrew and dependencies are already installed.--non-interactive— Fail if required secrets are missing (no prompts).
Verify:
# Worker status
launchctl print gui/$(id -u)/com.notes-mcp.worker
# Logs
tail -f /tmp/notes-mcp-worker.out
tail -f /tmp/notes-mcp-worker.err
# Health (local)
curl http://127.0.0.1:8443/health
# Health (tailnet; use your machine’s Tailscale name)
curl http://YOUR_TAILSCALE_NAME:8443/health
# Retrieve MCP token from Keychain
security find-generic-password -s notes_mcp_token -wUninstall:
zsh scripts/uninstall_macos.shThis unloads the launchd services and optionally removes Keychain entries (you’ll be asked before deleting secrets).
Installation
Clone or download this repository:
cd /path/to/notes_mcpCreate a virtual environment (recommended):
python3 -m venv venv
source venv/bin/activateInstall dependencies:
python3 -m pip install -e .Note: On macOS, use python3 -m pip instead of just pip if the pip command is not found.
Configuration
Initial Setup
Copy the configuration template:
cp start_worker.sh.example start_worker.shEdit
start_worker.shand replace all placeholder values with your actual tokens and IDs.Generate secure tokens:
# MCP Authentication Token openssl rand -hex 32 # Ingress API Key (for Tailscale Funnel) openssl rand -hex 32
Important: start_worker.sh is gitignored and contains sensitive secrets. Never commit it to version control.
Required Environment Variables
NOTES_MCP_TOKEN: Shared secret token for authenticationGITHUB_TOKEN: GitHub Personal Access Token (for Gist queue, requires 'gist' scope)NOTES_QUEUE_GIST_ID: Your GitHub Gist ID (from the Gist URL)
Optional Environment Variables
NOTES_MCP_ALLOWED_FOLDERS: Comma-separated list of allowed folder names (default: "MCP Inbox")NOTES_MCP_INGRESS_KEY: Ingress API key for Tailscale Funnel public accessNOTES_MCP_REQUIRE_CONFIRM: If"true", all create requests must includeconfirm: trueNOTES_QUEUE_POLL_SECONDS: Polling interval in seconds (default: 60)NOTES_MCP_MAX_JOB_AGE_SECONDS: Maximum age for jobs in seconds (default: 86400 = 24 hours)
Quick Start
Option 1: Direct MCP Server (for Cursor/IDE integration)
The server uses stdio transport (standard input/output) for MCP communication. Run it directly:
python3 -m notes_mcp.serverNote: On macOS, use python3 instead of python if the python command is not found.
The server will read JSON-RPC requests from stdin and write responses to stdout.
Option 2: Pull Worker (for async processing via Gist queue)
Set up environment variables (see Pull Worker section below) and run:
python3 -m notes_mcp.pull_workerOr set up as a background service:
./setup_service.shOption 3: Tailscale Ingress API (for HTTP access)
Start the ingress service:
./start_ingress.shThen expose via Tailscale:
# For Tailnet-only access (private)
sudo /Applications/Tailscale.app/Contents/MacOS/tailscale serve --bg --http=8443 http://127.0.0.1:8443
# For public access via Funnel (requires ingress key)
sudo /Applications/Tailscale.app/Contents/MacOS/tailscale funnel --bg http://127.0.0.1:8443Note: For public Funnel access, ensure NOTES_MCP_INGRESS_KEY is set in start_worker.sh and include it in requests as X-Notes-MCP-Key header.
Cursor MCP Client Configuration
To use this server with Cursor, add the following to your Cursor MCP configuration file (typically ~/.cursor/mcp.json or similar):
{
"mcpServers": {
"notes-mcp": {
"command": "python3",
"args": ["-m", "notes_mcp.server"],
"env": {
"NOTES_MCP_TOKEN": "your-secret-token-here",
"NOTES_MCP_ALLOWED_FOLDERS": "MCP Inbox,Work,Personal",
"NOTES_MCP_REQUIRE_CONFIRM": "false"
}
}
}
}Important: Make sure the NOTES_MCP_TOKEN in the Cursor config matches the token you set in your environment, or pass it via the _token field in tool calls.
Alternative: Using a Wrapper Script
If you prefer to load environment variables from a file, create a wrapper script:
#!/bin/bash
# ~/bin/notes-mcp-server.sh
source ~/.notes-mcp-env
exec python -m notes_mcp.serverThen reference it in your Cursor config:
{
"mcpServers": {
"notes-mcp": {
"command": "/Users/yourusername/bin/notes-mcp-server.sh"
}
}
}Note: In the wrapper script, use python3 instead of python if needed.
macOS Permissions
The server requires Automation permissions to control Apple Notes. When you first run the server, macOS will prompt you to grant these permissions.
Granting Permissions
Automatic Prompt: When the server first tries to create a note, macOS will show a permission dialog. Click "OK" to grant access.
Manual Setup: If you need to grant permissions manually:
Open System Settings (or System Preferences on older macOS)
Go to Privacy & Security → Automation
Find Terminal (or Python) in the list
Enable Notes in the allowed apps list
Troubleshooting: If permissions are denied:
Check System Settings → Privacy & Security → Automation
Remove and re-add the permission if needed
Restart Terminal/Python after granting permissions
API Reference
Tool: notes.create
Creates a new note in Apple Notes.
Parameters:
title(string, required): Note title (max 200 characters)body(string, required): Note body (max 50,000 characters)folder(string, optional): Target folder name (default: "MCP Inbox")account(string, optional): Target account -"iCloud"or"On My Mac"(default: "iCloud")confirm(boolean, optional): Confirmation flag (required ifNOTES_MCP_REQUIRE_CONFIRM=true)_token(string, required): Authentication token (must matchNOTES_MCP_TOKENenvironment variable)
Returns:
{
"ok": true,
"location": {
"account": "iCloud",
"folder": "MCP Inbox"
},
"reference": "timestamp - title"
}Errors:
-32000: Validation error (invalid input, rate limit, permission denied, etc.)-32601: Unknown tool or method-32700: Parse error
Manual Testing
Using Python
Create a test script test_notes_mcp.py:
import json
import subprocess
import os
# Set token
os.environ["NOTES_MCP_TOKEN"] = "test-token-123"
os.environ["NOTES_MCP_ALLOWED_FOLDERS"] = "MCP Inbox"
# Initialize request
init_request = {
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {}
}
# Create note request
create_request = {
"jsonrpc": "2.0",
"id": 2,
"method": "tools/call",
"params": {
"name": "notes.create",
"arguments": {
"title": "Test Note",
"body": "This is a test note created via MCP",
"folder": "MCP Inbox",
"account": "iCloud",
"_token": "test-token-123"
}
}
}
# Run server
proc = subprocess.Popen(
["python3", "-m", "notes_mcp.server"],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
text=True
)
# Send requests
proc.stdin.write(json.dumps(init_request) + "\n")
proc.stdin.write(json.dumps(create_request) + "\n")
proc.stdin.close()
# Read responses
for line in proc.stdout:
print(json.loads(line))Run it:
python test_notes_mcp.pyUsing echo and pipe
export NOTES_MCP_TOKEN="test-token-123"
export NOTES_MCP_ALLOWED_FOLDERS="MCP Inbox"
echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}' | python3 -m notes_mcp.server
echo '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"notes.create","arguments":{"title":"Test","body":"Test body","_token":"test-token-123"}}}' | python3 -m notes_mcp.serverPull Worker (Queue-Based Processing)
The pull worker allows you to process note creation jobs from a remote GitHub Gist queue. Jobs are signed with HMAC to prevent tampering, and the worker maintains idempotency using SQLite to prevent duplicate execution.
Overview
Queue: Private GitHub Gist with
queue.jsonl(newline-delimited JSON jobs)Results: Same Gist with
results.jsonl(appended results)Security: HMAC-SHA256 signatures on all jobs
Idempotency: SQLite database tracks processed job IDs
Reuses: All existing security controls (allowlist, confirmation, rate limiting, logging)
Job Schema
Job line (queue.jsonl):
{
"job_id": "uuid",
"created_at": "ISO-8601",
"tool": "notes.create",
"args": {
"title": "...",
"body": "...",
"folder": "MCP Inbox",
"account": "iCloud",
"confirm": true
},
"sig": "base64(hmac_sha256(canonical_json))"
}Result line (results.jsonl):
{
"job_id": "uuid",
"processed_at": "ISO-8601",
"status": "ok|denied|error",
"location": {
"account": "iCloud",
"folder": "MCP Inbox"
},
"reference": "timestamp - title",
"error": {
"code": "...",
"message": "..."
}
}Setup
1. Create a Private GitHub Gist
Go to https://gist.github.com
Create a new private gist
Add two files:
queue.jsonl(initially empty or with a comment)results.jsonl(initially empty)
Copy the Gist ID from the URL (e.g.,
https://gist.github.com/username/abc123def456→ ID isabc123def456)
2. Create a GitHub Fine-Grained Token
Go to GitHub Settings → Developer settings → Personal access tokens → Fine-grained tokens
Create a new token with:
Repository access: Only select gists
Permissions:
gists(read and write)Expiration: Set as needed
Copy the token
3. Configure Environment Variables
Edit start_worker.sh (created from start_worker.sh.example) and set:
NOTES_QUEUE_GIST_ID- Your GitHub Gist IDGITHUB_TOKEN- Your GitHub Personal Access TokenNOTES_MCP_TOKEN- Your MCP authentication tokenNOTES_MCP_ALLOWED_FOLDERS- Comma-separated list of allowed foldersNOTES_MCP_INGRESS_KEY- (Optional) Ingress API key for Tailscale Funnel
Important:
start_worker.shis gitignored and contains sensitive secrets. Never commit it.The HMAC secret defaults to
NOTES_MCP_TOKENifNOTES_QUEUE_HMAC_SECRETis not set.
4. Generate a Signed Job
Use the helper script to generate a signed job line:
python3 -m notes_mcp.sign_job \
--title "My Note Title" \
--body "Note content here" \
--folder "MCP Inbox" \
--account "iCloud" \
--confirmThis outputs a JSON line that you can paste into queue.jsonl in your Gist.
5. Run the Worker
Foreground (for testing):
python3 -m notes_mcp.pull_workerBackground with launchd (macOS):
# Automated setup
./setup_service.sh
# Or manually
cp com.notes-mcp.worker.plist ~/Library/LaunchAgents/
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.notes-mcp.worker.plistManaging the service:
Start:
launchctl kickstart -k gui/$(id -u)/com.notes-mcp.workerStop:
launchctl bootout gui/$(id -u)/com.notes-mcp.workerCheck status:
launchctl list | grep notes-mcpView logs: Check
~/Library/Logs/notes-mcp/directory
Security Notes
HMAC Signatures: Jobs are signed with HMAC-SHA256 over a canonical JSON representation. The signature covers all fields except
sigitself. This prevents anyone with gist access from injecting malicious jobs.Canonicalization: Jobs are canonicalized by:
Sorting keys alphabetically
Removing the
sigfieldUsing compact JSON (no spaces)
Least Privilege:
GitHub token should only have gists read/write permissions
Worker only reads
queue.jsonland appends toresults.jsonl(never deletes or modifies existing lines)All existing security controls (allowlist, confirmation, rate limiting) apply
Idempotency: SQLite database at
~/.notes-mcp-queue/worker.sqlite3tracks processed job IDs to prevent replay attacks.Risks:
If your GitHub token is compromised, an attacker could read/write the gist but cannot create valid jobs without the HMAC secret
If your HMAC secret is compromised, an attacker could create valid jobs (but still subject to allowlist, confirmation, rate limits)
Keep both secrets secure
Troubleshooting
GitHub 401/403 errors:
Verify
GITHUB_TOKENis set and validCheck token has gists read/write permissions
Verify token hasn't expired
Gist file missing:
Ensure
queue.jsonlandresults.jsonlexist in the gistCheck
NOTES_QUEUE_GIST_IDis correct
Invalid signature:
Verify
NOTES_QUEUE_HMAC_SECRET(orNOTES_MCP_TOKEN) matches what was used to sign the jobCheck job wasn't modified after signing
Ensure canonicalization matches (use
sign_job.pyhelper)
Already processed job:
This is expected - jobs are only processed once
Check SQLite database:
sqlite3 ~/.notes-mcp-queue/worker.sqlite3 "SELECT * FROM processed_jobs;"
Notes automation permissions:
Same as MCP server - grant in System Settings → Privacy & Security → Automation
Security Features
Authentication: All requests require a valid token via
NOTES_MCP_TOKENRate Limiting: Maximum 10 create calls per minute per token
Folder Allowlisting: Only specified folders can be used (configured via
NOTES_MCP_ALLOWED_FOLDERS)Confirmation Mode: Optional requirement for explicit confirmation on write operations
Input Validation: Title max 200 chars, body max 50,000 chars, no null bytes
AppleScript Injection Defense: User input is passed as arguments, never concatenated into script strings
Audit Logging: All actions logged to
~/Library/Logs/notes-mcp/notes-mcp.log
Audit Logging
All actions are logged to ~/Library/Logs/notes-mcp/notes-mcp.log in JSON format. Each log entry includes:
timestamp: UTC timestampaction: Action performed (e.g., "create")title_length: Length of note titlebody_length: Length of note bodyaccount: Target accountfolder: Target folderoutcome: "allowed", "denied", or "error"error: Error message (if outcome is "error" or "denied")
Note: The log does NOT include the full body content for privacy.
Troubleshooting
Common Issues
"AppleScript error: Notes got an error: Can't get account"
Ensure Notes app is installed and configured
Check that you have an iCloud account or "On My Mac" account set up in Notes
Grant Automation permissions (see Permissions section)
"Invalid or missing authentication token"
Ensure
NOTES_MCP_TOKENenvironment variable is setVerify the token matches between server and client
"Folder 'X' is not in the allowlist"
Add the folder to
NOTES_MCP_ALLOWED_FOLDERSenvironment variableOr use "MCP Inbox" which is always allowed
"Confirmation required but not provided"
Set
confirm: truein the tool call argumentsOr set
NOTES_MCP_REQUIRE_CONFIRM="false"to disable confirmation mode
"Rate limit exceeded"
Wait 60 seconds before making another request
Maximum 10 calls per minute per token
Permission Denied
Check System Settings → Privacy & Security → Automation
Ensure Terminal/Python has permission to control Notes
Restart Terminal after granting permissions
Pull Worker Issues
GitHub 401/403 errors
Verify
GITHUB_TOKENis set and validCheck token has gists read/write permissions
Verify token hasn't expired
Rate limit exceeded:
Wait 5-10 minutes and try again
Check your token's rate limit:
curl -H "Authorization: token YOUR_TOKEN" https://api.github.com/rate_limitIncrease poll interval:
export NOTES_QUEUE_POLL_SECONDS="30"
Gist file missing
Ensure
queue.jsonlandresults.jsonlexist in the gistCheck
NOTES_QUEUE_GIST_IDis correct
Invalid signature
Verify
NOTES_QUEUE_HMAC_SECRET(orNOTES_MCP_TOKEN) matches what was used to sign the jobCheck job wasn't modified after signing
Use
sign_job.pyhelper to generate correctly signed jobs
Already processed job
This is expected - jobs are only processed once (idempotency)
Check SQLite database:
sqlite3 ~/.notes-mcp-queue/worker.sqlite3 "SELECT * FROM processed_jobs;"
Viewing Logs
tail -f ~/Library/Logs/notes-mcp/notes-mcp.logTailscale Ingress API
The Tailscale Ingress API provides a secure HTTP endpoint for creating notes without requiring copy/paste of commands. It's accessible only over your Tailnet (not the public internet).
Features
Tailnet-only access - Only accessible via Tailscale VPN
HTTP API - Simple POST requests to create notes
No copy/paste - Direct API calls from iOS Shortcuts, webhooks, etc.
Optional authentication - Additional header-based auth (beyond Tailscale ACLs)
Rate limiting - 30 requests per minute per IP
Reuses all security controls - Folder allowlist, confirmation, validation
Quick Start
Install dependencies:
python3 -m pip install fastapi uvicorn pydanticStart the ingress service:
./start_ingress.shExpose via Tailscale:
sudo /Applications/Tailscale.app/Contents/MacOS/tailscale serve --bg --http=8443 http://127.0.0.1:8443Test it:
curl -X POST http://yourmachine.tailnet.ts.net:8443/notes \ -H "Content-Type: application/json" \ -d '{"title": "Test", "body": "Test content", "folder": "MCP Inbox"}'
API Endpoints:
GET /health- Health checkPOST /notes- Create a note (requiresX-Notes-MCP-Keyheader ifNOTES_MCP_INGRESS_KEYis set)
Request format:
{
"title": "Note Title",
"body": "Note content",
"folder": "MCP Inbox",
"account": "iCloud"
}Notes Export
The export tool allows you to read and export Apple Notes content locally for search, summarization, or backup purposes.
Features
Local-only - All data stays on your Mac
Privacy-first - Metadata-only export by default (bodies excluded unless explicitly requested)
Multiple formats - Export to SQLite or JSONL
Filtering - Filter by date, limit count, include/exclude bodies
Tag extraction - Automatically extracts tags from title prefixes (e.g.,
[WORK])All folders - Export all notes regardless of folder allowlist
Duplicate detection - Identify duplicate notes based on title and body similarity
Usage
# Export metadata only from allowed folders (default)
python3 -m notes_mcp.export_notes
# Export ALL notes from ALL folders (for organization/cleanup)
python3 -m notes_mcp.export_notes --all-folders --max-notes 0 --include-body
# Export with duplicate detection
python3 -m notes_mcp.export_notes --all-folders --find-duplicates --include-body
# Export to SQLite for analysis
python3 -m notes_mcp.export_notes --all-folders --format sqlite --include-body --find-duplicatesOrganizing and Cleaning Notes
To organize all your notes and find duplicates:
# Export all notes with bodies and duplicate detection
python3 -m notes_mcp.export_notes --all-folders --max-notes 0 --include-body --find-duplicates --format sqlite
# Then query the SQLite database
sqlite3 .data/notes_export.db "SELECT * FROM notes_export WHERE is_duplicate = 1 ORDER BY duplicate_group, modified_at"Output files are saved to .data/ directory (gitignored).
Development
Container Deployment
Notes MCP can be run in Podman or Docker containers using a hybrid architecture:
Container: Runs ingress API and/or pull worker (handles HTTP, GitHub Gist, queue management)
Host Bridge Service: Runs on macOS host (handles Apple Notes via AppleScript)
For deployment, see DEPLOYMENT.md for the minimum artifacts in this repo.
Project Structure
notes_mcp/
├── pyproject.toml # Project configuration
├── README.md # This file
├── DEPLOYMENT.md # What's needed to deploy (minimum artifacts)
├── start_worker.sh.example # Configuration template (copy to start_worker.sh)
├── start_ingress.sh.example # Ingress template (copy to start_ingress.sh)
├── setup_service.sh # Worker service setup (launchd)
├── gpt_action.json.example # Custom GPT schema (copy to gpt_action.json)
├── openapi-schema.json # OpenAPI schema
├── CUSTOM_GPT_CONFIG.md # Custom GPT setup guide
├── NOTE_TAGS.md # Tags (hashtags) feature
├── APPLE_NOTES_FORMATTING_SUPPORT.md # HTML formatting in Notes
├── docs/
│ └── launchd/ # Launchd plists (ingress, export)
└── src/
└── notes_mcp/
├── __init__.py
├── server.py # MCP server (stdio)
├── pull_worker.py # Queue-based worker
├── ingress.py # Tailscale ingress API
├── applescript.py # AppleScript integration
├── bridge_client.py # Bridge client (container)
├── bridge_server.py # Bridge server (host)
├── export_notes.py # Notes export tool
├── security.py
├── logging.py
├── formatting.py
├── sign_job.py
└── enqueue_job.pyConfig files with secrets (start_worker.sh, start_ingress.sh, gpt_action.json, plists) are gitignored. Helper scripts, container files, and experimental docs live in _unsynced/ (gitignored).
Current Status
✅ Implemented:
MCP server with stdio transport
Pull worker with GitHub Gist queue
Tailscale Ingress API (HTTP endpoint)
Notes export tool (local read access)
Comprehensive security controls
Launchd service setup for worker and ingress
Queue clearing and idempotency
Text formatting normalization
Rate limit handling improvements
📚 Documentation:
Complete setup guides for all components
API reference documentation
Security review
Troubleshooting guides
🔒 Security:
HMAC-signed jobs
Folder allowlisting
Rate limiting
Optional confirmation modes
Audit logging (no secrets)
Input validation
AppleScript injection defense
License
This project is provided as-is for local use.
This server cannot be installed
Maintenance
Resources
Unclaimed servers have limited discoverability.
Looking for Admin?
If you are the server author, to access and configure the admin panel.
Related MCP Connectors
AI access to your aNotepad online notes: read, search, write, and organize via 22 tools.
Search, save, and set reminders in your personal MarkIt library of links, posts, and notes.
Mobile-first AI life planner — turn goals into daily, synced tasks from your AI assistant.
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/apj72/notes_mcp'
If you have feedback or need assistance with the MCP directory API, please join our Discord server