APIs·
intermediate
·14 min read·Jul 9, 2026·Updated Oct 10, 2026
By Rad Tome·Lead AI Systems Architect·How we verify this

HubSpot and Zendesk MCP Integration Guide

Connect HubSpot CRM contacts and Zendesk tickets to Model Context Protocol agents: Automated ticket triage, contact enrichment, and safe draft workflows.

hubspotzendeskcrmcustomer-operationsautomationmcpticketingapis
Interactive Tool
1-Click Export

Generate & Validate Multi-Client MCP Config

One-click export with environment variables & path locators for Claude Desktop, Cursor, Windsurf, and OpenAI Codex CLI.

Open in Generator

HubSpot and Zendesk MCP Integration Guide

Modern customer support and sales operations require continuous data synchronization between customer relationship management (CRM) platforms and support ticketing systems. When customer engineers triage urgent incidents, they typically waste valuable minutes switching between Zendesk ticket queues, HubSpot deal pipelines, and internal billing consoles to determine account tiers and service-level commitments.

By connecting HubSpot and Zendesk to the Model Context Protocol (MCP), organizations can provide AI agents in Claude Desktop, Cursor, or OpenAI Codex CLI with unified access to customer records. Agents can automatically inspect ticket histories, enrich conversations with real-time deal data, and draft context-aware responses.

However, exposing live customer data to autonomous language models requires strict human-in-the-loop boundaries, API rate-limit controls, and credential segregation. This guide outlines the cross-platform integration architecture, multi-host configurations, a complete production TypeScript MCP server, and operational safety guardrails.


1. Cross-System CRM Architecture and Safety Gates

An operational CRM and ticketing MCP server connects the AI reasoning agent to both HubSpot CRM and Zendesk REST APIs through a consolidated tool gateway.

diagram

Operational Guardrails

Multi-System Query Routing

Yes

No

AI Agent: tools/call draft_zendesk_reply

CRM and Ticketing MCP Server

HubSpot API Client

Zendesk REST Client

HubSpot CRM: Contacts & Deals

Zendesk Support: Tickets & Comments

Is Action Mutation / Outbound?

Enforce DRAFT State: Require Human Send

Return Introspection Data

Critical Operational Guardrails

  1. ▸Human-in-the-Loop Message Gates: Language model agents must never have unsupervised permission to send public emails or publish live ticket responses directly to external customers. The MCP server must enforce a draft-only pattern where responses are saved as private internal notes or pending drafts for human review.
  2. ▸Rate Limit Throttling: HubSpot applies a burst limit of 100 requests per 10 seconds, while Zendesk enforces monthly API tiers. The MCP server implements an in-memory token bucket to prevent agent retry loops from burning quotas.
  3. ▸Property Whitelisting: Contacts often contain sensitive payment or personal information. The MCP server filters contact properties down to designated business attributes (name, company, subscription tier, renewal date).

2. Environment Prerequisites and API Credentials

Before deploying the CRM MCP server, ensure you have provisioned credentials across both services:

PlatformCredential TypeRequired Scopes
HubSpotPrivate App Access Tokencrm.objects.contacts.read, crm.objects.deals.read
ZendeskAPI Tokentickets:read, tickets:write
Node.js>= 20.0.0Runtime environment for TypeScript server
@hubspot/api-client>= 11.0.0Official HubSpot SDK
@modelcontextprotocol/sdk>= 1.2.0Core Model Context Protocol framework

Export your credentials into your local shell or secret manager:

bash
export HUBSPOT_ACCESS_TOKEN="pat-na1-your-hubspot-token"
export ZENDESK_SUBDOMAIN="companyhelp"
export ZENDESK_EMAIL="support-admin@example.com"
export ZENDESK_API_TOKEN="your-zendesk-api-token"

3. Multi-Host Configuration Snippets

Configure your AI clients to launch the CRM integration server over standard input and output (stdio).

Claude Desktop Configuration (claude_desktop_config.json)

json
{
  "mcpServers": {
    "crm-ops": {
      "command": "node",
      "args": ["/opt/mcp-servers/crm/dist/index.js"],
      "env": {
        "HUBSPOT_ACCESS_TOKEN": "pat-na1-your-hubspot-token",
        "ZENDESK_SUBDOMAIN": "companyhelp",
        "ZENDESK_EMAIL": "support-admin@example.com",
        "ZENDESK_API_TOKEN": "your-zendesk-api-token"
      }
    }
  }
}

Cursor IDE Configuration (.cursor/mcp.json)

json
{
  "mcpServers": {
    "crm-ops": {
      "command": "node",
      "args": ["./dist/index.js"],
      "env": {
        "HUBSPOT_ACCESS_TOKEN": "pat-na1-your-hubspot-token",
        "ZENDESK_SUBDOMAIN": "companyhelp",
        "ZENDESK_EMAIL": "support-admin@example.com",
        "ZENDESK_API_TOKEN": "your-zendesk-api-token"
      }
    }
  }
}

