Skip to main content
Glama
paper-handler-mcp

paper-handler-mcp

by Cairnstew

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 Networks

Why

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 render works with no network and no separately-installed TeX.

  • Templates ship inside the package. paper-handler new works offline on a fresh clone; a corpus can still add or override templates.

  • Configuration is layered and local. /etc < XDG config < a corpus's own paper-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-handler

Or, with any Python ≥ 3.11 and a Quarto on PATH:

$ pip install .

Usage

Command

What it does

paper-handler init [DIR]

Start a corpus: paper-handler.toml, _quarto.yml, papers/.

paper-handler new NAME

Scaffold a paper from a template. --engine, --template, --title, --force.

paper-handler render TARGET

Render one paper. --all for the whole corpus, --keep-going, -Q to pass an argument through to Quarto.

paper-handler list

Corpus contents and render state. --json for scripting.

paper-handler outline TARGET

Heading tree and per-section word counts. --json for scripting.

paper-handler lint TARGET

Placeholders, heading jumps, long prose, undefined citations, readability. Exits 1 on errors.

paper-handler meta get|set KEY

Read or edit YAML front matter. set parses YAML, so set year 2026 stores a number.

paper-handler templates list|add

Built-in and corpus templates; add installs a Quarto journal template.

paper-handler bib init|check

Starter references.bib, and a structural check (brace balance, duplicate keys).

paper-handler research import HANDOFF --paper NAME

Create a paper from a researcher-python handoff bundle.

paper-handler research context TARGET

Counts, screening state, core papers and citation keys.

paper-handler research status TARGET

A handoff or imported paper's counts.

paper-handler research sync TARGET --handoff DIR

Refresh generated research/ files without touching prose.

paper-handler research prisma TARGET

Append the generated PRISMA flow as a Quarto include.

paper-handler doctor

Probe the toolchain and render a minimal document. --json for CI; exit 1 if the smoke render fails.

paper-handler mcp

Run the MCP server over stdio (same as paper-handler-mcp).

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 first References, Bibliography or Appendix heading, 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-article

typst

format: typst

latex-article

latex

format: pdf, 11pt article class

research-article

typst

research-paper sections (Related Work, Limitations)

literature-review

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 flake

Flake output

What it is

packages.mcp-server

The server binary (paper-handler-mcp)

apps.mcp-server

nix run .#mcp-server

checks.mcp-protocol

Drives the packaged server with real JSON-RPC in the sandbox

homeManagerModules.opencode

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

paper_list

Corpus index with status, engine and word counts. Start here.

paper_context

Budgeted context pack: outline, citations, issues, style rules.

paper_outline

Heading tree and per-section word counts.

paper_read_section

One section, truncated to max_tokens.

paper_write_section

replace / append / insert; before= or after="" inserts at the start, create=true lets replace create a missing heading.

paper_write_abstract

Set the front-matter abstract.

paper_lint

Placeholders, heading jumps, long prose, undefined citations, readability.

paper_new / paper_init

Scaffold a paper or a corpus.

paper_render

Render and get the PDF path, size and a short failure tail.

paper_meta_get / paper_meta_set

Read or edit front matter.

paper_bib_check / paper_bib_init

Bibliography hygiene.

paper_templates / paper_doctor

Discover templates; doctor probes the tools and runs an HTML smoke render.

paper_open_corpus / paper_close_corpus

Pin a corpus for the session (for a server launched outside one).

paper_research_import

Scaffold a paper from a researcher-python handoff.

paper_research_context

Compact counts, core papers and citation keys.

paper_research_sync

Refresh generated research/ files from a newer handoff.

paper_cite_key

Resolve a DOI/arXiv id/title to its BibTeX key.

paper_research_prisma

Insert/replace the PRISMA appendix include.

Resources carry pull-based context:

  • paper://style-guide — the house style (read once)

  • paper://corpus — the corpus index

  • paper://templates — available templates

  • paper://papers/{name}/outline — one paper's outline

  • paper://papers/{name}/context — one paper's budgeted context pack

  • paper://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:

  1. paper_context — outline, citations, current issues, style.

  2. paper_read_section — only the section being changed.

  3. paper_write_section — one section at a time.

  4. paper_lint — fix errors, then warnings.

  5. 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.md

Import 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 edit

That split is intentional. Re-running the researcher handoff and:

$ paper-handler research sync papers/ai-governance --handoff research-out/cw1-handoff

refreshes 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:

  1. paper_research_import (or the CLI above).

  2. paper_research_context — counts, screening, top core papers, citation keys.

  3. paper_read_section / paper_write_section, one section at a time.

  4. paper_cite_key when you have a DOI/title but not the key.

  5. paper_lint, then paper_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"  # optional

Precedence, 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/render

Dependency 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-python

License

MIT.

Available Tools

23 tools
paper_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

    B3.2/5.0

    Scored across 23 tools

    Disambiguation4/5

    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.

    Naming Consistency4/5

    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.

    Tool Count3/5

    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.

    Completeness4/5

    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

    ActivityNo data
    ResponsivenessOutdated data

    Related MCP Connectors

    Related MCP Servers

    • A
      license
      Not graded
      quality
      D
      maintenance
      Enables 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 npm
      MIT
    • A
      license
      Not graded
      quality
      D
      maintenance
      Enables 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 PyPI
      94
      Apache 2.0
    • F
      license
      A
      quality
      D
      maintenance
      Enables 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.
      10
      1
      -
    • A
      license
      Not graded
      quality
      D
      maintenance
      Enables 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.
      1
      Apache 2.0