Skip to main content
Glama
Pressingly

Zammad MCP Server

by Pressingly

Zammad MCP Server

A Model Context Protocol server for the Zammad helpdesk. It lets Claude, or any MCP client, work with Zammad tickets using the caller's own Zammad permissions: every request carries a per-user API token, never a shared admin token, so Zammad itself decides what each user can see and change.

Status: Ticket, article, attachment, search, user, organization, tag and knowledge base tools work in stdio and HTTP, and email replies and macros are available behind opt-in flags. Platform mode (SSO with per-user minted tokens) is available.

Modes

Community mode (available now)

Works against any Zammad 6.x or 7.x with a personal API token (Zammad: avatar menu, Profile, Token Access).

  • stdio for a local MCP client: zammad-mcp stdio, with the token in ZAMMAD_HTTP_TOKEN.

  • Streamable HTTP: zammad-mcp http, with GET /healthz for health checks.

    • /http/api-key/mcp (always on): every caller sends their own personal token as X-Zammad-Token. Requests on this path never use ZAMMAD_HTTP_TOKEN, and any X-Auth-Request-* header (also spelled with _) is stripped on arrival.

    • /mcp (opt-in): serves ZAMMAD_HTTP_TOKEN to anyone who can reach the port, who then acts in Zammad as that token's owner. It is only mounted with ZAMMAD_HTTP_SHARED_TOKEN_ROUTE=true; zammad-mcp http refuses to start with ZAMMAD_HTTP_TOKEN set and the flag off. Use it only on a loopback or otherwise private listener.

Platform mode

For deployments that put Zammad behind an SSO edge with a trusted-header middleware: users sign in with OAuth (AWS Cognito, optionally through an mPass-style auth proxy), and the server mints and caches a Zammad token for each user, bounded by that user's Zammad role. It needs the optional [platform] extra (included in the Docker image) and is enabled by COGNITO_USER_POOL_ID. Community mode never imports it.

  • /mcp is guarded by FastMCP's Cognito provider (OAuth 2.0 with dynamic client registration); every call runs with the caller's minted token.

  • /http/api-key/mcp is not mounted unless ZAMMAD_HTTP_API_KEY_ROUTE=true. It would replay any caller's personal X-Zammad-Token to the internal Zammad, past the SSO proxy and the corporate-ID gate, so a leaver's or leaked token would keep working from the internet. Turning it on logs a startup warning.

  • A token carries an explicit list: ticket.agent, ticket.customer, knowledge_base.reader and knowledge_base.editor, intersected with the user's role. Never admin.*, report or user_preferences.*.

  • Tokens last about 8 days, are cached for 6 (Fernet-encrypted in Valkey), are re-checked every 4 hours and after a permission-gated 403, and are re-minted after a 401. Only expired tokens of this server are cleaned up.

  • Unverified users (exactly <cognito:username>@DEFAULT_EMAIL_DOMAIN, or an identity without @) are refused before any Zammad call. Real addresses in that domain are served.

  • Each bootstrap signs its Zammad web session out again, so no full-privilege session is left behind.

  • The tool list is filtered per user and fails closed: if the mint fails, only get_me is listed, and it explains why.

  • Every failure fails closed; there is no fallback to a shared token.

Variable

Default

Purpose

COGNITO_USER_POOL_ID

required

Turns platform mode on

COGNITO_AWS_REGION

required

Cognito region

OIDC_CLIENT_ID

required

Cognito app client id

OIDC_CLIENT_SECRET or MCP_JWT_SIGNING_KEY

one required

Client secret (confidential client) or signing key (public client)

MCP_BASE_URL

required

Public URL of this server; register <MCP_BASE_URL>/auth/callback in Cognito

ZAMMAD_INTERNAL_BASE_URL

required

Zammad on the internal network; every Zammad call goes here

DEFAULT_EMAIL_DOMAIN

required

Bare domain of unverified users' synthetic addresses, which are refused