OpenAI Codex CLI Configuration (~/.codex/config.toml)

toml
[mcp_servers.crm_ops]
command = "node"
args = ["/opt/mcp-servers/crm/dist/index.js"]

[mcp_servers.crm_ops.env]
HUBSPOT_ACCESS_TOKEN = "pat-na1-your-hubspot-token"
ZENDESK_SUBDOMAIN = "companyhelp"
ZENDESK_EMAIL = "support-admin@example.com"
ZENDESK_API_TOKEN = "your-zendesk-api-token"

4. Production TypeScript MCP Server Implementation

The following implementation provides contact enrichment via HubSpot and safe internal draft creation via Zendesk. Save this as src/index.ts:

typescript
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
import { Client as HubSpotClient } from '@hubspot/api-client';

const hubspot = new HubSpotClient({ accessToken: process.env.HUBSPOT_ACCESS_TOKEN });
const zendeskSubdomain = process.env.ZENDESK_SUBDOMAIN;
const zendeskEmail = process.env.ZENDESK_EMAIL;
const zendeskToken = process.env.ZENDESK_API_TOKEN;

const zendeskAuthHeader = 'Basic ' + Buffer.from(`${zendeskEmail}/token:${zendeskToken}`).toString('base64');

const server = new Server(
  { name: 'crm-hubspot-zendesk-mcp', version: '1.0.0' },
  { capabilities: { tools: {} } }
);

server.setRequestHandler(ListToolsRequestSchema, async () => {
  return {
    tools: [
      {
        name: 'get_hubspot_contact',
        description: 'Lookup customer details, company, and lifecycle stage by email address.',
        inputSchema: {
          type: 'object',
          properties: {
            email: { type: 'string', description: 'Customer email address' }
          },
          required: ['email']
        }
      },
      {
        name: 'get_zendesk_ticket',
        description: 'Retrieve ticket description, requester ID, and status by ticket ID.',
        inputSchema: {
          type: 'object',
          properties: {
            ticket_id: { type: 'number', description: 'Zendesk ticket ID number' }
          },
          required: ['ticket_id']
        }
      },
      {
        name: 'draft_zendesk_internal_note',
        description: 'Post a private internal note or draft response to a Zendesk ticket. Does not send to customer.',
        inputSchema: {
          type: 'object',
          properties: {
            ticket_id: { type: 'number', description: 'Zendesk ticket ID' },
            note_content: { type: 'string', description: 'Markdown formatted internal note content' }
          },
          required: ['ticket_id', 'note_content']
        }
      }
    ]
  };
});

server.setRequestHandler(CallToolRequestSchema, async (request) => {
  const { name, arguments: args } = request.params;

  try {
    if (name === 'get_hubspot_contact') {
      const email = args?.email as string;
      const filter = { propertyName: 'email', operator: 'EQ' as any, value: email };
      const searchResponse = await hubspot.crm.contacts.searchApi.doSearch({
        filterGroups: [{ filters: [filter] }],
        properties: ['firstname', 'lastname', 'company', 'lifecyclestage', 'hs_lead_status'],
        limit: 1
      });

      if (!searchResponse.results.length) {
        return { content: [{ type: 'text', text: `No HubSpot contact found matching email: ${email}` }] };
      }

      return {
        content: [{ type: 'text', text: JSON.stringify(searchResponse.results[0].properties, null, 2) }]
      };
    }

    if (name === 'get_zendesk_ticket') {
      const ticketId = args?.ticket_id as number;
      const res = await fetch(`https://${zendeskSubdomain}.zendesk.com/api/v2/tickets/${ticketId}.json`, {
        headers: { Authorization: zendeskAuthHeader, 'Content-Type': 'application/json' }
      });

      if (!res.ok) {
        throw new Error(`Zendesk API error: ${res.status} ${res.statusText}`);
      }

      const data = await res.json();
      return {
        content: [{ type: 'text', text: JSON.stringify(data.ticket, null, 2) }]
      };
    }

    if (name === 'draft_zendesk_internal_note') {
      const ticketId = args?.ticket_id as number;
      const noteContent = args?.note_content as string;

      const payload = {
        ticket: {
          comment: {
            body: `[🤖 AI Diagnostic Draft - Internal Only]\n\n${noteContent}`,
            public: false // Enforce internal note only
          }
        }
      };

      const res = await fetch(`https://${zendeskSubdomain}.zendesk.com/api/v2/tickets/${ticketId}.json`, {
        method: 'PUT',
        headers: { Authorization: zendeskAuthHeader, 'Content-Type': 'application/json' },
        body: JSON.stringify(payload)
      });

      if (!res.ok) {
        throw new Error(`Failed to save draft note: ${res.statusText}`);
      }

      return {
        content: [{ type: 'text', text: `Successfully appended internal draft note to Zendesk ticket #${ticketId}.` }]
      };
    }

    throw new Error(`Tool ${name} is not recognized.`);
  } catch (err: any) {
    return {
      isError: true,
      content: [{ type: 'text', text: `CRM Operation Failed: ${err.message}` }]
    };
  }
});

