Zendesk MCP Server (Extended)
# Zendesk MCP Server (Extended Edition)
[](https://opensource.org/licenses/Apache-2.0)

A production-grade Model Context Protocol (MCP) server for Zendesk — run it locally
with Claude Desktop over stdio, or deploy it to the cloud as a secure, multi-tenant,
OAuth-protected remote MCP server that any AI application can connect to.
> **Note — extension of the original project**
>
> This is an extended fork of [reminia/zendesk-mcp-server](https://github.com/reminia/zendesk-mcp-server).
> The original provides a local, stdio-only Zendesk MCP server with basic ticket
> tools. This edition keeps full backward compatibility with it (stdio mode,
> original tools, prompts, and knowledge-base resource) and extends it for
> remote, internet-facing production use.
## Issues with the original repo that this edition addresses
| # | Issue in original | How it's addressed here |
|---|---|---|
| 1 | **stdio transport only** — could not be reached over a network, so it only worked on the same machine as the AI client | Streamable HTTP transport (`/mcp` endpoint, MCP spec 2025-03-26+; SSE-as-transport is deprecated and intentionally not used) alongside stdio |
| 2 | **No authentication of any kind** — anyone who could reach the process could use it | Three auth modes: internal scoped API keys, OAuth 2.1 resource server (generic OIDC + RFC 9728 discovery), or both simultaneously |
| 3 | **No permission model** — every caller could read *and* write tickets | 4-layer read/write control: per-tool scopes (fail-closed), tool-list filtering, human-in-the-loop confirmation, dual Zendesk identity backstop |
| 4 | **Blocking I/O inside async handlers** — sync Zenpy calls froze the event loop under concurrent HTTP load | All Zendesk calls run in worker threads (`run_in_thread`) |
| 5 | **Deprecated offset pagination** — Zendesk is sunsetting it; comments were not paginated at all (context blowout on long tickets) | Cursor pagination (`page[size]`/`page[after]`) for tickets and comments |
| 6 | **Sparse Zendesk coverage** — only 5 tools; user/assignee IDs could not be resolved, custom fields were opaque, no search | 14 tools including search, users, groups, ticket-field metadata, KB article search, attachment upload |
| 7 | **Single Zendesk account hard-wired at startup** from `.env` | Multi-tenancy: per-tenant Zendesk credentials, keys/OAuth claims mapped to tenants, hot-swappable connection settings |
| 8 | **No deployment story** — no TLS, no health check, stdio-oriented Docker image | Docker (HTTP-first, healthcheck), docker-compose with Caddy auto-TLS, Terraform for EC2 and for ECS Fargate + ALB |
| 9 | **No admin tooling** — key rotation/credential changes required editing `.env` and restarting | Web admin console (separate port) + `zendesk-keys` CLI; connection hot-swap without restart |
| 10 | **No tests** | 38 unit tests + HTTP/admin/OAuth smoke test suites; tests run in CI |
## Feature overview
**Original features (retained):**
- Ticket tools: get ticket, list tickets, get comments, create ticket, update ticket, comment on ticket
- Image attachment download with security hardening (MIME allowlist, magic-byte validation, 10 MB cap)
- Prompts: `analyze-ticket`, `draft-ticket-response`
- Resource: `zendesk://knowledge-base` (all Help Center articles, cached 1 h)
- stdio transport for Claude Desktop / Claude Code local use
**New in this edition:**
- Streamable HTTP transport with `/health` endpoint
- Internal API keys: `zmk_` prefix, SHA-256 hashed at rest, scopes, expiry, instant revocation, audit log
- OAuth 2.1 resource server: JWKS/issuer/audience JWT validation, RFC 9728 Protected Resource
Metadata at `/.well-known/oauth-protected-resource/mcp` — works with Auth0, Descope, Cognito, Keycloak, WorkOS
- Scope model: `tickets:read`, `tickets:write`, `kb:read`, `*` — enforced per tool, fail-closed, with tools/list filtering
- Optional elicitation: in-client approve/decline before posting public (customer-visible) comments
- Dual Zendesk identity: reads via a restricted user (e.g. light agent), writes via a full agent
- Multi-tenancy with per-tenant Zendesk credentials
- Web admin console: connection settings (hot-swap + test), tenant management, key lifecycle
- New tools: `search_tickets`, `get_user`, `search_users`, `list_groups`, `list_ticket_fields`,
`search_articles`, `upload_attachment`
- Tool annotations (`readOnlyHint` / `destructiveHint`) and structured output on all tools
- Deployment: Dockerfile, docker-compose + Caddy (auto-TLS), Terraform for EC2 and ECS Fargate
- MCP Registry manifest (`server.json`) and publishing guide
## Tools
| Tool | Scope | Description |
|---|---|---|
| `get_ticket` | tickets:read | Get a ticket by ID |
| `get_tickets` | tickets:read | List tickets (cursor pagination, sortable) |
| `get_ticket_comments` | tickets:read | Ticket comments incl. attachment metadata (cursor pagination) |
| `search_tickets` | tickets:read | Zendesk search syntax, e.g. `status:open priority:high` |
| `get_ticket_attachment` | tickets:read | Download an image attachment (base64, validated) |
| `get_user` | tickets:read | Resolve a user ID to name/email/role |
| `search_users` | tickets:read | Search users by name or email |
| `list_groups` | tickets:read | Agent groups (teams) for routing |
| `list_ticket_fields` | tickets:read | Field metadata — interpret/set `custom_fields` `{id, value}` |
| `search_articles` | kb:read | Search Help Center articles (context-safe) |
| `create_ticket` | tickets:write | Create a ticket |
| `update_ticket` | tickets:write | Update status/priority/assignee/tags/custom fields/due date |
| `create_ticket_comment` | tickets:write | Comment on a ticket (optional elicitation for public comments; supports attachments) |
| `upload_attachment` | tickets:write | Upload a file (≤10 MB), returns token for comment attachment |
Prompts: `analyze-ticket`, `draft-ticket-response`. Resource: `zendesk://knowledge-base` (kb:read).
## Quick start (local, stdio — same as the original)
```bash
git clone <this-repo>
cd zendesk-mcp-server
uv venv && uv pip install -e .
cp .env.example .env # fill in ZENDESK_SUBDOMAIN, ZENDESK_EMAIL, ZENDESK_API_KEY
```
Claude Desktop config:
```json
{
"mcpServers": {
"zendesk": {
"command": "uv",
"args": ["--directory", "/path/to/zendesk-mcp-server", "run", "zendesk"]
}
}
}
```
stdio mode is trusted-local: no auth, full access, identical behavior to the original repo.
## Remote mode (Streamable HTTP)
```bash
MCP_TRANSPORT=http zendesk # serves http://0.0.0.0:8000/mcp + /health
```
Create scoped API keys (shown once, stored hashed):
```bash
zendesk-keys create --name "reader" --scopes tickets:read,kb:read
zendesk-keys create --name "agent" --scopes tickets:read,tickets:write,kb:read --expires-days 30
zendesk-keys create --name "admin" --scopes "*"
zendesk-keys list
zendesk-keys revoke --id 2
```
Connect a client:
```bash
claude mcp add zendesk --transport http https://mcp.example.com/mcp \
--header "Authorization: Bearer zmk_..."
```
Read-only keys never see write tools in `tools/list`; write calls without
`tickets:write` are denied; unknown tools are denied by default (fail closed).
## Configuration reference
All configuration is via environment variables (or `.env`; the admin console can
override connection settings at runtime, persisted in the key-store DB).
### Zendesk connection
| Variable | Required | Default | Description |
|---|---|---|---|
| `ZENDESK_SUBDOMAIN` | yes | — | `<subdomain>.zendesk.com` |
| `ZENDESK_API_KEY` | yes | — | Zendesk API token |
| `ZENDESK_EMAIL` | yes* | — | Single-identity mode: agent email paired with the token |
| `ZENDESK_READ_EMAIL` | no | `ZENDESK_EMAIL` | Dual identity: restricted user (light agent) for all reads |
| `ZENDESK_WRITE_EMAIL` | no | `ZENDESK_EMAIL` | Dual identity: full agent for all writes |
*Either `ZENDESK_EMAIL` or both `ZENDESK_READ_EMAIL`/`ZENDESK_WRITE_EMAIL`.
Zendesk roles live on the *user*, not the token — pairing the same token with a
restricted user email yields restricted permissions (Layer 4 backstop).
### Transport
| Variable | Default | Description |
|---|---|---|
| `MCP_TRANSPORT` | `stdio` | `stdio` (local, trusted) or `http` (remote) |
| `MCP_HOST` | `0.0.0.0` | HTTP bind address |
| `MCP_PORT` | `8000` | HTTP port; MCP endpoint is `/mcp` |
### Authentication
| Variable | Default | Description |
|---|---|---|
| `MCP_AUTH_ENABLED` | `true` | Set `false` only for trusted private networks |
| `MCP_AUTH_MODE` | `keys` | `keys` \| `oauth` \| `both` |
| `MCP_KEYS_DB` | `data/keys.db` | SQLite store for keys, tenants, config, audit log |
| `MCP_PUBLIC_URL` | — | Public base URL (required for oauth/both; used in RFC 9728 metadata) |
| `MCP_OAUTH_ISSUER` | — | OIDC issuer, e.g. `https://your-tenant.auth0.com/` |
| `MCP_OAUTH_AUDIENCE` | `MCP_PUBLIC_URL` | Audience/identifier of this server at the IdP |
| `MCP_OAUTH_JWKS_URI` | `<issuer>/.well-known/jwks.json` | Override if your IdP differs |
| `MCP_OAUTH_AUTH_SERVERS` | issuer | Comma-separated authorization server URLs |
| `MCP_OAUTH_TENANT_CLAIM` | `zendesk_tenant` | JWT claim naming the caller's tenant (id or name) |
### Admin console & safety
| Variable | Default | Description |
|---|---|---|
| `MCP_ADMIN_PASSWORD` | — (disabled) | Setting it enables the admin console |
| `MCP_ADMIN_HOST` | `127.0.0.1` | Keep loopback; reach via SSH/SSM tunnel |
| `MCP_ADMIN_PORT` | `9000` | Admin console port (never expose publicly) |
| `MCP_WRITE_CONFIRMATION` | `false` | Elicit user approval before PUBLIC comments (Layer 3) |
## Security model
| Layer | Mechanism |
|---|---|
| Edge | TLS 1.2+ (Caddy or ALB/ACM), security headers, 80/443 only |
| AuthN | API keys (hashed, expiring, revocable) and/or OAuth 2.1 JWTs (PKCE at the IdP) |
| L1 AuthZ | Central `TOOL_PERMISSIONS` map, enforced pre-dispatch, fail-closed |
| L2 Visibility | `tools/list` filtered to caller's scopes — models can't attempt what they can't see |
| L3 Confirmation | `destructiveHint` annotations + optional elicitation for public comments |
| L4 Zendesk | Dual identity — reads through a restricted Zendesk user, writes through a full agent |
| Admin | Separate loopback port, password + CSRF, secrets never re-displayed |
| Audit | Append-only log: key lifecycle, admin actions, logins, writes |
## Multi-tenancy
By default every caller uses the server's own Zendesk connection. To let other
teams/customers connect **their** Zendesk:
1. Admin console → *Tenants* → add name, subdomain, API token, read/write emails.
2. Bind credentials to the tenant: create an API key with that tenant selected
(or `zendesk-keys create ... --tenant-id N`), or configure your IdP to issue
the tenant's name/id in the `MCP_OAUTH_TENANT_CLAIM` JWT claim.
3. All tool calls from that identity are routed to the tenant's Zendesk.
Deleting a tenant revokes its keys immediately.
## Web admin console
Enable with `MCP_ADMIN_PASSWORD`. Reach it via tunnel — never expose it:
```bash
ssh -L 9000:localhost:9000 user@host # or the SSM equivalent, see docs/DEPLOYMENT.md
# open http://localhost:9000
```
Provides: Zendesk connection editor with hot-swap (no restart) and "test connection",
tenant management, API key create/revoke with scope checkboxes and expiry, and key
usage visibility. Stored tokens are never re-displayed; new keys are shown exactly once.
## Deployment
**Docker (single container):**
```bash
docker build -t zendesk-mcp-server .
docker run --rm -p 8000:8000 --env-file .env -v zmcp-keys:/data zendesk-mcp-server
```
**Docker Compose + automatic TLS (recommended single-box):** set `MCP_DOMAIN` in
`.env`, then `docker compose up -d` — Caddy terminates TLS with Let's Encrypt and
proxies to the server. Create keys with
`docker compose exec zendesk-mcp zendesk-keys create ...`.
**AWS EC2 (Terraform):** `terraform/` provisions EC2 (SSM access, no SSH, IMDSv2),
security group (80/443 only), Elastic IP, optional Route53, and bootstraps
Docker + the repo. Full runbook: [docs/DEPLOYMENT.md](docs/DEPLOYMENT.md).
**Windows Server without Docker:** native Python + Caddy with your own TLS
certificate + NSSM services, including a troubleshooting FAQ of real-world
Windows issues (Node/PATH, cert chains, arg mangling):
[docs/WINDOWS_SETUP.md](docs/WINDOWS_SETUP.md).
**AWS ECS Fargate + ALB (scale-out):** `terraform/ecs/` provisions ECR, Fargate
service, ALB with ACM/TLS 1.3, EFS-backed key store, Secrets Manager injection,
and CloudWatch. Keep `desired_count=1` until the key store is migrated off SQLite.
**MCP Registry:** fill in `server.json` and follow [docs/REGISTRY.md](docs/REGISTRY.md)
to publish your deployed server to registry.modelcontextprotocol.io.
## Development
```bash
uv sync --extra dev
uv run pytest tests/ -v # 38 tests: tools, keystore, permissions, tenancy, auth modes
```
Project layout:
```
src/zendesk_mcp_server/
server.py # FastMCP app: tools, prompts, resources, transports
zendesk_client.py # Zendesk API client (dual identity, cursor pagination)
auth.py # API-key verifier, OIDC JWT verifier, auth-mode factory
permissions.py # Scope model + enforcement/filtering middleware
keystore.py # SQLite: keys, tenants, audit log
runtime.py # Client holder: hot-swap + per-tenant routing
admin.py # Web admin console (separate port)
keys_cli.py # zendesk-keys CLI
config.py # Env-based settings
terraform/ # EC2 deployment terraform/ecs/ # Fargate deployment
docs/ # Architecture plan, deployment runbook, registry guide
```
Architecture decisions and full history: [docs/REMOTE_MCP_ARCHITECTURE_PLAN.md](docs/REMOTE_MCP_ARCHITECTURE_PLAN.md).
## License
Apache 2.0 — same as the original project. Original work by
[reminia](https://github.com/reminia/zendesk-mcp-server); extensions as described above.
TDQS
Scored across 14 tools
Each tool targets a distinct resource or action: groups, ticket fields, attachments, users, tickets (CRUD and search), comments, articles. Descriptions clearly differentiate similar tools like get_ticket vs get_tickets and search_tickets vs get_tickets.
All tools follow a consistent verb_snake_case pattern (list_, get_, search_, create_, update_, upload_). No mixing of conventions or ambiguous verb choices.
14 tools cover the essential operations for a Zendesk support MCP server: ticket lifecycle, user/group lookup, comment management, attachments, and article search. The count feels well-scoped without bloat.
Core ticket workflows are covered (CRUD, comments, attachments, search, user/group resolution). Missing delete ticket and some admin features, but these are reasonable omissions for an agent-focused server.