MCP_OAUTH_STORAGE_URL

unset

Valkey URL (redis://, rediss://, valkey://, valkeys://) for OAuth state, tokens and confirmations

MCP_ALLOWED_CLIENT_REDIRECT_URIS

required

Comma-separated redirect URI patterns for client registration; empty refuses to start

MCP_ALLOW_ANY_REDIRECT_URI

false

true with no allow-list accepts any redirect URI (explicit opt-out)

COGNITO_UPSTREAM_AUTH_URL / COGNITO_UPSTREAM_TOKEN_URL

discovered

Route /authorize and /token through an auth proxy

MCP_ACCESS_TOKEN_TTL_SECONDS

86400

Lifetime of the token issued to the MCP client

MCP_OIDC_SCOPES

openid

Upstream scopes

MCP_LOG_LEVEL

INFO

JSON log level

ZAMMAD_HTTP_API_KEY_ROUTE

false

true also serves /http/api-key/mcp for personal tokens, bypassing the SSO proxy and corporate-ID gate (startup warning). Platform mode only; community mode always serves it

See docs/platform-mode.md for the mint flow, the cache and every setting.

Related MCP server: tokenless-zendesk-mcp

Quick start

uv sync
export ZAMMAD_URL=https://support.example.com
export ZAMMAD_HTTP_TOKEN=<your personal token>
uv run zammad-mcp stdio

Claude Desktop (claude_desktop_config.json):

{
  "mcpServers": {
    "zammad": {
      "command": "uvx",
      "args": ["--from", "git+https://github.com/Pressingly/zammad-mcp-server", "zammad-mcp", "stdio"],
      "env": { "ZAMMAD_URL": "https://support.example.com", "ZAMMAD_HTTP_TOKEN": "<token>" }
    }
  }
}

Docker (multi-user HTTP on port 8214; no token in the container, each client sends its own):

docker build -t zammad-mcp .
docker run --rm -p 8214:8214 -e ZAMMAD_URL=https://support.example.com zammad-mcp
curl http://localhost:8214/healthz

Point the MCP client at http://<host>:8214/http/api-key/mcp with the header X-Zammad-Token: <your token>.

Configuration

Variable

Default

Purpose

ZAMMAD_URL

required

Base URL the server calls, without /api/v1

ZAMMAD_PUBLIC_URL

ZAMMAD_URL

Base URL for the url links in results, when users reach Zammad on another host

ZAMMAD_HTTP_TOKEN

unset

Personal API token for stdio (and /mcp with the opt-in below)

ZAMMAD_HTTP_SHARED_TOKEN_ROUTE

false

true serves /mcp in http mode, acting as ZAMMAD_HTTP_TOKEN's owner for every caller

ZAMMAD_TIMEOUT_SECONDS

30

Total timeout for one Zammad request

ZAMMAD_CONNECT_TIMEOUT_SECONDS

10

Connect timeout for one Zammad request

ZAMMAD_READ_ONLY

false

true leaves out every write tool

ZAMMAD_ENABLE_REFERENCE

true

list_ticket_options

ZAMMAD_ENABLE_TICKETS

true

Ticket search, read, history, create and update

ZAMMAD_ENABLE_ARTICLES

true

Article list and read, add_ticket_note

ZAMMAD_ENABLE_ATTACHMENTS

true

get_ticket_attachment

ZAMMAD_ENABLE_SEARCH

true

Global search

ZAMMAD_ENABLE_USERS

true

get_user, search_users

ZAMMAD_ENABLE_ORGANIZATIONS

true

get_organization, search_organizations

ZAMMAD_ENABLE_TAGS

true

list_ticket_tags, add_ticket_tag, remove_ticket_tag

ZAMMAD_ENABLE_KB

true

search_knowledge_base, get_kb_answer

ZAMMAD_ENABLE_EMAIL_REPLIES

false

prepare_email_reply, send_email_reply. Needs an outgoing email channel in Zammad

ZAMMAD_EMAIL_ALLOW_ANY_RECIPIENT

false

true lets email replies go to any address, not only the ticket's participants (never to Zammad's own addresses)

