OAuth-gated remote MCP bridge that exposes your self-hosted Zammad helpdesk (v6.x / v7.x) to AI clients — Claude Web Connectors, Claude Desktop, Microsoft 365 Copilot, ChatGPT Connectors, Cursor, Continue.
| Feature | What it means |
|---|---|
| User-context | The user's Zammad access token is forwarded end-to-end. Zammad sees every API call as coming from the actual person, not a shared service account — its own role and permission system enforces fine-grained access. |
| Zammad as OAuth IdP | Primary auth mode uses Zammad's built-in OAuth2 Applications feature (Admin → Manage → OAuth2 Applications → Add). No external IdP required. |
| External OIDC also supported | Falls back to any standard OIDC provider — Entra ID, Keycloak, Authentik, Zitadel, Auth0, Okta — paired with a static Zammad API token. |
| Role-based MCP access | MCP_ALLOWED_ROLES=Admin,Agent lets you say "this MCP is for staff only, never expose to Customers" as a coarse gate above Zammad's per-endpoint permissions. |
| v6 / v7 compatible | The shared /api/v1 surface is exercised against both majors. Runtime version probe surfaces the live release in the boot banner. |
| 36 hand-curated tools | Tickets, articles, users, organizations, groups, tags, reference data, notifications. Auto-run safety encoded as MCP tool annotations. |
| Multi-arch | linux/amd64 and linux/arm64 images on GHCR. |
| Three deploy flavours | Local development, self-hosted Traefik, Coolify-managed — same image, same source. |
| Test-gated builds | The Docker multi-stage build fails if pytest fails — no green push when tests are red. |
| Restart-safe sessions | Encrypted OAuth state store: Redis-backed (production) or disk-backed (single-node fallback), both Fernet-encrypted at rest. |
| Token-bucket rate limiting | Per-OAuth-subject (or proxy-aware client-IP) limiter on every MCP request. |
AI Clients
┌────────────┬───────────────┬────────────┐
│ Claude Web │ Claude Desktop│ MS Copilot │ ...
└─────┬──────┴───────┬───────┴──────┬─────┘
│ │ │
└──────────────┼──────────────┘
│ HTTPS (OAuth 2.1 + PKCE,
│ Streamable HTTP /mcp)
▼
┌───────────────────────┐
│ Traefik / Coolify │
└──────────┬────────────┘
│
▼
┌──────────────────────────────────────────────────────────┐
│ bg-zammad-mcp container (FastMCP, port 8000) │
│ ┌────────────────────────────────────────────────────┐ │
│ │ Inbound: Zammad OAuth | external OIDC | none │ │
│ └────────────────────────────────────────────────────┘ │
│ ┌────────────────────────────────────────────────────┐ │
│ │ Role allowlist middleware (Admin/Agent/Customer) │ │
│ └────────────────────────────────────────────────────┘ │
│ ┌────────────────────────────────────────────────────┐ │
│ │ 36 hand-curated MCP tools │ │
│ │ list_tickets, search_tickets, create_ticket, │ │
│ │ get_me, list_users, list_groups, … │ │
│ └────────────────────────────────────────────────────┘ │
│ ┌────────────────────────────────────────────────────┐ │
│ │ Outbound httpx client │ │
│ │ Bearer <upstream user token> (zammad mode) │ │
│ │ Token token=<api token> (oidc / none) │ │
│ └─────────────────────────┬──────────────────────────┘ │
└────────────────────────────┼─────────────────────────────┘
│ HTTPS
▼
┌────────────────────────┐
│ Zammad REST API │
│ /api/v1/* (v6 / v7) │
└────────────────────────┘
In AUTH_MODE=zammad, the OAuth token authenticates the inbound MCP
call AND the outbound Zammad call. Everything happens in the
authenticated user's context. In OIDC / none modes, the inbound and
outbound trust boundaries are separate (external IdP inbound + static
API token outbound) — Zammad sees one shared identity for every user.
python scripts/generate-env.pyThen edit .env to fill:
ZAMMAD_URL(your Zammad base URL)AUTH_MODE=zammad(the recommended default)ZAMMAD_OAUTH_CLIENT_ID+ZAMMAD_OAUTH_CLIENT_SECRET— generated in Zammad: Admin → Manage → OAuth2 Applications → Add.PUBLIC_BASE_URLmatching the hostname you'll expose.MCP_ALLOWED_ROLES=Admin,Agent(or whatever role mix fits your case).
# Local development (LOG_FORMAT=console, AUTH_MODE=none allowed for testing)
docker compose -f docker-compose.development.yml up -d
# Self-hosted Traefik (HTTPS, Let's Encrypt)
docker compose -f docker-compose.traefik.yml up -d
# Coolify — paste env into the dashboard, deploy from this compose file
docker compose -f docker-compose.coolify.yml up -dSee docs/client-setup.md. Short version:
| Client | URL |
|---|---|
| Claude Web | Settings → Connectors → Add custom → https://your-host/mcp |
| Claude Desktop | Add to mcp.json: {"command": "npx", "args": ["mcp-remote", "https://your-host/mcp"]} |
| Microsoft 365 Copilot Studio | Custom connector → MCP → https://your-host/mcp |
| Cursor / Continue | Add to mcp.json with the same URL |
This is the primary auth mode (AUTH_MODE=zammad) and uses Zammad's
built-in OAuth2 Applications feature shipped in Zammad v6.0 and later.
-
Sign in to Zammad as an administrator.
-
Open Admin → Manage → OAuth2 Applications.
-
Click + Add and fill in:
Field Value Name BAUER GROUP MCP(any human-friendly label)Redirect URI ${PUBLIC_BASE_URL}/auth/callback(must match exactly)Scopes read writeConfidential Yes -
Save and copy the generated Client ID + Client Secret into
.env(ZAMMAD_OAUTH_CLIENT_ID/ZAMMAD_OAUTH_CLIENT_SECRET). The secret cannot be retrieved later — save it immediately.
Full walkthrough with the user-context flow diagram: docs/authentication.md.
MCP_ALLOWED_ROLES is a coarse gate that controls who can use the MCP
server at all, on top of Zammad's own per-endpoint permission system.
# Internal MCP for agents and admins (the default)
MCP_ALLOWED_ROLES=Admin,Agent
# Admins only - automation-style deployments
MCP_ALLOWED_ROLES=Admin
# Customer-facing self-service MCP
MCP_ALLOWED_ROLES=CustomerCustom Zammad role names work — pass whatever appears in Zammad's role list. Casing and whitespace are normalised. Full details: docs/role-based-access.md.
IP-ZAMMAD-MCPServer/
├── README.md ← you are here
├── LICENSE ← MIT
├── CHANGELOG.md
├── .env.example ← canonical config surface
├── docker-compose.development.yml ← local dev
├── docker-compose.traefik.yml ← self-hosted production
├── docker-compose.coolify.yml ← Coolify deployment
│
├── scripts/
│ ├── generate-env.py ← cross-platform .env secret generator
│ └── dev-inspector.py ← one-command MCP Inspector launcher
│
├── app/
│ └── bg-zammad-mcp/ ← the only image this repo builds
│ ├── Dockerfile ← multi-stage, test-gated
│ ├── pyproject.toml
│ ├── README.md ← internal architecture
│ ├── src/ ← only the Zammad-specific code lives here;
│ │ │ all cross-cutting infra is in bg-mcpcore
│ │ ├── profiles/zammad.json ← declarative profile (backend, auth, tools)
│ │ ├── main.py ← entrypoint: make_cli(load_profile(...), Settings)
│ │ ├── config.py ← Settings(bg_mcpcore.BaseMcpSettings) + Zammad fields
│ │ ├── server.py ← the one Zammad seam: register() (decode shim → tool modules)
│ │ ├── auth/
│ │ │ └── zammad_oauth.py ← Zammad as OAuth2 IdP (auth_providers entry point)
│ │ ├── zammad/
│ │ │ ├── errors.py ← typed exception hierarchy
│ │ │ └── tools/ ← hand-written tool surface (36 tools)
│ │ │ ├── tickets.py · articles.py · users.py · organizations.py
│ │ │ └── groups.py · tags.py · reference.py · notifications.py
│ │ └── static/
│ │ ├── index.html ← landing page served at /
│ │ └── logo.svg ← consent-screen brand asset
│ └── tests/ ← pytest (gates the Docker build)
│
│ The role gate (now the profile's `access_control` block) and the per-user
│ on-behalf-of outbound auth (now the `per_user_token` resolver) are
│ declarative in bg-mcpcore — no server code.
│
├── docs/
│ ├── ZAMMAD-MCP-SPEC.md ← design specification
│ ├── installation.md
│ ├── authentication.md
│ ├── role-based-access.md
│ ├── client-setup.md
│ ├── testing.md
│ └── troubleshooting.md
│
└── .github/
├── CODEOWNERS
├── dependabot.yml
├── config/
│ ├── docker-base-image-monitor/base-images.json
│ └── release/semantic-release.json
└── workflows/
├── docker-release.yml ← semantic-release + build/push
├── docker-maintenance.yml ← auto-merge Dependabot
├── check-base-images.yml ← daily base-image scan
├── ai-issue-summary.yml
└── teams-notifications.yml
| Component | Tested | Notes |
|---|---|---|
| Python | 3.13 / 3.14 | Container ships 3.14 Alpine |
| Zammad | v6.0+ / v7.x | OAuth2 Applications feature requires v6.0; v5.x and earlier can use AUTH_MODE=oidc + ZAMMAD_API_TOKEN |
| FastMCP | ≥ 3.0 | OIDCProxy, OAuthProxy, streamable-http transport |
| Redis | 7.x / 8.x | OAuth client storage (recommended for production) |
- docs/installation.md — step-by-step deploy for each flavour
- docs/authentication.md — Zammad OAuth2 setup + external OIDC walkthroughs
- docs/role-based-access.md —
MCP_ALLOWED_ROLESmodel - docs/client-setup.md — adding the connector in each AI client
- docs/testing.md — local & remote testing with MCP Inspector
- docs/troubleshooting.md — common errors & fixes
- docs/ZAMMAD-MCP-SPEC.md — design specification (source of truth)
- zammad/zammad — upstream Zammad helpdesk.
- jlowin/fastmcp — the MCP framework
underneath.
OAuthProxy,OIDCProxy, and the streamable-HTTP transport all live here. - bauer-group/Shlink-MCPServer — sister project: same architectural pattern applied to self-hosted Shlink (URL shortener).
MIT — see LICENSE.
Maintained by BAUER GROUP · Today, Tomorrow, Together