HubSpot MCP
by shinzo-labs
Verified
#!/usr/bin/env node
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
import { z } from "zod"
const server = new McpServer({
name: "HubSpot-MCP",
version: "1.2.3",
description: "An extensive MCP for the HubSpot API"
})
// Unified response formatter for both success and error responses
function formatResponse(messageOrData, status = 200) {
const isError = typeof messageOrData === 'string';
return {
content: [{
type: "text",
text: JSON.stringify(isError ? { error: messageOrData, status } : messageOrData)
}]
}
}
// Helper function for making API requests to HubSpot
async function makeApiRequest(endpoint, params = {}, method = 'GET', body = null) {
const apiKey = process.env.HUBSPOT_ACCESS_TOKEN
if (!apiKey) {
throw new Error("HUBSPOT_ACCESS_TOKEN environment variable is not set")
}
const queryParams = new URLSearchParams()
Object.entries(params).forEach(([key, value]) => {
if (value !== undefined) {
queryParams.append(key, value.toString())
}
})
const url = `https://api.hubapi.com${endpoint}${queryParams.toString() ? `?${queryParams.toString()}` : ''}`
const requestOptions = {
method,
headers: {
'Accept': 'application/json',
'Authorization': `Bearer ${apiKey}`,
}
}
if (body) {
requestOptions.body = JSON.stringify(body)
requestOptions.headers['Content-Type'] = 'application/json'
}
const response = await fetch(url, requestOptions)
if (!response.ok) {
const errorData = await response.json().catch(() => null)
throw new Error(errorData?.message || `Error fetching data from HubSpot: ${response.statusText}`)
}
return await response.json()
}
// Enhanced API request wrapper with error handling
async function makeApiRequestWithErrorHandling(endpoint, params = {}, method = 'GET', body = null) {
try {
const data = await makeApiRequest(endpoint, params, method, body)
return formatResponse(data)
} catch (error) {
return formatResponse(`Error performing request: ${error.message}`, 500)
}
}
// Wrapper function to handle common endpoint patterns
async function handleEndpoint(apiCall) {
try {
return await apiCall()
} catch (error) {
return formatResponse(error.message, error.status || 403)
}
}
// Company-specific property schema
const companyPropertiesSchema = z.object({
name: z.string().optional(),
domain: z.string().optional(),
website: z.string().url().optional(),
description: z.string().optional(),
industry: z.string().optional(),
numberofemployees: z.number().optional(),
annualrevenue: z.number().optional(),
city: z.string().optional(),
state: z.string().optional(),
country: z.string().optional(),
phone: z.string().optional(),
address: z.string().optional(),
address2: z.string().optional(),
zip: z.string().optional(),
type: z.string().optional(),
lifecyclestage: z.enum(['lead', 'customer', 'opportunity', 'subscriber', 'other']).optional(),
}).catchall(z.any())
// Company-specific CRM endpoints
server.tool("crm_create_company",
"Create a new company with validated properties",
{
properties: companyPropertiesSchema,
associations: z.array(z.object({
to: z.object({ id: z.string() }),
types: z.array(z.object({
associationCategory: z.string(),
associationTypeId: z.number()
}))
})).optional()
},
async (params) => {
return handleEndpoint(async () => {
const endpoint = '/crm/v3/objects/companies'
return await makeApiRequestWithErrorHandling(endpoint, {}, 'POST', {
properties: params.properties,
associations: params.associations
})
})
}
)
server.tool("crm_update_company",
"Update an existing company with validated properties",
{
companyId: z.string(),
properties: companyPropertiesSchema
},
async (params) => {
return handleEndpoint(async () => {
const endpoint = `/crm/v3/objects/companies/${params.companyId}`
return await makeApiRequestWithErrorHandling(endpoint, {}, 'PATCH', {
properties: params.properties
})
})
}
)
server.tool("crm_get_company",
"Get a single company by ID with specific properties and associations",
{
companyId: z.string(),
properties: z.array(z.string()).optional(),
associations: z.array(z.enum(['contacts', 'deals', 'tickets'])).optional()
},
async (params) => {
return handleEndpoint(async () => {
const endpoint = `/crm/v3/objects/companies/${params.companyId}`
return await makeApiRequestWithErrorHandling(endpoint, {
properties: params.properties?.join(','),
associations: params.associations?.join(',')
})
})
}
)
server.tool("crm_search_companies",
"Search companies with company-specific filters",
{
filterGroups: z.array(z.object({
filters: z.array(z.object({
propertyName: z.string(),
operator: z.enum(['EQ', 'NEQ', 'LT', 'LTE', 'GT', 'GTE', 'BETWEEN', 'IN', 'NOT_IN', 'HAS_PROPERTY', 'NOT_HAS_PROPERTY', 'CONTAINS_TOKEN', 'NOT_CONTAINS_TOKEN']),
value: z.any()
}))
})),
properties: z.array(z.string()).optional(),
limit: z.number().min(1).max(100).optional(),
after: z.string().optional(),
sorts: z.array(z.object({
propertyName: z.string(),
direction: z.enum(['ASCENDING', 'DESCENDING'])
})).optional()
},
async (params) => {
return handleEndpoint(async () => {
const endpoint = '/crm/v3/objects/companies/search'
return await makeApiRequestWithErrorHandling(endpoint, {}, 'POST', {
filterGroups: params.filterGroups,
properties: params.properties,
limit: params.limit,
after: params.after,
sorts: params.sorts
})
})
}
)
server.tool("crm_batch_create_companies",
"Create multiple companies in a single request",
{
inputs: z.array(z.object({
properties: companyPropertiesSchema,
associations: z.array(z.object({
to: z.object({ id: z.string() }),
types: z.array(z.object({
associationCategory: z.string(),
associationTypeId: z.number()
}))
})).optional()
}))
},
async (params) => {
return handleEndpoint(async () => {
const endpoint = '/crm/v3/objects/companies/batch/create'
return await makeApiRequestWithErrorHandling(endpoint, {}, 'POST', {
inputs: params.inputs
})
})
}
)
server.tool("crm_batch_update_companies",
"Update multiple companies in a single request",
{
inputs: z.array(z.object({
id: z.string(),
properties: companyPropertiesSchema
}))
},
async (params) => {
return handleEndpoint(async () => {
const endpoint = '/crm/v3/objects/companies/batch/update'
return await makeApiRequestWithErrorHandling(endpoint, {}, 'POST', {
inputs: params.inputs
})
})
}
)
server.tool("crm_get_company_properties",
"Get all properties for companies",
{
archived: z.boolean().optional(),
properties: z.array(z.string()).optional()
},
async (params) => {
return handleEndpoint(async () => {
const endpoint = '/crm/v3/properties/companies'
return await makeApiRequestWithErrorHandling(endpoint, {
archived: params.archived,
properties: params.properties?.join(',')
})
})
}
)
server.tool("crm_create_company_property",
"Create a new company property",
{
name: z.string(),
label: z.string(),
type: z.enum(['string', 'number', 'date', 'datetime', 'enumeration', 'bool']),
fieldType: z.enum(['text', 'textarea', 'select', 'radio', 'checkbox', 'number', 'date', 'file']),
groupName: z.string(),
description: z.string().optional(),
options: z.array(z.object({
label: z.string(),
value: z.string(),
description: z.string().optional(),
displayOrder: z.number().optional(),
hidden: z.boolean().optional()
})).optional(),
displayOrder: z.number().optional(),
hasUniqueValue: z.boolean().optional(),
hidden: z.boolean().optional(),
formField: z.boolean().optional()
},
async (params) => {
return handleEndpoint(async () => {
const endpoint = '/crm/v3/properties/companies'
return await makeApiRequestWithErrorHandling(endpoint, {}, 'POST', params)
})
}
)
// CRM Object API Endpoints
server.tool("crm_list_objects",
"List CRM objects of a specific type with optional filtering and pagination",
{
objectType: z.enum(['companies', 'contacts', 'deals', 'tickets', 'products', 'line_items', 'quotes', 'custom']),
properties: z.array(z.string()).optional(),
after: z.string().optional(),
limit: z.number().min(1).max(100).optional(),
archived: z.boolean().optional()
},
async (params) => {
return handleEndpoint(async () => {
const endpoint = `/crm/v3/objects/${params.objectType}`
return await makeApiRequestWithErrorHandling(endpoint, {
properties: params.properties?.join(','),
after: params.after,
limit: params.limit,
archived: params.archived
})
})
}
)
server.tool("crm_get_object",
"Get a single CRM object by ID",
{
objectType: z.enum(['companies', 'contacts', 'deals', 'tickets', 'products', 'line_items', 'quotes', 'custom']),
objectId: z.string(),
properties: z.array(z.string()).optional(),
associations: z.array(z.string()).optional()
},
async (params) => {
return handleEndpoint(async () => {
const endpoint = `/crm/v3/objects/${params.objectType}/${params.objectId}`
return await makeApiRequestWithErrorHandling(endpoint, {
properties: params.properties?.join(','),
associations: params.associations?.join(',')
})
})
}
)
server.tool("crm_create_object",
"Create a new CRM object",
{
objectType: z.enum(['companies', 'contacts', 'deals', 'tickets', 'products', 'line_items', 'quotes', 'custom']),
properties: z.record(z.any()),
associations: z.array(z.object({
to: z.object({ id: z.string() }),
types: z.array(z.object({
associationCategory: z.string(),
associationTypeId: z.number()
}))
})).optional()
},
async (params) => {
return handleEndpoint(async () => {
const endpoint = `/crm/v3/objects/${params.objectType}`
return await makeApiRequestWithErrorHandling(endpoint, {}, 'POST', {
properties: params.properties,
associations: params.associations
})
})
}
)
server.tool("crm_update_object",
"Update an existing CRM object",
{
objectType: z.enum(['companies', 'contacts', 'deals', 'tickets', 'products', 'line_items', 'quotes', 'custom']),
objectId: z.string(),
properties: z.record(z.any())
},
async (params) => {
return handleEndpoint(async () => {
const endpoint = `/crm/v3/objects/${params.objectType}/${params.objectId}`
return await makeApiRequestWithErrorHandling(endpoint, {}, 'PATCH', {
properties: params.properties
})
})
}
)
server.tool("crm_delete_object",
"Delete a CRM object",
{
objectType: z.enum(['companies', 'contacts', 'deals', 'tickets', 'products', 'line_items', 'quotes', 'custom']),
objectId: z.string()
},
async (params) => {
return handleEndpoint(async () => {
const endpoint = `/crm/v3/objects/${params.objectType}/${params.objectId}`
return await makeApiRequestWithErrorHandling(endpoint, {}, 'DELETE')
})
}
)
server.tool("crm_search_objects",
"Search CRM objects using filters",
{
objectType: z.enum(['companies', 'contacts', 'deals', 'tickets', 'products', 'line_items', 'quotes', 'custom']),
filterGroups: z.array(z.object({
filters: z.array(z.object({
propertyName: z.string(),
operator: z.enum(['EQ', 'NEQ', 'LT', 'LTE', 'GT', 'GTE', 'BETWEEN', 'IN', 'NOT_IN', 'HAS_PROPERTY', 'NOT_HAS_PROPERTY', 'CONTAINS_TOKEN', 'NOT_CONTAINS_TOKEN']),
value: z.any()
}))
})),
properties: z.array(z.string()).optional(),
limit: z.number().min(1).max(100).optional(),
after: z.string().optional(),
sorts: z.array(z.object({
propertyName: z.string(),
direction: z.enum(['ASCENDING', 'DESCENDING'])
})).optional()
},
async (params) => {
return handleEndpoint(async () => {
const endpoint = `/crm/v3/objects/${params.objectType}/search`
return await makeApiRequestWithErrorHandling(endpoint, {}, 'POST', {
filterGroups: params.filterGroups,
properties: params.properties,
limit: params.limit,
after: params.after,
sorts: params.sorts
})
})
}
)
server.tool("crm_batch_create_objects",
"Create multiple CRM objects in a single request",
{
objectType: z.enum(['companies', 'contacts', 'deals', 'tickets', 'products', 'line_items', 'quotes', 'custom']),
inputs: z.array(z.object({
properties: z.record(z.any()),
associations: z.array(z.object({
to: z.object({ id: z.string() }),
types: z.array(z.object({
associationCategory: z.string(),
associationTypeId: z.number()
}))
})).optional()
}))
},
async (params) => {
return handleEndpoint(async () => {
const endpoint = `/crm/v3/objects/${params.objectType}/batch/create`
return await makeApiRequestWithErrorHandling(endpoint, {}, 'POST', {
inputs: params.inputs
})
})
}
)
server.tool("crm_batch_update_objects",
"Update multiple CRM objects in a single request",
{
objectType: z.enum(['companies', 'contacts', 'deals', 'tickets', 'products', 'line_items', 'quotes', 'custom']),
inputs: z.array(z.object({
id: z.string(),
properties: z.record(z.any())
}))
},
async (params) => {
return handleEndpoint(async () => {
const endpoint = `/crm/v3/objects/${params.objectType}/batch/update`
return await makeApiRequestWithErrorHandling(endpoint, {}, 'POST', {
inputs: params.inputs
})
})
}
)
server.tool("crm_batch_delete_objects",
"Delete multiple CRM objects in a single request",
{
objectType: z.enum(['companies', 'contacts', 'deals', 'tickets', 'products', 'line_items', 'quotes', 'custom']),
objectIds: z.array(z.string())
},
async (params) => {
return handleEndpoint(async () => {
const endpoint = `/crm/v3/objects/${params.objectType}/batch/archive`
return await makeApiRequestWithErrorHandling(endpoint, {}, 'POST', {
inputs: params.objectIds.map(id => ({ id }))
})
})
}
)
// CRM Associations v4 API Endpoints
server.tool("crm_list_association_types",
"List all available association types for a given object type pair",
{
fromObjectType: z.enum(['companies', 'contacts', 'deals', 'tickets', 'products', 'line_items', 'quotes', 'custom']),
toObjectType: z.enum(['companies', 'contacts', 'deals', 'tickets', 'products', 'line_items', 'quotes', 'custom'])
},
async (params) => {
return handleEndpoint(async () => {
const endpoint = `/crm/v4/associations/${params.fromObjectType}/${params.toObjectType}/types`
return await makeApiRequestWithErrorHandling(endpoint)
})
}
)
server.tool("crm_get_associations",
"Get all associations of a specific type between objects",
{
fromObjectType: z.enum(['companies', 'contacts', 'deals', 'tickets', 'products', 'line_items', 'quotes', 'custom']),
toObjectType: z.enum(['companies', 'contacts', 'deals', 'tickets', 'products', 'line_items', 'quotes', 'custom']),
fromObjectId: z.string(),
after: z.string().optional(),
limit: z.number().min(1).max(500).optional()
},
async (params) => {
return handleEndpoint(async () => {
const endpoint = `/crm/v4/objects/${params.fromObjectType}/${params.fromObjectId}/associations/${params.toObjectType}`
return await makeApiRequestWithErrorHandling(endpoint, {
after: params.after,
limit: params.limit
})
})
}
)
server.tool("crm_create_association",
"Create an association between two objects",
{
fromObjectType: z.enum(['companies', 'contacts', 'deals', 'tickets', 'products', 'line_items', 'quotes', 'custom']),
toObjectType: z.enum(['companies', 'contacts', 'deals', 'tickets', 'products', 'line_items', 'quotes', 'custom']),
fromObjectId: z.string(),
toObjectId: z.string(),
associationTypes: z.array(z.object({
associationCategory: z.string(),
associationTypeId: z.number()
}))
},
async (params) => {
return handleEndpoint(async () => {
const endpoint = `/crm/v4/objects/${params.fromObjectType}/${params.fromObjectId}/associations/${params.toObjectType}/${params.toObjectId}`
return await makeApiRequestWithErrorHandling(endpoint, {}, 'PUT', {
types: params.associationTypes
})
})
}
)
server.tool("crm_delete_association",
"Delete an association between two objects",
{
fromObjectType: z.enum(['companies', 'contacts', 'deals', 'tickets', 'products', 'line_items', 'quotes', 'custom']),
toObjectType: z.enum(['companies', 'contacts', 'deals', 'tickets', 'products', 'line_items', 'quotes', 'custom']),
fromObjectId: z.string(),
toObjectId: z.string()
},
async (params) => {
return handleEndpoint(async () => {
const endpoint = `/crm/v4/objects/${params.fromObjectType}/${params.fromObjectId}/associations/${params.toObjectType}/${params.toObjectId}`
return await makeApiRequestWithErrorHandling(endpoint, {}, 'DELETE')
})
}
)
server.tool("crm_batch_create_associations",
"Create multiple associations in a single request",
{
fromObjectType: z.enum(['companies', 'contacts', 'deals', 'tickets', 'products', 'line_items', 'quotes', 'custom']),
toObjectType: z.enum(['companies', 'contacts', 'deals', 'tickets', 'products', 'line_items', 'quotes', 'custom']),
inputs: z.array(z.object({
from: z.object({ id: z.string() }),
to: z.object({ id: z.string() }),
types: z.array(z.object({
associationCategory: z.string(),
associationTypeId: z.number()
}))
}))
},
async (params) => {
return handleEndpoint(async () => {
const endpoint = `/crm/v4/associations/${params.fromObjectType}/${params.toObjectType}/batch/create`
return await makeApiRequestWithErrorHandling(endpoint, {}, 'POST', {
inputs: params.inputs
})
})
}
)
server.tool("crm_batch_delete_associations",
"Delete multiple associations in a single request",
{
fromObjectType: z.enum(['companies', 'contacts', 'deals', 'tickets', 'products', 'line_items', 'quotes', 'custom']),
toObjectType: z.enum(['companies', 'contacts', 'deals', 'tickets', 'products', 'line_items', 'quotes', 'custom']),
inputs: z.array(z.object({
from: z.object({ id: z.string() }),
to: z.object({ id: z.string() })
}))
},
async (params) => {
return handleEndpoint(async () => {
const endpoint = `/crm/v4/associations/${params.fromObjectType}/${params.toObjectType}/batch/archive`
return await makeApiRequestWithErrorHandling(endpoint, {}, 'POST', {
inputs: params.inputs
})
})
}
)
// Contact-specific property schema
const contactPropertiesSchema = z.object({
email: z.string().email().optional(),
firstname: z.string().optional(),
lastname: z.string().optional(),
phone: z.string().optional(),
mobilephone: z.string().optional(),
company: z.string().optional(),
jobtitle: z.string().optional(),
lifecyclestage: z.enum(['subscriber', 'lead', 'marketingqualifiedlead', 'salesqualifiedlead', 'opportunity', 'customer', 'evangelist', 'other']).optional(),
leadstatus: z.enum(['new', 'open', 'inprogress', 'opennotcontacted', 'opencontacted', 'closedconverted', 'closednotconverted']).optional(),
address: z.string().optional(),
city: z.string().optional(),
state: z.string().optional(),
zip: z.string().optional(),
country: z.string().optional(),
website: z.string().url().optional(),
twitterhandle: z.string().optional(),
facebookfanpage: z.string().optional(),
linkedinbio: z.string().optional(),
}).catchall(z.any())
// Contact-specific CRM endpoints
server.tool("crm_create_contact",
"Create a new contact with validated properties",
{
properties: contactPropertiesSchema,
associations: z.array(z.object({
to: z.object({ id: z.string() }),
types: z.array(z.object({
associationCategory: z.string(),
associationTypeId: z.number()
}))
})).optional()
},
async (params) => {
return handleEndpoint(async () => {
const endpoint = '/crm/v3/objects/contacts'
return await makeApiRequestWithErrorHandling(endpoint, {}, 'POST', {
properties: params.properties,
associations: params.associations
})
})
}
)
server.tool("crm_update_contact",
"Update an existing contact with validated properties",
{
contactId: z.string(),
properties: contactPropertiesSchema
},
async (params) => {
return handleEndpoint(async () => {
const endpoint = `/crm/v3/objects/contacts/${params.contactId}`
return await makeApiRequestWithErrorHandling(endpoint, {}, 'PATCH', {
properties: params.properties
})
})
}
)
server.tool("crm_get_contact",
"Get a single contact by ID with specific properties and associations",
{
contactId: z.string(),
properties: z.array(z.string()).optional(),
associations: z.array(z.enum(['companies', 'deals', 'tickets', 'calls', 'emails', 'meetings', 'notes'])).optional()
},
async (params) => {
return handleEndpoint(async () => {
const endpoint = `/crm/v3/objects/contacts/${params.contactId}`
return await makeApiRequestWithErrorHandling(endpoint, {
properties: params.properties?.join(','),
associations: params.associations?.join(',')
})
})
}
)
server.tool("crm_search_contacts",
"Search contacts with contact-specific filters",
{
filterGroups: z.array(z.object({
filters: z.array(z.object({
propertyName: z.string(),
operator: z.enum(['EQ', 'NEQ', 'LT', 'LTE', 'GT', 'GTE', 'BETWEEN', 'IN', 'NOT_IN', 'HAS_PROPERTY', 'NOT_HAS_PROPERTY', 'CONTAINS_TOKEN', 'NOT_CONTAINS_TOKEN']),
value: z.any()
}))
})),
properties: z.array(z.string()).optional(),
limit: z.number().min(1).max(100).optional(),
after: z.string().optional(),
sorts: z.array(z.object({
propertyName: z.string(),
direction: z.enum(['ASCENDING', 'DESCENDING'])
})).optional()
},
async (params) => {
return handleEndpoint(async () => {
const endpoint = '/crm/v3/objects/contacts/search'
return await makeApiRequestWithErrorHandling(endpoint, {}, 'POST', {
filterGroups: params.filterGroups,
properties: params.properties,
limit: params.limit,
after: params.after,
sorts: params.sorts
})
})
}
)
server.tool("crm_batch_create_contacts",
"Create multiple contacts in a single request",
{
inputs: z.array(z.object({
properties: contactPropertiesSchema,
associations: z.array(z.object({
to: z.object({ id: z.string() }),
types: z.array(z.object({
associationCategory: z.string(),
associationTypeId: z.number()
}))
})).optional()
}))
},
async (params) => {
return handleEndpoint(async () => {
const endpoint = '/crm/v3/objects/contacts/batch/create'
return await makeApiRequestWithErrorHandling(endpoint, {}, 'POST', {
inputs: params.inputs
})
})
}
)
server.tool("crm_batch_update_contacts",
"Update multiple contacts in a single request",
{
inputs: z.array(z.object({
id: z.string(),
properties: contactPropertiesSchema
}))
},
async (params) => {
return handleEndpoint(async () => {
const endpoint = '/crm/v3/objects/contacts/batch/update'
return await makeApiRequestWithErrorHandling(endpoint, {}, 'POST', {
inputs: params.inputs
})
})
}
)
server.tool("crm_get_contact_properties",
"Get all properties for contacts",
{
archived: z.boolean().optional(),
properties: z.array(z.string()).optional()
},
async (params) => {
return handleEndpoint(async () => {
const endpoint = '/crm/v3/properties/contacts'
return await makeApiRequestWithErrorHandling(endpoint, {
archived: params.archived,
properties: params.properties?.join(',')
})
})
}
)
server.tool("crm_create_contact_property",
"Create a new contact property",
{
name: z.string(),
label: z.string(),
type: z.enum(['string', 'number', 'date', 'datetime', 'enumeration', 'bool']),
fieldType: z.enum(['text', 'textarea', 'select', 'radio', 'checkbox', 'number', 'date', 'file']),
groupName: z.string(),
description: z.string().optional(),
options: z.array(z.object({
label: z.string(),
value: z.string(),
description: z.string().optional(),
displayOrder: z.number().optional(),
hidden: z.boolean().optional()
})).optional(),
displayOrder: z.number().optional(),
hasUniqueValue: z.boolean().optional(),
hidden: z.boolean().optional(),
formField: z.boolean().optional()
},
async (params) => {
return handleEndpoint(async () => {
const endpoint = '/crm/v3/properties/contacts'
return await makeApiRequestWithErrorHandling(endpoint, {}, 'POST', params)
})
}
)
const transport = new StdioServerTransport()
await server.connect(transport)