Self-hosted MCP server for the Printix Cloud Print API — with a web admin UI, AI-assistant integration (claude.ai / ChatGPT / Claude Code), an optional cloud-print gateway and a capture webhook endpoint.
Runs as a cross-platform Docker container (Linux / macOS / Windows / Synology NAS / TrueNAS / Unraid / …).
This repo is the Docker distribution as a standalone project. The original Home Assistant add-on variant lives on separately in
printix-mcp-addon.
Current stable: v7.2.10 (April 2026) · 127 MCP tools · iOS mobile companion app · Auto-PDL conversion (PDF → PCL XL via Ghostscript) · Time-bomb-driven onboarding workflows · Multi-recipient secure print · Microsoft Entra ID + Authorization Code + PKCE flow.
v7.0.0 is single-tenant. One installation hosts exactly one tenant; all users share it. The earlier "one-tenant-per-user" model from v6.7.x has been removed (see CHANGELOG).
Two tiers: most features ship as Basic (free, no activation needed). Three operational features are Pro and require a one-time activation code from your contact person — once entered under Server settings → Pro features, they remain unlocked for the lifetime of the installation.
AI-assistant integration — 129 MCP tools
- MCP server for claude.ai (Streamable HTTP), ChatGPT (SSE) and Claude Code (CLI)
- Built-in OAuth 2.0 endpoints — no manual token juggling
- All tools ship with structured docstrings (when-to-use / when-NOT / returns / args + concrete example prompts) so the AI picks the right tool reliably
- See
docs/MCP_MANUAL_EN.md/docs/MCP_MANUAL_DE.mdfor the complete tool catalogue
GDPR-compliant role-based access control (v7.2.23+)
- Five built-in roles mapped to GDPR articles: End User (Art. 15-22), Helpdesk (Art. 32 — separation of duties), Admin (Art. 24), Auditor / DPO (Art. 37-39), Service Account (Art. 28+32)
- Two assignment paths: per-user override and per-Printix-group default ("highest role wins" on multi-group membership)
- Live status banner on
/admin/mcp-permissionsshows whether enforcement is active - Built-in compliance documentation: every customer-hosted instance ships with a GDPR Compliance Guide (PDF) and a Permission Matrix (PDF) downloadable from the admin UI
- Denied tool calls are recorded in the audit log for ongoing compliance review
- Activation is opt-in via
MCP_RBAC_ENABLED(defaults to1in the bundleddocker-compose.yml)
One-click HTTPS via Cloudflare Tunnel (v7.2.32+)
- Hosted on a public-IP-only Azure VM, on-prem, or behind NAT?
/admin/tunnelprovides a built-in tunnel manager — no external setup, no port-forwarding, no certbot - Quick Tunnel mode — anonymous
*.trycloudflare.comURL in 30 seconds, no Cloudflare account needed (testing/demo) - Named Tunnel mode — paste a free Cloudflare tunnel token, get persistent HTTPS on your own domain with built-in DDoS/bot protection
- The active tunnel URL is automatically stored as the
public_urlsetting so Connect-Center, OAuth flows, and MCP endpoints all just work - Bundled
cloudflaredbinary; multi-arch (amd64/arm64); auto-restart on container reboot
GDPR data subject rights — built-in MCP tools (v7.2.30+)
printix_personal_data_export(GDPR Art. 15) — every user can ask their AI assistant "What data do you have about me?" and receive a structured ZIP with profile, group memberships, cards, audit-log entries, time-bombs, print statistics and MCP role overrideprintix_personal_data_purge_request(GDPR Art. 17) — non-destructive deletion request: records the request in the audit log, sends a structured email to the configured tenant admins with the data summary and the requester's reason, returns a request ID. The admin reviews and executes the deletion viaprintix_offboard_user/printix_delete_userwithin the GDPR Art. 12(3) one-month deadline- End users are restricted to their own data (self-check at the argument level); Helpdesk and Admin can act on any subject in support of formal access/deletion requests
Per-user Connect-Center (v7.2.21+ · expanded v7.7.5+)
- One-page personal connection profile at
/my/connect - All connection data (MCP URL, SSE URL, OAuth ID + Secret, Bearer Token with reveal toggle, Authorize/Token URLs) in copy-buttoned cards
- Step-by-step instructions per platform (Claude.ai, ChatGPT, Claude Code CLI) plus a dedicated „🔗 Make.com / n8n / Zapier / Custom"-section with bearer-token examples (v7.7.6)
- 🩺 Connection self-test button (v7.7.5) — runs 7 server-side checks (public_url set, HTTPS active, OAuth discovery returns the right issuer, /mcp + /sse reachable, etc.) and shows ok/warn/error per item. First stop when a client says „connection failed" without explaining why.
- URL-mapping table spells out which URL goes into which AI client + the warning that swapping
/mcpand/sseis the most common mistake - Direct downloads for the localised user manuals (DE / EN / NO)
- Localised in DE / EN / NO; non-DE locales default to English
MCP Reports Cookbook (v7.7.0+)
- New admin page
/admin/mcp-reports-cookbookwith four ready-to-paste example prompts: top-user report with schedule, cost report with custom rates, anomaly detection, end-user self-service - Plus a card-grid of all 12 supported query types and an RBAC explainer per role
- Linked from
/reportsvia a blue hint banner: „💡 Reports can also be built via Claude/ChatGPT"
End-user self-service reports via MCP (v7.7.0+)
- New
printix_my_print_history,printix_my_costs,printix_my_environment_impactMCP tools — end-users ask their own AI assistant "how much did I print this month?" and get scoped data back, no admin needed - Optional
printix_my_group_print_history(v7.7.1) — peer comparison with colleagues in the caller's own Printix group, with anonymized colleague names ("Colleague AB12CD") and stable hash. Default OFF — tenant admin must enable per setting, GDPR/works-council notes shipped in the UI - All scoped via
mcp:self— no user-id parameter accepted, identity comes from the bearer token, SQL filter set server-side
Cache prefetch (v7.6.0+)
- Background prefetch of users/printers/workstations/sites/networks/groups/snmp on every login → first navigation to
/tenant/*is instant - Bulk card-count prefetch parallel per user (asyncio.gather) →
/tenant/usersshows card numbers without n+1 API calls - Periodic 60-second refresher keeps the cache warm — no stale lookups during the session
- Top-nav „⏳ loading data…" pill appears during the initial warm-up, switches to ✓ when done
Workflow tools (v6.8.x / v7.2.x — AI-driven workflows)
printix_print_self— AI generates a PDF inline and queues it on the caller's own secure-print queue (auto-PDL conversion to PCL XL)printix_print_to_recipients— multi-recipient secure print, accepts emails,group:<Name>,entra:<group-OID>mixedprintix_send_to_capture— push files straight into the capture pipeline (e.g. Paperless-ngx) without the printer detourprintix_welcome_user— onboarding workflow with conditional time-bombs (auto-reminder if user hasn't enrolled a card / printed yet after N days)printix_session_print— secure-print job that auto-expires after N hoursprintix_card_enrol_assist— register an NFC card UID with auto-transform via the user's profileprintix_describe_user_print_pattern,printix_quota_guard,printix_print_history_natural,printix_resolve_recipientsand more
Mobile app (iOS) — Printix MobilePrint
- Native SwiftUI app for iPhone/iPad
- Microsoft sign-in via in-app Safari sheet (
ASWebAuthenticationSession+ Authorization Code Flow with PKCE — no device-code prompt) - NFC card enrolment (tap an HID/Mifare/FeliCa/DESFire badge, UID is decoded via the profile transformer and registered to the Printix user)
- Share Extension: send any file from any iOS app via Share → Printix → choose target
- QR onboarding from the admin portal (
/my/setup-guide) — no manual server URL entry - Keychain-stored bearer token with Face ID / Touch ID unlock
Web admin (/admin)
- User management: create, invite, CSV bulk-import, Printix direct import (pull users straight from the Printix cloud into local accounts, optionally with an invitation mail)
- 2-role model (
admin|employee) with last-admin safeguard - Printix credentials management (Print / Card / WS / UM scopes)
- SMTP configuration for report and invitation mails
- Audit log with a searchable event history
- Backup / restore of the entire
/datavolume
Self-service (/my)
- View and delete jobs, delegate printing to other users
- Personal dashboard (own jobs, delegations, managed employees)
- QR code for iOS app pairing (
/my/setup-guide)
Reporting
- Report templates with design options (colour, logo, chart type)
- Report language + currency picker in the builder (v7.6.9) — German admin can ship English reports without switching the UI
- Demo-data notice on
/reportswhen active demo sessions feed into results (v7.6.10) - Scheduled reports (daily / weekly / monthly) delivered by mail
- Live queries: top users, top printers, cost per department, trends, anomalies
Backup & restore — production-grade (v7.6.5–7.6.7)
- Backup ZIP contains the complete persistent state — DB, demo DB, Fernet key, report templates, MCP secrets, web session signing key, plus the
tls/andletsencrypt/directories so restores never lose the cert + ACME rate-limit headroom - Optional AES-encrypted backup (Fernet, PBKDF2-HMAC-SHA256 600 k iters, salt in manifest) — set a passphrase in the create form and the ZIP is cloud-storage-safe (without it, useless)
verify_backup()pre-flight before restore — manifest check, SQLite header check, size limit (200 MB default). No partial restores on broken archives- 13-step end-to-end test (
bin/test-backup-restore.py) — runs both plain and encrypted round-trips; ships in the image and runnable in the live container
Cloud-print gateway (optional)
- IPP/IPPS listener on port 631 — PCs can treat the container as a network printer
- Capture webhook endpoint (Papercut-style follow-me-print trigger)
- Auto-PDL conversion (PDF / PostScript / Text → PCL XL via Ghostscript) for every server-side print path — so printers without a built-in PDF RIP no longer print hieroglyphs
Auth
- Local accounts (username / password, PIN, ID code)
- Microsoft Entra ID / Azure AD SSO (optional)
- Web SSO (Authorization Code + client_secret)
- macOS / Windows desktop client (Device Code Flow)
- iOS mobile app (Authorization Code + PKCE — public client)
- Single Entra app registration covers all three flows
- OAuth for AI assistants
i18n
- Multi-language web UI (de / en / more), invitation mails localised
The following features ship in the same image as Basic, but the corresponding admin pages stay locked behind a 🔒 marker until an activation code is entered under Server settings → Pro features. The MCP tools (Claude/ChatGPT side) and webhook endpoints remain functional regardless of license — only the human-facing admin pages are gated. Ask your contact person for the activation code.
📥 Capture Store
- Document capture with webhook profiles, automatic indexing, and routing to Paperless-NGX, SharePoint, or any third-party system
- Per-profile signature requirement, custom metadata fields, target whitelisting
📮 Guest-Print
- Email-based guest-print mailboxes for external users without a Printix account
- Per-mailbox configuration of approval workflow, site routing, and storage behaviour
- Microsoft Entra integration for inbound mail polling
🖨️ Print Job Management
- Extended admin UI for bulk actions across print jobs: reassignment, bulk-cancel, audit trail with replay, anomaly detection
- Currently scaffolded for upcoming releases — license slot reserved.
# 1. Create a project folder
mkdir printix-mcp && cd printix-mcp
# 2. Grab the compose file and sample config
curl -O https://raw.githubusercontent.com/mnimtz/printix-mcp-docker/main/docker-compose.yml
curl -O https://raw.githubusercontent.com/mnimtz/printix-mcp-docker/main/.env.example
mv .env.example .env
# 3. Adjust the config (at minimum set MCP_PUBLIC_URL if behind a tunnel/proxy)
nano .env
# 4. Start
docker compose up -d
# 5. Open the browser for the first-time setup
open http://localhost:8080That's it. In the web UI you register the first admin user and store your Printix API credentials.
Portainer offers three ways to create a stack — each one works with this project.
Stacks → Add stack → Repository
| Field | Value |
|---|---|
| Repository URL | https://github.com/mnimtz/printix-mcp-docker |
| Repository reference | refs/heads/main |
| Compose path | docker-compose.yml |
| Auto update | (optional) webhook or polling interval |
Under Environment variables set at least MCP_PUBLIC_URL (if you're behind a tunnel/proxy). Everything else is optional — see .env.example.
Benefit: Portainer pulls updates directly from the repo, no more manual docker compose pull.
Stacks → Add stack → Web editor → paste the following compose snippet:
services:
printix-mcp:
image: ghcr.io/mnimtz/printix-mcp-docker:latest
container_name: printix-mcp
restart: unless-stopped
environment:
MCP_PUBLIC_URL: ${MCP_PUBLIC_URL:-}
MCP_LOG_LEVEL: ${MCP_LOG_LEVEL:-info}
WEB_PORT: 8080
MCP_PORT: 8765
CAPTURE_PORT: 8775
CAPTURE_ENABLED: ${CAPTURE_ENABLED:-false}
IPP_PORT: ${IPP_PORT:-0}
# Container timezone — affects `docker logs` and all subprocess
# timestamps. The web UI also has a Display Timezone setting under
# Administration → Settings, but that only retags the rendered
# tables; container/MCP/capture process logs follow this TZ.
TZ: ${TZ:-Europe/Berlin}
ports:
- "8080:8080"
- "8765:8765"
- "8775:8775"
# - "631:631" # only when IPP_PORT is set
volumes:
- printix-data:/data
healthcheck:
test: ["CMD", "curl", "-fsS", "http://127.0.0.1:8765/health"]
interval: 30s
timeout: 5s
retries: 3
start_period: 30s
volumes:
printix-data:
driver: localIn the Environment variables section below, set:
MCP_PUBLIC_URL=https://mcp.example.com(if behind a tunnel/proxy — leave empty otherwise)CAPTURE_ENABLED=false(set totrueif capture webhooks should arrive from outside)IPP_PORT=0(set to631to enable the cloud-print gateway)
Then Deploy the stack — done.
Stacks → Add stack → Upload → upload the docker-compose.yml from this repo, set env vars as above, deploy.
Portainer lists the container under Containers; logs are available directly in the Portainer UI. Open the web UI at http://<portainer-host>:8080 and finish the first-time setup (create admin, store Printix credentials).
Updating to a new version: Portainer → Stack → Pull and redeploy (or automatic via webhook/polling with Option A).
The container starts three Python services (no separate reverse proxy required):
| Port (default) | Service | Purpose |
|---|---|---|
| 8080 | Web admin UI | Registration, admin UI, mobile-app onboarding |
| 8765 | MCP endpoint | claude.ai (Streamable HTTP) + ChatGPT (SSE) + OAuth |
| 8775 | Capture webhook (optional) | Papercut-style follow-me-print trigger |
| 631 | IPP/IPPS listener (optional) | Cloud-print input for printer drivers |
All ports and their host mappings live in docker-compose.yml and .env.
All data lives in the Docker volume printix-data (mounted at /data in the container):
| Path | Contents |
|---|---|
/data/printix_multi.db |
SQLite — users, tenants, jobs, reports, audit log |
/data/fernet.key |
Symmetric key for DB-field encryption (auto-generated on first start) |
/data/web_session_key |
Session-signing key for the web UI |
/data/demo_data.db |
Local demo / playground data (only when demo mode is used) |
/data/report_templates.json |
Saved report templates |
/data/ipp-spool/ |
IPP spool (only when the cloud-print listener is active) |
/data volume is enough — everything sensitive is encrypted with the Fernet key (which also lives there).
If you want the data visible on the host, change the volume in docker-compose.yml:
volumes:
- ./data:/data # instead of: printix-data:/dataand drop the printix-data: named volume at the bottom. Important: set the bind-mount ownership or the container cannot write:
mkdir -p ./data
sudo chown -R 1000:1000 ./dataThe container runs as the non-root user printix (UID 1000, GID 1000).
Two places, two responsibilities (since v7.0.0):
- Ports → only in
docker-compose.ymlunderports:. To move to a different host port, change only the left-hand number:ports: - "9000:8080" # web UI now on host port 9000
- Runtime settings →
.env(env defaults) or the admin UI at/admin/settings(overrides.env).
The most important environment variables:
| Variable | Default | Purpose |
|---|---|---|
MCP_PUBLIC_URL |
(empty) | Public URL (tunnel/proxy, e.g. https://mcp.example.com). Can be overridden at runtime under /admin/settings — the DB setting takes precedence. |
MCP_LOG_LEVEL |
info |
debug | info | warning | error | critical |
MCP_RBAC_ENABLED |
1 (in compose; 0 if compose is bypassed) |
Role-based access control for MCP tool calls. 1 enforces the roles configured at /admin/mcp-permissions; tools outside the caller's scope return a permission_denied payload and an audit-log entry. 0 is pass-through (anyone can call any tool). See the GDPR Compliance Guide shipped in the image. |
CAPTURE_ENABLED |
false |
Separate capture server on port 8775 instead of going through the MCP port |
IPP_PORT |
0 |
IPP listener port (0 = disabled, 631 = default) |
IPPS_CERTFILE / IPPS_KEYFILE |
(empty) | TLS certificate for IPPS (when IPP_PORT is set) |
See .env.example for the full, annotated list.
Lets users sign in with their work Microsoft account instead of a local password. Works for the web UI, the Windows / macOS desktop clients (Device Code Flow) and the iOS Printix MobilePrint app (Authorization Code + PKCE, native in-app Safari sheet — v7.1.4+).
- Open the web UI → log in as admin → Settings → Microsoft Entra ID.
- Click „Auto-Setup". The page shows a one-time device code.
- Open
https://microsoft.com/deviceloginon any device, paste the code, sign in with an account that has Application Administrator (or Global Admin) rights in the target Entra tenant, grant consent. - The server uses Microsoft Graph to create an App Registration named „Printix Management Console", generates a client secret, and stores
tenant_id/client_id/client_secretin the settings table. Done — web SSO works.
Already done on a sister instance? You can skip the auto-setup and paste an existing
tenant_id/client_id/client_secrettriple manually — the same App Registration can serve multiple MCP server instances.
The iOS app (MySecurePrint, 1.0.0+) uses Authorization Code Flow with PKCE. Microsoft treats the custom URL scheme mysecureprint:// as a public client, so it needs an extra platform on the App Registration:
-
Open https://portal.azure.com → Microsoft Entra ID → App registrations → tab All applications → search for the Client-ID shown in the MCP web UI (or the name „Printix Management Console").
-
Open the app → Authentication → click + Add a platform → choose Mobile and desktop applications.
-
In Custom redirect URIs enter exactly:
mysecureprint://oauth/callbackNo
https://, no trailing slash, no spaces. -
Configure → Save. Allow public client flows stays No.
That's it. The same App Registration now serves all four flows:
| Flow | Used by | Redirect / Mode |
|---|---|---|
| Auth Code (confidential) | Web UI | https://<your-host>/auth/entra/callback |
| Device Code | macOS, Windows clients, admin auto-setup | none — code-based |
| Auth Code + PKCE (public) | iOS MySecurePrint | mysecureprint://oauth/callback |
| Auth Code (confidential) | Guest-Print mailbox onboarding | https://<your-host>/admin/guestprint/... |
# Web SSO: visit https://<your-host>/login → "Sign in with Microsoft"
# iOS: install the TestFlight build, open the app, tap
# "Sign in with Microsoft" — an in-app Safari sheet should open
# and return automatically after authentication.
# Server smoketest for the iOS-PKCE endpoint:
curl -sS -X POST https://<your-host>/desktop/auth/entra/authcode/start \
-d 'device_name=test' \
-d 'redirect_uri=mysecureprint://oauth/callback' \
| python3 -m json.tool
# Expected: {session_id, auth_url, state, expires_in: 600}| Symptom | Cause | Fix |
|---|---|---|
AADSTS50011 redirect URI mismatch |
URI typo or missing platform entry | Re-check Step 2; URI must match byte-for-byte |
AADSTS700025 Client is public, no client_secret allowed |
Mobile redirect on a server that still sends the secret | Already handled in v7.1.4+. Make sure you're on at least 7.1.4 (docker compose pull && docker compose up -d) |
Graph /me returns 403 Forbidden |
Access token without User.Read permission |
Already handled in v7.1.4+. First sign-in per user shows a one-time consent prompt — accept it |
| iOS app shows „the data couldn't be read" on tap | Server endpoint not reachable or version too old | Server must be ≥ 7.1.4 and expose /desktop/* |
| Auto-Setup wizard fails with „insufficient privileges" | Signed-in user is not Application Administrator | Use a Global Admin account for the device-code login or have your tenant admin run the wizard once |
The auto-setup wizard registers a single-tenant App by default (only users from the tenant where the app was created can log in). If you operate the MCP server for multiple Entra tenants, switch the App's Supported account types in the Azure Portal to „Accounts in any organizational directory (Multitenant)" and set tenant_id = "common" in the settings — the rest of the code already handles multi-tenant tokens.
# Pull the latest version and restart
docker compose pull
docker compose up -d
# Or pin to a specific tag in .env:
# PRINTIX_TAG=7.0.0All available tags: https://github.com/mnimtz/printix-mcp-docker/pkgs/container/printix-mcp-docker
Updates are safe — all persistent data in the /data volume survives the container swap. DB migrations run automatically on startup.
claude.ai, ChatGPT and the Claude Code CLI all require an HTTPS MCP endpoint. Plain http://my-vm:8765 does not work for connector setup. The container ships three independent paths to HTTPS, all manageable from /admin — pick whichever fits your situation.
Best when you have any domain (or can bring a subdomain) under Cloudflare. No inbound ports needed, automatic SSL, DDoS/bot protection, free tier.
- Quick Tunnel — anonymous
*.trycloudflare.comURL, zero setup, zero config. Perfect for 30-second tests, but the URL changes on every container restart, so it's not suitable as a permanent claude.ai connector. - Named Tunnel — paste a Cloudflare tunnel token + your subdomain. Persistent URL, auto-renews TLS at the Cloudflare edge. Recommended production setup.
The admin UI under /admin/tunnel provides a 6-step setup wizard with deep links to the Cloudflare dashboard. See the Cloudflare guide for context on free-tier limits.
For users with a fixed public IP but no domain — typical Azure VM / Hetzner Cloud / on-prem scenario.
- One click on
/admin/auto-tls→ public IP detected → sslip.io hostname generated (e.g.52-143-121-45.sslip.io) → Let's Encrypt cert acquired → auto-renewal scheduled - Requires inbound port 80 for the ACME HTTP-01 challenge (~30 s every 60 days). The bundled
docker-compose.ymlexposes it by default.
If your firewall does not allow inbound port 80, use Option 1 (Cloudflare Tunnel) instead — that uses only outbound connections.
For users with an existing TLS certificate from a commercial CA, internal PKI, certbot run elsewhere, or any other source.
- Paste PEM cert + private key on
/admin/tls - Cert + key validated server-side, stored under
/data/tls/, uvicorn picks them up at next start - Certificate details (subject, issuer, validity, SAN) displayed live in the UI; expiry warning at 30 days remaining
- No auto-renewal — manual replacement required when the cert nears expiry
Use this when you have a wildcard cert, regulated-industry constraints prohibiting third-party routing, or want to integrate with existing certbot/cron setups.
| Option | Domain required | Inbound ports | Auto-renewal | DDoS protection |
|---|---|---|---|---|
| 🌐 Cloudflare Tunnel | yes (free if existing) | none | yes (Cloudflare) | yes |
| 🌍 Auto-HTTPS / sslip.io | no | 80 | yes (Let's Encrypt) | no |
| 🔒 BYO Certificate | yes | 8080 (or 443) | no | no |
MCP_PUBLIC_URL is updated automatically when you activate any of the three options — Connect-Center and OAuth flows immediately pick up the new URL without manual config.
The MCP server can enforce scope-based permissions on every tool call. Five built-in roles map to GDPR articles:
| Role | GDPR reference | Permitted scopes |
|---|---|---|
end_user |
Art. 15-22 (data subject rights) | mcp:self |
helpdesk |
Art. 32 (separation of duties) | mcp:self, mcp:read |
admin |
Art. 24 (controller obligations) | all five scopes |
auditor (DPO) |
Art. 37-39 | mcp:read, mcp:audit |
service_account |
Art. 28 + 32 | empty (whitelisted per token) |
- Per-Printix-group — assign an MCP role to any Printix group; all members inherit (highest role wins on multi-group membership)
- Per-user override — explicit role for individual users; takes precedence over group resolution
Assignments are managed under /admin/mcp-permissions (live status banner, group dropdowns, user override list, orphan cleanup, downloadable GDPR Compliance Guide and Permission Matrix as PDF).
RBAC enforcement is opt-in. Three ways to toggle:
- UI toggle (easiest, v7.2.38+) — green "Enable" / red "Disable" button right in the status banner on
/admin/mcp-permissions. Wirkt immediately, no container restart. - Environment variable — set
MCP_RBAC_ENABLED=1indocker-compose.yml. Becomes the default on first boot, then DB setting takes over once toggled in the UI. - DB setting —
rbac_enabledin the settings table (set programmatically). Same as the UI toggle.
When inactive, all authenticated users can call every tool (PR-1 compatibility mode). When active, denied calls return a structured permission_denied payload and are recorded in the audit log with action='mcp_permission_denied' for compliance review.
Each container ships two ready-to-share PDFs:
- GDPR Compliance Guide (
/manuals/gdpr-compliance.pdf) — article-by-article coverage map for procurement, DPO, or external auditor review - Permission Matrix (
/manuals/permission-matrix.pdf) — auto-generated list of all 129 tools grouped by required scope
Both are linked directly from /admin/mcp-permissions.
The container runs two HTTP listeners that work as a pair:
| Port | What lives there |
|---|---|
| 8080 (web) | Admin UI, OAuth-flow login pages, employee portal /my, capture/guestprint UIs, /health, /status, /admin/*, the MCP proxy for /mcp, /sse, /oauth, /.well-known |
| 8765 (mcp) | The actual FastMCP server — Streamable HTTP transport, SSE transport, OAuth issuer, .well-known/oauth-authorization-server. Plain HTTP only. |
Since v7.2.43, the web port (8080) proxies the four MCP path families internally to port 8765. This means a single public URL on port 8080 serves both admin UI and AI-assistant traffic — perfect for setups that can only expose one port (Cloudflare Quick Tunnel, single-NAT-rule deployments, simple reverse-proxy configurations).
| Setup | Path | Proxy active? |
|---|---|---|
| 🌐 Cloudflare Quick Tunnel (single URL → 8080) | Tunnel → 8080 → proxy → 8765 |
yes |
| 🌐 Cloudflare Named Tunnel with path routing | Tunnel → 8765 directly for /mcp//sse//oauth |
no |
| 🌍 Auto-HTTPS (sslip.io) on port 8080 | Internet → 8080 → proxy → 8765 |
yes |
| 🔒 Manual TLS-Import on port 8080 | Internet → 8080 → proxy → 8765 |
yes |
| Reverse proxy (Traefik / nginx / Caddy) with path rules | RP → 8765 directly for MCP paths |
no |
| Direct 8765 access (port-forwarded + own TLS) | 8765 directly |
no |
Direct access to port 8765 is never blocked by the proxy — it remains a fully functional first-class endpoint for setups that prefer to bypass the extra hop.
| Path | Latency per call | Suitable for |
|---|---|---|
| Direct 8765 | ~0.5 ms loopback | high-throughput production, latency-critical workflows |
| Via 8080 proxy | ~1–2 ms (httpx async stream) | typical SMB usage, single-URL deployments |
For the typical MCP tool call (50–500 ms server-side) the proxy overhead is below 1 % and not measurable. The trade-off only matters for very high call rates or real-time streaming pipelines.
Recipe 1: Cloudflare Named Tunnel with path routing (cleanest production setup)
In the Cloudflare Zero Trust dashboard → Tunnels → Public Hostnames, configure five entries on the same hostname:
| Path | Service |
|---|---|
/mcp and /mcp/* |
http://localhost:8765 |
/sse and /sse/* |
http://localhost:8765 |
/oauth/* |
http://localhost:8765 |
/.well-known/* |
http://localhost:8765 |
| (catch-all) | http://localhost:8080 |
→ MCP traffic lands on 8765 directly, web UI on 8080. One public URL, zero proxy overhead, full DDoS protection from Cloudflare.
Recipe 2: Traefik / nginx with path rules
labels:
- "traefik.enable=true"
- "traefik.http.routers.printix-mcp.rule=Host(`mcp.example.com`) && PathPrefix(`/mcp`,`/sse`,`/oauth`,`/.well-known`)"
- "traefik.http.routers.printix-mcp.service=printix-mcp"
- "traefik.http.routers.printix-mcp.entrypoints=websecure"
- "traefik.http.routers.printix-mcp.tls.certresolver=le"
- "traefik.http.services.printix-mcp.loadbalancer.server.port=8765"
- "traefik.http.routers.printix-web.rule=Host(`mcp.example.com`)"
- "traefik.http.routers.printix-web.service=printix-web"
- "traefik.http.routers.printix-web.entrypoints=websecure"
- "traefik.http.routers.printix-web.tls.certresolver=le"
- "traefik.http.services.printix-web.loadbalancer.server.port=8080"The PathPrefix rule is more specific than the host-only rule, so Traefik routes MCP paths to 8765 first, web UI to 8080 as the fallback.
Recipe 3: Direct port 8765 with external TLS termination
Put a TLS terminator (Caddy, nginx, stunnel, or a cloud load balancer) in front of port 8765. The MCP server speaks plain HTTP — the terminator handles HTTPS. Consumers point at the terminator, no proxy hop.
For all bypass recipes: set MCP_PUBLIC_URL in .env to the public URL — otherwise OAuth redirects and Connect-Center URLs will not match what's actually reachable.
If you prefer a manual reverse-proxy setup (Traefik, nginx, Caddy, HAProxy, …) instead of the built-in HTTPS options, the container listens on plain HTTP — the proxy terminates TLS and forwards. The simplest configuration uses a single backend on port 8080 (proxy stays in the loop):
services:
printix-mcp:
# ...
labels:
- "traefik.enable=true"
- "traefik.http.routers.printix.rule=Host(`mcp.example.com`)"
- "traefik.http.routers.printix.entrypoints=websecure"
- "traefik.http.routers.printix.tls.certresolver=le"
- "traefik.http.services.printix.loadbalancer.server.port=8080"For higher-throughput production deployments use the path-based routing recipes above instead.
After the first-time setup in the web UI:
| Client | URL | Transport | Auth |
|---|---|---|---|
| claude.ai (Settings → Integrations → Add MCP Server) | <MCP_PUBLIC_URL>/mcp |
Streamable HTTP | OAuth (auto) |
| Claude Desktop / Claude Code | <MCP_PUBLIC_URL>/mcp |
Streamable HTTP | OAuth (auto) |
| Claude Code (CLI) | claude mcp add printix <MCP_PUBLIC_URL>/mcp |
Streamable HTTP | OAuth (auto) |
| ChatGPT (Custom GPT → MCP) | <MCP_PUBLIC_URL>/sse |
SSE | OAuth (auto) |
| Make.com / n8n / Zapier / custom scripts | <MCP_PUBLIC_URL>/mcp |
Streamable HTTP | Bearer Token (paste from Connect-Center) |
⚠️ Don't swap/mcp↔/sse. Claude.ai rejects/sse(different transport), ChatGPT rejects/mcp. The single most common „it doesn't connect" cause. The Connect-Center shows the right URL per client + a 🩺 self-test button (v7.7.5+).
Authentication modes:
- OAuth flow (Claude.ai, ChatGPT, Claude Code) — handled automatically by the client.
/oauth/authorizeand/oauth/tokenare used end-to-end; no manual token management. - Static Bearer Token (Make.com, n8n, Zapier, custom Python/Node scripts) — paste
Authorization: Bearer <token>per request. Token visible in/my/connectwith a reveal toggle. RBAC scopes apply same as for OAuth callers.
⚠️ After every server upgrade: refresh the AI assistant's tool list, otherwise it keeps using stale tool definitions. claude.ai: start a new conversation or Settings → Connectors → disconnect / reconnect. ChatGPT custom connector: Disconnect / Connect in the Custom GPT editor. Claude Desktop: full app restart (Cmd+Q). Cursor / Continue: toggle the connector or use/mcp reload. Seedocs/MCP_MANUAL_EN.mdfor details + the full 129-tool reference.
🩺 Connection won't establish? Open
/my/connectand click 🩺 Test connection. It runs 7 server-side checks (public_url set, HTTPS, OAuth discovery, /mcp + /sse reachable, etc.) and tells you exactly what's wrong — typically a missingMCP_PUBLIC_URLenv (v7.7.5 reads it from the Web-UI DB setting as a fallback, so existing setups that only configured it in the admin form now work too).
# Follow the logs
docker compose logs -f printix-mcp
# Container status + health
docker compose ps
# Shell into the container
docker compose exec printix-mcp bash
# Poke at the SQLite DB directly
docker compose exec printix-mcp sqlite3 /data/printix_multi.db '.tables'
# Reset the container completely (⚠️ ALL data is gone)
docker compose down -v"Permission denied" on bind mount: see Bind mount instead of named volume — ownership must be 1000:1000.
Web UI returns 502 / timeout behind Cloudflare: MCP_PUBLIC_URL must be set so internal redirects use the correct scheme/host.
Container restarted but login fails: check that /data/fernet.key and /data/web_session_key are still there — if the volume was accidentally emptied, they get regenerated and all previously-encrypted secrets become unreadable.
# Clone + build
git clone https://github.com/mnimtz/printix-mcp-docker.git
cd printix-mcp-docker
# In docker-compose.yml swap image: for build: ., then:
docker compose up --build
# Or directly:
docker build -t printix-mcp-docker:dev .
docker run --rm -p 8080:8080 -p 8765:8765 -v printix-data:/data printix-mcp-docker:devMulti-arch builds (amd64 + arm64) run in CI via GitHub Actions — see .github/workflows/docker-publish.yml. armv7 / i386 are no longer built (no pre-built wheels for Python 3.13 on 32-bit ARM) — can be re-enabled in the workflow if anyone needs it.
Licensed under the Apache License 2.0 — Copyright © 2026 Marcus Nimtz.
Fork of the HA-add-on code base (printix-mcp-addon), stripped of its HA scaffolding and repackaged as a standalone Docker distribution. Both projects keep evolving in parallel — changes in the HA add-on core are ported over when it makes sense.
Maintainer: Marcus Nimtz · marcus@nimtz.email