Source docs/mcp.md · 1de96aa

The MCP server: driving Libris from a coding agent

Libris answers the Model Context Protocol at POST /mcp. Any MCP client — opencode, Claude Code, Codex, or anything else that speaks the protocol — can then read your library, set a book up, keep the glossary, start and steer work, and read the translation back, in its own words rather than through curl.

It is the same installation, the same account and the same rules as the interface: access() decides what a book is yours to touch, the licence guard still refuses work that costs words, the budgets still pause a job that overspends, and everything lands in the same audit log. There is no second way in.

Make a token

Mon compte › Jetons d’API, Créer un jeton. Give it only the permissions the agent needs — this is the whole point:

PermissionWhat it opens
library:readThe library, a book, its chapters, its quality, its glossary, its bible, the licence, the cost estimates
series:readThe series and the consistency report
library:writeSettings of a book or a series, archiving, glossary entries, attaching volumes, previewing and applying term propagation
library:deleteDeleting a book, a series or a term — and only with confirm
pipeline:startStarting work (this one spends money)
jobs:read, jobs:controlFollowing work, and pausing, resuming, cancelling or retrying it
results:readReading the translation back as text
content:writeListing the providers

A token that only reads cannot delete, however the agent is asked to. The secret is shown once.

The MCP server is part of the automation API, which the Studio and Pro plans include. On an installation whose licence does not (Trial, Personal), no token can be made, and POST /mcp answers 402 with the JSON-RPC error -32002 and data.code = "automation_not_licensed"; existing tokens are kept, and work again once the licence includes the API.

Connect a client

The transport is streamable HTTP and the credential is an ordinary bearer token.

# Claude Code
claude mcp add --transport http libris https://libris.example/mcp \
  --header "Authorization: Bearer lbr_…"
// opencode.json
{
  "mcp": {
    "libris": {
      "type": "remote",
      "url": "https://libris.example/mcp",
      "headers": { "Authorization": "Bearer lbr_…" }
    }
  }
}

Codex uses TOML, not the opencode JSON format. Add this to ~/.codex/config.toml on the machine where Codex runs:

[mcp_servers.libris]
url = "https://libris.example/mcp"
bearer_token_env_var = "LIBRIS_MCP_TOKEN"

Provide LIBRIS_MCP_TOKEN in the environment of the process that launches Codex. For persistence, use that launcher’s protected environment configuration; exporting it in another terminal does not change a running process. Restart Codex after changing it, and never commit the secret to a repository. Libris uses its own API tokens, not OAuth: this connection requires no browser login or localhost callback. A separate GitLab MCP server may have different authentication requirements. See the official Codex MCP configuration reference.

Nothing is kept between two calls: the token carries the whole context, so there is no session to open and none to lose. The server never speaks first, so GET /mcp answers 405 — that is the protocol’s way of saying there is no stream to listen to.

The tools

The catalogue contains 28 tools. initialize reports the count from the running catalogue.

Reading — list_books, get_book, list_chapters, book_quality, list_terms, read_bible, list_series, get_series, series_consistency, list_providers, licence_status, estimate_job.

Changing — configure_book, shelve_book, create_series, update_series, attach_volume, add_term, edit_term, propagate_term.

attach_volume takes a volume_number, or a free volume_label for a special volume (« DX1 », « EX », « LN 14+ »), never both; list_books gives both fields.

Deleting — delete_book, delete_series, delete_term.

Work — start_job, list_jobs, control_job, control_series, export_book.

Three things an agent is made to deal with, and that are worth knowing when you read what yours did:

Confirmation accepts only the JSON boolean true. Strings such as "false" or "yes", numbers and arrays never authorize deletion, even when Python would consider them truthy.

  • configure_book, update_series and edit_term change only what they are given. The current settings or term are read first, so a call that mentions the quality does not quietly reset the target language, and a call that changes a term’s translation does not unlock it (only locked: false does);
  • anything that deletes needs confirm: true. Without it the tool destroys nothing and answers what would have gone — the title of the book, the number of volumes of the series;
  • lists are paginated and long answers are cut, always with the total stated. A library of five hundred books is not an answer, it is a context window. export_book returns text: ask for a range of chapters rather than a whole serial.

Arguments must follow the published schema: an unknown field or a wrong type is an isError answer, not a coerced value. limit is an integer from 1 to 200; offset is a non-negative integer. An offset beyond the end returns an empty page. An inaccessible object and an absent object give the same refusal, without revealing whether another account owns it.

Start, suspend or resume a whole series

