A self-hosted ebook reader your LLM can read.
Upload PDF/EPUB/MOBI, read them in the browser on any device, keep your progress in sync — and let Claude, Cursor, or any MCP client browse your library and quote chapters through the built-in read-only MCP server.
One command:
docker run -d -p 18094:18093 -v ownbook-data:/app/pb_data uzqw/ownbook:latestOpen http://127.0.0.1:18094 — first-run bootstrap creates the schema and a demo account automatically.
- Single container, single Go binary + PocketBase — no external DB, no sidecars.
- Per-device reading progress, bookmarks, and notes sync through one backend.
- Embedded MCP server: your LLM assistant can list shelves, walk TOCs, and read page ranges — no plugins, no extra service. (See MCP server.)
⭐ If this is useful, a star helps others find it.
The published image currently supports linux/amd64:
docker run -d \
--name ownbook \
--restart unless-stopped \
-p 18094:18093 \
-v ownbook-data:/app/pb_data \
uzqw/ownbook:latestOpen http://127.0.0.1:18094. On first start the container automatically creates the PocketBase schema and the default accounts shown below; no separate bootstrap command is required.
Upgrade without losing books or reading data:
docker pull uzqw/ownbook:latest
docker rm -f ownbook
docker run -d --name ownbook --restart unless-stopped \
-p 18094:18093 -v ownbook-data:/app/pb_data uzqw/ownbook:latestA Linux host only needs Docker with Compose v2 and make:
git clone https://github.com/uzqw/ownbook.git
cd ownbook
make deployThe command creates .env when missing, builds the image, and starts the app at
http://127.0.0.1:18094. Review the generated .env credentials before exposing
that port publicly. Set DOCKER_HOST_PORT to use another host port.
Deployment stores PocketBase data under
${APP_DATA_ROOT}/ownbook/pb_data. Set APP_DATA_ROOT to an absolute
application data root, or leave it unset to use the XDG user default:
${XDG_DATA_HOME:-$HOME/.local/share}/uzqw/apps
The deployment path resolver rejects repository-local and unsafe roots. Path parameterization does not migrate existing data; follow the migration runbook before deploying against a new data directory.
For local development, run the foreground services with:
task setup
task doctor
task devThe backend initializes missing collections and default accounts automatically.
task bootstrap remains available to reconcile schema changes manually.
Default local backend address:
http://127.0.0.1:8090
The Docker image serves HTTP; configure HTTPS in your own Caddy, Nginx, or other
reverse proxy. The local platform deployment's https://${PLATFORM_HOST}:18094
endpoint uses Caddy's internal CA.
Every client must trust the gateway's root CA; merely bypassing the browser
warning for one leaf certificate is not sufficient. Caddy rotates its default
12-hour leaf certificates, and an untrusted replacement makes in-page requests
fail with Failed to fetch until a top-level navigation handles the certificate
again.
For the local platform deployment, download the public root certificate over the non-TLS gateway endpoint, install it in the client operating system, and restart the browser:
curl http://${PLATFORM_HOST}:18089/caddy-local-root.crt \
-o caddy-local-root.crtInstall it on Arch/Manjaro Linux:
sudo trust anchor --store caddy-local-root.crt
sudo update-ca-trustOr on Debian/Ubuntu Linux:
sudo install -m 0644 caddy-local-root.crt \
/usr/local/share/ca-certificates/caddy-local-root.crt
sudo update-ca-certificatesFully quit and reopen the browser after changing the trust store. See the
platform ACCESS.md for macOS, Fedora/RHEL, Chrome/Chromium NSS, and Firefox
instructions. Preserve the platform Caddy data directory; replacing it creates
a new root CA that must be installed on every client again.
- PocketBase-based authentication and file storage.
- Upload, parse, list, and delete PDF, EPUB, and MOBI books.
- Page image rendering and extracted page text for reading.
- Bookmarks, notes, reading progress, and per-book metadata.
- Shared reading state across devices through one deployed backend.
- Read-only MCP server so LLM clients can browse the library and read chapters.
The backend embeds a read-only Model Context Protocol
server at <base-url>/mcp (streamable HTTP, same PocketBase process, no second
service). Any MCP client can list the library, walk a book's table of contents, and
read chapters or page ranges.
Use the URL you already reach the app on:
task devserves the backend directly, so it is the default local addresshttp://127.0.0.1:8090/mcp.- A deployed stack is reached through its reverse proxy — on the local platform
deployment,
https://${PLATFORM_HOST}:18094/mcp. The compose stack publishes no host port, so there is no127.0.0.1:<port>shortcut to the running container.
Authentication reuses PocketBase user tokens: send Authorization: Bearer <token>.
Every tool is scoped to the token's user, so one account cannot read another
account's books; mint the token from the account that owns the books. Credentials
live in .env as APP_USER_EMAIL / APP_USER_PASSWORD:
set -a; . ./.env; set +a
TOKEN=$(curl -s -X POST "${EBOOK_BASE_URL:-https://${PLATFORM_HOST}:18094}"\
/api/collections/users/auth-with-password \
-H 'Content-Type: application/json' \
--data "$(jq -nc --arg i "$APP_USER_EMAIL" --arg p "$APP_USER_PASSWORD" \
'{identity:$i,password:$p}')" | jq -r .token)
export EBOOK_PB_TOKEN="$TOKEN"The demo@e.co / demo1234 pair in .env.example only matches a stack
bootstrapped from that file; a stack with its own .env has its own accounts.
Tokens are not permanent. The users collection sets authToken.duration to
432000, i.e. 5 days, so any token a client caches goes stale on its own and the
client has to mint a fresh one once calls start failing with
401 {"error":"invalid auth token"}.
Tools:
| Tool | Arguments | Returns |
|---|---|---|
list_books |
– | Books with title, author, page_count, parse_status |
get_toc |
book_id |
TOC tree: title, page, level, children |
get_section_text |
book_id, section_title, start_page (optional) |
Section text plus a next_page cursor |
get_pages |
book_id, from_page, to_page |
Text for an explicit page range |
search_in_book |
book_id, query |
Matches as page_number and line snippets |
Behaviour worth knowing when calling the tools:
get_section_textends a section at the page before the next same-or-higher-level TOC entry (the last section runs to the end of the book), returns about ten pages per call, and pages through longer sections withnext_page.- Sections that share their start page with a sibling come back as that single page with
is_partial: trueand anote, never as empty text. - Page text is joined with
--- p.N ---separators; responses over roughly 100KB are cut with a[truncated: ...]marker andtruncated: true. - The route is registered for
POSTonly. The frontend'sGET /{path...}static route conflicts with an all-methods/mcppattern in Go'sServeMux, and streamable HTTP clients send JSON-RPC exclusively overPOST.
Any client that accepts a URL plus headers can use it directly:
{
"mcpServers": {
"ebook": {
"url": "https://${PLATFORM_HOST}:18094/mcp",
"headers": { "Authorization": "Bearer ${EBOOK_PB_TOKEN}" }
}
}
}For pi, put that block in a file and pass --mcp-config <file>; the adapter supports
"auth": "bearer" with "bearerTokenEnv": "EBOOK_PB_TOKEN" instead of a literal header,
and prefixes tool names with the server key (ebook_list_books, ebook_get_toc, ...).
Because the token expires in 5 days, a bearerTokenEnv holding a static token goes
stale silently. Point bearerToken at a command instead: a leading ! marks it as a
command, and the adapter re-runs it every time the server connects, so the token is
always fresh:
{
"mcpServers": {
"ebook": {
"url": "https://${PLATFORM_HOST}:18094/mcp",
"auth": "bearer",
"bearerToken": "!${HOME}/.config/mcp/ebook-token.sh",
"lifecycle": "lazy"
}
}
}where that script runs the auth-with-password call above and prints nothing but the
token.
Smoke test with curl. The session is stateful: capture the Mcp-Session-Id response
header from initialize and echo it on every later call, or the server answers
404 Invalid session ID:
# initialize, capturing the session id
SID=$(curl -s -D - -o /dev/null -X POST https://${PLATFORM_HOST}:18094/mcp \
-H "Authorization: Bearer $EBOOK_PB_TOKEN" -H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"curl","version":"0"}}}' \
| sed -n 's/^[Mm]cp-[Ss]ession-[Ii]d: *//p' | tr -d '\r')
# any later call must echo it back
curl -s -X POST https://${PLATFORM_HOST}:18094/mcp \
-H "Authorization: Bearer $EBOOK_PB_TOKEN" -H "Mcp-Session-Id: $SID" \
-H 'Content-Type: application/json' -H 'Accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":2,"method":"tools/list"}'Requirements:
- Go
- Node.js and npm
- Task
Useful commands:
task setup # install locked frontend and Go dependencies
task doctor # check the runtime environment without modifying it
task dev # run the frontend and backend in the foreground
task bootstrap # create collections and demo users
task fmt:check # verify frontend and Go formatting
task lint # run frontend, shell, and Go linters
task typecheck # run frontend type checking
task test # run frontend and backend tests
task build # build production frontend and backend artifacts
task config # validate manifest and merged Compose configuration
task check # run all checks above in the documented order
task deploy:down # stop deployment without deleting persistent data
task deploy:logs # show deployment logsDefault accounts created on first startup:
User: demo@e.co / demo1234
Admin: admin@e.co / admin123
Change these credentials before exposing the service publicly. Existing accounts
and an existing .env are not changed automatically.
src/ Vue frontend
backend/cmd/ebook-pocketbase PocketBase Go extension
pb_hooks/ PocketBase hooks
scripts/ bootstrap and container scripts
fonts/ required CJK font asset for EPUB rendering
This repository intentionally excludes or ignores:
- PocketBase runtime data under
.local/andbackend/pb_data/ - Local environment files such as
.env - Build outputs such as
dist/ - Dependency directories such as
node_modules/ - Local agent/runtime metadata such as
.omx/,.agents/, andomx_wiki/ - Generated backend binary outputs
fonts/DroidSansFallback.ttf is kept in the repository because EPUB/CJK rendering depends on it for consistent output across devices.



