Skip to main content
Glama
gavricci

artwork-preflight-mcp

by gavricci
README.md
# artwork-preflight-mcp

An MCP server that tells an agent whether a customer's artwork can actually be printed.

Print shops lose time in the same place every day: a file arrives, it looks fine on screen,
and only at the proof stage does someone notice it is 900 px wide, has no bleed, or hides a
logo 2 mm from the cut line. Those checks are mechanical. This server exposes them as tools,
so an agent can answer "what is wrong with this file, and what should we tell the customer"
before a human opens it.

It reads files. It never modifies artwork and never places an order.

```
$ npx artwork-preflight-mcp   # speaks MCP over stdio
```

## What it checks

| Check | Fails when |
|---|---|
| `resolution` | pixels per inch of the finished piece fall below the product minimum |
| `aspect-mismatch` | artwork is a different shape from the product, so fitting it means cropping |
| `bleed-missing` | artwork is exactly trim size, so any cut movement shows white |
| `safe-area` | inset content sits closer to the edge than the cut tolerance allows |
| `transparency` | the file has alpha but the product prints on solid stock |
| `color-mode` | file is RGB and the press runs CMYK |
| `missing-density` | the file declares no resolution, so bleed cannot be confirmed |
| `density-implausible` | the declared resolution implies a size this product is not, and was ignored |

Errors block printing. Warnings are things the customer should know before approving a proof.

## Tools

| Tool | Scope | What it returns |
|---|---|---|
| `inspect_artwork` | `inspect` | pixel size, declared dpi, colour space, ICC profile, alpha, content box |
| `check_print_readiness` | `check` | every issue found against a print product, each with a severity |
| `list_presets` | `check` | the print products this server knows, with their physical requirements |
| `suggest_fixes` | `fix` | per issue: one message for the customer, one action for the operator |

A fix names the number to change:

```json
{
  "code": "resolution",
  "customerMessage": "At die-cut sticker, 3 x 3 in this file needs to be at least 971 x 971 px. Yours is 300 x 300 px. Send the original artwork, a vector file, or order a smaller size.",
  "operatorAction": "Do not upscale. Upscaled raster art prints soft and the customer blames the press."
}
```

## Try it

`examples/` holds five files covering the cases that actually arrive. Point the server at that
folder and run the checks below - the expected result is what you should see, verbatim.

```bash
PREFLIGHT_ROOT=./examples node dist/index.js
```

| File | Preset | Expected |
|---|---|---|
| `01-good-sticker.png` | `die-cut-sticker-3in` | **ok** - one `color-mode` warning (RGB file, CMYK press) |
| `02-press-ready-label.tif` | `label-roll-2in` | **ok** - no issues at all |
| `03-no-bleed.png` | `die-cut-sticker-3in` | **error** `bleed-missing` - exported at trim size |
| `04-low-res.png` | `die-cut-sticker-3in` | **error** `resolution` at 93 dpi, plus `missing-density` |
| `05-tight-content.png` | `die-cut-sticker-3in` | ok, with a `safe-area` warning - design stops 2 mm from the edge |
| `06-screen-export.png` | `die-cut-sticker-3in` | **ok** - 3000 px tagged 96 dpi; the tag is ignored, 927 dpi effective |
| `06-screen-export-wide.png` | `die-cut-sticker-3in` | **error** `aspect-mismatch` - same file, not square |
| `01-good-sticker.png` | `sticker-sheet-a6` | **errors** `aspect-mismatch`, `transparency`, `resolution` - right file, wrong product |

That last row is the point of presets: a file is not "good" or "bad" on its own, only against
something you are about to print it on.

A full report looks like this (`04-low-res.png`, abbreviated):

```json
{
  "preset": "die-cut-sticker-3in",
  "ok": false,
  "effectiveDpi": 93,
  "sizeIntent": "unknown",
  "issues": [
    {
      "code": "missing-density",
      "severity": "warning",
      "message": "The file declares no resolution, so we cannot tell whether bleed was included. Resolution below is measured against the finished size."
    },
    {
      "code": "resolution",
      "severity": "error",
      "message": "Effective resolution is 93 dpi, below the 300 dpi this product needs.",
      "detail": { "requiredWidthPx": 971, "actualWidthPx": 300 }
    }
  ]
}
```

The examples are committed so the server can be tried without setting anything up, and
regenerated with `npm run examples` - every one of them states its defect in code, in
`scripts/make-examples.mjs`, next to the numbers that cause it.

## Install

```bash
npm install
npm run build
```

Claude Code / Claude Desktop config:

