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). Eighteen tools, 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

Both REST and MCP authenticate the same way: a long-lived API key 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, agents get keys, the underlying behavior is identical.

Keys are scoped to the issuing user and act as that user. There are no per-key scopes or rate limits in V1; that lands once the surface needs it.

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 — Libris will not silently search the global catalog.

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

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 — writes one row to activity_events. That row is what an agent reads to discover what happened, when, and why. See Activity events for the type catalog. 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.

28 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
fieldtypenotes
querystring
isbnstring
statusenum: "to_read" | "reading" | "completed" | "dnf" | "paused"
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}/favorite

Toggle favorite

Path parameters
  • idstringreq
Request body (required)
fieldtypenotes
favoritereqboolean
Responses
  • 200OK
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}/status

Change reading status

Path parameters
  • idstringreq
Request body (required)
fieldtypenotes
statusreqenum: "to_read" | "reading" | "completed" | "dnf" | "paused"
Responses
  • 200OK
curl — search the library
curl -H "Authorization: Bearer $LIBRIS_KEY" \
  "https://your-libris.example.com/v1/books?status=reading&limit=10"

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
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"

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
DELETE/v1/users/me/avatar

Clear the authenticated user's avatar

Responses
  • 200Avatar cleared
    fieldtypenotes
    okreqboolean
  • 401Not authenticated

uploads

POST/v1/uploads/avatar

Mint a Vercel Blob upload token for an avatar image

Request body (required)

(empty object)

Responses
  • 200Blob upload token

    (empty object)

  • 400Invalid request body
  • 401Not authenticated
28 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), run the HTTP transport. Each request carries its own API key, so a single deployment serves multiple users.

POST /mcp — list tools
curl -X POST https://your-mcp-host.example.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 key, runs the request, and tears down. Headers GET /mcp and DELETE /mcp return 405 by spec.

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.

add_book

Add a book to the caller's library. Supply either a free-text query or an ISBN. Optionally set the initial status, add to a collection, tag with genres, or record who recommended it.

Arguments
fieldtypenotes
querystringTitle/author search string
isbnstring
statusenum: "to_read" | "reading" | "completed" | "dnf" | "paused"
collectionstring
genresarray of string
recommendedBystring
recommendedSourceUrlstring (uri)
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"
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
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
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
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.

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 (via add_book or import).
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).
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.

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
}