# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Project
`pdf-porter`: a minimal MCP server exposing one tool — `pdf_to_markdown` — for layout-aware PDF→Markdown conversion via Docling. Pitch: one tool, zero config, five minutes.

**Note:** `docling-mcp` (the obvious name) is taken by IBM Research Zurich, the Docling authors themselves, at v1.3.2. This package is intentionally minimal — one tool, no config. The README should reference IBM's `docling-mcp` as the full-featured alternative: "if you want the full Docling toolset, see docling-mcp."

## Stack
- Python 3.11+
- `mcp[cli]` (FastMCP)
- `docling`
- `uv` for packaging and running
- `reportlab` (dev only, for generating test fixtures)

## Structure
```
pdf_porter/
  __init__.py       # __version__ = "0.2.0"
  server.py         # FastMCP app, tool definitions, module-level converter singleton
tests/
  test_server.py    # integration tests (no mocking)
  fixtures/         # simple.pdf, two_column.pdf, table.pdf (generated by generate_fixtures.py)
generate_fixtures.py  # standalone script to regenerate test PDFs via reportlab
pyproject.toml
README.md
```

## Commands
```bash
uv run python -m pdf_porter          # start server
uv run pytest                        # run tests
uv run mcp dev pdf_porter/server.py  # MCP inspector
uv run pytest tests/test_server.py::test_table_pdf  # run a single test
python generate_fixtures.py          # regenerate test fixture PDFs
uv build                             # build wheel + sdist into dist/
```

## Publishing to PyPI

`PYPI_TOKEN` must be set in the environment before publishing. Claude Code's Bash tool does **not** inherit variables exported in the user's interactive shell — the user must publish manually from their own terminal:

```bash
# In your terminal (not via Claude Code):
uv build
uv publish --token $PYPI_TOKEN
```

After publishing, verify the release:
```bash
curl -s https://pypi.org/pypi/pdf-porter/json | python -m json.tool | grep '"version"'
uvx pdf-porter &   # should start cleanly; kill with Ctrl-C
```

## Architecture

`server.py` holds a module-level `DocumentConverter` singleton (instantiated once, not per-call) and a boolean flag to log "Loading Docling models..." to stderr on the first conversion only. The `pdf_to_markdown` tool validates path existence and `.pdf` extension before calling Docling, and wraps all exceptions as `"Error: {e}"` strings — never raw tracebacks.

Entry point: `pdf_porter.server:main` → calls `mcp.run()`. Also handles `if __name__ == "__main__"`.

## Tool signature
```python
pdf_to_markdown(
    path: str = None,
    pdf_base64: str = None,
    save_output: bool = False,
    output_dir: str = None,
) -> str
```
- `path`: local filesystem path to a PDF (use when file exists on the server).
- `pdf_base64`: base64-encoded PDF bytes (use when the file is an attachment, e.g. claude.ai uploads at `/mnt/user-data/uploads/` which the server cannot access).
- `save_output=True`: write Markdown to a `.md` file and return `"Saved to: {path}"` instead of the content.
- `output_dir`: directory for saved output (defaults to same dir as source in path mode, or a temp dir in base64 mode). Pass `/mnt/user-data/outputs/` to surface via `present_files`.
- Returns `"Error: ..."` on any failure (bad path, wrong extension, missing args, conversion error).

## Rules
- No global state beyond the converter singleton and the first-run flag — document both in server.py.
- All errors surface as readable strings, never raw tracebacks to the LLM.
- Tests are integration tests — do not mock `DocumentConverter`.
- Tests must cover: simple PDF, two-column PDF, table PDF (asserts `|` present), bad path, non-PDF extension, `save_output=True`, base64 mode, base64+save_output, output_dir, neither-arg error.
- Do not add dependencies beyond `mcp`, `docling`, `pytest`, and `reportlab` (dev).
- README must include a working `claude_desktop_config.json` snippet.

## Claude Desktop integration

### Custom instruction (add to Claude Desktop → Settings → Custom Instructions)

> When I share a path to a PDF file or ask you to read a PDF, call the `pdf_to_markdown` tool before doing anything else. Pass the file's local path as the `path` argument. Use the returned markdown as the document content for your analysis. Only fall back to native PDF attachment handling if the tool returns an `Error:` response or is unavailable.

This must be paired with the MCP server registered in `claude_desktop_config.json` (see README). Restart Claude Desktop after adding the server config.

### Why this is preferred over native PDF ingestion
Native Claude Desktop PDF handling processes each page as a vision input (~800–1,600 image tokens/page). A 10-page PDF costs ~10,000–16,000 tokens before Claude reads a word. pdf-porter converts locally (zero API cost via Docling) and returns plain markdown — typically 3,000–8,000 text input tokens for the same document. Text tokens are cheaper than image tokens, and Docling preserves tables and multi-column layout that native ingestion scrambles, reducing follow-up turns.

## Known limitations (inform users)
- First run downloads Docling model weights (~500MB); subsequent runs are fast.
- OCR mode not enabled in v0.1 — scanned/image-only PDFs will return empty text.
- Windows path handling not validated.


## PATHS

NEVER HARD CODE PATHS. /path/to/repo should NEVER APPEAR in the codebase.
