paperpress
paperpress
Una API que detecta la marca de una empresa a partir de su URL en vivo — logo, color primario, tipografía — y renderiza markdown en un PDF con el estilo de esa marca, en una sola llamada HTTP. Se ofrece como API REST simple y como servidor MCP.
POST /v1/documents
{ "markdown": "# Q4 report\n...", "brandFromUrl": "stripe.com" }
→ ~1s → signed URL to a PDF in Stripe's brandSobre este proyecto
Esto se construyó y se ejecutó brevemente como un servicio real desplegado antes de que examinara de cerca el mercado en el que tendría que competir: Claude ahora genera PDF/PPTX/DOCX de forma nativa, y Brandfetch ya vende una Brand Context API construida específicamente para fundamentar agentes de IA, con clientes reales de pago. Ambas mitades de lo que esto hace — detectar una marca, renderizar un documento — son ahora casi un commodity o ya pertenecen a un competidor con financiación. No lo estoy persiguiendo como producto.
Está público como pieza de portafolio / implementación de referencia: un detector de marca funcional basado en Playwright (propiedades personalizadas de CSS, muestreo de color de botones CTA, extracción de candidatos a logo con puntuación, protección de contraste WCAG), un pipeline de renderizado síncrono con Fastify, un buscador de URLs endurecido contra SSRF, y un servidor MCP que lo envuelve. Lee el código, hazle fork, ejecútalo — tiene licencia MIT. No se mantiene como producto: no hay procesamiento de pagos conectado y el paquete MCP (mcp/) no está publicado en npm.
Related MCP server: Markitdown Universal MCP Server
Cómo funciona
Un único proceso Fastify hace tres cosas:
Detectar (
src/render/detect.ts) — navega a la URL objetivo con una instancia agrupada de Playwright/Chromium, leetheme-color, las propiedades personalizadas de CSS de la marca, los colores de botones CTA, los candidatos<img>del encabezado puntuados por posición/tamaño/formato, y las pilas de fuentes calculadas. Filtra el ruido casi-blanco/casi-negro/casi-gris, aplica una protección de luminancia WCAG para que un color de marca demasiado claro no reviente el contraste del texto.Renderizar (
src/render/) — convierte markdown en HTML medianteunified/remark/rehype(conallowDangerousHtml: false), aplica uno de los cinco temas y el kit de marca detectado/proporcionado, e imprime a PDF con Playwright.Servir — los PDF van al disco local (o a un volumen montado) detrás de URLs firmadas con HMAC y con límite de tiempo.
Sin cola, sin proceso worker, sin Redis. Los renderizados son síncronos y normalmente tardan 100–400 ms una vez que Chromium está caliente; la detección se almacena en caché 24 horas por host.
Qué hay aquí
.
├── 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)Superficie de la API (v1)
Autenticación
Método | Ruta | Autenticación | Qué hace |
POST |
| - | Solicita una clave por correo. Siempre 202; la clave se envía a la bandeja de entrada. Primera vez = nuevo usuario + créditos gratuitos. Llamadas posteriores = rotación (las claves antiguas son válidas 24h, luego se revocan). |
Renderizado
Método | Ruta | Autenticación | Qué hace |
POST |
| Clave Bearer | Markdown → PDF. Acepta |
GET |
| URL firmada | Transmite el PDF |
Kits de marca
Método | Ruta | Autenticación | Qué hace |
POST |
| Clave Bearer | Crea/actualiza un kit guardado por nombre (uno por usuario por nombre) |
GET |
| Clave Bearer | Lista tus kits |
GET |
| Clave Bearer | Lee un kit |
DELETE |
| Clave Bearer | Elimina un kit |
POST |
| Clave Bearer | Pasa una URL y recibe |
POST |
| Clave Bearer | Hasta 20 URLs en paralelo a través del pool existente de Playwright. Los errores por elemento se devuelven en línea. |
Cuenta / estado
Método | Ruta | Autenticación | Qué hace |
GET |
| Clave Bearer | Correo, créditos, lista de claves API activas (en texto plano, para que los usuarios puedan recuperarlas) |
GET |
| - |
|
Demo (anónima, limitada)
Sin autenticación, sin cobro de créditos. Límite estricto por IP (30/hora) sobre el global de 60/min.
Método | Ruta | Qué hace |
POST |
|
|
Admin (solo lectura)
Protegido por la cabecera X-Admin-Token. Cuando ADMIN_TOKEN no está definido, todas las rutas /admin/* devuelven 404 — sin superficie, sin descubrimiento.
Método | Ruta | Qué hace |
GET |
| Totales: usuarios, claves activas, documentos, páginas, bytes, créditos, recuentos de documentos de las últimas 24h / 7d |
GET |
| Lista paginada con recuentos de documentos y claves por usuario |
GET |
| Lista paginada con el correo del usuario unido. Filtra por |
GET |
| Transmite cualquier PDF sin URL firmada |
Postura de seguridad
Claves API: aleatorias de 192 bits, almacenadas en texto plano (para que
/accountpueda mostrarlas); la revocación usa una marca de tiemporevokedAtcon una ventana de gracia.URLs firmadas: HMAC-SHA256, parámetros de consulta
exp+sig, TTL por defecto de 7 días.Protección SSRF:
assertPublicUrlresuelve DNS y rechaza RFC1918, loopback, link-local y ULA IPv6 en la URL que envía un llamador. Se aplica abrandKit.logoUrly a/v1/brand-kits/detect. Que el host enviado sea público no garantiza que cada salto lo sea — un host público puede redirigir a una dirección privada —, así que tanto la ruta de navegación de Playwright (src/render/detect.ts) como la búsqueda de imágenes en línea (src/lib/inline-image.ts) revalidan la dirección en cada salto de redirección antes de aceptarla y de nuevo después de la navegación final. Esto reduce la ventana pero no la elimina por completo: la conexión inicial a un destino de redirección ocurre antes de que la revalidación pueda rechazarlo, por lo que un atacante decidido puede provocar una solicitud saliente ciega (no se le devuelve ningún dato de respuesta) aunque nunca se extrae ni se renderiza contenido de página de un destino rechazado. El cierre completo requeriría fijación de IP en la capa de red.Protección de inyección CSS: el campo
cssrechaza<style>,</style>,<script>,</script>— de lo contrario, la inserción en bruto dentro de<style>${css}</style>permitiría a un atacante escapar y ejecutar JS en el pool de Chromium.Saneamiento de Markdown:
remark-rehypese ejecuta conallowDangerousHtml: false, por lo que<script>en los cuerpos de markdown se elimina.Límites: markdown ≤ 500KB, css ≤ 50KB, cuerpo ≤ 2MB, renderizado ≤ 30s, páginas renderizadas ≤
MAX_PAGES_PER_RENDER(por defecto 200), tasa ≤ 60 req/min/clave.Endpoint de admin: comparación de token en tiempo constante; las rutas devuelven 404 (no 401) cuando el token es incorrecto o no está definido.
Limitaciones conocidas
No es un producto, así que esto se divulga en lugar de gestionarse como backlog:
Sin suite de pruebas automatizada. Todo lo anterior se verificó manualmente contra una instancia en ejecución; no hay red de regresión.
Sin migraciones de Prisma confirmadas. El comando de inicio del contenedor ejecuta
prisma db push --skip-generate --accept-data-loss.Límite de tasa y caché de detección en memoria. Bien para una sola instancia; no sobreviviría a múltiples réplicas sin mover ambos a algo compartido.
Sin procesamiento de pagos conectado. El sistema de créditos existe en el esquema y la API; nada cobra una tarjeta.
El paquete MCP (
mcp/) no está publicado en npm y no será — ver mcp/README.md.
Desarrollo local
# 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 devPrueba de humo:
# 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
Ver mcp/README.md. Un cliente MCP ligero sobre la API REST. No publicado en npm — incluido como código de referencia, no como herramienta instalable.
Despliegue (ejemplo con Railway)
Los pasos exactos utilizados para el despliegue real bajo el que se ejecutó esto — se mantienen aquí como documentación, no como invitación a ejecutarlo en producción.
# 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 -cNotas:
El Dockerfile usa
mcr.microsoft.com/playwright:vX.Y-jammycomo base. Mantén esa versión en sintonía con el paquete npmplaywright— un desajuste significa que el binario del navegador no existirá y los renderizados fallarán.El
startCommandenrailway.jsonestá intencionalmente ausente: Railway lo analiza como argv (no como shell), así que los comandos encadenados con&&fallan. ElCMDenDockerfilelo envuelve ensh -cy ejecuta la secuencia completa de inicio.Correos: hasta que se defina
RESEND_API_KEY, las claves de registro se registran en stdout. Busca[email:console].
Ejemplos
Todos estos están incluidos en samples/ — generados por scripts/preview.ts y scripts/detect.ts, regenerálos tú mismo con npx tsx scripts/preview.ts / npx tsx scripts/detect.ts <url>.
Mismo markdown, cinco temas (clean mostrado abajo — PDF completo):

Tema | |
| |
| |
| |
|
Marca auto-detectada desde una URL en vivo (brandFromUrl: "stripe.com" — PDF completo):

URL de origen | Detectado + renderizado |
github.com | |
railway.com | |
vercel.com |
Kits de marca en línea (sin URL, campos pasados directamente en la solicitud) — forest, mono-coral, stripe-colors.
Markdown de entrada utilizado arriba: sample.md, demo-q4-review.md (el utilizado por /v1/demo).
Estado
Construido: markdown → PDF en 5 temas, detección automática de kit de marca desde una URL (pila de font-family real, no solo un grupo serif|sans|mono), caché de detección de 24 h, acceso directo de una llamada brandFromUrl, detección por lotes (20 URL en paralelo), protección de luminancia WCAG, emisión de claves por correo electrónico con período de gracia de rotación, un servidor MCP, una superficie de administración de solo lectura, URL para compartir firmadas, documentación pre-renderizada.
No construido, a propósito: procesamiento de pagos, publicación npm del paquete MCP, pruebas automatizadas, migraciones reales de Prisma. Esto no es un backlog en vivo: está terminado como pieza de portafolio, no se está desarrollando hacia un 1.0.
Licencia
MIT — ver 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