Pre-signed URLs expire in 24 hours. Refresh by polling GET /v1/jobs/{job_id} again.
Supabase job table
-- Recent jobsSELECT id, project_name, filename, status, requested_by, created_at, error_messageFROM dev_r_pdf_jobsORDER BY created_at DESCLIMIT 20;-- Reset stuck jobs (> 10 min in processing)UPDATE dev_r_pdf_jobsSET status = 'pending', started_at = NULLWHERE status = 'processing' AND started_at < NOW() - INTERVAL '10 minutes';
Prometheus metrics
Scraped from /metrics via job pdf_service. Alert rules: monitoring/prometheus/rules/pdf-service.yml.
Metric
Description
pdf_conversions_total{status,type}
Counter per conversion outcome
pdf_conversion_duration_seconds
Histogram of conversion time
pdf_gotenberg_healthy
0/1 — Gotenberg reachability
Alerts
Alert
Severity
Condition
PdfServiceDown
critical
No scrape for 2 min
PdfServiceHighErrorRate
warning
Error rate > 10% over 5 min
PdfServiceSlowConversions
warning
p95 latency > 30s over 5 min
PdfServiceGotenbergUnhealthy
warning
Gotenberg reports unhealthy for 5 min
Troubleshooting
Symptom
Action
401 Unauthorized
Check PDF_SERVICE_API_KEY in monitoring/.env on vps-i1
413 File too large
Markdown > 1 MB — split the document
422 Unprocessable
File is not valid UTF-8
status: error in job
Check error_message in Supabase; often Gotenberg or Wasabi issue
Job stuck in processing
Run the reset query above
Gotenberg unhealthy
docker compose restart gotenberg in /opt/p24-infra/monitoring/
p24-infra-mcp (MCP server)
Overview
HTTP MCP server built with FastMCP. Exposes pdf-service functionality as Claude MCP tools. Claude Code clients connect via .mcp.json using the MCP Streamable HTTP transport.
Source:infra-src/p24-infra-mcp/ Auth:X-Api-Key header (same PDF_SERVICE_API_KEY — no new secret needed) Upstream: calls pdf-service internally at http://pdf-service:8000
MCP tools
Tool
Description
render_markdown_to_pdf
Submits Markdown, polls until done, returns 24h pre-signed URL
get_pdf_job_status
Returns {status, output_url?, error_message?} for a job ID
list_pdf_jobs
Lists recent jobs from Supabase; optional project filter, max 100
render_markdown_to_pdf parameters
Parameter
Required
Default
Notes
content
yes
—
Markdown text (not a file path)
project
yes
—
S3 prefix slug: p24-infra, dev, etc.
filename
no
document
Base name without extension
requested_by
no
mcp-client
Caller label for Supabase traceability
Client configuration
Claude Code reads .mcp.json. The entry is present in:
Local (Windows): C:\Users\konar\.claude\.mcp.json
Repo root: C:\code_2026\p24-infra\.mcp.json
vps-i1: /home/claude-runner/.claude/.mcp.json
vps-h1: /home/claude-runner/.claude/.mcp.json
PDF_SERVICE_API_KEY is sourced from /home/claude-runner/.claude-env on both VPSes (auto-loaded by .bashrc). On local, it is in .env.local.
# On vps-i1cd /opt/p24-infra/monitoringdocker compose logs --tail=50 p24-infra-mcp
Restart / redeploy
# Pull latest code and rebuildcd /opt/p24-infragit pullcd monitoringdocker compose up -d --build p24-infra-mcp
Troubleshooting
Symptom
Action
401 Unauthorized from MCP
Wrong or missing PDF_SERVICE_API_KEY in client .mcp.json
Invalid Host header
MCP_PUBLIC_HOST env var not set / wrong; container uses mcp.vps-i1.infra.zintegrowana.online
TLS error on new deploy
Restart Caddy to trigger cert issuance: docker compose restart caddy
Tool hangs (no response in 60s)
pdf-service or Gotenberg unhealthy; check their logs
RuntimeError: Task group not initialized
MCP app mounted incorrectly inside another Starlette app — lifespan skipped. See server.py for correct raw-ASGI middleware pattern.
FastMCP notes (version 1.27.2)
Lifespan:streamable_http_app() contains an internal AnyIO task group initialized by its own lifespan. Do not mount it as a sub-app inside another Starlette instance — that skips the lifespan. Instead, add routes/middleware on top of the returned app directly.
DNS rebinding protection:TransportSecuritySettings blocks non-localhost Host headers by default. allowed_hosts must include the public hostname.
Transport: Streamable HTTP at /mcp (same pattern as WAHA MCP on this infra).