# Directory Structure ``` ├── .gitignore ├── Dockerfile ├── LICENSE ├── package-lock.json ├── package.json ├── README.md ├── src │ ├── hubspot-client.ts │ └── index.ts ├── tsconfig.json └── yarn.lock ``` # Files -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- ``` 1 | # Dependency directories 2 | node_modules/ 3 | 4 | # Build output 5 | dist/ 6 | 7 | # Environment variables 8 | .env 9 | 10 | # Logs 11 | logs 12 | *.log 13 | npm-debug.log* 14 | yarn-debug.log* 15 | yarn-error.log* 16 | 17 | # Editor directories and files 18 | .idea 19 | .vscode 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | 26 | # OS generated files 27 | .DS_Store 28 | .DS_Store? 29 | ._* 30 | .Spotlight-V100 31 | .Trashes 32 | ehthumbs.db 33 | Thumbs.db 34 | ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- ```markdown 1 | # HubSpot MCP Server 2 | 3 | [](https://www.typescriptlang.org/) 4 | [](https://developers.hubspot.com/docs/api/overview) 5 | [](https://github.com/modelcontextprotocol/sdk) 6 | [](https://opensource.org/licenses/MIT) 7 | 8 | A powerful Model Context Protocol (MCP) server implementation for seamless HubSpot CRM integration, enabling AI assistants to interact with your HubSpot data. 9 | 10 | ## Overview 11 | 12 | This MCP server provides a comprehensive set of tools for interacting with the HubSpot CRM API, allowing AI assistants to: 13 | 14 | - Create and manage contacts and companies in your HubSpot CRM 15 | - Retrieve detailed company activity history and engagement timelines 16 | - Access recent engagement data across your entire HubSpot instance 17 | - Get lists of recently active companies and contacts 18 | - Perform CRM operations without leaving your AI assistant interface 19 | 20 | ## Why Use This MCP Server? 21 | 22 | - **Seamless AI Integration**: Connect your AI assistants directly to your HubSpot CRM data 23 | - **Simplified CRM Operations**: Perform common HubSpot tasks through natural language commands 24 | - **Real-time Data Access**: Get up-to-date information from your HubSpot instance 25 | - **Secure Authentication**: Uses HubSpot's secure API token authentication 26 | - **Extensible Design**: Easily add more HubSpot API capabilities as needed 27 | 28 | ## Installation 29 | 30 | ```bash 31 | # Clone the repository 32 | git clone https://github.com/lkm1developer/hubspot-mcp-server.git 33 | cd hubspot-mcp-server 34 | 35 | # Install dependencies 36 | npm install 37 | 38 | # Build the project 39 | npm run build 40 | ``` 41 | 42 | ## Configuration 43 | 44 | The server requires a HubSpot API access token. You can obtain one by: 45 | 46 | 1. Going to your [HubSpot Developer Account](https://developers.hubspot.com/) 47 | 2. Creating a private app with the necessary scopes (contacts, companies, engagements) 48 | 3. Copying the generated access token 49 | 50 | You can provide the token in two ways: 51 | 52 | 1. As an environment variable: 53 | ``` 54 | HUBSPOT_ACCESS_TOKEN=your-access-token 55 | ``` 56 | 57 | 2. As a command-line argument: 58 | ``` 59 | npm start -- --access-token=your-access-token 60 | ``` 61 | 62 | For development, create a `.env` file in the project root to store your environment variables: 63 | 64 | ``` 65 | HUBSPOT_ACCESS_TOKEN=your-access-token 66 | ``` 67 | 68 | ## Usage 69 | 70 | ### Starting the Server 71 | 72 | ```bash 73 | # Start the server 74 | npm start 75 | 76 | # Or with a specific access token 77 | npm start -- --access-token=your-access-token 78 | 79 | # Run the SSE server with authentication 80 | npx mcp-proxy-auth node dist/index.js 81 | ``` 82 | 83 | ### Implementing Authentication in SSE Server 84 | 85 | The SSE server uses the [mcp-proxy-auth](https://www.npmjs.com/package/mcp-proxy-auth) package for authentication. To implement authentication: 86 | 87 | 1. Install the package: 88 | ```bash 89 | npm install mcp-proxy-auth 90 | ``` 91 | 92 | 2. Set the `AUTH_SERVER_URL` environment variable to point to your API key verification endpoint: 93 | ```bash 94 | export AUTH_SERVER_URL=https://your-auth-server.com/verify 95 | ``` 96 | 97 | 3. Run the SSE server with authentication: 98 | ```bash 99 | npx mcp-proxy-auth node dist/index.js 100 | ``` 101 | 102 | 4. The SSE URL will be available at: 103 | ``` 104 | localhost:8080/sse?apiKey=apikey 105 | ``` 106 | 107 | Replace `apikey` with your actual API key for authentication. 108 | 109 | The `mcp-proxy-auth` package acts as a proxy that: 110 | - Intercepts requests to your SSE server 111 | - Verifies API keys against your authentication server 112 | - Only allows authenticated requests to reach your SSE endpoint 113 | 114 | ### Integrating with AI Assistants 115 | 116 | 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. 117 | 118 | ### Available Tools 119 | 120 | The server exposes the following powerful HubSpot integration tools: 121 | 122 | 1. **hubspot_create_contact** 123 | - Create a new contact in HubSpot with duplicate checking 124 | - Parameters: 125 | - `firstname` (string, required): Contact's first name 126 | - `lastname` (string, required): Contact's last name 127 | - `email` (string, optional): Contact's email address 128 | - `properties` (object, optional): Additional contact properties like company, phone, etc. 129 | - Example: 130 | ```json 131 | { 132 | "firstname": "John", 133 | "lastname": "Doe", 134 | "email": "[email protected]", 135 | "properties": { 136 | "company": "Acme Inc", 137 | "phone": "555-123-4567", 138 | "jobtitle": "Software Engineer" 139 | } 140 | } 141 | ``` 142 | 143 | 2. **hubspot_create_company** 144 | - Create a new company in HubSpot with duplicate checking 145 | - Parameters: 146 | - `name` (string, required): Company name 147 | - `properties` (object, optional): Additional company properties 148 | - Example: 149 | ```json 150 | { 151 | "name": "Acme Corporation", 152 | "properties": { 153 | "domain": "acme.com", 154 | "industry": "Technology", 155 | "phone": "555-987-6543", 156 | "city": "San Francisco", 157 | "state": "CA" 158 | } 159 | } 160 | ``` 161 | 162 | 3. **hubspot_get_company_activity** 163 | - Get comprehensive activity history for a specific company 164 | - Parameters: 165 | - `company_id` (string, required): HubSpot company ID 166 | - Returns detailed engagement data including emails, calls, meetings, notes, and tasks 167 | 168 | 4. **hubspot_get_recent_engagements** 169 | - Get recent engagement activities across all contacts and companies 170 | - Parameters: 171 | - `days` (number, optional, default: 7): Number of days to look back 172 | - `limit` (number, optional, default: 50): Maximum number of engagements to return 173 | - Returns a chronological list of all recent CRM activities 174 | 175 | 5. **hubspot_get_active_companies** 176 | - Get most recently active companies from HubSpot 177 | - Parameters: 178 | - `limit` (number, optional, default: 10): Maximum number of companies to return 179 | - Returns companies sorted by last modified date 180 | 181 | 6. **hubspot_get_active_contacts** 182 | - Get most recently active contacts from HubSpot 183 | - Parameters: 184 | - `limit` (number, optional, default: 10): Maximum number of contacts to return 185 | - Returns contacts sorted by last modified date 186 | 187 | 7. **hubspot_update_contact** 188 | - Update an existing contact in HubSpot (ignores if contact does not exist) 189 | - Parameters: 190 | - `contact_id` (string, required): HubSpot contact ID to update 191 | - `properties` (object, required): Contact properties to update 192 | - Example: 193 | ```json 194 | { 195 | "contact_id": "12345", 196 | "properties": { 197 | "email": "[email protected]", 198 | "phone": "555-987-6543", 199 | "jobtitle": "Senior Software Engineer" 200 | } 201 | } 202 | ``` 203 | 204 | 8. **hubspot_update_company** 205 | - Update an existing company in HubSpot (ignores if company does not exist) 206 | - Parameters: 207 | - `company_id` (string, required): HubSpot company ID to update 208 | - `properties` (object, required): Company properties to update 209 | - Example: 210 | ```json 211 | { 212 | "company_id": "67890", 213 | "properties": { 214 | "domain": "updated-domain.com", 215 | "phone": "555-123-4567", 216 | "industry": "Software", 217 | "city": "New York", 218 | "state": "NY" 219 | } 220 | } 221 | ``` 222 | 223 | ## Extending the Server 224 | 225 | The server is designed to be easily extensible. To add new HubSpot API capabilities: 226 | 227 | 1. Add new methods to the `HubSpotClient` class in `src/hubspot-client.ts` 228 | 2. Register new tools in the `setupToolHandlers` method in `src/index.ts` 229 | 3. Rebuild the project with `npm run build` 230 | 231 | ## License 232 | 233 | This project is licensed under the MIT License - see the LICENSE file for details. 234 | 235 | ## Keywords 236 | 237 | HubSpot, CRM, Model Context Protocol, MCP, AI Assistant, TypeScript, API Integration, HubSpot API, CRM Integration, Contact Management, Company Management, Engagement Tracking, AI Tools 238 | ``` -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "module": "NodeNext", 5 | "moduleResolution": "NodeNext", 6 | "esModuleInterop": true, 7 | "strict": true, 8 | "outDir": "dist", 9 | "sourceMap": true, 10 | "declaration": true, 11 | "skipLibCheck": true, 12 | "forceConsistentCasingInFileNames": true 13 | }, 14 | "include": ["src/**/*"], 15 | "exclude": ["node_modules", "dist"] 16 | } 17 | ``` -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- ```dockerfile 1 | FROM node:22.12-alpine AS builder 2 | 3 | COPY . /app 4 | 5 | WORKDIR /app 6 | 7 | RUN npm install 8 | 9 | FROM node:22-alpine AS release 10 | 11 | WORKDIR /app 12 | 13 | COPY --from=builder /app/build /app/build 14 | COPY --from=builder /app/package.json /app/package.json 15 | COPY --from=builder /app/package-lock.json /app/package-lock.json 16 | 17 | ENV NODE_ENV=production 18 | 19 | 20 | RUN npm ci --ignore-scripts --omit-dev 21 | EXPOSE 8080 22 | ENTRYPOINT ["npx", "mcp-proxy", "node", "/app/dist/index.js"] 23 | ``` -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "name": "hubspot-mcp-server", 3 | "version": "0.1.0", 4 | "description": "A powerful Model Context Protocol (MCP) server implementation for seamless HubSpot CRM integration, enabling AI assistants to interact with your HubSpot data", 5 | "main": "dist/index.js", 6 | "type": "module", 7 | "scripts": { 8 | "build": "tsc", 9 | "start": "node dist/index.js", 10 | "dev": "tsx --watch src/index.ts", 11 | "stdio": "node dist/index.js" 12 | }, 13 | "keywords": [ 14 | "mcp", 15 | "hubspot", 16 | "crm", 17 | "model-context-protocol", 18 | "ai-assistant", 19 | "hubspot-api", 20 | "hubspot-integration", 21 | "crm-integration", 22 | "typescript", 23 | "contact-management", 24 | "company-management", 25 | "engagement-tracking", 26 | "ai-tools" 27 | ], 28 | "author": "lakhvinder singh", 29 | "license": "MIT", 30 | "dependencies": { 31 | "@hubspot/api-client": "^12.0.1", 32 | "@modelcontextprotocol/sdk": "^1.8.0", 33 | "dotenv": "^16.4.7", 34 | "mcp-proxy-auth": "^1.0.1", 35 | "zod": "^3.24.2" 36 | }, 37 | "devDependencies": { 38 | "@types/node": "^20.10.5", 39 | "tsx": "^4.7.0", 40 | "typescript": "^5.3.3" 41 | } 42 | } 43 | ``` -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | #!/usr/bin/env node 2 | import { Server } from '@modelcontextprotocol/sdk/server/index.js'; 3 | import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; 4 | import { 5 | CallToolRequestSchema, 6 | ErrorCode, 7 | ListToolsRequestSchema, 8 | McpError, 9 | Tool 10 | } from '@modelcontextprotocol/sdk/types.js'; 11 | import { HubSpotClient } from './hubspot-client.js'; 12 | import dotenv from 'dotenv'; 13 | import { parseArgs } from 'node:util'; 14 | 15 | // Load environment variables 16 | dotenv.config(); 17 | 18 | // Parse command line arguments 19 | const { values } = parseArgs({ 20 | options: { 21 | 'access-token': { type: 'string' } 22 | } 23 | }); 24 | 25 | // Initialize HubSpot client 26 | const accessToken = values['access-token'] || process.env.HUBSPOT_ACCESS_TOKEN; 27 | if (!accessToken) { 28 | throw new Error('HUBSPOT_ACCESS_TOKEN environment variable is required'); 29 | } 30 | 31 | class HubSpotServer { 32 | // Core server properties 33 | private server: Server; 34 | private hubspot: HubSpotClient; 35 | 36 | constructor() { 37 | this.server = new Server( 38 | { 39 | name: 'hubspot-manager', 40 | version: '0.1.0', 41 | }, 42 | { 43 | capabilities: { 44 | resources: {}, 45 | tools: {}, 46 | }, 47 | } 48 | ); 49 | 50 | this.hubspot = new HubSpotClient(accessToken); 51 | 52 | this.setupToolHandlers(); 53 | this.setupErrorHandling(); 54 | } 55 | 56 | private setupErrorHandling(): void { 57 | this.server.onerror = (error) => { 58 | console.error('[MCP Error]', error); 59 | }; 60 | 61 | process.on('SIGINT', async () => { 62 | await this.server.close(); 63 | process.exit(0); 64 | }); 65 | 66 | process.on('uncaughtException', (error) => { 67 | console.error('Uncaught exception:', error); 68 | }); 69 | 70 | process.on('unhandledRejection', (reason, promise) => { 71 | console.error('Unhandled rejection at:', promise, 'reason:', reason); 72 | }); 73 | } 74 | 75 | private setupToolHandlers(): void { 76 | this.server.setRequestHandler(ListToolsRequestSchema, async () => { 77 | // Define available tools 78 | const tools: Tool[] = [ 79 | { 80 | name: 'hubspot_create_contact', 81 | description: 'Create a new contact in HubSpot', 82 | inputSchema: { 83 | type: 'object', 84 | properties: { 85 | firstname: { 86 | type: 'string', 87 | description: "Contact's first name" 88 | }, 89 | lastname: { 90 | type: 'string', 91 | description: "Contact's last name" 92 | }, 93 | email: { 94 | type: 'string', 95 | description: "Contact's email address" 96 | }, 97 | properties: { 98 | type: 'object', 99 | description: 'Additional contact properties', 100 | additionalProperties: true 101 | } 102 | }, 103 | required: ['firstname', 'lastname'] 104 | } 105 | }, 106 | { 107 | name: 'hubspot_create_company', 108 | description: 'Create a new company in HubSpot', 109 | inputSchema: { 110 | type: 'object', 111 | properties: { 112 | name: { 113 | type: 'string', 114 | description: 'Company name' 115 | }, 116 | properties: { 117 | type: 'object', 118 | description: 'Additional company properties', 119 | additionalProperties: true 120 | } 121 | }, 122 | required: ['name'] 123 | } 124 | }, 125 | { 126 | name: 'hubspot_get_company_activity', 127 | description: 'Get activity history for a specific company', 128 | inputSchema: { 129 | type: 'object', 130 | properties: { 131 | company_id: { 132 | type: 'string', 133 | description: 'HubSpot company ID' 134 | } 135 | }, 136 | required: ['company_id'] 137 | } 138 | }, 139 | { 140 | name: 'hubspot_get_recent_engagements', 141 | description: 'Get recent engagement activities across all contacts and companies', 142 | inputSchema: { 143 | type: 'object', 144 | properties: { 145 | days: { 146 | type: 'number', 147 | description: 'Number of days to look back (default: 7)', 148 | default: 7 149 | }, 150 | limit: { 151 | type: 'number', 152 | description: 'Maximum number of engagements to return (default: 50)', 153 | default: 50 154 | } 155 | } 156 | } 157 | }, 158 | { 159 | name: 'hubspot_get_active_companies', 160 | description: 'Get most recently active companies from HubSpot', 161 | inputSchema: { 162 | type: 'object', 163 | properties: { 164 | limit: { 165 | type: 'number', 166 | description: 'Maximum number of companies to return (default: 10)', 167 | default: 10 168 | } 169 | } 170 | } 171 | }, 172 | { 173 | name: 'hubspot_get_active_contacts', 174 | description: 'Get most recently active contacts from HubSpot', 175 | inputSchema: { 176 | type: 'object', 177 | properties: { 178 | limit: { 179 | type: 'number', 180 | description: 'Maximum number of contacts to return (default: 10)', 181 | default: 10 182 | } 183 | } 184 | } 185 | }, 186 | { 187 | name: 'hubspot_update_contact', 188 | description: 'Update an existing contact in HubSpot (ignores if contact does not exist)', 189 | inputSchema: { 190 | type: 'object', 191 | properties: { 192 | contact_id: { 193 | type: 'string', 194 | description: 'HubSpot contact ID to update' 195 | }, 196 | properties: { 197 | type: 'object', 198 | description: 'Contact properties to update', 199 | additionalProperties: true 200 | } 201 | }, 202 | required: ['contact_id', 'properties'] 203 | } 204 | }, 205 | { 206 | name: 'hubspot_update_company', 207 | description: 'Update an existing company in HubSpot (ignores if company does not exist)', 208 | inputSchema: { 209 | type: 'object', 210 | properties: { 211 | company_id: { 212 | type: 'string', 213 | description: 'HubSpot company ID to update' 214 | }, 215 | properties: { 216 | type: 'object', 217 | description: 'Company properties to update', 218 | additionalProperties: true 219 | } 220 | }, 221 | required: ['company_id', 'properties'] 222 | } 223 | } 224 | ]; 225 | 226 | return { tools }; 227 | }); 228 | 229 | this.server.setRequestHandler(CallToolRequestSchema, async (request) => { 230 | try { 231 | const args = request.params.arguments ?? {}; 232 | 233 | switch (request.params.name) { 234 | case 'hubspot_create_contact': { 235 | const result = await this.hubspot.createContact( 236 | args.firstname as string, 237 | args.lastname as string, 238 | args.email as string | undefined, 239 | args.properties as Record<string, any> | undefined 240 | ); 241 | return { 242 | content: [{ 243 | type: 'text', 244 | text: JSON.stringify(result, null, 2) 245 | }] 246 | }; 247 | } 248 | 249 | case 'hubspot_create_company': { 250 | const result = await this.hubspot.createCompany( 251 | args.name as string, 252 | args.properties as Record<string, any> | undefined 253 | ); 254 | return { 255 | content: [{ 256 | type: 'text', 257 | text: JSON.stringify(result, null, 2) 258 | }] 259 | }; 260 | } 261 | 262 | case 'hubspot_get_company_activity': { 263 | const result = await this.hubspot.getCompanyActivity(args.company_id as string); 264 | return { 265 | content: [{ 266 | type: 'text', 267 | text: JSON.stringify(result, null, 2) 268 | }] 269 | }; 270 | } 271 | 272 | case 'hubspot_get_recent_engagements': { 273 | const result = await this.hubspot.getRecentEngagements( 274 | args.days as number | undefined, 275 | args.limit as number | undefined 276 | ); 277 | return { 278 | content: [{ 279 | type: 'text', 280 | text: JSON.stringify(result, null, 2) 281 | }] 282 | }; 283 | } 284 | 285 | case 'hubspot_get_active_companies': { 286 | const result = await this.hubspot.getRecentCompanies(args.limit as number | undefined); 287 | return { 288 | content: [{ 289 | type: 'text', 290 | text: JSON.stringify(result, null, 2) 291 | }] 292 | }; 293 | } 294 | 295 | case 'hubspot_get_active_contacts': { 296 | const result = await this.hubspot.getRecentContacts(args.limit as number | undefined); 297 | return { 298 | content: [{ 299 | type: 'text', 300 | text: JSON.stringify(result, null, 2) 301 | }] 302 | }; 303 | } 304 | 305 | case 'hubspot_update_contact': { 306 | const result = await this.hubspot.updateContact( 307 | args.contact_id as string, 308 | args.properties as Record<string, any> 309 | ); 310 | return { 311 | content: [{ 312 | type: 'text', 313 | text: JSON.stringify(result, null, 2) 314 | }] 315 | }; 316 | } 317 | 318 | case 'hubspot_update_company': { 319 | const result = await this.hubspot.updateCompany( 320 | args.company_id as string, 321 | args.properties as Record<string, any> 322 | ); 323 | return { 324 | content: [{ 325 | type: 'text', 326 | text: JSON.stringify(result, null, 2) 327 | }] 328 | }; 329 | } 330 | 331 | default: 332 | throw new McpError( 333 | ErrorCode.MethodNotFound, 334 | `Unknown tool: ${request.params.name}` 335 | ); 336 | } 337 | } catch (error: any) { 338 | console.error(`Error executing tool ${request.params.name}:`, error); 339 | return { 340 | content: [{ 341 | type: 'text', 342 | text: `HubSpot API error: ${error.message}` 343 | }], 344 | isError: true, 345 | }; 346 | } 347 | }); 348 | } 349 | 350 | async run(): Promise<void> { 351 | const transport = new StdioServerTransport(); 352 | await this.server.connect(transport); 353 | console.log('HubSpot MCP server started'); 354 | } 355 | } 356 | export async function serve(): Promise<void> { 357 | const client = new HubSpotServer(); 358 | await client.run(); 359 | } 360 | const server = new HubSpotServer(); 361 | server.run().catch(console.error); 362 | ``` -------------------------------------------------------------------------------- /src/hubspot-client.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { Client } from '@hubspot/api-client'; 2 | import dotenv from 'dotenv'; 3 | 4 | dotenv.config(); 5 | 6 | // Convert any datetime objects to ISO strings 7 | export function convertDatetimeFields(obj: any): any { 8 | if (obj === null || obj === undefined) { 9 | return obj; 10 | } 11 | 12 | if (typeof obj === 'object') { 13 | if (obj instanceof Date) { 14 | return obj.toISOString(); 15 | } 16 | 17 | if (Array.isArray(obj)) { 18 | return obj.map(item => convertDatetimeFields(item)); 19 | } 20 | 21 | const result: Record<string, any> = {}; 22 | for (const [key, value] of Object.entries(obj)) { 23 | result[key] = convertDatetimeFields(value); 24 | } 25 | return result; 26 | } 27 | 28 | return obj; 29 | } 30 | 31 | export class HubSpotClient { 32 | private client: Client; 33 | 34 | constructor(accessToken?: string) { 35 | const token = accessToken || process.env.HUBSPOT_ACCESS_TOKEN; 36 | 37 | if (!token) { 38 | throw new Error('HUBSPOT_ACCESS_TOKEN environment variable is required'); 39 | } 40 | 41 | this.client = new Client({ accessToken: token }); 42 | } 43 | 44 | async getRecentCompanies(limit: number = 10): Promise<any> { 45 | try { 46 | // Create search request with sort by lastmodifieddate 47 | const searchRequest = { 48 | sorts: ['lastmodifieddate:desc'], 49 | limit, 50 | properties: ['name', 'domain', 'website', 'phone', 'industry', 'hs_lastmodifieddate'] 51 | }; 52 | 53 | // Execute the search 54 | const searchResponse = await this.client.crm.companies.searchApi.doSearch(searchRequest); 55 | 56 | // Convert the response to a dictionary 57 | const companiesDict = searchResponse.results; 58 | return convertDatetimeFields(companiesDict); 59 | } catch (error: any) { 60 | console.error('Error getting recent companies:', error); 61 | return { error: error.message }; 62 | } 63 | } 64 | 65 | async getRecentContacts(limit: number = 10): Promise<any> { 66 | try { 67 | // Create search request with sort by lastmodifieddate 68 | const searchRequest = { 69 | sorts: ['lastmodifieddate:desc'], 70 | limit, 71 | properties: ['firstname', 'lastname', 'email', 'phone', 'company', 'hs_lastmodifieddate', 'lastmodifieddate'] 72 | }; 73 | 74 | // Execute the search 75 | const searchResponse = await this.client.crm.contacts.searchApi.doSearch(searchRequest); 76 | 77 | // Convert the response to a dictionary 78 | const contactsDict = searchResponse.results; 79 | return convertDatetimeFields(contactsDict); 80 | } catch (error: any) { 81 | console.error('Error getting recent contacts:', error); 82 | return { error: error.message }; 83 | } 84 | } 85 | 86 | async getCompanyActivity(companyId: string): Promise<any> { 87 | try { 88 | // Step 1: Get all engagement IDs associated with the company using CRM Associations v4 API 89 | const associatedEngagements = await this.client.crm.associations.v4.basicApi.getPage( 90 | 'companies', 91 | companyId, 92 | 'engagements', 93 | undefined, 94 | 500 95 | ); 96 | 97 | // Extract engagement IDs from the associations response 98 | const engagementIds: string[] = []; 99 | if (associatedEngagements.results) { 100 | for (const result of associatedEngagements.results) { 101 | engagementIds.push(result.toObjectId); 102 | } 103 | } 104 | 105 | // Step 2: Get detailed information for each engagement 106 | const activities = []; 107 | for (const engagementId of engagementIds) { 108 | const engagementResponse = await this.client.apiRequest({ 109 | method: 'GET', 110 | path: `/engagements/v1/engagements/${engagementId}` 111 | }); 112 | 113 | // Ensure we have a proper response body 114 | const responseBody = engagementResponse.body as any; 115 | const engagementData = responseBody.engagement || {}; 116 | const metadata = responseBody.metadata || {}; 117 | 118 | // Format the engagement 119 | const formattedEngagement: Record<string, any> = { 120 | id: engagementData.id, 121 | type: engagementData.type, 122 | created_at: engagementData.createdAt, 123 | last_updated: engagementData.lastUpdated, 124 | created_by: engagementData.createdBy, 125 | modified_by: engagementData.modifiedBy, 126 | timestamp: engagementData.timestamp, 127 | associations: (engagementResponse.body as any).associations || {} 128 | }; 129 | 130 | // Add type-specific metadata formatting 131 | if (engagementData.type === 'NOTE') { 132 | formattedEngagement.content = metadata.body || ''; 133 | } else if (engagementData.type === 'EMAIL') { 134 | formattedEngagement.content = { 135 | subject: metadata.subject || '', 136 | from: { 137 | raw: metadata.from?.raw || '', 138 | email: metadata.from?.email || '', 139 | firstName: metadata.from?.firstName || '', 140 | lastName: metadata.from?.lastName || '' 141 | }, 142 | to: (metadata.to || []).map((recipient: any) => ({ 143 | raw: recipient.raw || '', 144 | email: recipient.email || '', 145 | firstName: recipient.firstName || '', 146 | lastName: recipient.lastName || '' 147 | })), 148 | cc: (metadata.cc || []).map((recipient: any) => ({ 149 | raw: recipient.raw || '', 150 | email: recipient.email || '', 151 | firstName: recipient.firstName || '', 152 | lastName: recipient.lastName || '' 153 | })), 154 | bcc: (metadata.bcc || []).map((recipient: any) => ({ 155 | raw: recipient.raw || '', 156 | email: recipient.email || '', 157 | firstName: recipient.firstName || '', 158 | lastName: recipient.lastName || '' 159 | })), 160 | sender: { 161 | email: metadata.sender?.email || '' 162 | }, 163 | body: metadata.text || metadata.html || '' 164 | }; 165 | } else if (engagementData.type === 'TASK') { 166 | formattedEngagement.content = { 167 | subject: metadata.subject || '', 168 | body: metadata.body || '', 169 | status: metadata.status || '', 170 | for_object_type: metadata.forObjectType || '' 171 | }; 172 | } else if (engagementData.type === 'MEETING') { 173 | formattedEngagement.content = { 174 | title: metadata.title || '', 175 | body: metadata.body || '', 176 | start_time: metadata.startTime, 177 | end_time: metadata.endTime, 178 | internal_notes: metadata.internalMeetingNotes || '' 179 | }; 180 | } else if (engagementData.type === 'CALL') { 181 | formattedEngagement.content = { 182 | body: metadata.body || '', 183 | from_number: metadata.fromNumber || '', 184 | to_number: metadata.toNumber || '', 185 | duration_ms: metadata.durationMilliseconds, 186 | status: metadata.status || '', 187 | disposition: metadata.disposition || '' 188 | }; 189 | } 190 | 191 | activities.push(formattedEngagement); 192 | } 193 | 194 | // Convert any datetime fields and return 195 | return convertDatetimeFields(activities); 196 | } catch (error: any) { 197 | console.error('Error getting company activity:', error); 198 | return { error: error.message }; 199 | } 200 | } 201 | 202 | async getRecentEngagements(days: number = 7, limit: number = 50): Promise<any> { 203 | try { 204 | // Calculate the date range (past N days) 205 | const endTime = new Date(); 206 | const startTime = new Date(endTime); 207 | startTime.setDate(startTime.getDate() - days); 208 | 209 | // Format timestamps for API call 210 | const startTimestamp = Math.floor(startTime.getTime()); 211 | const endTimestamp = Math.floor(endTime.getTime()); 212 | 213 | // Get all recent engagements 214 | const engagementsResponse = await this.client.apiRequest({ 215 | method: 'GET', 216 | path: '/engagements/v1/engagements/recent/modified', 217 | qs: { 218 | count: limit, 219 | since: startTimestamp, 220 | offset: 0 221 | } 222 | }); 223 | 224 | // Format the engagements similar to company_activity 225 | const formattedEngagements = []; 226 | 227 | // Ensure we have a proper response body 228 | const responseBody = engagementsResponse.body as any; 229 | for (const engagement of responseBody.results || []) { 230 | const engagementData = engagement.engagement || {}; 231 | const metadata = engagement.metadata || {}; 232 | 233 | const formattedEngagement: Record<string, any> = { 234 | id: engagementData.id, 235 | type: engagementData.type, 236 | created_at: engagementData.createdAt, 237 | last_updated: engagementData.lastUpdated, 238 | created_by: engagementData.createdBy, 239 | modified_by: engagementData.modifiedBy, 240 | timestamp: engagementData.timestamp, 241 | associations: engagement.associations || {} 242 | }; 243 | 244 | // Add type-specific metadata formatting identical to company_activity 245 | if (engagementData.type === 'NOTE') { 246 | formattedEngagement.content = metadata.body || ''; 247 | } else if (engagementData.type === 'EMAIL') { 248 | formattedEngagement.content = { 249 | subject: metadata.subject || '', 250 | from: { 251 | raw: metadata.from?.raw || '', 252 | email: metadata.from?.email || '', 253 | firstName: metadata.from?.firstName || '', 254 | lastName: metadata.from?.lastName || '' 255 | }, 256 | to: (metadata.to || []).map((recipient: any) => ({ 257 | raw: recipient.raw || '', 258 | email: recipient.email || '', 259 | firstName: recipient.firstName || '', 260 | lastName: recipient.lastName || '' 261 | })), 262 | cc: (metadata.cc || []).map((recipient: any) => ({ 263 | raw: recipient.raw || '', 264 | email: recipient.email || '', 265 | firstName: recipient.firstName || '', 266 | lastName: recipient.lastName || '' 267 | })), 268 | bcc: (metadata.bcc || []).map((recipient: any) => ({ 269 | raw: recipient.raw || '', 270 | email: recipient.email || '', 271 | firstName: recipient.firstName || '', 272 | lastName: recipient.lastName || '' 273 | })), 274 | sender: { 275 | email: metadata.sender?.email || '' 276 | }, 277 | body: metadata.text || metadata.html || '' 278 | }; 279 | } else if (engagementData.type === 'TASK') { 280 | formattedEngagement.content = { 281 | subject: metadata.subject || '', 282 | body: metadata.body || '', 283 | status: metadata.status || '', 284 | for_object_type: metadata.forObjectType || '' 285 | }; 286 | } else if (engagementData.type === 'MEETING') { 287 | formattedEngagement.content = { 288 | title: metadata.title || '', 289 | body: metadata.body || '', 290 | start_time: metadata.startTime, 291 | end_time: metadata.endTime, 292 | internal_notes: metadata.internalMeetingNotes || '' 293 | }; 294 | } else if (engagementData.type === 'CALL') { 295 | formattedEngagement.content = { 296 | body: metadata.body || '', 297 | from_number: metadata.fromNumber || '', 298 | to_number: metadata.toNumber || '', 299 | duration_ms: metadata.durationMilliseconds, 300 | status: metadata.status || '', 301 | disposition: metadata.disposition || '' 302 | }; 303 | } 304 | 305 | formattedEngagements.push(formattedEngagement); 306 | } 307 | 308 | // Convert any datetime fields and return 309 | return convertDatetimeFields(formattedEngagements); 310 | } catch (error: any) { 311 | console.error('Error getting recent engagements:', error); 312 | return { error: error.message }; 313 | } 314 | } 315 | 316 | async createContact( 317 | firstname: string, 318 | lastname: string, 319 | email?: string, 320 | properties?: Record<string, any> 321 | ): Promise<any> { 322 | try { 323 | // Search for existing contacts with same name and company 324 | const company = properties?.company; 325 | 326 | // Use type assertion to satisfy the HubSpot API client types 327 | const searchRequest = { 328 | filterGroups: [{ 329 | filters: [ 330 | { 331 | propertyName: 'firstname', 332 | operator: 'EQ', 333 | value: firstname 334 | } as any, 335 | { 336 | propertyName: 'lastname', 337 | operator: 'EQ', 338 | value: lastname 339 | } as any 340 | ] 341 | }] 342 | } as any; 343 | 344 | // Add company filter if provided 345 | if (company) { 346 | searchRequest.filterGroups[0].filters.push({ 347 | propertyName: 'company', 348 | operator: 'EQ', 349 | value: company 350 | } as any); 351 | } 352 | 353 | const searchResponse = await this.client.crm.contacts.searchApi.doSearch(searchRequest); 354 | 355 | if (searchResponse.total > 0) { 356 | // Contact already exists 357 | return { 358 | message: 'Contact already exists', 359 | contact: searchResponse.results[0] 360 | }; 361 | } 362 | 363 | // If no existing contact found, proceed with creation 364 | const contactProperties: Record<string, any> = { 365 | firstname, 366 | lastname 367 | }; 368 | 369 | // Add email if provided 370 | if (email) { 371 | contactProperties.email = email; 372 | } 373 | 374 | // Add any additional properties 375 | if (properties) { 376 | Object.assign(contactProperties, properties); 377 | } 378 | 379 | // Create contact 380 | const apiResponse = await this.client.crm.contacts.basicApi.create({ 381 | properties: contactProperties 382 | }); 383 | 384 | return apiResponse; 385 | } catch (error: any) { 386 | console.error('Error creating contact:', error); 387 | throw new Error(`HubSpot API error: ${error.message}`); 388 | } 389 | } 390 | 391 | async createCompany(name: string, properties?: Record<string, any>): Promise<any> { 392 | try { 393 | // Search for existing companies with same name 394 | // Use type assertion to satisfy the HubSpot API client types 395 | const searchRequest = { 396 | filterGroups: [{ 397 | filters: [ 398 | { 399 | propertyName: 'name', 400 | operator: 'EQ', 401 | value: name 402 | } as any 403 | ] 404 | }] 405 | } as any; 406 | 407 | const searchResponse = await this.client.crm.companies.searchApi.doSearch(searchRequest); 408 | 409 | if (searchResponse.total > 0) { 410 | // Company already exists 411 | return { 412 | message: 'Company already exists', 413 | company: searchResponse.results[0] 414 | }; 415 | } 416 | 417 | // If no existing company found, proceed with creation 418 | const companyProperties: Record<string, any> = { 419 | name 420 | }; 421 | 422 | // Add any additional properties 423 | if (properties) { 424 | Object.assign(companyProperties, properties); 425 | } 426 | 427 | // Create company 428 | const apiResponse = await this.client.crm.companies.basicApi.create({ 429 | properties: companyProperties 430 | }); 431 | 432 | return apiResponse; 433 | } catch (error: any) { 434 | console.error('Error creating company:', error); 435 | throw new Error(`HubSpot API error: ${error.message}`); 436 | } 437 | } 438 | 439 | async updateContact( 440 | contactId: string, 441 | properties: Record<string, any> 442 | ): Promise<any> { 443 | try { 444 | // Check if contact exists 445 | try { 446 | await this.client.crm.contacts.basicApi.getById(contactId); 447 | } catch (error: any) { 448 | // If contact doesn't exist, return a message 449 | if (error.statusCode === 404) { 450 | return { 451 | message: 'Contact not found, no update performed', 452 | contactId 453 | }; 454 | } 455 | // For other errors, throw them to be caught by the outer try/catch 456 | throw error; 457 | } 458 | 459 | // Update the contact 460 | const apiResponse = await this.client.crm.contacts.basicApi.update(contactId, { 461 | properties 462 | }); 463 | 464 | return { 465 | message: 'Contact updated successfully', 466 | contactId, 467 | properties 468 | }; 469 | } catch (error: any) { 470 | console.error('Error updating contact:', error); 471 | throw new Error(`HubSpot API error: ${error.message}`); 472 | } 473 | } 474 | 475 | async updateCompany( 476 | companyId: string, 477 | properties: Record<string, any> 478 | ): Promise<any> { 479 | try { 480 | // Check if company exists 481 | try { 482 | await this.client.crm.companies.basicApi.getById(companyId); 483 | } catch (error: any) { 484 | // If company doesn't exist, return a message 485 | if (error.statusCode === 404) { 486 | return { 487 | message: 'Company not found, no update performed', 488 | companyId 489 | }; 490 | } 491 | // For other errors, throw them to be caught by the outer try/catch 492 | throw error; 493 | } 494 | 495 | // Update the company 496 | const apiResponse = await this.client.crm.companies.basicApi.update(companyId, { 497 | properties 498 | }); 499 | 500 | return { 501 | message: 'Company updated successfully', 502 | companyId, 503 | properties 504 | }; 505 | } catch (error: any) { 506 | console.error('Error updating company:', error); 507 | throw new Error(`HubSpot API error: ${error.message}`); 508 | } 509 | } 510 | } 511 | ```