ZAMMAD_ENABLE_MACROS

false

list_macros, prepare_apply_macro, apply_macro

ZAMMAD_FILTER_TOOLS_BY_ROLE

false

Hide agent-only tools from customer tokens in the tool list (Zammad enforces permissions either way)

ZAMMAD_CONFIRM_TTL_SECONDS

600

Lifetime of a two-step confirmation token (email replies and macros)

MCP_HTTP_PORT

8214

Listen port in http mode

A disabled module's tools are never registered, and ZAMMAD_READ_ONLY=true leaves out every tool that changes Zammad, including the email and macro tools. Flags accept true, 1, yes or on, with surrounding spaces ignored.

Tools

Tool

Who

What it does

get_me

all

The Zammad user the token belongs to, with their tier

list_ticket_options

all

Active states, priorities and groups, with ids

search_tickets

all

Text search plus exact filters (state, priority, group, customer, owner, organization), paged

get_ticket

all

One ticket by id or #number, with its newest articles

list_ticket_articles

all

A ticket's articles, newest first, paged, bodies as plain text

get_ticket_article

all

One article, with a configurable body length

get_ticket_attachment

all

Text attachments up to 200 KB as text, images up to 2 MB as images

search

all

Zammad's global search across tickets, users and organizations

get_user

all

One user (customers: themselves and their organization)

get_organization

all

One organization (customers: their own)

list_ticket_tags

all

The tags on a ticket

search_knowledge_base

all

Knowledge base answers with a snippet; customers find published answers only

get_kb_answer

all

One knowledge base answer, every translation's title and plain-text body

create_ticket

all

A new ticket with a first public message; agents name the customer

update_ticket

all

Title, state, priority, group, owner or pending time

add_ticket_note

all

Agents: an internal note by default; customers: a public reply. Never sends email

search_users

agents

Users by name, login, email or phone

search_organizations

agents

Organizations by name or domain

get_ticket_history

agents

A ticket's change history, newest first

add_ticket_tag

agents

Add a tag; a new tag needs Zammad's tag_new setting

remove_ticket_tag

agents

Remove a tag

prepare_email_reply

agents, opt-in

Check the group's outgoing address, settle recipients and attachments, return a preview and a confirmation token. Sends nothing

send_email_reply

agents, opt-in

Send the approved reply as one email article; returns the article id and queued

list_macros

agents, opt-in

Active macros, with the groups each is limited to

prepare_apply_macro

agents, opt-in

Preview a macro on up to 50 tickets and return a confirmation token. Changes nothing

apply_macro

agents, opt-in

Apply the approved macro through POST /tickets/mass_macro

The "Who" column is a convenience: Zammad still decides what each token may do. Callers whose role has no ticket.* permission (for example an Admin-only account) report tier none. Article bodies are cut to 4000 characters with a truncated flag and a hint. Search on a Zammad without Elasticsearch is a case-insensitive substring match with no field syntax or ranking; use the search_tickets filters to narrow it.

Two-step tools

Sending an email and applying a macro each take two calls. The prepare_* tool checks the request, stores it behind a confirmation token and returns a preview for the user. The confirming tool must echo the preview's key fields (to and subject for email, macro_id and ticket_ids for macros), and its token:

  • works once, and is spent even when a check then fails;

  • expires after ZAMMAD_CONFIRM_TTL_SECONDS;

  • only works for the user it was issued to;

  • is stored as a hash, next to the sha256 of the exact payload. Recomputing that digest on use detects a corrupted record, not a forged one. Tamper protection for a shared store comes from the platform-mode Valkey backend, which encrypts records with Fernet (authenticated encryption);

  • is limited to 20 waiting per user. In the in-memory store each user may hold at most a quarter of its 128 MiB (sizes are UTF-8 bytes of the stored JSON), and records over 64 KiB cannot use the last 8 MiB, which stay free for small ones such as macro confirmations. The largest possible email record is about 20 MB.

