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.
Generate & Validate Multi-Client MCP Config
One-click export with environment variables & path locators for Claude Desktop, Cursor, Windsurf, and OpenAI Codex CLI.
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.
Critical Operational Guardrails
- ▸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.
- ▸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.
- ▸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:
| Platform | Credential Type | Required Scopes |
|---|---|---|
| HubSpot | Private App Access Token | crm.objects.contacts.read, crm.objects.deals.read |
| Zendesk | API Token | tickets:read, tickets:write |
| Node.js | >= 20.0.0 | Runtime environment for TypeScript server |
| @hubspot/api-client | >= 11.0.0 | Official HubSpot SDK |
| @modelcontextprotocol/sdk | >= 1.2.0 | Core Model Context Protocol framework |
Export your credentials into your local shell or secret manager:
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)
{
"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)
{
"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)
[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:
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)
{
"jsonrpc": "2.0",
"id": "crm-op-101",
"method": "tools/call",
"params": {
"name": "get_hubspot_contact",
"arguments": {
"email": "sarah.connor@cyberdyne.example"
}
}
}Result Payload
{
"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
/tokensuffix in the Basic authentication username string. - ▸Resolution: Ensure the Authorization header is constructed using
${email}/token:${token}encoded in Base64:
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-Afterresponse 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_revenueinstead ofAnnual Recurring Revenue).
7. External Citations and Security Guardrails
Customer data must be protected under strict enterprise compliance controls:
- ▸Customer PII Scoping: Restrict MCP tools from pulling sensitive personal data (e.g., social security numbers or billing credit card details) into the agent context window.
- ▸Official Standards and Repositories:
Weekly Protocol Diffs & Verified Drops
New server guides, benchmarks, and security audits. No spam. Unsubscribe anytime.
Zero marketing spam. 1-click unsubscribe anytime.
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.
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.
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.
Related Guides
Alpaca Finance API MCP Server Guide
Connect Alpaca Finance API to Model Context Protocol: Stream real-time market data, execute paper trades safely, and enforce autonomous budget guardrails.
APIsAccepting Stripe's Machine Payments Protocol (MPP) in Your MCP Server
Charge agents per tool call with MPP: an MCP tool that returns a payment link, an endpoint that answers 402 with a challenge, and Shared Payment Token verification.
APIsPaying for MCP Tools with Stripe's Machine Payments Protocol (MPP)
How an MCP client pays for a tool call with MPP: the HTTP 402 challenge, Shared Payment Tokens from a Link agent wallet, and the stablecoin alternative.