The full upstream README, mirrored here for reference. Install config, tool schemas, adoption signals, and an original overview live on the DocSlicer listing page.
Lightning-fast (31 pages/sec), deterministic document parser and chunker for business documents. No LLM calls or heavy ML models.
DocSlicer turns PDFs, Word documents, HTML pages, and PowerPoint files into clean chunks, structured blocks, tables, charts, markdown and a navigable heading hierarchy.
Top score on BizDocBench (0.88 overall vs 0.70 for the next-best tool). 0.80 table accuracy, 0.98 content faithfulness, 0.85 heading recognition and hierarchy preservation, and 0.76 RAG retrieval performance.
Two ways to use it:
1., 1.2., 1.2.3) and free-form headings; uses font size, bold weight, and document structure — not inference; handles re-entry after exhibit breaks and repeated navigation headings across pageschunks, blocks, tables, charts, metadata, and hierarchy in one placepdf, docx, pptx, and html — including JS-rendered pages via PlaywrightMeasured with BizDocBench — an open benchmark for multi-format business document parsing. All scores are 0–1 (higher is better); pages_per_sec_aggregate is throughput across the full corpus.
| Tool | Score | Coverage | Speed | Hierarchy | Faithfulness | Tables | Retrieval | Pages/sec |
|---|---|---|---|---|---|---|---|---|
| docslicer | 0.8796 | 1.0000 | 0.8836 | 0.8466 | 0.9824 | 0.8047 | 0.7601 | 31.27 |
| docling | 0.7036 | 1.0000 | 0.3805 | 0.4905 | 0.8927 | 0.7467 | 0.7111 | 3.46 |
| markitdown | 0.5838 | 1.0000 | 0.8513 | 0.0604 | 0.7972 | 0.2584 | 0.5357 | 27.42 |
| unstructured | 0.5798 | 0.9091 | 0.1073 | 0.4327 | 0.9057 | 0.4812 | 0.6430 | 0.52 |
| opendataloader | 0.5359 | 0.5844 | 1.0000 | 0.3853 | 0.6484 | 0.2655 | 0.3317 | 117.26 |
| pymupdf4llm | 0.4519 | 0.5974 | 0.6492 | 0.1089 | 0.6456 | 0.3551 | 0.3552 | 11.84 |
| mineru | 0.4107 | 0.5974 | 0.1353 | 0.4220 | 0.6176 | 0.3012 | 0.3910 | 0.70 |
| marker | 0.3735 | 0.5974 | 0.1598 | 0.1926 | 0.6121 | 0.3012 | 0.3778 | 0.87 |
The core install is dependency-light. Optional features are available as extras:
Extras can be combined: pip install 'docslicer[html,ocr,llm]'.
Requires Python 3.10+
parse_document returns a ParseResult:
Each Chunk carries:
Every chunk carries its full heading breadcrumb, no matter how deeply nested. For example, a paragraph six levels deep in a financial filing:
This lets downstream code filter or group chunks by any level of the hierarchy without re-parsing the document.
| Format | Extension | Notes |
|---|---|---|
.pdf | Text-based and scanned (OCR extra required for scanned) | |
| Word | .docx | Full style and outline hierarchy |
| HTML | .html, URLs | Static files and JS-rendered pages (html extra required for URLs) |
| PowerPoint | .pptx | Slides, speaker notes, charts |
Not supported: .doc, .ppt (legacy Office formats), .xlsx.
parse_document auto-detects the format from the file extension or magic bytes. Pass a file path, URL, raw bytes, or a file-like object:
parse_document (and the format-specific functions) accept options that control what
gets parsed and how, before it's chunked. Format-specific toggles are accepted everywhere
for a uniform API but only take effect for the relevant format.
Because DocSlicer is structure-aware, it initially produces one chunk per heading or paragraph boundary. For documents with many short sections this can yield a lot of small chunks. With merge_small_chunks=True (the default), sibling sections under the same parent heading are merged together until they reach min_chunk_size — but never across heading boundaries into a different parent.
For example, these five short sections all fall under ## Products and Services Performance:
Instead of four tiny chunks, they get merged into one coherent chunk that still carries the correct path for each paragraph. Set merge_small_chunks=False if you need one chunk per section regardless of size.
table_representation controls how tables are serialised into chunk text. Given a
financial table with multi-row column headers:
"markdown" (default) — preserves the original 2D layout:
"melted" — one row per cell, headers joined with >. Good for sparse or
pivot-style tables where individual cell retrieval matters:
"jsonl" — one JSON object per row, multi-row headers joined with _. Useful
when chunks are fed into structured extraction or tool-use pipelines:
Point parse_all at a folder (or pass a list of paths/URLs). It yields (source, result)
pairs, and a file that fails to parse yields the Exception instead of aborting the batch.
Any parse_document keyword — chunk sizes, include_*, etc. — is forwarded per document.
DocumentParser holds a fixed ParseConfig across many documents and keeps a single
browser open across HTML/URL inputs (launched lazily on the first HTML parse), so a batch
of URLs starts Chromium once instead of once per document. Use it as a context manager so
that browser is always released:
There are two independent knobs, and they compose:
ParseConfig(max_workers=N) — within a single document: parallelizes PDF word
extraction, cell building, and OCR across processes (default: auto, sized to CPU cores).
Best when documents are large.DocumentParser(config, workers=N) — across documents: fans whole documents out
over N worker processes, each with its own config and browser. Best when you have many
documents. Results arrive in submission order (this path isn't lazy per-document).Setting workers alone defaults each worker's max_workers to 1, so nested pools don't
oversubscribe the machine; set both explicitly to run both levels at once. The workers
path can't forward a browser session or on_stage callback across processes — leave
workers unset when you need those.
Guard your entry point. DocSlicer uses a
ProcessPoolExecutorwhenever there's real CPU work to fan out — any PDF over ~50 pages, any scanned/OCR PDF of any length, and both parallelism knobs above. This is not opt-in: a plaindocslicer.parse_document("big.pdf")triggers it too. On macOS and Windows, Python spawns workers by re-importing your script top to bottom, so a parse that runs at module level makes each worker re-run it and spawn again — raisingRuntimeError: An attempt has been made to start a new process before the current process ... bootstrapping phase. Put your parsing code inside a function behind anif __name__ == "__main__":guard:
Most chunking libraries give you a flat list of text segments. DocSlicer also gives you a navigable tree of the document's heading structure, extracted deterministically from the document itself.
This is particularly useful for agents and retrieval pipelines working with long documents: rather than feeding the entire document into context, the agent can inspect the outline first to understand the structure, decide which sections are relevant, and then pull only those chunks — keeping token usage proportional to the task.
.level(n) returns all headings at depth n. Pass a parent to scope it to a
specific subtree — the typical pattern for an agent navigating a long document:
find_heading matches any node whose text contains the search term (case-insensitive).
All retrieval methods recurse into subsections by default.
A parsed result is plain data, so you can persist it and reload it later. When an agent asks many questions about the same document, there's no need to parse it again on every question:
A reloaded result supports the full API — hierarchy, find_heading,
chunks_under, tables — so a long-running agent session or document server can
keep documents open across requests without re-parsing.
save() decides what to write from the path you give it.
Only result.json round-trips — the collection and directory forms write flat rows
without the heading hierarchy, so ParseResult.load() can't read them back.
parse_document automatically detects scanned pages and falls back to OCR when the
[ocr] extra is installed. No configuration needed — result.metadata.has_ocr
tells you whether OCR was used.
DocSlicer ships an MCP server, so LLM clients (Claude Desktop, Claude Code, Cursor, …) can parse and read documents directly.
Download docslicer-X.Y.Z.mcpb from the
latest release and
double-click it, or drag it onto the Claude Desktop window. You pick the folder
DocSlicer is allowed to read and write during install; no config file, and no
Python of your own — uv provisions the interpreter.
Every client below launches the server over stdio. uvx needs nothing
installed ahead of time:
If you'd rather install it once and skip the resolve on every launch, use
pip install 'docslicer[mcp]' (or uv tool install) and set
"command": "docslicer-mcp" with no args.
| Client | Where the config goes |
|---|---|
| Claude Code | claude mcp add docslicer -- uvx --from 'docslicer[mcp]' docslicer-mcp |
| Cursor | ~/.cursor/mcp.json, or .cursor/mcp.json per project |
| VS Code | .vscode/mcp.json (use a servers key instead of mcpServers) |
| Windsurf | ~/.codeium/windsurf/mcp_config.json |
| Zed | settings.json, under context_servers |
On GUI-launched clients, prefer the
.mcpb. An app started from the dock does not inherit your shellPATH— on macOS that excludes/opt/homebrew/bin— so a bareuvxordocslicer-mcpcan work in a terminal and fail when the client spawns it. Use an absolute path (which uvx) if you hit this. The extension sidesteps it entirely.
A parsed document is far larger than a model's context window, so the server
never returns one in a single call. parse registers the document and hands
back a doc_id handle plus a heading outline. Every other tool takes that
handle and returns a bounded slice — the model pulls in only what it needs.
| Tool | Returns |
|---|---|
parse | doc_id handle, title, page count, heading outline |
get_outline | The outline again, for when it scrolls out of context |
read | The text under one or more headings, named from the outline |
search | Headings to read, ranked, each with a snippet |
to_markdown | Writes the whole document to disk; returns the path |
Every outline line carries what reading it would cost:
That figure is the same estimate read reports back, so a budget made from the
outline holds when it is spent. Sizes are cumulative — a parent never costs less
than the children beneath it — which is what makes "descend or just read it" a
decision the model can make before spending the context rather than after.
read takes heading text exactly as the outline prints it. Where a heading
appears twice, prefixing any ancestor disambiguates it ("Notes > Revenue");
the full chain is never required. Returned text is interleaved with [Page X]
markers using the document's own page labels (S-23, iv), so a quotation can
be cited to the page it actually came from rather than to wherever its section
began.
search is the fallback for when the outline does not settle the question —
headings that name nothing useful (Note 14, Item 7A), or a figure buried in
a table no heading mentions. It combines a whole-word literal match with BM25
over the chunks, and returns places, not answers: each hit is a heading to
pass to read. Query terms that appear nowhere in the document are reported
back, so a query that scored well on one rare word can be recognised as the bad
query it was.
to_markdown is the escape hatch for when the user wants the document itself
rather than an answer drawn from it. It writes to disk and returns a path, so
nothing enters the model's context and document size stops mattering.
Parsed results are cached on disk, so re-parsing the same file with the same
options is free. The cache key includes the file's size and mtime — edit the
document and the next parse re-parses it automatically.
| Variable | Effect |
|---|---|
DOCSLICER_MCP_ROOT | Restrict file sources and written output to this directory tree. Several may be given, separated by : (; on Windows) |
DOCSLICER_MCP_ALLOW_CLAUDE_DIR | Set to 0 to drop the Claude desktop app's own directory from the allowed roots (default 1) |
DOCSLICER_MCP_ALLOW_URLS | Set to 0 to reject http(s) sources |
DOCSLICER_MCP_CACHE | Where parsed results are persisted (default ~/.cache/docslicer-mcp) |
DOCSLICER_MCP_CACHE_MAX_MB | Cache size ceiling, oldest pruned first (default 2048; 0 disables) |
Set DOCSLICER_MCP_ROOT when exposing the server to anything but yourself —
without it, any readable path on the machine is parseable, and to_markdown
can write anywhere the server process can.
Documents dropped into a chat. Attaching a file to a Claude conversation
does not hand the server the path you know it by: the app first copies it into
a per-session workspace under its own data directory (~/Library/Application Support/Claude on macOS, %APPDATA%\Claude on Windows), which is nowhere near
the folder you would have picked as your root. That directory is therefore
allowed alongside DOCSLICER_MCP_ROOT, so both routes work — the folder you
chose, and whatever you drop into the chat. It is only ever added to a root
you set; leaving DOCSLICER_MCP_ROOT unset still means no sandbox at all, not
a sandbox of that one directory. Set DOCSLICER_MCP_ALLOW_CLAUDE_DIR=0 to opt
out and accept only your own roots.
to_markdown writes beside the source document, except for a document dropped
into a chat: that copy lives in a session folder you cannot navigate to, so the
markdown goes to your first DOCSLICER_MCP_ROOT instead.
docslicer parses one document to JSON on stdout — for a quick look at a file,
or to pipe into jq.
It takes the same parsing and chunking options as parse_document; run
docslicer --help for the full list.
If you know the format upfront and want explicit failure on unexpected input, use the
format-specific variants. They accept the same arguments as parse_document:
Full policy: https://docslicer.ai/privacy
What is collected. Nothing. DocSlicer has no telemetry, analytics, crash reporting, or usage tracking, and requires no account, licence key, or registration.
How your documents are used. Parsing runs entirely on your own machine, in a
local process. Document contents are used only to produce the outline, text
slices, search results, and markdown you ask for, and are returned only to the
caller. Documents are never uploaded to DocSlicer or to any third party. When
running as an MCP server, DOCSLICER_MCP_ROOT bounds which directory tree may
be read from and written to.
Where data is stored, and for how long. Parsed results are cached on your
own disk — by default ~/.cache/docslicer-mcp, configurable with
DOCSLICER_MCP_CACHE. The cache is pruned to a size ceiling
(DOCSLICER_MCP_CACHE_MAX_MB, default 2048 MB); otherwise it persists until you
delete it, and deleting the directory removes it permanently with no copy
retained elsewhere. Nothing is written outside the cache directory and any
output path you supply.
Network access and third parties. No network request is made for a local
file. Requests leave your machine only when you pass an http(s) source: that
URL is fetched directly from the host you named, and for HTML pages Playwright
may load the subresources that page references, exactly as a browser would.
Requests to sec.gov send a User-Agent header identifying the client, as the
SEC fair-access policy requires. These hosts are third parties chosen by you,
not by DocSlicer, and their own policies govern what they log. Set
DOCSLICER_MCP_ALLOW_URLS=0 to reject remote sources entirely.
Third-party clients. When DocSlicer runs as an MCP server, the client (Claude, Cursor, …) handles the conversation under its own privacy policy. DocSlicer is not a party to that and receives nothing from it.
Contact. Privacy questions: jelle@docslicer.ai · Issues: https://github.com/DocSlicer/DocSlicer/issues
DocSlicer is dual-licensed:
See LICENSE-COMMERCIAL.md for details, or reach out about a commercial license.
mcp-name: io.github.DocSlicer/docslicer