# Directory Structure ``` ├── .gitignore ├── LICENSE ├── package.json ├── README.md ├── src │ ├── index.ts │ └── tools │ └── index.ts └── tsconfig.json ``` # Files -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- ``` # Dependencies node_modules/ package-lock.json # Build dist/ build/ # TypeScript *.tsbuildinfo # IDE .vscode/ .idea/ # Environment .env .env.local .env.*.local # Logs *.log npm-debug.log* # OS .DS_Store Thumbs.db ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- ```markdown # Zendesk MCP Server [](https://www.npmjs.com/package/zd-mcp-server) [](https://opensource.org/licenses/MIT) A Model Context Protocol (MCP) server that provides AI assistants like Claude with seamless integration to Zendesk Support. Enables natural language interactions with Zendesk tickets, allowing you to search, create, update, and manage support tickets through conversational AI. ## ✨ Features - 🎫 **Complete Ticket Management**: Create, read, update, and search Zendesk tickets - 💬 **Comments & Notes**: Add public comments and private internal notes - 🔍 **Advanced Search**: Search tickets using Zendesk's powerful query syntax - 🔗 **Incident Management**: Retrieve and manage linked incident tickets - 🏷️ **Tag Management**: Add and manage ticket tags and metadata - 🔒 **Secure Authentication**: Uses Zendesk API tokens for secure access - 🚀 **Easy Installation**: Available via npm, npx, or manual setup ## 🚀 Quick Start ### Option 1: NPM Installation (Recommended) ```bash npm install -g zd-mcp-server ``` ### Option 2: Use with npx (No Installation) ```bash npx zd-mcp-server ``` ### Option 3: Development Setup ```bash git clone https://github.com/koundinya/zd-mcp-server.git cd zd-mcp-server npm install npm run build ``` ## ⚙️ Configuration ### Environment Variables Set these environment variables in your system or MCP client configuration: ```bash export ZENDESK_EMAIL="[email protected]" export ZENDESK_TOKEN="your-zendesk-api-token" export ZENDESK_SUBDOMAIN="your-company" # from https://your-company.zendesk.com ``` ### Claude Desktop Setup Add to your Claude Desktop configuration file: **Location:** - **macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json` - **Windows**: `%APPDATA%/Claude/claude_desktop_config.json` **Configuration:** ```json { "mcpServers": { "zendesk": { "command": "npx", "args": ["-y", "zd-mcp-server"], "env": { "ZENDESK_EMAIL": "[email protected]", "ZENDESK_TOKEN": "your-zendesk-api-token", "ZENDESK_SUBDOMAIN": "your-company" } } } } ``` **Alternative (if installed globally):** ```json { "mcpServers": { "zendesk": { "command": "zd-mcp-server", "env": { "ZENDESK_EMAIL": "[email protected]", "ZENDESK_TOKEN": "your-zendesk-api-token", "ZENDESK_SUBDOMAIN": "your-company" } } } } ``` ### Cursor IDE Setup Add to `~/.cursor/mcp.json` or `.cursor/mcp.json` in your project: ```json { "mcpServers": { "zendesk": { "command": "npx", "args": ["-y", "zd-mcp-server"], "env": { "ZENDESK_EMAIL": "[email protected]", "ZENDESK_TOKEN": "your-zendesk-api-token", "ZENDESK_SUBDOMAIN": "your-company" } } } } ``` ### Other MCP Clients For other MCP-compatible clients (Cline, Windsurf, etc.), refer to their documentation for MCP server configuration. The server supports standard MCP protocols. ## 🛠️ Available Tools | Tool | Description | Example Usage | |------|-------------|---------------| | `zendesk_get_ticket` | Retrieve a ticket by ID | "Get ticket #12345" | | `zendesk_get_ticket_details` | Get detailed ticket with comments | "Show me full details for ticket #67890" | | `zendesk_search` | Search tickets with query syntax | "Find all urgent tickets from last week" | | `zendesk_create_ticket` | Create a new ticket | "Create a high priority ticket for login issues" | | `zendesk_update_ticket` | Update ticket properties | "Set ticket #555 to solved status" | | `zendesk_add_private_note` | Add internal agent notes | "Add a private note about investigation progress" | | `zendesk_add_public_note` | Add public customer comments | "Reply to customer with solution steps" | | `zendesk_get_linked_incidents` | Get incident tickets linked to problems | "Show incidents related to this problem ticket" | ## 💬 Usage Examples Once configured, you can use natural language with your AI assistant: ### Ticket Management ``` "Show me all high priority tickets assigned to me" "Create a new ticket: Customer can't access dashboard, priority urgent" "Update ticket #12345 status to pending and add a note about waiting for customer response" ``` ### Search & Discovery ``` "Find all solved tickets from this week tagged with 'billing'" "Search for open tickets containing 'password reset'" "Show me tickets created by [email protected] in the last 30 days" ``` ### Customer Communication ``` "Add a public comment to ticket #789: 'We've identified the issue and working on a fix'" "Add a private note: 'Customer confirmed the workaround is effective'" ``` ### Advanced Queries ``` "Find all problem tickets that have linked incidents" "Show me escalated tickets that haven't been updated in 2 days" "Get details for ticket #456 including all comments and history" ``` ## 🔑 Authentication Setup ### 1. Generate API Token 1. Log in to your Zendesk account 2. Go to **Admin Center** → **Apps and integrations** → **APIs** → **Zendesk API** 3. Click **Add API token** 4. Add description: "MCP Server Integration" 5. Click **Create** and copy the token 6. **Important**: Save this token securely - you won't see it again ### 2. Find Your Subdomain Your Zendesk URL format: `https://YOUR-SUBDOMAIN.zendesk.com` Use `YOUR-SUBDOMAIN` as the `ZENDESK_SUBDOMAIN` value. ### 3. Required Permissions Ensure your Zendesk user account has: - **Agent** role (minimum) - **Ticket access** permissions - **API access** enabled ## 🔧 Development ### Project Structure ``` zd-mcp-server/ ├── src/ │ ├── index.ts # Server entry point │ └── tools/ │ └── index.ts # Zendesk tool implementations ├── dist/ # Compiled JavaScript ├── package.json ├── tsconfig.json └── README.md ``` ### Building from Source ```bash git clone https://github.com/koundinya/zd-mcp-server.git cd zd-mcp-server npm install npm run build ``` ### Running Locally ```bash # Start the server npm start # Development mode with auto-rebuild npm run dev ``` ### Testing ```bash # Test with MCP Inspector (if available) npx @modelcontextprotocol/inspector zd-mcp-server # Or test the built version npx @modelcontextprotocol/inspector node dist/index.js ``` ## 🔍 Troubleshooting ### Common Issues **❌ "Authentication failed" errors** - Verify your API token is correct and hasn't expired - Ensure your email address matches your Zendesk account - Check that your subdomain is spelled correctly (no `.zendesk.com` suffix) **❌ "Permission denied" errors** - Verify your Zendesk user has Agent permissions or higher - Ensure API access is enabled for your account - Check if your token has the required scopes **❌ "Server not found" errors** - Ensure you've installed the package: `npm install -g zd-mcp-server` - Try using npx instead: `npx zd-mcp-server` - Check that your MCP client configuration file syntax is correct **❌ "Environment variables not set" errors** - Verify all three environment variables are set: `ZENDESK_EMAIL`, `ZENDESK_TOKEN`, `ZENDESK_SUBDOMAIN` - Restart your MCP client after setting environment variables - Check for typos in environment variable names ### Debug Mode Enable debug logging: ```bash DEBUG=zd-mcp-server:* zd-mcp-server ``` ### Log Files Check MCP client logs: - **Claude Desktop**: `~/Library/Logs/Claude/` (macOS) or `%APPDATA%/Claude/logs/` (Windows) - **Cursor**: Check the output panel for MCP server logs - **Terminal**: Run server directly to see real-time logs ## 📚 Advanced Usage ### Search Query Syntax Zendesk search supports powerful query operators: ```bash # Status-based searches status:open status:pending status:solved # Priority searches priority:urgent priority:high priority:normal priority:low # Date-based searches created>2024-01-01 updated<2024-01-31 # Tag searches tags:billing tags:technical-issue # Requester searches requester:[email protected] # Complex combinations status:open priority:high created>2024-01-01 tags:billing ``` ### Batch Operations While the server doesn't directly support batch operations, you can chain commands: ``` "Search for all urgent tickets, then show me details for the first 3 results" "Find tickets tagged 'billing', update them to normal priority, and add a note about the billing system maintenance" ``` ## 🤝 Contributing Contributions are welcome! Please feel free to submit a Pull Request. ### Development Setup 1. Fork the repository 2. Create your feature branch (`git checkout -b feature/amazing-feature`) 3. Commit your changes (`git commit -m 'Add amazing feature'`) 4. Push to the branch (`git push origin feature/amazing-feature`) 5. Open a Pull Request ### Reporting Issues Found a bug? Please open an issue with: - Description of the problem - Steps to reproduce - Expected behavior - Your environment (OS, Node.js version, MCP client) - Relevant log outputs ## 📄 License This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. ## 🔗 Links - **GitHub**: https://github.com/koundinya/zd-mcp-server - **npm**: https://www.npmjs.com/package/zd-mcp-server - **Zendesk API Docs**: https://developer.zendesk.com/api-reference/ - **Model Context Protocol**: https://modelcontextprotocol.io/ ## 🆘 Support - **Issues**: [GitHub Issues](https://github.com/koundinya/zd-mcp-server/issues) - **Zendesk API**: [Zendesk Developer Documentation](https://developer.zendesk.com/) - **MCP Protocol**: [MCP Documentation](https://modelcontextprotocol.io/docs/) --- Made with ❤️ for the MCP and Zendesk communities ``` -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- ```json { "compilerOptions": { "target": "ES2020", "module": "ES2020", "moduleResolution": "node", "outDir": "./dist", "rootDir": "./src", "strict": true, "esModuleInterop": true, "skipLibCheck": true }, "include": ["src/**/*"] } ``` -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- ```json { "name": "zd-mcp-server", "version": "0.5.0", "description": "Zendesk MCP Server - Model Context Protocol server for Zendesk Support integration", "main": "dist/index.js", "bin": { "zd-mcp-server": "dist/index.js" }, "type": "module", "keywords": [ "mcp", "model-context-protocol", "zendesk", "ai", "claude", "support", "tickets" ], "author": "Girish Koundinya", "license": "MIT", "repository": { "type": "git", "url": "https://github.com/koundinya/zd-mcp-server.git" }, "bugs": { "url": "https://github.com/koundinya/zd-mcp-server/issues" }, "homepage": "https://github.com/koundinya/zd-mcp-server#readme", "dependencies": { "@modelcontextprotocol/sdk": "^1.6.1", "node-zendesk": "^2.1.0", "zod": "^3.24.2" }, "devDependencies": { "@types/node": "^20.0.0", "@types/node-zendesk": "^2.0.15", "typescript": "^5.0.0", "vitest": "^1.0.0" }, "scripts": { "build": "tsc", "start": "node dist/index.js", "dev": "tsc --watch", "prepublishOnly": "npm run build" }, "files": [ "dist/**/*", "README.md", "LICENSE" ], "engines": { "node": ">=18.0.0" } } ``` -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- ```typescript #!/usr/bin/env node import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { zenDeskTools, createZendeskClient, searchTickets, getTicket, getTicketDetails, getLinkedIncidents } from "./tools/index.js"; // Re-export the functions for library usage export { createZendeskClient, searchTickets, getTicket, getTicketDetails, getLinkedIncidents } from "./tools/index.js"; export type { ZendeskConfig } from "./tools/index.js"; import { fileURLToPath } from "url"; import { dirname, resolve } from "path"; import { readFileSync } from "fs"; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); const packageJson = JSON.parse( readFileSync(resolve(__dirname, "../package.json"), "utf8") ); const VERSION = packageJson.version; async function main() { const server = new McpServer( { name: "zendesk-mcp", version: VERSION, }, { capabilities: { logging: {}, }, } ); zenDeskTools(server); const transport = new StdioServerTransport(); await server.connect(transport); console.error(`Zendesk MCP Server v${VERSION} running on stdio`); } main().catch((error) => { console.error("Fatal error in main():", error); process.exit(1); }); ``` -------------------------------------------------------------------------------- /src/tools/index.ts: -------------------------------------------------------------------------------- ```typescript import type * as ZendeskTypes from "node-zendesk"; import zendesk from "node-zendesk"; import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { z } from "zod"; // Types for exported functions export interface ZendeskConfig { email: string; token: string; subdomain: string; } // Create Zendesk client export function createZendeskClient(config: ZendeskConfig) { return zendesk.createClient({ username: config.email, token: config.token, remoteUri: `https://${config.subdomain}.zendesk.com/api/v2`, }); } // Exported read-only tool functions export async function getTicket(client: any, ticketId: number): Promise<any> { return new Promise((resolve, reject) => { client.tickets.show(ticketId, (error: Error | undefined, req: any, result: any) => { if (error) { reject(error); } else { resolve(result); } }); }); } export async function searchTickets(client: any, query: string): Promise<any> { return new Promise((resolve, reject) => { client.search.query(query, (error: Error | undefined, req: any, result: any) => { if (error) { reject(error); } else { resolve(result); } }); }); } export async function getTicketDetails(client: any, ticketId: number): Promise<any> { const ticketResult = await getTicket(client, ticketId); const commentsResult = await new Promise((resolve, reject) => { client.tickets.getComments(ticketId, (error: Error | undefined, req: any, result: any) => { if (error) { reject(error); } else { resolve(result); } }); }); return { ticket: ticketResult, comments: commentsResult }; } export async function getLinkedIncidents(client: any, ticketId: number): Promise<any> { return new Promise((resolve, reject) => { client.tickets.listIncidents(ticketId, (error: Error | undefined, req: any, result: any) => { if (error) { reject(error); } else { resolve(result); } }); }); } // Environment-based client for backward compatibility if (!process.env.ZENDESK_EMAIL || !process.env.ZENDESK_TOKEN || !process.env.ZENDESK_SUBDOMAIN) { throw new Error('Missing required environment variables: ZENDESK_EMAIL, ZENDESK_TOKEN, ZENDESK_SUBDOMAIN'); } const client = zendesk.createClient({ username: process.env.ZENDESK_EMAIL as string, token: process.env.ZENDESK_TOKEN as string, remoteUri: `https://${process.env.ZENDESK_SUBDOMAIN}.zendesk.com/api/v2`, }); export function zenDeskTools(server: McpServer) { server.tool( "zendesk_get_ticket", "Get a Zendesk ticket by ID", { ticket_id: z.string().describe("The ID of the ticket to retrieve"), }, async ({ ticket_id }) => { try { const result = await getTicket(client, parseInt(ticket_id, 10)); return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] }; } catch (error: any) { return { content: [{ type: "text", text: `Error: ${error.message || 'Unknown error occurred'}` }], isError: true }; } } ); server.tool( "zendesk_update_ticket", "Update a Zendesk ticket's properties", { ticket_id: z.string().describe("The ID of the ticket to update"), subject: z.string().optional().describe("The new subject of the ticket"), status: z.enum(['new', 'open', 'pending', 'hold', 'solved', 'closed']).optional().describe("The new status of the ticket"), priority: z.enum(['low', 'normal', 'high', 'urgent']).optional().describe("The new priority of the ticket"), type: z.enum(['problem', 'incident', 'question', 'task']).optional().describe("The new type of the ticket"), assignee_id: z.string().optional().describe("The ID of the agent to assign the ticket to"), tags: z.array(z.string()).optional().describe("Tags to set on the ticket (replaces existing tags)") }, async ({ ticket_id, subject, status, priority, type, assignee_id, tags }) => { try { const ticketData: any = { ticket: {} }; // Only add properties that are provided if (subject) ticketData.ticket.subject = subject; if (status) ticketData.ticket.status = status; if (priority) ticketData.ticket.priority = priority; if (type) ticketData.ticket.type = type; if (assignee_id) ticketData.ticket.assignee_id = parseInt(assignee_id, 10); if (tags) ticketData.ticket.tags = tags; const result = await new Promise((resolve, reject) => { (client as any).tickets.update(parseInt(ticket_id, 10), ticketData, (error: Error | undefined, req: any, result: any) => { if (error) { console.log(error); reject(error); } else { resolve(result); } }); }); return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] }; } catch (error: any) { return { content: [{ type: "text", text: `Error: ${error.message || 'Unknown error occurred'}` }], isError: true }; } } ); server.tool( "zendesk_create_ticket", "Create a new Zendesk ticket", { subject: z.string().describe("The subject of the ticket"), description: z.string().describe("The initial description or comment for the ticket"), priority: z.enum(['low', 'normal', 'high', 'urgent']).optional().describe("The priority of the ticket"), status: z.enum(['new', 'open', 'pending', 'hold', 'solved', 'closed']).optional().describe("The status of the ticket"), type: z.enum(['problem', 'incident', 'question', 'task']).optional().describe("The type of the ticket"), tags: z.array(z.string()).optional().describe("Tags to add to the ticket") }, async ({ subject, description, priority, status, type, tags }) => { try { const ticketData: any = { ticket: { subject, comment: { body: description }, } }; if (priority) ticketData.ticket.priority = priority; if (status) ticketData.ticket.status = status; if (type) ticketData.ticket.type = type; if (tags) ticketData.ticket.tags = tags; const result = await new Promise((resolve, reject) => { (client as any).tickets.create(ticketData, (error: Error | undefined, req: any, result: any) => { if (error) { console.log(error); reject(error); } else { resolve(result); } }); }); return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] }; } catch (error: any) { return { content: [{ type: "text", text: `Error: ${error.message || 'Unknown error occurred'}` }], isError: true }; } } ); server.tool( "zendesk_add_private_note", "Add a private internal note to a Zendesk ticket", { ticket_id: z.string().describe("The ID of the ticket to add a note to"), note: z.string().describe("The content of the private note") }, async ({ ticket_id, note }) => { try { const result = await new Promise((resolve, reject) => { (client as any).tickets.update(parseInt(ticket_id, 10), { ticket: { comment: { body: note, public: false } } }, (error: Error | undefined, req: any, result: any) => { if (error) { console.log(error); reject(error); } else { resolve(result); } }); }); return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] }; } catch (error: any) { return { content: [{ type: "text", text: `Error: ${error.message || 'Unknown error occurred'}` }], isError: true }; } } ); server.tool( "zendesk_add_public_note", "Add a public comment to a Zendesk ticket", { ticket_id: z.string().describe("The ID of the ticket to add a comment to"), comment: z.string().describe("The content of the public comment") }, async ({ ticket_id, comment }) => { try { const result = await new Promise((resolve, reject) => { (client as any).tickets.update(parseInt(ticket_id, 10), { ticket: { comment: { body: comment, public: true } } }, (error: Error | undefined, req: any, result: any) => { if (error) { console.log(error); reject(error); } else { resolve(result); } }); }); return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] }; } catch (error: any) { return { content: [{ type: "text", text: `Error: ${error.message || 'Unknown error occurred'}` }], isError: true }; } } ); server.tool( "zendesk_search", "Search for Zendesk tickets based on a query", { query: z.string().describe("Search query (e.g., 'status:open', 'priority:urgent', 'tags:need_help')"), }, async ({ query }) => { try { const result = await searchTickets(client, query); return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] }; } catch (error: any) { return { content: [{ type: "text", text: `Error: ${error.message || 'Unknown error occurred'}` }], isError: true }; } } ); server.tool( "zendesk_get_ticket_details", "Get detailed information about a Zendesk ticket including comments", { ticket_id: z.string().describe("The ID of the ticket to retrieve details for"), }, async ({ ticket_id }) => { try { const result = await getTicketDetails(client, parseInt(ticket_id, 10)); return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] }; } catch (error: any) { return { content: [{ type: "text", text: `Error: ${error.message || 'Unknown error occurred'}` }], isError: true }; } } ); server.tool( "zendesk_get_linked_incidents", "Fetch all incident tickets linked to a particular ticket", { ticket_id: z.string().describe("The ID of the ticket to retrieve linked incidents for"), }, async ({ ticket_id }) => { try { const result = await getLinkedIncidents(client, parseInt(ticket_id, 10)); return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] }; } catch (error: any) { return { content: [{ type: "text", text: `Error: ${error.message || 'Unknown error occurred'}` }], isError: true }; } } ); } ```