# 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 |
```