Skip to main content
Glama
kakalition

apollo-pdf-creator

by kakalition
README.md
# apollo-pdf-creator

[![CI](https://github.com/kakalition/apollo-pdf-creator/actions/workflows/ci.yml/badge.svg)](https://github.com/kakalition/apollo-pdf-creator/actions/workflows/ci.yml)
[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
[![Python 3.10+](https://img.shields.io/badge/python-3.10%2B-blue.svg)](pyproject.toml)
[![MCP](https://img.shields.io/badge/MCP-server-6E56CF.svg)](https://modelcontextprotocol.io)

A standalone, **stateless** [Model Context Protocol](https://modelcontextprotocol.io)
server that renders **PDF documents** — reports, invoices, letters, resumes, and
book-style documents — from a compact JSON spec, and converts Markdown to PDF.

It is the document engine that used to ship as the `pdf-creator`
[Agent Skill](https://skills.sh) in
[apollo-pack](https://github.com/kakalition/apollo-pack), re-homed as an MCP.
The render core (`scripts/_lib.py`, `scripts/_engine.py`, `scripts/_themes.py`,
`scripts/templates.py`) is reused as-is; reportlab is pulled in lazily only for
the actual render.

- **Spec in, PDF out** — one JSON object (or a Markdown string), one PDF
  (returned inline and written to disk).
- **Stateless** — no database, no render history, no state written; safe to run
  as many copies as you like.
- **stdio or HTTP** — MCP over stdio by default, with Streamable HTTP (`/mcp`)
  and SSE (`/sse`) transports built in.
- **Modern house style** — near-black type, generous spacing, one restrained
  accent, hairline rules, and a family of themes (`default`, `report`,
  `invoice`, `letter`, `resume`, `book`, `minimal`, `elegant`).
- **Rich blocks** — headings, lists, tables, images, code, callouts, columns,
  TOC, cover pages, QR codes, barcodes, and more.
- **Discoverable** — themes, demo specs, and the spec schema are read-only MCP
  resources, plus a `make-pdf` prompt.

## Requirements

- **Python 3.10+**. The MCP layer depends on `mcp`; rendering depends on
  `reportlab` (and `Pillow` for extra image formats). Both are installed by
  `uv sync`.
- Non-rendering calls (`validate_spec`, plus `create_pdf`'s validation step)
  work without reportlab; a render fails fast with a `dependency_missing` error
  if it is absent.

## Install

```bash
git clone https://github.com/kakalition/apollo-pdf-creator
cd apollo-pdf-creator
./install.sh
```

`install.sh` is idempotent: it checks for `uv`, runs `uv sync`, and prints
run/transport/inspector examples. `uv` is the only extra tool you need; install
it from <https://docs.astral.sh/uv/>.

## Transports

The server speaks MCP over **stdio** by default, and can serve the same API over
HTTP:

| Transport | Flag | Endpoint |
|---|---|---|
| stdio (default) | `--transport stdio` | stdin/stdout |
| Streamable HTTP | `--transport streamable-http` | `http://HOST:PORT/mcp` |
| HTTP + SSE | `--transport sse` | `http://HOST:PORT/sse` |

```bash
uv run apollo-pdf-creator                                          # stdio
uv run apollo-pdf-creator --transport streamable-http --port 8000  # HTTP
uv run apollo-pdf-creator --transport sse --port 8000              # SSE
```

`--transport`, `--host` (default `127.0.0.1`), and `--port` (default `8000`)
also read `APOLLO_PDF_TRANSPORT`, `APOLLO_PDF_HOST`, and `APOLLO_PDF_PORT`; log
verbosity reads `APOLLO_PDF_LOG_LEVEL` (`DEBUG`…`CRITICAL`, default `INFO`). The
HTTP transports bind to loopback by default; put a reverse proxy (and auth) in
front before exposing them beyond the host.

## Use with an MCP client

Point a stdio client at the console script:

```json
{
  "mcpServers": {
    "apollo-pdf-creator": {
      "command": "uv",
      "args": ["run", "--directory", "/absolute/path/to/apollo-pdf-creator", "apollo-pdf-creator"]
    }
  }
}
```

Or run it over HTTP and point a streamable-HTTP client at
`http://127.0.0.1:8000/mcp`:

```bash
uv run apollo-pdf-creator --transport streamable-http --port 8000
```

Explore it interactively with the MCP Inspector:

```bash
uv run mcp dev src/apollo_pdf_creator/server.py
```

## Tools

| Tool | Arguments | Returns |
|---|---|---|
| `create_pdf` | `spec` (required), `out`, `home`, `theme`, `base_dir`, `allow_remote`, `force` | JSON metadata (path, pages, bytes, sha256, width/height, page size, orientation, theme, warnings, home) **and** an inline `application/pdf`. |
| `validate_spec` | `spec` (required), `strict` | JSON mirroring `render.py check`: `valid`, `spec_version`, `blocks`, `theme`, `warnings`; with `strict`, engine-resolved `flowables` and engine warnings. |
| `from_markdown` | `markdown` (required), `title`, `author`, `theme`, `toc`, `number_headings`, `header`, `footer`, `out`, `home`, `base_dir`, `allow_remote`, `force` | JSON metadata **and** an inline `application/pdf`. |

Defaults:

- `out` — `<output_dir>/<slug>.pdf`, named from `meta.title` (or `document`).
- `home` — `$PDF_CREATOR_HOME`, else `~/.local/share/pdf-creator`; read-only for
  `fonts/` and `assets/`, and the default output root.
- `base_dir` — the server process cwd, used to resolve relative image/font paths.
- `author` (from_markdown) — the `PDF_CREATOR_AUTHOR` setting.
- `allow_remote` — off; the renderer will not fetch remote images unless asked.
- `force` — `false`; an existing `out` raises a `conflict` error.

### Example

```json
{
  "spec": {
    "spec_version": 1,
    "theme": "report",
    "meta": { "title": "Quarterly Report", "author": "Acme Analytics" },
    "header": { "text": "Quarterly Report", "align": "right", "divider": true },
    "footer": { "text": "Page {page} of {pages}", "align": "center" },
    "toc": { "title": "Contents", "depth": 2 },
    "content": [
      { "type": "heading", "level": 1, "text": "Executive summary" },
      { "type": "paragraph", "text": "Revenue grew **12%** quarter over quarter." },
      { "type": "table", "header": ["Region", "Revenue"], "zebra": true,
        "rows": [["North America", "$4.1M"], ["Europe", "$2.6M"]] }
    ]
  }
}
```

`create_pdf` returns a text block like:

```json
{
  "out": "/home/you/.local/share/pdf-creator/output/Quarterly-Report.pdf",
  "pages": 2,
  "bytes": 28411,
  "sha256": "6f1c…",
  "width_pt": 595.28,
  "height_pt": 841.89,
  "page_size": "595.3x841.9",
  "orientation": "portrait",
  "theme": "report",
  "warnings": [],
  "home": "/home/you/.local/share/pdf-creator"
}
```

followed by an `application/pdf` content block with the document.

## Resources (read-only)

| URI | Content |
|---|---|
| `pdfcreator://themes` | Every theme's name, label, and font family. |
| `pdfcreator://theme/{name}` | One theme's merged palette, styles, header, and footer. |
| `pdfcreator://demos` | Names of the bundled example specs. |
| `pdfcreator://demo/{name}` | A full demo spec (`blank`, `report`, `invoice`, `letter`, `resume`). |
| `pdfcreator://schema` | A concise reference for every spec key and block type. |

## Prompt

| Name | Purpose |
|---|---|
| `make-pdf` | Gives the model the spec contract (top-level keys, block types, units and colors, themes, the `allow_remote` privacy note, the `dependency_missing` hint) and instructs it to compose a spec and call `create_pdf`. |

## The document spec

```json
{
  "spec_version": 1,
  "meta": { "title": "Field Guide", "author": "Acme" },
  "page": { "size": "A4", "orientation": "portrait", "margins": "20mm" },
  "theme": "report",
  "toc": { "title": "Contents", "depth": 2 },
  "content": [
    { "type": "heading", "level": 1, "text": "Field Guide" },
    { "type": "paragraph", "text": "A short **guide**." },
    { "type": "list", "items": ["Alpha", "Beta"] },
    { "type": "callout", "kind": "info", "title": "Note", "text": "Read the schema." }
  ]
}
```

All top-level keys except `content` are optional. Blocks include `heading`,
`paragraph`, `rich`, `list`, `table`, `image`, `figure`, `code`, `callout`,
`blockquote`, `divider`/`hr`, `spacer`, `page_break`, `page_template`, `columns`,
`toc`, `checkbox_list`, `definition_list`, `key_values`, `anchor`, `qr`, and
`barcode`.

Read `pdfcreator://schema` (or [`references/schema.md`](references/schema.md))
for the full reference, [`references/layout.md`](references/layout.md) for a
worked example of each block, and
[`references/commands.md`](references/commands.md) for the CLI verbs.

## Errors

Tool failures surface as MCP errors whose message starts with the error code:

| Code | Meaning |
|---|---|
| `invalid_spec` | The spec failed validation (not an object, unknown block type, bad field type, …). |
| `dependency_missing` | reportlab is absent; the message includes the install hint. |
| `conflict` | The output file exists and `force` was not set. |
| `render_error` | reportlab raised while building the document. |
| `io_error` / `not_found` | A referenced file could not be read. |

## Configuration

Settings come from the built-in defaults (`scripts/_lib.py`) and optional
`PDF_CREATOR_*` environment variables. They apply only to fields the spec leaves
unset.

| Env var | Default | Env var | Default |
|---|---|---|---|
| `PDF_CREATOR_HOME` | `~/.local/share/pdf-creator` | `PDF_CREATOR_FONT_FAMILY` | `Helvetica` |
| `PDF_CREATOR_PAGE_SIZE` | `A4` | `PDF_CREATOR_FONT_SIZE` | `10.5` |
| `PDF_CREATOR_ORIENTATION` | `portrait` | `PDF_CREATOR_LINE_HEIGHT` | `1.4` |
| `PDF_CREATOR_UNIT` | `mm` | `PDF_CREATOR_TEXT_COLOR` | `#1A1A1A` |
| `PDF_CREATOR_THEME` | `default` | `PDF_CREATOR_ACCENT` | `#2563EB` |
| `PDF_CREATOR_AUTHOR` | *(empty)* | `PDF_CREATOR_OUTPUT_DIR` | `<home>/output` |
| `PDF_CREATOR_LANGUAGE` | `en` | `PDF_CREATOR_ALLOW_REMOTE` | `false` |

The data root holds only generated artifacts: `output/` (the default PDF
destination). `fonts/` and `assets/` are read-only inputs. No database is opened
and no render is recorded.

## How it works

```
spec ──▶ _lib.validate_spec ──▶ _engine.Builder ──▶ reportlab ──▶ PDF
```

The engine resolves the theme, geometry, page templates, and every content
block into a reportlab story, then builds the document (multi-pass when a TOC is
present). The MCP layer reuses those modules unchanged and returns the PDF both
inline (base64 `application/pdf`) and on disk. The database-backed document
library, settings CRUD, asset store, render history, scheduler artifacts, and
the `schedule-hint` verb still exist as CLI code in `scripts/`, but are **not
exposed over MCP**.

## Development

```bash
uv sync
bash tests/smoke.sh                    # CLI engine end-to-end
uv run python tests/mcp_smoke.py       # MCP tools, resources, validation, and render roundtrips
uv run python tests/transport_smoke.py # stdio + streamable HTTP + SSE, end to end
```

`tests/smoke.sh` generates its own fixtures and exercises every CLI verb against
a throwaway data root (render sections self-skip when reportlab is absent).
`tests/mcp_smoke.py` drives the FastMCP server in-process and checks the tool
trio, the resources, validation, error surfacing, Markdown conversion, and an
end-to-end render. `tests/transport_smoke.py` runs the server as a subprocess
and, over each of stdio, streamable HTTP, and SSE, initializes, lists tools,
validates a spec, and renders a PDF.

## Layout

```
apollo-pdf-creator/
├── install.sh              # one-time setup (uv sync)
├── pyproject.toml          # uv project; runtime deps: mcp, reportlab, Pillow; dev: mcp[cli], pypdf
├── src/apollo_pdf_creator/
│   ├── __init__.py         # __version__
│   └── server.py           # FastMCP server (tools + resources + prompt + transports)
├── scripts/                # the engine and CLI verbs (reused as-is)
│   ├── _lib.py  _engine.py  _engine_blocks.py  _engine_blocks2.py
│   ├── _markdown.py  _rl.py  _themes.py  templates.py
│   ├── render.py  documents.py  settings.py  assets.py
│   └── reports.py  init.py
├── references/             # schema, layout, commands, scheduling
├── tests/
│   ├── smoke.sh            # CLI engine end-to-end
│   ├── mcp_smoke.py        # MCP-level roundtrips (in-process)
│   └── transport_smoke.py  # stdio + HTTP + SSE, end to end
├── .github/workflows/ci.yml
└── LICENSE
```

## License

MIT. See [`LICENSE`](LICENSE).

TDQS

A4.1/5.0

Scored across 3 tools

Disambiguation5/5

Each tool serves a distinct purpose: validate_spec checks spec correctness, create_pdf renders a spec to PDF, and from_markdown converts Markdown to PDF. There is no overlap in functionality, making tool selection unambiguous.

Naming Consistency4/5

Two tools follow a clear verb_noun pattern (validate_spec, create_pdf), but from_markdown deviates by using a prepositional prefix. The inconsistency is minor and does not hinder readability.

Tool Count5/5

With only 3 tools, the server is tightly scoped to its core purpose of PDF creation. Each tool fills a necessary role, and the count falls within the ideal range for a focused server.

Completeness4/5

The server covers validation, spec-based PDF generation, and Markdown conversion, which are the essential workflows for a PDF creator. A minor gap is the lack of a tool for inspecting or managing generated PDFs, but this does not break core functionality.

Maintenance

ActivityMaintained
ResponsivenessNo issues