```json
{
  "mcpServers": {
    "preflight": {
      "command": "node",
      "args": ["/absolute/path/to/artwork-preflight-mcp/dist/index.js"],
      "env": {
        "PREFLIGHT_ROOT": "/absolute/path/to/incoming-artwork",
        "PREFLIGHT_SCOPES": "inspect,check,fix",
        "PREFLIGHT_AUDIT_LOG": "/var/log/preflight-audit.jsonl"
      }
    }
  }
}
```

| Variable | Default | Meaning |
|---|---|---|
| `PREFLIGHT_ROOT` | current directory | every file path is resolved inside this directory |
| `PREFLIGHT_SCOPES` | all | which tools get registered at all |
| `PREFLIGHT_AUDIT_LOG` | unset | JSONL file to append one line per call |

## Design notes

**Read-only, by construction.** No tool writes, converts or flattens anything. A preflight
service that silently "fixes" a customer's file is how you end up printing 5,000 stickers
with a background someone's code guessed at.

**Scopes decide registration, not permission.** A tool outside `PREFLIGHT_SCOPES` is never
listed, so the model cannot call it and be refused - it does not exist in that session.
Refusing a tool the model can see is a prompt-injection surface; not having it is not.

**Paths are resolved inside one root.** Without that, a server that takes a file path from a
model is a general-purpose file reader wearing a print-shop hat.

**The audit log counts, it does not copy.** Each line records tool, file name, duration and
issue count. Artwork contents and full paths stay out: an audit trail that copies the
customer's file is not an audit trail, it is a second copy.

**A full-bleed design is not a safe-area error.** Content touching all four edges is
deliberate - that is what bleed is for. Only an inset design that is inset too little gets
flagged. Most naive preflight rules get this wrong and train people to ignore warnings.

**A declared resolution is a hint, not a fact.** 72 and 96 dpi are what editors write when
nobody set anything, and a 3000 px logo tagged 96 dpi is not a low-resolution file - it is an
untagged one. So the dpi in a file is trusted only when it implies a size this product could
plausibly be (trim, or trim plus bleed). Otherwise it is ignored, `sizeIntent` says `unknown`,
and resolution is measured as pixels per inch of the finished piece. Every millimetre figure
quoted to a customer comes from a resolution we believed; quoting "your logo is 794 mm wide"
from a meaningless tag destroys trust in the whole report.

**Shape is reported as shape.** When artwork does not match the product's proportions, the
answer is `aspect-mismatch`, not a size in millimetres. Cropping or padding changes someone's
design, so the server names the conflict and stops.

**Test fixtures are generated; examples are committed.** A folder of `bad3.png` files tells
nobody what is wrong with them six months later, so `test/fixtures.ts` builds each test case
with the defect named in code. The files in `examples/` are the exception: a reader should be
able to clone and run something immediately, and `scripts/make-examples.mjs` keeps them
reproducible rather than mysterious.

## Limits

- Raster formats only. SVG, PDF and AI files are the other half of a real preflight service
  and are not handled here; vector artwork should never fail a resolution check to begin with.
- Colour handling is coarse: the server reports the colour space and whether an ICC profile is
  present. It does not soft-proof, check ink coverage, or predict a specific shift.
- Spot colours, overprint and trapping are out of scope.
- The content box comes from trimming uniform borders, so a design on a busy photographic
  background will report no inset even when its text is close to the edge.
- Presets are hard-coded in `src/domain/presets.ts`. A real deployment would read them from
  the product catalogue.

## Development

```bash
npm run typecheck
npm test
```

Tests generate their fixtures into a temp directory and run entirely offline.

## License

MIT

TDQS

A4.3/5.0

Scored across 4 tools

Disambiguation5/5

Each tool has a clearly distinct role: inspect_artwork reads raw file properties, check_print_readiness validates against a product, list_presets enumerates available products, and suggest_fixes generates recommendations. There is no overlap or ambiguity between them.

Naming Consistency5/5

All four tool names follow a consistent verb_noun pattern: inspect_artwork, check_print_readiness, list_presets, suggest_fixes. The naming convention is uniform and predictable.

Tool Count5/5

With four tools, the server is tightly scoped to the preflight workflow. Each tool earns its place, covering the essential operations without unnecessary bloat or missing coverage.

Completeness5/5

The domain is artwork preflight, and the set covers the full workflow: inspect raw properties, check against a specific product, discover available products, and get recommended fixes. There are no obvious dead ends or missing operations for a non-mutating preflight service.

Maintenance

ActivityMaintained
ResponsivenessNo issues