Skip to main content
Glama

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 brand

Sobre 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:

  1. Detectar (src/render/detect.ts) — navega a la URL objetivo con una instancia agrupada de Playwright/Chromium, lee theme-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.

  2. Renderizar (src/render/) — convierte markdown en HTML mediante unified/remark/rehype (con allowDangerousHtml: false), aplica uno de los cinco temas y el kit de marca detectado/proporcionado, e imprime a PDF con Playwright.

  3. 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

/auth/register

-

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

/v1/documents

Clave Bearer

Markdown → PDF. Acepta theme, brandKit (nombre guardado o en línea), brandFromUrl (atajo de detectar-y-aplicar), css, format, landscape, title. Devuelve URL firmada. 413 si las páginas renderizadas > MAX_PAGES_PER_RENDER (por defecto 200).

GET

/pdf/:id?exp=&sig=

URL firmada

Transmite el PDF

Kits de marca

Método

Ruta

Autenticación

Qué hace

POST

/v1/brand-kits

Clave Bearer

Crea/actualiza un kit guardado por nombre (uno por usuario por nombre)

GET

/v1/brand-kits

Clave Bearer

Lista tus kits

GET

/v1/brand-kits/:id

Clave Bearer

Lee un kit

DELETE

/v1/brand-kits/:id

Clave Bearer

Elimina un kit

POST

/v1/brand-kits/detect

Clave Bearer

Pasa una URL y recibe primaryColor, logoUrl, favicon, fontFamily, fontStyle. Caché de 24h. Pasa { refresh: true } para omitirlo.

POST

/v1/brand-kits/detect-batch

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

/account

Clave Bearer

Correo, créditos, lista de claves API activas (en texto plano, para que los usuarios puedan recuperarlas)

GET

/health

-

{ status: 'ok' }

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

/v1/demo

{ url } → detecta la marca (cacheada) + renderiza el samples/demo-q4-review.md incluido como PDF. Devuelve kit + URL firmada.

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

/admin/stats

Totales: usuarios, claves activas, documentos, páginas, bytes, créditos, recuentos de documentos de las últimas 24h / 7d

GET

/admin/users

Lista paginada con recuentos de documentos y claves por usuario

GET

/admin/documents

Lista paginada con el correo del usuario unido. Filtra por userId, status.

GET

/admin/documents/:id/pdf

Transmite cualquier PDF sin URL firmada

Postura de seguridad

  • Claves API: aleatorias de 192 bits, almacenadas en texto plano (para que /account pueda mostrarlas); la revocación usa una marca de tiempo revokedAt con una ventana de gracia.

  • URLs firmadas: HMAC-SHA256, parámetros de consulta exp + sig, TTL por defecto de 7 días.

  • Protección SSRF: assertPublicUrl resuelve DNS y rechaza RFC1918, loopback, link-local y ULA IPv6 en la URL que envía un llamador. Se aplica a brandKit.logoUrl y 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 css rechaza <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-rehype se ejecuta con allowDangerousHtml: 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 dev

Prueba 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 -c

Notas:

  • El Dockerfile usa mcr.microsoft.com/playwright:vX.Y-jammy como base. Mantén esa versión en sintonía con el paquete npm playwright — un desajuste significa que el binario del navegador no existirá y los renderizados fallarán.

  • El startCommand en railway.json está intencionalmente ausente: Railway lo analiza como argv (no como shell), así que los comandos encadenados con && fallan. El CMD en Dockerfile lo envuelve en sh -c y 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):

clean theme example

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

stripe brand-detected example

URL de origen

Detectado + renderizado

github.com

detect-github-com.pdf

railway.com

detect-railway-com.pdf

vercel.com

detect-vercel-com.pdf

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.

A
license - permissive license
Not graded
quality - not tested
C
maintenance

Maintenance

Maintainers
Response time
Release cycle
Releases (12mo)
Commit activity

Related MCP Servers

  • A
    license
    A
    quality
    A
    maintenance
    Turn 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.
    2
    38
    1
    MIT
  • A
    license
    Not graded
    quality
    D
    maintenance
    Provides a standardized interface for interacting with Markitdown's tools and services through a unified API, compatible with MCP-compliant services.
    MIT

View all related MCP servers

Related MCP Connectors

View all MCP Connectors

Latest Blog Posts

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