An open-source, minimalistic, standalone PDF and Markdown reader that puts the features usually locked behind paywalls (editing PDFs, annotating, and signing) — into a free, local-first desktop app with advanced AI features.
On top of typical pdf reader features (view, rotate, zoom etc.) the app contains the following capabilities:
- The app is minimalistic by design and thus majority of junk present in your standard PDF reader is gone
- Autodetection of all your CLI agents
- Connect any LLM provider (remote or local)
- Semantic search in your file (type keyward and press enter to see the results)
- OCR conversion
- Group of images to PDF conversion
- Edit PDF (for now just page ordering)
- Light and dark themes with a theme switcher; defaults to the OS theme on first launch and remembers your choice.
- To see edits saved by another app, open the Markdown tab and click Refresh file beside Sign. The arrows turn once the tab has loaded the latest content from disk.
- Add, move, resize, edit, and delete text overlays (with font-size control).
- Highlight selected text.
- Add pinned comments from selected text; edit and delete them.
- Comments and highlights are saved as standard PDF annotations, viewable in other Acrobat-compatible readers.
- Detect fillable PDF form fields.
- Fill text fields, checkboxes, radio groups, and dropdowns.
- Form values are preserved in saved and exported PDFs.
- Draw a signature with mouse/trackpad, type it as text, or upload a signature image.
- Place, move, and resize visual signatures on the page.
Note: signatures are visual only — MarkPDF does not provide certificate-backed digital signatures, identity verification, or legal/compliance guarantees yet.
I am at the very early stage of what this app should be, but so far I see it as:
- Free, open-source alternative to paid alternatives
- PDF/Markdown viewer with advanced AI features for humans and AI agents.
- Minimalistic design
- Extensible with community plugins (just like Obsidian is)
- Not a "chat with pdf" app - you have your favorite chatbot for that (though possible via plugins in future - see below)
Looking for people who want to contribute to codebase and bring it to next level. Thus far, these are my ideas:
- Plugin interface - to enable community building easily on top of the core (just like in Obsidian)
- Signature interface - Bring Your Own Key (BYOK) for any signature provider - to remove vendor lock-in like the one in traditial PDF reader
- Expose as MCP/CLI (plus a Skill.md) for pdf-to-markdown and image-to-pdf conversions - to have a fixed realiable tool for this task
- Discussion interface for AI agents - read your PDF and discuss with multiple AI agents
- Obsidian plugin - read and discuss with agents in MarkPDF, save conclusions in Obsidian/MD file.
- Make semantic search state-of-the-art - I just did the basic one, good but not great
- Make OCR state of the art - handling images is missing
and anything else you think we should implement to make it awesome.
- TypeScript + React 19 for the UI.
- Electron for the desktop shell.
- PDF.js (
pdfjs-dist) for rendering. - pdf-lib for editing, annotations, forms, and export.
- Vite for bundling, electron-builder for packaging.
- Embeddings: generated locally with Transformers.js running ONNX models in-process. Curated models include BGE Small EN v1.5 (384-dim), MiniLM L6 v2 (384-dim), and BGE Base EN v1.5 (768-dim). Embeddings use mean pooling with L2 normalization.
- Vector store: a local SQLite database (via
better-sqlite3, with write-ahead logging) in the app's user-data directory, written from the main process so themarkpdfcommand and the app share one index. Document text is chunked, embedded, and stored as Float32 vector blobs alongside their source page and heading path, with deduplication by content hash so re-opening a document doesn't re-index it. - Retrieval: queries are embedded with the same model and ranked by cosine similarity against the stored chunk vectors, with a configurable score threshold (loose / balanced / strict) to tune precision vs. recall.
- Tunable chunking: precise, balanced, and contextual presets control chunk size and overlap to trade granularity against context.
- Text extraction: native PDF text where available, falling back to Tesseract.js OCR for scanned pages so even image-only PDFs become searchable.
MarkPDF ships a markpdf command that runs the same index, the same extractor and the same
embedding model as the application. Install it from Settings › CLI & MCP › Command Line.
markpdf prepare <path> [--json]
markpdf index <path...> [--recursive] [--force]
markpdf search <query> (--path <pdf> | --id <hash> | --library) [--mode hybrid|semantic|exact] [--top-k 12] [--min-score 0.3]
markpdf outline <path> [--depth 3]
markpdf convert <path...> [--pages 3-7] [--mode page-preserving|clean] [--out file.md]
Add --json to any of them for machine-readable output on standard output; progress and
diagnostics go to standard error, so the two never mix.
It starts with no permission to read anything. Grant a folder once and it is remembered:
markpdf --allow-read ~/Papers
markpdf --allow-write ~/Notes # separate: reading never implies writing
markpdf --revoke-read ~/Papers # and withdrawing a folder withdraws everything inside it
Run it on a terminal and it offers the grant with a single keystroke. Run it from a script and it never prompts: it exits 5 and prints the exact command that would grant what it needed. A document that is already indexed can still be searched by its path or hash after you withdraw the folder, because that answer comes from the index and never opens the file.
Search the whole library with --library. One query ranks the passages of every document that
markpdf index read from a folder you still grant. Withdrawing a folder takes its documents out of
library results, and a document the app indexed on its own joins them once you grant its folder and
run markpdf index on it. The command says how many indexed documents it left out, never which.
Choose how passages are ranked with --mode. hybrid, the default, fuses the ranking by meaning
with a ranking by exact words. semantic ranks by meaning only. exact returns only passages that
contain every word or quoted phrase of the query. "Exact" means the words, ignoring case and
accents, so "clause 4.2(b)" also matches "Clause 4-2 b". --min-score filters the ranking by
meaning only, so it has no effect in exact mode, and the command refuses it there.
Scanned pages are read with OCR, entirely offline — the language data ships with the application.
Exit codes: 0 success, including an empty search result. 1 an unexpected failure, 2 usage,
3 not found, 4 not indexed, 5 access denied, 6 could not be read as a PDF, 7 some of a
batch failed, 8 a bundled dependency is missing, 9 the index is busy, 69 MarkPDF itself
could not be found or run, 130 interrupted.
MarkPDF also speaks the Model Context Protocol, so an assistant that supports MCP can read your documents directly instead of shelling out to the command. It is the same index, the same extractor and the same permissions — an MCP client is another way in, not another set of rules.
Register it once with your client — the server runs through the installed markpdf command, so
install that first from Settings › CLI & MCP. For Claude Code:
claude mcp add --scope user markpdf -- markpdf mcp
For a client that reads a JSON configuration file, the same thing as entries in its server map —
markpdf as command and ["mcp"] as args:
{
"mcpServers": {
"markpdf": { "command": "markpdf", "args": ["mcp"] }
}
}Add MARKPDF_DATA_DIR in env if you keep your index somewhere other than the default. After replacing or updating MarkPDF, reconnect its MCP server in your client so requests use the current application; an already-running server keeps its previously loaded code.
If MCP lists documents but cannot show passages or add suggestions, check the desktop request troubleshooting.
Eleven tools:
| Tool | What it does | What it needs |
|---|---|---|
prepare_document |
Open a local PDF, save eligible generated outline metadata, and prepare the shared retrieval index | Existing read permission; separate write permission when saving |
outline |
The heading tree with page numbers, the page count, and whether the document has a text layer | Nothing, if the document is indexed; otherwise permission to read it |
search |
The passages of one indexed document that answer a question, each with its page and headings, ranked by meaning, exact words or both (mode) |
Nothing — it reads the index and never opens the file |
search_library |
The passages across the whole library that answer a question, each with its document's hash and name, page and headings | Nothing to run. It finds only documents markpdf index read from a folder that is still granted |
read_pages |
The text of specific pages, which is how you get from a search hit to the material around it | Nothing — index only |
to_markdown |
The document as Markdown, or written to a file | Permission to read it, and separately to write, if you give output_path |
list_open_documents |
The names, opaque references, and state of documents currently open in MarkPDF, without their paths | Nothing |
read_open_document |
Specific pages from an open PDF, or a bounded slice of an open Markdown buffer | Nothing for indexed PDFs or captured Markdown; otherwise an existing read grant |
reveal |
Bring a document forward at a page and mark a quoted passage for a few seconds, or return a signed markpdf:// link that does that when clicked |
A read grant on the file; a link needs only the index |
read_comments |
Every comment and highlight of a saved PDF: author, whether a person, an AI agent or another application wrote it, page, marked words and note | A read grant on the file, also when it is indexed |
suggest_annotation |
Propose a highlight or a comment on an open PDF; the person accepts or rejects it, and nothing reaches the file before that | A read grant on the file, and the document open in one tab |
outline, read_pages, and to_markdown name one document by path or by id — the content
hash another tool returned — never both. search also accepts an opaque ref returned by
list_open_documents, including ref: "active", so an assistant can search the PDF in front
without learning its path. Open-document references are session-scoped and must not be reused after
the tab closes.
No tool grants or forgets anything. prepare_document can index and save only within existing grants. Give permission out of band in Settings > General > Folder access (R for read, RW for read and write), or with markpdf --allow-read and, when saving is needed, markpdf --allow-write. An assistant cannot widen its own access. Existing search and read_pages still use the index without reopening the PDF.
For setup, separate save/index outcomes, incomplete-page fallback and recovery, see agent PDF preparation. For showing evidence, reading comments and suggesting annotations, see agent evidence and comments. An agent's annotations are marked as an AI agent's on the page, in the comments pane, and — once the person accepts and saves one — in the file's own author field, which every PDF reader shows.
Every reply is bounded, and says so: if a document is longer than one answer can carry you are told how much was left out rather than handed a shortened one that reads as complete.
The reusable agent instructions for selecting the narrowest route live in
skills/markpdf-retrieval/SKILL.md.
npm install
npm run devnpm run buildnpm run package # unpacked app directory
npm run dist # distributable installer
npm run dist:mac # macOS DMG and ZIP for the current architectureThe public download channel is GitHub Releases:
- Latest release page: https://github.com/cwik-tech/MarkPDF/releases/latest
- Direct latest Apple Silicon download: https://github.com/cwik-tech/MarkPDF/releases/latest/download/MarkPDF-mac-arm64.dmg
Before publishing macOS releases, add these GitHub Actions secrets:
MAC_CSC_LINK: base64-encoded Apple Developer ID Application.p12certificate.MAC_CSC_KEY_PASSWORD: certificate password.MAC_CSC_NAME: Developer ID Application signing identity qualifier without theDeveloper ID Application:prefix.APPLE_ID: Apple ID used for notarization.APPLE_APP_SPECIFIC_PASSWORD: app-specific Apple ID password.APPLE_TEAM_ID: Apple Developer Team ID.
To publish a release, update package.json version, commit the change, tag the same version with a v prefix, and push the tag:
git tag v0.1.0
git push origin main --tagsThe release workflow builds an Apple Silicon macOS DMG and ZIP on a native runner, signs and notarizes them, then uploads them to the GitHub Release for the pushed tag. MarkPDF requires an Apple Silicon Mac.
MarkPDF source code is licensed under the Apache License, Version 2.0. See LICENSE.
The MarkPDF name, logo, icons, product identity, and associated branding are not licensed under Apache-2.0. See NOTICE and TRADEMARKS.md.
Third-party package notices are listed in THIRD_PARTY_NOTICES.md.
Contributions are accepted under Apache-2.0. See CONTRIBUTING.md.


