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:
| Permission | What it opens |
|---|---|
library:read | The library, a book, its chapters, its quality, its glossary, its bible, the licence, the cost estimates |
series:read | The series and the consistency report |
library:write | Settings of a book or a series, archiving, glossary entries, attaching volumes, previewing and applying term propagation |
library:delete | Deleting a book, a series or a term — and only with confirm |
pipeline:start | Starting work (this one spends money) |
jobs:read, jobs:control | Following work, and pausing, resuming, cancelling or retrying it |
results:read | Reading the translation back as text |
content:write | Listing 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_seriesandedit_termchange 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 (onlylocked: falsedoes);- 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_bookreturns 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.