A local co-reading MCP server for chunked books, reading progress, search, and margin annotations.
Co-Reading MCP
A local MCP server that gives Claude a durable reading room:
- import EPUB or plain text into stable chunks while preserving EPUB spine/chapter boundaries
- list books and chunks
- read chunk-by-chunk with
prevId/nextId - continue directly from the next unread chunk
- search across a book with cached chunk text
- write margin annotations
- stage user notes, submit them to Claude once, and attach Claude replies under them
- track reading progress
- return a small finish ritual when a book is completed
The goal is not one-shot summarization. The goal is a shared reading surface where a human and Claude can both read, leave anchored notes, and resume smoothly.
For a step-by-step setup and usage flow, see docs/user-guide.md.
Quick Start
Requirements:
- Node.js 18+
- Python 3.10+ for the import scripts
cd co-reading-mcp
cp -R data.example data
node src/server.js
If you also want a human-friendly reading surface, start the bundled reader:
npm run reader
Open http://127.0.0.1:8787. This serves a small reference reader and local HTTP API while also keeping the MCP stdio server active in the same process. In Claude Desktop / Claude Code you can point the MCP command at src/http.js instead of src/server.js when you want one process to handle both:
{
"mcpServers": {
"co-reading": {
"command": "node",
"args": ["/absolute/path/to/co-reading-mcp/src/http.js"],
"env": {
"READING_MCP_DATA_DIR": "/absolute/path/to/co-reading-mcp/data",
"READING_HTTP_PORT": "8787"
}
}
}
}
The reader's Library header includes an import button for EPUB, TXT, or Markdown files. Browser imports upload the file directly to the co-reading server, so they also work with remote claude.ai setups where chat attachments are isolated from the MCP server filesystem.
For Claude Desktop / Claude Code, configure the MCP server as a stdio command:
{
"mcpServers": {
"co-reading": {
"command": "node",
"args": ["/absolute/path/to/co-reading-mcp/src/server.js"],
"env": {
"READING_MCP_DATA_DIR": "/absolute/path/to/co-reading-mcp/data"
}
}
}
}
Remote Server
For VPS, reverse-proxy, tunnel, or remote MCP clients, run one process:
READING_MCP_DATA_DIR=./data MCP_AUTH_TOKEN="change-me" npm run start:sse
The same port serves the human reader, REST API, and remote MCP transports:
https://your-domain.example/: reference reader UIhttps://your-domain.example/?token=change-me: reader UI with auth saved in local storage and a cookiehttps://your-domain.example/api/*: reader REST APIhttps://your-domain.example/sse: MCP SSE transporthttps://your-domain.example/mcp: MCP JSON-RPC over POST
Environment variables:
MCP_SSE_PORTorPORT: listen port, default3100MCP_SSE_HOST: listen host, default0.0.0.0MCP_AUTH_TOKEN: bearer token required by remote clientsMCP_CORS_ORIGIN: CORS origin, default*MCP_MAX_BODY_BYTES: max JSON-RPC POST body size, default25000000READING_IMPORT_MAX_BYTES: max EPUB/TXT upload size, default25000000
Do not expose the remote server on the public internet without HTTPS and MCP_AUTH_TOKEN. When MCP_AUTH_TOKEN is set, the reader, static assets, /api/*, /sse, /messages, /mcp, and /health require the token. Open the reader once with /?token=...; the server sets a same-site cookie and the reader stores the token for API calls. If you use nginx, Caddy, or cloudflared, proxy /, /api/*, /sse, /messages, and /mcp to the same local process and make sure streaming responses are not buffered.
Import Books
Plain text:
python3 scripts/import_text.py ./book.txt --title "Book Title" --author "Author" --out ./data/books
Plain text can also preserve section headings with a multiline regex:
python3 scripts/import_text.py ./book.txt \
--title "Book Title" \
--heading-regex "^第[一二三四五六七八九十百零〇0-9]+[章节回].*$"
If a loose heading regex catches navigation labels or other tiny sections, add
--min-section-chars 100 or a similar threshold.
EPUB:
python3 scripts/import_epub.py ./book.epub --out ./data/books
Claude can also import books through MCP, which is useful on claude.ai or mobile devices where the user cannot SSH into the server:
reading_import_book: one EPUB/TXT as a base64 payloadreading_import_begin/reading_import_part/reading_import_finish: chunked upload for larger files
For example, after a user drops book.epub into a Claude chat, Claude can read the file, base64-encode it, and call reading_import_book:
{
"filename": "book.epub",
"dataBase64": "...",
"bookId": "optional-stable-id"
}
TXT imports can pass the same heading options as the command-line script:
{
"filename": "book.txt",
"dataBase64": "...",
"title": "Book Title",
"headingRegex": "^Chapter\\s+\\w+"
}
The import tools write into data/books immediately; no server restart is needed.
Both importers create:
data/books/<book-id>/
manifest.json
chunks/
ch00.txt
ch01.txt
EPUB imports keep each spine item as a section boundary. If an EPUB stores the whole book in a single spine item, the importer falls back to internal h1/h2/h3 headings. If a chapter is longer than --max-chars, only that chapter is split into Chapter Title Part 1/N, Part 2/N, and so on.
Runtime state is stored outside book content:
data/
annotations.jsonl
progress.json
reading_sessions.json
reading_submit_user_notes includes full chunk text once per sessionId by default, then sends only new notes for the same chunk in that session. Use a new sessionId when Claude starts a new conversation/session so the relevant chunk context is sent again.
Tools
reading_list_booksreading_list_chunksreading_read_chunkreading_continuereading_search_chunksreading_import_bookreading_import_beginreading_import_partreading_import_finishreading_import_cancelreading_annotate_passagereading_list_annotationsreading_submit_user_notesreading_reply_to_annotationreading_mark_readreading_get_progress
See docs/mcp-tools.md and docs/data-format.md. For the intended Claude workflow, see docs/claude-workflow.md.
Frontend Integration
The bundled reader is intentionally small: it is a reference UI, not a required frontend. Existing apps can talk to the same local HTTP API:
GET /api/booksGET /api/books/:bookId/chunksGET /api/books/:bookId/chunks/:chunkIdGET /api/continue?bookId=...GET /api/annotations?bookId=...&chunkId=...POST /api/annotationsPOST /api/repliesPOST /api/submit-notesPOST /api/mark-readGET /api/search?q=...&bookId=...POST /api/import
Human notes are saved as open local notes first. Pressing "Send to Claude" calls reading_submit_user_notes, includes chunk context according to the session policy, marks those notes submitted, and avoids resending the same open notes.
Privacy
This repo is designed so private content stays in data/, which is ignored by git. data.example/ contains only toy text.
Contributors
- GPT
- Claude
- Koshi