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):
Authorization: Bearer libris_xxx # or, equivalently: x-api-key: libris_xxx
A minimal smoke test once you have a key:
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:
- If the value is a UUID, treat it as a
books.id. - Otherwise, if it looks like an ISBN-13, match against
books.isbn13. - Otherwise, do a fuzzy title match scoped to the caller's library (i.e. through their
user_booksrows). 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:
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.
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/booksSearch the caller's library
Search the caller's library
qstringstatus"to_read" | "reading" | "completed" | "dnf" | "paused"collectionstringgenrestringfavoritebooleansort"date_added" | "date_started" | "date_completed" | "rating" | "title"direction"asc" | "desc"limitinteger
- 200OK
field type notes items array of object
POST/v1/booksAdd a book to the caller's library
Add a book to the caller's library
query</code> or <code>isbn</code> or <code>title| field | type | notes |
|---|---|---|
| query | string | |
| isbn | string | |
| title | string | maxLength 500 |
| subtitle | string | null | maxLength 500 |
| type | string | null | maxLength 80 |
| isbn13 | string | null | Manual-mode stored ISBN-13 metadata. Unlike `isbn`, this does not trigger lookup. · maxLength 32 |
| pages | integer | null | min 1 · max 100000 |
| publishYear | integer | null | Signed historical year; negative values display as BC. · min -10000 · max 3000 |
| publisher | string | null | maxLength 300 |
| status | enum: "to_read" | "reading" | "completed" | "dnf" | "paused" | |
| authors | array of any | |
| collection | string | |
| genres | array of string | |
| recommendedBy | string | |
| recommendedSourceUrl | string (uri) |
- 201Created
field type notes bookId string (uuid) created boolean
GET/v1/books/{id}Fetch book + caller's user_books state + authors + genres
Fetch book + caller's user_books state + authors + genres
idstringreq
- 200OK
- 404Not found
DELETE/v1/books/{id}Remove the book from the caller's library
Remove the book from the caller's library
idstringreq
- 204No Content
- 404Not in library
PUT/v1/books/{id}/external-linksSet per-user external book links
Set per-user external book links
idstringreq
| field | type | notes |
|---|---|---|
| goodreadsId | string | null | maxLength 80 |
| amazonUrl | string (uri) | null |
- 200OK
PUT/v1/books/{id}/favoriteToggle favorite
Toggle favorite
idstringreq
| field | type | notes |
|---|---|---|
| favoritereq | boolean |
- 200OK
PUT/v1/books/{id}/metadataCorrect shared catalog metadata
Correct shared catalog metadata
idstringreq
| field | type | notes |
|---|---|---|
| title | string | maxLength 500 |
| subtitle | string | null | maxLength 500 |
| type | string | null | maxLength 80 |
| isbn13 | string | null | maxLength 32 |
| pages | integer | null | min 1 · max 100000 |
| publishYear | integer | null | Signed historical year; negative values display as BC. · min -10000 · max 3000 |
| publisher | string | null | maxLength 300 |
| seriesName | string | null | maxLength 300 |
| seriesPart | integer | null | min 1 · max 10000 |
| externalOlId | string | null | maxLength 120 |
| authors | array of any | |
| genres | array of string |
- 200OK
- 400Validation failed
- 404Book not in library
- 409Metadata conflict
PUT/v1/books/{id}/notesSet free-text notes (no activity event)
Set free-text notes (no activity event)
idstringreq
| field | type | notes |
|---|---|---|
| notesreq | string | maxLength 20000 |
- 200OK
PUT/v1/books/{id}/ratingSet rating and optional review
Set rating and optional review
idstringreq
| field | type | notes |
|---|---|---|
| starsreq | integer | min 1 · max 5 |
| review | string |
- 200OK
PUT/v1/books/{id}/recommendationSet recommendation provenance
Set recommendation provenance
idstringreq
| field | type | notes |
|---|---|---|
| recommendedBy | string | null | maxLength 200 |
| recommendedSourceUrl | string (uri) | null | |
| recommendedSourceBookId | string (uuid) | null |
- 200OK
PUT/v1/books/{id}/seriesUpdate book series metadata
Update book series metadata
idstringreq
| field | type | notes |
|---|---|---|
| seriesName | string | null | maxLength 300 |
| seriesPart | integer | null | min 1 · max 10000 |
- 200OK
- 400Validation failed
- 404Book not in library
PUT/v1/books/{id}/statusChange reading status
Change reading status
idstringreq
| field | type | notes |
|---|---|---|
| statusreq | enum: "to_read" | "reading" | "completed" | "dnf" | "paused" |
- 200OK
POST/v1/books/batchBatch add books to the caller's library by ISBN
Batch add books to the caller's library by ISBN
| field | type | notes |
|---|---|---|
| isbnsreq | array of string | |
| status | enum: "to_read" | "reading" | "completed" | "dnf" | "paused" | |
| collection | string | |
| genres | array of string | |
| recommendedBy | string | |
| recommendedSourceUrl | string (uri) |
- 200Batch processed with per-ISBN results
field type notes summaryreq object itemsreq array of object - 400Validation failed
- 401Unauthorized
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-sessionsList import sessions
List import sessions
- 200OK
field type notes sessions array of object - 401Unauthorized
POST/v1/import-sessionsCreate an import session
Create an import session
| field | type | notes |
|---|---|---|
| sourceType | string | null | maxLength 120 |
| sourceLabel | string | null | maxLength 300 |
| sourceFingerprint | string | null | maxLength 300 |
- 201Created
field type notes session object - 400Validation failed
- 401Unauthorized
GET/v1/import-sessions/{id}Fetch import session
Fetch import session
idstringreq
- 200OK
field type notes session object - 401Unauthorized
- 404Import session not found
POST/v1/import-sessions/{id}/commitCommit approved import rows
Commit approved import rows
idstringreq
| field | type | notes |
|---|---|---|
| confirmreq | boolean | |
| itemIds | array of string (uuid) |
- 200Commit processed with item-level results
field type notes session object previewSummary object commitSummary object results array of object - 400Validation failed
- 401Unauthorized
- 404Import session not found
GET/v1/import-sessions/{id}/itemsList import items
List import items
idstringreq
bucket"create" | "exact_match" | "update_existing" | "likely_match" | "conflict" | "invalid" | "skipped" | "committed" | "failed" | "enrichment_needed"state"pending" | "approved" | "skipped" | "committed" | "failed"qstringlimitintegeroffsetinteger
- 200OK
field type notes items array of object limit integer offset integer - 401Unauthorized
- 404Import session not found
POST/v1/import-sessions/{id}/itemsStage normalized import candidates
Stage normalized import candidates
idstringreq
| field | type | notes |
|---|---|---|
| candidatesreq | array of object | |
| replaceExisting | boolean |
- 200Candidates staged
field type notes session object previewSummary object staged integer - 400Validation failed
- 401Unauthorized
- 404Import session not found
PUT/v1/import-sessions/{id}/items/{itemId}/decisionSet import item decision
Set import item decision
idstringreqitemIdstringreq
| field | type | notes |
|---|---|---|
| kindreq | enum: "apply_source" | "keep_existing" | "merge" | "skip" | "leave_pending" | |
| note | string | null | maxLength 1000 |
| merged | object |
- 200Decision stored
field type notes item object previewSummary object - 400Validation failed
- 401Unauthorized
- 404Import item not found
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/sessionsList the caller's reading sessions
List the caller's reading sessions
limitinteger
- 200OK
POST/v1/sessionsLog a completed (retroactive) reading session
Log a completed (retroactive) reading session
| field | type | notes |
|---|---|---|
| bookIdreq | string (uuid) | |
| sessionDate | string (date-time) | |
| startPagereq | integer | min 0 |
| endPagereq | integer | min 0 |
| durationMinutes | integer | min 0 |
| notes | string |
- 201Created
POST/v1/sessions/startStart the caller's live reading timer (one per user)
Start the caller's live reading timer (one per user)
| field | type | notes |
|---|---|---|
| bookIdreq | string (uuid) | |
| startPage | integer | min 0 |
- 201Created
POST/v1/sessions/stopStop the caller's live timer; promotes to a reading_sessions row
Stop the caller's live timer; promotes to a reading_sessions row
| field | type | notes |
|---|---|---|
| endPage | integer | min 0 |
- 200OK
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/collectionsList the caller's collections
List the caller's collections
- 200OK
POST/v1/collectionsCreate or update a collection by name
Create or update a collection by name
| field | type | notes |
|---|---|---|
| namereq | string | |
| description | string |
- 201Created
GET/v1/collections/{id}Fetch a single collection
Fetch a single collection
idstringreq
- 200OK
- 404Not found
PATCH/v1/collections/{id}Rename or update description on a collection
Rename or update description on a collection
idstringreq
| field | type | notes |
|---|---|---|
| name | string | maxLength 120 · minLength 1 |
| description | string | null | maxLength 2000 |
- 200OK
- 404Not found
DELETE/v1/collections/{id}Delete a collection (member books are not deleted)
Delete a collection (member books are not deleted)
idstringreq
- 204No Content
- 404Not found
POST/v1/collections/{id}/books/{bookId}Add a book to a collection
Add a book to a collection
idstringreqbookIdstringreq
- 201Created
DELETE/v1/collections/{id}/books/{bookId}Remove a book from a collection
Remove a book from a collection
idstringreqbookIdstringreq
- 204No Content
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
Get the goal for a year
yearintegerreq
- 200OK
field type notes id string (uuid) year integer bookTarget integer | null pageTarget integer | null
PUT/v1/goals/{year}Set or update the annual goal
Set or update the annual goal
yearintegerreq
| field | type | notes |
|---|---|---|
| bookTarget | integer | null | min 0 |
| pageTarget | integer | null | min 0 |
- 200OK
GET/v1/goals/{year}/progressYear-to-date progress vs the annual goal
Year-to-date progress vs the annual goal
yearintegerreq
- 200OK
field type notes year integer bookTarget integer | null pageTarget integer | null booksCompleted integer pagesRead integer
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/activityRead the caller's activity feed (reverse chronological)
Read the caller's activity feed (reverse chronological)
sincestring— ISO timestamp or duration like "7d", "1h"untilstringtypesstring— Comma-separated list of event_type values.limitintegercursorstring— Pass the previous response's nextCursor to fetch older events.
- 200OK
field type notes items array of object nextCursor string | null
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/booksPassthrough search against Open Library (does not add anything)
Passthrough search against Open Library (does not add anything)
qstringreqlimitinteger
- 200OK
curl -H "Authorization: Bearer $LIBRIS_KEY" \ "https://your-libris.example.com/v1/search/books?q=tolkien&limit=5"
authors
GET/v1/authorsBrowse authors in the caller's library
Browse authors in the caller's library
qstringsort"name" | "book_count" | "recently_added"direction"asc" | "desc"pageintegerlimitinteger
- 200OK
field type notes itemsreq array of object pagereq integer limitreq integer totalreq integer hasPreviousPagereq boolean hasNextPagereq boolean - 400Invalid query
- 401Unauthorized
GET/v1/authors/suggestSuggest existing authors from the caller's library
Suggest existing authors from the caller's library
qstringlimitinteger
- 200OK
field type notes results array of object - 400Invalid query
- 401Unauthorized
series
GET/v1/seriesSuggest existing series names from the caller's library
Suggest existing series names from the caller's library
qstringlimitinteger
- 200OK
field type notes items array of object
users
GET/v1/users/{username}Public profile projection
Public profile projection
usernamestringreq
- 200OK
field type notes profilereq object Public 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. currentlyReadingreq array of object publicCollectionsreq array of object - 404User does not exist or profile is private
GET/v1/users/{username}/libraryPublic library list for a user
Public library list for a user
usernamestringreq
qstringstatus"to_read" | "reading" | "completed" | "dnf" | "paused"collectionstringgenrestringfavoritebooleanformatstringownedbooleansort"date_added" | "date_started" | "date_completed" | "rating" | "title" | "re_read_count"direction"asc" | "desc"limitinteger
- 200OK
field type notes items array of object - 404User missing, profile private, or library private
PUT/v1/users/me/avatarSet the authenticated user's avatar URL
Set the authenticated user's avatar URL
| field | type | notes |
|---|---|---|
| urlreq | string (uri) |
- 200Avatar updated
field type notes okreq boolean avatarUrlreq string - 400Invalid URL
- 401Not authenticated
DELETE/v1/users/me/avatarClear the authenticated user's avatar
Clear the authenticated user's avatar
- 200Avatar cleared
field type notes okreq boolean - 401Not authenticated
uploads
POST/v1/uploads/avatarUpload an avatar image
Upload an avatar image
- 200Avatar uploaded
field type notes okreq boolean avatarUrlreq string - 400Invalid request body
- 401Not authenticated
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.
{
"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.
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_bookAdd 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.
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.
| field | type | notes |
|---|---|---|
| query | string | Title/author search string |
| isbn | string | |
| title | string | maxLength 500 |
| subtitle | any | |
| type | any | |
| isbn13 | any | |
| pages | any | |
| publishYear | any | |
| publisher | any | |
| authors | array of any | |
| status | enum: "to_read" | "reading" | "completed" | "dnf" | "paused" | |
| collection | string | |
| genres | array of string | |
| recommendedBy | string | |
| recommendedSourceUrl | string (uri) |
add_books_by_isbnBatch 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.
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.
| field | type | notes |
|---|---|---|
| isbnsreq | array of string | |
| status | enum: "to_read" | "reading" | "completed" | "dnf" | "paused" | |
| collection | string | |
| genres | array of string | |
| recommendedBy | string | |
| recommendedSourceUrl | string (uri) |
create_import_sessionCreate 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.
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.
| field | type | notes |
|---|---|---|
| sourceType | any | |
| sourceLabel | any | |
| sourceFingerprint | any |
stage_import_candidatesStage 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.
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.
| field | type | notes |
|---|---|---|
| sessionIdreq | string (uuid) | |
| candidatesreq | array of object | |
| replaceExisting | boolean |
set_import_item_decisionResolve 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.
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.
| field | type | notes |
|---|---|---|
| sessionIdreq | string (uuid) | |
| itemIdreq | string (uuid) | |
| kindreq | enum: "apply_source" | "keep_existing" | "merge" | "skip" | "leave_pending" | |
| note | any | |
| merged | object |
commit_import_sessionCommit 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.
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.
| field | type | notes |
|---|---|---|
| sessionIdreq | string (uuid) | |
| confirmreq | boolean | |
| itemIds | array of string (uuid) |
remove_bookRemove 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.
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.
| field | type | notes |
|---|---|---|
| bookRefreq | string |
update_statusChange 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.
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.
| field | type | notes |
|---|---|---|
| bookRefreq | string | |
| statusreq | enum: "to_read" | "reading" | "completed" | "dnf" | "paused" |
update_book_metadataCorrect 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.
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.
| field | type | notes |
|---|---|---|
| bookRefreq | string | |
| title | string | maxLength 500 |
| subtitle | any | |
| type | any | |
| isbn13 | any | |
| pages | any | |
| publishYear | any | |
| publisher | any | |
| seriesName | any | |
| seriesPart | any | |
| externalOlId | any | |
| authors | array of any | |
| genres | array of string |
list_seriesList 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.
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.
| field | type | notes |
|---|---|---|
| q | string | maxLength 100 |
| limit | integer | min 1 · max 50 |
set_book_seriesSet 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.
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.
| field | type | notes |
|---|---|---|
| bookRefreq | string | |
| seriesNamereq | any | |
| seriesPart | any |
log_sessionLog a completed reading session (retroactive). Page range is required; duration is optional.
Log a completed reading session (retroactive). Page range is required; duration is optional.
| field | type | notes |
|---|---|---|
| bookRefreq | string | |
| sessionDate | string (date-time) | |
| startPagereq | integer | min 0 |
| endPagereq | integer | min 0 |
| durationMinutes | integer | min 0 |
| notes | string |
start_sessionStart 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.
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.
| field | type | notes |
|---|---|---|
| bookRefreq | string | |
| startPage | integer | min 0 |
stop_sessionStop the caller's live reading timer and promote it to a completed session.
Stop the caller's live reading timer and promote it to a completed session.
| field | type | notes |
|---|---|---|
| endPage | integer | min 0 |
rate_bookRate a book 1-5 stars and optionally attach a review.
Rate a book 1-5 stars and optionally attach a review.
| field | type | notes |
|---|---|---|
| bookRefreq | string | |
| starsreq | integer | min 1 · max 5 |
| review | string |
set_timelineUpdate 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.
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.
| field | type | notes |
|---|---|---|
| bookRefreq | string | |
| dateAdded | any | |
| dateStarted | any | |
| dateCompleted | any |
set_current_pageSet 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.
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.
| field | type | notes |
|---|---|---|
| bookRefreq | string | |
| pagereq | any |
set_favoriteToggle favorite status on a book in the caller's library.
Toggle favorite status on a book in the caller's library.
| field | type | notes |
|---|---|---|
| bookRefreq | string | |
| favoritereq | boolean |
set_book_provenanceUpdate 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.
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.
| field | type | notes |
|---|---|---|
| bookRefreq | string | |
| recommendedBy | any | |
| recommendedSourceUrl | any | |
| recommendedSourceBookRef | any |
set_book_external_linksSet 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.
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.
| field | type | notes |
|---|---|---|
| bookRefreq | string | |
| goodreadsId | any | |
| amazonUrl | any |
list_cover_candidatesList 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.
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.
| field | type | notes |
|---|---|---|
| bookRefreq | string |
set_cover_overrideSet 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.
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.
| field | type | notes |
|---|---|---|
| bookRefreq | string | |
| urlreq | string (uri) |
clear_cover_overrideClear the caller's per-book cover override for a book in their library. Cover rendering falls back to the global books.image_url.
Clear the caller's per-book cover override for a book in their library. Cover rendering falls back to the global books.image_url.
| field | type | notes |
|---|---|---|
| bookRefreq | string |
add_to_collectionAdd a book to a collection. Creates the collection if it doesn't exist.
Add a book to a collection. Creates the collection if it doesn't exist.
| field | type | notes |
|---|---|---|
| bookRefreq | string | |
| collectionreq | string |
set_goalSet or update the caller's annual reading goal.
Set or update the caller's annual reading goal.
| field | type | notes |
|---|---|---|
| yearreq | integer | min 1900 · max 3000 |
| bookTarget | integer | min 0 |
| pageTarget | integer | min 0 |
list_import_sessionsList 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.
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.
| field | type | notes |
|---|---|---|
| limit | integer | min 1 · max 100 |
get_import_sessionFetch one import session summary by ID. Returns the durable preview/commit state but not every row; call list_import_items for filtered rows.
Fetch one import session summary by ID. Returns the durable preview/commit state but not every row; call list_import_items for filtered rows.
| field | type | notes |
|---|---|---|
| sessionIdreq | string (uuid) |
list_import_itemsList staged import rows with optional bucket/state/search filters. Limit defaults are bounded so agents do not accidentally pull hundreds of full rows.
List staged import rows with optional bucket/state/search filters. Limit defaults are bounded so agents do not accidentally pull hundreds of full rows.
| field | type | notes |
|---|---|---|
| sessionIdreq | string (uuid) | |
| bucket | enum: "create" | "exact_match" | "update_existing" | "likely_match" | "conflict" | "invalid" | "skipped" | "committed" | "failed" | "enrichment_needed" | |
| state | enum: "pending" | "approved" | "skipped" | "committed" | "failed" | |
| query | string | maxLength 200 |
| limit | integer | min 1 · max 500 |
| offset | integer | min 0 |
explain_import_sessionExplain 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.
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.
| field | type | notes |
|---|---|---|
| sessionIdreq | string (uuid) | |
| limit | integer | min 1 · max 100 |
search_librarySearch 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.
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.
| field | type | notes |
|---|---|---|
| query | string | |
| status | enum: "to_read" | "reading" | "completed" | "dnf" | "paused" | |
| collection | string | |
| genre | string | |
| favorite | boolean | |
| sort | enum: "date_added" | "date_started" | "date_completed" | "rating" | "title" | "re_read_count" | |
| direction | enum: "asc" | "desc" | |
| limit | integer | min 1 · max 200 |
get_bookFetch one book with global metadata and the caller's user_books state.
Fetch one book with global metadata and the caller's user_books state.
| field | type | notes |
|---|---|---|
| bookRefreq | string |
list_authorsBrowse 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.
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.
| field | type | notes |
|---|---|---|
| query | string | maxLength 100 |
| sort | enum: "name" | "book_count" | "recently_added" | |
| direction | enum: "asc" | "desc" | |
| page | integer | min 1 |
| limit | integer | min 1 · max 100 |
get_authorFetch 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.
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.
| field | type | notes |
|---|---|---|
| authorIdreq | string (uuid) |
recent_activityReturn 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.
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.
| field | type | notes |
|---|---|---|
| since | string | |
| types | array of string | |
| limit | integer | min 1 · max 200 |
goal_progressReturn year-to-date books completed and pages read vs the caller's goal for a given year.
Return year-to-date books completed and pages read vs the caller's goal for a given year.
| field | type | notes |
|---|---|---|
| year | integer | min 1900 · max 3000 |
get_goalFetch the raw goal row for a year (null fields mean unset).
Fetch the raw goal row for a year (null fields mean unset).
| field | type | notes |
|---|---|---|
| yearreq | integer | min 1900 · max 3000 |
list_collectionsList all collections owned by the caller.
List all collections owned by the caller.
No arguments.
rename_collectionRename a collection and/or update its description. Pass only the fields to change.
Rename a collection and/or update its description. Pass only the fields to change.
| field | type | notes |
|---|---|---|
| collectionIdreq | string (uuid) | |
| name | string | maxLength 120 · minLength 1 |
| description | any |
delete_collectionDelete a collection. Member books remain in the library; only the collection and its memberships are removed.
Delete a collection. Member books remain in the library; only the collection and its memberships are removed.
| field | type | notes |
|---|---|---|
| collectionIdreq | string (uuid) |
search_open_librarySearch Open Library for books to potentially add. Does not touch the library — call add_book once you've chosen one.
Search Open Library for books to potentially add. Does not touch the library — call add_book once you've chosen one.
| field | type | notes |
|---|---|---|
| queryreq | string | |
| limit | integer | min 1 · max 20 |
get_public_profileRead 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.
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.
| field | type | notes |
|---|---|---|
| usernamereq | string | minLength 1 |
set_avatar_urlSet 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.
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.
| field | type | notes |
|---|---|---|
| urlreq | string (uri) |
clear_avatarClear 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.
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.
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_type | meaning |
|---|---|
| book_added | A book entered the caller's library through the normal add path. |
| book_removed | A book was removed from the caller's library while preserving global metadata. |
| status_changed | The reading_status moved between values. Specific transitions also emit one of the book_completed / book_dnfed / book_paused / book_resumed shorthands below. |
| session_logged | A reading_sessions row was inserted — either retroactively (log_session) or by stopping a live timer. |
| rated | Star rating set or changed (1-5). |
| reviewed | Review body set or changed. |
| favorited | favorite flag turned on. |
| unfavorited | favorite flag turned off. |
| collection_added | Book joined a collection. |
| collection_removed | Book left a collection (collection itself untouched). |
| collection_updated | A collection was renamed or its description changed. |
| collection_deleted | A collection was deleted; member books remained in the library. |
| goal_set | An annual goal was created for the first time for that year. |
| goal_updated | An existing annual goal had its targets adjusted. |
| book_completed | Status transitioned to completed. Fires alongside status_changed. |
| book_dnfed | Status transitioned to dnf. Fires alongside status_changed. |
| book_paused | Status transitioned to paused. Fires alongside status_changed. |
| book_resumed | Status moved out of paused back to reading. Fires alongside status_changed. |
| format_changed | Owned formats or the current reading format changed. |
| owned_changed | The owned flag changed. |
| reread_logged | The re-read count was incremented. |
| metadata_updated | Shared catalog metadata was corrected by the caller. |
| metadata_refreshed | External metadata refresh filled missing catalog fields. |
| recommendation_set | Recommendation provenance changed. |
| external_links_changed | Per-user Goodreads or Amazon links changed. |
| timeline_changed | Date added, date started, or date completed changed. |
| import_committed | An 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.
{ "error": "Unauthorized" } {
"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.
{ "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.
{
"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.
{
"jsonrpc": "2.0",
"error": { "code": -32603, "message": "Internal error" },
"id": null
}