control_series, with the existing jobs:control scope, takes series_id and an action: start, pause, resume or cancel. Only the series owner can use it; sharing one volume grants no control over the rest of its series.

start runs the whole pipeline — analysis, translation, review — on every volume, and asks for pipeline:start as well: an agent given the right to stop a series cannot be talked into translating it. It refuses a suspended series (resume first, deliberately) unless the call also sets resume_if_paused: true — off by default, so an agent only lifts a pause when that is actually what it was asked to do. It leaves archived volumes out, and does not start a volume that is already working.

pause persists a suspension on the series before visiting its jobs. New paid launches, including source-watch autostarts, are refused until resume clears that suspension. Already sent provider calls may finish; no new paid admission crosses the suspension. Reading, editing and exporting remain available. cancel cancels jobs but does not clear a suspension or create one.

The answer is best-effort, not a global 409: volumes names every volume, the number of changed jobs, whether it is done, and a refusal code and message when applicable. Totals done, refused and deferred distinguish success, refusal and scheduled resumption. A full account or token queue schedules excess resumes persistently; provider/account/token running quotas still apply. Licence and budget refusals are rechecked even after a manual pause.

A token may only resume paid jobs originally started by that same token; session-started work and another token’s work are refused individually. Pause and cancel remain available to its owner.

Propagate a glossary decision

Changing a glossary term governs future translation. propagate_term also carries that decision back through existing translations without calling a model or spending licence words:

{
  "name": "propagate_term",
  "arguments": {
    "book_id": "an-id-from-list_books",
    "previous": "Pilule Dorée",
    "replacement": "Noyau d’Or"
  }
}

This only previews. dry_run defaults to true in both the catalogue and the handler. The answer counts passages, occurrences and chapters, reports protected human corrections separately, and provides up to five before/after examples. After examining them, repeat the same call with dry_run: false and the JSON boolean confirm: true to write. Neither a missing confirmation nor strings such as "false" authorize changes. Every changed passage gets a version in its history, and a passage modified concurrently is skipped instead of overwritten.

series defaults to true, but includes only volumes this account may edit; read-only and private sibling volumes are excluded from both preview and writing. Set series: false for just this volume, or chapter_id for one chapter. Replacement is exact and case-sensitive. Human corrections remain excluded unless include_human: true is explicitly requested. This tool requires library:write even for a preview and advertises readOnlyHint: false, destructiveHint: false. An explicitly included human correction keeps its human protection and validation state after the term changes: subsequent model output still cannot replace it.

One interactive scan accepts at most 20,000 translated passages and 10 million serialized characters (source, translation and passage metadata), across the entire selected series. Replacement output has the same character ceiling. The database checks the input before loading the text; an oversized request writes nothing. Narrow it to a volume or a chapter and preview again. Thai and Lao, like Han and kana, match literal terms inside unspaced text. Case remains deliberate: Pilule and pilule require separate previews, so a proper name never silently becomes a noun. Tools and database sessions execute together in the API’s bounded worker pool, not on its async event loop. This keeps unrelated requests responsive without sharing a session between threads.

What it costs

start_job spends money at your provider and words of your licence. estimate_job answers what an operation would cost on that book — words, passages, price, and what is left of the budget — and an agent should be told to call it first. A job paused by the licence guard or by a budget never resumes on its own: control_job with resume is what restarts it.

MCP jobs keep the originating token: its budget, waiting/running quotas and maximum priority apply just as they do to automation requests. Direct MCP calls and REST requests are counted once each. An agent may pause or cancel any job it may edit, but it may resume paid work only when that work was started by the same token. Use the interface to resume work started elsewhere. The free sync_memory operation is exempt from that ownership restriction.

A rate limit is not a spending reservation: concurrent jobs can still overshoot the shared token cap while already-started calls finish. Keep concurrency and provider-side limits conservative; the shared-reservation improvement is tracked separately.

Rate limit

The same ceiling per token as the automation API (API_RATE_LIMIT_PER_MINUTE), because an agent in a loop is the ordinary case rather than the exception. Every message in a JSON-RPC batch counts, including notifications, not just its HTTP envelope. The entire batch is admitted or refused before any tool executes. Over the limit, the response is 429 with Retry-After.

A batch contains at most 200 messages and a request body at most 1 MiB, regardless of cookies or the installation’s upload limit. A valid session cookie never authenticates /mcp: only its bearer token does. A request with id: null receives an answer; a notification without id does not. Empty arrays, null, strings or numbers are not valid parameter objects. Unpaired Unicode surrogates are refused as malformed JSON before any tool in the request or batch can write. Proper surrogate pairs, emoji and CJK text remain accepted.