# Directory Structure ``` ├── .gitignore ├── Dockerfile ├── LICENSE ├── package-lock.json ├── package.json ├── README.md ├── src │ ├── hubspot-client.ts │ └── index.ts ├── tsconfig.json └── yarn.lock ``` # Files -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- ``` # Dependency directories node_modules/ # Build output dist/ # Environment variables .env # Logs logs *.log npm-debug.log* yarn-debug.log* yarn-error.log* # Editor directories and files .idea .vscode *.suo *.ntvs* *.njsproj *.sln *.sw? # OS generated files .DS_Store .DS_Store? ._* .Spotlight-V100 .Trashes ehthumbs.db Thumbs.db ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- ```markdown # HubSpot MCP Server [](https://www.typescriptlang.org/) [](https://developers.hubspot.com/docs/api/overview) [](https://github.com/modelcontextprotocol/sdk) [](https://opensource.org/licenses/MIT) A powerful Model Context Protocol (MCP) server implementation for seamless HubSpot CRM integration, enabling AI assistants to interact with your HubSpot data. ## Overview This MCP server provides a comprehensive set of tools for interacting with the HubSpot CRM API, allowing AI assistants to: - Create and manage contacts and companies in your HubSpot CRM - Retrieve detailed company activity history and engagement timelines - Access recent engagement data across your entire HubSpot instance - Get lists of recently active companies and contacts - Perform CRM operations without leaving your AI assistant interface ## Why Use This MCP Server? - **Seamless AI Integration**: Connect your AI assistants directly to your HubSpot CRM data - **Simplified CRM Operations**: Perform common HubSpot tasks through natural language commands - **Real-time Data Access**: Get up-to-date information from your HubSpot instance - **Secure Authentication**: Uses HubSpot's secure API token authentication - **Extensible Design**: Easily add more HubSpot API capabilities as needed ## Installation ```bash # Clone the repository git clone https://github.com/lkm1developer/hubspot-mcp-server.git cd hubspot-mcp-server # Install dependencies npm install # Build the project npm run build ``` ## Configuration The server requires a HubSpot API access token. You can obtain one by: 1. Going to your [HubSpot Developer Account](https://developers.hubspot.com/) 2. Creating a private app with the necessary scopes (contacts, companies, engagements) 3. Copying the generated access token You can provide the token in two ways: 1. As an environment variable: ``` HUBSPOT_ACCESS_TOKEN=your-access-token ``` 2. As a command-line argument: ``` npm start -- --access-token=your-access-token ``` For development, create a `.env` file in the project root to store your environment variables: ``` HUBSPOT_ACCESS_TOKEN=your-access-token ``` ## Usage ### Starting the Server ```bash # Start the server npm start # Or with a specific access token npm start -- --access-token=your-access-token # Run the SSE server with authentication npx mcp-proxy-auth node dist/index.js ``` ### Implementing Authentication in SSE Server The SSE server uses the [mcp-proxy-auth](https://www.npmjs.com/package/mcp-proxy-auth) package for authentication. To implement authentication: 1. Install the package: ```bash npm install mcp-proxy-auth ``` 2. Set the `AUTH_SERVER_URL` environment variable to point to your API key verification endpoint: ```bash export AUTH_SERVER_URL=https://your-auth-server.com/verify ``` 3. Run the SSE server with authentication: ```bash npx mcp-proxy-auth node dist/index.js ``` 4. The SSE URL will be available at: ``` localhost:8080/sse?apiKey=apikey ``` Replace `apikey` with your actual API key for authentication. The `mcp-proxy-auth` package acts as a proxy that: - Intercepts requests to your SSE server - Verifies API keys against your authentication server - Only allows authenticated requests to reach your SSE endpoint ### Integrating with AI Assistants This MCP server is designed to work with AI assistants that support the Model Context Protocol. Once running, the server exposes a set of tools that can be used by compatible AI assistants to interact with your HubSpot CRM data. ### Available Tools The server exposes the following powerful HubSpot integration tools: 1. **hubspot_create_contact** - Create a new contact in HubSpot with duplicate checking - Parameters: - `firstname` (string, required): Contact's first name - `lastname` (string, required): Contact's last name - `email` (string, optional): Contact's email address - `properties` (object, optional): Additional contact properties like company, phone, etc. - Example: ```json { "firstname": "John", "lastname": "Doe", "email": "[email protected]", "properties": { "company": "Acme Inc", "phone": "555-123-4567", "jobtitle": "Software Engineer" } } ``` 2. **hubspot_create_company** - Create a new company in HubSpot with duplicate checking - Parameters: - `name` (string, required): Company name - `properties` (object, optional): Additional company properties - Example: ```json { "name": "Acme Corporation", "properties": { "domain": "acme.com", "industry": "Technology", "phone": "555-987-6543", "city": "San Francisco", "state": "CA" } } ``` 3. **hubspot_get_company_activity** - Get comprehensive activity history for a specific company - Parameters: - `company_id` (string, required): HubSpot company ID - Returns detailed engagement data including emails, calls, meetings, notes, and tasks 4. **hubspot_get_recent_engagements** - Get recent engagement activities across all contacts and companies - Parameters: - `days` (number, optional, default: 7): Number of days to look back - `limit` (number, optional, default: 50): Maximum number of engagements to return - Returns a chronological list of all recent CRM activities 5. **hubspot_get_active_companies** - Get most recently active companies from HubSpot - Parameters: - `limit` (number, optional, default: 10): Maximum number of companies to return - Returns companies sorted by last modified date 6. **hubspot_get_active_contacts** - Get most recently active contacts from HubSpot - Parameters: - `limit` (number, optional, default: 10): Maximum number of contacts to return - Returns contacts sorted by last modified date 7. **hubspot_update_contact** - Update an existing contact in HubSpot (ignores if contact does not exist) - Parameters: - `contact_id` (string, required): HubSpot contact ID to update - `properties` (object, required): Contact properties to update - Example: ```json { "contact_id": "12345", "properties": { "email": "[email protected]", "phone": "555-987-6543", "jobtitle": "Senior Software Engineer" } } ``` 8. **hubspot_update_company** - Update an existing company in HubSpot (ignores if company does not exist) - Parameters: - `company_id` (string, required): HubSpot company ID to update - `properties` (object, required): Company properties to update - Example: ```json { "company_id": "67890", "properties": { "domain": "updated-domain.com", "phone": "555-123-4567", "industry": "Software", "city": "New York", "state": "NY" } } ``` ## Extending the Server The server is designed to be easily extensible. To add new HubSpot API capabilities: 1. Add new methods to the `HubSpotClient` class in `src/hubspot-client.ts` 2. Register new tools in the `setupToolHandlers` method in `src/index.ts` 3. Rebuild the project with `npm run build` ## License This project is licensed under the MIT License - see the LICENSE file for details. ## Keywords HubSpot, CRM, Model Context Protocol, MCP, AI Assistant, TypeScript, API Integration, HubSpot API, CRM Integration, Contact Management, Company Management, Engagement Tracking, AI Tools ``` -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- ```json { "compilerOptions": { "target": "ES2022", "module": "NodeNext", "moduleResolution": "NodeNext", "esModuleInterop": true, "strict": true, "outDir": "dist", "sourceMap": true, "declaration": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true }, "include": ["src/**/*"], "exclude": ["node_modules", "dist"] } ``` -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- ```dockerfile FROM node:22.12-alpine AS builder COPY . /app WORKDIR /app RUN npm install FROM node:22-alpine AS release WORKDIR /app COPY --from=builder /app/build /app/build COPY --from=builder /app/package.json /app/package.json COPY --from=builder /app/package-lock.json /app/package-lock.json ENV NODE_ENV=production RUN npm ci --ignore-scripts --omit-dev EXPOSE 8080 ENTRYPOINT ["npx", "mcp-proxy", "node", "/app/dist/index.js"] ``` -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- ```json { "name": "hubspot-mcp-server", "version": "0.1.0", "description": "A powerful Model Context Protocol (MCP) server implementation for seamless HubSpot CRM integration, enabling AI assistants to interact with your HubSpot data", "main": "dist/index.js", "type": "module", "scripts": { "build": "tsc", "start": "node dist/index.js", "dev": "tsx --watch src/index.ts", "stdio": "node dist/index.js" }, "keywords": [ "mcp", "hubspot", "crm", "model-context-protocol", "ai-assistant", "hubspot-api", "hubspot-integration", "crm-integration", "typescript", "contact-management", "company-management", "engagement-tracking", "ai-tools" ], "author": "lakhvinder singh", "license": "MIT", "dependencies": { "@hubspot/api-client": "^12.0.1", "@modelcontextprotocol/sdk": "^1.8.0", "dotenv": "^16.4.7", "mcp-proxy-auth": "^1.0.1", "zod": "^3.24.2" }, "devDependencies": { "@types/node": "^20.10.5", "tsx": "^4.7.0", "typescript": "^5.3.3" } } ``` -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- ```typescript #!/usr/bin/env node import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError, Tool } from '@modelcontextprotocol/sdk/types.js'; import { HubSpotClient } from './hubspot-client.js'; import dotenv from 'dotenv'; import { parseArgs } from 'node:util'; // Load environment variables dotenv.config(); // Parse command line arguments const { values } = parseArgs({ options: { 'access-token': { type: 'string' } } }); // Initialize HubSpot client const accessToken = values['access-token'] || process.env.HUBSPOT_ACCESS_TOKEN; if (!accessToken) { throw new Error('HUBSPOT_ACCESS_TOKEN environment variable is required'); } class HubSpotServer { // Core server properties private server: Server; private hubspot: HubSpotClient; constructor() { this.server = new Server( { name: 'hubspot-manager', version: '0.1.0', }, { capabilities: { resources: {}, tools: {}, }, } ); this.hubspot = new HubSpotClient(accessToken); this.setupToolHandlers(); this.setupErrorHandling(); } private setupErrorHandling(): void { this.server.onerror = (error) => { console.error('[MCP Error]', error); }; process.on('SIGINT', async () => { await this.server.close(); process.exit(0); }); process.on('uncaughtException', (error) => { console.error('Uncaught exception:', error); }); process.on('unhandledRejection', (reason, promise) => { console.error('Unhandled rejection at:', promise, 'reason:', reason); }); } private setupToolHandlers(): void { this.server.setRequestHandler(ListToolsRequestSchema, async () => { // Define available tools const tools: Tool[] = [ { name: 'hubspot_create_contact', description: 'Create a new contact in HubSpot', inputSchema: { type: 'object', properties: { firstname: { type: 'string', description: "Contact's first name" }, lastname: { type: 'string', description: "Contact's last name" }, email: { type: 'string', description: "Contact's email address" }, properties: { type: 'object', description: 'Additional contact properties', additionalProperties: true } }, required: ['firstname', 'lastname'] } }, { name: 'hubspot_create_company', description: 'Create a new company in HubSpot', inputSchema: { type: 'object', properties: { name: { type: 'string', description: 'Company name' }, properties: { type: 'object', description: 'Additional company properties', additionalProperties: true } }, required: ['name'] } }, { name: 'hubspot_get_company_activity', description: 'Get activity history for a specific company', inputSchema: { type: 'object', properties: { company_id: { type: 'string', description: 'HubSpot company ID' } }, required: ['company_id'] } }, { name: 'hubspot_get_recent_engagements', description: 'Get recent engagement activities across all contacts and companies', inputSchema: { type: 'object', properties: { days: { type: 'number', description: 'Number of days to look back (default: 7)', default: 7 }, limit: { type: 'number', description: 'Maximum number of engagements to return (default: 50)', default: 50 } } } }, { name: 'hubspot_get_active_companies', description: 'Get most recently active companies from HubSpot', inputSchema: { type: 'object', properties: { limit: { type: 'number', description: 'Maximum number of companies to return (default: 10)', default: 10 } } } }, { name: 'hubspot_get_active_contacts', description: 'Get most recently active contacts from HubSpot', inputSchema: { type: 'object', properties: { limit: { type: 'number', description: 'Maximum number of contacts to return (default: 10)', default: 10 } } } }, { name: 'hubspot_update_contact', description: 'Update an existing contact in HubSpot (ignores if contact does not exist)', inputSchema: { type: 'object', properties: { contact_id: { type: 'string', description: 'HubSpot contact ID to update' }, properties: { type: 'object', description: 'Contact properties to update', additionalProperties: true } }, required: ['contact_id', 'properties'] } }, { name: 'hubspot_update_company', description: 'Update an existing company in HubSpot (ignores if company does not exist)', inputSchema: { type: 'object', properties: { company_id: { type: 'string', description: 'HubSpot company ID to update' }, properties: { type: 'object', description: 'Company properties to update', additionalProperties: true } }, required: ['company_id', 'properties'] } } ]; return { tools }; }); this.server.setRequestHandler(CallToolRequestSchema, async (request) => { try { const args = request.params.arguments ?? {}; switch (request.params.name) { case 'hubspot_create_contact': { const result = await this.hubspot.createContact( args.firstname as string, args.lastname as string, args.email as string | undefined, args.properties as Record<string, any> | undefined ); return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }; } case 'hubspot_create_company': { const result = await this.hubspot.createCompany( args.name as string, args.properties as Record<string, any> | undefined ); return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }; } case 'hubspot_get_company_activity': { const result = await this.hubspot.getCompanyActivity(args.company_id as string); return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }; } case 'hubspot_get_recent_engagements': { const result = await this.hubspot.getRecentEngagements( args.days as number | undefined, args.limit as number | undefined ); return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }; } case 'hubspot_get_active_companies': { const result = await this.hubspot.getRecentCompanies(args.limit as number | undefined); return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }; } case 'hubspot_get_active_contacts': { const result = await this.hubspot.getRecentContacts(args.limit as number | undefined); return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }; } case 'hubspot_update_contact': { const result = await this.hubspot.updateContact( args.contact_id as string, args.properties as Record<string, any> ); return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }; } case 'hubspot_update_company': { const result = await this.hubspot.updateCompany( args.company_id as string, args.properties as Record<string, any> ); return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }; } default: throw new McpError( ErrorCode.MethodNotFound, `Unknown tool: ${request.params.name}` ); } } catch (error: any) { console.error(`Error executing tool ${request.params.name}:`, error); return { content: [{ type: 'text', text: `HubSpot API error: ${error.message}` }], isError: true, }; } }); } async run(): Promise<void> { const transport = new StdioServerTransport(); await this.server.connect(transport); console.log('HubSpot MCP server started'); } } export async function serve(): Promise<void> { const client = new HubSpotServer(); await client.run(); } const server = new HubSpotServer(); server.run().catch(console.error); ``` -------------------------------------------------------------------------------- /src/hubspot-client.ts: -------------------------------------------------------------------------------- ```typescript 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}`); } } } ```