paper-handler-mcp
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@paper-handler-mcp outline the graph-nets paper, then tighten the methods section"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
paper-handler
Scaffold, render and manage academic papers with Quarto — Typst by default, LaTeX when a journal template needs it. Ships as a Python CLI, an MCP server for agents, and Nix modules, so the same corpus renders identically on a laptop and on a server.
$ paper-handler new graph-nets --title "Graph Neural Networks"
$ paper-handler render papers/graph-nets
$ paper-handler list
NAME STATUS ENGINE RENDERED TITLE
graph-nets draft typst yes Graph Neural NetworksWhy
A paper corpus is a directory tree of .qmd files, a references.bib and
whatever data they cite. Getting from that to a PDF usually means "install
Quarto, install Typst, hope the fonts resolve". paper-handler removes the
guesswork:
One toolchain derivation. Quarto (which bundles Pandoc and Typst), an optional TeX Live scheme and an optional Jupyter kernel, in a single Nix package.
quarto renderworks with no network and no separately-installed TeX.Templates ship inside the package.
paper-handler newworks offline on a fresh clone; a corpus can still add or override templates.Configuration is layered and local.
/etc< XDG config < a corpus's ownpaper-handler.toml<--config<PAPER_HANDLER_*. Relative paths resolve against the file they came from, so a corpus stays self-describing.Agent-native without a token tax. The bundled MCP server exposes the outline/section/edit/lint loop as compact JSON tools, so a model can write a paper one section at a time instead of loading the whole document. It is stdlib-only and runs in the same derivation as the CLI.
Related MCP server: Academia MCP
Install
NixOS
{
inputs.paper-handler.url = "github:Cairnstew/paper-handler";
# ...
imports = [ inputs.paper-handler.nixosModules.default ];
my.programs.paper-handler = {
enable = true;
engine = "typst"; # or "latex" / "both"
users = [ "seanc" ]; # write access to the shared corpus
};
}That puts the CLI, the MCP server (paper-handler-mcp) and the toolchain on
PATH system-wide, creates /var/lib/paper-handler/papers (mode 2775, group
paper-handler), writes /etc/paper-handler/config.toml, and optionally starts
a render timer with render.enable = true.
Home Manager
{
imports = [ inputs.paper-handler.homeManagerModules.default ];
my.programs.paper-handler.enable = true;
}For opencode, use the combined module instead — it imports the package module, installs the CLI/toolchain and registers the MCP server, with an optional researcher-python entry in the same import:
{
imports = [ inputs.paper-handler.homeManagerModules.opencode ];
my.programs.paper-handler-opencode = {
enable = true;
configFile = "${config.home.homeDirectory}/papers/paper-handler.toml";
# Add this to register researcher-python-mcp alongside paper-handler-mcp:
researcher.package =
inputs.researcher-python.packages.${pkgs.system}.default;
};
}Both entries use a 120 s startup timeout; opencode's 5 s default is shorter
than a cold Nix shell. researcher.package is optional — if you import
researcher-python's own Home Manager module instead, leave it null so only one
module owns that entry.
Bare / non-Nix
$ nix run github:Cairnstew/paper-handler -- doctor
$ nix profile install github:Cairnstew/paper-handlerOr, with any Python ≥ 3.11 and a Quarto on PATH:
$ pip install .Usage
Command | What it does |
| Start a corpus: |
| Scaffold a paper from a template. |
| Render one paper. |
| Corpus contents and render state. |
| Heading tree and per-section word counts. |
| Placeholders, heading jumps, long prose, undefined citations, readability. Exits 1 on errors. |
| Read or edit YAML front matter. |
| Built-in and corpus templates; |
| Starter |
| Create a paper from a researcher-python handoff bundle. |
| Counts, screening state, core papers and citation keys. |
| A handoff or imported paper's counts. |
| Refresh generated |
| Append the generated PRISMA flow as a Quarto include. |
| Probe the toolchain and render a minimal document. |
| Run the MCP server over stdio (same as |
Every render runs with the paper's own directory as the working directory,
so bibliography: references.bib and relative include paths resolve the way
you expect no matter where you invoked the CLI from.
Toolchain overrides for a long-lived MCP server
paper_doctor/doctor no longer equates "the binary exists" with "rendering
works": they render a minimal HTML document and report render: ok | BROKEN.
A long-lived server cannot change its own PATH, so the lookup honours:
PAPER_HANDLER_QUARTO,PAPER_HANDLER_TYPST,..._PANDOC,..._PDFLATEX,..._LUALATEX,..._LATEXMK,..._JUPYTER,..._PYTHON— an exact binary path or a directory containing it;PAPER_HANDLER_TOOLCHAIN_DIR— a bin directory searched before PATH.
An explicit override that does not resolve is an error naming the variable,
not a silent fall-back. paper_render adds a hint when Quarto's stderr shows
the Pandoc version-skew signature (Aeson exception / Unknown option).
Word counts
paper_context, paper_lint and the write receipt report the assessable body
separately from the rest:
words/words_body— prose between the front matter and the firstReferences,BibliographyorAppendixheading, headings excluded;words_references,words_appendix,abstract_words.
A 2,000-word limit therefore compares against words_body, not against a total
that includes the reference list.
Templates
Built-in templates live at paper_handler/templates/:
Template | Engine | Output |
| typst |
|
| latex |
|
| typst | research-paper sections (Related Work, Limitations) |
| typst | Introduction, Background, Themes, Synthesis, Gaps, Conclusion |
Put a directory of the same shape in <papers_dir>/.templates/ (or set
templates_dir) to add your own; same-named corpus templates win.
Inside a template, {{name}}, {{title}}, {{date}}, {{engine}} and
{{format}} are substituted in .qmd/.md/.yml/.bib/.typ/.tex/.toml
files.
MCP server
paper-handler speaks the Model Context Protocol
over stdio. The server is stdlib-only, so it ships inside the same Nix package
as the CLI — no extra Python closure, no daemon, no network.
$ paper-handler-mcp # standalone console script
$ paper-handler mcp # same server, launched through the CLI
$ nix run .#mcp-server # straight from the flakeFlake output | What it is |
| The server binary ( |
|
|
| Drives the packaged server with real JSON-RPC in the sandbox |
| One import: CLI/toolchain + paper-handler MCP (+ optional researcher MCP) |
Tools
Every tool returns compact JSON. Paper text never crosses the wire unless you ask for one section.
Tool | Use it for |
| Corpus index with status, engine and word counts. Start here. |
| Budgeted context pack: outline, citations, issues, style rules. |
| Heading tree and per-section word counts. |
| One section, truncated to |
|
|
| Set the front-matter abstract. |
| Placeholders, heading jumps, long prose, undefined citations, readability. |
| Scaffold a paper or a corpus. |
| Render and get the PDF path, size and a short failure tail. |
| Read or edit front matter. |
| Bibliography hygiene. |
| Discover templates; doctor probes the tools and runs an HTML smoke render. |
| Pin a corpus for the session (for a server launched outside one). |
| Scaffold a paper from a researcher-python handoff. |
| Compact counts, core papers and citation keys. |
| Refresh generated |
| Resolve a DOI/arXiv id/title to its BibTeX key. |
| Insert/replace the PRISMA appendix include. |
Resources carry pull-based context:
paper://style-guide— the house style (read once)paper://corpus— the corpus indexpaper://templates— available templatespaper://papers/{name}/outline— one paper's outlinepaper://papers/{name}/context— one paper's budgeted context packpaper://papers/{name}/research— counts, screening state and citation keys
Prompts encode the writing loop: write_paper, revise_paper,
review_paper, and research_to_paper (handoff import → write → lint →
render).
Client configuration
A generic local stdio server entry:
{
"mcp": {
"paper-handler": {
"type": "local",
"command": ["paper-handler-mcp"],
"enabled": true
}
}
}For this repository's opencode setup, point at the flake output so the server and its config stay pinned:
my.programs.opencode.mcp.paper-handler = {
type = "local";
command = [
"${inputs.paper-handler.packages.${pkgs.system}.mcp-server}/bin/paper-handler-mcp"
];
};The server discovers the corpus from its working directory, exactly like the
CLI. Launch it from a corpus root, or set PAPER_HANDLER_CONFIG to an
explicit paper-handler.toml.
Writing loop
The intended low-token sequence is:
paper_context— outline, citations, current issues, style.paper_read_section— only the section being changed.paper_write_section— one section at a time.paper_lint— fix errors, then warnings.paper_render— once, at the end.
Extending it
Adding a tool is one decorated function:
@tool("paper_words", "Count words in one section.", schema({"heading": {"type": "string"}}))
def paper_words(args):
...Put it in a module under src/paper_handler/mcp/tools/ and import that module
from tools/__init__.py; the server discovers tools from the registry. Shared
document logic belongs in paper_handler.documents so the CLI can use it too.
Resources live in mcp/resources.py, prompts in mcp/prompts.py.
Using with researcher-python
researcher-python owns the
evidence half: search across arXiv/OpenAlex/Semantic Scholar/Crossref/PubMed,
screen with core|general|excluded, snowball citations, and render matrices and
PRISMA counts. paper-handler owns the manuscript half.
They meet at a versioned handoff bundle, not a Python dependency:
$ researcher-python handoff research-out/cw1 --out research-out/cw1-handoff
$ paper-handler research import research-out/cw1-handoff --paper ai-governance \
--title "AI Governance"The bundle contains only the included records (default: core + general):
handoff/
handoff.json # schema_version, source, counts, screening, file index
papers.json # included records, export envelope
references.bib # included records only, unique keys
citation-map.json # DOI/arXiv id/folded title -> citation key
matrix.md # literature matrix with a cite_key column
synthesis.md # dimension x triage-label counts
prisma.md # screening flow
README.mdImport creates the paper and copies the generated files under
<paper>/research/. The author keeps their own bibliography beside it:
bibliography:
- references.bib # yours; never overwritten
- research/references.bib # generated; do not editThat split is intentional. Re-running the researcher handoff and:
$ paper-handler research sync papers/ai-governance --handoff research-out/cw1-handoffrefreshes research/ and leaves the manuscript and your own entries alone.
Quarto/Pandoc accepts multiple bibliography files, so both sets of citations
resolve in one document.
The agent loop is:
paper_research_import(or the CLI above).paper_research_context— counts, screening, top core papers, citation keys.paper_read_section/paper_write_section, one section at a time.paper_cite_keywhen you have a DOI/title but not the key.paper_lint, thenpaper_render.
paper_context includes a small research block when a handoff is attached;
the full brief is only fetched on demand. The handoff schema is versioned
(schema_version: 1); a newer bundle is rejected with a clear message rather
than read incorrectly.
Configuration
# paper-handler.toml — at the root of a corpus, or ~/.config/paper-handler/config.toml
engine = "typst" # typst | latex | both
papers_dir = "papers" # relative to this file
templates_dir = "templates" # optionalPrecedence, lowest first: /etc/paper-handler/config.toml →
$XDG_CONFIG_HOME/paper-handler/config.toml → the nearest
paper-handler.toml walking up from $PWD → --config /
PAPER_HANDLER_CONFIG → PAPER_HANDLER_{ENGINE,PAPERS_DIR,TEMPLATES_DIR}.
Developing
$ nix develop # editable venv + full render toolchain
$ nix develop .#bootstrap # python + uv only; run `uv lock` from here on a fresh clone
$ just test # pytest
$ just mcp-test # MCP registry + JSON-RPC/stdio tests
$ just render-example # render papers/example
$ nix flake check # pytest + module evals + MCP + handoff import/renderDependency source of truth is pyproject.toml + uv.lock, consumed by
uv2nix. There is no dependency list in flake.nix.
Layout
flake.nix outputs: packages, overlays, both modules, checks, apps, devShells
nix/default.nix the CLI/MCP package (uv2nix venv + mkApplication)
nix/toolchain.nix Quarto + Typst/TeX Live + Jupyter, plus paper-toolchain-doctor
nix/module.nix NixOS: my.programs.paper-handler
nix/home-module.nix Home Manager: my.programs.paper-handler
nix/opencode-module.nix one-import opencode registration (CLI + MCP, optional researcher)
nix/checks.nix nix flake check (MCP protocol + researcher handoff + render)
src/paper_handler/ CLI, document core, and MCP server
documents.py outline, section edits, citations, lint, readability
research.py read and attach a researcher-python handoff bundle
templates/ typst-article, latex-article, research-article, literature-review
mcp/registry.py tool decorator + discovery
mcp/tools/ corpus, writing, quality, build, research tools
mcp/server.py stdio JSON-RPC (tools, resources, prompts)
tests/ pytest suite (sandboxed: no /etc, no ambient PATH)
fixtures/research-handoff/ a real bundle from researcher-pythonLicense
MIT.
Available Tools
23 toolspaper_bib_checkC
Check references.bib for structural problems (brace balance, duplicate or empty keys).
Inputs: file, name.
paper_bib_initC
Create a starter references.bib in a paper.
Inputs: name, force.
paper_cite_keyA
Resolve a DOI/arXiv id/title from the attached handoff to its BibTeX key. Use this before citing a record whose key you do not know.
Inputs: identifier (required), name.
paper_close_corpusA
Forget the session corpus and return to cwd/config discovery.
Inputs: no named parameters.
paper_contextA
A compact context pack for writing one paper: front matter summary, outline, citation keys, current diagnostics and house style. This is the best first call; it is bounded by budget tokens.
Inputs: name, budget.
paper_doctorA
Probe the render tools and render a minimal document; ok means a render actually succeeded, not just that quarto exists.
Inputs: no named parameters.
paper_initC
Initialise a paper corpus (config, _quarto.yml, papers/ directory).
Inputs: force, engine, directory, papers_dir.
paper_lintB
Structural and style diagnostics: placeholders, heading hierarchy, missing sections, long paragraphs/sentences, abstract length, undefined citations, readability. Returns at most max_issues; the counts are always complete.
Inputs: name, max_issues.
paper_listA
List the papers in the corpus with title, status, engine, render state and word count. Start here; it is much cheaper than reading any paper.
Inputs: no named parameters.
paper_meta_getB
Read one front-matter key, or the whole front matter when key is omitted.
Inputs: key, name.
paper_meta_setB
Set a front-matter key. The value is parsed as YAML, so 2026 becomes a number and '[a, b]' a list.
Inputs: key (required), value (required), name.
paper_newB
Scaffold a new paper from a template. Returns the entry path; then write it section by section with paper_write_section.
Inputs: name (required), force, title, engine, +1 more.
paper_open_corpusA
Pin a corpus for this MCP session (a corpus root or a papers/ directory). Use it when the server was launched outside the corpus.
Inputs: path (required).
paper_outlineB
The heading tree with per-section word counts, plus missing standard sections. Cheaper than reading the paper.
Inputs: name.
paper_read_sectionB
Read exactly one section (heading plus body), truncated to max_tokens. Use this instead of reading the whole file.
Inputs: heading (required), name, level, max_tokens.
paper_renderB
Render one paper (or the whole corpus with all=true) to PDF. Returns the output path, size and, on failure, a short stderr tail — not the full Quarto log.
Inputs: all, name, engine, keep_going.
paper_research_contextC
Compact evidence brief for an imported paper: counts, screening, top core papers and citation keys. Bounded by budget; never returns the bundle.
Inputs: name, budget, max_keys, max_papers.
paper_research_importB
Scaffold a paper from a researcher-python handoff bundle and wire its bibliographies. The manual references.bib is never overwritten.
Inputs: name (required), handoff (required), force, title, +2 more.
paper_research_prismaC
Insert or replace a Quarto appendix that includes the generated PRISMA screening flow (research/prisma.md).
Inputs: name, after, level, heading.
paper_research_syncB
Refresh a paper's generated research/ files from a newer handoff bundle. Manuscript text and the manual references.bib are untouched.
Inputs: handoff (required), name.
paper_templatesB
List built-in and corpus-local paper templates.
Inputs: no named parameters.
paper_write_abstractA
Set the front-matter abstract. Keeps the abstract out of the section flow; returns its word count and any diagnostics.
Inputs: text (required), name.
paper_write_sectionB
Replace, append to, or insert a section. Content is markdown without the heading. Returns a small receipt and the updated outline — not the whole document.
Inputs: content (required), heading (required), mode, name, +4 more.
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
Loading tool schema changelog…
TDQS
Scored across 23 tools
Most tools have clearly distinct purposes: corpus setup, paper scaffolding, section writing, metadata, diagnostics, rendering, and research import are well separated. A few pairs could be confused at first glance, especially paper_context vs paper_research_context and paper_lint vs paper_bib_check, but their descriptions clarify the boundaries.
All names use snake_case and share the paper_ prefix, with a clear paper_research_* subfamily for research tools. The internal pattern is not perfectly uniform (paper_read_section vs paper_bib_check vs paper_meta_get), but it remains predictable and readable.
With 23 tools, the server is on the heavy side for a paper-handling workflow and includes several specialized diagnostic and research-import operations. The tools are mostly justified by a broad domain, but the surface is large enough to feel borderline rather than tightly scoped.
The surface covers the paper lifecycle well: corpus setup, templates, scaffolding, metadata, section reading/writing, abstract writing, linting, bibliography handling, rendering, and research import/sync. Minor gaps remain, such as explicit paper deletion/renaming or section deletion, but core workflows are well supported.
Maintenance
Related MCP Connectors
Search arXiv/Semantic Scholar/OpenAlex + medical evidence (PubMed/Europe PMC) + LaTeX/PDF tools.
Write, compile and share Typst documents (papers, slides, drawings) from any AI agent.
Research-backed lint + generation for agent context files (CLAUDE.md, AGENTS.md) + prompt enhancer.
arXiv and OpenAlex/Crossref paper search (an open Google Scholar alternative) as agent tools.
Related MCP Servers
- AlicenseNot gradedqualityDmaintenanceEnables LLMs to search, analyze, and summarize academic research papers in real-time from arXiv, Semantic Scholar, and PubMed. Provides automatic deduplication, citation analysis, and BibTeX generation across multiple research databases.26 npmMIT
- AlicenseNot gradedqualityDmaintenanceEnables searching, fetching, and analyzing scientific papers from ArXiv, ACL Anthology, Semantic Scholar, and Hugging Face datasets, with optional LLM-powered document QA and research proposal workflows.245 PyPI94Apache 2.0
- FlicenseAqualityDmaintenanceEnables agents to search papers across Semantic Scholar and arXiv, read and extract text from arXiv PDFs, align records across sources, and produce structured literature-analysis digests.101-
- AlicenseNot gradedqualityDmaintenanceEnables MCP agents to search academic literature across multiple scholarly engines, explore papers, authors, and references, inspect abstracts and full text, maintain saved paper collections, and export citations, BibTeX, abstracts, or full-text corpora.1Apache 2.0