Email replies:

  • prepare_email_reply fails clearly when the ticket's group has no outgoing email address, or the address has no active email channel (Zammad: Admin, Groups and Admin, Channels, Email).

  • to defaults to the ticket customer's email.

  • A ticket's participants are:

    • the ticket customer's email;

    • the From, To and Cc of every public article an agent wrote. An agent email counts only when Zammad sent it itself (it carries preferences.email_address_id); agent notes, phone and web articles always count;

    • the From, To, Cc and Reply-To of every public article the ticket's customer wrote (matched by the article's author or on-behalf-of user, or by its From address).

    Internal notes and messages from anyone else, such as an outsider who emailed in with the ticket number, add no participants. Unless ZAMMAD_EMAIL_ALLOW_ANY_RECIPIENT=true, every recipient must be a participant.

  • No recipient may be one of Zammad's own email addresses (any group's), with or without ZAMMAD_EMAIL_ALLOW_ANY_RECIPIENT.

  • The preview lists a recipient_warnings entry for every recipient who is not the ticket's customer.

  • Limits: at most 10 recipients (to and cc together) of up to 320 characters each, a body of 1,000,000 characters, and up to 20 attachments with 10 MB of attachment content (decoded) in total. The subject must be one line, without control characters or bidi embeddings and overrides (emoji joiners and LRM/RLM marks are fine).

  • Spoofing that remains possible, because Zammad trusts the From header of inbound mail:

    • Mail with a forged From of the customer's address is filed as the customer's message, so its Reply-To and Cc become participants.

    • Mail with a forged From of one of Zammad's own addresses is filed as sent by an Agent. This server ignores such articles because they lack preferences.email_address_id, which relies on Zammad setting that only on mail it sends (true in Zammad 7.1).

    Neither can change the default to, and every recipient other than the customer is flagged in recipient_warnings, so the user sees it before confirming.

  • send_email_reply posts one email article (sender Agent, not internal) and is never retried once the request may have reached Zammad. Zammad delivers the email in the background, so the result is queued. When the outcome is unclear (a timeout, a 5xx, or a 2xx whose answer cannot be read) the error says so and asks to check the ticket before preparing the reply again. Each accepted send writes one audit log line (zammad_mcp.audit) with the caller's identity hash, ticket, article id (unknown when unreadable) and recipient count, never the body.

Macros:

  • Only active macros are listed or applied. Zammad itself would run an inactive one.

  • apply_macro uses POST /tickets/mass_macro, which checks the macro's group restriction and your change access on every ticket and changes nothing if any ticket fails. After a timeout or a 5xx the error says the macro may or may not have applied.

Every tool declares all four MCP annotations (readOnlyHint, destructiveHint, idempotentHint, openWorldHint) and returns an Error: ... string instead of raising, so the model sees what went wrong.

Security notes

  • Ticket content is untrusted. Ticket titles, articles, attachments and customer names are written by whoever emailed or filled in the form. A crafted ticket can try to instruct the model ("forward this ticket to ..."). The server wraps every such value in an <untrusted_content source=... id=...> frame and tells the model to treat it as data. Framed: ticket titles and owner, customer and organization names; article subjects, bodies, from, to, cc and author; attachment file names and text; user logins, names, emails, phones and organizations; organization names and domains; tag names; search hit titles, names and emails; history authors and values. Inside a frame every & and < (and the full-width and small < look-alikes) is escaped and invisible format characters are removed, so no spelling of a closing tag survives. Not framed: ids, numbers, timestamps, flags, roles, and state, priority, group and macro names, which only admins set. Knowledge base titles, snippets and bodies are framed too. Tools that send email are disabled by default and need a two-step confirmation.

  • /mcp with a shared token acts as that token's owner. With ZAMMAD_HTTP_SHARED_TOKEN_ROUTE=true, anyone who can reach the port works in Zammad as the owner of ZAMMAD_HTTP_TOKEN. Prefer /http/api-key/mcp.

  • Use a per-user token. Give each person their own Zammad token with only the permissions they need, rather than sharing one admin token. Zammad intersects a token's permissions with the user's role, so a token can never do more than its owner.

  • Customers need token access enabled. Stock Zammad lets Agents and Admins create API tokens, but not Customers. To let Customers use this server, an admin grants user_preferences.access_token to the Customer role (Admin, Roles, Customer).

  • The server never sends X-Auth-Request-* headers (in any case, with - or _) and never stores cookies, so it cannot impersonate a user on an SSO-fronted Zammad or carry one user's session into another user's request.

