Zendesk MCP Server
A Model Context Protocol server for Zendesk API integration with AI-powered ticket analysis
Quick Start • Configuration • Tools • Architecture • Development
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 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:
{
"zendesk": {
"type": "stdio",
"command": "npx",
"args": ["@sshadows/zendesk-mcp-server"],
"env": {
"ZENDESK_SUBDOMAIN": "your-subdomain",
"ZENDESK_EMAIL": "[email protected]",
"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:
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:
npm start
# Visit http://localhost:3030/oauth/authorize in your browser
4. Use the token with your MCP client: Authorization: Bearer mcp_...
Installation
# 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):
ZENDESK_SUBDOMAIN=mycompany
[email protected]
ZENDESK_API_TOKEN=your-api-token
HTTP mode (set ZENDESK_OAUTH_CLIENT_ID):
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):
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:
PORT=3030
SERVER_BASE_URL=http://localhost:3030 # Use https:// in production
See .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.
MODE=lite npm start
Read-Only Mode
Set READ_ONLY=true to prevent the server from modifying Zendesk data:
READ_ONLY=true npm start
- Every
create_*,update_*anddelete_*tool is unregistered — 28 tools remain infullmode. add_ticket_commentstays available for internal notes only. Atype: '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-
GETrequest other than an internal-note payload. A write cannot reach the Zendesk API by another route. - Composes with
MODE.MODE=lite READ_ONLY=truestill exposes all 10 lite tools, since onlyadd_ticket_commentmutates 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:
READ_ONLY=true READ_ONLY_STRICT=true npm start
add_ticket_commentis unregistered, leaving 27 tools infullmode and 9 underMODE=lite.- The transport guard drops the internal-note exemption, so
request()permitsGETand 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
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:
ZendeskClientBasesupports bothsetApiTokenAuth()(Basic) andsetAccessToken()(Bearer). The_authModefield determines which headergetAuthHeader()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.jsandsrc/auth/*are only imported in HTTP mode. No changes were needed.
Development
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 and are in tests/ mirroring the src/ directory:
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
Troubleshooting
The server needs either API token or OAuth credentials. Set one of:
# Stdio mode
ZENDESK_SUBDOMAIN=... ZENDESK_EMAIL=... ZENDESK_API_TOKEN=...
# HTTP mode
ZENDESK_SUBDOMAIN=... ZENDESK_OAUTH_CLIENT_ID=... ZENDESK_OAUTH_CLIENT_SECRET=...
- 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_
- Verify
ZENDESK_EMAILis correct - Verify
ZENDESK_API_TOKENis a valid API token (not a password) - Verify
ZENDESK_SUBDOMAINis correct - Check the connection test output in stderr on startup
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).
Set ANTHROPIC_API_KEY in your environment. This is only needed for analyze_ticket_images, analyze_ticket_documents, and get_document_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.
Contributing
- Fork the repository
- Create your feature branch (
git checkout -b feature/my-feature) - Run tests (
npm test) - Commit your changes
- Open a Pull Request
License
MIT License — see LICENSE for details.
Acknowledgments
- Original implementation by @mattcoatsworth
- Built with Model Context Protocol
- AI features powered by Anthropic Claude
Made with care by SShadowS