Zendesk MCP Server
<div align="center">
# Zendesk MCP Server
[](https://www.npmjs.com/package/@sshadows/zendesk-mcp-server)
[](https://opensource.org/licenses/MIT)
[](https://nodejs.org)
[](https://modelcontextprotocol.com)
**A Model Context Protocol server for Zendesk API integration with AI-powered ticket analysis**
[Quick Start](#quick-start) • [Configuration](#configuration) • [Tools](#available-tools) • [Architecture](#architecture) • [Development](#development)
</div>
---
## Overview
Zendesk MCP Server provides comprehensive access to the Zendesk API through the Model Context Protocol. It supports **two transport modes** that are auto-detected from environment variables:
- **Stdio mode** — API token auth, for CLI-based MCP clients (Claude Code, Cursor, etc.)
- **HTTP mode** — OAuth 2.1 with PKCE, for web-based MCP clients
Both modes expose the same set of tools. No code changes are needed to switch between them.
> Based on [mattcoatsworth/zendesk-mcp-server](https://github.com/mattcoatsworth/zendesk-mcp-server) with significant enhancements including AI-powered features, dual-mode authentication, improved error handling, and comprehensive retry logic.
## Quick Start
### Stdio Mode (API Token)
Best for CLI-based MCP clients like Claude Code or Cursor. Each user only needs their own email + API token.
**1. Get your API token** from Zendesk Admin Center → Apps and integrations → APIs → Zendesk API → Add API token.
**2. Configure your MCP client:**
```json
{
"zendesk": {
"type": "stdio",
"command": "npx",
"args": ["@sshadows/zendesk-mcp-server"],
"env": {
"ZENDESK_SUBDOMAIN": "your-subdomain",
"ZENDESK_EMAIL": "you@example.com",
"ZENDESK_API_TOKEN": "your-api-token"
}
}
}
```
That's it. The server auto-detects stdio mode and connects.
### HTTP Mode (OAuth 2.1)
Best for web-based MCP clients or multi-user deployments with centralized OAuth.
**1. Create an OAuth app** in Zendesk Admin Center → Apps and integrations → APIs → OAuth Clients.
**2. Create a `.env` file:**
```bash
ZENDESK_SUBDOMAIN=your-subdomain
ZENDESK_OAUTH_CLIENT_ID=your_client_id
ZENDESK_OAUTH_CLIENT_SECRET=your_client_secret
ZENDESK_OAUTH_REDIRECT_URI=http://localhost:3030/zendesk/oauth/callback
```
**3. Start and authorize:**
```bash
npm start
# Visit http://localhost:3030/oauth/authorize in your browser
```
**4. Use the token** with your MCP client: `Authorization: Bearer mcp_...`
## Installation
| Requirement | Version | Notes |
|------------|---------|-------|
| Node.js | >= 18.0.0 | Required |
| Zendesk Account | Any plan | Required |
| Anthropic API Key | - | Only for AI analysis features |
```bash
# npm (recommended)
npm install -g @sshadows/zendesk-mcp-server
# Or from source
git clone https://github.com/SShadowS/zendesk-mcp-server.git
cd zendesk-mcp-server
npm install
```
## Configuration
### Environment Variables
The server auto-detects which mode to use based on which variables are set.
**Stdio mode** (set `ZENDESK_EMAIL` + `ZENDESK_API_TOKEN`, without `ZENDESK_OAUTH_CLIENT_ID`):
```bash
ZENDESK_SUBDOMAIN=mycompany
ZENDESK_EMAIL=user@example.com
ZENDESK_API_TOKEN=your-api-token
```
**HTTP mode** (set `ZENDESK_OAUTH_CLIENT_ID`):
```bash
ZENDESK_SUBDOMAIN=mycompany
ZENDESK_OAUTH_CLIENT_ID=your_client_id
ZENDESK_OAUTH_CLIENT_SECRET=your_client_secret
ZENDESK_OAUTH_REDIRECT_URI=http://localhost:3030/zendesk/oauth/callback
```
**Common (both modes):**
```bash
MODE=full # 'full' (all 55 tools) or 'lite' (10 essential tools)
READ_ONLY=false # 'true' blocks every write except internal ticket notes
READ_ONLY_STRICT=false # 'true' also blocks internal notes, leaving no write path
ANTHROPIC_API_KEY=sk-ant-... # Required for AI image/document analysis
ZENDESK_DEBUG=false # Enable debug logging
```
**HTTP mode only:**
```bash
PORT=3030
SERVER_BASE_URL=http://localhost:3030 # Use https:// in production
```
See [`.env.example`](.env.example) for the full list.
### Tool Modes
Control which tools are exposed with the `MODE` environment variable:
- **`full`** (default) — All 55 tools available.
- **`lite`** — 10 essential tools for reduced context usage: `search`, `get_user`, `list_tickets`, `get_ticket`, `get_ticket_comments`, `add_ticket_comment`, `get_ticket_attachments`, `analyze_ticket_images`, `analyze_ticket_documents`, `get_document_summary`.
```bash
MODE=lite npm start
```
### Read-Only Mode
Set `READ_ONLY=true` to prevent the server from modifying Zendesk data:
```bash
READ_ONLY=true npm start
```
- Every `create_*`, `update_*` and `delete_*` tool is unregistered — 28 tools remain in `full` mode.
- `add_ticket_comment` stays available for **internal notes only**. A `type: 'public'` call is refused rather than downgraded, so a caller is never left believing it replied to a customer who received nothing.
- Enforcement also sits in the HTTP client, which rejects any non-`GET` request other than an internal-note payload. A write cannot reach the Zendesk API by another route.
- Composes with `MODE`. `MODE=lite READ_ONLY=true` still exposes all 10 lite tools, since only `add_ticket_comment` mutates and it survives.
Recognized truthy values are `true` and `1` (case-insensitive); anything else, including unset, leaves the server writable. An unrecognized value is treated as `false` and logs a warning at startup, so a typo cannot silently reopen a write path.
#### Strict read-only
`READ_ONLY` still leaves one write path open: internal ticket notes. Where even that is unacceptable — for example a sidecar whose spec says only an orchestrator may write — add `READ_ONLY_STRICT=true`:
```bash
READ_ONLY=true READ_ONLY_STRICT=true npm start
```
- `add_ticket_comment` is unregistered, leaving 27 tools in `full` mode and 9 under `MODE=lite`.
- The transport guard drops the internal-note exemption, so `request()` permits `GET` and nothing else. Both layers tighten together; unregistering the tool alone would be cosmetic, since anything holding the client object could still write.
- Strict is defined as *the allowlist minus anything mutating*, so it stays correct as tools are added rather than depending on a list someone has to remember to update.
Strict is a **separate boolean rather than a third value of `READ_ONLY`** on purpose. A single `READ_ONLY=true` is often shared across several MCP servers, and some treat an unrecognized value as `false` with only a warning — so `READ_ONLY=strict` would quietly make those servers writable.
`READ_ONLY_STRICT=true` implies read-only even if `READ_ONLY` is unset or `false`. A server told to be strict must never come up writable.
The active mode is logged at startup (`mode=OFF`, `mode=STANDARD`, or `mode=STRICT`).
## Available Tools
<details>
<summary><b>Ticket Management</b></summary>
| Tool | Description |
|------|-------------|
| `list_tickets` | List tickets with filters (status, assignee, etc.) |
| `get_ticket` | Get ticket details with optional comments |
| `create_ticket` | Create a new ticket |
| `update_ticket` | Update ticket fields |
| `get_ticket_comments` | Get all comments on a ticket |
| `add_ticket_comment` | Add public or internal comment |
| `get_ticket_attachments` | Get ticket attachments |
| `analyze_ticket_images` | AI-powered image analysis with Claude Vision |
| `analyze_ticket_documents` | AI-powered document analysis |
| `get_document_summary` | Quick document summary |
</details>
<details>
<summary><b>User Management</b></summary>
| Tool | Description |
|------|-------------|
| `list_users` | List all users |
| `get_user` | Get user details |
| `create_user` | Create new user |
| `update_user` | Update user info |
| `delete_user` | Delete a user |
</details>
<details>
<summary><b>Organizations</b></summary>
| Tool | Description |
|------|-------------|
| `list_organizations` | List all organizations |
| `get_organization` | Get organization details |
| `create_organization` | Create new organization |
| `update_organization` | Update organization |
| `delete_organization` | Delete organization |
</details>
<details>
<summary><b>Automation & Workflows</b></summary>
| Category | Tools |
|----------|-------|
| **Groups** | `list_groups`, `get_group`, `create_group`, `update_group`, `delete_group` |
| **Macros** | `list_macros`, `get_macro`, `create_macro`, `update_macro`, `delete_macro` |
| **Views** | `list_views`, `get_view`, `create_view`, `update_view`, `delete_view` |
| **Triggers** | `list_triggers`, `get_trigger`, `create_trigger`, `update_trigger`, `delete_trigger` |
| **Automations** | `list_automations`, `get_automation`, `create_automation`, `update_automation`, `delete_automation` |
</details>
<details>
<summary><b>Help Center, Search, Talk & Chat</b></summary>
| Category | Tools |
|----------|-------|
| **Search** | `search` — Search across all Zendesk data |
| **Help Center** | `list_articles`, `get_article`, `create_article`, `update_article`, `delete_article` |
| **Talk** | `get_talk_stats` — Phone support statistics |
| **Chat** | `list_chats` — Chat conversations |
</details>
## Architecture
### Transport Modes
```
src/index.js (auto-detection)
├── ZENDESK_EMAIL + ZENDESK_API_TOKEN → Stdio mode
│ ├── ZendeskClient.setApiTokenAuth()
│ ├── setDefaultZendeskClient(client)
│ └── StdioServerTransport (stdin/stdout)
│
└── ZENDESK_OAUTH_CLIENT_ID → HTTP mode
├── Express server (src/http-server.js)
├── OAuth 2.1 with PKCE (src/auth/)
├── Per-session ZendeskClient instances
└── StreamableHTTPServerTransport
```
Tools are identical in both modes. They call `getZendeskClient()` which resolves to:
- **HTTP mode**: Per-session client via AsyncLocalStorage
- **Stdio mode**: Singleton default client
### Project Structure
```
zendesk-mcp-server/
├── src/
│ ├── index.js # Entry point (auto-detects mode)
│ ├── http-server.js # Express server with OAuth (HTTP mode only)
│ ├── server.js # MCP server setup and tool registration
│ ├── request-context.js # Per-session + default client context
│ ├── auth/
│ │ ├── oauth-handler.js # OAuth 2.1 with PKCE
│ │ ├── session-store.js # Session management
│ │ └── middleware.js # Bearer token auth middleware
│ ├── zendesk-client/
│ │ ├── base.js # Auth, HTTP requests, retry logic
│ │ ├── index.js # Mixin composition
│ │ ├── tickets.js # Ticket API methods
│ │ ├── users.js # User API methods
│ │ └── ... # Other API domain mixins
│ ├── tools/ # MCP tool implementations
│ ├── config/
│ │ └── tool-modes.js # Full/lite mode filtering
│ └── utils/
│ ├── errors.js # Classified error types
│ ├── retry.js # Exponential backoff
│ ├── ticket-context.js # AI prompt context builder
│ ├── document-handler.js # Document routing
│ └── converter-client.js # Office-to-PDF conversion
├── tests/ # Vitest test suite
├── .env.example # Environment variable template
└── CLAUDE.md # AI assistant project guide
```
### Key Design Decisions
- **Dual auth in one client**: `ZendeskClientBase` supports both `setApiTokenAuth()` (Basic) and `setAccessToken()` (Bearer). The `_authMode` field determines which header `getAuthHeader()` returns.
- **Default client fallback**: AsyncLocalStorage doesn't propagate through StdioServerTransport's event callbacks. Instead of fighting that, `getZendeskClient()` falls back to a module-level default client in stdio mode. Zero changes needed in any tool file.
- **Console.error everywhere**: In stdio mode, stdout is the MCP transport. All diagnostic logging in shared code paths uses `console.error`.
- **HTTP mode is unchanged**: `src/http-server.js` and `src/auth/*` are only imported in HTTP mode. No changes were needed.
## Development
```bash
npm start # Start server (auto-detects mode)
npm run dev # Start with auto-reload
npm test # Run all tests
npm run test:watch # Run tests in watch mode
npm run inspect # Launch MCP Inspector
```
### Testing
Tests use [Vitest](https://vitest.dev/) and are in `tests/` mirroring the `src/` directory:
```bash
npm test # Run all tests
npm run test:watch # Watch mode
```
Integration tests (against real Zendesk + Anthropic APIs) require `.env` credentials and are automatically skipped when credentials are missing.
### HTTP Mode Endpoints
| Endpoint | Description |
|----------|-------------|
| `ALL /mcp` | Main MCP endpoint (requires Bearer token) |
| `GET /oauth/authorize` | Start OAuth flow |
| `GET /zendesk/oauth/callback` | OAuth callback |
| `POST /oauth/token` | Token exchange |
| `POST /oauth/register` | Dynamic client registration (RFC 7591) |
| `GET /.well-known/oauth-authorization-server` | OAuth metadata (RFC 8414) |
| `GET /.well-known/oauth-protected-resource` | Protected resource metadata (RFC 9728) |
| `GET /health` | Health check |
## Troubleshooting
<details>
<summary><b>Missing configuration error on startup</b></summary>
The server needs either API token or OAuth credentials. Set one of:
```bash
# Stdio mode
ZENDESK_SUBDOMAIN=... ZENDESK_EMAIL=... ZENDESK_API_TOKEN=...
# HTTP mode
ZENDESK_SUBDOMAIN=... ZENDESK_OAUTH_CLIENT_ID=... ZENDESK_OAUTH_CLIENT_SECRET=...
```
</details>
<details>
<summary><b>401 Unauthorized (OAuth / HTTP mode)</b></summary>
- Complete OAuth flow: visit `http://localhost:3030/oauth/authorize`
- Check if token expired (24-hour TTL) — re-authorize if needed
- Ensure Bearer token is included: `Authorization: Bearer mcp_xxx`
- Token format should start with `mcp_`
</details>
<details>
<summary><b>401 Unauthorized (API token / stdio mode)</b></summary>
- Verify `ZENDESK_EMAIL` is correct
- Verify `ZENDESK_API_TOKEN` is a valid API token (not a password)
- Verify `ZENDESK_SUBDOMAIN` is correct
- Check the connection test output in stderr on startup
</details>
<details>
<summary><b>Session lost after server restart (HTTP mode)</b></summary>
In-memory sessions are cleared on restart. Re-authorize to get a new token. For production, implement a Redis-based session store (see `src/auth/session-store.js`).
</details>
<details>
<summary><b>AI analysis features not working</b></summary>
Set `ANTHROPIC_API_KEY` in your environment. This is only needed for `analyze_ticket_images`, `analyze_ticket_documents`, and `get_document_summary`.
</details>
<details>
<summary><b>Rate limiting (429 errors)</b></summary>
The server includes exponential backoff retry logic. If you hit rate limits frequently, consider using `MODE=lite` to reduce API calls, or check if multiple clients share the same credentials.
</details>
## Contributing
1. Fork the repository
2. Create your feature branch (`git checkout -b feature/my-feature`)
3. Run tests (`npm test`)
4. Commit your changes
5. Open a Pull Request
## License
MIT License — see [LICENSE](LICENSE) for details.
## Acknowledgments
- Original implementation by [@mattcoatsworth](https://github.com/mattcoatsworth)
- Built with [Model Context Protocol](https://modelcontextprotocol.com)
- AI features powered by [Anthropic Claude](https://anthropic.com)
---
<div align="center">
Made with care by [SShadowS](https://github.com/SShadowS)
</div>
TDQS
Scored across 55 tools
Tools are organized by resource (groups, organizations, tickets, users, macros, views, automations, triggers, articles) and each has a clear responsibility. The only mild overlaps are list_tickets vs search and the three document-analysis tools (analyze_ticket_documents, get_document_summary, analyze_ticket_images), but descriptions clarify when to use which.
The vast majority follow a consistent verb_noun pattern (create_organization, update_ticket, delete_macro, list_views). A few exceptions like add_ticket_comment, support_info, get_talk_stats, list_chats, and get_document_summary deviate slightly but are still intuitive and readable.
55 tools is far above the 25-tool threshold and spans Support, Help Center, Talk, Chat, and AI-analysis features. While each tool has a purpose, the surface is large enough that agents will spend significant effort just discovering and selecting the right tool.
Core CRUD is covered for tickets, users, organizations, groups, macros, views, automations, triggers, and articles, plus search and AI analysis. Gaps exist: group memberships are explicitly not exposed, article sections cannot be listed/created, execute_view is missing, and there are no tools for satisfaction ratings or ticket metrics.