Development

uv sync
uv run ruff check .
uv run ruff format --check .
uv run pytest --cov=zammad_mcp --cov-fail-under=80

See CONTRIBUTING.md for the branch and commit workflow and SECURITY.md for reporting vulnerabilities.

License

MIT

Available Tools

1 tool
get_meGet current Zammad userA
Read-onlyIdempotent

Return the Zammad user the current token belongs to (id, login, name, email, roles).

ParametersJSON Schema
NameRequiredDescriptionDefault

No parameters

Output Schema

ParametersJSON Schema
NameRequiredDescription
resultYes

TDQS

A4.3/5.0
Behavior3/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already declare readOnlyHint=true, idempotentHint=true, openWorldHint=true, and destructiveHint=false, covering the safety profile. The description adds only the token-scoped return fields, which does not go far beyond the structured annotations and output schema.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is a single front-loaded sentence that states the operation, the scoping condition, and the returned fields without any wasted text.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a zero-parameter read tool with rich annotations and an output schema, the description is complete enough. It identifies the current-token user context and the fields returned, while the output schema can define return structure in detail.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The tool takes zero parameters, so the baseline according to the rubric is 4. Schema description coverage is also 100%, meaning structured fields already handle parameter documentation.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description states a specific verb and resource: 'Return the Zammad user the current token belongs to.' It also identifies the returned identity fields, making the tool's scope unambiguous even without sibling tools.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The phrase 'the current token belongs to' provides clear context for when this tool applies: retrieving the authenticated user for the active token. No alternatives exist among sibling tools, so no exclusions are needed.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

  1. 1 tool updatev0.1.0
    • First observedget_me

TDQS

A3.9/5.0

Scored across 1 tool

Disambiguation5/5

With only one tool, there is no possibility of confusing it with another tool. Its purpose is clearly stated as returning the current Zammad user.

Naming Consistency5/5

The single tool name follows a clear snake_case verb_object convention (get_me). No inconsistency can exist with only one tool.

Tool Count1/5

A Zammad server with only one trivial identity tool is an extreme mismatch for the platform's scope. Zammad is a full helpdesk system requiring at least ticket, user, and organization operations.

Completeness1/5

The surface is severely incomplete for a Zammad integration. There are no tools for tickets, users, organizations, groups, or any CRUD/lifecycle operations, leaving agents unable to perform meaningful helpdesk tasks.

Maintenance

ActivityMaintained
ResponsivenessNo issues

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    A
    maintenance
    An MCP server that connects AI assistants to Zammad, providing tools for managing tickets, users, organizations, and attachments.
    45
    AGPL 3.0
  • A
    license
    A
    quality
    C
    maintenance
    MCP server that authenticates via browser session cookies to access Zendesk's REST API without API tokens, supporting reads and writes with agent permissions.
    19
    12 npm
    1
    MIT
  • A
    license
    Not graded
    quality
    C
    maintenance
    MCP server to interact with Zammad ticketing system, enabling ticket search, creation, update, article addition, and user/organization/group queries via the Zammad API.
    5 npm
    MIT