# 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 | [](https://hub.docker.com/r/buryhuang/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])) ```