Any file in.Markdown out.

  • 22 formats
  • No LLM in the path
  • Runs on your machine

Every agent, index and eval starts with the same unglamorous problem: the knowledge is locked in files, and models read text. Papyrus is the layer in between — and it keeps the structure that makes the text worth reading.

waiting for a file
Source

Drop a document

PDF, Word, PowerPoint, Excel, HTML, EPUB, notebooks, archives — 22 formats.

Nothing is stored — files are converted and discarded.

Markdown

The converted Markdown lands here — with YAML provenance, page anchors you can cite, and tables that survived the trip.

One line

Give your agent eyes on any file.

Your agent can already read .txt and .md. It cannot read the PDF, the deck, or the spreadsheet — those are opaque binaries, and the usual workaround is a shell pipeline it reinvents every session.

Papyrus ships an MCP server. Five tools, shaped around the context window rather than around the Python API: a 300-page report paginates instead of swallowing the conversation, and inspect_document tells the model what reading a file would cost before it spends the tokens.

Install
# Claude Code
claude mcp add papyrus -- papyrus-mcp

# or any MCP host, in its config
{
  "papyrus": { "command": "papyrus-mcp" }
}
inspect_documentwhat is this, and what would reading it cost?
convert_documentread it as Markdown, paginated
convert_to_filewrite to disk, spend almost no context
convert_to_chunksretrieval chunks with page citations
list_supported_formatseverything it can read

The argument

Text extraction gets the words and loses the meaning.

Below is one page of a financial report. On the left, what a text-extraction call returns. On the right, what Papyrus returns. The words are nearly the same. Only one of them can answer “what was EBITDA in 2025?”

Extracted text

Annual Report 2025 | Confidential      ← on every page

Executive Summary                       ← was a heading
Revenue grew 41% year over year, ahead
of the conservative plan set at the
start of the period.                    ← lines, not a paragraph

Metric 2024 2025 Revenue 10.0M 14.1M
EBITDA 0.9M 2.1M                        ← was a table

3                                       ← page number

Papyrus

---
source: {filename: report.pdf, sha256: 9f2a…}
document: {pages: 48, author: Finance}
---
<!-- papyrus:page 3 -->

## Executive Summary

Revenue grew 41% year over year, ahead of the
conservative plan set at the start of the period.

| Metric  |  2024 |  2025 |
| ---     |  ---: |  ---: |
| Revenue | 10.0M | 14.1M |
| EBITDA  |  0.9M |  2.1M |

How it works

One contract in the middle.

Parsers never emit Markdown. Renderers never parse files. Everything meets at the Document IR — which is why adding a format is one self-contained file, and why the same engine can target something other than Markdown tomorrow.

bytes

Detect

Magic bytes decide, not the filename. A PDF renamed .docx is still a PDF.

parse

Parse

One parser per format, each about 150 lines. None of them writes Markdown.

ir

Document IR

Headings, lists, tables, code, images, page anchors. The contract everything meets.

render

Render

CommonMark plus GFM tables. If the output is wrong, the bug is in a parser.

chunk

Chunk

Split on headings, never mid-table. Each chunk carries its section path and page.

Coverage

What each format actually keeps.

FormatRecovered
pdfOutline, font-ranked headings, geometric tables, running headers dropped
docxStyles, true document order, list nesting, hyperlinks, inline emphasis
pptxSlides as sections, reading order, speaker notes, chart source data
xlsxEvery sheet, number formats honoured, regions split on blank rows
html · xmlDOM walk — headings, nested lists, tables, fenced code, links
epubChapters in spine order, Dublin Core metadata
csv · tsvDelimiter sniffed, header row detected, numeric columns aligned
json · jsonlArrays of objects become tables; nesting becomes headings
ipynbCells, outputs, errors and plots
emlHeaders, preferred body part, attachment manifest
rtfText and emphasis recovered without a dependency
zipEvery member converted, recursively, with the bombs refused
imagesMetadata, and a text layer when OCR is enabled
code · text · mdFenced with the right language; Markdown passes through

Four ways in

A command, an import, an endpoint, or a tool call.

Command line
# one file, or a whole tree
papyrus convert report.pdf
papyrus convert docs/ -o out/ -r

# + chunks.jsonl for your index
papyrus convert report.pdf --chunk

# what is this file, and why
papyrus inspect weird.bin
Python
from papyrus import convert, ConvertOptions

result = convert(
    "report.pdf",
    ConvertOptions(chunk=True),
)

result.markdown          # str
result.document.blocks   # the IR
result.chunks            # with citations
result.write("out/")      # bundle
HTTP
papyrus serve --port 8787

curl -F file=@report.pdf \
  localhost:8787/v1/convert

# POST /v1/convert   markdown | bundle
# POST /v1/chunk     embedding-ready
# POST /v1/compare   before and after
# POST /v1/detect    identify only
# GET  /v1/formats   what is supported
MCP
claude mcp add papyrus -- papyrus-mcp

# then, in the conversation:
"Read Q3-board-deck.pptx and pull out the revenue table."

# the model calls inspect_document,
# then convert_document, and answers

What it is for

Four jobs, one unglamorous step.

retrieval

Filling an index

Chunks arrive with heading paths and page numbers, so a retrieved fragment can still cite where it came from. Retrieval quality is capped by ingestion quality.

agents

Handing files to a model

One MCP install and the agent reads decks, contracts and workbooks the same way it reads source code.

evals

Building a test set

Conversion is deterministic — the same bytes always produce the same Markdown — so a fixture stays a fixture and a diff means something.

archives

Moving a decade of files

Point it at a directory or a zip. It converts every member, reports what it could not read, and never stops on the first bad file.

Straight answers

The questions people actually ask.

Does it use an LLM?

No. There is no model call in the conversion path, which is what makes cost predictable and output reproducible. An AI cleanup pass belongs after the intermediate representation, never inside a parser.

What happens to my documents?

Nothing is written to disk and nothing is retained. The hosted demo converts in memory and discards; run the container and the file never leaves your network at all. Even share links carry their excerpt inside the URL rather than a database.

How is this different from pdftotext or MarkItDown?

Those give you the words. Papyrus keeps the structure the words were arranged in — heading levels, table headers, reading order, page provenance — because that is the part a model needs and the part flat text throws away.

What about scanned PDFs?

It detects them and says so rather than returning a confident blank. Install the OCR extra and pass --ocr to transcribe them; that path needs Tesseract on the host and is approximate rather than deterministic.

Will it choke on a 400 MB zip of mixed junk?

It will refuse it, on purpose, and tell you which ceiling it hit. Every limit — size, pages, cells, archive members, compression ratio, nesting depth — is an environment variable.

Can I add a format?

A parser is one file of about 150 lines that returns the intermediate representation. It inherits the whole invariant suite the moment its fixture lands, so you find out immediately if it produces Markdown nobody can parse.

Where it runs

Your documents never leave the building.

There is no model call in the conversion path. That makes cost predictable, output reproducible — the same bytes always produce the same Markdown — and deployment a matter of running a container next to the files it reads.

GuardDefault
file size50 MB
pdf pages2,000
sheet cells1,000,000
csv rows50,000
archive members500
compression ratio200:1, then refused
archive nesting3 deep
uploaded namessanitised, no traversal

Nothing uploaded is ever executed. Every limit is an environment variable.