poster-design-mcp
# poster-design-mcp
An **MCP server that designs business posters.** Ask your AI client for a poster in plain
language and it writes a real PNG or JPG to disk — print-resolution, laid out by a design system
rather than by guesswork.
<p align="center">
<img src="docs/samples/insurance-photo-overlay.jpg" width="31%" alt="Insurance poster: full-bleed family photograph, headline 'Protect What Matters Most', yellow pill CTA">
<img src="docs/samples/bakery-banded-editorial.jpg" width="31%" alt="Bakery poster: pattern band, bread photograph, orange type block">
<img src="docs/samples/template-stat-hero.jpg" width="31%" alt="Anniversary poster: giant numeral 8 on a teal field">
</p>
Every poster on this page was produced by a single tool call. More in **[Gallery](#gallery)**.
---
## Contents
- [What it does](#what-it-does)
- [How it works](#how-it-works)
- [Install](#install)
- [Connect it to your AI client](#connect-it-to-your-ai-client)
- [Using it](#using-it)
- [Tools](#tools)
- [Templates](#templates)
- [Sizes, palettes and images](#sizes-palettes-and-images)
- [Where posters are saved](#where-posters-are-saved)
- [Token cost](#token-cost)
- [Development](#development)
- [Troubleshooting](#troubleshooting)
- [Creator](#creator)
- [License](#license)
---
## What it does
Most AI image tools *paint* a poster, so the type comes out melted and the phone number is
fiction. This one **typesets** a poster. The headline is real text in a real font, the price is
the price you gave it, and the output lands on exact pixel dimensions — 2480×3508 for A4 at
300 dpi, ready for a printer.
You get:
- **Four layouts** derived from four reference posters — events, brand statements, product ads,
and milestone numbers.
- **A palette from one hex code.** Give it your brand colour; field, ink, accent and pattern
ramp are derived from it, with the ink picked by contrast ratio so text stays legible.
- **Auto-fitted display type.** The headline grows until the longest line touches the margin,
then stops. You choose the line breaks; the server chooses the size.
- **Real photograph handling** — full-bleed crops, focal points, gradient scrims so type never
sits raw on an image.
- **Print or social sizes**, PNG or JPG, on demand.
## How it works
```
your prompt
│
▼
AI client ──MCP/stdio──▶ poster-design-mcp
│
│ 1. pick a template (src/templates/*.js)
│ 2. derive the palette from brandColor
│ 3. build an HTML + CSS document
▼
headless Chrome ── screenshot ──▶ poster.png
│
▼
~/Documents/Poster Designs/
```
1. **The design system is numbers, not vibes.** Twelve rules were read off four reference
posters and written into `src/design-system.js` as values the templates actually consume —
margin at 6.8% of the width, hero type between 0.055 and 0.155 of the width, leading 0.90,
tracking −0.03em. The prose in
[DESIGN-PRINCIPLES.md](DESIGN-PRINCIPLES.md) and the rendered pixels read from the same
constants, so the documentation cannot drift away from the output.
2. **A template turns your copy into a document.** Each of the four templates
(`src/templates/`) is a function: content in, HTML and CSS out. It places the bands, sets
the type roles, generates the decorative pattern from a seed, and inlines every local image
as a data URI.
3. **Headless Chrome does the layout.** Rendering in a browser buys real text layout — font
fallback, hyphenation, `object-fit` cropping, `mix-blend-mode: multiply` letterforms — and
the auto-fit loop measures the actual laid-out headline and rescales until it fills the
measure. The page is then screenshotted at the exact output dimensions with
`deviceScaleFactor` doing the work, so a 300 dpi A4 is genuinely 2480×3508 pixels rather
than an upscale.
4. **The file goes to Documents.** An MCP server is launched by its client and has no
meaningful working directory, so results are written somewhere findable and the tool returns
the path.
Nothing is uploaded. No API key, no network call — the whole pipeline runs on your machine.
## Install
**Requires Node 20+** and a Chrome/Chromium binary.
```bash
git clone https://github.com/suprisedr/poster-design-mcp.git
cd poster-design-mcp
npm install
```
`npm install` pulls Puppeteer, which normally downloads its own Chrome. If it didn't, or if
you skipped the download:
```bash
npx puppeteer browsers install chrome
```
Chrome is located in this order: `$POSTER_CHROME_PATH` → Puppeteer's managed Chrome → Google
Chrome or Chromium in the usual system locations. To point at a specific binary:
```bash
export POSTER_CHROME_PATH="/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"
```
Check that it works:
```bash
npm test
```
That boots the server over stdio, exercises every tool and asserts the token budgets. Then, for
something to look at:
```bash
npm run demo
```
One poster per template into `./examples`.
## Connect it to your AI client
### Claude Code
```bash
claude mcp add poster-design -- node /absolute/path/to/poster-design-mcp/src/server.js
```
### Claude Desktop, or any MCP config file
Add to `mcpServers` (on macOS, Claude Desktop's config lives at
`~/Library/Application Support/Claude/claude_desktop_config.json`):
```json
{
"mcpServers": {
"poster-design": {
"command": "node",
"args": ["/absolute/path/to/poster-design-mcp/src/server.js"],
"env": {
"POSTER_BASE_DIR": "/absolute/path/to/poster-design-mcp"
}
}
}
}
```
Use absolute paths — the client launches the server from an unpredictable directory.
| Environment variable | Meaning |
|---|---|
| `POSTER_BASE_DIR` | What relative image and output paths resolve against |
| `POSTER_OUTPUT_DIR` | Moves the default output folder off `~/Documents/Poster Designs` |
| `POSTER_CHROME_PATH` | Explicit Chrome/Chromium binary |
Restart the client, and the four tools appear.
## Using it
Once connected, you just ask:
> Make me an A4 poster for Golden Crust Bakery — handcrafted breads and pastries, 123 Maple
> Street, open 6am–6pm Tuesday to Sunday. Warm orange, use this bread photo.
The client picks a template and calls `create_poster`. Under the hood that is one JSON payload:
```json
{
"template": "photo-overlay",
"brandColor": "#0F918F",
"size": "instagram-portrait",
"format": "jpg",
"content": {
"brandName": "Guski",
"logoBadge": true,
"headline": "You love it,\nwe cover it",
"subhead": "Cover that changes everything",
"cta": { "label": "Get a quote", "phone": "061 306 081" },
"legal": "Guski Short Term Insurance Ltd. Reg. No. 89/524.",
"image": "examples/sample-photo.jpg",
"imageFocus": "50% 45%"
}
}
```
It returns one terse line — `path | 2480x3508 png | 191KB | ...` — and the file is on disk.
Two things worth knowing when you write the copy:
- **Use `\n` in a headline** to choose where it breaks. The generator sizes the type to fill
whatever rag you write, so the break is a design decision and it's yours.
- **Keep it short.** Headline 2–5 words, subhead under 45 characters, body two lines. The type
scale is built on extreme contrast; long copy is what makes a poster look like a flyer.
## Gallery
Real output, one tool call each.
| | |
|:--:|:--:|
| <img src="docs/samples/bakery-banded-editorial.jpg" width="330" alt="Golden Crust Bakery poster with bread photograph"> | <img src="docs/samples/bakery-banded-editorial-monogram.jpg" width="330" alt="Golden Crust Bakery poster with a giant G monogram"> |
| **`banded-editorial`** — pattern band, photograph, type block. A4, 2480×3508. | **The same poster with no photo.** Drop the `image` field and the middle band becomes a monogram of the brand initial. |
| <img src="docs/samples/insurance-photo-overlay.jpg" width="330" alt="SecureLife Insurance poster"> | <img src="docs/samples/template-stat-hero.jpg" width="330" alt="Eight years of good work poster"> |
| **`photo-overlay`** — full-bleed photograph, duotone scrim, headline low-left, pill CTA and a legal rail. | **`stat-hero`** — one flat field, a giant numeral as artwork, tight copy stack. |
| <img src="docs/samples/template-chromatic-type.jpg" width="330" alt="MOMENTUM typographic poster"> | <img src="docs/samples/template-banded-editorial.jpg" width="330" alt="Northern Lights festival poster"> |
| **`chromatic-type`** — one word past the margins, letters overlapped and multiplied into new colours. | **`banded-editorial`** as an event bill, with a badge and a sponsor rail. |
Sample photography is placeholder imagery included for demonstration; swap in your own.
## Tools
| Tool | What it does |
|---|---|
| `list_poster_templates` | The four layouts and when to use each, plus palettes, sizes and fonts. Pass a `template` id for that one layout's full field guide |
| `get_design_principles` | The twelve rules, type scale and layout ratios. Pass `detail: true` for the reasoning, `brandColor` for the derived palette |
| `create_poster` | Renders one poster to PNG/JPG and returns the path. Pass `preview: true` for an inline thumbnail |
| `create_poster_variants` | The same copy across up to 4 templates or palettes, for comparison |
## Templates
| id | Based on | Use it for |
|---|---|---|
| `banded-editorial` | A jazz festival bill | Events, launches, line-ups. Pattern band + photograph + type block + sponsor rail |
| `chromatic-type` | A typographic brand poster | Brand statements and campaign words, when there is no photograph worth printing |
| `photo-overlay` | An insurance campaign ad | Product and service advertising with a call to action and small print |
| `stat-hero` | An anniversary ad | Milestones, results and offers with a number in them |
The rules behind them are in **[DESIGN-PRINCIPLES.md](DESIGN-PRINCIPLES.md)** — one message per
poster, bands not boxes, 6–12× scale contrast, one margin value, end on a pill.
## Sizes, palettes and images
**Sizes.** `a4-print` (2480×3508 @300dpi), `a3-print`, `a5-flyer`, `poster-hd`,
`instagram-post`, `instagram-portrait`, `story`, `web-banner` — or pass `width` and `height` in
pixels. Output lands on exactly those dimensions.
**Colour.** Pass `brandColor` as a hex and the full palette is derived from it. Or pass
`palette` for one of the named sets (`festival-block`, `prism-glass`, `night-drive`,
`signal-teal`, `mono-ink`), which wins over `brandColor`.
**Images.** `image`, `logo`, `badge.logo` and `partners[].logo` take a local path (absolute,
`~`, or relative to `POSTER_BASE_DIR`) or an `http(s)` URL. Local files are inlined as data URIs
before rendering. For `stat-hero`, a cut-out subject on a transparent PNG gives you the effect
of the subject crossing the numeral.
## Where posters are saved
Finished posters go to **`~/Documents/Poster Designs`**.
| `outputDir` | Resolves to |
|---|---|
| omitted | `~/Documents/Poster Designs` |
| `"Q3 campaign"` | `~/Documents/Poster Designs/Q3 campaign` |
| `"./build"`, `"~/Desktop"`, or an absolute path | exactly that — relative forms against `POSTER_BASE_DIR` |
Set `POSTER_OUTPUT_DIR` to move the default somewhere else, or `outputPath` for one exact file.
## Token cost
An MCP server's tool list is re-sent on **every request of every session**, so this one is
written to stay cheap to keep in context:
- The tool list is **~990 tokens, down from ~2,600.** It carries names, options and field
*names*; field *meanings* moved behind `list_poster_templates`, which is called once by the
callers that need it.
- `list_poster_templates` returns ~390 tokens instead of ~1,700; pass a `template` id for one
layout instead of all four.
- `get_design_principles` returns ~280 tokens instead of ~1,700; `detail: true` for the full
reasoning.
- Results are one terse line rather than pretty JSON.
- The inline preview image (200–500 tokens per poster) is **opt-in** via `preview: true`. The
file is in Documents, which is a better place to look at it.
`npm test` asserts the tool-list budget, so a regression in the most expensive surface fails
the build.
## Development
```bash
npm run demo # one poster per template into ./examples
npm test # boots the server over stdio, exercises every tool, checks the token budgets
```
```
src/
server.js MCP wiring: tool list, dispatch, terse results
poster.js orchestration — template → HTML → render → write
design-system.js the twelve rules as numbers: palettes, type scale, layout ratios
templates/ one file per layout, each content → { body, css }
lib/
html.js document assembly and escaping
pattern.js seeded decorative pattern generation
media.js image resolution and data-URI inlining
render.js headless Chrome lifecycle and screenshotting
paths.js output directory resolution
```
`saveHtml: true` writes the HTML source next to the image, which is the fastest way to see why
something laid out the way it did. `seed` makes the pattern and jitter reproducible.
## Troubleshooting
| Symptom | Fix |
|---|---|
| `Could not find Chrome` | `npx puppeteer browsers install chrome`, or set `POSTER_CHROME_PATH` |
| Tools don't appear in the client | Use an **absolute** path to `src/server.js`, then restart the client |
| Images don't render | Local paths resolve against `POSTER_BASE_DIR`; set it, or pass an absolute path |
| Poster written somewhere unexpected | Default is `~/Documents/Poster Designs`; override with `outputDir` or `POSTER_OUTPUT_DIR` |
| Headline overflows | Put your own `\n` breaks in it, or shorten it — 2–5 words is the design target |
## Creator
Built by **Suprise Nkosi**.
- Facebook — [facebook.com/suprise.nkosi.524](https://www.facebook.com/suprise.nkosi.524)
- GitHub — [@suprisedr](https://github.com/suprisedr)
Issues and pull requests are welcome.
## License
MIT — see [LICENSE](LICENSE).
TDQS
Scored across 4 tools
Each tool has a clearly distinct purpose: discovering templates, learning design principles, creating a single poster, and creating variants. Even the two create tools are distinguishable by single vs. batch behavior.
All tool names follow the same verb_noun snake_case pattern: list_, get_, create_, create_. The naming is predictable and indicates the action and resource clearly.
Four tools is well-scoped for a poster design server, covering discovery, guidance, creation, and variation. The set feels intentionally minimal rather than bloated or incomplete.
The core workflow is covered: browse templates, learn design rules, create a poster, and generate variants. Minor gaps exist around modifying or deleting generated posters, but these are not clearly required for the server's stated purpose.