MCP server
bibr mcp runs a Model Context Protocol
server over stdio, so agents (Claude Code, Claude Desktop, any MCP client)
can extract papers and query the results as tools — no shelling out to the
CLI, no parsing whole export files into the agent's context.
One warm pipeline serves the whole session: it is created on the first
extraction call and reused by later calls. Model residency follows the
configured memory mode, just as it does with bibr.Chewer.
Setup
The server uses the MCP Python SDK v2 (minimum 2.2.0) and needs the mcp extra:
uv sync --extra mcp # add MCP to a source checkout
uv add 'bibr[mcp]' # add MCP to a project that installed bibr from PyPI
Keep the extras required by your selected OCR/runtime as well; for example,
uv sync --extra all --extra vllm includes MCP, ML models, and the CUDA vLLM
runtime. See Installation for platform
choices. Loading and querying saved exports does not load extraction models
and does not need LLM credentials: if the LLM check at startup fails (no API
key, no local launcher), the server still starts, logs the problem on
stderr, and chew_paper / chew_url return it as a tool error.
Register it with your client — for Claude Code, from your project directory:
or in JSON client configs (Claude Desktop and most others):
{
"mcpServers": {
"bibr": {
"command": "uv",
"args": ["--directory", "/path/to/bibr", "run", "bibr", "mcp",
"--allow-dir", "/path/to/papers"]
}
}
}
uv --directory also sets the directory the server starts in, so the second
example names the papers directory with --allow-dir; see
File access.
Pipeline options are fixed at server start (they configure the shared warm
pipeline, exactly like bibr.Chewer): pass a subset of the bibr chew
flags after bibr mcp, e.g. bibr mcp --refs ner --crossref (enrichment is
off unless --crossref or CROSSREF_ENRICH=true turns it on). Everything
else (provider keys, model choice) comes from your .env /
settings as usual.
The HTTP endpoint keeps papers within an initialized MCP session and negotiates
protocol 2025-11-25. SDK v2 clients in automatic mode negotiate this protocol;
clients configured for the sessionless 2026-07-28 protocol must enable automatic
or legacy negotiation. The stdio server also supports the newer protocol because
its process already belongs to a single client.
Exports and query results use schema v12. get_metadata returns its paper fields
under metadata; summaries expose LLM usage as totals and breakdown within
the existing llm_usage summary field.
The tool surface
A full export is far too large for a single tool result (sentence-level
spans, table HTML, optionally base64 figures), so the tools follow a
chew once, query granularly contract: extraction returns only a compact
summary plus a paper_id, and the query tools read slices on demand.
| Tool | What it returns |
|---|---|
chew_paper(path, paper_id?) |
Runs the pipeline on one file in an allowed directory; compact summary (title, DOI, counts, validation, LLM token usage) + paper_id |
chew_url(url, paper_id?) |
Downloads a public https:// URL (SSRF-guarded — see below) and runs the same extraction |
load_paper(path) |
Registers an existing bibr export JSON (a regular file of at most 256 MB in an allowed directory) without re-processing; same summary |
list_papers() |
Papers loaded this session (id, title, DOI, source) |
get_paper_summary(paper_id) |
The compact summary again |
get_metadata(paper_id) |
Full metadata block (title, abstract, journal, paper type, integrity statements, …), authors, affiliations, funding |
get_sections(paper_id) |
Section hierarchy with IMRaD-style types and per-section sentence counts |
get_text(paper_id, section_id?, page?, offset?, limit?) |
Sentence spans with text_id/section_id/page_number, filtered and paginated |
search_text(paper_id, query, limit?) |
Case-insensitive substring search over sentences |
get_references(paper_id, offset?, limit?) |
Parsed bibliography entries, paginated, empty fields omitted |
get_reference_citations(paper_id, bib_id) |
Every in-text citation of one reference, with the full source sentence and page |
export_references(paper_id, format?, prefer?) |
The reference list as the text of one CSL-JSON (default), BibTeX or RIS file, with its content type and reference count (see Reference-manager files) |
get_tables(paper_id, table_id?) |
Printed label and caption per table; full HTML + cells for one table_id |
get_figures(paper_id, figure_id?) |
Captions and pages; the image is replaced by has_image |
save_paper(paper_id, path, compact?, overwrite?) |
Writes the complete export JSON to a .json path in an allowed directory; refuses to overwrite an existing file unless overwrite=True, and never writes through a symlink |
save_references(paper_id, path, format?, prefer?, overwrite?) |
Writes the reference list to a .bib, .ris or .json (CSL-JSON) path in an allowed directory, under the same rules as save_paper |
This maps directly onto bibr's auditability contract: an agent can pull a
claim from get_references, then get_reference_citations to see the exact
sentences and pages that cite it.
Stdio results live in server memory for the process lifetime, without a paper
count cap. save_paper persists one, and load_paper brings saved exports
(or bibr chew output) into a later session. Always use the returned
paper_id: if different sources have the same ID, the store adds a suffix.
get_text defaults to 200 rows and caps requests at 500; offset is
zero-based and page matches the exported 1-based PDF page number.
get_tables(table_id=...) returns the complete table, including every
page's HTML for a table continued across pages. get_figures replaces the
figure image with has_image, so no image data reaches a tool response.
File access
An agent reads the papers it processes, and text in a paper can try to
instruct it (prompt injection). So chew_paper, load_paper, save_paper
and save_references only reach files inside the directories you allow: by default
the directory bibr mcp starts in. Pass --allow-dir to choose others; it
can be repeated, and ~ is expanded:
Each path is checked after ~, .. and symlinks are resolved, so a symlink
inside an allowed directory that points outside it is refused. A refused path
fails with a tool error that names the allowed directories and this option;
the server's instructions name them too, so the agent knows where to look.
A --allow-dir that is not an existing directory stops the server at
startup. If your client starts servers in your home directory or in / (as
Claude Desktop can), the default covers that whole tree: the server logs a
warning on stderr then, and you should set --allow-dir explicitly.
Behaviour change
Earlier releases let these tools read and write any file your user
account could. Files outside the server's working directory now need
--allow-dir.
save_paper writes only .json files and never through a symbolic link: an
existing link at the target path is refused whether it points inside or
outside the allowed directories, or nowhere. It replaces an existing regular
file only with overwrite=True. load_paper reads only regular files of at
most 256 MB, so a device or named pipe fails at once instead of exhausting
memory or blocking the server.
save_references writes only .bib, .ris and .json files, in the
format the extension names (a format that names another fails), under the
same symlink and overwrite rules.
URL downloads (chew_url)
chew_url lets an agent go straight from a link (arXiv, publisher, data
repository) to queryable extraction. Because fetching a caller-supplied URL
is the canonical SSRF surface — even locally, a hostile link must not reach
loopback services or a cloud VM's metadata endpoint — the download is
policy-gated by bibr.utils.safe_fetch:
- HTTPS only, port 443 only, no credentials in the URL. An
internationalised host name (
bücher.de) is fetched by its IDNA form (xn--bcher-kva.de). - Every DNS answer must be a public unicast address (private, loopback,
link-local, CGNAT, multicast, IPv4-mapped and IPv4-compatible tricks,
6to4, Teredo and site-local IPv6 are refused; a NAT64
64:ff9b::/96answer is judged by the IPv4 address it carries), and the connection is pinned to the validated IP — TLS SNI and certificate verification still use the hostname — so a DNS-rebinding race can't redirect the connection after validation. - The download always connects directly:
HTTPS_PROXY/ALL_PROXYfrom the environment are ignored, because a proxy would choose the destination itself. Custom CA bundles (SSL_CERT_FILE/SSL_CERT_DIR) still apply. - Redirects are followed manually (bounded) and every hop re-validated, so a public URL can't 302 into an internal network or downgrade to HTTP.
- Downloads are size-capped (100MB on the stdio server; the serve upload
limit remotely) under a wall-clock deadline. Compressed transfers are
refused rather than inflated: the request asks for
Accept-Encoding: identity, and a server that still answers with aContent-Encoding(gzip, br, ...) fails the download, since a few hundred bytes of stacked gzip can expand to gigabytes.
The SSRF guard keeps the download off internal addresses, but any public
HTTPS host is reachable, with a path and query string the agent chooses. An
agent following injected instructions could therefore send text it has read
(from a paper, or from a file in an allowed directory) to a server of the
attacker's choosing, encoded in the URL. If that matters for your setup,
limit downloads to hosts you trust
(MCP_URL_ALLOWED_HOSTS=arxiv.org,zenodo.org — subdomains included) or
remove the tool with MCP_CHEW_URL_ENABLED=false. Both settings apply to
bibr mcp and to the serve endpoint below.
Long extractions
A PDF through OCR + LLM can take minutes, and the first call also loads
models. The server streams MCP progress notifications (stage transitions,
per-region OCR progress) while it works; if your client enforces a tool
timeout, raise it for chew_paper (in Claude Code:
MCP_TOOL_TIMEOUT=600000). One paper is processed at a time on the shared
pipeline; concurrent chew_paper calls queue.
Remote MCP on bibr serve
A self-hosted deployment can expose the same tool surface
over the network: set MCP_ENABLED=true (with the mcp extra installed)
and bibr serve mounts a streamable-HTTP MCP endpoint at /mcp, e.g. for
Claude Code:
claude mcp add --transport http bibr https://bibr.example.org/mcp \
--header "Authorization: Bearer $AUTH_API_KEY"
It is gated by the same bearer auth as every REST route (AUTH_API_KEY;
a keyless loopback server answers only loopback Host and Origin headers,
see Authentication),
and extraction rides the regular serve dispatch — the LitServe workers'
resident models, admission control, and upload size caps all apply; no
second pipeline is loaded. Concretely: chew_paper and chew_url accept
files up to PIPELINE_MAX_FILE_SIZE (50 MiB), and because chew_paper
carries its file base64-encoded inside the JSON-RPC body, enabling MCP raises
the outer request-body cap to fit a full-size file in that form (about
68 MiB by default) — a larger body is refused with a 413 that says so. An
upload counts against PIPELINE_MAX_ACTIVE_UPLOADS both while its body is
received (HTTP 429) and while it is extracted (a server busy tool
error), exactly like a POST /papers/extract request.
Three differences from the stdio server:
chew_papertakes an upload, not a path —filenameplus base64content_base64, because client paths don't exist on the server (and the server never reads its own filesystem for clients). Per-call options matchPOST /papers/extract:start_page/end_page,refs,consolidate, andcrossref(true/false; omit to follow the server'sCROSSREF_ENRICH, which is off by default). Both MCP and REST use zero-based, inclusive page indices (0is the first page).chew_urlavoids the upload entirely: the server downloads a publichttps://URL itself under the SSRF policy above, capped at the serve upload limit. Operators can pin it to specific hosts withMCP_URL_ALLOWED_HOSTSor remove the tool withMCP_CHEW_URL_ENABLED=false, as on the stdio server.- No
load_paper/save_paper/save_references— these are host-filesystem tools; use the REST API when you want the full export JSON or the reference list as a file (export_referencesreturns the list's text). - Papers are per-session and bounded — each MCP client session gets its
own in-memory store, capped at
MCP_MAX_PAPERS_PER_SESSION(default 16; the paper chewed least recently is evicted, and re-chewing a paper counts as recent) and dropped when the session ends. A session that goes quiet forMCP_SESSION_IDLE_TIMEOUT_SECONDS(default 1800) is closed by the server and its papers are dropped, so clients that disconnect withoutDELETEdo not pin memory. The server as a whole keeps the papers of at most 64 sessions and at most 256 MiB of export JSON: beyond either, the least recently used session's or paper's data is dropped first, however many sessions are open. A query for a dropped paper fails withunknown paper_id; re-chew it, as after a disconnect.