Readwise MCP Server
Minimal Python MCP server for Readwise integration - token-efficient, single-file implementation using FastMCP.
Features
- Token-efficient: 8 essential tools (vs 13+ in Node.js version)
- Single-file architecture: ~350 lines of code
- Proven logic: Reuses battle-tested deduplication and pagination from backfill script
- State compatibility: Preserves existing state file format
- Smart optimization: Uses synced ranges to skip unnecessary API calls
Installation
1. Clone repository
git clone https://github.com/ngpestelos/readwise-mcp-server.git
cd readwise-mcp-server
2. Create virtual environment
python3 -m venv venv
source venv/bin/activate
3. Install dependencies
pip install -r requirements.txt
4. Configure environment variables
Set these in your .mcp.json file:
READWISE_TOKEN: Your Readwise API tokenVAULT_PATH: Path to your PARA vault (e.g.,/path/to/your/vault)
Configuration
Update .mcp.json
Add or update the readwise entry in your vault's .mcp.json:
{
"mcpServers": {
"readwise": {
"command": "/absolute/path/to/readwise-mcp-server/venv/bin/python",
"args": ["/absolute/path/to/readwise-mcp-server/server.py"],
"env": {
"READWISE_TOKEN": "your_token_here",
"VAULT_PATH": "/absolute/path/to/your/vault"
}
}
}
}
Replace the paths with your actual installation locations.
Tools Reference
1. readwise_daily_review()
Fetch today's highlights and save to Daily Reviews directory.
Parameters: None
Returns:
{
"status": "success",
"count": 42,
"file": "/path/to/daily-review.md"
}
Example:
Call readwise_daily_review()
2. readwise_import_recent(category="tweet", limit=20)
Import recent documents since last import with automatic deduplication.
Parameters:
category(string, optional): Document category (default: "tweet")limit(int, optional): Maximum documents to fetch (default: 20)
Returns:
{
"status": "success",
"imported": 5,
"skipped": 15,
"total_analyzed": 20
}
Example:
Call readwise_import_recent(category="article", limit=50)
3. readwise_backfill(target_date, category="tweet")
Paginate backwards to target date with synced range optimization.
Parameters:
target_date(string, required): Target date in YYYY-MM-DD formatcategory(string, optional): Document category (default: "tweet")
Returns:
{
"status": "success",
"imported": 67,
"skipped": 433,
"pages": 10,
"reached_target": true
}
Example:
Call readwise_backfill(target_date="2026-01-01")
4. readwise_book_highlights(title=None, book_id=None)
Get highlights for a specific book.
Parameters:
title(string, optional): Book title to search forbook_id(string, optional): Specific book ID
Returns:
{
"status": "success",
"count": 15,
"highlights": [...]
}
Example:
Call readwise_book_highlights(title="Atomic Habits")
5. readwise_search_highlights(query, limit=50)
Search highlights by text query.
Parameters:
query(string, required): Search querylimit(int, optional): Maximum results (default: 50)
Returns:
{
"status": "success",
"count": 8,
"highlights": [...]
}
Example:
Call readwise_search_highlights(query="productivity")
6. readwise_state_info()
Show current import state and synced ranges.
Parameters: None
Returns:
{
"status": "success",
"last_import": "2026-01-22T04:29:12Z",
"oldest_imported": "2026-01-01",
"synced_ranges": [...],
"backfill_in_progress": false,
"documents_on_disk": 1044,
"documents_with_ids": 614
}
Example:
Call readwise_state_info()
7. readwise_init_ranges()
Scan filesystem to build synced_ranges from existing documents.
Parameters: None
Returns:
{
"status": "success",
"range": {
"start": "2026-01-01T00:00:00Z",
"end": "2026-01-21T00:00:00Z",
"doc_count": 614
},
"documents_analyzed": 614
}
Example:
Call readwise_init_ranges()
8. readwise_reset_state(clear_ranges=False)
Clear state file (optionally preserve synced_ranges).
Parameters:
clear_ranges(bool, optional): Whether to clear synced ranges (default: False)
Returns:
{
"status": "success",
"message": "State reset",
"cleared_ranges": false
}
Example:
Call readwise_reset_state(clear_ranges=True)
State File Format
The server maintains state at .claude/state/readwise-import.json:
{
"last_import_timestamp": "2026-01-22T04:29:12.864733Z",
"oldest_imported_date": "2026-01-01",
"synced_ranges": [
{
"start": "2026-01-01T06:17:43.693000+00:00",
"end": "2026-01-21T02:33:27.975000+00:00",
"doc_count": 614,
"verified_at": "2026-01-21T10:43:56.626549Z"
}
],
"backfill_in_progress": false
}
Testing
Run the test suite:
source venv/bin/activate
pytest test_server.py -v
Test Categories
Unit Tests:
- State file reading/writing
- Synced range optimization logic
- Filename sanitization
- ID extraction from URLs
- Document scanning for deduplication
Integration Tests:
- API calls with mocked responses
- Markdown formatting
- Document saving with collision handling
Troubleshooting
Connection Issues
-
Check MCP connection:
/mcpShould show "Connected to readwise"
-
Verify environment variables are set correctly in
.mcp.json -
Check logs in stderr output
State Issues
If state file appears corrupted:
-
View current state:
Call readwise_state_info() -
Reset state (preserve ranges):
Call readwise_reset_state() -
Rebuild ranges from filesystem:
Call readwise_init_ranges()
Deduplication Issues
If documents are being imported multiple times:
-
Rebuild synced ranges:
Call readwise_init_ranges() -
Check filesystem for duplicate filenames manually
-
Verify readwise_url frontmatter is present in existing documents
Architecture
Single-File Design
The server is intentionally kept to a single file (~350 lines) for:
- Simplicity and maintainability
- Easy deployment and updates
- Minimal dependencies
- Clear code organization
Reused Logic
Key functions reused from .claude/scripts/readwise-backfill.py:
load_state()/write_state()- State managementoptimize_backfill()- Synced range optimizationscan_existing_documents()- Filesystem deduplicationsanitize_filename()- Safe filename generationextract_id_from_url()- ID extraction
Token Efficiency
Optimizations for reduced token usage:
- 8 tools vs 13+ in Node.js version (38% reduction)
- Combined operations (fetch + dedupe + save in one call)
- Smart defaults minimize required parameters
- Tool descriptions limited to 20-30 words
- Returns structured summaries, not full markdown dumps
Estimated token overhead reduction: ~60%
Comparison with Node.js Version
| Feature | Python MCP | Node.js MCP |
|---|---|---|
| Lines of code | ~350 | ~6,749 |
| Tool count | 8 | 13+ |
| Dependencies | 4 | 10+ |
| State compatibility | ✓ | ✓ |
| Token efficiency | High | Medium |
| Maintenance | Simple | Complex |
Development
Running locally for development
source venv/bin/activate
python server.py
Adding new tools
- Add tool function using
@mcp.tool()decorator - Follow existing patterns for error handling and logging
- Return structured dict with
statusfield - Add tests to
test_server.py - Update README.md with tool documentation
License
MIT
Credits
- Built with FastMCP by Anthropic
- Based on proven logic from
.claude/scripts/readwise-backfill.py - Designed for token efficiency and simplicity