PostScript MCP
# PostScript MCP
An [MCP](https://modelcontextprotocol.io) server that gives AI agents the
tools to write **high-quality PostScript** — grounded in the Adobe manuals and
Kees van der Laan's PSlib rather than guessed from memory.
It exposes four capabilities:
| Capability | Tools | What it does |
|---|---|---|
| **Render & validate** | `validate_postscript`, `render_postscript` | Run a program through Ghostscript, get a PDF/PNG/SVG and structured diagnostics — the write→check→fix loop that lets an agent self-correct. |
| **Operator lookup** | `lookup_operator`, `search_operators` | Exact PLRM operator definitions (stack signature, LanguageLevel, errors). 270 operators shipped, offline. |
| **PSlib fragments** | `get_pslib_fragment`, `search_pslib` | Retrieve van der Laan's vetted procedures by name, with their stack signatures and source, so agents reuse code instead of reinventing it. |
| **Reference search** | `search_reference` | Cited passages from the PLRM, Blue/Green books, DSC/EPS specs and tutorials. |
Plus `postscript_capabilities` for setup/debugging.
---
## Why
LLMs write plausible-looking PostScript that fails on a real interpreter:
wrong operand order, undefined names, unbalanced `gsave`/`grestore`, EPS files
with no `%%BoundingBox`. This server closes that gap. The agent can confirm an
operator's signature before using it, paste in a procedure that is known to
work, quote the spec when behaviour is subtle, and — crucially — **actually run
the program and read the errors back** before handing code to a human.
Pair it with the [skill suite](#skills) for the authoring conventions.
---
## Requirements
* **Python 3.10+**
* **Ghostscript** — the `gs` binary (rendering & validation)
* macOS: `brew install ghostscript` · Debian/Ubuntu: `apt install ghostscript`
* Windows: the official installer, then ensure `gswin64c.exe` is on `PATH`
* **pdftotext** (poppler-utils) — only needed to build the reference corpus
* macOS: `brew install poppler` · Debian/Ubuntu: `apt install poppler-utils`
## Install
```bash
cd postscript-mcp
python -m venv .venv && source .venv/bin/activate # optional
pip install -e . # installs the `mcp` dep
```
That is enough for **operator lookup** and **render/validate** to work
immediately (the operator dataset ships in `data/operators.json`).
## Build the indexes (PSlib + reference corpus)
Point the ingester at your PostScript reference library — the folder with the
Adobe PDFs and the PSlib sources (this project was built around exactly such a
library; see the accompanying `REFERENCE_INDEX.md`):
```bash
python scripts/ingest.py --library "/path/to/postscript reference"
```
This writes:
* `data/pslib_index.json` — every PSlib procedure, parsed from `PSlib.ps` /
`PSlib.eps` (auto-discovered; or pass `--pslib PSlib.ps`).
* `corpus/corpus_index.json` — one searchable chunk per PDF page and per block
of each `.ps`/`.txt` source.
Re-run it whenever your library changes. Useful flags: `--skip-corpus`,
`--skip-pslib`, `--pslib <files…>`, `--library`, `--corpus`, `--data`.
You can also set defaults via environment variables (below) instead of flags.
## Connect it to a client
### Claude Desktop
Edit `claude_desktop_config.json`
(macOS: `~/Library/Application Support/Claude/`,
Windows: `%APPDATA%\Claude\`):
```json
{
"mcpServers": {
"postscript": {
"command": "python",
"args": ["-m", "postscript_mcp"],
"env": {
"PSMCP_LIBRARY_DIR": "/path/to/postscript reference",
"PSMCP_DATA_DIR": "/path/to/postscript-mcp/data",
"PSMCP_CORPUS_DIR": "/path/to/postscript-mcp/corpus"
}
}
}
}
```
If you installed with `pip install -e .`, you can use the console script
instead: `"command": "postscript-mcp", "args": []`. Point `command` at the
Python inside your virtualenv if you used one.
### Claude Code
```bash
claude mcp add postscript -- python -m postscript_mcp
```
(run from the project directory, or give the absolute interpreter path).
### Any MCP client
The server speaks MCP over **stdio**: launch `python -m postscript_mcp`.
### Remote / cloud deployment (public HTTPS endpoint)
To expose this server on the network so any client can reach it over HTTP, see
the deployment guides. It serves MCP over **Streamable HTTP** at `/mcp`,
containerised with Docker (Ghostscript included):
* **[`DEPLOY_RENDER.md`](DEPLOY_RENDER.md) — recommended, free, no credit
card:** deploy to the Render free tier (GitHub sign-in) and point your own
domain at it.
* **[`DEPLOY.md`](DEPLOY.md) — alternative:** a free **Oracle Cloud Always
Free** VM running Docker + Caddy (TLS).
```json
{
"mcpServers": {
"postscript": {
"type": "http",
"url": "https://mcp.example.com/mcp"
}
}
}
```
---
## Tool reference
**`validate_postscript(source="", path="")`** → `{ok, diagnostics[], bbox,
hires_bbox, hint, …}`
Interpret without rasterising (fast). Diagnostics carry `error`,
`offending_command` and whether it is a genuine interpreter error. `hint`
gives a plain-language nudge for common failures. Great for filling an EPS
`%%BoundingBox` from `hires_bbox`.
**`render_postscript(source="", path="", output_format="png", resolution=150,
page=None, eps_crop=True, return_base64=False)`** → `{ok, output_path,
page_count, bbox, diagnostics[], …}`
`output_format` ∈ `png`, `png-alpha`, `pdf`, `svg`. Returns the output file
path; set `return_base64=True` to also get PNG bytes inline.
**`lookup_operator(name)`** → the operator's `signature`, `operands`,
`results`, `summary`, `category`, `level`, `errors`. On a miss, returns
`did_you_mean`.
**`search_operators(query, category="", limit=20)`** → ranked operators, or a
whole category listing.
**`get_pslib_fragment(name)`** → `{signature, source, category, attribution,
line}` for one PSlib procedure.
**`search_pslib(query="", category="", limit=20)`** → matching procedures
(names + one-line signatures); follow up with `get_pslib_fragment`.
**`search_reference(query, limit=8, doc="")`** → cited passages
`{doc, page, snippet, score}`. Restrict to a document with `doc` (e.g.
`"PLRM"`, `"Green"`, `"DSC"`).
**`postscript_capabilities()`** → what is available right now (Ghostscript
present? indexes built?) and how to build what is missing.
### Recommended agent workflow
1. **Draft** using `lookup_operator` / `search_pslib` + `get_pslib_fragment`.
2. **Consult** `search_reference` when behaviour is subtle (imaging model,
`save`/`restore`, DSC rules).
3. **Check** with `validate_postscript` — fix every diagnostic.
4. **Render** with `render_postscript` and, for figures, verify the bounding
box and that the image looks right.
5. For embeddable output, emit a correct `%%BoundingBox` and DSC comments
(see `examples/square.eps`).
---
## Configuration (environment variables)
| Variable | Default | Purpose |
|---|---|---|
| `PSMCP_LIBRARY_DIR` | `./library` | Where your reference PDFs / PSlib sources live (for ingest). |
| `PSMCP_DATA_DIR` | `./data` | Location of `operators.json` and `pslib_index.json`. |
| `PSMCP_CORPUS_DIR` | `./corpus` | Location of the search corpus. |
| `PSMCP_GHOSTSCRIPT` | auto-detected | Path to the `gs` binary. |
| `PSMCP_OUTPUT_DIR` | temp dir | Where rendered files are written. |
| `PSMCP_RENDER_TIMEOUT` | `30` | Ghostscript wall-clock limit (seconds). |
| `PSMCP_MAX_SOURCE_BYTES` | `4194304` | Max program size accepted. |
## Safety
Every Ghostscript invocation uses `-dSAFER` (no arbitrary file writes / device
control from the program) and a wall-clock timeout, and source size is capped.
The server never executes PostScript except through Ghostscript.
## Development
```bash
pip install -e ".[dev]"
python scripts/gen_operators.py # regenerate data/operators.json
python -m pytest -q # run the test suite
```
## Layout
```
postscript_mcp/ server + the four capability modules
server.py FastMCP wiring (the tools)
render.py Ghostscript validate/render
operators.py operator reference access
pslib.py PSlib tokeniser / parser / index
reference.py BM25 corpus search
config.py paths & limits from env
data/operators.json curated PLRM operator reference (shipped)
scripts/ingest.py build pslib_index.json + corpus from your library
scripts/gen_operators.py rebuild the operator dataset
skills/ agent skill suite (see below)
examples/ conformant EPS + PSlib usage
tests/ pytest suite (+ a real PSlib fixture)
```
## Skills
The `skills/` folder holds a composable suite an agent loads alongside this
server:
* **postscript-authoring** — stack discipline, structure, the render/verify loop.
* **postscript-pslib** — reusing van der Laan's library correctly.
* **postscript-eps-dsc** — conformant EPS / DSC output for embedding & print.
## Credits & licences
* PSlib © Kees van der Laan (kisa1@xs4all.nl) — parsed and indexed here for
reference; its own terms apply to the code itself.
* Operator reference curated from the *PostScript Language Reference Manual,
3rd ed.* (Adobe, 1999).
* This server's code: MIT (see `LICENSE`).
TDQS
Scored across 8 tools
Each tool targets a distinct job: exact operator definitions vs operator search, validation vs rendering, PSlib retrieval vs searching, and reference/capability lookups. Even the paired search/get tools are clearly separated by role.
Most tools follow a consistent verb_noun pattern (lookup_operator, validate_postscript, search_operators). The only deviation is postscript_capabilities, which lacks a verb; otherwise the naming is uniform and predictable.
Eight tools is an appropriate size for a PostScript assistant: each one covers a necessary stage of authoring, checking, rendering, and reference lookup without redundancy. The count feels intentional rather than padded.
The surface covers a full workflow: discover operators, validate code, render output, reuse library procedures, and consult authoritative references. There are no dead ends or obvious missing operations for the stated domain.