#
tokens: 11053/50000 7/7 files
lines: on (toggle) GitHub
raw markdown copy reset
# 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 | [![TypeScript](https://img.shields.io/badge/TypeScript-4.9.5-blue.svg)](https://www.typescriptlang.org/)
  4 | [![HubSpot API](https://img.shields.io/badge/HubSpot%20API-v3-orange.svg)](https://developers.hubspot.com/docs/api/overview)
  5 | [![MCP SDK](https://img.shields.io/badge/MCP%20SDK-1.8.0-green.svg)](https://github.com/modelcontextprotocol/sdk)
  6 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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 | 
```