#
tokens: 14656/50000 16/16 files
lines: on (toggle) GitHub
raw markdown copy reset
# Directory Structure

```
├── .replit
├── api
│   └── index.js
├── build.sh
├── Dockerfile
├── LICENSE
├── package.json
├── public
│   └── index.html
├── pyproject.toml
├── README.md
├── replit.nix
├── requirements.txt
├── runtime.txt
├── server.js
├── smithery.yaml
├── src
│   └── mcp_server_hubspot
│       ├── __init__.py
│       ├── debug.py
│       └── server.py
└── vercel.json
```

# Files

--------------------------------------------------------------------------------
/.replit:
--------------------------------------------------------------------------------

```
 1 | run = "npm start"
 2 | entrypoint = "server.js"
 3 | language = "nodejs"
 4 | 
 5 | [nix]
 6 | channel = "stable-22_11"
 7 | 
 8 | [env]
 9 | PORT = "3000"
10 | 
11 | [packager]
12 | language = "nodejs"
13 | 
14 | [packager.features]
15 | packageSearch = true
16 | guessImports = true
17 | 
18 | [languages.js]
19 | pattern = "**/*.js"
20 | syntax = "javascript"
21 | 
22 | [languages.js.languageServer]
23 | start = [ "typescript-language-server", "--stdio" ]
24 | 
25 | [deployment]
26 | run = ["sh", "-c", "npm start"]
27 | deploymentTarget = "cloudrun"
28 | 
```

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

```markdown
  1 | # HubSpot MCP Server
  2 | [![Docker Hub](https://img.shields.io/docker/v/buryhuang/mcp-hubspot?label=Docker%20Hub)](https://hub.docker.com/r/buryhuang/mcp-hubspot) [![smithery badge](https://smithery.ai/badge/mcp-hubspot)](https://smithery.ai/server/mcp-hubspot/prod)
  3 | 
  4 | ## Overview
  5 | 
  6 | A Model Context Protocol (MCP) server implementation that provides integration with HubSpot CRM. This server enables AI models to interact with HubSpot data and operations through a standardized interface.
  7 | 
  8 | For more information about the Model Context Protocol and how it works, see [Anthropic's MCP documentation](https://www.anthropic.com/news/model-context-protocol).
  9 | 
 10 | <a href="https://glama.ai/mcp/servers/vpoifk4jai"><img width="380" height="200" src="https://glama.ai/mcp/servers/vpoifk4jai/badge" alt="HubSpot Server MCP server" /></a>
 11 | 
 12 | ## Components
 13 | 
 14 | ### Resources
 15 | 
 16 | The server exposes the following resources:
 17 | 
 18 | * `hubspot://hubspot_contacts`: A dynamic resource that provides access to HubSpot contacts
 19 | * `hubspot://hubspot_companies`: A dynamic resource that provides access to HubSpot companies
 20 | * `hubspot://hubspot_recent_engagements`: A dynamic resource that provides access to HubSpot engagements from the last 3 days
 21 | 
 22 | All resources auto-update as their respective objects are modified in HubSpot.
 23 | 
 24 | ### Example Prompts
 25 | 
 26 | - Create Hubspot contacts by copying from LinkedIn profile webpage: 
 27 |     ```
 28 |     Create HubSpot contacts and companies from following:
 29 | 
 30 |     John Doe
 31 |     Software Engineer at Tech Corp
 32 |     San Francisco Bay Area • 500+ connections
 33 |     
 34 |     Experience
 35 |     Tech Corp
 36 |     Software Engineer
 37 |     Jan 2020 - Present · 4 yrs
 38 |     San Francisco, California
 39 |     
 40 |     Previous Company Inc.
 41 |     Senior Developer
 42 |     2018 - 2020 · 2 yrs
 43 |     
 44 |     Education
 45 |     University of California, Berkeley
 46 |     Computer Science, BS
 47 |     2014 - 2018
 48 |     ```
 49 | 
 50 | - Get latest activities for your company:
 51 |     ```
 52 |     What's happening latestly with my pipeline?
 53 |     ```
 54 | 
 55 | 
 56 | 
 57 | ### Tools
 58 | 
 59 | The server offers several tools for managing HubSpot objects:
 60 | 
 61 | #### Contact Management Tools
 62 | * `hubspot_get_contacts`
 63 |   * Retrieve contacts from HubSpot
 64 |   * No input required
 65 |   * Returns: Array of contact objects
 66 | 
 67 | * `hubspot_create_contact`
 68 |   * Create a new contact in HubSpot (checks for duplicates before creation)
 69 |   * Input:
 70 |     * `firstname` (string): Contact's first name
 71 |     * `lastname` (string): Contact's last name
 72 |     * `email` (string, optional): Contact's email address
 73 |     * `properties` (dict, optional): Additional contact properties
 74 |       * Example: `{"phone": "123456789", "company": "HubSpot"}`
 75 |   * Behavior:
 76 |     * Checks for existing contacts with the same first name and last name
 77 |     * If `company` is provided in properties, also checks for matches with the same company
 78 |     * Returns existing contact details if a match is found
 79 |     * Creates new contact only if no match is found
 80 | 
 81 | #### Company Management Tools
 82 | * `hubspot_get_companies`
 83 |   * Retrieve companies from HubSpot
 84 |   * No input required
 85 |   * Returns: Array of company objects
 86 | 
 87 | * `hubspot_create_company`
 88 |   * Create a new company in HubSpot (checks for duplicates before creation)
 89 |   * Input:
 90 |     * `name` (string): Company name
 91 |     * `properties` (dict, optional): Additional company properties
 92 |       * Example: `{"domain": "example.com", "industry": "Technology"}`
 93 |   * Behavior:
 94 |     * Checks for existing companies with the same name
 95 |     * Returns existing company details if a match is found
 96 |     * Creates new company only if no match is found
 97 | 
 98 | * `hubspot_get_company_activity`
 99 |   * Get activity history for a specific company
