Skip to content

mnimtz/printix-mcp-docker

Repository files navigation

Printix MCP Server — Docker

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


Feature overview

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.

🟢 Basic — included for everyone

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.md for 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-permissions shows 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 to 1 in the bundled docker-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/tunnel provides a built-in tunnel manager — no external setup, no port-forwarding, no certbot
  • Quick Tunnel mode — anonymous *.trycloudflare.com URL 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_url setting so Connect-Center, OAuth flows, and MCP endpoints all just work
  • Bundled cloudflared binary; 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 override
  • printix_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 via printix_offboard_user / printix_delete_user within 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 /mcp and /sse is 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-cookbook with 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 /reports via 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_impact MCP 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/users shows 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> mixed
  • printix_send_to_capture — push files straight into the capture pipeline (e.g. Paperless-ngx) without the printer detour
  • printix_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 hours
  • printix_card_enrol_assist — register an NFC card UID with auto-transform via the user's profile
  • printix_describe_user_print_pattern, printix_quota_guard, printix_print_history_natural, printix_resolve_recipients and 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 /data volume

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 /reports when 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/ and letsencrypt/ 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

💎 Pro — activation code required

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.

Quick install

# 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:8080

That's it. In the web UI you register the first admin user and store your Printix API credentials.


Deployment via Portainer

Portainer offers three ways to create a stack — each one works with this project.

Option A — Repository (recommended, automatic updates)

Stacks → Add stackRepository

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.

Option B — Web editor (fastest, no Git needed)

Stacks → Add stackWeb 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: local

In 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 to true if capture webhooks should arrive from outside)
  • IPP_PORT=0 (set to 631 to enable the cloud-print gateway)

Then Deploy the stack — done.

Option C — Upload

Stacks → Add stackUpload → upload the docker-compose.yml from this repo, set env vars as above, deploy.

After 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).


What's running inside

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.


Persistence & data

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)

⚠️ Backup recommendation: backing up the whole /data volume is enough — everything sensitive is encrypted with the Fernet key (which also lives there).

Bind mount instead of named volume

If you want the data visible on the host, change the volume in docker-compose.yml:

volumes:
  - ./data:/data          # instead of: printix-data:/data

and 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 ./data

The container runs as the non-root user printix (UID 1000, GID 1000).


Configuration

Two places, two responsibilities (since v7.0.0):

  1. Ports → only in docker-compose.yml under ports:. To move to a different host port, change only the left-hand number:
    ports:
      - "9000:8080"   # web UI now on host port 9000
  2. 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.


Microsoft Entra ID / SSO

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+).

Step 1 — Auto-create the Entra App Registration

  1. Open the web UI → log in as admin → SettingsMicrosoft Entra ID.
  2. Click „Auto-Setup". The page shows a one-time device code.
  3. Open https://microsoft.com/devicelogin on 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.
  4. 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_secret in 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_secret triple manually — the same App Registration can serve multiple MCP server instances.

Step 2 — Add a redirect URI for the iOS app (only if you use the mobile app)

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:

  1. Open https://portal.azure.comMicrosoft Entra IDApp registrations → tab All applications → search for the Client-ID shown in the MCP web UI (or the name „Printix Management Console").

  2. Open the app → Authentication → click + Add a platform → choose Mobile and desktop applications.

  3. In Custom redirect URIs enter exactly:

    mysecureprint://oauth/callback
    

    No https://, no trailing slash, no spaces.

  4. ConfigureSave. 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/...

Step 3 — Verify

# 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}

Common errors

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

Single tenant vs. multi-tenant App

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.


Updates

# 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.0

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


HTTPS — three built-in options (v7.2.32+)

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.

Option 1 — 🌐 Cloudflare Tunnel (recommended for most users)

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.com URL, 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.

Option 2 — 🌍 Auto-HTTPS via sslip.io + Let's Encrypt (no domain at all)

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.yml exposes it by default.

If your firewall does not allow inbound port 80, use Option 1 (Cloudflare Tunnel) instead — that uses only outbound connections.

Option 3 — 🔒 Bring your own certificate

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.

Comparison table

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.


Role-based access control (v7.2.23+)

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)

Two assignment paths

  • 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).

Activation

RBAC enforcement is opt-in. Three ways to toggle:

  1. 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.
  2. Environment variable — set MCP_RBAC_ENABLED=1 in docker-compose.yml. Becomes the default on first boot, then DB setting takes over once toggled in the UI.
  3. DB settingrbac_enabled in 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.

Bundled compliance documentation

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.


Network architecture (v7.2.43+)

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

When the proxy is used vs bypassed

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.

Performance trade-off

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.

Bypassing the proxy — three recipes

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.


Reverse proxy — manual setups (without built-in HTTPS)

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.


AI-assistant integration

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/authorize and /oauth/token are 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/connect with 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. See docs/MCP_MANUAL_EN.md for details + the full 129-tool reference.

🩺 Connection won't establish? Open /my/connect and 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 missing MCP_PUBLIC_URL env (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).


Troubleshooting

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


Building locally (developers)

# 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:dev

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


License & origin

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

About

Printix MCP & Management Server

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages