Hirarapdf
Self-hosted PDF tools for AI agents — read, parse, and create. No API keys.
Part of the Hirara tool hub.
Pure-Python: pypdf reads,
reportlab writes. URL fetches route through the
hub's shared SSRF guard, hirara-core.
Three tools
| Tool | Role | What it does |
|---|---|---|
pdf_read |
reader | Full text + per-page text + metadata out of a PDF |
pdf_info |
parser | Structure only — page count, metadata, outline, form fields, page sizes |
pdf_create |
maker | A real, paginating PDF from Markdown, plain text, or HTML |
pdf_info is the cheap "what is this file" call — use it to decide whether or
how to read a large or unfamiliar PDF before paying to extract every page.
Quick start
pip install -e ../hirara-core # shared SSRF guard (repo-local)
pip install -e ".[service,mcp]"
As an HTTP service:
uvicorn hirarapdf.service:app --host 127.0.0.1 --port 8200
Read a PDF's text (base64, path, or URL):
curl -X POST localhost:8200/pdf_read -H 'content-type: application/json' \
-d '{"pdf_path":"/path/to/report.pdf"}'
Or upload it directly:
curl -X POST localhost:8200/pdf_read/upload -F file=@report.pdf
Inspect structure without extracting all the text:
curl -X POST localhost:8200/pdf_info -H 'content-type: application/json' \
-d '{"pdf_path":"/path/to/report.pdf"}'
Create a PDF from Markdown — returns pdf_base64, or writes to output_path:
curl -X POST localhost:8200/pdf_create -H 'content-type: application/json' \
-d '{"content":"# Hello\n\n- one\n- two","title":"Demo","output_path":"out.pdf"}'
As an MCP server over stdio (Claude Desktop / Cursor / agent runtimes):
python -m hirarapdf.mcp_server
With Docker (loopback-bound, nothing to download at boot):
docker compose up -d --build
The maker's input formats
pdf_create accepts three formats, all rendered pure-Python on top of
reportlab — no browser engine, no cairo/pango system libraries, the usual way
"HTML to PDF" balloons a container.
markdown — a lightweight subset, deliberately not full CommonMark:
#/##/### headings, -/* bullet lists, 1. numbered lists,
``` fenced code blocks, and inline **bold**, *italic*, `code`.
Not supported: tables, images, nested blockquotes. User content is XML-escaped
before the inline markup is applied, so a document that literally contains
<script> or & renders as text, not broken markup.
text — plain text; blank lines separate paragraphs. No markup at all.
html — a practical subset of HTML + CSS, rendered by
xhtml2pdf: headings, paragraphs,
lists, tables, images, and simple inline/@page CSS. It is not a modern
layout engine — JavaScript, flexbox and grid do not apply, so feed it
document-style HTML. A bare fragment is wrapped in a minimal document (so
title/author/subject reach the PDF metadata); a full <html> document is
passed through and controls its own <head>.
curl -X POST localhost:8200/pdf_create -H 'content-type: application/json' \
-d '{"format":"html","content":"<h1>Invoice</h1><table><tr><td>A</td><td>1</td></tr></table>","title":"Invoice"}'
Security
This tool turns attacker-influenceable bytes into and out of PDFs, so three levers matter:
pdf_url is off by default. When you enable it
(CPDF_ALLOW_URL_FETCH=true), the fetch goes through hirara-core's
safe_download — resolve-then-pin, every redirect hop re-validated, streamed
under a hard byte cap — so a public URL that 302s to 169.254.169.254 is
rejected at the hop. It is never a bare httpx.get.
Local filesystem access is gated. pdf_path (read) and output_path
(write) let a caller name any path on the host. Fine for a local MCP server or a
loopback service you trust; off in the Docker image because a
network-exposed service should not read or write arbitrary files. Set
CPDF_ALLOW_LOCAL_PATH=true only for a trusted, local deployment.
Everything is size-capped. Input PDFs are bounded by CPDF_MAX_BYTES,
extracted text by CPDF_MAX_CHARS (the result flags truncated=true), and
pdf_create input by CPDF_MAX_CREATE_CHARS.
Errors come back in the response body, not as HTTP status codes — the caller is an agent loop, and "PDF too large" is a result to reason about, not a transport failure. Every response carries the same keys whether it succeeded or failed.
Note — no OCR.
pdf_readextracts a PDF's embedded text layer. A scanned PDF that is just images with no text layer comes back with little or no text. OCR is a separate concern (and a candidate for a future hub tool).
API
| Endpoint | Purpose |
|---|---|
POST /pdf_read |
Extract text. pdf_path / pdf_url / pdf_base64, password, include_pages |
POST /pdf_read/upload |
Same, multipart file upload |
POST /pdf_info |
Structure only. Same sources |
POST /pdf_info/upload |
Same, multipart file upload |
POST /pdf_create |
Build a PDF. content, format, title, author, subject, page_size, output_path |
GET /schemas |
Tool definitions, ready for an LLM tools array |
GET /health |
Liveness + effective config |
Configuration
All via environment variable.
| Variable | Default | Notes |
|---|---|---|
CPDF_MAX_BYTES |
26214400 |
Cap on an input PDF (25 MB) |
CPDF_MAX_CHARS |
200000 |
Cap on extracted text; result flags truncated |
CPDF_ALLOW_URL_FETCH |
false |
Enable pdf_url (through the SSRF guard) |
CPDF_URL_TIMEOUT |
30 |
Seconds for a URL fetch |
CPDF_ALLOW_LOCAL_PATH |
true (lib) / false (image) |
pdf_path read + output_path write |
CPDF_PAGE_SIZE |
A4 |
Default page size for pdf_create |
CPDF_MAX_CREATE_CHARS |
500000 |
Cap on pdf_create input length |
Testing
pip install -e ".[dev]"
pytest -q
The suite roundtrips through both engines — the maker builds a PDF, the reader
reads it back — so no binary fixtures are checked in. It also proves pdf_url
routes through the shared SSRF guard and surfaces a block as an error envelope
rather than an exception.
License
Apache License 2.0 — see LICENSE and NOTICE.
Copyright 2026 Lucas D Marshall (Kaung Myat San).
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file hirarapdf-0.1.0.tar.gz.
File metadata
- Download URL: hirarapdf-0.1.0.tar.gz
- Upload date:
- Size: 28.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
78135eca2807b38001a9fb80dcbf3cb40d32ddef81950d7f39b92a3d63ec667c
|
|
| MD5 |
71901349af5b76e98a92820f8724dc5c
|
|
| BLAKE2b-256 |
60878a5c86961f0c29dc5a7c7ee6bc23a43f402c05d8e4f75c15b64760527e20
|
Provenance
The following attestation bundles were made for hirarapdf-0.1.0.tar.gz:
Publisher:
pypi-publish.yml on lucasdmarshall/Hirara
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
hirarapdf-0.1.0.tar.gz -
Subject digest:
78135eca2807b38001a9fb80dcbf3cb40d32ddef81950d7f39b92a3d63ec667c - Sigstore transparency entry: 2361786504
- Sigstore integration time:
-
Permalink:
lucasdmarshall/Hirara@a2dce6ceff7db8527a994bec2286597464bec7f9 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/lucasdmarshall
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
pypi-publish.yml@a2dce6ceff7db8527a994bec2286597464bec7f9 -
Trigger Event:
workflow_dispatch
-
Statement type:
File details
Details for the file hirarapdf-0.1.0-py3-none-any.whl.
File metadata
- Download URL: hirarapdf-0.1.0-py3-none-any.whl
- Upload date:
- Size: 26.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ada0109efab22c5de2337066a4ba4d3fff9e41dfb527ef532832b02248eaee0e
|
|
| MD5 |
3a1f75c9470b15e606845fd7633fddab
|
|
| BLAKE2b-256 |
d55ac9749f608f38d6fc557a9951c4087ff830c2edac640362b96a1013c69a6f
|
Provenance
The following attestation bundles were made for hirarapdf-0.1.0-py3-none-any.whl:
Publisher:
pypi-publish.yml on lucasdmarshall/Hirara
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
hirarapdf-0.1.0-py3-none-any.whl -
Subject digest:
ada0109efab22c5de2337066a4ba4d3fff9e41dfb527ef532832b02248eaee0e - Sigstore transparency entry: 2361786507
- Sigstore integration time:
-
Permalink:
lucasdmarshall/Hirara@a2dce6ceff7db8527a994bec2286597464bec7f9 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/lucasdmarshall
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
pypi-publish.yml@a2dce6ceff7db8527a994bec2286597464bec7f9 -
Trigger Event:
workflow_dispatch
-
Statement type: