Skip to main content
Glama
hubspot-client.ts17 kB
import { Client } from '@hubspot/api-client'; import dotenv from 'dotenv'; dotenv.config(); // Convert any datetime objects to ISO strings export function convertDatetimeFields(obj: any): any { if (obj === null || obj === undefined) { return obj; } if (typeof obj === 'object') { if (obj instanceof Date) { return obj.toISOString(); } if (Array.isArray(obj)) { return obj.map(item => convertDatetimeFields(item)); } const result: Record<string, any> = {}; for (const [key, value] of Object.entries(obj)) { result[key] = convertDatetimeFields(value); } return result; } return obj; } export class HubSpotClient { private client: Client; constructor(accessToken?: string) { const token = accessToken || process.env.HUBSPOT_ACCESS_TOKEN; if (!token) { throw new Error('HUBSPOT_ACCESS_TOKEN environment variable is required'); } this.client = new Client({ accessToken: token }); } async getRecentCompanies(limit: number = 10): Promise<any> { try { // Create search request with sort by lastmodifieddate const searchRequest = { sorts: ['lastmodifieddate:desc'], limit, properties: ['name', 'domain', 'website', 'phone', 'industry', 'hs_lastmodifieddate'] }; // Execute the search const searchResponse = await this.client.crm.companies.searchApi.doSearch(searchRequest); // Convert the response to a dictionary const companiesDict = searchResponse.results; return convertDatetimeFields(companiesDict); } catch (error: any) { console.error('Error getting recent companies:', error); return { error: error.message }; } } async getRecentContacts(limit: number = 10): Promise<any> { try { // Create search request with sort by lastmodifieddate const searchRequest = { sorts: ['lastmodifieddate:desc'], limit, properties: ['firstname', 'lastname', 'email', 'phone', 'company', 'hs_lastmodifieddate', 'lastmodifieddate'] }; // Execute the search const searchResponse = await this.client.crm.contacts.searchApi.doSearch(searchRequest); // Convert the response to a dictionary const contactsDict = searchResponse.results; return convertDatetimeFields(contactsDict); } catch (error: any) { console.error('Error getting recent contacts:', error); return { error: error.message }; } } async getCompanyActivity(companyId: string): Promise<any> { try { // Step 1: Get all engagement IDs associated with the company using CRM Associations v4 API const associatedEngagements = await this.client.crm.associations.v4.basicApi.getPage( 'companies', companyId, 'engagements', undefined, 500 ); // Extract engagement IDs from the associations response const engagementIds: string[] = []; if (associatedEngagements.results) { for (const result of associatedEngagements.results) { engagementIds.push(result.toObjectId); } } // Step 2: Get detailed information for each engagement const activities = []; for (const engagementId of engagementIds) { const engagementResponse = await this.client.apiRequest({ method: 'GET', path: `/engagements/v1/engagements/${engagementId}` }); // Ensure we have a proper response body const responseBody = engagementResponse.body as any; const engagementData = responseBody.engagement || {}; const metadata = responseBody.metadata || {}; // Format the engagement const formattedEngagement: Record<string, any> = { id: engagementData.id, type: engagementData.type, created_at: engagementData.createdAt, last_updated: engagementData.lastUpdated, created_by: engagementData.createdBy, modified_by: engagementData.modifiedBy, timestamp: engagementData.timestamp, associations: (engagementResponse.body as any).associations || {} }; // Add type-specific metadata formatting if (engagementData.type === 'NOTE') { formattedEngagement.content = metadata.body || ''; } else if (engagementData.type === 'EMAIL') { formattedEngagement.content = { subject: metadata.subject || '', from: { raw: metadata.from?.raw || '', email: metadata.from?.email || '', firstName: metadata.from?.firstName || '', lastName: metadata.from?.lastName || '' }, to: (metadata.to || []).map((recipient: any) => ({ raw: recipient.raw || '', email: recipient.email || '', firstName: recipient.firstName || '', lastName: recipient.lastName || '' })), cc: (metadata.cc || []).map((recipient: any) => ({ raw: recipient.raw || '', email: recipient.email || '', firstName: recipient.firstName || '', lastName: recipient.lastName || '' })), bcc: (metadata.bcc || []).map((recipient: any) => ({ raw: recipient.raw || '', email: recipient.email || '', firstName: recipient.firstName || '', lastName: recipient.lastName || '' })), sender: { email: metadata.sender?.email || '' }, body: metadata.text || metadata.html || '' }; } else if (engagementData.type === 'TASK') { formattedEngagement.content = { subject: metadata.subject || '', body: metadata.body || '', status: metadata.status || '', for_object_type: metadata.forObjectType || '' }; } else if (engagementData.type === 'MEETING') { formattedEngagement.content = { title: metadata.title || '', body: metadata.body || '', start_time: metadata.startTime, end_time: metadata.endTime, internal_notes: metadata.internalMeetingNotes || '' }; } else if (engagementData.type === 'CALL') { formattedEngagement.content = { body: metadata.body || '', from_number: metadata.fromNumber || '', to_number: metadata.toNumber || '', duration_ms: metadata.durationMilliseconds, status: metadata.status || '', disposition: metadata.disposition || '' }; } activities.push(formattedEngagement); } // Convert any datetime fields and return return convertDatetimeFields(activities); } catch (error: any) { console.error('Error getting company activity:', error); return { error: error.message }; } } async getRecentEngagements(days: number = 7, limit: number = 50): Promise<any> { try { // Calculate the date range (past N days) const endTime = new Date(); const startTime = new Date(endTime); startTime.setDate(startTime.getDate() - days); // Format timestamps for API call const startTimestamp = Math.floor(startTime.getTime()); const endTimestamp = Math.floor(endTime.getTime()); // Get all recent engagements const engagementsResponse = await this.client.apiRequest({ method: 'GET', path: '/engagements/v1/engagements/recent/modified', qs: { count: limit, since: startTimestamp, offset: 0 } }); // Format the engagements similar to company_activity const formattedEngagements = []; // Ensure we have a proper response body const responseBody = engagementsResponse.body as any; for (const engagement of responseBody.results || []) { const engagementData = engagement.engagement || {}; const metadata = engagement.metadata || {}; const formattedEngagement: Record<string, any> = { id: engagementData.id, type: engagementData.type, created_at: engagementData.createdAt, last_updated: engagementData.lastUpdated, created_by: engagementData.createdBy, modified_by: engagementData.modifiedBy, timestamp: engagementData.timestamp, associations: engagement.associations || {} }; // Add type-specific metadata formatting identical to company_activity if (engagementData.type === 'NOTE') { formattedEngagement.content = metadata.body || ''; } else if (engagementData.type === 'EMAIL') { formattedEngagement.content = { subject: metadata.subject || '', from: { raw: metadata.from?.raw || '', email: metadata.from?.email || '', firstName: metadata.from?.firstName || '', lastName: metadata.from?.lastName || '' }, to: (metadata.to || []).map((recipient: any) => ({ raw: recipient.raw || '', email: recipient.email || '', firstName: recipient.firstName || '', lastName: recipient.lastName || '' })), cc: (metadata.cc || []).map((recipient: any) => ({ raw: recipient.raw || '', email: recipient.email || '', firstName: recipient.firstName || '', lastName: recipient.lastName || '' })), bcc: (metadata.bcc || []).map((recipient: any) => ({ raw: recipient.raw || '', email: recipient.email || '', firstName: recipient.firstName || '', lastName: recipient.lastName || '' })), sender: { email: metadata.sender?.email || '' }, body: metadata.text || metadata.html || '' }; } else if (engagementData.type === 'TASK') { formattedEngagement.content = { subject: metadata.subject || '', body: metadata.body || '', status: metadata.status || '', for_object_type: metadata.forObjectType || '' }; } else if (engagementData.type === 'MEETING') { formattedEngagement.content = { title: metadata.title || '', body: metadata.body || '', start_time: metadata.startTime, end_time: metadata.endTime, internal_notes: metadata.internalMeetingNotes || '' }; } else if (engagementData.type === 'CALL') { formattedEngagement.content = { body: metadata.body || '', from_number: metadata.fromNumber || '', to_number: metadata.toNumber || '', duration_ms: metadata.durationMilliseconds, status: metadata.status || '', disposition: metadata.disposition || '' }; } formattedEngagements.push(formattedEngagement); } // Convert any datetime fields and return return convertDatetimeFields(formattedEngagements); } catch (error: any) { console.error('Error getting recent engagements:', error); return { error: error.message }; } } async createContact( firstname: string, lastname: string, email?: string, properties?: Record<string, any> ): Promise<any> { try { // Search for existing contacts with same name and company const company = properties?.company; // Use type assertion to satisfy the HubSpot API client types const searchRequest = { filterGroups: [{ filters: [ { propertyName: 'firstname', operator: 'EQ', value: firstname } as any, { propertyName: 'lastname', operator: 'EQ', value: lastname } as any ] }] } as any; // Add company filter if provided if (company) { searchRequest.filterGroups[0].filters.push({ propertyName: 'company', operator: 'EQ', value: company } as any); } const searchResponse = await this.client.crm.contacts.searchApi.doSearch(searchRequest); if (searchResponse.total > 0) { // Contact already exists return { message: 'Contact already exists', contact: searchResponse.results[0] }; } // If no existing contact found, proceed with creation const contactProperties: Record<string, any> = { firstname, lastname }; // Add email if provided if (email) { contactProperties.email = email; } // Add any additional properties if (properties) { Object.assign(contactProperties, properties); } // Create contact const apiResponse = await this.client.crm.contacts.basicApi.create({ properties: contactProperties }); return apiResponse; } catch (error: any) { console.error('Error creating contact:', error); throw new Error(`HubSpot API error: ${error.message}`); } } async createCompany(name: string, properties?: Record<string, any>): Promise<any> { try { // Search for existing companies with same name // Use type assertion to satisfy the HubSpot API client types const searchRequest = { filterGroups: [{ filters: [ { propertyName: 'name', operator: 'EQ', value: name } as any ] }] } as any; const searchResponse = await this.client.crm.companies.searchApi.doSearch(searchRequest); if (searchResponse.total > 0) { // Company already exists return { message: 'Company already exists', company: searchResponse.results[0] }; } // If no existing company found, proceed with creation const companyProperties: Record<string, any> = { name }; // Add any additional properties if (properties) { Object.assign(companyProperties, properties); } // Create company const apiResponse = await this.client.crm.companies.basicApi.create({ properties: companyProperties }); return apiResponse; } catch (error: any) { console.error('Error creating company:', error); throw new Error(`HubSpot API error: ${error.message}`); } } async updateContact( contactId: string, properties: Record<string, any> ): Promise<any> { try { // Check if contact exists try { await this.client.crm.contacts.basicApi.getById(contactId); } catch (error: any) { // If contact doesn't exist, return a message if (error.statusCode === 404) { return { message: 'Contact not found, no update performed', contactId }; } // For other errors, throw them to be caught by the outer try/catch throw error; } // Update the contact const apiResponse = await this.client.crm.contacts.basicApi.update(contactId, { properties }); return { message: 'Contact updated successfully', contactId, properties }; } catch (error: any) { console.error('Error updating contact:', error); throw new Error(`HubSpot API error: ${error.message}`); } } async updateCompany( companyId: string, properties: Record<string, any> ): Promise<any> { try { // Check if company exists try { await this.client.crm.companies.basicApi.getById(companyId); } catch (error: any) { // If company doesn't exist, return a message if (error.statusCode === 404) { return { message: 'Company not found, no update performed', companyId }; } // For other errors, throw them to be caught by the outer try/catch throw error; } // Update the company const apiResponse = await this.client.crm.companies.basicApi.update(companyId, { properties }); return { message: 'Company updated successfully', companyId, properties }; } catch (error: any) { console.error('Error updating company:', error); throw new Error(`HubSpot API error: ${error.message}`); } } }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/lkm1developer/hubspot-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server