100 |   * Input:
101 |     * `company_id` (string): HubSpot company ID
102 |   * Returns: Array of activity objects
103 | 
104 | #### Engagement Tools
105 | * `hubspot_get_recent_engagements`
106 |   * Get HubSpot engagements from all companies and contacts from the last 3 days
107 |   * No input required
108 |   * Returns: Array of engagement objects with full metadata
109 | 
110 | 
111 | ## Multi-User Support
112 | 
113 | This MCP server is designed to work with multiple HubSpot users, each with their own access token. The server does not use a global environment variable for the access token.
114 | 
115 | Instead, each request to the MCP server should include the user's specific access token in one of the following ways:
116 | 
117 | 1. In the request header: `X-HubSpot-Access-Token: your-token-here`
118 | 2. In the request body as `accessToken`: `{"accessToken": "your-token-here"}`
119 | 3. In the request body as `hubspotAccessToken`: `{"hubspotAccessToken": "your-token-here"}`
120 | 
121 | This design allows you to store user tokens in your own backend (e.g., Supabase) and pass them along with each request.
122 | 
123 | ### Example Multi-User Integration
124 | 
125 | ```javascript
126 | // Example of how to use this MCP server in a multi-user setup
127 | async function makeHubSpotRequest(userId, action, params) {
128 |   // Retrieve the user's HubSpot token from your database
129 |   const userToken = await getUserHubSpotToken(userId); 
130 | 
131 |   // Make request to MCP server with the user's token
132 |   const response = await fetch('https://your-mcp-server.vercel.app/', {
133 |     method: 'POST',
134 |     headers: {
135 |       'Content-Type': 'application/json',
136 |       'X-HubSpot-Access-Token': userToken
137 |     },
138 |     body: JSON.stringify({
139 |       action,
140 |       ...params
141 |     })
142 |   });
143 |   
144 |   return await response.json();
145 | }
146 | ```
147 | 
148 | ## Setup
149 | 
150 | ### Prerequisites
151 | 
152 | You'll need a HubSpot access token for each user. You can obtain this by:
153 | 1. Creating a private app in your HubSpot account:
154 |    Follow the [HubSpot Private Apps Guide](https://developers.hubspot.com/docs/guides/apps/private-apps/overview)
155 |    - Go to your HubSpot account settings
156 |    - Navigate to Integrations > Private Apps
157 |    - Click "Create private app"
158 |    - Fill in the basic information:
159 |      - Name your app
160 |      - Add description
161 |      - Upload logo (optional)
162 |    - Define required scopes:
163 |      - oauth (required)
164 |      
165 |    - Optional scopes:
166 |      - crm.dealsplits.read_write
167 |      - crm.objects.companies.read
168 |      - crm.objects.companies.write
169 |      - crm.objects.contacts.read
170 |      - crm.objects.contacts.write
171 |      - crm.objects.deals.read
172 |    - Review and create the app
173 |    - Copy the generated access token
174 | 
175 | Note: Keep your access token secure and never commit it to version control.
176 | 
177 | ### Docker Installation
178 | 
179 | You can either build the image locally or pull it from Docker Hub. The image is built for the Linux platform.
180 | 
181 | #### Supported Platforms
182 | - Linux/amd64
183 | - Linux/arm64
184 | - Linux/arm/v7
185 | 
186 | #### Option 1: Pull from Docker Hub
187 | ```bash
188 | docker pull buryhuang/mcp-hubspot:latest
189 | ```
190 | 
191 | #### Option 2: Build Locally
192 | ```bash
193 | docker build -t mcp-hubspot .
194 | ```
195 | 
196 | Run the container:
197 | ```bash
198 | docker run \
199 |   buryhuang/mcp-hubspot:latest
200 | ```
201 | 
202 | ## Cross-Platform Publishing
203 | 
204 | To publish the Docker image for multiple platforms, you can use the `docker buildx` command. Follow these steps:
205 | 
206 | 1. **Create a new builder instance** (if you haven't already):
207 |    ```bash
208 |    docker buildx create --use
209 |    ```
210 | 
211 | 2. **Build and push the image for multiple platforms**:
212 |    ```bash
213 |    docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 -t buryhuang/mcp-hubspot:latest --push .
214 |    ```
215 | 
216 | 3. **Verify the image is available for the specified platforms**:
217 |    ```bash
218 |    docker buildx imagetools inspect buryhuang/mcp-hubspot:latest
219 |    ```
220 | 
221 | 
222 | ## Usage with Claude Desktop
223 | 
224 | ### Installing via Smithery
225 | 
226 | To install mcp-hubspot for Claude Desktop automatically via [Smithery](https://smithery.ai/server/mcp-hubspot/prod):
227 | 
228 | ```bash
229 | npx -y @smithery/cli@latest install mcp-hubspot --client claude
230 | ```
231 | 
232 | ### Docker Usage
233 | ```json
234 | {
235 |   "mcpServers": {
236 |     "hubspot": {
237 |       "command": "docker",
238 |       "args": [
239 |         "run",
240 |         "-i",
241 |         "--rm",
242 |         "buryhuang/mcp-hubspot:latest"
243 |       ]
244 |     }
245 |   }
246 | }
247 | ```
248 | 
249 | ## Development
250 | 
251 | To set up the development environment:
252 | 
253 | ```bash
254 | pip install -e .
255 | ```
256 | 
257 | ## License
258 | 
259 | This project is licensed under the MIT License. 
260 | 
```

--------------------------------------------------------------------------------
/runtime.txt:
--------------------------------------------------------------------------------

```
1 | python-3.9
2 | 
```

--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------

```
1 | hubspot-api-client>=8.0.0
2 | python-dotenv>=1.0.0
3 | mcp>=0.0.1
4 | pydantic>=2.0.0
5 | python-dateutil>=2.8.2
6 | 
```

--------------------------------------------------------------------------------
/vercel.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |   "version": 2,
 3 |   "functions": {
 4 |     "api/index.js": {
 5 |       "memory": 1024,
 6 |       "maxDuration": 10
 7 |     }
 8 |   },
 9 |   "routes": [
10 |     { "src": "/(.*)", "dest": "/api/index.js" }
11 |   ]
12 | }
13 | 
```

--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------

```dockerfile
 1 | # Use Python base image
 2 | FROM python:3.10-slim-bookworm
 3 | 
 4 | # Install the project into `/app`
 5 | WORKDIR /app
 6 | 
 7 | # Copy the entire project
 8 | COPY . /app
 9 | 
10 | # Install the package
11 | RUN pip install --no-cache-dir .
12 | 
13 | # Run the server
14 | ENTRYPOINT ["mcp-server-hubspot"] 
```

--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------

```toml
 1 | [project]
 2 | name = "mcp-server-hubspot"
 3 | version = "0.1.0"
 4 | description = "A simple Hubspot MCP server"
 5 | readme = "README.md"
 6 | requires-python = ">=3.10"
 7 | dependencies = ["mcp>=1.0.0", "hubspot-api-client>=8.1.0", "python-dotenv>=1.0.0"]
 8 | 
 9 | [build-system]
10 | requires = ["hatchling"]
11 | build-backend = "hatchling.build"
12 | 
13 | [tool.uv]
14 | dev-dependencies = ["pyright>=1.1.389"]
15 | 
16 | [project.scripts]
17 | mcp-server-hubspot = "mcp_server_hubspot:main" 
```

--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |   "name": "hubspot-mcp",
 3 |   "version": "1.0.0",
 4 |   "description": "HubSpot MCP Server for Windsurf",
 5 |   "main": "server.js",
 6 |   "scripts": {
 7 |     "start": "node server.js",
 8 |     "install-python-deps": "pip install -r requirements.txt",
 9 |     "build": "chmod +x ./build.sh && ./build.sh",
10 |     "postinstall": "npm run install-python-deps"
11 |   },
12 |   "engines": {
13 |     "node": ">=14"
14 |   },
15 |   "dependencies": {
16 |     "express": "^4.18.2"
17 |   }
18 | }
19 | 
```

--------------------------------------------------------------------------------
/build.sh:
--------------------------------------------------------------------------------

```bash
 1 | #!/bin/bash
 2 | 
 3 | # Install Python dependencies
 4 | python -m pip install -r requirements.txt 2>/dev/null || pip3 install -r requirements.txt 2>/dev/null || echo "Warning: Failed to install Python dependencies"
 5 | 
 6 | # Create necessary directories
 7 | mkdir -p api
 8 | 
 9 | # Create the public directory that Vercel requires
10 | mkdir -p public
11 | touch public/.gitkeep
12 | 
13 | # Print debug info
14 | echo "Build script executed successfully"
15 | echo "Current directory: $(pwd)"
16 | echo "Files in current directory:"
17 | ls -la
18 | echo "Files in public directory:"
19 | ls -la public
20 | 
21 | # Exit successfully
22 | exit 0
23 | 
```

--------------------------------------------------------------------------------
/src/mcp_server_hubspot/__init__.py:
--------------------------------------------------------------------------------

```python
 1 | import argparse
 2 | import asyncio
 3 | import logging
 4 | from . import server
 5 | 
 6 | logging.basicConfig(level=logging.DEBUG)
 7 | logger = logging.getLogger('mcp_hubspot')
 8 | 
 9 | def main():
10 |     logger.debug("Starting mcp-server-hubspot main()")
11 |     parser = argparse.ArgumentParser(description='HubSpot MCP Server')
12 |     parser.add_argument('--access-token', help='HubSpot access token')
13 |     args = parser.parse_args()
14 |     
15 |     logger.debug(f"Access token from args: {args.access_token}")
16 |     # Run the async main function
17 |     logger.debug("About to run server.main()")
18 |     asyncio.run(server.main(args.access_token))
19 |     logger.debug("Server main() completed")
20 | 
21 | if __name__ == "__main__":
22 |     main()
23 | 
24 | # Expose important items at package level
25 | __all__ = ["main", "server"] 
```

--------------------------------------------------------------------------------
/smithery.yaml:
--------------------------------------------------------------------------------

```yaml
 1 | # Smithery configuration file: https://smithery.ai/docs/deployments
 2 | 
 3 | startCommand:
 4 |   type: stdio
 5 |   configSchema:
 6 |     # JSON Schema defining the configuration options for the MCP.
 7 |     # This MCP server requires the following HubSpot scopes:
 8 |     # Required: oauth
 9 |     # Optional: crm.dealsplits.read_write crm.objects.companies.read crm.objects.companies.write 
10 |     #          crm.objects.contacts.read crm.objects.contacts.write crm.objects.deals.read
11 |     type: object
12 |     required:
13 |       - hubspotAccessToken
14 |     properties:
15 |       hubspotAccessToken:
16 |         type: string
17 |         description: The access token for the HubSpot API.
18 |   commandFunction:
19 |     # A function that produces the CLI command to start the MCP on stdio.
20 |     |-
21 |     config => ({ command: 'docker', args: ['run', '--rm', '-e', `HUBSPOT_ACCESS_TOKEN=${config.hubspotAccessToken}`, 'buryhuang/mcp-hubspot:latest'] })
```

--------------------------------------------------------------------------------
/src/mcp_server_hubspot/debug.py:
--------------------------------------------------------------------------------

```python
 1 | #!/usr/bin/env python
 2 | # Debug script to test Python environment in Replit
 3 | 
 4 | import sys
 5 | import os
 6 | import json
 7 | import traceback
 8 | 
 9 | def main():
10 |     # Print debug information about the environment
11 |     debug_info = {
12 |         "python_version": sys.version,
13 |         "python_path": sys.executable,
14 |         "cwd": os.getcwd(),
15 |         "env_vars": {k: v for k, v in os.environ.items() if not k.startswith("_")},
16 |         "sys_path": sys.path,
17 |         "arguments": sys.argv
18 |     }
19 |     
20 |     # Try to import important modules
21 |     try:
22 |         import hubspot
23 |         debug_info["hubspot_version"] = hubspot.__version__
24 |     except Exception as e:
25 |         debug_info["hubspot_import_error"] = str(e)
26 |         debug_info["hubspot_traceback"] = traceback.format_exc()
27 |     
28 |     try:
29 |         from mcp import server
30 |         debug_info["mcp_found"] = True
31 |     except Exception as e:
32 |         debug_info["mcp_import_error"] = str(e)
33 |         debug_info["mcp_traceback"] = traceback.format_exc()
34 |     
35 |     print(json.dumps(debug_info, indent=2, default=str))
36 | 
37 | if __name__ == "__main__":
38 |     try:
39 |         main()
40 |     except Exception as e:
41 |         error_info = {
42 |             "error": str(e),
43 |             "traceback": traceback.format_exc()
44 |         }
45 |         print(json.dumps(error_info, indent=2))
46 | 
```

--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------

```html
 1 | <!DOCTYPE html>
 2 | <html lang="en">
 3 | <head>
 4 |     <meta charset="UTF-8">
 5 |     <meta name="viewport" content="width=device-width, initial-scale=1.0">
 6 |     <title>HubSpot MCP Server</title>
 7 |     <style>
 8 |         body {
 9 |             font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
10 |             max-width: 800px;
11 |             margin: 0 auto;
12 |             padding: 20px;
13 |             line-height: 1.6;
14 |         }
15 |         h1 {
16 |             color: #333;
17 |             border-bottom: 1px solid #eee;
18 |             padding-bottom: 10px;
19 |         }
20 |         pre {
21 |             background-color: #f5f5f5;
22 |             padding: 10px;
23 |             border-radius: 5px;
24 |             overflow-x: auto;
25 |         }
26 |         code {
27 |             font-family: 'Courier New', Courier, monospace;
28 |         }
29 |     </style>
30 | </head>
31 | <body>
32 |     <h1>HubSpot MCP Server</h1>
33 |     <p>This is a Machine Communication Protocol (MCP) server for HubSpot integration.</p>
34 |     
35 |     <h2>API Endpoints</h2>
36 |     <ul>
37 |         <li><code>/ping</code> - Health check endpoint</li>
38 |         <li><code>/echo</code> - Debug endpoint that echoes back the request body</li>
39 |         <li><code>/</code> - Main endpoint for MCP requests</li>
40 |     </ul>
41 | 
42 |     <h2>Authentication</h2>
43 |     <p>All requests must include a HubSpot access token in one of the following ways:</p>
44 |     <ul>
45 |         <li>Header: <code>X-HubSpot-Access-Token: your-token-here</code></li>
46 |         <li>Request body: <code>{"accessToken": "your-token-here"}</code></li>
47 |         <li>Request body: <code>{"hubspotAccessToken": "your-token-here"}</code></li>
48 |     </ul>
49 | 
50 |     <h2>Example Request</h2>
51 |     <pre><code>curl -X POST https://your-vercel-domain.vercel.app/ \
52 |     -H "Content-Type: application/json" \
53 |     -H "X-HubSpot-Access-Token: your-token-here" \
54 |     -d '{"action": "get_contacts"}'
55 | </code></pre>
56 | </body>
57 | </html>
58 | 
```

--------------------------------------------------------------------------------
/api/index.js:
--------------------------------------------------------------------------------

```javascript
  1 | // API handler for both Vercel serverless and Replit
  2 | const { spawn } = require('child_process');
  3 | const path = require('path');
  4 | const fs = require('fs');
  5 | 
  6 | // Detect if running on Vercel or Replit
  7 | const isVercel = process.env.VERCEL === '1';
  8 | 
  9 | // Express middleware style handler for Replit
 10 | const handleRequest = async (req, res) => {
 11 |   // CORS headers
 12 |   res.setHeader('Access-Control-Allow-Credentials', true);
 13 |   res.setHeader('Access-Control-Allow-Origin', '*');
 14 |   res.setHeader('Access-Control-Allow-Methods', 'GET,OPTIONS,POST');
 15 |   res.setHeader('Access-Control-Allow-Headers', 'X-CSRF-Token, X-Requested-With, Accept, Accept-Version, Content-Length, Content-MD5, Content-Type, Date, X-Api-Version, X-HubSpot-Access-Token');
 16 | 
 17 |   // Handle OPTIONS request (preflight)
 18 |   if (req.method === 'OPTIONS') {
 19 |     res.status(200).end();
 20 |     return;
 21 |   }
 22 | 
 23 |   // Health check endpoint
 24 |   if (req.method === 'GET' && (req.url === '/health' || req.path === '/health')) {
 25 |     return res.status(200).json({ status: 'ok' });
 26 |   }
 27 | 
 28 |   // Test endpoint
 29 |   if (req.method === 'GET' && (req.url === '/ping' || req.path === '/ping')) {
 30 |     return res.status(200).send('pong');
 31 |   }
 32 | 
 33 |   // Debug endpoint
 34 |   if (req.method === 'GET' && (req.url === '/debug' || req.path === '/debug')) {
 35 |     const env = process.env;
 36 |     const pythonInfo = {};
 37 |     
 38 |     try {
 39 |       const pythonVersionCmd = spawn('python', ['--version']);
 40 |       let pythonVersion = '';
 41 |       
 42 |       pythonVersionCmd.stdout.on('data', (data) => {
 43 |         pythonVersion += data.toString();
 44 |       });
 45 |       
 46 |       await new Promise((resolve) => {
 47 |         pythonVersionCmd.on('close', (code) => {
 48 |           pythonInfo.versionExitCode = code;
 49 |           pythonInfo.version = pythonVersion.trim();
 50 |           resolve();
 51 |         });
 52 |       });
 53 |     } catch (e) {
 54 |       pythonInfo.error = e.message;
 55 |     }
 56 |     
 57 |     return res.status(200).json({
 58 |       nodeVersion: process.version,
 59 |       cwd: process.cwd(),
 60 |       environment: isVercel ? 'Vercel' : 'Replit/Other',
 61 |       pythonInfo,
 62 |       serverPath: path.join(process.cwd(), 'src', 'mcp_server_hubspot', 'server.py'),
 63 |       serverExists: fs.existsSync(path.join(process.cwd(), 'src', 'mcp_server_hubspot', 'server.py'))
 64 |     });
 65 |   }
 66 | 
 67 |   // Main endpoint for MCP requests
 68 |   if (req.method === 'POST') {
 69 |     try {
 70 |       // Get the request body
 71 |       const body = typeof req.body === 'string' ? JSON.parse(req.body) : req.body;
 72 |       
 73 |       // Check for access token in header first, then in body
 74 |       const accessToken = req.headers['x-hubspot-access-token'] || body.accessToken || body.hubspotAccessToken;
 75 |       if (!accessToken) {
 76 |         return res.status(400).json({
 77 |           error: 'Missing access token',
 78 |           message: 'HubSpot access token must be provided in either the X-HubSpot-Access-Token header or in the request body as accessToken or hubspotAccessToken'
 79 |         });
 80 |       }
 81 | 
 82 |       // Simple echo for debugging
 83 |       if (req.url === '/echo' || req.path === '/echo') {
 84 |         return res.status(200).json({
 85 |           receivedBody: body,
 86 |           receivedToken: accessToken ? '[PRESENT]' : '[MISSING]'
 87 |         });
 88 |       }
 89 | 
 90 |       // Create public directory if it doesn't exist
 91 |       const publicDir = path.join(process.cwd(), 'public');
 92 |       if (!fs.existsSync(publicDir)) {
 93 |         fs.mkdirSync(publicDir, { recursive: true });
 94 |       }
 95 | 
 96 |       // Use mock data on Vercel, real data elsewhere
 97 |       if (isVercel) {
 98 |         // Mock responses for Vercel
 99 |         if (body.action === 'get_contacts') {
100 |           return res.status(200).json({
101 |             status: 'success',
102 |             message: 'Mock contacts data',
103 |             data: [
104 |               { id: '1', firstName: 'John', lastName: 'Doe', email: '[email protected]' },
105 |               { id: '2', firstName: 'Jane', lastName: 'Smith', email: '[email protected]' }
106 |             ]
107 |           });
108 |         } else if (body.action === 'get_companies') {
109 |           return res.status(200).json({
110 |             status: 'success',
111 |             message: 'Mock companies data',
112 |             data: [
113 |               { id: '101', name: 'Acme Corp', domain: 'acme.com' },
114 |               { id: '102', name: 'Globex', domain: 'globex.com' }
115 |             ]
116 |           });
117 |         } else {
118 |           // For any other action, return a generic success response
119 |           return res.status(200).json({
120 |             status: 'success',
121 |             message: `Mock response for action: ${body.action}`,
122 |             tokenValid: true
123 |           });
124 |         }
125 |       } else {
126 |         // Real Python execution (works on Replit but not Vercel)
127 |         // Get the path to the MCP server script
128 |         const scriptPath = path.join(process.cwd(), 'src', 'mcp_server_hubspot', 'server.py');
129 | 
130 |         // Spawn a Python process to run the MCP server, passing the access token as an argument
131 |         const pythonProcess = spawn('python', [scriptPath, accessToken]);
132 |         
133 |         // Set up response buffers
134 |         let responseData = '';
135 |         let errorData = '';
136 | 
137 |         // Send the input to the Python process
138 |         pythonProcess.stdin.write(JSON.stringify(body) + '\n');
139 |         pythonProcess.stdin.end();
140 | 
141 |         // Listen for data from the Python process
142 |         pythonProcess.stdout.on('data', (data) => {
143 |           responseData += data.toString();
144 |         });
145 | 
146 |         // Listen for errors
147 |         pythonProcess.stderr.on('data', (data) => {
148 |           errorData += data.toString();
149 |         });
150 | 
151 |         // Wait for the process to exit
152 |         await new Promise((resolve) => {
153 |           pythonProcess.on('close', (code) => {
154 |             resolve(code);
155 |           });
156 |         });
157 | 
158 |         // If there was an error, return it
159 |         if (errorData) {
160 |           console.error('Python error:', errorData);
161 |           return res.status(500).json({ error: 'Internal server error', details: errorData });
162 |         }
163 | 
164 |         // Try to parse the response as JSON
165 |         try {
166 |           const jsonResponse = JSON.parse(responseData);
167 |           return res.status(200).json(jsonResponse);
168 |         } catch (e) {
169 |           // If the response isn't valid JSON, just return it as text
170 |           return res.status(200).send(responseData);
171 |         }
172 |       }
173 |     } catch (error) {
174 |       console.error('Error processing request:', error);
175 |       return res.status(500).json({ error: 'Internal server error', details: error.message });
176 |     }
177 |   }
178 | 
179 |   // If we get here, the method is not supported
180 |   return res.status(405).json({ error: 'Method not allowed' });
181 | };
182 | 
183 | // Export for both Express and Vercel
184 | module.exports = handleRequest;
185 | 
```

--------------------------------------------------------------------------------
/server.js:
--------------------------------------------------------------------------------

```javascript
  1 | const express = require('express');
  2 | const { spawn } = require('child_process');
  3 | const path = require('path');
  4 | const fs = require('fs');
  5 | 
  6 | const app = express();
  7 | const PORT = process.env.PORT || 3000;
  8 | 
  9 | // Middleware to parse JSON bodies
 10 | app.use(express.json());
 11 | 
 12 | // Serve static files from public directory
 13 | app.use(express.static('public'));
 14 | 
 15 | // CORS headers
 16 | app.use((req, res, next) => {
 17 |   res.setHeader('Access-Control-Allow-Credentials', true);
 18 |   res.setHeader('Access-Control-Allow-Origin', '*');
 19 |   res.setHeader('Access-Control-Allow-Methods', 'GET,OPTIONS,POST');
 20 |   res.setHeader('Access-Control-Allow-Headers', 'X-CSRF-Token, X-Requested-With, Accept, Accept-Version, Content-Length, Content-MD5, Content-Type, Date, X-Api-Version, X-HubSpot-Access-Token');
 21 |   
 22 |   if (req.method === 'OPTIONS') {
 23 |     return res.status(200).end();
 24 |   }
 25 |   
 26 |   next();
 27 | });
 28 | 
 29 | // Handle MCP protocol messages
 30 | const handleMcpRequest = async (req, res) => {
 31 |   const body = req.body;
 32 |   console.log('Received MCP request:', JSON.stringify(body));
 33 |   
 34 |   // Extract access token
 35 |   const accessToken = req.headers['x-hubspot-access-token'] || body.accessToken || body.hubspotAccessToken;
 36 |   
 37 |   if (!accessToken) {
 38 |     return res.status(400).json({
 39 |       error: 'Missing access token',
 40 |       message: 'HubSpot access token must be provided in either the X-HubSpot-Access-Token header or in the request body'
 41 |     });
 42 |   }
 43 |   
 44 |   // Basic MCP initialization response
 45 |   if (body.mcp === true || body.action === 'initialize') {
 46 |     return res.status(200).json({
 47 |       mcp: true,
 48 |       version: '1.0.0',
 49 |       name: 'HubSpot MCP Server',
 50 |       status: 'ready'
 51 |     });
 52 |   }
 53 |   
 54 |   try {
 55 |     // Get the path to the MCP server script
 56 |     const scriptPath = path.join(process.cwd(), 'src', 'mcp_server_hubspot', 'server.py');
 57 |     console.log('Script path:', scriptPath);
 58 |     console.log('Script exists:', fs.existsSync(scriptPath));
 59 | 
 60 |     // Spawn a Python process to run the MCP server
 61 |     const pythonProcess = spawn('python', [scriptPath, accessToken]);
 62 |     
 63 |     let responseData = '';
 64 |     let errorData = '';
 65 | 
 66 |     // Send the input to the Python process
 67 |     pythonProcess.stdin.write(JSON.stringify(body) + '\n');
 68 |     pythonProcess.stdin.end();
 69 | 
 70 |     // Listen for data from the Python process
 71 |     pythonProcess.stdout.on('data', (data) => {
 72 |       console.log('Python output:', data.toString());
 73 |       responseData += data.toString();
 74 |     });
 75 | 
 76 |     // Listen for errors
 77 |     pythonProcess.stderr.on('data', (data) => {
 78 |       console.error('Python error:', data.toString());
 79 |       errorData += data.toString();
 80 |     });
 81 | 
 82 |     // Wait for the process to exit
 83 |     const exitCode = await new Promise((resolve) => {
 84 |       pythonProcess.on('close', (code) => {
 85 |         console.log('Python process exited with code:', code);
 86 |         resolve(code);
 87 |       });
 88 |     });
 89 | 
 90 |     if (exitCode !== 0 || errorData) {
 91 |       console.error('Error output:', errorData);
 92 |       if (exitCode === 127) { // Command not found
 93 |         return res.status(500).json({
 94 |           error: 'Python execution failed',
 95 |           details: 'Python command not found. Please make sure Python is installed.',
 96 |           errorOutput: errorData
 97 |         });
 98 |       }
 99 |       return res.status(500).json({
100 |         error: 'Internal server error',
101 |         details: errorData || `Process exited with code ${exitCode}`
102 |       });
103 |     }
104 | 
105 |     // Try to parse the response as JSON
106 |     try {
107 |       const jsonResponse = JSON.parse(responseData);
108 |       return res.status(200).json(jsonResponse);
109 |     } catch (e) {
110 |       // If we can't parse as JSON, just return the raw output
111 |       console.log('Could not parse Python output as JSON:', e.message);
112 |       return res.status(200).send(responseData || 'No output from Python script');
113 |     }
114 |   } catch (error) {
115 |     console.error('Error processing request:', error);
116 |     return res.status(500).json({
117 |       error: 'Internal server error',
118 |       details: error.message
119 |     });
120 |   }
121 | };
122 | 
123 | // Health check endpoint
124 | app.get('/health', (req, res) => {
125 |   res.status(200).json({ status: 'ok' });
126 | });
127 | 
128 | // Test endpoint
129 | app.get('/ping', (req, res) => {
130 |   res.status(200).send('pong');
131 | });
132 | 
133 | // Debug endpoint
134 | app.get('/debug', async (req, res) => {
135 |   const pythonInfo = {};
136 |   
137 |   try {
138 |     const pythonVersionCmd = spawn('python', ['--version']);
139 |     let pythonVersion = '';
140 |     
141 |     pythonVersionCmd.stdout.on('data', (data) => {
142 |       pythonVersion += data.toString();
143 |     });
144 |     
145 |     await new Promise((resolve) => {
146 |       pythonVersionCmd.on('close', (code) => {
147 |         pythonInfo.versionExitCode = code;
148 |         pythonInfo.version = pythonVersion.trim();
149 |         resolve();
150 |       });
151 |     });
152 |   } catch (e) {
153 |     pythonInfo.error = e.message;
154 |   }
155 |   
156 |   res.status(200).json({
157 |     nodeVersion: process.version,
158 |     cwd: process.cwd(),
159 |     pythonInfo,
160 |     serverPath: path.join(process.cwd(), 'src', 'mcp_server_hubspot', 'server.py'),
161 |     serverExists: fs.existsSync(path.join(process.cwd(), 'src', 'mcp_server_hubspot', 'server.py')),
162 |     env: process.env.NODE_ENV || 'development'
163 |   });
164 | });
165 | 
166 | // Debug Python environment endpoint
167 | app.get('/debug-python', async (req, res) => {
168 |   try {
169 |     // Get the path to the debug script
170 |     const scriptPath = path.join(process.cwd(), 'src', 'mcp_server_hubspot', 'debug.py');
171 |     console.log('Debug script path:', scriptPath);
172 |     console.log('Debug script exists:', fs.existsSync(scriptPath));
173 | 
174 |     // Spawn a Python process to run the debug script
175 |     const pythonProcess = spawn('python', [scriptPath]);
176 |     
177 |     let responseData = '';
178 |     let errorData = '';
179 | 
180 |     // Listen for data from the Python process
181 |     pythonProcess.stdout.on('data', (data) => {
182 |       console.log('Python debug output:', data.toString());
183 |       responseData += data.toString();
184 |     });
185 | 
186 |     // Listen for errors
187 |     pythonProcess.stderr.on('data', (data) => {
188 |       console.error('Python debug error:', data.toString());
189 |       errorData += data.toString();
190 |     });
191 | 
192 |     // Wait for the process to exit
193 |     const exitCode = await new Promise((resolve) => {
194 |       pythonProcess.on('close', (code) => {
195 |         console.log('Python debug process exited with code:', code);
196 |         resolve(code);
197 |       });
198 |     });
199 | 
200 |     res.status(200).json({
201 |       pythonOutput: responseData,
202 |       pythonError: errorData,
203 |       exitCode
204 |     });
205 |   } catch (error) {
206 |     console.error('Error in debug-python endpoint:', error);
207 |     res.status(500).json({
208 |       error: 'Internal server error',
209 |       details: error.message
210 |     });
211 |   }
212 | });
213 | 
214 | // Echo endpoint for debugging
215 | app.post('/echo', (req, res) => {
216 |   const accessToken = req.headers['x-hubspot-access-token'] || req.body.accessToken || req.body.hubspotAccessToken;
217 |   res.status(200).json({
218 |     receivedBody: req.body,
219 |     receivedToken: accessToken ? '[PRESENT]' : '[MISSING]'
220 |   });
221 | });
222 | 
223 | // Main MCP endpoint
224 | app.post('/', handleMcpRequest);
225 | 
226 | // Start the server
227 | app.listen(PORT, () => {
228 |   console.log(`Server running on port ${PORT}`);
229 | });
230 | 
```

--------------------------------------------------------------------------------
/src/mcp_server_hubspot/server.py:
--------------------------------------------------------------------------------

```python
  1 | import logging
  2 | from typing import Any, Dict, List, Optional
  3 | import os
  4 | from dotenv import load_dotenv
  5 | from hubspot import HubSpot
  6 | from hubspot.crm.contacts import SimplePublicObjectInputForCreate
  7 | from hubspot.crm.contacts.exceptions import ApiException
  8 | from mcp.server.models import InitializationOptions
  9 | import mcp.types as types
 10 | from mcp.server import NotificationOptions, Server
 11 | import mcp.server.stdio
 12 | from pydantic import AnyUrl
 13 | import json
 14 | from datetime import datetime, timedelta
 15 | from dateutil.tz import tzlocal
 16 | 
 17 | logger = logging.getLogger('mcp_hubspot_server')
 18 | 
 19 | def convert_datetime_fields(obj: Any) -> Any:
 20 |     """Convert any datetime or tzlocal objects to string in the given object"""
 21 |     if isinstance(obj, dict):
 22 |         return {k: convert_datetime_fields(v) for k, v in obj.items()}
 23 |     elif isinstance(obj, list):
 24 |         return [convert_datetime_fields(item) for item in obj]
 25 |     elif isinstance(obj, datetime):
 26 |         return obj.isoformat()
 27 |     elif isinstance(obj, tzlocal):
 28 |         # Get the current timezone offset
 29 |         offset = datetime.now(tzlocal()).strftime('%z')
 30 |         return f"UTC{offset[:3]}:{offset[3:]}"  # Format like "UTC+08:00" or "UTC-05:00"
 31 |     return obj
 32 | 
 33 | class HubSpotClient:
 34 |     def __init__(self, access_token: str):
 35 |         logger.debug(f"Using access token: {'[MASKED]' if access_token else 'None'}")
 36 |         if not access_token:
 37 |             raise ValueError("HubSpot access token is required. It must be provided as an argument.")
 38 |         
 39 |         # Initialize HubSpot client with the provided token
 40 |         # This allows for multi-user support by passing user-specific tokens
 41 |         self.client = HubSpot(access_token=access_token)
 42 | 
 43 |     def get_contacts(self) -> str:
 44 |         """Get all contacts from HubSpot (requires optional crm.objects.contacts.read scope)"""
 45 |         try:
 46 |             contacts = self.client.crm.contacts.get_all()
 47 |             contacts_dict = [contact.to_dict() for contact in contacts]
 48 |             converted_contacts = convert_datetime_fields(contacts_dict)
 49 |             return json.dumps(converted_contacts)
 50 |         except ApiException as e:
 51 |             logger.error(f"API Exception in get_contacts: {str(e)}")
 52 |             return json.dumps({"error": str(e)})
 53 |         except Exception as e:
 54 |             logger.error(f"Exception in get_contacts: {str(e)}")
 55 |             return json.dumps({"error": str(e)})
 56 | 
 57 |     def get_companies(self) -> str:
 58 |         """Get all companies from HubSpot (requires optional crm.objects.companies.read scope)"""
 59 |         try:
 60 |             companies = self.client.crm.companies.get_all()
 61 |             companies_dict = [company.to_dict() for company in companies]
 62 |             converted_companies = convert_datetime_fields(companies_dict)
 63 |             return json.dumps(converted_companies)
 64 |         except ApiException as e:
 65 |             logger.error(f"API Exception in get_companies: {str(e)}")
 66 |             return json.dumps({"error": str(e)})
 67 |         except Exception as e:
 68 |             logger.error(f"Exception in get_companies: {str(e)}")
 69 |             return json.dumps({"error": str(e)})
 70 | 
 71 |     def get_company_activity(self, company_id: str) -> str:
 72 |         """Get activity history for a specific company (requires optional crm.objects.companies.read scope)"""
 73 |         try:
 74 |             # Note: This method only requires standard read scopes, not sensitive scopes
 75 |             # Step 1: Get all engagement IDs associated with the company using CRM Associations v4 API
 76 |             associated_engagements = self.client.crm.associations.v4.basic_api.get_page(
 77 |                 object_type="companies",
 78 |                 object_id=company_id,
 79 |                 to_object_type="engagements",
 80 |                 limit=500
 81 |             )
 82 |             
 83 |             # Extract engagement IDs from the associations response
 84 |             engagement_ids = []
 85 |             if hasattr(associated_engagements, 'results'):
 86 |                 for result in associated_engagements.results:
 87 |                     engagement_ids.append(result.to_object_id)
 88 | 
 89 |             # Step 2: Get detailed information for each engagement
 90 |             activities = []
 91 |             for engagement_id in engagement_ids:
 92 |                 engagement_response = self.client.api_request({
 93 |                     "method": "GET",
 94 |                     "path": f"/engagements/v1/engagements/{engagement_id}"
 95 |                 }).json()
 96 |                 
 97 |                 engagement_data = engagement_response.get('engagement', {})
 98 |                 metadata = engagement_response.get('metadata', {})
 99 |                 
100 |                 # Format the engagement
101 |                 formatted_engagement = {
102 |                     "id": engagement_data.get("id"),
103 |                     "type": engagement_data.get("type"),
104 |                     "created_at": engagement_data.get("createdAt"),
105 |                     "last_updated": engagement_data.get("lastUpdated"),
106 |                     "created_by": engagement_data.get("createdBy"),
107 |                     "modified_by": engagement_data.get("modifiedBy"),
108 |                     "timestamp": engagement_data.get("timestamp"),
109 |                     "associations": engagement_response.get("associations", {})
110 |                 }
111 |                 
112 |                 # Add type-specific metadata formatting
113 |                 if engagement_data.get("type") == "NOTE":
114 |                     formatted_engagement["content"] = metadata.get("body", "")
115 |                 elif engagement_data.get("type") == "EMAIL":
116 |                     formatted_engagement["content"] = {
117 |                         "subject": metadata.get("subject", ""),
118 |                         "from": {
119 |                             "raw": metadata.get("from", {}).get("raw", ""),
120 |                             "email": metadata.get("from", {}).get("email", ""),
121 |                             "firstName": metadata.get("from", {}).get("firstName", ""),
122 |                             "lastName": metadata.get("from", {}).get("lastName", "")
123 |                         },
124 |                         "to": [{
125 |                             "raw": recipient.get("raw", ""),
126 |                             "email": recipient.get("email", ""),
127 |                             "firstName": recipient.get("firstName", ""),
128 |                             "lastName": recipient.get("lastName", "")
129 |                         } for recipient in metadata.get("to", [])],
130 |                         "cc": [{
131 |                             "raw": recipient.get("raw", ""),
132 |                             "email": recipient.get("email", ""),
133 |                             "firstName": recipient.get("firstName", ""),
134 |                             "lastName": recipient.get("lastName", "")
135 |                         } for recipient in metadata.get("cc", [])],
136 |                         "bcc": [{
137 |                             "raw": recipient.get("raw", ""),
138 |                             "email": recipient.get("email", ""),
139 |                             "firstName": recipient.get("firstName", ""),
140 |                             "lastName": recipient.get("lastName", "")
141 |                         } for recipient in metadata.get("bcc", [])],
142 |                         "sender": {
143 |                             "email": metadata.get("sender", {}).get("email", "")
144 |                         },
145 |                         "body": metadata.get("text", "") or metadata.get("html", "")
146 |                     }
147 |                 elif engagement_data.get("type") == "TASK":
148 |                     formatted_engagement["content"] = {
149 |                         "subject": metadata.get("subject", ""),
150 |                         "body": metadata.get("body", ""),
151 |                         "status": metadata.get("status", ""),
152 |                         "for_object_type": metadata.get("forObjectType", "")
153 |                     }
154 |                 elif engagement_data.get("type") == "MEETING":
155 |                     formatted_engagement["content"] = {
156 |                         "title": metadata.get("title", ""),
157 |                         "body": metadata.get("body", ""),
158 |                         "start_time": metadata.get("startTime"),
159 |                         "end_time": metadata.get("endTime"),
160 |                         "internal_notes": metadata.get("internalMeetingNotes", "")
161 |                     }
162 |                 elif engagement_data.get("type") == "CALL":
163 |                     formatted_engagement["content"] = {
164 |                         "body": metadata.get("body", ""),
165 |                         "from_number": metadata.get("fromNumber", ""),
166 |                         "to_number": metadata.get("toNumber", ""),
167 |                         "duration_ms": metadata.get("durationMilliseconds"),
168 |                         "status": metadata.get("status", ""),
169 |                         "disposition": metadata.get("disposition", "")
170 |                     }
171 |                 
172 |                 activities.append(formatted_engagement)
173 | 
174 |             # Convert any datetime fields and return
175 |             converted_activities = convert_datetime_fields(activities)
176 |             return json.dumps(converted_activities)
177 |             
178 |         except ApiException as e:
179 |             logger.error(f"API Exception: {str(e)}")
180 |             return json.dumps({"error": str(e)})
181 |         except Exception as e:
182 |             logger.error(f"Exception: {str(e)}")
183 |             return json.dumps({"error": str(e)})
184 | 
185 | async def main(access_token: str):
186 |     """
187 |     Run the HubSpot MCP server.
188 |     
189 |     Args:
190 |         access_token: HubSpot access token
191 |     
192 |     Note:
193 |         This server requires the following HubSpot scopes:
194 |         Required:
195 |         - oauth
196 |         
197 |         Optional:
198 |         - crm.dealsplits.read_write
199 |         - crm.objects.companies.read
200 |         - crm.objects.companies.write
201 |         - crm.objects.contacts.read
202 |         - crm.objects.contacts.write
203 |         - crm.objects.deals.read
204 |     """
205 |     logger.info("Server starting")
206 |     hubspot = HubSpotClient(access_token)
207 |     server = Server("hubspot-manager")
208 | 
209 |     @server.list_resources()
210 |     async def handle_list_resources() -> list[types.Resource]:
211 |         return [
212 |             types.Resource(
213 |                 uri=AnyUrl("hubspot://hubspot_contacts"),
214 |                 name="HubSpot Contacts",
215 |                 description="List of HubSpot contacts (requires optional crm.objects.contacts.read scope)",
216 |                 mimeType="application/json",
217 |             ),
218 |             types.Resource(
219 |                 uri=AnyUrl("hubspot://hubspot_companies"),
220 |                 name="HubSpot Companies", 
221 |                 description="List of HubSpot companies (requires optional crm.objects.companies.read scope)",
222 |                 mimeType="application/json",
223 |             ),
224 |         ]
225 | 
226 |     @server.read_resource()
227 |     async def handle_read_resource(uri: AnyUrl) -> str:
228 |         if uri.scheme != "hubspot":
229 |             raise ValueError(f"Unsupported URI scheme: {uri.scheme}")
230 | 
231 |         path = str(uri).replace("hubspot://", "")
232 |         if path == "hubspot_contacts":
233 |             return str(hubspot.get_contacts())
234 |         elif path == "hubspot_companies":
235 |             return str(hubspot.get_companies())
236 |         else:
237 |             raise ValueError(f"Unknown resource path: {path}")
238 | 
239 |     @server.list_tools()
240 |     async def handle_list_tools() -> list[types.Tool]:
241 |         """List available tools"""
242 |         return [
243 |             types.Tool(
244 |                 name="hubspot_get_contacts",
245 |                 description="Get contacts from HubSpot (requires optional crm.objects.contacts.read scope)",
246 |                 inputSchema={
247 |                     "type": "object",
248 |                     "properties": {},
249 |                 },
250 |             ),
251 |             types.Tool(
252 |                 name="hubspot_create_contact",
253 |                 description="Create a new contact in HubSpot (requires optional crm.objects.contacts.write scope)",
254 |                 inputSchema={
255 |                     "type": "object",
256 |                     "properties": {
257 |                         "firstname": {"type": "string", "description": "Contact's first name"},
258 |                         "lastname": {"type": "string", "description": "Contact's last name"},
259 |                         "email": {"type": "string", "description": "Contact's email address"},
260 |                         "properties": {"type": "object", "description": "Additional contact properties"}
261 |                     },
262 |                     "required": ["firstname", "lastname"]
263 |                 },
264 |             ),
265 |             types.Tool(
266 |                 name="hubspot_get_companies",
267 |                 description="Get companies from HubSpot (requires optional crm.objects.companies.read scope)",
268 |                 inputSchema={
269 |                     "type": "object",
270 |                     "properties": {},
271 |                 },
272 |             ),
273 |             types.Tool(
274 |                 name="hubspot_create_company",
275 |                 description="Create a new company in HubSpot (requires optional crm.objects.companies.write scope)",
276 |                 inputSchema={
277 |                     "type": "object",
278 |                     "properties": {
279 |                         "name": {"type": "string", "description": "Company name"},
280 |                         "properties": {"type": "object", "description": "Additional company properties"}
281 |                     },
282 |                     "required": ["name"]
283 |                 },
284 |             ),
285 |             types.Tool(
286 |                 name="hubspot_get_company_activity",
287 |                 description="Get activity history for a specific company (requires optional crm.objects.companies.read scope)",
288 |                 inputSchema={
289 |                     "type": "object",
290 |                     "properties": {
291 |                         "company_id": {"type": "string", "description": "HubSpot company ID"}
292 |                     },
293 |                     "required": ["company_id"]
294 |                 },
295 |             ),
296 |         ]
297 | 
298 |     @server.call_tool()
299 |     async def handle_call_tool(
300 |         name: str, arguments: dict[str, Any] | None
301 |     ) -> list[types.TextContent | types.ImageContent | types.EmbeddedResource]:
302 |         """Handle tool execution requests"""
303 |         try:
304 |             if name == "hubspot_get_contacts":
305 |                 results = hubspot.get_contacts()
306 |                 return [types.TextContent(type="text", text=str(results))]
307 | 
308 |             elif name == "hubspot_create_contact":
309 |                 if not arguments:
310 |                     raise ValueError("Missing arguments for create_contact")
311 |                 
312 |                 firstname = arguments["firstname"]
313 |                 lastname = arguments["lastname"]
314 |                 company = arguments.get("properties", {}).get("company")
315 |                 
316 |                 # Search for existing contacts with same name and company
317 |                 try:
318 |                     from hubspot.crm.contacts import PublicObjectSearchRequest
319 |                     
320 |                     filter_groups = [{
321 |                         "filters": [
322 |                             {
323 |                                 "propertyName": "firstname",
324 |                                 "operator": "EQ",
325 |                                 "value": firstname
326 |                             },
327 |                             {
328 |                                 "propertyName": "lastname",
329 |                                 "operator": "EQ",
330 |                                 "value": lastname
331 |                             }
332 |                         ]
333 |                     }]
334 |                     
335 |                     # Add company filter if provided
336 |                     if company:
337 |                         filter_groups[0]["filters"].append({
338 |                             "propertyName": "company",
339 |                             "operator": "EQ",
340 |                             "value": company
341 |                         })
342 |                     
343 |                     search_request = PublicObjectSearchRequest(
344 |                         filter_groups=filter_groups
345 |                     )
346 |                     
347 |                     search_response = hubspot.client.crm.contacts.search_api.do_search(
348 |                         public_object_search_request=search_request
349 |                     )
350 |                     
351 |                     if search_response.total > 0:
352 |                         # Contact already exists
353 |                         return [types.TextContent(
354 |                             type="text", 
355 |                             text=f"Contact already exists: {search_response.results[0].to_dict()}"
356 |                         )]
357 |                     
358 |                     # If no existing contact found, proceed with creation
359 |                     properties = {
360 |                         "firstname": firstname,
361 |                         "lastname": lastname
362 |                     }
363 |                     
364 |                     # Add email if provided
365 |                     if "email" in arguments:
366 |                         properties["email"] = arguments["email"]
367 |                     
368 |                     # Add any additional properties
369 |                     if "properties" in arguments:
370 |                         properties.update(arguments["properties"])
371 |                     
372 |                     # Create contact using SimplePublicObjectInputForCreate
373 |                     simple_public_object_input = SimplePublicObjectInputForCreate(
374 |                         properties=properties
375 |                     )
376 |                     
377 |                     api_response = hubspot.client.crm.contacts.basic_api.create(
378 |                         simple_public_object_input_for_create=simple_public_object_input
379 |                     )
380 |                     return [types.TextContent(type="text", text=str(api_response.to_dict()))]
381 |                     
382 |                 except ApiException as e:
383 |                     return [types.TextContent(type="text", text=f"HubSpot API error: {str(e)}")]
384 | 
385 |             elif name == "hubspot_get_companies":
386 |                 results = hubspot.get_companies()
387 |                 return [types.TextContent(type="text", text=str(results))]
388 | 
389 |             elif name == "hubspot_create_company":
390 |                 if not arguments:
391 |                     raise ValueError("Missing arguments for create_company")
392 |                 
393 |                 company_name = arguments["name"]
394 |                 
395 |                 # Search for existing companies with same name
396 |                 try:
397 |                     from hubspot.crm.companies import PublicObjectSearchRequest
398 |                     
399 |                     search_request = PublicObjectSearchRequest(
400 |                         filter_groups=[{
401 |                             "filters": [
402 |                                 {
403 |                                     "propertyName": "name",
404 |                                     "operator": "EQ",
405 |                                     "value": company_name
406 |                                 }
407 |                             ]
408 |                         }]
409 |                     )
410 |                     
411 |                     search_response = hubspot.client.crm.companies.search_api.do_search(
412 |                         public_object_search_request=search_request
413 |                     )
414 |                     
415 |                     if search_response.total > 0:
416 |                         # Company already exists
417 |                         return [types.TextContent(
418 |                             type="text", 
419 |                             text=f"Company already exists: {search_response.results[0].to_dict()}"
420 |                         )]
421 |                     
422 |                     # If no existing company found, proceed with creation
423 |                     properties = {
424 |                         "name": company_name
425 |                     }
426 |                     
427 |                     # Add any additional properties
428 |                     if "properties" in arguments:
429 |                         properties.update(arguments["properties"])
430 |                     
431 |                     # Create company using SimplePublicObjectInputForCreate
432 |                     simple_public_object_input = SimplePublicObjectInputForCreate(
433 |                         properties=properties
434 |                     )
435 |                     
436 |                     api_response = hubspot.client.crm.companies.basic_api.create(
437 |                         simple_public_object_input_for_create=simple_public_object_input
438 |                     )
439 |                     return [types.TextContent(type="text", text=str(api_response.to_dict()))]
440 |                     
441 |                 except ApiException as e:
442 |                     return [types.TextContent(type="text", text=f"HubSpot API error: {str(e)}")]
443 | 
444 |             elif name == "hubspot_get_company_activity":
445 |                 if not arguments:
446 |                     raise ValueError("Missing arguments for get_company_activity")
447 |                 results = hubspot.get_company_activity(arguments["company_id"])
448 |                 return [types.TextContent(type="text", text=results)]
449 | 
450 |             else:
451 |                 raise ValueError(f"Unknown tool: {name}")
452 | 
453 |         except ApiException as e:
454 |             return [types.TextContent(type="text", text=f"HubSpot API error: {str(e)}")]
455 |         except Exception as e:
456 |             return [types.TextContent(type="text", text=f"Error: {str(e)}")]
457 | 
458 |     async with mcp.server.stdio.stdio_server() as (read_stream, write_stream):
459 |         logger.info("Server running with stdio transport")
460 |         await server.run(
461 |             read_stream,
462 |             write_stream,
463 |             InitializationOptions(
464 |                 server_name="hubspot",
465 |                 server_version="0.1.0",
466 |                 capabilities=server.get_capabilities(
467 |                     notification_options=NotificationOptions(),
468 |                     experimental_capabilities={},
469 |                 ),
470 |             ),
471 |         )
472 | 
473 | if __name__ == "__main__":
474 |     import asyncio
475 |     import sys
476 |     if len(sys.argv) != 2:
477 |         print("Usage: python server.py <access_token>")
478 |         sys.exit(1)
479 |     asyncio.run(main(sys.argv[1]))
```