Skip to main content
Glama
KalchevS

mcp-server-zendesk

by KalchevS
README.md
# mcp-server-zendesk

An MCP server that gives AI assistants full access to your Zendesk instance — search tickets, manage tags, create tickets, inspect automations, and more.

Works with [Kiro](https://kiro.dev), [Claude Code](https://docs.anthropic.com/en/docs/claude-code), Claude Desktop, Cursor, Windsurf, and any MCP-compatible client.

## What it does

Connect your AI assistant to Zendesk and ask things like:

- "How many unsolved tickets do we have?"
- "Show me all high-priority tickets assigned to john@company.com"
- "Create a ticket for the billing team about the invoice issue"
- "Add an internal note to ticket 4521 saying we're waiting on the vendor"
- "Who solved the most tickets today?"
- "What macros do we have for password reset requests?"
- "List all automations that fire on pending tickets"

## Tools (27)

### Tickets
| Tool | Description |
|------|-------------|
| `count_tickets` | Count tickets matching any search query |
| `search_tickets` | Search tickets with full details and pagination |
| `get_ticket` | Get single ticket with all comments and custom fields |
| `get_ticket_audits` | Get change history (status changes, reassignments) |
| `get_ticket_comments` | Get all comments and internal notes |
| `create_ticket` | Create a new ticket |
| `edit_ticket` | Update ticket fields (status, priority, tags, etc.) |
| `solve_ticket` | Mark a ticket as solved |
| `bulk_solve_tickets_by_type` | Solve all tickets matching a tag |
| `add_comment` | Add a public reply or internal note |
| `add_ticket_tags` | Add tags without removing existing ones |
| `remove_ticket_tags` | Remove specific tags |

### Users & Organizations
| Tool | Description |
|------|-------------|
| `get_user` | Get user details by ID |
| `search_users` | Search users by name or email |
| `get_organization` | Get organization details by ID |
| `search_organizations` | Search organizations by name |

### Views
| Tool | Description |
|------|-------------|
| `get_view` | Get view configuration and conditions |
| `count_view` | Get ticket count for a view |
| `list_view_tickets` | List tickets in a view |
| `list_ticket_fields` | List all ticket fields including custom fields |

### Business Rules
| Tool | Description |
|------|-------------|
| `list_triggers` | List event-based automation rules |
| `get_trigger` | Get full trigger details |
| `search_triggers` | Search triggers by title |
| `list_automations` | List time-based automation rules |
| `list_macros` | List prepared agent responses and actions |

### Groups & Performance
| Tool | Description |
|------|-------------|
| `list_groups` | List agent groups |
| `get_agent_performance_today` | Agent ranking by tickets solved |

## Quick Start

### 1. Install

**macOS / Linux:**
```bash
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
```

**Windows (PowerShell):**
```powershell
python -m venv .venv
.venv\Scripts\Activate.ps1
pip install -r requirements.txt
```

### 2. Configure

```bash
cp .env.example .env
```

Edit `.env` — choose one authentication mode:

**Option A — API Token (simple, no expiry):**
```env
ZD_SUBDOMAIN=your-company
ZD_EMAIL=agent@company.com
ZD_API_TOKEN=your-token-here
```

**Option B — OAuth 2.0 Authorization Code + PKCE (public client, recommended for production):**
```env
ZD_SUBDOMAIN=your-company
ZD_OAUTH_CLIENT_ID=your-oauth-client-id
ZD_OAUTH_REDIRECT_URI=http://localhost:8765/callback
ZD_OAUTH_SCOPE=read write
# ZD_OAUTH_TOKEN_STORE defaults to ~/.config/zendesk-mcp/oauth_token.json
```

Then run the one-time authorization CLI:

```bash
./oauth_authorize.sh        # macOS/Linux
.\oauth_authorize.ps1        # Windows
```

Both launchers resolve this project's own `.venv` automatically, so you don't need to activate it first (running `python oauth_authorize.py` directly will fail with `ModuleNotFoundError: No module named 'httpx'` unless your venv is already active — use the launcher script, or run `source .venv/bin/activate && python3 oauth_authorize.py` yourself). It prints an authorization URL to open in your browser, receives the callback automatically (or prompts you to paste it), exchanges the code, and stores the resulting credentials. No client secret is ever required or stored.

OAuth access tokens are refreshed automatically before they expire, using the stored refresh token — no client secret is ever sent. If the refresh token itself becomes invalid or missing, the server reports that reauthorization is required; re-run `./oauth_authorize.sh` (or `.\oauth_authorize.ps1` on Windows) to fix it.

Configuring both Option A and Option B at the same time is a configuration error — the server reports the conflict and refuses to start.

### 3. Connect to your AI client

See detailed setup for each client below.

---

## Client Setup

### Kiro IDE

**Option A — Stdio (local, recommended for development):**

Create `.kiro/settings/mcp.json` in your workspace root:

```json
{
  "mcpServers": {
    "zendesk": {
      "command": "./.venv/bin/python3",
      "args": ["./mcp_server.py"],
      "autoApprove": [
        "count_tickets",
        "search_tickets",
        "get_ticket",
        "get_ticket_comments",
        "get_user",
        "search_users",
        "list_triggers",
        "list_macros",
        "list_groups"
      ]
    }
  }
}
```

Or copy the included example:
```bash
mkdir -p .kiro/settings
cp mcp.json.example .kiro/settings/mcp.json
```

**Option B — SSE (remote Docker server):**

```json
{
  "mcpServers": {
    "zendesk": {
      "url": "http://localhost:8998/sse"
    }
  }
}
```

After saving, the server connects automatically. Check the MCP Server panel in Kiro to confirm. If it doesn't connect, use Command Palette → "MCP: Reconnect Server".

### Kiro CLI

Same config file at `~/.kiro/settings/mcp.json` (global) or `.kiro/settings/mcp.json` (workspace):

```bash
cp mcp.json.example ~/.kiro/settings/mcp.json
```

Then start a session:
```bash
kiro
# "How many unsolved tickets do we have?"
# "Show me tickets assigned to john@company.com"
# "Create a ticket about the login issue for customer@example.com"
```

### Claude Code

Add to your Claude Code MCP configuration (`~/.claude/mcp.json` or project-level):

**Stdio (local):**
```json
{
  "mcpServers": {
    "zendesk": {
      "command": "/path/to/zendesk-mcp/.venv/bin/python3",
      "args": ["/path/to/zendesk-mcp/mcp_server.py"]
    }
  }
}
```

**SSE (remote/Docker):**
```json
{
  "mcpServers": {
    "zendesk": {
      "url": "http://localhost:8998/sse"
    }
  }
}
```

> **Note for Claude Code stdio**: Use absolute paths since Claude Code may not run from the project directory.

### Claude Desktop

**macOS:**

Edit `~/Library/Application Support/Claude/claude_desktop_config.json`:

```json
{
  "mcpServers": {
    "zendesk": {
      "command": "/path/to/zendesk-mcp/run_mcp.sh",
      "args": []
    }
  }
}
```

The `run_mcp.sh` wrapper auto-detects its directory, so it works regardless of Claude Desktop's working directory.

**Windows:**

Edit `%APPDATA%\Claude\claude_desktop_config.json`, using the included `run_mcp.ps1` launcher (the PowerShell equivalent of `run_mcp.sh`):

```json
{
  "mcpServers": {
    "zendesk": {
      "command": "powershell.exe",
      "args": ["-ExecutionPolicy", "Bypass", "-File", "C:\\path\\to\\zendesk-mcp\\run_mcp.ps1"]
    }
  }
}
```

### Cursor / Windsurf / Other MCP Clients

Any client that supports the MCP protocol works. Use either:
- **Stdio**: point to `./.venv/bin/python3` with args `["./mcp_server.py"]`
- **SSE**: connect to `http://localhost:8998/sse` (requires Docker or `python3 mcp_server_http.py` running)

---

## Docker

Run as a remote HTTP/SSE server:

```bash
# With Docker Compose (recommended)
docker compose up -d --build

# Health check
curl http://localhost:8998/health
```

Credentials are supplied at runtime, not baked into the image. `docker-compose.yml` already reads `.env` via `env_file:`; if you use `docker run` directly instead of Compose, pass `--env-file .env` explicitly.

For OAuth PKCE mode, the Credential_Store must persist across container restarts. Mount a host directory over `/root/.config/zendesk-mcp` (the image's default store path) and run the one-time authorization step against that same mount:

```bash
docker run --rm -it -v $(pwd)/oauth_data:/root/.config/zendesk-mcp \
  --env-file .env zendesk-mcp python oauth_authorize.py
docker run -p 8998:8998 -v $(pwd)/oauth_data:/root/.config/zendesk-mcp \
  --env-file .env zendesk-mcp
```

Then connect clients using the SSE URL:
```json
{
  "mcpServers": {
    "zendesk": {
      "url": "http://localhost:8998/sse"
    }
  }
}
```

## Architecture

```
mcp_server.py        — MCP protocol server (stdio transport)
mcp_server_http.py   — HTTP/SSE transport (for Docker/network)
tools.py             — Core ticket tools (async)
tools_extra.py       — User, org, view, trigger, macro, group tools (async)
zendesk_client.py    — Async HTTP client with retry, rate-limit handling, path allowlist
config.py            — Settings from .env via pydantic-settings
```

Key design decisions:
- **Async-first** — all tools use `httpx.AsyncClient` for non-blocking I/O
- **Path allowlisting** — only pre-approved Zendesk API endpoints can be called
- **Rate limit retry** — automatic backoff on 429 and 5xx errors
- **Error masking** — set `MASK_ERRORS=true` in production to hide internal details
- **Lifespan management** — HTTP client properly opened/closed on server start/stop

## Query Syntax

All ticket search queries must include `type:ticket`:

```
type:ticket status<solved                           # All unsolved
type:ticket priority:high status:open               # High priority open
type:ticket assignee:user@company.com               # By assignee
type:ticket created>=2025-01-01 created<2025-02-01  # Date range
type:ticket tags:billing organization:ACME          # Tag + org
type:ticket "exact phrase"                          # Text search
```

## Security

- API credentials stay in `.env` (gitignored)
- Supports both API token and OAuth 2.0 Authorization Code + PKCE authentication
- OAuth access tokens are refreshed automatically before expiry using the stored refresh token — no client secret is ever sent or stored, and no manual intervention is needed unless the refresh token itself becomes invalid
- The OAuth Credential_Store (`ZD_OAUTH_TOKEN_STORE`) is written with owner-only file permissions (mode `0600`, parent directory `0700`) on POSIX filesystems (Linux, macOS). OAuth PKCE mode also runs on Windows, but NTFS ignores POSIX permission bits, so the file is not locked down to the same degree there — the OAuth flow, token refresh, and credential storage all still work, just without the same file-level access restriction
- Path allowlist prevents access to unauthorized Zendesk endpoints
- Write tools require explicit approval (not auto-approved)
- `MASK_ERRORS=true` hides internal error details in production
- Credentials are supplied at runtime (via `.env`/`--env-file`/`env_file:`), never baked into the Docker image; the OAuth Credential_Store is mounted as a volume so refreshed tokens persist across container restarts

## Data Exposure Model

What a Kiro (or other MCP client) user can see and do through this server depends entirely on the credentials configured for the deployment — the server itself adds no additional access control on top of whatever the underlying Zendesk credential is authorized for.

**API-token mode**: one shared `ZD_EMAIL`/`ZD_API_TOKEN` pair is configured per deployment. Every MCP client connected to that server instance acts as that single Zendesk agent — all tool calls (searches, ticket reads, ticket writes) are attributed to that one agent account in Zendesk's own audit trail (e.g. `get_ticket_audits`, comment authorship). There is no per-user scoping at the MCP layer: anyone who can reach the server (locally via stdio, or over the network via the HTTP/SSE transport) can perform any action that shared agent account is permitted to perform in Zendesk.

**OAuth PKCE mode**: each deployment is authorized against a single Zendesk agent's account via `./oauth_authorize.sh` (or `.\oauth_authorize.ps1` on Windows; that agent logs in and approves the authorization request in their own browser). The resulting Credential_Store — and therefore the running server process — is scoped to that one agent's permissions, exactly as in API-token mode. There is no shared secret embedded in the codebase or configuration (no client secret is ever requested or stored), but this is a *credential-handling* improvement, not a *multi-tenancy* one: a single running server process still represents exactly one Zendesk agent identity, not one identity per connecting MCP client. Running the server as multiple agents requires running multiple separate server instances, each authorized against its own Credential_Store.

**What every tool call can reach, regardless of mode**: only the Zendesk REST endpoints in `zendesk_client.py`'s `ALLOWED_PATHS` allowlist are reachable at all (tickets, users, organizations, views, ticket fields, triggers, automations, macros, groups) — no arbitrary Zendesk API path can be constructed by a tool call. Within those endpoints, visibility and write permissions are exactly what the configured Zendesk agent account already has in Zendesk itself (e.g. an agent without access to a given ticket in Zendesk's own permission model will not gain access to it through this server). Write tools (`create_ticket`, `edit_ticket`, `solve_ticket`, `bulk_solve_tickets_by_type`, `add_comment`, `add_ticket_tags`, `remove_ticket_tags`) are excluded from `autoApprove` in `mcp.json.example` by default, requiring explicit per-call approval in the MCP client regardless of auth mode — but this is a client-side approval gate, not a server-side authorization boundary; the server executes any allowlisted-path write tool call it receives.

## Requirements

- Python 3.10+
- Zendesk account with API access
- One of:
  - API token from Zendesk Admin > Channels > API
  - OAuth PKCE public client (Client ID + Redirect URI + Scope registered with Zendesk; obtain credentials via `./oauth_authorize.sh` / `.\oauth_authorize.ps1`)

## License

MIT