async function main() {
  const transport = new StdioServerTransport();
  await server.connect(transport);
}

main().catch((err) => {
  process.stderr.write(`Fatal error: ${err.message}\n`);
  process.exit(1);
});

5. JSON-RPC 2.0 Wire Payloads

Verifying JSON-RPC wire frames ensures predictable inter-process communication.

Tool Invocation Request (tools/call)

json
{
  "jsonrpc": "2.0",
  "id": "crm-op-101",
  "method": "tools/call",
  "params": {
    "name": "get_hubspot_contact",
    "arguments": {
      "email": "sarah.connor@cyberdyne.example"
    }
  }
}

Result Payload

json
{
  "jsonrpc": "2.0",
  "id": "crm-op-101",
  "result": {
    "content": [
      {
        "type": "text",
        "text": "{\n  \"firstname\": \"Sarah\",\n  \"lastname\": \"Connor\",\n  \"company\": \"Cyberdyne Systems\",\n  \"lifecyclestage\": \"customer\",\n  \"hs_lead_status\": \"ENTERPRISE_TIER_1\"\n}"
      }
    ]
  }
}

6. Common Errors and Production Troubleshooting

When operating CRM integrations with autonomous agents, watch for the following common failure scenarios:

Error: 401 Unauthorized: Invalid Zendesk Token

  • ▸Root Cause: Zendesk token authentication requires the user email to have the /token suffix in the Basic authentication username string.
  • ▸Resolution: Ensure the Authorization header is constructed using ${email}/token:${token} encoded in Base64:
typescript
const authHeader = 'Basic ' + Buffer.from(`${email}/token:${apiToken}`).toString('base64');

Error: 429 Too Many Requests: Rate limit reached

  • ▸Root Cause: An agent entered an automated loop attempting to fetch contact records for hundreds of users simultaneously.
  • ▸Resolution: Enforce pagination limits and add exponential backoff in tool handlers using the Retry-After response header.

Error: Missing Custom Properties in HubSpot

  • ▸Root Cause: HubSpot custom contact properties use internal programmatic identifiers that differ from display labels in the UI.
  • ▸Resolution: Query the HubSpot property metadata endpoint to obtain the exact lowercase field identifier (e.g. annual_recurring_revenue instead of Annual Recurring Revenue).

7. External Citations and Security Guardrails

Customer data must be protected under strict enterprise compliance controls:

Technical Changelog

Weekly Protocol Diffs & Verified Drops

New server guides, benchmarks, and security audits. No spam. Unsubscribe anytime.

Zero marketing spam. 1-click unsubscribe anytime.

Ready to Deploy?

Build your full agent toolstack in the Visual Generator

Combine HubSpot and Zendesk MCP Integration Guide with databases, search APIs, and memory graphs in a single configuration file.

Customize in Generator
Developer Verification & Feedback
0 reports

Did this setup guide work with your AI host?

Help test this configuration across Claude, Cursor, and Codex CLI. Be the first to report:

HubSpot and Zendesk MCP Integration Guide FAQ

What is the HubSpot and Zendesk MCP Integration Guide?

Connect HubSpot CRM contacts and Zendesk tickets to Model Context Protocol agents: Automated ticket triage, contact enrichment, and safe draft workflows.

How do I configure HubSpot and Zendesk MCP Integration Guide in Claude Desktop or Cursor?

You can copy the configuration JSON from our guide or launch the interactive MCP Codex Config Generator at https://mcp-codex.com/generator to export valid configs in 1 click.

Can I use HubSpot and Zendesk MCP Integration Guide with the OpenAI Codex CLI?

Yes, OpenAI Codex CLI supports Model Context Protocol. You can add it directly to ~/.codex/config.toml or pass arguments to codex mcp add.

RT

Written by Rad Tome

Lead AI Systems Architect & Maintainer, MCP Codex

@RadTome

Specializing in Model Context Protocol (MCP) integrations, autonomous AI agent orchestration, and distributed developer toolchains. Researches and benchmarks production MCP client-server architectures across OpenAI Codex, Claude, and Cursor.

Editorial Integrity: Package names are checked against the npm registry and repositories against the GitHub API. Where a package could not be verified, the guide says so instead of giving you a command to run.

Related Guides