Documentation

Libris API & MCP

Libris is API-first. Every feature works through REST or MCP — the web UI is a thin client over the same surface. This page is the reference for both; agents should be able to read it and start calling tools without further hand-holding.

Start here

Overview

Libris is an agent-native reading library — Goodreads × Strava, but designed so an agent can drive it as fluently as a human. The library is a plain, honest record of what you read; agents bring the intelligence.

There is no first-party AI inside Libris. No summaries, no recommendations, no "smart" widgets. The product is the data layer and the surfaces that expose it. If you want recommendations, you bring an agent; if you want a year-in-review, you bring an agent. Libris will tell that agent everything it knows.

Two surfaces sit on the same service layer:

  • REST at /v1/*. Every endpoint is documented in REST API. The web UI is a thin client over these same routes — there are no human-only escape hatches.
  • MCP over stdio (Claude Desktop / Claude Code) and Streamable HTTP (remote agents). Tools are all listed in MCP server.

Anything a human can do, an agent can do. Anything an agent can do, a human can do. That parity is enforced at the auth layer — both surfaces accept the same API keys, and both write to the same activity-event feed. If the two ever drift, that's a bug.

Authentication

REST authenticates with long-lived API keys prefixed with libris_. Issue and revoke keys at /settings/api-keys. The raw key is shown exactly once at creation — store it somewhere you can reach it.

Send it on every request, in either header (the API accepts both):

header
Authorization: Bearer libris_xxx
# or, equivalently:
x-api-key: libris_xxx

A minimal smoke test once you have a key:

curl
curl -H "Authorization: Bearer $LIBRIS_KEY" \
  https://your-libris.example.com/v1/activity?limit=5

The web UI uses cookie-based sessions for the same routes — REST endpoints accept either an API key or a logged-in session, with the key path checked first. Browsers get sessions, scripts get keys, the underlying behavior is identical.

Remote MCP clients can use either an API key bearer token or OAuth. OAuth clients discover Libris through the MCP protected-resource metadata at https://mcp.readlibris.com/.well-known/oauth-protected-resource, then complete browser authorization with the Libris auth server. OAuth access tokens use libris:read and libris:write scopes; OAuth clients may also request offline_access so they can refresh tokens without asking you to sign in again.

API keys are scoped to the issuing user and act as that user. There are no per-key scopes or rate limits in V1; OAuth tokens are scope-limited by the authorization grant.

Conventions

A few cross-cutting conventions apply across both REST and MCP. Knowing these up front avoids re-deriving them from individual endpoint shapes.

Book references

MCP tools that operate on a book accept a bookRef string instead of forcing the caller to look up an ID first. Resolution is in order:

  1. If the value is a UUID, treat it as a books.id.
  2. Otherwise, if it looks like an ISBN-13, match against books.isbn13.
  3. Otherwise, do a fuzzy title match scoped to the caller's library (i.e. through their user_books rows). The agent should have already added the book by lookup or manual creation — Libris will not silently search the global catalog while resolving refs.

REST endpoints take a UUID directly. The fuzzy-match convenience is an MCP affordance only.

Import sessions

Import sessions are durable previews, not raw bulk-add shortcuts. In V1, an MCP agent reads source material and submits normalized JSON candidates with rawSource and unmapped context attached. Libris validates those records, classifies preview buckets, stores conflicts, and commits only clean or explicitly approved rows.

Source facts win during commit. Enrichment may be flagged afterward, but imported dates, ratings, authors, bindings, links, and review/notes are not overwritten by external metadata during the import.

Time inputs

Endpoints that accept a since parameter (notably GET /v1/activity and recent_activity) take either an ISO 8601 timestamp or a duration shorthand:

duration shorthand
30s   # 30 seconds ago
15m   # 15 minutes ago
2h    # 2 hours ago
7d    # 7 days ago

All timestamps emitted by the API are UTC ISO 8601. Date-only values (e.g. session_date) are still serialized as full ISO timestamps with a 00:00 UTC component.

Pagination

Listing endpoints take a limit query parameter (default 50, max 200). Results are ordered most-recent-first by default; sort controls live on individual endpoints (e.g. GET /v1/books accepts sort and direction). The activity feed returns a nextCursor when there is more.

The activity-event guarantee

Every state-changing call — adding a book, changing status, logging a session, rating, favoriting, joining a collection, setting a goal, or committing an import — writes to activity_events. That row is what an agent reads to discover what happened, when, and why. See Activity events for the type catalog. Import commits emit one summary event for the session instead of hundreds of per-book historical events. The one carve-out is PUT /v1/books/{id}/notes: free-text notes are private scratch space and intentionally do not produce a feed entry.

44 endpoints

REST API

All endpoints live under /v1 and accept the auth headers described above. Click any row to expand its parameters, request body, and response shapes. The canonical spec lives in openapi.yaml at the repo root — this page is generated from it.

books

Add books to the caller's library, fetch them, and update per-user state (status, rating, review, favorite, notes).

GET/v1/books

Search the caller's library

Query parameters
  • qstring
  • status"to_read" | "reading" | "completed" | "dnf" | "paused"
  • collectionstring
  • genrestring
  • favoriteboolean
  • sort"date_added" | "date_started" | "date_completed" | "rating" | "title"
  • direction"asc" | "desc"
  • limitinteger
Responses
  • 200OK
    fieldtypenotes
    itemsarray of object
POST/v1/books

Add a book to the caller's library

Request body (required)
One of these is required: query</code> or <code>isbn</code> or <code>title
fieldtypenotes
querystring
isbnstring
titlestringmaxLength 500
subtitlestring | nullmaxLength 500
typestring | nullmaxLength 80
isbn13string | nullManual-mode stored ISBN-13 metadata. Unlike `isbn`, this does not trigger lookup. · maxLength 32
pagesinteger | nullmin 1 · max 100000
publishYearinteger | nullSigned historical year; negative values display as BC. · min -10000 · max 3000
publisherstring | nullmaxLength 300
statusenum: "to_read" | "reading" | "completed" | "dnf" | "paused"
authorsarray of any
collectionstring
genresarray of string
recommendedBystring
recommendedSourceUrlstring (uri)
Responses
  • 201Created
    fieldtypenotes
    bookIdstring (uuid)
    createdboolean
GET/v1/books/{id}

Fetch book + caller's user_books state + authors + genres

Path parameters
  • idstringreq
Responses
  • 200OK
  • 404Not found
DELETE/v1/books/{id}

Remove the book from the caller's library

Path parameters
  • idstringreq
Responses
  • 204No Content
  • 404Not in library
PUT/v1/books/{id}/external-links

Set per-user external book links

Path parameters
  • idstringreq
Request body (required)
fieldtypenotes
goodreadsIdstring | nullmaxLength 80
amazonUrlstring (uri) | null
Responses
  • 200OK
PUT/v1/books/{id}/favorite

Toggle favorite

Path parameters
  • idstringreq
Request body (required)
fieldtypenotes
favoritereqboolean
Responses
  • 200OK
PUT/v1/books/{id}/metadata

Correct shared catalog metadata

Path parameters
  • idstringreq
Request body (required)
fieldtypenotes
titlestringmaxLength 500
subtitlestring | nullmaxLength 500
typestring | nullmaxLength 80
isbn13string | nullmaxLength 32
pagesinteger | nullmin 1 · max 100000
publishYearinteger | nullSigned historical year; negative values display as BC. · min -10000 · max 3000
publisherstring | nullmaxLength 300
seriesNamestring | nullmaxLength 300
seriesPartinteger | nullmin 1 · max 10000
externalOlIdstring | nullmaxLength 120
authorsarray of any
genresarray of string
Responses
  • 200OK
  • 400Validation failed
  • 404Book not in library
  • 409Metadata conflict
PUT/v1/books/{id}/notes

Set free-text notes (no activity event)

Path parameters
  • idstringreq
Request body (required)
fieldtypenotes
notesreqstringmaxLength 20000
Responses
  • 200OK
PUT/v1/books/{id}/rating

Set rating and optional review

Path parameters
  • idstringreq
Request body (required)
fieldtypenotes
starsreqintegermin 1 · max 5
reviewstring
Responses
  • 200OK
PUT/v1/books/{id}/recommendation

Set recommendation provenance

Path parameters
  • idstringreq
Request body (required)
fieldtypenotes
recommendedBystring | nullmaxLength 200
recommendedSourceUrlstring (uri) | null
recommendedSourceBookIdstring (uuid) | null
Responses
  • 200OK
PUT/v1/books/{id}/series

Update book series metadata

Path parameters
  • idstringreq
Request body (required)
fieldtypenotes
seriesNamestring | nullmaxLength 300
seriesPartinteger | nullmin 1 · max 10000
Responses
  • 200OK
  • 400Validation failed
  • 404Book not in library
PUT/v1/books/{id}/status

Change reading status

Path parameters
  • idstringreq
Request body (required)
fieldtypenotes
statusreqenum: "to_read" | "reading" | "completed" | "dnf" | "paused"
Responses
  • 200OK
POST/v1/books/batch

Batch add books to the caller's library by ISBN

Request body (required)
fieldtypenotes
isbnsreqarray of string
statusenum: "to_read" | "reading" | "completed" | "dnf" | "paused"
collectionstring
genresarray of string
recommendedBystring
recommendedSourceUrlstring (uri)
Responses
  • 200Batch processed with per-ISBN results
    fieldtypenotes
    summaryreqobject
    itemsreqarray of object
  • 400Validation failed
  • 401Unauthorized
curl — manually add a book
curl -X POST -H "Authorization: Bearer $LIBRIS_KEY" \
  -H "Content-Type: application/json" \
  -d '{"title":"Handbound Zine","authors":[{"name":"M. Author"}],"status":"to_read"}' \
  "https://your-libris.example.com/v1/books"

import-sessions

Agent-assisted library imports: create a session, stage normalized candidates, review buckets, store row decisions, and commit approved rows.

GET/v1/import-sessions

List import sessions

Responses
  • 200OK
    fieldtypenotes
    sessionsarray of object
  • 401Unauthorized
POST/v1/import-sessions

Create an import session

Request body
fieldtypenotes
sourceTypestring | nullmaxLength 120
sourceLabelstring | nullmaxLength 300
sourceFingerprintstring | nullmaxLength 300
Responses
  • 201Created
    fieldtypenotes
    sessionobject
  • 400Validation failed
  • 401Unauthorized
GET/v1/import-sessions/{id}

Fetch import session

Path parameters
  • idstringreq
Responses
  • 200OK
    fieldtypenotes
    sessionobject
  • 401Unauthorized
  • 404Import session not found
POST/v1/import-sessions/{id}/commit

Commit approved import rows

Path parameters
  • idstringreq
Request body (required)
fieldtypenotes
confirmreqboolean
itemIdsarray of string (uuid)
Responses
  • 200Commit processed with item-level results
    fieldtypenotes
    sessionobject
    previewSummaryobject
    commitSummaryobject
    resultsarray of object
  • 400Validation failed
  • 401Unauthorized
  • 404Import session not found
GET/v1/import-sessions/{id}/items

List import items

Path parameters
  • idstringreq
Query parameters
  • bucket"create" | "exact_match" | "update_existing" | "likely_match" | "conflict" | "invalid" | "skipped" | "committed" | "failed" | "enrichment_needed"
  • state"pending" | "approved" | "skipped" | "committed" | "failed"
  • qstring
  • limitinteger
  • offsetinteger
Responses
  • 200OK
    fieldtypenotes
    itemsarray of object
    limitinteger
    offsetinteger
  • 401Unauthorized
  • 404Import session not found
POST/v1/import-sessions/{id}/items

Stage normalized import candidates

Path parameters
  • idstringreq
Request body (required)
fieldtypenotes
candidatesreqarray of object
replaceExistingboolean
Responses
  • 200Candidates staged
    fieldtypenotes
    sessionobject
    previewSummaryobject
    stagedinteger
  • 400Validation failed
  • 401Unauthorized
  • 404Import session not found
PUT/v1/import-sessions/{id}/items/{itemId}/decision

Set import item decision

Path parameters
  • idstringreq
  • itemIdstringreq
Request body (required)
fieldtypenotes
kindreqenum: "apply_source" | "keep_existing" | "merge" | "skip" | "leave_pending"
notestring | nullmaxLength 1000
mergedobject
Responses
  • 200Decision stored
    fieldtypenotes
    itemobject
    previewSummaryobject
  • 400Validation failed
  • 401Unauthorized
  • 404Import item not found
curl — stage one normalized candidate
curl -X POST -H "Authorization: Bearer $LIBRIS_KEY" \
  -H "Content-Type: application/json" \
  -d '{"candidates":[{"sourceKey":"pollan-omnivore","title":"The Omnivores Dilemma","authors":["Michael Pollan"],"rawSource":{"slug":"pollan-omnivore"}}]}' \
  "https://your-libris.example.com/v1/import-sessions/<session-id>/items"

sessions

Reading sessions — log retroactively or use a single live timer. Each session is one continuous stretch on one book.

GET/v1/sessions

List the caller's reading sessions

Query parameters
  • limitinteger
Responses
  • 200OK
POST/v1/sessions

Log a completed (retroactive) reading session

Request body (required)
fieldtypenotes
bookIdreqstring (uuid)
sessionDatestring (date-time)
startPagereqintegermin 0
endPagereqintegermin 0
durationMinutesintegermin 0
notesstring
Responses
  • 201Created
POST/v1/sessions/start

Start the caller's live reading timer (one per user)

Request body (required)
fieldtypenotes
bookIdreqstring (uuid)
startPageintegermin 0
Responses
  • 201Created
POST/v1/sessions/stop

Stop the caller's live timer; promotes to a reading_sessions row

Request body (required)
fieldtypenotes
endPageintegermin 0
Responses
  • 200OK
curl — log a retroactive session
curl -X POST -H "Authorization: Bearer $LIBRIS_KEY" \
  -H "Content-Type: application/json" \
  -d '{"bookId":"<uuid>","startPage":120,"endPage":156,"durationMinutes":42}' \
  "https://your-libris.example.com/v1/sessions"

collections

User-defined groups of books. Collections do not own their members; deleting one leaves the books in place.

GET/v1/collections

List the caller's collections

Responses
  • 200OK
POST/v1/collections

Create or update a collection by name

Request body (required)
fieldtypenotes
namereqstring
descriptionstring
Responses
  • 201Created
GET/v1/collections/{id}

Fetch a single collection

Path parameters
  • idstringreq
Responses
  • 200OK
  • 404Not found
PATCH/v1/collections/{id}

Rename or update description on a collection

Path parameters
  • idstringreq
Request body (required)
fieldtypenotes
namestringmaxLength 120 · minLength 1
descriptionstring | nullmaxLength 2000
Responses
  • 200OK
  • 404Not found
DELETE/v1/collections/{id}

Delete a collection (member books are not deleted)

Path parameters
  • idstringreq
Responses
  • 204No Content
  • 404Not found
POST/v1/collections/{id}/books/{bookId}

Add a book to a collection

Path parameters
  • idstringreq
  • bookIdstringreq
Responses
  • 201Created
DELETE/v1/collections/{id}/books/{bookId}

Remove a book from a collection

Path parameters
  • idstringreq
  • bookIdstringreq
Responses
  • 204No Content
curl — list collections
curl -H "Authorization: Bearer $LIBRIS_KEY" \
  "https://your-libris.example.com/v1/collections"

goals

Annual reading targets in books and/or pages, with year-to-date progress.

GET/v1/goals/{year}

Get the goal for a year

Path parameters
  • yearintegerreq
Responses
  • 200OK
    fieldtypenotes
    idstring (uuid)
    yearinteger
    bookTargetinteger | null
    pageTargetinteger | null
PUT/v1/goals/{year}

Set or update the annual goal

Path parameters
  • yearintegerreq
Request body (required)
fieldtypenotes
bookTargetinteger | nullmin 0
pageTargetinteger | nullmin 0
Responses
  • 200OK
GET/v1/goals/{year}/progress

Year-to-date progress vs the annual goal

Path parameters
  • yearintegerreq
Responses
  • 200OK
    fieldtypenotes
    yearinteger
    bookTargetinteger | null
    pageTargetinteger | null
    booksCompletedinteger
    pagesReadinteger
curl — get progress for the current year
curl -H "Authorization: Bearer $LIBRIS_KEY" \
  "https://your-libris.example.com/v1/goals/2026/progress"

activity

Read the unified mutation feed for the caller.

GET/v1/activity

Read the caller's activity feed (reverse chronological)

Query parameters
  • sincestringISO timestamp or duration like "7d", "1h"
  • untilstring
  • typesstringComma-separated list of event_type values.
  • limitinteger
  • cursorstringPass the previous response's nextCursor to fetch older events.
Responses
  • 200OK
    fieldtypenotes
    itemsarray of object
    nextCursorstring | null
curl — last 24h of activity
curl -H "Authorization: Bearer $LIBRIS_KEY" \
  "https://your-libris.example.com/v1/activity?since=24h"

search

Look up books in Open Library before adding them. This endpoint never writes to the library.

GET/v1/search/books

Passthrough search against Open Library (does not add anything)

Query parameters
  • qstringreq
  • limitinteger
Responses
  • 200OK
curl — search Open Library
curl -H "Authorization: Bearer $LIBRIS_KEY" \
  "https://your-libris.example.com/v1/search/books?q=tolkien&limit=5"

authors

GET/v1/authors

Browse authors in the caller's library

Query parameters
  • qstring
  • sort"name" | "book_count" | "recently_added"
  • direction"asc" | "desc"
  • pageinteger
  • limitinteger
Responses
  • 200OK
    fieldtypenotes
    itemsreqarray of object
    pagereqinteger
    limitreqinteger
    totalreqinteger
    hasPreviousPagereqboolean
    hasNextPagereqboolean
  • 400Invalid query
  • 401Unauthorized
GET/v1/authors/suggest

Suggest existing authors from the caller's library

Query parameters
  • qstring
  • limitinteger
Responses
  • 200OK
    fieldtypenotes
    resultsarray of object
  • 400Invalid query
  • 401Unauthorized

series

GET/v1/series

Suggest existing series names from the caller's library

Query parameters
  • qstring
  • limitinteger
Responses
  • 200OK
    fieldtypenotes
    itemsarray of object

users

GET/v1/users/{username}

Public profile projection

Path parameters
  • usernamestringreq
Responses
  • 200OK
    fieldtypenotes
    profilereqobjectPublic projection of a user. Notes, email, and admin flags are never returned. The `libraryPublic` flag tells the caller whether the user has opened their library for browsing.
    currentlyReadingreqarray of object
    publicCollectionsreqarray of object
  • 404User does not exist or profile is private
GET/v1/users/{username}/library

Public library list for a user

Path parameters
  • usernamestringreq
Query parameters
  • qstring
  • status"to_read" | "reading" | "completed" | "dnf" | "paused"
  • collectionstring
  • genrestring
  • favoriteboolean
  • formatstring
  • ownedboolean
  • sort"date_added" | "date_started" | "date_completed" | "rating" | "title" | "re_read_count"
  • direction"asc" | "desc"
  • limitinteger
Responses
  • 200OK
    fieldtypenotes
    itemsarray of object
  • 404User missing, profile private, or library private
PUT/v1/users/me/avatar

Set the authenticated user's avatar URL

Request body (required)
fieldtypenotes
urlreqstring (uri)
Responses
  • 200Avatar updated
    fieldtypenotes
    okreqboolean
    avatarUrlreqstring
  • 400Invalid URL
  • 401Not authenticated
DELETE/v1/users/me/avatar

Clear the authenticated user's avatar

Responses
  • 200Avatar cleared
    fieldtypenotes
    okreqboolean
  • 401Not authenticated

uploads

POST/v1/uploads/avatar

Upload an avatar image

Responses
  • 200Avatar uploaded
    fieldtypenotes
    okreqboolean
    avatarUrlreqstring
  • 400Invalid request body
  • 401Not authenticated
43 tools

MCP server

Libris ships a Model Context Protocol server with two transports. Both expose the same tool set; the only difference is wiring.

stdio — Claude Desktop / Claude Code

Run the server as a child process; the agent client speaks JSON-RPC over stdin/stdout. The server reads LIBRIS_API_KEYfrom the environment and scopes every call to that key's user. One user per process.

claude_desktop_config.json
{
  "mcpServers": {
    "libris": {
      "command": "pnpm",
      "args": ["mcp:stdio"],
      "cwd": "/path/to/librisnew",
      "env": {
        "LIBRIS_API_KEY": "libris_xxx"
      }
    }
  }
}

For Claude Code, add the same entry to your project- or user-scoped MCP config, or run pnpm mcp:stdio directly.

Streamable HTTP — remote agents

For agents that don't live on the same machine as Libris (most of them), use the hosted HTTP transport at https://mcp.readlibris.com/mcp. Each request carries its own API key or OAuth bearer token, so a single deployment serves multiple users.

POST /mcp — list tools
curl -X POST https://mcp.readlibris.com/mcp \
  -H "Authorization: Bearer libris_xxx" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -d '{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "tools/list"
  }'

The HTTP transport is stateless — there are no MCP sessions to resume. Each POST /mcp spins up a fresh per-user server scoped to the bearer token, runs the request, and tears down. Methods GET /mcp and DELETE /mcp return 405 by spec.

OAuth-capable MCP clients should be pointed at https://mcp.readlibris.com/mcp. They can discover the protected resource metadata at https://mcp.readlibris.com/.well-known/oauth-protected-resource. The authorization server issuer is https://www.readlibris.com/api/auth, with discovery metadata at https://www.readlibris.com/.well-known/oauth-authorization-server/api/auth.

Tools

Click any tool to see its argument schema. Names mirror the conceptual action; bookRef arguments accept the UUID / ISBN-13 / fuzzy-title forms described in Conventions.

Use add_book with a search query, lookup ISBN, or manual title. Manual title mode creates a library record from supplied metadata when external catalogs do not have the book.

For historic archives, use the import tools instead of looping add_book. The agent reads local files, submits normalized candidates, inspects the preview, asks before committing, and lets Libris preserve conflicts and provenance in the import session.

add_book

Add a book to the caller's library. Supply either a free-text query, a lookup ISBN, or a manual title when search/ISBN lookup cannot find the book. Manual creation may include stored metadata such as ISBN-13, subtitle, type, pages, publish year, publisher, ordered authors, and genres. Optionally set the initial status, add to a collection, or record who recommended it. Author selections may be existing author IDs or new names; string author arrays remain accepted.

Arguments
fieldtypenotes
querystringTitle/author search string
isbnstring
titlestringmaxLength 500
subtitleany
typeany
isbn13any
pagesany
publishYearany
publisherany
authorsarray of any
statusenum: "to_read" | "reading" | "completed" | "dnf" | "paused"
collectionstring
genresarray of string
recommendedBystring
recommendedSourceUrlstring (uri)
add_books_by_isbn

Batch add books to the caller's library from ISBNs. Each ISBN returns its own result, so invalid, duplicate, not-found, or failed items do not prevent neighboring books from being added. Optionally apply one initial status, collection, genre list, or recommendation source to every added book.

Arguments
fieldtypenotes
isbnsreqarray of string
statusenum: "to_read" | "reading" | "completed" | "dnf" | "paused"
collectionstring
genresarray of string
recommendedBystring
recommendedSourceUrlstring (uri)
create_import_session

Create a durable import session for agent-assisted imports. The agent should read local source files itself and then call stage_import_candidates with normalized records; Libris does not accept a local path and parse files on the server.

Arguments
fieldtypenotes
sourceTypeany
sourceLabelany
sourceFingerprintany
stage_import_candidates

Stage normalized import candidates into an existing import session. Each candidate may include rawSource and unmapped fields for audit context. Staging validates, matches, buckets, and updates the preview summary but does not mutate the library.

Arguments
fieldtypenotes
sessionIdreqstring (uuid)
candidatesreqarray of object
replaceExistingboolean
set_import_item_decision

Resolve or defer one staged import row. Conflicts and likely matches require apply_source, keep_existing, merge, skip, or leave_pending before those rows can commit.

Arguments
fieldtypenotes
sessionIdreqstring (uuid)
itemIdreqstring (uuid)
kindreqenum: "apply_source" | "keep_existing" | "merge" | "skip" | "leave_pending"
noteany
mergedobject
commit_import_session

Commit clean and approved import rows after explicit user confirmation. Invalid rows and unresolved conflicts stay pending; row-level failures do not roll back neighboring rows.

Arguments
fieldtypenotes
sessionIdreqstring (uuid)
confirmreqboolean
itemIdsarray of string (uuid)
remove_book

Remove a book from the caller's library. Deletes the user_books row, this user's reading sessions and any live timer for it, the book's membership in this user's collections, and mentions sourced from this user's review/notes/sessions for the book. The global book record (and other users' state) is untouched. Activity history is preserved with a final book_removed event. bookRef may be a uuid, isbn13, or fuzzy title match scoped to the library.

Arguments
fieldtypenotes
bookRefreqstring
update_status

Change the reading status of a book in the caller's library. bookRef may be a uuid, isbn13, or fuzzy title match scoped to the library.

Arguments
fieldtypenotes
bookRefreqstring
statusreqenum: "to_read" | "reading" | "completed" | "dnf" | "paused"
update_book_metadata

Correct shared catalog metadata for a book in the caller's library. Updates title, subtitle, type, ISBN-13, page count, publish year, publisher, series, Open Library ID, ordered authors, or genres. Omitted fields are left unchanged; null clears nullable scalar fields. Author arrays may contain strings for compatibility or identity-aware selections with authorId/name; genre arrays replace the current list.

Arguments
fieldtypenotes
bookRefreqstring
titlestringmaxLength 500
subtitleany
typeany
isbn13any
pagesany
publishYearany
publisherany
seriesNameany
seriesPartany
externalOlIdany
authorsarray of any
genresarray of string
list_series

List existing series names from books already in the caller's library. Use this before setting series metadata so agents can reuse the user's existing names; new names are created implicitly by saving them on a book.

Arguments
fieldtypenotes
qstringmaxLength 100
limitintegermin 1 · max 50
set_book_series

Set or clear shared series metadata for a book in the caller's library. bookRef may be a uuid, isbn13, or fuzzy title match scoped to the library. Pass null or an empty seriesName to clear series membership; new series names are created implicitly by saving them on a book.

Arguments
fieldtypenotes
bookRefreqstring
seriesNamereqany
seriesPartany
log_session

Log a completed reading session (retroactive). Page range is required; duration is optional.

Arguments
fieldtypenotes
bookRefreqstring
sessionDatestring (date-time)
startPagereqintegermin 0
endPagereqintegermin 0
durationMinutesintegermin 0
notesstring
start_session

Start a live reading timer for a book. Only one active session per user at a time; starting a new one replaces any existing live session.

Arguments
fieldtypenotes
bookRefreqstring
startPageintegermin 0
stop_session

Stop the caller's live reading timer and promote it to a completed session.

Arguments
fieldtypenotes
endPageintegermin 0
rate_book

Rate a book 1-5 stars and optionally attach a review.

Arguments
fieldtypenotes
bookRefreqstring
starsreqintegermin 1 · max 5
reviewstring
set_timeline

Update one or more timeline dates (dateAdded, dateStarted, dateCompleted) on a book in the caller's library. Each field is optional: omit to leave unchanged, or pass null/empty string to clear. Accepts ISO 8601 datetimes or YYYY-MM-DD date-only strings. dateStarted must not be after dateCompleted.

Arguments
fieldtypenotes
bookRefreqstring
dateAddedany
dateStartedany
dateCompletedany
set_current_page

Set the reader's current page in a book. Use this to record where you left off between sessions. Pass null to clear. The value is automatically advanced when sessions are logged — only call this to make a manual correction. Values above the book's known page count are silently clamped; the response returns the value actually written, which may differ from what was sent.

Arguments
fieldtypenotes
bookRefreqstring
pagereqany
set_favorite

Toggle favorite status on a book in the caller's library.

Arguments
fieldtypenotes
bookRefreqstring
favoritereqboolean
set_book_provenance

Update recommendation provenance for a book in the caller's library. Pass recommendedBy, recommendedSourceUrl, or recommendedSourceBookRef. Use null to clear recommendation text/source URL/source book. recommendedSourceBookRef may be a uuid, isbn13, or fuzzy title match scoped to the caller's library.

Arguments
fieldtypenotes
bookRefreqstring
recommendedByany
recommendedSourceUrlany
recommendedSourceBookRefany
set_book_external_links

Set or clear per-user external links for a book in the caller's library. Pass goodreadsId or amazonUrl; null clears a field. Amazon URL must be HTTPS.

Arguments
fieldtypenotes
bookRefreqstring
goodreadsIdany
amazonUrlany
list_cover_candidates

List candidate cover-image URLs for a book in the caller's library, aggregated from OpenLibrary editions and Google Books. Returns an ordered, deduped list with each entry tagged by source.

Arguments
fieldtypenotes
bookRefreqstring
set_cover_override

Set the caller's per-book cover override URL for a book in their library. Accepts any HTTPS URL (third-party image, Vercel Blob URL, etc.). HTTP and other schemes are rejected. Replaces any prior override; cleans up the previous Vercel Blob upload if applicable.

Arguments
fieldtypenotes
bookRefreqstring
urlreqstring (uri)
clear_cover_override

Clear the caller's per-book cover override for a book in their library. Cover rendering falls back to the global books.image_url.

Arguments
fieldtypenotes
bookRefreqstring
add_to_collection

Add a book to a collection. Creates the collection if it doesn't exist.

Arguments
fieldtypenotes
bookRefreqstring
collectionreqstring
set_goal

Set or update the caller's annual reading goal.

Arguments
fieldtypenotes
yearreqintegermin 1900 · max 3000
bookTargetintegermin 0
pageTargetintegermin 0
list_import_sessions

List recent import sessions owned by the caller with preview and commit summaries. Use this to resume an interrupted import or find the browser preview URL.

Arguments
fieldtypenotes
limitintegermin 1 · max 100
get_import_session

Fetch one import session summary by ID. Returns the durable preview/commit state but not every row; call list_import_items for filtered rows.

Arguments
fieldtypenotes
sessionIdreqstring (uuid)
list_import_items

List staged import rows with optional bucket/state/search filters. Limit defaults are bounded so agents do not accidentally pull hundreds of full rows.

Arguments
fieldtypenotes
sessionIdreqstring (uuid)
bucketenum: "create" | "exact_match" | "update_existing" | "likely_match" | "conflict" | "invalid" | "skipped" | "committed" | "failed" | "enrichment_needed"
stateenum: "pending" | "approved" | "skipped" | "committed" | "failed"
querystringmaxLength 200
limitintegermin 1 · max 500
offsetintegermin 0
explain_import_session

Explain pending, invalid, conflict, likely-match, and failed import rows using the same preview state shown in the browser. Use this before asking the user for decisions.

Arguments
fieldtypenotes
sessionIdreqstring (uuid)
limitintegermin 1 · max 100
search_library

Search the caller's library by free-text query, status, collection, genre, or favorite filter. Sort by date_added, date_started, date_completed, rating, or title.

Arguments
fieldtypenotes
querystring
statusenum: "to_read" | "reading" | "completed" | "dnf" | "paused"
collectionstring
genrestring
favoriteboolean
sortenum: "date_added" | "date_started" | "date_completed" | "rating" | "title" | "re_read_count"
directionenum: "asc" | "desc"
limitintegermin 1 · max 200
get_book

Fetch one book with global metadata and the caller's user_books state.

Arguments
fieldtypenotes
bookRefreqstring
list_authors

Browse authors represented in the caller's library. Supports display-name search, sorting by name, book_count, or recently_added, and bounded pagination. Returns author rows with book counts and compact sample-book context.

Arguments
fieldtypenotes
querystringmaxLength 100
sortenum: "name" | "book_count" | "recently_added"
directionenum: "asc" | "desc"
pageintegermin 1
limitintegermin 1 · max 100
get_author

Fetch one author with their full catalog bibliography (capped at 200 books, sorted publishYear DESC, NULLS LAST) and the caller's mentions of that author grouped by source type. Returns { author, books: [{book, userBook}], mentions: { reviews, notes, collections, fromBookMetadata } }. Books include the caller's user_books row when shelved, null otherwise.

Arguments
fieldtypenotes
authorIdreqstring (uuid)
recent_activity

Return the caller's activity feed (book adds, status changes, sessions, ratings, goals…) in reverse chronological order. `since` accepts durations like '1h', '7d' or ISO timestamps.

Arguments
fieldtypenotes
sincestring
typesarray of string
limitintegermin 1 · max 200
goal_progress

Return year-to-date books completed and pages read vs the caller's goal for a given year.

Arguments
fieldtypenotes
yearintegermin 1900 · max 3000
get_goal

Fetch the raw goal row for a year (null fields mean unset).

Arguments
fieldtypenotes
yearreqintegermin 1900 · max 3000
list_collections

List all collections owned by the caller.

Arguments

No arguments.

rename_collection

Rename a collection and/or update its description. Pass only the fields to change.

Arguments
fieldtypenotes
collectionIdreqstring (uuid)
namestringmaxLength 120 · minLength 1
descriptionany
delete_collection

Delete a collection. Member books remain in the library; only the collection and its memberships are removed.

Arguments
fieldtypenotes
collectionIdreqstring (uuid)
search_open_library

Search Open Library for books to potentially add. Does not touch the library — call add_book once you've chosen one.

Arguments
fieldtypenotes
queryreqstring
limitintegermin 1 · max 20
get_public_profile

Read another user's public profile by username. Returns the same projection a signed-out browser would see at /u/[username]: header info, currently reading, and public collections. Returns null when the user does not exist or has set their profile to private. Does not require the target user to be the caller.

Arguments
fieldtypenotes
usernamereqstringminLength 1
set_avatar_url

Set the caller's avatar to a publicly accessible HTTPS image URL. HTTP and other schemes are rejected. Replaces any prior avatar; cleans up the previous Vercel Blob upload if applicable. To upload a file directly, use POST /v1/uploads/avatar via the REST API, then persist the returned URL with PUT /v1/users/me/avatar.

Arguments
fieldtypenotes
urlreqstring (uri)
clear_avatar

Clear the caller's avatar. Avatar rendering falls back to initials. Cleans up the previous Vercel Blob upload if applicable. Idempotent — safe to call when no avatar is set.

Arguments

No arguments.

Activity events

Every mutation writes one row to activity_events. Reading the feed is how an agent reconstructs "what changed since the last time I looked" without polling individual resources. Filter by type with ?types=book_completed,session_logged, or timebox with ?since=24h.

event_typemeaning
book_addedA book entered the caller's library through the normal add path.
book_removedA book was removed from the caller's library while preserving global metadata.
status_changedThe reading_status moved between values. Specific transitions also emit one of the book_completed / book_dnfed / book_paused / book_resumed shorthands below.
session_loggedA reading_sessions row was inserted — either retroactively (log_session) or by stopping a live timer.
ratedStar rating set or changed (1-5).
reviewedReview body set or changed.
favoritedfavorite flag turned on.
unfavoritedfavorite flag turned off.
collection_addedBook joined a collection.
collection_removedBook left a collection (collection itself untouched).
collection_updatedA collection was renamed or its description changed.
collection_deletedA collection was deleted; member books remained in the library.
goal_setAn annual goal was created for the first time for that year.
goal_updatedAn existing annual goal had its targets adjusted.
book_completedStatus transitioned to completed. Fires alongside status_changed.
book_dnfedStatus transitioned to dnf. Fires alongside status_changed.
book_pausedStatus transitioned to paused. Fires alongside status_changed.
book_resumedStatus moved out of paused back to reading. Fires alongside status_changed.
format_changedOwned formats or the current reading format changed.
owned_changedThe owned flag changed.
reread_loggedThe re-read count was incremented.
metadata_updatedShared catalog metadata was corrected by the caller.
metadata_refreshedExternal metadata refresh filled missing catalog fields.
recommendation_setRecommendation provenance changed.
external_links_changedPer-user Goodreads or Amazon links changed.
timeline_changedDate added, date started, or date completed changed.
import_committedAn import session committed clean or approved rows. Payload counts summarize created, updated, failed, pending, and enrichment-needed rows.

Each event carries a payload object with type-specific detail (e.g. session_logged includes startPage, endPage, durationMinutes; status_changed includes from and to). The exact shape is intentionally not part of the v1 contract — read what's there, treat unknown keys as informational.

Errors

REST errors are JSON with an errorfield and an HTTP status that's actually meaningful. MCP errors come back through JSON-RPC with an error.code and error.message.

401 — missing or invalid key

Returned when the request has no recognized credential, or the key has been revoked.

rest
 { "error": "Unauthorized" } 
mcp
{
  "jsonrpc": "2.0",
  "error": { "code": -32000, "message": "Invalid API key" },
  "id": null
}

404 — resource not found

The book, collection, goal, or session ID isn't in the caller's scope. Libris doesn't leak existence — the same 404 is returned whether the row doesn't exist or belongs to a different user.

rest
{ "error": "Not found" }

400 / 422 — validation failure

Request body or query parameters didn't match the expected schema. The error includes a details object with the failing field path so an agent can self-correct.

rest
{
  "error": "Validation failed",
  "details": [
    { "path": ["stars"], "message": "Number must be greater than or equal to 1" }
  ]
}

409 — conflict

Returned when a write would violate a uniqueness or state invariant — e.g. starting a live timer when one is already running for the user (the existing one is replaced; this is informational, not a hard error in current implementation).

500 — internal error

Something on the server side broke. Retry once with the same payload; if it persists, check the server logs (or the Sentry dashboard once Phase 4 lands). MCP wraps the underlying message in JSON-RPC code -32603.

mcp
{
  "jsonrpc": "2.0",
  "error": { "code": -32603, "message": "Internal error" },
  "id": null
}