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