paperpress
paperpress
Eine API, die die Marke eines Unternehmens anhand seiner Live-URL erkennt – Logo, Primärfarbe, Schriftart – und Markdown in einem einzigen HTTP-Aufruf in ein PDF rendert, das im Stil dieser Marke gestaltet ist. Erhältlich als reine REST-API und als MCP-Server.
POST /v1/documents
{ "markdown": "# Q4 report\n...", "brandFromUrl": "stripe.com" }
→ ~1s → signed URL to a PDF in Stripe's brandÜber dieses Projekt
Dies wurde als echter, bereitgestellter Dienst gebaut und kurz betrieben, bevor ich mir den Markt genau ansah, in dem es konkurrieren müsste: Claude bietet jetzt native PDF/PPTX/DOCX-Erzeugung an, und Brandfetch verkauft bereits eine Brand Context API, die speziell für die Verankerung von KI-Agenten entwickelt wurde, mit echten zahlenden Kunden. Beide Hälften dessen, was dies tut – eine Marke erkennen, ein Dokument rendern – sind inzwischen quasi Commodity oder bereits im Besitz eines finanzierten Konkurrenten. Ich verfolge dies nicht als Produkt.
Es ist öffentlich als Portfolio-Stück / Referenzimplementierung: ein funktionierender, auf Playwright basierender Markendetektor (CSS-Custom-Properties, CTA-Farbabtastung, bewertete Logo-Kandidatenextraktion, WCAG-Kontrastschutz), eine synchrone Fastify-Render-Pipeline, ein SSRF-gehärteter URL-Fetcher und ein MCP-Server, der dies umschließt. Lies den Code, forke ihn, führe ihn aus – er ist MIT-lizenziert. Er wird nicht als Produkt gepflegt: Es ist keine Zahlungsabwicklung eingerichtet und das MCP-Paket (mcp/) ist nicht auf npm veröffentlicht.
Related MCP server: Markitdown Universal MCP Server
So funktioniert es
Ein einzelner Fastify-Prozess erledigt drei Dinge:
Erkennen (
src/render/detect.ts) – navigiert zur Ziel-URL mit einer gepoolten Playwright/Chromium-Instanz, liesttheme-color, Marken-CSS-Custom-Properties, CTA-Button-Farben, Header-<img>-Kandidaten, die nach Position/Größe/Format bewertet werden, und berechnete Font-Stacks. Filtert nahezu weißes/schwarzes/graues Rauschen, wendet einen WCAG-Luminanzschutz an, damit eine zu helle Markenfarbe den Textkontrast nicht sprengt.Rendern (
src/render/) – wandelt Markdown überunified/remark/rehypein HTML um (mitallowDangerousHtml: false), wendet eines von fünf Themes plus das erkannte/übergebene Brand Kit an und druckt mit Playwright als PDF.Bereitstellen – PDFs werden auf die lokale Festplatte (oder ein eingebundenes Volume) gelegt, hinter HMAC-signierten, zeitlich begrenzten URLs.
Keine Warteschlange, kein Worker-Prozess, kein Redis. Renderings sind synchron und dauern typischerweise 100–400 ms, sobald Chromium warm ist; die Erkennung wird 24 Stunden pro Host zwischengespeichert.
Was hier enthalten ist
.
├── src/ Fastify API (single process)
│ ├── index.ts Bootstrap, route registration
│ ├── env.ts Env validation (zod)
│ ├── lib/ prisma, auth, billing, storage, email, url-fetch (SSRF guard), inline-image
│ ├── render/ markdown → HTML → PDF (themes/, detect.ts)
│ └── routes/ auth, documents, demo, account, pdf, brand-kits, admin
├── prisma/schema.prisma 5 models: User, ApiKey, Document, CreditTransaction, BrandKit
├── mcp/ MCP server (unpublished — see mcp/README.md)
├── samples/ Example output (see Examples below) + input markdown used to generate it
├── scripts/ preview.ts / detect.ts — regenerate the samples/ output locally
├── Dockerfile Single-image deploy (Playwright base)
└── railway.json Railway config (healthcheck only — start cmd is in Dockerfile)API-Oberfläche (v1)
Authentifizierung
Methode | Pfad | Auth | Was es tut |
POST |
| - | Schlüssel per E-Mail anfordern. Immer 202; Schlüssel wird an das Postfach gesendet. Beim ersten Mal = neuer Benutzer + kostenlose Credits. Bei späteren Aufrufen = Rotation (alte Schlüssel 24h gültig, dann widerrufen). |
Rendern
Methode | Pfad | Auth | Was es tut |
POST |
| Bearer key | Markdown → PDF. Akzeptiert |
GET |
| signierte URL | PDF streamen |
Brand Kits
Methode | Pfad | Auth | Was es tut |
POST |
| Bearer key | Erstellt/aktualisiert ein gespeichertes Kit nach Namen (eines pro Benutzer und Name) |
GET |
| Bearer key | Listet deine Kits auf |
GET |
| Bearer key | Liest ein Kit |
DELETE |
| Bearer key | Löscht ein Kit |
POST |
| Bearer key | Übergib eine URL, erhalte |
POST |
| Bearer key | Bis zu 20 URLs parallel über den vorhandenen Playwright-Pool. Fehler pro Element werden inline zurückgegeben. |
Konto / Gesundheit
Methode | Pfad | Auth | Was es tut |
GET |
| Bearer key | E-Mail, Credits, Liste der aktiven API-Schlüssel (Klartext, damit Benutzer sie wiederherstellen können) |
GET |
| - |
|
Demo (anonym, eingeschränkt)
Keine Authentifizierung, keine Credits werden berechnet. Strenge Pro-IP-Ratenbegrenzung (30/Stunde) zusätzlich zur globalen 60/min.
Methode | Pfad | Was es tut |
POST |
|
|
Admin (nur lesen)
Geschützt durch den X-Admin-Token-Header. Wenn ADMIN_TOKEN nicht gesetzt ist, gibt jede /admin/*-Route 404 zurück – keine Oberfläche, keine Entdeckung.
Methode | Pfad | Was es tut |
GET |
| Summen: Benutzer, aktive Schlüssel, Dokumente, Seiten, Bytes, Credits, Dokumentanzahl für letzte 24h / 7d |
GET |
| Paginiert Liste mit Dokument- und Schlüsselanzahl pro Benutzer |
GET |
| Paginiert Liste mit verknüpfter Benutzer-E-Mail. Filter nach |
GET |
| Beliebiges PDF ohne signierte URL streamen |
Sicherheitslage
API-Schlüssel: 192-Bit zufällig, als Klartext gespeichert (damit
/accountsie anzeigen kann); Widerruf verwendet einenrevokedAt-Zeitstempel mit einem Gnadenfenster.Signierte URLs: HMAC-SHA256,
exp+sig-Abfrageparameter, Standard-TTL 7 Tage.SSRF-Schutz:
assertPublicUrllöst DNS auf und lehnt RFC1918, Loopback, Link-Local, IPv6-ULA auf der vom Aufrufer übermittelten URL ab. Angewendet aufbrandKit.logoUrlund/v1/brand-kits/detect. Dass der übermittelte Host öffentlich ist, garantiert nicht, dass jeder Hop es ist – ein öffentlicher Host kann auf eine private Adresse umleiten – daher validieren sowohl der Playwright-Navigationspfad (src/render/detect.ts) als auch der Bild-Inlining-Fetch (src/lib/inline-image.ts) die Adresse bei jedem Redirect-Hop erneut, bevor sie ihr folgen, und erneut nach der endgültigen Navigation. Dies verengt das Fenster, beseitigt es aber nicht vollständig: Die anfängliche Verbindung zu einem Redirect-Ziel erfolgt, bevor die erneute Prüfung es ablehnen kann, sodass ein entschlossener Angreifer immer noch eine blinde ausgehende Anfrage verursachen kann (keine Antwortdaten werden an ihn zurückgegeben), obwohl niemals Seiteninhalt aus einem abgelehnten Ziel extrahiert oder gerendert wird. Eine vollständige Schließung würde IP-Pinning auf Netzwerkebene erfordern.CSS-Injektionsschutz: Das
css-Feld lehnt<style>,</style>,<script>,</script>ab – andernfalls würde das rohe Einbetten innerhalb von<style>${css}</style>einem Angreifer ermöglichen, auszubrechen und JS im Chromium-Pool auszuführen.Markdown-Bereinigung:
remark-rehypeläuft mitallowDangerousHtml: false, sodass<script>in Markdown-Textkörpern entfernt wird.Grenzen: Markdown ≤ 500KB, CSS ≤ 50KB, Body ≤ 2MB, Rendern ≤ 30s, gerenderte Seiten ≤
MAX_PAGES_PER_RENDER(Standard 200), Rate ≤ 60 Anfragen/min/Schlüssel.Admin-Endpunkt: Konstantzeit-Token-Vergleich; Routen geben 404 (nicht 401) zurück, wenn Token falsch oder nicht gesetzt.
Bekannte Einschränkungen
Kein Produkt, daher werden diese offengelegt und nicht als Backlog verfolgt:
Keine automatisierte Testsuite. Alles oben Genannte wurde von Hand gegen eine laufende Instanz verifiziert; es gibt kein Regressionsnetz.
Keine eingecheckten Prisma-Migrationen. Der Container-Startbefehl führt
prisma db push --skip-generate --accept-data-lossaus.In-Memory-Ratenbegrenzung und Erkennungs-Cache. Für eine einzelne Instanz in Ordnung; würde mehrere Replikate nicht überleben, ohne beide auf etwas Gemeinsames zu verschieben.
Keine Zahlungsabwicklung eingerichtet. Das Creditsystem existiert im Schema und in der API; nichts belastet eine Karte.
Das MCP-Paket (
mcp/) ist nicht auf npm veröffentlicht und wird es auch nicht – siehe mcp/README.md.
Lokale Entwicklung
# 1. Postgres running locally on 5432
# 2. Env
cp .env.example .env
# (set SIGNING_SECRET to `openssl rand -base64 32`)
# 3. Install + migrate
npm install
npx prisma migrate dev
# 4. Run
npm run devSmoke-Test:
# Request a key. Response is { "sent": true } - the key arrives by email.
# In dev (RESEND_API_KEY unset) the server logs the email to stdout; grab the
# key from there.
curl -X POST http://localhost:3000/auth/register \
-H "Content-Type: application/json" \
-d '{"email":"you@example.com"}'
export PP_KEY="pp_live_..."
# Render
curl -X POST http://localhost:3000/v1/documents \
-H "Authorization: Bearer $PP_KEY" \
-H "Content-Type: application/json" \
-d '{"markdown":"# Hello\n\nWorld.","title":"Test"}'MCP
Siehe mcp/README.md. Ein dünner MCP-Client über die REST-API. Nicht auf npm veröffentlicht – als Referenzcode enthalten, nicht als installierbares Werkzeug.
Bereitstellung (Railway-Beispiel)
Die genauen Schritte, die für die echte Bereitstellung verwendet wurden, unter der dies lief – hier als Dokumentation aufbewahrt, nicht als Einladung, dies in Produktion zu betreiben.
# 1. Create project with a Postgres database
railway init --name paperpress
railway add --database postgres
# 2. Create the app service. DATABASE_URL is wired via service reference.
railway add --service paperpress \
--variables "DATABASE_URL=\${{Postgres.DATABASE_URL}}" \
--variables "SIGNING_SECRET=$(openssl rand -base64 32)" \
--variables "PUBLIC_BASE_URL=https://your-app.up.railway.app" \
--variables "STORAGE_DIR=/data/storage" \
--variables "NODE_ENV=production" \
--variables "FREE_TIER_CREDITS=100" \
--variables "PLAYWRIGHT_MAX_CONTEXTS=2" \
--variables "RENDER_TIMEOUT_MS=30000" \
--variables "KEY_GRACE_PERIOD_HOURS=24" \
--variables "MAX_PAGES_PER_RENDER=200" \
--variables "ADMIN_TOKEN=$(openssl rand -base64 36 | tr -d '\n')"
# 3. Attach a volume so PDFs survive container restarts
railway service paperpress
railway volume add --mount-path /data/storage
# 4. Domain (auto-detects the container port)
railway domain --port 3000
# 5. Ship
railway up --detach -cHinweise:
Das Dockerfile verwendet
mcr.microsoft.com/playwright:vX.Y-jammyals Basis. Halte diese Version im Gleichschritt mit demplaywright-npm-Paket – eine Abweichung bedeutet, dass die Browser-Binärdatei nicht existiert und Renderings fehlschlagen.Der
startCommandinrailway.jsonist absichtlich nicht vorhanden: Railway parst ihn als argv (nicht als Shell), daher schlagen verkettete&&-Befehle fehl. DasCMDimDockerfileumschließt mitsh -cund führt die vollständige Startsequenz aus.E-Mails: Bis
RESEND_API_KEYgesetzt ist, werden Registrierungsschlüssel auf stdout protokolliert. Grep nach[email:console].
Beispiele
Alle diese sind in samples/ eingecheckt – generiert von scripts/preview.ts und scripts/detect.ts, generiere sie selbst mit npx tsx scripts/preview.ts / npx tsx scripts/detect.ts <url>.
Gleiches Markdown, fünf Themes (clean unten gezeigt – vollständiges PDF):

Theme | |
| |
| |
| |
|
Marke automatisch von einer Live-URL erkannt (brandFromUrl: "stripe.com" – vollständiges PDF):

Quell-URL | Erkannt + gerendert |
github.com | |
railway.com | |
vercel.com |
Inline-Brand-Kits (keine URL, Felder werden direkt in der Anfrage übergeben) — forest, mono-coral, stripe-colors.
Eingabe-Markdown oben verwendet: sample.md, demo-q4-review.md (das von /v1/demo verwendete).
Status
Gebaut: Markdown → PDF in 5 Themes, Brand-Kit-Autoerkennung von einer URL (echter font-family-Stack, nicht nur ein serif|sans|mono-Bucket), 24h-Erkennungscache, brandFromUrl-Ein-Aufruf-Verknüpfung, Batch-Erkennung (20 URLs parallel), WCAG-Luminanzschutz, E-Mail-basierte Schlüsselausstellung mit Rotationsfrist, ein MCP-Server, eine schreibgeschützte Admin-Oberfläche, signierte Freigabe-URLs, vorgerenderte Dokumentation.
Bewusst nicht gebaut: Zahlungsabwicklung, npm-Publish des MCP-Pakets, automatisierte Tests, echte Prisma-Migrationen. Das ist kein aktives Backlog — es ist als Portfolio-Stück abgeschlossen und wird nicht auf eine 1.0 hin entwickelt.
Lizenz
MIT — siehe LICENSE.
This server cannot be installed
Maintenance
Related MCP Servers
- AlicenseAqualityAmaintenanceTurn markdown into designed PDFs with cover page, table of contents, and code blocks that hold across pages. One command from Claude Desktop, Claude Code, Cursor, Cline, Zed, or any MCP-capable client.2381MIT
- AlicenseNot gradedqualityDmaintenanceProvides a standardized interface for interacting with Markitdown's tools and services through a unified API, compatible with MCP-compliant services.MIT

docjet-mcpofficial
AlicenseAqualityAmaintenanceRender branded PDF documents (invoices, reports, certificates) and PNG social images directly from any MCP client using DocJet templates or raw HTML with JSON data.340MIT- AlicenseNot gradedqualityBmaintenanceMCP server that exposes brand identity guidelines (visual look and voice) as markdown, enabling LLMs to produce on-brand content.13MIT
Related MCP Connectors
Render HTML, Markdown, or URLs to images, PDF, or branded artifacts; extract and watch pages.
Turn HTML or Markdown into a clean, styled PDF and get a download link.
Markdown in, any format out. PDFs merged, split, watermarked. Runs on our own doc engines.
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/rozetyp/paperpress'
If you have feedback or need assistance with the MCP directory API, please join our Discord server